@namzu/sdk 3.0.0 → 3.2.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 (307) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
  3. package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
  4. package/dist/advisory/__tests__/consultation-context.test.js +124 -0
  5. package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
  6. package/dist/advisory/context.d.ts +25 -0
  7. package/dist/advisory/context.d.ts.map +1 -1
  8. package/dist/advisory/context.js +18 -0
  9. package/dist/advisory/context.js.map +1 -1
  10. package/dist/advisory/executor.d.ts.map +1 -1
  11. package/dist/advisory/executor.js +25 -3
  12. package/dist/advisory/executor.js.map +1 -1
  13. package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
  14. package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
  15. package/dist/compaction/__tests__/context-reducer.test.js +197 -0
  16. package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
  17. package/dist/compaction/factory.d.ts +7 -0
  18. package/dist/compaction/factory.d.ts.map +1 -1
  19. package/dist/compaction/factory.js +7 -0
  20. package/dist/compaction/factory.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -0
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js +1 -0
  24. package/dist/compaction/index.js.map +1 -1
  25. package/dist/compaction/interface.d.ts +13 -0
  26. package/dist/compaction/interface.d.ts.map +1 -1
  27. package/dist/compaction/managers/null.d.ts +3 -0
  28. package/dist/compaction/managers/null.d.ts.map +1 -1
  29. package/dist/compaction/managers/null.js +3 -0
  30. package/dist/compaction/managers/null.js.map +1 -1
  31. package/dist/compaction/managers/slidingWindow.d.ts +6 -0
  32. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
  33. package/dist/compaction/managers/slidingWindow.js +6 -0
  34. package/dist/compaction/managers/slidingWindow.js.map +1 -1
  35. package/dist/compaction/managers/structured.d.ts +10 -0
  36. package/dist/compaction/managers/structured.d.ts.map +1 -1
  37. package/dist/compaction/managers/structured.js +10 -0
  38. package/dist/compaction/managers/structured.js.map +1 -1
  39. package/dist/compaction/reducer.d.ts +86 -0
  40. package/dist/compaction/reducer.d.ts.map +1 -0
  41. package/dist/compaction/reducer.js +77 -0
  42. package/dist/compaction/reducer.js.map +1 -0
  43. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
  44. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
  45. package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
  46. package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
  47. package/dist/connector/builtins/http.d.ts.map +1 -1
  48. package/dist/connector/builtins/http.js +24 -2
  49. package/dist/connector/builtins/http.js.map +1 -1
  50. package/dist/connector/builtins/http.test.js +18 -2
  51. package/dist/connector/builtins/http.test.js.map +1 -1
  52. package/dist/connector/index.d.ts +2 -2
  53. package/dist/connector/index.d.ts.map +1 -1
  54. package/dist/connector/index.js +1 -1
  55. package/dist/connector/index.js.map +1 -1
  56. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
  57. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
  58. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
  59. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
  60. package/dist/connector/mcp/client.d.ts +52 -1
  61. package/dist/connector/mcp/client.d.ts.map +1 -1
  62. package/dist/connector/mcp/client.js +86 -0
  63. package/dist/connector/mcp/client.js.map +1 -1
  64. package/dist/connector/mcp/discovery.d.ts +12 -1
  65. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  66. package/dist/connector/mcp/discovery.js +19 -4
  67. package/dist/connector/mcp/discovery.js.map +1 -1
  68. package/dist/connector/mcp/index.d.ts +2 -2
  69. package/dist/connector/mcp/index.d.ts.map +1 -1
  70. package/dist/connector/mcp/index.js +1 -1
  71. package/dist/connector/mcp/index.js.map +1 -1
  72. package/dist/connector/mcp/server.d.ts +42 -1
  73. package/dist/connector/mcp/server.d.ts.map +1 -1
  74. package/dist/connector/mcp/server.js +77 -4
  75. package/dist/connector/mcp/server.js.map +1 -1
  76. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
  77. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
  78. package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
  79. package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
  80. package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
  81. package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
  82. package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
  83. package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
  84. package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
  85. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
  86. package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
  87. package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
  88. package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
  89. package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
  90. package/dist/plugin/lifecycle.d.ts +41 -0
  91. package/dist/plugin/lifecycle.d.ts.map +1 -1
  92. package/dist/plugin/lifecycle.js +29 -1
  93. package/dist/plugin/lifecycle.js.map +1 -1
  94. package/dist/plugin/loader.d.ts +39 -3
  95. package/dist/plugin/loader.d.ts.map +1 -1
  96. package/dist/plugin/loader.js +37 -4
  97. package/dist/plugin/loader.js.map +1 -1
  98. package/dist/public-runtime.d.ts +5 -3
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +11 -3
  101. package/dist/public-runtime.js.map +1 -1
  102. package/dist/public-types.d.ts +2 -2
  103. package/dist/public-types.d.ts.map +1 -1
  104. package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
  105. package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
  106. package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
  107. package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
  108. package/dist/rag/ingestion.d.ts.map +1 -1
  109. package/dist/rag/ingestion.js +1 -0
  110. package/dist/rag/ingestion.js.map +1 -1
  111. package/dist/rag/retriever.d.ts.map +1 -1
  112. package/dist/rag/retriever.js +2 -0
  113. package/dist/rag/retriever.js.map +1 -1
  114. package/dist/rag/vector-store.d.ts.map +1 -1
  115. package/dist/rag/vector-store.js +6 -0
  116. package/dist/rag/vector-store.js.map +1 -1
  117. package/dist/registry/tool/execute.d.ts.map +1 -1
  118. package/dist/registry/tool/execute.js +113 -109
  119. package/dist/registry/tool/execute.js.map +1 -1
  120. package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
  121. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  122. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  125. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  128. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  129. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  132. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  133. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  134. package/dist/runtime/query/executor.d.ts +1 -0
  135. package/dist/runtime/query/executor.d.ts.map +1 -1
  136. package/dist/runtime/query/executor.js +10 -1
  137. package/dist/runtime/query/executor.js.map +1 -1
  138. package/dist/runtime/query/index.d.ts +10 -0
  139. package/dist/runtime/query/index.d.ts.map +1 -1
  140. package/dist/runtime/query/index.js +26 -0
  141. package/dist/runtime/query/index.js.map +1 -1
  142. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  143. package/dist/runtime/query/iteration/index.js +118 -34
  144. package/dist/runtime/query/iteration/index.js.map +1 -1
  145. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  146. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  147. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  148. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  149. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  150. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  151. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  153. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  154. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  155. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  156. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  157. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  158. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  159. package/dist/runtime/query/resume-run.d.ts +70 -0
  160. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  161. package/dist/runtime/query/resume-run.js +46 -0
  162. package/dist/runtime/query/resume-run.js.map +1 -0
  163. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  164. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  165. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  166. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  167. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  168. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  169. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  170. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  171. package/dist/tools/advisory/index.js +1 -1
  172. package/dist/tools/advisory/index.js.map +1 -1
  173. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
  174. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
  175. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
  176. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
  177. package/dist/tools/builtins/__tests__/edit.test.js +242 -1
  178. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  179. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
  180. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
  181. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
  182. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
  183. package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
  184. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
  185. package/dist/tools/builtins/atomic-write-file.js +31 -2
  186. package/dist/tools/builtins/atomic-write-file.js.map +1 -1
  187. package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
  188. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
  189. package/dist/tools/builtins/content-fingerprint.js +30 -0
  190. package/dist/tools/builtins/content-fingerprint.js.map +1 -0
  191. package/dist/tools/builtins/edit.d.ts.map +1 -1
  192. package/dist/tools/builtins/edit.js +172 -31
  193. package/dist/tools/builtins/edit.js.map +1 -1
  194. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  195. package/dist/tools/builtins/read-file.js +5 -2
  196. package/dist/tools/builtins/read-file.js.map +1 -1
  197. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  198. package/dist/tools/builtins/write-file.js +75 -27
  199. package/dist/tools/builtins/write-file.js.map +1 -1
  200. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
  201. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
  202. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  203. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  204. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  205. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  206. package/dist/tools/coordinator/index.d.ts.map +1 -1
  207. package/dist/tools/coordinator/index.js +75 -5
  208. package/dist/tools/coordinator/index.js.map +1 -1
  209. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  210. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  211. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  212. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  213. package/dist/types/agent/supervisor.d.ts +15 -0
  214. package/dist/types/agent/supervisor.d.ts.map +1 -1
  215. package/dist/types/connector/core.d.ts +34 -0
  216. package/dist/types/connector/core.d.ts.map +1 -1
  217. package/dist/types/connector/definition.d.ts +10 -0
  218. package/dist/types/connector/definition.d.ts.map +1 -1
  219. package/dist/types/connector/mcp.d.ts +13 -0
  220. package/dist/types/connector/mcp.d.ts.map +1 -1
  221. package/dist/types/rag/retrieval.d.ts +16 -0
  222. package/dist/types/rag/retrieval.d.ts.map +1 -1
  223. package/dist/types/rag/storage.d.ts +9 -0
  224. package/dist/types/rag/storage.d.ts.map +1 -1
  225. package/dist/types/rag/vector.d.ts +11 -0
  226. package/dist/types/rag/vector.d.ts.map +1 -1
  227. package/dist/types/router/task-router.d.ts +19 -0
  228. package/dist/types/router/task-router.d.ts.map +1 -1
  229. package/dist/types/run/prepare-step.d.ts +56 -3
  230. package/dist/types/run/prepare-step.d.ts.map +1 -1
  231. package/dist/types/tool/index.d.ts +17 -1
  232. package/dist/types/tool/index.d.ts.map +1 -1
  233. package/dist/types/tool/index.js.map +1 -1
  234. package/dist/types/toolset/index.d.ts +22 -0
  235. package/dist/types/toolset/index.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  238. package/src/advisory/context.ts +32 -0
  239. package/src/advisory/executor.ts +30 -3
  240. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  241. package/src/compaction/factory.ts +7 -0
  242. package/src/compaction/index.ts +8 -0
  243. package/src/compaction/interface.ts +13 -0
  244. package/src/compaction/managers/null.ts +3 -0
  245. package/src/compaction/managers/slidingWindow.ts +6 -0
  246. package/src/compaction/managers/structured.ts +10 -0
  247. package/src/compaction/reducer.ts +154 -0
  248. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  249. package/src/connector/builtins/http.test.ts +28 -2
  250. package/src/connector/builtins/http.ts +26 -2
  251. package/src/connector/index.ts +6 -2
  252. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  253. package/src/connector/mcp/client.ts +95 -0
  254. package/src/connector/mcp/discovery.ts +19 -4
  255. package/src/connector/mcp/index.ts +6 -2
  256. package/src/connector/mcp/server.ts +101 -3
  257. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  258. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  259. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  260. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  261. package/src/plugin/lifecycle.ts +57 -1
  262. package/src/plugin/loader.ts +57 -3
  263. package/src/public-runtime.ts +16 -1
  264. package/src/public-types.ts +5 -0
  265. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  266. package/src/rag/ingestion.ts +1 -0
  267. package/src/rag/retriever.ts +2 -0
  268. package/src/rag/vector-store.ts +5 -0
  269. package/src/registry/tool/execute.ts +123 -119
  270. package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
  271. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  272. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  273. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  274. package/src/runtime/query/executor.ts +9 -1
  275. package/src/runtime/query/index.ts +39 -0
  276. package/src/runtime/query/iteration/index.ts +130 -34
  277. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  278. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  279. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  280. package/src/runtime/query/iteration/phases/context.ts +11 -0
  281. package/src/runtime/query/resume-run.ts +93 -0
  282. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  283. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  284. package/src/tools/advisory/index.ts +1 -1
  285. package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
  286. package/src/tools/builtins/__tests__/edit.test.ts +324 -1
  287. package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
  288. package/src/tools/builtins/atomic-write-file.ts +30 -2
  289. package/src/tools/builtins/content-fingerprint.ts +33 -0
  290. package/src/tools/builtins/edit.ts +188 -36
  291. package/src/tools/builtins/read-file.ts +5 -2
  292. package/src/tools/builtins/write-file.ts +79 -28
  293. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
  294. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  295. package/src/tools/coordinator/index.ts +116 -42
  296. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  297. package/src/types/agent/supervisor.ts +15 -0
  298. package/src/types/connector/core.ts +34 -0
  299. package/src/types/connector/definition.ts +10 -0
  300. package/src/types/connector/mcp.ts +14 -0
  301. package/src/types/rag/retrieval.ts +16 -0
  302. package/src/types/rag/storage.ts +9 -0
  303. package/src/types/rag/vector.ts +11 -0
  304. package/src/types/router/task-router.ts +19 -0
  305. package/src/types/run/prepare-step.ts +58 -3
  306. package/src/types/tool/index.ts +17 -1
  307. package/src/types/toolset/index.ts +22 -0
@@ -1,8 +1,10 @@
1
- import { access, mkdir, writeFile } from 'node:fs/promises'
1
+ import { access, mkdir } from 'node:fs/promises'
2
2
  import { dirname, resolve } from 'node:path'
3
3
  import { z } from 'zod'
4
4
  import type { ToolContext } from '../../types/tool/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
+ import { atomicWriteFile } from './atomic-write-file.js'
7
+ import { withFileMutationLock } from './file-mutation-lock.js'
6
8
 
7
9
  const inputSchema = z
8
10
  .object({
@@ -25,17 +27,46 @@ const inputSchema = z
25
27
  'Alias for content. Useful for hosts that expose create/write operations as newStr. Self-budget this payload under 12000 characters before calling.',
26
28
  ),
27
29
  })
30
+ .strict()
28
31
  .refine((value) => typeof value.content === 'string' || typeof value.newStr === 'string', {
29
32
  message: 'Either content or newStr is required.',
30
33
  })
31
34
 
32
35
  type WriteInput = z.infer<typeof inputSchema>
33
36
 
37
+ /**
38
+ * The single shape a model is constrained to emit.
39
+ *
40
+ * `newStr` is a host affordance and deliberately absent here: a model given
41
+ * two names for the body has to pick, and picking is what produces the
42
+ * half-filled calls this schema exists to prevent.
43
+ */
44
+ const modelInputSchema: Record<string, unknown> = {
45
+ type: 'object',
46
+ properties: {
47
+ path: {
48
+ type: 'string',
49
+ description: 'Relative path to the file to write. Must not be empty.',
50
+ },
51
+ content: {
52
+ type: 'string',
53
+ description:
54
+ 'Complete file body. Use "" only for an intentionally empty file. Keep under 12000 characters.',
55
+ },
56
+ },
57
+ required: ['path', 'content'],
58
+ additionalProperties: false,
59
+ }
60
+
34
61
  export const WriteFileTool = defineTool({
35
62
  name: 'write',
36
63
  description:
37
64
  'Writes a file to the local filesystem. Overwrites the existing file at the path if there is one.\n\n- If the file already exists, you must use the `read` tool on it first in this conversation, or this call will fail.\n- Prefer the `edit` tool for modifying existing files — it only sends the diff and preserves the rest of the file byte-for-byte.\n- Use `write` to create a new file or to perform a deliberate full rewrite of a file you have already read.\n- Self-budget content/newStr under 12000 characters before emitting the tool call. For long content, write a smaller opening section, then use `edit` with insertLine: "end" to extend the file section by section. Do not chain multiple `write` calls — each one overwrites the previous.',
38
65
  inputSchema,
66
+ modelInputSchema,
67
+ enforceModelInput: true,
68
+ validationErrorHint:
69
+ 'Required shape: {"path":"file.md","content":"complete file body"}. Pass the whole body, not a diff.',
39
70
  category: 'filesystem',
40
71
  permissions: ['file_write'],
41
72
  readOnly: false,
@@ -43,40 +74,60 @@ export const WriteFileTool = defineTool({
43
74
  concurrencySafe: false,
44
75
 
45
76
  async execute(input: WriteInput, context) {
46
- const content = input.content ?? input.newStr ?? ''
47
- // Sandbox-aware: route through sandbox.writeFile() when available
48
- if (context.sandbox) {
49
- const sandboxExists = await sandboxFileExists(context, input.path)
50
- if (sandboxExists) {
51
- const guard = enforceReadBeforeOverwrite(context, input.path)
52
- if (guard) return guard
53
- }
54
- await context.sandbox.writeFile(input.path, content)
55
- context.fileReadTracker?.recordRead(input.path)
77
+ // `execute` is reachable without going through the registry, so the
78
+ // closed contract has to be enforced on this path too or it is not
79
+ // closed at all.
80
+ const parsed = inputSchema.safeParse(input)
81
+ if (!parsed.success) {
56
82
  return {
57
- success: true,
58
- output: `File written successfully: ${input.path} (${content.length} chars) [sandboxed]`,
59
- data: { path: input.path, size: content.length, sandboxed: true },
83
+ success: false,
84
+ output: '',
85
+ error: `Invalid write input: ${parsed.error.issues.map((issue) => issue.message).join('; ')}`,
60
86
  }
61
87
  }
88
+ const valid = parsed.data
89
+ const content = valid.content ?? valid.newStr ?? ''
90
+ const filePath = resolve(context.workingDirectory, valid.path)
91
+ // The exists-check and the write are a check-then-act pair. Unlocked,
92
+ // two writers both see "absent", both skip the read-before-overwrite
93
+ // guard, and the second silently discards the first.
94
+ const lockKey = `${context.sandbox ? 'sandbox' : 'local'}:${filePath}`
62
95
 
63
- const filePath = resolve(context.workingDirectory, input.path)
96
+ return withFileMutationLock(lockKey, async () => {
97
+ if (context.sandbox) {
98
+ const sandboxExists = await sandboxFileExists(context, valid.path)
99
+ if (sandboxExists) {
100
+ const guard = enforceReadBeforeOverwrite(context, valid.path)
101
+ if (guard) return guard
102
+ }
103
+ await context.sandbox.writeFile(valid.path, content)
104
+ context.fileReadTracker?.recordRead(valid.path)
105
+ return {
106
+ success: true as const,
107
+ output: `File written successfully: ${valid.path} (${content.length} chars) [sandboxed]`,
108
+ data: { path: valid.path, size: content.length, sandboxed: true },
109
+ }
110
+ }
64
111
 
65
- const localExists = await pathExists(filePath)
66
- if (localExists) {
67
- const guard = enforceReadBeforeOverwrite(context, filePath)
68
- if (guard) return guard
69
- }
112
+ const localExists = await pathExists(filePath)
113
+ if (localExists) {
114
+ const guard = enforceReadBeforeOverwrite(context, filePath)
115
+ if (guard) return guard
116
+ }
70
117
 
71
- await mkdir(dirname(filePath), { recursive: true })
72
- await writeFile(filePath, content, 'utf-8')
73
- context.fileReadTracker?.recordRead(filePath)
118
+ await mkdir(dirname(filePath), { recursive: true })
119
+ // Temp file, fsync, rename. A plain write that fails partway
120
+ // leaves the destination truncated — and this tool overwrites a
121
+ // whole file, so the truncation is the user's previous work.
122
+ await atomicWriteFile(filePath, content)
123
+ context.fileReadTracker?.recordRead(filePath)
74
124
 
75
- return {
76
- success: true,
77
- output: `File written successfully: ${filePath} (${content.length} chars)`,
78
- data: { path: filePath, size: content.length },
79
- }
125
+ return {
126
+ success: true as const,
127
+ output: `File written successfully: ${filePath} (${content.length} chars)`,
128
+ data: { path: filePath, size: content.length },
129
+ }
130
+ })
80
131
  },
81
132
  })
82
133
 
@@ -368,3 +368,71 @@ describe('coordinator ask_user_question decision -> output mapping', () => {
368
368
  expect(result.output).toBe('User answered "Who is the audience?": "Board"')
369
369
  })
370
370
  })
371
+
372
+ describe('the question contract is closed, not merely shaped', () => {
373
+ const noopHandler: ResumeHandler = async () => ({ action: 'continue' })
374
+
375
+ it('publishes one closed model-facing schema', () => {
376
+ const tool = askTool(noopHandler)
377
+
378
+ expect(tool.modelInputSchema).toMatchObject({
379
+ type: 'object',
380
+ required: ['question', 'options'],
381
+ additionalProperties: false,
382
+ })
383
+ // The whole point of the array declaration: a model that serializes
384
+ // its options once tends to keep doing it, and a closed schema makes
385
+ // a capable provider refuse at generation time instead of after.
386
+ const properties = (tool.modelInputSchema as { properties: Record<string, { type: string }> })
387
+ .properties
388
+ expect(properties.options?.type).toBe('array')
389
+ expect(tool.enforceModelInput).toBe(true)
390
+ })
391
+
392
+ it('rejects a serialized options string and other malformed option shapes', () => {
393
+ const tool = askTool(noopHandler)
394
+
395
+ for (const options of [
396
+ '<options><option><label>Board</label></option></options>',
397
+ '[{"label":"Board"},{"label":"Engineering"}]',
398
+ [42, { label: 'Engineering' }],
399
+ [{ description: 'Missing label' }, { label: 'Engineering' }],
400
+ [{ label: 'Board', description: 42 }, { label: 'Engineering' }],
401
+ ]) {
402
+ expect(
403
+ tool.inputSchema.safeParse({ question: 'Who is the audience?', options }).success,
404
+ JSON.stringify(options),
405
+ ).toBe(false)
406
+ }
407
+ })
408
+
409
+ it('rejects a field it does not declare, rather than dropping it', () => {
410
+ const tool = askTool(noopHandler)
411
+ const valid = {
412
+ question: 'Who is the audience?',
413
+ options: [{ label: 'Board' }, { label: 'Engineering' }],
414
+ }
415
+
416
+ expect(tool.inputSchema.safeParse(valid).success).toBe(true)
417
+
418
+ // Without `.strict()` zod strips these, so the call proceeds as if the
419
+ // caller had never written them — a misspelling becomes a silent no-op.
420
+ for (const extra of [
421
+ { ...valid, multiSelct: true },
422
+ { ...valid, choices: ['a', 'b'] },
423
+ {
424
+ ...valid,
425
+ options: [{ label: 'Board', reason: 'why' }, { label: 'Engineering' }],
426
+ },
427
+ ]) {
428
+ expect(tool.inputSchema.safeParse(extra).success, JSON.stringify(extra)).toBe(false)
429
+ }
430
+ })
431
+
432
+ it('carries a recovery hint that names the shape to retry with', () => {
433
+ const tool = askTool(noopHandler)
434
+
435
+ expect(tool.validationErrorHint).toContain('"options" must be a JSON array')
436
+ expect(tool.validationErrorHint).toContain('never a string')
437
+ })
438
+ })
@@ -0,0 +1,186 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { resolvePlanDependencies } from '../plan-dependencies.js'
4
+
5
+ /**
6
+ * The model is shown `depends_on` on every plan step and told it means
7
+ * "Step descriptions this depends on". `approve_plan` then passed
8
+ * `dependsOn: []` for every step, so the ordering it declared was discarded
9
+ * at the only place it entered the system.
10
+ *
11
+ * The cost is not scheduling — the dependency gate in `PlanManager` has no
12
+ * callers — it is the approval. `dependsOn` is serialized into the
13
+ * `plan_approval` payload a human reads before saying yes, so the reviewer
14
+ * saw a plan whose steps all looked independent however carefully the model
15
+ * had ordered them.
16
+ */
17
+
18
+ const id = (index: number) => `step_${index + 1}`
19
+
20
+ describe('what the model described becomes what the plan holds', () => {
21
+ it('resolves a description to the step that carries it', () => {
22
+ const result = resolvePlanDependencies(
23
+ [
24
+ { description: 'Read the config' },
25
+ { description: 'Write the report', depends_on: ['Read the config'] },
26
+ ],
27
+ id,
28
+ )
29
+
30
+ expect(result).toEqual({ ok: true, dependsOn: [[], ['step_1']] })
31
+ })
32
+
33
+ it('resolves several dependencies on one step', () => {
34
+ const result = resolvePlanDependencies(
35
+ [
36
+ { description: 'Read the config' },
37
+ { description: 'Read the code' },
38
+ { description: 'Write the report', depends_on: ['Read the config', 'Read the code'] },
39
+ ],
40
+ id,
41
+ )
42
+
43
+ expect(result.ok && result.dependsOn[2]).toEqual(['step_1', 'step_2'])
44
+ })
45
+
46
+ it('resolves a forward dependency, since order of declaration is not order of execution', () => {
47
+ const result = resolvePlanDependencies(
48
+ [
49
+ { description: 'Write the report', depends_on: ['Read the config'] },
50
+ { description: 'Read the config' },
51
+ ],
52
+ id,
53
+ )
54
+
55
+ expect(result.ok && result.dependsOn[0]).toEqual(['step_2'])
56
+ })
57
+
58
+ it('leaves a step with no dependencies empty', () => {
59
+ const result = resolvePlanDependencies([{ description: 'Read the config' }], id)
60
+
61
+ expect(result).toEqual({ ok: true, dependsOn: [[]] })
62
+ })
63
+
64
+ it('forgives whitespace and casing a model did not keep identical', () => {
65
+ const result = resolvePlanDependencies(
66
+ [
67
+ { description: 'Read the config' },
68
+ { description: 'Write', depends_on: ['read the CONFIG '] },
69
+ ],
70
+ id,
71
+ )
72
+
73
+ // Rejecting this would teach the model nothing, for a plan that was
74
+ // right.
75
+ expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
76
+ })
77
+
78
+ it('collapses the same dependency named twice', () => {
79
+ const result = resolvePlanDependencies(
80
+ [{ description: 'Read' }, { description: 'Write', depends_on: ['Read', 'Read'] }],
81
+ id,
82
+ )
83
+
84
+ expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
85
+ })
86
+ })
87
+
88
+ describe('a dependency that cannot mean anything is refused, not dropped', () => {
89
+ it('refuses a dependency naming no step', () => {
90
+ const result = resolvePlanDependencies(
91
+ [{ description: 'Write', depends_on: ['Read the config'] }],
92
+ id,
93
+ )
94
+
95
+ expect(result.ok).toBe(false)
96
+ // The model has to be able to act on it, so the offending text is named.
97
+ expect(!result.ok && result.error).toContain('Read the config')
98
+ })
99
+
100
+ it('refuses a dependency two steps could answer', () => {
101
+ const result = resolvePlanDependencies(
102
+ [
103
+ { description: 'Review' },
104
+ { description: 'Review' },
105
+ { description: 'Report', depends_on: ['Review'] },
106
+ ],
107
+ id,
108
+ )
109
+
110
+ // Picking either is a coin flip whose result a human then approves as
111
+ // if it were the model's intent.
112
+ expect(result.ok).toBe(false)
113
+ expect(!result.ok && result.error).toContain('2 steps share that description')
114
+ })
115
+
116
+ it('refuses a step that depends on itself', () => {
117
+ const result = resolvePlanDependencies([{ description: 'Loop', depends_on: ['Loop'] }], id)
118
+
119
+ expect(result.ok).toBe(false)
120
+ expect(!result.ok && result.error).toContain('depends on itself')
121
+ })
122
+
123
+ it('refuses a two-step cycle', () => {
124
+ const result = resolvePlanDependencies(
125
+ [
126
+ { description: 'A', depends_on: ['B'] },
127
+ { description: 'B', depends_on: ['A'] },
128
+ ],
129
+ id,
130
+ )
131
+
132
+ expect(result.ok).toBe(false)
133
+ expect(!result.ok && result.error).toContain('loop')
134
+ })
135
+
136
+ it('refuses a cycle several steps long', () => {
137
+ const result = resolvePlanDependencies(
138
+ [
139
+ { description: 'A', depends_on: ['C'] },
140
+ { description: 'B', depends_on: ['A'] },
141
+ { description: 'C', depends_on: ['B'] },
142
+ ],
143
+ id,
144
+ )
145
+
146
+ // This is the failure worth catching hardest: no step in a loop can
147
+ // start, so the plan does not error — it simply stops.
148
+ expect(result.ok).toBe(false)
149
+ expect(!result.ok && result.error).toContain('loop')
150
+ })
151
+
152
+ it('names every step in the loop so it can be broken', () => {
153
+ const result = resolvePlanDependencies(
154
+ [
155
+ { description: 'Alpha', depends_on: ['Gamma'] },
156
+ { description: 'Beta' },
157
+ { description: 'Gamma', depends_on: ['Alpha'] },
158
+ ],
159
+ id,
160
+ )
161
+
162
+ expect(result.ok).toBe(false)
163
+ if (result.ok) return
164
+ expect(result.error).toContain('Alpha')
165
+ expect(result.error).toContain('Gamma')
166
+ // Beta is not in the loop and must not be blamed for it.
167
+ expect(result.error).not.toContain('Beta')
168
+ })
169
+
170
+ it('accepts a diamond, which is not a cycle', () => {
171
+ const result = resolvePlanDependencies(
172
+ [
173
+ { description: 'Start' },
174
+ { description: 'Left', depends_on: ['Start'] },
175
+ { description: 'Right', depends_on: ['Start'] },
176
+ { description: 'Join', depends_on: ['Left', 'Right'] },
177
+ ],
178
+ id,
179
+ )
180
+
181
+ // Two paths reaching one step is an ordinary plan shape, and a naive
182
+ // visited-set cycle check calls it a loop.
183
+ expect(result.ok).toBe(true)
184
+ expect(result.ok && result.dependsOn[3]).toEqual(['step_2', 'step_3'])
185
+ })
186
+ })
@@ -8,6 +8,7 @@ import type { RunId, TaskId } from '../../types/ids/index.js'
8
8
  import type { TaskStore } from '../../types/task/index.js'
9
9
  import type { ToolDefinition } from '../../types/tool/index.js'
10
10
  import { defineTool } from '../defineTool.js'
11
+ import { resolvePlanDependencies } from './plan-dependencies.js'
11
12
 
12
13
  export type TaskLaunchedCallback = (
13
14
  agentTaskId: TaskId,
@@ -78,6 +79,59 @@ const approvePlanStepSchema = z.object({
78
79
  depends_on: z.array(z.string()).optional().describe('Step descriptions this depends on'),
79
80
  })
80
81
 
82
+ /**
83
+ * The single closed shape a capable provider constrains this call to.
84
+ *
85
+ * `options` arriving as a STRING is the failure this exists for: a model that
86
+ * serializes the array once tends to keep doing it, and the parse error it
87
+ * gets back never says the array was the problem.
88
+ * `additionalProperties: false` turns that into a refusal at generation time
89
+ * rather than a rejection after the fact.
90
+ */
91
+ const askUserQuestionModelInputSchema: Record<string, unknown> = {
92
+ type: 'object',
93
+ properties: {
94
+ question: {
95
+ type: 'string',
96
+ description: 'Full question text — clear, specific, and ending with a question mark.',
97
+ },
98
+ header: {
99
+ type: 'string',
100
+ description: 'Optional very short topic label, no more than 24 characters.',
101
+ },
102
+ options: {
103
+ type: 'array',
104
+ description: 'A JSON array of 2-4 genuinely distinct, context-derived option objects.',
105
+ items: {
106
+ type: 'object',
107
+ properties: {
108
+ label: {
109
+ type: 'string',
110
+ description:
111
+ 'Concise option label. Put the recommended option first and append " (Recommended)".',
112
+ },
113
+ description: {
114
+ type: 'string',
115
+ description: 'Optional one-line explanation of what changes if selected.',
116
+ },
117
+ },
118
+ required: ['label'],
119
+ additionalProperties: false,
120
+ },
121
+ },
122
+ multiSelect: {
123
+ type: 'boolean',
124
+ description: 'True only when several options can apply at once.',
125
+ },
126
+ allowFreeText: {
127
+ type: 'boolean',
128
+ description: 'Whether the user may answer in their own words.',
129
+ },
130
+ },
131
+ required: ['question', 'options'],
132
+ additionalProperties: false,
133
+ }
134
+
81
135
  function normalizeApprovePlanSteps(value: unknown): unknown {
82
136
  if (typeof value !== 'string') return value
83
137
 
@@ -384,6 +438,15 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
384
438
  }
385
439
  }
386
440
 
441
+ // Resolve BEFORE touching the plan manager. A refusal here has to
442
+ // leave no half-built plan behind: `startGenerating` replaces the
443
+ // current plan, so failing after it would discard a plan that was
444
+ // fine in favour of one that never completes.
445
+ const dependencies = resolvePlanDependencies(steps, (index) => `step_${index + 1}`)
446
+ if (!dependencies.ok) {
447
+ return { success: false, output: '', error: dependencies.error }
448
+ }
449
+
387
450
  pm.startGenerating(title)
388
451
  for (let i = 0; i < steps.length; i++) {
389
452
  const step = steps[i]
@@ -392,7 +455,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
392
455
  id: `step_${i + 1}`,
393
456
  description: step.description,
394
457
  toolName: step.agent_id ? 'create_task' : undefined,
395
- dependsOn: [],
458
+ // Was `[]` unconditionally, which dropped every ordering
459
+ // constraint the model was invited to express — and put an
460
+ // empty dependency list in front of the human approving it.
461
+ dependsOn: [...(dependencies.dependsOn[i] ?? [])],
396
462
  order: i + 1,
397
463
  })
398
464
  }
@@ -437,47 +503,55 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
437
503
  name: 'ask_user_question',
438
504
  description:
439
505
  'Ask the user ONE question ONLY when you are blocked on a decision that is genuinely theirs to make — one you cannot resolve from their request, your tools, the files you can read, or sensible defaults. The question must be the genuinely undecidable thing in THIS task. Never ask for information a tool can discover (do not ask what you can read, list, or search), never re-ask what the conversation already answers, and never ask meta-questions like "Shall I proceed?" — plan ratification goes through approve_plan. Provide 2-4 genuinely distinct options derived from the actual context — concrete paths, never generic placeholders (for example, asked to prepare a presentation, ask "Who is the audience?" with options like Board / Engineering team / Customer); keep labels short (1-5 words) and give each option a one-line description of what practically changes if it is chosen. Put your recommended option FIRST and append " (Recommended)" to its label. Set multiSelect: true only when several options can apply at once. A free-text "Something else" escape hatch is always shown automatically — do not add your own "Other" option. Ask ONE question per call and prefer at most one question per assistant turn; if several decisions block you, ask only the ones that materially change your next actions, in sequence — most work needs at most 2-3 questions, so prefer proceeding on stated defaults over interrogating the user. Never invent answers or synthetic content on the user\'s behalf unless they explicitly asked for a random/test scenario. The answer arrives as this tool\'s result; if the result says the user did not answer, do not ask this or any other question again — proceed on your best judgment without assuming consent.',
440
- inputSchema: z.object({
441
- question: z
442
- .string()
443
- .min(1)
444
- .describe('Full question text — clear, specific, ends with a question mark.'),
445
- header: z
446
- .string()
447
- .max(24)
448
- .optional()
449
- .describe('Very short topic label for the question (e.g. "Audience", "Auth method").'),
450
- options: z
451
- .array(
452
- z.object({
453
- label: z
454
- .string()
455
- .min(1)
456
- .max(80)
457
- .describe(
458
- 'Concise option label (1-5 words). Recommended option goes first with " (Recommended)" appended.',
459
- ),
460
- description: z
461
- .string()
462
- .max(300)
463
- .optional()
464
- .describe('One line on what practically changes if this option is chosen.'),
465
- }),
466
- )
467
- .min(2)
468
- .max(4)
469
- .describe('2-4 genuinely distinct, context-derived options.'),
470
- multiSelect: z
471
- .boolean()
472
- .optional()
473
- .default(false)
474
- .describe('True only when several options can apply at once.'),
475
- allowFreeText: z
476
- .boolean()
477
- .optional()
478
- .default(true)
479
- .describe('Whether the user may answer in their own words.'),
480
- }),
506
+ inputSchema: z
507
+ .object({
508
+ question: z
509
+ .string()
510
+ .min(1)
511
+ .describe('Full question text — clear, specific, ends with a question mark.'),
512
+ header: z
513
+ .string()
514
+ .max(24)
515
+ .optional()
516
+ .describe('Very short topic label for the question (e.g. "Audience", "Auth method").'),
517
+ options: z
518
+ .array(
519
+ z
520
+ .object({
521
+ label: z
522
+ .string()
523
+ .min(1)
524
+ .max(80)
525
+ .describe(
526
+ 'Concise option label (1-5 words). Recommended option goes first with " (Recommended)" appended.',
527
+ ),
528
+ description: z
529
+ .string()
530
+ .max(300)
531
+ .optional()
532
+ .describe('One line on what practically changes if this option is chosen.'),
533
+ })
534
+ .strict(),
535
+ )
536
+ .min(2)
537
+ .max(4)
538
+ .describe('2-4 genuinely distinct, context-derived options.'),
539
+ multiSelect: z
540
+ .boolean()
541
+ .optional()
542
+ .default(false)
543
+ .describe('True only when several options can apply at once.'),
544
+ allowFreeText: z
545
+ .boolean()
546
+ .optional()
547
+ .default(true)
548
+ .describe('Whether the user may answer in their own words.'),
549
+ })
550
+ .strict(),
551
+ modelInputSchema: structuredClone(askUserQuestionModelInputSchema),
552
+ enforceModelInput: true,
553
+ validationErrorHint:
554
+ 'Required shape: {"question":"...?","options":[{"label":"First (Recommended)","description":"What changes"},{"label":"Second","description":"What changes"}]}. "options" must be a JSON array of 2-4 objects, never a string.',
481
555
  category: 'custom',
482
556
  permissions: [],
483
557
  readOnly: true,