makersclaw 0.8.0 → 0.8.1

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 (401) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/r/activity-log.json +52 -0
  4. package/r/agent-changes.json +51 -0
  5. package/r/agent-chat.json +53 -0
  6. package/r/agent-column.json +85 -0
  7. package/r/agent-dock.json +57 -0
  8. package/r/agent-kit.json +49 -0
  9. package/r/agent-marks.json +49 -0
  10. package/r/agent-pane.json +93 -0
  11. package/r/ai-action.json +56 -0
  12. package/r/annotate.json +3 -3
  13. package/r/approval-request.json +2 -2
  14. package/r/approval.json +54 -0
  15. package/r/ask-bar.json +50 -0
  16. package/r/audio-player.json +2 -2
  17. package/r/auth-form.json +2 -2
  18. package/r/automation-editor.json +66 -0
  19. package/r/automation-list.json +57 -0
  20. package/r/automation-model.json +118 -0
  21. package/r/automations-screen.json +71 -0
  22. package/r/board.json +2 -2
  23. package/r/calendar-view.json +3 -3
  24. package/r/cart.json +2 -2
  25. package/r/chart.json +2 -2
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +2 -2
  28. package/r/code-editor.json +2 -2
  29. package/r/comment-box.json +3 -3
  30. package/r/connect-card.json +51 -0
  31. package/r/contact-section.json +2 -2
  32. package/r/core.json +13 -7
  33. package/r/cost.json +51 -0
  34. package/r/csv-import.json +3 -3
  35. package/r/data-picker.json +60 -0
  36. package/r/data-table.json +2 -2
  37. package/r/diagram.json +2 -2
  38. package/r/doc-editor.json +2 -2
  39. package/r/doc-page.json +3 -3
  40. package/r/email-render.json +2 -2
  41. package/r/faq.json +2 -2
  42. package/r/file-upload.json +2 -2
  43. package/r/first-run.json +63 -0
  44. package/r/fix-note.json +57 -0
  45. package/r/flow-canvas.json +2 -2
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +2 -2
  48. package/r/gantt.json +3 -3
  49. package/r/job-panel.json +2 -2
  50. package/r/json-editor.json +3 -3
  51. package/r/library-shelf.json +56 -0
  52. package/r/map.json +2 -2
  53. package/r/margin-notes.json +67 -0
  54. package/r/menubar.json +2 -2
  55. package/r/mind-map.json +3 -3
  56. package/r/navigation-menu.json +2 -2
  57. package/r/needs-you.json +55 -0
  58. package/r/order-detail.json +2 -2
  59. package/r/org-chart.json +3 -3
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +2 -2
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +2 -2
  65. package/r/product-detail.json +2 -2
  66. package/r/product-grid.json +3 -3
  67. package/r/record-form.json +2 -2
  68. package/r/record-grid.json +52 -0
  69. package/r/record-notes.json +4 -4
  70. package/r/registry.json +4086 -1512
  71. package/r/run-history.json +52 -0
  72. package/r/run-view.json +80 -0
  73. package/r/schedule-card.json +54 -0
  74. package/r/schedule-engine.json +79 -0
  75. package/r/schedule-gantt.json +83 -0
  76. package/r/schedule-ruler.json +64 -0
  77. package/r/search-dialog.json +2 -2
  78. package/r/settings-list.json +2 -2
  79. package/r/setup-checklist.json +2 -2
  80. package/r/share-dialog.json +2 -2
  81. package/r/sidebar.json +2 -2
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +2 -2
  84. package/r/source-receipt.json +52 -0
  85. package/r/spreadsheet.json +3 -3
  86. package/r/stat-cards.json +3 -3
  87. package/r/step-picker.json +53 -0
  88. package/r/step-settings.json +72 -0
  89. package/r/suggestion.json +58 -0
  90. package/r/sum-bar.json +64 -0
  91. package/r/task-list.json +3 -3
  92. package/r/task-model.json +2 -2
  93. package/r/testimonials.json +2 -2
  94. package/r/tier-map.json +67 -0
  95. package/r/time-grid.json +73 -0
  96. package/r/time-strip.json +64 -0
  97. package/r/top-bar.json +66 -0
  98. package/r/transaction-list.json +2 -2
  99. package/r/trigger-picker.json +61 -0
  100. package/r/whiteboard.json +3 -3
  101. package/r/workflow-view.json +55 -0
  102. package/registry/blocks/activity-log/README.md +112 -0
  103. package/registry/blocks/activity-log/activity-log.jsx +351 -0
  104. package/registry/blocks/activity-log/block.json +33 -0
  105. package/registry/blocks/activity-log/demo.jsx +48 -0
  106. package/registry/blocks/activity-log/index.js +4 -0
  107. package/registry/blocks/agent-changes/README.md +94 -0
  108. package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
  109. package/registry/blocks/agent-changes/block.json +28 -0
  110. package/registry/blocks/agent-changes/demo.jsx +39 -0
  111. package/registry/blocks/agent-changes/index.js +3 -0
  112. package/registry/blocks/agent-chat/README.md +145 -0
  113. package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
  114. package/registry/blocks/agent-chat/block.json +30 -0
  115. package/registry/blocks/agent-chat/demo.jsx +165 -0
  116. package/registry/blocks/agent-chat/index.js +3 -0
  117. package/registry/blocks/agent-column/README.md +197 -0
  118. package/registry/blocks/agent-column/agent-column.jsx +492 -0
  119. package/registry/blocks/agent-column/block.json +67 -0
  120. package/registry/blocks/agent-column/column-picker.jsx +163 -0
  121. package/registry/blocks/agent-column/demo.jsx +50 -0
  122. package/registry/blocks/agent-column/index.js +7 -0
  123. package/registry/blocks/agent-column/model.js +164 -0
  124. package/registry/blocks/agent-column/receipt.jsx +483 -0
  125. package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
  126. package/registry/blocks/agent-dock/README.md +210 -0
  127. package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
  128. package/registry/blocks/agent-dock/block.json +47 -0
  129. package/registry/blocks/agent-dock/demo.jsx +153 -0
  130. package/registry/blocks/agent-dock/dock-model.js +118 -0
  131. package/registry/blocks/agent-dock/index.js +5 -0
  132. package/registry/blocks/agent-kit/README.md +73 -0
  133. package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
  134. package/registry/blocks/agent-kit/block.json +10 -0
  135. package/registry/blocks/agent-kit/demo.jsx +35 -0
  136. package/registry/blocks/agent-kit/index.js +3 -0
  137. package/registry/blocks/agent-marks/README.md +80 -0
  138. package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
  139. package/registry/blocks/agent-marks/block.json +27 -0
  140. package/registry/blocks/agent-marks/demo.jsx +49 -0
  141. package/registry/blocks/agent-marks/index.js +3 -0
  142. package/registry/blocks/agent-pane/README.md +225 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
  144. package/registry/blocks/agent-pane/block.json +41 -0
  145. package/registry/blocks/agent-pane/composer.jsx +6 -0
  146. package/registry/blocks/agent-pane/demo.jsx +233 -0
  147. package/registry/blocks/agent-pane/dock-context.js +10 -0
  148. package/registry/blocks/agent-pane/index.js +4 -0
  149. package/registry/blocks/agent-pane/prose.jsx +84 -0
  150. package/registry/blocks/agent-pane/questions.jsx +134 -0
  151. package/registry/blocks/agent-pane/transcript.jsx +268 -0
  152. package/registry/blocks/agent-pane/work.jsx +368 -0
  153. package/registry/blocks/ai-action/README.md +94 -0
  154. package/registry/blocks/ai-action/ai-action.jsx +188 -0
  155. package/registry/blocks/ai-action/block.json +34 -0
  156. package/registry/blocks/ai-action/demo.jsx +93 -0
  157. package/registry/blocks/ai-action/index.js +3 -0
  158. package/registry/blocks/approval/README.md +145 -0
  159. package/registry/blocks/approval/approval.jsx +163 -0
  160. package/registry/blocks/approval/block.json +31 -0
  161. package/registry/blocks/approval/demo.jsx +121 -0
  162. package/registry/blocks/approval/index.js +3 -0
  163. package/registry/blocks/ask-bar/README.md +105 -0
  164. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  165. package/registry/blocks/ask-bar/block.json +26 -0
  166. package/registry/blocks/ask-bar/demo.jsx +78 -0
  167. package/registry/blocks/ask-bar/index.js +3 -0
  168. package/registry/blocks/automation-editor/README.md +291 -0
  169. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  170. package/registry/blocks/automation-editor/block.json +39 -0
  171. package/registry/blocks/automation-editor/demo.jsx +284 -0
  172. package/registry/blocks/automation-editor/index.js +4 -0
  173. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  174. package/registry/blocks/automation-list/README.md +129 -0
  175. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  176. package/registry/blocks/automation-list/block.json +35 -0
  177. package/registry/blocks/automation-list/demo.jsx +67 -0
  178. package/registry/blocks/automation-list/index.js +3 -0
  179. package/registry/blocks/automation-model/README.md +109 -0
  180. package/registry/blocks/automation-model/block.json +135 -0
  181. package/registry/blocks/automation-model/catalog.js +256 -0
  182. package/registry/blocks/automation-model/check.js +161 -0
  183. package/registry/blocks/automation-model/demo.jsx +42 -0
  184. package/registry/blocks/automation-model/edit.js +262 -0
  185. package/registry/blocks/automation-model/fixes.js +287 -0
  186. package/registry/blocks/automation-model/glyph.jsx +13 -0
  187. package/registry/blocks/automation-model/graph.js +410 -0
  188. package/registry/blocks/automation-model/index.js +33 -0
  189. package/registry/blocks/automation-model/overlay.js +175 -0
  190. package/registry/blocks/automation-model/parts.jsx +94 -0
  191. package/registry/blocks/automation-model/schedule.js +53 -0
  192. package/registry/blocks/automation-model/templates.js +128 -0
  193. package/registry/blocks/automation-model/values.js +29 -0
  194. package/registry/blocks/automation-model/words.js +439 -0
  195. package/registry/blocks/automations-screen/README.md +207 -0
  196. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  197. package/registry/blocks/automations-screen/block.json +47 -0
  198. package/registry/blocks/automations-screen/demo.jsx +364 -0
  199. package/registry/blocks/automations-screen/index.js +4 -0
  200. package/registry/blocks/automations-screen/screen-model.js +129 -0
  201. package/registry/blocks/chat-pane/README.md +143 -0
  202. package/registry/blocks/chat-pane/block.json +48 -0
  203. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  204. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  205. package/registry/blocks/chat-pane/chat-context.js +17 -0
  206. package/registry/blocks/chat-pane/chat-model.js +43 -0
  207. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  208. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  209. package/registry/blocks/chat-pane/demo.jsx +103 -0
  210. package/registry/blocks/chat-pane/index.js +9 -0
  211. package/registry/blocks/connect-card/README.md +99 -0
  212. package/registry/blocks/connect-card/block.json +10 -0
  213. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  214. package/registry/blocks/connect-card/demo.jsx +49 -0
  215. package/registry/blocks/connect-card/index.js +3 -0
  216. package/registry/blocks/cost/README.md +115 -0
  217. package/registry/blocks/cost/block.json +10 -0
  218. package/registry/blocks/cost/cost.jsx +176 -0
  219. package/registry/blocks/cost/demo.jsx +66 -0
  220. package/registry/blocks/cost/index.js +3 -0
  221. package/registry/blocks/data-picker/README.md +81 -0
  222. package/registry/blocks/data-picker/block.json +38 -0
  223. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  224. package/registry/blocks/data-picker/demo.jsx +34 -0
  225. package/registry/blocks/data-picker/fields.js +89 -0
  226. package/registry/blocks/data-picker/index.js +4 -0
  227. package/registry/blocks/data-table/demo.jsx +1 -1
  228. package/registry/blocks/first-run/README.md +118 -0
  229. package/registry/blocks/first-run/block.json +11 -0
  230. package/registry/blocks/first-run/demo.jsx +88 -0
  231. package/registry/blocks/first-run/first-run-model.js +51 -0
  232. package/registry/blocks/first-run/first-run.jsx +156 -0
  233. package/registry/blocks/first-run/index.js +5 -0
  234. package/registry/blocks/first-run/probe-rules.js +26 -0
  235. package/registry/blocks/fix-note/README.md +79 -0
  236. package/registry/blocks/fix-note/block.json +35 -0
  237. package/registry/blocks/fix-note/demo.jsx +30 -0
  238. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  239. package/registry/blocks/fix-note/index.js +3 -0
  240. package/registry/blocks/flow-editor/README.md +108 -0
  241. package/registry/blocks/flow-editor/block.json +44 -0
  242. package/registry/blocks/flow-editor/demo.jsx +115 -0
  243. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  244. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  245. package/registry/blocks/flow-editor/index.js +6 -0
  246. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  247. package/registry/blocks/flow-editor/outline.jsx +134 -0
  248. package/registry/blocks/library-shelf/README.md +117 -0
  249. package/registry/blocks/library-shelf/block.json +28 -0
  250. package/registry/blocks/library-shelf/demo.jsx +68 -0
  251. package/registry/blocks/library-shelf/index.js +3 -0
  252. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  253. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  254. package/registry/blocks/margin-notes/README.md +222 -0
  255. package/registry/blocks/margin-notes/block.json +10 -0
  256. package/registry/blocks/margin-notes/demo.jsx +144 -0
  257. package/registry/blocks/margin-notes/index.js +20 -0
  258. package/registry/blocks/margin-notes/margin-model.js +224 -0
  259. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  260. package/registry/blocks/needs-you/README.md +177 -0
  261. package/registry/blocks/needs-you/block.json +34 -0
  262. package/registry/blocks/needs-you/demo.jsx +143 -0
  263. package/registry/blocks/needs-you/index.js +3 -0
  264. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  265. package/registry/blocks/pace-bar/README.md +230 -0
  266. package/registry/blocks/pace-bar/block.json +10 -0
  267. package/registry/blocks/pace-bar/demo.jsx +255 -0
  268. package/registry/blocks/pace-bar/index.js +22 -0
  269. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  270. package/registry/blocks/pace-bar/pace-model.js +288 -0
  271. package/registry/blocks/record-grid/README.md +100 -0
  272. package/registry/blocks/record-grid/block.json +28 -0
  273. package/registry/blocks/record-grid/demo.jsx +43 -0
  274. package/registry/blocks/record-grid/index.js +3 -0
  275. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  276. package/registry/blocks/run-history/README.md +138 -0
  277. package/registry/blocks/run-history/block.json +10 -0
  278. package/registry/blocks/run-history/demo.jsx +74 -0
  279. package/registry/blocks/run-history/index.js +3 -0
  280. package/registry/blocks/run-history/run-history.jsx +218 -0
  281. package/registry/blocks/run-view/README.md +174 -0
  282. package/registry/blocks/run-view/block.json +50 -0
  283. package/registry/blocks/run-view/demo.jsx +286 -0
  284. package/registry/blocks/run-view/index.js +5 -0
  285. package/registry/blocks/run-view/run-controls.jsx +266 -0
  286. package/registry/blocks/run-view/run-view.jsx +236 -0
  287. package/registry/blocks/run-view/runs-list.jsx +99 -0
  288. package/registry/blocks/schedule-card/README.md +121 -0
  289. package/registry/blocks/schedule-card/block.json +10 -0
  290. package/registry/blocks/schedule-card/demo.jsx +82 -0
  291. package/registry/blocks/schedule-card/index.js +3 -0
  292. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  293. package/registry/blocks/schedule-engine/README.md +137 -0
  294. package/registry/blocks/schedule-engine/block.json +10 -0
  295. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  296. package/registry/blocks/schedule-engine/engine.js +769 -0
  297. package/registry/blocks/schedule-engine/index.js +9 -0
  298. package/registry/blocks/schedule-engine/ops.js +71 -0
  299. package/registry/blocks/schedule-engine/place.js +294 -0
  300. package/registry/blocks/schedule-engine/tree.js +79 -0
  301. package/registry/blocks/schedule-engine/waits.js +80 -0
  302. package/registry/blocks/schedule-gantt/README.md +159 -0
  303. package/registry/blocks/schedule-gantt/block.json +10 -0
  304. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  305. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  306. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  307. package/registry/blocks/schedule-gantt/index.js +26 -0
  308. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  309. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  310. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  311. package/registry/blocks/schedule-ruler/README.md +148 -0
  312. package/registry/blocks/schedule-ruler/block.json +10 -0
  313. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  314. package/registry/blocks/schedule-ruler/index.js +21 -0
  315. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  316. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  317. package/registry/blocks/source-receipt/README.md +97 -0
  318. package/registry/blocks/source-receipt/block.json +30 -0
  319. package/registry/blocks/source-receipt/demo.jsx +71 -0
  320. package/registry/blocks/source-receipt/index.js +3 -0
  321. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  322. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  323. package/registry/blocks/step-picker/README.md +78 -0
  324. package/registry/blocks/step-picker/block.json +31 -0
  325. package/registry/blocks/step-picker/demo.jsx +23 -0
  326. package/registry/blocks/step-picker/index.js +3 -0
  327. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  328. package/registry/blocks/step-settings/README.md +100 -0
  329. package/registry/blocks/step-settings/block.json +44 -0
  330. package/registry/blocks/step-settings/demo.jsx +93 -0
  331. package/registry/blocks/step-settings/fields.jsx +58 -0
  332. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  333. package/registry/blocks/step-settings/index.js +5 -0
  334. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  335. package/registry/blocks/suggestion/README.md +171 -0
  336. package/registry/blocks/suggestion/block.json +40 -0
  337. package/registry/blocks/suggestion/demo.jsx +91 -0
  338. package/registry/blocks/suggestion/index.js +3 -0
  339. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  340. package/registry/blocks/sum-bar/README.md +122 -0
  341. package/registry/blocks/sum-bar/block.json +10 -0
  342. package/registry/blocks/sum-bar/demo.jsx +110 -0
  343. package/registry/blocks/sum-bar/index.js +5 -0
  344. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  345. package/registry/blocks/sum-bar/sum-model.js +126 -0
  346. package/registry/blocks/tier-map/README.md +217 -0
  347. package/registry/blocks/tier-map/block.json +10 -0
  348. package/registry/blocks/tier-map/demo.jsx +222 -0
  349. package/registry/blocks/tier-map/index.js +24 -0
  350. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  351. package/registry/blocks/tier-map/tier-model.js +318 -0
  352. package/registry/blocks/time-grid/README.md +305 -0
  353. package/registry/blocks/time-grid/block.json +10 -0
  354. package/registry/blocks/time-grid/demo.jsx +279 -0
  355. package/registry/blocks/time-grid/grid-model.js +322 -0
  356. package/registry/blocks/time-grid/index.js +25 -0
  357. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  358. package/registry/blocks/time-strip/README.md +141 -0
  359. package/registry/blocks/time-strip/block.json +10 -0
  360. package/registry/blocks/time-strip/demo.jsx +129 -0
  361. package/registry/blocks/time-strip/index.js +19 -0
  362. package/registry/blocks/time-strip/strip-model.js +178 -0
  363. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  364. package/registry/blocks/top-bar/README.md +149 -0
  365. package/registry/blocks/top-bar/block.json +35 -0
  366. package/registry/blocks/top-bar/demo.jsx +78 -0
  367. package/registry/blocks/top-bar/index.js +5 -0
  368. package/registry/blocks/top-bar/probe-rules.js +74 -0
  369. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  370. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  371. package/registry/blocks/trigger-picker/README.md +98 -0
  372. package/registry/blocks/trigger-picker/block.json +38 -0
  373. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  374. package/registry/blocks/trigger-picker/index.js +3 -0
  375. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  376. package/registry/blocks/workflow-view/README.md +123 -0
  377. package/registry/blocks/workflow-view/block.json +10 -0
  378. package/registry/blocks/workflow-view/demo.jsx +90 -0
  379. package/registry/blocks/workflow-view/index.js +3 -0
  380. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  381. package/src/install.mjs +77 -16
  382. package/src/registry.mjs +3 -0
  383. package/templates/next/browser/lib/data.js +6 -1
  384. package/templates/next/files/README.md +627 -24
  385. package/templates/next/files/check/automation-format.ts +2103 -0
  386. package/templates/next/files/check/automation-package.ts +142 -0
  387. package/templates/next/files/check/run.mjs +968 -9
  388. package/templates/next/files/components/ui/field.jsx +17 -4
  389. package/templates/next/files/components/ui/icon.jsx +13 -0
  390. package/templates/next/files/components/ui/index.js +2 -1
  391. package/templates/next/files/components/ui/layout.jsx +77 -2
  392. package/templates/next/files/components/ui/notice.jsx +70 -0
  393. package/templates/next/files/components/ui/page.jsx +124 -0
  394. package/templates/next/files/components/ui/stat.jsx +22 -7
  395. package/templates/next/files/components/ui/toast.jsx +6 -3
  396. package/templates/next/files/lib/automations.js +660 -0
  397. package/templates/next/files/lib/claw.js +241 -5
  398. package/templates/next/files/lib/door.js +92 -0
  399. package/templates/next/files/lib/hooks.js +1627 -8
  400. package/templates/next/files/lib/platform.js +529 -21
  401. package/templates/next/template.json +13 -3
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "run-history",
4
+ "type": "registry:block",
5
+ "title": "RunHistory",
6
+ "description": "A job's runs and the account of one, drawn inside the app. RunHistory is the list, newest first: each run's outcome as a mark and a word (completed, failed, running, needs you, stopped, skipped), its title, one line of what it did, when and what it cost. RunAccount is one run: its steps in order with how each ended, a row of figures and the account itself, one line a heading. RunOverview is the summary: completed, failed, in progress, the average run time, and the credits used against the week's cap as a bar. They draw what useRuns() and useRun() return (lib/hooks.js, GET _claw/runs and _claw/runs/<id>). Their own width decides their layout.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/run-history/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/run-history/index.js",
16
+ "content": "// The RunHistory block — `makersclaw add run-history`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { RunHistory, RunAccount, RunOverview } from \"./run-history\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/run-history/run-history.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/run-history/run-history.jsx",
22
+ "content": "\"use client\";\n\n// A job's runs, the account of one, and the overview of them all. Props\n// in, callbacks out; the block never reads runs itself and formats no\n// time (each comes in as words). Tokens only. An outcome is a mark AND a\n// word, so it is never told by colour alone. Its own width decides its\n// layout (@container), never the viewport.\n//\n// RunHistory and RunAccount moved from the CRM (makersclaw-store\n// packages/crm 0.7.0) into the template's kit (0.8.1, D15); RunOverview\n// is new.\n\nimport { Icon, cx } from \"../../ui\";\n\nconst MARK = {\n ok: { icon: \"circle-check\", tone: \"text-success\", word: \"Completed\" },\n done: { icon: \"circle-check\", tone: \"text-success\", word: \"Completed\" },\n complete: { icon: \"circle-check\", tone: \"text-success\", word: \"Completed\" },\n failed: { icon: \"circle-x\", tone: \"text-destructive\", word: \"Failed\" },\n running: { icon: \"loader\", tone: \"text-info\", word: \"Running\" },\n needs_you: { icon: \"circle-help\", tone: \"text-warning\", word: \"Needs you\" },\n stopped: { icon: \"stop\", tone: \"text-muted-foreground\", word: \"Stopped\" },\n skipped: { icon: \"minus\", tone: \"text-muted-foreground\", word: \"Skipped\" },\n};\nconst markOf = (s) => MARK[s] ?? MARK.skipped;\nconst spin = (s) => (s === \"running\" ? \"animate-spin motion-reduce:animate-none\" : null);\n\n/** A count as the overview shows it: words as they come, a number with\n * its thousands marked the same way on the server and in the browser. */\nfunction countOf(value) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value.toLocaleString(\"en-US\");\n return value ?? \"—\";\n}\n\n/**\n * The runs, newest first.\n *\n * runs [{ id, status, title, line, cost?, when? }]\n * status: ok|done|failed|running|needs_you|stopped|skipped\n * title: \"Run #7 · Tue 29 Sep, 07:02 · Read now, by Owen\"; line: what it did\n * when: the muted words at its end (\"yesterday\", \"2 min\")\n * selected the id shown in the account\n * onSelect(run)\n * empty what an empty history says\n */\nexport function RunHistory({ runs = [], selected, onSelect, label = \"Runs\", empty, className }) {\n if (!runs.length) return <div className={cx(\"p-4 text-sm text-muted-foreground\", className)}>{empty ?? \"No runs yet.\"}</div>;\n return (\n <ul aria-label={label} className={cx(\"m-0 grid min-w-0 list-none p-0\", className)}>\n {runs.map((r) => {\n const m = markOf(r.status);\n const on = r.id === selected;\n const tail = r.cost || r.when;\n return (\n <li key={r.id} className=\"min-w-0\">\n <button\n type=\"button\"\n aria-current={on ? \"true\" : undefined}\n onClick={() => onSelect?.(r)}\n className={cx(\n \"grid w-full grid-cols-[16px_minmax(0,1fr)_auto] items-start gap-2.5 border-b border-border px-3.5 py-2.5 text-left text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n on ? \"bg-accent text-accent-foreground\" : \"hover:bg-muted\",\n )}\n >\n <Icon name={m.icon} size={16} className={cx(\"mt-0.5\", m.tone, spin(r.status))} label={m.word} />\n <span className=\"grid min-w-0 gap-0.5\">\n <span className={cx(\"truncate\", on && \"font-semibold\")}>{r.title}</span>\n {r.line ? (\n <span className={cx(\"truncate text-xs\", r.status === \"failed\" ? \"text-destructive\" : on ? \"\" : \"text-muted-foreground\")}>{r.line}</span>\n ) : null}\n </span>\n {tail ? (\n <span className={cx(\"grid justify-items-end gap-0.5 whitespace-nowrap text-xs tabular-nums\", on ? \"\" : \"text-muted-foreground\")}>\n {r.when ? <span>{r.when}</span> : null}\n {r.cost ? <span>{r.cost}</span> : null}\n </span>\n ) : null}\n </button>\n </li>\n );\n })}\n </ul>\n );\n}\n\nfunction StepsStrip({ steps }) {\n return (\n <ol aria-label=\"Steps\" className=\"m-0 flex list-none flex-wrap items-center gap-y-2 p-0\">\n {steps.map((s, i) => {\n const m = markOf(s.status);\n return (\n <li key={`${s.label}-${i}`} className=\"flex min-w-0 items-center\">\n {i ? <span aria-hidden=\"true\" className=\"h-px w-4 shrink-0 bg-border\" /> : null}\n <span className=\"grid min-w-0 max-w-full gap-0.5 rounded-[var(--radius)] border border-border bg-card px-2.5 py-2 text-card-foreground @xs:min-w-[150px]\">\n <b className=\"truncate text-xs font-semibold\">{s.label}</b>\n <span className={cx(\"flex items-center gap-1 text-[11.5px]\", m.tone)}>\n <Icon name={m.icon} size={12} className={spin(s.status)} />\n {m.word}\n {s.count ? <span>· {s.count}</span> : null}\n </span>\n </span>\n </li>\n );\n })}\n </ol>\n );\n}\n\nconst BAR = { neutral: \"bg-muted-foreground\", danger: \"bg-destructive\", info: \"bg-info\" };\n\n/**\n * One run's account.\n *\n * title \"Tue 29 Sep, 07:02\"\n * sub the muted line beside it (what it read, the cost, the next run)\n * steps [{ label, status: complete|done|failed|skipped|running, count? }]\n * figures [{ label, value, note?, noteTone?: \"success\"|\"danger\", progress?: 0..1, progressTone? }]\n * lines [{ label, text }]: the account, one line a heading\n * empty a node when the run says nothing yet (running)\n */\nexport function RunAccount({ title, sub, steps = [], figures = [], lines = [], empty, className }) {\n return (\n <div className={cx(\"@container flex min-h-0 min-w-0 flex-col\", className)}>\n {steps.length ? (\n <div className=\"shrink-0 border-b border-border px-4 py-4 @md:px-[18px]\">\n <StepsStrip steps={steps} />\n </div>\n ) : null}\n <div className=\"grid content-start gap-3.5 px-4 py-4 @md:px-5\">\n {figures.length ? (\n <div className=\"grid grid-cols-1 gap-2.5 @2xs:grid-cols-2 @2xl:grid-cols-4\">\n {figures.map((f) => (\n <div key={f.label} className=\"grid min-w-0 gap-0.5 rounded-[var(--radius)] border border-border bg-card px-3 py-2.5 text-card-foreground\">\n <span className=\"truncate text-xs text-muted-foreground\">{f.label}</span>\n <span className=\"truncate text-[22px] font-semibold leading-tight tabular-nums\">{f.value}</span>\n {f.progress != null ? (\n <span aria-hidden=\"true\" className=\"mt-1 block h-1 overflow-hidden rounded-sm bg-border\">\n <span className={cx(\"block h-full\", BAR[f.progressTone] ?? BAR.neutral)} style={{ width: `${Math.max(0, Math.min(1, f.progress)) * 100}%` }} />\n </span>\n ) : null}\n {f.note ? <span className={cx(\"text-xs\", f.noteTone === \"success\" ? \"text-success\" : f.noteTone === \"danger\" ? \"text-destructive\" : \"text-muted-foreground\")}>{f.note}</span> : null}\n </div>\n ))}\n </div>\n ) : null}\n <div className=\"grid gap-0\">\n {title ? (\n <div className=\"mb-1 flex flex-wrap items-baseline gap-x-2.5\">\n <b className=\"text-sm font-semibold\">{title}</b>\n {sub ? <span className=\"text-[13px] text-muted-foreground\">{sub}</span> : null}\n </div>\n ) : null}\n {lines.length ? (\n <dl className=\"m-0 grid text-[13px]\">\n {lines.map((l, i) => (\n <div key={`${l.label}-${i}`} className=\"grid grid-cols-1 gap-0.5 border-b border-border py-2 last:border-b-0 @sm:grid-cols-[120px_minmax(0,1fr)] @sm:gap-3\">\n <dt className=\"text-muted-foreground\">{l.label}</dt>\n <dd className=\"m-0 min-w-0\">{l.text}</dd>\n </div>\n ))}\n </dl>\n ) : (\n empty ?? null\n )}\n </div>\n </div>\n </div>\n );\n}\n\n/** One tile of the overview: a mark and a word, never a colour alone. */\nfunction Tile({ value, word, icon, tone, spinning, className, children }) {\n return (\n <div className={cx(\"grid min-w-0 gap-0.5 rounded-[var(--radius)] border border-border bg-card px-3 py-2.5 text-card-foreground\", className)}>\n <div className=\"flex min-w-0 items-start gap-2\">\n <span className=\"min-w-0 flex-1 truncate text-lg font-semibold leading-tight tabular-nums\">{value}</span>\n {icon ? <Icon name={icon} size={14} className={cx(\"mt-1 shrink-0\", tone, spinning && \"animate-spin motion-reduce:animate-none\")} /> : null}\n </div>\n <span className={cx(\"truncate text-xs\", tone ?? \"text-muted-foreground\")}>{word}</span>\n {children}\n </div>\n );\n}\n\n/**\n * The overview of a job's runs: completed, failed, in progress, the\n * average run time, and the credits used against the week's cap.\n *\n * completed, failed, running counts (numbers, or words)\n * avg the average run time, in words (\"2 seconds\")\n * credits { used, of, words: \"7 of 37,000 this week\" }\n * title the heading (default \"Overview\"); null for none\n */\nexport function RunOverview({ completed, failed, running, avg, credits, title = \"Overview\", className }) {\n const pct = credits && typeof credits.used === \"number\" && credits.of > 0 ? Math.max(0, Math.min(1, credits.used / credits.of)) : null;\n const full = pct != null && pct >= 1;\n return (\n <section aria-label={title ?? \"Overview\"} className={cx(\"@container grid min-w-0 gap-2\", className)}>\n {title ? <h3 className=\"m-0 text-xs font-medium text-muted-foreground\">{title}</h3> : null}\n <div className=\"grid grid-cols-1 gap-2 @2xs:grid-cols-2\">\n <Tile value={countOf(completed ?? 0)} word=\"Completed\" icon=\"circle-check\" tone=\"text-success\" />\n <Tile value={countOf(failed ?? 0)} word=\"Failed\" icon=\"circle-x\" tone={failed ? \"text-destructive\" : \"text-muted-foreground\"} />\n <Tile value={countOf(running ?? 0)} word=\"In progress\" icon=\"loader\" tone={running ? \"text-info\" : \"text-muted-foreground\"} spinning={Boolean(running)} />\n <Tile value={avg ?? \"—\"} word=\"Average run time\" icon=\"clock\" />\n {credits ? (\n <Tile value={countOf(credits.used)} word=\"Credits used\" icon=\"zap\" tone={full ? \"text-destructive\" : undefined} className=\"@2xs:col-span-2\">\n {credits.words ? <span className={cx(\"text-xs tabular-nums\", full ? \"text-destructive\" : \"text-muted-foreground\")}>{credits.words}</span> : null}\n {pct != null ? (\n <span aria-hidden=\"true\" className=\"mt-1 block h-1 overflow-hidden rounded-sm bg-border\">\n <span className={cx(\"block h-full\", full ? \"bg-destructive\" : \"bg-muted-foreground\")} style={{ width: `${pct * 100}%` }} />\n </span>\n ) : null}\n </Tile>\n ) : null}\n </div>\n </section>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/run-history/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/run-history/README.md",
28
+ "content": "# RunHistory\n\n> **Superseded** (0.8.1, the automations engine, D26). A job's runs and the account of one, from `useRuns` and `useRun`. For an app whose jobs run on the automations engine, use `run-view`'s `RunsList` and `RunView` (`useAutomation`, `useAutomationRun`). Keep using this block for an app that has not moved: it works as it did.\n\n\nA job's runs, the account of one, and the overview of them all, drawn\ninside the app in its own look. Part of the agent kit (0.8.1): the app\nshows its agent's work itself, beside the rows that work wrote; the\nwindow's Runs tab stays as the full history.\n\n- `RunHistory` — the runs, newest first: each one's outcome as a mark\n and a word, its title, one line of what it did, when and what it cost.\n One is selected.\n- `RunAccount` — that run: its steps in order with how each ended, a row\n of figures, and the account itself, one line a heading (Read,\n Suggested, Drafted, Failed).\n- `RunOverview` — completed, failed and in progress, the average run\n time, and the credits used against the week's cap as a bar.\n\n```jsx\nimport { RunHistory, RunAccount, RunOverview } from \"../components/blocks/run-history\"; // a Makersclaw app\nimport { RunHistory, RunAccount, RunOverview } from \"@/components/makersclaw/blocks/run-history\"; // a shadcn project\n```\n\nClient components. They format no time: each comes in as words. Their own\nwidth decides their layout: the account's figures run two to a row,\nfour from about 672 px; its lines stack their heading over the text\nunder about 384 px.\n\n## Props\n\n### RunHistory\n\n- `runs` — `[{ id, status, title, line, when?, cost? }]`, newest first.\n `status` is `done` (or `ok`) · `failed` · `running` · `needs_you` ·\n `stopped` · `skipped`: useRuns' `state` passes as it is. A failed\n run's `line` is in the danger tone: write it as the reason (\"Didn't\n run: Google asked Nadia to sign in again\").\n- `selected` — the id the account shows. `onSelect(run)`.\n- `empty` — what an empty history says: when the first run will be.\n- `label` — the list's name for a screen reader (default \"Runs\").\n- `className`.\n\n### RunAccount\n\n- `title`, `sub` — the run's time, and the muted line beside it.\n- `steps` — `[{ label, status, count? }]`: useRun's `steps` pass as they\n are.\n- `figures` — `[{ label, value, note?, noteTone?, progress?,\n progressTone? }]`: a tile each (`progress` 0 to 1 draws a bar;\n `progressTone` `neutral` · `danger` · `info`).\n- `lines` — `[{ label, text }]`: the account.\n- `empty` — shown while there are no lines (a run still going).\n- `className`.\n\n### RunOverview\n\n- `completed`, `failed`, `running` — counts.\n- `avg` — the average run time, in words (\"2 seconds\").\n- `credits` — `{ used, of, words }`: the credits used, the week's cap,\n and their words (\"7 of 37,000 this week\"); the bar is full in the\n danger tone. Leave it out with no cap.\n- `title` — default \"Overview\"; `null` for none. `className`.\n\n## useRuns and useRun (lib/hooks.js)\n\n```js\nconst { runs, overview } = useRuns({ job: wakeId, limit: 20 });\nconst { run, steps, text, needs } = useRun(selectedId);\n```\n\n`useRuns` reads `GET _claw/runs`: each run's `id`, `title`, `state`\n(`running` · `done` · `failed` · `stopped` · `needs_you`), `job`,\n`trigger` (`schedule` · `manual` · `event` · `ask`), `by` (who pressed\nRun now), `number` (1 is the oldest), `started_at`, `duration_ms`,\n`credits`; and `overview` (`{ completed, failed, running, avg_ms,\ncredits_week, cap_week_credits }`). It looks every 5 s while a run is\ngoing, else every 30 s, and at once after useJobs' Run now. `useRun`\nreads `GET _claw/runs/<id>`: the run, its `steps`, the agent's report\n(`text`) and what it waits on (`needs`), every 2.5 s while it runs. A\nclaw from before the doors is `unavailable`: say \"No runs yet\", never an\nerror.\n\n## Who may do what\n\nAny member reads the jobs' runs and their own asks. A teammate's ask is\nnever listed: the door leaves it out. Running a job is the\n`schedule-card`'s or the `workflow-view`'s, never this block's.\n\n## Never\n\n- Never a run's outcome by colour alone: the mark and the word come\n with it.\n- Never an Approve on a run that needs the member: \"Needs you\" opens the\n desk, where the yes is given.\n- Never a time or a figure formatted here: the screen says it in the\n app's words.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { useRun, useRuns } from \"../lib/hooks\";\nimport { RunAccount, RunHistory, RunOverview } from \"../components/blocks/run-history\";\n\nconst at = (iso) => new Date(iso).toLocaleString(\"en-GB\", { weekday: \"short\", day: \"numeric\", month: \"short\", hour: \"2-digit\", minute: \"2-digit\" });\nconst cr = (n) => (n == null ? null : `${Math.round(n)} cr`);\n\nexport default function ReadRuns({ jobId }) {\n const { runs, overview } = useRuns({ job: jobId });\n const [id, setId] = useState(null);\n const open = id ?? runs[0]?.id;\n const { run, steps, text } = useRun(open);\n return (\n <div className=\"@container grid @3xl:grid-cols-[320px_minmax(0,1fr)]\">\n <RunHistory\n runs={runs.map((r) => ({ id: r.id, status: r.state, title: `Run #${r.number} · ${at(r.started_at)}`, line: r.trigger === \"manual\" ? `Run now, by ${r.by}` : r.job, cost: cr(r.credits) }))}\n selected={open}\n onSelect={(r) => setId(r.id)}\n empty=\"No runs yet. The first runs Mon 12 Oct, 07:00.\"\n />\n <div className=\"grid content-start gap-4\">\n {run ? <RunAccount title={at(run.started_at)} sub={cr(run.credits)} steps={steps} lines={text ? [{ label: \"Report\", text }] : []} /> : null}\n {overview ? (\n <RunOverview\n completed={overview.completed}\n failed={overview.failed}\n running={overview.running}\n avg={overview.avg_ms == null ? null : `${Math.round(overview.avg_ms / 1000)} seconds`}\n credits={overview.cap_week_credits ? { used: overview.credits_week, of: overview.cap_week_credits, words: `${overview.credits_week} of ${overview.cap_week_credits} this week` } : null}\n />\n ) : null}\n </div>\n </div>\n );\n}\n```\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "runs",
33
+ "history",
34
+ "schedule",
35
+ "automation",
36
+ "agent",
37
+ "account",
38
+ "steps",
39
+ "overview",
40
+ "credits",
41
+ "report",
42
+ "log",
43
+ "kit"
44
+ ],
45
+ "docs": "Installed to components/makersclaw/blocks/run-history. Import: import { RunHistory, RunAccount, RunOverview } from \"@/components/makersclaw/blocks/run-history\". Props and an example: components/makersclaw/blocks/run-history/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
+ "meta": {
47
+ "makersclaw": {
48
+ "version": "0.8.1",
49
+ "useWhen": "Superseded by run-view for an app whose jobs run on the automations engine; still the block for an app that has not moved (its jobs are the agent's wakes). An app whose agent runs on a schedule, or does work on asks, accounts for itself inside the app: a morning read's page, a nightly import's log, the runs beside a job's card. For the job drawn as a graph with its runs beside it use workflow-view (it uses these); for who did what to the rows use activity-log; for one ask in progress use job-panel."
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "run-view",
4
+ "type": "registry:block",
5
+ "title": "RunView",
6
+ "description": "One run of an automation on the same canvas as the builder: every card wears how it went, the path the run took is lit and the paths it did not take are dimmed, a looped step says how many times, a failed step says why, and a step the agent fixed says so on its card and in its panel. A side panel gives the run in a few lines (who started it, how long, what it cost of what it may, the version) and, for a card pressed, how that step went and what went in and out for those who may read it. RunsList is the automation's runs, newest first, with an overview (done, failed, in progress, spent) and a filter. For a run that is not over it offers what the run allows (RunControls): pause, resume, stop; when it is stuck on a step, try it again, skip it, change the data it was given for this run, or raise the run's limit and go on. On a step of a run that is over (done, failed, stopped) or waiting on its owner, a quiet \"Retry from this step\" starts the run over there, asking first when a step from there on already sent something. A step the agent fixed shows what changed, what it cost, whether a write can be taken back, and keeps or drops the change the agent drafted for next time. A test run (a dry run) draws a strip over the canvas, with what each step would have done and what could not be checked, and its steps as dashed pencil cards.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/automation-model.json",
10
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/flow-editor.json",
11
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/step-settings.json",
12
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/fix-note.json"
13
+ ],
14
+ "dependencies": [],
15
+ "files": [
16
+ {
17
+ "path": "registry/blocks/run-view/index.js",
18
+ "type": "registry:component",
19
+ "target": "@components/makersclaw/blocks/run-view/index.js",
20
+ "content": "// The RunView block — `makersclaw add run-view`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { RunView } from \"./run-view\";\nexport { RunsList } from \"./runs-list\";\nexport { RetryFromStep, RunControls } from \"./run-controls\";\n"
21
+ },
22
+ {
23
+ "path": "registry/blocks/run-view/run-view.jsx",
24
+ "type": "registry:component",
25
+ "target": "@components/makersclaw/blocks/run-view/run-view.jsx",
26
+ "content": "\"use client\";\n\n// One run, on the same canvas as the builder: every card wears how it\n// went, the path the run took is lit and the paths it did not take are\n// dimmed, and a step the agent fixed says so on its card and in its panel.\n//\n// Read-only: nothing here changes the flow. \"Run again\", the controls for\n// a run (pause, resume, stop; retry, skip, change the data\n// or raise the limit when it is stuck; retry from a step of one that is over) and keeping or dropping the change\n// the agent drafted from a fix are the platform's doors; the screen around\n// this view calls them. Props in, callbacks out.\n\nimport { useMemo, useState } from \"react\";\nimport { Button, Icon, cx } from \"../../ui\";\nimport {\n Chip,\n RunPicker,\n SidePanel,\n PanelSection,\n agoWords,\n availableBefore,\n clockWords,\n creditsWords,\n draftOfStep,\n durationWords,\n findStep,\n layoutAutomation,\n overlayOf,\n reportLines,\n runStatusWords,\n stepTitle,\n testWords,\n triggerTypeWords,\n triggerWords,\n} from \"../automation-model\";\nimport { FlowEditor } from \"../flow-editor\";\nimport { FixNote } from \"../fix-note\";\nimport { StepSettings } from \"../step-settings\";\nimport { RetryFromStep, RunControls } from \"./run-controls\";\n\nfunction titleOfId(def, id) {\n const step = findStep(def.steps, id);\n return step ? stepTitle(step) : id;\n}\n\n/** The strip over a test run's canvas: how the test went in a line, what the\n * steps would have done, what could not be checked, what was only sampled.\n * The platform's own account (`run.result.dry`); nothing here is invented. */\nfunction TestStrip({ detail, def }) {\n const run = detail.run;\n const line = testWords(detail);\n const dry = run.result && typeof run.result === \"object\" ? run.result.dry : null;\n const would = (Array.isArray(dry?.would) ? dry.would : []).filter((w) => w && typeof w.words === \"string\" && w.words.trim());\n const unchecked = Array.isArray(dry?.unchecked) ? dry.unchecked.length : 0;\n const sampled = (Array.isArray(dry?.sampled) ? dry.sampled : []).filter((x) => x && Number.isFinite(Number(x.of)) && Number.isFinite(Number(x.looked_at)));\n const shown = would.slice(0, 5);\n const tone = run.status === \"failed\" ? \"border-destructive/50 bg-destructive/10\" : run.status === \"done\" ? \"border-border bg-muted/40\" : \"border-warning/50 bg-warning/10\";\n return (\n <section aria-label=\"What this test did\" className={cx(\"grid gap-1.5 border-b px-3 py-2 text-[13px]\", tone)}>\n {line ? <p className=\"m-0 font-medium\">{line}</p> : null}\n {shown.length > 0 ? (\n <ul className=\"m-0 grid list-none gap-0.5 p-0 text-muted-foreground\">\n {shown.map((w, i) => (\n <li key={`${w.path ?? w.step ?? i}`} className=\"flex min-w-0 gap-1.5\">\n <Icon name=\"pencil\" size={13} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 break-words\">{w.words}</span>\n </li>\n ))}\n {would.length > shown.length ? <li className=\"pl-5\">and {would.length - shown.length} more</li> : null}\n </ul>\n ) : null}\n {unchecked > 0 ? <p className=\"m-0 text-muted-foreground\">{unchecked === 1 ? \"1 step could not be checked\" : `${unchecked} steps could not be checked`}: they need what an earlier step would have made.</p> : null}\n {sampled.map((x) => (\n <p key={x.step} className=\"m-0 text-muted-foreground\">\n Looked at the first {Number(x.looked_at)} of {Number(x.of)}\n {x.step ? ` in “${titleOfId(def, x.step)}”` : \"\"}.\n </p>\n ))}\n </section>\n );\n}\n\nfunction Note({ tone, children }) {\n return <p className={cx(\"m-0 rounded-md border px-3 py-2 text-[13px]\", tone === \"red\" ? \"border-destructive/50 bg-destructive/10\" : \"border-warning/50 bg-warning/10\")}>{children}</p>;\n}\n\n/**\n * def the definition the run used (or the closest one: say so in versionNote)\n * versionNote \"This run used version 3. The steps drawn are the current ones.\"\n * detail a run's account: { run, steps, steps_visible, fixes? } (GET automation-runs/<id>);\n * `fixes` are the fixer's attempts and the changes it drafted\n * runs the automation's runs, newest first, for the picker and the number\n * apps, actions, ownedByApp as the builder's, for the read-only step panel\n * tz the automation's time zone, for the clock words\n * tabs the Flow / Runs tabs, drawn in the header\n * name the automation's name, on the back button\n * A TEST run (`run.dry`: it read real data and changed nothing) draws a strip over the\n * canvas (`testWords`, what each step would have done from `run.result.dry`, what\n * could not be checked, what was only sampled), its chip says Test passed / Test\n * failed / Test stopped, its steps that stood in for themselves are dashed with a\n * pencil, and the button reads \"Test again\".\n * onBack, onPickRun(runId), onRunAgain(), runAgainBusy\n * onControl(op, { path?, input?, confirmNotSent?, raiseBy?, confirmResend? }) pause · resume · stop · retry ·\n * skip · edit (a run's step data) · retry_from (start over at a step, from its panel); resolves { ok } or { ok: false, error, code }\n * controlBusy the op that is out\n * onKeepFix(stepId), onDropFix(stepId) keep or drop the change the agent drafted for a\n * step it fixed (a new version that runs). A fix with no draft has no button.\n * onUndoFix(stepId) take back a write the agent made while fixing a step\n */\nexport function RunView({ def, versionNote = null, detail, runs = [], apps = [], actions, ownedByApp = false, tz = \"UTC\", tabs, name, onBack, onPickRun, onRunAgain, runAgainBusy = false, onControl, controlBusy = null, onKeepFix, onDropFix, onUndoFix, className }) {\n const run = detail.run;\n const [selected, setSelected] = useState(null);\n const fixes = detail.fixes ?? [];\n // The change the agent drafted for a step it fixed: Keep and Drop act on it.\n // With none drafted there is nothing to keep, so no button.\n const keepProps = (stepId) => {\n const draft = draftOfStep(fixes, stepId);\n return {\n draft,\n onKeep: draft && draft.status === \"proposed\" && onKeepFix ? () => onKeepFix(stepId) : undefined,\n onDrop: draft && draft.status === \"proposed\" && onDropFix ? () => onDropFix(stepId) : undefined,\n onUndo: onUndoFix ? () => onUndoFix(stepId) : undefined,\n };\n };\n\n const words = triggerWords(def.trigger);\n const layout = useMemo(() => layoutAutomation(def, { triggerWords: words }), [def, words]);\n const overlay = useMemo(() => overlayOf(layout, detail.steps, run.status), [layout, detail.steps, run.status]);\n\n const dry = run.dry === true;\n const status = runStatusWords(run.status, dry);\n const at = runs.findIndex((r) => r.id === run.id);\n const n = at >= 0 ? runs.length - at : 0;\n const selectedStep = selected && selected !== \"trigger\" ? findStep(def.steps, selected) : null;\n const fixed = [...overlay.summary.values()].filter((s) => s.fix && s.fix.by !== \"member\");\n const edited = [...overlay.summary.values()].filter((s) => s.fix && s.fix.by === \"member\");\n const lines = reportLines(run.result);\n const errorWords = run.error && typeof run.error.words === \"string\" ? run.error.words : null;\n const started = clockWords(run.started_at ?? run.created_at, tz);\n\n const side = selectedStep ? (\n <StepSettings\n key={selectedStep.id}\n step={selectedStep}\n available={availableBefore(def, selectedStep.id, stepTitle)}\n apps={apps}\n actions={actions}\n ownedByApp={ownedByApp}\n problems={[]}\n readOnly\n summary={overlay.summary.get(selectedStep.id) ?? null}\n dataVisible={detail.steps_visible}\n fixProps={keepProps(selectedStep.id)}\n runActions={<RetryFromStep run={run} steps={detail.steps} def={def} stepId={selectedStep.id} onControl={onControl} busy={controlBusy} />}\n onChange={() => undefined}\n onClose={() => setSelected(null)}\n />\n ) : (\n <SidePanel title={`${run.is_test ? \"Test run\" : \"Run\"} ${n > 0 ? n : \"\"}`.trim()} subtitle={`${agoWords(run.created_at) ?? \"\"}${started ? ` · ${started}` : \"\"}`} label=\"This run\">\n <dl className=\"m-0 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-[13px]\">\n <dt className=\"text-muted-foreground\">Started by</dt>\n <dd className=\"m-0\">{triggerTypeWords(run.trigger_type)}</dd>\n <dt className=\"text-muted-foreground\">Took</dt>\n <dd className=\"m-0\">{durationWords(run.started_at, run.ended_at) ?? (run.status === \"running\" ? \"Still running\" : \"—\")}</dd>\n <dt className=\"text-muted-foreground\">Cost</dt>\n <dd className=\"m-0 font-mono\">\n {creditsWords(run.credits)}\n {run.cap_credits ? ` of ${creditsWords(run.cap_credits)}` : \"\"}\n </dd>\n <dt className=\"text-muted-foreground\">Version</dt>\n <dd className=\"m-0 font-mono\">{run.version}</dd>\n </dl>\n {run.status === \"waiting\" && run.wake_at ? <Note tone=\"amber\">Waiting until {clockWords(run.wake_at, tz)}.</Note> : null}\n {run.status === \"needs_you\" && !onControl ? <Note tone=\"amber\">It is waiting for its owner. Open the step that is marked.</Note> : null}\n {errorWords ? <Note tone=\"red\">{errorWords}</Note> : null}\n <RunControls run={run} steps={detail.steps} stepsVisible={detail.steps_visible} titleOf={(id) => titleOfId(def, id)} onControl={onControl} busy={controlBusy} />\n {fixed.length > 0 ? (\n <PanelSection title={fixed.length === 1 ? \"The agent fixed one step\" : `The agent fixed ${fixed.length} steps`}>\n {fixed.map((s) => (\n <FixNote key={s.stepId} fix={s.fix} stepTitle={titleOfId(def, s.stepId)} compact {...keepProps(s.stepId)} />\n ))}\n </PanelSection>\n ) : null}\n {edited.length > 0 ? (\n <PanelSection title={edited.length === 1 ? \"You changed one step\" : `You changed ${edited.length} steps`}>\n {edited.map((s) => (\n <FixNote key={s.stepId} fix={s.fix} stepTitle={titleOfId(def, s.stepId)} compact />\n ))}\n </PanelSection>\n ) : null}\n {lines.length > 0 ? (\n <PanelSection title=\"What it did\">\n <dl className=\"m-0 grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-[13px]\">\n {lines.map((l) => (\n <div key={l.label} className=\"contents\">\n <dt className=\"text-muted-foreground\">{l.label.charAt(0).toUpperCase() + l.label.slice(1)}</dt>\n <dd className=\"m-0 text-right font-mono\">{l.value}</dd>\n </div>\n ))}\n </dl>\n </PanelSection>\n ) : null}\n <p className=\"m-0 text-xs text-muted-foreground\">Press a card to see how that step went{detail.steps_visible ? \", and what went in and out\" : \"\"}.</p>\n </SidePanel>\n );\n\n return (\n <div className={cx(\"@container flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground\", className)}>\n <header className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1.5 border-b border-border px-3 py-2\">\n {onBack ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"arrow-left\" onClick={onBack}>\n {name ?? detail.automation?.name ?? \"Back\"}\n </Button>\n ) : null}\n <Chip tone={run.paused ? \"amber\" : status.tone}>{run.paused ? \"Paused\" : status.text}</Chip>\n {run.is_test ? <Chip tone=\"grey\">Test</Chip> : null}\n {tabs}\n <span className=\"ml-auto\" />\n {runs.length ? <RunPicker runs={runs} value={run.id} onPick={onPickRun} /> : null}\n {onRunAgain ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"play\" loading={runAgainBusy} onClick={onRunAgain}>\n {dry ? \"Test again\" : \"Run again\"}\n </Button>\n ) : null}\n </header>\n {versionNote ? <div className=\"border-b border-warning/50 bg-warning/10 px-3 py-2 text-sm\">{versionNote}</div> : null}\n <div className=\"flex min-h-0 flex-1 flex-col @3xl:flex-row\">\n <div className=\"flex min-h-[360px] min-w-0 flex-1 flex-col\">\n {dry ? <TestStrip detail={detail} def={def} /> : null}\n <FlowEditor def={def} selectedId={selected} onSelect={setSelected} overlay={overlay} fitKey={run.id} />\n </div>\n <div className=\"flex max-h-[60%] min-h-0 shrink-0 flex-col border-t border-border @3xl:max-h-none @3xl:w-[360px] @3xl:border-l @3xl:border-t-0 [&>aside]:min-h-0 [&>aside]:flex-1\">{side}</div>\n </div>\n </div>\n );\n}\n"
27
+ },
28
+ {
29
+ "path": "registry/blocks/run-view/runs-list.jsx",
30
+ "type": "registry:component",
31
+ "target": "@components/makersclaw/blocks/run-view/runs-list.jsx",
32
+ "content": "\"use client\";\n\n// An automation's runs, newest first: what ran, how it ended, when, how\n// long, what it cost. Above them the little overview a person wants first:\n// how many finished, how many failed, what is still going, what it cost.\n// A row opens the run on the canvas. Props in, callbacks out.\n\nimport { useMemo, useState } from \"react\";\nimport { cx } from \"../../ui\";\nimport { ACTIVE, Dot, agoWords, clockWords, creditsWords, durationWords, overviewOf, runStatusWords, triggerTypeWords } from \"../automation-model\";\n\nconst FILTERS = [\n { value: \"all\", label: \"All\" },\n { value: \"done\", label: \"Done\" },\n { value: \"failed\", label: \"Failed\" },\n { value: \"active\", label: \"In progress\" },\n];\n\nconst STATUS_TEXT = { green: \"text-success\", blue: \"text-info\", amber: \"text-warning\", red: \"text-destructive\", grey: \"text-muted-foreground\" };\n\n/**\n * runs [{ id, status, trigger_type, is_test, dry?, created_at, started_at, ended_at, credits }], newest first\n * tz the automation's time zone, for the clock words\n * onOpen(runId)\n * howToTest the words under \"It has not run yet\" (default says Test run)\n */\nexport function RunsList({ runs, tz = \"UTC\", onOpen, howToTest = \"When it runs, each run is listed here, with how it went and what it cost. Use Test run to try it once.\", className }) {\n const [filter, setFilter] = useState(\"all\");\n const total = runs.length;\n const o = useMemo(() => overviewOf(runs), [runs]);\n const shown = runs.filter((r) => (filter === \"all\" ? true : filter === \"active\" ? ACTIVE.has(r.status) : r.status === filter));\n\n if (total === 0) {\n return (\n <div className={cx(\"grid content-start gap-1 p-6\", className)}>\n <h3 className=\"m-0 text-base font-semibold\">It has not run yet</h3>\n <p className=\"m-0 text-sm text-muted-foreground\">{howToTest}</p>\n </div>\n );\n }\n return (\n <div data-scroll className={cx(\"@container grid min-h-0 min-w-0 flex-1 content-start gap-3 overflow-y-auto p-4\", className)}>\n <dl className=\"m-0 grid grid-cols-2 gap-2 @xl:grid-cols-4\">\n {[\n [\"Done\", o.done, false],\n [\"Failed\", o.failed, o.failed > 0],\n [\"In progress\", o.active, false],\n [\"Spent\", creditsWords(o.credits), false],\n ].map(([label, value, bad]) => (\n <div key={label} className=\"grid gap-0.5 rounded-lg border border-border bg-card px-3 py-2 text-card-foreground\">\n <dt className=\"text-xs text-muted-foreground\">{label}</dt>\n <dd className={cx(\"m-0 text-lg font-semibold tabular-nums\", bad && \"text-destructive\")}>{value}</dd>\n </div>\n ))}\n </dl>\n <div role=\"group\" aria-label=\"Show\" className=\"flex flex-wrap gap-1\">\n {FILTERS.map((f) => (\n <button\n key={f.value}\n type=\"button\"\n aria-pressed={filter === f.value}\n onClick={() => setFilter(f.value)}\n className={cx(\"min-h-9 rounded-full border px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\", filter === f.value ? \"border-foreground bg-accent font-medium\" : \"border-border text-muted-foreground hover:bg-accent\")}\n >\n {f.label}\n </button>\n ))}\n </div>\n {shown.length === 0 ? (\n <p className=\"m-0 text-sm text-muted-foreground\">No run matches.</p>\n ) : (\n <ol className=\"m-0 grid list-none gap-1.5 p-0\">\n {shown.map((r) => {\n const idx = total - runs.indexOf(r);\n const h = runStatusWords(r.status, r.dry === true);\n return (\n <li key={r.id}>\n <button type=\"button\" onClick={() => onOpen?.(r.id)} className=\"grid w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5 rounded-lg border border-border bg-card px-3 py-2 text-left text-card-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring @xl:grid-cols-[auto_minmax(0,1fr)_auto_auto_auto]\">\n <Dot tone={h.tone} />\n <span className=\"grid min-w-0 gap-0.5\">\n <b className=\"text-sm font-semibold\">\n {r.is_test ? \"Test run\" : \"Run\"} {idx}\n </b>\n <span className=\"truncate text-xs text-muted-foreground\">\n {triggerTypeWords(r.trigger_type)} · {clockWords(r.started_at ?? r.created_at, tz) ?? \"\"} · {agoWords(r.created_at) ?? \"\"}\n </span>\n </span>\n <span className={cx(\"text-[13px] font-medium\", STATUS_TEXT[h.tone])}>{h.text}</span>\n <span className=\"hidden font-mono text-xs text-muted-foreground @xl:inline\">{durationWords(r.started_at, r.ended_at) ?? \"—\"}</span>\n <span className=\"hidden font-mono text-xs text-muted-foreground @xl:inline\">{creditsWords(r.credits)}</span>\n </button>\n </li>\n );\n })}\n </ol>\n )}\n </div>\n );\n}\n"
33
+ },
34
+ {
35
+ "path": "registry/blocks/run-view/run-controls.jsx",
36
+ "type": "registry:component",
37
+ "target": "@components/makersclaw/blocks/run-view/run-controls.jsx",
38
+ "content": "\"use client\";\n\n// What a person can do with a run that is not over: pause it, let it go\n// on, stop it; and when it is stuck on a step, try that step again, skip it,\n// change the data it was given for this run, or raise the run's limit and\n// go on. It offers only what the run's state allows (automation-model's\n// controlsOf), says plainly what each does, and takes the platform's\n// refusal in the platform's words. It changes nothing itself: every press\n// is `onControl(op, extra)`, which resolves { ok } or { ok: false, error,\n// code }. Props in, callbacks out.\n\nimport { useMemo, useState } from \"react\";\nimport { Button, Field, Input, Textarea, cx } from \"../../ui\";\nimport { CONTROL_DONE, CONTROL_REFUSED, PanelSection, RETRY_FROM, controlsOf, creditsWords, retryFromOf } from \"../automation-model\";\n\nconst jsonText = (v) => (v === undefined || v === null ? \"{}\" : JSON.stringify(v, null, 2));\n\n/**\n * run { status, paused, cap_credits, error }\n * steps the run's step rows (for the step it is stuck at and its data)\n * stepsVisible the member may read a step's data (so may change it)\n * titleOf(id) a step's name\n * onControl(op, { path?, input?, confirmNotSent?, raiseBy?, confirmResend? })\n * op: pause · resume · stop · retry · skip · edit · retry_from\n * busy the op that is out, or null\n */\nexport function RunControls({ run, steps = [], stepsVisible = false, titleOf, onControl, busy = null, className }) {\n const c = useMemo(() => controlsOf(run, steps, { stepsVisible }), [run, steps, stepsVisible]);\n const [mode, setMode] = useState(null);\n const [note, setNote] = useState(null);\n const [text, setText] = useState(\"\");\n const [raise, setRaise] = useState(\"\");\n if (!onControl || c.state === \"ended\") return null;\n\n const blockedRow = c.blocked ? steps.find((r) => r.path === c.blocked.path && r.status !== \"done\") : null;\n const stuck = c.blocked ? (titleOf?.(c.blocked.stepId) ?? c.blocked.stepId) : null;\n\n const send = async (op, extra = {}) => {\n setNote(null);\n let r;\n try {\n r = await onControl(op, extra);\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n if (r && r.ok === false) {\n setNote({ tone: \"bad\", text: r.error || CONTROL_REFUSED[r.code] || \"That did not work. Try again.\" });\n return false;\n }\n setMode(null);\n setNote({ tone: \"ok\", text: CONTROL_DONE[op] ?? \"Done.\" });\n return true;\n };\n\n const openEdit = () => {\n setText(jsonText(blockedRow?.input));\n setNote(null);\n setMode(\"edit\");\n };\n const sendEdit = () => {\n let input;\n try {\n input = JSON.parse(text);\n } catch {\n setNote({ tone: \"bad\", text: \"That is not valid data. Check the commas and quotes.\" });\n return;\n }\n if (input === null || typeof input !== \"object\" || Array.isArray(input)) {\n setNote({ tone: \"bad\", text: \"The data is a set of named values, like { \\\"limit\\\": 50 }.\" });\n return;\n }\n void send(\"edit\", { path: c.blocked.path, input });\n };\n const sendRaise = () => {\n const by = Number(raise || c.raiseDefault);\n if (!Number.isFinite(by) || by <= 0) {\n setNote({ tone: \"bad\", text: \"Say how many credits to add.\" });\n return;\n }\n void send(\"resume\", { raiseBy: by });\n };\n const off = busy !== null;\n\n return (\n <PanelSection title=\"What you can do\">\n <div className={cx(\"grid gap-2\", className)}>\n {c.state === \"paused\" ? <p className=\"m-0 text-[13px]\">Paused. Nothing new starts until you resume it. A step that is already going finishes.</p> : null}\n {c.state === \"parked\" && stuck ? (\n <p className=\"m-0 text-[13px]\">\n {c.blocked.unknownSend ? `“${stuck}” was cut off while it was sending. Nobody knows whether it went.` : `It is stuck at “${stuck}”.`}\n </p>\n ) : null}\n {c.state === \"parked\" && !stuck && !c.raise ? <p className=\"m-0 text-[13px]\">It is waiting for its owner.</p> : null}\n\n {mode === null ? (\n <div className=\"flex flex-wrap gap-2\">\n {c.pause ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"pause\" loading={busy === \"pause\"} disabled={off} onClick={() => void send(\"pause\")}>\n Pause\n </Button>\n ) : null}\n {c.resume && !c.raise ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"play\" loading={busy === \"resume\"} disabled={off} onClick={() => void send(\"resume\")}>\n {c.state === \"paused\" ? \"Resume\" : \"Try again\"}\n </Button>\n ) : null}\n {c.raise ? (\n <Button size=\"sm\" variant=\"secondary\" disabled={off} onClick={() => (setRaise(String(c.raiseDefault)), setMode(\"raise\"))}>\n Raise its limit\n </Button>\n ) : null}\n {c.retry ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"rotate-ccw\" loading={busy === \"retry\"} disabled={off} onClick={() => void send(\"retry\", { path: c.blocked.path, ...(c.blocked.unknownSend ? { confirmNotSent: true } : {}) })}>\n {c.blocked.unknownSend ? \"It did not go: send it again\" : \"Try this step again\"}\n </Button>\n ) : null}\n {c.skip ? (\n <Button size=\"sm\" variant=\"secondary\" loading={busy === \"skip\"} disabled={off} onClick={() => void send(\"skip\", { path: c.blocked.path })}>\n {c.blocked.unknownSend ? \"It did go: skip it\" : \"Skip this step\"}\n </Button>\n ) : null}\n {c.edit ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"pencil\" disabled={off} onClick={openEdit}>\n Change its data\n </Button>\n ) : null}\n {c.stop ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"square\" disabled={off} onClick={() => setMode(\"stop\")}>\n Stop\n </Button>\n ) : null}\n </div>\n ) : null}\n\n {mode === \"stop\" ? (\n <div className=\"grid gap-2\">\n <p className=\"m-0 text-[13px]\">Stop this run? It ends here and cannot be started again. Run the automation again to get a new one.</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"danger\" loading={busy === \"stop\"} disabled={off} onClick={() => void send(\"stop\")}>\n Stop it\n </Button>\n <Button size=\"sm\" variant=\"quiet\" disabled={off} onClick={() => setMode(null)}>\n Let it go on\n </Button>\n </div>\n </div>\n ) : null}\n\n {mode === \"raise\" ? (\n <div className=\"grid gap-2\">\n <p className=\"m-0 text-[13px]\">\n It stopped at its limit{run.cap_credits ? ` of ${creditsWords(run.cap_credits)}` : \"\"}. Add credits to this run’s limit and it goes on from where it stopped. The automation’s own limit stays as it is.\n </p>\n <Field id=\"raise-by\" label=\"Add credits\">\n <Input id=\"raise-by\" type=\"number\" inputMode=\"numeric\" min={1} value={raise} onChange={(e) => setRaise(e.target.value)} className=\"w-32\" />\n </Field>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"primary\" loading={busy === \"resume\"} disabled={off} onClick={sendRaise}>\n Add and go on\n </Button>\n <Button size=\"sm\" variant=\"quiet\" disabled={off} onClick={() => setMode(null)}>\n Cancel\n </Button>\n </div>\n </div>\n ) : null}\n\n {mode === \"edit\" ? (\n <div className=\"grid gap-2\">\n <p className=\"m-0 text-[13px]\">What “{stuck}” is given, for this run only. The automation itself does not change.</p>\n <Field id=\"run-data\" label=\"Its data\">\n <Textarea id=\"run-data\" rows={8} spellCheck={false} value={text} onChange={(e) => setText(e.target.value)} className=\"font-mono text-xs\" />\n </Field>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"primary\" loading={busy === \"edit\"} disabled={off} onClick={sendEdit}>\n Try it with this\n </Button>\n <Button size=\"sm\" variant=\"quiet\" disabled={off} onClick={() => setMode(null)}>\n Cancel\n </Button>\n </div>\n </div>\n ) : null}\n\n {note ? (\n <p role={note.tone === \"bad\" ? \"alert\" : \"status\"} className={cx(\"m-0 text-xs\", note.tone === \"bad\" ? \"text-destructive\" : \"text-muted-foreground\")}>\n {note.text}\n </p>\n ) : null}\n </div>\n </PanelSection>\n );\n}\n\n/**\n * \"Retry from this step\", for the step a member is looking at in a run that\n * is over (done, failed, stopped) or waiting on its owner: start the run over\n * at that step, running it and every step after it again. A step inside a\n * loop starts over from the loop. If a step from there on already sent\n * something, it asks first, in its own words, and only then sends again.\n * Quiet: the common press is a look, not a redo. Draws nothing when the run\n * or the step does not allow it, or without `onControl`.\n *\n * run, steps, def the run, its step rows, the definition it used\n * stepId the step shown\n * onControl(op, { path, confirmResend? }) resolves { ok } or { ok: false, error, code }\n * busy the op that is out, or null\n */\nexport function RetryFromStep({ run, steps = [], def, stepId, onControl, busy = null, className }) {\n const r = useMemo(() => retryFromOf(run, steps, def, stepId), [run, steps, def, stepId]);\n const [asking, setAsking] = useState(false);\n const [note, setNote] = useState(null);\n if (!onControl || !r.show) return null;\n\n const go = async (confirmResend) => {\n setNote(null);\n let out;\n try {\n out = await onControl(\"retry_from\", { path: r.path, ...(confirmResend ? { confirmResend: true } : {}) });\n } catch (e) {\n out = { ok: false, error: e?.message ?? String(e) };\n }\n if (out && out.ok === false) {\n if (out.code === \"needs_confirm_resend\") {\n setAsking(true);\n return;\n }\n setAsking(false);\n setNote({ tone: \"bad\", text: out.error || CONTROL_REFUSED[out.code] || \"That did not work. Try again.\" });\n return;\n }\n setAsking(false);\n setNote({ tone: \"ok\", text: CONTROL_DONE.retry_from });\n };\n const off = busy !== null;\n\n return (\n <div className={cx(\"grid gap-2\", className)}>\n {asking ? (\n <div className=\"grid gap-2\">\n <p className=\"m-0 text-[13px]\">{RETRY_FROM.ask}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"primary\" loading={busy === \"retry_from\"} disabled={off} onClick={() => void go(true)}>\n {RETRY_FROM.yes}\n </Button>\n <Button size=\"sm\" variant=\"quiet\" disabled={off} onClick={() => setAsking(false)}>\n {RETRY_FROM.no}\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" variant=\"quiet\" icon=\"rotate-ccw\" loading={busy === \"retry_from\"} disabled={off} onClick={() => (r.sent ? setAsking(true) : void go(false))}>\n {RETRY_FROM.button}\n </Button>\n </div>\n )}\n <p className=\"m-0 text-xs text-muted-foreground\">{r.inLoop ? `${RETRY_FROM.inLoop(r.loopTitle)} ` : \"\"}{RETRY_FROM.what}</p>\n {note ? (\n <p role={note.tone === \"bad\" ? \"alert\" : \"status\"} className={cx(\"m-0 text-xs\", note.tone === \"bad\" ? \"text-destructive\" : \"text-muted-foreground\")}>\n {note.text}\n </p>\n ) : null}\n </div>\n );\n}\n"
39
+ },
40
+ {
41
+ "path": "registry/blocks/run-view/README.md",
42
+ "type": "registry:file",
43
+ "target": "@components/makersclaw/blocks/run-view/README.md",
44
+ "content": "# RunView\n\nWhat an automation did, on the same canvas as the builder.\n\n- **Every card wears how it went** — done, running, failed, waiting,\n skipped, fixed by the agent, or not reached — as a mark and a word, never\n colour alone. A looped step says how many times; a failed one says why,\n in plain words.\n- **The path taken is lit** and the paths not taken are dimmed, so a run\n reads as one route through the flow. A condition shows which way it went\n (from its own output when the member may read it, else from what ran).\n- **The side panel** gives the run in a few lines: who started it, how\n long, what it cost of what it may, the version; a note when it waits or\n stopped and why; a \"The agent fixed N steps\" list; the report it left.\n Press a card and it becomes that step's account: how it went, what went\n in and what came out (for those who may read it), the agent's fix with\n before and after.\n- **Controls for a run that is not over** (`RunControls`, drawn in the panel\n when you pass `onControl`). Only what the run's state allows:\n *running* — Pause, Stop; *paused* — Resume, Stop; *waiting on its\n owner* — Resume, Stop, and for the step that is stuck: Try this step\n again, Skip this step, Change its data (for this run only; for those who\n may read the step); *held at the run's own limit* — Raise its limit (adds\n credits to this run and goes on; the automation's limit stays). A step\n cut off in a send is said as unknown: \"It did not go: send it again\" or \"It\n did go: skip it\", never repeated on a guess. A refusal is shown in the\n platform's words. Stop asks first.\n- **A test run** (`detail.run.dry`: it read real data and changed\n nothing). A strip over the canvas says how it went in one line (`testWords`:\n \"Test passed: nothing was sent or changed. 3 steps would have run.\"), then\n what the steps would have done (the platform's `run.result.dry.would`:\n \"Would send with send_email.\"), a note for steps that could not be checked\n (they need what an earlier step would have made) and for loops it only\n sampled (\"Looked at the first 5 of 38\"). The chip reads Test passed, Test\n failed or Test stopped; a step that stood in for itself is a dashed card\n with a pencil (the state `would`). A real run draws as before.\n- **Retry from this step** (`RetryFromStep`, drawn in a step's panel when\n you pass `onControl`). A quiet button on a step that ran, in a run that is\n over (done, failed, stopped) or waiting on its owner; not in one that is\n going or paused. It runs that step and every step after it again; the\n steps before keep what they made. A step inside a loop or a branch\n starts over from the one that holds it, and the panel says so. If a step\n from there on already sent something, it asks first, in its own words\n (\"It already sent something. Run it again from here?\"), and only then\n sends again. A refusal is shown in the platform's words.\n- **The agent's fix** on a card and in its panel: what changed, what it\n cost, whether a write can be taken back (a send cannot), and the change\n the agent drafted for next time, to **keep** (a new version that runs) or\n **drop**. A person's own edit of a run's data reads \"You changed its data\"\n and is not marked as the agent's.\n- **RunsList** — the automation's runs, newest first, with a little\n overview (done, failed, in progress, spent), a filter, and for each run:\n what started it, when (in the automation's zone), how it ended, how long\n and what it cost.\n\n```jsx\nimport { RunView, RunsList } from \"../components/blocks/run-view\"; // a Makersclaw app\nimport { RunView, RunsList } from \"@/components/makersclaw/blocks/run-view\"; // a shadcn project\n```\n\n`makersclaw add run-view` lays `flow-editor`, `step-settings`, `fix-note`\nand `automation-model` with it. Client components. Read-only: nothing here\nchanges the flow. Its own width decides its layout: the panel sits beside\nthe canvas from about 768 px and under it below.\n\n## Props\n\n### RunView\n\n- `def` — the definition the run used. Pass `useAutomationRun().definition`\n (the platform pins it) and, when it is null, the live or draft one with a\n `versionNote` (\"This run used version 3. The steps drawn are the current\n ones.\").\n- `detail` — the run's account: `{ run, steps, steps_visible }`\n (`useAutomationRun().detail`). `run` carries `status`, `version`,\n `trigger_type`, `is_test`, `credits`, `cap_credits`, `result`, `error`,\n `wake_at`. A step: `{ path, step_id, attempt, status, credits, error,\n input?, output?, fix? }`.\n- `runs` — the automation's runs, newest first, for the picker and the\n run's number. `tz` — the automation's time zone.\n- `apps`, `actions`, `ownedByApp` — as the builder's, for the read-only\n step panel.\n- `name`, `onBack`, `tabs`, `onPickRun(runId)`.\n- `onRunAgain()`, `runAgainBusy` — Run again (it spends: say so on the\n control where you can). For a test run the button reads \"Test again\".\n- `onControl(op, { path?, input?, confirmNotSent?, raiseBy?, confirmResend? })`\n — `op` is `pause` · `resume` · `stop` · `retry` · `skip` · `edit` ·\n `retry_from` (the step's panel: `path` is the top-level step to start\n over from, `confirmResend` says sending again is right); resolves\n `{ ok }` or `{ ok: false, error, code }` (`useAutomationRun().control`).\n Without it the panel only says a parked run waits for its owner.\n `controlBusy` is the op that is out.\n- `onKeepFix(stepId)`, `onDropFix(stepId)` — keep or drop the change the\n agent drafted for a step it fixed (`useAutomationRun().keepFix` and\n `dropFix`). A fix with no draft has no button. `onUndoFix(stepId)` takes\n back a write.\n- `detail.fixes` — the fixer's attempts and the changes it drafted\n (`useAutomationRun().detail` carries them).\n\n### RunControls\n\nThe controls alone: `run`, `steps`, `stepsVisible`, `titleOf(stepId)`,\n`onControl`, `busy`. Draws nothing for a run that is over, or without\n`onControl`.\n\n### RetryFromStep\n\nThe button alone: `run`, `steps`, `def`, `stepId`, `onControl`, `busy`.\nDraws nothing for a step that did not run, a run that is going or paused,\nor without `onControl`. The rules are `retryFromOf(run, rows, def, stepId)`\nin automation-model (`{ show, path, inLoop, loopTitle, sent }`).\n\n### RunsList\n\n- `runs`, `tz`, `onOpen(runId)`, `howToTest` (the words under \"It has not\n run yet\").\n\n## Hooks (lib/hooks.js)\n\n`useAutomationRun(runId)` → `{ run, steps, stepsVisible, automation,\ndefinition, fixes, detail, control, keepFix, dropFix, busy, loading, error,\nunavailable, reload }`. Looks every 2.5 s while the run goes; a run that\nhas ended is read once. `control(op, opts)` (`opts.confirmResend` is sent as `confirm_resend`) and\n`keepFix(stepId)` resolve\nthe platform's answer (a refusal is `{ ok: false, error, code }`, never\nthrown) and read the run again.\n`useAutomation(id).runs` is the list.\n\n## Never\n\n- Never an Approve in a run: an automation has none. A run waiting on its\n owner says so; the owner answers on the desk. Resume, retry and skip are\n not approvals: they are the owner's controls on a stuck run.\n- Never a Retry for a send that may have gone, without asking whether it did.\n- Never \"Retry from this step\" that sends again without asking first.\n- Never a state by colour alone.\n- Never a step's data to someone who may not read it: pass\n `steps_visible` as the platform sent it.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { useAutomation, useAutomationRun } from \"../lib/hooks\";\nimport { RunView } from \"../components/blocks/run-view\";\n\nexport default function RunPage() {\n const router = useRouter();\n const q = useSearchParams();\n const id = q.get(\"automation\");\n const a = useAutomation(id);\n const r = useAutomationRun(q.get(\"run\"));\n if (!a.definition || !r.detail) return <p className=\"p-4 text-sm text-muted-foreground\">Opening the run…</p>;\n return (\n <div className=\"flex h-[640px] flex-col\">\n <RunView\n def={r.definition ?? a.definition}\n versionNote={r.definition ? null : `This run used version ${r.run.version}. The steps drawn are the current ones.`}\n detail={r.detail}\n runs={a.runs}\n tz={a.automation.tz}\n name={a.automation.name}\n onBack={() => router.push(`?automation=${id}`)}\n onPickRun={(run) => router.push(`?automation=${id}&run=${run}`)}\n onControl={r.control}\n controlBusy={r.busy}\n onKeepFix={r.keepFix}\n onDropFix={r.dropFix}\n />\n </div>\n );\n}\n```\n"
45
+ }
46
+ ],
47
+ "categories": [
48
+ "automation",
49
+ "workflow",
50
+ "run",
51
+ "history",
52
+ "runs",
53
+ "trace",
54
+ "execution",
55
+ "steps",
56
+ "path taken",
57
+ "failed",
58
+ "fixed",
59
+ "retry",
60
+ "overview",
61
+ "n8n",
62
+ "make",
63
+ "pause",
64
+ "resume",
65
+ "stop",
66
+ "skip",
67
+ "retry",
68
+ "control",
69
+ "retry from this step",
70
+ "start over",
71
+ "re-run"
72
+ ],
73
+ "docs": "Installed to components/makersclaw/blocks/run-view. Import: import { RunView, RunsList, RunControls, RetryFromStep } from \"@/components/makersclaw/blocks/run-view\". Props and an example: components/makersclaw/blocks/run-view/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
74
+ "meta": {
75
+ "makersclaw": {
76
+ "version": "0.8.1",
77
+ "useWhen": "A member looks at what an automation did: the page behind a run row, and the Runs tab beside the builder. Fed by useAutomationRun and useAutomation (lib/hooks.js). For a job's run drawn top to bottom in the older kit use workflow-view."
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "schedule-card",
4
+ "type": "registry:block",
5
+ "title": "ScheduleCard",
6
+ "description": "One scheduled job of the app's agent on a card: its name, how its last run went as a mark and words (Ran 07:02, Didn't run, Reading now, Off, Not set up), one line of what it did, this run's cost and the week's credits against the allowance as a bar, when it runs next, Run now with its price on the button, and an on/off switch. It draws what useJobs() returns (lib/hooks.js, GET _claw/jobs); Run now and the switch show only where the door says the member may press them. Its own width decides its layout.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/schedule-card/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/schedule-card/index.js",
16
+ "content": "// The ScheduleCard block — `makersclaw add schedule-card`. Fixed: compose\n// it through its props (README.md beside this file).\nexport { ScheduleCard } from \"./schedule-card\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/schedule-card/schedule-card.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/schedule-card/schedule-card.jsx",
22
+ "content": "\"use client\";\n\n// One scheduled job on a card. Props in, callbacks out; the block never\n// reads the schedule or the runs itself, and formats nothing (each line\n// comes in as words). Tokens only. The state is a mark and a word, never\n// a colour alone. Its own width decides its layout (@container): under\n// about 320 px the next run and Run now stack, the button full width.\n// With an `href` the WHOLE card opens the job: the chevron is a stretched\n// link over the card, and the switch, the Run button and any link or\n// button in `children` sit above it and keep their own press.\n//\n// Moved from the CRM (makersclaw-store packages/crm 0.7.0) into the\n// template's kit (0.8.1, D15), with the on/off switch.\n\nimport { Button, Icon, IconButton, Switch, cx } from \"../../ui\";\n\nconst STATE = {\n ok: { icon: \"circle-check\", text: \"text-success\" },\n failed: { icon: \"circle-x\", text: \"text-destructive\" },\n running: { icon: \"loader\", text: \"text-info\" },\n off: { icon: \"pause\", text: \"text-warning\" },\n none: { icon: \"circle-dashed\", text: \"text-muted-foreground\" },\n};\n\n/**\n * title \"Morning read\"\n * icon a kit Icon name (default \"clock\")\n * state { tone: ok|failed|running|off|none, words: \"Ran 07:02\" }\n * summary what the last run did, one or two lines\n * cost \"640 credits\" (this run)\n * allowance { used: 4180, of: 10000, words: \"4,180 of 10,000 this week\" }\n * next \"Next: Wed 30 Sep, 07:00\"\n * run { label: \"Read now\", price: \"about 700 cr\", onRun, loading, disabled, why }\n * enabled the job is on (the switch's state)\n * onToggle(next) the switch: drawn only when given\n * toggleLabel the switch's words (default \"On\" / \"Off\")\n * toggleBusy the switch's press is out: it waits\n * href the job's page: a chevron at the card's head opens it\n * children anything more, under the summary (a reason, a desk link)\n */\nexport function ScheduleCard({\n title,\n icon = \"clock\",\n state,\n summary,\n cost,\n allowance,\n next,\n run,\n enabled,\n onToggle,\n toggleLabel,\n toggleBusy = false,\n href,\n className,\n children,\n}) {\n const s = STATE[state?.tone] ?? STATE.none;\n const pct = allowance && allowance.of > 0 ? Math.max(0, Math.min(1, allowance.used / allowance.of)) : null;\n const full = pct != null && pct >= 1;\n const on = Boolean(enabled);\n return (\n <section\n aria-label={title}\n className={cx(\n \"@container grid min-w-0 gap-2 rounded-[var(--radius)] border border-border bg-card p-3 text-[13px] text-card-foreground\",\n href && \"relative transition-colors hover:bg-accent/30 [&_:is(a,button):not([data-card-link])]:relative [&_:is(a,button):not([data-card-link])]:z-10\",\n className,\n )}\n >\n <div className=\"flex min-w-0 items-start gap-2\">\n <Icon name={icon} size={16} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <b className=\"min-w-0 truncate font-semibold\">{title}</b>\n {state?.words ? (\n <span className={cx(\"inline-flex min-w-0 items-center gap-1.5 text-xs\", s.text)}>\n <Icon name={s.icon} size={12} className={cx(\"shrink-0\", state?.tone === \"running\" && \"animate-spin motion-reduce:animate-none\")} />\n <span className=\"min-w-0 truncate\">{state.words}</span>\n </span>\n ) : null}\n </div>\n {onToggle ? (\n <Switch\n checked={on}\n disabled={toggleBusy}\n onChange={(e) => onToggle(Boolean(e?.target?.checked))}\n label={toggleLabel ?? (on ? \"On\" : \"Off\")}\n className=\"relative z-10 -my-2 shrink-0 text-xs\"\n />\n ) : null}\n {href ? (\n <IconButton\n icon=\"chevron-right\"\n size=\"sm\"\n href={href}\n label={`Open ${title}`}\n data-card-link=\"\"\n className=\"-my-1.5 -mr-1.5 after:absolute after:inset-0 after:rounded-[var(--radius)] after:content-['']\"\n />\n ) : null}\n </div>\n {summary ? <p className=\"text-xs leading-snug\">{summary}</p> : null}\n {children}\n {cost || allowance ? (\n <div className=\"grid gap-1\">\n <span className={cx(\"text-xs\", full ? \"text-destructive\" : \"text-muted-foreground\")}>{[cost, allowance?.words].filter(Boolean).join(\" · \")}</span>\n {pct != null ? (\n <span aria-hidden=\"true\" className=\"block h-1 overflow-hidden rounded-sm bg-border\">\n <span className={cx(\"block h-full\", full ? \"bg-destructive\" : \"bg-muted-foreground\")} style={{ width: `${pct * 100}%` }} />\n </span>\n ) : null}\n </div>\n ) : null}\n {next || run ? (\n <div className=\"flex flex-col items-stretch gap-1.5 text-xs @xs:flex-row @xs:flex-wrap @xs:items-center\">\n {next ? <span className=\"min-w-0 text-muted-foreground\">{next}</span> : null}\n <span className=\"hidden flex-1 @xs:block\" />\n {run ? (\n <Button\n size=\"sm\"\n icon=\"play\"\n loading={run.loading}\n disabled={run.disabled}\n onClick={run.onRun}\n title={run.why}\n className=\"max-w-full\"\n >\n <span className=\"truncate\">{run.label ?? \"Run now\"}</span>\n {run.price ? <span className=\"font-normal text-muted-foreground\">{run.price}</span> : null}\n </Button>\n ) : null}\n </div>\n ) : null}\n </section>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/schedule-card/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/schedule-card/README.md",
28
+ "content": "# ScheduleCard\n\n> **Superseded** (0.8.1, the automations engine, D26). A job's card for an app whose jobs still run as the agent's wakes (`useJobs`). For an app whose jobs run on the automations engine (`useAutomations`), use `automation-list`: whole rows that open the automation, with the switch, health, next and last run and cost, and the editor and run views behind them. Keep using this block for an app that has not moved: it works as it did.\n\n\nOne scheduled job of the app's agent, on a card beside the day's work:\nits name, how its last run went (a mark and words), one line of what it\ndid, this run's cost and the week's credits against the allowance as a\nbar, when it runs next, **Run now** with its price on the button, and an\non/off switch.\n\nIt is part of the agent kit (0.8.1). The app shows its agent's work\nitself, in its own look; the window's Runs tab stays as the full\nhistory.\n\n```jsx\nimport { ScheduleCard } from \"../components/blocks/schedule-card\"; // a Makersclaw app (\"./blocks/schedule-card\" from components/)\nimport { ScheduleCard } from \"@/components/makersclaw/blocks/schedule-card\"; // a shadcn project\n```\n\nA client component. It formats nothing: every line comes in as words,\nso the screen says \"Next: Wed 30 Sep, 07:00\" in the app's voice. Its own\nwidth decides its layout: under about 320 px the next run and Run now\nstack, the button full width.\n\n## Props\n\n- `title` — \"Morning read\". `icon` — a kit `Icon` name (default\n `\"clock\"`).\n- `state` — `{ tone, words }`. `tone` is `ok` · `failed` · `running` ·\n `off` · `none`; `words` \"Ran 07:02\", \"Didn't run\", \"Reading now\",\n \"Off\", \"Not set up\". Each tone has its own mark, so the state is never\n told by colour alone.\n- `summary` — what the last run did, one or two lines. A failed run's\n summary is the reason, in plain words.\n- `cost` — this run's cost, in words (\"640 credits\").\n- `allowance` — `{ used, of, words }`: the week's credits as a bar, in\n the danger tone once full.\n- `next` — \"Next: Wed 30 Sep, 07:00\".\n- `run` — `{ label, price, onRun, loading, disabled, why }`: Run now,\n its price muted inside the button (\"Read now · about 700 cr\"). `why`\n is the button's title when it is disabled. Leave `run` out where the\n member may not run the job.\n- `enabled`, `onToggle(next)` — the on/off switch. Drawn only when\n `onToggle` is given; leave it out where the member may not switch the\n job. `toggleLabel` — its words (default \"On\" / \"Off\"). `toggleBusy` —\n the press is out.\n- `href` — the job's page. The whole card opens it (the chevron is a stretched link over the card); the switch, the Run button and any link or button in `children` keep their own press.\n- `children` — more under the summary: a reason, a link to the desk.\n- `className` — layout around it. Never colour.\n\n## useJobs (lib/hooks.js)\n\n```js\nconst { jobs, budget, may, busy, runNow, setEnabled } = useJobs();\n```\n\nIt reads `GET _claw/jobs`: each job's `id`, `name`, `does`, `words` (the\nschedule in words, \"Weekdays at 07:00\"), `enabled`, `next_run_at`,\n`last_run` (`{ id, status, ended_at }`), `plan`; the week's `budget`\n(`{ week_cap_credits, week_used_credits }`, or null with no cap); and\n`may` (`{ run, switch, pause }`). `runNow(id)` posts `_claw/run`,\n`setEnabled(id, on)` patches `_claw/jobs/<id>`; both look again after.\n`busy[id]` is `\"run\"` or `\"switch\"` while a press is out. It looks every\n30 s, every 5 s while a run is going, and not at all while the page is\nhidden. On a claw from before the door it is `unavailable`: show the\ncard's quiet \"Not set up\", never an error.\n\n## Who may do what\n\n- Anyone who opens the app reads the card.\n- **Run now** and **the switch**: the app's owner, a workspace owner or\n an admin — `may.run` and `may.switch`. Pass `run` and `onToggle` only\n when they are true. The door refuses the others anyway; a hidden\n button is never the guard.\n\n## Never\n\n- Never Run now without its price on the button: the press spends.\n- Never an Approve: a run that needs the member's yes waits for it on\n the desk.\n- Never change the schedule's time here: that is the window's Settings ›\n Schedule.\n- Never a colour of your own: the app's theme paints it.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useJobs } from \"../lib/hooks\";\nimport { ScheduleCard } from \"../components/blocks/schedule-card\";\n\nconst when = (iso) => (iso ? new Date(iso).toLocaleString(\"en-GB\", { weekday: \"short\", day: \"numeric\", month: \"short\", hour: \"2-digit\", minute: \"2-digit\" }) : null);\n\nexport default function MorningRead() {\n const { jobs, budget, may, busy, runNow, setEnabled } = useJobs();\n const job = jobs.find((j) => j.name === \"Morning read\");\n if (!job) return null;\n const last = job.last_run;\n return (\n <ScheduleCard\n title={job.name}\n icon=\"sun\"\n state={\n !job.enabled ? { tone: \"off\", words: \"Off\" }\n : !last ? { tone: \"none\", words: \"Not run yet\" }\n : last.status === \"running\" ? { tone: \"running\", words: \"Reading now\" }\n : last.status === \"failed\" ? { tone: \"failed\", words: \"Didn't run\" }\n : { tone: \"ok\", words: `Ran ${when(last.ended_at)}` }\n }\n summary={job.does}\n allowance={budget && { used: budget.week_used_credits, of: budget.week_cap_credits, words: `${budget.week_used_credits} of ${budget.week_cap_credits} credits this week` }}\n next={job.enabled && job.next_run_at ? `Next: ${when(job.next_run_at)}` : null}\n run={may.run ? { label: \"Read now\", price: \"about 700 cr\", onRun: () => runNow(job.id), loading: busy[job.id] === \"run\" } : undefined}\n enabled={job.enabled}\n onToggle={may.switch ? (on) => setEnabled(job.id, on) : undefined}\n toggleBusy={busy[job.id] === \"switch\"}\n />\n );\n}\n```\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "schedule",
33
+ "job",
34
+ "automation",
35
+ "agent",
36
+ "run now",
37
+ "next run",
38
+ "cron",
39
+ "credits",
40
+ "allowance",
41
+ "cost",
42
+ "switch",
43
+ "enabled",
44
+ "status",
45
+ "kit"
46
+ ],
47
+ "docs": "Installed to components/makersclaw/blocks/schedule-card. Import: import { ScheduleCard } from \"@/components/makersclaw/blocks/schedule-card\". Props and an example: components/makersclaw/blocks/schedule-card/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
48
+ "meta": {
49
+ "makersclaw": {
50
+ "version": "0.8.1",
51
+ "useWhen": "Superseded by automation-list for an app whose jobs run on the automations engine; still the block for an app that has not moved (its jobs are the agent's wakes). An app whose agent runs on a schedule shows that job beside the day's work: a CRM's morning read on Today, a nightly import, a weekly report, an inbox checked every 15 minutes. For the runs themselves use run-history; for the job drawn as a graph with its runs use workflow-view; for one ask in progress use job-panel."
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "schedule-engine",
4
+ "type": "registry:block",
5
+ "title": "Schedule engine",
6
+ "description": "The rules a dated plan follows, as pure functions over plain rows: working days (Monday to Friday), the four link kinds (finish to start, start to start, finish to finish, start to finish) with lag and lead, the forward and backward pass, slack, the critical path and the long pole, shifting every dependent when a task moves, re-planning unfinished work from today, mending links the dates ignore, summaries that span their sub-tasks, finished work pinned in place, and the outline tree with its 1, 1.1, 1.2 numbers. Also the plans for a write: where a new task goes and with what dates, what an indent, outdent, move or reorder changes, a task's length, who is on two things at once, the rows a baseline keeps, and the short form of a link (2.4FS+2d) both ways. One entry, operate(), answers all of it as JSON so an agent's script and a screen get the same dates.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/schedule-engine/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/schedule-engine/index.js",
16
+ "content": "// The schedule-engine block — `makersclaw add schedule-engine`. Fixed:\n// compose it through its functions (README.md beside this file). Pure\n// JavaScript, no React: the browser, a script and an agent run the same\n// rules.\nexport * from \"./engine\";\nexport { taskTree, descendantsOf, positionBetween, siblingOrder } from \"./tree\";\nexport { KIND_WORDS, lagWords, shortForm, waitsWords, parseWaits } from \"./waits\";\nexport { nextWorkday, summaryChanges, placeNew, familyLinkProblem, outlineChange, lengthChange, overlapsOf, snapshotOf } from \"./place\";\nexport { operate, plain, OPERATIONS } from \"./ops\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/schedule-engine/engine.js",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/schedule-engine/engine.js",
22
+ "content": "// Schedule engine: the rules every date in a plan follows.\n//\n// Pure functions over plain rows: no React, no lib/, no I/O. The browser\n// runs them on every pointer move of a drag (the live preview), the server\n// runs them again on fresh rows before it writes, and the block's tests\n// (test/schedule-engine.test.mjs) run them under node. A database function\n// and an agent's reference can state the same rules. README.md beside this\n// file lists them.\n//\n// Rows in:\n// task { id, parent_id, start_date, due_date, is_milestone, progress, status }\n// link { id, task_id, depends_on_id, kind, lag_days } (task_id waits on depends_on_id)\n// Dates are 'YYYY-MM-DD' text: calendar days, never instants.\n//\n// The model, in working days (Monday to Friday by default):\n// * A task occupies [s, s + d): it starts at the beginning of working day s\n// and runs d working days (d >= 1). Its due date is working day s + d - 1.\n// * A milestone takes no time. It sits at the end of its day, so s is the\n// index after that day and d = 0. A finish-to-start successor of a task\n// that ends on Friday starts Monday; a milestone after it shows Friday.\n// * A task's own planned start is a floor (\"not before\"): the forward pass\n// never moves a task earlier than its planned start, only later.\n// * Finished work (status done, or 100%) is pinned where it happened.\n// * A task with sub-tasks is a summary: its dates are its leaves' span. A\n// link to a summary binds every leaf under it; a link from a summary\n// reads the summary's span.\n// * Cancelled tasks are off the plan; so are tasks with no dates.\n\nconst DAY_MS = 86400000;\n\nconst pad = (n) => String(n).padStart(2, \"0\");\n\n/** 'YYYY-MM-DD' (or a longer ISO string) → whole UTC day number, or null. */\nexport function dayOf(iso) {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(String(iso ?? \"\"));\n return m ? Math.round(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / DAY_MS) : null;\n}\n\n/** Whole UTC day number → 'YYYY-MM-DD'. */\nexport function isoOf(day) {\n const d = new Date(day * DAY_MS);\n return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;\n}\n\n/** Monday 0 … Sunday 6 (1970-01-01 was a Thursday). */\nexport const weekdayOf = (day) => (((day + 3) % 7) + 7) % 7;\n\n/**\n * A working calendar over day numbers.\n * startIndex(day) the working day on or after `day`, as an index\n * lastIndex(day) the working day on or before `day`, as an index\n * dayAt(index) back to a day number\n * \"weekdays\" (the default) works Monday to Friday; \"everyday\" works all\n * seven days.\n */\nexport function makeCalendar(kind = \"weekdays\") {\n if (kind && typeof kind === \"object\") return kind;\n if (kind === \"everyday\") {\n return { kind: \"everyday\", isWorkday: () => true, startIndex: (d) => d, lastIndex: (d) => d, dayAt: (i) => i };\n }\n const index = (day) => Math.floor((day + 3) / 7) * 5 + weekdayOf(day);\n return {\n kind: \"weekdays\",\n isWorkday: (day) => weekdayOf(day) < 5,\n startIndex: (day) => {\n const w = weekdayOf(day);\n return index(w >= 5 ? day + 7 - w : day);\n },\n lastIndex: (day) => {\n const w = weekdayOf(day);\n return index(w >= 5 ? day - (w - 4) : day);\n },\n dayAt: (i) => {\n const weeks = Math.floor(i / 5);\n return weeks * 7 + (i - weeks * 5) - 3;\n },\n };\n}\n\nconst KINDS = {\n finish_to_start: \"FS\",\n start_to_start: \"SS\",\n finish_to_finish: \"FF\",\n start_to_finish: \"SF\",\n fs: \"FS\",\n ss: \"SS\",\n ff: \"FF\",\n sf: \"SF\",\n};\n/** The link kinds as the database writes them. */\nexport const LINK_KINDS = { FS: \"finish_to_start\", SS: \"start_to_start\", FF: \"finish_to_finish\", SF: \"start_to_finish\" };\n\n/** finish_to_start | start_to_start | … (or FS/SS/FF/SF) → \"FS\" | \"SS\" | \"FF\" | \"SF\". */\nexport function linkKind(kind) {\n return KINDS[String(kind ?? \"\").toLowerCase()] ?? \"FS\";\n}\n\n/** A task's stored dates as a span in working days: { dated, s, d, milestone }. */\nexport function spanOf(task, calendar = \"weekdays\") {\n const cal = makeCalendar(calendar);\n const milestone = Boolean(task?.is_milestone);\n const a = dayOf(task?.start_date);\n const b = dayOf(task?.due_date);\n if (a == null && b == null) return { dated: false, s: 0, d: 0, milestone };\n if (milestone) return { dated: true, s: cal.lastIndex(b ?? a) + 1, d: 0, milestone };\n const start = a ?? b;\n const due = Math.max(start, b ?? a);\n const s = cal.startIndex(start);\n return { dated: true, s, d: Math.max(1, cal.lastIndex(due) + 1 - s), milestone };\n}\n\n/** A span back to dates: { start, due } ('YYYY-MM-DD'). A milestone shows its day. */\nexport function datesOf(s, d, milestone, calendar = \"weekdays\") {\n const cal = makeCalendar(calendar);\n if (milestone) {\n const day = isoOf(cal.dayAt(s - 1));\n return { start: day, due: day };\n }\n return { start: isoOf(cal.dayAt(s)), due: isoOf(cal.dayAt(s + Math.max(1, d) - 1)) };\n}\n\n/** Working days from `from` to `to`: 0 when the same working day, negative when `to` is earlier. */\nexport function workdaysBetween(from, to, calendar = \"weekdays\") {\n const cal = makeCalendar(calendar);\n const a = dayOf(from);\n const b = dayOf(to);\n if (a == null || b == null) return null;\n return cal.lastIndex(b) - cal.lastIndex(a);\n}\n\n/** The date `n` working days after `iso` (n may be negative). A weekend `iso` counts from the Friday before. */\nexport function addWorkdays(iso, n, calendar = \"weekdays\") {\n const cal = makeCalendar(calendar);\n const day = dayOf(iso);\n if (day == null) return null;\n return isoOf(cal.dayAt(cal.lastIndex(day) + n));\n}\n\n/** Working days a task runs, from its dates (milestones 0). */\nexport function durationOf(start, due, milestone = false, calendar = \"weekdays\") {\n const span = spanOf({ start_date: start, due_date: due, is_milestone: milestone }, calendar);\n return span.dated ? span.d : null;\n}\n\n/* ------------------------------------------------------------------ */\n/* The network */\n/* ------------------------------------------------------------------ */\n\n/**\n * Build the scheduling network: nodes for tasks (leaf, summary or undated),\n * dependency edges expanded to the successor's leaves, and a topological\n * order over both the edges and the summary roll-ups. Tasks stuck in (or\n * behind) a loop are left out of the order and listed in `stuck`.\n */\nexport function buildNetwork(tasks, links, { calendar = \"weekdays\" } = {}) {\n const cal = makeCalendar(calendar);\n const nodes = new Map();\n const list = [];\n for (const t of tasks ?? []) {\n if (!t || t.id == null) continue;\n const status = String(t.status ?? \"\");\n if (status === \"cancelled\") continue;\n const id = String(t.id);\n if (nodes.has(id)) continue;\n const span = spanOf(t, cal);\n const node = {\n id,\n task: t,\n parentId: t.parent_id == null ? null : String(t.parent_id),\n children: [],\n milestone: span.milestone,\n pinned: status === \"done\" || Number(t.progress ?? 0) >= 100,\n datedSelf: span.dated,\n s: span.s,\n d: span.d,\n kind: null,\n es: null,\n ef: null,\n ls: null,\n lf: null,\n slack: null,\n stuck: false,\n in: [],\n out: [],\n lfLimit: Infinity,\n lsLimit: Infinity,\n leaves: null,\n };\n nodes.set(id, node);\n list.push(node);\n }\n\n // Parents: one outside the set (another project, cancelled) makes a\n // root, and a parent chain that loops is cut where it closes.\n for (const n of list) if (n.parentId && (!nodes.has(n.parentId) || n.parentId === n.id)) n.parentId = null;\n for (const n of list) {\n const seen = new Set([n.id]);\n let cur = n;\n while (cur.parentId) {\n if (seen.has(cur.parentId)) {\n cur.parentId = null;\n break;\n }\n seen.add(cur.parentId);\n cur = nodes.get(cur.parentId);\n }\n }\n for (const n of list) if (n.parentId) nodes.get(n.parentId).children.push(n);\n\n // Kinds, bottom up: a task with a dated leaf below it is a summary.\n const kindOf = (n) => {\n if (n.kind) return n.kind;\n let dated = false;\n for (const c of n.children) {\n const k = kindOf(c);\n if (k === \"leaf\" || k === \"summary\") dated = true;\n }\n n.kind = dated ? \"summary\" : n.datedSelf ? \"leaf\" : \"undated\";\n return n.kind;\n };\n for (const n of list) kindOf(n);\n const leavesOf = (n) => {\n if (n.leaves) return n.leaves;\n n.leaves = n.kind === \"leaf\" ? [n] : n.kind === \"summary\" ? n.children.flatMap(leavesOf) : [];\n return n.leaves;\n };\n for (const n of list) leavesOf(n);\n const isAncestor = (a, b) => {\n for (let p = b.parentId ? nodes.get(b.parentId) : null; p; p = p.parentId ? nodes.get(p.parentId) : null) if (p === a) return true;\n return false;\n };\n\n // Stored spans of summaries (for the conflict check on stored dates).\n for (const n of list) {\n if (n.kind !== \"summary\") continue;\n let s = Infinity;\n let f = -Infinity;\n for (const l of n.leaves) {\n s = Math.min(s, l.s);\n f = Math.max(f, l.s + l.d);\n }\n n.s = s;\n n.d = f - s;\n }\n\n // Dependency edges.\n const linkInfo = new Map();\n const edges = [];\n for (const l of links ?? []) {\n if (!l) continue;\n const id = String(l.id ?? `${l.task_id}>${l.depends_on_id}`);\n const kind = linkKind(l.kind);\n const lag = Math.trunc(Number(l.lag_days ?? 0)) || 0;\n const info = { id, taskId: String(l.task_id), dependsOnId: String(l.depends_on_id), kind, lag, valid: false, reason: null, edges: [] };\n linkInfo.set(id, info);\n const succ = nodes.get(info.taskId);\n const pred = nodes.get(info.dependsOnId);\n if (!succ || !pred) info.reason = \"missing\";\n else if (succ === pred) info.reason = \"self\";\n else if (succ.kind === \"undated\" || pred.kind === \"undated\") info.reason = \"undated\";\n else if (isAncestor(succ, pred) || isAncestor(pred, succ)) info.reason = \"family\";\n else {\n info.valid = true;\n for (const leaf of succ.leaves) {\n const e = { link: info, from: pred, to: leaf, kind, lag };\n edges.push(e);\n info.edges.push(e);\n pred.out.push(e);\n leaf.in.push(e);\n }\n }\n }\n\n // Topological order over dependency edges and roll-ups (child → summary).\n const active = list.filter((n) => n.kind === \"leaf\" || n.kind === \"summary\");\n const indeg = new Map(active.map((n) => [n, 0]));\n const next = new Map(active.map((n) => [n, []]));\n const edge = (a, b) => {\n next.get(a).push(b);\n indeg.set(b, indeg.get(b) + 1);\n };\n for (const e of edges) edge(e.from, e.to);\n for (const n of active) if (n.parentId) edge(n, nodes.get(n.parentId));\n const order = active.filter((n) => indeg.get(n) === 0);\n for (let i = 0; i < order.length; i += 1) {\n for (const m of next.get(order[i])) {\n indeg.set(m, indeg.get(m) - 1);\n if (indeg.get(m) === 0) order.push(m);\n }\n }\n const stuck = active.filter((n) => indeg.get(n) > 0);\n for (const n of stuck) n.stuck = true;\n\n return { calendar: cal, nodes, list, order, stuck, edges, links: linkInfo, isAncestor, finish: null, start: null };\n}\n\n/** The earliest start a dependency edge allows its successor leaf. */\nfunction required(e, pred = e.from) {\n const d = e.to.d;\n switch (e.kind) {\n case \"SS\":\n return pred.es + e.lag;\n case \"FF\":\n return pred.ef + e.lag - d;\n case \"SF\":\n return pred.es + e.lag - d;\n default:\n return pred.ef + e.lag;\n }\n}\n\n/** The same, on stored dates (the conflict check). */\nfunction requiredStored(e) {\n return required(e, { es: e.from.s, ef: e.from.s + e.from.d });\n}\n\n/**\n * Forward pass: each leaf starts at the later of its own planned start and\n * what every link into it allows (pinned work stays put); each summary\n * spans its children. Sets es/ef (working-day indexes) on every node, and\n * net.start / net.finish.\n */\nexport function forwardPass(net) {\n const rollup = (n) => {\n let es = Infinity;\n let ef = -Infinity;\n for (const c of n.children) {\n if (c.es == null) continue;\n es = Math.min(es, c.es);\n ef = Math.max(ef, c.ef);\n }\n n.es = es;\n n.ef = ef;\n };\n for (const n of net.order) {\n if (n.kind === \"summary\") {\n rollup(n);\n continue;\n }\n let es = n.s;\n if (!n.pinned) for (const e of n.in) es = Math.max(es, required(e));\n n.es = es;\n n.ef = es + n.d;\n }\n // Stuck in a loop: keep the stored dates; summaries roll up deepest first.\n const depth = (n) => {\n let k = 0;\n for (let p = n.parentId; p; p = net.nodes.get(p).parentId) k += 1;\n return k;\n };\n for (const n of net.stuck) {\n if (n.kind === \"leaf\") {\n n.es = n.s;\n n.ef = n.s + n.d;\n }\n }\n for (const n of [...net.stuck].filter((k) => k.kind === \"summary\").sort((a, b) => depth(b) - depth(a))) rollup(n);\n\n let start = Infinity;\n let finish = -Infinity;\n for (const n of net.list) {\n if (n.kind !== \"leaf\") continue;\n start = Math.min(start, n.es);\n finish = Math.max(finish, n.ef);\n }\n net.start = Number.isFinite(start) ? start : null;\n net.finish = Number.isFinite(finish) ? finish : null;\n return net;\n}\n\n/**\n * Backward pass: the latest each leaf may finish without moving the\n * project's finish (or breaking a link to work that is itself as late as\n * it may be). Total slack = latest finish − earliest finish, in working\n * days. A link to a summary binds its leaves; a link from a summary binds\n * every leaf for a finish (FS, FF) and the leaves that set its start for a\n * start (SS, SF).\n */\nexport function backwardPass(net) {\n if (net.finish == null) return net;\n for (const n of net.list) {\n n.lfLimit = Infinity;\n n.lsLimit = Infinity;\n n.ls = null;\n n.lf = null;\n n.slack = null;\n }\n for (let i = net.order.length - 1; i >= 0; i -= 1) {\n const n = net.order[i];\n if (n.kind !== \"leaf\") continue;\n let lf = Math.min(net.finish, n.lfLimit, n.lsLimit + n.d);\n for (let a = n.parentId ? net.nodes.get(n.parentId) : null; a; a = a.parentId ? net.nodes.get(a.parentId) : null) {\n lf = Math.min(lf, a.lfLimit);\n if (n.es === a.es) lf = Math.min(lf, a.lsLimit + n.d);\n }\n n.lf = lf;\n n.ls = lf - n.d;\n n.slack = n.lf - n.ef;\n for (const e of n.in) {\n const p = e.from;\n if (e.kind === \"SS\") p.lsLimit = Math.min(p.lsLimit, n.ls - e.lag);\n else if (e.kind === \"FF\") p.lfLimit = Math.min(p.lfLimit, n.lf - e.lag);\n else if (e.kind === \"SF\") p.lsLimit = Math.min(p.lsLimit, n.lf - e.lag);\n else p.lfLimit = Math.min(p.lfLimit, n.ls - e.lag);\n }\n }\n // Summaries: the least slack of their leaves.\n for (const n of net.list) {\n if (n.kind !== \"summary\") continue;\n let slack = null;\n for (const l of n.leaves) if (l.slack != null) slack = slack == null ? l.slack : Math.min(slack, l.slack);\n n.slack = slack;\n }\n return net;\n}\n\n/** The critical path: leaves with no slack, in plan order (start, then finish). */\nexport function criticalPath(net) {\n return net.list\n .filter((n) => n.kind === \"leaf\" && n.slack != null && n.slack <= 0)\n .sort((a, b) => a.es - b.es || a.ef - b.ef || net.list.indexOf(a) - net.list.indexOf(b))\n .map((n) => n.id);\n}\n\n/* ------------------------------------------------------------------ */\n/* The plan: everything the screen draws */\n/* ------------------------------------------------------------------ */\n\nconst iso10 = (v) => (v == null ? null : String(v).slice(0, 10));\n\n/**\n * Forward pass, backward pass and critical path over one project's tasks\n * and links, described in dates.\n *\n * → {\n * rows: Map(id → { id, parentId, summary, milestone, pinned, dated, stuck,\n * start, due stored dates (a summary: its leaves' span)\n * planStart, planDue where the forward pass puts it\n * lateStart, lateDue the latest it may run (leaves)\n * duration working days\n * slack, critical, progress, shifted, drivers, leafIds }),\n * links: Map(id → { id, taskId, dependsOnId, kind, lag, valid, reason,\n * slack, critical, violated, violation }),\n * start, finish, the plan's first and last working day\n * critical, critical leaf ids in plan order\n * stuck, violations, link ids whose stored dates break them\n * }\n */\nexport function schedule(tasks, links, opts = {}) {\n const net = buildNetwork(tasks, links, opts);\n forwardPass(net);\n backwardPass(net);\n return describe(net);\n}\n\nfunction describe(net) {\n const cal = net.calendar;\n const rows = new Map();\n const criticalIds = criticalPath(net);\n const critical = new Set(criticalIds);\n\n for (const n of net.list) {\n const t = n.task;\n const base = {\n id: n.id,\n parentId: n.parentId,\n summary: n.kind === \"summary\",\n milestone: n.milestone,\n pinned: n.pinned,\n dated: n.kind !== \"undated\",\n stuck: n.stuck,\n progress: Math.max(0, Math.min(100, Number(t.progress ?? 0) || 0)),\n start: null,\n due: null,\n planStart: null,\n planDue: null,\n lateStart: null,\n lateDue: null,\n duration: null,\n slack: n.slack,\n critical: false,\n shifted: false,\n drivers: [],\n leafIds: n.leaves.map((l) => l.id),\n };\n if (n.kind === \"leaf\") {\n const a = iso10(t.start_date);\n const b = iso10(t.due_date);\n const stored = datesOf(n.s, n.d, n.milestone, cal);\n const plan = datesOf(n.es, n.d, n.milestone, cal);\n base.start = n.milestone ? b ?? a : a ?? b;\n base.due = n.milestone ? b ?? a : b ?? a;\n base.planStart = plan.start;\n base.planDue = plan.due;\n if (n.ls != null) {\n const late = datesOf(n.ls, n.d, n.milestone, cal);\n base.lateStart = late.start;\n base.lateDue = late.due;\n }\n base.duration = n.d;\n base.critical = critical.has(n.id);\n base.shifted = plan.start !== stored.start || plan.due !== stored.due;\n const drivers = new Set();\n if (!n.pinned) for (const e of n.in) if (required(e) === n.es && n.es > n.s) drivers.add(e.link.id);\n base.drivers = [...drivers];\n }\n rows.set(n.id, base);\n }\n\n // Summaries: the span of their leaves, stored and planned; progress\n // weighted by working days.\n for (const n of net.list) {\n if (n.kind !== \"summary\") continue;\n const row = rows.get(n.id);\n let work = 0;\n let done = 0;\n for (const l of n.leaves) {\n const r = rows.get(l.id);\n if (!row.start || r.start < row.start) row.start = r.start;\n if (!row.due || r.due > row.due) row.due = r.due;\n if (!row.planStart || r.planStart < row.planStart) row.planStart = r.planStart;\n if (!row.planDue || r.planDue > row.planDue) row.planDue = r.planDue;\n if (r.critical) row.critical = true;\n if (r.shifted) row.shifted = true;\n work += l.d;\n done += (l.d * r.progress) / 100;\n }\n row.duration = Math.max(0, cal.lastIndex(dayOf(row.due)) + 1 - cal.startIndex(dayOf(row.start)));\n row.progress = work > 0 ? Math.round((done / work) * 100) : n.leaves.every((l) => l.pinned) ? 100 : 0;\n }\n\n // Links: tightness on the plan, and conflicts on the stored dates.\n const outLinks = new Map();\n const violations = [];\n for (const info of net.links.values()) {\n const out = { id: info.id, taskId: info.taskId, dependsOnId: info.dependsOnId, kind: info.kind, lag: info.lag, valid: info.valid, reason: info.reason, slack: null, critical: false, violated: false, violation: 0 };\n if (info.valid && !info.edges.some((e) => e.to.stuck || e.from.stuck)) {\n let slack = Infinity;\n let tightCritical = false;\n let worst = 0;\n for (const e of info.edges) {\n const gap = e.to.es - required(e);\n slack = Math.min(slack, gap);\n if (gap <= 0 && critical.has(e.to.id)) tightCritical = true;\n if (!e.to.pinned) worst = Math.max(worst, requiredStored(e) - e.to.s);\n }\n out.slack = slack;\n const predCritical = info.edges[0].from.kind === \"summary\" ? info.edges[0].from.leaves.some((l) => critical.has(l.id)) : critical.has(info.dependsOnId);\n out.critical = tightCritical && predCritical;\n if (worst > 0) {\n out.violated = true;\n out.violation = worst;\n violations.push(info.id);\n }\n }\n outLinks.set(info.id, out);\n }\n\n let start = null;\n let finish = null;\n for (const r of rows.values()) {\n if (!r.dated || r.summary) continue;\n if (!start || r.planStart < start) start = r.planStart;\n if (!finish || r.planDue > finish) finish = r.planDue;\n }\n\n return {\n calendar: cal.kind,\n rows,\n links: outLinks,\n start,\n finish,\n critical: criticalIds,\n stuck: net.stuck.filter((n) => n.kind === \"leaf\").map((n) => n.id),\n violations,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Moving work */\n/* ------------------------------------------------------------------ */\n\nconst sameDates = (task, start, due) => iso10(task?.start_date) === start && iso10(task?.due_date) === due;\n\n/**\n * Move tasks and shift everything that depends on them.\n *\n * `moves` is [{ id, start, due }]: a task's new dates (a summary's `start`\n * moves its whole block of unfinished leaves by the same working days).\n * Each moved task keeps its new start as its floor; the forward pass then\n * pushes every task a link binds, as far as it must and no further.\n *\n * → { changes: [{ id, start_date, due_date, from: { start_date, due_date }, summary? }],\n * clamped: [{ id, asked, got, drivers }] a move the links pushed later\n * before, after (plans), finishBefore, finishAfter }\n *\n * Only tasks the move affects are in `changes`: a task whose plan did not\n * change keeps its stored dates, even if they were already off.\n *\n * `opts.nextLinks`: the links after the change, when the change is a link\n * (a new link pushes its successor the same way a move does).\n * `opts.nextTasks`: the tasks after a change of outline (a task put under\n * another: the new parent's links now bind it). Their dates are the same.\n */\nexport function shiftDependents(tasks, links, moves, opts = {}) {\n const cal = makeCalendar(opts.calendar ?? \"weekdays\");\n const before = schedule(tasks, links, { ...opts, calendar: cal });\n const byId = new Map((tasks ?? []).filter(Boolean).map((t) => [String(t.id), t]));\n const patched = new Map();\n const asked = new Map();\n\n const put = (id, start, due) => {\n const t = patched.get(id) ?? byId.get(id);\n const milestone = Boolean(t.is_milestone);\n patched.set(id, { ...t, start_date: milestone ? due ?? start : start, due_date: milestone ? due ?? start : due });\n asked.set(id, milestone ? due ?? start : start);\n };\n\n for (const m of moves ?? []) {\n const id = String(m?.id ?? \"\");\n const row = before.rows.get(id);\n if (!row || !byId.has(id)) continue;\n if (row.summary) {\n const target = dayOf(m.start);\n if (target == null || !row.start) continue;\n const delta = cal.startIndex(target) - cal.startIndex(dayOf(row.start));\n if (!delta) continue;\n for (const leafId of row.leafIds) {\n const leaf = before.rows.get(leafId);\n if (!leaf || leaf.pinned) continue;\n const span = spanOf(patched.get(leafId) ?? byId.get(leafId), cal);\n const moved = datesOf(span.s + delta, span.d, span.milestone, cal);\n put(leafId, moved.start, moved.due);\n }\n } else {\n const t = byId.get(id);\n const start = iso10(m.start) ?? iso10(t.start_date) ?? iso10(m.due);\n const due = iso10(m.due) ?? iso10(t.due_date) ?? start;\n put(id, start, due);\n }\n }\n\n const base = opts.nextTasks ?? tasks ?? [];\n const movedTasks = base.map((t) => (t && patched.has(String(t.id)) ? { ...t, ...patched.get(String(t.id)), parent_id: t.parent_id } : t));\n const after = schedule(movedTasks, opts.nextLinks ?? links, { ...opts, calendar: cal });\n\n const changes = [];\n const changedLeaves = new Set();\n for (const [id, a] of after.rows) {\n if (!a.dated || a.summary) continue;\n const b = before.rows.get(id);\n const touched = patched.has(id) || !b || a.planStart !== b.planStart || a.planDue !== b.planDue;\n if (!touched) continue;\n const t = byId.get(id);\n if (sameDates(t, a.planStart, a.planDue)) continue;\n changes.push({ id, start_date: a.planStart, due_date: a.planDue, from: { start_date: iso10(t.start_date), due_date: iso10(t.due_date) } });\n changedLeaves.add(id);\n }\n for (const [id, a] of after.rows) {\n if (!a.summary || !a.leafIds.some((l) => changedLeaves.has(l))) continue;\n const t = byId.get(id);\n if (sameDates(t, a.planStart, a.planDue)) continue;\n changes.push({ id, start_date: a.planStart, due_date: a.planDue, from: { start_date: iso10(t.start_date), due_date: iso10(t.due_date) }, summary: true });\n }\n\n const clamped = [];\n for (const [id, wanted] of asked) {\n const a = after.rows.get(id);\n if (!a) continue;\n const want = datesOf(spanOf(patched.get(id), cal).s, 1, a.milestone, cal).start;\n if (a.planStart > want) clamped.push({ id, asked: wanted, got: a.planStart, drivers: a.drivers });\n }\n\n return { changes, clamped, before, after, finishBefore: before.finish, finishAfter: after.finish };\n}\n\n/**\n * Re-plan unfinished work off the past, the way the morning check does:\n * * not started (0%) and its start has passed → it starts today\n * * started, and its due date has passed → it keeps its start and needs\n * its remaining share of the work from today: ceil(duration × (100 −\n * progress) / 100) working days, at least 1\n * * a milestone whose day has passed, not done → today\n * Then every dependent shifts. `today` is 'YYYY-MM-DD'; on a weekend\n * \"today\" is the next Monday. Dates compared are the plan's.\n *\n * → shiftDependents' result, plus `moves` [{ id, start, due, reason }]\n * (reason: not_started | overdue | milestone).\n */\nexport function replanFromToday(tasks, links, today, opts = {}) {\n const cal = makeCalendar(opts.calendar ?? \"weekdays\");\n const t0 = dayOf(today);\n const plan = schedule(tasks, links, { ...opts, calendar: cal });\n const moves = [];\n if (t0 != null) {\n const ti = cal.startIndex(t0);\n const todayIso = isoOf(cal.dayAt(ti));\n for (const r of plan.rows.values()) {\n if (!r.dated || r.summary || r.pinned || r.stuck) continue;\n if (r.milestone) {\n if (dayOf(r.planDue) < t0) moves.push({ id: r.id, start: todayIso, due: todayIso, reason: \"milestone\" });\n continue;\n }\n if (r.progress === 0 && dayOf(r.planStart) < t0) {\n moves.push({ id: r.id, start: todayIso, due: isoOf(cal.dayAt(ti + r.duration - 1)), reason: \"not_started\" });\n } else if (r.progress > 0 && dayOf(r.planDue) < t0) {\n const remaining = Math.max(1, Math.ceil((r.duration * (100 - r.progress)) / 100));\n moves.push({ id: r.id, start: r.planStart, due: isoOf(cal.dayAt(ti + remaining - 1)), reason: \"overdue\" });\n }\n }\n }\n return { ...shiftDependents(tasks, links, moves, { ...opts, calendar: cal }), moves };\n}\n\n/**\n * Mend broken links: every unfinished task that starts earlier than its\n * links allow moves to its earliest date, and whatever waits on it follows.\n * → shiftDependents' result, plus `moves`.\n */\nexport function alignConflicts(tasks, links, opts = {}) {\n const plan = schedule(tasks, links, opts);\n const moves = [];\n for (const r of plan.rows.values()) if (r.dated && !r.summary && !r.pinned && !r.stuck && r.shifted) moves.push({ id: r.id, start: r.planStart, due: r.planDue });\n return { ...shiftDependents(tasks, links, moves, opts), moves };\n}\n\n/**\n * Why `taskId` can't wait on `dependsOnId`, in words, or null when it can.\n * Refuses: itself, its own summary or sub-task, a pair already linked\n * (either way round), and any link that would close a loop.\n */\nexport function linkProblem(tasks, links, taskId, dependsOnId, opts = {}) {\n const a = String(taskId ?? \"\");\n const b = String(dependsOnId ?? \"\");\n if (!a || !b) return \"Pick two tasks.\";\n if (a === b) return \"A task can't wait on itself.\";\n const net = buildNetwork(tasks, links, opts);\n const succ = net.nodes.get(a);\n const pred = net.nodes.get(b);\n if (!succ || !pred) return \"Both tasks must be in this project.\";\n if (net.isAncestor(succ, pred) || net.isAncestor(pred, succ)) return \"A task can't wait on its own summary or sub-task.\";\n if ((links ?? []).some((l) => l && ((String(l.task_id) === a && String(l.depends_on_id) === b) || (String(l.task_id) === b && String(l.depends_on_id) === a))))\n return \"These two are already linked.\";\n if (succ.kind === \"undated\" || pred.kind === \"undated\") return \"Give both tasks dates first.\";\n const trial = buildNetwork(tasks, [...(links ?? []), { id: \"__trial\", task_id: a, depends_on_id: b }], opts);\n if (trial.stuck.length > net.stuck.length) return \"That would make a loop: the other task already waits on this one.\";\n return null;\n}\n\n/**\n * The long pole: the unfinished critical task with the most work left\n * (working days × share not done); the later wins a tie (it sits nearer\n * the finish, so the finish waits on it most directly). Null when\n * nothing critical is left.\n */\nexport function longPole(plan) {\n let best = null;\n let bestLeft = -1;\n for (const id of plan?.critical ?? []) {\n const r = plan.rows.get(id);\n if (!r || r.pinned || r.milestone) continue;\n const left = (r.duration * (100 - r.progress)) / 100;\n if (left >= bestLeft) {\n best = r;\n bestLeft = left;\n }\n }\n return best ? best.id : null;\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/schedule-engine/tree.js",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/schedule-engine/tree.js",
28
+ "content": "// The task tree: outline order and outline numbers (1, 1.1, 1.2, 2 …).\n// Pure, like engine.js: the chart, the server's writes and the tests use it.\n\nconst num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);\n\n/** Sibling order: position, then when it was made, then id. */\nexport function siblingOrder(a, b) {\n return num(a.position) - num(b.position) || String(a.created_at ?? \"\").localeCompare(String(b.created_at ?? \"\")) || String(a.id).localeCompare(String(b.id));\n}\n\n/**\n * → { order: [{ id, parentId, depth, outline, childCount, index }], byId: Map(id → entry) }\n * `order` walks the tree depth first. A parent outside `tasks` makes a\n * root; a parent chain that loops is cut.\n */\nexport function taskTree(tasks) {\n const list = (tasks ?? []).filter((t) => t && t.id != null);\n const ids = new Set(list.map((t) => String(t.id)));\n const kids = new Map();\n for (const t of list) {\n const pid = t.parent_id != null && ids.has(String(t.parent_id)) && String(t.parent_id) !== String(t.id) ? String(t.parent_id) : \"\";\n if (!kids.has(pid)) kids.set(pid, []);\n kids.get(pid).push(t);\n }\n for (const arr of kids.values()) arr.sort(siblingOrder);\n const order = [];\n const byId = new Map();\n const seen = new Set();\n const walk = (pid, depth, prefix) => {\n (kids.get(pid) ?? []).forEach((t, i) => {\n const id = String(t.id);\n if (seen.has(id)) return;\n seen.add(id);\n const outline = prefix ? `${prefix}.${i + 1}` : String(i + 1);\n const entry = { id, parentId: pid || null, depth, outline, childCount: (kids.get(id) ?? []).length, index: order.length };\n order.push(entry);\n byId.set(id, entry);\n walk(id, depth + 1, outline);\n });\n };\n walk(\"\", 0, \"\");\n // Anything a loop hid becomes a root at the end.\n for (const t of list) {\n const id = String(t.id);\n if (seen.has(id)) continue;\n seen.add(id);\n const entry = { id, parentId: null, depth: 0, outline: String(order.filter((e) => e.depth === 0).length + 1), childCount: 0, index: order.length };\n order.push(entry);\n byId.set(id, entry);\n }\n return { order, byId };\n}\n\n/** Every id below `id` (not including it). */\nexport function descendantsOf(tasks, id) {\n const out = new Set();\n const want = [String(id)];\n while (want.length) {\n const p = want.pop();\n for (const t of tasks ?? []) {\n const tid = String(t.id);\n if (String(t.parent_id ?? \"\") === p && !out.has(tid)) {\n out.add(tid);\n want.push(tid);\n }\n }\n }\n return out;\n}\n\n/** A position between two siblings (or after the last one). */\nexport function positionBetween(before, after) {\n const a = before == null ? null : num(before.position);\n const b = after == null ? null : num(after.position);\n if (a == null && b == null) return 1;\n if (a == null) return b - 1;\n if (b == null) return a + 1;\n return (a + b) / 2;\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/schedule-engine/waits.js",
32
+ "type": "registry:component",
33
+ "target": "@components/makersclaw/blocks/schedule-engine/waits.js",
34
+ "content": "// The short form schedulers write for \"what a task waits on\": `2.5FS`,\n// `6.3SS+5d`, `3.4SF-2d`, several joined with a semicolon. An outline\n// number (the tree's 1, 1.1, 1.2.3) names the task, FS / SS / FF / SF the\n// kind (finish-to-start when left out), and a signed number of working\n// days the lag. Pure, like the engine.\n\nimport { LINK_KINDS, linkKind } from \"./engine\";\n\n/** The four kinds, named in full and in a clause (\"B starts after A finishes\"). */\nexport const KIND_WORDS = {\n FS: { name: \"Finish to start\", clause: (b, a) => `${b} starts after ${a} finishes` },\n SS: { name: \"Start to start\", clause: (b, a) => `${b} can start once ${a} has started` },\n FF: { name: \"Finish to finish\", clause: (b, a) => `${b} can finish once ${a} has finished` },\n SF: { name: \"Start to finish\", clause: (b, a) => `${b} can finish once ${a} has started` },\n};\n\n/** `+2d`, `-1d` or \"\" for no lag. */\nexport function lagWords(lag) {\n const n = Math.trunc(Number(lag) || 0);\n return n ? `${n > 0 ? \"+\" : \"-\"}${Math.abs(n)}d` : \"\";\n}\n\n/** One link as the short form: `2.4FS+2d`. */\nexport function shortForm(outline, kind, lag) {\n return `${outline}${linkKind(kind)}${lagWords(lag)}`;\n}\n\n/**\n * What a task waits on, in the short form.\n * `links`: [{ task_id, depends_on_id, kind, lag_days }]; `outlineOf(id)` → \"2.4\".\n * Only links of `taskId`; ordered by the predecessor's outline.\n */\nexport function waitsWords(links, taskId, outlineOf) {\n return (links ?? [])\n .filter((l) => String(l.task_id) === String(taskId))\n .map((l) => ({ outline: outlineOf(String(l.depends_on_id)) ?? \"\", kind: l.kind, lag: l.lag_days }))\n .filter((l) => l.outline)\n .sort((a, b) => a.outline.localeCompare(b.outline, undefined, { numeric: true }))\n .map((l) => shortForm(l.outline, l.kind, l.lag))\n .join(\"; \");\n}\n\nconst PART = /^\\s*([0-9]+(?:\\.[0-9]+)*)\\s*(FS|SS|FF|SF)?\\s*(?:([+-])\\s*([0-9]+)\\s*d?)?\\s*$/i;\n\n/**\n * Read the short form back. `idOf(outline)` → the task's id, or null when\n * the file / the screen has no such task.\n * → { links: [{ depends_on_id, outline, kind (\"finish_to_start\" …), lag_days }],\n * problems: [{ part, why }] }\n */\nexport function parseWaits(text, idOf) {\n const links = [];\n const problems = [];\n for (const raw of String(text ?? \"\").split(/[;,]/)) {\n const part = raw.trim();\n if (!part) continue;\n const m = PART.exec(part);\n if (!m) {\n problems.push({ part, why: `\"${part}\" isn't a task number with a kind and a lag, like 2.4FS+2d.` });\n continue;\n }\n const outline = m[1];\n const id = idOf(outline);\n if (id == null) {\n problems.push({ part, why: `There is no task ${outline}.` });\n continue;\n }\n if (links.some((l) => l.depends_on_id === String(id))) {\n problems.push({ part, why: `Task ${outline} is listed twice.` });\n continue;\n }\n const lag = m[3] ? Number(m[4]) * (m[3] === \"-\" ? -1 : 1) : 0;\n if (lag < -250 || lag > 250) {\n problems.push({ part, why: \"A lag is between -250 and 250 working days.\" });\n continue;\n }\n links.push({ depends_on_id: String(id), outline, kind: LINK_KINDS[linkKind(m[2] || \"FS\")], lag_days: lag });\n }\n return { links, problems };\n}\n"
35
+ },
36
+ {
37
+ "path": "registry/blocks/schedule-engine/place.js",
38
+ "type": "registry:component",
39
+ "target": "@components/makersclaw/blocks/schedule-engine/place.js",
40
+ "content": "// Planning a write: where a new task goes, what an outline change moves, which\n// summaries must re-span. Pure functions over the same rows the engine takes,\n// so a screen and an agent that ask the same question get the same answer.\n//\n// None of these writes anything. Each returns exactly the values a write\n// needs ({ parent_id, sort_order, start_date, due_date, summaries, changes … }),\n// and the caller hands them to its own save.\n//\n// Rows in (as the engine's):\n// task { id, parent_id, title, start_date, due_date, is_milestone, progress, status, position }\n// link { id, task_id, depends_on_id, kind, lag_days }\n\nimport { addWorkdays, datesOf, dayOf, isoOf, makeCalendar, schedule, shiftDependents, spanOf } from \"./engine\";\nimport { descendantsOf, positionBetween, siblingOrder, taskTree } from \"./tree\";\n\nconst ISO = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst same = (a, b) => String(a ?? \"\") === String(b ?? \"\");\nconst quote = (s) => `“${String(s ?? \"\").slice(0, 90)}”`;\n\n/** The working day on or after `iso`, as a date. */\nexport function nextWorkday(iso, calendar = \"weekdays\") {\n const cal = makeCalendar(calendar);\n const d = dayOf(iso);\n return d == null ? null : isoOf(cal.dayAt(cal.startIndex(d)));\n}\n\n/**\n * Summaries whose stored dates differ from the span of the work under them:\n * the ancestors of each id (and each id itself when it is a summary).\n * → [{ id, start_date, due_date }]\n */\nexport function summaryChanges(tasks, links, ids) {\n const plan = schedule(tasks, links);\n const byId = new Map((tasks ?? []).map((t) => [String(t.id), t]));\n const want = new Set();\n for (const id of ids ?? []) {\n for (let p = byId.get(String(id))?.parent_id; p; p = byId.get(String(p))?.parent_id) {\n if (want.has(String(p))) break;\n want.add(String(p));\n }\n if (id) want.add(String(id));\n }\n const out = [];\n for (const id of want) {\n const row = plan.rows.get(id);\n const t = byId.get(id);\n if (!row?.summary || !t) continue;\n if (!same(t.start_date, row.start) || !same(t.due_date, row.due)) out.push({ id, start_date: row.start, due_date: row.due });\n }\n return out;\n}\n\n/**\n * Where a new task goes and the dates it starts with.\n *\n * add: { title, parent_id, after_id, start, due, days, milestone }\n * today: 'YYYY-MM-DD' in the team's zone\n * project: { start_date }\n *\n * It lands after `after_id` at the same level, or as the last sub-task of\n * `parent_id`, or last in the project. Undated input gets dates: a first\n * sub-task takes its parent's dates; a task added after another starts the\n * next working day; otherwise the later of today and the project's start.\n * Three working days long unless `days` says.\n *\n * → { ok, parent_id, sort_order, start_date, due_date, is_milestone,\n * resum: boolean (the parent already had sub-tasks: re-span it after) }\n */\nexport function placeNew(tasks, { add = {}, today, project = null } = {}) {\n const list = tasks ?? [];\n const byId = new Map(list.map((t) => [String(t.id), t]));\n const after = add.after_id && byId.has(String(add.after_id)) ? byId.get(String(add.after_id)) : null;\n const parent = add.parent_id && byId.has(String(add.parent_id)) ? byId.get(String(add.parent_id)) : after?.parent_id ? byId.get(String(after.parent_id)) ?? null : null;\n const siblings = list.filter((t) => same(t.parent_id, parent?.id)).sort(siblingOrder);\n let sort_order;\n if (after && same(after.parent_id, parent?.id)) {\n const i = siblings.findIndex((t) => same(t.id, after.id));\n sort_order = positionBetween(after, siblings[i + 1] ?? null);\n } else sort_order = positionBetween(siblings[siblings.length - 1] ?? null, null);\n\n let s = ISO.test(String(add.start ?? \"\")) ? add.start : null;\n let d = ISO.test(String(add.due ?? \"\")) ? add.due : null;\n if (!s) {\n const parentLeaf = parent && !list.some((t) => same(t.parent_id, parent.id)) && parent.start_date;\n if (parentLeaf) {\n s = String(parent.start_date);\n d = d ?? String(parent.due_date ?? parent.start_date);\n } else if (after?.due_date) s = addWorkdays(String(after.due_date), 1);\n else {\n const now = ISO.test(String(today ?? \"\")) ? today : isoOf(Math.floor(Date.now() / 86400000));\n const begins = project?.start_date ? String(project.start_date) : null;\n s = nextWorkday(begins && begins > now ? begins : now);\n }\n }\n const length = Math.max(1, Math.min(500, Math.trunc(Number(add.days) || 0)));\n const milestone = Boolean(add.milestone);\n if (milestone) d = s = d ?? s;\n else if (Number(add.days) >= 1) d = addWorkdays(nextWorkday(s), length - 1);\n else if (!d || d < s) d = addWorkdays(nextWorkday(s), 2);\n return {\n ok: true,\n parent_id: parent ? String(parent.id) : null,\n sort_order,\n start_date: s,\n due_date: d,\n is_milestone: milestone,\n resum: Boolean(parent && list.some((t) => same(t.parent_id, parent.id))),\n };\n}\n\n/** A link between a task (or anything under it) and a would-be parent can't stay. */\nexport function familyLinkProblem(tasks, links, id, parentId) {\n const under = descendantsOf(tasks, id);\n under.add(String(id));\n const bad = (links ?? []).find((l) => (under.has(String(l.task_id)) && same(l.depends_on_id, parentId)) || (under.has(String(l.depends_on_id)) && same(l.task_id, parentId)));\n if (!bad) return null;\n const title = (x) => (tasks ?? []).find((t) => same(t.id, x))?.title ?? \"a task\";\n return `Remove the link between ${quote(title(bad.task_id))} and ${quote(title(bad.depends_on_id))} first: a task can't wait on its own summary.`;\n}\n\n/**\n * A change of outline. `kind`: \"in\" (a sub-task of the row above), \"out\"\n * (up a level, just after its old parent), \"up\" / \"down\" (one place among\n * its siblings), \"under\" (last child of `parent_id`, or the top level for\n * null).\n *\n * → { ok: false, error } or { ok: true, id, parent_id, old_parent_id,\n * sort_order, order_only, changes (date changes the outline binds, as\n * shiftDependents), summaries (re-spans: the old parent, the new one),\n * finishBefore, finishAfter, words }\n */\nexport function outlineChange(tasks, links, { id, kind, parent_id = null }) {\n const list = tasks ?? [];\n const task = list.find((t) => same(t.id, id));\n if (!task) return { ok: false, error: \"That task is gone.\" };\n const level = (pid) => list.filter((t) => same(t.parent_id, pid)).sort(siblingOrder);\n const siblings = level(task.parent_id);\n const i = siblings.findIndex((t) => same(t.id, id));\n let newParent = task.parent_id ?? null;\n let position;\n let words;\n if (kind === \"up\" || kind === \"down\") {\n const j = kind === \"up\" ? i - 1 : i + 1;\n if (j < 0 || j >= siblings.length) return { ok: false, error: kind === \"up\" ? \"It's already first in its phase.\" : \"It's already last in its phase.\" };\n position = kind === \"up\" ? positionBetween(siblings[j - 1] ?? null, siblings[j]) : positionBetween(siblings[j], siblings[j + 1] ?? null);\n words = `${quote(task.title)} ${kind} one place`;\n } else if (kind === \"in\") {\n if (i <= 0) return { ok: false, error: \"There's nothing above it at this level to put it under.\" };\n const parent = siblings[i - 1];\n const problem = familyLinkProblem(list, links, id, parent.id);\n if (problem) return { ok: false, error: problem };\n const kids = level(parent.id);\n newParent = String(parent.id);\n position = positionBetween(kids[kids.length - 1] ?? null, null);\n words = `${quote(task.title)} under ${quote(parent.title)}`;\n } else if (kind === \"out\") {\n const parent = task.parent_id ? list.find((t) => same(t.id, task.parent_id)) : null;\n if (!parent) return { ok: false, error: \"It's already at the top level.\" };\n const grand = parent.parent_id ?? null;\n if (grand) {\n const problem = familyLinkProblem(list, links, id, grand);\n if (problem) return { ok: false, error: problem };\n }\n const above = level(grand);\n const j = above.findIndex((t) => same(t.id, parent.id));\n newParent = grand == null ? null : String(grand);\n position = positionBetween(parent, above[j + 1] ?? null);\n words = `${quote(task.title)} out of ${quote(parent.title)}`;\n } else if (kind === \"under\") {\n const parent = parent_id ? list.find((t) => same(t.id, parent_id)) : null;\n if (parent_id && !parent) return { ok: false, error: \"That phase is gone.\" };\n if (parent && (same(parent.id, id) || descendantsOf(list, id).has(String(parent.id)))) return { ok: false, error: \"A task can't go under itself or one of its own sub-tasks.\" };\n if (same(task.parent_id, parent?.id)) return { ok: false, error: \"It's there already.\" };\n if (parent) {\n const problem = familyLinkProblem(list, links, id, parent.id);\n if (problem) return { ok: false, error: problem };\n }\n const kids = level(parent?.id ?? null);\n newParent = parent ? String(parent.id) : null;\n position = positionBetween(kids[kids.length - 1] ?? null, null);\n words = `${quote(task.title)} under ${parent ? quote(parent.title) : \"the top level\"}`;\n } else return { ok: false, error: \"Unknown outline change.\" };\n\n const moved = !same(newParent, task.parent_id);\n let result = { changes: [], finishBefore: schedule(list, links).finish, finishAfter: schedule(list, links).finish };\n const summaries = [];\n if (moved) {\n const nextTasks = list.map((t) => (same(t.id, id) ? { ...t, parent_id: newParent } : t));\n result = shiftDependents(list, links, [], { nextTasks });\n const afterTasks = nextTasks.map((t) => {\n const c = result.changes.find((x) => same(x.id, t.id));\n return c ? { ...t, start_date: c.start_date, due_date: c.due_date } : t;\n });\n // The summary it left is re-spanned. The one it joined keeps its own\n // dates, unless the task was put there by name (\"under\"): putting a task\n // under another with a key and taking it out again loses nothing.\n if (task.parent_id) summaries.push(...summaryChanges(afterTasks, links, [task.parent_id]));\n if (newParent && kind === \"under\") summaries.push(...summaryChanges(afterTasks, links, [newParent]).filter((s) => !summaries.some((x) => same(x.id, s.id))));\n }\n return {\n ok: true,\n id: String(id),\n parent_id: newParent,\n old_parent_id: task.parent_id == null ? null : String(task.parent_id),\n sort_order: position,\n order_only: !moved,\n changes: result.changes.filter((c) => !c.summary),\n summaries,\n finishBefore: result.finishBefore,\n finishAfter: result.finishAfter,\n words,\n };\n}\n\n/**\n * Give a task a length: it keeps its start (or takes the project's) and\n * the dependents follow. → shiftDependents' result, or { ok: false, error }.\n */\nexport function lengthChange(tasks, links, { id, days, project = null }) {\n const len = Math.trunc(Number(days));\n if (!(len >= 1 && len <= 500)) return { ok: false, error: \"A length is a whole number of working days, 1 or more.\" };\n const task = (tasks ?? []).find((t) => same(t.id, id));\n if (!task) return { ok: false, error: \"That task is gone.\" };\n if (task.is_milestone) return { ok: false, error: \"A milestone has no length.\" };\n const start = task.start_date ? String(task.start_date).slice(0, 10) : project?.start_date ? nextWorkday(String(project.start_date)) : null;\n if (!start) return { ok: false, error: \"Set the project's start date first: a length needs a day to count from.\" };\n const s = spanOf({ start_date: start, due_date: start });\n const m = datesOf(s.s, len, false);\n return { ok: true, ...shiftDependents(tasks, links, [{ id: String(id), start: m.start, due: m.due }]), moves: [{ id: String(id), start: m.start, due: m.due }] };\n}\n\n/**\n * Who is on two things at once: for each owner, the stretches of working\n * days where two or more of their unfinished, dated tasks overlap.\n * `ownerOf(task)` → a key (default `assignee_id`).\n * → [{ owner, from, to, count, ids }] sorted by owner then date.\n */\nexport function overlapsOf(tasks, plan, { ownerOf = (t) => t.assignee_id } = {}) {\n const cal = makeCalendar();\n const by = new Map();\n for (const t of tasks ?? []) {\n const r = plan?.rows?.get(String(t.id));\n const owner = ownerOf(t);\n if (!r || !r.dated || r.summary || r.milestone || r.pinned || owner == null || owner === \"\") continue;\n const a = dayOf(r.planStart);\n const b = dayOf(r.planDue);\n if (a == null || b == null) continue;\n const k = String(owner);\n if (!by.has(k)) by.set(k, []);\n by.get(k).push({ id: String(t.id), s: cal.startIndex(a), e: cal.lastIndex(b) });\n }\n const out = [];\n for (const [owner, spans] of by) {\n const points = new Map();\n for (const sp of spans) for (let x = sp.s; x <= sp.e; x += 1) points.set(x, (points.get(x) ?? 0) + 1);\n let run = null;\n const flush = () => {\n if (!run) return;\n const ids = spans.filter((sp) => sp.s <= run.e && sp.e >= run.s).map((sp) => sp.id);\n out.push({ owner, from: isoOf(cal.dayAt(run.s)), to: isoOf(cal.dayAt(run.e)), count: run.n, ids });\n run = null;\n };\n const days = [...points.keys()].sort((x, y) => x - y);\n for (const x of days) {\n const n = points.get(x);\n if (n >= 2) {\n if (run && run.e === x - 1) {\n run.e = x;\n run.n = Math.max(run.n, n);\n } else {\n flush();\n run = { s: x, e: x, n };\n }\n } else flush();\n }\n flush();\n }\n return out.sort((a, b) => String(a.owner).localeCompare(String(b.owner)) || a.from.localeCompare(b.from));\n}\n\n/** The rows a baseline keeps: every dated, live task as it is planned. → { finish_date, data } */\nexport function snapshotOf(tasks, links) {\n const plan = schedule(tasks, links);\n const data = [];\n for (const t of tasks ?? []) {\n if (!t || String(t.status ?? \"\") === \"cancelled\") continue;\n const r = plan.rows.get(String(t.id));\n if (!r?.dated) continue;\n data.push({ task_id: String(t.id), title: String(t.title ?? \"\"), start_date: r.start, due_date: r.due, is_milestone: Boolean(t.is_milestone) });\n }\n return { finish_date: plan.finish, data };\n}\n\nexport { taskTree };\n"
41
+ },
42
+ {
43
+ "path": "registry/blocks/schedule-engine/ops.js",
44
+ "type": "registry:component",
45
+ "target": "@components/makersclaw/blocks/schedule-engine/ops.js",
46
+ "content": "// One entry for the questions an agent (or a script) asks of the rules:\n// the same functions a screen runs, over JSON. A package's\n// `references/plan.mjs` reads `{ operation, tasks, links, … }` on stdin and\n// prints `operate(input)` as JSON, so the dates an agent states or writes\n// are the dates the chart shows.\n//\n// schedule the plan: rows, links, finish, critical path, stuck, violations\n// move { moves: [{ id, start, due }] } what a move shifts\n// replan { today } late work off the past\n// mend {} broken links aligned\n// place { add, today, project } where a new task goes\n// outline { id, kind, parent_id } an outline change\n// length { id, days, project } a length, dependents follow\n// link { task_id, depends_on_id, kind, lag } can it be made; what it shifts\n// snapshot {} the rows a baseline keeps\n// load { owner_key } who is on two things at once\n//\n// Maps become arrays of [id, value] pairs' objects (`rows` an object keyed\n// by id); Sets become arrays. Throws nothing: a bad input answers { ok: false, error }.\n\nimport { alignConflicts, linkKind, linkProblem, longPole, replanFromToday, schedule, shiftDependents } from \"./engine\";\nimport { lengthChange, outlineChange, overlapsOf, placeNew, snapshotOf, summaryChanges } from \"./place\";\n\n/** Maps and Sets as plain JSON values. */\nexport function plain(value) {\n return JSON.parse(JSON.stringify(value, (_k, v) => (v instanceof Map ? Object.fromEntries(v) : v instanceof Set ? [...v] : v)));\n}\n\n/** The operations an agent may ask for. */\nexport const OPERATIONS = [\"schedule\", \"move\", \"replan\", \"mend\", \"place\", \"outline\", \"length\", \"link\", \"snapshot\", \"load\"];\n\nexport function operate(input = {}) {\n try {\n const { tasks = [], links = [], operation = \"schedule\" } = input;\n switch (operation) {\n case \"schedule\": {\n const plan = schedule(tasks, links);\n return plain({ ok: true, ...plan, longPole: longPole(plan) });\n }\n case \"move\":\n return plain({ ok: true, ...shiftDependents(tasks, links, input.moves ?? []) });\n case \"replan\":\n return plain({ ok: true, ...replanFromToday(tasks, links, input.today) });\n case \"mend\":\n return plain({ ok: true, ...alignConflicts(tasks, links) });\n case \"place\": {\n const placed = placeNew(tasks, { add: input.add ?? {}, today: input.today, project: input.project ?? null });\n const next = [...tasks, { id: \"__new\", parent_id: placed.parent_id, start_date: placed.start_date, due_date: placed.due_date, is_milestone: placed.is_milestone, progress: 0, status: \"todo\" }];\n return plain({ ...placed, summaries: placed.resum ? summaryChanges(next, links, [\"__new\"]) : [] });\n }\n case \"outline\":\n return plain(outlineChange(tasks, links, { id: input.id, kind: input.kind, parent_id: input.parent_id ?? null }));\n case \"length\":\n return plain(lengthChange(tasks, links, { id: input.id, days: input.days, project: input.project ?? null }));\n case \"link\": {\n const problem = linkProblem(tasks, links, input.task_id, input.depends_on_id);\n if (problem) return { ok: false, error: problem };\n const next = [...links, { id: \"__new\", task_id: input.task_id, depends_on_id: input.depends_on_id, kind: input.kind ?? \"finish_to_start\", lag_days: Number(input.lag ?? 0) || 0 }];\n return plain({ ok: true, kind: linkKind(input.kind), ...shiftDependents(tasks, links, [], { nextLinks: next }) });\n }\n case \"snapshot\":\n return plain({ ok: true, ...snapshotOf(tasks, links) });\n case \"load\":\n return plain({ ok: true, overlaps: overlapsOf(tasks, schedule(tasks, links), { ownerOf: (t) => t[input.owner_key ?? \"assignee_id\"] }) });\n default:\n return { ok: false, error: `Unknown operation \"${operation}\". Use one of: ${OPERATIONS.join(\", \")}.` };\n }\n } catch (error) {\n return { ok: false, error: error?.message ?? String(error) };\n }\n}\n"
47
+ },
48
+ {
49
+ "path": "registry/blocks/schedule-engine/README.md",
50
+ "type": "registry:file",
51
+ "target": "@components/makersclaw/blocks/schedule-engine/README.md",
52
+ "content": "# Schedule engine\n\nThe rules a dated plan follows, as plain functions over plain rows. No\nReact, no I/O: the browser runs them on every pointer move of a drag, a\nscript runs them for an agent, and the tests run them under node. The same\ndates everywhere.\n\nBorn in a Gantt planner's app, which carried 770 lines of it (and an SQL\ncopy that the tests pin to it). It is the engine under the `schedule-gantt`\nblock, and it works alone.\n\n```js\nimport { schedule, shiftDependents, replanFromToday } from \"../components/blocks/schedule-engine\"; // a Makersclaw app (\"./blocks/schedule-engine\" from components/)\n// shadcn project: import { schedule } from \"@/components/makersclaw/blocks/schedule-engine\"\n```\n\n## The model\n\n- A task runs `d` working days (Monday to Friday; `calendar: \"everyday\"`\n counts all seven). A milestone takes none and sits at the end of its day.\n- A task's planned start is a floor: the forward pass only ever moves work\n later. **Finished work** (status `done`, or progress 100) stays where it\n happened.\n- A task with sub-tasks is a **summary**: its dates are its leaves' span. A\n link **to** a summary binds every leaf under it; a link **from** one reads\n its span (last finish, or first start for start links).\n- Cancelled tasks and tasks with no dates are off the plan, and so are\n their links.\n- **Slack** is the latest finish minus the earliest finish, in working days.\n Slack 0 is critical. The **long pole** is the unfinished critical task\n with the most work left; the later wins a tie.\n\n## Rows in\n\n- task: `{ id, parent_id, title, start_date, due_date, is_milestone,\n progress, status, position }` (dates `'YYYY-MM-DD'`).\n- link: `{ id, task_id, depends_on_id, kind, lag_days }`: `task_id` waits on\n `depends_on_id`. `kind`: `finish_to_start` (default), `start_to_start`,\n `finish_to_finish`, `start_to_finish`, or `FS` / `SS` / `FF` / `SF`.\n\n## The plan\n\n- `schedule(tasks, links, { calendar })` → `{ rows, links, start, finish,\n critical, stuck, violations }`.\n - `rows`: `Map(id → { id, parentId, summary, milestone, pinned, dated,\n stuck, start, due, planStart, planDue, lateStart, lateDue, duration,\n slack, critical, progress, shifted, drivers, leafIds })`. `start` / `due`\n are the stored dates (a summary: its leaves' span); `plan*` where the\n forward pass puts it; `shifted` is true when the stored dates break a\n link.\n - `links`: `Map(id → { id, taskId, dependsOnId, kind, lag, valid, reason,\n slack, critical, violated, violation })`.\n - `critical`: critical leaf ids in plan order. `stuck`: ids in or behind\n a loop. `violations`: link ids whose stored dates break them.\n- `shiftDependents(tasks, links, moves, { nextLinks, nextTasks })` → `{\n changes: [{ id, start_date, due_date, from, summary? }], clamped, before,\n after, finishBefore, finishAfter }`. `moves`: `[{ id, start, due }]`; a\n summary's `start` moves its unfinished leaves as a block. Only tasks the\n move affects are in `changes`.\n- `replanFromToday(tasks, links, today)`: not started and its start has\n passed → today; started and overdue → its remaining share from today;\n a passed milestone → today. Then the dependents shift. Adds `moves`.\n- `alignConflicts(tasks, links)`: every task that starts before its links\n allow moves to its earliest date. Adds `moves`.\n- `linkProblem(tasks, links, taskId, dependsOnId)` → why the link can't be\n made, in words, or `null`.\n- `longPole(plan)` → the id.\n- Lower level: `buildNetwork`, `forwardPass`, `backwardPass`,\n `criticalPath`, `makeCalendar`, `spanOf`, `datesOf`, `dayOf`, `isoOf`,\n `addWorkdays`, `workdaysBetween`, `durationOf`, `linkKind`, `LINK_KINDS`.\n- The tree: `taskTree(tasks)` → `{ order: [{ id, parentId, depth, outline,\n childCount, index }], byId }`, `descendantsOf`, `positionBetween`,\n `siblingOrder`.\n\n## Planning a write\n\nNone of these writes. Each returns the values a save needs.\n\n- `placeNew(tasks, { add: { parent_id, after_id, start, due, days,\n milestone }, today, project })` → `{ parent_id, sort_order, start_date,\n due_date, is_milestone, resum }`.\n- `outlineChange(tasks, links, { id, kind: \"in\" | \"out\" | \"up\" | \"down\" |\n \"under\", parent_id })` → `{ ok, parent_id, old_parent_id, sort_order,\n order_only, changes, summaries, finishBefore, finishAfter, words }` or\n `{ ok: false, error }`.\n- `lengthChange(tasks, links, { id, days, project })`.\n- `summaryChanges(tasks, links, ids)` → the summaries to re-span.\n- `overlapsOf(tasks, plan, { ownerOf })` → who is on two things at once,\n `[{ owner, from, to, count, ids }]`.\n- `snapshotOf(tasks, links)` → `{ finish_date, data }`, the rows a baseline\n keeps.\n\n## The short form of a link\n\n`2.5FS`, `6.3SS+5d`, `3.4SF-2d`, several joined with a semicolon: an outline\nnumber, the kind (finish to start when left out), a signed lag in working\ndays. `shortForm(outline, kind, lag)`, `waitsWords(links, taskId,\noutlineOf)`, `parseWaits(text, idOf)` → `{ links, problems }`, `lagWords`,\n`KIND_WORDS` (the four kinds named in full, each with a clause).\n\n## One entry for an agent\n\n`operate({ operation, tasks, links, ... })` answers `schedule`, `move`,\n`replan`, `mend`, `place`, `outline`, `length`, `link`, `snapshot` and\n`load` as plain JSON (a Map is an object keyed by id, a Set a list). A\npackage's `references/plan.mjs` is ten lines around it:\n\n```js\nimport { operate } from \"../app/components/blocks/schedule-engine/ops.js\";\nlet input = \"\";\nfor await (const chunk of process.stdin) input += chunk;\nprocess.stdout.write(JSON.stringify(operate(JSON.parse(input))) + \"\\n\");\n```\n\n## Example\n\n```js\nconst plan = schedule(tasks, links);\nplan.finish; // \"2026-11-03\"\nplan.rows.get(\"t7\").slack; // 0 = on the critical path\nconst moved = shiftDependents(tasks, links, [{ id: \"t7\", start: \"2026-10-12\", due: \"2026-10-16\" }]);\nmoved.finishAfter; // what the move cost the finish\nmoved.changes; // every date to save, in one write\n```\n\n## States and edges\n\nAn empty plan has no finish (`null`) and no critical path. A loop leaves the\ntasks in it where they were (`stuck`) and everything else is planned. A\nweekend date counts from the Monday after (a start) or back to the Friday\n(a finish). A link to a task with no dates is `valid: false, reason:\n\"undated\"`. A parent that is not in the list makes a root.\n\n## Tests\n\n`test/schedule-engine.test.mjs` (the rules, every expected date worked out\nby hand on a real calendar) and `test/schedule-engine-writes.test.mjs`.\n"
53
+ }
54
+ ],
55
+ "categories": [
56
+ "schedule",
57
+ "gantt",
58
+ "critical path",
59
+ "slack",
60
+ "dependencies",
61
+ "working days",
62
+ "lag",
63
+ "baseline",
64
+ "re-plan",
65
+ "outline",
66
+ "project plan",
67
+ "milestones",
68
+ "ms project",
69
+ "pure",
70
+ "agent"
71
+ ],
72
+ "docs": "Installed to components/makersclaw/blocks/schedule-engine. Import: import { dayOf, isoOf, weekdayOf, makeCalendar, LINK_KINDS, linkKind, spanOf, datesOf, workdaysBetween, addWorkdays, durationOf, buildNetwork, forwardPass, backwardPass, criticalPath, schedule, shiftDependents, replanFromToday, alignConflicts, linkProblem, longPole, taskTree, descendantsOf, positionBetween, siblingOrder, KIND_WORDS, lagWords, shortForm, waitsWords, parseWaits, nextWorkday, summaryChanges, placeNew, familyLinkProblem, outlineChange, lengthChange, overlapsOf, snapshotOf, operate, plain, OPERATIONS } from \"@/components/makersclaw/blocks/schedule-engine\". Props and an example: components/makersclaw/blocks/schedule-engine/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
73
+ "meta": {
74
+ "makersclaw": {
75
+ "version": "0.8.1",
76
+ "useWhen": "A plan with tasks that wait on each other and dates that must follow: a project schedule, a launch plan, a roadmap whose slips ripple, an agent that re-plans or answers 'when do we finish and what decides it'. The engine under the schedule-gantt block, usable alone with no chart. Not for undated to-dos (task-list) or stages (board)."
77
+ }
78
+ }
79
+ }