plugsuits 1.0.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 (560) hide show
  1. package/cli.js +3 -0
  2. package/dist/agent-reasoning-default.test.d.ts +2 -0
  3. package/dist/agent-reasoning-default.test.d.ts.map +1 -0
  4. package/dist/agent-reasoning-default.test.js +38 -0
  5. package/dist/agent-reasoning-default.test.js.map +1 -0
  6. package/dist/agent.d.ts +61 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +308 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/agent.test.d.ts +2 -0
  11. package/dist/agent.test.d.ts.map +1 -0
  12. package/dist/agent.test.js +38 -0
  13. package/dist/agent.test.js.map +1 -0
  14. package/dist/cli-args.d.ts +15 -0
  15. package/dist/cli-args.d.ts.map +1 -0
  16. package/dist/cli-args.js +63 -0
  17. package/dist/cli-args.js.map +1 -0
  18. package/dist/cli-args.test.d.ts +2 -0
  19. package/dist/cli-args.test.d.ts.map +1 -0
  20. package/dist/cli-args.test.js +105 -0
  21. package/dist/cli-args.test.js.map +1 -0
  22. package/dist/commands/aliases-and-tool-fallback.test.d.ts +2 -0
  23. package/dist/commands/aliases-and-tool-fallback.test.d.ts.map +1 -0
  24. package/dist/commands/aliases-and-tool-fallback.test.js +132 -0
  25. package/dist/commands/aliases-and-tool-fallback.test.js.map +1 -0
  26. package/dist/commands/clear.d.ts +3 -0
  27. package/dist/commands/clear.d.ts.map +1 -0
  28. package/dist/commands/clear.js +12 -0
  29. package/dist/commands/clear.js.map +1 -0
  30. package/dist/commands/factories/create-toggle-command.d.ts +13 -0
  31. package/dist/commands/factories/create-toggle-command.d.ts.map +1 -0
  32. package/dist/commands/factories/create-toggle-command.js +38 -0
  33. package/dist/commands/factories/create-toggle-command.js.map +1 -0
  34. package/dist/commands/help.d.ts +3 -0
  35. package/dist/commands/help.d.ts.map +1 -0
  36. package/dist/commands/help.js +23 -0
  37. package/dist/commands/help.js.map +1 -0
  38. package/dist/commands/index.d.ts +18 -0
  39. package/dist/commands/index.d.ts.map +1 -0
  40. package/dist/commands/index.js +82 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/model.d.ts +15 -0
  43. package/dist/commands/model.d.ts.map +1 -0
  44. package/dist/commands/model.js +100 -0
  45. package/dist/commands/model.js.map +1 -0
  46. package/dist/commands/reasoning-mode.d.ts +3 -0
  47. package/dist/commands/reasoning-mode.d.ts.map +1 -0
  48. package/dist/commands/reasoning-mode.js +47 -0
  49. package/dist/commands/reasoning-mode.js.map +1 -0
  50. package/dist/commands/render.d.ts +19 -0
  51. package/dist/commands/render.d.ts.map +1 -0
  52. package/dist/commands/render.js +140 -0
  53. package/dist/commands/render.js.map +1 -0
  54. package/dist/commands/render.test.d.ts +2 -0
  55. package/dist/commands/render.test.d.ts.map +1 -0
  56. package/dist/commands/render.test.js +36 -0
  57. package/dist/commands/render.test.js.map +1 -0
  58. package/dist/commands/tool-fallback.d.ts +3 -0
  59. package/dist/commands/tool-fallback.d.ts.map +1 -0
  60. package/dist/commands/tool-fallback.js +38 -0
  61. package/dist/commands/tool-fallback.js.map +1 -0
  62. package/dist/commands/translate.d.ts +3 -0
  63. package/dist/commands/translate.d.ts.map +1 -0
  64. package/dist/commands/translate.js +12 -0
  65. package/dist/commands/translate.js.map +1 -0
  66. package/dist/commands/translate.test.d.ts +2 -0
  67. package/dist/commands/translate.test.d.ts.map +1 -0
  68. package/dist/commands/translate.test.js +49 -0
  69. package/dist/commands/translate.test.js.map +1 -0
  70. package/dist/commands/types.d.ts +17 -0
  71. package/dist/commands/types.d.ts.map +1 -0
  72. package/dist/commands/types.js +2 -0
  73. package/dist/commands/types.js.map +1 -0
  74. package/dist/context/environment-context.d.ts +2 -0
  75. package/dist/context/environment-context.d.ts.map +1 -0
  76. package/dist/context/environment-context.js +11 -0
  77. package/dist/context/environment-context.js.map +1 -0
  78. package/dist/context/paths.d.ts +3 -0
  79. package/dist/context/paths.d.ts.map +1 -0
  80. package/dist/context/paths.js +3 -0
  81. package/dist/context/paths.js.map +1 -0
  82. package/dist/context/session.d.ts +4 -0
  83. package/dist/context/session.d.ts.map +1 -0
  84. package/dist/context/session.js +16 -0
  85. package/dist/context/session.js.map +1 -0
  86. package/dist/context/skill-command-prefix.d.ts +4 -0
  87. package/dist/context/skill-command-prefix.d.ts.map +1 -0
  88. package/dist/context/skill-command-prefix.js +15 -0
  89. package/dist/context/skill-command-prefix.js.map +1 -0
  90. package/dist/context/skills-integration.test.d.ts +2 -0
  91. package/dist/context/skills-integration.test.d.ts.map +1 -0
  92. package/dist/context/skills-integration.test.js +87 -0
  93. package/dist/context/skills-integration.test.js.map +1 -0
  94. package/dist/context/skills.d.ts +18 -0
  95. package/dist/context/skills.d.ts.map +1 -0
  96. package/dist/context/skills.js +431 -0
  97. package/dist/context/skills.js.map +1 -0
  98. package/dist/context/skills.test.d.ts +2 -0
  99. package/dist/context/skills.test.d.ts.map +1 -0
  100. package/dist/context/skills.test.js +20 -0
  101. package/dist/context/skills.test.js.map +1 -0
  102. package/dist/context/system-prompt.d.ts +2 -0
  103. package/dist/context/system-prompt.d.ts.map +1 -0
  104. package/dist/context/system-prompt.js +100 -0
  105. package/dist/context/system-prompt.js.map +1 -0
  106. package/dist/context/translation-integration.test.d.ts +2 -0
  107. package/dist/context/translation-integration.test.d.ts.map +1 -0
  108. package/dist/context/translation-integration.test.js +138 -0
  109. package/dist/context/translation-integration.test.js.map +1 -0
  110. package/dist/context/translation.d.ts +21 -0
  111. package/dist/context/translation.d.ts.map +1 -0
  112. package/dist/context/translation.js +82 -0
  113. package/dist/context/translation.js.map +1 -0
  114. package/dist/context/translation.test.d.ts +2 -0
  115. package/dist/context/translation.test.d.ts.map +1 -0
  116. package/dist/context/translation.test.js +129 -0
  117. package/dist/context/translation.test.js.map +1 -0
  118. package/dist/entrypoints/cli-input-rendering.test.d.ts +2 -0
  119. package/dist/entrypoints/cli-input-rendering.test.d.ts.map +1 -0
  120. package/dist/entrypoints/cli-input-rendering.test.js +192 -0
  121. package/dist/entrypoints/cli-input-rendering.test.js.map +1 -0
  122. package/dist/entrypoints/cli.d.ts +3 -0
  123. package/dist/entrypoints/cli.d.ts.map +1 -0
  124. package/dist/entrypoints/cli.js +1268 -0
  125. package/dist/entrypoints/cli.js.map +1 -0
  126. package/dist/entrypoints/headless-agent-config.d.ts +21 -0
  127. package/dist/entrypoints/headless-agent-config.d.ts.map +1 -0
  128. package/dist/entrypoints/headless-agent-config.js +15 -0
  129. package/dist/entrypoints/headless-agent-config.js.map +1 -0
  130. package/dist/entrypoints/headless-agent-config.test.d.ts +2 -0
  131. package/dist/entrypoints/headless-agent-config.test.d.ts.map +1 -0
  132. package/dist/entrypoints/headless-agent-config.test.js +63 -0
  133. package/dist/entrypoints/headless-agent-config.test.js.map +1 -0
  134. package/dist/entrypoints/headless.d.ts +3 -0
  135. package/dist/entrypoints/headless.d.ts.map +1 -0
  136. package/dist/entrypoints/headless.js +396 -0
  137. package/dist/entrypoints/headless.js.map +1 -0
  138. package/dist/env.d.ts +8 -0
  139. package/dist/env.d.ts.map +1 -0
  140. package/dist/env.js +14 -0
  141. package/dist/env.js.map +1 -0
  142. package/dist/friendli-models.d.ts +21 -0
  143. package/dist/friendli-models.d.ts.map +1 -0
  144. package/dist/friendli-models.js +57 -0
  145. package/dist/friendli-models.js.map +1 -0
  146. package/dist/friendli-reasoning.d.ts +10 -0
  147. package/dist/friendli-reasoning.d.ts.map +1 -0
  148. package/dist/friendli-reasoning.js +181 -0
  149. package/dist/friendli-reasoning.js.map +1 -0
  150. package/dist/friendli-reasoning.test.d.ts +2 -0
  151. package/dist/friendli-reasoning.test.d.ts.map +1 -0
  152. package/dist/friendli-reasoning.test.js +77 -0
  153. package/dist/friendli-reasoning.test.js.map +1 -0
  154. package/dist/index.d.ts +3 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +3 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/interaction/colors.d.ts +22 -0
  159. package/dist/interaction/colors.d.ts.map +1 -0
  160. package/dist/interaction/colors.js +24 -0
  161. package/dist/interaction/colors.js.map +1 -0
  162. package/dist/interaction/pi-tui-stream-renderer.d.ts +19 -0
  163. package/dist/interaction/pi-tui-stream-renderer.d.ts.map +1 -0
  164. package/dist/interaction/pi-tui-stream-renderer.js +1509 -0
  165. package/dist/interaction/pi-tui-stream-renderer.js.map +1 -0
  166. package/dist/interaction/pi-tui-stream-renderer.test.d.ts +2 -0
  167. package/dist/interaction/pi-tui-stream-renderer.test.d.ts.map +1 -0
  168. package/dist/interaction/pi-tui-stream-renderer.test.js +1314 -0
  169. package/dist/interaction/pi-tui-stream-renderer.test.js.map +1 -0
  170. package/dist/interaction/spinner.d.ts +13 -0
  171. package/dist/interaction/spinner.d.ts.map +1 -0
  172. package/dist/interaction/spinner.js +51 -0
  173. package/dist/interaction/spinner.js.map +1 -0
  174. package/dist/middleware/index.d.ts +7 -0
  175. package/dist/middleware/index.d.ts.map +1 -0
  176. package/dist/middleware/index.js +15 -0
  177. package/dist/middleware/index.js.map +1 -0
  178. package/dist/middleware/todo-continuation.d.ts +11 -0
  179. package/dist/middleware/todo-continuation.d.ts.map +1 -0
  180. package/dist/middleware/todo-continuation.js +103 -0
  181. package/dist/middleware/todo-continuation.js.map +1 -0
  182. package/dist/middleware/trim-leading-newlines.d.ts +3 -0
  183. package/dist/middleware/trim-leading-newlines.d.ts.map +1 -0
  184. package/dist/middleware/trim-leading-newlines.js +49 -0
  185. package/dist/middleware/trim-leading-newlines.js.map +1 -0
  186. package/dist/reasoning-mode.d.ts +5 -0
  187. package/dist/reasoning-mode.d.ts.map +1 -0
  188. package/dist/reasoning-mode.js +30 -0
  189. package/dist/reasoning-mode.js.map +1 -0
  190. package/dist/reasoning-mode.test.d.ts +2 -0
  191. package/dist/reasoning-mode.test.d.ts.map +1 -0
  192. package/dist/reasoning-mode.test.js +22 -0
  193. package/dist/reasoning-mode.test.js.map +1 -0
  194. package/dist/tool-fallback-mode.d.ts +6 -0
  195. package/dist/tool-fallback-mode.d.ts.map +1 -0
  196. package/dist/tool-fallback-mode.js +25 -0
  197. package/dist/tool-fallback-mode.js.map +1 -0
  198. package/dist/tools/execute/shell-execute.d.ts +17 -0
  199. package/dist/tools/execute/shell-execute.d.ts.map +1 -0
  200. package/dist/tools/execute/shell-execute.js +55 -0
  201. package/dist/tools/execute/shell-execute.js.map +1 -0
  202. package/dist/tools/execute/shell-execute.test.d.ts +2 -0
  203. package/dist/tools/execute/shell-execute.test.d.ts.map +1 -0
  204. package/dist/tools/execute/shell-execute.test.js +86 -0
  205. package/dist/tools/execute/shell-execute.test.js.map +1 -0
  206. package/dist/tools/execute/shell-interact.d.ts +10 -0
  207. package/dist/tools/execute/shell-interact.d.ts.map +1 -0
  208. package/dist/tools/execute/shell-interact.js +122 -0
  209. package/dist/tools/execute/shell-interact.js.map +1 -0
  210. package/dist/tools/execute/shell-interact.test.d.ts +2 -0
  211. package/dist/tools/execute/shell-interact.test.d.ts.map +1 -0
  212. package/dist/tools/execute/shell-interact.test.js +175 -0
  213. package/dist/tools/execute/shell-interact.test.js.map +1 -0
  214. package/dist/tools/explore/glob.d.ts +15 -0
  215. package/dist/tools/explore/glob.d.ts.map +1 -0
  216. package/dist/tools/explore/glob.js +107 -0
  217. package/dist/tools/explore/glob.js.map +1 -0
  218. package/dist/tools/explore/glob.test.d.ts +2 -0
  219. package/dist/tools/explore/glob.test.d.ts.map +1 -0
  220. package/dist/tools/explore/glob.test.js +183 -0
  221. package/dist/tools/explore/glob.test.js.map +1 -0
  222. package/dist/tools/explore/grep.d.ts +27 -0
  223. package/dist/tools/explore/grep.d.ts.map +1 -0
  224. package/dist/tools/explore/grep.js +203 -0
  225. package/dist/tools/explore/grep.js.map +1 -0
  226. package/dist/tools/explore/grep.test.d.ts +2 -0
  227. package/dist/tools/explore/grep.test.d.ts.map +1 -0
  228. package/dist/tools/explore/grep.test.js +132 -0
  229. package/dist/tools/explore/grep.test.js.map +1 -0
  230. package/dist/tools/explore/read-file.d.ts +23 -0
  231. package/dist/tools/explore/read-file.d.ts.map +1 -0
  232. package/dist/tools/explore/read-file.js +84 -0
  233. package/dist/tools/explore/read-file.js.map +1 -0
  234. package/dist/tools/explore/read-file.test.d.ts +2 -0
  235. package/dist/tools/explore/read-file.test.d.ts.map +1 -0
  236. package/dist/tools/explore/read-file.test.js +278 -0
  237. package/dist/tools/explore/read-file.test.js.map +1 -0
  238. package/dist/tools/index.d.ts +71 -0
  239. package/dist/tools/index.d.ts.map +1 -0
  240. package/dist/tools/index.js +26 -0
  241. package/dist/tools/index.js.map +1 -0
  242. package/dist/tools/modify/delete-file.d.ts +19 -0
  243. package/dist/tools/modify/delete-file.d.ts.map +1 -0
  244. package/dist/tools/modify/delete-file.js +71 -0
  245. package/dist/tools/modify/delete-file.js.map +1 -0
  246. package/dist/tools/modify/delete-file.test.d.ts +2 -0
  247. package/dist/tools/modify/delete-file.test.d.ts.map +1 -0
  248. package/dist/tools/modify/delete-file.test.js +136 -0
  249. package/dist/tools/modify/delete-file.test.js.map +1 -0
  250. package/dist/tools/modify/edit-file-diagnostics.d.ts +17 -0
  251. package/dist/tools/modify/edit-file-diagnostics.d.ts.map +1 -0
  252. package/dist/tools/modify/edit-file-diagnostics.js +157 -0
  253. package/dist/tools/modify/edit-file-diagnostics.js.map +1 -0
  254. package/dist/tools/modify/edit-file-repair.d.ts +13 -0
  255. package/dist/tools/modify/edit-file-repair.d.ts.map +1 -0
  256. package/dist/tools/modify/edit-file-repair.js +135 -0
  257. package/dist/tools/modify/edit-file-repair.js.map +1 -0
  258. package/dist/tools/modify/edit-file-stress.test.d.ts +2 -0
  259. package/dist/tools/modify/edit-file-stress.test.d.ts.map +1 -0
  260. package/dist/tools/modify/edit-file-stress.test.js +163 -0
  261. package/dist/tools/modify/edit-file-stress.test.js.map +1 -0
  262. package/dist/tools/modify/edit-file-validation.d.ts +9 -0
  263. package/dist/tools/modify/edit-file-validation.d.ts.map +1 -0
  264. package/dist/tools/modify/edit-file-validation.js +86 -0
  265. package/dist/tools/modify/edit-file-validation.js.map +1 -0
  266. package/dist/tools/modify/edit-file-whitespace.test.d.ts +2 -0
  267. package/dist/tools/modify/edit-file-whitespace.test.d.ts.map +1 -0
  268. package/dist/tools/modify/edit-file-whitespace.test.js +90 -0
  269. package/dist/tools/modify/edit-file-whitespace.test.js.map +1 -0
  270. package/dist/tools/modify/edit-file.d.ts +33 -0
  271. package/dist/tools/modify/edit-file.d.ts.map +1 -0
  272. package/dist/tools/modify/edit-file.js +177 -0
  273. package/dist/tools/modify/edit-file.js.map +1 -0
  274. package/dist/tools/modify/edit-file.test.d.ts +2 -0
  275. package/dist/tools/modify/edit-file.test.d.ts.map +1 -0
  276. package/dist/tools/modify/edit-file.test.js +948 -0
  277. package/dist/tools/modify/edit-file.test.js.map +1 -0
  278. package/dist/tools/modify/write-file.d.ts +17 -0
  279. package/dist/tools/modify/write-file.d.ts.map +1 -0
  280. package/dist/tools/modify/write-file.js +39 -0
  281. package/dist/tools/modify/write-file.js.map +1 -0
  282. package/dist/tools/modify/write-file.test.d.ts +2 -0
  283. package/dist/tools/modify/write-file.test.d.ts.map +1 -0
  284. package/dist/tools/modify/write-file.test.js +168 -0
  285. package/dist/tools/modify/write-file.test.js.map +1 -0
  286. package/dist/tools/planning/load-skill.d.ts +13 -0
  287. package/dist/tools/planning/load-skill.d.ts.map +1 -0
  288. package/dist/tools/planning/load-skill.js +101 -0
  289. package/dist/tools/planning/load-skill.js.map +1 -0
  290. package/dist/tools/planning/load-skill.test.d.ts +2 -0
  291. package/dist/tools/planning/load-skill.test.d.ts.map +1 -0
  292. package/dist/tools/planning/load-skill.test.js +37 -0
  293. package/dist/tools/planning/load-skill.test.js.map +1 -0
  294. package/dist/tools/planning/todo-write.d.ts +49 -0
  295. package/dist/tools/planning/todo-write.d.ts.map +1 -0
  296. package/dist/tools/planning/todo-write.js +118 -0
  297. package/dist/tools/planning/todo-write.js.map +1 -0
  298. package/dist/tools/planning/todo-write.test.d.ts +2 -0
  299. package/dist/tools/planning/todo-write.test.d.ts.map +1 -0
  300. package/dist/tools/planning/todo-write.test.js +82 -0
  301. package/dist/tools/planning/todo-write.test.js.map +1 -0
  302. package/dist/tools/utils/execute/format-utils.d.ts +4 -0
  303. package/dist/tools/utils/execute/format-utils.d.ts.map +1 -0
  304. package/dist/tools/utils/execute/format-utils.js +31 -0
  305. package/dist/tools/utils/execute/format-utils.js.map +1 -0
  306. package/dist/tools/utils/execute/format-utils.test.d.ts +2 -0
  307. package/dist/tools/utils/execute/format-utils.test.d.ts.map +1 -0
  308. package/dist/tools/utils/execute/format-utils.test.js +40 -0
  309. package/dist/tools/utils/execute/format-utils.test.js.map +1 -0
  310. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts +12 -0
  311. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts.map +1 -0
  312. package/dist/tools/utils/execute/noninteractive-wrapper.js +269 -0
  313. package/dist/tools/utils/execute/noninteractive-wrapper.js.map +1 -0
  314. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts +2 -0
  315. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts.map +1 -0
  316. package/dist/tools/utils/execute/noninteractive-wrapper.test.js +233 -0
  317. package/dist/tools/utils/execute/noninteractive-wrapper.test.js.map +1 -0
  318. package/dist/tools/utils/execute/output-handler.d.ts +14 -0
  319. package/dist/tools/utils/execute/output-handler.d.ts.map +1 -0
  320. package/dist/tools/utils/execute/output-handler.js +71 -0
  321. package/dist/tools/utils/execute/output-handler.js.map +1 -0
  322. package/dist/tools/utils/execute/output-handler.test.d.ts +2 -0
  323. package/dist/tools/utils/execute/output-handler.test.d.ts.map +1 -0
  324. package/dist/tools/utils/execute/output-handler.test.js +58 -0
  325. package/dist/tools/utils/execute/output-handler.test.js.map +1 -0
  326. package/dist/tools/utils/execute/process-manager.d.ts +17 -0
  327. package/dist/tools/utils/execute/process-manager.d.ts.map +1 -0
  328. package/dist/tools/utils/execute/process-manager.js +229 -0
  329. package/dist/tools/utils/execute/process-manager.js.map +1 -0
  330. package/dist/tools/utils/execute/process-manager.test.d.ts +2 -0
  331. package/dist/tools/utils/execute/process-manager.test.d.ts.map +1 -0
  332. package/dist/tools/utils/execute/process-manager.test.js +139 -0
  333. package/dist/tools/utils/execute/process-manager.test.js.map +1 -0
  334. package/dist/tools/utils/execute/shell-detection.d.ts +3 -0
  335. package/dist/tools/utils/execute/shell-detection.d.ts.map +1 -0
  336. package/dist/tools/utils/execute/shell-detection.js +56 -0
  337. package/dist/tools/utils/execute/shell-detection.js.map +1 -0
  338. package/dist/tools/utils/execute/shell-detection.test.d.ts +2 -0
  339. package/dist/tools/utils/execute/shell-detection.test.d.ts.map +1 -0
  340. package/dist/tools/utils/execute/shell-detection.test.js +86 -0
  341. package/dist/tools/utils/execute/shell-detection.test.js.map +1 -0
  342. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts +5 -0
  343. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts.map +1 -0
  344. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js +113 -0
  345. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js.map +1 -0
  346. package/dist/tools/utils/hashline/constants.d.ts +5 -0
  347. package/dist/tools/utils/hashline/constants.d.ts.map +1 -0
  348. package/dist/tools/utils/hashline/constants.js +11 -0
  349. package/dist/tools/utils/hashline/constants.js.map +1 -0
  350. package/dist/tools/utils/hashline/diff-utils.d.ts +7 -0
  351. package/dist/tools/utils/hashline/diff-utils.d.ts.map +1 -0
  352. package/dist/tools/utils/hashline/diff-utils.js +50 -0
  353. package/dist/tools/utils/hashline/diff-utils.js.map +1 -0
  354. package/dist/tools/utils/hashline/diff-utils.test.d.ts +2 -0
  355. package/dist/tools/utils/hashline/diff-utils.test.d.ts.map +1 -0
  356. package/dist/tools/utils/hashline/diff-utils.test.js +46 -0
  357. package/dist/tools/utils/hashline/diff-utils.test.js.map +1 -0
  358. package/dist/tools/utils/hashline/edit-deduplication.d.ts +6 -0
  359. package/dist/tools/utils/hashline/edit-deduplication.d.ts.map +1 -0
  360. package/dist/tools/utils/hashline/edit-deduplication.js +32 -0
  361. package/dist/tools/utils/hashline/edit-deduplication.js.map +1 -0
  362. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts +11 -0
  363. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts.map +1 -0
  364. package/dist/tools/utils/hashline/edit-operation-primitives.js +93 -0
  365. package/dist/tools/utils/hashline/edit-operation-primitives.js.map +1 -0
  366. package/dist/tools/utils/hashline/edit-operations.d.ts +9 -0
  367. package/dist/tools/utils/hashline/edit-operations.d.ts.map +1 -0
  368. package/dist/tools/utils/hashline/edit-operations.js +101 -0
  369. package/dist/tools/utils/hashline/edit-operations.js.map +1 -0
  370. package/dist/tools/utils/hashline/edit-operations.test.d.ts +2 -0
  371. package/dist/tools/utils/hashline/edit-operations.test.d.ts.map +1 -0
  372. package/dist/tools/utils/hashline/edit-operations.test.js +135 -0
  373. package/dist/tools/utils/hashline/edit-operations.test.js.map +1 -0
  374. package/dist/tools/utils/hashline/edit-ordering.d.ts +5 -0
  375. package/dist/tools/utils/hashline/edit-ordering.d.ts.map +1 -0
  376. package/dist/tools/utils/hashline/edit-ordering.js +54 -0
  377. package/dist/tools/utils/hashline/edit-ordering.js.map +1 -0
  378. package/dist/tools/utils/hashline/edit-text-normalization.d.ts +9 -0
  379. package/dist/tools/utils/hashline/edit-text-normalization.d.ts.map +1 -0
  380. package/dist/tools/utils/hashline/edit-text-normalization.js +135 -0
  381. package/dist/tools/utils/hashline/edit-text-normalization.js.map +1 -0
  382. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts +8 -0
  383. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts.map +1 -0
  384. package/dist/tools/utils/hashline/file-text-canonicalization.js +42 -0
  385. package/dist/tools/utils/hashline/file-text-canonicalization.js.map +1 -0
  386. package/dist/tools/utils/hashline/hash-computation.d.ts +14 -0
  387. package/dist/tools/utils/hashline/hash-computation.d.ts.map +1 -0
  388. package/dist/tools/utils/hashline/hash-computation.js +158 -0
  389. package/dist/tools/utils/hashline/hash-computation.js.map +1 -0
  390. package/dist/tools/utils/hashline/hash-computation.test.d.ts +2 -0
  391. package/dist/tools/utils/hashline/hash-computation.test.d.ts.map +1 -0
  392. package/dist/tools/utils/hashline/hash-computation.test.js +63 -0
  393. package/dist/tools/utils/hashline/hash-computation.test.js.map +1 -0
  394. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts +11 -0
  395. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts.map +1 -0
  396. package/dist/tools/utils/hashline/hashline-chunk-formatter.js +41 -0
  397. package/dist/tools/utils/hashline/hashline-chunk-formatter.js.map +1 -0
  398. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts +2 -0
  399. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts.map +1 -0
  400. package/dist/tools/utils/hashline/hashline-edit-diff.js +27 -0
  401. package/dist/tools/utils/hashline/hashline-edit-diff.js.map +1 -0
  402. package/dist/tools/utils/hashline/index.d.ts +14 -0
  403. package/dist/tools/utils/hashline/index.d.ts.map +1 -0
  404. package/dist/tools/utils/hashline/index.js +11 -0
  405. package/dist/tools/utils/hashline/index.js.map +1 -0
  406. package/dist/tools/utils/hashline/merge-expansion.d.ts +4 -0
  407. package/dist/tools/utils/hashline/merge-expansion.d.ts.map +1 -0
  408. package/dist/tools/utils/hashline/merge-expansion.js +105 -0
  409. package/dist/tools/utils/hashline/merge-expansion.js.map +1 -0
  410. package/dist/tools/utils/hashline/normalize-edits.d.ts +11 -0
  411. package/dist/tools/utils/hashline/normalize-edits.d.ts.map +1 -0
  412. package/dist/tools/utils/hashline/normalize-edits.js +81 -0
  413. package/dist/tools/utils/hashline/normalize-edits.js.map +1 -0
  414. package/dist/tools/utils/hashline/types.d.ts +18 -0
  415. package/dist/tools/utils/hashline/types.d.ts.map +1 -0
  416. package/dist/tools/utils/hashline/types.js +2 -0
  417. package/dist/tools/utils/hashline/types.js.map +1 -0
  418. package/dist/tools/utils/hashline/validation.d.ts +19 -0
  419. package/dist/tools/utils/hashline/validation.d.ts.map +1 -0
  420. package/dist/tools/utils/hashline/validation.js +161 -0
  421. package/dist/tools/utils/hashline/validation.js.map +1 -0
  422. package/dist/tools/utils/hashline/validation.test.d.ts +2 -0
  423. package/dist/tools/utils/hashline/validation.test.d.ts.map +1 -0
  424. package/dist/tools/utils/hashline/validation.test.js +86 -0
  425. package/dist/tools/utils/hashline/validation.test.js.map +1 -0
  426. package/dist/tools/utils/safety-utils.d.ts +66 -0
  427. package/dist/tools/utils/safety-utils.d.ts.map +1 -0
  428. package/dist/tools/utils/safety-utils.js +681 -0
  429. package/dist/tools/utils/safety-utils.js.map +1 -0
  430. package/dist/utils/tools-manager.d.ts +16 -0
  431. package/dist/utils/tools-manager.d.ts.map +1 -0
  432. package/dist/utils/tools-manager.js +257 -0
  433. package/dist/utils/tools-manager.js.map +1 -0
  434. package/package.json +49 -0
  435. package/src/AGENTS.md +52 -0
  436. package/src/agent-reasoning-default.test.ts +48 -0
  437. package/src/agent.test.ts +49 -0
  438. package/src/agent.ts +455 -0
  439. package/src/cli-args.test.ts +152 -0
  440. package/src/cli-args.ts +90 -0
  441. package/src/commands/aliases-and-tool-fallback.test.ts +172 -0
  442. package/src/commands/clear.ts +14 -0
  443. package/src/commands/factories/create-toggle-command.ts +68 -0
  444. package/src/commands/help.ts +30 -0
  445. package/src/commands/index.ts +125 -0
  446. package/src/commands/model.ts +146 -0
  447. package/src/commands/reasoning-mode.ts +55 -0
  448. package/src/commands/render.test.ts +47 -0
  449. package/src/commands/render.ts +205 -0
  450. package/src/commands/tool-fallback.ts +47 -0
  451. package/src/commands/translate.test.ts +62 -0
  452. package/src/commands/translate.ts +14 -0
  453. package/src/commands/types.ts +18 -0
  454. package/src/context/environment-context.ts +11 -0
  455. package/src/context/paths.ts +2 -0
  456. package/src/context/session.ts +18 -0
  457. package/src/context/skill-command-prefix.ts +18 -0
  458. package/src/context/skills-integration.test.ts +113 -0
  459. package/src/context/skills.test.ts +25 -0
  460. package/src/context/skills.ts +566 -0
  461. package/src/context/system-prompt.ts +100 -0
  462. package/src/context/translation-integration.test.ts +194 -0
  463. package/src/context/translation.test.ts +186 -0
  464. package/src/context/translation.ts +122 -0
  465. package/src/entrypoints/AGENTS.md +33 -0
  466. package/src/entrypoints/cli-input-rendering.test.ts +236 -0
  467. package/src/entrypoints/cli.ts +1845 -0
  468. package/src/entrypoints/headless-agent-config.test.ts +82 -0
  469. package/src/entrypoints/headless-agent-config.ts +42 -0
  470. package/src/entrypoints/headless.ts +622 -0
  471. package/src/env.ts +14 -0
  472. package/src/friendli-models.ts +81 -0
  473. package/src/friendli-reasoning.test.ts +147 -0
  474. package/src/friendli-reasoning.ts +280 -0
  475. package/src/index.ts +3 -0
  476. package/src/interaction/colors.ts +24 -0
  477. package/src/interaction/pi-tui-stream-renderer.test.ts +1471 -0
  478. package/src/interaction/pi-tui-stream-renderer.ts +2150 -0
  479. package/src/interaction/spinner.ts +61 -0
  480. package/src/middleware/index.ts +32 -0
  481. package/src/middleware/todo-continuation.ts +128 -0
  482. package/src/middleware/trim-leading-newlines.ts +66 -0
  483. package/src/reasoning-mode.test.ts +24 -0
  484. package/src/reasoning-mode.ts +40 -0
  485. package/src/skills/example/SKILL.md +44 -0
  486. package/src/skills/example/references/api.md +37 -0
  487. package/src/skills/example/scripts/setup.sh +13 -0
  488. package/src/skills/git-workflow.md +405 -0
  489. package/src/tool-fallback-mode.ts +34 -0
  490. package/src/tools/AGENTS.md +44 -0
  491. package/src/tools/execute/shell-execute.test.ts +114 -0
  492. package/src/tools/execute/shell-execute.ts +74 -0
  493. package/src/tools/execute/shell-execute.txt +27 -0
  494. package/src/tools/execute/shell-interact.test.ts +236 -0
  495. package/src/tools/execute/shell-interact.ts +151 -0
  496. package/src/tools/execute/shell-interact.txt +15 -0
  497. package/src/tools/explore/glob-files.txt +8 -0
  498. package/src/tools/explore/glob.test.ts +217 -0
  499. package/src/tools/explore/glob.ts +137 -0
  500. package/src/tools/explore/grep-files.txt +12 -0
  501. package/src/tools/explore/grep.test.ts +183 -0
  502. package/src/tools/explore/grep.ts +266 -0
  503. package/src/tools/explore/read-file.test.ts +355 -0
  504. package/src/tools/explore/read-file.ts +102 -0
  505. package/src/tools/explore/read-file.txt +24 -0
  506. package/src/tools/index.ts +29 -0
  507. package/src/tools/modify/AGENTS.md +38 -0
  508. package/src/tools/modify/delete-file.test.ts +200 -0
  509. package/src/tools/modify/delete-file.ts +95 -0
  510. package/src/tools/modify/delete-file.txt +9 -0
  511. package/src/tools/modify/edit-file-diagnostics.ts +210 -0
  512. package/src/tools/modify/edit-file-repair.ts +183 -0
  513. package/src/tools/modify/edit-file-stress.test.ts +200 -0
  514. package/src/tools/modify/edit-file-validation.ts +134 -0
  515. package/src/tools/modify/edit-file-whitespace.test.ts +117 -0
  516. package/src/tools/modify/edit-file.test.ts +1231 -0
  517. package/src/tools/modify/edit-file.ts +252 -0
  518. package/src/tools/modify/edit-file.txt +73 -0
  519. package/src/tools/modify/write-file.test.ts +240 -0
  520. package/src/tools/modify/write-file.ts +56 -0
  521. package/src/tools/modify/write-file.txt +9 -0
  522. package/src/tools/planning/load-skill.test.ts +48 -0
  523. package/src/tools/planning/load-skill.ts +136 -0
  524. package/src/tools/planning/load-skill.txt +6 -0
  525. package/src/tools/planning/todo-write.test.ts +91 -0
  526. package/src/tools/planning/todo-write.ts +141 -0
  527. package/src/tools/planning/todo-write.txt +7 -0
  528. package/src/tools/utils/execute/format-utils.test.ts +53 -0
  529. package/src/tools/utils/execute/format-utils.ts +37 -0
  530. package/src/tools/utils/execute/noninteractive-wrapper.test.ts +306 -0
  531. package/src/tools/utils/execute/noninteractive-wrapper.ts +314 -0
  532. package/src/tools/utils/execute/output-handler.test.ts +72 -0
  533. package/src/tools/utils/execute/output-handler.ts +101 -0
  534. package/src/tools/utils/execute/process-manager.test.ts +175 -0
  535. package/src/tools/utils/execute/process-manager.ts +310 -0
  536. package/src/tools/utils/execute/shell-detection.test.ts +112 -0
  537. package/src/tools/utils/execute/shell-detection.ts +72 -0
  538. package/src/tools/utils/hashline/autocorrect-replacement-lines.ts +159 -0
  539. package/src/tools/utils/hashline/constants.ts +13 -0
  540. package/src/tools/utils/hashline/diff-utils.test.ts +61 -0
  541. package/src/tools/utils/hashline/diff-utils.ts +64 -0
  542. package/src/tools/utils/hashline/edit-deduplication.ts +40 -0
  543. package/src/tools/utils/hashline/edit-operation-primitives.ts +149 -0
  544. package/src/tools/utils/hashline/edit-operations.test.ts +154 -0
  545. package/src/tools/utils/hashline/edit-operations.ts +132 -0
  546. package/src/tools/utils/hashline/edit-ordering.ts +60 -0
  547. package/src/tools/utils/hashline/edit-text-normalization.ts +180 -0
  548. package/src/tools/utils/hashline/file-text-canonicalization.ts +58 -0
  549. package/src/tools/utils/hashline/hash-computation.test.ts +82 -0
  550. package/src/tools/utils/hashline/hash-computation.ts +199 -0
  551. package/src/tools/utils/hashline/hashline-chunk-formatter.ts +61 -0
  552. package/src/tools/utils/hashline/hashline-edit-diff.ts +35 -0
  553. package/src/tools/utils/hashline/index.ts +55 -0
  554. package/src/tools/utils/hashline/merge-expansion.ts +120 -0
  555. package/src/tools/utils/hashline/normalize-edits.ts +127 -0
  556. package/src/tools/utils/hashline/types.ts +20 -0
  557. package/src/tools/utils/hashline/validation.test.ts +109 -0
  558. package/src/tools/utils/hashline/validation.ts +212 -0
  559. package/src/tools/utils/safety-utils.ts +938 -0
  560. package/src/utils/tools-manager.ts +353 -0
@@ -0,0 +1,1231 @@
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ } from "bun:test";
9
+ import {
10
+ existsSync,
11
+ mkdtempSync,
12
+ readFileSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { executeGrep } from "../explore/grep";
20
+ import { executeReadFile } from "../explore/read-file";
21
+ import { executeEditFile } from "./edit-file";
22
+ import { resetMissingLinesFailures } from "./edit-file-diagnostics";
23
+ import { repairMalformedEdit } from "./edit-file-repair";
24
+
25
+ const FILE_HASH_REGEX = /^file_hash:\s+([0-9a-f]{8})$/m;
26
+ const LINE_REF_REGEX_TEMPLATE = (lineNumber: number): RegExp =>
27
+ new RegExp(`\\b${lineNumber}#([ZPMQVRWSNKTXJBYH]{2})\\|`);
28
+ const HASHLINE_LINE_PREFIX_REGEX = /^\d+#/;
29
+
30
+ function extractFileHash(readOutput: string): string {
31
+ const matched = readOutput.match(FILE_HASH_REGEX);
32
+ if (!matched?.[1]) {
33
+ throw new Error("Failed to extract file_hash from read output");
34
+ }
35
+ return matched[1];
36
+ }
37
+
38
+ function extractLineRef(readOutput: string, lineNumber: number): string {
39
+ const matched = readOutput.match(LINE_REF_REGEX_TEMPLATE(lineNumber));
40
+ if (!matched?.[1]) {
41
+ throw new Error(`Failed to extract line reference for line ${lineNumber}`);
42
+ }
43
+ return `${lineNumber}#${matched[1]}`;
44
+ }
45
+
46
+ describe("edit_file (hashline-only)", () => {
47
+ let tempDir: string;
48
+
49
+ beforeAll(() => {
50
+ tempDir = mkdtempSync(join(tmpdir(), "edit-file-test-"));
51
+ });
52
+
53
+ afterAll(() => {
54
+ if (existsSync(tempDir)) {
55
+ rmSync(tempDir, { recursive: true });
56
+ }
57
+ });
58
+
59
+ it("replaces target line using {line_number}#{hash_id} anchor", async () => {
60
+ const testFile = join(tempDir, "hashline-replace.txt");
61
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
62
+
63
+ const readOutput = await executeReadFile({ path: testFile });
64
+ const lineRef = extractLineRef(readOutput, 2);
65
+ const fileHash = extractFileHash(readOutput);
66
+
67
+ const result = await executeEditFile({
68
+ path: testFile,
69
+ expected_file_hash: fileHash,
70
+ edits: [
71
+ {
72
+ op: "replace",
73
+ pos: lineRef,
74
+ lines: ["BRAVO"],
75
+ },
76
+ ],
77
+ }, { rootDir: tempDir });
78
+
79
+ expect(result).toContain(`Updated ${testFile}`);
80
+ expect(result).toContain("1 edit(s) applied");
81
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nBRAVO\ncharlie\n");
82
+ });
83
+
84
+ it("accepts grep output line as direct hashline anchor", async () => {
85
+ const testFile = join(tempDir, "hashline-from-grep.txt");
86
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
87
+
88
+ const grepOutput = await executeGrep({
89
+ pattern: "bravo",
90
+ path: tempDir,
91
+ include: "hashline-from-grep.txt",
92
+ });
93
+
94
+ const grepLine = grepOutput
95
+ .split("\n")
96
+ .find(
97
+ (line) =>
98
+ line.includes("hashline-from-grep.txt:2#") && line.includes("|bravo")
99
+ );
100
+
101
+ if (!grepLine) {
102
+ throw new Error("Failed to extract hashline anchor from grep output");
103
+ }
104
+
105
+ await executeEditFile({
106
+ path: testFile,
107
+ edits: [
108
+ {
109
+ op: "replace",
110
+ pos: grepLine,
111
+ lines: ["BRAVO"],
112
+ },
113
+ ],
114
+ }, { rootDir: tempDir });
115
+
116
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nBRAVO\ncharlie\n");
117
+ });
118
+
119
+ it("fails with helpful mismatch when anchor is stale", async () => {
120
+ const testFile = join(tempDir, "hashline-stale.txt");
121
+ writeFileSync(testFile, "one\ntwo\nthree\n");
122
+
123
+ const readOutput = await executeReadFile({ path: testFile });
124
+ const lineRef = extractLineRef(readOutput, 2);
125
+
126
+ writeFileSync(testFile, "one\nTWO-CHANGED\nthree\n");
127
+
128
+ await expect(
129
+ executeEditFile({
130
+ path: testFile,
131
+ edits: [
132
+ {
133
+ op: "replace",
134
+ pos: lineRef,
135
+ lines: ["two-updated"],
136
+ },
137
+ ],
138
+ }, { rootDir: tempDir })
139
+ ).rejects.toThrow("changed since last read");
140
+ });
141
+
142
+ it("rejects stale expected_file_hash", async () => {
143
+ const testFile = join(tempDir, "hashline-filehash.txt");
144
+ writeFileSync(testFile, "x\ny\n");
145
+
146
+ const readOutput = await executeReadFile({ path: testFile });
147
+ const lineRef = extractLineRef(readOutput, 2);
148
+ const fileHash = extractFileHash(readOutput);
149
+
150
+ writeFileSync(testFile, "x\nY-NEW\n");
151
+
152
+ await expect(
153
+ executeEditFile({
154
+ path: testFile,
155
+ expected_file_hash: fileHash,
156
+ edits: [
157
+ {
158
+ op: "replace",
159
+ pos: lineRef,
160
+ lines: ["y-updated"],
161
+ },
162
+ ],
163
+ }, { rootDir: tempDir })
164
+ ).rejects.toThrow("File changed since read_file output");
165
+ });
166
+
167
+ it("supports prepend and append in one call", async () => {
168
+ const testFile = join(tempDir, "hashline-insert.txt");
169
+ writeFileSync(testFile, "a\nb\nc\n");
170
+
171
+ const readOutput = await executeReadFile({ path: testFile });
172
+ const middleRef = extractLineRef(readOutput, 2);
173
+
174
+ await executeEditFile({
175
+ path: testFile,
176
+ edits: [
177
+ {
178
+ op: "prepend",
179
+ pos: middleRef,
180
+ lines: ["before-b"],
181
+ },
182
+ {
183
+ op: "append",
184
+ pos: middleRef,
185
+ lines: ["after-b"],
186
+ },
187
+ ],
188
+ }, { rootDir: tempDir });
189
+
190
+ expect(readFileSync(testFile, "utf-8")).toBe(
191
+ "a\nbefore-b\nb\nafter-b\nc\n"
192
+ );
193
+ });
194
+
195
+ it("creates missing file with anchorless append", async () => {
196
+ const testFile = join(tempDir, "hashline-create.txt");
197
+
198
+ const result = await executeEditFile({
199
+ path: testFile,
200
+ edits: [
201
+ {
202
+ op: "append",
203
+ lines: ["created-via-hashline"],
204
+ },
205
+ ],
206
+ }, { rootDir: tempDir });
207
+
208
+ expect(result).toContain(`Created ${testFile}`);
209
+ expect(result).toContain("1 edit(s) applied");
210
+ expect(readFileSync(testFile, "utf-8")).toBe("created-via-hashline");
211
+ });
212
+
213
+ it("rejects empty edits", async () => {
214
+ const testFile = join(tempDir, "hashline-empty-edits.txt");
215
+ writeFileSync(testFile, "a\nb\n");
216
+
217
+ await expect(
218
+ executeEditFile({
219
+ path: testFile,
220
+ edits: [],
221
+ }, { rootDir: tempDir })
222
+ ).rejects.toThrow();
223
+ });
224
+
225
+ it("rejects no-op edits", async () => {
226
+ const testFile = join(tempDir, "hashline-noop.txt");
227
+ writeFileSync(testFile, "a\nb\n");
228
+
229
+ const readOutput = await executeReadFile({ path: testFile });
230
+ const lineRef = extractLineRef(readOutput, 2);
231
+
232
+ const result = await executeEditFile({
233
+ path: testFile,
234
+ edits: [
235
+ {
236
+ op: "replace",
237
+ pos: lineRef,
238
+ lines: ["b"],
239
+ },
240
+ ],
241
+ }, { rootDir: tempDir });
242
+ expect(result).toContain("No changes made");
243
+ });
244
+
245
+ it("rejects replace when lines are omitted", async () => {
246
+ const testFile = join(tempDir, "hashline-replace-missing-lines.txt");
247
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
248
+
249
+ const readOutput = await executeReadFile({ path: testFile });
250
+ const lineRef = extractLineRef(readOutput, 2);
251
+
252
+ await expect(
253
+ executeEditFile({
254
+ path: testFile,
255
+ edits: [
256
+ {
257
+ op: "replace",
258
+ pos: lineRef,
259
+ },
260
+ ],
261
+ }, { rootDir: tempDir })
262
+ ).rejects.toThrow("explicit 'lines' field");
263
+
264
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nbravo\ncharlie\n");
265
+ });
266
+
267
+ it("silently deletes lines via replace with null", async () => {
268
+ const testFile = join(tempDir, "hashline-deletion.txt");
269
+ writeFileSync(testFile, "heading\nbody\n");
270
+ const readOutput = await executeReadFile({ path: testFile });
271
+ const lineRef = extractLineRef(readOutput, 1);
272
+ const result = await executeEditFile({
273
+ path: testFile,
274
+ edits: [
275
+ {
276
+ op: "replace",
277
+ pos: lineRef,
278
+ lines: null,
279
+ },
280
+ ],
281
+ }, { rootDir: tempDir });
282
+ expect(readFileSync(testFile, "utf-8")).toBe("body\n");
283
+ // No deletion warning — lines: null is intentional deletion
284
+ expect(result).not.toContain("Deleted");
285
+ });
286
+
287
+ it("appends after blank line without warnings", async () => {
288
+ const testFile = join(tempDir, "hashline-blank-anchor.txt");
289
+ writeFileSync(testFile, "hello\n\nworld\n");
290
+ const readOutput = await executeReadFile({ path: testFile });
291
+ const blankLineRef = extractLineRef(readOutput, 2);
292
+ const result = await executeEditFile({
293
+ path: testFile,
294
+ edits: [
295
+ {
296
+ op: "append",
297
+ pos: blankLineRef,
298
+ lines: ["inserted"],
299
+ },
300
+ ],
301
+ }, { rootDir: tempDir });
302
+ expect(readFileSync(testFile, "utf-8")).toBe("hello\n\ninserted\nworld\n");
303
+ expect(result).not.toContain("Warnings:");
304
+ });
305
+
306
+ it("falls back to end anchor when replace pos is invalid", async () => {
307
+ const testFile = join(tempDir, "hashline-replace-end-fallback.txt");
308
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
309
+
310
+ const readOutput = await executeReadFile({ path: testFile });
311
+ const lineRef = extractLineRef(readOutput, 2);
312
+
313
+ await executeEditFile({
314
+ path: testFile,
315
+ edits: [
316
+ {
317
+ op: "replace",
318
+ pos: "invalid-anchor",
319
+ end: lineRef,
320
+ lines: ["BRAVO"],
321
+ },
322
+ ],
323
+ }, { rootDir: tempDir });
324
+
325
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nBRAVO\ncharlie\n");
326
+ });
327
+
328
+ it("falls back to end anchor when append pos is invalid", async () => {
329
+ const testFile = join(tempDir, "hashline-append-end-fallback.txt");
330
+ writeFileSync(testFile, "alpha\nbravo\n");
331
+
332
+ const readOutput = await executeReadFile({ path: testFile });
333
+ const lineRef = extractLineRef(readOutput, 2);
334
+
335
+ await executeEditFile({
336
+ path: testFile,
337
+ edits: [
338
+ {
339
+ op: "append",
340
+ pos: "invalid-anchor",
341
+ end: lineRef,
342
+ lines: ["after-bravo"],
343
+ },
344
+ ],
345
+ }, { rootDir: tempDir });
346
+
347
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nbravo\nafter-bravo\n");
348
+ });
349
+
350
+ it("falls back to pos anchor when prepend end is invalid", async () => {
351
+ const testFile = join(tempDir, "hashline-prepend-pos-fallback.txt");
352
+ writeFileSync(testFile, "alpha\nbravo\n");
353
+
354
+ const readOutput = await executeReadFile({ path: testFile });
355
+ const lineRef = extractLineRef(readOutput, 2);
356
+
357
+ await executeEditFile({
358
+ path: testFile,
359
+ edits: [
360
+ {
361
+ op: "prepend",
362
+ pos: lineRef,
363
+ end: "invalid-anchor",
364
+ lines: ["before-bravo"],
365
+ },
366
+ ],
367
+ }, { rootDir: tempDir });
368
+
369
+ expect(readFileSync(testFile, "utf-8")).toBe(
370
+ "alpha\nbefore-bravo\nbravo\n"
371
+ );
372
+ });
373
+
374
+ it("accepts long hash anchors by truncating to 2 chars", async () => {
375
+ const testFile = join(tempDir, "hashline-long-hash-anchor.txt");
376
+ writeFileSync(testFile, "alpha\nbravo\n");
377
+
378
+ const readOutput = await executeReadFile({ path: testFile });
379
+ const lineRef = extractLineRef(readOutput, 2);
380
+ const longLineRef = `${lineRef}ZZ`;
381
+
382
+ await executeEditFile({
383
+ path: testFile,
384
+ edits: [
385
+ {
386
+ op: "replace",
387
+ pos: longLineRef,
388
+ lines: ["BRAVO"],
389
+ },
390
+ ],
391
+ }, { rootDir: tempDir });
392
+
393
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nBRAVO\n");
394
+ });
395
+
396
+ it("rejects multiline pos payload copied from hashline output", async () => {
397
+ const testFile = join(tempDir, "hashline-multiline-pos.txt");
398
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
399
+
400
+ const readOutput = await executeReadFile({ path: testFile });
401
+ const line1 = extractLineRef(readOutput, 1);
402
+ const line2 = extractLineRef(readOutput, 2);
403
+ const line3 = extractLineRef(readOutput, 3);
404
+ const multilinePos = [
405
+ `${line1}|alpha`,
406
+ `${line2}|bravo`,
407
+ `${line3}|charlie`,
408
+ ].join("\n");
409
+
410
+ await expect(
411
+ executeEditFile({
412
+ path: testFile,
413
+ edits: [
414
+ {
415
+ op: "replace",
416
+ pos: multilinePos,
417
+ },
418
+ ],
419
+ }, { rootDir: tempDir })
420
+ ).rejects.toThrow("single-line");
421
+
422
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nbravo\ncharlie\n");
423
+ });
424
+
425
+ it("preserves CRLF endings after edit", async () => {
426
+ const testFile = join(tempDir, "hashline-crlf.txt");
427
+ writeFileSync(testFile, "foo\r\nbar\r\n");
428
+
429
+ const readOutput = await executeReadFile({ path: testFile });
430
+ const lineRef = extractLineRef(readOutput, 1);
431
+
432
+ await executeEditFile({
433
+ path: testFile,
434
+ edits: [
435
+ {
436
+ op: "replace",
437
+ pos: lineRef,
438
+ lines: ["FOO"],
439
+ },
440
+ ],
441
+ }, { rootDir: tempDir });
442
+
443
+ expect(readFileSync(testFile, "utf-8")).toBe("FOO\r\nbar\r\n");
444
+ });
445
+
446
+ it("preserves UTF-8 BOM with CRLF endings", async () => {
447
+ const testFile = join(tempDir, "hashline-bom-crlf.txt");
448
+ writeFileSync(testFile, "\uFEFFfoo\r\nbar\r\n");
449
+
450
+ const readOutput = await executeReadFile({ path: testFile });
451
+ const lineRef = extractLineRef(readOutput, 1);
452
+
453
+ await executeEditFile({
454
+ path: testFile,
455
+ edits: [
456
+ {
457
+ op: "replace",
458
+ pos: lineRef,
459
+ lines: ["FOO"],
460
+ },
461
+ ],
462
+ }, { rootDir: tempDir });
463
+
464
+ expect(readFileSync(testFile, "utf-8")).toBe("\uFEFFFOO\r\nbar\r\n");
465
+ });
466
+
467
+ it("rejects non-numeric prefix in pos with diagnostic error", async () => {
468
+ const testFile = join(tempDir, "hashline-line-prefix.txt");
469
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
470
+ const readOutput = await executeReadFile({ path: testFile });
471
+ const lineRef = extractLineRef(readOutput, 1);
472
+ const hash = lineRef.split("#")[1];
473
+ await expect(
474
+ executeEditFile({
475
+ path: testFile,
476
+ edits: [{ op: "replace", pos: `LINE#${hash}`, lines: ["replaced"] }],
477
+ }, { rootDir: tempDir })
478
+ ).rejects.toThrow("not a line number");
479
+ });
480
+
481
+ it("suggests correct line number when hash matches a file line", async () => {
482
+ const testFile = join(tempDir, "hashline-suggest.txt");
483
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
484
+ const readOutput = await executeReadFile({ path: testFile });
485
+ const lineRef = extractLineRef(readOutput, 1);
486
+ const hash = lineRef.split("#")[1];
487
+ await expect(
488
+ executeEditFile({
489
+ path: testFile,
490
+ edits: [{ op: "replace", pos: `LINE#${hash}`, lines: ["replaced"] }],
491
+ }, { rootDir: tempDir })
492
+ ).rejects.toThrow("Did you mean");
493
+ });
494
+
495
+ it("includes line count in multiline pos rejection", async () => {
496
+ const testFile = join(tempDir, "hashline-multiline-count.txt");
497
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
498
+ const readOutput = await executeReadFile({ path: testFile });
499
+ const lines = readOutput
500
+ .split("\n")
501
+ .filter((l: string) => HASHLINE_LINE_PREFIX_REGEX.test(l));
502
+ const multilinePos = lines.join("\n");
503
+ await expect(
504
+ executeEditFile({
505
+ path: testFile,
506
+ edits: [{ op: "replace", pos: multilinePos, lines: ["x"] }],
507
+ }, { rootDir: tempDir })
508
+ ).rejects.toThrow("lines");
509
+ });
510
+
511
+ it("accepts anchor with diff marker prefix via normalization", async () => {
512
+ const testFile = join(tempDir, "hashline-normalize-prefix.txt");
513
+ writeFileSync(testFile, "alpha\nbravo\n");
514
+ const readOutput = await executeReadFile({ path: testFile });
515
+ const lineRef = extractLineRef(readOutput, 1);
516
+ const result = await executeEditFile({
517
+ path: testFile,
518
+ edits: [{ op: "replace", pos: `>>> ${lineRef}`, lines: ["ALPHA"] }],
519
+ }, { rootDir: tempDir });
520
+ expect(result).toContain("1 edit(s) applied");
521
+ expect(readFileSync(testFile, "utf-8")).toBe("ALPHA\nbravo\n");
522
+ });
523
+
524
+ it("rejects append with invalid-only anchor with diagnostic", async () => {
525
+ const testFile = join(tempDir, "hashline-append-invalid.txt");
526
+ writeFileSync(testFile, "alpha\nbravo\n");
527
+ await expect(
528
+ executeEditFile({
529
+ path: testFile,
530
+ edits: [{ op: "append", pos: "LINE#XX", lines: ["inserted"] }],
531
+ }, { rootDir: tempDir })
532
+ ).rejects.toThrow("not a line number");
533
+ });
534
+
535
+ // ── diagnoseMissingLines pattern-specific errors ──────────────
536
+
537
+ it("Pattern A: pos with |content suffix and no lines → anchor mismatch after content extraction", async () => {
538
+ const testFile = join(tempDir, "diag-pattern-a.txt");
539
+ writeFileSync(testFile, "alpha\nbravo\n");
540
+ await expect(
541
+ executeEditFile({
542
+ path: testFile,
543
+ edits: [{ op: "replace", pos: "1#ZZ|alpha" }],
544
+ }, { rootDir: tempDir })
545
+ ).rejects.toThrow("changed since last read");
546
+ });
547
+
548
+ it("Pattern B: pos with Python dict syntax auto-repairs and succeeds", async () => {
549
+ const testFile = join(tempDir, "diag-pattern-b.txt");
550
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
551
+ const readOutput = await executeReadFile({ path: testFile });
552
+ const lineRef = extractLineRef(readOutput, 2);
553
+ const malformedPos = `${lineRef}', 'lines': ['replaced text']}`;
554
+
555
+ const result = await executeEditFile({
556
+ path: testFile,
557
+ edits: [{ op: "replace", pos: malformedPos }],
558
+ }, { rootDir: tempDir });
559
+
560
+ expect(
561
+ result.includes("Warnings:") || result.includes("Auto-repaired")
562
+ ).toBe(true);
563
+ expect(readFileSync(testFile, "utf-8")).toBe(
564
+ "alpha\nreplaced text\ncharlie\n"
565
+ );
566
+ });
567
+
568
+ it("Pattern C: pos with =separator content and no lines → anchor mismatch after content extraction", async () => {
569
+ const testFile = join(tempDir, "diag-pattern-c.txt");
570
+ writeFileSync(testFile, "alpha\nbravo\n");
571
+ await expect(
572
+ executeEditFile({
573
+ path: testFile,
574
+ edits: [{ op: "replace", pos: "1#ZZ=some content here" }],
575
+ }, { rootDir: tempDir })
576
+ ).rejects.toThrow("changed since last read");
577
+ });
578
+
579
+ it("Pattern D: pos with XML markup and no lines → explicit lines diagnostic", async () => {
580
+ const testFile = join(tempDir, "diag-pattern-d.txt");
581
+ writeFileSync(testFile, "alpha\nbravo\n");
582
+ await expect(
583
+ executeEditFile({
584
+ path: testFile,
585
+ edits: [
586
+ {
587
+ op: "replace",
588
+ pos: "1#ZZ']}</parameter><parameter>",
589
+ },
590
+ ],
591
+ }, { rootDir: tempDir })
592
+ ).rejects.toThrow("explicit 'lines'");
593
+ });
594
+
595
+ it("Pattern B auto-repair: replace with Python dict pos succeeds", async () => {
596
+ const testFile = join(tempDir, "diag-pattern-b-auto-repair-success.txt");
597
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
598
+ const readOutput = await executeReadFile({ path: testFile });
599
+ const line2Anchor = extractLineRef(readOutput, 2);
600
+
601
+ const result = await executeEditFile({
602
+ path: testFile,
603
+ edits: [
604
+ {
605
+ op: "replace",
606
+ pos: `${line2Anchor}', 'lines': ['REPLACED']}`,
607
+ },
608
+ ],
609
+ }, { rootDir: tempDir });
610
+
611
+ expect(
612
+ result.includes("Warnings:") || result.includes("Auto-repaired")
613
+ ).toBe(true);
614
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nREPLACED\ncharlie\n");
615
+ });
616
+
617
+ it("Pattern A: repaired anchor with |content suffix → content extracted as lines and applied", async () => {
618
+ const testFile = join(tempDir, "diag-pattern-a-repaired-content.txt");
619
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
620
+ const readOutput = await executeReadFile({ path: testFile });
621
+ const line2Anchor = extractLineRef(readOutput, 2);
622
+
623
+ const result = await executeEditFile({
624
+ path: testFile,
625
+ edits: [{ op: "replace", pos: `${line2Anchor}|old bravo content` }],
626
+ }, { rootDir: tempDir });
627
+
628
+ // Content 'old bravo content' extracted from pos as lines replacement
629
+ expect(result).toContain("Warnings:");
630
+ expect(readFileSync(testFile, "utf-8")).toBe(
631
+ "alpha\nold bravo content\ncharlie\n"
632
+ );
633
+ });
634
+
635
+ it("Clean anchor with no lines → mentions the anchor in diagnostic", async () => {
636
+ const testFile = join(tempDir, "diag-clean-anchor.txt");
637
+ writeFileSync(testFile, "alpha\nbravo\n");
638
+ await expect(
639
+ executeEditFile({
640
+ path: testFile,
641
+ edits: [{ op: "replace", pos: "1#ZZ" }],
642
+ }, { rootDir: tempDir })
643
+ ).rejects.toThrow("'lines'");
644
+ });
645
+
646
+ it("No pos with no lines → generic missing lines error", async () => {
647
+ const testFile = join(tempDir, "diag-no-pos.txt");
648
+ writeFileSync(testFile, "alpha\nbravo\n");
649
+ await expect(
650
+ executeEditFile({
651
+ path: testFile,
652
+ edits: [{ op: "replace" }],
653
+ }, { rootDir: tempDir })
654
+ ).rejects.toThrow("explicit 'lines'");
655
+ });
656
+
657
+ it("Long pos content is truncated in error message", async () => {
658
+ const testFile = join(tempDir, "diag-long-pos.txt");
659
+ writeFileSync(testFile, "alpha\nbravo\n");
660
+ const longPos = `not-an-anchor-${"x".repeat(200)}`;
661
+ await expect(
662
+ executeEditFile({
663
+ path: testFile,
664
+ edits: [{ op: "replace", pos: longPos }],
665
+ }, { rootDir: tempDir })
666
+ ).rejects.toThrow("...");
667
+ });
668
+ });
669
+
670
+ describe("repairMalformedEdit", () => {
671
+ it("A: extracts clean anchor and content as lines from pos with |content suffix", () => {
672
+ const result = repairMalformedEdit({
673
+ op: "replace",
674
+ pos: "2#HR|old content",
675
+ });
676
+
677
+ expect(result.edit.pos).toBe("2#HR");
678
+ expect(result.edit.lines).toEqual(["old content"]);
679
+ expect(result.warnings.length).toBeGreaterThan(0);
680
+ });
681
+
682
+ it("A: preserves existing lines when pos has |content", () => {
683
+ const result = repairMalformedEdit({
684
+ op: "replace",
685
+ pos: "2#HR|old",
686
+ lines: ["new"],
687
+ });
688
+
689
+ expect(result.edit.pos).toBe("2#HR");
690
+ expect(result.edit.lines).toEqual(["new"]);
691
+ });
692
+
693
+ it("C: extracts anchor and content from pos with =separator", () => {
694
+ const result = repairMalformedEdit({
695
+ op: "replace",
696
+ pos: "2#HR=some content",
697
+ });
698
+
699
+ expect(result.edit.pos).toBe("2#HR");
700
+ expect(result.edit.lines).toEqual(["some content"]);
701
+ });
702
+
703
+ it("D: extracts anchor from pos with XML markup", () => {
704
+ const result = repairMalformedEdit({
705
+ op: "replace",
706
+ pos: "3#YH']}</parameter><parameter>",
707
+ });
708
+
709
+ expect(result.edit.pos).toBe("3#YH");
710
+ expect(result.edit.lines).toBeUndefined();
711
+ });
712
+
713
+ it("B: extracts anchor + single-element lines from Python dict", () => {
714
+ const result = repairMalformedEdit({
715
+ op: "replace",
716
+ pos: "3#YH', 'lines': ['new text']}",
717
+ });
718
+
719
+ expect(result.edit.pos).toBe("3#YH");
720
+ expect(result.edit.lines).toEqual(["new text"]);
721
+ });
722
+
723
+ it("B: extracts multi-element array from embedded lines", () => {
724
+ const result = repairMalformedEdit({
725
+ op: "replace",
726
+ pos: "3#YH', 'lines': ['line1', 'line2']}",
727
+ });
728
+
729
+ expect(result.edit.lines).toEqual(["line1", "line2"]);
730
+ });
731
+
732
+ it("B: extracts null from embedded lines (deletion)", () => {
733
+ const result = repairMalformedEdit({
734
+ op: "replace",
735
+ pos: "3#YH', 'lines': null}",
736
+ });
737
+
738
+ expect(result.edit.lines).toBeNull();
739
+ });
740
+
741
+ it("F: extracts anchor and lines from query-string", () => {
742
+ const result = repairMalformedEdit({
743
+ op: "replace",
744
+ pos: "2#SR&lines: ['new']",
745
+ });
746
+
747
+ expect(result.edit.pos).toBe("2#SR");
748
+ expect(result.edit.lines).toEqual(["new"]);
749
+ });
750
+
751
+ it("E: returns unchanged when pos is clean and lines undefined", () => {
752
+ const edit = { op: "replace" as const, pos: "2#KB" };
753
+ const result = repairMalformedEdit(edit);
754
+
755
+ expect(result.edit).toEqual(edit);
756
+ expect(result.warnings.length).toBe(0);
757
+ });
758
+
759
+ it("no-op when pos is undefined", () => {
760
+ const edit = { op: "replace" as const };
761
+ const result = repairMalformedEdit(edit);
762
+
763
+ expect(result.edit).toEqual(edit);
764
+ expect(result.warnings.length).toBe(0);
765
+ });
766
+
767
+ it("no-op when pos has no recognizable anchor", () => {
768
+ const edit = { op: "replace" as const, pos: "garbage text" };
769
+ const result = repairMalformedEdit(edit);
770
+
771
+ expect(result.edit).toEqual(edit);
772
+ expect(result.warnings.length).toBe(0);
773
+ });
774
+
775
+ it("skips multiline pos", () => {
776
+ const edit = {
777
+ op: "replace" as const,
778
+ pos: "1#HR|alpha\n2#KB|bravo",
779
+ };
780
+ const result = repairMalformedEdit(edit);
781
+
782
+ expect(result.edit).toEqual(edit);
783
+ expect(result.warnings.length).toBe(0);
784
+ });
785
+
786
+ it("extracts clean anchor from end with |content suffix", () => {
787
+ const result = repairMalformedEdit({
788
+ op: "replace",
789
+ pos: "1#HR",
790
+ end: "3#YH|charlie",
791
+ lines: ["x"],
792
+ });
793
+
794
+ expect(result.edit.end).toBe("3#YH");
795
+ });
796
+
797
+ it("does NOT extract lines from end field", () => {
798
+ const result = repairMalformedEdit({
799
+ op: "replace",
800
+ pos: "1#HR",
801
+ end: "3#YH', 'lines': ['x']}",
802
+ lines: ["y"],
803
+ });
804
+
805
+ expect(result.edit.end).toBe("3#YH");
806
+ expect(result.edit.lines).toEqual(["y"]);
807
+ });
808
+
809
+ it("works with append op", () => {
810
+ const result = repairMalformedEdit({ op: "append", pos: "2#HR|content" });
811
+
812
+ expect(result.edit.pos).toBe("2#HR");
813
+ });
814
+
815
+ it("works with prepend op", () => {
816
+ const result = repairMalformedEdit({ op: "prepend", pos: "2#HR=content" });
817
+
818
+ expect(result.edit.pos).toBe("2#HR");
819
+ });
820
+
821
+ it("A: skips multiline pos with content (too risky to repair)", () => {
822
+ const edit = {
823
+ op: "replace" as const,
824
+ pos: "2#HR|line one\nline two",
825
+ };
826
+ const result = repairMalformedEdit(edit);
827
+
828
+ expect(result.edit).toEqual(edit);
829
+ expect(result.warnings.length).toBe(0);
830
+ });
831
+
832
+ it("A: does NOT extract content from pos with XML-like garbage", () => {
833
+ const result = repairMalformedEdit({
834
+ op: "replace",
835
+ pos: "3#YH|</parameter>",
836
+ });
837
+
838
+ expect(result.edit.pos).toBe("3#YH");
839
+ expect(result.edit.lines).toBeUndefined();
840
+ });
841
+
842
+ it("A: does NOT extract content from pos with JSON closing brackets", () => {
843
+ const result = repairMalformedEdit({
844
+ op: "replace",
845
+ pos: "3#YH|'}]}",
846
+ });
847
+
848
+ expect(result.edit.pos).toBe("3#YH");
849
+ expect(result.edit.lines).toBeUndefined();
850
+ });
851
+
852
+ it("A: extracts real code content like comments", () => {
853
+ const result = repairMalformedEdit({
854
+ op: "replace",
855
+ pos: "1#KM|// Greeting function",
856
+ });
857
+
858
+ expect(result.edit.pos).toBe("1#KM");
859
+ expect(result.edit.lines).toEqual(["// Greeting function"]);
860
+ });
861
+
862
+ it("A: extracts code content with = separator", () => {
863
+ const result = repairMalformedEdit({
864
+ op: "replace",
865
+ pos: "5#XY=const x = 42;",
866
+ });
867
+
868
+ expect(result.edit.pos).toBe("5#XY");
869
+ expect(result.edit.lines).toEqual(["const x = 42;"]);
870
+ });
871
+ });
872
+
873
+ describe("repeated failure escalation", () => {
874
+ let tempDir: string;
875
+
876
+ beforeAll(() => {
877
+ tempDir = mkdtempSync(join(tmpdir(), "edit-file-escalation-test-"));
878
+ });
879
+
880
+ afterAll(() => {
881
+ if (existsSync(tempDir)) {
882
+ rmSync(tempDir, { recursive: true });
883
+ }
884
+ });
885
+
886
+ beforeEach(() => {
887
+ resetMissingLinesFailures();
888
+ });
889
+
890
+ async function captureMissingLinesError(
891
+ testFile: string,
892
+ anchor: string
893
+ ): Promise<Error> {
894
+ try {
895
+ await executeEditFile({
896
+ path: testFile,
897
+ edits: [{ op: "replace", pos: anchor }],
898
+ }, { rootDir: tempDir });
899
+ throw new Error("Expected executeEditFile to reject");
900
+ } catch (error) {
901
+ if (error instanceof Error) {
902
+ return error;
903
+ }
904
+ throw error;
905
+ }
906
+ }
907
+
908
+ it("first missing-lines failure returns standard diagnostic", async () => {
909
+ const testFile = join(tempDir, "escalation-first-failure.txt");
910
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
911
+
912
+ const readOutput = await executeReadFile({ path: testFile });
913
+ const line2Anchor = extractLineRef(readOutput, 2);
914
+ const error = await captureMissingLinesError(testFile, line2Anchor);
915
+
916
+ expect(error.message).toContain("explicit 'lines'");
917
+ expect(error.message).not.toContain("contains");
918
+ });
919
+
920
+ it("third identical failure escalates with line content", async () => {
921
+ const testFile = join(tempDir, "escalation-third-failure.txt");
922
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
923
+
924
+ const readOutput = await executeReadFile({ path: testFile });
925
+ const line2Anchor = extractLineRef(readOutput, 2);
926
+
927
+ await captureMissingLinesError(testFile, line2Anchor);
928
+ await captureMissingLinesError(testFile, line2Anchor);
929
+ const thirdError = await captureMissingLinesError(testFile, line2Anchor);
930
+
931
+ expect(thirdError.message).toContain("explicit 'lines'");
932
+ expect(thirdError.message).toContain("contains 'bravo'");
933
+ });
934
+
935
+ it("different anchor resets to standard diagnostic", async () => {
936
+ const testFile = join(tempDir, "escalation-different-anchor.txt");
937
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
938
+
939
+ const readOutput = await executeReadFile({ path: testFile });
940
+ const line1Anchor = extractLineRef(readOutput, 1);
941
+ const line2Anchor = extractLineRef(readOutput, 2);
942
+
943
+ await captureMissingLinesError(testFile, line2Anchor);
944
+ await captureMissingLinesError(testFile, line2Anchor);
945
+ await captureMissingLinesError(testFile, line2Anchor);
946
+ const error = await captureMissingLinesError(testFile, line1Anchor);
947
+
948
+ expect(error.message).toContain("explicit 'lines'");
949
+ expect(error.message).not.toContain("contains");
950
+ });
951
+ });
952
+
953
+ describe("repairMalformedEdit — end extraction from pos", () => {
954
+ it("B: extracts end anchor from embedded key-value in pos", () => {
955
+ const result = repairMalformedEdit({
956
+ op: "replace",
957
+ pos: "3#QH', 'end': '4#KR', 'lines': ['RESOLVED']",
958
+ });
959
+
960
+ expect(result.edit.pos).toBe("3#QH");
961
+ expect(result.edit.end).toBe("4#KR");
962
+ expect(result.edit.lines).toEqual(["RESOLVED"]);
963
+ expect(result.warnings.length).toBeGreaterThan(0);
964
+ });
965
+
966
+ it("B: extracts end from embedded content, preserves existing end", () => {
967
+ const result = repairMalformedEdit({
968
+ op: "replace",
969
+ pos: "3#QH', 'end': '4#KR'",
970
+ end: "5#AB",
971
+ lines: ["x"],
972
+ });
973
+
974
+ // Existing end should NOT be overridden
975
+ expect(result.edit.pos).toBe("3#QH");
976
+ expect(result.edit.end).toBe("5#AB");
977
+ });
978
+
979
+ it("B: extracts end but no lines when lines already provided", () => {
980
+ const result = repairMalformedEdit({
981
+ op: "replace",
982
+ pos: "3#QH', 'end': '4#KR', 'lines': ['embedded']",
983
+ lines: ["explicit"],
984
+ });
985
+
986
+ expect(result.edit.pos).toBe("3#QH");
987
+ expect(result.edit.end).toBe("4#KR");
988
+ expect(result.edit.lines).toEqual(["explicit"]);
989
+ });
990
+ });
991
+
992
+ describe("soft-reject after repeated failures", () => {
993
+ let tempDir: string;
994
+
995
+ beforeAll(() => {
996
+ tempDir = mkdtempSync(join(tmpdir(), "edit-file-soft-reject-test-"));
997
+ });
998
+
999
+ afterAll(() => {
1000
+ if (existsSync(tempDir)) {
1001
+ rmSync(tempDir, { recursive: true });
1002
+ }
1003
+ });
1004
+
1005
+ beforeEach(() => {
1006
+ resetMissingLinesFailures();
1007
+ });
1008
+
1009
+ it("returns soft-reject string after 6+ identical missing-lines failures", async () => {
1010
+ const testFile = join(tempDir, "soft-reject.txt");
1011
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\n");
1012
+
1013
+ const readOutput = await executeReadFile({ path: testFile });
1014
+ const line2Anchor = extractLineRef(readOutput, 2);
1015
+
1016
+ // First 5 failures should throw errors
1017
+ for (let i = 0; i < 5; i++) {
1018
+ await expect(
1019
+ executeEditFile({
1020
+ path: testFile,
1021
+ edits: [{ op: "replace", pos: line2Anchor }],
1022
+ }, { rootDir: tempDir })
1023
+ ).rejects.toThrow("explicit 'lines'");
1024
+ }
1025
+
1026
+ // 6th+ failure should return a string (soft-reject) instead of throwing
1027
+ const result = await executeEditFile({
1028
+ path: testFile,
1029
+ edits: [{ op: "replace", pos: line2Anchor }],
1030
+ }, { rootDir: tempDir });
1031
+
1032
+ expect(typeof result).toBe("string");
1033
+ expect(result).toContain("NOT APPLIED");
1034
+ expect(result).toContain(line2Anchor);
1035
+ expect(result).toContain("bravo");
1036
+
1037
+ // File should NOT have been changed
1038
+ expect(readFileSync(testFile, "utf-8")).toBe("alpha\nbravo\ncharlie\n");
1039
+ });
1040
+
1041
+ it("soft-reject suggests write_file as alternative", async () => {
1042
+ const testFile = join(tempDir, "soft-reject-alt.txt");
1043
+ writeFileSync(testFile, "line1\nline2\n");
1044
+
1045
+ const readOutput = await executeReadFile({ path: testFile });
1046
+ const line1Anchor = extractLineRef(readOutput, 1);
1047
+
1048
+ // Trigger 6 failures
1049
+ for (let i = 0; i < 6; i++) {
1050
+ try {
1051
+ await executeEditFile({
1052
+ path: testFile,
1053
+ edits: [{ op: "replace", pos: line1Anchor }],
1054
+ }, { rootDir: tempDir });
1055
+ } catch {
1056
+ // expected for first 5
1057
+ }
1058
+ }
1059
+
1060
+ const result = await executeEditFile({
1061
+ path: testFile,
1062
+ edits: [{ op: "replace", pos: line1Anchor }],
1063
+ }, { rootDir: tempDir });
1064
+
1065
+ expect(result).toContain("write_file");
1066
+ });
1067
+
1068
+ it("triggers soft-reject via file-level threshold when alternating anchors", async () => {
1069
+ const testFile = join(tempDir, "soft-reject-file-bail.txt");
1070
+ writeFileSync(testFile, "alpha\nbravo\ncharlie\ndelta\n");
1071
+
1072
+ const readOutput = await executeReadFile({ path: testFile });
1073
+ const line1Anchor = extractLineRef(readOutput, 1);
1074
+ const line2Anchor = extractLineRef(readOutput, 2);
1075
+ const line3Anchor = extractLineRef(readOutput, 3);
1076
+
1077
+ // Alternate between anchors so no single anchor hits ESCALATION_BAIL_THRESHOLD (6)
1078
+ // but total file failures exceed FILE_BAIL_THRESHOLD (10)
1079
+ const anchors = [line1Anchor, line2Anchor, line3Anchor];
1080
+ for (let i = 0; i < 9; i++) {
1081
+ try {
1082
+ await executeEditFile({
1083
+ path: testFile,
1084
+ edits: [{ op: "replace", pos: anchors[i % anchors.length] }],
1085
+ }, { rootDir: tempDir });
1086
+ } catch {
1087
+ // expected errors
1088
+ }
1089
+ }
1090
+
1091
+ // 10th file-level failure should trigger soft-reject
1092
+ const result = await executeEditFile({
1093
+ path: testFile,
1094
+ edits: [{ op: "replace", pos: anchors[0] }],
1095
+ }, { rootDir: tempDir });
1096
+
1097
+ expect(typeof result).toBe("string");
1098
+ expect(result).toContain("NOT APPLIED");
1099
+ expect(result).toContain("write_file");
1100
+
1101
+ // File should NOT have been changed
1102
+ expect(readFileSync(testFile, "utf-8")).toBe(
1103
+ "alpha\nbravo\ncharlie\ndelta\n"
1104
+ );
1105
+ });
1106
+ });
1107
+
1108
+ describe("edit_file safety (C-1, C-2, H-1)", () => {
1109
+ let tempDir: string;
1110
+
1111
+ beforeAll(() => {
1112
+ tempDir = mkdtempSync(join(tmpdir(), "edit-file-safety-test-"));
1113
+ });
1114
+
1115
+ afterAll(() => {
1116
+ if (existsSync(tempDir)) {
1117
+ rmSync(tempDir, { recursive: true });
1118
+ }
1119
+ });
1120
+
1121
+ it("C-1: blocks path traversal via .. segments", async () => {
1122
+ const traversalPath = join(tempDir, "..", "..", "etc", "passwd");
1123
+ await expect(
1124
+ executeEditFile(
1125
+ {
1126
+ path: traversalPath,
1127
+ edits: [{ op: "append", lines: ["malicious"] }],
1128
+ },
1129
+ { rootDir: tempDir }
1130
+ )
1131
+ ).rejects.toThrow(/[Pp]ath traversal blocked/);
1132
+ });
1133
+
1134
+ it("C-1: blocks absolute paths outside project root", async () => {
1135
+ await expect(
1136
+ executeEditFile(
1137
+ {
1138
+ path: "/tmp/outside-project-edit.txt",
1139
+ edits: [{ op: "append", lines: ["bad"] }],
1140
+ },
1141
+ { rootDir: tempDir }
1142
+ )
1143
+ ).rejects.toThrow(/[Pp]ath traversal blocked|outside/);
1144
+ });
1145
+
1146
+ it("C-2: blocks edits through symlinks", async () => {
1147
+ const realFile = join(tempDir, "real-edit-target.txt");
1148
+ writeFileSync(realFile, "original\n");
1149
+ const symlinkPath = join(tempDir, "symlink-to-edit.txt");
1150
+ symlinkSync(realFile, symlinkPath);
1151
+
1152
+ await expect(
1153
+ executeEditFile(
1154
+ {
1155
+ path: symlinkPath,
1156
+ edits: [{ op: "append", lines: ["through symlink"] }],
1157
+ },
1158
+ { rootDir: tempDir }
1159
+ )
1160
+ ).rejects.toThrow(/symlink/i);
1161
+
1162
+ // Original file should be unchanged
1163
+ expect(readFileSync(realFile, "utf-8")).toBe("original\n");
1164
+ });
1165
+
1166
+ it("C-2: blocks edits through symlinks pointing outside root", async () => {
1167
+ const outsideDir = mkdtempSync(join(tmpdir(), "outside-edit-root-"));
1168
+ const outsideFile = join(outsideDir, "secret.txt");
1169
+ writeFileSync(outsideFile, "secret data\n");
1170
+ const symlinkPath = join(tempDir, "escape-edit-link.txt");
1171
+ symlinkSync(outsideFile, symlinkPath);
1172
+
1173
+ try {
1174
+ await expect(
1175
+ executeEditFile(
1176
+ {
1177
+ path: symlinkPath,
1178
+ edits: [{ op: "append", lines: ["overwrite"] }],
1179
+ },
1180
+ { rootDir: tempDir }
1181
+ )
1182
+ ).rejects.toThrow(/symlink/i);
1183
+ expect(readFileSync(outsideFile, "utf-8")).toBe("secret data\n");
1184
+ } finally {
1185
+ rmSync(outsideDir, { recursive: true });
1186
+ }
1187
+ });
1188
+
1189
+ it("H-1: edit uses atomic write (no temp file residue)", async () => {
1190
+ const testFile = join(tempDir, "atomic-edit-test.txt");
1191
+ writeFileSync(testFile, "alpha\nbravo\n");
1192
+
1193
+ const readOutput = await executeReadFile({ path: testFile });
1194
+ const lineRef = extractLineRef(readOutput, 1);
1195
+
1196
+ await executeEditFile(
1197
+ {
1198
+ path: testFile,
1199
+ edits: [{ op: "replace", pos: lineRef, lines: ["ALPHA"] }],
1200
+ },
1201
+ { rootDir: tempDir }
1202
+ );
1203
+
1204
+ expect(readFileSync(testFile, "utf-8")).toBe("ALPHA\nbravo\n");
1205
+
1206
+ // Check no .tmp- files remain
1207
+ const { readdirSync } = require("node:fs");
1208
+ const files: string[] = readdirSync(tempDir);
1209
+ const tmpFiles = files.filter((f: string) => f.includes(".tmp-"));
1210
+ expect(tmpFiles.length).toBe(0);
1211
+ });
1212
+
1213
+ it("allows edits within project root (normal operation)", async () => {
1214
+ const safeFile = join(tempDir, "safe-edit.txt");
1215
+ writeFileSync(safeFile, "original content\n");
1216
+
1217
+ const readOutput = await executeReadFile({ path: safeFile });
1218
+ const lineRef = extractLineRef(readOutput, 1);
1219
+
1220
+ const result = await executeEditFile(
1221
+ {
1222
+ path: safeFile,
1223
+ edits: [{ op: "replace", pos: lineRef, lines: ["updated content"] }],
1224
+ },
1225
+ { rootDir: tempDir }
1226
+ );
1227
+
1228
+ expect(result).toContain("Updated");
1229
+ expect(readFileSync(safeFile, "utf-8")).toBe("updated content\n");
1230
+ });
1231
+ });