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,10 @@
1
+ import { recordedNotifications, saveNotifications, } from "./notify-journal.js";
1
2
  import { spawn } from "node:child_process";
2
- import { readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { send } from "../cli/client.js";
5
5
  import { record } from "../protocol.js";
6
6
  import { decisionNotification, outcomeNotification, } from "./notify-message.js";
7
+ const retentionMilliseconds = 7 * 24 * 60 * 60_000;
7
8
  async function request(runtimeDirectory, request) {
8
9
  const reply = await send(runtimeDirectory, request);
9
10
  if (reply.kind === "error")
@@ -20,6 +21,20 @@ function rows(value) {
20
21
  return row;
21
22
  });
22
23
  }
24
+ async function flushNotifications(journal) {
25
+ if (journal.unsaved.size === 0)
26
+ return;
27
+ await saveNotifications(journal.file, journal);
28
+ for (const entry of journal.unsaved.values())
29
+ if (entry.state === "failed" && entry.attempts === 5)
30
+ process.stderr.write(`Notify delivery failed: ${entry.key}\n`);
31
+ journal.unsaved.clear();
32
+ }
33
+ async function finishDelivery(journal, entry) {
34
+ journal.entries.set(entry.key, entry);
35
+ journal.unsaved.set(entry.key, entry);
36
+ await flushNotifications(journal);
37
+ }
23
38
  async function observedOutcome(summary, source) {
24
39
  const { runtimeDirectory, cache } = source;
25
40
  const latestSequence = summary["latestSequence"];
@@ -74,26 +89,6 @@ async function events(runtimeDirectory, cache) {
74
89
  }
75
90
  return found;
76
91
  }
77
- async function recorded(file) {
78
- const text = await readFile(file, "utf8").catch((error) => {
79
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
80
- return undefined;
81
- throw error;
82
- });
83
- if (text === undefined)
84
- return undefined;
85
- const value = JSON.parse(text);
86
- if (!Array.isArray(value) || !value.every((key) => typeof key === "string"))
87
- throw new Error("invalid-notify-record");
88
- return new Set(value.map(String));
89
- }
90
- async function save(file, keys) {
91
- await writeFile(`${file}.tmp`, JSON.stringify([...keys]) + "\n", {
92
- mode: 0o600,
93
- flush: true,
94
- });
95
- await rename(`${file}.tmp`, file);
96
- }
97
92
  export function notifyCommand(command, notification) {
98
93
  return new Promise((resolve, reject) => {
99
94
  const [executable = "", ...argumentsList] = command;
@@ -117,45 +112,189 @@ function report(label, error) {
117
112
  const reason = error instanceof Error ? error.message : String(error);
118
113
  process.stderr.write(`${label}: ${JSON.stringify(reason)}\n`);
119
114
  }
115
+ function failureReason(error) {
116
+ if (!(error instanceof Error))
117
+ return "command error";
118
+ if ("code" in error && (error.code === "ENOENT" || error.code === "EACCES"))
119
+ return `spawn ${error.code}`;
120
+ return /^(?:exit code (?:\d+|null)|signal SIG[A-Z0-9]+)$/.test(error.message)
121
+ ? error.message
122
+ : "command error";
123
+ }
124
+ async function prepareDelivery(journal, entry) {
125
+ if (entry.message === null || entry.state === "sent")
126
+ return undefined;
127
+ if (entry.attempts === 5 && entry.state === "pending")
128
+ await finishDelivery(journal, {
129
+ ...entry,
130
+ state: "failed",
131
+ lastFailure: "attempt interrupted",
132
+ });
133
+ if (entry.attempts === 5)
134
+ return undefined;
135
+ const backoff = [0, 60_000, 300_000, 900_000, 3_600_000][entry.attempts] ?? 0;
136
+ if (entry.lastAttemptAt !== null &&
137
+ Date.now() - entry.lastAttemptAt < backoff)
138
+ return undefined;
139
+ const pending = {
140
+ ...entry,
141
+ message: entry.message,
142
+ state: "pending",
143
+ attempts: entry.attempts + 1,
144
+ lastAttemptAt: Date.now(),
145
+ };
146
+ const next = new Map(journal.entries).set(entry.key, pending);
147
+ await saveNotifications(journal.file, { ...journal, entries: next });
148
+ journal.entries.set(entry.key, pending);
149
+ return pending;
150
+ }
151
+ async function deliver(settings, entry) {
152
+ try {
153
+ await notifyCommand(settings.command, entry.message);
154
+ return { ...entry, state: "sent" };
155
+ }
156
+ catch (error) {
157
+ return {
158
+ ...entry,
159
+ state: "failed",
160
+ lastFailure: failureReason(error),
161
+ };
162
+ }
163
+ }
164
+ async function settleDelivery(journal, progress) {
165
+ if (progress.completed !== undefined) {
166
+ const entry = progress.completed;
167
+ progress.completed = undefined;
168
+ await finishDelivery(journal, entry);
169
+ }
170
+ await flushNotifications(journal);
171
+ }
172
+ async function scheduleDelivery(settings, delivery) {
173
+ const { journal, progress } = delivery;
174
+ const stopped = () => progress.stopping.aborted;
175
+ if (stopped() ||
176
+ progress.active !== undefined ||
177
+ progress.completed !== undefined ||
178
+ journal.unsaved.size > 0)
179
+ return;
180
+ for (const entry of journal.entries.values()) {
181
+ if (stopped())
182
+ return;
183
+ const pending = await prepareDelivery(journal, entry);
184
+ if (pending === undefined)
185
+ continue;
186
+ if (stopped()) {
187
+ await finishDelivery(journal, entry);
188
+ return;
189
+ }
190
+ progress.active = deliver(settings, pending).then((completed) => {
191
+ progress.completed = completed;
192
+ progress.active = undefined;
193
+ delivery.ready();
194
+ });
195
+ return;
196
+ }
197
+ }
120
198
  async function check(settings, journal) {
199
+ const retained = new Map([...journal.entries].filter(([, entry]) => entry.state === "pending" ||
200
+ (entry.state === "failed" && entry.attempts < 5) ||
201
+ entry.lastAttemptAt === null ||
202
+ entry.lastAttemptAt >= Date.now() - retentionMilliseconds));
203
+ const removed = [...journal.entries.keys()].filter((key) => !retained.has(key));
204
+ if (removed.length > 0) {
205
+ const retiredKeys = new Set([...journal.retiredKeys, ...removed]);
206
+ await saveNotifications(journal.file, { entries: retained, retiredKeys });
207
+ for (const key of removed) {
208
+ journal.entries.delete(key);
209
+ journal.retiredKeys.add(key);
210
+ }
211
+ }
121
212
  for (const event of await events(settings.runtimeDirectory, journal.cache)) {
122
- if (journal.keys.has(event.key))
213
+ if (journal.entries.has(event.key) || journal.retiredKeys.has(event.key))
123
214
  continue;
124
- await save(journal.file, new Set([...journal.keys, event.key]));
125
- journal.keys.add(event.key);
126
- await notifyCommand(settings.command, event.message).catch((error) => {
127
- report("Notify command failed", error);
128
- });
215
+ const entry = {
216
+ ...event,
217
+ state: "pending",
218
+ attempts: 0,
219
+ lastAttemptAt: null,
220
+ lastFailure: null,
221
+ };
222
+ journal.entries.set(event.key, entry);
223
+ journal.unsaved.set(event.key, entry);
129
224
  }
225
+ await flushNotifications(journal);
130
226
  }
131
227
  export async function notifyRuns(settings) {
132
228
  const file = join(settings.directory, "notify-events.json");
133
- const keys = await recorded(file);
229
+ const recorded = await recordedNotifications(file);
134
230
  const cache = new Map();
135
- const journal = { file, keys: keys ?? new Set(), cache };
136
- if (keys === undefined) {
231
+ const journal = {
232
+ file,
233
+ entries: recorded?.entries ?? new Map(),
234
+ retiredKeys: recorded?.retiredKeys ?? new Set(),
235
+ cache,
236
+ unsaved: new Map(),
237
+ };
238
+ if (recorded === undefined) {
137
239
  for (const event of await events(settings.runtimeDirectory, cache))
138
- journal.keys.add(event.key);
139
- await save(file, journal.keys);
240
+ journal.entries.set(event.key, {
241
+ ...event,
242
+ state: "sent",
243
+ attempts: 0,
244
+ lastAttemptAt: Date.now(),
245
+ lastFailure: null,
246
+ });
140
247
  }
248
+ await saveNotifications(file, journal);
249
+ return notificationLoop(settings, journal);
250
+ }
251
+ function notificationLoop(settings, journal) {
141
252
  let busy = false;
142
253
  let running = Promise.resolve();
254
+ const stopping = new AbortController();
255
+ const progress = {
256
+ stopping: stopping.signal,
257
+ active: undefined,
258
+ completed: undefined,
259
+ };
260
+ const ready = () => {
261
+ if (stopping.signal.aborted)
262
+ return;
263
+ running = running
264
+ .then(() => settleDelivery(journal, progress))
265
+ .then(() => scheduleDelivery(settings, { journal, progress, ready }))
266
+ .catch((error) => {
267
+ report("Notify delivery failed", error);
268
+ });
269
+ };
143
270
  const timer = setInterval(() => {
144
271
  if (busy)
145
272
  return;
146
273
  busy = true;
147
- running = check(settings, journal)
274
+ running = running
275
+ .then(() => settleDelivery(journal, progress))
276
+ .then(() => check(settings, journal))
148
277
  .catch((error) => {
149
278
  report("Notify check failed", error);
150
- })
151
- .finally(() => {
279
+ });
280
+ ready();
281
+ running = running.finally(() => {
152
282
  busy = false;
153
283
  });
154
284
  }, settings.intervalMilliseconds ?? 15_000);
155
285
  return {
156
- stop: async () => {
286
+ stop: () => {
287
+ stopping.abort();
157
288
  clearInterval(timer);
158
- await running;
289
+ running = running
290
+ .then(async () => {
291
+ await progress.active;
292
+ await settleDelivery(journal, progress);
293
+ })
294
+ .catch((error) => {
295
+ report("Notify delivery failed", error);
296
+ });
297
+ return running;
159
298
  },
160
299
  };
161
300
  }
@@ -1,5 +1,8 @@
1
+ import { type TokenPrices } from "../token-prices.ts";
1
2
  import { type RunningTurn, type TurnStart } from "./claude-turn.ts";
2
3
  export interface RetrySettings {
4
+ readonly tokenPrices?: TokenPrices;
5
+ readonly fallbackModels?: readonly string[];
3
6
  readonly retryDelay?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
4
7
  }
5
8
  interface RetryOptions extends RetrySettings {
@@ -1,7 +1,9 @@
1
1
  import { join } from "node:path";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
- import { priceOf, sumUsage, usageOf } from "../worker-usage.js";
4
+ import { priceOf, sumUsage, usageOf, } from "../worker-usage.js";
5
+ import { commandModel, replaceCommandModel, estimatedCostUsd, priceForModel, } from "../token-prices.js";
6
+ import { analysisCommand } from "./analysis-launch.js";
5
7
  import { capacityRefusal, startTurn, } from "./claude-turn.js";
6
8
  import { readRecord, writeRecord } from "./operation-journal.js";
7
9
  const waits = [60_000, 180_000];
@@ -14,10 +16,9 @@ function total(values) {
14
16
  return null;
15
17
  return priceOf(values.reduce((sum, value) => sum + (value ?? 0), 0));
16
18
  }
17
- function combined(ends) {
19
+ function combined(attempts) {
20
+ const { ends } = attempts;
18
21
  const last = ends.at(-1) ?? { exitCode: null, result: undefined };
19
- if (ends.length < 2)
20
- return last;
21
22
  const result = last.result ?? ends.find((end) => end.result)?.result;
22
23
  if (result === undefined)
23
24
  return last;
@@ -25,6 +26,7 @@ function combined(ends) {
25
26
  ...last,
26
27
  result: {
27
28
  ...result,
29
+ attempts: ends.map((end, index) => attemptUsage(attempts, { end, attempt: index + 1 })),
28
30
  ...(last.result === undefined ? { failed: true, text: "" } : {}),
29
31
  usage: sumUsage(ends.map((end) => end.result?.usage ?? usageOf(null))),
30
32
  costUsd: total(ends.map((end) => end.result?.costUsd ?? null)),
@@ -45,14 +47,52 @@ function controlled(attempts, start) {
45
47
  }),
46
48
  };
47
49
  }
50
+ function attemptCommand(attempts, attempt) {
51
+ const { start, options } = attempts;
52
+ const original = start.mode === "analysis" ? analysisCommand() : start.command;
53
+ const fallback = start.mode === "analysis"
54
+ ? undefined
55
+ : options.fallbackModels?.[attempt - 3];
56
+ const command = fallback === undefined ? original : replaceCommandModel(original, fallback);
57
+ const model = commandModel(command);
58
+ return {
59
+ command,
60
+ model,
61
+ ...(fallback !== undefined && model === null
62
+ ? { fallbackReason: "fallback-model-unavailable" }
63
+ : {}),
64
+ };
65
+ }
66
+ function attemptUsage(attempts, completion) {
67
+ const { end, attempt } = completion;
68
+ const model = end.result?.model ?? attemptCommand(attempts, attempt).model;
69
+ const usage = end.result?.usage ?? usageOf(null);
70
+ return {
71
+ attempt,
72
+ model,
73
+ usage,
74
+ costUsd: end.result?.costUsd ?? null,
75
+ estimatedCostUsd: estimatedCostUsd(usage, {
76
+ worker: attempts.start.mode === "analysis"
77
+ ? "claude"
78
+ : (attempts.start.worker ?? "claude"),
79
+ price: priceForModel(attempts.options.tokenPrices ?? {}, model),
80
+ }),
81
+ };
82
+ }
48
83
  async function launched(attempts) {
49
84
  const { start, options, ends } = attempts;
50
85
  const attempt = ends.length + 1;
86
+ const selected = attemptCommand(attempts, attempt);
51
87
  const folder = start.mode === "analysis" || attempt === 1
52
88
  ? start.folder
53
89
  : join(start.folder, `attempt-${String(attempt)}`);
54
90
  const entry = {
55
91
  ...options.metadata,
92
+ model: selected.model,
93
+ ...(selected.fallbackReason === undefined
94
+ ? {}
95
+ : { fallbackReason: selected.fallbackReason }),
56
96
  attempt,
57
97
  startedAt: attempt === 1 ? attempts.startedAt : new Date().toISOString(),
58
98
  };
@@ -70,9 +110,12 @@ async function launched(attempts) {
70
110
  return undefined;
71
111
  const turn = controlled(attempts, {
72
112
  ...start,
113
+ command: selected.command,
73
114
  folder,
74
115
  ...(attempt === 1 ? {} : { activityFolder: start.folder }),
75
- prompt: attempt === 1 ? start.prompt : continuation + start.prompt,
116
+ prompt: start.mode === "analysis" || attempt === 1
117
+ ? start.prompt
118
+ : continuation + start.prompt,
76
119
  });
77
120
  try {
78
121
  const value = { ...entry, phase: "running", processId: turn.processId };
@@ -96,6 +139,7 @@ async function recordedEnd(attempts, completion) {
96
139
  const { start, ends, options } = attempts;
97
140
  const { processId, end } = completion;
98
141
  const attempt = ends.length;
142
+ const selected = attemptCommand(attempts, attempt);
99
143
  const name = `attempt-${String(attempt)}.json`;
100
144
  const started = await readRecord(start.folder, name);
101
145
  const startedAt = Reflect.get(Object(started), "startedAt");
@@ -105,14 +149,16 @@ async function recordedEnd(attempts, completion) {
105
149
  name,
106
150
  value: {
107
151
  ...options.metadata,
152
+ ...attemptUsage(attempts, { end, attempt }),
153
+ ...(selected.fallbackReason === undefined
154
+ ? {}
155
+ : { fallbackReason: selected.fallbackReason }),
108
156
  attempt,
109
157
  processId,
110
158
  startedAt,
111
159
  endedAt: new Date().toISOString(),
112
160
  exitCode: end.exitCode,
113
161
  capacity: capacityRefusal(end.result),
114
- usage: end.result?.usage ?? usageOf(null),
115
- costUsd: end.result?.costUsd ?? null,
116
162
  },
117
163
  });
118
164
  }
@@ -151,11 +197,11 @@ async function finished(attempts, first) {
151
197
  break;
152
198
  await waited(attempts, milliseconds);
153
199
  if (cancelled(attempts))
154
- return { ...combined(ends), stopped: true };
200
+ return { ...combined(attempts), stopped: true };
155
201
  turn = await launched(attempts);
156
202
  }
157
203
  return {
158
- ...combined(ends),
204
+ ...combined(attempts),
159
205
  ...(cancelled(attempts) || turn === undefined ? { stopped: true } : {}),
160
206
  };
161
207
  }
@@ -0,0 +1,18 @@
1
+ import { type Fields } from "../protocol.ts";
2
+ interface Source {
3
+ readonly repository: string;
4
+ readonly command: readonly string[] | undefined;
5
+ readonly githubCommand?: readonly string[] | undefined;
6
+ }
7
+ export declare class TicketDependencyReadError extends Error {
8
+ }
9
+ export declare function ticketDependencyWaits(source: Source): {
10
+ readonly check: (candidate: {
11
+ readonly ticketId: string;
12
+ readonly ticket: string;
13
+ readonly signal: AbortSignal;
14
+ }) => Promise<readonly string[]>;
15
+ readonly refresh: (signal: AbortSignal) => Promise<readonly string[]>;
16
+ readonly skipped: () => readonly Fields[];
17
+ };
18
+ export {};
@@ -0,0 +1,60 @@
1
+ import { hardDependencies } from "../ticket-dependencies.js";
2
+ import { issueIdentity } from "../issue-identity.js";
3
+ import { record } from "../protocol.js";
4
+ import { calls } from "./github-calls.js";
5
+ import { ticketText } from "./ticket-source.js";
6
+ export class TicketDependencyReadError extends Error {
7
+ }
8
+ async function openDependencies(source, candidate) {
9
+ const github = calls({ repository: source.repository, gh: source.githubCommand ?? ["gh"] }, candidate.signal);
10
+ const issue = issueIdentity(candidate.ticket);
11
+ const repository = issue === null ? "{owner}/{repo}" : `${issue.owner}/${issue.repository}`;
12
+ const open = [];
13
+ for (const id of hardDependencies(candidate.ticket)) {
14
+ const response = JSON.parse(await github.gh(["api", `repos/${repository}/issues/${id}`]).catch(() => {
15
+ throw new TicketDependencyReadError("github-call-failed");
16
+ }));
17
+ const state = record(response)?.["state"];
18
+ if (state !== "open" && state !== "closed")
19
+ throw new Error("invalid-dependency-state");
20
+ if (state === "open")
21
+ open.push(id);
22
+ }
23
+ return open;
24
+ }
25
+ export function ticketDependencyWaits(source) {
26
+ const waiting = new Map();
27
+ let nextRefresh;
28
+ const check = async (candidate) => {
29
+ const open = await openDependencies(source, candidate);
30
+ if (open.length === 0)
31
+ waiting.delete(candidate.ticketId);
32
+ else
33
+ waiting.set(candidate.ticketId, open);
34
+ return open;
35
+ };
36
+ return {
37
+ check,
38
+ refresh: async (signal) => {
39
+ const tickets = [...waiting.keys()];
40
+ const ticketId = nextRefresh !== undefined && waiting.has(nextRefresh)
41
+ ? nextRefresh
42
+ : tickets[0];
43
+ if (ticketId === undefined)
44
+ return [];
45
+ nextRefresh = tickets[tickets.indexOf(ticketId) + 1] ?? tickets[0];
46
+ if (source.command === undefined)
47
+ throw new Error("ticket-source-not-configured");
48
+ const read = await ticketText(source.command, { ticketId, signal });
49
+ if (read.kind === "failed")
50
+ throw new TicketDependencyReadError("ticket-not-available");
51
+ await check({ ticketId, ticket: read.text, signal });
52
+ return [...waiting.keys()];
53
+ },
54
+ skipped: () => [...waiting].map(([ticketId, dependencies]) => ({
55
+ ticketId,
56
+ reason: "ticket-dependency-open",
57
+ dependencies,
58
+ })),
59
+ };
60
+ }
@@ -1,8 +1,8 @@
1
- import type { DeclaredWorkflow } from "../workflow-declaration.ts";
1
+ import type { DeclaredWorkflow, WorkflowStage } from "../workflow-declaration.ts";
2
2
  import type { Resolver, WorkflowRegistration } from "../service/registry.ts";
3
- export declare const ticketWorkflow = "develop-ticket";
4
- export declare const promptWorkflow = "develop-prompt";
5
- export declare const issueWorkflow = "develop-issue";
3
+ export declare const ticketWorkflow: "develop-ticket";
4
+ export declare const promptWorkflow: "develop-prompt";
5
+ export declare const issueWorkflow: "develop-issue";
6
6
  export declare const ticketCapabilities: string[];
7
7
  export type TicketRegistrations = {
8
8
  readonly kind: "admitted";
@@ -15,6 +15,8 @@ export interface TicketSource {
15
15
  readonly repository: string;
16
16
  readonly command: readonly string[] | undefined;
17
17
  readonly nextCommand?: readonly string[] | undefined;
18
+ readonly githubCommand?: readonly string[] | undefined;
19
+ readonly skipTickets?: readonly string[];
18
20
  }
19
21
  export interface TicketRegistration {
20
22
  readonly workflowId: string;
@@ -23,6 +25,8 @@ export interface TicketRegistration {
23
25
  readonly submission: WorkflowRegistration["submission"];
24
26
  readonly effectLimit: number;
25
27
  readonly capabilities?: readonly string[];
28
+ readonly stages?: readonly WorkflowStage[];
29
+ readonly handoffs?: readonly string[];
26
30
  }
27
31
  export declare function ticketResolver(source: TicketSource): Resolver;
28
32
  export declare function issueResolver(source: TicketSource): Resolver;