@owlmeans/viable-sdk 0.1.18-rc.8 → 0.1.18

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 (498) hide show
  1. package/README.md +29 -11
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/viable-sdk/SKILL.md +1033 -0
  4. package/build/api/collect/types.d.ts +30 -0
  5. package/build/api/collect/types.d.ts.map +1 -0
  6. package/build/api/collect/types.js +2 -0
  7. package/build/api/collect/types.js.map +1 -0
  8. package/build/api/collect.d.ts +14 -0
  9. package/build/api/collect.d.ts.map +1 -0
  10. package/build/api/collect.js +132 -0
  11. package/build/api/collect.js.map +1 -0
  12. package/build/api/consts.local.d.ts +21 -0
  13. package/build/api/consts.local.d.ts.map +1 -0
  14. package/build/api/consts.local.js +25 -0
  15. package/build/api/consts.local.js.map +1 -0
  16. package/build/api/index.d.ts +5 -0
  17. package/build/api/index.d.ts.map +1 -1
  18. package/build/api/index.js +2 -0
  19. package/build/api/index.js.map +1 -1
  20. package/build/api/remote.d.ts +13 -6
  21. package/build/api/remote.d.ts.map +1 -1
  22. package/build/api/remote.js +302 -35
  23. package/build/api/remote.js.map +1 -1
  24. package/build/api/transport/types.d.ts +14 -0
  25. package/build/api/transport/types.d.ts.map +1 -0
  26. package/build/api/transport/types.js +2 -0
  27. package/build/api/transport/types.js.map +1 -0
  28. package/build/api/transport.d.ts +4 -0
  29. package/build/api/transport.d.ts.map +1 -0
  30. package/build/api/transport.js +32 -0
  31. package/build/api/transport.js.map +1 -0
  32. package/build/api/types.d.ts +9 -0
  33. package/build/api/types.d.ts.map +1 -0
  34. package/build/api/types.js +2 -0
  35. package/build/api/types.js.map +1 -0
  36. package/build/api/types.local.d.ts +3 -0
  37. package/build/api/types.local.d.ts.map +1 -0
  38. package/build/api/types.local.js +2 -0
  39. package/build/api/types.local.js.map +1 -0
  40. package/build/consts.d.ts +29 -14
  41. package/build/consts.d.ts.map +1 -1
  42. package/build/consts.js +30 -14
  43. package/build/consts.js.map +1 -1
  44. package/build/context/consts.local.d.ts +18 -0
  45. package/build/context/consts.local.d.ts.map +1 -0
  46. package/build/context/consts.local.js +18 -0
  47. package/build/context/consts.local.js.map +1 -0
  48. package/build/context/index.d.ts +3 -7
  49. package/build/context/index.d.ts.map +1 -1
  50. package/build/context/index.js +58 -25
  51. package/build/context/index.js.map +1 -1
  52. package/build/context/types.d.ts +26 -0
  53. package/build/context/types.d.ts.map +1 -0
  54. package/build/context/types.js +2 -0
  55. package/build/context/types.js.map +1 -0
  56. package/build/executor/boot-check.d.ts +1 -40
  57. package/build/executor/boot-check.d.ts.map +1 -1
  58. package/build/executor/boot-check.js +22 -19
  59. package/build/executor/boot-check.js.map +1 -1
  60. package/build/executor/consts.d.ts +55 -0
  61. package/build/executor/consts.d.ts.map +1 -0
  62. package/build/executor/consts.js +66 -0
  63. package/build/executor/consts.js.map +1 -0
  64. package/build/executor/consts.local.d.ts +63 -0
  65. package/build/executor/consts.local.d.ts.map +1 -0
  66. package/build/executor/consts.local.js +69 -0
  67. package/build/executor/consts.local.js.map +1 -0
  68. package/build/executor/env/types.d.ts +40 -0
  69. package/build/executor/env/types.d.ts.map +1 -0
  70. package/build/executor/env/types.js +2 -0
  71. package/build/executor/env/types.js.map +1 -0
  72. package/build/executor/env.d.ts +11 -20
  73. package/build/executor/env.d.ts.map +1 -1
  74. package/build/executor/env.js +39 -59
  75. package/build/executor/env.js.map +1 -1
  76. package/build/executor/errors.d.ts +15 -0
  77. package/build/executor/errors.d.ts.map +1 -1
  78. package/build/executor/errors.js +19 -2
  79. package/build/executor/errors.js.map +1 -1
  80. package/build/executor/files/types.d.ts +106 -0
  81. package/build/executor/files/types.d.ts.map +1 -0
  82. package/build/executor/files/types.js +2 -0
  83. package/build/executor/files/types.js.map +1 -0
  84. package/build/executor/files.d.ts +2 -50
  85. package/build/executor/files.d.ts.map +1 -1
  86. package/build/executor/files.js +225 -68
  87. package/build/executor/files.js.map +1 -1
  88. package/build/executor/git/types.d.ts +19 -0
  89. package/build/executor/git/types.d.ts.map +1 -0
  90. package/build/executor/git/types.js +2 -0
  91. package/build/executor/git/types.js.map +1 -0
  92. package/build/executor/git.d.ts +4 -36
  93. package/build/executor/git.d.ts.map +1 -1
  94. package/build/executor/git.js +206 -241
  95. package/build/executor/git.js.map +1 -1
  96. package/build/executor/glob/types.d.ts +12 -0
  97. package/build/executor/glob/types.d.ts.map +1 -0
  98. package/build/executor/glob/types.js +2 -0
  99. package/build/executor/glob/types.js.map +1 -0
  100. package/build/executor/glob.d.ts +4 -0
  101. package/build/executor/glob.d.ts.map +1 -0
  102. package/build/executor/glob.js +11 -0
  103. package/build/executor/glob.js.map +1 -0
  104. package/build/executor/health/types.d.ts +39 -0
  105. package/build/executor/health/types.d.ts.map +1 -0
  106. package/build/executor/health/types.js +2 -0
  107. package/build/executor/health/types.js.map +1 -0
  108. package/build/executor/health.d.ts +3 -83
  109. package/build/executor/health.d.ts.map +1 -1
  110. package/build/executor/health.js +76 -91
  111. package/build/executor/health.js.map +1 -1
  112. package/build/executor/index.d.ts +14 -15
  113. package/build/executor/index.d.ts.map +1 -1
  114. package/build/executor/index.js +36 -18
  115. package/build/executor/index.js.map +1 -1
  116. package/build/executor/integrity/types.d.ts +25 -0
  117. package/build/executor/integrity/types.d.ts.map +1 -0
  118. package/build/executor/integrity/types.js +2 -0
  119. package/build/executor/integrity/types.js.map +1 -0
  120. package/build/executor/integrity.d.ts +5 -15
  121. package/build/executor/integrity.d.ts.map +1 -1
  122. package/build/executor/integrity.js +28 -39
  123. package/build/executor/integrity.js.map +1 -1
  124. package/build/executor/layout/types.d.ts +69 -0
  125. package/build/executor/layout/types.d.ts.map +1 -0
  126. package/build/executor/layout/types.js +2 -0
  127. package/build/executor/layout/types.js.map +1 -0
  128. package/build/executor/layout.d.ts +9 -56
  129. package/build/executor/layout.d.ts.map +1 -1
  130. package/build/executor/layout.js +35 -82
  131. package/build/executor/layout.js.map +1 -1
  132. package/build/executor/shell/types.d.ts +76 -0
  133. package/build/executor/shell/types.d.ts.map +1 -0
  134. package/build/executor/shell/types.js +2 -0
  135. package/build/executor/shell/types.js.map +1 -0
  136. package/build/executor/shell.d.ts +3 -78
  137. package/build/executor/shell.d.ts.map +1 -1
  138. package/build/executor/shell.js +61 -91
  139. package/build/executor/shell.js.map +1 -1
  140. package/build/executor/spawn/types.d.ts +98 -0
  141. package/build/executor/spawn/types.d.ts.map +1 -0
  142. package/build/executor/spawn/types.js +2 -0
  143. package/build/executor/spawn/types.js.map +1 -0
  144. package/build/executor/spawn.d.ts +3 -103
  145. package/build/executor/spawn.d.ts.map +1 -1
  146. package/build/executor/spawn.js +192 -274
  147. package/build/executor/spawn.js.map +1 -1
  148. package/build/executor/types.d.ts +123 -0
  149. package/build/executor/types.d.ts.map +1 -0
  150. package/build/executor/types.js +2 -0
  151. package/build/executor/types.js.map +1 -0
  152. package/build/executor/types.local.d.ts +6 -0
  153. package/build/executor/types.local.d.ts.map +1 -0
  154. package/build/executor/types.local.js +2 -0
  155. package/build/executor/types.local.js.map +1 -0
  156. package/build/index.d.ts +14 -1
  157. package/build/index.d.ts.map +1 -1
  158. package/build/index.js +5 -1
  159. package/build/index.js.map +1 -1
  160. package/build/project/consts.d.ts +17 -0
  161. package/build/project/consts.d.ts.map +1 -0
  162. package/build/project/consts.js +17 -0
  163. package/build/project/consts.js.map +1 -0
  164. package/build/project/consts.local.d.ts +4 -0
  165. package/build/project/consts.local.d.ts.map +1 -0
  166. package/build/project/consts.local.js +4 -0
  167. package/build/project/consts.local.js.map +1 -0
  168. package/build/project/dotenv/types.d.ts +39 -0
  169. package/build/project/dotenv/types.d.ts.map +1 -0
  170. package/build/project/dotenv/types.js +2 -0
  171. package/build/project/dotenv/types.js.map +1 -0
  172. package/build/project/dotenv.d.ts +4 -0
  173. package/build/project/dotenv.d.ts.map +1 -0
  174. package/build/project/dotenv.js +56 -0
  175. package/build/project/dotenv.js.map +1 -0
  176. package/build/project/env/types.d.ts +40 -0
  177. package/build/project/env/types.d.ts.map +1 -0
  178. package/build/project/env/types.js +2 -0
  179. package/build/project/env/types.js.map +1 -0
  180. package/build/project/env.d.ts +2 -98
  181. package/build/project/env.d.ts.map +1 -1
  182. package/build/project/env.js +63 -215
  183. package/build/project/env.js.map +1 -1
  184. package/build/project/marker/types.d.ts +33 -0
  185. package/build/project/marker/types.d.ts.map +1 -0
  186. package/build/project/marker/types.js +2 -0
  187. package/build/project/marker/types.js.map +1 -0
  188. package/build/project/marker.d.ts +4 -25
  189. package/build/project/marker.d.ts.map +1 -1
  190. package/build/project/marker.js +40 -43
  191. package/build/project/marker.js.map +1 -1
  192. package/build/project/probe/types.d.ts +22 -0
  193. package/build/project/probe/types.d.ts.map +1 -0
  194. package/build/project/probe/types.js +2 -0
  195. package/build/project/probe/types.js.map +1 -0
  196. package/build/project/probe.d.ts +4 -0
  197. package/build/project/probe.d.ts.map +1 -0
  198. package/build/project/probe.js +39 -0
  199. package/build/project/probe.js.map +1 -0
  200. package/build/project/report/types.d.ts +18 -0
  201. package/build/project/report/types.d.ts.map +1 -0
  202. package/build/project/report/types.js +2 -0
  203. package/build/project/report/types.js.map +1 -0
  204. package/build/project/report.d.ts +7 -0
  205. package/build/project/report.d.ts.map +1 -0
  206. package/build/project/report.js +68 -0
  207. package/build/project/report.js.map +1 -0
  208. package/build/project/setup/types.d.ts +32 -0
  209. package/build/project/setup/types.d.ts.map +1 -0
  210. package/build/project/setup/types.js +2 -0
  211. package/build/project/setup/types.js.map +1 -0
  212. package/build/project/setup.d.ts +4 -53
  213. package/build/project/setup.d.ts.map +1 -1
  214. package/build/project/setup.js +112 -175
  215. package/build/project/setup.js.map +1 -1
  216. package/build/project/types.d.ts +37 -0
  217. package/build/project/types.d.ts.map +1 -0
  218. package/build/project/types.js +2 -0
  219. package/build/project/types.js.map +1 -0
  220. package/build/run/consts.local.d.ts +3 -0
  221. package/build/run/consts.local.d.ts.map +1 -0
  222. package/build/run/consts.local.js +26 -0
  223. package/build/run/consts.local.js.map +1 -0
  224. package/build/run/index.d.ts +11 -69
  225. package/build/run/index.d.ts.map +1 -1
  226. package/build/run/index.js +9 -147
  227. package/build/run/index.js.map +1 -1
  228. package/build/run/local/types.d.ts +39 -0
  229. package/build/run/local/types.d.ts.map +1 -0
  230. package/build/run/local/types.js +2 -0
  231. package/build/run/local/types.js.map +1 -0
  232. package/build/run/local.d.ts +3 -0
  233. package/build/run/local.d.ts.map +1 -0
  234. package/build/run/local.js +130 -0
  235. package/build/run/local.js.map +1 -0
  236. package/build/run/process/types.d.ts +15 -0
  237. package/build/run/process/types.d.ts.map +1 -0
  238. package/build/run/process/types.js +2 -0
  239. package/build/run/process/types.js.map +1 -0
  240. package/build/run/process.d.ts +4 -0
  241. package/build/run/process.d.ts.map +1 -0
  242. package/build/run/process.js +20 -0
  243. package/build/run/process.js.map +1 -0
  244. package/build/run/serve.d.ts +14 -9
  245. package/build/run/serve.d.ts.map +1 -1
  246. package/build/run/serve.js +1 -25
  247. package/build/run/serve.js.map +1 -1
  248. package/build/run/state/types.d.ts +35 -0
  249. package/build/run/state/types.d.ts.map +1 -0
  250. package/build/run/state/types.js +2 -0
  251. package/build/run/state/types.js.map +1 -0
  252. package/build/run/state.d.ts +3 -62
  253. package/build/run/state.d.ts.map +1 -1
  254. package/build/run/state.js +96 -123
  255. package/build/run/state.js.map +1 -1
  256. package/build/run/types.d.ts +78 -0
  257. package/build/run/types.d.ts.map +1 -0
  258. package/build/run/types.js +2 -0
  259. package/build/run/types.js.map +1 -0
  260. package/build/session/consts.local.d.ts +5 -0
  261. package/build/session/consts.local.d.ts.map +1 -0
  262. package/build/session/consts.local.js +5 -0
  263. package/build/session/consts.local.js.map +1 -0
  264. package/build/session/index.d.ts +5 -10
  265. package/build/session/index.d.ts.map +1 -1
  266. package/build/session/index.js +84 -15
  267. package/build/session/index.js.map +1 -1
  268. package/build/session/questions.d.ts +16 -0
  269. package/build/session/questions.d.ts.map +1 -0
  270. package/build/session/questions.js +17 -0
  271. package/build/session/questions.js.map +1 -0
  272. package/build/session/queue.d.ts +61 -0
  273. package/build/session/queue.d.ts.map +1 -0
  274. package/build/session/queue.js +156 -0
  275. package/build/session/queue.js.map +1 -0
  276. package/build/session/tasks.d.ts +11 -30
  277. package/build/session/tasks.d.ts.map +1 -1
  278. package/build/session/tasks.js +18 -78
  279. package/build/session/tasks.js.map +1 -1
  280. package/build/session/types.d.ts +20 -0
  281. package/build/session/types.d.ts.map +1 -0
  282. package/build/session/types.js +2 -0
  283. package/build/session/types.js.map +1 -0
  284. package/build/task/consts.d.ts +12 -0
  285. package/build/task/consts.d.ts.map +1 -0
  286. package/build/task/consts.js +12 -0
  287. package/build/task/consts.js.map +1 -0
  288. package/build/task/consts.local.d.ts +19 -0
  289. package/build/task/consts.local.d.ts.map +1 -0
  290. package/build/task/consts.local.js +72 -0
  291. package/build/task/consts.local.js.map +1 -0
  292. package/build/task/driver.d.ts +1 -28
  293. package/build/task/driver.d.ts.map +1 -1
  294. package/build/task/driver.js +2 -9
  295. package/build/task/driver.js.map +1 -1
  296. package/build/task/envelope/types.d.ts +29 -0
  297. package/build/task/envelope/types.d.ts.map +1 -0
  298. package/build/task/envelope/types.js +2 -0
  299. package/build/task/envelope/types.js.map +1 -0
  300. package/build/task/envelope.d.ts +4 -34
  301. package/build/task/envelope.d.ts.map +1 -1
  302. package/build/task/envelope.js +103 -165
  303. package/build/task/envelope.js.map +1 -1
  304. package/build/task/index.d.ts +5 -0
  305. package/build/task/index.d.ts.map +1 -1
  306. package/build/task/index.js +2 -0
  307. package/build/task/index.js.map +1 -1
  308. package/build/task/inquiry/types.d.ts +38 -0
  309. package/build/task/inquiry/types.d.ts.map +1 -0
  310. package/build/task/inquiry/types.js +2 -0
  311. package/build/task/inquiry/types.js.map +1 -0
  312. package/build/task/inquiry.d.ts +5 -0
  313. package/build/task/inquiry.d.ts.map +1 -0
  314. package/build/task/inquiry.js +188 -0
  315. package/build/task/inquiry.js.map +1 -0
  316. package/build/task/types.d.ts +48 -0
  317. package/build/task/types.d.ts.map +1 -0
  318. package/build/task/types.js +2 -0
  319. package/build/task/types.js.map +1 -0
  320. package/build/tools/account/types.d.ts +24 -0
  321. package/build/tools/account/types.d.ts.map +1 -0
  322. package/build/tools/account/types.js +2 -0
  323. package/build/tools/account/types.js.map +1 -0
  324. package/build/tools/account.d.ts +4 -0
  325. package/build/tools/account.d.ts.map +1 -0
  326. package/build/tools/account.js +72 -0
  327. package/build/tools/account.js.map +1 -0
  328. package/build/tools/blueprints/types.d.ts +5 -0
  329. package/build/tools/blueprints/types.d.ts.map +1 -0
  330. package/build/tools/blueprints/types.js +2 -0
  331. package/build/tools/blueprints/types.js.map +1 -0
  332. package/build/tools/blueprints.d.ts +4 -0
  333. package/build/tools/blueprints.d.ts.map +1 -0
  334. package/build/tools/blueprints.js +23 -0
  335. package/build/tools/blueprints.js.map +1 -0
  336. package/build/tools/catalogue/types.d.ts +8 -0
  337. package/build/tools/catalogue/types.d.ts.map +1 -0
  338. package/build/tools/catalogue/types.js +2 -0
  339. package/build/tools/catalogue/types.js.map +1 -0
  340. package/build/tools/catalogue.d.ts +8 -6
  341. package/build/tools/catalogue.d.ts.map +1 -1
  342. package/build/tools/catalogue.js +2565 -213
  343. package/build/tools/catalogue.js.map +1 -1
  344. package/build/tools/config/types.d.ts +19 -0
  345. package/build/tools/config/types.d.ts.map +1 -0
  346. package/build/tools/config/types.js +2 -0
  347. package/build/tools/config/types.js.map +1 -0
  348. package/build/tools/config.d.ts +4 -0
  349. package/build/tools/config.d.ts.map +1 -0
  350. package/build/tools/config.js +42 -0
  351. package/build/tools/config.js.map +1 -0
  352. package/build/tools/consts.d.ts +61 -0
  353. package/build/tools/consts.d.ts.map +1 -0
  354. package/build/tools/consts.js +515 -0
  355. package/build/tools/consts.js.map +1 -0
  356. package/build/tools/consts.local.d.ts +131 -0
  357. package/build/tools/consts.local.d.ts.map +1 -0
  358. package/build/tools/consts.local.js +236 -0
  359. package/build/tools/consts.local.js.map +1 -0
  360. package/build/tools/control/types.d.ts +17 -0
  361. package/build/tools/control/types.d.ts.map +1 -0
  362. package/build/tools/control/types.js +2 -0
  363. package/build/tools/control/types.js.map +1 -0
  364. package/build/tools/control.d.ts +4 -0
  365. package/build/tools/control.d.ts.map +1 -0
  366. package/build/tools/control.js +44 -0
  367. package/build/tools/control.js.map +1 -0
  368. package/build/tools/feed/types.d.ts +21 -0
  369. package/build/tools/feed/types.d.ts.map +1 -0
  370. package/build/tools/feed/types.js +2 -0
  371. package/build/tools/feed/types.js.map +1 -0
  372. package/build/tools/feed.d.ts +4 -0
  373. package/build/tools/feed.d.ts.map +1 -0
  374. package/build/tools/feed.js +75 -0
  375. package/build/tools/feed.js.map +1 -0
  376. package/build/tools/git/types.d.ts +25 -0
  377. package/build/tools/git/types.d.ts.map +1 -0
  378. package/build/tools/git/types.js +2 -0
  379. package/build/tools/git/types.js.map +1 -0
  380. package/build/tools/git.d.ts +4 -0
  381. package/build/tools/git.d.ts.map +1 -0
  382. package/build/tools/git.js +83 -0
  383. package/build/tools/git.js.map +1 -0
  384. package/build/tools/handover/types.d.ts +49 -0
  385. package/build/tools/handover/types.d.ts.map +1 -0
  386. package/build/tools/handover/types.js +2 -0
  387. package/build/tools/handover/types.js.map +1 -0
  388. package/build/tools/handover.d.ts +8 -0
  389. package/build/tools/handover.d.ts.map +1 -0
  390. package/build/tools/handover.js +138 -0
  391. package/build/tools/handover.js.map +1 -0
  392. package/build/tools/host/types.d.ts +34 -0
  393. package/build/tools/host/types.d.ts.map +1 -0
  394. package/build/tools/host/types.js +2 -0
  395. package/build/tools/host/types.js.map +1 -0
  396. package/build/tools/host.d.ts +4 -0
  397. package/build/tools/host.d.ts.map +1 -0
  398. package/build/tools/host.js +13 -0
  399. package/build/tools/host.js.map +1 -0
  400. package/build/tools/iam/types.d.ts +44 -0
  401. package/build/tools/iam/types.d.ts.map +1 -0
  402. package/build/tools/iam/types.js +2 -0
  403. package/build/tools/iam/types.js.map +1 -0
  404. package/build/tools/iam.d.ts +4 -0
  405. package/build/tools/iam.d.ts.map +1 -0
  406. package/build/tools/iam.js +229 -0
  407. package/build/tools/iam.js.map +1 -0
  408. package/build/tools/index.d.ts +27 -1
  409. package/build/tools/index.d.ts.map +1 -1
  410. package/build/tools/index.js +14 -1
  411. package/build/tools/index.js.map +1 -1
  412. package/build/tools/inflight/types.d.ts +47 -0
  413. package/build/tools/inflight/types.d.ts.map +1 -0
  414. package/build/tools/inflight/types.js +2 -0
  415. package/build/tools/inflight/types.js.map +1 -0
  416. package/build/tools/inflight.d.ts +3 -0
  417. package/build/tools/inflight.d.ts.map +1 -0
  418. package/build/tools/inflight.js +88 -0
  419. package/build/tools/inflight.js.map +1 -0
  420. package/build/tools/kits/types.d.ts +12 -0
  421. package/build/tools/kits/types.d.ts.map +1 -0
  422. package/build/tools/kits/types.js +2 -0
  423. package/build/tools/kits/types.js.map +1 -0
  424. package/build/tools/kits.d.ts +4 -0
  425. package/build/tools/kits.d.ts.map +1 -0
  426. package/build/tools/kits.js +33 -0
  427. package/build/tools/kits.js.map +1 -0
  428. package/build/tools/mcp.d.ts +13 -19
  429. package/build/tools/mcp.d.ts.map +1 -1
  430. package/build/tools/mcp.js +117 -38
  431. package/build/tools/mcp.js.map +1 -1
  432. package/build/tools/platform.d.ts +14 -0
  433. package/build/tools/platform.d.ts.map +1 -0
  434. package/build/tools/platform.js +90 -0
  435. package/build/tools/platform.js.map +1 -0
  436. package/build/tools/production/types.d.ts +22 -0
  437. package/build/tools/production/types.d.ts.map +1 -0
  438. package/build/tools/production/types.js +2 -0
  439. package/build/tools/production/types.js.map +1 -0
  440. package/build/tools/production.d.ts +4 -0
  441. package/build/tools/production.d.ts.map +1 -0
  442. package/build/tools/production.js +80 -0
  443. package/build/tools/production.js.map +1 -0
  444. package/build/tools/refusal/types.d.ts +95 -0
  445. package/build/tools/refusal/types.d.ts.map +1 -0
  446. package/build/tools/refusal/types.js +2 -0
  447. package/build/tools/refusal/types.js.map +1 -0
  448. package/build/tools/refusal.d.ts +4 -0
  449. package/build/tools/refusal.d.ts.map +1 -0
  450. package/build/tools/refusal.js +663 -0
  451. package/build/tools/refusal.js.map +1 -0
  452. package/build/tools/settings/types.d.ts +42 -0
  453. package/build/tools/settings/types.d.ts.map +1 -0
  454. package/build/tools/settings/types.js +2 -0
  455. package/build/tools/settings/types.js.map +1 -0
  456. package/build/tools/settings.d.ts +4 -0
  457. package/build/tools/settings.d.ts.map +1 -0
  458. package/build/tools/settings.js +71 -0
  459. package/build/tools/settings.js.map +1 -0
  460. package/build/tools/status/types.d.ts +25 -0
  461. package/build/tools/status/types.d.ts.map +1 -0
  462. package/build/tools/status/types.js +2 -0
  463. package/build/tools/status/types.js.map +1 -0
  464. package/build/tools/status.d.ts +4 -0
  465. package/build/tools/status.d.ts.map +1 -0
  466. package/build/tools/status.js +183 -0
  467. package/build/tools/status.js.map +1 -0
  468. package/build/tools/stories/types.d.ts +48 -0
  469. package/build/tools/stories/types.d.ts.map +1 -0
  470. package/build/tools/stories/types.js +2 -0
  471. package/build/tools/stories/types.js.map +1 -0
  472. package/build/tools/stories.d.ts +8 -0
  473. package/build/tools/stories.d.ts.map +1 -0
  474. package/build/tools/stories.js +79 -0
  475. package/build/tools/stories.js.map +1 -0
  476. package/build/tools/types.d.ts +177 -26
  477. package/build/tools/types.d.ts.map +1 -1
  478. package/build/tools/types.js +1 -32
  479. package/build/tools/types.js.map +1 -1
  480. package/build/types.d.ts +391 -26
  481. package/build/types.d.ts.map +1 -1
  482. package/build/utils.d.ts +8 -0
  483. package/build/utils.d.ts.map +1 -0
  484. package/build/utils.js +12 -0
  485. package/build/utils.js.map +1 -0
  486. package/package.json +30 -34
  487. package/build/harness/index.d.ts +0 -28
  488. package/build/harness/index.d.ts.map +0 -1
  489. package/build/harness/index.js +0 -83
  490. package/build/harness/index.js.map +0 -1
  491. package/build/harness/templates.d.ts +0 -30
  492. package/build/harness/templates.d.ts.map +0 -1
  493. package/build/harness/templates.js +0 -154
  494. package/build/harness/templates.js.map +0 -1
  495. package/build/tools/jobs.d.ts +0 -12
  496. package/build/tools/jobs.d.ts.map +0 -1
  497. package/build/tools/jobs.js +0 -47
  498. package/build/tools/jobs.js.map +0 -1
@@ -1,15 +1,110 @@
1
1
  import { z } from 'zod';
2
- import { ConnectTarget, MODEL_TIER_ROLES } from '@owlmeans/viable-common';
3
- import { JOB_POLL_MAX_SEC, NEXT_TASK_WAIT_MS } from '../consts.js';
4
- import { describeHarness, installHarness } from '../harness/index.js';
5
- import { envStatus } from '../project/env.js';
6
- import { missingServices, readSetupReport, renderSetupGuide, setUserEnv } from '../project/setup.js';
7
- import { localStatus, runLocal, stopLocal } from '../run/index.js';
8
- import { parseTaskResult, renderTaskEnvelope } from '../task/envelope.js';
9
- import { renderJob } from './jobs.js';
10
- import { anyHost, cloudTarget, localTarget, performsModelTasks, sessionCapable, ToolHostKind, withExecutor } from './types.js';
2
+ import { TransitionAction, WorkcardKind } from '@owlmeans/planning';
3
+ import { AGENT_NOT_RUNNING_MARKER, AGENT_STEER_PROMPT_MAX, AgentNotRunning, AgentStopScope, STORY_BATCH_MAX, ConnectConfirmationRequired, ConnectTarget, IamDefaultClass, IamGrantMode, ConversionDecision, ConversionStatus, ConvertibilityVerdict, MetadataListKind, MODEL_TIER_ROLES, OriginKind, SpecCategory, STORY_BAND_MAX_USD, STORY_BAND_MIN_USD, VIABLE_STORY_TYPE, ViableStoryTransition } from '@owlmeans/viable-common';
4
+ import { COMMIT_WAIT_MS, HANDOVER_WAIT_MS, NEXT_QUESTION_WAIT_MS, NEXT_TASK_WAIT_MS, STORY_PAGE_SIZE } from '../consts.js';
5
+ import { makeProjectEnvHelper } from '../project/env.js';
6
+ import { makeSetupReportModel } from '../project/report.js';
7
+ import { setupHelper } from '../project/setup.js';
8
+ import { makeLocalRunHelper } from '../run/local.js';
9
+ import { makeTaskEnvelopeModel } from '../task/envelope.js';
10
+ import { makeQuestionEnvelopeModel } from '../task/inquiry.js';
11
+ import { renderPlatform } from './platform.js';
12
+ import { blueprintDescription } from './blueprints.js';
13
+ import { GENERATED_SUMMARY, PLATFORM_CATALOGUE, STORY_ORDER, ToolHostKind } from './consts.js';
14
+ import { refusalHelper } from './refusal.js';
15
+ import { kitsUtils } from './kits.js';
16
+ import { settingsHelper } from './settings.js';
17
+ import { accountHelper } from './account.js';
18
+ import { configHelper } from './config.js';
19
+ import { gitToolHelper } from './git.js';
20
+ import { productionToolHelper } from './production.js';
21
+ import { iamToolHelper } from './iam.js';
22
+ import { feedToolHelper } from './feed.js';
23
+ import { controlToolHelper } from './control.js';
24
+ import { statusTextHelper } from './status.js';
25
+ import { storyHelper } from './stories.js';
26
+ import { toolHostHelper } from './host.js';
27
+ import { makeHandoverHelper } from './handover.js';
28
+ import { APP_GRANT_ACTIONS, APP_GROUP_ACTIONS, APP_ORGANIZATION_ACTIONS, APP_USER_ACTIONS, BARE_428, CONFIG_SCOPES, CONVERSION_COST, DESTRUCTIVE, DOMAIN_ACTIONS, DRAFT_CAP, FEED_DETAILS, FILE_READ_CAP, GIT_SYNC_DIRECTIONS, IDEMPOTENT_WRITE, INFERENCE_LEVELS, INFERENCE_MODES, PREVIEW_ACTIONS, PROCEED_UPDATE_FIELDS, PRODUCTION_ACTIONS, READ_ONLY, SOURCES_KIND, STOP_SCOPES, WRITE } from './consts.local.js';
29
+ export { ToolHostKind };
11
30
  const ok = (text, structured) => ({ text, structured });
12
31
  const fail = (text) => ({ text, isError: true });
32
+ /**
33
+ * A conversion refusal is an ANSWER, so it comes back as one.
34
+ *
35
+ * Every conversion verb can be refused by a decision rather than by a failure — this origin is the
36
+ * project, that decision is not available from this stage, the balance will not cover it — and the
37
+ * refusal reaches this process as a marshalled `type|||marker|||stack` whose class is declared in
38
+ * a package the SDK does not depend on. Left to escape, the parent agent is handed that string and
39
+ * a stack trace from a machine it cannot reach; what it does with one is retry a call that can
40
+ * never succeed. {@link RefusalHelper.refusalPhrase} turns the marker into the sentence, and the result carries
41
+ * `isError` so the model still reads it as a refusal rather than as an answer.
42
+ *
43
+ * The MCP boundary phrases whatever escapes any other tool the same way; this wrapper is what puts
44
+ * the answer inside the tool, where the reason it was refused is still known.
45
+ *
46
+ * It also writes the LOG line that boundary would have written, and for the same reason: answering
47
+ * inside the tool is exactly what takes these five out of `registerCatalogue`'s catch, so without
48
+ * it a refused conversion is the one thing an operator can find nothing about in the connector log
49
+ * while every other tool is still recorded there. The marker is what is kept — that is what a
50
+ * person greps for — and the stack belongs to a machine they cannot reach.
51
+ *
52
+ * The refusals only a PERSON resolves (the balance, the spend consent, a confirmation) are phrased
53
+ * and notified exactly as `registerCatalogue` does it — a conversion's balance refusal is no
54
+ * different from any other tool's.
55
+ */
56
+ const answering = async (deps, name, run) => {
57
+ try {
58
+ return await run();
59
+ }
60
+ catch (e) {
61
+ deps.log(`${name} refused: ${refusalHelper.refusalMessage(e)}`);
62
+ const person = refusalHelper.personRefusalPhrase(e);
63
+ if (person != null) {
64
+ deps.notify?.('warning', person);
65
+ return fail(person);
66
+ }
67
+ return fail(refusalHelper.refusalPhrase(e));
68
+ }
69
+ };
70
+ /**
71
+ * The exact call that gives a conversion's confirmation: the same tool, the PROJECT named — a host
72
+ * that keeps no attachment between calls would otherwise file another conversion on the repeat —
73
+ * and `confirm: true`.
74
+ */
75
+ const confirmedCall = (tool, args) => `${tool} ${JSON.stringify({ ...args, confirm: true })}`;
76
+ /**
77
+ * A conversion verb that may stop for the person's agreement (`ConnectConfirmationRequired`): the
78
+ * refusal is answered with what to tell them and the exact call to repeat once they agree, and
79
+ * notified like every refusal only a person resolves. Sent without `confirm`, a bare 428 — all a
80
+ * production body leaves — is answered as that confirmation, the consent named beside it. Anything
81
+ * else is left to {@link answering}.
82
+ */
83
+ const confirming = async (deps, tool, args, confirmed, call) => {
84
+ try {
85
+ return conversionResult(await call());
86
+ }
87
+ catch (e) {
88
+ const retry = confirmedCall(tool, args);
89
+ const text = e instanceof ConnectConfirmationRequired
90
+ ? refusalHelper.confirmationRequiredPhrase(e, retry)
91
+ : !confirmed && refusalHelper.refusalMessage(e).includes(BARE_428) ? refusalHelper.unconfirmedConversionPhrase(retry) : null;
92
+ if (text == null)
93
+ throw e;
94
+ deps.log(`${tool} refused: ${refusalHelper.refusalMessage(e)}`);
95
+ deps.notify?.('warning', text);
96
+ return fail(text);
97
+ }
98
+ };
99
+ /**
100
+ * The person's agreement a conversion verb carries — said in its input schema, so a parent knows
101
+ * what it means before the first refusal explains it.
102
+ */
103
+ const CONFIRM_INPUT = z.boolean().default(false).describe('Set true ONLY after the user agreed to what this call said the step costs. Without it, a step'
104
+ + ' that would use the project conversion the plan includes, or spend the organization\'s credit'
105
+ + ' limits or topped-up credits, starts nothing and answers with its cost instead.');
106
+ /** What one of `proceed_conversion`'s edits is, and the one decision it travels with. */
107
+ const updateInput = (what) => `${what}, as the user edited it. Accepted only with the decision that starts the extraction.`;
13
108
  /** The project a tool acts on: the one named, or the one the connector is attached to. */
14
109
  /** The roles the platform maps onto each power class, so a caller sees what it is sizing. */
15
110
  const rolesByTier = () => {
@@ -23,13 +118,6 @@ const roleSummary = () => 'The platform asks for three power classes:\n'
23
118
  + Object.entries(rolesByTier())
24
119
  .map(([tier, roles]) => ` ${tier} → ${roles.length} roles`)
25
120
  .join('\n');
26
- /**
27
- * How much of a drafted field one status answer carries.
28
- *
29
- * Generous, because reading the specification IS the confirm step — but bounded, since a tool
30
- * answer shares one output budget with everything else the host is holding.
31
- */
32
- const DRAFT_CAP = 8_000;
33
121
  const projectOf = (args, deps) => {
34
122
  const named = typeof args.projectId === 'string' ? args.projectId : null;
35
123
  const project = named ?? deps.attached();
@@ -38,7 +126,71 @@ const projectOf = (args, deps) => {
38
126
  }
39
127
  return project;
40
128
  };
41
- const jobResult = (job) => ok(renderJob(job), { job: job });
129
+ const projectResult = (status) => ok(statusTextHelper.renderProjectStatus(status), { project: status });
130
+ /** A story's domain status, with what its card says that the status route does not carry. */
131
+ const storyResult = (status, card) => {
132
+ const landing = storyHelper.isLandingStory(card);
133
+ return ok(statusTextHelper.renderStoryStatus(status, { landing }), { story: status, ...(landing ? { landing } : {}) });
134
+ };
135
+ /** The project settings, led by what a save just did where one did. */
136
+ const settingsResult = (projectId, settings, lead, scope) => ok((lead != null ? `${lead}\n\n` : '') + settingsHelper.renderProjectSettings(projectId, settings, scope), { projectId, ...(scope != null ? { scope } : {}), settings: settings });
137
+ /** The scope a configuration or settings tool names: production's own set, or (absent) the preview's. */
138
+ const scopeOf = (args) => CONFIG_SCOPES.includes(args.scope) ? args.scope : undefined;
139
+ /** The input every configuration and settings tool takes to address production's own set. */
140
+ const SCOPE_INPUT = z.enum(CONFIG_SCOPES).optional().describe('production addresses the published site\'s own set, taken at its next Publish; leave it out for the'
141
+ + ' preview\'s set — the project\'s own, which a new production copies.');
142
+ /** The input every tool of the generated app's sign-in takes to address production's own client. */
143
+ const APP_SCOPE_INPUT = z.enum(CONFIG_SCOPES).optional().describe('production addresses the published site\'s own sign-in — its own users, permissions and groups; leave'
144
+ + ' it out for the preview\'s.');
145
+ /** One group's bundle — named permissions, or every permission a filter matches. */
146
+ const BUNDLE_INPUT = z.object({
147
+ permissions: z.array(z.string().min(1).max(128)).max(256).optional().describe('Permissions by name.'),
148
+ filter: z.object({
149
+ areas: z.array(z.string().min(1).max(64)).max(16).optional(),
150
+ managed: z.boolean().optional(),
151
+ resourceScoped: z.boolean().optional(),
152
+ entityScoped: z.boolean().optional(),
153
+ }).optional().describe('Every permission matching these flags, evaluated as the app declares more.'),
154
+ });
155
+ const pipelineResult = (status) => ok(statusTextHelper.renderPipelineStatus(status), { pipeline: status });
156
+ /** A story batch, led by what a call just did where one did. */
157
+ const batchResult = (view, lead) => ok((lead != null ? `${lead}\n\n` : '') + statusTextHelper.renderBatchStatus(view), { batch: view });
158
+ /**
159
+ * Stop the story a cancelled batch was implementing — the second half of the web's "Stop all",
160
+ * which cancels FIRST so no next story can start in between. Nothing running is no failure: the
161
+ * batch may have been between two stories. Answers whether a stop was requested.
162
+ */
163
+ const stopBatchStory = async (deps, projectId, storyId) => {
164
+ try {
165
+ await deps.api.project.stop(projectId, { scope: AgentStopScope.Story, storyId, confirm: true });
166
+ return true;
167
+ }
168
+ catch (e) {
169
+ if (e instanceof AgentNotRunning || refusalHelper.refusalMessage(e).includes(AGENT_NOT_RUNNING_MARKER)) {
170
+ deps.log(`cancel_story_batch: nothing to stop for ${storyId}: ${refusalHelper.refusalMessage(e)}`);
171
+ return false;
172
+ }
173
+ throw e;
174
+ }
175
+ };
176
+ const conversionResult = (status) => ok(renderConversion(status), { conversion: status });
177
+ /** The cursor, page size and hold a feed tool was given — only what it named crosses the wire. */
178
+ const feedQueryOf = (args) => ({
179
+ ...(typeof args.after === 'string' && args.after.trim() !== '' ? { after: args.after.trim() } : {}),
180
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
181
+ ...(typeof args.wait === 'number' ? { wait: args.wait } : {}),
182
+ });
183
+ /** The input every feed tool takes: the cursor and the hold. */
184
+ const FEED_AFTER_INPUT = z.string().max(41).optional().describe('The cursor the previous call of this tool answered — leave it out for the latest entries.');
185
+ const FEED_WAIT_INPUT = z.number().int().min(0).max(20).optional().describe('Seconds to hold for the next entry when none has come yet (at most 20); leave it out to answer at once.');
186
+ /** A file path a tool was given, or null when it was given none. */
187
+ const pathOf = (args) => typeof args.path === 'string' && args.path.trim() !== '' ? args.path.trim() : null;
188
+ /** What a write or a delete did to the preview: rebuilt, or the build's failure and what still serves. */
189
+ const rebuildNote = (buildWarning) => buildWarning != null && buildWarning !== ''
190
+ ? ` The preview rebuild FAILED, so the preview still serves the last version that built: ${refusalHelper.refusalPhrase(buildWarning)}`
191
+ : ' The preview was rebuilt.';
192
+ /** The preview workload as a control answered it. */
193
+ const slotResult = (lead, projectId, slot) => ok(`${lead}\n${statusTextHelper.renderSlot(slot)}`, { projectId, slot: slot });
42
194
  /**
43
195
  * Attach a connector, where this host can hold one.
44
196
  *
@@ -49,7 +201,7 @@ const jobResult = (job) => ok(renderJob(job), { job: job });
49
201
  * opens none and the platform performs the run itself.
50
202
  */
51
203
  const ensureSession = async (deps, projectId) => {
52
- if (!sessionCapable(deps.host))
204
+ if (!toolHostHelper.sessionCapable(deps.host))
53
205
  return;
54
206
  // The session is filed against the project it will answer for, so a tool naming a project other
55
207
  // than the attached one moves the connector BEFORE opening. Opening first would file the session
@@ -58,21 +210,241 @@ const ensureSession = async (deps, projectId) => {
58
210
  deps.attach(projectId);
59
211
  await deps.session();
60
212
  };
213
+ /**
214
+ * The id of a session that names NO project — what a delegated create sends, so the platform can
215
+ * hand the checks it runs before the project card exists to this session's parent.
216
+ *
217
+ * A session filed against another project would be refused there, so an attached connector is
218
+ * detached first; the create then attaches it to the project it filed. `undefined` where the host
219
+ * cannot detach, and the platform performs those checks itself.
220
+ */
221
+ const unattachedSessionId = async (deps) => {
222
+ if (deps.attached() != null) {
223
+ if (deps.detach == null)
224
+ return undefined;
225
+ deps.detach();
226
+ }
227
+ return (await deps.session()).session.id;
228
+ };
229
+ /**
230
+ * A story deletion starts its slot cleanup through the agent queue. The manager mutation returns
231
+ * once that work has been accepted, while the cleanup still owns the project lock; returning the
232
+ * MCP tool at that boundary lets the parent's very next project operation lose a race it cannot
233
+ * observe. Wait for two stable unlocked observations so a just-dispatched cleanup also has time
234
+ * to acquire the lock before this tool declares the mutation settled.
235
+ *
236
+ * The delete's COMMIT is no substitute: it says the card is gone, and nothing about the
237
+ * placeholder screens the slot is still retiring under the project lock.
238
+ */
239
+ const waitForStoryCleanup = async (deps, projectId) => {
240
+ const deadline = Date.now() + 5 * 60_000;
241
+ let unlockedAt = null;
242
+ while (Date.now() < deadline) {
243
+ const status = await deps.api.project.status(projectId);
244
+ if (status.agent.locked) {
245
+ unlockedAt = null;
246
+ }
247
+ else if (unlockedAt == null) {
248
+ unlockedAt = Date.now();
249
+ }
250
+ else if (Date.now() - unlockedAt >= 500) {
251
+ return;
252
+ }
253
+ await new Promise(resolve => setTimeout(resolve, 150));
254
+ }
255
+ throw new Error('Story deleted, but its scaffold cleanup did not release the project lock.');
256
+ };
257
+ /** The project a tool acts on when it can also work without one. */
258
+ const projectOrNull = (args, deps) => typeof args.projectId === 'string' ? args.projectId : deps.attached();
259
+ /**
260
+ * The question a parked domain operation is waiting on, when this session holds none.
261
+ *
262
+ * A question is delivered as an operation and answered against it, and that is the fast path. But
263
+ * an operation expires: a run that asked while nobody was attached, or while this connector was
264
+ * being restarted, is left `Waiting` with a question no queue here has ever seen. Without this the
265
+ * only way back to it is the web application, and a parent watching only the run would wait out the
266
+ * whole interaction on a question it could have carried in seconds.
267
+ *
268
+ * Best-effort on both reads. A project with no conversion answers an error, and failing the call
269
+ * that asked for a question because the lookup for it failed would be worse than answering "none".
270
+ */
271
+ const parkedQuestion = async (deps, projectId) => {
272
+ try {
273
+ const status = await deps.api.project.status(projectId);
274
+ if (status.pendingInquiry != null)
275
+ return status.pendingInquiry;
276
+ }
277
+ catch (e) {
278
+ deps.log(`no project question for ${projectId}: ${e.message}`);
279
+ }
280
+ try {
281
+ const status = await deps.api.convert.status(projectId);
282
+ if (status.pendingInquiry != null)
283
+ return status.pendingInquiry;
284
+ }
285
+ catch (e) {
286
+ deps.log(`no conversion question for ${projectId}: ${e.message}`);
287
+ }
288
+ return null;
289
+ };
290
+ const questionResult = (inquiry, deps, note) => ok((note != null ? `${note}\n\n` : '') + makeQuestionEnvelopeModel(inquiry).renderQuestionEnvelope({ harness: deps.host.harness }), { questionId: inquiry.id });
291
+ /** A cost estimate, always with the sentence that says what it is not. */
292
+ const renderEstimate = (estimate) => [
293
+ estimate.delegated
294
+ ? `estimated cost of ${estimate.stage}: nothing — you perform this conversion's model calls`
295
+ : `estimated cost of ${estimate.stage}: $${estimate.usd.toFixed(2)}`,
296
+ ' An estimate, not a price: it is computed from the size of the code before any of it is read.',
297
+ ];
298
+ /**
299
+ * Whether this project has a conversion already, and one that has actually begun.
300
+ *
301
+ * A check answers a question a project already converting has moved past. Sending its parent to
302
+ * `convert_project` there points at the one tool that cannot help — the platform refuses a second
303
+ * Start over a live run — while the conversion sits at the decision or the question it is actually
304
+ * waiting on, which is what nobody notices.
305
+ *
306
+ * `Pending` and `Cancelled` are read as none: the record exists, nothing is under way, and
307
+ * starting one really is the next step.
308
+ */
309
+ const conversionUnderWay = async (deps, projectId) => {
310
+ try {
311
+ const view = await deps.api.convert.status(projectId);
312
+ return view.status === ConversionStatus.Pending || view.status === ConversionStatus.Cancelled
313
+ ? null
314
+ : view;
315
+ }
316
+ catch (e) {
317
+ // Best-effort, like every other second read here: a project with no conversion answers an
318
+ // error, and failing the check because the lookup for one failed is worse than answering
319
+ // without it.
320
+ deps.log(`no conversion for ${projectId}: ${e.message}`);
321
+ return null;
322
+ }
323
+ };
324
+ /**
325
+ * How a check names the conversion the project already has.
326
+ *
327
+ * Derived from the STATUS, never from the mere presence of a record. {@link conversionUnderWay}
328
+ * filters `Pending` and `Cancelled` and nothing else, so a check over a finished or failed
329
+ * conversion read "already under way — stage implementation · done" one line above "next: nothing
330
+ * — this conversion is finished": one answer contradicting itself about whether anything is
331
+ * running, which leaves a parent polling a run that ended.
332
+ */
333
+ const conversionLead = (view) => {
334
+ const where = `stage ${view.stage} · ${view.status}`;
335
+ switch (view.status) {
336
+ case ConversionStatus.Done:
337
+ return `conversion: finished — ${where}`;
338
+ case ConversionStatus.Failed:
339
+ return `conversion: stopped at ${where}`;
340
+ default:
341
+ return `conversion: already under way — ${where}`;
342
+ }
343
+ };
344
+ const renderCheck = (check, conversion) => {
345
+ const lines = [
346
+ `verdict: ${check.verdict}`
347
+ + (check.reasons.length > 0 ? ` — ${check.reasons.join(', ')}` : ''),
348
+ `shape: ${check.shape}${check.architecture != null ? ` · ${check.architecture}` : ''}`
349
+ + `${check.monorepo ? ' · monorepo' : ''}`,
350
+ ...(check.stack != null
351
+ ? [`stack: ${check.stack.label} (${check.stack.language}`
352
+ + `${check.stack.framework != null ? `, ${check.stack.framework}` : ''})`]
353
+ : ['stack: not recognised']),
354
+ `${check.files} files · ${Math.round(check.bytes / 1024)} KB`
355
+ + (check.bulk > 0 ? ` · ${check.bulk} bulk data file(s), sampled rather than read` : ''),
356
+ ];
357
+ if (check.unlinked.length > 0) {
358
+ // Named rather than counted: each one is a decision somebody has to make, and the conversion
359
+ // will ask about it rather than guess.
360
+ lines.push(`nothing declares: ${check.unlinked.join(', ')}`);
361
+ }
362
+ if (check.estimate != null)
363
+ lines.push(...renderEstimate(check.estimate));
364
+ if (conversion != null) {
365
+ lines.push(`${conversionLead(conversion)} (conversion_status carries the whole picture)`);
366
+ }
367
+ lines.push(`next: ${check.verdict === ConvertibilityVerdict.Refused
368
+ // The verdict outranks a run, and deliberately: an origin nothing can convert has no next
369
+ // step whatever a record says about the attempt that found that out.
370
+ ? 'nothing — this origin cannot be converted'
371
+ : conversion != null
372
+ ? statusTextHelper.conversionNext(conversion)
373
+ : 'convert_project to start, then read conversion_status'}`);
374
+ return lines.join('\n');
375
+ };
376
+ const renderConversion = (view) => {
377
+ const lines = [
378
+ `conversion of ${view.projectId} · stage ${view.stage} · ${view.status}`,
379
+ ...(view.verdict != null ? [`verdict: ${view.verdict}`] : []),
380
+ ...(view.stack != null
381
+ ? [`origin stack: ${view.stack.label}`
382
+ + `${view.architecture != null ? ` · ${view.architecture}` : ''}`]
383
+ : []),
384
+ ...(view.targetStack != null ? [`rebuilt onto: ${view.targetStack.label}`] : []),
385
+ `origin sources: ${view.originState}`,
386
+ ...(view.assumptions > 0
387
+ ? [`${view.assumptions} question(s) answered by the platform itself —`
388
+ + ' they are listed in docs/conversion/assumptions.md']
389
+ : []),
390
+ ...(view.runId != null ? [`run ${view.runId}`] : []),
391
+ ];
392
+ const estimate = view.estimates[view.estimates.length - 1];
393
+ if (estimate != null)
394
+ lines.push(...renderEstimate(estimate));
395
+ if (view.storyEstimate != null) {
396
+ lines.push(`implementing the stories: $${view.storyEstimate.minUsd}–$${view.storyEstimate.maxUsd}`, ` A band, not a price: a story costs what it turns out to need, usually`
397
+ + ` $${STORY_BAND_MIN_USD}–$${STORY_BAND_MAX_USD} each.`);
398
+ }
399
+ // Phrased, exactly like a thrown one: the platform writes a failed stage's cause here with
400
+ // `describeFailure`, which for a refusal is the marker verbatim. Phrase the stored value the
401
+ // same way as a thrown refusal so every conversion status has one user-facing reading.
402
+ if (view.lastError != null && view.lastError !== '') {
403
+ lines.push(`error: ${refusalHelper.refusalPhrase(view.lastError)}`);
404
+ }
405
+ lines.push(`next: ${statusTextHelper.conversionNext(view)}`);
406
+ return lines.join('\n');
407
+ };
61
408
  /**
62
409
  * Everything a parent agent can ask the platform to do.
63
410
  *
64
411
  * Two rules shape the whole list. Every tool answers inside the strictest host's per-tool ceiling,
65
- * so anything that takes minutes returns a JOB at once and is polled — a coding agent that blocks
66
- * for twenty minutes is reported to its user as a hung server. And every tool that cannot work in
412
+ * so anything that takes minutes returns its current domain status and continues server-side. And
413
+ * every tool that cannot work in
67
414
  * a given mode is HIDDEN rather than failing, so the catalogue a parent reads is exactly the set
68
415
  * of things that work for it.
69
416
  */
70
417
  export const catalogue = [
418
+ {
419
+ name: 'describe_blueprints',
420
+ title: 'Supported blueprints, cases and application categories',
421
+ description: 'Read the installed blueprint registry, its complete case list, supported app categories, tenancy and planning resources. Call before choosing a project type. Authenticated, read-only, model-free; requires no project or connector session.',
422
+ input: {},
423
+ availability: toolHostHelper.anyHost,
424
+ annotations: READ_ONLY,
425
+ run: async (_args, deps) => {
426
+ const catalogue = await deps.api.account.blueprints();
427
+ return ok(blueprintDescription.render(catalogue), catalogue);
428
+ },
429
+ },
430
+ {
431
+ name: 'describe_platform',
432
+ title: 'What this platform can build, and what you can drive from here',
433
+ description: 'Everything the platform runs — building an application from a description, implementing'
434
+ + ' user stories, converting an application you already have — what every application it'
435
+ + ' generates carries, and which of it this session can start. Read it before deciding how to'
436
+ + ' approach a request. Needs no project and makes no network call.',
437
+ input: {},
438
+ availability: toolHostHelper.anyHost,
439
+ annotations: READ_ONLY,
440
+ run: async (_args, deps) => ok(renderPlatform(PLATFORM_CATALOGUE, deps.host)),
441
+ },
71
442
  {
72
443
  name: 'describe_capabilities',
73
444
  title: 'Report what your models can do',
74
445
  description: 'Tell the platform which of your models it may use, so it can size each call. Call this once,'
75
- + ' before creating or developing anything. Returns the mapping it will use.',
446
+ + ' before creating or developing anything. Returns the mapping it will use, and what every'
447
+ + ' generated application carries.',
76
448
  input: {
77
449
  strong: z.string().optional().describe('Your most capable model, e.g. the one you plan with'),
78
450
  standard: z.string().optional().describe('Your everyday model'),
@@ -80,7 +452,8 @@ export const catalogue = [
80
452
  subagents: z.boolean().optional().describe('Can you run a task in an isolated subagent?'),
81
453
  effortControl: z.boolean().optional().describe('Can you ask for low reasoning effort?'),
82
454
  },
83
- availability: anyHost,
455
+ availability: toolHostHelper.anyHost,
456
+ annotations: READ_ONLY,
84
457
  run: async (args, deps) => {
85
458
  const tiers = {};
86
459
  for (const tier of ['strong', 'standard', 'cheap']) {
@@ -99,7 +472,8 @@ export const catalogue = [
99
472
  return ok('No model was recorded: this tool takes `strong`, `standard` and `cheap` as TOP-LEVEL'
100
473
  + ' string arguments, not nested under another key. The platform will size every call'
101
474
  + ' with its own defaults until you send them.\n\n'
102
- + roleSummary(), { tiers, roles: rolesByTier() });
475
+ + roleSummary()
476
+ + `\n\n${GENERATED_SUMMARY}`, { tiers, roles: rolesByTier() });
103
477
  }
104
478
  const byTier = rolesByTier();
105
479
  return ok('Recorded. The platform asks for three power classes:\n'
@@ -108,48 +482,18 @@ export const catalogue = [
108
482
  .join('\n')
109
483
  + `\n\nsubagents: ${subagents == null ? 'not stated' : subagents ? 'yes' : 'no'}`
110
484
  + ` · low reasoning effort: ${effortControl == null ? 'not stated' : effortControl ? 'yes' : 'no'}`
111
- + (performsModelTasks(deps.host)
112
- ? '\n\nThis session runs the platform\'s model calls on YOUR side. Whenever a job reports'
113
- + ' "blocked on: model-task", call next_task.'
114
- : ''), { tiers, subagents, effortControl, roles: byTier });
115
- },
116
- },
117
- {
118
- name: 'describe_harness',
119
- title: 'What the harness files would say',
120
- description: 'Preview the files install_harness would write for a coding agent, so you can read them'
121
- + ' before anything is changed on disk.',
122
- input: { harness: z.string().optional().describe('claude-code | codex | copilot | opencode') },
123
- availability: anyHost,
124
- run: async (args, deps) => {
125
- const harness = args.harness ?? deps.host.harness;
126
- const files = describeHarness(harness);
127
- return ok(`${harness} — ${files.length} files:\n`
128
- + files.map(file => `\n--- ${file.path} ---\n${file.content}`).join('\n'), { harness, files: files.map(file => file.path) });
129
- },
130
- },
131
- {
132
- name: 'install_harness',
133
- title: 'Set this agent up to work with the platform',
134
- description: 'Write the instruction and subagent files this coding agent needs, into the project directory.'
135
- + ' Idempotent, and it never writes your token — only a reference to the environment variable.',
136
- input: {
137
- dir: z.string().optional().describe('Where to write. Defaults to the project directory.'),
138
- harness: z.string().optional(),
139
- mcpConfig: z.boolean().optional().describe('Also write the MCP server entry'),
140
- },
141
- availability: withExecutor,
142
- run: async (args, deps) => {
143
- const dir = args.dir ?? deps.dir;
144
- if (dir == null)
145
- return fail('No directory to write to.');
146
- const harness = args.harness ?? deps.host.harness;
147
- const result = await installHarness(dir, harness, {
148
- mcpConfig: args.mcpConfig === true,
149
- });
150
- return ok(`Wrote ${result.written.length} file(s) for ${harness}:\n`
151
- + result.written.map(path => ` ${path}`).join('\n')
152
- + (result.skipped.length > 0 ? `\nUnchanged: ${result.skipped.join(', ')}` : ''), result);
485
+ // The answer to "which calls are mine" is all or none: a delegated session performs every
486
+ // model call the platform makes for it, a cloud one none — a conversion's included.
487
+ + (toolHostHelper.performsModelTasks(deps.host)
488
+ ? '\n\nThis session runs in the delegated mode: EVERY model call the platform makes for it'
489
+ + ' is yours to perform — project drafting, content checks and formatting included.'
490
+ + ' Whenever a tool answers with a model task, or a domain status reports waiting for one,'
491
+ + ' perform it and call submit_task_result; next_task hands you the next.'
492
+ : toolHostHelper.sessionCapable(deps.host)
493
+ ? '\n\nThis session runs in the cloud mode: the platform performs every model call'
494
+ + ' itself, a conversion\'s included.'
495
+ : '')
496
+ + `\n\n${GENERATED_SUMMARY}`, { tiers, subagents, effortControl, roles: byTier });
153
497
  },
154
498
  },
155
499
  {
@@ -157,7 +501,8 @@ export const catalogue = [
157
501
  title: 'What this connector is doing',
158
502
  description: 'The mode, the attached project, and what the connector has handled so far.',
159
503
  input: {},
160
- availability: anyHost,
504
+ availability: toolHostHelper.anyHost,
505
+ annotations: READ_ONLY,
161
506
  run: async (_args, deps) => {
162
507
  const session = deps.currentSession();
163
508
  const lines = [
@@ -166,48 +511,37 @@ export const catalogue = [
166
511
  ...(deps.dir != null ? [`directory: ${deps.dir}`] : []),
167
512
  ];
168
513
  if (session == null) {
169
- lines.push(sessionCapable(deps.host)
514
+ lines.push(toolHostHelper.sessionCapable(deps.host)
170
515
  ? 'session: not opened yet — it opens with the first operation'
171
516
  : 'session: none — this host holds none, and the platform performs the work itself');
172
517
  }
173
518
  else {
174
519
  const stats = session.stats;
175
520
  lines.push(`session ${session.session.id} · ${stats.transport}`, `operations: ${stats.opsDone} done, ${stats.opsFailed} failed`, `model tasks: ${stats.tasksDelivered} delivered, ${stats.tasksSubmitted} answered,`
176
- + ` ${session.pendingTasks()} waiting`);
521
+ + ` ${session.pendingTasks()} waiting`, `questions: ${stats.questionsDelivered} asked, ${stats.questionsAnswered} answered,`
522
+ + ` ${session.pendingQuestions()} waiting`);
177
523
  }
178
524
  return ok(lines.join('\n'));
179
525
  },
180
526
  },
181
- {
182
- name: 'wait_for',
183
- title: 'Wait for a job',
184
- description: `Poll one job for up to ${JOB_POLL_MAX_SEC} seconds. Returns as soon as it finishes or`
185
- + ' becomes blocked; call again while it is still running.',
186
- input: {
187
- jobId: z.string(),
188
- projectId: z.string().optional(),
189
- maxWaitSec: z.number().int().min(0).max(JOB_POLL_MAX_SEC).optional(),
190
- },
191
- availability: anyHost,
192
- run: async (args, deps) => {
193
- const project = projectOf(args, deps);
194
- const wait = Math.min(args.maxWaitSec ?? JOB_POLL_MAX_SEC, JOB_POLL_MAX_SEC);
195
- const job = await deps.api.project.job(project, args.jobId, wait);
196
- return jobResult(job);
197
- },
198
- },
199
527
  {
200
528
  name: 'create_project',
201
529
  title: 'Start a project from a description',
202
530
  description: 'Describe the application in a sentence or two. The platform writes a specification, names it'
203
- + ' and drafts a vision. Returns a job — poll it with wait_for, then read the draft and call'
204
- + ' confirm_project.',
531
+ + ' and drafts a vision. Returns the project status; read the draft and call confirm_project.',
205
532
  input: { prompt: z.string().min(1) },
206
- availability: anyHost,
533
+ availability: toolHostHelper.anyHost,
534
+ annotations: WRITE,
207
535
  run: async (args, deps) => {
208
- const job = await deps.api.project.create(args.prompt, deps.host.target === ConnectTarget.Local ? ConnectTarget.Local : ConnectTarget.Cloud);
209
- deps.attach(job.projectId);
210
- return jobResult(job);
536
+ // In the delegated mode the platform's checks of the prompt run before the project exists, so
537
+ // they go to a session that names no project yet — and the drafting after them to the
538
+ // project's own, attached as soon as the platform answers with it.
539
+ const sessionId = toolHostHelper.performsModelTasks(deps.host) ? await unattachedSessionId(deps) : undefined;
540
+ const status = await deps.api.project.create(args.prompt, deps.host.target === ConnectTarget.Local ? ConnectTarget.Local : ConnectTarget.Cloud, sessionId);
541
+ deps.attach(status.project.id);
542
+ if (sessionId != null)
543
+ await ensureSession(deps, status.project.id);
544
+ return projectResult(status);
211
545
  },
212
546
  },
213
547
  {
@@ -215,15 +549,17 @@ export const catalogue = [
215
549
  title: 'Accept the specification and build the application',
216
550
  description: 'Confirm the drafted project, optionally editing what the analysis produced. This is what'
217
551
  + ' starts the build: the template lands, dependencies install, the whole application is'
218
- + ' drawn. Returns a job — it takes several minutes.',
552
+ + ' drawn. Returns the project status while the server-side run continues.',
219
553
  input: {
220
554
  projectId: z.string().optional(),
221
555
  name: z.string().optional(),
222
556
  description: z.string().optional(),
223
557
  specification: z.string().optional(),
224
558
  vision: z.string().optional(),
559
+ designSystem: z.string().optional(),
225
560
  },
226
- availability: anyHost,
561
+ availability: toolHostHelper.anyHost,
562
+ annotations: WRITE,
227
563
  run: async (args, deps) => {
228
564
  const project = projectOf(args, deps);
229
565
  // Attached BEFORE the call that starts the build, not after it. The platform begins
@@ -231,13 +567,14 @@ export const catalogue = [
231
567
  // whose connector has not filed a session yet is not queued for later — a local target
232
568
  // answers it with `ConnectSessionGone` and the step fails.
233
569
  await ensureSession(deps, project);
234
- const job = await deps.api.project.confirm(project, {
570
+ const status = await deps.api.project.confirm(project, {
235
571
  ...(typeof args.name === 'string' ? { name: args.name } : {}),
236
572
  ...(typeof args.description === 'string' ? { description: args.description } : {}),
237
573
  ...(typeof args.specification === 'string' ? { specification: args.specification } : {}),
238
574
  ...(typeof args.vision === 'string' ? { vision: args.vision } : {}),
575
+ ...(typeof args.designSystem === 'string' ? { designSystem: args.designSystem } : {}),
239
576
  });
240
- return jobResult(job);
577
+ return projectResult(status);
241
578
  },
242
579
  },
243
580
  {
@@ -245,11 +582,18 @@ export const catalogue = [
245
582
  title: 'Where the project stands',
246
583
  description: 'The project, its slot, whether the agent is busy, and the last run.',
247
584
  input: { projectId: z.string().optional() },
248
- availability: anyHost,
585
+ availability: toolHostHelper.anyHost,
586
+ annotations: READ_ONLY,
249
587
  run: async (args, deps) => {
250
588
  const status = await deps.api.project.status(projectOf(args, deps));
251
589
  const lines = [
252
590
  `${status.project.name} (${status.project.alias}) · ${status.project.id}`,
591
+ // The project card's own status, from the project flow. Absent only from a platform that
592
+ // predates it, which says nothing rather than something false.
593
+ ...(status.project.status != null
594
+ ? [`project: ${status.project.status}`
595
+ + `${status.project.intrinsic != null ? ` (${status.project.intrinsic})` : ''}`]
596
+ : []),
253
597
  status.slot != null
254
598
  ? `slot: ${status.slot.kind} · ${status.slot.status}`
255
599
  + `${status.slot.initialized === true ? ' · initialized' : ' · not initialized'}`
@@ -268,14 +612,21 @@ export const catalogue = [
268
612
  ['description', status.project.description],
269
613
  ['specification', status.project.specification],
270
614
  ['vision', status.project.vision],
615
+ ['design system', status.project.designSystem],
271
616
  ]) {
272
617
  if (value != null && value.trim() !== '') {
273
618
  lines.push('', `--- ${label} ---`, value.slice(0, DRAFT_CAP));
274
619
  }
275
620
  }
621
+ // `slot.lastError` is the channel the platform stores a content refusal, a reserved name or
622
+ // a legacy-layout verdict on, with no class left on it — so it is phrased here for the same
623
+ // reason the manager phrases it, and `backendWarning` carries an integrity verdict the same
624
+ // way. All three go through one helper because only the TEXT says which it is: anything that
625
+ // does not read as a marker — the build diagnostics somebody asked for, stack-shaped lines
626
+ // included — comes back exactly as it stands.
276
627
  for (const warning of [status.slot?.lastError, status.slot?.buildWarning, status.slot?.backendWarning]) {
277
628
  if (warning != null && warning !== '')
278
- lines.push(`warning: ${warning}`);
629
+ lines.push(`warning: ${refusalHelper.refusalPhrase(warning)}`);
279
630
  }
280
631
  return ok(lines.join('\n'), status);
281
632
  },
@@ -286,7 +637,8 @@ export const catalogue = [
286
637
  description: 'Every project this access token can reach, newest first, with the id, slug and status'
287
638
  + ' each of the other project tools takes.',
288
639
  input: {},
289
- availability: anyHost,
640
+ availability: toolHostHelper.anyHost,
641
+ annotations: READ_ONLY,
290
642
  run: async (_args, deps) => {
291
643
  const projects = await deps.api.project.list();
292
644
  return ok(projects.length < 1
@@ -303,7 +655,8 @@ export const catalogue = [
303
655
  projectId: z.string().optional(),
304
656
  slug: z.string().optional(),
305
657
  },
306
- availability: anyHost,
658
+ availability: toolHostHelper.anyHost,
659
+ annotations: IDEMPOTENT_WRITE,
307
660
  run: async (args, deps) => {
308
661
  const status = await deps.api.project.attach({
309
662
  ...(typeof args.projectId === 'string' ? { projectId: args.projectId } : {}),
@@ -319,18 +672,459 @@ export const catalogue = [
319
672
  description: 'Wipe the generated sources and lay the template down again. The user stories are KEPT and'
320
673
  + ' reset to planned; your own configuration and git history survive.',
321
674
  input: { projectId: z.string().optional() },
322
- availability: anyHost,
675
+ availability: toolHostHelper.anyHost,
676
+ annotations: DESTRUCTIVE,
677
+ run: async (args, deps) => {
678
+ const project = projectOf(args, deps);
679
+ await ensureSession(deps, project);
680
+ return projectResult(await deps.api.project.reinit(project));
681
+ },
682
+ },
683
+ {
684
+ name: 'describe_planning_kits',
685
+ title: 'The planning kits a project can take',
686
+ description: 'A planning kit is a ready set of card types and status flows — tasks and bugs moving to done,'
687
+ + ' deals through a sales pipeline, tickets to resolution, and so on — that the platform writes'
688
+ + ' into the project\'s common package, so the application\'s records and their workflows are'
689
+ + ' declared once instead of story by story. This lists each kit: its purpose, the container its'
690
+ + ' cards live in, every card type with its main flow, and each flow\'s statuses. Call it before'
691
+ + ' apply_planning_kit.',
692
+ input: { projectId: z.string().optional() },
693
+ availability: toolHostHelper.anyHost,
694
+ annotations: READ_ONLY,
695
+ run: async (args, deps) => {
696
+ const project = projectOf(args, deps);
697
+ const { kits } = await deps.api.project.kitDescribe(project);
698
+ return ok(kitsUtils.renderKits(project, kits), { projectId: project, kits: kits });
699
+ },
700
+ },
701
+ {
702
+ name: 'apply_planning_kit',
703
+ title: 'Write a planning kit into the project',
704
+ description: 'Write one planning kit — its card types and status flows — into the project\'s common package.'
705
+ + ' Describe the kits first (describe_planning_kits) and pass the kit id; `types` keeps only'
706
+ + ' those card-type keys of the kit, and leaving it out keeps every type. Applying the same kit'
707
+ + ' again changes nothing. The platform rebuilds the preview itself; the answer lists the types'
708
+ + ' written, those left out, and any warnings.',
709
+ input: {
710
+ projectId: z.string().optional(),
711
+ kit: z.string().min(1).describe('The kit id, as describe_planning_kits lists it'),
712
+ types: z.array(z.string().min(1)).optional()
713
+ .describe('The card-type keys of the kit to keep; every type when omitted'),
714
+ },
715
+ availability: toolHostHelper.anyHost,
716
+ annotations: WRITE,
717
+ run: async (args, deps) => await answering(deps, 'apply_planning_kit', async () => {
718
+ const project = projectOf(args, deps);
719
+ const kit = typeof args.kit === 'string' ? args.kit : '';
720
+ if (kit === '') {
721
+ return fail('Name the kit: describe_planning_kits lists the kit ids this project can take.');
722
+ }
723
+ const types = Array.isArray(args.types)
724
+ ? args.types.filter((type) => typeof type === 'string' && type !== '')
725
+ : undefined;
726
+ // The kit is written into the project's files, which for a local project is an operation THIS
727
+ // connector answers — so it is attached before the platform is asked.
728
+ await ensureSession(deps, project);
729
+ const result = await deps.api.project.kitApply(project, { kit, ...(types != null ? { types } : {}) });
730
+ return ok(kitsUtils.renderKitApply(project, kit, result), {
731
+ projectId: project, kit, result: result,
732
+ });
733
+ }),
734
+ },
735
+ {
736
+ name: 'project_settings',
737
+ title: 'The project\'s settings',
738
+ description: 'The settings a person edits on the project\'s control panel: the copyright line, the'
739
+ + ' organization name, the Terms and Privacy links, and the Google tag — and where the platform'
740
+ + ' credit stands (shown or hidden, and whether the plan allows hiding it). A relative link such'
741
+ + ' as /terms or /privacy is normal — it is the page the platform generated inside the'
742
+ + ' application, and the generated pages name the organization and copyright set here.',
743
+ input: { projectId: z.string().optional(), scope: SCOPE_INPUT },
744
+ availability: toolHostHelper.anyHost,
745
+ annotations: READ_ONLY,
746
+ run: async (args, deps) => {
747
+ const project = projectOf(args, deps);
748
+ const scope = scopeOf(args);
749
+ return settingsResult(project, await deps.api.projectBranding(project, scope), undefined, scope);
750
+ },
751
+ },
752
+ {
753
+ name: 'update_project_settings',
754
+ title: 'Change the project\'s settings',
755
+ description: 'Change any of the copyright line, the organization name, the Terms and Privacy links and the'
756
+ + ' Google tag; whatever you leave out keeps its value. Copyright and organization are never'
757
+ + ' empty. A legal link is an https:// address or a path on the application itself — /terms'
758
+ + ' and /privacy are the generated pages. A Google tag is a GTM-, G-, GT-, AW- or DC- id, or'
759
+ + ' an empty string to remove it; it loads on the preview and in production behind the cookie'
760
+ + ' consent. useOrganizationDefaults: true instead replaces the organization name and copyright'
761
+ + ' with the organization\'s defaults (organization_branding) — on its own, never with other'
762
+ + ' fields. The preview is rebuilt with the change; production takes it at the next Publish.'
763
+ + ' The platform credit is set_platform_credit.',
764
+ input: {
765
+ projectId: z.string().optional(),
766
+ copyright: z.string().optional().describe('The copyright line, e.g. "© 2026 Acme Ltd"'),
767
+ organizationName: z.string().optional().describe('Who runs the application'),
768
+ termsUrl: z.string().optional().describe('https://… or a path such as /terms'),
769
+ privacyUrl: z.string().optional().describe('https://… or a path such as /privacy'),
770
+ googleTag: z.string().optional()
771
+ .describe('GTM-XXXXXXX, G-XXXXXXXXXX, GT-…, AW-… or DC-…; an empty string removes it'),
772
+ useOrganizationDefaults: z.boolean().optional()
773
+ .describe('true: copy the organization\'s name and copyright into this project. Alone.'),
774
+ scope: SCOPE_INPUT,
775
+ },
776
+ availability: toolHostHelper.anyHost,
777
+ annotations: IDEMPOTENT_WRITE,
778
+ run: async (args, deps) => await answering(deps, 'update_project_settings', async () => {
779
+ const project = projectOf(args, deps);
780
+ const scope = scopeOf(args);
781
+ const patch = settingsHelper.settingsPatch(args);
782
+ const changed = Object.keys(patch);
783
+ if (args.useOrganizationDefaults === true) {
784
+ if (changed.length > 0) {
785
+ return fail('Not done. useOrganizationDefaults replaces the organization name and copyright on its own:'
786
+ + ` call it without ${changed.join(', ')}, then change the rest in a second call.`);
787
+ }
788
+ // A configuration push follows the copy, so a local project's connector is attached first.
789
+ await ensureSession(deps, project);
790
+ const copied = await deps.api.copyOrganizationBranding(project, scope);
791
+ return settingsResult(project, copied, `Copied the organization's name and copyright into the project. ${settingsHelper.settingsReach(deps.host.target, scope)}`, scope);
792
+ }
793
+ if (changed.length < 1) {
794
+ return fail('Nothing to change. Give at least one of copyright, organizationName, termsUrl, privacyUrl'
795
+ + ' or googleTag, or useOrganizationDefaults: true — project_settings shows the current values.');
796
+ }
797
+ // The save ends in a configuration push, and for a local project that push is an operation
798
+ // THIS connector answers — so it is attached before the platform is asked, exactly as a story
799
+ // mutation is. A host that holds no session skips it and the platform delivers the push itself.
800
+ await ensureSession(deps, project);
801
+ const saved = await deps.api.saveProjectBranding(project, patch, scope);
802
+ return settingsResult(project, saved, `Saved ${changed.join(', ')}. ${settingsHelper.settingsReach(deps.host.target, scope)}`, scope);
803
+ }),
804
+ },
805
+ {
806
+ name: 'set_platform_credit',
807
+ title: 'Hide or show the platform credit',
808
+ description: 'Hide (hidden: true) or show the "Built with OwlMeans" credit in the application\'s footer —'
809
+ + ' white label, which the organization\'s plan must include; without it nothing changes and the'
810
+ + ' answer says so. The request is kept if the plan later lapses: the credit then shows, and is'
811
+ + ' hidden again on its own once the plan includes white label. The preview is rebuilt;'
812
+ + ' production takes it at the next Publish.',
813
+ input: {
814
+ projectId: z.string().optional(),
815
+ hidden: z.boolean().describe('true hides the credit, false shows it.'),
816
+ scope: SCOPE_INPUT,
817
+ },
818
+ availability: toolHostHelper.anyHost,
819
+ annotations: IDEMPOTENT_WRITE,
820
+ run: async (args, deps) => await answering(deps, 'set_platform_credit', async () => {
821
+ if (typeof args.hidden !== 'boolean')
822
+ return fail('Say whether to hide the credit with hidden: true or false.');
823
+ const project = projectOf(args, deps);
824
+ const scope = scopeOf(args);
825
+ await ensureSession(deps, project);
826
+ const saved = await deps.api.setPlatformCredit(project, args.hidden, scope);
827
+ return settingsResult(project, saved, `The platform credit is ${args.hidden ? 'hidden' : 'shown'}. ${settingsHelper.settingsReach(deps.host.target, scope)}`, scope);
828
+ }),
829
+ },
830
+ {
831
+ name: 'project_configuration',
832
+ title: 'The project\'s configuration variables',
833
+ description: 'The environment variables the generated application declares — backend and frontend — and'
834
+ + ' which of them still need a value. A backend value is a secret: the answer says only whether'
835
+ + ' it is set, never what it is. A frontend value is public (it is built into the pages every'
836
+ + ' visitor loads) and is shown.',
837
+ input: { projectId: z.string().optional(), scope: SCOPE_INPUT },
838
+ availability: toolHostHelper.anyHost,
839
+ annotations: READ_ONLY,
323
840
  run: async (args, deps) => {
324
841
  const project = projectOf(args, deps);
842
+ const config = await deps.api.config.get(project, scopeOf(args));
843
+ return ok(configHelper.renderConfiguration(project, config), {
844
+ projectId: project, configuration: config,
845
+ });
846
+ },
847
+ },
848
+ {
849
+ name: 'update_project_configuration',
850
+ title: 'Set configuration variables',
851
+ description: 'Set environment variables of the generated application: backend and frontend, each a map of'
852
+ + ' NAME → value; variables you leave out keep their values, an empty value unsets one, and a new'
853
+ + ' name is declared on the side you give it. A variable keeps its side — a backend secret is'
854
+ + ' never moved to the frontend, where it would be public. Ask the user for secret values;'
855
+ + ' never invent one. The preview is reconfigured and rebuilt; production\'s own set'
856
+ + ' (scope: production) is taken at the next Publish.',
857
+ input: {
858
+ projectId: z.string().optional(),
859
+ backend: z.record(z.string(), z.string()).optional()
860
+ .describe('NAME → value for the backend, e.g. {"STRIPE_SECRET_KEY":"sk_live_…"}. Never shown back.'),
861
+ frontend: z.record(z.string(), z.string()).optional()
862
+ .describe('NAME → value for the frontend — PUBLIC, built into the pages.'),
863
+ scope: SCOPE_INPUT,
864
+ },
865
+ availability: toolHostHelper.anyHost,
866
+ annotations: IDEMPOTENT_WRITE,
867
+ run: async (args, deps) => await answering(deps, 'update_project_configuration', async () => {
868
+ const project = projectOf(args, deps);
869
+ const body = configHelper.configBody(args);
870
+ const names = configHelper.namesOf(body);
871
+ if (names.length < 1) {
872
+ return fail('Nothing to set. Give backend and/or frontend as maps of NAME → value —'
873
+ + ' project_configuration lists what the application declares.');
874
+ }
875
+ const scope = scopeOf(args);
876
+ // A save of the preview's set ends in a configuration push — for a local project, an operation
877
+ // this connector answers.
878
+ await ensureSession(deps, project);
879
+ const config = await deps.api.config.save(project, body, scope);
880
+ return ok(`Set ${names.join(', ')}. ${settingsHelper.settingsReach(deps.host.target, scope)}\n\n`
881
+ + configHelper.renderConfiguration(project, config), { projectId: project, configuration: config });
882
+ }),
883
+ },
884
+ {
885
+ name: 'recollect_configuration',
886
+ title: 'Find the variables the sources declare',
887
+ description: 'Let the platform\'s own agent read the generated sources again for the environment variables'
888
+ + ' they use, and declare any the configuration does not list yet — after a change that added'
889
+ + ' one. It is the platform\'s own model call. Answers the configuration as it then stands.',
890
+ input: { projectId: z.string().optional() },
891
+ availability: toolHostHelper.anyHost,
892
+ annotations: IDEMPOTENT_WRITE,
893
+ run: async (args, deps) => await answering(deps, 'recollect_configuration', async () => {
894
+ const project = projectOf(args, deps);
895
+ // It reads the project's tree — for a local project, through this connector.
325
896
  await ensureSession(deps, project);
326
- const job = await deps.api.project.reinit(project);
327
- return jobResult(job);
897
+ const config = await deps.api.config.recollect(project);
898
+ return ok(`The sources were read again.\n\n${configHelper.renderConfiguration(project, config)}`, { projectId: project, configuration: config });
899
+ }),
900
+ },
901
+ {
902
+ name: 'organization_branding',
903
+ title: 'The organization\'s branding defaults',
904
+ description: 'The organization name and copyright line every new project of this organization starts with.'
905
+ + ' A project keeps its own copy after it is created.',
906
+ input: {},
907
+ availability: toolHostHelper.anyHost,
908
+ annotations: READ_ONLY,
909
+ run: async (_args, deps) => {
910
+ const branding = await deps.api.account.branding.get();
911
+ return ok(settingsHelper.renderOrganizationBranding(branding), { organization: branding });
912
+ },
913
+ },
914
+ {
915
+ name: 'update_organization_branding',
916
+ title: 'Change the organization\'s branding defaults',
917
+ description: 'Change the organization name and/or the copyright line new projects start with; what you leave'
918
+ + ' out keeps its value and neither may be empty. Existing projects keep their own — copy them in'
919
+ + ' with update_project_settings { useOrganizationDefaults: true }.',
920
+ input: {
921
+ organizationName: z.string().optional().describe('Who runs the applications, e.g. "Acme Ltd"'),
922
+ copyright: z.string().optional().describe('The copyright line, e.g. "© 2026 Acme Ltd"'),
923
+ },
924
+ availability: toolHostHelper.anyHost,
925
+ annotations: IDEMPOTENT_WRITE,
926
+ run: async (args, deps) => await answering(deps, 'update_organization_branding', async () => {
927
+ const patch = Object.fromEntries(['organizationName', 'copyright']
928
+ .filter(key => typeof args[key] === 'string').map(key => [key, args[key].trim()]));
929
+ if (Object.keys(patch).length < 1) {
930
+ return fail('Nothing to change. Give organizationName and/or copyright — organization_branding shows them.');
931
+ }
932
+ const saved = await deps.api.account.branding.save(patch);
933
+ return ok(`Saved ${Object.keys(patch).join(', ')}.\n\n${settingsHelper.renderOrganizationBranding(saved)}`, { organization: saved });
934
+ }),
935
+ },
936
+ {
937
+ name: 'backfill_project_branding',
938
+ title: 'Fill blank project settings from the defaults',
939
+ description: 'Fill every project\'s still-blank organization name, copyright and legal links from the'
940
+ + ' organization\'s defaults — values a person set are never touched. Running previews are'
941
+ + ' refreshed; a stopped one takes the values at its next start.',
942
+ input: {},
943
+ availability: toolHostHelper.anyHost,
944
+ annotations: IDEMPOTENT_WRITE,
945
+ run: async (_args, deps) => await answering(deps, 'backfill_project_branding', async () => {
946
+ const { projects } = await deps.api.account.branding.backfill();
947
+ return ok(projects > 0
948
+ ? `Filled the blank settings of ${projects} project(s) from the organization's defaults.`
949
+ : 'No project had blank settings — nothing was changed.', { projects });
950
+ }),
951
+ },
952
+ {
953
+ name: 'inference_settings',
954
+ title: 'Who performs the model calls',
955
+ description: 'Whether the platform\'s model calls are made on its own models (cloud, billed in credits) or'
956
+ + ' performed by the connected coding agent (local, which the organization\'s plan must include) —'
957
+ + ' the user\'s own default and, for a project, its override and what it resolves to. This is the'
958
+ + ' default the web application and connector hosts use. Restart a running stdio connector to'
959
+ + ' reload the preference; an explicit --llm or VIABLE_LLM override takes precedence.',
960
+ input: { projectId: z.string().optional().describe('A project to show the override of; the attached one by default.') },
961
+ availability: toolHostHelper.anyHost,
962
+ annotations: READ_ONLY,
963
+ run: async (args, deps) => {
964
+ const project = projectOrNull(args, deps);
965
+ const [account, settings] = await Promise.all([
966
+ deps.api.account.llm.get(),
967
+ project != null ? deps.api.project.llm(project) : Promise.resolve(null),
968
+ ]);
969
+ return ok(accountHelper.renderInference(account, project != null && settings != null ? { id: project, settings } : undefined), {
970
+ account: account,
971
+ ...(project != null && settings != null ? { projectId: project, project: settings } : {}),
972
+ });
973
+ },
974
+ },
975
+ {
976
+ name: 'set_inference_mode',
977
+ title: 'Change who performs the model calls',
978
+ description: 'Set the user\'s default (level: account) or one project\'s override (level: project) to cloud — the'
979
+ + ' platform\'s own models, billed in credits — or local, where the connected coding agent performs'
980
+ + ' the platform\'s model calls; mode: inherit clears a project\'s override so it follows the default.'
981
+ + ' Every change needs a plan that includes the local mode (exactly as in the web application); without'
982
+ + ' it nothing changes and the answer says so. It changes the default the web application and'
983
+ + ' connector hosts use. Restart a running stdio connector to reload it; explicit --llm or'
984
+ + ' VIABLE_LLM overrides take precedence.',
985
+ input: {
986
+ level: z.enum(INFERENCE_LEVELS).describe('account: the user\'s own default. project: one project\'s override.'),
987
+ mode: z.enum(INFERENCE_MODES).describe('cloud, local, or inherit (a project only: follow the default).'),
988
+ projectId: z.string().optional().describe('With level: project — the attached project by default.'),
989
+ },
990
+ availability: toolHostHelper.anyHost,
991
+ annotations: IDEMPOTENT_WRITE,
992
+ run: async (args, deps) => await answering(deps, 'set_inference_mode', async () => {
993
+ const mode = args.mode;
994
+ if (!INFERENCE_MODES.includes(mode)) {
995
+ return fail('Say which mode with mode: cloud, local, or inherit (a project only).');
996
+ }
997
+ if (args.level === 'account') {
998
+ if (mode === 'inherit') {
999
+ return fail('Not done. Your own default inherits from nothing: give mode cloud or local, or level: project'
1000
+ + ' to make one project follow your default.');
1001
+ }
1002
+ const account = await deps.api.account.llm.set(mode);
1003
+ return ok(`Your default is now ${mode}.\n\n${accountHelper.renderInference(account)}`, { account: account });
1004
+ }
1005
+ if (args.level !== 'project')
1006
+ return fail('Say what to change with level: account or project.');
1007
+ const project = projectOf(args, deps);
1008
+ const settings = await deps.api.project.setLlm(project, mode === 'inherit' ? null : mode);
1009
+ const account = await deps.api.account.llm.get();
1010
+ return ok(`${mode === 'inherit' ? `${project} now follows your default` : `${project} is now set to ${mode}`}.`
1011
+ + `\n\n${accountHelper.renderInference(account, { id: project, settings })}`, { projectId: project, project: settings });
1012
+ }),
1013
+ },
1014
+ {
1015
+ name: 'list_access_tokens',
1016
+ title: 'The user\'s access tokens',
1017
+ description: 'The user\'s own access tokens — the credentials coding agents and scripts sign in to the platform'
1018
+ + ' with: name, the visible prefix, when each was created and last used, when it expires, and'
1019
+ + ' whether it was revoked. A token\'s secret is never shown; a new token is created only in the web'
1020
+ + ' application.',
1021
+ input: {},
1022
+ availability: toolHostHelper.anyHost,
1023
+ annotations: READ_ONLY,
1024
+ run: async (_args, deps) => {
1025
+ const list = await deps.api.account.tokens.list();
1026
+ return ok(accountHelper.renderTokens(list), { total: list.items.length, tokens: list.items });
1027
+ },
1028
+ },
1029
+ {
1030
+ name: 'revoke_access_token',
1031
+ title: 'Revoke an access token',
1032
+ description: 'Revoke one of the user\'s own access tokens by its id (list_access_tokens shows them): whatever'
1033
+ + ' signs in with it is refused from then on. It cannot be undone — ask the user first. Revoking the'
1034
+ + ' token this connector itself uses signs this connector out.',
1035
+ input: {
1036
+ tokenId: z.string().min(1).describe('The token\'s id, as list_access_tokens shows it.'),
1037
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to revoking this token.'),
1038
+ },
1039
+ availability: toolHostHelper.anyHost,
1040
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
1041
+ run: async (args, deps) => await answering(deps, 'revoke_access_token', async () => {
1042
+ if (args.confirm !== true) {
1043
+ return fail('Not done. A revoked token stops working at once and cannot be restored. Ask the user,'
1044
+ + ' then call it again with confirm: true.');
1045
+ }
1046
+ const tokenId = typeof args.tokenId === 'string' ? args.tokenId.trim() : '';
1047
+ if (tokenId === '')
1048
+ return fail('Not done. Name the token with tokenId — list_access_tokens shows them.');
1049
+ const revoked = await deps.api.account.tokens.revoke(tokenId);
1050
+ return ok(`Revoked the token ${revoked.id}: whatever signs in with it is refused from now on. If it was the token`
1051
+ + ' this connector uses, the next call asks for a new sign-in.', { tokenId: revoked.id });
1052
+ }),
1053
+ },
1054
+ {
1055
+ name: 'privacy_choices',
1056
+ title: 'The user\'s marketing consents',
1057
+ description: 'The marketing consents the user was asked for — newsletters and other optional uses of their'
1058
+ + ' data — and each one\'s saved answer: given, not given, never answered, or asked again after its'
1059
+ + ' wording changed. Giving a consent is the user\'s own choice in the web application;'
1060
+ + ' withdraw_marketing_consent withdraws one.',
1061
+ input: {},
1062
+ availability: toolHostHelper.anyHost,
1063
+ annotations: READ_ONLY,
1064
+ run: async (_args, deps) => {
1065
+ const choices = await deps.api.account.privacy.status();
1066
+ return ok(accountHelper.renderPrivacy(choices), { privacy: choices });
1067
+ },
1068
+ },
1069
+ {
1070
+ name: 'withdraw_marketing_consent',
1071
+ title: 'Withdraw marketing consents',
1072
+ description: 'Withdraw the user\'s marketing consents — the keys privacy_choices lists, or all: true for every one'
1073
+ + ' they gave. Each is recorded as not given, dated, exactly like switching it off in the web'
1074
+ + ' application. Nothing here can give a consent: that stays the user\'s own act in the browser.',
1075
+ input: {
1076
+ keys: z.array(z.string().min(1)).optional().describe('Consent keys, e.g. ["marketing.email"].'),
1077
+ all: z.boolean().optional().describe('true: withdraw every consent the user has given.'),
1078
+ },
1079
+ availability: toolHostHelper.anyHost,
1080
+ annotations: IDEMPOTENT_WRITE,
1081
+ run: async (args, deps) => await answering(deps, 'withdraw_marketing_consent', async () => {
1082
+ const named = Array.isArray(args.keys)
1083
+ ? [...new Set(args.keys.filter((key) => typeof key === 'string')
1084
+ .map(key => key.trim()).filter(key => key !== ''))]
1085
+ : [];
1086
+ if (args.all === true && named.length > 0) {
1087
+ return fail('Not done. Give either keys or all: true, not both.');
1088
+ }
1089
+ let keys = named;
1090
+ if (args.all === true) {
1091
+ keys = (await deps.api.account.privacy.status()).items.filter(item => item.granted).map(item => item.key);
1092
+ if (keys.length < 1) {
1093
+ return ok('The user has given no marketing consent — there is nothing to withdraw.', { withdrawn: [] });
1094
+ }
1095
+ }
1096
+ if (keys.length < 1) {
1097
+ return fail('Nothing to withdraw. Give keys (privacy_choices lists them) or all: true.');
1098
+ }
1099
+ const choices = await deps.api.account.privacy.withdraw(keys);
1100
+ return ok(`Withdrawn: ${keys.join(', ')}.\n\n${accountHelper.renderPrivacy(choices)}`, { withdrawn: keys, privacy: choices });
1101
+ }),
1102
+ },
1103
+ {
1104
+ name: 'pickup_intent',
1105
+ title: 'Pick up a prompt typed on the public site',
1106
+ description: 'Collect the project idea a visitor typed on the OwlMeans public site, by the code of the address it'
1107
+ + ' sent them to (/start?ref=… — the whole address may be given). A code works ONCE and only for two'
1108
+ + ' minutes; the prompt is then the user\'s to build with create_project once they confirm it.',
1109
+ input: {
1110
+ code: z.string().min(1).describe('The ref of the /start?ref=… address, or the address itself.'),
328
1111
  },
1112
+ availability: toolHostHelper.anyHost,
1113
+ annotations: WRITE,
1114
+ run: async (args, deps) => await answering(deps, 'pickup_intent', async () => {
1115
+ const ref = accountHelper.intentRefOf(args.code);
1116
+ if (ref == null)
1117
+ return fail('Not done. Give the code — the ref of the /start?ref=… address the site opened.');
1118
+ const { prompt } = await deps.api.account.intent.pickup(ref);
1119
+ return ok(`The prompt typed on the public site:\n\n${prompt}\n\nThe code is spent. next: show it to the user and,`
1120
+ + ' once they confirm that is what they want built, create_project with it.', { prompt });
1121
+ }),
329
1122
  },
330
1123
  {
331
1124
  name: 'list_stories',
332
1125
  title: 'The user stories of a project',
333
- description: 'One page of the project\'s stories, with their status.',
1126
+ description: 'One page of the project\'s stories, with their status, the primary story, and the landing'
1127
+ + ' gate story a guest starts on the guest home.',
334
1128
  input: {
335
1129
  projectId: z.string().optional(),
336
1130
  page: z.number().int().min(0).optional(),
@@ -338,15 +1132,20 @@ export const catalogue = [
338
1132
  status: z.string().optional(),
339
1133
  area: z.string().optional(),
340
1134
  },
341
- availability: anyHost,
1135
+ availability: toolHostHelper.anyHost,
1136
+ annotations: READ_ONLY,
342
1137
  run: async (args, deps) => {
343
- const list = await deps.api.story.list(projectOf(args, deps), {
344
- ...(typeof args.page === 'number' ? { page: args.page } : {}),
345
- ...(typeof args.size === 'number' ? { size: args.size } : {}),
346
- ...(typeof args.status === 'string' ? { status: args.status } : {}),
347
- ...(typeof args.area === 'string' ? { area: args.area } : {}),
1138
+ const page = typeof args.page === 'number' ? args.page : 0;
1139
+ const list = await deps.api.planning.cards.list({
1140
+ ...storyHelper.storyQuery(projectOf(args, deps), {
1141
+ ...(typeof args.status === 'string' ? { status: args.status } : {}),
1142
+ ...(typeof args.area === 'string' ? { area: args.area } : {}),
1143
+ }),
1144
+ page,
1145
+ size: typeof args.size === 'number' ? args.size : STORY_PAGE_SIZE,
1146
+ sort: STORY_ORDER,
348
1147
  });
349
- return ok(renderStories(list.items, list.page, list.total), list);
1148
+ return ok(storyHelper.renderStories(list.items, list.page ?? page, list.total), list);
350
1149
  },
351
1150
  },
352
1151
  {
@@ -355,10 +1154,16 @@ export const catalogue = [
355
1154
  description: 'The user stories of a project whose code, title or text match a search term. Use it instead'
356
1155
  + ' of paging through list_stories when you know what you are looking for.',
357
1156
  input: { q: z.string().min(1), projectId: z.string().optional() },
358
- availability: anyHost,
1157
+ availability: toolHostHelper.anyHost,
1158
+ annotations: READ_ONLY,
359
1159
  run: async (args, deps) => {
360
- const list = await deps.api.story.list(projectOf(args, deps), { q: args.q });
361
- return ok(renderStories(list.items, list.page, list.total), list);
1160
+ const list = await deps.api.planning.cards.list({
1161
+ ...storyHelper.storyQuery(projectOf(args, deps), { q: args.q }),
1162
+ page: 0,
1163
+ size: STORY_PAGE_SIZE,
1164
+ sort: STORY_ORDER,
1165
+ });
1166
+ return ok(storyHelper.renderStories(list.items, list.page ?? 0, list.total), list);
362
1167
  },
363
1168
  },
364
1169
  {
@@ -367,10 +1172,30 @@ export const catalogue = [
367
1172
  description: 'Add a story in your own words. The platform rewrites it in the form its pipeline needs and'
368
1173
  + ' decides which area of the application it belongs to.',
369
1174
  input: { story: z.string().min(1), projectId: z.string().optional() },
370
- availability: anyHost,
1175
+ availability: toolHostHelper.anyHost,
1176
+ annotations: WRITE,
371
1177
  run: async (args, deps) => {
372
- const created = await deps.api.story.create(projectOf(args, deps), args.story);
373
- return ok('Story created.', created);
1178
+ const project = projectOf(args, deps);
1179
+ // Formatting reads the target's file-backed entities before it persists the story. A local
1180
+ // project therefore needs its connector back after an MCP restart even though this tool does
1181
+ // not itself start a long-running operation.
1182
+ await ensureSession(deps, project);
1183
+ // The narrative goes as written and with no area: rewriting it and deciding the area is the
1184
+ // platform's, done on the way in for a person's story — a connector that guessed an area
1185
+ // would be a second answer the platform then has to overrule.
1186
+ const receipt = await deps.api.planning.execute({
1187
+ action: TransitionAction.Create,
1188
+ card: {
1189
+ kind: WorkcardKind.Card,
1190
+ type: VIABLE_STORY_TYPE,
1191
+ parent: project,
1192
+ title: args.story,
1193
+ fields: { primary: false },
1194
+ },
1195
+ cause: 'connector:create-story',
1196
+ }, { wait: true, timeout: COMMIT_WAIT_MS });
1197
+ const card = receipt.card;
1198
+ return ok(card?.code != null ? `Story ${card.code} created.` : 'Story created.', card ?? receipt.transition);
374
1199
  },
375
1200
  },
376
1201
  {
@@ -378,10 +1203,23 @@ export const catalogue = [
378
1203
  title: 'Reword a user story',
379
1204
  description: 'Change what a story says. Its status is not changed — develop_story does that.',
380
1205
  input: { storyId: z.string(), story: z.string().min(1), projectId: z.string().optional() },
381
- availability: anyHost,
1206
+ availability: toolHostHelper.anyHost,
1207
+ annotations: IDEMPOTENT_WRITE,
382
1208
  run: async (args, deps) => {
383
- const updated = await deps.api.story.update(projectOf(args, deps), args.storyId, args.story);
384
- return ok('Story updated.', updated);
1209
+ const project = projectOf(args, deps);
1210
+ await ensureSession(deps, project);
1211
+ const card = await storyHelper.resolveStory(deps, project, args.storyId);
1212
+ // Only the narrative: the status moves through the story flow and nowhere else, and the area
1213
+ // follows the narrative on the platform's side. Guarded by the head the story was read at, so
1214
+ // a change somebody else made in between is refused rather than overwritten.
1215
+ const receipt = await deps.api.planning.execute({
1216
+ card: card.id,
1217
+ action: TransitionAction.Update,
1218
+ changes: { title: args.story },
1219
+ expectSeq: card.head ?? card.seq,
1220
+ cause: 'connector:update-story',
1221
+ }, { wait: true, timeout: COMMIT_WAIT_MS });
1222
+ return ok('Story updated.', receipt.card ?? card);
385
1223
  },
386
1224
  },
387
1225
  {
@@ -389,9 +1227,20 @@ export const catalogue = [
389
1227
  title: 'Remove a user story',
390
1228
  description: 'Delete a story that has not been implemented. Its placeholder screens are retired.',
391
1229
  input: { storyId: z.string(), projectId: z.string().optional() },
392
- availability: anyHost,
1230
+ availability: toolHostHelper.anyHost,
1231
+ annotations: DESTRUCTIVE,
393
1232
  run: async (args, deps) => {
394
- await deps.api.story.remove(projectOf(args, deps), args.storyId);
1233
+ const project = projectOf(args, deps);
1234
+ // Removing a story also retires its file-backed scaffold, so it has the same local-session
1235
+ // requirement as formatting and development.
1236
+ await ensureSession(deps, project);
1237
+ const card = await storyHelper.resolveStory(deps, project, args.storyId);
1238
+ await deps.api.planning.execute({
1239
+ card: card.id,
1240
+ action: TransitionAction.Delete,
1241
+ cause: 'connector:delete-story',
1242
+ }, { wait: true, timeout: COMMIT_WAIT_MS });
1243
+ await waitForStoryCleanup(deps, project);
395
1244
  return ok('Story deleted.');
396
1245
  },
397
1246
  },
@@ -399,101 +1248,364 @@ export const catalogue = [
399
1248
  name: 'develop_story',
400
1249
  title: 'Implement a user story',
401
1250
  description: 'Ask the platform to design and build one story: its screens, its data, its endpoints, its'
402
- + ' navigation. This is the main event and takes many minutes. Returns a job.',
1251
+ + ' navigation. Stories may extend an existing collection/detail host; editing, deletion and'
1252
+ + ' owned children are contextual controls. This takes many minutes. Returns the story status.',
403
1253
  input: { storyId: z.string(), projectId: z.string().optional() },
404
- availability: anyHost,
1254
+ availability: toolHostHelper.anyHost,
1255
+ annotations: WRITE,
405
1256
  run: async (args, deps) => {
406
1257
  const project = projectOf(args, deps);
407
1258
  await ensureSession(deps, project);
408
- const job = await deps.api.story.develop(project, args.storyId);
409
- return jobResult(job);
1259
+ // Development is not a call of its own: it is the story's `start`, and the platform begins the
1260
+ // run once that move has COMMITTED — which is also where it is refused (the wrong status,
1261
+ // another story in progress, the balance).
1262
+ const { card, status } = await storyHelper.transit(deps, project, args.storyId, ViableStoryTransition.Start, 'connector:develop-story');
1263
+ return storyResult(status, card);
410
1264
  },
411
1265
  },
412
1266
  {
413
- name: 'story_status',
414
- title: 'What happened to a story',
415
- description: 'One story: its status, and the warning explaining a failure.',
1267
+ name: 'reset_story',
1268
+ title: 'Put a story back to planned',
1269
+ description: 'Move a story back to planned from any status — the manual recovery for a story that failed,'
1270
+ + ' is stuck in progress, or was completed and must be developed again. The code already'
1271
+ + ' generated for it stays; develop_story starts it again. Returns the story status.',
416
1272
  input: { storyId: z.string(), projectId: z.string().optional() },
417
- availability: anyHost,
1273
+ availability: toolHostHelper.anyHost,
1274
+ annotations: IDEMPOTENT_WRITE,
418
1275
  run: async (args, deps) => {
419
- const story = await deps.api.story.get(projectOf(args, deps), args.storyId);
420
- return ok(`${story.code ?? story.id} · ${story.status}`
421
- + (story.warning != null ? `\nwarning: ${String(story.warning)}` : ''), story);
1276
+ const project = projectOf(args, deps);
1277
+ await ensureSession(deps, project);
1278
+ const { card, status } = await storyHelper.transit(deps, project, args.storyId, ViableStoryTransition.Reset, 'connector:reset-story');
1279
+ return storyResult(status, card);
422
1280
  },
423
1281
  },
424
1282
  {
425
- name: 'modify_project',
426
- title: 'Ask the agent for an open-ended change',
427
- description: 'Describe a change in words and let the platform\'s own coding agent make it. For anything'
428
- + ' that is not a user story: a rename, a fix, a styling change. Returns a job.',
429
- input: { prompt: z.string().min(1), projectId: z.string().optional() },
430
- availability: anyHost,
1283
+ name: 'complete_story',
1284
+ title: 'Mark a story as completed',
1285
+ description: 'Mark a story in progress as completed — for a story whose work is done although its run never'
1286
+ + ' recorded it. Only a story in progress can be completed, and nothing is generated or stopped'
1287
+ + ' by it. Returns the story status.',
1288
+ input: { storyId: z.string(), projectId: z.string().optional() },
1289
+ availability: toolHostHelper.anyHost,
1290
+ annotations: WRITE,
431
1291
  run: async (args, deps) => {
432
1292
  const project = projectOf(args, deps);
433
1293
  await ensureSession(deps, project);
434
- const job = await deps.api.project.modify(project, args.prompt);
435
- return jobResult(job);
1294
+ const { card, status } = await storyHelper.transit(deps, project, args.storyId, ViableStoryTransition.Complete, 'connector:complete-story');
1295
+ return storyResult(status, card);
436
1296
  },
437
1297
  },
438
1298
  {
439
- name: 'pipeline_status',
440
- title: 'Where a run stopped',
441
- description: 'The steps a run completed and the one it stopped at.',
442
- input: { runId: z.string(), projectId: z.string().optional() },
443
- availability: anyHost,
1299
+ name: 'story_status',
1300
+ title: 'What happened to a story',
1301
+ description: 'One story, its development run, any warning explaining a failure, and whether it is the'
1302
+ + ' project\'s landing gate story — the one a guest starts on the guest home.',
1303
+ input: { storyId: z.string(), projectId: z.string().optional() },
1304
+ availability: toolHostHelper.anyHost,
1305
+ annotations: READ_ONLY,
444
1306
  run: async (args, deps) => {
445
- const state = await deps.api.pipeline.state(projectOf(args, deps), args.runId);
446
- return ok(`${String(state.pipeline)} · ${String(state.status)}`
447
- + (state.step != null ? ` · stopped at ${String(state.step)}` : '')
448
- + (state.error != null ? `\nerror: ${String(state.error)}` : ''), state);
1307
+ const project = projectOf(args, deps);
1308
+ const card = await storyHelper.resolveStory(deps, project, args.storyId);
1309
+ return storyResult(await deps.api.story.status(project, card.id), card);
449
1310
  },
450
1311
  },
451
1312
  {
452
- name: 'resume_pipeline',
453
- title: 'Continue a run that stopped',
454
- description: 'Pick a crashed or interrupted run up where it stopped, rather than starting over. Use it'
455
- + ' after fixing whatever it failed on — a missing database, a connector that dropped.',
1313
+ name: 'start_story_batch',
1314
+ title: 'Implement several stories, one after another',
1315
+ description: 'Hand several planned stories to the platform at once — the web application\'s Run all. It first'
1316
+ + ' orders them for building (a story comes after the records, screens and navigation it builds on),'
1317
+ + ' then implements them one at a time, each exactly as a single development would; a story that'
1318
+ + ' fails does not stop the rest. Leave storyIds out for every planned story. Takes many minutes per'
1319
+ + ' story and returns the batch — follow it with story_batch_status. For one story, use develop_story.',
456
1320
  input: {
457
- runId: z.string(),
1321
+ storyIds: z.array(z.string().min(1)).min(1).max(STORY_BATCH_MAX).optional().describe('The stories to implement, by code or id — planned ones only. Leave it out for every planned story.'),
458
1322
  projectId: z.string().optional(),
459
- from: z.string().optional().describe('Re-enter this step, and everything after it'),
460
1323
  },
461
- availability: anyHost,
1324
+ availability: toolHostHelper.anyHost,
1325
+ annotations: WRITE,
462
1326
  run: async (args, deps) => {
463
1327
  const project = projectOf(args, deps);
1328
+ // Every story of it is a run with the same connector needs as develop_story's.
464
1329
  await ensureSession(deps, project);
465
- const job = await deps.api.pipeline.resume(project, args.runId, {
466
- ...(typeof args.from === 'string' ? { from: args.from } : {}),
467
- });
468
- return jobResult(job);
1330
+ // A code is what list_stories prints; the platform keys a batch on card ids.
1331
+ const refs = Array.isArray(args.storyIds) ? args.storyIds : [];
1332
+ const cards = await Promise.all(refs.map(async (ref) => await storyHelper.resolveStory(deps, project, ref)));
1333
+ const stories = [...new Set(cards.map(card => card.id))];
1334
+ const view = await deps.api.project.batch.start(project, stories.length > 0 ? { stories } : {});
1335
+ return batchResult(view, `Batch ${view.id} started over ${view.stories.length} `
1336
+ + `${view.stories.length === 1 ? 'story' : 'stories'}: the platform orders them, then implements them one at a time.`);
469
1337
  },
470
1338
  },
471
1339
  {
472
- name: 'next_task',
473
- title: 'Get the next model call to perform',
474
- description: 'This session runs the platform\'s model calls on your side. Returns one task with'
475
- + ' instructions for running it in a clean subagent, or says there is nothing yet. Call it'
476
- + ' whenever a job reports "blocked on: model-task", and keep calling until it says none.',
1340
+ name: 'story_batch_status',
1341
+ title: 'Where a story batch stands',
1342
+ description: 'The project\'s latest story batch: its stories in implementation order with the reason for each'
1343
+ + ' place, which are completed, failed, stopped or still queued, the one in progress, and why it'
1344
+ + ' halted when it did.',
1345
+ input: { projectId: z.string().optional() },
1346
+ availability: toolHostHelper.anyHost,
1347
+ annotations: READ_ONLY,
1348
+ run: async (args, deps) => {
1349
+ const project = projectOf(args, deps);
1350
+ const view = await deps.api.project.batch.get(project);
1351
+ if (view == null) {
1352
+ return ok('No story batch has run on this project.\nnext: start_story_batch implements its planned stories one after another', { projectId: project, batch: null });
1353
+ }
1354
+ return batchResult(view);
1355
+ },
1356
+ },
1357
+ {
1358
+ name: 'cancel_story_batch',
1359
+ title: 'Stop a story batch',
1360
+ description: 'Stop the project\'s running story batch: no further story of it starts, and the story it is'
1361
+ + ' implementing now stops at its next step and is ROLLED BACK to where that run started — it'
1362
+ + ' returns to planned. Stories it already completed stay. Ask the user first.',
477
1363
  input: {
478
- maxWaitSec: z.number().int().min(0).max(45).optional(),
479
- taskId: z.string().optional()
480
- .describe('Re-read a task you were already given, if you lost its text.'),
1364
+ projectId: z.string().optional(),
1365
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to stopping the batch and rolling back the story in progress.'),
481
1366
  },
482
- availability: performsModelTasks,
1367
+ availability: toolHostHelper.anyHost,
1368
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
483
1369
  run: async (args, deps) => {
484
- const session = await deps.session();
485
- // Asking for one by id re-reads it rather than taking a new one. A task is handed out once
486
- // and the platform waits on it for up to 45 minutes, so a parent that lost the envelope —
487
- // a compacted conversation, a subagent that died before answering — otherwise has no way
488
- // back to it and the job blocks until the deadline for no reason.
489
- if (typeof args.taskId === 'string') {
490
- const known = session.taskById(args.taskId);
491
- if (known == null) {
1370
+ if (args.confirm !== true)
1371
+ return fail(controlToolHelper.unconfirmedCancel());
1372
+ const project = projectOf(args, deps);
1373
+ const view = await deps.api.project.batch.cancel(project);
1374
+ const current = view.current != null && view.outcomes[view.current] == null ? view.current : null;
1375
+ const stopped = current != null && await stopBatchStory(deps, project, current);
1376
+ return ok(controlToolHelper.renderCancel(view, stopped), {
1377
+ batch: view, ...(stopped ? { stopped: current } : {}),
1378
+ });
1379
+ },
1380
+ },
1381
+ {
1382
+ name: 'modify_project',
1383
+ title: 'Ask the agent for an open-ended change',
1384
+ description: 'Describe a change in words and let the platform\'s own coding agent make it. For anything'
1385
+ + ' that is not a user story: a fix, a styling change. To rename the product, use'
1386
+ + ' rename_project. Returns project status.',
1387
+ input: { prompt: z.string().min(1), projectId: z.string().optional() },
1388
+ availability: toolHostHelper.anyHost,
1389
+ annotations: WRITE,
1390
+ run: async (args, deps) => {
1391
+ const project = projectOf(args, deps);
1392
+ await ensureSession(deps, project);
1393
+ return projectResult(await deps.api.project.modify(project, args.prompt));
1394
+ },
1395
+ },
1396
+ {
1397
+ name: 'steer_agent',
1398
+ title: 'Send the running agent a message',
1399
+ description: 'Add a message to the open-ended change the platform\'s agent is making right now (modify_project,'
1400
+ + ' rename_project) — a correction, a detail, a priority. It is checked like any prompt, read before'
1401
+ + ' the agent\'s next model call, and the agent goes on from there. Only a running open-ended change'
1402
+ + ' takes messages; to ask for a new change, use modify_project.',
1403
+ input: { prompt: z.string().min(1).max(AGENT_STEER_PROMPT_MAX), projectId: z.string().optional() },
1404
+ availability: toolHostHelper.anyHost,
1405
+ annotations: WRITE,
1406
+ run: async (args, deps) => {
1407
+ const project = projectOf(args, deps);
1408
+ // The content check is a model call, the delegating session's parent's to perform.
1409
+ await ensureSession(deps, project);
1410
+ const result = await deps.api.project.steer(project, { prompt: args.prompt });
1411
+ return ok(controlToolHelper.renderSteer(project, result), { projectId: project, ...result });
1412
+ },
1413
+ },
1414
+ {
1415
+ name: 'rename_project',
1416
+ title: 'Rename the project',
1417
+ description: 'Give the product a new name. The platform renames the project record, restates the name in'
1418
+ + ' its specification and vision, and its coding agent restates it through the app\'s code'
1419
+ + ' (titles, copy, docs). The project\'s web address does not change. A new name is paid like'
1420
+ + ' an open-ended change; a new description alone is free. Returns project status.',
1421
+ input: {
1422
+ name: z.string().min(1).max(128),
1423
+ description: z.string().min(1).max(2048).optional(),
1424
+ projectId: z.string().optional(),
1425
+ },
1426
+ availability: toolHostHelper.anyHost,
1427
+ annotations: WRITE,
1428
+ run: async (args, deps) => {
1429
+ const project = projectOf(args, deps);
1430
+ await ensureSession(deps, project);
1431
+ return projectResult(await deps.api.project.rename(project, args.name, args.description));
1432
+ },
1433
+ },
1434
+ {
1435
+ name: 'delete_project',
1436
+ title: 'Delete a project',
1437
+ description: 'Delete a project and everything it owns: its preview and production workloads, its user'
1438
+ + ' stories and documents, its configuration and secrets, and its connector sessions. This'
1439
+ + ' CANNOT be undone — ask the user first. The project is always named: the attached one is'
1440
+ + ' never deleted by default.',
1441
+ input: {
1442
+ projectId: z.string().min(1).describe('The project to delete — always named explicitly.'),
1443
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to deleting this project.'),
1444
+ },
1445
+ availability: toolHostHelper.anyHost,
1446
+ annotations: DESTRUCTIVE,
1447
+ run: async (args, deps) => {
1448
+ if (args.confirm !== true) {
1449
+ return fail('Not done. Deleting a project removes its workloads, stories, documents and configuration,'
1450
+ + ' and cannot be undone. Ask the user, then call it again with confirm: true.');
1451
+ }
1452
+ const project = typeof args.projectId === 'string' && args.projectId !== '' ? args.projectId : null;
1453
+ if (project == null) {
1454
+ return fail('Not done. Name the project to delete with projectId — list_projects shows them.');
1455
+ }
1456
+ // A connector attached to the project says goodbye first: nothing may be delivered to a
1457
+ // project that is going away, and the session would otherwise outlive what it answers for.
1458
+ if (toolHostHelper.sessionCapable(deps.host) && deps.attached() === project) {
1459
+ if (deps.release != null) {
1460
+ await deps.release();
1461
+ }
1462
+ else {
1463
+ await deps.currentSession()?.close();
1464
+ }
1465
+ deps.detach?.();
1466
+ }
1467
+ const deleted = await deps.api.project.destroy(project);
1468
+ return ok(`Deleted ${deleted.name} (${deleted.id}).`
1469
+ + (deps.host.target === ConnectTarget.Local
1470
+ ? ' The files on this machine are kept; the .viable directory still names the deleted project.'
1471
+ : ''), { project: deleted });
1472
+ },
1473
+ },
1474
+ {
1475
+ name: 'unlock_project_agent',
1476
+ title: 'Release a stuck project lock',
1477
+ description: 'Release the project\'s agent lock by force — the recovery when project_status keeps reporting'
1478
+ + ' the agent locked by a run that crashed or will never finish. A run that is still working is'
1479
+ + ' NOT stopped and may collide with whatever starts next, so ask the user first.',
1480
+ input: {
1481
+ projectId: z.string().optional(),
1482
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to releasing the lock.'),
1483
+ },
1484
+ availability: toolHostHelper.anyHost,
1485
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
1486
+ run: async (args, deps) => {
1487
+ if (args.confirm !== true) {
1488
+ return fail('Not done. Releasing the lock does not stop a run that is still working, and the next run'
1489
+ + ' may then collide with it. Ask the user, then call it again with confirm: true.');
1490
+ }
1491
+ const project = projectOf(args, deps);
1492
+ const lock = await deps.api.project.unlock(project);
1493
+ return ok(lock.locked
1494
+ ? `The lock is held again${lock.task != null ? ` (${lock.task})` : ''}: a run took it as soon as it was`
1495
+ + ' released. Read project_status.'
1496
+ : 'The project lock is released. project_status shows the project as it now stands.', { projectId: project, lock: lock });
1497
+ },
1498
+ },
1499
+ {
1500
+ name: 'stop_agent',
1501
+ title: 'Stop what the agent is doing',
1502
+ description: 'Stop the run the platform\'s agent works on now — the web application\'s Stop. A story being'
1503
+ + ' implemented stops at its next step and is ROLLED BACK to where that run started (it returns to'
1504
+ + ' planned); an open-ended change (modify_project, rename_project) stops once the model\'s current'
1505
+ + ' answer arrives and ends with a short summary, its edits kept; scope batch also cancels the story'
1506
+ + ' batch. Every stop but scope run needs the user\'s agreement (confirm: true).',
1507
+ input: {
1508
+ scope: z.enum(STOP_SCOPES).optional().describe('run — an open-ended change: it stops and keeps its edits; story — the story in progress, rolled back;'
1509
+ + ' batch — the story batch: no further story starts and the story in progress is rolled back. Leave it'
1510
+ + ' out to stop whatever runs.'),
1511
+ storyId: z.string().optional().describe('The story in progress a story stop is meant for, by code or id — refused when another one is in progress.'),
1512
+ confirm: z.boolean().optional().describe('Must be true for every scope but run: the user has to have agreed to rolling the story back.'),
1513
+ projectId: z.string().optional(),
1514
+ },
1515
+ availability: toolHostHelper.anyHost,
1516
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
1517
+ run: async (args, deps) => {
1518
+ const scope = STOP_SCOPES.includes(args.scope)
1519
+ ? args.scope : undefined;
1520
+ if (controlToolHelper.stopNeedsConfirm(scope) && args.confirm !== true) {
1521
+ return fail(controlToolHelper.unconfirmedStop(scope));
1522
+ }
1523
+ const project = projectOf(args, deps);
1524
+ const storyId = typeof args.storyId === 'string' && args.storyId.trim() !== ''
1525
+ ? (await storyHelper.resolveStory(deps, project, args.storyId.trim())).id
1526
+ : undefined;
1527
+ const lock = await deps.api.project.stop(project, {
1528
+ ...(scope != null ? { scope } : {}),
1529
+ ...(storyId != null ? { storyId } : {}),
1530
+ ...(args.confirm === true ? { confirm: true } : {}),
1531
+ });
1532
+ return ok(controlToolHelper.renderStop(project, lock, scope), {
1533
+ projectId: project, lock: lock,
1534
+ });
1535
+ },
1536
+ },
1537
+ {
1538
+ name: 'pipeline_status',
1539
+ title: 'Where a run stopped',
1540
+ description: 'The steps a run completed and the one it stopped at.',
1541
+ input: { runId: z.string(), projectId: z.string().optional() },
1542
+ availability: toolHostHelper.anyHost,
1543
+ annotations: READ_ONLY,
1544
+ run: async (args, deps) => {
1545
+ const state = await deps.api.pipeline.state(projectOf(args, deps), args.runId);
1546
+ return pipelineResult(state);
1547
+ },
1548
+ },
1549
+ {
1550
+ name: 'resume_pipeline',
1551
+ title: 'Continue a run that stopped',
1552
+ description: 'Pick a crashed or interrupted run up where it stopped, rather than starting over. Use it'
1553
+ + ' after fixing whatever it failed on — a missing database, a connector that dropped.',
1554
+ input: {
1555
+ runId: z.string(),
1556
+ projectId: z.string().optional(),
1557
+ from: z.string().optional().describe('Re-enter this step, and everything after it'),
1558
+ },
1559
+ availability: toolHostHelper.anyHost,
1560
+ annotations: WRITE,
1561
+ run: async (args, deps) => {
1562
+ const project = projectOf(args, deps);
1563
+ await ensureSession(deps, project);
1564
+ const status = await deps.api.pipeline.resume(project, args.runId, {
1565
+ ...(typeof args.from === 'string' ? { from: args.from } : {}),
1566
+ });
1567
+ return pipelineResult(status);
1568
+ },
1569
+ },
1570
+ {
1571
+ name: 'next_task',
1572
+ title: 'Get the next model call to perform',
1573
+ description: 'In the delegated mode every model call the platform makes for this session is yours to'
1574
+ + ' perform — project drafting, content checks and formatting included. Returns one task with'
1575
+ + ' instructions for running it in a clean subagent, or says there is nothing yet, and reports'
1576
+ + ' any tool call that finished after it answered with a task. Call it whenever a domain status'
1577
+ + ' says it is waiting for a model task, and keep calling until it says none.',
1578
+ input: {
1579
+ maxWaitSec: z.number().int().min(0).max(45).optional(),
1580
+ taskId: z.string().optional()
1581
+ .describe('Re-read a task you were already given, if you lost its text.'),
1582
+ },
1583
+ // Only where this session performs the platform's model calls: in the cloud mode the platform
1584
+ // performs every one of them, a conversion's included, and there is never a task to collect.
1585
+ availability: toolHostHelper.performsModelTasks,
1586
+ annotations: WRITE,
1587
+ run: async (args, deps) => {
1588
+ const session = await deps.session();
1589
+ // Asking for one by id re-reads it rather than taking a new one. A task is handed out once
1590
+ // and the platform waits on it for up to 45 minutes, so a parent that lost the envelope —
1591
+ // a compacted conversation, a subagent that died before answering — otherwise has no way
1592
+ // back to it and the run waits until the deadline for no reason.
1593
+ if (typeof args.taskId === 'string') {
1594
+ const known = session.taskById(args.taskId);
1595
+ if (known == null) {
492
1596
  return fail(`No task ${args.taskId} is outstanding. Call next_task with no id.`);
493
1597
  }
494
- return ok(renderTaskEnvelope(known, { harness: deps.host.harness }), { taskId: known.id });
1598
+ return ok(makeTaskEnvelopeModel(known).renderTaskEnvelope({ harness: deps.host.harness }), { taskId: known.id });
495
1599
  }
496
1600
  const wait = Math.min((args.maxWaitSec ?? 30) * 1000, NEXT_TASK_WAIT_MS);
1601
+ // A tool that answered with a task left its call running; its result is told here, beside
1602
+ // the next task, whichever comes first.
1603
+ const handover = makeHandoverHelper(deps);
1604
+ if (handover.pending()) {
1605
+ const reported = handover.report(await handover.collect(session, wait));
1606
+ if (reported != null)
1607
+ return reported;
1608
+ }
497
1609
  const task = await session.nextTask(wait);
498
1610
  if (task == null) {
499
1611
  const outstanding = session.outstandingTasks();
@@ -504,10 +1616,10 @@ export const catalogue = [
504
1616
  + ` ${outstanding.map(one => one.id).join(', ')}. Submit the answer with`
505
1617
  + ' submit_task_result, or call next_task with that taskId to read it again.');
506
1618
  }
507
- return ok('No task right now. If a job is still running, call wait_for; if it reported'
508
- + ' "blocked on: model-task", call next_task again.');
1619
+ return ok('No task right now. Read the matching project, story, conversion, or pipeline status;'
1620
+ + ' call next_task again only while it reports waiting for a model task.');
509
1621
  }
510
- return ok(renderTaskEnvelope(task, { harness: deps.host.harness }), { taskId: task.id });
1622
+ return ok(makeTaskEnvelopeModel(task).renderTaskEnvelope({ harness: deps.host.harness }), { taskId: task.id });
511
1623
  },
512
1624
  },
513
1625
  {
@@ -519,7 +1631,8 @@ export const catalogue = [
519
1631
  taskId: z.string(),
520
1632
  result: z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())]),
521
1633
  },
522
- availability: performsModelTasks,
1634
+ availability: toolHostHelper.performsModelTasks,
1635
+ annotations: WRITE,
523
1636
  run: async (args, deps) => {
524
1637
  const session = await deps.session();
525
1638
  const pending = session;
@@ -527,37 +1640,1273 @@ export const catalogue = [
527
1640
  if (task == null) {
528
1641
  return fail(`No task ${String(args.taskId)} is waiting. Call next_task for the current one.`);
529
1642
  }
530
- const { result, problem } = parseTaskResult(task, args.result);
1643
+ const { result, problem } = makeTaskEnvelopeModel(task).parseTaskResult(args.result);
531
1644
  if (problem != null) {
532
1645
  // Refused HERE, with the subagent's context still open — a malformed answer that reached
533
1646
  // the platform would cost a whole new task, a new subagent and another wait.
534
1647
  return fail(problem);
535
1648
  }
536
1649
  await session.submitTask(result);
1650
+ // The answer may be what a tool's call was blocked on: its result — or the next model call
1651
+ // it waits on — is this tool's reply.
1652
+ const handover = makeHandoverHelper(deps);
1653
+ if (handover.pending()) {
1654
+ const reported = handover.report(await handover.collect(session, HANDOVER_WAIT_MS), 'Accepted.');
1655
+ if (reported != null)
1656
+ return reported;
1657
+ }
537
1658
  const next = session.pendingTasks();
538
1659
  return ok(next > 0
539
1660
  ? `Accepted. ${next} more task(s) waiting — call next_task.`
540
- : 'Accepted. Call wait_for on the job, or next_task if it is still blocked.');
1661
+ : 'Accepted. Read the matching project, story, conversion, or pipeline status.');
541
1662
  },
542
1663
  },
1664
+ {
1665
+ name: 'next_question',
1666
+ title: 'Get the question the platform is asking',
1667
+ description: 'The platform sometimes needs a decision only the person you are working for can make.'
1668
+ + ' Returns one question to put to them, or says there is none. Domain status responses also'
1669
+ + ' include parked questions directly.',
1670
+ input: {
1671
+ maxWaitSec: z.number().int().min(0).max(45).optional(),
1672
+ questionId: z.string().optional().describe('Re-read a question you were already given.'),
1673
+ projectId: z.string().optional(),
1674
+ },
1675
+ // Not `performsModelTasks`: a question has nothing to do with who performs the model calls,
1676
+ // and a session billed to the platform must still be askable. What it does need is a host
1677
+ // that STAYS — a question is delivered to a connector and answered minutes later.
1678
+ availability: toolHostHelper.sessionCapable,
1679
+ annotations: WRITE,
1680
+ run: async (args, deps) => {
1681
+ const session = await deps.session();
1682
+ const project = projectOrNull(args, deps);
1683
+ // Asking for one by id re-reads it rather than taking a new one — the same reason
1684
+ // `next_task` does: a parent that lost the envelope has no other way back to it.
1685
+ if (typeof args.questionId === 'string') {
1686
+ const known = session.questionById(args.questionId)
1687
+ ?? (project != null ? await parkedQuestion(deps, project) : null);
1688
+ if (known == null || known.id !== args.questionId) {
1689
+ return fail(`No question ${args.questionId} is waiting. Call next_question with no id.`);
1690
+ }
1691
+ return questionResult(known, deps);
1692
+ }
1693
+ const wait = Math.min((args.maxWaitSec ?? 30) * 1000, NEXT_QUESTION_WAIT_MS);
1694
+ const question = await session.nextQuestion(wait);
1695
+ if (question != null)
1696
+ return questionResult(question, deps);
1697
+ const outstanding = session.outstandingQuestions();
1698
+ if (outstanding.length > 0) {
1699
+ return ok(`No new question. ${outstanding.length} already put to you and still unanswered:`
1700
+ + ` ${outstanding.map(one => one.id).join(', ')}. Send the person's answer with`
1701
+ + ' answer_question, or call next_question with that questionId to read it again.');
1702
+ }
1703
+ // Nothing queued here, which does not mean nothing is being asked: a run that parked while
1704
+ // this connector was away is waiting on a question whose operation has long expired.
1705
+ const parked = project != null ? await parkedQuestion(deps, project) : null;
1706
+ if (parked != null) {
1707
+ return questionResult(parked, deps, 'This run parked on a question before this session attached. It is still open:');
1708
+ }
1709
+ return ok('No question right now. Read the matching domain status and answer any pendingInquiry it'
1710
+ + ' returns; call next_question again only while it reports waiting for a person.');
1711
+ },
1712
+ },
1713
+ {
1714
+ name: 'answer_question',
1715
+ title: 'Send back what they decided',
1716
+ description: 'Send the person\'s answer to a question the platform asked. If it is not one the question'
1717
+ + ' allows, this says so and the question stays open. Answer with declined: true when'
1718
+ + ' nobody is available to decide — the platform then assumes and records an assumption.',
1719
+ input: {
1720
+ questionId: z.string(),
1721
+ answer: z.union([z.string(), z.array(z.string())]).optional(),
1722
+ text: z.string().optional().describe('What they said, where the question takes words.'),
1723
+ declined: z.boolean().optional().describe('Nobody could decide this.'),
1724
+ projectId: z.string().optional(),
1725
+ },
1726
+ availability: toolHostHelper.sessionCapable,
1727
+ annotations: WRITE,
1728
+ run: async (args, deps) => {
1729
+ const session = await deps.session();
1730
+ const questionId = args.questionId;
1731
+ const project = projectOrNull(args, deps);
1732
+ // The question this session is holding, if it still is. That is what decides HOW the answer
1733
+ // travels: an operation the platform is waiting on, or the question's own id.
1734
+ const held = session.questionById(questionId);
1735
+ const inquiry = held ?? (project != null ? await parkedQuestion(deps, project) : null);
1736
+ if (inquiry == null || inquiry.id !== questionId) {
1737
+ return fail(`No question ${questionId} is waiting. Call next_question for the current one.`);
1738
+ }
1739
+ const { answer, problem } = makeQuestionEnvelopeModel(inquiry).parseAnswer(args);
1740
+ if (problem != null) {
1741
+ // Refused HERE, with the person still in front of the parent — a mismatch the platform
1742
+ // catches costs a round trip on a question a human has already answered.
1743
+ return fail(problem);
1744
+ }
1745
+ if (held != null) {
1746
+ await session.answerQuestion(answer);
1747
+ }
1748
+ else {
1749
+ if (project == null) {
1750
+ return fail('No project. Call attach_project first, or name one with projectId.');
1751
+ }
1752
+ // The answer resumes a parked run, whose model calls in the delegated mode go to the
1753
+ // project's live session — so it is attached before the platform is told.
1754
+ if (toolHostHelper.performsModelTasks(deps.host))
1755
+ await ensureSession(deps, project);
1756
+ await deps.api.inquiry.answer(project, questionId, answer);
1757
+ }
1758
+ return ok('Recorded. Read the matching project, story, conversion, or pipeline status.');
1759
+ },
1760
+ },
1761
+ {
1762
+ name: 'check_convertible',
1763
+ title: 'Can this application be converted',
1764
+ description: 'Say whether the platform can convert an existing codebase, what it is built with, what will'
1765
+ + ' be limited, and what the next stage will cost. Nothing is provisioned and nothing is'
1766
+ + ' charged. It reads what the intake found, so convert_project starts one first; on a'
1767
+ + ' conversion already under way it also says where that one stands.',
1768
+ input: { projectId: z.string().optional() },
1769
+ availability: toolHostHelper.anyHost,
1770
+ annotations: READ_ONLY,
1771
+ run: async (args, deps) => await answering(deps, 'check_convertible', async () => {
1772
+ const project = projectOf(args, deps);
1773
+ // The platform reads the tree through THIS connector when it lives on this machine, so the
1774
+ // session has to exist before the check runs rather than after it.
1775
+ await ensureSession(deps, project);
1776
+ const check = await deps.api.convert.check(project);
1777
+ return ok(
1778
+ // Read after the check rather than before it: the check is what this tool is for, and the
1779
+ // conversion only decides what to do NEXT with what it said.
1780
+ renderCheck(check, await conversionUnderWay(deps, project)), check);
1781
+ }),
1782
+ },
1783
+ {
1784
+ name: 'convert_project',
1785
+ title: 'Convert an application you already have',
1786
+ description: 'Bring an existing application onto the platform: it reads the code, restores the'
1787
+ + ' specification and the user stories nobody wrote down, and rebuilds it on the platform\'s'
1788
+ + ' stack. The original is kept beside it. Returns conversion status and stops at your decision after'
1789
+ + ` each stage. ${CONVERSION_COST}`,
1790
+ input: {
1791
+ projectId: z.string().optional().describe('Convert into a project that already exists.'),
1792
+ name: z.string().optional(),
1793
+ about: z.string().optional().describe('What the application is for, in your own words.'),
1794
+ repoUrl: z.string().optional()
1795
+ .describe('A GitHub repository to convert. Named, it is converted rather than this directory.'),
1796
+ branch: z.string().optional(),
1797
+ confirm: CONFIRM_INPUT,
1798
+ },
1799
+ availability: toolHostHelper.anyHost,
1800
+ annotations: { ...WRITE, openWorldHint: true },
1801
+ run: async (args, deps) => await answering(deps, 'convert_project', async () => {
1802
+ const confirm = args.confirm === true;
1803
+ // The start is what spends the plan's conversion, so it is the call a confirmation stops —
1804
+ // repeated with the project named, never by filing the conversion again.
1805
+ const start = async (projectId) => await confirming(deps, 'convert_project', { projectId }, confirm, async () => await deps.api.convert.start(projectId, confirm ? { confirm } : {}));
1806
+ const named = typeof args.projectId === 'string' ? args.projectId : deps.attached();
1807
+ if (named != null) {
1808
+ await ensureSession(deps, named);
1809
+ return await start(named);
1810
+ }
1811
+ const repoUrl = typeof args.repoUrl === 'string' && args.repoUrl.trim() !== ''
1812
+ ? args.repoUrl.trim()
1813
+ : null;
1814
+ if (deps.dir == null && repoUrl == null) {
1815
+ return fail('Nothing to convert. Name a project, attach one, run the connector in the directory you'
1816
+ + ' want converted, or give a repoUrl.');
1817
+ }
1818
+ // Nothing is attached here, so the session is an unattached one: in the delegated mode the
1819
+ // platform hands it the checks it runs before the project exists.
1820
+ const sessionId = toolHostHelper.performsModelTasks(deps.host) ? await unattachedSessionId(deps) : undefined;
1821
+ const status = await deps.api.convert.create({
1822
+ ...(sessionId != null ? { sessionId } : {}),
1823
+ ...(typeof args.name === 'string' ? { name: args.name } : {}),
1824
+ ...(typeof args.about === 'string' ? { about: args.about } : {}),
1825
+ // A NAMED repository outranks the directory this connector runs in. A stdio connector
1826
+ // always has a directory, so reading that first would convert the caller's own working
1827
+ // copy instead of the repository it asked for — with `repoUrl` and `branch` dropped in
1828
+ // silence, which is the one outcome nobody could diagnose from the answer.
1829
+ ...(repoUrl == null
1830
+ // The directory IS the origin: nothing is cloned, and the relocation and the template
1831
+ // install run as ordinary operations through this connector.
1832
+ ? { target: ConnectTarget.Local, origin: { kind: OriginKind.Local } }
1833
+ : {
1834
+ target: ConnectTarget.Cloud,
1835
+ origin: {
1836
+ kind: OriginKind.Github,
1837
+ repoUrl,
1838
+ ...(typeof args.branch === 'string' ? { branch: args.branch } : {}),
1839
+ },
1840
+ }),
1841
+ });
1842
+ deps.attach(status.projectId);
1843
+ await ensureSession(deps, status.projectId);
1844
+ return await start(status.projectId);
1845
+ }),
1846
+ },
1847
+ {
1848
+ name: 'proceed_conversion',
1849
+ title: 'Decide what a conversion does next',
1850
+ description: 'A conversion stops after each stage and waits for a decision: analyze what was read,'
1851
+ + ' extract the user stories, implement them, leave it as it stands, retry a stage that'
1852
+ + ' failed, or cancel. With the decision that starts the extraction it also takes the user\'s'
1853
+ + ' edits to the name, description, specification, vision and design system the analysis'
1854
+ + ' drafted — refused with any other decision. Returns the conversion status. A stage the'
1855
+ + ` conversion limit still covers runs at once. ${CONVERSION_COST}`,
1856
+ input: {
1857
+ decision: z.enum(Object.values(ConversionDecision)),
1858
+ projectId: z.string().optional(),
1859
+ note: z.string().optional().describe('What the user said about the decision. Recorded.'),
1860
+ confirm: CONFIRM_INPUT,
1861
+ name: z.string().min(1).max(128).optional().describe(updateInput('The project\'s new name')),
1862
+ description: z.string().optional().describe(updateInput('The project\'s new one-line description')),
1863
+ specification: z.string().optional().describe(updateInput('The edited specification')),
1864
+ vision: z.string().optional().describe(updateInput('The edited vision')),
1865
+ designSystem: z.string().optional().describe(updateInput('The edited design system')),
1866
+ },
1867
+ availability: toolHostHelper.anyHost,
1868
+ annotations: WRITE,
1869
+ run: async (args, deps) => await answering(deps, 'proceed_conversion', async () => {
1870
+ const project = projectOf(args, deps);
1871
+ await ensureSession(deps, project);
1872
+ const decision = args.decision;
1873
+ const note = typeof args.note === 'string' ? args.note : undefined;
1874
+ const confirm = args.confirm === true;
1875
+ // The person's edits travel only where they were given, and only as the fields they named.
1876
+ const update = Object.fromEntries(PROCEED_UPDATE_FIELDS
1877
+ .filter(field => typeof args[field] === 'string')
1878
+ .map(field => [field, args[field]]));
1879
+ const edited = Object.keys(update).length > 0;
1880
+ return await confirming(deps, 'proceed_conversion', {
1881
+ projectId: project, decision, ...(note != null ? { note } : {}), ...update,
1882
+ }, confirm, async () => await deps.api.convert.proceed(project, {
1883
+ decision, ...(note != null ? { note } : {}), ...(confirm ? { confirm } : {}),
1884
+ ...(edited ? { update } : {}),
1885
+ }));
1886
+ }),
1887
+ },
1888
+ {
1889
+ name: 'conversion_status',
1890
+ title: 'Where a conversion stands',
1891
+ description: 'The stage a conversion has reached, what it found, what it has cost so far, whether the'
1892
+ + ' original sources are still there, and what to call next.',
1893
+ input: { projectId: z.string().optional() },
1894
+ availability: toolHostHelper.anyHost,
1895
+ annotations: READ_ONLY,
1896
+ run: async (args, deps) => await answering(deps, 'conversion_status', async () => {
1897
+ const view = await deps.api.convert.status(projectOf(args, deps));
1898
+ return ok(renderConversion(view), view);
1899
+ }),
1900
+ },
1901
+ {
1902
+ name: 'purge_origin',
1903
+ title: 'Delete the converted original',
1904
+ description: 'Delete the original sources a conversion kept beside the converted project, and rewrite'
1905
+ + ' the conversion documents so they stop quoting them. This CANNOT be undone — ask the'
1906
+ + ' user before calling it.',
1907
+ input: {
1908
+ projectId: z.string().optional(),
1909
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to this.'),
1910
+ },
1911
+ availability: toolHostHelper.anyHost,
1912
+ annotations: DESTRUCTIVE,
1913
+ run: async (args, deps) => await answering(deps, 'purge_origin', async () => {
1914
+ if (args.confirm !== true) {
1915
+ return fail('Not done. This deletes the original sources under __viable_converted/ and every'
1916
+ + ' reference to them, and cannot be undone. Ask the user, then call it again with'
1917
+ + ' confirm: true.');
1918
+ }
1919
+ const project = projectOf(args, deps);
1920
+ await ensureSession(deps, project);
1921
+ return conversionResult(await deps.api.convert.purge(project));
1922
+ }),
1923
+ },
543
1924
  {
544
1925
  name: 'list_files',
545
1926
  title: 'The generated project\'s files',
546
- description: 'What the platform generated, for a project whose sources live in its own slot.',
1927
+ description: 'What the platform generated, for a project whose sources live in its own slot. By default the'
1928
+ + ' source files; kind "stories" lists the user-story documents, "meta" the specifications kept'
1929
+ + ' beside the sources (narrowed by category: ba, ux or ui), "all" every metadata document —'
1930
+ + ' docs/, .agents/ and those specifications. read_file reads any path listed.',
1931
+ input: {
1932
+ projectId: z.string().optional(),
1933
+ kind: z.enum([SOURCES_KIND, ...Object.values(MetadataListKind)]).optional()
1934
+ .describe('sources (default), stories, meta or all.'),
1935
+ category: z.enum(Object.values(SpecCategory)).optional()
1936
+ .describe('With kind "meta" only: the analysis (ba), experience (ux) or interface (ui) specifications.'),
1937
+ },
1938
+ availability: toolHostHelper.cloudTarget,
1939
+ annotations: READ_ONLY,
1940
+ run: async (args, deps) => {
1941
+ const project = projectOf(args, deps);
1942
+ const kind = typeof args.kind === 'string' ? args.kind : SOURCES_KIND;
1943
+ if (kind === SOURCES_KIND) {
1944
+ const files = await deps.api.files.list(project);
1945
+ return ok(files.length > 0
1946
+ ? `${files.length} generated file(s):\n${files.join('\n')}`
1947
+ : 'No generated files were found.', { projectId: project, total: files.length, files });
1948
+ }
1949
+ const category = kind === MetadataListKind.Meta && typeof args.category === 'string'
1950
+ ? args.category : undefined;
1951
+ const files = await deps.api.files.meta(project, {
1952
+ kind: kind, ...(category != null ? { category } : {}),
1953
+ });
1954
+ return ok(files.length > 0
1955
+ ? `${files.length} ${kind} document(s)${category != null ? ` (${category})` : ''}:\n${files.join('\n')}`
1956
+ : `No ${kind} documents were found.`, { projectId: project, kind, ...(category != null ? { category } : {}), total: files.length, files });
1957
+ },
1958
+ },
1959
+ {
1960
+ name: 'read_file',
1961
+ title: 'Read one generated file',
1962
+ description: 'The content of one file of a project whose sources live in its own slot — any path list_files'
1963
+ + ' prints, sources and metadata documents alike.',
1964
+ input: {
1965
+ projectId: z.string().optional(),
1966
+ path: z.string().min(1).max(512).describe('The path relative to the project root, as list_files prints it.'),
1967
+ },
1968
+ availability: toolHostHelper.cloudTarget,
1969
+ annotations: READ_ONLY,
1970
+ run: async (args, deps) => {
1971
+ const path = pathOf(args);
1972
+ if (path == null)
1973
+ return fail('Name the file with path — list_files shows them.');
1974
+ const project = projectOf(args, deps);
1975
+ const file = await deps.api.files.get(project, path);
1976
+ const truncated = file.content.length > FILE_READ_CAP;
1977
+ return ok(truncated
1978
+ ? `${file.content.slice(0, FILE_READ_CAP)}\n\n[${path}: the first ${FILE_READ_CAP} of ${file.content.length} characters]`
1979
+ : file.content, { projectId: project, path, length: file.content.length, ...(truncated ? { truncated } : {}) });
1980
+ },
1981
+ },
1982
+ {
1983
+ name: 'write_file',
1984
+ title: 'Write one generated file',
1985
+ description: 'Replace one file\'s whole content in a project whose sources live in its own slot (a new path'
1986
+ + ' creates the file), exactly as the web editor saves it: the content is checked, the file is'
1987
+ + ' written and the preview is rebuilt. A build that fails leaves the last good preview serving'
1988
+ + ' and says why; the write stands either way. Files that decide how the project builds and'
1989
+ + ' starts (manifests, build configuration) are refused. For a change that spans many files,'
1990
+ + ' modify_project lets the platform\'s own coding agent make it.',
1991
+ input: {
1992
+ projectId: z.string().optional(),
1993
+ path: z.string().min(1).max(512).describe('The path relative to the project root.'),
1994
+ content: z.string().describe('The whole new content of the file.'),
1995
+ },
1996
+ availability: toolHostHelper.cloudTarget,
1997
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
1998
+ run: async (args, deps) => {
1999
+ const path = pathOf(args);
2000
+ if (path == null)
2001
+ return fail('Name the file with path.');
2002
+ if (typeof args.content !== 'string')
2003
+ return fail('Give the whole new content of the file as content.');
2004
+ const project = projectOf(args, deps);
2005
+ // The content check is a model call: in the delegated mode it is this session's to perform.
2006
+ await ensureSession(deps, project);
2007
+ const written = await deps.api.files.save(project, path, args.content);
2008
+ return ok(`Wrote ${written.path}.${rebuildNote(written.buildWarning)}`, { projectId: project, ...written });
2009
+ },
2010
+ },
2011
+ {
2012
+ name: 'delete_file',
2013
+ title: 'Delete one generated file',
2014
+ description: 'Delete one file of a project whose sources live in its own slot, then rebuild the preview —'
2015
+ + ' what the web editor\'s delete does. It cannot be undone from here, so ask the user first.'
2016
+ + ' Files that decide how the project builds and starts are refused.',
2017
+ input: {
2018
+ projectId: z.string().optional(),
2019
+ path: z.string().min(1).max(512).describe('The path relative to the project root.'),
2020
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to deleting this file.'),
2021
+ },
2022
+ availability: toolHostHelper.cloudTarget,
2023
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
2024
+ run: async (args, deps) => {
2025
+ if (args.confirm !== true) {
2026
+ return fail('Not done. Deleting a file cannot be undone from here. Ask the user, then call it'
2027
+ + ' again with confirm: true.');
2028
+ }
2029
+ const path = pathOf(args);
2030
+ if (path == null)
2031
+ return fail('Not done. Name the file to delete with path — list_files shows them.');
2032
+ const project = projectOf(args, deps);
2033
+ const removed = await deps.api.files.remove(project, path);
2034
+ return ok(`Deleted ${removed.path}.${rebuildNote(removed.buildWarning)}`, { projectId: project, ...removed });
2035
+ },
2036
+ },
2037
+ {
2038
+ name: 'preview_control',
2039
+ title: 'Start, restart, stop or rebuild the preview',
2040
+ description: 'The web\'s sandbox controls for a project whose sources live in its own slot: start the preview'
2041
+ + ' (creating its workload when it has none), restart it, stop it, or rebuild it — a clean'
2042
+ + ' dependency install and build, the repair for a preview that will not build; a rebuild goes on'
2043
+ + ' after the answer, holding the project lock (project_status shows it). Only the PREVIEW:'
2044
+ + ' the published production site is never touched.',
2045
+ input: {
2046
+ projectId: z.string().optional(),
2047
+ action: z.enum(PREVIEW_ACTIONS).describe('start, restart, stop or rebuild.'),
2048
+ },
2049
+ availability: toolHostHelper.cloudTarget,
2050
+ annotations: IDEMPOTENT_WRITE,
2051
+ run: async (args, deps) => {
2052
+ const project = projectOf(args, deps);
2053
+ switch (args.action) {
2054
+ case 'start':
2055
+ return slotResult('The preview is starting.', project, await deps.api.sandbox.run(project));
2056
+ case 'restart':
2057
+ return slotResult('The preview was restarted.', project, await deps.api.sandbox.restart(project));
2058
+ case 'stop':
2059
+ return slotResult('The preview was stopped.', project, await deps.api.sandbox.stop(project));
2060
+ case 'rebuild':
2061
+ return slotResult('The rebuild has started and goes on after this answer; project_status shows the project'
2062
+ + ' locked until it is done.', project, await deps.api.sandbox.rebuild(project));
2063
+ default:
2064
+ return fail('Say what to do with action: start, restart, stop or rebuild.');
2065
+ }
2066
+ },
2067
+ },
2068
+ {
2069
+ name: 'git_status',
2070
+ title: 'The project\'s git status and GitHub connection',
2071
+ description: 'For a project whose sources live in its own slot: its GitHub connection (who connected it, the'
2072
+ + ' repository it publishes to, whether it still works) and its working tree — branch, last commit,'
2073
+ + ' uncommitted changes and where it stands against GitHub. The tree is read only while the preview'
2074
+ + ' is ready, because git lives in the preview.',
547
2075
  input: { projectId: z.string().optional() },
548
- availability: cloudTarget,
549
- run: async () => fail('Not available yet in this build.'),
2076
+ availability: toolHostHelper.cloudTarget,
2077
+ annotations: READ_ONLY,
2078
+ run: async (args, deps) => {
2079
+ const project = projectOf(args, deps);
2080
+ const state = await deps.api.git.status(project);
2081
+ return ok(gitToolHelper.renderState(state), { projectId: project, ...state });
2082
+ },
2083
+ },
2084
+ {
2085
+ name: 'git_history',
2086
+ title: 'The project\'s latest commits',
2087
+ description: 'The latest commits of a project whose sources live in its own slot, newest first — each with the'
2088
+ + ' hash git_revert names.',
2089
+ input: { projectId: z.string().optional() },
2090
+ availability: toolHostHelper.cloudTarget,
2091
+ annotations: READ_ONLY,
2092
+ run: async (args, deps) => {
2093
+ const project = projectOf(args, deps);
2094
+ const commits = await deps.api.git.log(project);
2095
+ return ok(gitToolHelper.renderHistory(commits), {
2096
+ projectId: project, total: commits.length, commits: commits,
2097
+ });
2098
+ },
2099
+ },
2100
+ {
2101
+ name: 'git_commit',
2102
+ title: 'Commit the project\'s changes',
2103
+ description: 'Commit every uncommitted change of a project whose sources live in its own slot, with a message —'
2104
+ + ' what the web Git dialog\'s commit does. Refused while the platform\'s agent is working on the project.',
2105
+ input: {
2106
+ projectId: z.string().optional(),
2107
+ message: z.string().min(1).max(200).describe('The commit message.'),
2108
+ },
2109
+ availability: toolHostHelper.cloudTarget,
2110
+ annotations: WRITE,
2111
+ run: async (args, deps) => {
2112
+ const message = typeof args.message === 'string' ? args.message.trim() : '';
2113
+ if (message === '')
2114
+ return fail('Not done. Give the commit a message.');
2115
+ const project = projectOf(args, deps);
2116
+ const { commit } = await deps.api.git.commit(project, message);
2117
+ return ok(commit != null ? `Committed ${commit.shortHash} ${commit.subject}.` : 'Nothing to commit — the working tree is clean.', { projectId: project, commit: commit });
2118
+ },
2119
+ },
2120
+ {
2121
+ name: 'git_discard',
2122
+ title: 'Throw away uncommitted changes',
2123
+ description: 'Throw away every uncommitted change of a project whose sources live in its own slot — the working'
2124
+ + ' tree goes back to the last commit. Uncommitted work cannot be recovered afterwards, so ask the'
2125
+ + ' user first.',
2126
+ input: {
2127
+ projectId: z.string().optional(),
2128
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to losing the uncommitted changes.'),
2129
+ },
2130
+ availability: toolHostHelper.cloudTarget,
2131
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
2132
+ run: async (args, deps) => {
2133
+ if (args.confirm !== true) {
2134
+ return fail('Not done. Discarding throws away every uncommitted change for good. Ask the user, then'
2135
+ + ' call it again with confirm: true.');
2136
+ }
2137
+ const project = projectOf(args, deps);
2138
+ const status = await deps.api.git.discard(project);
2139
+ return ok('The uncommitted changes were discarded; the working tree is back at the last commit.', { projectId: project, git: status });
2140
+ },
2141
+ },
2142
+ {
2143
+ name: 'git_revert',
2144
+ title: 'Go back to an earlier commit',
2145
+ description: 'Bring back the tree of one earlier commit of a project whose sources live in its own slot, as a NEW'
2146
+ + ' commit — history is never rewritten, so the later commits stay and can be reverted to in turn.'
2147
+ + ' Uncommitted changes are committed first; the preview is rebuilt and the database migrated to the'
2148
+ + ' older code, which may not match data written since. Ask the user first.',
2149
+ input: {
2150
+ projectId: z.string().optional(),
2151
+ hash: z.string().regex(/^[0-9a-f]{7,40}$/).describe('The commit\'s hash, as git_history prints it.'),
2152
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to going back to this commit.'),
2153
+ },
2154
+ availability: toolHostHelper.cloudTarget,
2155
+ annotations: DESTRUCTIVE,
2156
+ run: async (args, deps) => await answering(deps, 'git_revert', async () => {
2157
+ if (args.confirm !== true) {
2158
+ return fail('Not done. Going back replaces the project\'s files with that commit\'s and migrates its'
2159
+ + ' database. Ask the user, then call it again with confirm: true.');
2160
+ }
2161
+ const hash = typeof args.hash === 'string' ? args.hash.trim().toLowerCase() : '';
2162
+ if (!/^[0-9a-f]{7,40}$/.test(hash))
2163
+ return fail('Not done. Name the commit with hash — git_history shows them.');
2164
+ const project = projectOf(args, deps);
2165
+ const result = await deps.api.git.revert(project, hash);
2166
+ return ok((result.commit != null
2167
+ ? `Went back to ${hash}: the new commit is ${result.commit.shortHash} ${result.commit.subject}. The preview was rebuilt.`
2168
+ : `The tree already matched ${hash}; nothing was committed.`)
2169
+ + (result.dbWarning != null && result.dbWarning !== '' ? `\nThe database migration said: ${result.dbWarning}` : ''), { projectId: project, ...result });
2170
+ }),
2171
+ },
2172
+ {
2173
+ name: 'connect_github',
2174
+ title: 'Connect the project to GitHub',
2175
+ description: 'Begin connecting a project whose sources live in its own slot to the user\'s GitHub account: answers'
2176
+ + ' an address the USER opens in their browser to authorize it. The connection is finished by the'
2177
+ + ' OwlMeans web application when GitHub returns there — never by a tool. Then publish_to_github, or'
2178
+ + ' github_repositories and link_github_origin for an import.',
2179
+ input: { projectId: z.string().optional() },
2180
+ availability: toolHostHelper.cloudTarget,
2181
+ annotations: WRITE,
2182
+ run: async (args, deps) => {
2183
+ const project = projectOf(args, deps);
2184
+ const { authorizeUrl } = await deps.api.github.authorize(project);
2185
+ return ok(gitToolHelper.renderAuthorize(authorizeUrl), { projectId: project, authorizeUrl });
2186
+ },
2187
+ },
2188
+ {
2189
+ name: 'publish_to_github',
2190
+ title: 'Publish the project to a GitHub repository',
2191
+ description: 'Publish a project whose sources live in its own slot to GitHub: a NEW repository (repoName, private'
2192
+ + ' unless private is false) or one that exists (owner and repo). The project\'s commits are pushed'
2193
+ + ' there and github_sync works from then on. Never onto the repository a converted project came from'
2194
+ + ' while its original sources are still kept.',
2195
+ input: {
2196
+ projectId: z.string().optional(),
2197
+ repoName: z.string().min(1).max(100).optional().describe('A new repository\'s name; the project\'s code when left out.'),
2198
+ private: z.boolean().optional().describe('A new repository is private unless this is false.'),
2199
+ owner: z.string().min(1).max(100).optional().describe('With repo: publish to this existing repository.'),
2200
+ repo: z.string().min(1).max(100).optional().describe('With owner: publish to this existing repository.'),
2201
+ },
2202
+ availability: toolHostHelper.cloudTarget,
2203
+ annotations: WRITE,
2204
+ run: async (args, deps) => await answering(deps, 'publish_to_github', async () => {
2205
+ const owner = typeof args.owner === 'string' ? args.owner.trim() : '';
2206
+ const repo = typeof args.repo === 'string' ? args.repo.trim() : '';
2207
+ if ((owner === '') !== (repo === '')) {
2208
+ return fail('Not done. An existing repository is named by both owner and repo.');
2209
+ }
2210
+ const project = projectOf(args, deps);
2211
+ const connection = await deps.api.github.publish(project, owner !== ''
2212
+ ? { existing: { owner, repo } }
2213
+ : {
2214
+ ...(typeof args.repoName === 'string' && args.repoName.trim() !== '' ? { repoName: args.repoName.trim() } : {}),
2215
+ ...(typeof args.private === 'boolean' ? { private: args.private } : {}),
2216
+ });
2217
+ return ok(`Published to ${connection.repoFullName ?? 'GitHub'}${connection.repoUrl != null ? ` (${connection.repoUrl})` : ''}.`
2218
+ + ' github_sync pushes and pulls from now on.', { projectId: project, connection: connection });
2219
+ }),
2220
+ },
2221
+ {
2222
+ name: 'github_sync',
2223
+ title: 'Push to or pull from GitHub',
2224
+ description: 'For a project published to GitHub (publish_to_github): push its commits there, or pull GitHub\'s'
2225
+ + ' into it — a pulled tree is checked before the preview is rebuilt from it, and a conflict aborts the'
2226
+ + ' merge leaving nothing changed. Never a force push. Refused while the platform\'s agent is working'
2227
+ + ' on the project.',
2228
+ input: {
2229
+ projectId: z.string().optional(),
2230
+ direction: z.enum(GIT_SYNC_DIRECTIONS).describe('push or pull.'),
2231
+ },
2232
+ availability: toolHostHelper.cloudTarget,
2233
+ annotations: WRITE,
2234
+ run: async (args, deps) => {
2235
+ const direction = args.direction;
2236
+ if (direction !== 'push' && direction !== 'pull')
2237
+ return fail('Say which way with direction: push or pull.');
2238
+ const project = projectOf(args, deps);
2239
+ const result = direction === 'push' ? await deps.api.github.push(project) : await deps.api.github.pull(project);
2240
+ const refused = result.status !== 'ok' && result.status !== 'up-to-date';
2241
+ return {
2242
+ ...ok(gitToolHelper.renderSync(direction, result), { projectId: project, direction, ...result }),
2243
+ ...(refused ? { isError: true } : {}),
2244
+ };
2245
+ },
2246
+ },
2247
+ {
2248
+ name: 'disconnect_github',
2249
+ title: 'Disconnect the project from GitHub',
2250
+ description: 'Forget a project\'s GitHub connection — its stored access and the repository it publishes to. The'
2251
+ + ' repository on GitHub is left exactly as it is; connecting again needs the user to authorize again.'
2252
+ + ' Ask the user first.',
2253
+ input: {
2254
+ projectId: z.string().optional(),
2255
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to disconnecting GitHub.'),
2256
+ },
2257
+ availability: toolHostHelper.cloudTarget,
2258
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
2259
+ run: async (args, deps) => {
2260
+ if (args.confirm !== true) {
2261
+ return fail('Not done. Disconnecting forgets the stored GitHub access; reconnecting needs the user in a'
2262
+ + ' browser again. Ask the user, then call it again with confirm: true.');
2263
+ }
2264
+ const project = projectOf(args, deps);
2265
+ await deps.api.github.disconnect(project);
2266
+ return ok('GitHub is disconnected from this project. The repository on GitHub is untouched.', { projectId: project, connection: null });
2267
+ },
2268
+ },
2269
+ {
2270
+ name: 'github_repositories',
2271
+ title: 'The user\'s GitHub repositories and branches',
2272
+ description: 'For a project connected to GitHub: one page of the repositories the user can reach (search narrows'
2273
+ + ' the page that came back), or — with owner and repo — one page of that repository\'s branches.'
2274
+ + ' What link_github_origin and publish_to_github name.',
2275
+ input: {
2276
+ projectId: z.string().optional(),
2277
+ owner: z.string().min(1).max(100).optional().describe('With repo: list this repository\'s branches.'),
2278
+ repo: z.string().min(1).max(100).optional().describe('With owner: list this repository\'s branches.'),
2279
+ page: z.number().int().min(1).max(1000).optional().describe('The page, from 1.'),
2280
+ search: z.string().max(200).optional().describe('Keep the repositories of this page whose name contains it.'),
2281
+ },
2282
+ availability: toolHostHelper.cloudTarget,
2283
+ annotations: READ_ONLY,
2284
+ run: async (args, deps) => {
2285
+ const owner = typeof args.owner === 'string' ? args.owner.trim() : '';
2286
+ const repo = typeof args.repo === 'string' ? args.repo.trim() : '';
2287
+ if ((owner === '') !== (repo === '')) {
2288
+ return fail('Name a repository by both owner and repo to list its branches, or neither to list repositories.');
2289
+ }
2290
+ const page = typeof args.page === 'number' ? args.page : undefined;
2291
+ const project = projectOf(args, deps);
2292
+ if (owner !== '') {
2293
+ const list = await deps.api.github.branches(project, { owner, repo, ...(page != null ? { page } : {}) });
2294
+ return ok(gitToolHelper.renderBranches(`${owner}/${repo}`, list), {
2295
+ projectId: project, owner, repo, ...list,
2296
+ });
2297
+ }
2298
+ const search = typeof args.search === 'string' && args.search.trim() !== '' ? args.search.trim() : undefined;
2299
+ const list = await deps.api.github.repos(project, {
2300
+ ...(page != null ? { page } : {}), ...(search != null ? { search } : {}),
2301
+ });
2302
+ return ok(gitToolHelper.renderRepos(list, search), { projectId: project, ...list });
2303
+ },
2304
+ },
2305
+ {
2306
+ name: 'link_github_origin',
2307
+ title: 'Record the repository a project was imported from',
2308
+ description: 'For a project being imported from GitHub: record which repository (and branch — its default one'
2309
+ + ' when left out) it comes FROM, so the conversion clones it. Nothing is pushed or published, and'
2310
+ + ' the repository it publishes TO stays publish_to_github\'s choice.',
2311
+ input: {
2312
+ projectId: z.string().optional(),
2313
+ owner: z.string().min(1).max(100),
2314
+ repo: z.string().min(1).max(100),
2315
+ branch: z.string().min(1).max(200).optional().describe('The branch to import; the repository\'s default when left out.'),
2316
+ },
2317
+ availability: toolHostHelper.cloudTarget,
2318
+ annotations: IDEMPOTENT_WRITE,
2319
+ run: async (args, deps) => {
2320
+ const owner = typeof args.owner === 'string' ? args.owner.trim() : '';
2321
+ const repo = typeof args.repo === 'string' ? args.repo.trim() : '';
2322
+ if (owner === '' || repo === '')
2323
+ return fail('Not done. Name the repository by owner and repo.');
2324
+ const branch = typeof args.branch === 'string' && args.branch.trim() !== '' ? args.branch.trim() : undefined;
2325
+ const project = projectOf(args, deps);
2326
+ const { connection } = await deps.api.github.link(project, { owner, repo, ...(branch != null ? { branch } : {}) });
2327
+ return ok(`The project now comes from ${owner}/${repo}${branch != null ? ` (${branch})` : ' (its default branch)'}.`
2328
+ + ` ${gitToolHelper.renderConnection(connection)}`, { projectId: project, owner, repo, ...(branch != null ? { branch } : {}), connection: connection });
2329
+ },
2330
+ },
2331
+ {
2332
+ name: 'production_status',
2333
+ title: 'The project\'s published production site',
2334
+ description: 'For a project whose sources live in its own slot: its PRODUCTION site — whether it was published,'
2335
+ + ' its status (building, deploying, live, stopped or in error) and address, and its custom domain'
2336
+ + ' with the DNS records still to be created. Never the preview: project_status shows that.',
2337
+ input: { projectId: z.string().optional() },
2338
+ availability: toolHostHelper.cloudTarget,
2339
+ annotations: READ_ONLY,
2340
+ run: async (args, deps) => {
2341
+ const project = projectOf(args, deps);
2342
+ const status = await deps.api.production.status(project);
2343
+ return ok(productionToolHelper.renderStatus(status), { projectId: project, ...status });
2344
+ },
2345
+ },
2346
+ {
2347
+ name: 'publish_production',
2348
+ title: 'Publish the project as its production site',
2349
+ description: 'Build the current sources of a project whose sources live in its own slot and deploy them as its'
2350
+ + ' PRODUCTION site, which real users reach — replacing whatever was published before. It holds one'
2351
+ + ' of the plan\'s published sites (a republish holds the one it has). The build goes on after the'
2352
+ + ' answer; production_status shows when it is live. Ask the user first.',
2353
+ input: {
2354
+ projectId: z.string().optional(),
2355
+ confirm: z.boolean().describe('Must be true. The user has to have agreed to publishing (or replacing) the live site.'),
2356
+ },
2357
+ availability: toolHostHelper.cloudTarget,
2358
+ annotations: DESTRUCTIVE,
2359
+ run: async (args, deps) => await answering(deps, 'publish_production', async () => {
2360
+ if (args.confirm !== true) {
2361
+ return fail('Not done. Publishing replaces the live production site real users reach, and uses one of'
2362
+ + ' the plan\'s published sites. Ask the user, then call it again with confirm: true.');
2363
+ }
2364
+ const project = projectOf(args, deps);
2365
+ const status = await deps.api.production.publish(project);
2366
+ return ok(`The publish has started.\n${productionToolHelper.renderStatus(status)}`, { projectId: project, ...status });
2367
+ }),
2368
+ },
2369
+ {
2370
+ name: 'production_control',
2371
+ title: 'Restart or stop the production site',
2372
+ description: 'Restart the PRODUCTION site of a project whose sources live in its own slot, or stop it. A restart'
2373
+ + ' redeploys the last published build (a stopped site comes back and takes one of the plan\'s'
2374
+ + ' published sites again); a stop takes the site offline for its users and gives that published site'
2375
+ + ' back — its data is kept. Never the preview: that is preview_control.',
2376
+ input: {
2377
+ projectId: z.string().optional(),
2378
+ action: z.enum(PRODUCTION_ACTIONS).describe('restart or stop.'),
2379
+ },
2380
+ availability: toolHostHelper.cloudTarget,
2381
+ annotations: { ...DESTRUCTIVE, idempotentHint: true },
2382
+ run: async (args, deps) => await answering(deps, 'production_control', async () => {
2383
+ const project = projectOf(args, deps);
2384
+ switch (args.action) {
2385
+ case 'restart': {
2386
+ const status = await deps.api.production.restart(project);
2387
+ return ok(`The production site is restarting.\n${productionToolHelper.renderStatus(status)}`, { projectId: project, ...status });
2388
+ }
2389
+ case 'stop': {
2390
+ const status = await deps.api.production.stop(project);
2391
+ return ok(`The production site was stopped.\n${productionToolHelper.renderStatus(status)}`, { projectId: project, ...status });
2392
+ }
2393
+ default:
2394
+ return fail('Say what to do with action: restart or stop.');
2395
+ }
2396
+ }),
2397
+ },
2398
+ {
2399
+ name: 'custom_domain',
2400
+ title: 'Attach, verify or detach a custom domain',
2401
+ description: 'The custom domain of a project\'s PRODUCTION site (a paid feature): attach a domain the user owns —'
2402
+ + ' answered with the two DNS records they create at their DNS provider —, verify asks the provider'
2403
+ + ' again once the records are in place, detach removes it (the generated address keeps serving).'
2404
+ + ' A domain attached to another project is refused.',
2405
+ input: {
2406
+ projectId: z.string().optional(),
2407
+ action: z.enum(DOMAIN_ACTIONS).describe('attach, verify or detach.'),
2408
+ domain: z.string().min(4).max(253).optional().describe('With attach: the host name, e.g. shop.example.com.'),
2409
+ },
2410
+ availability: toolHostHelper.cloudTarget,
2411
+ annotations: WRITE,
2412
+ run: async (args, deps) => await answering(deps, 'custom_domain', async () => {
2413
+ const project = projectOf(args, deps);
2414
+ const result = (domain, lead) => ok(`${lead}\n${productionToolHelper.renderDomain(domain)}`, { projectId: project, domain: domain });
2415
+ switch (args.action) {
2416
+ case 'attach': {
2417
+ const domain = typeof args.domain === 'string' ? args.domain.trim().toLowerCase() : '';
2418
+ if (domain === '')
2419
+ return fail('Not done. Name the domain to attach, e.g. shop.example.com.');
2420
+ const attached = await deps.api.production.domain.attach(project, domain);
2421
+ if (attached == null) {
2422
+ return fail('Not done. The project has no production site yet — publish_production first, then attach the domain.');
2423
+ }
2424
+ return result(attached, `${domain} is attached.`);
2425
+ }
2426
+ case 'verify':
2427
+ return result(await deps.api.production.domain.verify(project), 'Asked the provider again.');
2428
+ case 'detach':
2429
+ return result(await deps.api.production.domain.detach(project), 'The custom domain is detached.');
2430
+ default:
2431
+ return fail('Say what to do with action: attach, verify or detach.');
2432
+ }
2433
+ }),
2434
+ },
2435
+ {
2436
+ name: 'production_auth',
2437
+ title: 'The production site\'s standalone sign-in',
2438
+ description: 'The sign-in (OIDC) configuration a self-hosted copy of a project\'s production site uses — the'
2439
+ + ' issuer, the client id and the owner\'s own redirect addresses (a paid feature). The client secret'
2440
+ + ' is never shown here: only whether it is set; the user reads it in the OwlMeans web application.',
2441
+ input: { projectId: z.string().optional() },
2442
+ availability: toolHostHelper.cloudTarget,
2443
+ annotations: READ_ONLY,
2444
+ run: async (args, deps) => await answering(deps, 'production_auth', async () => {
2445
+ const project = projectOf(args, deps);
2446
+ const auth = await deps.api.production.auth(project);
2447
+ return ok(productionToolHelper.renderAuth(auth), { projectId: project, ...auth });
2448
+ }),
2449
+ },
2450
+ {
2451
+ name: 'set_production_redirects',
2452
+ title: 'Set the standalone sign-in\'s redirect addresses',
2453
+ description: 'Replace the owner\'s own redirect addresses of a project\'s production sign-in — the callbacks of a'
2454
+ + ' self-hosted copy (a paid feature). The whole list: an empty one clears it. Applied on the next'
2455
+ + ' publish_production.',
2456
+ input: {
2457
+ projectId: z.string().optional(),
2458
+ redirects: z.array(z.string().max(2048)).max(32).describe('Every address, e.g. https://my-host.example.com/dispatcher.'),
2459
+ },
2460
+ availability: toolHostHelper.cloudTarget,
2461
+ annotations: IDEMPOTENT_WRITE,
2462
+ run: async (args, deps) => await answering(deps, 'set_production_redirects', async () => {
2463
+ if (!Array.isArray(args.redirects) || args.redirects.some(uri => typeof uri !== 'string')) {
2464
+ return fail('Not done. Give redirects as a list of addresses — an empty list clears them.');
2465
+ }
2466
+ const project = projectOf(args, deps);
2467
+ const { redirectUris } = await deps.api.production.setRedirects(project, args.redirects);
2468
+ return ok(productionToolHelper.renderRedirects(redirectUris), { projectId: project, redirectUris });
2469
+ }),
2470
+ },
2471
+ {
2472
+ name: 'app_users',
2473
+ title: 'The generated app\'s users',
2474
+ description: 'The end users of a project\'s generated application — the people who signed in to it or were invited,'
2475
+ + ' each with their role, state and profileId. all: true lists every end user of every application this'
2476
+ + ' organization owns instead (read-only). Works for local projects too: the sign-in is the platform\'s.',
2477
+ input: {
2478
+ projectId: z.string().optional(),
2479
+ scope: APP_SCOPE_INPUT,
2480
+ all: z.boolean().optional().describe('Every application of the organization instead of one project\'s.'),
2481
+ },
2482
+ availability: toolHostHelper.anyHost,
2483
+ annotations: READ_ONLY,
2484
+ run: async (args, deps) => await answering(deps, 'app_users', async () => {
2485
+ if (args.all === true) {
2486
+ const users = await deps.api.iam.organizationUsers();
2487
+ return ok(iamToolHelper.renderUsers(users, 'End users of every application of this organization'), { users: users });
2488
+ }
2489
+ const project = projectOf(args, deps);
2490
+ const scope = scopeOf(args);
2491
+ const users = await deps.api.iam.users.list(project, scope);
2492
+ return ok(iamToolHelper.renderUsers(users, 'The application\'s users'), { projectId: project, ...(scope != null ? { scope } : {}), users: users });
2493
+ }),
2494
+ },
2495
+ {
2496
+ name: 'manage_app_user',
2497
+ title: 'Invite, update or remove an app user',
2498
+ description: 'Change an end user of a project\'s generated application: invite one by e-mail (find-or-create),'
2499
+ + ' update their name, role or disabled state, or remove them from this application — their account and'
2500
+ + ' other applications stay. A removal needs the user\'s agreement (confirm: true).',
2501
+ input: {
2502
+ projectId: z.string().optional(),
2503
+ scope: APP_SCOPE_INPUT,
2504
+ action: z.enum(APP_USER_ACTIONS).describe('invite, update or remove.'),
2505
+ email: z.string().min(3).max(254).optional().describe('With invite: the person\'s e-mail.'),
2506
+ name: z.string().min(1).max(128).optional(),
2507
+ role: z.string().min(1).max(32).optional(),
2508
+ disabled: z.boolean().optional().describe('With update: true blocks their sign-in, false lets them back.'),
2509
+ profileId: z.string().min(1).max(256).optional().describe('With update or remove: the user (app_users lists them).'),
2510
+ confirm: z.boolean().optional().describe('With remove: must be true — the user agreed.'),
2511
+ },
2512
+ availability: toolHostHelper.anyHost,
2513
+ annotations: DESTRUCTIVE,
2514
+ run: async (args, deps) => await answering(deps, 'manage_app_user', async () => {
2515
+ const refused = iamToolHelper.missing('manage_app_user', args);
2516
+ if (refused != null)
2517
+ return fail(refused);
2518
+ const project = projectOf(args, deps);
2519
+ const scope = scopeOf(args);
2520
+ const profileId = typeof args.profileId === 'string' ? args.profileId.trim() : '';
2521
+ const person = {
2522
+ ...(typeof args.name === 'string' ? { name: args.name } : {}),
2523
+ ...(typeof args.role === 'string' ? { role: args.role } : {}),
2524
+ ...(scope != null ? { scope } : {}),
2525
+ };
2526
+ switch (args.action) {
2527
+ case 'invite': {
2528
+ const user = await deps.api.iam.users.invite(project, { email: args.email.trim(), ...person });
2529
+ return ok(`Invited: ${iamToolHelper.renderUser(user)}`, { projectId: project, user: user });
2530
+ }
2531
+ case 'update': {
2532
+ const user = await deps.api.iam.users.update(project, profileId, {
2533
+ ...person, ...(typeof args.disabled === 'boolean' ? { disabled: args.disabled } : {}),
2534
+ });
2535
+ return ok(`Updated: ${iamToolHelper.renderUser(user)}`, { projectId: project, user: user });
2536
+ }
2537
+ default:
2538
+ await deps.api.iam.users.remove(project, profileId, scope);
2539
+ return ok(`Removed ${profileId} from the application; their account and other applications stay.`, { projectId: project, profileId, removed: true });
2540
+ }
2541
+ }),
2542
+ },
2543
+ {
2544
+ name: 'app_permissions',
2545
+ title: 'The generated app\'s permissions',
2546
+ description: 'The permissions a project\'s generated application declares — each with its area, who holds it by'
2547
+ + ' default and whether its grants are bound to one organization — and whether the app\'s operators'
2548
+ + ' and users act inside organizations at all.',
2549
+ input: { projectId: z.string().optional(), scope: APP_SCOPE_INPUT },
2550
+ availability: toolHostHelper.anyHost,
2551
+ annotations: READ_ONLY,
2552
+ run: async (args, deps) => await answering(deps, 'app_permissions', async () => {
2553
+ const project = projectOf(args, deps);
2554
+ const permissions = await deps.api.iam.permissions(project, scopeOf(args));
2555
+ return ok(iamToolHelper.renderPermissions(permissions), { projectId: project, permissions: permissions });
2556
+ }),
2557
+ },
2558
+ {
2559
+ name: 'set_app_permission_default',
2560
+ title: 'Set a permission\'s default holder',
2561
+ description: 'Change who holds one of the generated application\'s permissions without a grant (defaultClass:'
2562
+ + ' none, user — every signed-in user, only for a user-area permission —, member, owner…) or whether'
2563
+ + ' its grants are bound to one organization (entityScoped). An omitted setting is kept; the platform\'s'
2564
+ + ' own permissions are not editable.',
2565
+ input: {
2566
+ projectId: z.string().optional(),
2567
+ scope: APP_SCOPE_INPUT,
2568
+ permission: z.string().min(1).max(128).describe('The permission by name (app_permissions lists them).'),
2569
+ defaultClass: z.enum(Object.values(IamDefaultClass)).optional(),
2570
+ entityScoped: z.boolean().optional(),
2571
+ },
2572
+ availability: toolHostHelper.anyHost,
2573
+ annotations: IDEMPOTENT_WRITE,
2574
+ run: async (args, deps) => await answering(deps, 'set_app_permission_default', async () => {
2575
+ const refused = iamToolHelper.missing('set_app_permission_default', args);
2576
+ if (refused != null)
2577
+ return fail(refused);
2578
+ const project = projectOf(args, deps);
2579
+ const scope = scopeOf(args);
2580
+ const definition = await deps.api.iam.setDefault(project, {
2581
+ permission: args.permission.trim(),
2582
+ ...(typeof args.defaultClass === 'string' ? { defaultClass: args.defaultClass } : {}),
2583
+ ...(typeof args.entityScoped === 'boolean' ? { entityScoped: args.entityScoped } : {}),
2584
+ ...(scope != null ? { scope } : {}),
2585
+ });
2586
+ return ok(`Saved: ${iamToolHelper.renderDefinition(definition)}`, { projectId: project, definition: definition });
2587
+ }),
2588
+ },
2589
+ {
2590
+ name: 'app_grants',
2591
+ title: 'Who holds which permission in the generated app',
2592
+ description: 'The grants of a project\'s generated application — every one, one user\'s (profileId; with'
2593
+ + ' entitySlug, only those bound to that organization) or one group\'s (group with its entitySlug) —'
2594
+ + ' each with where it comes from: direct, a default, through a group, or a group\'s bundle. Also lists'
2595
+ + ' what a subject\'s own restriction withholds from what it would inherit, marked restricted: true —'
2596
+ + ' those are NOT held.',
2597
+ input: {
2598
+ projectId: z.string().optional(),
2599
+ scope: APP_SCOPE_INPUT,
2600
+ profileId: z.string().min(1).max(256).optional(),
2601
+ group: z.string().min(1).max(128).optional().describe('A group key; needs entitySlug.'),
2602
+ entitySlug: z.string().min(1).max(63).optional(),
2603
+ },
2604
+ availability: toolHostHelper.anyHost,
2605
+ annotations: READ_ONLY,
2606
+ run: async (args, deps) => await answering(deps, 'app_grants', async () => {
2607
+ const refused = iamToolHelper.missing('app_grants', args);
2608
+ if (refused != null)
2609
+ return fail(refused);
2610
+ const project = projectOf(args, deps);
2611
+ const scope = scopeOf(args);
2612
+ const grants = await deps.api.iam.grants.list(project, {
2613
+ ...(typeof args.profileId === 'string' ? { profileId: args.profileId.trim() } : {}),
2614
+ ...(typeof args.group === 'string' ? { group: args.group.trim() } : {}),
2615
+ ...(typeof args.entitySlug === 'string' ? { entitySlug: args.entitySlug.trim() } : {}),
2616
+ ...(scope != null ? { scope } : {}),
2617
+ });
2618
+ return ok(iamToolHelper.renderGrants(grants), { projectId: project, grants: grants });
2619
+ }),
2620
+ },
2621
+ {
2622
+ name: 'manage_app_grant',
2623
+ title: 'Grant or revoke a permission in the generated app',
2624
+ description: 'Grant one of the generated application\'s permissions to exactly ONE subject — a user (profileId;'
2625
+ + ' entitySlug binds the grant to that organization when the permission is organization-bound) or a'
2626
+ + ' group (group key with its entitySlug) — or revoke it. Revoking a whole permission RESTRICTS the'
2627
+ + ' subject: it also loses what it inherits (a default, a group, a bundle) until a revoke with mode:'
2628
+ + ' inherit takes back only its own grant or restriction, so it holds what it inherits again. A'
2629
+ + ' per-record permission takes resources (record ids) or covers every record; mode names which form'
2630
+ + ' when both exist.',
2631
+ input: {
2632
+ projectId: z.string().optional(),
2633
+ scope: APP_SCOPE_INPUT,
2634
+ action: z.enum(APP_GRANT_ACTIONS).describe('assign or revoke.'),
2635
+ permission: z.string().min(1).max(128).describe('The permission by name (app_permissions lists them).'),
2636
+ profileId: z.string().min(1).max(256).optional().describe('The user — or name a group instead.'),
2637
+ group: z.string().min(1).max(128).optional().describe('The group key — needs entitySlug.'),
2638
+ entitySlug: z.string().min(1).max(63).optional(),
2639
+ resources: z.array(z.string().min(1).max(128)).max(256).optional().describe('Record ids of a per-record permission.'),
2640
+ mode: z.enum(Object.values(IamGrantMode)).optional()
2641
+ .describe('blanket (every record) or resources (only those listed); revoke only: inherit (take back'
2642
+ + ' the subject\'s own grant or restriction) or all (every form, a revoke\'s default).'),
2643
+ },
2644
+ availability: toolHostHelper.anyHost,
2645
+ annotations: DESTRUCTIVE,
2646
+ run: async (args, deps) => await answering(deps, 'manage_app_grant', async () => {
2647
+ const refused = iamToolHelper.missing('manage_app_grant', args);
2648
+ if (refused != null)
2649
+ return fail(refused);
2650
+ const project = projectOf(args, deps);
2651
+ const body = iamToolHelper.grantBody(args);
2652
+ if (args.action === 'assign') {
2653
+ const grant = await deps.api.iam.grants.assign(project, body);
2654
+ return ok(`Granted: ${iamToolHelper.renderGrant(grant)}`, { projectId: project, grant: grant });
2655
+ }
2656
+ await deps.api.iam.grants.revoke(project, body);
2657
+ return ok(iamToolHelper.renderRevoke(body), { projectId: project, revoked: body });
2658
+ }),
2659
+ },
2660
+ {
2661
+ name: 'app_organizations',
2662
+ title: 'The generated app\'s organizations',
2663
+ description: 'The organizations a project\'s generated application has people in, by entitySlug, with their'
2664
+ + ' member counts — or, with entitySlug, that organization\'s members (owner flag, groups, profileId).',
2665
+ input: { projectId: z.string().optional(), scope: APP_SCOPE_INPUT, entitySlug: z.string().min(1).max(63).optional() },
2666
+ availability: toolHostHelper.anyHost,
2667
+ annotations: READ_ONLY,
2668
+ run: async (args, deps) => await answering(deps, 'app_organizations', async () => {
2669
+ const project = projectOf(args, deps);
2670
+ const scope = scopeOf(args);
2671
+ const entitySlug = typeof args.entitySlug === 'string' ? args.entitySlug.trim() : '';
2672
+ if (entitySlug !== '') {
2673
+ const members = await deps.api.iam.organizations.members(project, entitySlug, scope);
2674
+ return ok(iamToolHelper.renderMembers(members, `Members of ${entitySlug}`), { projectId: project, entitySlug, members: members });
2675
+ }
2676
+ const organizations = await deps.api.iam.organizations.list(project, scope);
2677
+ return ok(iamToolHelper.renderOrganizations(organizations), { projectId: project, organizations: organizations });
2678
+ }),
2679
+ },
2680
+ {
2681
+ name: 'manage_app_organization',
2682
+ title: 'Rename an organization or manage its members',
2683
+ description: 'Change an organization of a project\'s generated application (by entitySlug): rename it (its title —'
2684
+ + ' the slug stays), add a member by e-mail (owner optional), update a member\'s owner flag or groups,'
2685
+ + ' or remove a member. The last owner always stays one.',
2686
+ input: {
2687
+ projectId: z.string().optional(),
2688
+ scope: APP_SCOPE_INPUT,
2689
+ action: z.enum(APP_ORGANIZATION_ACTIONS).describe('rename, add-member, update-member or remove-member.'),
2690
+ entitySlug: z.string().min(1).max(63).describe('The organization (app_organizations lists them).'),
2691
+ title: z.string().min(1).max(256).optional().describe('With rename: the new title.'),
2692
+ email: z.string().min(3).max(254).optional().describe('With add-member.'),
2693
+ name: z.string().min(1).max(128).optional(),
2694
+ owner: z.boolean().optional(),
2695
+ profileId: z.string().min(1).max(256).optional().describe('With update-member or remove-member.'),
2696
+ groups: z.array(z.string().min(1).max(128)).max(64).optional().describe('With update-member: the member\'s whole group list.'),
2697
+ },
2698
+ availability: toolHostHelper.anyHost,
2699
+ annotations: DESTRUCTIVE,
2700
+ run: async (args, deps) => await answering(deps, 'manage_app_organization', async () => {
2701
+ const refused = iamToolHelper.missing('manage_app_organization', args);
2702
+ if (refused != null)
2703
+ return fail(refused);
2704
+ const project = projectOf(args, deps);
2705
+ const scope = scopeOf(args);
2706
+ const scoped = scope != null ? { scope } : {};
2707
+ const entitySlug = args.entitySlug.trim();
2708
+ const profileId = typeof args.profileId === 'string' ? args.profileId.trim() : '';
2709
+ switch (args.action) {
2710
+ case 'rename': {
2711
+ const organization = await deps.api.iam.organizations.update(project, entitySlug, { title: args.title.trim(), ...scoped });
2712
+ return ok(`Renamed: ${iamToolHelper.renderOrganization(organization)}`, { projectId: project, organization: organization });
2713
+ }
2714
+ case 'add-member': {
2715
+ const member = await deps.api.iam.organizations.addMember(project, entitySlug, {
2716
+ email: args.email.trim(),
2717
+ ...(typeof args.name === 'string' ? { name: args.name } : {}),
2718
+ ...(typeof args.owner === 'boolean' ? { owner: args.owner } : {}),
2719
+ ...scoped,
2720
+ });
2721
+ return ok(`Added to ${entitySlug}: ${iamToolHelper.renderMember(member)}`, { projectId: project, entitySlug, member: member });
2722
+ }
2723
+ case 'update-member': {
2724
+ const member = await deps.api.iam.organizations.updateMember(project, entitySlug, profileId, {
2725
+ ...(typeof args.owner === 'boolean' ? { owner: args.owner } : {}),
2726
+ ...(Array.isArray(args.groups) ? { groups: args.groups } : {}),
2727
+ ...scoped,
2728
+ });
2729
+ return ok(`Updated in ${entitySlug}: ${iamToolHelper.renderMember(member)}`, { projectId: project, entitySlug, member: member });
2730
+ }
2731
+ default:
2732
+ await deps.api.iam.organizations.removeMember(project, entitySlug, profileId, scope);
2733
+ return ok(`Removed ${profileId} from ${entitySlug}.`, { projectId: project, entitySlug, profileId, removed: true });
2734
+ }
2735
+ }),
2736
+ },
2737
+ {
2738
+ name: 'app_groups',
2739
+ title: 'The groups of an organization in the generated app',
2740
+ description: 'The groups of one organization of a project\'s generated application (by entitySlug), each with what'
2741
+ + ' it grants and its member count — or, with group, that group\'s members.',
2742
+ input: {
2743
+ projectId: z.string().optional(),
2744
+ scope: APP_SCOPE_INPUT,
2745
+ entitySlug: z.string().min(1).max(63).describe('The organization (app_organizations lists them).'),
2746
+ group: z.string().min(1).max(128).optional().describe('A group key: list its members instead.'),
2747
+ },
2748
+ availability: toolHostHelper.anyHost,
2749
+ annotations: READ_ONLY,
2750
+ run: async (args, deps) => await answering(deps, 'app_groups', async () => {
2751
+ const entitySlug = typeof args.entitySlug === 'string' ? args.entitySlug.trim() : '';
2752
+ if (entitySlug === '')
2753
+ return fail('Not done. Name the organization by entitySlug (app_organizations lists them).');
2754
+ const project = projectOf(args, deps);
2755
+ const scope = scopeOf(args);
2756
+ const group = typeof args.group === 'string' ? args.group.trim() : '';
2757
+ if (group !== '') {
2758
+ const members = await deps.api.iam.groups.members(project, entitySlug, group, scope);
2759
+ return ok(iamToolHelper.renderMembers(members, `Members of group ${group} of ${entitySlug}`), { projectId: project, entitySlug, group, members: members });
2760
+ }
2761
+ const groups = await deps.api.iam.groups.list(project, entitySlug, scope);
2762
+ return ok(iamToolHelper.renderGroups(groups, entitySlug), { projectId: project, entitySlug, groups: groups });
2763
+ }),
2764
+ },
2765
+ {
2766
+ name: 'manage_app_group',
2767
+ title: 'Create, change or delete a group, or manage its members',
2768
+ description: 'Change a group of an organization of a project\'s generated application: create it (find-or-create'
2769
+ + ' by key), update its title or what it grants (bundles — the WHOLE list, it replaces the group\'s),'
2770
+ + ' delete it (the user agreed: confirm: true), or add and remove members by profileIds. The platform\'s'
2771
+ + ' own staff group keeps its members, bundles and existence — those changes are refused; its permissions'
2772
+ + ' are granted and restricted with manage_app_grant (group).',
2773
+ input: {
2774
+ projectId: z.string().optional(),
2775
+ scope: APP_SCOPE_INPUT,
2776
+ action: z.enum(APP_GROUP_ACTIONS).describe('create, update, delete, add-members or remove-members.'),
2777
+ entitySlug: z.string().min(1).max(63).describe('The group\'s organization.'),
2778
+ group: z.string().min(1).max(128).describe('The group key.'),
2779
+ title: z.string().min(1).max(256).optional(),
2780
+ bundles: z.array(BUNDLE_INPUT).max(32).optional().describe('With update: everything the group grants.'),
2781
+ profileIds: z.array(z.string().min(1).max(256)).max(256).optional().describe('With add-members or remove-members.'),
2782
+ confirm: z.boolean().optional().describe('With delete: must be true — the user agreed.'),
2783
+ },
2784
+ availability: toolHostHelper.anyHost,
2785
+ annotations: DESTRUCTIVE,
2786
+ run: async (args, deps) => await answering(deps, 'manage_app_group', async () => {
2787
+ const refused = iamToolHelper.missing('manage_app_group', args);
2788
+ if (refused != null)
2789
+ return fail(refused);
2790
+ const project = projectOf(args, deps);
2791
+ const scope = scopeOf(args);
2792
+ const scoped = scope != null ? { scope } : {};
2793
+ const entitySlug = args.entitySlug.trim();
2794
+ const key = args.group.trim();
2795
+ const title = typeof args.title === 'string' ? { title: args.title.trim() } : {};
2796
+ switch (args.action) {
2797
+ case 'create': {
2798
+ const group = await deps.api.iam.groups.ensure(project, entitySlug, { key, ...title, ...scoped });
2799
+ return ok(`Group ready: ${iamToolHelper.renderGroup(group)}`, { projectId: project, group: group });
2800
+ }
2801
+ case 'update': {
2802
+ const group = await deps.api.iam.groups.update(project, entitySlug, key, {
2803
+ ...title, ...(Array.isArray(args.bundles) ? { bundles: args.bundles } : {}), ...scoped,
2804
+ });
2805
+ return ok(`Updated: ${iamToolHelper.renderGroup(group)}`, { projectId: project, group: group });
2806
+ }
2807
+ case 'delete':
2808
+ await deps.api.iam.groups.remove(project, entitySlug, key, scope);
2809
+ return ok(`Deleted group ${key} of ${entitySlug}.`, { projectId: project, entitySlug, group: key, deleted: true });
2810
+ default: {
2811
+ const profileIds = args.profileIds;
2812
+ if (args.action === 'add-members') {
2813
+ await deps.api.iam.groups.addMembers(project, entitySlug, key, profileIds, scope);
2814
+ }
2815
+ else {
2816
+ await deps.api.iam.groups.removeMembers(project, entitySlug, key, profileIds, scope);
2817
+ }
2818
+ return ok(`${args.action === 'add-members' ? 'Added' : 'Removed'} ${profileIds.length} member(s)`
2819
+ + ` ${args.action === 'add-members' ? 'to' : 'from'} group ${key} of ${entitySlug}.`, { projectId: project, entitySlug, group: key, profileIds });
2820
+ }
2821
+ }
2822
+ }),
2823
+ },
2824
+ {
2825
+ name: 'list_slots',
2826
+ title: 'The organization\'s workloads',
2827
+ description: 'Every workload of every project of this organization — previews, published production sites'
2828
+ + ' and local targets — with its status, its address and the warnings it carries.',
2829
+ input: {},
2830
+ availability: toolHostHelper.anyHost,
2831
+ annotations: READ_ONLY,
2832
+ run: async (_args, deps) => {
2833
+ const slots = await deps.api.slot.list();
2834
+ return ok(slots.length > 0
2835
+ ? `${slots.length} workload(s):\n${slots.map(slot => statusTextHelper.renderSlot(slot)).join('\n')}`
2836
+ : 'This organization has no workloads yet.', { total: slots.length, slots: slots });
2837
+ },
2838
+ },
2839
+ {
2840
+ name: 'project_activity',
2841
+ title: 'What the project\'s agent is doing',
2842
+ description: 'Follow a project\'s agent while it works: run starts and stops, pipeline steps, story and project'
2843
+ + ' card changes, preview status changes, conversion moves, uncommitted-change proposals and tool'
2844
+ + ' outcomes — and with detail: thinking, the model\'s own words. Without after it answers the latest'
2845
+ + ' entries and a cursor; call it again with after: <cursor> and wait (up to 20 s) to read what comes'
2846
+ + ' next. A feed, not a status: project_status says where the work stands.',
2847
+ input: {
2848
+ projectId: z.string().optional(),
2849
+ after: FEED_AFTER_INPUT,
2850
+ limit: z.number().int().min(1).max(200).optional().describe('At most this many entries (default 50).'),
2851
+ wait: FEED_WAIT_INPUT,
2852
+ detail: z.enum(FEED_DETAILS).optional().describe('progress (default) — what happened; thinking — also the model\'s own words.'),
2853
+ },
2854
+ availability: toolHostHelper.anyHost,
2855
+ annotations: READ_ONLY,
2856
+ run: async (args, deps) => await answering(deps, 'project_activity', async () => {
2857
+ const project = projectOf(args, deps);
2858
+ const detail = FEED_DETAILS.includes(args.detail)
2859
+ ? args.detail : undefined;
2860
+ const page = await deps.api.project.activity(project, { ...feedQueryOf(args), ...(detail != null ? { detail } : {}) });
2861
+ const next = feedToolHelper.nextCall('project_activity', { projectId: project, ...(detail != null ? { detail } : {}) }, page.cursor);
2862
+ return ok(feedToolHelper.renderPage(page, { empty: 'No new activity.', next }), { projectId: project, cursor: page.cursor, gap: page.gap, entries: page.entries });
2863
+ }),
2864
+ },
2865
+ {
2866
+ name: 'notifications',
2867
+ title: 'The organization\'s notices',
2868
+ description: 'The notices the web application shows this organization as pop-ups — a run that stopped because'
2869
+ + ' the credits or the plan allowance ran out. Without after it answers the latest ones and a cursor;'
2870
+ + ' call it again with after: <cursor> and wait (up to 20 s) to read new ones.',
2871
+ input: { after: FEED_AFTER_INPUT, wait: FEED_WAIT_INPUT },
2872
+ availability: toolHostHelper.anyHost,
2873
+ annotations: READ_ONLY,
2874
+ run: async (args, deps) => await answering(deps, 'notifications', async () => {
2875
+ const page = await deps.api.account.notifications(feedQueryOf(args));
2876
+ return ok(feedToolHelper.renderPage(page, { empty: 'No new notices.', next: feedToolHelper.nextCall('notifications', {}, page.cursor) }), { cursor: page.cursor, gap: page.gap, entries: page.entries });
2877
+ }),
2878
+ },
2879
+ {
2880
+ name: 'file_changes',
2881
+ title: 'Files changed in the project\'s preview',
2882
+ description: 'For a project whose sources live in its own slot: the files added, changed and removed in its tree'
2883
+ + ' — by its agent, a git operation or a write — after a cursor. Without after it answers the latest'
2884
+ + ' changes and a cursor; call it again with after: <cursor> and wait (up to 20 s). It starts watching'
2885
+ + ' the tree when nothing watches it yet; a preview that is not running records nothing.',
2886
+ input: { projectId: z.string().optional(), after: FEED_AFTER_INPUT, wait: FEED_WAIT_INPUT },
2887
+ availability: toolHostHelper.cloudTarget,
2888
+ annotations: READ_ONLY,
2889
+ run: async (args, deps) => await answering(deps, 'file_changes', async () => {
2890
+ const project = projectOf(args, deps);
2891
+ const page = await deps.api.files.changes(project, feedQueryOf(args));
2892
+ const text = feedToolHelper.renderPage(page, {
2893
+ empty: 'No new file changes.', next: feedToolHelper.nextCall('file_changes', { projectId: project }, page.cursor),
2894
+ });
2895
+ return ok(page.watching ? text : 'The preview is not running, so no file change is being recorded —'
2896
+ + ` preview_control { action: start } starts it.\n${text}`, { projectId: project, cursor: page.cursor, gap: page.gap, watching: page.watching, entries: page.entries });
2897
+ }),
550
2898
  },
551
2899
  {
552
2900
  name: 'local_status',
553
2901
  title: 'Is this machine ready to run the app',
554
2902
  description: 'What the generated project still needs before it can start here, and whether it is running.',
555
2903
  input: {},
556
- availability: localTarget,
2904
+ availability: toolHostHelper.localTarget,
2905
+ annotations: READ_ONLY,
557
2906
  run: async (_args, deps) => {
558
2907
  if (deps.dir == null)
559
2908
  return fail('No project directory.');
560
- const [env, running] = await Promise.all([envStatus(deps.dir), localStatus(deps.dir)]);
2909
+ const [env, running] = await Promise.all([makeProjectEnvHelper(deps.dir).envStatus(), makeLocalRunHelper(deps.dir).localStatus()]);
561
2910
  const lines = [
562
2911
  `directory: ${deps.dir}`,
563
2912
  env.missing.length < 1
@@ -577,11 +2926,12 @@ export const catalogue = [
577
2926
  description: 'Build the generated project and start it: the API on 3000, the app on 5173. The build takes'
578
2927
  + ' a while — this returns once it has started, and local_status says when it is up.',
579
2928
  input: { build: z.boolean().optional().describe('Build first. Defaults to true.') },
580
- availability: localTarget,
2929
+ availability: toolHostHelper.localTarget,
2930
+ annotations: WRITE,
581
2931
  run: async (args, deps) => {
582
2932
  if (deps.dir == null)
583
2933
  return fail('No project directory.');
584
- const env = await envStatus(deps.dir);
2934
+ const env = await makeProjectEnvHelper(deps.dir).envStatus();
585
2935
  if (env.missing.length > 0) {
586
2936
  // Refused rather than started: an app launched without a database URL fails at boot with a
587
2937
  // message about a connection, which sends the reader looking for a database that was never
@@ -589,7 +2939,7 @@ export const catalogue = [
589
2939
  return fail(`The project still needs ${env.missing.join(', ')} in its .env. Call local_setup_guide,`
590
2940
  + ' which reports what is missing and what to ask the user for.');
591
2941
  }
592
- const started = await runLocal(deps.dir, { build: args.build !== false });
2942
+ const started = await makeLocalRunHelper(deps.dir).runLocal({ build: args.build !== false });
593
2943
  return ok(`Started. The app is at http://localhost:${started.web}, its API at`
594
2944
  + ` http://localhost:${started.api}. Call local_status if it does not answer yet.`, started);
595
2945
  },
@@ -600,11 +2950,12 @@ export const catalogue = [
600
2950
  description: 'Stop the application run_local started here: its API, its worker and the server holding'
601
2951
  + ' port 5173. Leaves the project files alone.',
602
2952
  input: {},
603
- availability: localTarget,
2953
+ availability: toolHostHelper.localTarget,
2954
+ annotations: IDEMPOTENT_WRITE,
604
2955
  run: async (_args, deps) => {
605
2956
  if (deps.dir == null)
606
2957
  return fail('No project directory.');
607
- await stopLocal(deps.dir);
2958
+ await makeLocalRunHelper(deps.dir).stopLocal();
608
2959
  return ok('Stopped.');
609
2960
  },
610
2961
  },
@@ -614,12 +2965,13 @@ export const catalogue = [
614
2965
  description: 'What to install and what to put in the project\'s .env so the generated application can'
615
2966
  + ' start here — a database, optionally a queue store, and the ports it will use.',
616
2967
  input: {},
617
- availability: localTarget,
2968
+ availability: toolHostHelper.localTarget,
2969
+ annotations: READ_ONLY,
618
2970
  run: async (_args, deps) => {
619
2971
  if (deps.dir == null)
620
2972
  return fail('No project directory.');
621
- const report = await readSetupReport(deps.dir);
622
- return ok(renderSetupGuide(report), report);
2973
+ const report = await setupHelper.readSetupReport(deps.dir);
2974
+ return ok(makeSetupReportModel(report).renderSetupGuide(), report);
623
2975
  },
624
2976
  },
625
2977
  {
@@ -635,7 +2987,8 @@ export const catalogue = [
635
2987
  .describe('redis://host:port — only for a project with a background worker'),
636
2988
  schema: z.string().optional().describe('Postgres schema. Defaults to "app".'),
637
2989
  },
638
- availability: localTarget,
2990
+ availability: toolHostHelper.localTarget,
2991
+ annotations: IDEMPOTENT_WRITE,
639
2992
  run: async (args, deps) => {
640
2993
  if (deps.dir == null)
641
2994
  return fail('No project directory.');
@@ -644,7 +2997,7 @@ export const catalogue = [
644
2997
  if (databaseUrl === '' && valkeyUrl === '') {
645
2998
  return fail('Give at least one of databaseUrl or valkeyUrl — the value the user supplied.');
646
2999
  }
647
- const { written, file } = await setUserEnv(deps.dir, {
3000
+ const { written, file } = await setupHelper.setUserEnv(deps.dir, {
648
3001
  ...(databaseUrl !== '' ? { DATABASE_URL: databaseUrl } : {}),
649
3002
  ...(databaseUrl !== ''
650
3003
  ? { DATABASE_SCHEMA: typeof args.schema === 'string' && args.schema.trim() !== ''
@@ -656,7 +3009,7 @@ export const catalogue = [
656
3009
  // Probed straight away, because a value that does not answer is the whole failure this tool
657
3010
  // exists to prevent: accepted silently, it surfaces minutes later as a boot error about a
658
3011
  // connection, and the reader goes looking for a database rather than for a typo.
659
- const report = await readSetupReport(deps.dir);
3012
+ const report = await setupHelper.readSetupReport(deps.dir);
660
3013
  const lines = [`Wrote ${written.join(', ')} into ${file}, outside the managed block.`];
661
3014
  if (databaseUrl !== '') {
662
3015
  lines.push(report.database.reachable
@@ -671,21 +3024,20 @@ export const catalogue = [
671
3024
  if (!report.envIgnored) {
672
3025
  lines.push('That file is not git-ignored. Tell the user, so a credential does not reach a commit.');
673
3026
  }
674
- lines.push(missingServices(report).length < 1
3027
+ const missing = makeSetupReportModel(report).missingServices();
3028
+ lines.push(missing.length < 1
675
3029
  ? 'Nothing else is missing — run_local can build and start the application.'
676
- : 'Still missing: ' + missingServices(report).join(', ') + '. Call local_setup_guide again.');
3030
+ : 'Still missing: ' + missing.join(', ') + '. Call local_setup_guide again.');
677
3031
  return ok(lines.join('\n'), report);
678
3032
  },
679
3033
  },
680
3034
  ];
681
- const renderStories = (items, page, total) => items.length < 1
682
- ? 'No stories.'
683
- : `${items.length} of ${total} (page ${page}):\n`
684
- + items.map(item => ` ${String(item.code ?? item.id)} · ${String(item.status)}`
685
- + `${item.primary === true ? ' · primary' : ''}`
686
- + `${item.area != null ? ` · ${String(item.area)}` : ''}\n ${String(item.story).slice(0, 160)}`).join('\n');
687
- /** The tools a given host actually offers. */
688
- export const visibleTools = (host) => catalogue.filter(tool => tool.availability(host));
689
- export const toolByName = (name) => catalogue.find(tool => tool.name === name);
690
- export { ToolHostKind };
3035
+ export const createCatalogueHelper = () => {
3036
+ const visibleTools = (host) => [...catalogue, ...(host.extensions ?? [])].filter(tool => tool.availability(host));
3037
+ const toolByName = (name) => catalogue.find(tool => tool.name === name);
3038
+ return { visibleTools, toolByName };
3039
+ };
3040
+ export const catalogueHelper = createCatalogueHelper();
3041
+ /** @deprecated compat:factory-refactor — use `catalogueHelper.visibleTools(…)` */
3042
+ export const visibleTools = (host) => catalogueHelper.visibleTools(host);
691
3043
  //# sourceMappingURL=catalogue.js.map