cloving 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +251 -20
  2. package/assets/watch-fonts/fira-code-OFL.md +93 -0
  3. package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
  4. package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
  5. package/assets/watch-fonts/heebo-OFL.md +93 -0
  6. package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
  7. package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
  8. package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
  9. package/assets/watch-fonts/manifest.json +112 -0
  10. package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
  11. package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
  12. package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
  13. package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
  14. package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
  15. package/dist/src/authority/commands.d.ts +1 -0
  16. package/dist/src/authority/commands.js +33 -4
  17. package/dist/src/authority/dispatch-decision.d.ts +1 -0
  18. package/dist/src/authority/dispatch-decision.js +7 -4
  19. package/dist/src/authority/events.js +2 -2
  20. package/dist/src/authority/handoff-chain.d.ts +24 -0
  21. package/dist/src/authority/handoff-chain.js +46 -0
  22. package/dist/src/authority/reconciliation.d.ts +3 -0
  23. package/dist/src/authority/reconciliation.js +21 -5
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +5 -0
  26. package/dist/src/broker/actor/authority-queries.d.ts +4 -0
  27. package/dist/src/broker/actor/authority-queries.js +51 -2
  28. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  29. package/dist/src/broker/actor/authority-session.js +3 -4
  30. package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
  31. package/dist/src/broker/actor/effect-delivery.js +9 -0
  32. package/dist/src/broker/authority-admission.js +13 -5
  33. package/dist/src/chats/analysis-batches.d.ts +42 -0
  34. package/dist/src/chats/analysis-batches.js +97 -0
  35. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  36. package/dist/src/chats/analysis-checkpoint.js +31 -0
  37. package/dist/src/chats/analysis-contract.d.ts +80 -0
  38. package/dist/src/chats/analysis-contract.js +16 -0
  39. package/dist/src/chats/analysis-data.d.ts +3 -0
  40. package/dist/src/chats/analysis-data.js +85 -0
  41. package/dist/src/chats/analysis-input.d.ts +18 -0
  42. package/dist/src/chats/analysis-input.js +97 -0
  43. package/dist/src/chats/analysis-links.d.ts +2 -0
  44. package/dist/src/chats/analysis-links.js +25 -0
  45. package/dist/src/chats/analysis-merge.d.ts +16 -0
  46. package/dist/src/chats/analysis-merge.js +134 -0
  47. package/dist/src/chats/analysis-prompt.d.ts +18 -0
  48. package/dist/src/chats/analysis-prompt.js +42 -0
  49. package/dist/src/chats/analysis-proposal.d.ts +4 -0
  50. package/dist/src/chats/analysis-proposal.js +41 -0
  51. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  52. package/dist/src/chats/analysis-provenance.js +99 -0
  53. package/dist/src/chats/analysis-shape.d.ts +4 -0
  54. package/dist/src/chats/analysis-shape.js +235 -0
  55. package/dist/src/chats/analysis.d.ts +3 -0
  56. package/dist/src/chats/analysis.js +42 -0
  57. package/dist/src/chats/corpus-input.d.ts +5 -0
  58. package/dist/src/chats/corpus-input.js +288 -0
  59. package/dist/src/chats/destination.js +3 -1
  60. package/dist/src/chats/import.d.ts +8 -6
  61. package/dist/src/chats/import.js +195 -23
  62. package/dist/src/chats/input-file.d.ts +3 -0
  63. package/dist/src/chats/input-file.js +32 -0
  64. package/dist/src/chats/normalize.d.ts +11 -2
  65. package/dist/src/chats/normalize.js +18 -2
  66. package/dist/src/chats/previous-import.d.ts +5 -0
  67. package/dist/src/chats/previous-import.js +34 -0
  68. package/dist/src/chats/reader.d.ts +7 -1
  69. package/dist/src/chats/reader.js +46 -15
  70. package/dist/src/chats/redact-import.d.ts +14 -0
  71. package/dist/src/chats/redact-import.js +141 -0
  72. package/dist/src/chats/redaction.d.ts +41 -0
  73. package/dist/src/chats/redaction.js +161 -0
  74. package/dist/src/chats/reference-mapping.d.ts +16 -0
  75. package/dist/src/chats/reference-mapping.js +153 -0
  76. package/dist/src/chats/revision-input.d.ts +3 -0
  77. package/dist/src/chats/revision-input.js +180 -0
  78. package/dist/src/chats/revision.d.ts +48 -0
  79. package/dist/src/chats/revision.js +94 -0
  80. package/dist/src/chats/selection.d.ts +25 -0
  81. package/dist/src/chats/selection.js +109 -0
  82. package/dist/src/chats/source.d.ts +23 -0
  83. package/dist/src/chats/source.js +135 -0
  84. package/dist/src/chats/structure.d.ts +57 -0
  85. package/dist/src/chats/structure.js +216 -0
  86. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  87. package/dist/src/chats/workflow-generation-projection.js +75 -0
  88. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  89. package/dist/src/chats/workflow-generation-source.js +124 -0
  90. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  91. package/dist/src/chats/workflow-generation-text.js +74 -0
  92. package/dist/src/chats/workflow-generation.d.ts +69 -0
  93. package/dist/src/chats/workflow-generation.js +142 -0
  94. package/dist/src/cli/arguments.d.ts +8 -0
  95. package/dist/src/cli/arguments.js +94 -13
  96. package/dist/src/cli/client.d.ts +1 -0
  97. package/dist/src/cli/client.js +38 -1
  98. package/dist/src/cli/commands.d.ts +3 -0
  99. package/dist/src/cli/commands.js +29 -11
  100. package/dist/src/cli/main.d.ts +2 -1
  101. package/dist/src/cli/main.js +61 -14
  102. package/dist/src/cli/render.d.ts +3 -0
  103. package/dist/src/cli/render.js +122 -7
  104. package/dist/src/cli/usage-render.d.ts +3 -0
  105. package/dist/src/cli/usage-render.js +28 -0
  106. package/dist/src/cli/usage.d.ts +3 -2
  107. package/dist/src/cli/usage.js +42 -7
  108. package/dist/src/cli/watch-actions.d.ts +10 -0
  109. package/dist/src/cli/watch-actions.js +58 -0
  110. package/dist/src/cli/watch-board-view.d.ts +44 -1
  111. package/dist/src/cli/watch-board-view.js +137 -18
  112. package/dist/src/cli/watch-board.d.ts +7 -1
  113. package/dist/src/cli/watch-board.js +48 -11
  114. package/dist/src/cli/watch-card-client.d.ts +1 -0
  115. package/dist/src/cli/watch-card-client.js +44 -0
  116. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  117. package/dist/src/cli/watch-card-forms.js +33 -0
  118. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  119. package/dist/src/cli/watch-card-reply.js +34 -0
  120. package/dist/src/cli/watch-card-view.d.ts +40 -0
  121. package/dist/src/cli/watch-card-view.js +86 -0
  122. package/dist/src/cli/watch-client.d.ts +1 -0
  123. package/dist/src/cli/watch-client.js +122 -0
  124. package/dist/src/cli/watch-model.d.ts +10 -4
  125. package/dist/src/cli/watch-model.js +61 -67
  126. package/dist/src/cli/watch-page.d.ts +1 -1
  127. package/dist/src/cli/watch-page.js +89 -86
  128. package/dist/src/cli/watch-phases.js +3 -3
  129. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  130. package/dist/src/cli/watch-pr-board.js +26 -0
  131. package/dist/src/cli/watch-pr-command.d.ts +18 -0
  132. package/dist/src/cli/watch-pr-command.js +74 -0
  133. package/dist/src/cli/watch-pr-identity.d.ts +32 -0
  134. package/dist/src/cli/watch-pr-identity.js +87 -0
  135. package/dist/src/cli/watch-pr-observer.d.ts +20 -0
  136. package/dist/src/cli/watch-pr-observer.js +150 -0
  137. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  138. package/dist/src/cli/watch-pr-settings.js +47 -0
  139. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  140. package/dist/src/cli/watch-pull-request.js +130 -0
  141. package/dist/src/cli/watch-reads.d.ts +7 -1
  142. package/dist/src/cli/watch-reads.js +44 -18
  143. package/dist/src/cli/watch-render.d.ts +29 -2
  144. package/dist/src/cli/watch-render.js +11 -3
  145. package/dist/src/cli/watch-run-input.d.ts +14 -0
  146. package/dist/src/cli/watch-run-input.js +49 -0
  147. package/dist/src/cli/watch-server.d.ts +3 -0
  148. package/dist/src/cli/watch-server.js +77 -8
  149. package/dist/src/cli/watch-start-view.d.ts +18 -0
  150. package/dist/src/cli/watch-start-view.js +77 -0
  151. package/dist/src/cli/watch-turn.d.ts +1 -0
  152. package/dist/src/cli/watch-turn.js +8 -3
  153. package/dist/src/cli/watch-usage.d.ts +6 -0
  154. package/dist/src/cli/watch-usage.js +65 -0
  155. package/dist/src/cli/watch-view.d.ts +22 -0
  156. package/dist/src/cli/watch-view.js +111 -19
  157. package/dist/src/cli.js +48 -6
  158. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  159. package/dist/src/daemon/analysis-launch.js +81 -0
  160. package/dist/src/daemon/analysis-token.d.ts +6 -0
  161. package/dist/src/daemon/analysis-token.js +88 -0
  162. package/dist/src/daemon/analysis-worker.d.ts +20 -0
  163. package/dist/src/daemon/analysis-worker.js +271 -0
  164. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  165. package/dist/src/daemon/chats-batch-binding.js +99 -0
  166. package/dist/src/daemon/chats-capture.d.ts +18 -0
  167. package/dist/src/daemon/chats-capture.js +194 -0
  168. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  169. package/dist/src/daemon/chats-checkpoints.js +186 -0
  170. package/dist/src/daemon/chats-files.d.ts +42 -0
  171. package/dist/src/daemon/chats-files.js +252 -0
  172. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  173. package/dist/src/daemon/chats-follow-up.js +180 -0
  174. package/dist/src/daemon/chats-question.d.ts +15 -0
  175. package/dist/src/daemon/chats-question.js +93 -0
  176. package/dist/src/daemon/chats-worker.d.ts +13 -0
  177. package/dist/src/daemon/chats-worker.js +201 -0
  178. package/dist/src/daemon/claude-turn.d.ts +11 -0
  179. package/dist/src/daemon/claude-turn.js +96 -15
  180. package/dist/src/daemon/claude-worker.d.ts +2 -1
  181. package/dist/src/daemon/claude-worker.js +92 -45
  182. package/dist/src/daemon/codex-stream.js +16 -0
  183. package/dist/src/daemon/daemon.d.ts +8 -1
  184. package/dist/src/daemon/daemon.js +51 -14
  185. package/dist/src/daemon/delivery-router.js +6 -0
  186. package/dist/src/daemon/github-calls.d.ts +4 -0
  187. package/dist/src/daemon/github-calls.js +16 -7
  188. package/dist/src/daemon/github-ci.d.ts +6 -0
  189. package/dist/src/daemon/github-ci.js +170 -0
  190. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  191. package/dist/src/daemon/github-merge-call.js +73 -0
  192. package/dist/src/daemon/github-merge.d.ts +3 -5
  193. package/dist/src/daemon/github-merge.js +52 -95
  194. package/dist/src/daemon/github-methods.d.ts +28 -0
  195. package/dist/src/daemon/github-methods.js +84 -16
  196. package/dist/src/daemon/github-operation.d.ts +2 -0
  197. package/dist/src/daemon/github-retry.d.ts +6 -0
  198. package/dist/src/daemon/github-retry.js +49 -0
  199. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  200. package/dist/src/daemon/github-ticket-status.js +294 -0
  201. package/dist/src/daemon/github-worker.js +2 -1
  202. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  203. package/dist/src/daemon/handoff-worker.js +232 -0
  204. package/dist/src/daemon/issue-queue.d.ts +2 -0
  205. package/dist/src/daemon/issue-queue.js +186 -43
  206. package/dist/src/daemon/notify-journal.d.ts +18 -0
  207. package/dist/src/daemon/notify-journal.js +108 -0
  208. package/dist/src/daemon/notify-message.js +2 -1
  209. package/dist/src/daemon/notify.js +176 -37
  210. package/dist/src/daemon/operation-journal.d.ts +1 -0
  211. package/dist/src/daemon/operation-journal.js +4 -2
  212. package/dist/src/daemon/retrying-turn.d.ts +12 -0
  213. package/dist/src/daemon/retrying-turn.js +231 -0
  214. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  215. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  216. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  217. package/dist/src/daemon/ticket-registration.js +76 -22
  218. package/dist/src/daemon/ticket-source.d.ts +4 -1
  219. package/dist/src/daemon/ticket-source.js +8 -2
  220. package/dist/src/decision-visibility.d.ts +2 -0
  221. package/dist/src/decision-visibility.js +10 -0
  222. package/dist/src/execution-pin.d.ts +4 -0
  223. package/dist/src/execution-pin.js +23 -2
  224. package/dist/src/host/start-delivery.d.ts +2 -1
  225. package/dist/src/host/start-delivery.js +1 -1
  226. package/dist/src/host/step-bridge-host.d.ts +1 -0
  227. package/dist/src/host/step-bridge-host.js +16 -0
  228. package/dist/src/host/world-reconciliation.d.ts +3 -0
  229. package/dist/src/host/world-reconciliation.js +9 -0
  230. package/dist/src/host/world-runs.js +14 -1
  231. package/dist/src/issue-identity.d.ts +6 -0
  232. package/dist/src/issue-identity.js +23 -0
  233. package/dist/src/service/queue-runs.d.ts +3 -0
  234. package/dist/src/service/queue-runs.js +39 -0
  235. package/dist/src/service/registry.d.ts +8 -2
  236. package/dist/src/service/registry.js +20 -8
  237. package/dist/src/service/replies.d.ts +2 -0
  238. package/dist/src/service/run-handlers.js +54 -30
  239. package/dist/src/service/service.d.ts +3 -0
  240. package/dist/src/service/service.js +3 -1
  241. package/dist/src/service/submission-handler.d.ts +1 -0
  242. package/dist/src/service/submission-handler.js +92 -13
  243. package/dist/src/service/worker-totals.d.ts +3 -0
  244. package/dist/src/service/worker-totals.js +52 -0
  245. package/dist/src/step-bridge.d.ts +9 -0
  246. package/dist/src/steps/handoff.d.ts +2 -0
  247. package/dist/src/steps/handoff.js +13 -0
  248. package/dist/src/ticket-dependencies.d.ts +1 -0
  249. package/dist/src/ticket-dependencies.js +15 -0
  250. package/dist/src/ticket-status.d.ts +8 -0
  251. package/dist/src/ticket-status.js +48 -0
  252. package/dist/src/token-prices.d.ts +21 -0
  253. package/dist/src/token-prices.js +108 -0
  254. package/dist/src/worker-usage.d.ts +31 -0
  255. package/dist/src/worker-usage.js +44 -0
  256. package/dist/src/workflow-admission.d.ts +1 -0
  257. package/dist/src/workflow-candidate.d.ts +29 -0
  258. package/dist/src/workflow-candidate.js +60 -0
  259. package/dist/src/workflow-contracts.d.ts +4 -0
  260. package/dist/src/workflow-contracts.js +32 -5
  261. package/dist/src/workflow-declaration.d.ts +12 -1
  262. package/dist/src/workflow-declaration.js +71 -13
  263. package/dist/src/workflow-discovery.d.ts +18 -0
  264. package/dist/src/workflow-discovery.js +33 -0
  265. package/dist/src/workflow-refusals.d.ts +6 -0
  266. package/dist/src/workflow-refusals.js +39 -0
  267. package/dist/src/workflows.d.ts +3 -1
  268. package/dist/src/workflows.js +2 -0
  269. package/dist/tools/admit-workflow.d.ts +13 -0
  270. package/dist/tools/admit-workflow.js +94 -0
  271. package/dist/tools/self-host-processes.js +7 -2
  272. package/dist/tools/self-host-project.d.ts +17 -0
  273. package/dist/tools/self-host-project.js +177 -0
  274. package/dist/tools/self-host-workflows.d.ts +1 -1
  275. package/dist/tools/self-host-workflows.js +2 -2
  276. package/dist/tools/self-host.js +30 -84
  277. package/dist/tools/self-update-host.d.ts +7 -0
  278. package/dist/tools/self-update-host.js +48 -1
  279. package/dist/tools/self-update.d.ts +1 -0
  280. package/dist/tools/self-update.js +85 -13
  281. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  282. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  283. package/dist/tools/workflow-bundles.d.ts +20 -0
  284. package/dist/tools/workflow-bundles.js +173 -0
  285. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  286. package/dist/tools/workflow-candidate-counts.js +251 -0
  287. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  288. package/dist/tools/workflow-candidate-program.js +78 -0
  289. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  290. package/dist/tools/workflow-candidate-source.js +107 -0
  291. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  292. package/dist/tools/workflow-candidate-syntax.js +219 -0
  293. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  294. package/dist/tools/workflow-folder-capture.js +216 -0
  295. package/dist/tools/workflow-folders.d.ts +33 -0
  296. package/dist/tools/workflow-folders.js +150 -0
  297. package/dist/tools/workflow-source-effects.d.ts +10 -0
  298. package/dist/tools/workflow-source-effects.js +213 -0
  299. package/dist/workflows/analyze-chats.d.ts +15 -0
  300. package/dist/workflows/analyze-chats.declaration.json +22 -0
  301. package/dist/workflows/analyze-chats.js +250 -0
  302. package/dist/workflows/develop-prd.js +71 -3
  303. package/dist/workflows/develop-ticket.d.ts +1 -7
  304. package/dist/workflows/develop-ticket.js +47 -141
  305. package/dist/workflows/issue-labels.d.ts +3 -1
  306. package/dist/workflows/issue-labels.js +34 -0
  307. package/dist/workflows/ticket-prompts.d.ts +35 -0
  308. package/dist/workflows/ticket-prompts.js +143 -0
  309. package/package.json +25 -5
@@ -50,29 +50,40 @@ Serves a page on http://127.0.0.1:<port>/ that shows the runs live: the state,
50
50
  the events, the work of the Claude worker and each pending decision.
51
51
  The default port is 3663. The page is read only. Stop it with Control-C.
52
52
  `,
53
- start: `Usage: cloving start [--repository <directory>]
53
+ start: `Usage: cloving start [--repository <directory>] [--reset-queue]
54
54
 
55
55
  Starts the Cloving service for one git repository, with the workflow host,
56
56
  the Claude worker and the watch page. The default repository is the current
57
57
  directory. This command works in a development checkout of Cloving.
58
58
  Stop it with Control-C.
59
59
 
60
+ The queue continues after unsuccessful runs and stops after three runs in a row
61
+ without a successful handoff. A successful handoff resets the count; ordinary restarts preserve the count.
62
+ Stop the service, inspect the failures and reconcile affected issues, then start with --reset-queue and the same queue settings to clear the failure streak once. Tracked commands are preserved.
63
+
60
64
  The command reads these settings from the environment:
61
65
  CLOVING_RUNTIME_DIR The path of the runtime directory. The default is ~/.local/state/cloving/run, if XDG_RUNTIME_DIR and XDG_STATE_HOME are not set.
62
66
  CLOVING_TICKET_COMMAND A JSON list of a command and its arguments. The command gets a ticket ID as its last argument and prints the ticket. The setting has no default, and a run with --ticket needs it.
63
67
  CLOVING_NOTIFY_COMMAND A JSON list of a command and its arguments. Every 15 seconds, new decisions, blocked runs and failed runs send JSON on standard input with no added argument. The setting has no default.
64
- CLOVING_NEXT_COMMAND A JSON list of a command and its arguments. The command runs with no added argument and prints one ready ticket ID for --next. The setting has no default.
65
- CLOVING_QUEUE_SECONDS A whole number from 10 to 3600 seconds between develop-issue queue checks; requires CLOVING_NEXT_COMMAND and CLOVING_TICKET_COMMAND. Unset means no queue. The queue stops after a failed, cancelled or unsuccessful run until restart; manual develop-issue submissions are refused with workflow-active while an issue run is active.
68
+ CLOVING_NEXT_COMMAND A JSON list of a command and its arguments. The command runs with no added argument and prints one ready ticket ID for --next. It must exclude the space-separated ticket IDs in CLOVING_SKIP_TICKETS. The setting has no default.
69
+ CLOVING_QUEUE_SECONDS A whole number from 10 to 3600 seconds between develop-issue queue checks; requires CLOVING_NEXT_COMMAND and CLOVING_TICKET_COMMAND. Unset means no queue. The queue stops after three runs in a row without a successful handoff; manual develop-issue submissions are refused with workflow-active when all lanes are busy.
70
+ CLOVING_QUEUE_LANES A whole number from 1 to 4; the default is 1. Requires CLOVING_QUEUE_SECONDS. Active tickets are refused with ticket-active. Needs #N sentences in the ticket's ## Dependencies section refuse admission with ticket-dependency-open while issue #N is open.
66
71
  CLOVING_SELF_UPDATE_SECONDS A whole number from 30 to 3600 seconds between checks of the own checkout; unset means no update. An update stops the daemon and restarts with new code. A change of package-lock.json needs the operator to update and install by hand.
72
+ CLOVING_ANALYSIS_TOKEN_COMMAND A JSON list of a program and its nonempty string arguments, run without a shell just before analysis. It has limits of 10 seconds and 4096 combined stdout/stderr bytes; stdout must be one printable ASCII token without spaces, at most 512 bytes after one final line break. Only analysis receives CLAUDE_CODE_OAUTH_TOKEN; API keys are excluded. There is no default.
67
73
  CLOVING_CLAUDE_COMMAND A JSON list of a command and its arguments for the Claude worker. The default is the claude command with a limit of 5 USD for each turn.
68
74
  CLOVING_PROFILES The path of a JSON file of operator-defined Claude turn profiles, read once at start. The setting has no default.
75
+ CLOVING_TOKEN_PRICES A JSON object from model names to input, cachedInput and output USD prices per million tokens for estimates; entries override matching built-in prices.
76
+ CLOVING_FALLBACK_MODELS A JSON array of at most three nonempty model names; the default is no fallback. After capacity refusals the first retry keeps the main model and the third attempt uses the first fallback, with the same waits and attempt limit. Requires an explicit -m, --model or --model= argument; does not change analyze-chats analysis. A fallback can have a higher price.
69
77
  CLOVING_WORKER The worker for each turn: claude or codex. The default is claude. Other values stop the start with invalid-worker.
70
78
  CLOVING_CODEX_COMMAND A JSON list of a command and its arguments, used only when CLOVING_WORKER is codex; CLOVING_CLAUDE_COMMAND is then ignored. The default is codex exec --json --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -, with no sandbox and no approval prompts.
79
+ CLOVING_WATCH_REPOSITORY The service repository context for the watch board, set by start from --repository. Its local origin supplies the GitHub identity. There is no current-directory default.
80
+ CLOVING_WATCH_ACTIONS The value off removes the board run form and its write endpoint. The default is on.
71
81
  CLOVING_WATCH_PORT The port number of the watch page. The value off starts no watch page. The default is 3663.
72
82
  CLOVING_OWNERSHIP_LEASE_SECONDS The time in seconds of the ownership lease of a started workflow step. The default is 15.
73
83
  CLOVING_HOST_BOUND The largest number of queue messages that the workflow host handles at one time, from 1 to 64. The default is 8.
74
- CLOVING_GH_COMMAND A JSON list of a command and its arguments for the GitHub CLI, which the workflow develop-issue uses for pull requests and develop-prd uses for issues. The default is gh.
84
+ CLOVING_GH_COMMAND A JSON list of a command and its arguments for the GitHub CLI, which the workflow develop-issue uses for pull requests and develop-prd uses for issues. The watch board also uses it to read pull request status. The default is gh.
75
85
  CLOVING_CHECK_COMMAND A JSON list of a command and its arguments that the workflow develop-issue runs in the work tree before a merge. A merge does not occur if the command fails. The setting has no default, and with no setting the merge occurs with no check.
86
+ CLOVING_REQUIRED_CHECKS A JSON list of required check run names before a develop-issue merge. Unset requires all GitHub Actions checks; a repository with no CI can merge after 60 seconds. The latest run of each name decides. The CI wait limit is 90 minutes.
76
87
  CLOVING_PRIVATE_TEXTS A JSON list of texts that the workflow develop-issue does not push in a file or in a commit message. The workflow develop-prd requires the list before posting an issue. The list adds to the path of the home folder, and the case of the letters does not matter. The setting has no default.
77
88
  `,
78
89
  };
@@ -82,6 +93,7 @@ export const usage = `Usage: cloving [--help] [--version]
82
93
 
83
94
  Commands:
84
95
  workflows validate Check a compiled workflow manifest, its artifacts and optional runtime input.
96
+ chats redact Redact one imported chat corpus into private runtime files.
85
97
  chats import Import explicitly selected Claude Code transcripts into private runtime files.
86
98
  run Submit one run of an admitted workflow.
87
99
  status List the runs or show one run.
@@ -100,14 +112,29 @@ The runtime commands need a Cloving service. CLOVING_RUNTIME_DIR names its runti
100
112
  Use npm exec -- cloving ... for a project-local installation.
101
113
  `;
102
114
  export const chatsImportUsage = `Usage: cloving chats import [--] <path> [<path> ...]
115
+ cloving chats import --root <projects-folder> [--root <projects-folder> ...] [--exclude-project <name> ...] [-- <path> ...]
103
116
 
104
117
  Imports only the selected Claude Code JSONL files. Relative paths use the current directory.
105
- This local command needs no service. It does not expand globs or scan directories.
118
+ This local command needs no service. It scans only explicitly named roots and does not expand globs or discover configuration.
119
+ Positional files come first, retaining order and repetitions. Roots sort by canonical path, then entries by relative path in JavaScript string order.
120
+ Exclusions match direct project folder names exactly and prune their children. Exclusions require a root.
121
+ Scans accept lowercase .jsonl filenames, attempt at most 100 eligible files across roots, and stop before entering folders at depth four.
122
+ Later file candidates and depth-limited folders are pending. Select pending files explicitly in a later import.
123
+ Linked components, containment changes and roots in Git repositories are refused. Scan protections require the supported macOS kernel.
106
124
  The file limit is 10 MiB (10,485,760 bytes). Links and nonregular files are refused.
107
- Private corpus.jsonl and coverage.json files go into a new import directory under the runtime directory's chats directory.
125
+ The total normalized corpus limit is 50 MiB (52,428,800 UTF-8 bytes), including newlines and repeated selections.
126
+ Exceeding that total returns output-failed with exit code 1 and no completed coverage manifest; partial files may remain.
127
+ --previous-import <IMPORT_ID> compares against one explicitly named UUID v4 import before --.
128
+ The earlier coverage and optional revision must be valid, completed private metadata; otherwise previous-import-refused returns exit 1 with no new import.
129
+ A comparison writes private revision.json with ordered selection evidence and six status counts. Matching uses source path and selected root path; repetitions pair in order.
130
+ Byte-verified extensions are extended; shorter sources are truncated. Legacy imports without sizes can establish unchanged digests but cannot prove extension or truncation.
131
+ Missing or truncated evidence makes the import partial. History completeness remains unknown.
132
+ Private corpus.jsonl, structure.json and coverage.json files go into a new import directory under the runtime directory's chats directory.
108
133
  The runtime selection is CLOVING_RUNTIME_DIR, XDG_RUNTIME_DIR, XDG_STATE_HOME, then HOME.
109
134
  Output contains only counts, fixed codes and an opaque import ID. Source paths stay in the private manifest.
110
- Exit codes: 0 for complete coverage, 1 for refused sources or output failure, 2 for invalid usage.
135
+ Structure preserves branches and marks known copies without removing records. History completeness remains unknown.
136
+ Root imports write coverage version 2 and add excluded and pending entry counts; positional-only imports retain coverage version 1.
137
+ Exit codes: 0 for no refused, pending, missing or truncated entries (exclusions alone permit success without comparison), 1 for partial coverage or processing failure, 2 for invalid usage.
111
138
  `;
112
139
  export const validateUsage = `Usage: cloving workflows validate <manifest-path> [--input <json-file>] [--json]
113
140
 
@@ -122,3 +149,11 @@ Validation does not admit a run, dispatch effects, import workflow code or call
122
149
  Example:
123
150
  cloving workflows validate cloving.workflow.json --input input.json
124
151
  `;
152
+ export const chatsRedactUsage = `Usage: cloving chats redact <IMPORT_ID>
153
+
154
+ Redact one existing import selected by its UUID v4 import ID. No service is required.
155
+ Write redacted.jsonl and redaction.json beside the original corpus with mode 0600.
156
+ HOME supplies the home folder. CLOVING_PRIVATE_TEXTS is an optional JSON list of nonblank strings.
157
+ Originals and metadata remain private. Redaction does not authorize model use or guarantee removal of every private value.
158
+ Exit codes: 0 complete, 1 processing failure, 2 invalid usage.
159
+ `;
@@ -0,0 +1,10 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { type CardForm } from "./watch-run-input.ts";
3
+ export interface WatchActions {
4
+ readonly forms: readonly CardForm[];
5
+ readonly token: string;
6
+ readonly runtimeDirectory: string;
7
+ }
8
+ export declare function watchActions(runtimeDirectory: string): Promise<WatchActions>;
9
+ export declare function watchHost(request: IncomingMessage): boolean;
10
+ export declare function watchAction(actions: WatchActions, request: IncomingMessage): Promise<string | null>;
@@ -0,0 +1,58 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { send } from "./client.js";
3
+ import { watchCardForms } from "./watch-card-forms.js";
4
+ import { join } from "node:path";
5
+ import { cardRunRequest } from "./watch-run-input.js";
6
+ export async function watchActions(runtimeDirectory) {
7
+ return {
8
+ runtimeDirectory,
9
+ forms: await watchCardForms(join(import.meta.dirname, "../../workflows")),
10
+ token: randomBytes(32).toString("hex"),
11
+ };
12
+ }
13
+ export function watchHost(request) {
14
+ const port = request.socket.localPort;
15
+ return [`127.0.0.1:${String(port)}`, `localhost:${String(port)}`].includes(request.headers.host ?? "");
16
+ }
17
+ function authorized(actions, request) {
18
+ return (watchHost(request) &&
19
+ request.headers.origin === `http://${request.headers.host ?? ""}` &&
20
+ request.headers["x-cloving-token"] === actions.token &&
21
+ /^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(request.headers["content-type"] ?? ""));
22
+ }
23
+ async function bodyOf(request) {
24
+ const chunks = [];
25
+ let size = 0;
26
+ for await (const chunk of request) {
27
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
28
+ size += bytes.length;
29
+ if (size > 16384)
30
+ return null;
31
+ chunks.push(bytes);
32
+ }
33
+ try {
34
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
35
+ return value;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function watchAction(actions, request) {
42
+ if (!authorized(actions, request))
43
+ return null;
44
+ const given = cardRunRequest(actions.forms, await bodyOf(request));
45
+ if (given === null)
46
+ return null;
47
+ const status = await send(actions.runtimeDirectory, {
48
+ operation: "status",
49
+ parameters: {},
50
+ });
51
+ if (status.kind === "error")
52
+ return JSON.stringify(status);
53
+ const workflows = status.result["workflows"];
54
+ if (!Array.isArray(workflows) ||
55
+ !workflows.includes(given.parameters["workflowId"]))
56
+ return null;
57
+ return JSON.stringify(await send(actions.runtimeDirectory, given));
58
+ }
@@ -1,22 +1,65 @@
1
+ import { type CardControls } from "./watch-card-view.ts";
2
+ import type { CardForm } from "./watch-run-input.ts";
1
3
  import { type Shown } from "./watch-element.ts";
2
4
  import type { WatchCard, WatchWorkflow } from "./watch-render.ts";
5
+ export declare const boardSizing: {
6
+ minimumColumnWidth: number;
7
+ collapsedColumnWidth: number;
8
+ gap: number;
9
+ };
10
+ export declare function boardLayout(input: {
11
+ readonly contentWidth: number;
12
+ readonly openColumns: number;
13
+ readonly collapsedColumns: number;
14
+ }): {
15
+ readonly columnWidth: number;
16
+ readonly totalWidth: number;
17
+ };
3
18
  export declare function cardStep(card: WatchCard, now: string): string;
4
19
  export declare function cardReview(card: WatchCard): string;
5
20
  export declare function CardReview(props: {
6
21
  readonly card: WatchCard;
7
22
  readonly workflowId: string;
8
23
  }): Shown;
9
- export declare function completionMark(card: WatchCard, workflowId: string): Shown;
24
+ export declare function staleObservation(card: WatchCard, now: string): Shown;
25
+ export declare function observationText(card: WatchCard, fallback: string): string;
26
+ export declare function completionMark(card: WatchCard, context: {
27
+ readonly workflowId: string;
28
+ readonly now: string;
29
+ }): Shown;
10
30
  export declare function Card(props: {
11
31
  readonly card: WatchCard;
12
32
  readonly workflowId: string;
13
33
  readonly now: string;
34
+ readonly observationNow?: string | undefined;
35
+ }): Shown;
36
+ export declare function HandoffLinks(props: {
37
+ readonly run: Pick<WatchCard, "parentRunId" | "childRunIds">;
14
38
  }): Shown;
15
39
  export declare function WorkflowList(props: {
16
40
  readonly workflows: readonly WatchWorkflow[];
17
41
  readonly selected: string | null;
18
42
  }): Shown;
43
+ export interface ColumnControls {
44
+ readonly choices: Readonly<Record<string, boolean>> | null;
45
+ readonly onToggle: (name: string) => void;
46
+ readonly onHover?: (name: string | null) => void;
47
+ readonly onFocus?: (name: string | null) => void;
48
+ }
49
+ export declare function columnCollapsed(choices: Readonly<Record<string, boolean>> | null, name: string): boolean;
50
+ export declare function Column(props: {
51
+ readonly column: WatchWorkflow["columns"][number];
52
+ readonly addCard?: Shown;
53
+ readonly workflowId: string;
54
+ readonly now: string;
55
+ readonly observationNow?: string | undefined;
56
+ readonly controls?: ColumnControls | undefined;
57
+ }): Shown;
19
58
  export declare function Board(props: {
20
59
  readonly workflow: WatchWorkflow;
60
+ readonly cardForm?: CardForm | undefined;
61
+ readonly cards?: CardControls | undefined;
21
62
  readonly now: string;
63
+ readonly observationNow?: string | undefined;
64
+ readonly controls?: ColumnControls | undefined;
22
65
  }): Shown;
@@ -1,5 +1,22 @@
1
+ import { AddCard } from "./watch-card-view.js";
2
+ import { priceText } from "./usage-render.js";
1
3
  import { component, element } from "./watch-element.js";
2
4
  import { watchHash } from "./watch-navigation.js";
5
+ export const boardSizing = {
6
+ minimumColumnWidth: 200,
7
+ collapsedColumnWidth: 44,
8
+ gap: 12,
9
+ };
10
+ export function boardLayout(input) {
11
+ const { contentWidth, openColumns, collapsedColumns } = input;
12
+ const fixedWidth = collapsedColumns * boardSizing.collapsedColumnWidth +
13
+ Math.max(0, openColumns + collapsedColumns - 1) * boardSizing.gap;
14
+ const columnWidth = Math.max(boardSizing.minimumColumnWidth, (contentWidth - fixedWidth) / Math.max(1, openColumns));
15
+ return {
16
+ columnWidth: openColumns === 0 ? boardSizing.minimumColumnWidth : columnWidth,
17
+ totalWidth: openColumns * columnWidth + fixedWidth,
18
+ };
19
+ }
3
20
  export function cardStep(card, now) {
4
21
  if (card.endedAt !== null)
5
22
  return new Date(card.endedAt).toLocaleString();
@@ -38,15 +55,45 @@ export function CardReview(props) {
38
55
  ]),
39
56
  });
40
57
  }
41
- export function completionMark(card, workflowId) {
58
+ export function staleObservation(card, now) {
59
+ const observation = card.pullRequestObservation;
60
+ const lastSuccess = observation?.lastSuccess;
61
+ if (observation?.status === "merged" ||
62
+ lastSuccess?.status !== "unmerged" ||
63
+ !(Date.parse(now) - lastSuccess.readAt > 120000))
64
+ return null;
65
+ return [
66
+ " ",
67
+ element("span", { class: "completion-stale", children: "Stale" }),
68
+ ];
69
+ }
70
+ export function observationText(card, fallback) {
71
+ if (fallback !== "Needs attention")
72
+ return fallback;
73
+ const status = card.pullRequestObservation?.status;
74
+ if (status === "merged")
75
+ return `PR merged · #${String(card.pullRequestNumber)}`;
76
+ if (status === "pending")
77
+ return "Checking PR";
78
+ if (status === "unavailable")
79
+ return "PR status unavailable";
80
+ return fallback;
81
+ }
82
+ export function completionMark(card, context) {
42
83
  const mark = card.completionMark;
43
84
  if (mark === null)
44
85
  return null;
86
+ const text = observationText(card, mark.text);
45
87
  if (mark.laterRunId === null)
46
- return element("span", { class: "completion-mark", children: mark.text });
88
+ return [
89
+ element("span", { class: "completion-mark", children: text }),
90
+ mark.text === "Needs attention"
91
+ ? staleObservation(card, context.now)
92
+ : null,
93
+ ];
47
94
  return element("a", {
48
95
  class: "completion-mark",
49
- href: watchHash({ workflowId, runId: mark.laterRunId }),
96
+ href: watchHash({ workflowId: context.workflowId, runId: mark.laterRunId }),
50
97
  children: mark.text,
51
98
  });
52
99
  }
@@ -58,8 +105,11 @@ export function Card(props) {
58
105
  element("a", {
59
106
  class: "card-run",
60
107
  href: watchHash({ workflowId, runId: card.runId }),
108
+ title: card.runId,
61
109
  children: [
62
- element("strong", { children: card.title ?? card.runId }),
110
+ element("strong", {
111
+ children: card.title ?? card.runId.slice(0, 12),
112
+ }),
63
113
  element("span", {
64
114
  class: card.endedAt === null && card.currentStep !== null
65
115
  ? "card-step active"
@@ -69,7 +119,7 @@ export function Card(props) {
69
119
  component(CardReview, { card, workflowId }),
70
120
  element("span", {
71
121
  class: "card-cost",
72
- children: `${card.claudeCostUsd.toFixed(2)} USD`,
122
+ children: `${card.currentStep === null ? "Totals" : "Totals so far"} · ${priceText(card.claudeCostUsd, card.estimatedCostUsd)}`,
73
123
  }),
74
124
  card.decisionWaiting
75
125
  ? element("span", {
@@ -77,13 +127,28 @@ export function Card(props) {
77
127
  children: "! Decision waiting",
78
128
  })
79
129
  : null,
80
- element("small", { children: card.runId }),
130
+ element("small", { children: card.runId.slice(0, 12) }),
81
131
  ],
82
132
  }),
83
- completionMark(card, workflowId),
133
+ completionMark(card, { workflowId, now: props.observationNow ?? now }),
134
+ component(HandoffLinks, { run: card }),
84
135
  ],
85
136
  });
86
137
  }
138
+ export function HandoffLinks(props) {
139
+ const { parentRunId, childRunIds } = props.run;
140
+ const links = [
141
+ ...(parentRunId == null
142
+ ? []
143
+ : [{ label: "Parent run", runId: parentRunId }]),
144
+ ...(childRunIds ?? []).map((runId) => ({ label: "Child run", runId })),
145
+ ];
146
+ return links.map(({ label, runId }) => element("a", {
147
+ class: "meta",
148
+ href: watchHash({ workflowId: null, runId }),
149
+ children: `${label}: ${runId}`,
150
+ }));
151
+ }
87
152
  export function WorkflowList(props) {
88
153
  return element("nav", {
89
154
  class: "workflows",
@@ -101,6 +166,59 @@ export function WorkflowList(props) {
101
166
  })),
102
167
  });
103
168
  }
169
+ export function columnCollapsed(choices, name) {
170
+ const value = choices !== null && Object.hasOwn(choices, name)
171
+ ? choices[name]
172
+ : undefined;
173
+ return value ?? ["Done", "Stopped"].includes(name);
174
+ }
175
+ export function Column(props) {
176
+ const { column, workflowId, now, controls } = props;
177
+ const collapsed = columnCollapsed(controls?.choices ?? null, column.name);
178
+ const tone = column.name === "Stopped" ? "column stopped" : "column";
179
+ return element("section", {
180
+ class: `${tone}${collapsed ? " collapsed" : ""}`,
181
+ onMouseEnter: () => controls?.onHover?.(column.name),
182
+ onMouseLeave: () => controls?.onHover?.(null),
183
+ onFocusIn: () => controls?.onFocus?.(column.name),
184
+ onFocusOut: () => controls?.onFocus?.(null),
185
+ children: [
186
+ element("h3", {
187
+ class: "column-name",
188
+ children: [
189
+ element("span", { children: column.name }),
190
+ " ",
191
+ element("span", {
192
+ class: "column-count",
193
+ children: String(column.cards.length),
194
+ }),
195
+ element("button", {
196
+ type: "button",
197
+ "aria-label": `${collapsed ? "Expand" : "Collapse"} ${column.name}`,
198
+ "aria-expanded": String(!collapsed),
199
+ onClick: () => controls?.onToggle(column.name),
200
+ children: collapsed ? "+" : "−",
201
+ }),
202
+ ],
203
+ }),
204
+ collapsed
205
+ ? null
206
+ : element("div", {
207
+ class: "column-cards",
208
+ children: [
209
+ column.cards.map((card) => component(Card, {
210
+ key: card.runId,
211
+ card,
212
+ workflowId,
213
+ now,
214
+ observationNow: props.observationNow,
215
+ })),
216
+ props.addCard,
217
+ ],
218
+ }),
219
+ ],
220
+ });
221
+ }
104
222
  export function Board(props) {
105
223
  const { workflow, now } = props;
106
224
  return element("main", {
@@ -109,18 +227,19 @@ export function Board(props) {
109
227
  element("h2", { children: workflow.workflowId }),
110
228
  element("div", {
111
229
  class: "columns",
112
- children: workflow.columns.map((column) => element("section", {
230
+ children: workflow.columns.map((column, index) => component(Column, {
113
231
  key: column.name,
114
- class: column.name === "Stopped" ? "column stopped" : "column",
115
- children: [
116
- element("h3", { class: "column-name", children: column.name }),
117
- column.cards.map((card) => component(Card, {
118
- key: card.runId,
119
- card,
120
- workflowId: workflow.workflowId,
121
- now,
122
- })),
123
- ],
232
+ column,
233
+ workflowId: workflow.workflowId,
234
+ now,
235
+ observationNow: props.observationNow,
236
+ controls: props.controls,
237
+ addCard: index === 0
238
+ ? component(AddCard, {
239
+ form: props.cardForm,
240
+ controls: props.cards,
241
+ })
242
+ : null,
124
243
  })),
125
244
  }),
126
245
  ],
@@ -1,11 +1,17 @@
1
+ import type { WorkflowStage, WorkflowStages } from "../workflow-declaration.ts";
1
2
  import type { WatchCompletionMark, WatchRun, WatchStep, WatchWorkflow } from "./watch-render.ts";
2
3
  export declare function completionMarkOf(run: WatchRun, candidates: readonly WatchRun[]): WatchCompletionMark | null;
3
4
  export declare function activeRun(state: string): boolean;
4
5
  export declare function recentRun(latestAt: string | null, now: string): boolean;
5
- export declare function currentStepOf(events: readonly unknown[], workflowId: string): WatchStep;
6
+ export declare function stagesFor(metadata: WorkflowStages | undefined, workflowId: string): readonly WorkflowStage[] | undefined;
7
+ export declare function currentStepOf(events: readonly unknown[], workflow: {
8
+ readonly workflowId: string;
9
+ readonly stages: readonly WorkflowStage[] | undefined;
10
+ }): WatchStep;
6
11
  export declare function roundOf(events: readonly unknown[]): number;
7
12
  interface BoardInputs {
8
13
  readonly workflows: readonly string[];
14
+ readonly workflowStages?: WorkflowStages;
9
15
  readonly updatedAt: string;
10
16
  readonly summaries?: readonly unknown[];
11
17
  readonly completionCandidates?: readonly WatchRun[];
@@ -1,6 +1,8 @@
1
+ import { linksOf } from "../authority/handoff-chain.js";
1
2
  import { record } from "../protocol.js";
2
3
  import { isInstant } from "../authority/instants.js";
3
4
  import { issueSteps } from "./watch-phases.js";
5
+ import { sameIssue } from "./watch-pr-identity.js";
4
6
  const weekMilliseconds = 7 * 24 * 60 * 60 * 1000;
5
7
  const terminalStates = ["completed", "failed", "cancelled"];
6
8
  function eventTime(run, type) {
@@ -12,15 +14,21 @@ export function completionMarkOf(run, candidates) {
12
14
  run.state !== "completed" ||
13
15
  run.outcome !== "blocked")
14
16
  return null;
17
+ if (run.mergeProof != null)
18
+ return {
19
+ text: `PR merged · #${String(run.mergeProof.number)}`,
20
+ laterRunId: null,
21
+ };
15
22
  const completedAt = eventTime(run, "run.completed");
16
23
  const later = candidates
17
- .filter((candidate) => typeof run.ticketId === "string" &&
18
- run.ticketId !== "" &&
19
- candidate.ticketId === run.ticketId &&
24
+ .filter((candidate) => sameIssue(run.issueIdentity, candidate.issueIdentity) &&
25
+ candidate.mergeProof != null &&
20
26
  candidate.runId !== run.runId &&
21
27
  candidate.workflowId === "develop-issue" &&
22
28
  candidate.state === "completed" &&
23
- candidate.outcome === "handoff_requested")
29
+ candidate.outcome === "handoff_requested" &&
30
+ candidate.ticketStatus !== "partial" &&
31
+ candidate.issueUpdate !== "failed")
24
32
  .map((candidate) => ({
25
33
  runId: candidate.runId,
26
34
  createdAt: eventTime(candidate, "run.created"),
@@ -30,7 +38,7 @@ export function completionMarkOf(run, candidates) {
30
38
  (first.runId < second.runId ? -1 : 1))[0];
31
39
  return later === undefined
32
40
  ? { text: "Needs attention", laterRunId: null }
33
- : { text: "Done in a later run", laterRunId: later.runId };
41
+ : { text: "Ticket completed by later run", laterRunId: later.runId };
34
42
  }
35
43
  export function activeRun(state) {
36
44
  return !terminalStates.includes(state);
@@ -39,10 +47,17 @@ export function recentRun(latestAt, now) {
39
47
  const age = Date.parse(now) - Date.parse(latestAt ?? "");
40
48
  return age >= 0 && age <= weekMilliseconds;
41
49
  }
42
- function stepNames(workflowId) {
50
+ export function stagesFor(metadata, workflowId) {
51
+ return metadata !== undefined && Object.hasOwn(metadata, workflowId)
52
+ ? metadata[workflowId]
53
+ : undefined;
54
+ }
55
+ function stepNames(workflowId, stages) {
56
+ if (workflowId !== "develop-issue" && stages !== undefined)
57
+ return [...stages.map((stage) => stage.label), "Other"];
43
58
  return workflowId === "develop-issue"
44
59
  ? issueSteps.map((step) => step.name)
45
- : ["Claude turn", "Your review"];
60
+ : ["Agent turn", "Your review"];
46
61
  }
47
62
  function operationOf(event) {
48
63
  const payload = record(record(event)?.["payload"]);
@@ -61,7 +76,7 @@ function stepName(event, workflowId) {
61
76
  if (type !== "effect.requested")
62
77
  return undefined;
63
78
  if (workflowId !== "develop-issue")
64
- return "Claude turn";
79
+ return "Agent turn";
65
80
  return issueSteps.find((step) => step.operation === operationOf(event)[4])
66
81
  ?.name;
67
82
  }
@@ -72,10 +87,24 @@ function startedAt(event) {
72
87
  function chronological(events) {
73
88
  return events.toSorted((first, second) => Date.parse(startedAt(first) ?? "") - Date.parse(startedAt(second) ?? ""));
74
89
  }
75
- export function currentStepOf(events, workflowId) {
90
+ function declaredStep(events, stages) {
91
+ const event = events
92
+ .filter((item) => ["effect.requested", "gate.opened"].includes(String(record(item)?.["type"])))
93
+ .toSorted((first, second) => Number(record(second)?.["sequence"]) -
94
+ Number(record(first)?.["sequence"]))[0];
95
+ const payload = record(record(event)?.["payload"]);
96
+ const selector = record(event)?.["type"] === "effect.requested" ? "operations" : "gateKinds";
97
+ const value = selector === "operations" ? operationOf(event)[4] : payload?.["gateKind"];
98
+ const stage = stages.find((item) => item[selector]?.some((name) => name === value));
99
+ return { name: stage?.label ?? "Other", startedAt: startedAt(event) };
100
+ }
101
+ export function currentStepOf(events, workflow) {
102
+ const { workflowId, stages } = workflow;
103
+ if (workflowId !== "develop-issue" && stages !== undefined)
104
+ return declaredStep(events, stages);
76
105
  const event = chronological(events).findLast((item) => stepName(item, workflowId) !== undefined);
77
106
  return {
78
- name: stepName(event, workflowId) ?? stepNames(workflowId)[0] ?? "Claude turn",
107
+ name: stepName(event, workflowId) ?? stepNames(workflowId)[0] ?? "Agent turn",
79
108
  startedAt: startedAt(event),
80
109
  };
81
110
  }
@@ -99,8 +128,12 @@ function cardOf(run, runs) {
99
128
  return {
100
129
  runId: run.runId,
101
130
  title: run.title,
131
+ ...linksOf(run),
102
132
  completionMark: completionMarkOf(run, runs),
133
+ pullRequestObservation: null,
103
134
  claudeCostUsd: run.claudeCostUsd,
135
+ estimatedCostUsd: run.estimatedCostUsd,
136
+ usage: run.usage,
104
137
  currentStep: activeRun(run.state) ? run.currentStep : null,
105
138
  endedAt: activeRun(run.state) ? null : run.latestAt,
106
139
  decisionWaiting: run.decisions.length > 0,
@@ -144,7 +177,11 @@ export function boardsOf(inputs, runs) {
144
177
  : inputs.summaries
145
178
  .filter((value) => record(value)?.["workflowId"] === workflowId)
146
179
  .map(summaryOf);
147
- const columns = [...stepNames(workflowId), "Done", "Stopped"].map((name) => {
180
+ const columns = [
181
+ ...stepNames(workflowId, stagesFor(inputs.workflowStages, workflowId)),
182
+ "Done",
183
+ "Stopped",
184
+ ].map((name) => {
148
185
  const cards = own
149
186
  .filter((run) => columnOf(run, inputs.updatedAt) === name)
150
187
  .map((run) => cardOf(run, inputs.completionCandidates ?? runs));
@@ -0,0 +1 @@
1
+ export declare const watchCardClient = "\nasync function submitCard(workflowId) {\n const draft = state.cards[workflowId];\n if (!draft || draft.pending || draft.admitted) return;\n state.cards[workflowId] = { ...draft, pending: true, message: \"\" };\n draw();\n let result;\n try {\n const response = await fetch(\"actions/run\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-cloving-token\": actionToken },\n body: JSON.stringify({ workflowId, fields: draft.fields, commandId: draft.commandId }),\n });\n result = response.ok ? cardReply(await response.json()) : { admitted: false, message: \"request-refused: Refresh the page and check the input (16 KiB maximum).\" };\n } catch {\n result = cardReply({ kind: \"error\", error: { code: \"outcome-unknown\" } });\n }\n if (state.cards[workflowId]?.commandId !== draft.commandId) return;\n state.cards[workflowId] = { ...draft, ...result, pending: false };\n draw();\n}\n\nfunction cardControls(model, workflowId) {\n if (actionToken === null || workflowId === null) return undefined;\n const form = model?.cardForms?.find((item) => item.workflowId === workflowId);\n if (form === undefined) return undefined;\n const draft = state.cards[workflowId] ?? null;\n return { draft, onOpen() {\n state.cards[workflowId] = { commandId: crypto.randomUUID(), fields: {}, pending: false, admitted: false, message: \"\" };\n draw();\n document.querySelector(\".add-card-form input, .add-card-form textarea, .add-card-form button\")?.focus();\n }, onCancel() {\n delete state.cards[workflowId];\n draw();\n document.querySelector(\".add-card\")?.focus();\n },\n onChange(name, value) {\n const fields = { ...state.cards[workflowId].fields, [name]: value };\n if (form.fields.find((field) => field.name === name)?.optional && !value.trim())\n delete fields[name];\n state.cards[workflowId] = { ...state.cards[workflowId], fields };\n }, onSubmit() { submitCard(workflowId); } };\n}\n";
@@ -0,0 +1,44 @@
1
+ export const watchCardClient = `
2
+ async function submitCard(workflowId) {
3
+ const draft = state.cards[workflowId];
4
+ if (!draft || draft.pending || draft.admitted) return;
5
+ state.cards[workflowId] = { ...draft, pending: true, message: "" };
6
+ draw();
7
+ let result;
8
+ try {
9
+ const response = await fetch("actions/run", {
10
+ method: "POST",
11
+ headers: { "content-type": "application/json", "x-cloving-token": actionToken },
12
+ body: JSON.stringify({ workflowId, fields: draft.fields, commandId: draft.commandId }),
13
+ });
14
+ result = response.ok ? cardReply(await response.json()) : { admitted: false, message: "request-refused: Refresh the page and check the input (16 KiB maximum)." };
15
+ } catch {
16
+ result = cardReply({ kind: "error", error: { code: "outcome-unknown" } });
17
+ }
18
+ if (state.cards[workflowId]?.commandId !== draft.commandId) return;
19
+ state.cards[workflowId] = { ...draft, ...result, pending: false };
20
+ draw();
21
+ }
22
+
23
+ function cardControls(model, workflowId) {
24
+ if (actionToken === null || workflowId === null) return undefined;
25
+ const form = model?.cardForms?.find((item) => item.workflowId === workflowId);
26
+ if (form === undefined) return undefined;
27
+ const draft = state.cards[workflowId] ?? null;
28
+ return { draft, onOpen() {
29
+ state.cards[workflowId] = { commandId: crypto.randomUUID(), fields: {}, pending: false, admitted: false, message: "" };
30
+ draw();
31
+ document.querySelector(".add-card-form input, .add-card-form textarea, .add-card-form button")?.focus();
32
+ }, onCancel() {
33
+ delete state.cards[workflowId];
34
+ draw();
35
+ document.querySelector(".add-card")?.focus();
36
+ },
37
+ onChange(name, value) {
38
+ const fields = { ...state.cards[workflowId].fields, [name]: value };
39
+ if (form.fields.find((field) => field.name === name)?.optional && !value.trim())
40
+ delete fields[name];
41
+ state.cards[workflowId] = { ...state.cards[workflowId], fields };
42
+ }, onSubmit() { submitCard(workflowId); } };
43
+ }
44
+ `;
@@ -0,0 +1,2 @@
1
+ import { type CardForm } from "./watch-run-input.ts";
2
+ export declare function watchCardForms(directory: string): Promise<readonly CardForm[]>;