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,4 +1,7 @@
1
+ import { type PrRepository } from "./watch-pr-identity.ts";
2
+ import { type WorkflowStages } from "../workflow-declaration.ts";
1
3
  import { type Fields } from "../protocol.ts";
4
+ import { type WorkflowRefusal } from "../workflow-refusals.ts";
2
5
  interface ReadStatus extends Fields {
3
6
  readonly runId: string;
4
7
  readonly latestSequence: number;
@@ -13,8 +16,11 @@ interface ReadRun {
13
16
  readonly events: readonly ReadEvent[];
14
17
  }
15
18
  export interface WatchRead {
19
+ readonly queue?: Fields;
16
20
  readonly service: string;
17
21
  readonly workflows: readonly string[];
22
+ readonly workflowRefusals: readonly WorkflowRefusal[];
23
+ readonly workflowStages?: WorkflowStages;
18
24
  readonly summaries: readonly ReadStatus[];
19
25
  readonly runs: readonly ReadRun[];
20
26
  readonly completionCandidates: readonly ReadRun[];
@@ -24,5 +30,5 @@ export interface WatchReader {
24
30
  readonly read: (now: string) => Promise<WatchRead>;
25
31
  readonly run: (runId: string) => Promise<WatchRead>;
26
32
  }
27
- export declare function watchReader(runtimeDirectory: string): WatchReader;
33
+ export declare function watchReader(runtimeDirectory: string, repository?: PrRepository | null): WatchReader;
28
34
  export {};
@@ -1,9 +1,13 @@
1
+ import { establishedIssue } from "./watch-pr-identity.js";
2
+ import { isWorkflowStages, } from "../workflow-declaration.js";
1
3
  import { record } from "../protocol.js";
2
4
  import { send } from "./client.js";
3
5
  import { activeRun, recentRun } from "./watch-board.js";
6
+ import { workflowRefusals, } from "../workflow-refusals.js";
4
7
  const unavailable = {
5
8
  service: "down",
6
9
  workflows: [],
10
+ workflowRefusals: [],
7
11
  summaries: [],
8
12
  runs: [],
9
13
  completionCandidates: [],
@@ -79,15 +83,15 @@ async function creationOf(reader, runId) {
79
83
  }
80
84
  return null;
81
85
  }
82
- function matchingCompletion(status, ticketIds) {
83
- const ticketId = status["ticketId"];
86
+ function matchingCompletion(status, context) {
87
+ const issue = establishedIssue(context.repository, status["issueIdentity"]);
84
88
  return (status.state === "completed" &&
85
89
  status["workflowId"] === "develop-issue" &&
86
90
  record(status["outcome"])?.["kind"] === "handoff_requested" &&
87
91
  record(status["outcome"])?.["ticketStatus"] !== "partial" &&
88
92
  record(status["outcome"])?.["issueUpdate"] !== "failed" &&
89
- typeof ticketId === "string" &&
90
- ticketIds.has(ticketId));
93
+ issue !== null &&
94
+ context.issues.has(JSON.stringify(issue)));
91
95
  }
92
96
  async function completionOf(reader, search) {
93
97
  const { runId, latestSequence } = search.summary;
@@ -106,12 +110,16 @@ async function completionOf(reader, search) {
106
110
  state: status.state,
107
111
  workflowId: status["workflowId"],
108
112
  ticketId: status["ticketId"],
113
+ issueIdentity: status["issueIdentity"],
109
114
  outcome: status["outcome"],
110
115
  },
111
116
  };
112
117
  reader.completions.set(runId, known);
113
118
  }
114
- if (!matchingCompletion(known.status, search.ticketIds))
119
+ if (!matchingCompletion(known.status, {
120
+ issues: search.issues,
121
+ repository: reader.repository,
122
+ }))
115
123
  return null;
116
124
  const creation = known.creation === undefined
117
125
  ? await creationOf(reader, runId)
@@ -119,11 +127,20 @@ async function completionOf(reader, search) {
119
127
  reader.completions.set(runId, { ...known, creation });
120
128
  return { status: known.status, events: creation === null ? [] : [creation] };
121
129
  }
130
+ function refreshedLinks(run, status) {
131
+ const links = Object.fromEntries(["parentRunId", "chainId", "childRunIds"]
132
+ .filter((name) => Object.hasOwn(status, name))
133
+ .map((name) => [name, status[name]]));
134
+ return {
135
+ ...run,
136
+ status: { ...run.status, ...links },
137
+ };
138
+ }
122
139
  async function detailed(reader, summary) {
123
140
  const { runId, latestSequence: sequence } = summary;
124
141
  const known = reader.details.get(runId);
125
142
  if (known !== undefined && known.sequence === sequence)
126
- return known.value;
143
+ return refreshedLinks(known.value, summary);
127
144
  const shown = await asked(reader, {
128
145
  operation: "status",
129
146
  parameters: { runId },
@@ -160,10 +177,10 @@ async function decisionsOf(reader) {
160
177
  });
161
178
  return list(questions["decisions"]);
162
179
  }
163
- function stoppedTicketIds(runs, now) {
180
+ function stoppedIssues(runs, context) {
164
181
  const stopped = runs
165
182
  .map((run) => run.status)
166
- .filter((status) => recentRun(status.latestAt, now) &&
183
+ .filter((status) => recentRun(status.latestAt, context.now) &&
167
184
  status["workflowId"] === "develop-issue" &&
168
185
  (["failed", "cancelled"].includes(status.state) ||
169
186
  (status.state === "completed" &&
@@ -171,16 +188,15 @@ function stoppedTicketIds(runs, now) {
171
188
  .toSorted((first, second) => Date.parse(second.latestAt ?? "") - Date.parse(first.latestAt ?? ""))
172
189
  .slice(0, 20);
173
190
  return new Set(stopped.flatMap((status) => {
174
- const ticketId = status["ticketId"];
175
- return status.state === "completed" &&
176
- record(status["outcome"])?.["ticketStatus"] !== "partial" &&
177
- record(status["outcome"])?.["issueUpdate"] !== "failed" &&
178
- typeof ticketId === "string" &&
179
- ticketId !== ""
180
- ? [ticketId]
191
+ const issue = establishedIssue(context.repository, status["issueIdentity"]);
192
+ return status.state === "completed" && issue !== null
193
+ ? [JSON.stringify(issue)]
181
194
  : [];
182
195
  }));
183
196
  }
197
+ function stageMetadata(value) {
198
+ return Object.fromEntries(Object.entries(record(value) ?? {}).flatMap(([workflowId, stages]) => isWorkflowStages(stages) ? [[workflowId, stages]] : []));
199
+ }
184
200
  async function service(reader, now) {
185
201
  const status = await asked(reader, {
186
202
  operation: "status",
@@ -194,20 +210,25 @@ async function service(reader, now) {
194
210
  const current = summaries.filter((summary) => activeRun(summary.state) || recentRun(summary.latestAt, now));
195
211
  for (const summary of current)
196
212
  runs.push(await detailed(reader, summary));
197
- const ticketIds = stoppedTicketIds(runs, now);
198
- const older = ticketIds.size === 0
213
+ const issues = stoppedIssues(runs, { now, repository: reader.repository });
214
+ const older = issues.size === 0
199
215
  ? []
200
216
  : summaries.filter((summary) => !recentRun(summary.latestAt, now) &&
201
217
  summary["workflowId"] === "develop-issue" &&
202
218
  summary.state === "completed");
203
219
  for (const summary of older) {
204
- const candidate = await completionOf(reader, { summary, ticketIds });
220
+ const candidate = await completionOf(reader, { summary, issues });
205
221
  if (candidate !== null)
206
222
  completionCandidates.push(candidate);
207
223
  }
208
224
  return {
209
225
  service: "up",
226
+ ...(record(status["queue"]) === undefined
227
+ ? {}
228
+ : { queue: record(status["queue"]) ?? {} }),
210
229
  workflows: list(status["workflows"]).filter((value) => typeof value === "string"),
230
+ workflowStages: stageMetadata(status["workflowStages"]),
231
+ workflowRefusals: workflowRefusals(status["workflowRefusals"]),
211
232
  summaries,
212
233
  runs,
213
234
  completionCandidates,
@@ -226,7 +247,7 @@ async function singleRun(reader, runId) {
226
247
  const status = statusOf(reply.result["run"]);
227
248
  const known = reader.details.get(runId);
228
249
  const value = known !== undefined && known.sequence === status.latestSequence
229
- ? known.value
250
+ ? refreshedLinks(known.value, status)
230
251
  : await remember(reader, status);
231
252
  return {
232
253
  ...unavailable,
@@ -235,9 +256,10 @@ async function singleRun(reader, runId) {
235
256
  decisions: await decisionsOf(reader),
236
257
  };
237
258
  }
238
- export function watchReader(runtimeDirectory) {
259
+ export function watchReader(runtimeDirectory, repository = null) {
239
260
  const reader = {
240
261
  runtimeDirectory,
262
+ repository,
241
263
  details: new Map(),
242
264
  completions: new Map(),
243
265
  };
@@ -1,6 +1,8 @@
1
1
  import type { CardForm } from "./watch-run-input.ts";
2
2
  import type { WorkerUsage } from "../worker-usage.ts";
3
- import type { PrObservation } from "./watch-pr-identity.ts";
3
+ import type { PrIdentity, PrObservation } from "./watch-pr-identity.ts";
4
+ import type { WorkflowRefusal } from "../workflow-refusals.ts";
5
+ import type { Fields } from "../protocol.ts";
4
6
  export interface WatchItem {
5
7
  readonly kind: string;
6
8
  readonly text: string;
@@ -62,6 +64,9 @@ export interface WatchStep {
62
64
  readonly startedAt: string | null;
63
65
  }
64
66
  export interface WatchCard {
67
+ readonly parentRunId?: string | null;
68
+ readonly chainId?: string;
69
+ readonly childRunIds?: readonly string[];
65
70
  readonly estimatedCostUsd: number | null;
66
71
  readonly runId: string;
67
72
  readonly title: string | null;
@@ -92,8 +97,14 @@ export interface WatchWorkflow {
92
97
  readonly columns: readonly WatchColumn[];
93
98
  }
94
99
  export interface WatchRun {
100
+ readonly parentRunId?: string | null;
101
+ readonly chainId?: string;
102
+ readonly childRunIds?: readonly string[];
103
+ readonly mergeProof?: PrIdentity | null;
104
+ readonly issueIdentity?: PrIdentity | null;
95
105
  readonly issueUpdate?: "failed";
96
106
  readonly ticketStatus?: "complete" | "partial";
107
+ readonly promotionId?: string;
97
108
  readonly estimatedCostUsd: number | null;
98
109
  readonly ticketId: string | null;
99
110
  readonly title: string | null;
@@ -121,6 +132,8 @@ export interface WatchRun {
121
132
  readonly mergeCommit: string | null;
122
133
  }
123
134
  export interface WatchModel {
135
+ readonly queue?: Fields;
136
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
124
137
  readonly cardForms?: readonly CardForm[];
125
138
  readonly workflows: readonly WatchWorkflow[];
126
139
  readonly startWorkflowId: string | null;
@@ -75,7 +75,8 @@ export function completedDetail(run) {
75
75
  const update = run.issueUpdate === "failed"
76
76
  ? " Issue status update failed; check the GitHub operation."
77
77
  : "";
78
- return `${verdicts[run.decision] ?? ""}The run completed${time} · cost ${cost}.${ticket}${update}`;
78
+ const promotion = run.promotionId === undefined ? "" : ` Promotion: ${run.promotionId}.`;
79
+ return `${verdicts[run.decision] ?? ""}The run completed${time} · cost ${cost}.${promotion}${ticket}${update}`;
79
80
  }
80
81
  export function phaseTime(phase, now) {
81
82
  if (phase.startedAt === null)
@@ -13,6 +13,20 @@ import { watchStream } from "./watch-stream.js";
13
13
  export const defaultWatchPort = 3663;
14
14
  const forbidden = { status: 403, type: "text/plain", body: "refused" };
15
15
  const notFound = { status: 404, type: "text/plain", body: "not found" };
16
+ const fontFiles = new Map([
17
+ "fira-code-latin-400-normal.woff2",
18
+ "fira-code-latin-500-normal.woff2",
19
+ "heebo-latin-500-normal.woff2",
20
+ "heebo-latin-700-normal.woff2",
21
+ "heebo-latin-800-normal.woff2",
22
+ "space-grotesk-latin-400-normal.woff2",
23
+ "space-grotesk-latin-500-normal.woff2",
24
+ "space-grotesk-latin-600-normal.woff2",
25
+ "space-grotesk-latin-700-normal.woff2",
26
+ ].map((name) => [
27
+ `/watch-fonts-v1/${name}`,
28
+ new URL(`assets/watch-fonts/${name}`, import.meta.resolve("#package")),
29
+ ]));
16
30
  const libraryFile = fileURLToPath(import.meta.resolve("preact"));
17
31
  function parsed(text) {
18
32
  try {
@@ -81,6 +95,7 @@ async function snapshot(watch, runId) {
81
95
  ]);
82
96
  const model = watchModel({
83
97
  ...read,
98
+ repository: watch.observer.repository ?? null,
84
99
  home: homedir(),
85
100
  client: watch.settings.client,
86
101
  updatedAt,
@@ -93,9 +108,25 @@ async function snapshot(watch, runId) {
93
108
  cardForms: watch.actions?.forms.filter((form) => shown.workflows.some((workflow) => workflow.workflowId === form.workflowId)) ?? [],
94
109
  };
95
110
  }
111
+ async function fontReply(request) {
112
+ const fontFile = fontFiles.get(request.url ?? "");
113
+ if (fontFile === undefined)
114
+ return request.url?.startsWith("/watch-fonts-v1/") ? notFound : null;
115
+ if (request.method !== "GET")
116
+ return notFound;
117
+ return {
118
+ status: 200,
119
+ type: "font/woff2",
120
+ body: await readFile(fontFile),
121
+ cache: "public, max-age=31536000, immutable",
122
+ };
123
+ }
96
124
  async function answered(watch, request) {
97
125
  if (!watchHost(request))
98
126
  return forbidden;
127
+ const font = await fontReply(request);
128
+ if (font !== null)
129
+ return font;
99
130
  const url = new URL(String(request.url), "http://localhost");
100
131
  if (request.method === "GET")
101
132
  return readReply(watch, url);
@@ -137,7 +168,7 @@ async function readReply(watch, url) {
137
168
  function replied(response, reply) {
138
169
  response.writeHead(reply.status, {
139
170
  "content-type": reply.type,
140
- "cache-control": "no-store",
171
+ "cache-control": reply.cache ?? "no-store",
141
172
  });
142
173
  response.end(reply.body);
143
174
  }
@@ -161,7 +192,7 @@ export async function watchServer(settings) {
161
192
  : await watchActions(settings.runtimeDirectory),
162
193
  settings,
163
194
  observer,
164
- reader: watchReader(settings.runtimeDirectory),
195
+ reader: watchReader(settings.runtimeDirectory, observer.repository ?? null),
165
196
  streams: new Map(),
166
197
  github: new Map(),
167
198
  };
@@ -32,7 +32,7 @@ export function Turn(props) {
32
32
  const groups = groupActivity(turn.activity);
33
33
  const working = turn.outcome === "in work";
34
34
  const cost = priceText(turn.costUsd, turn.estimatedCostUsd);
35
- const heading = ["Claude turn", turn.outcome, turnTime(turn, now), cost];
35
+ const heading = ["Agent turn", turn.outcome, turnTime(turn, now), cost];
36
36
  const place = `Branch ${turn.branch} · work tree ${turn.worktree}`;
37
37
  const activity = groups.map((items, index) => component(Group, { items, open: working && index === groups.length - 1 }));
38
38
  return element("section", {
@@ -2,6 +2,9 @@ import type { CardControls } from "./watch-card-view.ts";
2
2
  import { type Shown } from "./watch-element.ts";
3
3
  import { type Copying, type WatchModel, type WatchPhase, type WatchRun } from "./watch-render.ts";
4
4
  import { type ColumnControls } from "./watch-board-view.ts";
5
+ export declare function Queue(props: {
6
+ readonly model: WatchModel;
7
+ }): Shown;
5
8
  interface Timed {
6
9
  readonly run: WatchRun;
7
10
  readonly now: string;
@@ -38,6 +41,7 @@ export declare function Header(props: {
38
41
  export interface PageProps {
39
42
  readonly cards?: CardControls;
40
43
  readonly model: WatchModel | null;
44
+ readonly observationNow?: string | undefined;
41
45
  readonly selected: string;
42
46
  readonly now: string;
43
47
  readonly stale: boolean;
@@ -45,6 +49,9 @@ export interface PageProps {
45
49
  readonly columns?: ColumnControls;
46
50
  readonly start?: StartControls;
47
51
  }
52
+ export declare function RefusedWorkflows(props: {
53
+ readonly model: WatchModel;
54
+ }): Shown;
48
55
  export declare function Page(props: PageProps): Shown;
49
56
  export declare function BoardPage(props: PageProps & {
50
57
  readonly model: WatchModel;
@@ -1,10 +1,36 @@
1
1
  import { priceText, usageText, totalsSuffix } from "./usage-render.js";
2
2
  import { component, element } from "./watch-element.js";
3
3
  import { clockTime, completedDetail, counted, elapsed, phaseTime, runName, runTone, waitingDetail, } from "./watch-render.js";
4
- import { Board, WorkflowList, } from "./watch-board-view.js";
4
+ import { Board, HandoffLinks, WorkflowList, } from "./watch-board-view.js";
5
5
  import { boardWorkflow, watchHash, watchLocation } from "./watch-navigation.js";
6
6
  import { Decision, Events, Turn } from "./watch-turn.js";
7
7
  import { StartRun } from "./watch-start-view.js";
8
+ import { record } from "../protocol.js";
9
+ export function Queue(props) {
10
+ const queue = props.model.queue;
11
+ if (queue === undefined)
12
+ return null;
13
+ const skipped = Array.isArray(queue["skipped"])
14
+ ? queue["skipped"]
15
+ : [];
16
+ return [
17
+ element("p", {
18
+ class: "meta",
19
+ children: `Queue: ${String(queue["busy"])} of ${String(queue["lanes"])} lanes busy`,
20
+ }),
21
+ ...skipped.map((value) => {
22
+ const ticket = record(value) ?? {};
23
+ const dependencies = ticket["dependencies"];
24
+ const needs = Array.isArray(dependencies)
25
+ ? `; needs ${dependencies.map(String).join(", ")}`
26
+ : "";
27
+ return element("p", {
28
+ class: "meta",
29
+ children: `Skipped ticket ${String(ticket["ticketId"])}: ${String(ticket["reason"])}${needs}`,
30
+ });
31
+ }),
32
+ ];
33
+ }
8
34
  export const clove = {
9
35
  viewBox: "0.6625858545303345 0.6630800366401672 46.674827575683594 46.673831939697266",
10
36
  paths: [
@@ -98,7 +124,7 @@ export function PullRequest(props) {
98
124
  children: `#${String(pullRequest.number)}`,
99
125
  });
100
126
  const scores = reviews.map((review) => element("li", {
101
- children: `Round ${String(review.round)} · Greptile ${String(review.score)}/5 · commit ${review.commit.slice(0, 7)}`,
127
+ children: `Round ${String(review.round)} · Review ${String(review.score)}/5 · commit ${review.commit.slice(0, 7)}`,
102
128
  }));
103
129
  const merge = mergeCommit === null
104
130
  ? null
@@ -124,7 +150,7 @@ export function Run(props) {
124
150
  const budget = run.spent === null || run.limit === null
125
151
  ? ""
126
152
  : ` · ${String(run.spent)} of ${String(run.limit)} effect units`;
127
- const none = "No Claude turn for this run.";
153
+ const none = "No agent turn for this run.";
128
154
  const turns = run.turns.length === 0
129
155
  ? element("p", { class: "meta", children: none })
130
156
  : run.turns.map((item, index) => [
@@ -142,6 +168,7 @@ export function Run(props) {
142
168
  ];
143
169
  return [
144
170
  component(Banner, { run, now }),
171
+ component(HandoffLinks, { run }),
145
172
  element("section", {
146
173
  class: "run-usage",
147
174
  children: [
@@ -198,6 +225,22 @@ export function Header(props) {
198
225
  ],
199
226
  });
200
227
  }
228
+ export function RefusedWorkflows(props) {
229
+ const refusals = props.model.workflowRefusals ?? [];
230
+ if (refusals.length === 0)
231
+ return null;
232
+ return element("section", {
233
+ class: "workflow-refusals",
234
+ children: [
235
+ element("h2", { children: "Refused workflows" }),
236
+ element("ul", {
237
+ children: refusals.map((entry) => element("li", {
238
+ children: `${entry.scope} ${entry.label}: ${entry.codes.join(", ")}`,
239
+ })),
240
+ }),
241
+ ],
242
+ });
243
+ }
201
244
  export function Page(props) {
202
245
  const { model, selected, now, stale, copying } = props;
203
246
  const head = component(Header, { model, stale });
@@ -215,6 +258,8 @@ export function Page(props) {
215
258
  return [
216
259
  head,
217
260
  down,
261
+ component(Queue, { model }),
262
+ component(RefusedWorkflows, { model }),
218
263
  element("a", {
219
264
  class: "back",
220
265
  href: watchHash({ workflowId: back, runId: null }),
@@ -235,7 +280,9 @@ export function BoardPage(props) {
235
280
  class: "board-page",
236
281
  children: [
237
282
  component(Header, { model, stale, start }),
283
+ component(Queue, { model }),
238
284
  down,
285
+ component(RefusedWorkflows, { model }),
239
286
  component(StartRun, {
240
287
  workflows: model.workflows,
241
288
  copying,
@@ -256,6 +303,7 @@ export function BoardPage(props) {
256
303
  : component(Board, {
257
304
  workflow,
258
305
  now,
306
+ observationNow: props.observationNow,
259
307
  controls: props.columns,
260
308
  cardForm: model.cardForms?.find((form) => form.workflowId === workflowId),
261
309
  cards: props.cards,
@@ -1,9 +1,7 @@
1
1
  import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
2
- import { type TokenPrices } from "../token-prices.ts";
3
2
  import { type RetrySettings } from "./retrying-turn.ts";
4
3
  import { type WorkerReport } from "./operation-journal.ts";
5
4
  interface Settings extends RetrySettings {
6
- readonly tokenPrices?: TokenPrices;
7
5
  readonly journal: string;
8
6
  readonly document: (digest: string) => Promise<string | undefined>;
9
7
  readonly report: (report: WorkerReport) => Promise<unknown>;
@@ -1,9 +1,10 @@
1
1
  import { mkdir, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { analysisPrompt } from "../chats/analysis-prompt.js";
4
- import { isAnalysisSnapshot } from "../chats/analysis-input.js";
3
+ import { analysisFollowUp, analysisFollowUpPrompt, analysisLaunchBytes, } from "../chats/analysis-prompt.js";
4
+ import { prepareAnalysisBatch } from "../chats/analysis-batches.js";
5
+ import { isAnalysisSnapshot, analysisDocumentBytes, } from "../chats/analysis-input.js";
5
6
  import { record } from "../protocol.js";
6
- import { estimatedCostUsd, priceForModel, } from "../token-prices.js";
7
+ import { sumPrices } from "../worker-usage.js";
7
8
  import { startRetriedTurn } from "./retrying-turn.js";
8
9
  import { analysisAvailable, analysisMethod } from "./analysis-launch.js";
9
10
  import { analysisToken } from "./analysis-token.js";
@@ -19,13 +20,60 @@ function own(intent) {
19
20
  function failed(reason) {
20
21
  return { kind: "failed", reason };
21
22
  }
22
- function snapshotOf(text) {
23
- try {
24
- return record(JSON.parse(text ?? "null"))?.["snapshot"];
25
- }
26
- catch {
27
- return undefined;
23
+ function firstBatch(snapshot) {
24
+ const batch = prepareAnalysisBatch(snapshot, {
25
+ nextRecordIndex: 0,
26
+ completedBatchCount: 0,
27
+ previousResult: null,
28
+ });
29
+ if (batch.kind === "refused")
30
+ throw new Error(batch.code);
31
+ if (batch.nextRecordIndex !== batch.totalRecords)
32
+ throw new Error("analysis-too-large");
33
+ return batch;
34
+ }
35
+ function promptOf(text, target) {
36
+ if (text !== undefined && Buffer.byteLength(text) > analysisDocumentBytes)
37
+ throw new Error("analysis-too-large");
38
+ const payload = record(JSON.parse(text ?? "null"));
39
+ if (payload === undefined)
40
+ throw new Error("analysis-input-refused");
41
+ const snapshot = payload["snapshot"];
42
+ if (!isAnalysisSnapshot(snapshot) || snapshot.importId !== target)
43
+ throw new Error("analysis-input-refused");
44
+ const followUp = "followUp" in payload ? analysisFollowUp(payload["followUp"]) : undefined;
45
+ const data = batchInput(snapshot, payload);
46
+ const prompt = turnPrompt(data, followUp);
47
+ if (analysisLaunchBytes(prompt) > analysisDocumentBytes)
48
+ throw new Error("analysis-too-large");
49
+ return prompt;
50
+ }
51
+ function turnPrompt(data, followUp) {
52
+ const context = data.context === undefined ? {} : { context: data.context };
53
+ if (followUp === undefined)
54
+ return data.prompt;
55
+ return analysisFollowUpPrompt(data.snapshot, { ...followUp, ...context });
56
+ }
57
+ function batchInput(snapshot, payload) {
58
+ if (!("context" in payload)) {
59
+ const batch = firstBatch(snapshot);
60
+ return { snapshot: batch.snapshot, prompt: batch.prompt };
28
61
  }
62
+ const context = record(payload["context"]);
63
+ if (context === undefined || Object.keys(context).length !== 3)
64
+ throw new Error("analysis-input-refused");
65
+ const position = [context["batchIndex"], context["firstRecordIndex"]];
66
+ if (!position.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0) ||
67
+ Number(position[0]) > 3)
68
+ throw new Error("analysis-input-refused");
69
+ if (context["previousResult"] !== null &&
70
+ record(context["previousResult"]) === undefined)
71
+ throw new Error("analysis-input-refused");
72
+ return {
73
+ snapshot,
74
+ context,
75
+ prompt: JSON.stringify({ evidence: snapshot, context }),
76
+ };
29
77
  }
30
78
  async function cleaned(folder) {
31
79
  await Promise.all(["configuration", "working"].map((name) => rm(join(folder, name), { recursive: true, force: true })));
@@ -33,7 +81,7 @@ async function cleaned(folder) {
33
81
  async function privateRecord(folder, entry) {
34
82
  await writeRecord(folder, { ...entry, permissions: "private" });
35
83
  }
36
- function outcome(end, prices) {
84
+ function outcome(end) {
37
85
  const endedAt = new Date().toISOString();
38
86
  if (end.stopped)
39
87
  return { outcome: "stopped", endedAt };
@@ -44,12 +92,10 @@ function outcome(end, prices) {
44
92
  costUsd: result.costUsd,
45
93
  usage: result.usage,
46
94
  model: result.model,
95
+ ...(result.attempts === undefined ? {} : { attempts: result.attempts }),
47
96
  sessionId: result.sessionId,
48
97
  turns: result.turns,
49
- estimatedCostUsd: estimatedCostUsd(result.usage, {
50
- worker: "claude",
51
- price: priceForModel(prices, result.model),
52
- }),
98
+ estimatedCostUsd: sumPrices((result.attempts ?? []).map((attempt) => attempt.estimatedCostUsd)),
53
99
  };
54
100
  if (end.exitCode !== 0 || result.failed)
55
101
  return {
@@ -121,7 +167,7 @@ async function begun(parts, opening) {
121
167
  }, { ...parts.settings, metadata: started });
122
168
  const done = turn.ended
123
169
  .then(async (end) => {
124
- const result = outcome(end, parts.settings.tokenPrices ?? {});
170
+ const result = outcome(end);
125
171
  await privateRecord(folder, { name: "result.json", value: result });
126
172
  await cleaned(folder);
127
173
  await parts.settings.report(reportOf({ intent, worker: "claude", commandId: `analysis-turn:${key}` }, result));
@@ -142,9 +188,15 @@ async function deliver(parts, call) {
142
188
  return known;
143
189
  const { key, folder } = located(parts.settings.journal, intent.operationId);
144
190
  const text = await parts.settings.document(intent.payload);
145
- const snapshot = snapshotOf(text);
146
- if (!isAnalysisSnapshot(snapshot) || snapshot.importId !== intent.target)
147
- return failed("analysis-input-refused");
191
+ let prompt;
192
+ try {
193
+ prompt = promptOf(text, intent.target);
194
+ }
195
+ catch (error) {
196
+ return failed(error instanceof Error && error.message === "analysis-too-large"
197
+ ? "analysis-too-large"
198
+ : "analysis-input-refused");
199
+ }
148
200
  await mkdir(join(folder, "configuration"), { recursive: true, mode: 0o700 });
149
201
  if (!(await analysisAvailable(folder))) {
150
202
  await cleaned(folder);
@@ -173,7 +225,7 @@ async function deliver(parts, call) {
173
225
  key,
174
226
  folder,
175
227
  directory,
176
- prompt: analysisPrompt(snapshot),
228
+ prompt,
177
229
  token: credential.token,
178
230
  });
179
231
  return { receipt: key, worker: "claude" };
@@ -0,0 +1,23 @@
1
+ import { type AnalysisSnapshot } from "../chats/analysis-input.ts";
2
+ import { type AnalysisCapture } from "./chats-capture.ts";
3
+ interface AnalysisBatchBindingInput {
4
+ readonly runId: string;
5
+ readonly importId: string;
6
+ readonly snapshot: AnalysisSnapshot | undefined;
7
+ readonly batch?: unknown;
8
+ }
9
+ export declare function boundAnalysisBatch(directory: string, input: AnalysisBatchBindingInput): Promise<{
10
+ capture: AnalysisCapture;
11
+ snapshot: AnalysisSnapshot;
12
+ batch: {
13
+ batchIndex: number;
14
+ nextRecordIndex: number;
15
+ };
16
+ previous: import("../chats/analysis-checkpoint.ts").AnalysisCheckpoint;
17
+ }>;
18
+ export declare function previousCheckpoint(folder: string, input: {
19
+ readonly capture: AnalysisCapture;
20
+ readonly batchIndex: number;
21
+ readonly snapshot: AnalysisSnapshot;
22
+ }): Promise<import("../chats/analysis-checkpoint.ts").AnalysisCheckpoint>;
23
+ export {};