@wrongstack/tools 0.284.0 → 0.285.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 (311) hide show
  1. package/dist/_concurrency.d.ts +16 -0
  2. package/dist/_concurrency.d.ts.map +1 -0
  3. package/dist/_danger-detect.d.ts +54 -0
  4. package/dist/_danger-detect.d.ts.map +1 -0
  5. package/dist/_edit-match.d.ts +86 -0
  6. package/dist/_edit-match.d.ts.map +1 -0
  7. package/dist/_env.d.ts +6 -0
  8. package/dist/_env.d.ts.map +1 -0
  9. package/dist/_fetch-guard.d.ts +31 -0
  10. package/dist/_fetch-guard.d.ts.map +1 -0
  11. package/dist/_output-spool.d.ts +38 -0
  12. package/dist/_output-spool.d.ts.map +1 -0
  13. package/dist/_redact-command.d.ts +6 -0
  14. package/dist/_redact-command.d.ts.map +1 -0
  15. package/dist/_regex.d.ts +39 -0
  16. package/dist/_regex.d.ts.map +1 -0
  17. package/dist/_session-shell.d.ts +73 -0
  18. package/dist/_session-shell.d.ts.map +1 -0
  19. package/dist/_shell-pick.d.ts +158 -0
  20. package/dist/_shell-pick.d.ts.map +1 -0
  21. package/dist/_spawn-stream.d.ts +32 -0
  22. package/dist/_spawn-stream.d.ts.map +1 -0
  23. package/dist/_syntax-check.d.ts +16 -0
  24. package/dist/_syntax-check.d.ts.map +1 -0
  25. package/dist/_util.d.ts +68 -0
  26. package/dist/_util.d.ts.map +1 -0
  27. package/dist/_win32-resolve.d.ts +42 -0
  28. package/dist/_win32-resolve.d.ts.map +1 -0
  29. package/dist/audit.d.ts +4 -5
  30. package/dist/audit.d.ts.map +1 -0
  31. package/dist/audit.js +2743 -28
  32. package/dist/audit.js.map +7 -1
  33. package/dist/bash-kill-guard.d.ts +62 -0
  34. package/dist/bash-kill-guard.d.ts.map +1 -0
  35. package/dist/bash.d.ts +4 -5
  36. package/dist/bash.d.ts.map +1 -0
  37. package/dist/bash.js +59 -30
  38. package/dist/bash.js.map +7 -1
  39. package/dist/batch-tool-use.d.ts +4 -5
  40. package/dist/batch-tool-use.d.ts.map +1 -0
  41. package/dist/batch-tool-use.js +3 -3
  42. package/dist/batch-tool-use.js.map +7 -1
  43. package/dist/builtin.d.ts +7 -9
  44. package/dist/builtin.d.ts.map +1 -0
  45. package/dist/builtin.js +5282 -1019
  46. package/dist/builtin.js.map +7 -1
  47. package/dist/circuit-breaker.d.ts +4 -5
  48. package/dist/circuit-breaker.d.ts.map +1 -0
  49. package/dist/circuit-breaker.js +3 -3
  50. package/dist/circuit-breaker.js.map +7 -1
  51. package/dist/codebase-index/background-indexer.d.ts +116 -0
  52. package/dist/codebase-index/background-indexer.d.ts.map +1 -0
  53. package/dist/codebase-index/bm25.d.ts +40 -0
  54. package/dist/codebase-index/bm25.d.ts.map +1 -0
  55. package/dist/codebase-index/circuit-breaker.d.ts +94 -0
  56. package/dist/codebase-index/circuit-breaker.d.ts.map +1 -0
  57. package/dist/codebase-index/codebase-index-tool.d.ts +17 -0
  58. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -0
  59. package/dist/codebase-index/codebase-search-tool.d.ts +43 -0
  60. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -0
  61. package/dist/codebase-index/codebase-stats-tool.d.ts +23 -0
  62. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -0
  63. package/dist/codebase-index/gitignore.d.ts +22 -0
  64. package/dist/codebase-index/gitignore.d.ts.map +1 -0
  65. package/dist/codebase-index/go-parser.d.ts +16 -0
  66. package/dist/codebase-index/go-parser.d.ts.map +1 -0
  67. package/dist/codebase-index/index-service.d.ts +25 -0
  68. package/dist/codebase-index/index-service.d.ts.map +1 -0
  69. package/dist/codebase-index/index.d.ts +23 -271
  70. package/dist/codebase-index/index.d.ts.map +1 -0
  71. package/dist/codebase-index/index.js +136 -68
  72. package/dist/codebase-index/index.js.map +7 -1
  73. package/dist/codebase-index/indexer.d.ts +28 -0
  74. package/dist/codebase-index/indexer.d.ts.map +1 -0
  75. package/dist/codebase-index/json-parser.d.ts +8 -0
  76. package/dist/codebase-index/json-parser.d.ts.map +1 -0
  77. package/dist/codebase-index/lsp-kind.d.ts +53 -0
  78. package/dist/codebase-index/lsp-kind.d.ts.map +1 -0
  79. package/dist/codebase-index/py-parser.d.ts +16 -0
  80. package/dist/codebase-index/py-parser.d.ts.map +1 -0
  81. package/dist/codebase-index/refs-extractor.d.ts +25 -0
  82. package/dist/codebase-index/refs-extractor.d.ts.map +1 -0
  83. package/dist/codebase-index/rs-parser.d.ts +8 -0
  84. package/dist/codebase-index/rs-parser.d.ts.map +1 -0
  85. package/dist/codebase-index/schema.d.ts +82 -0
  86. package/dist/codebase-index/schema.d.ts.map +1 -0
  87. package/dist/codebase-index/ts-parser.d.ts +30 -0
  88. package/dist/codebase-index/ts-parser.d.ts.map +1 -0
  89. package/dist/codebase-index/worker-protocol.d.ts +77 -0
  90. package/dist/codebase-index/worker-protocol.d.ts.map +1 -0
  91. package/dist/codebase-index/worker.d.ts +16 -2
  92. package/dist/codebase-index/worker.d.ts.map +1 -0
  93. package/dist/codebase-index/worker.js +103 -65
  94. package/dist/codebase-index/worker.js.map +7 -1
  95. package/dist/codebase-index/writer.d.ts +189 -0
  96. package/dist/codebase-index/writer.d.ts.map +1 -0
  97. package/dist/codebase-index/yaml-parser.d.ts +8 -0
  98. package/dist/codebase-index/yaml-parser.d.ts.map +1 -0
  99. package/dist/design.d.ts +38 -0
  100. package/dist/design.d.ts.map +1 -0
  101. package/dist/design.js +270 -0
  102. package/dist/design.js.map +7 -0
  103. package/dist/diff.d.ts +4 -5
  104. package/dist/diff.d.ts.map +1 -0
  105. package/dist/diff.js +14 -12
  106. package/dist/diff.js.map +7 -1
  107. package/dist/document.d.ts +4 -5
  108. package/dist/document.d.ts.map +1 -0
  109. package/dist/document.js +8 -7
  110. package/dist/document.js.map +7 -1
  111. package/dist/edit.d.ts +5 -36
  112. package/dist/edit.d.ts.map +1 -0
  113. package/dist/edit.js +28 -16
  114. package/dist/edit.js.map +7 -1
  115. package/dist/exec.d.ts +67 -2
  116. package/dist/exec.d.ts.map +1 -0
  117. package/dist/exec.js +50 -25
  118. package/dist/exec.js.map +7 -1
  119. package/dist/fetch.d.ts +4 -32
  120. package/dist/fetch.d.ts.map +1 -0
  121. package/dist/fetch.js +89 -77
  122. package/dist/fetch.js.map +7 -1
  123. package/dist/format.d.ts +4 -5
  124. package/dist/format.d.ts.map +1 -0
  125. package/dist/format.js +2415 -28
  126. package/dist/format.js.map +7 -1
  127. package/dist/git.d.ts +4 -5
  128. package/dist/git.d.ts.map +1 -0
  129. package/dist/git.js +10 -9
  130. package/dist/git.js.map +7 -1
  131. package/dist/glob.d.ts +4 -5
  132. package/dist/glob.d.ts.map +1 -0
  133. package/dist/glob.js +15 -12
  134. package/dist/glob.js.map +7 -1
  135. package/dist/grep.d.ts +4 -5
  136. package/dist/grep.d.ts.map +1 -0
  137. package/dist/grep.js +13 -10
  138. package/dist/grep.js.map +7 -1
  139. package/dist/index.d.ts +19 -724
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +6088 -1690
  142. package/dist/index.js.map +7 -1
  143. package/dist/install.d.ts +4 -5
  144. package/dist/install.d.ts.map +1 -0
  145. package/dist/install.js +2680 -31
  146. package/dist/install.js.map +7 -1
  147. package/dist/json.d.ts +4 -5
  148. package/dist/json.d.ts.map +1 -0
  149. package/dist/json.js +13 -11
  150. package/dist/json.js.map +7 -1
  151. package/dist/kanban.d.ts +107 -0
  152. package/dist/kanban.d.ts.map +1 -0
  153. package/dist/kanban.js +845 -0
  154. package/dist/kanban.js.map +7 -0
  155. package/dist/languages/detect.d.ts +3 -0
  156. package/dist/languages/detect.d.ts.map +1 -0
  157. package/dist/languages/diagnostics.d.ts +15 -0
  158. package/dist/languages/diagnostics.d.ts.map +1 -0
  159. package/dist/languages/execute-tool.d.ts +34 -0
  160. package/dist/languages/execute-tool.d.ts.map +1 -0
  161. package/dist/languages/execute.d.ts +18 -0
  162. package/dist/languages/execute.d.ts.map +1 -0
  163. package/dist/languages/index.d.ts +12 -0
  164. package/dist/languages/index.d.ts.map +1 -0
  165. package/dist/languages/index.js +4457 -0
  166. package/dist/languages/index.js.map +7 -0
  167. package/dist/languages/legacy-bridge.d.ts +29 -0
  168. package/dist/languages/legacy-bridge.d.ts.map +1 -0
  169. package/dist/languages/package-tool.d.ts +29 -0
  170. package/dist/languages/package-tool.d.ts.map +1 -0
  171. package/dist/languages/plan.d.ts +4 -0
  172. package/dist/languages/plan.d.ts.map +1 -0
  173. package/dist/languages/profile-helpers.d.ts +14 -0
  174. package/dist/languages/profile-helpers.d.ts.map +1 -0
  175. package/dist/languages/profiles/additional.d.ts +3 -0
  176. package/dist/languages/profiles/additional.d.ts.map +1 -0
  177. package/dist/languages/profiles/primary.d.ts +3 -0
  178. package/dist/languages/profiles/primary.d.ts.map +1 -0
  179. package/dist/languages/registry.d.ts +11 -0
  180. package/dist/languages/registry.d.ts.map +1 -0
  181. package/dist/languages/tool.d.ts +32 -0
  182. package/dist/languages/tool.d.ts.map +1 -0
  183. package/dist/languages/types.d.ts +207 -0
  184. package/dist/languages/types.d.ts.map +1 -0
  185. package/dist/lint.d.ts +4 -5
  186. package/dist/lint.d.ts.map +1 -0
  187. package/dist/lint.js +2415 -27
  188. package/dist/lint.js.map +7 -1
  189. package/dist/logs.d.ts +4 -5
  190. package/dist/logs.d.ts.map +1 -0
  191. package/dist/logs.js +16 -10
  192. package/dist/logs.js.map +7 -1
  193. package/dist/memory.d.ts +7 -8
  194. package/dist/memory.d.ts.map +1 -0
  195. package/dist/memory.js +7 -5
  196. package/dist/memory.js.map +7 -1
  197. package/dist/mode.d.ts +4 -5
  198. package/dist/mode.d.ts.map +1 -0
  199. package/dist/mode.js +3 -3
  200. package/dist/mode.js.map +7 -1
  201. package/dist/next-steps.d.ts +5 -6
  202. package/dist/next-steps.d.ts.map +1 -0
  203. package/dist/next-steps.js +5 -4
  204. package/dist/next-steps.js.map +7 -1
  205. package/dist/outdated.d.ts +4 -5
  206. package/dist/outdated.d.ts.map +1 -0
  207. package/dist/outdated.js +4777 -68
  208. package/dist/outdated.js.map +7 -1
  209. package/dist/pack.d.ts +3 -6
  210. package/dist/pack.d.ts.map +1 -0
  211. package/dist/pack.js +5275 -1019
  212. package/dist/pack.js.map +7 -1
  213. package/dist/patch.d.ts +4 -5
  214. package/dist/patch.d.ts.map +1 -0
  215. package/dist/patch.js +18 -16
  216. package/dist/patch.js.map +7 -1
  217. package/dist/plan.d.ts +55 -0
  218. package/dist/plan.d.ts.map +1 -0
  219. package/dist/plan.js +280 -0
  220. package/dist/plan.js.map +7 -0
  221. package/dist/process-guardian.d.ts +100 -0
  222. package/dist/process-guardian.d.ts.map +1 -0
  223. package/dist/process-registry-persistent.d.ts +121 -0
  224. package/dist/process-registry-persistent.d.ts.map +1 -0
  225. package/dist/process-registry.d.ts +13 -17
  226. package/dist/process-registry.d.ts.map +1 -0
  227. package/dist/process-registry.js +15 -9
  228. package/dist/process-registry.js.map +7 -1
  229. package/dist/ps-slash.d.ts +106 -0
  230. package/dist/ps-slash.d.ts.map +1 -0
  231. package/dist/ps-slash.js +1291 -0
  232. package/dist/ps-slash.js.map +7 -0
  233. package/dist/read.d.ts +4 -5
  234. package/dist/read.d.ts.map +1 -0
  235. package/dist/read.js +13 -11
  236. package/dist/read.js.map +7 -1
  237. package/dist/replace.d.ts +4 -5
  238. package/dist/replace.d.ts.map +1 -0
  239. package/dist/replace.js +26 -13
  240. package/dist/replace.js.map +7 -1
  241. package/dist/scaffold.d.ts +4 -5
  242. package/dist/scaffold.d.ts.map +1 -0
  243. package/dist/scaffold.js +16 -14
  244. package/dist/scaffold.js.map +7 -1
  245. package/dist/search.d.ts +5 -6
  246. package/dist/search.d.ts.map +1 -0
  247. package/dist/search.js +24 -25
  248. package/dist/search.js.map +7 -1
  249. package/dist/set-working-dir.d.ts +18 -0
  250. package/dist/set-working-dir.d.ts.map +1 -0
  251. package/dist/shim/node-sqlite.d.ts +3 -0
  252. package/dist/shim/node-sqlite.d.ts.map +1 -0
  253. package/dist/skill.d.ts +45 -0
  254. package/dist/skill.d.ts.map +1 -0
  255. package/dist/skill.js +175 -0
  256. package/dist/skill.js.map +7 -0
  257. package/dist/spawn-background.d.ts +80 -0
  258. package/dist/spawn-background.d.ts.map +1 -0
  259. package/dist/task.d.ts +35 -0
  260. package/dist/task.d.ts.map +1 -0
  261. package/dist/task.js +380 -0
  262. package/dist/task.js.map +7 -0
  263. package/dist/test.d.ts +4 -5
  264. package/dist/test.d.ts.map +1 -0
  265. package/dist/test.js +2419 -27
  266. package/dist/test.js.map +7 -1
  267. package/dist/todo.d.ts +4 -5
  268. package/dist/todo.d.ts.map +1 -0
  269. package/dist/todo.js +5 -5
  270. package/dist/todo.js.map +7 -1
  271. package/dist/tool-diff.d.ts +16 -17
  272. package/dist/tool-diff.d.ts.map +1 -0
  273. package/dist/tool-diff.js +11 -3
  274. package/dist/tool-diff.js.map +7 -1
  275. package/dist/tool-help.d.ts +4 -5
  276. package/dist/tool-help.d.ts.map +1 -0
  277. package/dist/tool-help.js +3 -3
  278. package/dist/tool-help.js.map +7 -1
  279. package/dist/tool-icon-map.d.ts +45 -0
  280. package/dist/tool-icon-map.d.ts.map +1 -0
  281. package/dist/tool-icons.d.ts +5 -6
  282. package/dist/tool-icons.d.ts.map +1 -0
  283. package/dist/tool-icons.js +16 -4
  284. package/dist/tool-icons.js.map +7 -1
  285. package/dist/tool-search.d.ts +4 -5
  286. package/dist/tool-search.d.ts.map +1 -0
  287. package/dist/tool-search.js +3 -3
  288. package/dist/tool-search.js.map +7 -1
  289. package/dist/tool-summary.d.ts +4 -5
  290. package/dist/tool-summary.d.ts.map +1 -0
  291. package/dist/tool-summary.js +5 -3
  292. package/dist/tool-summary.js.map +7 -1
  293. package/dist/tool-use.d.ts +4 -5
  294. package/dist/tool-use.d.ts.map +1 -0
  295. package/dist/tool-use.js +3 -3
  296. package/dist/tool-use.js.map +7 -1
  297. package/dist/tree.d.ts +4 -5
  298. package/dist/tree.d.ts.map +1 -0
  299. package/dist/tree.js +11 -9
  300. package/dist/tree.js.map +7 -1
  301. package/dist/typecheck.d.ts +4 -5
  302. package/dist/typecheck.d.ts.map +1 -0
  303. package/dist/typecheck.js +2416 -28
  304. package/dist/typecheck.js.map +7 -1
  305. package/dist/write.d.ts +4 -5
  306. package/dist/write.d.ts.map +1 -0
  307. package/dist/write.js +21 -16
  308. package/dist/write.js.map +7 -1
  309. package/package.json +34 -6
  310. package/dist/background-indexer-BeDBxfSh.d.ts +0 -371
  311. package/dist/exec-2OKoT6tk.d.ts +0 -121
package/dist/patch.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Tool } from '@wrongstack/core';
2
-
1
+ import type { Tool } from '@wrongstack/core';
3
2
  interface PatchInput {
4
3
  patch: string;
5
4
  directory?: string | undefined;
@@ -13,6 +12,6 @@ interface PatchOutput {
13
12
  dry_run: boolean;
14
13
  message: string;
15
14
  }
16
- declare const patchTool: Tool<PatchInput, PatchOutput>;
17
-
18
- export { patchTool };
15
+ export declare const patchTool: Tool<PatchInput, PatchOutput>;
16
+ export {};
17
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAgInD,CAAC"}
package/dist/patch.js CHANGED
@@ -1,12 +1,14 @@
1
- import { spawn } from 'node:child_process';
2
- import * as fs from 'node:fs/promises';
3
- import * as os from 'node:os';
4
- import * as path from 'node:path';
5
- import * as Core from '@wrongstack/core';
6
- import { buildChildEnv } from '@wrongstack/core';
7
- import { createHash } from 'node:crypto';
8
-
9
1
  // src/patch.ts
2
+ import { spawn } from "node:child_process";
3
+ import * as fs from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path2 from "node:path";
6
+ import { buildChildEnv } from "@wrongstack/core";
7
+
8
+ // src/_util.ts
9
+ import { createHash } from "node:crypto";
10
+ import * as path from "node:path";
11
+ import * as Core from "@wrongstack/core";
10
12
  function sha256hex(content) {
11
13
  return createHash("sha256").update(content, "utf8").digest("hex");
12
14
  }
@@ -67,9 +69,9 @@ var patchTool = {
67
69
  for (const t of targets) {
68
70
  const stripped = stripPathComponents(t, strip);
69
71
  if (!stripped) continue;
70
- const candidate = path.resolve(dir, stripped);
71
- const rel = path.relative(ctx.projectRoot, candidate);
72
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
72
+ const candidate = path2.resolve(dir, stripped);
73
+ const rel = path2.relative(ctx.projectRoot, candidate);
74
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
73
75
  return {
74
76
  applied: 0,
75
77
  rejected: 1,
@@ -86,11 +88,11 @@ var patchTool = {
86
88
  beforeContents.set(target, await readTextForTracking(target));
87
89
  }
88
90
  }
89
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
91
+ const tmpDir = await fs.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
90
92
  try {
91
93
  await fs.chmod(tmpDir, 448).catch(() => {
92
94
  });
93
- const patchFile = path.join(tmpDir, "in.diff");
95
+ const patchFile = path2.join(tmpDir, "in.diff");
94
96
  await fs.writeFile(patchFile, input.patch, { mode: 384 });
95
97
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
96
98
  const result = await runPatch(args, dir, opts.signal);
@@ -185,7 +187,7 @@ function extractPatchedFiles(output) {
185
187
  }
186
188
  return files;
187
189
  }
188
-
189
- export { patchTool };
190
+ export {
191
+ patchTool
192
+ };
190
193
  //# sourceMappingURL=patch.js.map
191
- //# sourceMappingURL=patch.js.map
package/dist/patch.js.map CHANGED
@@ -1 +1,7 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/patch.ts"],"names":["path2","stat","resolve"],"mappings":"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","file":"patch.js","sourcesContent":["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` — 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 → yarn → 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→out-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 — macOS `/var`→`/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…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\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// ─── Command-output normalization (token-saving) ────────────────────────────\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) — 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 — 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]!, `… ⟨repeated ${run}×⟩`);\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 — 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…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → 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 → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve, sha256hex } from './_util.js';\n\ninterface PatchInput {\n patch: string;\n directory?: string | undefined;\n strip?: number | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface PatchOutput {\n applied: number;\n rejected: number;\n files: string[];\n dry_run: boolean;\n message: string;\n}\n\nexport const patchTool: Tool<PatchInput, PatchOutput> = {\n name: 'patch',\n category: 'Filesystem',\n description:\n 'Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.',\n usageHint:\n 'Best used when you already have a diff (from generation, external source, or previous step).\\n' +\n '- Use `dry_run: true` to see what would happen without modifying files.\\n' +\n '- On failure it creates .rej and .orig files for manual review.\\n' +\n 'Often cleaner than many small `edit` operations for larger changes.',\n selection: {\n doNotUseWhen: 'you do not already have a unified diff or only need one precise replacement.',\n useInstead: ['edit'],\n },\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n directory: { type: 'string', description: 'Root directory for patch (default: cwd)' },\n strip: { type: 'integer', description: 'Strip leading path components (default: 1)' },\n dry_run: { type: 'boolean', description: 'Preview without applying' },\n },\n required: ['patch'],\n },\n async execute(input, ctx, opts) {\n if (!input?.patch) throw new Error('patch: patch content is required');\n\n const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;\n // strip=0 lets a diff address absolute paths like /etc/passwd and\n // escape the project root entirely. Force >= 1.\n const strip = Math.max(1, input.strip ?? 1);\n const dryRun = input.dry_run ?? false;\n\n // Pre-flight: scan diff target paths and reject any that resolve outside\n // the project root. This catches `../../../etc/passwd`-style escapes\n // before we hand the diff to GNU patch.\n const targets = extractDiffTargets(input.patch);\n const resolvedTargets: string[] = [];\n for (const t of targets) {\n const stripped = stripPathComponents(t, strip);\n if (!stripped) continue;\n const candidate = path.resolve(dir, stripped);\n const rel = path.relative(ctx.projectRoot, candidate);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch refused: target \"${t}\" resolves outside project root`,\n };\n }\n resolvedTargets.push(candidate);\n }\n\n // Snapshot target contents before applying so the change can be recorded\n // for session rewind and stale-read tracking (same bookkeeping as `edit`).\n const beforeContents = new Map<string, string | null>();\n if (!dryRun) {\n for (const target of resolvedTargets) {\n beforeContents.set(target, await readTextForTracking(target));\n }\n }\n\n // Write the diff into a private 0700 temp directory rather than into\n // the user-controlled `dir` with a predictable timestamp name. Avoids\n // symlink-bait races on shared work trees.\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), '.wstack_patch_'));\n try {\n await fs.chmod(tmpDir, 0o700).catch(() => {\n /* best-effort on Windows */\n });\n const patchFile = path.join(tmpDir, 'in.diff');\n await fs.writeFile(patchFile, input.patch, { mode: 0o600 });\n\n const args = [`-p${strip}`, '--merge', ...(dryRun ? ['--dry-run'] : []), '-i', patchFile];\n\n const result = await runPatch(args, dir, opts.signal);\n\n if (result.exitCode !== 0 && !dryRun) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch failed: ${result.stderr || result.stdout}`,\n };\n }\n\n const patched = extractPatchedFiles(result.stdout);\n\n // Record what actually changed: mtime + hash (tagged 'write' so the\n // permission bypass does not widen) so a later `edit` doesn't trip the\n // stale-read guard on our own write, plus the before/after pair for\n // session rewind.\n if (!dryRun) {\n for (const target of resolvedTargets) {\n const before = beforeContents.get(target) ?? null;\n const after = await readTextForTracking(target);\n if (after === null || after === before) continue;\n const stat = await fs.stat(target).catch(() => null);\n // Optional calls: embedders may hand in a duck-typed Context.\n if (stat) ctx.recordRead?.(target, stat.mtimeMs, 'write', sha256hex(after));\n ctx.session?.recordFileChange?.({\n path: target,\n action: before === null ? 'created' : 'modified',\n before,\n after,\n });\n }\n }\n\n return {\n applied: patched.length,\n rejected: 0,\n files: patched,\n dry_run: dryRun,\n message: result.stdout || 'patch applied',\n };\n } finally {\n await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n },\n};\n\n/** Read a target file as UTF-8 for change tracking. Returns null when the\n * file is missing (new-file diff), binary, or too large to snapshot. */\nconst MAX_TRACKING_BYTES = 5 * 1024 * 1024;\nasync function readTextForTracking(absPath: string): Promise<string | null> {\n try {\n const stat = await fs.stat(absPath);\n if (!stat.isFile() || stat.size > MAX_TRACKING_BYTES) return null;\n const buf = await fs.readFile(absPath);\n if (buf.includes(0)) return null; // binary\n return buf.toString('utf8');\n } catch {\n return null;\n }\n}\n\n/** Extract every `+++ <path>` target from a unified diff. */\nfunction extractDiffTargets(patch: string): string[] {\n const out: string[] = [];\n // Matches `+++ path/to/file` and `+++ b/path/to/file` (also `a/`). Strips\n // optional tab-prefixed timestamp suffixes that some diff tools emit.\n // Cap each line at 4096 chars to prevent maliciously long lines from\n // causing regex backtracking issues in large patches.\n const re = /^\\+\\+\\+\\s+([^\\t\\r\\n]+)/gm;\n for (const m of patch.matchAll(re)) {\n const raw = m[1];\n if (!raw) continue;\n const target = raw.length > 4096 ? raw.slice(0, 4096).trim() : raw.trim();\n if (!target || target === '/dev/null') continue;\n out.push(target);\n }\n return out;\n}\n\n/** Mimic `patch -pN` path stripping on a single target. Returns undefined\n * if the path has fewer segments than `strip`. */\nfunction stripPathComponents(p: string, strip: number): string | undefined {\n // Normalize separators so the count works on both POSIX and Windows-style\n // paths embedded in LLM-generated diffs. Filter out empty segments (e.g.\n // from trailing slashes or `//` sequences) before counting.\n const parts = p.replace(/\\\\/g, '/').split('/').filter((s) => s !== '' && s !== '.');\n if (parts.length <= strip) return undefined;\n return parts.slice(strip).join('/');\n}\n\nfunction runPatch(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n // Force C locale so `extractPatchedFiles` (which greps for the English\n // \"patching file\" prefix) doesn't silently miss-count on systems with\n // localized GNU patch output (fr/de/es etc.). Use buildChildEnv to\n // strip API keys and other secrets from the parent environment.\n const env = { ...buildChildEnv(), LANG: 'C', LC_ALL: 'C' };\n const child = spawn('patch', args, { cwd, signal, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));\n child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message }));\n });\n}\n\nfunction extractPatchedFiles(output: string): string[] {\n const files: string[] = [];\n const re = /patching file (.+)/gi;\n for (const m of output.matchAll(re)) {\n if (m[1]) files.push(m[1]);\n }\n return files;\n}\n"]}
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/patch.ts", "../src/_util.ts"],
4
+ "sourcesContent": ["import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve, sha256hex } from './_util.js';\n\ninterface PatchInput {\n patch: string;\n directory?: string | undefined;\n strip?: number | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface PatchOutput {\n applied: number;\n rejected: number;\n files: string[];\n dry_run: boolean;\n message: string;\n}\n\nexport const patchTool: Tool<PatchInput, PatchOutput> = {\n name: 'patch',\n category: 'Filesystem',\n description:\n 'Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.',\n usageHint:\n 'Best used when you already have a diff (from generation, external source, or previous step).\\n' +\n '- Use `dry_run: true` to see what would happen without modifying files.\\n' +\n '- On failure it creates .rej and .orig files for manual review.\\n' +\n 'Often cleaner than many small `edit` operations for larger changes.',\n selection: {\n doNotUseWhen: 'you do not already have a unified diff or only need one precise replacement.',\n useInstead: ['edit'],\n },\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n directory: { type: 'string', description: 'Root directory for patch (default: cwd)' },\n strip: { type: 'integer', description: 'Strip leading path components (default: 1)' },\n dry_run: { type: 'boolean', description: 'Preview without applying' },\n },\n required: ['patch'],\n },\n async execute(input, ctx, opts) {\n if (!input?.patch) throw new Error('patch: patch content is required');\n\n const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;\n // strip=0 lets a diff address absolute paths like /etc/passwd and\n // escape the project root entirely. Force >= 1.\n const strip = Math.max(1, input.strip ?? 1);\n const dryRun = input.dry_run ?? false;\n\n // Pre-flight: scan diff target paths and reject any that resolve outside\n // the project root. This catches `../../../etc/passwd`-style escapes\n // before we hand the diff to GNU patch.\n const targets = extractDiffTargets(input.patch);\n const resolvedTargets: string[] = [];\n for (const t of targets) {\n const stripped = stripPathComponents(t, strip);\n if (!stripped) continue;\n const candidate = path.resolve(dir, stripped);\n const rel = path.relative(ctx.projectRoot, candidate);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch refused: target \"${t}\" resolves outside project root`,\n };\n }\n resolvedTargets.push(candidate);\n }\n\n // Snapshot target contents before applying so the change can be recorded\n // for session rewind and stale-read tracking (same bookkeeping as `edit`).\n const beforeContents = new Map<string, string | null>();\n if (!dryRun) {\n for (const target of resolvedTargets) {\n beforeContents.set(target, await readTextForTracking(target));\n }\n }\n\n // Write the diff into a private 0700 temp directory rather than into\n // the user-controlled `dir` with a predictable timestamp name. Avoids\n // symlink-bait races on shared work trees.\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), '.wstack_patch_'));\n try {\n await fs.chmod(tmpDir, 0o700).catch(() => {\n /* best-effort on Windows */\n });\n const patchFile = path.join(tmpDir, 'in.diff');\n await fs.writeFile(patchFile, input.patch, { mode: 0o600 });\n\n const args = [`-p${strip}`, '--merge', ...(dryRun ? ['--dry-run'] : []), '-i', patchFile];\n\n const result = await runPatch(args, dir, opts.signal);\n\n if (result.exitCode !== 0 && !dryRun) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch failed: ${result.stderr || result.stdout}`,\n };\n }\n\n const patched = extractPatchedFiles(result.stdout);\n\n // Record what actually changed: mtime + hash (tagged 'write' so the\n // permission bypass does not widen) so a later `edit` doesn't trip the\n // stale-read guard on our own write, plus the before/after pair for\n // session rewind.\n if (!dryRun) {\n for (const target of resolvedTargets) {\n const before = beforeContents.get(target) ?? null;\n const after = await readTextForTracking(target);\n if (after === null || after === before) continue;\n const stat = await fs.stat(target).catch(() => null);\n // Optional calls: embedders may hand in a duck-typed Context.\n if (stat) ctx.recordRead?.(target, stat.mtimeMs, 'write', sha256hex(after));\n ctx.session?.recordFileChange?.({\n path: target,\n action: before === null ? 'created' : 'modified',\n before,\n after,\n });\n }\n }\n\n return {\n applied: patched.length,\n rejected: 0,\n files: patched,\n dry_run: dryRun,\n message: result.stdout || 'patch applied',\n };\n } finally {\n await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n },\n};\n\n/** Read a target file as UTF-8 for change tracking. Returns null when the\n * file is missing (new-file diff), binary, or too large to snapshot. */\nconst MAX_TRACKING_BYTES = 5 * 1024 * 1024;\nasync function readTextForTracking(absPath: string): Promise<string | null> {\n try {\n const stat = await fs.stat(absPath);\n if (!stat.isFile() || stat.size > MAX_TRACKING_BYTES) return null;\n const buf = await fs.readFile(absPath);\n if (buf.includes(0)) return null; // binary\n return buf.toString('utf8');\n } catch {\n return null;\n }\n}\n\n/** Extract every `+++ <path>` target from a unified diff. */\nfunction extractDiffTargets(patch: string): string[] {\n const out: string[] = [];\n // Matches `+++ path/to/file` and `+++ b/path/to/file` (also `a/`). Strips\n // optional tab-prefixed timestamp suffixes that some diff tools emit.\n // Cap each line at 4096 chars to prevent maliciously long lines from\n // causing regex backtracking issues in large patches.\n const re = /^\\+\\+\\+\\s+([^\\t\\r\\n]+)/gm;\n for (const m of patch.matchAll(re)) {\n const raw = m[1];\n if (!raw) continue;\n const target = raw.length > 4096 ? raw.slice(0, 4096).trim() : raw.trim();\n if (!target || target === '/dev/null') continue;\n out.push(target);\n }\n return out;\n}\n\n/** Mimic `patch -pN` path stripping on a single target. Returns undefined\n * if the path has fewer segments than `strip`. */\nfunction stripPathComponents(p: string, strip: number): string | undefined {\n // Normalize separators so the count works on both POSIX and Windows-style\n // paths embedded in LLM-generated diffs. Filter out empty segments (e.g.\n // from trailing slashes or `//` sequences) before counting.\n const parts = p.replace(/\\\\/g, '/').split('/').filter((s) => s !== '' && s !== '.');\n if (parts.length <= strip) return undefined;\n return parts.slice(strip).join('/');\n}\n\nfunction runPatch(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n // Force C locale so `extractPatchedFiles` (which greps for the English\n // \"patching file\" prefix) doesn't silently miss-count on systems with\n // localized GNU patch output (fr/de/es etc.). Use buildChildEnv to\n // strip API keys and other secrets from the parent environment.\n const env = { ...buildChildEnv(), LANG: 'C', LC_ALL: 'C' };\n const child = spawn('patch', args, { cwd, signal, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));\n child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message }));\n });\n}\n\nfunction extractPatchedFiles(output: string): string[] {\n const files: string[] = [];\n const re = /patching file (.+)/gi;\n for (const m of output.matchAll(re)) {\n if (m[1]) files.push(m[1]);\n }\n return files;\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": ";AAAA,SAAS,aAAa;AACtB,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAYA,WAAU;AACtB,SAAS,qBAAqB;;;ACJ9B,SAAS,kBAAkB;AAE3B,YAAY,UAAU;AACtB,YAAY,UAAU;AAQf,SAAS,UAAU,SAAyB;AACjD,SAAO,WAAW,QAAQ,EAAE,OAAO,SAAS,MAAM,EAAE,OAAO,KAAK;AAClE;AA2BO,SAAS,YAAY,OAAe,KAAsB;AAC/D,SAAY,gBAAW,KAAK,IAAS,eAAU,KAAK,IAAS,aAAQ,IAAI,cAAc,IAAI,KAAK,KAAK;AACvG;AAOA,SAAS,aAAa,KAAwB;AAC5C,SAAO,CAAM,aAAQ,IAAI,WAAW,GAAQ,aAAa,sBAAiB,CAAC,CAAC;AAC9E;AAGA,SAAS,YAAY,QAAgB,OAA0B;AAC7D,SAAO,MAAM,KAAK,CAAC,SAAS;AAC1B,UAAM,MAAW,cAAS,MAAM,MAAM;AACtC,WAAO,QAAQ,MAAO,CAAC,IAAI,WAAW,IAAI,KAAK,CAAM,gBAAW,GAAG;AAAA,EACrE,CAAC;AACH;AAEO,SAAS,iBAAiB,SAAiB,KAAsB;AACtE,QAAM,SAAc,aAAQ,OAAO;AAEnC,MAAI,IAAI,wBAAyB,QAAO;AACxC,MAAI,YAAY,QAAQ,aAAa,GAAG,CAAC,EAAG,QAAO;AACnD,QAAM,IAAI,MAAM,SAAS,OAAO,8BAAmC,aAAQ,IAAI,WAAW,CAAC,GAAG;AAChG;AAEO,SAAS,YAAY,OAAe,KAAsB;AAC/D,SAAO,iBAAiB,YAAY,OAAO,GAAG,GAAG,GAAG;AACtD;;;ADhDO,IAAM,YAA2C;AAAA,EACtD,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aACE;AAAA,EACF,WACE;AAAA,EAIF,WAAW;AAAA,IACT,cAAc;AAAA,IACd,YAAY,CAAC,MAAM;AAAA,EACrB;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,cAAc,CAAC,UAAU;AAAA,EACzB,MAAM;AAAA,EACN,WAAW;AAAA,EACX,aAAa;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,MACV,OAAO,EAAE,MAAM,UAAU,aAAa,6BAA6B;AAAA,MACnE,WAAW,EAAE,MAAM,UAAU,aAAa,0CAA0C;AAAA,MACpF,OAAO,EAAE,MAAM,WAAW,aAAa,6CAA6C;AAAA,MACpF,SAAS,EAAE,MAAM,WAAW,aAAa,2BAA2B;AAAA,IACtE;AAAA,IACA,UAAU,CAAC,OAAO;AAAA,EACpB;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK,MAAM;AAC9B,QAAI,CAAC,OAAO,MAAO,OAAM,IAAI,MAAM,kCAAkC;AAErE,UAAM,MAAM,MAAM,YAAY,YAAY,MAAM,WAAW,GAAG,IAAI,IAAI;AAGtE,UAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,SAAS,CAAC;AAC1C,UAAM,SAAS,MAAM,WAAW;AAKhC,UAAM,UAAU,mBAAmB,MAAM,KAAK;AAC9C,UAAM,kBAA4B,CAAC;AACnC,eAAW,KAAK,SAAS;AACvB,YAAM,WAAW,oBAAoB,GAAG,KAAK;AAC7C,UAAI,CAAC,SAAU;AACf,YAAM,YAAiB,cAAQ,KAAK,QAAQ;AAC5C,YAAM,MAAW,eAAS,IAAI,aAAa,SAAS;AACpD,UAAI,IAAI,WAAW,IAAI,KAAU,iBAAW,GAAG,GAAG;AAChD,eAAO;AAAA,UACL,SAAS;AAAA,UACT,UAAU;AAAA,UACV,OAAO,CAAC;AAAA,UACR,SAAS;AAAA,UACT,SAAS,0BAA0B,CAAC;AAAA,QACtC;AAAA,MACF;AACA,sBAAgB,KAAK,SAAS;AAAA,IAChC;AAIA,UAAM,iBAAiB,oBAAI,IAA2B;AACtD,QAAI,CAAC,QAAQ;AACX,iBAAW,UAAU,iBAAiB;AACpC,uBAAe,IAAI,QAAQ,MAAM,oBAAoB,MAAM,CAAC;AAAA,MAC9D;AAAA,IACF;AAKA,UAAM,SAAS,MAAS,WAAa,WAAQ,UAAO,GAAG,gBAAgB,CAAC;AACxE,QAAI;AACF,YAAS,SAAM,QAAQ,GAAK,EAAE,MAAM,MAAM;AAAA,MAE1C,CAAC;AACD,YAAM,YAAiB,WAAK,QAAQ,SAAS;AAC7C,YAAS,aAAU,WAAW,MAAM,OAAO,EAAE,MAAM,IAAM,CAAC;AAE1D,YAAM,OAAO,CAAC,KAAK,KAAK,IAAI,WAAW,GAAI,SAAS,CAAC,WAAW,IAAI,CAAC,GAAI,MAAM,SAAS;AAExF,YAAM,SAAS,MAAM,SAAS,MAAM,KAAK,KAAK,MAAM;AAEpD,UAAI,OAAO,aAAa,KAAK,CAAC,QAAQ;AACpC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,UAAU;AAAA,UACV,OAAO,CAAC;AAAA,UACR,SAAS;AAAA,UACT,SAAS,iBAAiB,OAAO,UAAU,OAAO,MAAM;AAAA,QAC1D;AAAA,MACF;AAEA,YAAM,UAAU,oBAAoB,OAAO,MAAM;AAMjD,UAAI,CAAC,QAAQ;AACX,mBAAW,UAAU,iBAAiB;AACpC,gBAAM,SAAS,eAAe,IAAI,MAAM,KAAK;AAC7C,gBAAM,QAAQ,MAAM,oBAAoB,MAAM;AAC9C,cAAI,UAAU,QAAQ,UAAU,OAAQ;AACxC,gBAAMC,QAAO,MAAS,QAAK,MAAM,EAAE,MAAM,MAAM,IAAI;AAEnD,cAAIA,MAAM,KAAI,aAAa,QAAQA,MAAK,SAAS,SAAS,UAAU,KAAK,CAAC;AAC1E,cAAI,SAAS,mBAAmB;AAAA,YAC9B,MAAM;AAAA,YACN,QAAQ,WAAW,OAAO,YAAY;AAAA,YACtC;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAEA,aAAO;AAAA,QACL,SAAS,QAAQ;AAAA,QACjB,UAAU;AAAA,QACV,OAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS,OAAO,UAAU;AAAA,MAC5B;AAAA,IACF,UAAE;AACA,YAAS,MAAG,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACtE;AAAA,EACF;AACF;AAIA,IAAM,qBAAqB,IAAI,OAAO;AACtC,eAAe,oBAAoB,SAAyC;AAC1E,MAAI;AACF,UAAMA,QAAO,MAAS,QAAK,OAAO;AAClC,QAAI,CAACA,MAAK,OAAO,KAAKA,MAAK,OAAO,mBAAoB,QAAO;AAC7D,UAAM,MAAM,MAAS,YAAS,OAAO;AACrC,QAAI,IAAI,SAAS,CAAC,EAAG,QAAO;AAC5B,WAAO,IAAI,SAAS,MAAM;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,mBAAmB,OAAyB;AACnD,QAAM,MAAgB,CAAC;AAKvB,QAAM,KAAK;AACX,aAAW,KAAK,MAAM,SAAS,EAAE,GAAG;AAClC,UAAM,MAAM,EAAE,CAAC;AACf,QAAI,CAAC,IAAK;AACV,UAAM,SAAS,IAAI,SAAS,OAAO,IAAI,MAAM,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,KAAK;AACxE,QAAI,CAAC,UAAU,WAAW,YAAa;AACvC,QAAI,KAAK,MAAM;AAAA,EACjB;AACA,SAAO;AACT;AAIA,SAAS,oBAAoB,GAAW,OAAmC;AAIzE,QAAM,QAAQ,EAAE,QAAQ,OAAO,GAAG,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,MAAM,MAAM,MAAM,GAAG;AAClF,MAAI,MAAM,UAAU,MAAO,QAAO;AAClC,SAAO,MAAM,MAAM,KAAK,EAAE,KAAK,GAAG;AACpC;AAEA,SAAS,SACP,MACA,KACA,QAC+D;AAC/D,SAAO,IAAI,QAAQ,CAACC,aAAY;AAC9B,QAAI,SAAS;AACb,QAAI,SAAS;AAMb,UAAM,MAAM,EAAE,GAAG,cAAc,GAAG,MAAM,KAAK,QAAQ,IAAI;AACzD,UAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,KAAK,QAAQ,KAAK,OAAO,CAAC,QAAQ,QAAQ,MAAM,GAAG,aAAa,KAAK,CAAC;AAC3G,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM;AAC9B,gBAAU,EAAE,SAAS;AAAA,IACvB,CAAC;AACD,UAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM;AAC9B,gBAAU,EAAE,SAAS;AAAA,IACvB,CAAC;AACD,UAAM,GAAG,SAAS,CAAC,SAASA,SAAQ,EAAE,UAAU,QAAQ,GAAG,QAAQ,OAAO,CAAC,CAAC;AAC5E,UAAM,GAAG,SAAS,CAAC,MAAMA,SAAQ,EAAE,UAAU,GAAG,QAAQ,IAAI,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAAA,EAClF,CAAC;AACH;AAEA,SAAS,oBAAoB,QAA0B;AACrD,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK;AACX,aAAW,KAAK,OAAO,SAAS,EAAE,GAAG;AACnC,QAAI,EAAE,CAAC,EAAG,OAAM,KAAK,EAAE,CAAC,CAAC;AAAA,EAC3B;AACA,SAAO;AACT;",
6
+ "names": ["path", "stat", "resolve"]
7
+ }
package/dist/plan.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Tool } from '@wrongstack/core';
2
+ /**
3
+ * `planTool` — the LLM-callable counterpart to the `/plan` slash command.
4
+ *
5
+ * Plans capture strategic, multi-step approaches that survive across
6
+ * session resumes (unlike todos, which are tactical and per-turn).
7
+ * Storage path comes from `ctx.meta['plan.path']` — the CLI seeds this
8
+ * during startup so the tool always knows where to read/write.
9
+ *
10
+ * One tool, multiple actions, JSON in/out. The action discriminates the
11
+ * operation so the LLM can do show / add / start / done / remove / promote /
12
+ * derive / template_use / clear via a single tool registration instead of
13
+ * bloating the surface with nine near-identical tools.
14
+ */
15
+ interface PlanInput {
16
+ action: 'show' | 'add' | 'start' | 'done' | 'remove' | 'promote' | 'template_use' | 'clear' | 'taskify';
17
+ /** Required for add. */
18
+ title?: string | undefined;
19
+ /** Optional detail line for add. */
20
+ details?: string | undefined;
21
+ /** Required for start/done/remove/promote — accepts plan item id OR 1-based index OR title substring. */
22
+ target?: string | undefined;
23
+ /** Optional subtasks for promote. If omitted, a single todo is created from the plan item title. */
24
+ subtasks?: string[] | undefined;
25
+ /** Required for template_use — the template name (e.g. "new-feature", "bug-fix"). */
26
+ template?: string | undefined;
27
+ /**
28
+ * Storage scope. Default (unset): uses the session-scoped path — isolated to this
29
+ * session, survives resume within the same session.
30
+ * `scope: 'project'`: uses a shared project-level path, visible to all sessions
31
+ * for this project. Useful for a shared roadmap that outlasts any single session.
32
+ */
33
+ scope?: 'session' | 'project';
34
+ }
35
+ interface PlanOutput {
36
+ ok: boolean;
37
+ message: string;
38
+ /** Formatted plan after the operation. Same string the user sees from `/plan show`. */
39
+ plan: string;
40
+ /** Total item count after the operation. */
41
+ count: number;
42
+ /** Number of items not in 'done' status. */
43
+ open: number;
44
+ /** When promote/derive succeed, the generated todo items so the caller can inspect them. */
45
+ todos?: Array<{
46
+ id: string;
47
+ content: string;
48
+ status: string;
49
+ activeForm?: string | undefined;
50
+ promotedFromPlan?: string | undefined;
51
+ }>;
52
+ }
53
+ export declare const planTool: Tool<PlanInput, PlanOutput>;
54
+ export {};
55
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,UAAU,SAAS;IACjB,MAAM,EACF,MAAM,GACN,KAAK,GACL,OAAO,GACP,MAAM,GACN,QAAQ,GACR,SAAS,GACT,cAAc,GACd,OAAO,GACP,SAAS,CAAC;IACd,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAC/B;AAED,UAAU,UAAU;IAClB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;CACxI;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAyShD,CAAC"}
package/dist/plan.js ADDED
@@ -0,0 +1,280 @@
1
+ // src/plan.ts
2
+ import {
3
+ addPlanItem,
4
+ clearPlan,
5
+ deriveTodosFromPlanItem,
6
+ formatPlan,
7
+ getPlanTemplate,
8
+ mutatePlan,
9
+ removePlanItem,
10
+ setPlanItemStatus
11
+ } from "@wrongstack/core";
12
+ import {
13
+ mutateTasks,
14
+ formatTaskList
15
+ } from "@wrongstack/core";
16
+ import { randomUUID } from "node:crypto";
17
+ var planTool = {
18
+ name: "plan",
19
+ category: "Session",
20
+ description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
21
+ usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
22
+ permission: "confirm",
23
+ mutating: true,
24
+ capabilities: ["fs.write"],
25
+ icon: "plan",
26
+ timeoutMs: 2e3,
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ action: {
31
+ type: "string",
32
+ enum: [
33
+ "show",
34
+ "add",
35
+ "start",
36
+ "done",
37
+ "remove",
38
+ "promote",
39
+ "template_use",
40
+ "clear",
41
+ "taskify"
42
+ ],
43
+ description: "The operation to perform on the plan board."
44
+ },
45
+ title: {
46
+ type: "string",
47
+ description: "Title of the plan item. Required for action=add."
48
+ },
49
+ details: {
50
+ type: "string",
51
+ description: "Additional details or description for a new plan item (action=add)."
52
+ },
53
+ target: {
54
+ type: "string",
55
+ description: "Identifier for the target plan item (id, 1-based index, or partial title). Required for most actions except add/show/clear."
56
+ },
57
+ subtasks: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
61
+ },
62
+ template: {
63
+ type: "string",
64
+ description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
65
+ },
66
+ scope: {
67
+ type: "string",
68
+ enum: ["session", "project"],
69
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
70
+ }
71
+ },
72
+ required: ["action"]
73
+ },
74
+ async execute(input, ctx) {
75
+ const sessionPlanPath = ctx.meta["plan.path"];
76
+ let planPath;
77
+ if (input.scope === "project") {
78
+ if (typeof sessionPlanPath === "string") {
79
+ const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
80
+ planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
81
+ }
82
+ } else {
83
+ planPath = sessionPlanPath;
84
+ }
85
+ if (typeof planPath !== "string" || !planPath) {
86
+ return {
87
+ ok: false,
88
+ message: "Plan storage path is not configured for this session.",
89
+ plan: "",
90
+ count: 0,
91
+ open: 0
92
+ };
93
+ }
94
+ const sessionId = ctx.session?.id ?? "unknown";
95
+ let early = null;
96
+ const taskifyMeta = { title: "", details: "" };
97
+ let didTaskify = false;
98
+ let plan;
99
+ try {
100
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
101
+ switch (input.action) {
102
+ case "show":
103
+ break;
104
+ case "add": {
105
+ const title = input.title?.trim();
106
+ if (!title) {
107
+ early = mkResult(p, false, "add requires `title`.");
108
+ return p;
109
+ }
110
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
111
+ return updated;
112
+ }
113
+ case "start":
114
+ case "done": {
115
+ if (!input.target) {
116
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
117
+ return p;
118
+ }
119
+ const next = setPlanItemStatus(
120
+ p,
121
+ input.target,
122
+ input.action === "start" ? "in_progress" : "done"
123
+ );
124
+ if (next === p) {
125
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
126
+ return p;
127
+ }
128
+ return next;
129
+ }
130
+ case "remove": {
131
+ if (!input.target) {
132
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
133
+ return p;
134
+ }
135
+ const next = removePlanItem(p, input.target);
136
+ if (next === p) {
137
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
138
+ return p;
139
+ }
140
+ return next;
141
+ }
142
+ case "promote": {
143
+ if (!input.target) {
144
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
145
+ return p;
146
+ }
147
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
148
+ if (!derived) {
149
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
150
+ return p;
151
+ }
152
+ ctx.state.replaceTodos(derived.todos);
153
+ early = mkResult(
154
+ derived.plan,
155
+ true,
156
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
157
+ derived.todos
158
+ );
159
+ return derived.plan;
160
+ }
161
+ case "template_use": {
162
+ const templateName = input.template?.trim();
163
+ if (!templateName) {
164
+ early = mkResult(p, false, "template_use requires `template` name.");
165
+ return p;
166
+ }
167
+ const template = getPlanTemplate(templateName);
168
+ if (!template) {
169
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
170
+ return p;
171
+ }
172
+ let updated = p;
173
+ for (const item of template.items) {
174
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
175
+ }
176
+ early = mkResult(
177
+ updated,
178
+ true,
179
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
180
+ );
181
+ return updated;
182
+ }
183
+ case "clear":
184
+ return clearPlan(p);
185
+ case "taskify": {
186
+ if (!input.target) {
187
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
188
+ return p;
189
+ }
190
+ let itemIdx = -1;
191
+ const asNum = Number.parseInt(input.target, 10);
192
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
193
+ itemIdx = asNum - 1;
194
+ } else {
195
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
196
+ if (itemIdx === -1) {
197
+ const lower = input.target.toLowerCase();
198
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
199
+ }
200
+ }
201
+ if (itemIdx === -1 || !p.items[itemIdx]) {
202
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
203
+ return p;
204
+ }
205
+ const item = p.items[itemIdx];
206
+ taskifyMeta.title = item.title;
207
+ taskifyMeta.details = item.details ?? "";
208
+ didTaskify = true;
209
+ break;
210
+ }
211
+ default:
212
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
213
+ return p;
214
+ }
215
+ return p;
216
+ });
217
+ } catch (err) {
218
+ return {
219
+ ok: false,
220
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
221
+ plan: "",
222
+ count: 0,
223
+ open: 0
224
+ };
225
+ }
226
+ if (early) return early;
227
+ if (didTaskify) {
228
+ const taskPathRaw = ctx.meta["task.path"];
229
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
230
+ return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
231
+ }
232
+ let taskPath = taskPathRaw;
233
+ if (input.scope === "project") {
234
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
235
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
236
+ }
237
+ const now = (/* @__PURE__ */ new Date()).toISOString();
238
+ try {
239
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
240
+ f.tasks.push({
241
+ id: `task_${randomUUID()}`,
242
+ title: taskifyMeta.title,
243
+ description: taskifyMeta.details || void 0,
244
+ type: "feature",
245
+ priority: "medium",
246
+ status: "pending",
247
+ createdAt: now,
248
+ updatedAt: now
249
+ });
250
+ return f;
251
+ });
252
+ return mkResult(
253
+ plan,
254
+ true,
255
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
256
+ ${formatTaskList(taskFile.tasks)}`
257
+ );
258
+ } catch (err) {
259
+ return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
260
+ }
261
+ }
262
+ return mkResult(plan, true, `Plan ${input.action} ok.`);
263
+ }
264
+ };
265
+ function mkResult(plan, ok, message, todos) {
266
+ const open = plan.items.filter((i) => i.status !== "done").length;
267
+ const result = {
268
+ ok,
269
+ message,
270
+ plan: formatPlan(plan),
271
+ count: plan.items.length,
272
+ open
273
+ };
274
+ if (todos !== void 0) result.todos = todos;
275
+ return result;
276
+ }
277
+ export {
278
+ planTool
279
+ };
280
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plan.ts"],
4
+ "sourcesContent": ["import {\n type PlanFile,\n addPlanItem,\n clearPlan,\n deriveTodosFromPlanItem,\n formatPlan,\n getPlanTemplate,\n mutatePlan,\n removePlanItem,\n setPlanItemStatus,\n} from '@wrongstack/core';\nimport {\n type TaskFile,\n mutateTasks,\n formatTaskList,\n} from '@wrongstack/core';\nimport { randomUUID } from 'node:crypto';\nimport type { Tool } from '@wrongstack/core';\n\n/**\n * `planTool` \u2014 the LLM-callable counterpart to the `/plan` slash command.\n *\n * Plans capture strategic, multi-step approaches that survive across\n * session resumes (unlike todos, which are tactical and per-turn).\n * Storage path comes from `ctx.meta['plan.path']` \u2014 the CLI seeds this\n * during startup so the tool always knows where to read/write.\n *\n * One tool, multiple actions, JSON in/out. The action discriminates the\n * operation so the LLM can do show / add / start / done / remove / promote /\n * derive / template_use / clear via a single tool registration instead of\n * bloating the surface with nine near-identical tools.\n */\ninterface PlanInput {\n action:\n | 'show'\n | 'add'\n | 'start'\n | 'done'\n | 'remove'\n | 'promote'\n | 'template_use'\n | 'clear'\n | 'taskify';\n /** Required for add. */\n title?: string | undefined;\n /** Optional detail line for add. */\n details?: string | undefined;\n /** Required for start/done/remove/promote \u2014 accepts plan item id OR 1-based index OR title substring. */\n target?: string | undefined;\n /** Optional subtasks for promote. If omitted, a single todo is created from the plan item title. */\n subtasks?: string[] | undefined;\n /** Required for template_use \u2014 the template name (e.g. \"new-feature\", \"bug-fix\"). */\n template?: string | undefined;\n /**\n * Storage scope. Default (unset): uses the session-scoped path \u2014 isolated to this\n * session, survives resume within the same session.\n * `scope: 'project'`: uses a shared project-level path, visible to all sessions\n * for this project. Useful for a shared roadmap that outlasts any single session.\n */\n scope?: 'session' | 'project';\n}\n\ninterface PlanOutput {\n ok: boolean;\n message: string;\n /** Formatted plan after the operation. Same string the user sees from `/plan show`. */\n plan: string;\n /** Total item count after the operation. */\n count: number;\n /** Number of items not in 'done' status. */\n open: number;\n /** When promote/derive succeed, the generated todo items so the caller can inspect them. */\n todos?: Array<{ id: string; content: string; status: string; activeForm?: string | undefined; promotedFromPlan?: string | undefined }>;\n}\n\nexport const planTool: Tool<PlanInput, PlanOutput> = {\n name: 'plan',\n category: 'Session',\n description:\n 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. ' +\n 'Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. ' +\n 'Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. ' +\n 'By default plans are isolated to this session; use `scope: \"project\"` to store the plan in a shared project-level file visible to all sessions.',\n usageHint:\n 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\\n\\n' +\n '- Start by creating a high-level plan with `action: \"add\"` or using templates (`template_use`).\\n' +\n '- Use `promote` to turn a plan item into actionable todos.\\n' +\n '- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\\n' +\n '- Keep plans at the \"why and what\" level, and todos at the \"how and next step\" level.\\n' +\n '- Common templates: \"new-feature\", \"bug-fix\", \"refactor\", \"release\", \"security-audit\".\\n\\n' +\n 'This tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\\n' +\n 'Use `scope: \"project\"` to use a shared project-level plan file.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'plan',\n timeoutMs: 2_000,\n inputSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: [\n 'show',\n 'add',\n 'start',\n 'done',\n 'remove',\n 'promote',\n 'template_use',\n 'clear',\n 'taskify',\n ],\n description: 'The operation to perform on the plan board.',\n },\n title: {\n type: 'string',\n description: 'Title of the plan item. Required for action=add.',\n },\n details: {\n type: 'string',\n description: 'Additional details or description for a new plan item (action=add).',\n },\n target: {\n type: 'string',\n description:\n 'Identifier for the target plan item (id, 1-based index, or partial title). Required for most actions except add/show/clear.',\n },\n subtasks: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'List of subtask titles. Used with promote to break a plan item into multiple todos.',\n },\n template: {\n type: 'string',\n description:\n 'Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit.',\n },\n scope: {\n type: 'string',\n enum: ['session', 'project'],\n description: 'Storage scope: \"session\" (default, isolated to this session) or \"project\" (shared across all sessions for this project).',\n },\n },\n required: ['action'],\n },\n async execute(input, ctx) {\n const sessionPlanPath = (ctx.meta as Record<string, unknown>)['plan.path'] as string | undefined;\n let planPath: string | undefined;\n\n if (input.scope === 'project') {\n // Project-level: derive from the session path by replacing the filename with\n // 'backlog.plan.json' so all sessions share the same file.\n if (typeof sessionPlanPath === 'string') {\n // Handle BOTH separators \u2014 a Windows-native path uses '\\\\', and a\n // '/'-only search would miss it and fall back to a bare relative path\n // written into the process CWD instead of the sessions dir.\n const lastSep = Math.max(sessionPlanPath.lastIndexOf('/'), sessionPlanPath.lastIndexOf('\\\\'));\n planPath = lastSep >= 0\n ? sessionPlanPath.slice(0, lastSep + 1) + 'backlog.plan.json'\n : 'backlog.plan.json';\n }\n } else {\n planPath = sessionPlanPath;\n }\n if (typeof planPath !== 'string' || !planPath) {\n return {\n ok: false,\n message: 'Plan storage path is not configured for this session.',\n plan: '',\n count: 0,\n open: 0,\n };\n }\n const sessionId = ctx.session?.id ?? 'unknown';\n\n let early: PlanOutput | null = null;\n // Track taskify data \u2014 task write happens after the plan lock releases\n const taskifyMeta = { title: '', details: '' };\n let didTaskify = false;\n\n let plan: PlanFile;\n try {\n plan = await mutatePlan(planPath, sessionId, async (p) => {\n switch (input.action) {\n case 'show':\n break;\n\n case 'add': {\n const title = input.title?.trim();\n if (!title) {\n early = mkResult(p, false, 'add requires `title`.');\n return p;\n }\n const { plan: updated } = addPlanItem(p, title, input.details?.trim() || undefined);\n return updated;\n }\n\n case 'start':\n case 'done': {\n if (!input.target) {\n early = mkResult(p, false, `${input.action} requires \\`target\\` (id|index|substring).`);\n return p;\n }\n const next = setPlanItemStatus(\n p,\n input.target,\n input.action === 'start' ? 'in_progress' : 'done',\n );\n if (next === p) {\n early = mkResult(p, false, `No plan item matched \"${input.target}\".`);\n return p;\n }\n return next;\n }\n\n case 'remove': {\n if (!input.target) {\n early = mkResult(p, false, 'remove requires `target` (id|index|substring).');\n return p;\n }\n const next = removePlanItem(p, input.target);\n if (next === p) {\n early = mkResult(p, false, `No plan item matched \"${input.target}\".`);\n return p;\n }\n return next;\n }\n\n case 'promote': {\n if (!input.target) {\n early = mkResult(p, false, `${input.action} requires \\`target\\` (id|index|substring).`);\n return p;\n }\n const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);\n if (!derived) {\n early = mkResult(p, false, `No plan item matched \"${input.target}\".`);\n return p;\n }\n ctx.state.replaceTodos(derived.todos);\n early = mkResult(\n derived.plan,\n true,\n `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,\n derived.todos,\n );\n return derived.plan;\n }\n\n case 'template_use': {\n const templateName = input.template?.trim();\n if (!templateName) {\n early = mkResult(p, false, 'template_use requires `template` name.');\n return p;\n }\n const template = getPlanTemplate(templateName);\n if (!template) {\n early = mkResult(p, false, `Unknown template \"${templateName}\".`);\n return p;\n }\n let updated = p;\n for (const item of template.items) {\n ({ plan: updated } = addPlanItem(updated, item.title, item.details));\n }\n early = mkResult(\n updated,\n true,\n `Applied template \"${template.name}\" \u2014 ${template.items.length} items added.`,\n );\n return updated;\n }\n\n case 'clear':\n return clearPlan(p);\n\n case 'taskify': {\n if (!input.target) {\n early = mkResult(p, false, 'taskify requires `target` (plan item id|index|substring).');\n return p;\n }\n // Find plan item by 1-based index, exact id, or title substring\n let itemIdx = -1;\n const asNum = Number.parseInt(input.target, 10);\n if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {\n itemIdx = asNum - 1;\n } else {\n itemIdx = p.items.findIndex((it) => it.id === input.target);\n if (itemIdx === -1) {\n const lower = input.target.toLowerCase();\n itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));\n }\n }\n if (itemIdx === -1 || !p.items[itemIdx]) {\n early = mkResult(p, false, `No plan item matched \"${input.target}\".`);\n return p;\n }\n const item = p.items[itemIdx]!;\n // Extract data \u2014 task write happens after the plan lock releases\n taskifyMeta.title = item.title;\n taskifyMeta.details = item.details ?? '';\n didTaskify = true;\n break;\n }\n\n default:\n early = mkResult(p, false, `Unknown action \"${(input as { action: string }).action}\".`);\n return p;\n }\n\n return p;\n });\n } catch (err) {\n // Persist failed (mutatePlan throws on a failed save) \u2014 report ok:false\n // with the real reason instead of falsely claiming the plan was saved.\n return {\n ok: false,\n message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,\n plan: '',\n count: 0,\n open: 0,\n };\n }\n\n // If the callback set an early-return result, use it\n if (early) return early;\n\n // If taskify copied plan item data, write it to the task file now\n if (didTaskify) {\n const taskPathRaw = (ctx.meta as Record<string, unknown>)['task.path'];\n if (typeof taskPathRaw !== 'string' || !taskPathRaw) {\n return mkResult(plan, false, 'Task storage path not configured \u2014 cannot taskify.');\n }\n let taskPath: string = taskPathRaw;\n // Honor project scope for the TASK file too: a project-scoped taskify must\n // append to the shared backlog.tasks.json, not the per-session task file\n // (mirrors the plan-path derivation above; handles both separators).\n if (input.scope === 'project') {\n const lastSep = Math.max(taskPath.lastIndexOf('/'), taskPath.lastIndexOf('\\\\'));\n taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + 'backlog.tasks.json' : 'backlog.tasks.json';\n }\n const now = new Date().toISOString();\n // Mutate the cross-file under ITS OWN lock \u2014 a raw loadTasks/push/saveTasks\n // can interleave with a concurrent task tool call in the same batch and\n // clobber writes. mutateTasks is the documented race-safe write path.\n try {\n const taskFile: TaskFile = await mutateTasks(taskPath, sessionId, (f) => {\n f.tasks.push({\n id: `task_${randomUUID()}`,\n title: taskifyMeta.title,\n description: taskifyMeta.details || undefined,\n type: 'feature',\n priority: 'medium',\n status: 'pending',\n createdAt: now,\n updatedAt: now,\n });\n return f;\n });\n return mkResult(\n plan,\n true,\n `taskify ok \u2014 added \"${taskifyMeta.title}\" to tasks.\\n${formatTaskList(taskFile.tasks)}`,\n );\n } catch (err) {\n // The plan item was saved, but copying it into the task file failed.\n return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return mkResult(plan, true, `Plan ${input.action} ok.`);\n },\n};\n\nfunction mkResult(\n plan: PlanFile,\n ok: boolean,\n message: string,\n todos?: PlanOutput['todos'],\n): PlanOutput {\n const open = plan.items.filter((i) => i.status !== 'done').length;\n const result: PlanOutput = {\n ok,\n message,\n plan: formatPlan(plan),\n count: plan.items.length,\n open,\n };\n if (todos !== undefined) result.todos = todos;\n return result;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }