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
@@ -1,9 +1,30 @@
1
- import { Board, Card, CardReview, WorkflowList, cardReview, cardStep, completionMark, } from "./watch-board-view.js";
1
+ import { cardReply } from "./watch-card-reply.js";
2
+ import { record } from "../protocol.js";
3
+ import { watchCardClient } from "./watch-card-client.js";
4
+ import { AddCard, CardInput, cardKey, cardSubmitKey, } from "./watch-card-view.js";
5
+ import { watchClient } from "./watch-client.js";
6
+ import { priceOf, tokenCount, usageOf } from "../worker-usage.js";
7
+ import { priceText, usageText, totalsSuffix } from "./usage-render.js";
8
+ import { startPrompts, StartCard, StartRun } from "./watch-start-view.js";
9
+ import { Board, boardSizing, Card, HandoffLinks, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, observationText, staleObservation, } from "./watch-board-view.js";
2
10
  import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-navigation.js";
3
11
  import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
4
12
  import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
5
- import { clove, Banner, BannerDetail, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
13
+ import { clove, Banner, BannerDetail, BoardPage, Header, Page, Phases, PullRequest, Review, RefusedWorkflows, Queue, Run, } from "./watch-view.js";
6
14
  const functions = [
15
+ record,
16
+ Queue,
17
+ cardReply,
18
+ cardSubmitKey,
19
+ cardKey,
20
+ AddCard,
21
+ CardInput,
22
+ priceOf,
23
+ tokenCount,
24
+ usageOf,
25
+ priceText,
26
+ usageText,
27
+ totalsSuffix,
7
28
  watchHash,
8
29
  watchLocation,
9
30
  boardWorkflow,
@@ -11,8 +32,13 @@ const functions = [
11
32
  cardReview,
12
33
  cardStep,
13
34
  completionMark,
35
+ observationText,
36
+ staleObservation,
14
37
  CardReview,
15
38
  Card,
39
+ HandoffLinks,
40
+ Column,
41
+ columnCollapsed,
16
42
  WorkflowList,
17
43
  Board,
18
44
  counted,
@@ -35,6 +61,9 @@ const functions = [
35
61
  Command,
36
62
  Decision,
37
63
  Events,
64
+ startPrompts,
65
+ StartCard,
66
+ StartRun,
38
67
  BannerDetail,
39
68
  Banner,
40
69
  Phases,
@@ -42,6 +71,8 @@ const functions = [
42
71
  Review,
43
72
  Run,
44
73
  Header,
74
+ RefusedWorkflows,
75
+ BoardPage,
45
76
  Page,
46
77
  ];
47
78
  const light = `
@@ -69,6 +100,15 @@ const dark = `
69
100
  --bad: #FF7B72; --bad-tint: rgba(255,123,114,0.14);
70
101
  `;
71
102
  const style = `
103
+ @font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400; font-display: swap; src: url("/watch-fonts-v1/fira-code-latin-400-normal.woff2") format("woff2"); }
104
+ @font-face { font-family: "Fira Code"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/fira-code-latin-500-normal.woff2") format("woff2"); }
105
+ @font-face { font-family: "Heebo"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-500-normal.woff2") format("woff2"); }
106
+ @font-face { font-family: "Heebo"; font-style: normal; font-weight: 700; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-700-normal.woff2") format("woff2"); }
107
+ @font-face { font-family: "Heebo"; font-style: normal; font-weight: 800; font-display: swap; src: url("/watch-fonts-v1/heebo-latin-800-normal.woff2") format("woff2"); }
108
+ @font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 400; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-400-normal.woff2") format("woff2"); }
109
+ @font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-500-normal.woff2") format("woff2"); }
110
+ @font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 600; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-600-normal.woff2") format("woff2"); }
111
+ @font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap; src: url("/watch-fonts-v1/space-grotesk-latin-700-normal.woff2") format("woff2"); }
72
112
  :root {${light}
73
113
  --heading-font: "Heebo", system-ui, sans-serif;
74
114
  --body-font: "Space Grotesk", system-ui, sans-serif;
@@ -83,28 +123,59 @@ const style = `
83
123
  :root[data-theme="dark"] {${dark}}
84
124
  * { box-sizing: border-box; }
85
125
  body { margin: 0 auto; max-width: 1800px; padding: 0 20px 96px; }
126
+ body:has(.board-page) { padding-bottom: 0; overflow: hidden; }
127
+ .board-page { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
128
+ .board-page > header, .board-page > .down { flex-shrink: 0; }
129
+ .start-run { flex-shrink: 0; max-height: 45dvh; overflow-y: auto; }
130
+ .workflow-refusals { flex-shrink: 0; max-height: 25dvh; overflow-y: auto; overflow-wrap: anywhere; }
131
+ .workflow-refusals h2 { margin: 8px 0; }
86
132
  a { color: var(--accent-strong); }
87
133
  header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px 0 10px; color: var(--muted); font-size: 12px; }
88
134
  .brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font: 800 26px/1.2 var(--heading-font); }
89
135
  .brand svg { width: 30px; height: 30px; color: var(--accent); }
90
- a:focus-visible, button:focus-visible, summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
136
+ a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
91
137
  h2, h3 { font-family: var(--heading-font); font-weight: 700; }
92
- .workspace { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; margin-top: 20px; }
93
- .workflows { display: flex; flex-direction: column; gap: 8px; }
94
- .workflows a { display: grid; gap: 2px; border-bottom: 3px solid transparent; padding: 10px; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
138
+ .start-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 16px; margin-top: 12px; }
139
+ .start-card { min-width: 0; padding: 14px; border-radius: 14px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
140
+ .start-card h3 { margin: 0; }
141
+ .start-card p { margin: 6px 0 12px; }
142
+ .start-card .command { grid-template-columns: minmax(0, 1fr); margin: 0; }
143
+ .start-card button { justify-self: end; }
144
+ .workspace { display: flex; width: 100%; margin-top: 12px; flex: 1; min-height: 0; }
145
+ .workflows { display: flex; flex-direction: row; flex-shrink: 0; gap: 8px; min-width: 0; margin-top: 12px; padding: 6px; overflow-x: auto; overflow-y: hidden; }
146
+ .workflows a { display: grid; flex-shrink: 0; white-space: nowrap; gap: 2px; border-bottom: 3px solid transparent; padding: 10px; color: inherit; text-decoration: none; }
95
147
  .workflows a[aria-current="page"] { border-bottom-color: var(--accent); background: var(--surface); border-radius: 8px 8px 0 0; }
96
148
  .workflows span, .card small { color: var(--muted); font-size: 12px; }
97
- .board { min-width: 0; }
98
- .board h2 { margin: 0 0 14px; font-size: 24px; }
99
- .columns { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(230px, 1fr); gap: 12px; overflow-x: auto; padding: 6px 6px 24px; }
100
- .column { min-width: 0; padding: 10px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line); background: var(--surface-2); }
101
- .column-name { margin: 0 0 10px; padding: 8px 4px; color: var(--muted); font: 500 12px/1.5 var(--code-font); }
149
+ .board { width: 100%; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
150
+ .board h2 { margin: 0 0 8px; font-size: 24px; flex-shrink: 0; }
151
+ .columns { display: flex; align-items: flex-start; gap: ${String(boardSizing.gap)}px; flex: 1; min-height: 0; overflow-x: scroll; overflow-y: hidden; padding: 6px 6px 12px; scrollbar-color: var(--muted) var(--surface-2); }
152
+ @supports selector(::-webkit-scrollbar) { .columns { scrollbar-color: auto; } }
153
+ .columns::-webkit-scrollbar { height: 12px; }
154
+ .columns::-webkit-scrollbar-track { background: var(--surface-2); }
155
+ .columns::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 6px; }
156
+ .column { flex: 1 0 ${String(boardSizing.minimumColumnWidth)}px; min-width: ${String(boardSizing.minimumColumnWidth)}px; max-height: 100%; display: flex; flex-direction: column; overflow: hidden; padding: 10px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line); background: var(--surface-2); }
157
+ .column-name { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin: 0; padding: 4px 0 10px; color: var(--muted); font: 500 12px/1.5 var(--code-font); }
158
+ .column-name > span:first-child { flex: 1; }
159
+ .column-cards { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; }
160
+ .column.collapsed { flex: 0 0 ${String(boardSizing.collapsedColumnWidth)}px; min-width: ${String(boardSizing.collapsedColumnWidth)}px; padding: 6px; }
161
+ .collapsed .column-name { writing-mode: vertical-rl; padding: 0; }
162
+ .column-name button, .start-toggle { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 0; }
163
+ .column-name button:hover, .start-toggle:hover, .add-card:hover, .add-card-form button:hover { background: var(--surface); }
164
+ .add-card, .add-card-form button { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 4px 8px 4px 0; }
165
+ .add-card { width: 100%; text-align: left; }
166
+ .add-card-form { padding: 8px 0; overflow-wrap: anywhere; }
167
+ .add-card-form label { display: block; color: var(--ink); margin-bottom: 8px; }
168
+ .add-card-form input, .add-card-form textarea { display: block; width: 100%; min-width: 0; box-sizing: border-box; padding: 8px; border: 1px solid var(--line); border-radius: 6px; font: inherit; color: var(--ink); background: var(--surface); }
169
+ .add-card-form textarea { resize: vertical; }
170
+ .add-card-form button:disabled { opacity: 0.6; cursor: default; }
171
+ .add-card-form p { font-size: 12px; margin: 8px 0; }
102
172
  .card { display: grid; gap: 6px; margin-bottom: 10px; padding: 12px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line), var(--shadow); color: var(--ink); overflow-wrap: anywhere; background: var(--surface); }
103
173
  .card:hover { outline: 1px solid var(--accent-strong); }
104
- .card-run { display: grid; gap: 8px; color: inherit; text-decoration: none; }
105
- .card-run strong { font-family: var(--heading-font); font-weight: 700; }
174
+ .card-run { display: grid; gap: 4px; min-width: 0; color: inherit; text-decoration: none; }
175
+ .card-run strong { font-family: var(--heading-font); font-weight: 700; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
106
176
  .completion-mark { font-size: 12px; }
107
177
  .card-step, .card-review, .card-cost, .card small { font: 400 12px/1.6 var(--code-font); font-variant-numeric: tabular-nums; }
178
+ .card-step, .card-review { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
108
179
  .card-step { color: var(--muted); }
109
180
  .card-step.active { color: var(--accent-strong); }
110
181
  .card-review { color: var(--muted); }
@@ -180,90 +251,22 @@ button:hover { background: var(--code-head); }
180
251
  .events { margin-top: 18px; padding-bottom: 10px; }
181
252
  .events > summary { color: var(--code-fg); }
182
253
  .down { font-weight: 600; }
183
- @media (max-width: 700px) { body { padding-inline: 12px; } .workspace { grid-template-columns: minmax(0, 1fr); gap: 12px; } .workflows { flex-direction: row; overflow-x: auto; padding: 6px; } .workflows a { flex-shrink: 0; } }
254
+ @media (max-width: 700px) { body { padding-inline: 12px; } }
184
255
  `;
185
- const loader = `
186
- const state = { model: null, stale: false, copied: "", label: "", run: "", loading: false, again: false };
187
- function moved(next, followed) {
188
- const arrived = next === "waiting_gate" && next !== state.run;
189
- state.run = next;
190
- if (arrived) scrollTo(0, 0);
191
- else if (followed && next === "running") scrollTo(0, document.body.scrollHeight);
192
- }
193
- function copy(command) {
194
- const shown = (label) => {
195
- state.copied = command;
196
- state.label = label;
197
- draw();
198
- };
199
- navigator.clipboard.writeText(command).then(
200
- () => { shown("Copied"); },
201
- () => { shown("Copy failed"); },
202
- );
203
- }
204
- function draw() {
205
- const { model, stale } = state;
206
- const selected = location.hash.slice(1);
207
- const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;
208
- const copying = { command: state.copied, label: state.label, onCopy: copy };
209
- const now = new Date().toISOString();
210
- const page = component(Page, { model, selected, now, stale, copying });
211
- render(page, document.getElementById("app"));
212
- if (model === null) return;
213
- document.title = watchTitle(model, selected);
214
- const { runId } = watchLocation(selected);
215
- moved(model.runs.find((run) => run.runId === runId)?.state ?? "", followed);
216
- }
217
- async function withSelectedRun(model) {
218
- const { runId } = watchLocation(location.hash);
219
- if (runId === null || model.runs.some((run) => run.runId === runId)) return model;
220
- const detail = await fetch("data.json?runId=" + encodeURIComponent(runId), { cache: "no-store" });
221
- if (detail.status === 404) return model;
222
- if (!detail.ok) throw new Error("watch-run-not-available");
223
- const { run } = await detail.json();
224
- if (run?.runId !== runId) throw new Error("watch-run-mismatch");
225
- return { ...model, runs: [...model.runs, run] };
226
- }
227
- async function load() {
228
- state.again = state.loading;
229
- if (state.loading) return;
230
- state.loading = true;
231
- try {
232
- const reply = await fetch("data.json", { cache: "no-store" });
233
- if (!reply.ok) throw new Error("watch-not-available");
234
- state.model = await withSelectedRun(await reply.json());
235
- state.stale = false;
236
- } catch {
237
- state.stale = true;
238
- }
239
- state.loading = false;
240
- draw();
241
- if (state.again) load();
242
- }
243
- function listen() {
244
- const source = new EventSource("events");
245
- source.onmessage = () => { load(); };
246
- source.onerror = () => { state.stale = true; draw(); };
247
- }
248
- addEventListener("hashchange", () => { state.run = ""; draw(); load(); });
249
- draw();
250
- listen();
251
- setInterval(draw, 1000);
252
- setInterval(load, 15000);
253
- `;
254
- export function pageDocument() {
256
+ export function pageDocument(actionToken = null) {
255
257
  const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${clove.viewBox}">${clove.paths.map((d) => `<path fill="#0BBF64" d="${d}"/>`).join("")}</svg>`;
256
258
  const script = [
257
259
  'import { h as element, h as component, render } from "./preact.mjs";',
258
260
  `const clove = ${JSON.stringify(clove)};`,
261
+ `const actionToken = ${JSON.stringify(actionToken)};`,
259
262
  ...functions.map((item) => item.toString()),
260
- loader,
263
+ watchCardClient,
264
+ watchClient,
261
265
  ].join("\n");
262
266
  return [
263
267
  "<!doctype html>",
264
268
  '<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Cloving</title>',
265
269
  `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(icon)}">`,
266
- '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&amp;family=Heebo:wght@500;700;800&amp;family=Space+Grotesk:wght@400..700&amp;display=swap" media="print" onload="this.media=\'all\'">',
267
270
  `<style>${style}</style></head><body>`,
268
271
  '<div id="app"></div>',
269
272
  `<script type="module">${script}</script></body></html>`,
@@ -8,9 +8,9 @@ const effect = { open: "effect.requested", close: "effect.reported" };
8
8
  const issueWorkflow = "develop-issue";
9
9
  export const issueSteps = [
10
10
  { name: "Work tree", operation: "github-prepare" },
11
- { name: "Claude turn", operation: "claude-turn" },
11
+ { name: "Agent turn", operation: "claude-turn" },
12
12
  { name: "Pull request", operation: "github-open-pr" },
13
- { name: "Greptile review", operation: "github-wait-review" },
13
+ { name: "Code review", operation: "github-wait-review" },
14
14
  { name: "Merge", operation: "github-merge" },
15
15
  ];
16
16
  const verdicts = {
@@ -56,7 +56,7 @@ function ticketSpans(events) {
56
56
  return [
57
57
  ...admission(events),
58
58
  {
59
- name: "Claude turn",
59
+ name: "Agent turn",
60
60
  startedAt: firstAt(events, effect.open),
61
61
  endedAt: closedAt(events, effect),
62
62
  ending: "done",
@@ -0,0 +1,3 @@
1
+ import type { PrObserver } from "./watch-pr-observer.ts";
2
+ import type { WatchModel } from "./watch-render.ts";
3
+ export declare function observedBoard(model: WatchModel, observer: PrObserver): WatchModel;
@@ -0,0 +1,26 @@
1
+ function observedCard(context, card) {
2
+ if (card.completionMark?.text !== "Needs attention")
3
+ return card;
4
+ const run = context.model.runs.find((run) => run.runId === card.runId);
5
+ if (run?.workflowId !== "develop-issue" ||
6
+ run.state !== "completed" ||
7
+ run.outcome !== "blocked" ||
8
+ run.pullRequest === null)
9
+ return card;
10
+ return {
11
+ ...card,
12
+ pullRequestObservation: context.observer.observe(run.pullRequest),
13
+ };
14
+ }
15
+ export function observedBoard(model, observer) {
16
+ return {
17
+ ...model,
18
+ workflows: model.workflows.map((workflow) => ({
19
+ ...workflow,
20
+ columns: workflow.columns.map((column) => ({
21
+ ...column,
22
+ cards: column.cards.map((card) => observedCard({ model, observer }, card)),
23
+ })),
24
+ })),
25
+ };
26
+ }
@@ -0,0 +1,18 @@
1
+ import { type PrIdentity, type PrReadResult } from "./watch-pr-identity.ts";
2
+ export interface WatchCommandOutput {
3
+ readonly stdout: string;
4
+ readonly stderr: string;
5
+ readonly status: number | null;
6
+ readonly errorCode: string | null;
7
+ }
8
+ export interface WatchCommandRequest {
9
+ readonly command: readonly string[];
10
+ readonly directory: string;
11
+ readonly signal: AbortSignal;
12
+ }
13
+ export type WatchCommandRunner = (request: WatchCommandRequest) => Promise<WatchCommandOutput>;
14
+ export declare function prArguments(identity: PrIdentity): readonly string[];
15
+ export declare function prCommandResult(identity: PrIdentity, reply: {
16
+ readonly output: WatchCommandOutput;
17
+ readonly now: number;
18
+ }): PrReadResult;
@@ -0,0 +1,74 @@
1
+ import { prResponse, } from "./watch-pr-identity.js";
2
+ export function prArguments(identity) {
3
+ return [
4
+ "api",
5
+ "--method",
6
+ "GET",
7
+ "--hostname",
8
+ identity.host,
9
+ `repos/${identity.owner}/${identity.repository}/pulls/${String(identity.number)}`,
10
+ "--include",
11
+ ];
12
+ }
13
+ function commandFailure(output) {
14
+ const status = Number(/\(HTTP (\d{3})\)/.exec(output.stderr)?.[1] ??
15
+ /^HTTP\/[^ ]+ (\d{3})/m.exec(output.stdout)?.[1]);
16
+ if (status === 429 ||
17
+ /rate limit/i.test(output.stderr) ||
18
+ headerValues(output.stdout, "x-ratelimit-remaining").includes("0"))
19
+ return "rate-limit";
20
+ if (output.status === 4 || [401, 403, 404].includes(status))
21
+ return "access";
22
+ return "command";
23
+ }
24
+ function headerValues(stdout, name) {
25
+ if (!/^HTTP\/[^ ]+ \d{3}/.test(stdout))
26
+ return [];
27
+ const headers = stdout.split(/\r?\n\r?\n/, 1)[0] ?? "";
28
+ return headers.split(/\r?\n/).flatMap((line) => {
29
+ const separator = line.indexOf(":");
30
+ return line.slice(0, separator).toLowerCase() === name
31
+ ? [line.slice(separator + 1).trim()]
32
+ : [];
33
+ });
34
+ }
35
+ function millisecondsOf(value) {
36
+ if (!/^\d+$/.test(value))
37
+ return [];
38
+ const milliseconds = Number(value) * 1000;
39
+ return Number.isSafeInteger(milliseconds) ? [milliseconds] : [];
40
+ }
41
+ function retryDeadline(output, now) {
42
+ const delays = headerValues(output.stdout, "retry-after")
43
+ .flatMap(millisecondsOf)
44
+ .map((delay) => now + delay)
45
+ .filter(Number.isSafeInteger);
46
+ const resets = headerValues(output.stdout, "x-ratelimit-remaining").includes("0")
47
+ ? headerValues(output.stdout, "x-ratelimit-reset").flatMap(millisecondsOf)
48
+ : [];
49
+ const deadlines = [...delays, ...resets];
50
+ return deadlines.length === 0 ? now + 60000 : Math.max(now, ...deadlines);
51
+ }
52
+ export function prCommandResult(identity, reply) {
53
+ const { output, now } = reply;
54
+ if (output.status !== 0 || output.errorCode !== null) {
55
+ const failure = commandFailure(output);
56
+ return {
57
+ status: "unavailable",
58
+ failure,
59
+ ...(failure === "rate-limit"
60
+ ? { retryAt: retryDeadline(output, now) }
61
+ : {}),
62
+ };
63
+ }
64
+ const envelope = /^HTTP\/[^ ]+ 200[^\n]*\r?\n[\s\S]*?\r?\n\r?\n([\s\S]*)$/.exec(output.stdout);
65
+ if (envelope?.[1] === undefined)
66
+ return { status: "unavailable", failure: "response" };
67
+ try {
68
+ const response = JSON.parse(envelope[1]);
69
+ return prResponse(identity, response);
70
+ }
71
+ catch {
72
+ return { status: "unavailable", failure: "response" };
73
+ }
74
+ }
@@ -0,0 +1,32 @@
1
+ export interface PrIdentity {
2
+ readonly host: string;
3
+ readonly owner: string;
4
+ readonly repository: string;
5
+ readonly number: number;
6
+ }
7
+ export type PrFailure = "configuration" | "identity" | "command" | "access" | "rate-limit" | "response" | "timeout";
8
+ export type PrReadResult = {
9
+ readonly status: "merged" | "unmerged";
10
+ } | {
11
+ readonly status: "unavailable";
12
+ readonly failure: PrFailure;
13
+ readonly retryAt?: number;
14
+ };
15
+ export type PrObservation = (PrReadResult | {
16
+ readonly status: "pending";
17
+ }) & {
18
+ readonly retryAt?: number;
19
+ readonly lastSuccess?: {
20
+ readonly status: "merged" | "unmerged";
21
+ readonly readAt: number;
22
+ };
23
+ };
24
+ export declare function prResponse(identity: PrIdentity, value: unknown): PrReadResult;
25
+ export type PrRepository = Omit<PrIdentity, "number">;
26
+ export declare function establishedIssue(repository: PrRepository | null, value: unknown): PrIdentity | null;
27
+ export declare function sameIssue(first: PrIdentity | null | undefined, second: PrIdentity | null | undefined): boolean;
28
+ export declare function prRepository(origin: string): PrRepository | null;
29
+ export declare function prIdentity(repository: PrRepository, pullRequest: {
30
+ readonly number: number;
31
+ readonly url: string;
32
+ }): PrIdentity | null;
@@ -0,0 +1,87 @@
1
+ import { record } from "../protocol.js";
2
+ function responseNames(fields) {
3
+ const repository = record(record(fields["base"])?.["repo"]) ?? {};
4
+ return [
5
+ fields["html_url"],
6
+ repository["html_url"],
7
+ repository["full_name"],
8
+ repository["name"],
9
+ record(repository["owner"])?.["login"],
10
+ ];
11
+ }
12
+ export function prResponse(identity, value) {
13
+ const fields = record(value) ?? {};
14
+ const expected = `https://${identity.host}/${identity.owner}/${identity.repository}`;
15
+ const strings = responseNames(fields);
16
+ if (typeof fields["number"] !== "number" ||
17
+ strings.some((part) => typeof part !== "string"))
18
+ return { status: "unavailable", failure: "response" };
19
+ const wanted = [
20
+ `${expected}/pull/${String(identity.number)}`,
21
+ expected,
22
+ `${identity.owner}/${identity.repository}`,
23
+ identity.repository,
24
+ identity.owner,
25
+ ];
26
+ if (fields["number"] !== identity.number ||
27
+ strings.some((part, index) => String(part).toLowerCase() !== String(wanted[index]).toLowerCase()))
28
+ return { status: "unavailable", failure: "identity" };
29
+ const merged = fields["merged"];
30
+ if (typeof merged !== "boolean")
31
+ return { status: "unavailable", failure: "response" };
32
+ return { status: merged ? "merged" : "unmerged" };
33
+ }
34
+ export function establishedIssue(repository, value) {
35
+ const fields = record(value) ?? {};
36
+ const number = fields["number"];
37
+ if (repository === null ||
38
+ typeof number !== "number" ||
39
+ !Number.isSafeInteger(number) ||
40
+ number <= 0)
41
+ return null;
42
+ const owner = fields["owner"];
43
+ const name = fields["repository"];
44
+ if (typeof owner !== "string" ||
45
+ typeof name !== "string" ||
46
+ owner.toLowerCase() !== repository.owner ||
47
+ name.toLowerCase() !== repository.repository)
48
+ return null;
49
+ return { ...repository, number };
50
+ }
51
+ export function sameIssue(first, second) {
52
+ return (first != null &&
53
+ second != null &&
54
+ first.host === second.host &&
55
+ first.owner === second.owner &&
56
+ first.repository === second.repository &&
57
+ first.number === second.number);
58
+ }
59
+ export function prRepository(origin) {
60
+ const address = origin
61
+ .replace(/^git@([^:/]+):/, "ssh://git@$1/")
62
+ .replace(/\r?\n$/, "");
63
+ const matched = /^(?:https:\/\/|ssh:\/\/git@)([a-zA-Z0-9.-]+)\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9_.-]+)$/.exec(address);
64
+ const [, host, owner, name] = matched ?? [];
65
+ if (host === undefined || owner === undefined || name === undefined)
66
+ return null;
67
+ if (host
68
+ .split(".")
69
+ .some((label) => !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)))
70
+ return null;
71
+ const repository = name.replace(/\.git$/, "");
72
+ if (![host, owner].every((part) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(part)) ||
73
+ ["", ".", ".."].includes(repository))
74
+ return null;
75
+ return {
76
+ host: host.toLowerCase(),
77
+ owner: owner.toLowerCase(),
78
+ repository: repository.toLowerCase(),
79
+ };
80
+ }
81
+ export function prIdentity(repository, pullRequest) {
82
+ const { number, url } = pullRequest;
83
+ if (!Number.isSafeInteger(number) || number <= 0)
84
+ return null;
85
+ const expected = `https://${repository.host}/${repository.owner}/${repository.repository}/pull/${String(number)}`;
86
+ return url.toLowerCase() === expected ? { ...repository, number } : null;
87
+ }
@@ -0,0 +1,20 @@
1
+ import type { WatchPullRequest } from "./watch-render.ts";
2
+ import { type WatchCommandRunner } from "./watch-pr-command.ts";
3
+ import { type PrObservation, type PrRepository } from "./watch-pr-identity.ts";
4
+ export interface PrObserver {
5
+ readonly repository?: PrRepository | null;
6
+ readonly observe: (pullRequest: WatchPullRequest) => PrObservation | null;
7
+ readonly close: () => void;
8
+ }
9
+ export interface PrClock {
10
+ readonly now: () => number;
11
+ readonly after: (expired: () => void, milliseconds: number) => () => void;
12
+ }
13
+ export interface PrObserverSettings {
14
+ readonly clock?: PrClock;
15
+ readonly repository: PrRepository | null;
16
+ readonly directory: string;
17
+ readonly command: readonly string[] | null;
18
+ readonly execute: WatchCommandRunner;
19
+ }
20
+ export declare function prObserver(settings: PrObserverSettings): PrObserver;