cloving 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +251 -20
  2. package/assets/watch-fonts/fira-code-OFL.md +93 -0
  3. package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
  4. package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
  5. package/assets/watch-fonts/heebo-OFL.md +93 -0
  6. package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
  7. package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
  8. package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
  9. package/assets/watch-fonts/manifest.json +112 -0
  10. package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
  11. package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
  12. package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
  13. package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
  14. package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
  15. package/dist/src/authority/commands.d.ts +1 -0
  16. package/dist/src/authority/commands.js +33 -4
  17. package/dist/src/authority/dispatch-decision.d.ts +1 -0
  18. package/dist/src/authority/dispatch-decision.js +7 -4
  19. package/dist/src/authority/events.js +2 -2
  20. package/dist/src/authority/handoff-chain.d.ts +24 -0
  21. package/dist/src/authority/handoff-chain.js +46 -0
  22. package/dist/src/authority/reconciliation.d.ts +3 -0
  23. package/dist/src/authority/reconciliation.js +21 -5
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +5 -0
  26. package/dist/src/broker/actor/authority-queries.d.ts +4 -0
  27. package/dist/src/broker/actor/authority-queries.js +51 -2
  28. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  29. package/dist/src/broker/actor/authority-session.js +3 -4
  30. package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
  31. package/dist/src/broker/actor/effect-delivery.js +9 -0
  32. package/dist/src/broker/authority-admission.js +13 -5
  33. package/dist/src/chats/analysis-batches.d.ts +42 -0
  34. package/dist/src/chats/analysis-batches.js +97 -0
  35. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  36. package/dist/src/chats/analysis-checkpoint.js +31 -0
  37. package/dist/src/chats/analysis-contract.d.ts +80 -0
  38. package/dist/src/chats/analysis-contract.js +16 -0
  39. package/dist/src/chats/analysis-data.d.ts +3 -0
  40. package/dist/src/chats/analysis-data.js +85 -0
  41. package/dist/src/chats/analysis-input.d.ts +18 -0
  42. package/dist/src/chats/analysis-input.js +97 -0
  43. package/dist/src/chats/analysis-links.d.ts +2 -0
  44. package/dist/src/chats/analysis-links.js +25 -0
  45. package/dist/src/chats/analysis-merge.d.ts +16 -0
  46. package/dist/src/chats/analysis-merge.js +134 -0
  47. package/dist/src/chats/analysis-prompt.d.ts +18 -0
  48. package/dist/src/chats/analysis-prompt.js +42 -0
  49. package/dist/src/chats/analysis-proposal.d.ts +4 -0
  50. package/dist/src/chats/analysis-proposal.js +41 -0
  51. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  52. package/dist/src/chats/analysis-provenance.js +99 -0
  53. package/dist/src/chats/analysis-shape.d.ts +4 -0
  54. package/dist/src/chats/analysis-shape.js +235 -0
  55. package/dist/src/chats/analysis.d.ts +3 -0
  56. package/dist/src/chats/analysis.js +42 -0
  57. package/dist/src/chats/corpus-input.d.ts +5 -0
  58. package/dist/src/chats/corpus-input.js +288 -0
  59. package/dist/src/chats/destination.js +3 -1
  60. package/dist/src/chats/import.d.ts +8 -6
  61. package/dist/src/chats/import.js +195 -23
  62. package/dist/src/chats/input-file.d.ts +3 -0
  63. package/dist/src/chats/input-file.js +32 -0
  64. package/dist/src/chats/normalize.d.ts +11 -2
  65. package/dist/src/chats/normalize.js +18 -2
  66. package/dist/src/chats/previous-import.d.ts +5 -0
  67. package/dist/src/chats/previous-import.js +34 -0
  68. package/dist/src/chats/reader.d.ts +7 -1
  69. package/dist/src/chats/reader.js +46 -15
  70. package/dist/src/chats/redact-import.d.ts +14 -0
  71. package/dist/src/chats/redact-import.js +141 -0
  72. package/dist/src/chats/redaction.d.ts +41 -0
  73. package/dist/src/chats/redaction.js +161 -0
  74. package/dist/src/chats/reference-mapping.d.ts +16 -0
  75. package/dist/src/chats/reference-mapping.js +153 -0
  76. package/dist/src/chats/revision-input.d.ts +3 -0
  77. package/dist/src/chats/revision-input.js +180 -0
  78. package/dist/src/chats/revision.d.ts +48 -0
  79. package/dist/src/chats/revision.js +94 -0
  80. package/dist/src/chats/selection.d.ts +25 -0
  81. package/dist/src/chats/selection.js +109 -0
  82. package/dist/src/chats/source.d.ts +23 -0
  83. package/dist/src/chats/source.js +135 -0
  84. package/dist/src/chats/structure.d.ts +57 -0
  85. package/dist/src/chats/structure.js +216 -0
  86. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  87. package/dist/src/chats/workflow-generation-projection.js +75 -0
  88. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  89. package/dist/src/chats/workflow-generation-source.js +124 -0
  90. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  91. package/dist/src/chats/workflow-generation-text.js +74 -0
  92. package/dist/src/chats/workflow-generation.d.ts +69 -0
  93. package/dist/src/chats/workflow-generation.js +142 -0
  94. package/dist/src/cli/arguments.d.ts +8 -0
  95. package/dist/src/cli/arguments.js +94 -13
  96. package/dist/src/cli/client.d.ts +1 -0
  97. package/dist/src/cli/client.js +38 -1
  98. package/dist/src/cli/commands.d.ts +3 -0
  99. package/dist/src/cli/commands.js +29 -11
  100. package/dist/src/cli/main.d.ts +2 -1
  101. package/dist/src/cli/main.js +61 -14
  102. package/dist/src/cli/render.d.ts +3 -0
  103. package/dist/src/cli/render.js +122 -7
  104. package/dist/src/cli/usage-render.d.ts +3 -0
  105. package/dist/src/cli/usage-render.js +28 -0
  106. package/dist/src/cli/usage.d.ts +3 -2
  107. package/dist/src/cli/usage.js +42 -7
  108. package/dist/src/cli/watch-actions.d.ts +10 -0
  109. package/dist/src/cli/watch-actions.js +58 -0
  110. package/dist/src/cli/watch-board-view.d.ts +44 -1
  111. package/dist/src/cli/watch-board-view.js +137 -18
  112. package/dist/src/cli/watch-board.d.ts +7 -1
  113. package/dist/src/cli/watch-board.js +48 -11
  114. package/dist/src/cli/watch-card-client.d.ts +1 -0
  115. package/dist/src/cli/watch-card-client.js +44 -0
  116. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  117. package/dist/src/cli/watch-card-forms.js +33 -0
  118. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  119. package/dist/src/cli/watch-card-reply.js +34 -0
  120. package/dist/src/cli/watch-card-view.d.ts +40 -0
  121. package/dist/src/cli/watch-card-view.js +86 -0
  122. package/dist/src/cli/watch-client.d.ts +1 -0
  123. package/dist/src/cli/watch-client.js +122 -0
  124. package/dist/src/cli/watch-model.d.ts +10 -4
  125. package/dist/src/cli/watch-model.js +61 -67
  126. package/dist/src/cli/watch-page.d.ts +1 -1
  127. package/dist/src/cli/watch-page.js +89 -86
  128. package/dist/src/cli/watch-phases.js +3 -3
  129. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  130. package/dist/src/cli/watch-pr-board.js +26 -0
  131. package/dist/src/cli/watch-pr-command.d.ts +18 -0
  132. package/dist/src/cli/watch-pr-command.js +74 -0
  133. package/dist/src/cli/watch-pr-identity.d.ts +32 -0
  134. package/dist/src/cli/watch-pr-identity.js +87 -0
  135. package/dist/src/cli/watch-pr-observer.d.ts +20 -0
  136. package/dist/src/cli/watch-pr-observer.js +150 -0
  137. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  138. package/dist/src/cli/watch-pr-settings.js +47 -0
  139. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  140. package/dist/src/cli/watch-pull-request.js +130 -0
  141. package/dist/src/cli/watch-reads.d.ts +7 -1
  142. package/dist/src/cli/watch-reads.js +44 -18
  143. package/dist/src/cli/watch-render.d.ts +29 -2
  144. package/dist/src/cli/watch-render.js +11 -3
  145. package/dist/src/cli/watch-run-input.d.ts +14 -0
  146. package/dist/src/cli/watch-run-input.js +49 -0
  147. package/dist/src/cli/watch-server.d.ts +3 -0
  148. package/dist/src/cli/watch-server.js +77 -8
  149. package/dist/src/cli/watch-start-view.d.ts +18 -0
  150. package/dist/src/cli/watch-start-view.js +77 -0
  151. package/dist/src/cli/watch-turn.d.ts +1 -0
  152. package/dist/src/cli/watch-turn.js +8 -3
  153. package/dist/src/cli/watch-usage.d.ts +6 -0
  154. package/dist/src/cli/watch-usage.js +65 -0
  155. package/dist/src/cli/watch-view.d.ts +22 -0
  156. package/dist/src/cli/watch-view.js +111 -19
  157. package/dist/src/cli.js +48 -6
  158. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  159. package/dist/src/daemon/analysis-launch.js +81 -0
  160. package/dist/src/daemon/analysis-token.d.ts +6 -0
  161. package/dist/src/daemon/analysis-token.js +88 -0
  162. package/dist/src/daemon/analysis-worker.d.ts +20 -0
  163. package/dist/src/daemon/analysis-worker.js +271 -0
  164. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  165. package/dist/src/daemon/chats-batch-binding.js +99 -0
  166. package/dist/src/daemon/chats-capture.d.ts +18 -0
  167. package/dist/src/daemon/chats-capture.js +194 -0
  168. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  169. package/dist/src/daemon/chats-checkpoints.js +186 -0
  170. package/dist/src/daemon/chats-files.d.ts +42 -0
  171. package/dist/src/daemon/chats-files.js +252 -0
  172. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  173. package/dist/src/daemon/chats-follow-up.js +180 -0
  174. package/dist/src/daemon/chats-question.d.ts +15 -0
  175. package/dist/src/daemon/chats-question.js +93 -0
  176. package/dist/src/daemon/chats-worker.d.ts +13 -0
  177. package/dist/src/daemon/chats-worker.js +201 -0
  178. package/dist/src/daemon/claude-turn.d.ts +11 -0
  179. package/dist/src/daemon/claude-turn.js +96 -15
  180. package/dist/src/daemon/claude-worker.d.ts +2 -1
  181. package/dist/src/daemon/claude-worker.js +92 -45
  182. package/dist/src/daemon/codex-stream.js +16 -0
  183. package/dist/src/daemon/daemon.d.ts +8 -1
  184. package/dist/src/daemon/daemon.js +51 -14
  185. package/dist/src/daemon/delivery-router.js +6 -0
  186. package/dist/src/daemon/github-calls.d.ts +4 -0
  187. package/dist/src/daemon/github-calls.js +16 -7
  188. package/dist/src/daemon/github-ci.d.ts +6 -0
  189. package/dist/src/daemon/github-ci.js +170 -0
  190. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  191. package/dist/src/daemon/github-merge-call.js +73 -0
  192. package/dist/src/daemon/github-merge.d.ts +3 -5
  193. package/dist/src/daemon/github-merge.js +52 -95
  194. package/dist/src/daemon/github-methods.d.ts +28 -0
  195. package/dist/src/daemon/github-methods.js +84 -16
  196. package/dist/src/daemon/github-operation.d.ts +2 -0
  197. package/dist/src/daemon/github-retry.d.ts +6 -0
  198. package/dist/src/daemon/github-retry.js +49 -0
  199. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  200. package/dist/src/daemon/github-ticket-status.js +294 -0
  201. package/dist/src/daemon/github-worker.js +2 -1
  202. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  203. package/dist/src/daemon/handoff-worker.js +232 -0
  204. package/dist/src/daemon/issue-queue.d.ts +2 -0
  205. package/dist/src/daemon/issue-queue.js +186 -43
  206. package/dist/src/daemon/notify-journal.d.ts +18 -0
  207. package/dist/src/daemon/notify-journal.js +108 -0
  208. package/dist/src/daemon/notify-message.js +2 -1
  209. package/dist/src/daemon/notify.js +176 -37
  210. package/dist/src/daemon/operation-journal.d.ts +1 -0
  211. package/dist/src/daemon/operation-journal.js +4 -2
  212. package/dist/src/daemon/retrying-turn.d.ts +12 -0
  213. package/dist/src/daemon/retrying-turn.js +231 -0
  214. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  215. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  216. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  217. package/dist/src/daemon/ticket-registration.js +76 -22
  218. package/dist/src/daemon/ticket-source.d.ts +4 -1
  219. package/dist/src/daemon/ticket-source.js +8 -2
  220. package/dist/src/decision-visibility.d.ts +2 -0
  221. package/dist/src/decision-visibility.js +10 -0
  222. package/dist/src/execution-pin.d.ts +4 -0
  223. package/dist/src/execution-pin.js +23 -2
  224. package/dist/src/host/start-delivery.d.ts +2 -1
  225. package/dist/src/host/start-delivery.js +1 -1
  226. package/dist/src/host/step-bridge-host.d.ts +1 -0
  227. package/dist/src/host/step-bridge-host.js +16 -0
  228. package/dist/src/host/world-reconciliation.d.ts +3 -0
  229. package/dist/src/host/world-reconciliation.js +9 -0
  230. package/dist/src/host/world-runs.js +14 -1
  231. package/dist/src/issue-identity.d.ts +6 -0
  232. package/dist/src/issue-identity.js +23 -0
  233. package/dist/src/service/queue-runs.d.ts +3 -0
  234. package/dist/src/service/queue-runs.js +39 -0
  235. package/dist/src/service/registry.d.ts +8 -2
  236. package/dist/src/service/registry.js +20 -8
  237. package/dist/src/service/replies.d.ts +2 -0
  238. package/dist/src/service/run-handlers.js +54 -30
  239. package/dist/src/service/service.d.ts +3 -0
  240. package/dist/src/service/service.js +3 -1
  241. package/dist/src/service/submission-handler.d.ts +1 -0
  242. package/dist/src/service/submission-handler.js +92 -13
  243. package/dist/src/service/worker-totals.d.ts +3 -0
  244. package/dist/src/service/worker-totals.js +52 -0
  245. package/dist/src/step-bridge.d.ts +9 -0
  246. package/dist/src/steps/handoff.d.ts +2 -0
  247. package/dist/src/steps/handoff.js +13 -0
  248. package/dist/src/ticket-dependencies.d.ts +1 -0
  249. package/dist/src/ticket-dependencies.js +15 -0
  250. package/dist/src/ticket-status.d.ts +8 -0
  251. package/dist/src/ticket-status.js +48 -0
  252. package/dist/src/token-prices.d.ts +21 -0
  253. package/dist/src/token-prices.js +108 -0
  254. package/dist/src/worker-usage.d.ts +31 -0
  255. package/dist/src/worker-usage.js +44 -0
  256. package/dist/src/workflow-admission.d.ts +1 -0
  257. package/dist/src/workflow-candidate.d.ts +29 -0
  258. package/dist/src/workflow-candidate.js +60 -0
  259. package/dist/src/workflow-contracts.d.ts +4 -0
  260. package/dist/src/workflow-contracts.js +32 -5
  261. package/dist/src/workflow-declaration.d.ts +12 -1
  262. package/dist/src/workflow-declaration.js +71 -13
  263. package/dist/src/workflow-discovery.d.ts +18 -0
  264. package/dist/src/workflow-discovery.js +33 -0
  265. package/dist/src/workflow-refusals.d.ts +6 -0
  266. package/dist/src/workflow-refusals.js +39 -0
  267. package/dist/src/workflows.d.ts +3 -1
  268. package/dist/src/workflows.js +2 -0
  269. package/dist/tools/admit-workflow.d.ts +13 -0
  270. package/dist/tools/admit-workflow.js +94 -0
  271. package/dist/tools/self-host-processes.js +7 -2
  272. package/dist/tools/self-host-project.d.ts +17 -0
  273. package/dist/tools/self-host-project.js +177 -0
  274. package/dist/tools/self-host-workflows.d.ts +1 -1
  275. package/dist/tools/self-host-workflows.js +2 -2
  276. package/dist/tools/self-host.js +30 -84
  277. package/dist/tools/self-update-host.d.ts +7 -0
  278. package/dist/tools/self-update-host.js +48 -1
  279. package/dist/tools/self-update.d.ts +1 -0
  280. package/dist/tools/self-update.js +85 -13
  281. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  282. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  283. package/dist/tools/workflow-bundles.d.ts +20 -0
  284. package/dist/tools/workflow-bundles.js +173 -0
  285. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  286. package/dist/tools/workflow-candidate-counts.js +251 -0
  287. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  288. package/dist/tools/workflow-candidate-program.js +78 -0
  289. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  290. package/dist/tools/workflow-candidate-source.js +107 -0
  291. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  292. package/dist/tools/workflow-candidate-syntax.js +219 -0
  293. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  294. package/dist/tools/workflow-folder-capture.js +216 -0
  295. package/dist/tools/workflow-folders.d.ts +33 -0
  296. package/dist/tools/workflow-folders.js +150 -0
  297. package/dist/tools/workflow-source-effects.d.ts +10 -0
  298. package/dist/tools/workflow-source-effects.js +213 -0
  299. package/dist/workflows/analyze-chats.d.ts +15 -0
  300. package/dist/workflows/analyze-chats.declaration.json +22 -0
  301. package/dist/workflows/analyze-chats.js +250 -0
  302. package/dist/workflows/develop-prd.js +71 -3
  303. package/dist/workflows/develop-ticket.d.ts +1 -7
  304. package/dist/workflows/develop-ticket.js +47 -141
  305. package/dist/workflows/issue-labels.d.ts +3 -1
  306. package/dist/workflows/issue-labels.js +34 -0
  307. package/dist/workflows/ticket-prompts.d.ts +35 -0
  308. package/dist/workflows/ticket-prompts.js +143 -0
  309. package/package.json +25 -5
@@ -1,9 +1,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
  }
@@ -32,6 +32,7 @@ export declare function readRecord(folder: string, name: string): Promise<unknow
32
32
  export declare function writeRecord(folder: string, record: {
33
33
  readonly name: string;
34
34
  readonly value: unknown;
35
+ readonly permissions?: "private";
35
36
  }): Promise<void>;
36
37
  export declare function replied(identity: Identity, recorded: unknown): AdapterOutcome;
37
38
  export declare function interrupted(reason: string): Recorded;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  export function operationKey(text) {
5
5
  return createHash("sha256").update(text).digest("hex").slice(0, 32);
@@ -16,7 +16,9 @@ export async function readRecord(folder, name) {
16
16
  export async function writeRecord(folder, record) {
17
17
  const path = join(folder, record.name);
18
18
  await mkdir(folder, { recursive: true });
19
- await writeFile(`${path}.staged`, JSON.stringify(record.value, null, 2));
19
+ if (record.permissions === "private")
20
+ await chmod(folder, 0o700);
21
+ await writeFile(`${path}.staged`, JSON.stringify(record.value, null, 2), record.permissions === "private" ? { mode: 0o600 } : {});
20
22
  await rename(`${path}.staged`, path);
21
23
  }
22
24
  export function replied(identity, recorded) {
@@ -0,0 +1,12 @@
1
+ import { type TokenPrices } from "../token-prices.ts";
2
+ import { type RunningTurn, type TurnStart } from "./claude-turn.ts";
3
+ export interface RetrySettings {
4
+ readonly tokenPrices?: TokenPrices;
5
+ readonly fallbackModels?: readonly string[];
6
+ readonly retryDelay?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
7
+ }
8
+ interface RetryOptions extends RetrySettings {
9
+ readonly metadata: Readonly<Record<string, unknown>>;
10
+ }
11
+ export declare function startRetriedTurn(start: TurnStart, options: RetryOptions): Promise<RunningTurn>;
12
+ export {};
@@ -0,0 +1,231 @@
1
+ import { join } from "node:path";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { setTimeout as delay } from "node:timers/promises";
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";
7
+ import { capacityRefusal, startTurn, } from "./claude-turn.js";
8
+ import { readRecord, writeRecord } from "./operation-journal.js";
9
+ const waits = [60_000, 180_000];
10
+ const continuation = "An earlier attempt stopped because the model was at capacity; continue from the current state of the work tree.\n\n";
11
+ function written(folder, entry) {
12
+ return writeRecord(folder, { ...entry, permissions: "private" });
13
+ }
14
+ function total(values) {
15
+ if (values.includes(null))
16
+ return null;
17
+ return priceOf(values.reduce((sum, value) => sum + (value ?? 0), 0));
18
+ }
19
+ function combined(attempts) {
20
+ const { ends } = attempts;
21
+ const last = ends.at(-1) ?? { exitCode: null, result: undefined };
22
+ const result = last.result ?? ends.find((end) => end.result)?.result;
23
+ if (result === undefined)
24
+ return last;
25
+ return {
26
+ ...last,
27
+ result: {
28
+ ...result,
29
+ attempts: ends.map((end, index) => attemptUsage(attempts, { end, attempt: index + 1 })),
30
+ ...(last.result === undefined ? { failed: true, text: "" } : {}),
31
+ usage: sumUsage(ends.map((end) => end.result?.usage ?? usageOf(null))),
32
+ costUsd: total(ends.map((end) => end.result?.costUsd ?? null)),
33
+ turns: total(ends.map((end) => end.result?.turns ?? null)),
34
+ },
35
+ };
36
+ }
37
+ function controlled(attempts, start) {
38
+ const turn = startTurn(start);
39
+ const stop = () => {
40
+ turn.stop(attempts.grace.milliseconds);
41
+ };
42
+ attempts.stopping.signal.addEventListener("abort", stop, { once: true });
43
+ return {
44
+ ...turn,
45
+ ended: turn.ended.finally(() => {
46
+ attempts.stopping.signal.removeEventListener("abort", stop);
47
+ }),
48
+ };
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
+ }
83
+ async function launched(attempts) {
84
+ const { start, options, ends } = attempts;
85
+ const attempt = ends.length + 1;
86
+ const selected = attemptCommand(attempts, attempt);
87
+ const folder = start.mode === "analysis" || attempt === 1
88
+ ? start.folder
89
+ : join(start.folder, `attempt-${String(attempt)}`);
90
+ const entry = {
91
+ ...options.metadata,
92
+ model: selected.model,
93
+ ...(selected.fallbackReason === undefined
94
+ ? {}
95
+ : { fallbackReason: selected.fallbackReason }),
96
+ attempt,
97
+ startedAt: attempt === 1 ? attempts.startedAt : new Date().toISOString(),
98
+ };
99
+ await written(start.folder, {
100
+ name: "started.json",
101
+ value: {
102
+ ...entry,
103
+ startedAt: attempts.startedAt,
104
+ phase: "starting",
105
+ processId: null,
106
+ },
107
+ });
108
+ await mkdir(folder, { recursive: true, mode: 0o700 });
109
+ if (attempts.stopping.signal.aborted)
110
+ return undefined;
111
+ const turn = controlled(attempts, {
112
+ ...start,
113
+ command: selected.command,
114
+ folder,
115
+ ...(attempt === 1 ? {} : { activityFolder: start.folder }),
116
+ prompt: start.mode === "analysis" || attempt === 1
117
+ ? start.prompt
118
+ : continuation + start.prompt,
119
+ });
120
+ try {
121
+ const value = { ...entry, phase: "running", processId: turn.processId };
122
+ await written(start.folder, {
123
+ name: "started.json",
124
+ value: { ...value, startedAt: attempts.startedAt },
125
+ });
126
+ await written(start.folder, {
127
+ name: `attempt-${String(attempt)}.json`,
128
+ value,
129
+ });
130
+ return turn;
131
+ }
132
+ catch (error) {
133
+ turn.stop(0);
134
+ await turn.ended;
135
+ throw error;
136
+ }
137
+ }
138
+ async function recordedEnd(attempts, completion) {
139
+ const { start, ends, options } = attempts;
140
+ const { processId, end } = completion;
141
+ const attempt = ends.length;
142
+ const selected = attemptCommand(attempts, attempt);
143
+ const name = `attempt-${String(attempt)}.json`;
144
+ const started = await readRecord(start.folder, name);
145
+ const startedAt = Reflect.get(Object(started), "startedAt");
146
+ if (typeof startedAt !== "string")
147
+ throw new Error("attempt-start-time-missing");
148
+ await written(start.folder, {
149
+ name,
150
+ value: {
151
+ ...options.metadata,
152
+ ...attemptUsage(attempts, { end, attempt }),
153
+ ...(selected.fallbackReason === undefined
154
+ ? {}
155
+ : { fallbackReason: selected.fallbackReason }),
156
+ attempt,
157
+ processId,
158
+ startedAt,
159
+ endedAt: new Date().toISOString(),
160
+ exitCode: end.exitCode,
161
+ capacity: capacityRefusal(end.result),
162
+ },
163
+ });
164
+ }
165
+ async function waited(attempts, milliseconds) {
166
+ const { start, options, stopping, ends } = attempts;
167
+ await written(start.folder, {
168
+ name: "started.json",
169
+ value: {
170
+ ...options.metadata,
171
+ startedAt: attempts.startedAt,
172
+ attempt: ends.length,
173
+ phase: "waiting",
174
+ processId: null,
175
+ waitMilliseconds: milliseconds,
176
+ waitingAt: new Date().toISOString(),
177
+ },
178
+ });
179
+ const wait = options.retryDelay ??
180
+ ((duration, signal) => delay(duration, undefined, { signal }));
181
+ await wait(milliseconds, stopping.signal).catch((error) => {
182
+ if (!stopping.signal.aborted)
183
+ throw error;
184
+ });
185
+ }
186
+ async function finished(attempts, first) {
187
+ const { ends } = attempts;
188
+ let turn = first;
189
+ while (turn !== undefined) {
190
+ const end = await turn.ended;
191
+ ends.push(end);
192
+ await recordedEnd(attempts, { processId: turn.processId, end });
193
+ const milliseconds = waits[ends.length - 1];
194
+ if (cancelled(attempts) ||
195
+ !capacityRefusal(end.result) ||
196
+ milliseconds === undefined)
197
+ break;
198
+ await waited(attempts, milliseconds);
199
+ if (cancelled(attempts))
200
+ return { ...combined(attempts), stopped: true };
201
+ turn = await launched(attempts);
202
+ }
203
+ return {
204
+ ...combined(attempts),
205
+ ...(cancelled(attempts) || turn === undefined ? { stopped: true } : {}),
206
+ };
207
+ }
208
+ function cancelled(attempts) {
209
+ return attempts.stopping.signal.aborted;
210
+ }
211
+ export async function startRetriedTurn(start, options) {
212
+ const attempts = {
213
+ start,
214
+ startedAt: new Date().toISOString(),
215
+ options,
216
+ stopping: new AbortController(),
217
+ ends: [],
218
+ grace: { milliseconds: 2_000 },
219
+ };
220
+ const turn = await launched(attempts);
221
+ if (turn === undefined)
222
+ throw new Error("turn-start-stopped");
223
+ return {
224
+ processId: turn.processId,
225
+ ended: finished(attempts, turn),
226
+ stop: (graceMilliseconds) => {
227
+ attempts.grace.milliseconds = graceMilliseconds;
228
+ attempts.stopping.abort();
229
+ },
230
+ };
231
+ }
@@ -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;