@wrongstack/tools 0.292.1 → 0.295.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 (248) hide show
  1. package/dist/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/bash.d.ts +1 -1
  13. package/dist/bash.d.ts.map +1 -1
  14. package/dist/bash.js +66 -52
  15. package/dist/bash.js.map +3 -3
  16. package/dist/batch-tool-use.d.ts +1 -1
  17. package/dist/batch-tool-use.d.ts.map +1 -1
  18. package/dist/batch-tool-use.js +1 -1
  19. package/dist/batch-tool-use.js.map +1 -1
  20. package/dist/browser/index.js +10 -1
  21. package/dist/browser/index.js.map +2 -2
  22. package/dist/browser/manager.d.ts +2 -0
  23. package/dist/browser/manager.d.ts.map +1 -1
  24. package/dist/browser/tools.d.ts +1 -1
  25. package/dist/browser/tools.d.ts.map +1 -1
  26. package/dist/builtin.d.ts +1 -1
  27. package/dist/builtin.d.ts.map +1 -1
  28. package/dist/builtin.js +1394 -553
  29. package/dist/builtin.js.map +4 -4
  30. package/dist/codebase-index/background-indexer.d.ts +2 -2
  31. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  32. package/dist/codebase-index/bm25.d.ts +3 -1
  33. package/dist/codebase-index/bm25.d.ts.map +1 -1
  34. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  35. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  36. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  37. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/generic-parser.d.ts +34 -0
  41. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  42. package/dist/codebase-index/go-parser.d.ts +1 -1
  43. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/index.d.ts +4 -2
  45. package/dist/codebase-index/index.d.ts.map +1 -1
  46. package/dist/codebase-index/index.js +1036 -341
  47. package/dist/codebase-index/index.js.map +4 -4
  48. package/dist/codebase-index/indexer.d.ts +6 -1
  49. package/dist/codebase-index/indexer.d.ts.map +1 -1
  50. package/dist/codebase-index/json-parser.d.ts +1 -1
  51. package/dist/codebase-index/languages.d.ts +25 -0
  52. package/dist/codebase-index/languages.d.ts.map +1 -0
  53. package/dist/codebase-index/py-parser.d.ts +9 -1
  54. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  55. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  56. package/dist/codebase-index/rs-parser.d.ts +1 -1
  57. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  58. package/dist/codebase-index/schema.d.ts +9 -2
  59. package/dist/codebase-index/schema.d.ts.map +1 -1
  60. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  61. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  62. package/dist/codebase-index/ts-parser.d.ts +2 -2
  63. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  64. package/dist/codebase-index/worker.js +961 -317
  65. package/dist/codebase-index/worker.js.map +4 -4
  66. package/dist/codebase-index/writer.d.ts +83 -4
  67. package/dist/codebase-index/writer.d.ts.map +1 -1
  68. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  69. package/dist/design.d.ts +5 -2
  70. package/dist/design.d.ts.map +1 -1
  71. package/dist/design.js +36 -3
  72. package/dist/design.js.map +2 -2
  73. package/dist/diff.d.ts +1 -1
  74. package/dist/diff.d.ts.map +1 -1
  75. package/dist/diff.js +2 -2
  76. package/dist/diff.js.map +1 -1
  77. package/dist/document.d.ts +1 -1
  78. package/dist/document.d.ts.map +1 -1
  79. package/dist/document.js +1 -1
  80. package/dist/document.js.map +1 -1
  81. package/dist/e2e.d.ts +1 -1
  82. package/dist/e2e.d.ts.map +1 -1
  83. package/dist/e2e.js +1 -1
  84. package/dist/e2e.js.map +1 -1
  85. package/dist/edit.d.ts +1 -1
  86. package/dist/edit.d.ts.map +1 -1
  87. package/dist/edit.js +3 -3
  88. package/dist/edit.js.map +2 -2
  89. package/dist/exec.d.ts +1 -1
  90. package/dist/exec.d.ts.map +1 -1
  91. package/dist/exec.js +60 -21
  92. package/dist/exec.js.map +3 -3
  93. package/dist/fetch.d.ts +1 -1
  94. package/dist/fetch.d.ts.map +1 -1
  95. package/dist/fetch.js +4 -5
  96. package/dist/fetch.js.map +2 -2
  97. package/dist/format.d.ts +1 -1
  98. package/dist/format.d.ts.map +1 -1
  99. package/dist/format.js +81 -16
  100. package/dist/format.js.map +2 -2
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +3 -2
  104. package/dist/git.js.map +2 -2
  105. package/dist/glob.d.ts +1 -1
  106. package/dist/glob.d.ts.map +1 -1
  107. package/dist/glob.js +2 -2
  108. package/dist/glob.js.map +1 -1
  109. package/dist/grep.d.ts +1 -1
  110. package/dist/grep.d.ts.map +1 -1
  111. package/dist/grep.js +4 -3
  112. package/dist/grep.js.map +2 -2
  113. package/dist/index.d.ts +4 -4
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1657 -705
  116. package/dist/index.js.map +4 -4
  117. package/dist/install.d.ts +1 -1
  118. package/dist/install.d.ts.map +1 -1
  119. package/dist/install.js +85 -17
  120. package/dist/install.js.map +2 -2
  121. package/dist/json.d.ts +1 -1
  122. package/dist/json.d.ts.map +1 -1
  123. package/dist/json.js +2 -2
  124. package/dist/json.js.map +2 -2
  125. package/dist/kanban.d.ts +4 -3
  126. package/dist/kanban.d.ts.map +1 -1
  127. package/dist/kanban.js +94 -27
  128. package/dist/kanban.js.map +2 -2
  129. package/dist/languages/execute-tool.d.ts +1 -1
  130. package/dist/languages/execute-tool.d.ts.map +1 -1
  131. package/dist/languages/execute.d.ts +1 -1
  132. package/dist/languages/execute.d.ts.map +1 -1
  133. package/dist/languages/index.d.ts +1 -1
  134. package/dist/languages/index.d.ts.map +1 -1
  135. package/dist/languages/index.js +99 -17
  136. package/dist/languages/index.js.map +2 -2
  137. package/dist/languages/package-tool.d.ts +3 -4
  138. package/dist/languages/package-tool.d.ts.map +1 -1
  139. package/dist/languages/plan.d.ts.map +1 -1
  140. package/dist/languages/profiles/additional.d.ts.map +1 -1
  141. package/dist/languages/profiles/primary.d.ts.map +1 -1
  142. package/dist/languages/tool.d.ts +1 -1
  143. package/dist/languages/tool.d.ts.map +1 -1
  144. package/dist/lint.d.ts +1 -1
  145. package/dist/lint.d.ts.map +1 -1
  146. package/dist/lint.js +81 -16
  147. package/dist/lint.js.map +2 -2
  148. package/dist/logs.d.ts +1 -1
  149. package/dist/logs.d.ts.map +1 -1
  150. package/dist/logs.js +2 -2
  151. package/dist/logs.js.map +1 -1
  152. package/dist/memory.d.ts +1 -1
  153. package/dist/memory.d.ts.map +1 -1
  154. package/dist/memory.js +1 -1
  155. package/dist/memory.js.map +1 -1
  156. package/dist/mode.d.ts +1 -1
  157. package/dist/mode.d.ts.map +1 -1
  158. package/dist/mode.js.map +1 -1
  159. package/dist/outdated.d.ts +1 -1
  160. package/dist/outdated.d.ts.map +1 -1
  161. package/dist/outdated.js +101 -19
  162. package/dist/outdated.js.map +2 -2
  163. package/dist/pack.d.ts +1 -1
  164. package/dist/pack.js +1394 -553
  165. package/dist/pack.js.map +4 -4
  166. package/dist/patch.d.ts +1 -1
  167. package/dist/patch.d.ts.map +1 -1
  168. package/dist/patch.js +2 -2
  169. package/dist/patch.js.map +1 -1
  170. package/dist/plan.d.ts +1 -1
  171. package/dist/plan.d.ts.map +1 -1
  172. package/dist/plan.js +7 -7
  173. package/dist/plan.js.map +2 -2
  174. package/dist/process-guardian.d.ts +7 -0
  175. package/dist/process-guardian.d.ts.map +1 -1
  176. package/dist/process-registry-persistent.d.ts +5 -0
  177. package/dist/process-registry-persistent.d.ts.map +1 -1
  178. package/dist/process-registry.d.ts +9 -1
  179. package/dist/process-registry.d.ts.map +1 -1
  180. package/dist/process-registry.js +18 -3
  181. package/dist/process-registry.js.map +2 -2
  182. package/dist/ps-slash.js +54 -15
  183. package/dist/ps-slash.js.map +2 -2
  184. package/dist/read.d.ts +1 -1
  185. package/dist/read.d.ts.map +1 -1
  186. package/dist/read.js +3 -2
  187. package/dist/read.js.map +2 -2
  188. package/dist/replace.d.ts +1 -1
  189. package/dist/replace.d.ts.map +1 -1
  190. package/dist/replace.js +4 -4
  191. package/dist/replace.js.map +2 -2
  192. package/dist/scaffold.d.ts +1 -1
  193. package/dist/scaffold.d.ts.map +1 -1
  194. package/dist/scaffold.js +2 -2
  195. package/dist/scaffold.js.map +1 -1
  196. package/dist/search.d.ts +1 -1
  197. package/dist/search.d.ts.map +1 -1
  198. package/dist/search.js +4 -4
  199. package/dist/search.js.map +2 -2
  200. package/dist/session-kanban.d.ts +4 -1
  201. package/dist/session-kanban.d.ts.map +1 -1
  202. package/dist/session-kanban.js +23 -15
  203. package/dist/session-kanban.js.map +2 -2
  204. package/dist/set-working-dir.d.ts +1 -1
  205. package/dist/set-working-dir.d.ts.map +1 -1
  206. package/dist/skill.d.ts +1 -1
  207. package/dist/skill.d.ts.map +1 -1
  208. package/dist/skill.js +2 -5
  209. package/dist/skill.js.map +2 -2
  210. package/dist/spawn-background.d.ts.map +1 -1
  211. package/dist/task.d.ts +3 -2
  212. package/dist/task.d.ts.map +1 -1
  213. package/dist/task.js +6 -8
  214. package/dist/task.js.map +2 -2
  215. package/dist/test.d.ts +1 -1
  216. package/dist/test.d.ts.map +1 -1
  217. package/dist/test.js +81 -16
  218. package/dist/test.js.map +2 -2
  219. package/dist/todo.d.ts +2 -1
  220. package/dist/todo.d.ts.map +1 -1
  221. package/dist/todo.js +4 -4
  222. package/dist/todo.js.map +2 -2
  223. package/dist/tool-help.d.ts +1 -1
  224. package/dist/tool-help.d.ts.map +1 -1
  225. package/dist/tool-help.js.map +1 -1
  226. package/dist/tool-search.d.ts +1 -1
  227. package/dist/tool-search.d.ts.map +1 -1
  228. package/dist/tool-search.js.map +1 -1
  229. package/dist/tool-tier.d.ts +16 -0
  230. package/dist/tool-tier.d.ts.map +1 -0
  231. package/dist/tool-tier.js +22233 -0
  232. package/dist/tool-tier.js.map +7 -0
  233. package/dist/tool-use.d.ts +1 -1
  234. package/dist/tool-use.d.ts.map +1 -1
  235. package/dist/tool-use.js.map +1 -1
  236. package/dist/tree.d.ts +1 -1
  237. package/dist/tree.d.ts.map +1 -1
  238. package/dist/tree.js +2 -2
  239. package/dist/tree.js.map +1 -1
  240. package/dist/typecheck.d.ts +1 -1
  241. package/dist/typecheck.d.ts.map +1 -1
  242. package/dist/typecheck.js +81 -16
  243. package/dist/typecheck.js.map +2 -2
  244. package/dist/write.d.ts +1 -1
  245. package/dist/write.d.ts.map +1 -1
  246. package/dist/write.js +3 -2
  247. package/dist/write.js.map +2 -2
  248. package/package.json +7 -3
package/dist/edit.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/edit.ts", "../src/_edit-match.ts", "../src/_syntax-check.ts", "../src/_util.ts"],
4
- "sourcesContent": ["import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n ToolValidationError,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport {\n adjustIndent,\n findLadderMatches,\n firstLineIndent,\n type MatchTier,\n nearestMatchHint,\n TIER_CONFIDENCE,\n TIER_LABEL,\n} from './_edit-match.js';\nimport { checkSyntax } from './_syntax-check.js';\nimport { safeResolveReal, sha256hex } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists \u2014 use this to ensure you target the\n * right location.\n */\n replace_all?: boolean | undefined;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n /**\n * How `old_string` was located. Anything other than 'exact' means a\n * whitespace/fuzzy fallback fired \u2014 the note carries the confidence level\n * and the diff shows exactly what was replaced.\n */\n matched_by: MatchTier;\n /**\n * Parse errors found in the file AFTER the edit was applied (TS/JS/JSON\n * only). The edit is still on disk \u2014 fix these in a follow-up edit now,\n * before doing anything else.\n */\n syntax_errors?: string[] | undefined;\n note?: string | undefined;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. ' +\n 'It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. ' +\n 'Fails safely if the `old_string` appears more than once unless `replace_all` is set.',\n usageHint:\n 'RECOMMENDED WORKFLOW:\\n' +\n '1. Prefer calling `read` on the target file first when planning an edit.\\n' +\n '2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\\n' +\n '3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\\n' +\n '4. `new_string` must be the exact replacement text.\\n\\n' +\n 'If no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\\n' +\n 'If `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\\n' +\n 'After the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.',\n selection: {\n doNotUseWhen: 'creating a new file, replacing the whole file, or applying an existing unified diff.',\n useInstead: ['write', 'patch'],\n },\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx, opts) {\n if (!input?.path) {\n throw new ToolValidationError({ message: 'edit: path is required', field: 'path' });\n }\n if (input.old_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: old_string is required',\n field: 'old_string',\n });\n }\n if (input.new_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: new_string is required',\n field: 'new_string',\n });\n }\n if (input.old_string === '') {\n throw new ToolValidationError({\n message: 'edit: old_string cannot be empty',\n field: 'old_string',\n });\n }\n\n const absPath = await safeResolveReal(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`,\n field: 'path',\n context: { exists: false },\n });\n }\n throw err;\n });\n if (!stat.isFile()) {\n throw new ToolValidationError({\n message: `edit: \"${input.path}\" is not a regular file`,\n field: 'path',\n });\n }\n\n const autoRead = !ctx.hasRead(absPath);\n // Read BEFORE mtime check to eliminate TOCTOU window.\n // The sequence must be: read content \u2192 check mtime \u2192 apply edit.\n // If we check mtime first, a concurrent modification between the\n // stat call and the read gives us stale content to search/replace.\n const original = await fs.readFile(absPath, 'utf8');\n const updated = await fs.stat(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n const originalHash = sha256hex(original);\n // Optional call: embedders and older test fixtures may hand in a\n // duck-typed Context without hash tracking \u2014 they fall back to mtime.\n const lastReadHash = ctx.lastReadHash?.(absPath);\n if (lastReadHash !== undefined) {\n // Content hash is the authoritative arbiter when available: it closes\n // the mtime tolerance window (an external write within 2 s of the read\n // on Windows is invisible to mtime) and ignores content-preserving\n // mtime bumps (touch, checkout of identical content).\n if (lastReadHash !== originalHash) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n } else {\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && updated.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n }\n if (autoRead && updated.mtimeMs > stat.mtimeMs + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" changed while being auto-read. Retry the edit.`,\n field: 'path',\n context: { reason: 'auto_read_race' },\n });\n }\n const autoReadNote = autoRead\n ? `No prior read was recorded for \"${input.path}\"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.`\n : undefined;\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, 'user', originalHash);\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n matched_by: 'exact',\n note: autoReadNote,\n };\n }\n\n // Check abort signal before entering potentially slow matching logic\n opts?.signal?.throwIfAborted();\n const ladder = findLadderMatches(fileLf, oldLf);\n\n if (!ladder) {\n opts?.signal?.throwIfAborted();\n const hint = nearestMatchHint(fileLf, oldLf);\n throw new ToolValidationError({\n message: `edit: no match for old_string in \"${input.path}\".${\n hint\n ? ` Nearest match near line ${hint.line}:\\n${hint.snippet}\\n` +\n `Compare this against your old_string and retry with the file's actual text.`\n : ''\n }`,\n field: 'old_string',\n });\n }\n\n const { tier, matches } = ladder;\n const count = matches.length;\n\n if (ladder.ambiguous) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close ` +\n `to distinguish (lines: ${lines.join(', ')}) in \"${input.path}\". ` +\n `Re-read the file and use the exact text of the intended block.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n // Fuzzy/indent-insensitive matches are single-target operations: applying\n // them in bulk multiplies a low-confidence guess. replace_all therefore\n // requires at least a trailing-whitespace-level match.\n if (input.replace_all && tier !== 'exact' && tier !== 'trailing-whitespace') {\n throw new ToolValidationError({\n message:\n `edit: old_string only matched via ${TIER_LABEL[tier]} in \"${input.path}\", ` +\n `but replace_all requires an exact (or trailing-whitespace) match. ` +\n `Re-read the file and use its exact text.`,\n field: 'old_string',\n context: { matchTier: tier },\n });\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')})` +\n `${tier === 'exact' ? '' : ` via ${TIER_LABEL[tier]}`}. ` +\n `Add more context to make it unique, or set replace_all: true.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n let newFileLf: string;\n let tierNote: string | undefined;\n if (tier === 'exact') {\n newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n } else {\n // Fallback tiers matched whole-line windows \u2014 splice by char offsets,\n // right to left so earlier offsets stay valid. For indent-insensitive\n // tiers, shift the replacement to the file's actual indentation.\n let replacement = newLf;\n let indentSuffix = '';\n if (tier === 'whitespace-normalized' || tier === 'fuzzy') {\n const first = matches[0] as (typeof matches)[number];\n const matchedText = fileLf.slice(first.start, first.end);\n const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));\n replacement = adjusted.text;\n if (adjusted.adjusted) indentSuffix = '; replacement re-indented to match the file';\n }\n newFileLf = fileLf;\n const applied = input.replace_all ? matches : matches.slice(0, 1);\n for (let i = applied.length - 1; i >= 0; i--) {\n const m = applied[i] as (typeof matches)[number];\n newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);\n }\n const scoreSuffix =\n ladder.score !== undefined ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : '';\n tierNote =\n `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ` +\n `${(matches[0] as (typeof matches)[number]).startLine} ` +\n `(confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). ` +\n `Review the diff to confirm the intended target was edited.`;\n }\n const newFile = toStyle(newFileLf, style);\n\n // Last exit before mutating the filesystem: a run aborted during the\n // read/match phase must not leave the edit behind. (atomicWrite itself\n // is all-or-nothing \u2014 the file is old or new, never partial.)\n opts?.signal?.throwIfAborted();\n await atomicWrite(absPath, newFile, { mode: updated.mode & 0o777 });\n const written = await fs.stat(absPath);\n // Record mtime + content hash so a later edit detects external\n // modification, but tag as 'write' so the permission policy's\n // write-smart-bypass does NOT treat this as \"user already saw the\n // content\" (P1 #1).\n ctx.recordRead(absPath, written.mtimeMs, 'write', sha256hex(newFile));\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n // Check abort before diff generation (can be slow on large files)\n opts?.signal?.throwIfAborted();\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n // Post-edit syntax validation (TS/JS/JSON). The edit stays on disk \u2014\n // errors come back in the same turn so the model fixes them immediately.\n const syntax = await checkSyntax(absPath, newFile, original).catch(() => undefined);\n let syntaxNote: string | undefined;\n if (syntax && syntax.errors.length > 0) {\n syntaxNote = syntax.preExisting\n ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.`\n : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;\n }\n\n const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n matched_by: tier,\n syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : undefined,\n note: notes.length > 0 ? notes.join('\\n') : undefined,\n };\n },\n};\n", "/**\n * Matching ladder for the `edit` tool.\n *\n * Models routinely hallucinate whitespace when reproducing code they read\n * many turns ago: trailing spaces appear, indentation shifts by a level,\n * tabs become spaces. A strict exact-match `old_string` fails on all of\n * these even though the intended target is unambiguous. This module\n * implements a graded fallback chain \u2014 each tier is strictly more permissive\n * and strictly lower-confidence than the previous one:\n *\n * 1. `exact` \u2014 substring match (the classic behavior).\n * 2. `trailing-whitespace` \u2014 whole-line window match ignoring trailing\n * whitespace per line. Zero ambiguity risk\n * beyond exact (trailing blanks never carry\n * meaning in the languages we edit).\n * 3. `whitespace-normalized` \u2014 whole-line window match ignoring leading\n * AND trailing whitespace per line. The\n * replacement is re-indented to the file's\n * actual indentation.\n * 4. `fuzzy` \u2014 block-anchor match: first and last lines\n * must match (trimmed), interior lines are\n * compared by Levenshtein similarity. Only a\n * single candidate with a clear margin over\n * the runner-up is accepted.\n *\n * The caller (edit.ts) enforces per-tier uniqueness, restricts `replace_all`\n * to tiers 1\u20132, and surfaces the tier + confidence in the tool output so the\n * model and the session log both see when a fallback fired.\n */\n\nexport type MatchTier = 'exact' | 'trailing-whitespace' | 'whitespace-normalized' | 'fuzzy';\n\nexport interface LadderMatch {\n /** Char offset (inclusive) of the match start in the LF-normalized file. */\n start: number;\n /** Char offset (exclusive) of the match end. */\n end: number;\n /** 1-based line number of the first matched line (for error messages). */\n startLine: number;\n}\n\nexport interface LadderResult {\n tier: MatchTier;\n matches: LadderMatch[];\n /** Similarity score of the best candidate \u2014 only set for the fuzzy tier. */\n score?: number | undefined;\n /**\n * Fuzzy tier only: true when two candidates scored within the ambiguity\n * margin of each other, so no single target can be trusted. `matches`\n * then contains the rival candidates for the error message.\n */\n ambiguous?: boolean | undefined;\n}\n\n/** Human-readable label per tier, used in notes and error messages. */\nexport const TIER_LABEL: Record<MatchTier, string> = {\n exact: 'exact match',\n 'trailing-whitespace': 'whitespace-normalized match (trailing whitespace ignored)',\n 'whitespace-normalized':\n 'whitespace-normalized match (indentation ignored, replacement re-indented)',\n fuzzy: 'fuzzy match (block anchors exact, interior \u226590% similar)',\n};\n\n/** Confidence per tier, recorded in the tool output note. */\nexport const TIER_CONFIDENCE: Record<MatchTier, 'exact' | 'high' | 'medium' | 'low'> = {\n exact: 'exact',\n 'trailing-whitespace': 'high',\n 'whitespace-normalized': 'medium',\n fuzzy: 'low',\n};\n\n/** Below this many non-whitespace chars, indent-insensitive tiers are too\n * ambiguous to trust (`}` would match everywhere). */\nconst MIN_NORMALIZED_NEEDLE_CHARS = 16;\n/** Fuzzy interior similarity threshold. */\nconst FUZZY_MIN_SIMILARITY = 0.9;\n/** A fuzzy best-candidate must beat the runner-up by at least this margin. */\nconst FUZZY_AMBIGUITY_MARGIN = 0.05;\n/** Cost guard: skip fuzzy comparison on interiors larger than this. */\nconst FUZZY_MAX_INTERIOR_CHARS = 2000;\n\n/**\n * Walk the ladder and return the first tier that produces at least one\n * match. Returns `undefined` when no tier matches.\n */\nexport function findLadderMatches(fileLf: string, oldLf: string): LadderResult | undefined {\n // Tier 1 \u2014 exact substring scan (supports sub-line needles).\n const exact: LadderMatch[] = [];\n let idx = fileLf.indexOf(oldLf);\n while (idx !== -1) {\n exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n if (exact.length > 0) return { tier: 'exact', matches: exact };\n\n // Line-based tiers operate on whole-line windows.\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n const offsets = lineOffsets(fileLines);\n\n // Pre-trim file lines ONCE so the sliding window doesn't call trimEnd()/\n // trim() on every (fileLine \u00D7 needleLine) pair \u2014 avoiding O(n\u00B7m) string\n // allocations for every tier. For a 10 000-line \u00D7 10-line needle that\n // saves ~200 000 trim() calls per tier.\n const fileTrimEnd = fileLines.map((l) => l.trimEnd());\n const needleTrimEnd = needleLines.map((l) => l.trimEnd());\n\n const trailing = windowScan(\n fileTrimEnd,\n needleTrimEnd,\n offsets,\n fileLines,\n (a, b) => a === b,\n );\n if (trailing.length > 0) return { tier: 'trailing-whitespace', matches: trailing };\n\n const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);\n if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return undefined;\n\n const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());\n const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());\n const normalized = windowScan(\n fileTrimmed,\n needleTrimmed,\n offsets,\n fileLines,\n (a, b) => a === b,\n );\n if (normalized.length > 0) return { tier: 'whitespace-normalized', matches: normalized };\n\n return fuzzyScan(fileLines, needleLines, offsets);\n}\n\n/** 1-based line number containing char offset `pos`.\n * Uses indexOf('\\n') for O(log n) instead of scanning char-by-char.\n * For small pos (< 512 chars) the simple loop is faster, so we keep that\n * fast path for the common case (single-line needles, small files). */\nfunction lineAt(text: string, pos: number): number {\n if (pos < 512) {\n let line = 1;\n for (let i = 0; i < pos; i++) {\n if (text.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n }\n // For large offsets, binary-search through newline positions.\n let line = 1;\n let search = 0;\n while (true) {\n const idx = text.indexOf('\\n', search);\n if (idx === -1 || idx >= pos) return line;\n search = idx + 1;\n line++;\n }\n}\n\n/** Start char offset of each line in the LF-normalized text. */\nfunction lineOffsets(lines: string[]): number[] {\n const out = new Array<number>(lines.length);\n let pos = 0;\n for (let i = 0; i < lines.length; i++) {\n out[i] = pos;\n pos += (lines[i] as string).length + 1; // +1 for the '\\n'\n }\n return out;\n}\n\nfunction windowToMatch(\n fileLines: string[],\n offsets: number[],\n start: number,\n windowLen: number,\n): LadderMatch {\n const lastLine = start + windowLen - 1;\n return {\n start: offsets[start] as number,\n end: (offsets[lastLine] as number) + (fileLines[lastLine] as string).length,\n startLine: start + 1,\n };\n}\n\n/** Slide a needle-sized window over the file lines with a per-line comparator.\n * Matched windows never overlap (scan resumes after the window). */\nfunction windowScan(\n comparisonLines: string[],\n needleLines: string[],\n offsets: number[],\n originalLines: string[],\n eq: (fileLine: string, needleLine: string) => boolean,\n): LadderMatch[] {\n const n = needleLines.length;\n const out: LadderMatch[] = [];\n for (let i = 0; i + n <= comparisonLines.length; i++) {\n let all = true;\n for (let j = 0; j < n; j++) {\n if (!eq(comparisonLines[i + j] as string, needleLines[j] as string)) {\n all = false;\n break;\n }\n }\n if (all) {\n out.push(windowToMatch(originalLines, offsets, i, n));\n i += n - 1; // no overlapping windows\n }\n }\n return out;\n}\n\n/** Block-anchor fuzzy tier: first/last lines trim-exact, interior by\n * Levenshtein similarity. Requires \u22653 needle lines so anchors exist. */\nfunction fuzzyScan(\n fileLines: string[],\n needleLines: string[],\n offsets: number[],\n): LadderResult | undefined {\n const n = needleLines.length;\n if (n < 3) return undefined;\n const firstNeedle = (needleLines[0] as string).trim();\n const lastNeedle = (needleLines[n - 1] as string).trim();\n const needleInterior = needleLines\n .slice(1, -1)\n .map((l) => l.trim())\n .join('\\n');\n if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return undefined;\n\n const candidates: Array<{ match: LadderMatch; score: number }> = [];\n for (let i = 0; i + n <= fileLines.length; i++) {\n if ((fileLines[i] as string).trim() !== firstNeedle) continue;\n if ((fileLines[i + n - 1] as string).trim() !== lastNeedle) continue;\n const windowInterior = fileLines\n .slice(i + 1, i + n - 1)\n .map((l) => l.trim())\n .join('\\n');\n if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;\n const score = similarity(needleInterior, windowInterior);\n if (score >= FUZZY_MIN_SIMILARITY) {\n candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });\n }\n }\n\n if (candidates.length === 0) return undefined;\n candidates.sort((a, b) => b.score - a.score);\n const best = candidates[0] as { match: LadderMatch; score: number };\n const runnerUp = candidates[1];\n if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {\n return {\n tier: 'fuzzy',\n matches: candidates.map((c) => c.match),\n score: best.score,\n ambiguous: true,\n };\n }\n return { tier: 'fuzzy', matches: [best.match], score: best.score };\n}\n\n/** Normalized similarity in [0, 1]: 1 \u2212 levenshtein / max-length. */\nexport function similarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n // Quick reject: a length gap larger than the allowed edit budget can\n // never reach the threshold \u2014 skip the O(n\u00B7m) DP entirely.\n if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {\n return 1 - Math.abs(a.length - b.length) / max;\n }\n return 1 - levenshtein(a, b) / max;\n}\n\n/** Two-row Levenshtein distance. */\nfunction levenshtein(a: string, b: string): number {\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n let prev = new Array<number>(b.length + 1);\n let cur = new Array<number>(b.length + 1);\n for (let j = 0; j <= b.length; j++) prev[j] = j;\n for (let i = 1; i <= a.length; i++) {\n cur[0] = i;\n const ca = a.charCodeAt(i - 1);\n for (let j = 1; j <= b.length; j++) {\n const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;\n cur[j] = Math.min(\n (prev[j] as number) + 1,\n (cur[j - 1] as number) + 1,\n (prev[j - 1] as number) + cost,\n );\n }\n [prev, cur] = [cur, prev];\n }\n return prev[b.length] as number;\n}\n\n/** Leading whitespace (spaces/tabs) of the first non-empty line, or ''. */\nexport function firstLineIndent(text: string): string {\n for (const line of text.split('\\n')) {\n if (line.trim() === '') continue;\n return (/^[ \\t]*/.exec(line) as RegExpExecArray)[0];\n }\n return '';\n}\n\n/**\n * Shift `newLf` from the needle's indentation to the matched block's actual\n * indentation. Only clean prefix relationships are adjusted (purely adding\n * or removing leading whitespace); a tabs-vs-spaces mismatch is left alone\n * rather than guessed at \u2014 the diff in the tool output makes the result\n * visible either way.\n */\nexport function adjustIndent(\n newLf: string,\n fromIndent: string,\n toIndent: string,\n): { text: string; adjusted: boolean } {\n if (fromIndent === toIndent) return { text: newLf, adjusted: false };\n const lines = newLf.split('\\n');\n if (toIndent.startsWith(fromIndent)) {\n const extra = toIndent.slice(fromIndent.length);\n return {\n text: lines.map((l) => (l.trim() === '' ? l : extra + l)).join('\\n'),\n adjusted: true,\n };\n }\n if (fromIndent.startsWith(toIndent)) {\n const remove = fromIndent.slice(toIndent.length);\n return {\n text: lines.map((l) => (l.startsWith(remove) ? l.slice(remove.length) : l)).join('\\n'),\n adjusted: true,\n };\n }\n return { text: newLf, adjusted: false };\n}\n\n/**\n * Best-effort locator for the no-match error: find the window whose trimmed\n * lines are most similar to the needle (cheap common-prefix scoring \u2014 this\n * runs on the failure path where the needle by definition doesn't match, so\n * precision matters less than never being slow). Returns the 1-based line\n * and a short snippet the model can use to correct itself without re-reading.\n */\nexport function nearestMatchHint(\n fileLf: string,\n oldLf: string,\n): { line: number; snippet: string } | undefined {\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n\n const needleTrimmed = needleLines.map((l) => l.trim());\n let bestScore = 0;\n let bestStart = -1;\n for (let i = 0; i + needleLines.length <= fileLines.length; i++) {\n let sum = 0;\n for (let j = 0; j < needleLines.length; j++) {\n sum += prefixSimilarity(needleTrimmed[j] as string, (fileLines[i + j] as string).trim());\n }\n const score = sum / needleLines.length;\n if (score > bestScore) {\n bestScore = score;\n bestStart = i;\n }\n }\n if (bestStart === -1 || bestScore < 0.5) return undefined;\n\n const snippet = fileLines\n .slice(bestStart, bestStart + Math.min(3, needleLines.length))\n .map((l) => (l.length > 120 ? `${l.slice(0, 120)}\u2026` : l))\n .join('\\n');\n return { line: bestStart + 1, snippet };\n}\n\n/** Cheap similarity proxy: shared prefix length over max length. */\nfunction prefixSimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n let p = 0;\n const lim = Math.min(a.length, b.length);\n while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;\n return p / max;\n}\n", "/**\n * Post-edit syntax validation for `edit` and `write`.\n *\n * After a mutation lands on disk, the new content is parsed (TS/JS via the\n * TypeScript compiler's parser, JSON via JSON.parse with a JSONC fallback)\n * and any parse errors are surfaced in the tool output. The edit is NOT\n * rolled back \u2014 reverting would desynchronize the model's picture of the\n * file \u2014 but the errors come back in the same turn so the model fixes the\n * breakage before the user ever opens a broken file.\n *\n * The check is purely syntactic (no type checking, no project program), so\n * it costs single-digit milliseconds per file. Semantic type errors remain\n * the `type-gate` plugin's job.\n */\nimport * as path from 'node:path';\n\nconst TS_LIKE = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);\n/** Skip pathologically large files \u2014 the model never writes these in one go. */\nconst MAX_CHECK_CHARS = 1_500_000;\n/** Cap reported errors so a mangled file doesn't flood the context. */\nconst MAX_ERRORS = 5;\n\ntype Ts = typeof import('@typescript/typescript6');\n\nlet tsLoad: Promise<Ts | null> | null = null;\n/** Lazy-load the TypeScript compiler (already a dependency via the codebase\n * indexer) so tools that never touch TS files don't pay its import cost. */\nfunction loadTypescript(): Promise<Ts | null> {\n tsLoad ??= import('@typescript/typescript6').then(\n (m) => ((m as unknown as { default?: Ts }).default ?? m) as Ts,\n () => null,\n );\n return tsLoad;\n}\n\nexport interface SyntaxCheckResult {\n /** Parse errors in the new content (capped at {@link MAX_ERRORS}). */\n errors: string[];\n /** True when the pre-edit content already failed to parse \u2014 the edit did\n * not introduce the breakage (though it didn't fix it either). */\n preExisting: boolean;\n}\n\n/**\n * Check `content` for syntax errors based on the file extension.\n * Returns `undefined` when the file type is not checkable (or the checker\n * is unavailable) and a result with an empty `errors` array when clean.\n * Pass `previousContent` to distinguish newly-introduced errors from\n * pre-existing ones.\n */\nexport async function checkSyntax(\n filePath: string,\n content: string,\n previousContent?: string,\n): Promise<SyntaxCheckResult | undefined> {\n const errors = await parseErrors(filePath, content);\n if (errors === undefined) return undefined;\n if (errors.length === 0) return { errors, preExisting: false };\n let preExisting = false;\n if (previousContent !== undefined) {\n const prevErrors = await parseErrors(filePath, previousContent);\n preExisting = prevErrors !== undefined && prevErrors.length > 0;\n }\n return { errors, preExisting };\n}\n\n/** Files that are conventionally JSONC (comments + trailing commas allowed). */\nfunction isJsoncFile(filePath: string): boolean {\n const base = path.basename(filePath).toLowerCase();\n if (base.endsWith('.jsonc')) return true;\n if (/^(tsconfig|jsconfig)([.-].*)?\\.json$/.test(base)) return true;\n const dir = path.basename(path.dirname(filePath)).toLowerCase();\n return dir === '.vscode';\n}\n\nasync function parseErrors(filePath: string, content: string): Promise<string[] | undefined> {\n if (content.length > MAX_CHECK_CHARS) return undefined;\n const ext = path.extname(filePath).toLowerCase();\n\n if (ext === '.json' || ext === '.jsonc') {\n try {\n JSON.parse(content);\n return [];\n } catch (err) {\n // Known-JSONC files (tsconfig.json, VS Code settings, *.jsonc) accept\n // comments and trailing commas \u2014 re-parse with the TS config parser\n // before declaring them broken. Plain .json stays strict: a trailing\n // comma there IS an error the model should fix.\n if (isJsoncFile(filePath)) {\n const ts = await loadTypescript();\n if (ts) {\n const jsonc = ts.parseConfigFileTextToJson(filePath, content);\n if (!jsonc.error) return [];\n return [formatDiag(ts, jsonc.error, content)];\n }\n }\n return [`JSON parse error: ${(err as Error).message}`];\n }\n }\n\n if (!TS_LIKE.has(ext)) return undefined;\n const ts = await loadTypescript();\n if (!ts) return undefined;\n\n const scriptKind =\n ext === '.tsx'\n ? ts.ScriptKind.TSX\n : ext === '.ts' || ext === '.mts' || ext === '.cts'\n ? ts.ScriptKind.TS\n : // Plain JS may legitimately contain JSX; the JSX grammar is a\n // superset for untyped code, so parsing .js/.jsx as JSX avoids\n // false positives on React files.\n ts.ScriptKind.JSX;\n\n const sourceFile = ts.createSourceFile(\n path.basename(filePath),\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n scriptKind,\n );\n // parseDiagnostics is not in the public .d.ts but has been the stable\n // home of the parser's syntactic diagnostics for a decade (the public\n // alternative, program.getSyntacticDiagnostics, needs a full Program).\n const diags =\n (sourceFile as unknown as {\n parseDiagnostics?: import('@typescript/typescript6').Diagnostic[];\n })\n .parseDiagnostics ?? [];\n return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts, d, content, sourceFile));\n}\n\nfunction formatDiag(\n ts: Ts,\n diag: import('@typescript/typescript6').Diagnostic,\n content: string,\n sourceFile?: import('@typescript/typescript6').SourceFile,\n): string {\n const message = ts.flattenDiagnosticMessageText(diag.messageText, ' ');\n if (diag.start === undefined) return message;\n let line: number;\n if (sourceFile) {\n line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;\n } else {\n line = 1;\n for (let i = 0; i < diag.start && i < content.length; i++) {\n if (content.charCodeAt(i) === 0x0a) line++;\n }\n }\n return `line ${line}: ${message}`;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core/types';\nimport {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core/utils';\nimport { ToolValidationError } from '@wrongstack/core/types';\nimport {\n adjustIndent,\n findLadderMatches,\n firstLineIndent,\n type MatchTier,\n nearestMatchHint,\n TIER_CONFIDENCE,\n TIER_LABEL,\n} from './_edit-match.js';\nimport { checkSyntax } from './_syntax-check.js';\nimport { safeResolveReal, sha256hex } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists \u2014 use this to ensure you target the\n * right location.\n */\n replace_all?: boolean | undefined;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n /**\n * How `old_string` was located. Anything other than 'exact' means a\n * whitespace/fuzzy fallback fired \u2014 the note carries the confidence level\n * and the diff shows exactly what was replaced.\n */\n matched_by: MatchTier;\n /**\n * Parse errors found in the file AFTER the edit was applied (TS/JS/JSON\n * only). The edit is still on disk \u2014 fix these in a follow-up edit now,\n * before doing anything else.\n */\n syntax_errors?: string[] | undefined;\n note?: string | undefined;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. ' +\n 'It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. ' +\n 'Fails safely if the `old_string` appears more than once unless `replace_all` is set.',\n usageHint:\n 'RECOMMENDED WORKFLOW:\\n' +\n '1. Prefer calling `read` on the target file first when planning an edit.\\n' +\n '2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\\n' +\n '3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\\n' +\n '4. `new_string` must be the exact replacement text.\\n\\n' +\n 'If no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\\n' +\n 'If `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\\n' +\n 'After the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.',\n selection: {\n doNotUseWhen: 'creating a new file, replacing the whole file, or applying an existing unified diff.',\n useInstead: ['write', 'patch'],\n },\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx, opts) {\n if (!input?.path) {\n throw new ToolValidationError({ message: 'edit: path is required', field: 'path' });\n }\n if (input.old_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: old_string is required',\n field: 'old_string',\n });\n }\n if (input.new_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: new_string is required',\n field: 'new_string',\n });\n }\n if (input.old_string === '') {\n throw new ToolValidationError({\n message: 'edit: old_string cannot be empty',\n field: 'old_string',\n });\n }\n\n const absPath = await safeResolveReal(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`,\n field: 'path',\n context: { exists: false },\n });\n }\n throw err;\n });\n if (!stat.isFile()) {\n throw new ToolValidationError({\n message: `edit: \"${input.path}\" is not a regular file`,\n field: 'path',\n });\n }\n\n const autoRead = !ctx.hasRead(absPath);\n // Read BEFORE mtime check to eliminate TOCTOU window.\n // The sequence must be: read content \u2192 check mtime \u2192 apply edit.\n // If we check mtime first, a concurrent modification between the\n // stat call and the read gives us stale content to search/replace.\n const original = await fs.readFile(absPath, 'utf8');\n const updated = await fs.stat(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n const originalHash = sha256hex(original);\n // Optional call: embedders and older test fixtures may hand in a\n // duck-typed Context without hash tracking \u2014 they fall back to mtime.\n const lastReadHash = ctx.lastReadHash?.(absPath);\n if (lastReadHash !== undefined) {\n // Content hash is the authoritative arbiter when available: it closes\n // the mtime tolerance window (an external write within 2 s of the read\n // on Windows is invisible to mtime) and ignores content-preserving\n // mtime bumps (touch, checkout of identical content).\n if (lastReadHash !== originalHash) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n } else {\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && updated.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n }\n if (autoRead && updated.mtimeMs > stat.mtimeMs + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" changed while being auto-read. Retry the edit.`,\n field: 'path',\n context: { reason: 'auto_read_race' },\n });\n }\n const autoReadNote = autoRead\n ? `No prior read was recorded for \"${input.path}\"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.`\n : undefined;\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, 'user', originalHash);\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n matched_by: 'exact',\n note: autoReadNote,\n };\n }\n\n // Check abort signal before entering potentially slow matching logic\n opts?.signal?.throwIfAborted();\n const ladder = findLadderMatches(fileLf, oldLf);\n\n if (!ladder) {\n opts?.signal?.throwIfAborted();\n const hint = nearestMatchHint(fileLf, oldLf);\n throw new ToolValidationError({\n message: `edit: no match for old_string in \"${input.path}\".${\n hint\n ? ` Nearest match near line ${hint.line}:\\n${hint.snippet}\\n` +\n `Compare this against your old_string and retry with the file's actual text.`\n : ''\n }`,\n field: 'old_string',\n });\n }\n\n const { tier, matches } = ladder;\n const count = matches.length;\n\n if (ladder.ambiguous) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close ` +\n `to distinguish (lines: ${lines.join(', ')}) in \"${input.path}\". ` +\n `Re-read the file and use the exact text of the intended block.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n // Fuzzy/indent-insensitive matches are single-target operations: applying\n // them in bulk multiplies a low-confidence guess. replace_all therefore\n // requires at least a trailing-whitespace-level match.\n if (input.replace_all && tier !== 'exact' && tier !== 'trailing-whitespace') {\n throw new ToolValidationError({\n message:\n `edit: old_string only matched via ${TIER_LABEL[tier]} in \"${input.path}\", ` +\n `but replace_all requires an exact (or trailing-whitespace) match. ` +\n `Re-read the file and use its exact text.`,\n field: 'old_string',\n context: { matchTier: tier },\n });\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')})` +\n `${tier === 'exact' ? '' : ` via ${TIER_LABEL[tier]}`}. ` +\n `Add more context to make it unique, or set replace_all: true.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n let newFileLf: string;\n let tierNote: string | undefined;\n if (tier === 'exact') {\n newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n } else {\n // Fallback tiers matched whole-line windows \u2014 splice by char offsets,\n // right to left so earlier offsets stay valid. For indent-insensitive\n // tiers, shift the replacement to the file's actual indentation.\n let replacement = newLf;\n let indentSuffix = '';\n if (tier === 'whitespace-normalized' || tier === 'fuzzy') {\n const first = matches[0] as (typeof matches)[number];\n const matchedText = fileLf.slice(first.start, first.end);\n const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));\n replacement = adjusted.text;\n if (adjusted.adjusted) indentSuffix = '; replacement re-indented to match the file';\n }\n newFileLf = fileLf;\n const applied = input.replace_all ? matches : matches.slice(0, 1);\n for (let i = applied.length - 1; i >= 0; i--) {\n const m = applied[i] as (typeof matches)[number];\n newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);\n }\n const scoreSuffix =\n ladder.score !== undefined ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : '';\n tierNote =\n `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ` +\n `${(matches[0] as (typeof matches)[number]).startLine} ` +\n `(confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). ` +\n `Review the diff to confirm the intended target was edited.`;\n }\n const newFile = toStyle(newFileLf, style);\n\n // Last exit before mutating the filesystem: a run aborted during the\n // read/match phase must not leave the edit behind. (atomicWrite itself\n // is all-or-nothing \u2014 the file is old or new, never partial.)\n opts?.signal?.throwIfAborted();\n await atomicWrite(absPath, newFile, { mode: updated.mode & 0o777 });\n const written = await fs.stat(absPath);\n // Record mtime + content hash so a later edit detects external\n // modification, but tag as 'write' so the permission policy's\n // write-smart-bypass does NOT treat this as \"user already saw the\n // content\" (P1 #1).\n ctx.recordRead(absPath, written.mtimeMs, 'write', sha256hex(newFile));\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n // Check abort before diff generation (can be slow on large files)\n opts?.signal?.throwIfAborted();\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n // Post-edit syntax validation (TS/JS/JSON). The edit stays on disk \u2014\n // errors come back in the same turn so the model fixes them immediately.\n const syntax = await checkSyntax(absPath, newFile, original).catch(() => undefined);\n let syntaxNote: string | undefined;\n if (syntax && syntax.errors.length > 0) {\n syntaxNote = syntax.preExisting\n ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.`\n : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;\n }\n\n const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n matched_by: tier,\n syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : undefined,\n note: notes.length > 0 ? notes.join('\\n') : undefined,\n };\n },\n};\n", "/**\n * Matching ladder for the `edit` tool.\n *\n * Models routinely hallucinate whitespace when reproducing code they read\n * many turns ago: trailing spaces appear, indentation shifts by a level,\n * tabs become spaces. A strict exact-match `old_string` fails on all of\n * these even though the intended target is unambiguous. This module\n * implements a graded fallback chain \u2014 each tier is strictly more permissive\n * and strictly lower-confidence than the previous one:\n *\n * 1. `exact` \u2014 substring match (the classic behavior).\n * 2. `trailing-whitespace` \u2014 whole-line window match ignoring trailing\n * whitespace per line. Zero ambiguity risk\n * beyond exact (trailing blanks never carry\n * meaning in the languages we edit).\n * 3. `whitespace-normalized` \u2014 whole-line window match ignoring leading\n * AND trailing whitespace per line. The\n * replacement is re-indented to the file's\n * actual indentation.\n * 4. `fuzzy` \u2014 block-anchor match: first and last lines\n * must match (trimmed), interior lines are\n * compared by Levenshtein similarity. Only a\n * single candidate with a clear margin over\n * the runner-up is accepted.\n *\n * The caller (edit.ts) enforces per-tier uniqueness, restricts `replace_all`\n * to tiers 1\u20132, and surfaces the tier + confidence in the tool output so the\n * model and the session log both see when a fallback fired.\n */\n\nexport type MatchTier = 'exact' | 'trailing-whitespace' | 'whitespace-normalized' | 'fuzzy';\n\nexport interface LadderMatch {\n /** Char offset (inclusive) of the match start in the LF-normalized file. */\n start: number;\n /** Char offset (exclusive) of the match end. */\n end: number;\n /** 1-based line number of the first matched line (for error messages). */\n startLine: number;\n}\n\nexport interface LadderResult {\n tier: MatchTier;\n matches: LadderMatch[];\n /** Similarity score of the best candidate \u2014 only set for the fuzzy tier. */\n score?: number | undefined;\n /**\n * Fuzzy tier only: true when two candidates scored within the ambiguity\n * margin of each other, so no single target can be trusted. `matches`\n * then contains the rival candidates for the error message.\n */\n ambiguous?: boolean | undefined;\n}\n\n/** Human-readable label per tier, used in notes and error messages. */\nexport const TIER_LABEL: Record<MatchTier, string> = {\n exact: 'exact match',\n 'trailing-whitespace': 'whitespace-normalized match (trailing whitespace ignored)',\n 'whitespace-normalized':\n 'whitespace-normalized match (indentation ignored, replacement re-indented)',\n fuzzy: 'fuzzy match (block anchors exact, interior \u226590% similar)',\n};\n\n/** Confidence per tier, recorded in the tool output note. */\nexport const TIER_CONFIDENCE: Record<MatchTier, 'exact' | 'high' | 'medium' | 'low'> = {\n exact: 'exact',\n 'trailing-whitespace': 'high',\n 'whitespace-normalized': 'medium',\n fuzzy: 'low',\n};\n\n/** Below this many non-whitespace chars, indent-insensitive tiers are too\n * ambiguous to trust (`}` would match everywhere). */\nconst MIN_NORMALIZED_NEEDLE_CHARS = 16;\n/** Fuzzy interior similarity threshold. */\nconst FUZZY_MIN_SIMILARITY = 0.9;\n/** A fuzzy best-candidate must beat the runner-up by at least this margin. */\nconst FUZZY_AMBIGUITY_MARGIN = 0.05;\n/** Cost guard: skip fuzzy comparison on interiors larger than this. */\nconst FUZZY_MAX_INTERIOR_CHARS = 2000;\n\n/**\n * Walk the ladder and return the first tier that produces at least one\n * match. Returns `undefined` when no tier matches.\n */\nexport function findLadderMatches(fileLf: string, oldLf: string): LadderResult | undefined {\n // Tier 1 \u2014 exact substring scan (supports sub-line needles).\n const exact: LadderMatch[] = [];\n let idx = fileLf.indexOf(oldLf);\n while (idx !== -1) {\n exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n if (exact.length > 0) return { tier: 'exact', matches: exact };\n\n // Line-based tiers operate on whole-line windows.\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n const offsets = lineOffsets(fileLines);\n\n // Pre-trim file lines ONCE so the sliding window doesn't call trimEnd()/\n // trim() on every (fileLine \u00D7 needleLine) pair \u2014 avoiding O(n\u00B7m) string\n // allocations for every tier. For a 10 000-line \u00D7 10-line needle that\n // saves ~200 000 trim() calls per tier.\n const fileTrimEnd = fileLines.map((l) => l.trimEnd());\n const needleTrimEnd = needleLines.map((l) => l.trimEnd());\n\n const trailing = windowScan(\n fileTrimEnd,\n needleTrimEnd,\n offsets,\n fileLines,\n (a, b) => a === b,\n );\n if (trailing.length > 0) return { tier: 'trailing-whitespace', matches: trailing };\n\n const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);\n if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return undefined;\n\n const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());\n const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());\n const normalized = windowScan(\n fileTrimmed,\n needleTrimmed,\n offsets,\n fileLines,\n (a, b) => a === b,\n );\n if (normalized.length > 0) return { tier: 'whitespace-normalized', matches: normalized };\n\n return fuzzyScan(fileLines, needleLines, offsets);\n}\n\n/** 1-based line number containing char offset `pos`.\n * Uses indexOf('\\n') for O(log n) instead of scanning char-by-char.\n * For small pos (< 512 chars) the simple loop is faster, so we keep that\n * fast path for the common case (single-line needles, small files). */\nfunction lineAt(text: string, pos: number): number {\n if (pos < 512) {\n let line = 1;\n for (let i = 0; i < pos; i++) {\n if (text.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n }\n // For large offsets, binary-search through newline positions.\n let line = 1;\n let search = 0;\n while (true) {\n const idx = text.indexOf('\\n', search);\n if (idx === -1 || idx >= pos) return line;\n search = idx + 1;\n line++;\n }\n}\n\n/** Start char offset of each line in the LF-normalized text. */\nfunction lineOffsets(lines: string[]): number[] {\n const out = new Array<number>(lines.length);\n let pos = 0;\n for (let i = 0; i < lines.length; i++) {\n out[i] = pos;\n pos += (lines[i] as string).length + 1; // +1 for the '\\n'\n }\n return out;\n}\n\nfunction windowToMatch(\n fileLines: string[],\n offsets: number[],\n start: number,\n windowLen: number,\n): LadderMatch {\n const lastLine = start + windowLen - 1;\n return {\n start: offsets[start] as number,\n end: (offsets[lastLine] as number) + (fileLines[lastLine] as string).length,\n startLine: start + 1,\n };\n}\n\n/** Slide a needle-sized window over the file lines with a per-line comparator.\n * Matched windows never overlap (scan resumes after the window). */\nfunction windowScan(\n comparisonLines: string[],\n needleLines: string[],\n offsets: number[],\n originalLines: string[],\n eq: (fileLine: string, needleLine: string) => boolean,\n): LadderMatch[] {\n const n = needleLines.length;\n const out: LadderMatch[] = [];\n for (let i = 0; i + n <= comparisonLines.length; i++) {\n let all = true;\n for (let j = 0; j < n; j++) {\n if (!eq(comparisonLines[i + j] as string, needleLines[j] as string)) {\n all = false;\n break;\n }\n }\n if (all) {\n out.push(windowToMatch(originalLines, offsets, i, n));\n i += n - 1; // no overlapping windows\n }\n }\n return out;\n}\n\n/** Block-anchor fuzzy tier: first/last lines trim-exact, interior by\n * Levenshtein similarity. Requires \u22653 needle lines so anchors exist. */\nfunction fuzzyScan(\n fileLines: string[],\n needleLines: string[],\n offsets: number[],\n): LadderResult | undefined {\n const n = needleLines.length;\n if (n < 3) return undefined;\n const firstNeedle = (needleLines[0] as string).trim();\n const lastNeedle = (needleLines[n - 1] as string).trim();\n const needleInterior = needleLines\n .slice(1, -1)\n .map((l) => l.trim())\n .join('\\n');\n if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return undefined;\n\n const candidates: Array<{ match: LadderMatch; score: number }> = [];\n for (let i = 0; i + n <= fileLines.length; i++) {\n if ((fileLines[i] as string).trim() !== firstNeedle) continue;\n if ((fileLines[i + n - 1] as string).trim() !== lastNeedle) continue;\n const windowInterior = fileLines\n .slice(i + 1, i + n - 1)\n .map((l) => l.trim())\n .join('\\n');\n if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;\n const score = similarity(needleInterior, windowInterior);\n if (score >= FUZZY_MIN_SIMILARITY) {\n candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });\n }\n }\n\n if (candidates.length === 0) return undefined;\n candidates.sort((a, b) => b.score - a.score);\n const best = candidates[0] as { match: LadderMatch; score: number };\n const runnerUp = candidates[1];\n if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {\n return {\n tier: 'fuzzy',\n matches: candidates.map((c) => c.match),\n score: best.score,\n ambiguous: true,\n };\n }\n return { tier: 'fuzzy', matches: [best.match], score: best.score };\n}\n\n/** Normalized similarity in [0, 1]: 1 \u2212 levenshtein / max-length. */\nexport function similarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n // Quick reject: a length gap larger than the allowed edit budget can\n // never reach the threshold \u2014 skip the O(n\u00B7m) DP entirely.\n if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {\n return 1 - Math.abs(a.length - b.length) / max;\n }\n return 1 - levenshtein(a, b) / max;\n}\n\n/** Two-row Levenshtein distance. */\nfunction levenshtein(a: string, b: string): number {\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n let prev = new Array<number>(b.length + 1);\n let cur = new Array<number>(b.length + 1);\n for (let j = 0; j <= b.length; j++) prev[j] = j;\n for (let i = 1; i <= a.length; i++) {\n cur[0] = i;\n const ca = a.charCodeAt(i - 1);\n for (let j = 1; j <= b.length; j++) {\n const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;\n cur[j] = Math.min(\n (prev[j] as number) + 1,\n (cur[j - 1] as number) + 1,\n (prev[j - 1] as number) + cost,\n );\n }\n [prev, cur] = [cur, prev];\n }\n return prev[b.length] as number;\n}\n\n/** Leading whitespace (spaces/tabs) of the first non-empty line, or ''. */\nexport function firstLineIndent(text: string): string {\n for (const line of text.split('\\n')) {\n if (line.trim() === '') continue;\n return (/^[ \\t]*/.exec(line) as RegExpExecArray)[0];\n }\n return '';\n}\n\n/**\n * Shift `newLf` from the needle's indentation to the matched block's actual\n * indentation. Only clean prefix relationships are adjusted (purely adding\n * or removing leading whitespace); a tabs-vs-spaces mismatch is left alone\n * rather than guessed at \u2014 the diff in the tool output makes the result\n * visible either way.\n */\nexport function adjustIndent(\n newLf: string,\n fromIndent: string,\n toIndent: string,\n): { text: string; adjusted: boolean } {\n if (fromIndent === toIndent) return { text: newLf, adjusted: false };\n const lines = newLf.split('\\n');\n if (toIndent.startsWith(fromIndent)) {\n const extra = toIndent.slice(fromIndent.length);\n return {\n text: lines.map((l) => (l.trim() === '' ? l : extra + l)).join('\\n'),\n adjusted: true,\n };\n }\n if (fromIndent.startsWith(toIndent)) {\n const remove = fromIndent.slice(toIndent.length);\n return {\n text: lines.map((l) => (l.startsWith(remove) ? l.slice(remove.length) : l)).join('\\n'),\n adjusted: true,\n };\n }\n return { text: newLf, adjusted: false };\n}\n\n/**\n * Best-effort locator for the no-match error: find the window whose trimmed\n * lines are most similar to the needle (cheap common-prefix scoring \u2014 this\n * runs on the failure path where the needle by definition doesn't match, so\n * precision matters less than never being slow). Returns the 1-based line\n * and a short snippet the model can use to correct itself without re-reading.\n */\nexport function nearestMatchHint(\n fileLf: string,\n oldLf: string,\n): { line: number; snippet: string } | undefined {\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n\n const needleTrimmed = needleLines.map((l) => l.trim());\n let bestScore = 0;\n let bestStart = -1;\n for (let i = 0; i + needleLines.length <= fileLines.length; i++) {\n let sum = 0;\n for (let j = 0; j < needleLines.length; j++) {\n sum += prefixSimilarity(needleTrimmed[j] as string, (fileLines[i + j] as string).trim());\n }\n const score = sum / needleLines.length;\n if (score > bestScore) {\n bestScore = score;\n bestStart = i;\n }\n }\n if (bestStart === -1 || bestScore < 0.5) return undefined;\n\n const snippet = fileLines\n .slice(bestStart, bestStart + Math.min(3, needleLines.length))\n .map((l) => (l.length > 120 ? `${l.slice(0, 120)}\u2026` : l))\n .join('\\n');\n return { line: bestStart + 1, snippet };\n}\n\n/** Cheap similarity proxy: shared prefix length over max length. */\nfunction prefixSimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n let p = 0;\n const lim = Math.min(a.length, b.length);\n while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;\n return p / max;\n}\n", "/**\n * Post-edit syntax validation for `edit` and `write`.\n *\n * After a mutation lands on disk, the new content is parsed (TS/JS via the\n * TypeScript compiler's parser, JSON via JSON.parse with a JSONC fallback)\n * and any parse errors are surfaced in the tool output. The edit is NOT\n * rolled back \u2014 reverting would desynchronize the model's picture of the\n * file \u2014 but the errors come back in the same turn so the model fixes the\n * breakage before the user ever opens a broken file.\n *\n * The check is purely syntactic (no type checking, no project program), so\n * it costs single-digit milliseconds per file. Semantic type errors remain\n * the `type-gate` plugin's job.\n */\nimport * as path from 'node:path';\n\nconst TS_LIKE = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);\n/** Skip pathologically large files \u2014 the model never writes these in one go. */\nconst MAX_CHECK_CHARS = 1_500_000;\n/** Cap reported errors so a mangled file doesn't flood the context. */\nconst MAX_ERRORS = 5;\n\ntype Ts = typeof import('@typescript/typescript6');\n\nlet tsLoad: Promise<Ts | null> | null = null;\n/** Lazy-load the TypeScript compiler (already a dependency via the codebase\n * indexer) so tools that never touch TS files don't pay its import cost. */\nfunction loadTypescript(): Promise<Ts | null> {\n tsLoad ??= import('@typescript/typescript6').then(\n (m) => ((m as unknown as { default?: Ts }).default ?? m) as Ts,\n () => null,\n );\n return tsLoad;\n}\n\nexport interface SyntaxCheckResult {\n /** Parse errors in the new content (capped at {@link MAX_ERRORS}). */\n errors: string[];\n /** True when the pre-edit content already failed to parse \u2014 the edit did\n * not introduce the breakage (though it didn't fix it either). */\n preExisting: boolean;\n}\n\n/**\n * Check `content` for syntax errors based on the file extension.\n * Returns `undefined` when the file type is not checkable (or the checker\n * is unavailable) and a result with an empty `errors` array when clean.\n * Pass `previousContent` to distinguish newly-introduced errors from\n * pre-existing ones.\n */\nexport async function checkSyntax(\n filePath: string,\n content: string,\n previousContent?: string,\n): Promise<SyntaxCheckResult | undefined> {\n const errors = await parseErrors(filePath, content);\n if (errors === undefined) return undefined;\n if (errors.length === 0) return { errors, preExisting: false };\n let preExisting = false;\n if (previousContent !== undefined) {\n const prevErrors = await parseErrors(filePath, previousContent);\n preExisting = prevErrors !== undefined && prevErrors.length > 0;\n }\n return { errors, preExisting };\n}\n\n/** Files that are conventionally JSONC (comments + trailing commas allowed). */\nfunction isJsoncFile(filePath: string): boolean {\n const base = path.basename(filePath).toLowerCase();\n if (base.endsWith('.jsonc')) return true;\n if (/^(tsconfig|jsconfig)([.-].*)?\\.json$/.test(base)) return true;\n const dir = path.basename(path.dirname(filePath)).toLowerCase();\n return dir === '.vscode';\n}\n\nasync function parseErrors(filePath: string, content: string): Promise<string[] | undefined> {\n if (content.length > MAX_CHECK_CHARS) return undefined;\n const ext = path.extname(filePath).toLowerCase();\n\n if (ext === '.json' || ext === '.jsonc') {\n try {\n JSON.parse(content);\n return [];\n } catch (err) {\n // Known-JSONC files (tsconfig.json, VS Code settings, *.jsonc) accept\n // comments and trailing commas \u2014 re-parse with the TS config parser\n // before declaring them broken. Plain .json stays strict: a trailing\n // comma there IS an error the model should fix.\n if (isJsoncFile(filePath)) {\n const ts = await loadTypescript();\n if (ts) {\n const jsonc = ts.parseConfigFileTextToJson(filePath, content);\n if (!jsonc.error) return [];\n return [formatDiag(ts, jsonc.error, content)];\n }\n }\n return [`JSON parse error: ${(err as Error).message}`];\n }\n }\n\n if (!TS_LIKE.has(ext)) return undefined;\n const ts = await loadTypescript();\n if (!ts) return undefined;\n\n const scriptKind =\n ext === '.tsx'\n ? ts.ScriptKind.TSX\n : ext === '.ts' || ext === '.mts' || ext === '.cts'\n ? ts.ScriptKind.TS\n : // Plain JS may legitimately contain JSX; the JSX grammar is a\n // superset for untyped code, so parsing .js/.jsx as JSX avoids\n // false positives on React files.\n ts.ScriptKind.JSX;\n\n const sourceFile = ts.createSourceFile(\n path.basename(filePath),\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n scriptKind,\n );\n // parseDiagnostics is not in the public .d.ts but has been the stable\n // home of the parser's syntactic diagnostics for a decade (the public\n // alternative, program.getSyntacticDiagnostics, needs a full Program).\n const diags =\n (sourceFile as unknown as {\n parseDiagnostics?: import('@typescript/typescript6').Diagnostic[];\n })\n .parseDiagnostics ?? [];\n return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts, d, content, sourceFile));\n}\n\nfunction formatDiag(\n ts: Ts,\n diag: import('@typescript/typescript6').Diagnostic,\n content: string,\n sourceFile?: import('@typescript/typescript6').SourceFile,\n): string {\n const message = ts.flattenDiagnosticMessageText(diag.messageText, ' ');\n if (diag.start === undefined) return message;\n let line: number;\n if (sourceFile) {\n line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;\n } else {\n line = 1;\n for (let i = 0; i < diag.start && i < content.length; i++) {\n if (content.charCodeAt(i) === 0x0a) line++;\n }\n }\n return `line ${line}: ${message}`;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["line", "ts", "path", "stat"]
7
7
  }
package/dist/exec.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  import { type DangerAssessment } from './_danger-detect.js';
3
3
  /**
4
4
  * Apply the configured exec command policy. Recomputes the effective allowlist
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,IAAI,EACV,MAAM,kBAAkB,CAAC;AAU1B,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AA+J1E;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAO,GAAG,IAAI,CAQpI;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAkBD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAO,GAAG,IAAI,CAOjG;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,WAAW,CAAC,MAAM,CAAC,CAErD;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AA4GD,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAID,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA0JhD,CAAC"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAUnD,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AA+J1E;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAO,GAAG,IAAI,CAQpI;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAkBD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAO,GAAG,IAAI,CAOjG;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,WAAW,CAAC,MAAM,CAAC,CAErD;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AA4GD,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAID,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA2JhD,CAAC"}
package/dist/exec.js CHANGED
@@ -4,17 +4,17 @@ import {
4
4
  emitProcessCompleted,
5
5
  emitProcessOutput,
6
6
  emitProcessStarted
7
- } from "@wrongstack/core";
7
+ } from "@wrongstack/core/observability";
8
8
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
9
9
 
10
10
  // src/_env.ts
11
- import { buildChildEnv } from "@wrongstack/core";
11
+ import { buildChildEnv } from "@wrongstack/core/utils";
12
12
 
13
13
  // src/_output-spool.ts
14
14
  import { createWriteStream, mkdirSync } from "node:fs";
15
15
  import * as fsp from "node:fs/promises";
16
16
  import * as path from "node:path";
17
- import { wstackGlobalRoot } from "@wrongstack/core";
17
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
18
18
  var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
19
19
  var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
20
20
  var sweepStarted = false;
@@ -120,7 +120,7 @@ function createOutputSpool(opts) {
120
120
  // src/_util.ts
121
121
  import * as fsp2 from "node:fs/promises";
122
122
  import * as path2 from "node:path";
123
- import * as Core from "@wrongstack/core";
123
+ import * as Core from "@wrongstack/core/utils";
124
124
  function resolvePath(input, ctx) {
125
125
  return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
126
126
  }
@@ -440,7 +440,7 @@ var CircuitBreaker = class {
440
440
  };
441
441
 
442
442
  // src/_redact-command.ts
443
- import { expectDefined } from "@wrongstack/core";
443
+ import { expectDefined } from "@wrongstack/core/utils";
444
444
  var SENSITIVE_FLAG_PATTERNS = [
445
445
  // --flag=value or --flag "value" (value captured up to next space or comma)
446
446
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
@@ -534,7 +534,12 @@ var ProcessRegistryImpl = class {
534
534
  this.breaker.setEnabled(false);
535
535
  }
536
536
  register(info) {
537
- this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
537
+ this.processes.set(info.pid, {
538
+ ...info,
539
+ killed: false,
540
+ protected: info.protected ?? false,
541
+ background: info.background ?? false
542
+ });
538
543
  }
539
544
  _isSafeSignalPid(pid) {
540
545
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
@@ -587,12 +592,21 @@ var ProcessRegistryImpl = class {
587
592
  }
588
593
  return n;
589
594
  }
595
+ /** Count of active jobs explicitly launched in background mode. */
596
+ get activeBackgroundCount() {
597
+ let n = 0;
598
+ for (const p of this.processes.values()) {
599
+ if (p.background && !p.killed) n++;
600
+ }
601
+ return n;
602
+ }
590
603
  /**
591
604
  * Combined stats for observability — used by /ps and the TUI status bar.
592
605
  */
593
606
  stats() {
594
607
  return {
595
608
  activeCount: this.activeCount,
609
+ backgroundCount: this.activeBackgroundCount,
596
610
  totalCount: this.processes.size,
597
611
  breaker: this.breaker.snapshot()
598
612
  };
@@ -693,7 +707,7 @@ var ProcessRegistryImpl = class {
693
707
  this.autoKillTimer = setTimeout(() => {
694
708
  this.autoKillTimer = null;
695
709
  this.autoKillArmedAt = null;
696
- this.killAll({ force: false });
710
+ this.killAll({ force: false, preserveBackground: true });
697
711
  this.breaker.forceReset();
698
712
  this._emitBreakerCountdown();
699
713
  }, this.autoKillResetMs);
@@ -733,6 +747,7 @@ var ProcessRegistryImpl = class {
733
747
  if (!p) return false;
734
748
  if (p.killed) return true;
735
749
  if (p.protected) return false;
750
+ if (opts.preserveBackground && p.background) return false;
736
751
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
737
752
  const isWin3 = os.platform() === "win32";
738
753
  if (isWin3) {
@@ -1171,8 +1186,9 @@ import * as path5 from "node:path";
1171
1186
  // src/process-registry-persistent.ts
1172
1187
  import * as fs2 from "node:fs/promises";
1173
1188
  import * as os2 from "node:os";
1189
+ import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
1174
1190
  import * as path4 from "node:path";
1175
- var REGISTRY_FILE = ".wrongstack/process-registry.json";
1191
+ var REGISTRY_FILE = "process-registry.json";
1176
1192
  function toErrorMessage(err) {
1177
1193
  return err instanceof Error ? err.message : String(err);
1178
1194
  }
@@ -1190,7 +1206,7 @@ function emitStructuredLog(level, event, message, error) {
1190
1206
  }
1191
1207
  var HEARTBEAT_INTERVAL_MS = 5e3;
1192
1208
  var STALE_THRESHOLD_MS = 3e4;
1193
- var LOCKFILE = ".wrongstack/.process-registry.lock";
1209
+ var LOCKFILE = ".process-registry.lock";
1194
1210
  function generateInstanceId() {
1195
1211
  const hostname2 = os2.hostname();
1196
1212
  const pid = process.pid;
@@ -1279,12 +1295,18 @@ var PersistentProcessRegistry = class {
1279
1295
  lockPath;
1280
1296
  baseRegistry;
1281
1297
  heartbeatInterval = null;
1298
+ cleanupInterval = null;
1299
+ heartbeatRunning = false;
1300
+ cleanupRunning = false;
1282
1301
  isShuttingDown = false;
1302
+ onProcessExit = () => {
1303
+ void this.syncToPersistent();
1304
+ };
1283
1305
  constructor(baseRegistry) {
1284
1306
  this.instanceId = generateInstanceId();
1285
- const homeDir = os2.homedir();
1286
- this.registryPath = path4.join(homeDir, REGISTRY_FILE);
1287
- this.lockPath = path4.join(homeDir, LOCKFILE);
1307
+ const globalRoot = wstackGlobalRoot3();
1308
+ this.registryPath = path4.join(globalRoot, REGISTRY_FILE);
1309
+ this.lockPath = path4.join(globalRoot, LOCKFILE);
1288
1310
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
1289
1311
  this.ensureDirectory().catch((err) => {
1290
1312
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
@@ -1303,16 +1325,18 @@ var PersistentProcessRegistry = class {
1303
1325
  */
1304
1326
  start() {
1305
1327
  if (this.heartbeatInterval) return;
1306
- this.syncToPersistent();
1328
+ this.isShuttingDown = false;
1329
+ this.heartbeat();
1307
1330
  this.heartbeatInterval = setInterval(() => {
1308
1331
  this.heartbeat();
1309
1332
  }, HEARTBEAT_INTERVAL_MS);
1310
1333
  this.heartbeatInterval.unref?.();
1311
- setInterval(() => {
1312
- this.cleanupStaleEntries();
1313
- }, STALE_THRESHOLD_MS).unref?.();
1334
+ this.cleanupInterval = setInterval(() => {
1335
+ this.cleanup();
1336
+ }, STALE_THRESHOLD_MS);
1337
+ this.cleanupInterval.unref?.();
1314
1338
  this.registerMainProcess();
1315
- process.on("exit", () => this.syncToPersistent());
1339
+ process.on("exit", this.onProcessExit);
1316
1340
  }
1317
1341
  /**
1318
1342
  * Stop the heartbeat and clean up.
@@ -1323,7 +1347,12 @@ var PersistentProcessRegistry = class {
1323
1347
  clearInterval(this.heartbeatInterval);
1324
1348
  this.heartbeatInterval = null;
1325
1349
  }
1326
- this.syncToPersistent();
1350
+ if (this.cleanupInterval) {
1351
+ clearInterval(this.cleanupInterval);
1352
+ this.cleanupInterval = null;
1353
+ }
1354
+ process.off("exit", this.onProcessExit);
1355
+ void this.syncToPersistent();
1327
1356
  }
1328
1357
  /**
1329
1358
  * Register the main WrongStack process as protected.
@@ -1407,8 +1436,18 @@ var PersistentProcessRegistry = class {
1407
1436
  * Send heartbeat to mark all this instance's processes as alive.
1408
1437
  */
1409
1438
  heartbeat() {
1410
- if (this.isShuttingDown) return;
1411
- this.syncToPersistent();
1439
+ if (this.isShuttingDown || this.heartbeatRunning) return;
1440
+ this.heartbeatRunning = true;
1441
+ void this.syncToPersistent().finally(() => {
1442
+ this.heartbeatRunning = false;
1443
+ });
1444
+ }
1445
+ cleanup() {
1446
+ if (this.isShuttingDown || this.cleanupRunning) return;
1447
+ this.cleanupRunning = true;
1448
+ void this.cleanupStaleEntries().finally(() => {
1449
+ this.cleanupRunning = false;
1450
+ });
1412
1451
  }
1413
1452
  /**
1414
1453
  * Sync this instance's processes to the persistent registry.
@@ -2607,7 +2646,7 @@ var execTool = {
2607
2646
  command: cmd,
2608
2647
  args: input.args ?? [],
2609
2648
  stdout: "",
2610
- stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
2649
+ stderr: `Command "${cmd}" not in allowlist. Add it to your active profile config (~/.wrongstack/profiles/<name>/config.json) under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
2611
2650
  exitCode: 1,
2612
2651
  truncated: false,
2613
2652
  allowed: false,