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,5 +1,6 @@
1
1
  import { priceText, usageText, totalsSuffix } from "./usage-render.js";
2
2
  import { record } from "../protocol.js";
3
+ import { workflowRefusals } from "../workflow-refusals.js";
3
4
  function show(value) {
4
5
  return typeof value === "string" ? value : JSON.stringify(value);
5
6
  }
@@ -57,7 +58,15 @@ function admitted(result) {
57
58
  ];
58
59
  }
59
60
  function runLine(run) {
60
- return ` ${show(part(run, "runId"))} ${show(part(run, "workflowId"))} ${show(part(run, "condition"))} revision ${show(part(run, "revision"))} latest at ${show(part(run, "latestAt") ?? "none")}`;
61
+ const line = ` ${show(part(run, "runId"))} ${show(part(run, "workflowId"))} ${show(part(run, "condition"))} revision ${show(part(run, "revision"))} latest at ${show(part(run, "latestAt") ?? "none")}`;
62
+ return [line, ...chainLines(run).map((link) => ` ${link}`)].join("\n");
63
+ }
64
+ function chainLines(run) {
65
+ return [
66
+ `Parent run: ${visible(part(run, "parentRunId") ?? "none")}`,
67
+ `Chain: ${visible(part(run, "chainId") ?? part(run, "runId") ?? "none")}`,
68
+ `Child runs: ${visible(listing(part(run, "childRunIds")))}`,
69
+ ];
61
70
  }
62
71
  function ended(run) {
63
72
  return ["completed", "failed", "cancelled"].includes(show(run["state"]));
@@ -82,8 +91,16 @@ function runList(result) {
82
91
  const runs = result["runs"];
83
92
  const lines = Array.isArray(runs) ? runs.map(runLine) : [];
84
93
  const hiddenRuns = result["hiddenRuns"];
94
+ const refusals = workflowRefusals(result["workflowRefusals"]);
85
95
  return [
86
96
  `Workflows: ${visible(listing(result["workflows"]))}`,
97
+ ...queueLines(result["queue"]),
98
+ ...(refusals.length === 0
99
+ ? []
100
+ : [
101
+ "Refused workflows:",
102
+ ...refusals.map((entry) => ` ${entry.scope} ${entry.label}: ${entry.codes.join(", ")}`),
103
+ ]),
87
104
  "Runs:",
88
105
  ...(lines.length === 0 ? [" none"] : lines),
89
106
  ...(typeof hiddenRuns === "number" && hiddenRuns > 0
@@ -93,6 +110,24 @@ function runList(result) {
93
110
  : []),
94
111
  ];
95
112
  }
113
+ export function queueLines(value) {
114
+ const queue = record(value);
115
+ if (queue === undefined)
116
+ return [];
117
+ const skipped = queue["skipped"];
118
+ return [
119
+ `Queue: ${visible(queue["busy"])} of ${visible(queue["lanes"])} lanes busy`,
120
+ ...(Array.isArray(skipped)
121
+ ? skipped.map((item) => {
122
+ const dependencies = part(item, "dependencies");
123
+ const needs = Array.isArray(dependencies)
124
+ ? `; needs ${dependencies.map(visible).join(", ")}`
125
+ : "";
126
+ return `Skipped ticket ${visible(part(item, "ticketId"))}: ${visible(part(item, "reason"))}${needs}`;
127
+ })
128
+ : []),
129
+ ];
130
+ }
96
131
  function observation(value) {
97
132
  if (record(value) === undefined)
98
133
  return "none";
@@ -101,12 +136,23 @@ function observation(value) {
101
136
  function feedbackLines(value) {
102
137
  return (Array.isArray(value) ? value : []).map((item) => `Feedback of ${show(part(item, "decisionId"))}: ${visible(part(item, "text"))}`);
103
138
  }
104
- function outcome(value) {
139
+ function outcomeLines(value) {
105
140
  if (record(value) === undefined)
106
- return "none";
141
+ return ["Outcome: none"];
107
142
  const reason = part(value, "reason");
108
143
  const kind = visible(part(value, "kind"));
109
- return typeof reason === "string" ? `${kind}: ${visible(reason)}` : kind;
144
+ return [
145
+ `Outcome: ${typeof reason === "string" ? `${kind}: ${visible(reason)}` : kind}`,
146
+ ...(typeof part(value, "promotionId") === "string"
147
+ ? [`Promotion: ${visible(part(value, "promotionId"))}`]
148
+ : []),
149
+ ...(typeof part(value, "ticketStatus") === "string"
150
+ ? [`Ticket status: ${visible(part(value, "ticketStatus"))}`]
151
+ : []),
152
+ ...(part(value, "issueUpdate") === "failed"
153
+ ? ["Issue status update failed; check the GitHub operation."]
154
+ : []),
155
+ ];
110
156
  }
111
157
  function workerLines(value) {
112
158
  return (Array.isArray(value) ? value : []).flatMap((turn) => [
@@ -140,6 +186,13 @@ export function statusJson(result) {
140
186
  },
141
187
  };
142
188
  }
189
+ function inputText(value) {
190
+ const text = visible(record(value) ?? null);
191
+ const characters = Array.from(text);
192
+ return characters.length > 200
193
+ ? `${characters.slice(0, 200).join("")}…`
194
+ : text;
195
+ }
143
196
  export function runStatus(result) {
144
197
  const run = result["run"];
145
198
  const reason = part(run, "reason");
@@ -150,17 +203,10 @@ export function runStatus(result) {
150
203
  `Run ${show(part(run, "runId"))} (${show(part(run, "workflowId"))})`,
151
204
  `Title: ${visible(part(run, "title") ?? "none")}`,
152
205
  `State: ${show(part(run, "state"))}`,
153
- `Outcome: ${outcome(part(run, "outcome"))}`,
154
- ...(typeof part(part(run, "outcome"), "ticketStatus") === "string"
155
- ? [
156
- `Ticket status: ${visible(part(part(run, "outcome"), "ticketStatus"))}`,
157
- ]
158
- : []),
159
- ...(part(part(run, "outcome"), "issueUpdate") === "failed"
160
- ? ["Issue status update failed; check the GitHub operation."]
161
- : []),
206
+ ...outcomeLines(part(run, "outcome")),
162
207
  `Condition: ${show(part(run, "condition"))}`,
163
208
  `Reason: ${show(part(reason, "code"))}: ${show(part(reason, "detail"))}`,
209
+ `Input: ${inputText(part(run, "input"))}`,
164
210
  `Revision: ${show(part(run, "revision"))}, latest sequence ${show(part(run, "latestSequence"))}`,
165
211
  `Latest at: ${show(part(run, "latestAt") ?? "none")}`,
166
212
  `Pending decisions: ${listing(part(run, "pendingDecisions"))}`,
@@ -173,6 +219,7 @@ export function runStatus(result) {
173
219
  `Next action: ${show(part(run, "nextAction") ?? "none")}${at === null ? "" : ` at ${show(at)}`}`,
174
220
  `Human input can help: ${part(run, "humanInputCanHelp") === true ? "yes" : "no"}`,
175
221
  ...feedbackLines(part(run, "feedback")),
222
+ ...chainLines(run),
176
223
  ];
177
224
  }
178
225
  function binding(decision) {
@@ -65,13 +65,15 @@ The command reads these settings from the environment:
65
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.
66
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.
67
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.
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. 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 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.
70
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.
71
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.
72
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.
73
74
  CLOVING_PROFILES The path of a JSON file of operator-defined Claude turn profiles, read once at start. The setting has no default.
74
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.
75
77
  CLOVING_WORKER The worker for each turn: claude or codex. The default is claude. Other values stop the start with invalid-worker.
76
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.
77
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.
@@ -81,6 +83,7 @@ The command reads these settings from the environment:
81
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.
82
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.
83
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.
84
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.
85
88
  `,
86
89
  };
@@ -21,11 +21,20 @@ export declare function CardReview(props: {
21
21
  readonly card: WatchCard;
22
22
  readonly workflowId: string;
23
23
  }): Shown;
24
- 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;
25
30
  export declare function Card(props: {
26
31
  readonly card: WatchCard;
27
32
  readonly workflowId: string;
28
33
  readonly now: string;
34
+ readonly observationNow?: string | undefined;
35
+ }): Shown;
36
+ export declare function HandoffLinks(props: {
37
+ readonly run: Pick<WatchCard, "parentRunId" | "childRunIds">;
29
38
  }): Shown;
30
39
  export declare function WorkflowList(props: {
31
40
  readonly workflows: readonly WatchWorkflow[];
@@ -43,6 +52,7 @@ export declare function Column(props: {
43
52
  readonly addCard?: Shown;
44
53
  readonly workflowId: string;
45
54
  readonly now: string;
55
+ readonly observationNow?: string | undefined;
46
56
  readonly controls?: ColumnControls | undefined;
47
57
  }): Shown;
48
58
  export declare function Board(props: {
@@ -50,5 +60,6 @@ export declare function Board(props: {
50
60
  readonly cardForm?: CardForm | undefined;
51
61
  readonly cards?: CardControls | undefined;
52
62
  readonly now: string;
63
+ readonly observationNow?: string | undefined;
53
64
  readonly controls?: ColumnControls | undefined;
54
65
  }): Shown;
@@ -55,21 +55,45 @@ export function CardReview(props) {
55
55
  ]),
56
56
  });
57
57
  }
58
- 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) {
59
83
  const mark = card.completionMark;
60
84
  if (mark === null)
61
85
  return null;
62
- let text = mark.text;
63
- const status = card.pullRequestObservation?.status;
64
- if (text === "Needs attention" && status === "merged")
65
- text = `PR merged #${String(card.pullRequestNumber)}`;
66
- if (text === "Needs attention" && status === "unavailable")
67
- text = "PR status unavailable";
86
+ const text = observationText(card, mark.text);
68
87
  if (mark.laterRunId === null)
69
- return element("span", { class: "completion-mark", children: text });
88
+ return [
89
+ element("span", { class: "completion-mark", children: text }),
90
+ mark.text === "Needs attention"
91
+ ? staleObservation(card, context.now)
92
+ : null,
93
+ ];
70
94
  return element("a", {
71
95
  class: "completion-mark",
72
- href: watchHash({ workflowId, runId: mark.laterRunId }),
96
+ href: watchHash({ workflowId: context.workflowId, runId: mark.laterRunId }),
73
97
  children: mark.text,
74
98
  });
75
99
  }
@@ -106,10 +130,25 @@ export function Card(props) {
106
130
  element("small", { children: card.runId.slice(0, 12) }),
107
131
  ],
108
132
  }),
109
- completionMark(card, workflowId),
133
+ completionMark(card, { workflowId, now: props.observationNow ?? now }),
134
+ component(HandoffLinks, { run: card }),
110
135
  ],
111
136
  });
112
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
+ }
113
152
  export function WorkflowList(props) {
114
153
  return element("nav", {
115
154
  class: "workflows",
@@ -167,7 +206,13 @@ export function Column(props) {
167
206
  : element("div", {
168
207
  class: "column-cards",
169
208
  children: [
170
- column.cards.map((card) => component(Card, { key: card.runId, card, workflowId, now })),
209
+ column.cards.map((card) => component(Card, {
210
+ key: card.runId,
211
+ card,
212
+ workflowId,
213
+ now,
214
+ observationNow: props.observationNow,
215
+ })),
171
216
  props.addCard,
172
217
  ],
173
218
  }),
@@ -187,6 +232,7 @@ export function Board(props) {
187
232
  column,
188
233
  workflowId: workflow.workflowId,
189
234
  now,
235
+ observationNow: props.observationNow,
190
236
  controls: props.controls,
191
237
  addCard: index === 0
192
238
  ? component(AddCard, {
@@ -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,11 +14,15 @@ 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" &&
@@ -32,7 +38,7 @@ export function completionMarkOf(run, candidates) {
32
38
  (first.runId < second.runId ? -1 : 1))[0];
33
39
  return later === undefined
34
40
  ? { text: "Needs attention", laterRunId: null }
35
- : { text: "Done in a later run", laterRunId: later.runId };
41
+ : { text: "Ticket completed by later run", laterRunId: later.runId };
36
42
  }
37
43
  export function activeRun(state) {
38
44
  return !terminalStates.includes(state);
@@ -41,10 +47,17 @@ export function recentRun(latestAt, now) {
41
47
  const age = Date.parse(now) - Date.parse(latestAt ?? "");
42
48
  return age >= 0 && age <= weekMilliseconds;
43
49
  }
44
- 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"];
45
58
  return workflowId === "develop-issue"
46
59
  ? issueSteps.map((step) => step.name)
47
- : ["Claude turn", "Your review"];
60
+ : ["Agent turn", "Your review"];
48
61
  }
49
62
  function operationOf(event) {
50
63
  const payload = record(record(event)?.["payload"]);
@@ -63,7 +76,7 @@ function stepName(event, workflowId) {
63
76
  if (type !== "effect.requested")
64
77
  return undefined;
65
78
  if (workflowId !== "develop-issue")
66
- return "Claude turn";
79
+ return "Agent turn";
67
80
  return issueSteps.find((step) => step.operation === operationOf(event)[4])
68
81
  ?.name;
69
82
  }
@@ -74,10 +87,24 @@ function startedAt(event) {
74
87
  function chronological(events) {
75
88
  return events.toSorted((first, second) => Date.parse(startedAt(first) ?? "") - Date.parse(startedAt(second) ?? ""));
76
89
  }
77
- 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);
78
105
  const event = chronological(events).findLast((item) => stepName(item, workflowId) !== undefined);
79
106
  return {
80
- name: stepName(event, workflowId) ?? stepNames(workflowId)[0] ?? "Claude turn",
107
+ name: stepName(event, workflowId) ?? stepNames(workflowId)[0] ?? "Agent turn",
81
108
  startedAt: startedAt(event),
82
109
  };
83
110
  }
@@ -101,6 +128,7 @@ function cardOf(run, runs) {
101
128
  return {
102
129
  runId: run.runId,
103
130
  title: run.title,
131
+ ...linksOf(run),
104
132
  completionMark: completionMarkOf(run, runs),
105
133
  pullRequestObservation: null,
106
134
  claudeCostUsd: run.claudeCostUsd,
@@ -149,7 +177,11 @@ export function boardsOf(inputs, runs) {
149
177
  : inputs.summaries
150
178
  .filter((value) => record(value)?.["workflowId"] === workflowId)
151
179
  .map(summaryOf);
152
- 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) => {
153
185
  const cards = own
154
186
  .filter((run) => columnOf(run, inputs.updatedAt) === name)
155
187
  .map((run) => cardOf(run, inputs.completionCandidates ?? runs));
@@ -1 +1 @@
1
- export declare const watchClient = "\nconst state = { model: null, stale: false, copied: \"\", label: \"\", run: \"\", loading: false, again: false, columns: {}, cards: {}, start: null, hovered: null, focused: null };\nfunction readPreference(key) {\n try { return JSON.parse(localStorage.getItem(key)); }\n catch { return null; }\n}\nfunction readColumnChoices(key) {\n const value = readPreference(key);\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return null;\n return Object.fromEntries(Object.entries(value).filter(([, choice]) => typeof choice === \"boolean\"));\n}\nfunction savePreference(key, value) {\n try { localStorage.setItem(key, JSON.stringify(value)); }\n catch {}\n}\nfunction startControls() {\n if (state.start === null) state.start = readPreference(\"cloving.watch.start\") === true;\n return { shown: state.start, onToggle() {\n state.start = !state.start;\n savePreference(\"cloving.watch.start\", state.start);\n draw();\n } };\n}\nfunction columnControls(model, selected) {\n const workflowId = model === null ? null : boardWorkflow(model, watchLocation(selected).workflowId);\n if (workflowId === null) return undefined;\n const key = \"cloving.watch.columns:\" + workflowId;\n if (!Object.hasOwn(state.columns, workflowId))\n state.columns = { ...state.columns, [workflowId]: readColumnChoices(key) };\n const choices = state.columns[workflowId];\n return { choices, onHover(name) { state.hovered = name; }, onFocus(name) { state.focused = name; }, onToggle(name) {\n const changed = { ...choices, [name]: !columnCollapsed(choices, name) };\n state.columns = { ...state.columns, [workflowId]: changed };\n savePreference(key, changed);\n draw();\n } };\n}\nfunction moved(next, followed) {\n const arrived = next === \"waiting_gate\" && next !== state.run;\n state.run = next;\n if (arrived) scrollTo(0, 0);\n else if (followed && next === \"running\") scrollTo(0, document.body.scrollHeight);\n}\nfunction copy(command) {\n const shown = (label) => {\n state.copied = command;\n state.label = label;\n draw();\n };\n navigator.clipboard.writeText(command).then(\n () => { shown(\"Copied\"); },\n () => { shown(\"Copy failed\"); },\n );\n}\nfunction draw() {\n const { model, stale } = state;\n const selected = location.hash.slice(1);\n const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;\n const copying = { command: state.copied, label: state.label, onCopy: copy };\n const now = new Date().toISOString();\n const columns = columnControls(model, selected);\n const start = model === null ? undefined : startControls();\n const cards = cardControls(model, model === null ? null : boardWorkflow(model, watchLocation(selected).workflowId));\n const page = component(Page, { model, selected, now, stale, copying, columns, start, cards });\n render(page, document.getElementById(\"app\"));\n if (model === null) return;\n document.title = watchTitle(model, selected);\n const { runId } = watchLocation(selected);\n moved(model.runs.find((run) => run.runId === runId)?.state ?? \"\", followed);\n}\nasync function withSelectedRun(model) {\n const { runId } = watchLocation(location.hash);\n if (runId === null || model.runs.some((run) => run.runId === runId)) return model;\n const detail = await fetch(\"data.json?runId=\" + encodeURIComponent(runId), { cache: \"no-store\" });\n if (detail.status === 404) return model;\n if (!detail.ok) throw new Error(\"watch-run-not-available\");\n const { run } = await detail.json();\n if (run?.runId !== runId) throw new Error(\"watch-run-mismatch\");\n return { ...model, runs: [...model.runs, run] };\n}\nasync function load() {\n state.again = state.loading;\n if (state.loading) return;\n state.loading = true;\n try {\n const reply = await fetch(\"data.json\", { cache: \"no-store\" });\n if (!reply.ok) throw new Error(\"watch-not-available\");\n state.model = await withSelectedRun(await reply.json());\n state.stale = false;\n } catch {\n state.stale = true;\n }\n state.loading = false;\n draw();\n if (state.again) load();\n}\nfunction listen() {\n const source = new EventSource(\"events\");\n source.onmessage = () => { load(); };\n source.onerror = () => { state.stale = true; draw(); };\n}\naddEventListener(\"keydown\", (event) => {\n if (event.key !== String.fromCharCode(92) || event.defaultPrevented || event.repeat || event.isComposing) return;\n if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;\n if (event.target?.closest?.(\"input, textarea, select, [contenteditable]\")) return;\n const target = state.hovered ?? state.focused;\n if (target === null || watchLocation(location.hash).runId !== null) return;\n const controls = columnControls(state.model, location.hash);\n if (controls === undefined) return;\n event.preventDefault();\n controls.onToggle(target);\n});\naddEventListener(\"hashchange\", () => { state.run = \"\"; state.hovered = null; state.focused = null; draw(); load(); });\ndraw();\nlisten();\nsetInterval(draw, 1000);\nsetInterval(load, 15000);\n";
1
+ export declare const watchClient = "\nconst state = { model: null, receivedAt: 0, stale: false, copied: \"\", label: \"\", run: \"\", loading: false, again: false, columns: {}, cards: {}, start: null, hovered: null, focused: null };\nfunction readPreference(key) {\n try { return JSON.parse(localStorage.getItem(key)); }\n catch { return null; }\n}\nfunction readColumnChoices(key) {\n const value = readPreference(key);\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return null;\n return Object.fromEntries(Object.entries(value).filter(([, choice]) => typeof choice === \"boolean\"));\n}\nfunction savePreference(key, value) {\n try { localStorage.setItem(key, JSON.stringify(value)); }\n catch {}\n}\nfunction startControls() {\n if (state.start === null) state.start = readPreference(\"cloving.watch.start\") === true;\n return { shown: state.start, onToggle() {\n state.start = !state.start;\n savePreference(\"cloving.watch.start\", state.start);\n draw();\n } };\n}\nfunction columnControls(model, selected) {\n const workflowId = model === null ? null : boardWorkflow(model, watchLocation(selected).workflowId);\n if (workflowId === null) return undefined;\n const key = \"cloving.watch.columns:\" + workflowId;\n if (!Object.hasOwn(state.columns, workflowId))\n state.columns = { ...state.columns, [workflowId]: readColumnChoices(key) };\n const choices = state.columns[workflowId];\n return { choices, onHover(name) { state.hovered = name; }, onFocus(name) { state.focused = name; }, onToggle(name) {\n const changed = { ...choices, [name]: !columnCollapsed(choices, name) };\n state.columns = { ...state.columns, [workflowId]: changed };\n savePreference(key, changed);\n draw();\n } };\n}\nfunction moved(next, followed) {\n const arrived = next === \"waiting_gate\" && next !== state.run;\n state.run = next;\n if (arrived) scrollTo(0, 0);\n else if (followed && next === \"running\") scrollTo(0, document.body.scrollHeight);\n}\nfunction copy(command) {\n const shown = (label) => {\n state.copied = command;\n state.label = label;\n draw();\n };\n navigator.clipboard.writeText(command).then(\n () => { shown(\"Copied\"); },\n () => { shown(\"Copy failed\"); },\n );\n}\nfunction draw() {\n const { model, stale } = state;\n const selected = location.hash.slice(1);\n const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;\n const copying = { command: state.copied, label: state.label, onCopy: copy };\n const now = new Date().toISOString();\n const observationNow = model === null ? now : new Date(Date.parse(model.updatedAt) + performance.now() - state.receivedAt).toISOString();\n const columns = columnControls(model, selected);\n const start = model === null ? undefined : startControls();\n const cards = cardControls(model, model === null ? null : boardWorkflow(model, watchLocation(selected).workflowId));\n const page = component(Page, { model, selected, now, observationNow, stale, copying, columns, start, cards });\n render(page, document.getElementById(\"app\"));\n if (model === null) return;\n document.title = watchTitle(model, selected);\n const { runId } = watchLocation(selected);\n moved(model.runs.find((run) => run.runId === runId)?.state ?? \"\", followed);\n}\nasync function withSelectedRun(model) {\n const { runId } = watchLocation(location.hash);\n if (runId === null || model.runs.some((run) => run.runId === runId)) return model;\n const detail = await fetch(\"data.json?runId=\" + encodeURIComponent(runId), { cache: \"no-store\" });\n if (detail.status === 404) return model;\n if (!detail.ok) throw new Error(\"watch-run-not-available\");\n const { run } = await detail.json();\n if (run?.runId !== runId) throw new Error(\"watch-run-mismatch\");\n return { ...model, runs: [...model.runs, run] };\n}\nasync function load() {\n state.again = state.loading;\n if (state.loading) return;\n state.loading = true;\n try {\n const reply = await fetch(\"data.json\", { cache: \"no-store\" });\n if (!reply.ok) throw new Error(\"watch-not-available\");\n const model = await reply.json();\n const receivedAt = performance.now();\n state.model = await withSelectedRun(model);\n state.receivedAt = receivedAt;\n state.stale = false;\n } catch {\n state.stale = true;\n }\n state.loading = false;\n draw();\n if (state.again) load();\n}\nfunction listen() {\n const source = new EventSource(\"events\");\n source.onmessage = () => { load(); };\n source.onerror = () => { state.stale = true; draw(); };\n}\naddEventListener(\"keydown\", (event) => {\n if (event.key !== String.fromCharCode(92) || event.defaultPrevented || event.repeat || event.isComposing) return;\n if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;\n if (event.target?.closest?.(\"input, textarea, select, [contenteditable]\")) return;\n const target = state.hovered ?? state.focused;\n if (target === null || watchLocation(location.hash).runId !== null) return;\n const controls = columnControls(state.model, location.hash);\n if (controls === undefined) return;\n event.preventDefault();\n controls.onToggle(target);\n});\naddEventListener(\"hashchange\", () => { state.run = \"\"; state.hovered = null; state.focused = null; draw(); load(); });\ndraw();\nlisten();\nsetInterval(draw, 1000);\nsetInterval(load, 15000);\n";
@@ -1,5 +1,5 @@
1
1
  export const watchClient = `
2
- const state = { model: null, stale: false, copied: "", label: "", run: "", loading: false, again: false, columns: {}, cards: {}, start: null, hovered: null, focused: null };
2
+ const state = { model: null, receivedAt: 0, stale: false, copied: "", label: "", run: "", loading: false, again: false, columns: {}, cards: {}, start: null, hovered: null, focused: null };
3
3
  function readPreference(key) {
4
4
  try { return JSON.parse(localStorage.getItem(key)); }
5
5
  catch { return null; }
@@ -58,10 +58,11 @@ function draw() {
58
58
  const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;
59
59
  const copying = { command: state.copied, label: state.label, onCopy: copy };
60
60
  const now = new Date().toISOString();
61
+ const observationNow = model === null ? now : new Date(Date.parse(model.updatedAt) + performance.now() - state.receivedAt).toISOString();
61
62
  const columns = columnControls(model, selected);
62
63
  const start = model === null ? undefined : startControls();
63
64
  const cards = cardControls(model, model === null ? null : boardWorkflow(model, watchLocation(selected).workflowId));
64
- const page = component(Page, { model, selected, now, stale, copying, columns, start, cards });
65
+ const page = component(Page, { model, selected, now, observationNow, stale, copying, columns, start, cards });
65
66
  render(page, document.getElementById("app"));
66
67
  if (model === null) return;
67
68
  document.title = watchTitle(model, selected);
@@ -85,7 +86,10 @@ async function load() {
85
86
  try {
86
87
  const reply = await fetch("data.json", { cache: "no-store" });
87
88
  if (!reply.ok) throw new Error("watch-not-available");
88
- state.model = await withSelectedRun(await reply.json());
89
+ const model = await reply.json();
90
+ const receivedAt = performance.now();
91
+ state.model = await withSelectedRun(model);
92
+ state.receivedAt = receivedAt;
89
93
  state.stale = false;
90
94
  } catch {
91
95
  state.stale = true;
@@ -1,4 +1,10 @@
1
+ import type { WorkflowStages } from "../workflow-declaration.ts";
2
+ import { type WorkflowRefusal } from "../workflow-refusals.ts";
1
3
  import type { WatchDecision, WatchItem, WatchModel, WatchTurn } from "./watch-render.ts";
4
+ import { type GithubRecords } from "./watch-pull-request.ts";
5
+ export type { GithubRecords } from "./watch-pull-request.ts";
6
+ import { type PrRepository } from "./watch-pr-identity.ts";
7
+ type Fields = Readonly<Record<string, unknown>>;
2
8
  export declare function activityOf(line: string): WatchItem[];
3
9
  export interface TurnRecords {
4
10
  readonly key: string;
@@ -10,7 +16,11 @@ export interface TurnRecords {
10
16
  export declare function turnOf(records: TurnRecords): WatchTurn;
11
17
  export declare function decisionCommands(decision: unknown, client: string): WatchDecision;
12
18
  export interface WatchInputs {
19
+ readonly queue?: Fields;
20
+ readonly repository?: PrRepository | null;
13
21
  readonly workflows: readonly string[];
22
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
23
+ readonly workflowStages?: WorkflowStages;
14
24
  readonly summaries?: readonly unknown[];
15
25
  readonly home: string;
16
26
  readonly client: string;
@@ -22,8 +32,4 @@ export interface WatchInputs {
22
32
  readonly turns: readonly WatchTurn[];
23
33
  readonly githubRecords: readonly GithubRecords[];
24
34
  }
25
- export interface GithubRecords {
26
- readonly started: unknown;
27
- readonly result: unknown;
28
- }
29
35
  export declare function watchModel(inputs: WatchInputs): WatchModel;