cloving 1.1.0 → 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 (213) hide show
  1. package/README.md +121 -21
  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 +25 -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 +1 -0
  23. package/dist/src/authority/reconciliation.js +18 -11
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +3 -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/delivery-intents.d.ts +1 -0
  29. package/dist/src/broker/actor/effect-delivery.js +9 -0
  30. package/dist/src/broker/authority-admission.js +13 -5
  31. package/dist/src/chats/analysis-batches.d.ts +18 -0
  32. package/dist/src/chats/analysis-batches.js +15 -1
  33. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  34. package/dist/src/chats/analysis-checkpoint.js +31 -0
  35. package/dist/src/chats/analysis-input.d.ts +7 -1
  36. package/dist/src/chats/analysis-input.js +20 -6
  37. package/dist/src/chats/analysis-merge.d.ts +16 -0
  38. package/dist/src/chats/analysis-merge.js +134 -0
  39. package/dist/src/chats/analysis-prompt.d.ts +8 -0
  40. package/dist/src/chats/analysis-prompt.js +27 -1
  41. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  42. package/dist/src/chats/analysis-proposal.js +9 -0
  43. package/dist/src/chats/analysis-shape.d.ts +1 -0
  44. package/dist/src/chats/analysis-shape.js +11 -4
  45. package/dist/src/chats/reference-mapping.d.ts +16 -0
  46. package/dist/src/chats/reference-mapping.js +153 -0
  47. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  48. package/dist/src/chats/workflow-generation-projection.js +75 -0
  49. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  50. package/dist/src/chats/workflow-generation-source.js +124 -0
  51. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  52. package/dist/src/chats/workflow-generation-text.js +74 -0
  53. package/dist/src/chats/workflow-generation.d.ts +69 -0
  54. package/dist/src/chats/workflow-generation.js +142 -0
  55. package/dist/src/cli/render.d.ts +1 -0
  56. package/dist/src/cli/render.js +60 -13
  57. package/dist/src/cli/usage.js +5 -2
  58. package/dist/src/cli/watch-board-view.d.ts +12 -1
  59. package/dist/src/cli/watch-board-view.js +57 -11
  60. package/dist/src/cli/watch-board.d.ts +7 -1
  61. package/dist/src/cli/watch-board.js +42 -10
  62. package/dist/src/cli/watch-client.d.ts +1 -1
  63. package/dist/src/cli/watch-client.js +7 -3
  64. package/dist/src/cli/watch-model.d.ts +10 -4
  65. package/dist/src/cli/watch-model.js +35 -58
  66. package/dist/src/cli/watch-page.js +21 -4
  67. package/dist/src/cli/watch-phases.js +3 -3
  68. package/dist/src/cli/watch-pr-command.d.ts +5 -2
  69. package/dist/src/cli/watch-pr-command.js +41 -4
  70. package/dist/src/cli/watch-pr-identity.d.ts +14 -2
  71. package/dist/src/cli/watch-pr-identity.js +25 -0
  72. package/dist/src/cli/watch-pr-observer.d.ts +1 -0
  73. package/dist/src/cli/watch-pr-observer.js +48 -6
  74. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  75. package/dist/src/cli/watch-pull-request.js +130 -0
  76. package/dist/src/cli/watch-reads.d.ts +7 -1
  77. package/dist/src/cli/watch-reads.js +42 -20
  78. package/dist/src/cli/watch-render.d.ts +14 -1
  79. package/dist/src/cli/watch-render.js +2 -1
  80. package/dist/src/cli/watch-server.js +33 -2
  81. package/dist/src/cli/watch-turn.js +1 -1
  82. package/dist/src/cli/watch-view.d.ts +7 -0
  83. package/dist/src/cli/watch-view.js +51 -3
  84. package/dist/src/daemon/analysis-worker.d.ts +0 -2
  85. package/dist/src/daemon/analysis-worker.js +71 -19
  86. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  87. package/dist/src/daemon/chats-batch-binding.js +99 -0
  88. package/dist/src/daemon/chats-capture.d.ts +18 -0
  89. package/dist/src/daemon/chats-capture.js +194 -0
  90. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  91. package/dist/src/daemon/chats-checkpoints.js +186 -0
  92. package/dist/src/daemon/chats-files.d.ts +29 -2
  93. package/dist/src/daemon/chats-files.js +142 -3
  94. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  95. package/dist/src/daemon/chats-follow-up.js +180 -0
  96. package/dist/src/daemon/chats-question.d.ts +15 -0
  97. package/dist/src/daemon/chats-question.js +93 -0
  98. package/dist/src/daemon/chats-worker.d.ts +1 -0
  99. package/dist/src/daemon/chats-worker.js +52 -51
  100. package/dist/src/daemon/claude-turn.d.ts +2 -1
  101. package/dist/src/daemon/claude-turn.js +1 -1
  102. package/dist/src/daemon/claude-worker.d.ts +0 -2
  103. package/dist/src/daemon/claude-worker.js +6 -11
  104. package/dist/src/daemon/daemon.d.ts +5 -1
  105. package/dist/src/daemon/daemon.js +38 -10
  106. package/dist/src/daemon/delivery-router.js +6 -0
  107. package/dist/src/daemon/github-calls.d.ts +4 -0
  108. package/dist/src/daemon/github-calls.js +16 -7
  109. package/dist/src/daemon/github-ci.d.ts +6 -0
  110. package/dist/src/daemon/github-ci.js +170 -0
  111. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  112. package/dist/src/daemon/github-merge-call.js +73 -0
  113. package/dist/src/daemon/github-merge.d.ts +3 -5
  114. package/dist/src/daemon/github-merge.js +52 -95
  115. package/dist/src/daemon/github-methods.d.ts +28 -0
  116. package/dist/src/daemon/github-methods.js +80 -16
  117. package/dist/src/daemon/github-operation.d.ts +2 -0
  118. package/dist/src/daemon/github-retry.d.ts +6 -0
  119. package/dist/src/daemon/github-retry.js +49 -0
  120. package/dist/src/daemon/github-ticket-status.js +27 -1
  121. package/dist/src/daemon/github-worker.js +2 -1
  122. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  123. package/dist/src/daemon/handoff-worker.js +232 -0
  124. package/dist/src/daemon/issue-queue.d.ts +1 -0
  125. package/dist/src/daemon/issue-queue.js +94 -42
  126. package/dist/src/daemon/notify-journal.d.ts +18 -0
  127. package/dist/src/daemon/notify-journal.js +108 -0
  128. package/dist/src/daemon/notify-message.js +2 -1
  129. package/dist/src/daemon/notify.js +176 -37
  130. package/dist/src/daemon/retrying-turn.d.ts +3 -0
  131. package/dist/src/daemon/retrying-turn.js +55 -9
  132. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  133. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  134. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  135. package/dist/src/daemon/ticket-registration.js +76 -22
  136. package/dist/src/daemon/ticket-source.d.ts +4 -1
  137. package/dist/src/daemon/ticket-source.js +8 -2
  138. package/dist/src/decision-visibility.d.ts +2 -0
  139. package/dist/src/decision-visibility.js +10 -0
  140. package/dist/src/execution-pin.d.ts +4 -0
  141. package/dist/src/execution-pin.js +23 -2
  142. package/dist/src/host/start-delivery.d.ts +2 -1
  143. package/dist/src/host/start-delivery.js +1 -1
  144. package/dist/src/host/step-bridge-host.d.ts +1 -0
  145. package/dist/src/host/step-bridge-host.js +16 -0
  146. package/dist/src/host/world-reconciliation.d.ts +1 -0
  147. package/dist/src/host/world-reconciliation.js +3 -0
  148. package/dist/src/host/world-runs.js +5 -0
  149. package/dist/src/issue-identity.d.ts +6 -0
  150. package/dist/src/issue-identity.js +23 -0
  151. package/dist/src/service/queue-runs.d.ts +3 -0
  152. package/dist/src/service/queue-runs.js +39 -0
  153. package/dist/src/service/registry.d.ts +8 -2
  154. package/dist/src/service/registry.js +20 -8
  155. package/dist/src/service/replies.d.ts +2 -0
  156. package/dist/src/service/run-handlers.js +44 -12
  157. package/dist/src/service/service.d.ts +3 -0
  158. package/dist/src/service/service.js +3 -1
  159. package/dist/src/service/submission-handler.d.ts +1 -0
  160. package/dist/src/service/submission-handler.js +92 -13
  161. package/dist/src/service/worker-totals.js +25 -1
  162. package/dist/src/step-bridge.d.ts +9 -0
  163. package/dist/src/steps/handoff.d.ts +2 -0
  164. package/dist/src/steps/handoff.js +13 -0
  165. package/dist/src/ticket-dependencies.d.ts +1 -0
  166. package/dist/src/ticket-dependencies.js +15 -0
  167. package/dist/src/ticket-status.js +7 -4
  168. package/dist/src/token-prices.d.ts +2 -0
  169. package/dist/src/token-prices.js +41 -9
  170. package/dist/src/worker-usage.d.ts +9 -0
  171. package/dist/src/worker-usage.js +1 -1
  172. package/dist/src/workflow-admission.d.ts +1 -0
  173. package/dist/src/workflow-candidate.js +1 -0
  174. package/dist/src/workflow-contracts.d.ts +2 -0
  175. package/dist/src/workflow-contracts.js +8 -3
  176. package/dist/src/workflow-declaration.d.ts +11 -0
  177. package/dist/src/workflow-declaration.js +71 -15
  178. package/dist/src/workflow-discovery.d.ts +18 -0
  179. package/dist/src/workflow-discovery.js +33 -0
  180. package/dist/src/workflow-refusals.d.ts +6 -0
  181. package/dist/src/workflow-refusals.js +39 -0
  182. package/dist/src/workflows.d.ts +2 -1
  183. package/dist/src/workflows.js +1 -0
  184. package/dist/tools/admit-workflow.d.ts +13 -0
  185. package/dist/tools/admit-workflow.js +94 -0
  186. package/dist/tools/self-host-project.d.ts +17 -0
  187. package/dist/tools/self-host-project.js +177 -0
  188. package/dist/tools/self-host.js +27 -81
  189. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  190. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  191. package/dist/tools/workflow-bundles.d.ts +20 -0
  192. package/dist/tools/workflow-bundles.js +173 -0
  193. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  194. package/dist/tools/workflow-candidate-counts.js +251 -0
  195. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  196. package/dist/tools/workflow-candidate-program.js +78 -0
  197. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  198. package/dist/tools/workflow-candidate-source.js +107 -0
  199. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  200. package/dist/tools/workflow-candidate-syntax.js +219 -0
  201. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  202. package/dist/tools/workflow-folder-capture.js +216 -0
  203. package/dist/tools/workflow-folders.d.ts +33 -0
  204. package/dist/tools/workflow-folders.js +150 -0
  205. package/dist/tools/workflow-source-effects.d.ts +10 -0
  206. package/dist/tools/workflow-source-effects.js +213 -0
  207. package/dist/workflows/analyze-chats.d.ts +6 -1
  208. package/dist/workflows/analyze-chats.declaration.json +14 -1
  209. package/dist/workflows/analyze-chats.js +188 -11
  210. package/dist/workflows/develop-prd.js +71 -3
  211. package/dist/workflows/develop-ticket.js +5 -0
  212. package/dist/workflows/ticket-prompts.js +12 -2
  213. package/package.json +21 -3
@@ -1,9 +1,14 @@
1
+ import { operationalDecision } from "../decision-visibility.js";
2
+ import { workflowRefusals, } from "../workflow-refusals.js";
1
3
  import { isTicketStatus } from "../ticket-status.js";
4
+ import { isPromotionId } from "../workflow-contracts.js";
2
5
  import { priceFields, statusTurns } from "./watch-usage.js";
3
6
  import { priceOf, usageOf } from "../worker-usage.js";
4
7
  import { priceText } from "./usage-render.js";
5
- import { boardsOf, currentStepOf, roundOf, startWorkflowOf, } from "./watch-board.js";
8
+ import { boardsOf, currentStepOf, roundOf, stagesFor, startWorkflowOf, } from "./watch-board.js";
6
9
  import { decisionOf, phasesOf } from "./watch-phases.js";
10
+ import { pullOf } from "./watch-pull-request.js";
11
+ import { establishedIssue } from "./watch-pr-identity.js";
7
12
  import { displayRun } from "./watch-paths.js";
8
13
  const textLimit = 600;
9
14
  const activityLimit = 400;
@@ -156,54 +161,6 @@ export function decisionCommands(decision, client) {
156
161
  deny: `${client} deny ${decisionId} --text "<feedback>" ${binding}`,
157
162
  };
158
163
  }
159
- function githubOf(records) {
160
- const started = record(records.started);
161
- const operation = list(parsed(text(started["operationId"])));
162
- const result = record(records.result);
163
- const completed = result["outcome"] === "completed";
164
- return {
165
- runId: text(operation[2]),
166
- round: Number(operation[1]),
167
- method: text(started["method"]),
168
- report: completed ? record(parsed(text(result["result"]))) : {},
169
- };
170
- }
171
- function pullRequestOf(results) {
172
- const opened = results
173
- .filter((item) => item.method === "open-pr")
174
- .map((item) => item.report)
175
- .findLast((report) => text(report["url"]).startsWith("https://"));
176
- if (opened === undefined)
177
- return null;
178
- return { number: Number(opened["number"]), url: text(opened["url"]) };
179
- }
180
- function reviewsOf(results) {
181
- return results
182
- .filter((item) => item.method === "wait-review")
183
- .flatMap(({ round, report }) => {
184
- const score = count(report["score"]);
185
- if (score === null)
186
- return [];
187
- return [{ round, commit: text(report["commit"]), score }];
188
- });
189
- }
190
- function mergeCommitOf(results) {
191
- const merged = results
192
- .filter((item) => item.method === "merge")
193
- .map((item) => stamp(item.report["merged"]));
194
- return merged.find((commit) => commit !== null) ?? null;
195
- }
196
- function pullOf(inputs, runId) {
197
- const results = inputs.githubRecords
198
- .map(githubOf)
199
- .filter((item) => item.runId === runId)
200
- .toSorted((first, second) => first.round - second.round);
201
- return {
202
- pullRequest: pullRequestOf(results),
203
- reviews: reviewsOf(results),
204
- mergeCommit: mergeCommitOf(results),
205
- };
206
- }
207
164
  function feedbackOf(value) {
208
165
  const fields = record(value);
209
166
  return {
@@ -219,28 +176,43 @@ function eventOf(value) {
219
176
  type: text(fields["type"]),
220
177
  };
221
178
  }
179
+ function outcomeOf(value) {
180
+ const fields = record(value);
181
+ const { promotionId, ticketStatus, issueUpdate } = fields;
182
+ return {
183
+ outcome: stamp(fields["kind"]),
184
+ ...(isPromotionId(promotionId) ? { promotionId } : {}),
185
+ ...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
186
+ ...(issueUpdate === "failed" ? { issueUpdate } : {}),
187
+ };
188
+ }
222
189
  function runOf(inputs, value) {
223
190
  const status = record(record(value)["status"]);
224
191
  const runId = text(status["runId"]);
225
192
  const workflowId = text(status["workflowId"]);
226
193
  const budget = record(status["budget"]);
227
194
  const ticketId = text(status["ticketId"]);
228
- const ticketStatus = record(status["outcome"])["ticketStatus"];
195
+ const issue = workflowId === "develop-issue"
196
+ ? establishedIssue(inputs.repository ?? null, status["issueIdentity"])
197
+ : null;
229
198
  return {
230
199
  runId,
231
200
  workflowId,
201
+ parentRunId: stamp(status["parentRunId"]),
202
+ chainId: stamp(status["chainId"]) ?? runId,
203
+ childRunIds: texts(status["childRunIds"]),
232
204
  ticketId: ticketId === "" ? null : ticketId,
205
+ ...(issue === null ? {} : { issueIdentity: issue }),
233
206
  title: stamp(status["title"]),
234
- outcome: stamp(record(status["outcome"])["kind"]),
235
- ...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
236
- ...(record(status["outcome"])["issueUpdate"] === "failed"
237
- ? { issueUpdate: "failed" }
238
- : {}),
207
+ ...outcomeOf(status["outcome"]),
239
208
  latestAt: stamp(status["latestAt"]),
240
209
  claudeCostUsd: priceOf(status["claudeCostUsd"]),
241
210
  estimatedCostUsd: priceOf(status["estimatedCostUsd"]),
242
211
  usage: usageOf(status["usage"]),
243
- currentStep: currentStepOf(list(record(value)["events"]), workflowId),
212
+ currentStep: currentStepOf(list(record(value)["events"]), {
213
+ workflowId,
214
+ stages: stagesFor(inputs.workflowStages, workflowId),
215
+ }),
244
216
  round: roundOf(list(record(value)["events"])),
245
217
  state: text(status["state"]),
246
218
  code: text(record(status["reason"])["code"]),
@@ -249,13 +221,16 @@ function runOf(inputs, value) {
249
221
  limit: count(budget["limit"]),
250
222
  decisions: inputs.decisions
251
223
  .filter((decision) => record(decision)["runId"] === runId)
252
- .map((decision) => decisionCommands(decision, inputs.client)),
224
+ .map((decision) => decisionCommands(operationalDecision(record(decision), workflowId), inputs.client)),
253
225
  events: list(record(value)["events"]).map(eventOf),
254
226
  turns: statusTurns(inputs.turns, status),
255
227
  phases: phasesOf(list(record(value)["events"]), workflowId),
256
228
  decision: decisionOf(list(record(value)["events"])),
257
229
  feedback: list(status["feedback"]).map(feedbackOf),
258
- ...pullOf(inputs, runId),
230
+ ...pullOf({
231
+ records: inputs.githubRecords,
232
+ repository: workflowId === "develop-issue" ? (inputs.repository ?? null) : null,
233
+ }, runId),
259
234
  };
260
235
  }
261
236
  function began(run) {
@@ -270,6 +245,8 @@ export function watchModel(inputs) {
270
245
  const workflows = boardsOf({ ...inputs, completionCandidates: candidates }, runs);
271
246
  return {
272
247
  workflows,
248
+ ...(inputs.queue === undefined ? {} : { queue: inputs.queue }),
249
+ workflowRefusals: workflowRefusals(inputs.workflowRefusals),
273
250
  startWorkflowId: startWorkflowOf(workflows),
274
251
  updatedAt: inputs.updatedAt,
275
252
  service: inputs.service,
@@ -1,16 +1,19 @@
1
1
  import { cardReply } from "./watch-card-reply.js";
2
+ import { record } from "../protocol.js";
2
3
  import { watchCardClient } from "./watch-card-client.js";
3
4
  import { AddCard, CardInput, cardKey, cardSubmitKey, } from "./watch-card-view.js";
4
5
  import { watchClient } from "./watch-client.js";
5
6
  import { priceOf, tokenCount, usageOf } from "../worker-usage.js";
6
7
  import { priceText, usageText, totalsSuffix } from "./usage-render.js";
7
8
  import { startPrompts, StartCard, StartRun } from "./watch-start-view.js";
8
- import { Board, boardSizing, Card, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, } from "./watch-board-view.js";
9
+ import { Board, boardSizing, Card, HandoffLinks, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, observationText, staleObservation, } from "./watch-board-view.js";
9
10
  import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-navigation.js";
10
11
  import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
11
12
  import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
12
- import { clove, Banner, BannerDetail, BoardPage, 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";
13
14
  const functions = [
15
+ record,
16
+ Queue,
14
17
  cardReply,
15
18
  cardSubmitKey,
16
19
  cardKey,
@@ -29,8 +32,11 @@ const functions = [
29
32
  cardReview,
30
33
  cardStep,
31
34
  completionMark,
35
+ observationText,
36
+ staleObservation,
32
37
  CardReview,
33
38
  Card,
39
+ HandoffLinks,
34
40
  Column,
35
41
  columnCollapsed,
36
42
  WorkflowList,
@@ -65,6 +71,7 @@ const functions = [
65
71
  Review,
66
72
  Run,
67
73
  Header,
74
+ RefusedWorkflows,
68
75
  BoardPage,
69
76
  Page,
70
77
  ];
@@ -93,6 +100,15 @@ const dark = `
93
100
  --bad: #FF7B72; --bad-tint: rgba(255,123,114,0.14);
94
101
  `;
95
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"); }
96
112
  :root {${light}
97
113
  --heading-font: "Heebo", system-ui, sans-serif;
98
114
  --body-font: "Space Grotesk", system-ui, sans-serif;
@@ -111,6 +127,8 @@ body:has(.board-page) { padding-bottom: 0; overflow: hidden; }
111
127
  .board-page { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
112
128
  .board-page > header, .board-page > .down { flex-shrink: 0; }
113
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; }
114
132
  a { color: var(--accent-strong); }
115
133
  header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px 0 10px; color: var(--muted); font-size: 12px; }
116
134
  .brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font: 800 26px/1.2 var(--heading-font); }
@@ -142,7 +160,7 @@ h2, h3 { font-family: var(--heading-font); font-weight: 700; }
142
160
  .column.collapsed { flex: 0 0 ${String(boardSizing.collapsedColumnWidth)}px; min-width: ${String(boardSizing.collapsedColumnWidth)}px; padding: 6px; }
143
161
  .collapsed .column-name { writing-mode: vertical-rl; padding: 0; }
144
162
  .column-name button, .start-toggle { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 0; }
145
- .column-name button:hover, .start-toggle:hover { background: var(--surface); }
163
+ .column-name button:hover, .start-toggle:hover, .add-card:hover, .add-card-form button:hover { background: var(--surface); }
146
164
  .add-card, .add-card-form button { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 4px 8px 4px 0; }
147
165
  .add-card { width: 100%; text-align: left; }
148
166
  .add-card-form { padding: 8px 0; overflow-wrap: anywhere; }
@@ -249,7 +267,6 @@ export function pageDocument(actionToken = null) {
249
267
  "<!doctype html>",
250
268
  '<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Cloving</title>',
251
269
  `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(icon)}">`,
252
- '<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\'">',
253
270
  `<style>${style}</style></head><body>`,
254
271
  '<div id="app"></div>',
255
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",
@@ -1,4 +1,4 @@
1
- import { type PrIdentity, type PrObservation } from "./watch-pr-identity.ts";
1
+ import { type PrIdentity, type PrReadResult } from "./watch-pr-identity.ts";
2
2
  export interface WatchCommandOutput {
3
3
  readonly stdout: string;
4
4
  readonly stderr: string;
@@ -12,4 +12,7 @@ export interface WatchCommandRequest {
12
12
  }
13
13
  export type WatchCommandRunner = (request: WatchCommandRequest) => Promise<WatchCommandOutput>;
14
14
  export declare function prArguments(identity: PrIdentity): readonly string[];
15
- export declare function prCommandResult(identity: PrIdentity, output: WatchCommandOutput): PrObservation;
15
+ export declare function prCommandResult(identity: PrIdentity, reply: {
16
+ readonly output: WatchCommandOutput;
17
+ readonly now: number;
18
+ }): PrReadResult;
@@ -15,15 +15,52 @@ function commandFailure(output) {
15
15
  /^HTTP\/[^ ]+ (\d{3})/m.exec(output.stdout)?.[1]);
16
16
  if (status === 429 ||
17
17
  /rate limit/i.test(output.stderr) ||
18
- /x-ratelimit-remaining: 0\s*$/im.test(output.stdout))
18
+ headerValues(output.stdout, "x-ratelimit-remaining").includes("0"))
19
19
  return "rate-limit";
20
20
  if (output.status === 4 || [401, 403, 404].includes(status))
21
21
  return "access";
22
22
  return "command";
23
23
  }
24
- export function prCommandResult(identity, output) {
25
- if (output.status !== 0 || output.errorCode !== null)
26
- return { status: "unavailable", failure: commandFailure(output) };
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
+ }
27
64
  const envelope = /^HTTP\/[^ ]+ 200[^\n]*\r?\n[\s\S]*?\r?\n\r?\n([\s\S]*)$/.exec(output.stdout);
28
65
  if (envelope?.[1] === undefined)
29
66
  return { status: "unavailable", failure: "response" };
@@ -5,14 +5,26 @@ export interface PrIdentity {
5
5
  readonly number: number;
6
6
  }
7
7
  export type PrFailure = "configuration" | "identity" | "command" | "access" | "rate-limit" | "response" | "timeout";
8
- export type PrObservation = {
8
+ export type PrReadResult = {
9
9
  readonly status: "merged" | "unmerged";
10
10
  } | {
11
11
  readonly status: "unavailable";
12
12
  readonly failure: PrFailure;
13
+ readonly retryAt?: number;
13
14
  };
14
- export declare function prResponse(identity: PrIdentity, value: unknown): PrObservation;
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;
15
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;
16
28
  export declare function prRepository(origin: string): PrRepository | null;
17
29
  export declare function prIdentity(repository: PrRepository, pullRequest: {
18
30
  readonly number: number;
@@ -31,6 +31,31 @@ export function prResponse(identity, value) {
31
31
  return { status: "unavailable", failure: "response" };
32
32
  return { status: merged ? "merged" : "unmerged" };
33
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
+ }
34
59
  export function prRepository(origin) {
35
60
  const address = origin
36
61
  .replace(/^git@([^:/]+):/, "ssh://git@$1/")
@@ -2,6 +2,7 @@ import type { WatchPullRequest } from "./watch-render.ts";
2
2
  import { type WatchCommandRunner } from "./watch-pr-command.ts";
3
3
  import { type PrObservation, type PrRepository } from "./watch-pr-identity.ts";
4
4
  export interface PrObserver {
5
+ readonly repository?: PrRepository | null;
5
6
  readonly observe: (pullRequest: WatchPullRequest) => PrObservation | null;
6
7
  readonly close: () => void;
7
8
  }
@@ -9,13 +9,23 @@ const systemClock = {
9
9
  };
10
10
  },
11
11
  };
12
+ function observationOf(entry, reply) {
13
+ const { result, now } = reply;
14
+ if (result.status !== "unavailable")
15
+ return { ...result, lastSuccess: { status: result.status, readAt: now } };
16
+ const lastSuccess = entry.result?.lastSuccess;
17
+ return { ...result, ...(lastSuccess === undefined ? {} : { lastSuccess }) };
18
+ }
12
19
  async function readPr(state, attempt) {
13
20
  const { entry, controller } = attempt;
14
21
  const deadline = { expired: false };
15
22
  const timeout = { status: "unavailable", failure: "timeout" };
16
23
  const cancel = state.clock.after(() => {
17
24
  deadline.expired = true;
18
- entry.result = timeout;
25
+ entry.result = observationOf(entry, {
26
+ result: timeout,
27
+ now: state.clock.now(),
28
+ });
19
29
  controller.abort();
20
30
  }, 5000);
21
31
  controller.signal.addEventListener("abort", cancel, { once: true });
@@ -28,7 +38,9 @@ async function readPr(state, attempt) {
28
38
  directory: state.settings.directory,
29
39
  signal: controller.signal,
30
40
  }));
31
- return deadline.expired ? timeout : prCommandResult(entry.identity, output);
41
+ return deadline.expired
42
+ ? timeout
43
+ : prCommandResult(entry.identity, { output, now: state.clock.now() });
32
44
  }
33
45
  catch {
34
46
  return deadline.expired
@@ -43,14 +55,32 @@ async function readPr(state, attempt) {
43
55
  function pump(state) {
44
56
  if (state.closed || state.active !== null)
45
57
  return;
46
- const entry = state.queue.shift();
58
+ state.wake?.();
59
+ state.wake = null;
60
+ if (state.queue.length === 0)
61
+ return;
62
+ const now = state.clock.now();
63
+ const position = state.queue.findIndex((entry) => (state.retryDeadlines.get(entry.identity.host) ?? 0) <= now);
64
+ if (position === -1) {
65
+ const next = Math.min(...state.queue.map((entry) => state.retryDeadlines.get(entry.identity.host) ?? now));
66
+ state.wake = state.clock.after(() => {
67
+ state.wake = null;
68
+ pump(state);
69
+ }, Math.min(next - now, 2147483647));
70
+ return;
71
+ }
72
+ const [entry] = state.queue.splice(position, 1);
47
73
  if (entry === undefined)
48
74
  return;
49
75
  const controller = new AbortController();
50
76
  state.active = controller;
51
77
  entry.started = state.clock.now();
52
78
  const completed = (result) => {
53
- entry.result = result;
79
+ if (state.closed)
80
+ return;
81
+ entry.result = observationOf(entry, { result, now: state.clock.now() });
82
+ if (result.status === "unavailable" && result.failure === "rate-limit")
83
+ state.retryDeadlines.set(entry.identity.host, Math.max(state.retryDeadlines.get(entry.identity.host) ?? 0, result.retryAt ?? state.clock.now() + 60000));
54
84
  entry.pending = false;
55
85
  state.active = null;
56
86
  pump(state);
@@ -59,6 +89,11 @@ function pump(state) {
59
89
  completed({ status: "unavailable", failure: "command" });
60
90
  });
61
91
  }
92
+ function currentObservation(state, entry) {
93
+ const result = entry.result ?? { status: "pending" };
94
+ const retryAt = state.retryDeadlines.get(entry.identity.host) ?? 0;
95
+ return retryAt > state.clock.now() ? { ...result, retryAt } : result;
96
+ }
62
97
  function needsRead(entry, now) {
63
98
  if (entry === undefined)
64
99
  return true;
@@ -72,6 +107,8 @@ export function prObserver(settings) {
72
107
  clock: settings.clock ?? systemClock,
73
108
  cache: new Map(),
74
109
  queue: [],
110
+ retryDeadlines: new Map(),
111
+ wake: null,
75
112
  active: null,
76
113
  closed: false,
77
114
  };
@@ -85,7 +122,9 @@ export function prObserver(settings) {
85
122
  const key = JSON.stringify(identity);
86
123
  const known = state.cache.get(key);
87
124
  if (state.closed || !needsRead(known, state.clock.now()))
88
- return known?.result ?? null;
125
+ return known === undefined
126
+ ? { status: "pending" }
127
+ : currentObservation(state, known);
89
128
  const entry = {
90
129
  identity,
91
130
  result: known?.result ?? null,
@@ -95,13 +134,16 @@ export function prObserver(settings) {
95
134
  state.cache.set(key, entry);
96
135
  state.queue.push(entry);
97
136
  pump(state);
98
- return entry.result;
137
+ return currentObservation(state, entry);
99
138
  };
100
139
  return {
140
+ repository: settings.repository,
101
141
  observe,
102
142
  close: () => {
103
143
  state.closed = true;
104
144
  state.queue.length = 0;
145
+ state.wake?.();
146
+ state.wake = null;
105
147
  state.active?.abort();
106
148
  },
107
149
  };
@@ -0,0 +1,10 @@
1
+ import type { WatchRun } from "./watch-render.ts";
2
+ import { type PrRepository } from "./watch-pr-identity.ts";
3
+ export interface GithubRecords {
4
+ readonly started: unknown;
5
+ readonly result: unknown;
6
+ }
7
+ export declare function pullOf(inputs: {
8
+ readonly records: readonly GithubRecords[];
9
+ readonly repository: PrRepository | null;
10
+ }, runId: string): Pick<WatchRun, "pullRequest" | "reviews" | "mergeCommit" | "mergeProof">;
@@ -0,0 +1,130 @@
1
+ import { operationIdentity } from "../operation-identity.js";
2
+ import { prIdentity, } from "./watch-pr-identity.js";
3
+ function record(value) {
4
+ return typeof value === "object" && value !== null ? { ...value } : {};
5
+ }
6
+ function text(value) {
7
+ return typeof value === "string" ? value : "";
8
+ }
9
+ function stamp(value) {
10
+ return typeof value === "string" ? value : null;
11
+ }
12
+ function parsed(line) {
13
+ try {
14
+ return JSON.parse(line);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ function list(value) {
21
+ return Array.isArray(value) ? value.map((item) => item) : [];
22
+ }
23
+ function count(value) {
24
+ return typeof value === "number" ? value : null;
25
+ }
26
+ function attributed(started, operation) {
27
+ const stage = list(operation[5]);
28
+ const checked = operationIdentity({
29
+ round: operation[1],
30
+ run: operation[2],
31
+ phase: operation[3],
32
+ operation: operation[4],
33
+ ...(operation[5] === null
34
+ ? {}
35
+ : { stage: { id: stage[0], visit: stage[1], branch: stage[2] } }),
36
+ ...(operation[6] === null ? {} : { key: operation[6] }),
37
+ });
38
+ return (checked.kind === "valid" &&
39
+ checked.value === JSON.stringify(operation) &&
40
+ operation[3] === "pull-request" &&
41
+ operation[4] === `github-${text(started["method"])}` &&
42
+ (started["runId"] === undefined || started["runId"] === operation[2]));
43
+ }
44
+ function githubOf(records) {
45
+ const started = record(records.started);
46
+ const operation = list(parsed(text(started["operationId"])));
47
+ const result = record(records.result);
48
+ const completed = result["outcome"] === "completed";
49
+ return {
50
+ runId: text(operation[2]),
51
+ round: Number(operation[1]),
52
+ method: text(started["method"]),
53
+ completed,
54
+ attributed: attributed(started, operation) && !Object.hasOwn(result, "reason"),
55
+ report: completed ? record(parsed(text(result["result"]))) : {},
56
+ };
57
+ }
58
+ function pullRequestOf(results) {
59
+ const opened = results
60
+ .filter((item) => item.method === "open-pr")
61
+ .map((item) => item.report)
62
+ .findLast((report) => text(report["url"]).startsWith("https://"));
63
+ if (opened === undefined)
64
+ return null;
65
+ return { number: Number(opened["number"]), url: text(opened["url"]) };
66
+ }
67
+ function reviewsOf(results) {
68
+ return results
69
+ .filter((item) => item.method === "wait-review")
70
+ .flatMap(({ round, report }) => {
71
+ const score = count(report["score"]);
72
+ if (score === null)
73
+ return [];
74
+ return [{ round, commit: text(report["commit"]), score }];
75
+ });
76
+ }
77
+ function mergeCommitOf(results) {
78
+ const merged = results
79
+ .filter((item) => item.method === "merge")
80
+ .map((item) => stamp(item.report["merged"]));
81
+ return merged.find((commit) => commit !== null) ?? null;
82
+ }
83
+ function mergeProofOf(results, context) {
84
+ if (context.repository === null || context.pullRequest === null)
85
+ return null;
86
+ const repository = context.repository;
87
+ const identity = prIdentity(repository, context.pullRequest);
88
+ if (identity === null)
89
+ return null;
90
+ const opened = results.filter((item) => item.method === "open-pr" && item.completed && item.attributed);
91
+ if (opened.length === 0 ||
92
+ opened.some(({ report }) => report["number"] !== identity.number ||
93
+ typeof report["url"] !== "string" ||
94
+ prIdentity(repository, {
95
+ number: identity.number,
96
+ url: report["url"],
97
+ }) === null))
98
+ return null;
99
+ const merged = results.filter((item) => item.method === "merge" &&
100
+ item.completed &&
101
+ item.attributed &&
102
+ (typeof item.report["reason"] !== "string" ||
103
+ Object.hasOwn(item.report, "merged")));
104
+ if (merged.length === 0 ||
105
+ merged.some(({ report }) => report["number"] !== identity.number ||
106
+ typeof report["merged"] !== "string" ||
107
+ report["merged"].trim() === "" ||
108
+ Object.hasOwn(report, "reason")))
109
+ return null;
110
+ return new Set(merged.map(({ report }) => report["merged"])).size === 1
111
+ ? identity
112
+ : null;
113
+ }
114
+ export function pullOf(inputs, runId) {
115
+ const results = inputs.records
116
+ .map(githubOf)
117
+ .filter((item) => item.runId === runId)
118
+ .toSorted((first, second) => first.round - second.round);
119
+ const pullRequest = pullRequestOf(results);
120
+ const mergeProof = mergeProofOf(results, {
121
+ repository: inputs.repository,
122
+ pullRequest,
123
+ });
124
+ return {
125
+ pullRequest,
126
+ reviews: reviewsOf(results),
127
+ mergeCommit: mergeCommitOf(results),
128
+ ...(mergeProof === null ? {} : { mergeProof }),
129
+ };
130
+ }