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,10 +1,27 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile, rename, writeFile } from "node:fs/promises";
2
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
5
  import { send } from "../cli/client.js";
6
6
  import { record } from "../protocol.js";
7
7
  import { runIdentity } from "../service/submission-handler.js";
8
+ function savedHold(value) {
9
+ if (value === undefined)
10
+ return undefined;
11
+ const held = record(value);
12
+ if (typeof held?.["mark"] !== "string" ||
13
+ typeof held["startedAt"] !== "number" ||
14
+ !Number.isFinite(held["startedAt"]) ||
15
+ held["startedAt"] <= 0)
16
+ throw new Error("invalid-queue-commands");
17
+ return { mark: held["mark"], startedAt: held["startedAt"] };
18
+ }
19
+ function identifiers(value) {
20
+ if (!Array.isArray(value) ||
21
+ !value.every((id) => typeof id === "string" && id.length > 0))
22
+ throw new Error("invalid-queue-commands");
23
+ return value.map(String);
24
+ }
8
25
  async function journal(directory) {
9
26
  const file = join(directory, "queue-commands.json");
10
27
  const text = await readFile(file, "utf8").catch((error) => {
@@ -13,21 +30,26 @@ async function journal(directory) {
13
30
  throw error;
14
31
  });
15
32
  const value = JSON.parse(text);
16
- if (!Array.isArray(value) ||
17
- !value.every((id) => typeof id === "string" && id.length > 0))
33
+ const saved = record(value);
34
+ const commands = new Set(identifiers(Array.isArray(value) ? value : saved?.["commands"]));
35
+ const failures = Array.isArray(value) ? [] : identifiers(saved?.["failures"]);
36
+ if (failures.length > 3)
18
37
  throw new Error("invalid-queue-commands");
19
- const commands = new Set(value.map(String));
20
- return {
38
+ const pending = {
21
39
  commands,
40
+ failures,
22
41
  retryCommandId: undefined,
42
+ selfUpdateHold: savedHold(saved?.["selfUpdateHold"]),
23
43
  save: async () => {
24
- await writeFile(`${file}.tmp`, JSON.stringify([...commands]) + "\n", {
25
- mode: 0o600,
26
- flush: true,
27
- });
44
+ await writeFile(`${file}.tmp`, JSON.stringify({
45
+ commands: [...commands],
46
+ failures,
47
+ selfUpdateHold: pending.selfUpdateHold,
48
+ }) + "\n", { mode: 0o600, flush: true });
28
49
  await rename(`${file}.tmp`, file);
29
50
  },
30
51
  };
52
+ return pending;
31
53
  }
32
54
  function terminal(run) {
33
55
  return ["completed", "failed", "cancelled"].includes(String(run["state"]));
@@ -54,34 +76,79 @@ async function recover(settings, pending) {
54
76
  await pending.save();
55
77
  }
56
78
  }
57
- async function completed(settings, pending) {
58
- let failure;
59
- let pendingRun = false;
60
- const tracked = [...pending.commands].filter((commandId) => commandId !== pending.retryCommandId);
61
- for (const commandId of tracked) {
62
- const run = await runStatus(settings.runtimeDirectory, commandId);
79
+ async function trackedRuns(runtimeDirectory, commands) {
80
+ if (commands.length === 0)
81
+ return [];
82
+ if (commands.length === 1) {
83
+ const commandId = String(commands[0]);
84
+ const run = await runStatus(runtimeDirectory, commandId);
63
85
  if (run === undefined)
64
86
  throw new Error("queue-run-missing");
65
- if (!terminal(run)) {
66
- pendingRun = true;
67
- continue;
68
- }
69
- const outcome = record(run["outcome"]);
70
- if (run["state"] !== "completed" ||
71
- outcome?.["kind"] !== "handoff_requested")
72
- failure ??= JSON.stringify({
73
- runId: run["runId"],
74
- state: run["state"],
75
- outcome: run["outcome"] ?? null,
76
- });
77
- pending.commands.delete(commandId);
78
- await pending.save();
87
+ return [{ commandId, run }];
79
88
  }
80
- if (failure !== undefined)
81
- throw new Error(failure);
82
- return pendingRun;
89
+ const reply = await send(runtimeDirectory, {
90
+ operation: "status",
91
+ parameters: { runIds: commands.map(runIdentity) },
92
+ });
93
+ if (reply.kind === "error")
94
+ throw new Error(reply.error.code);
95
+ const runs = reply.result["runs"];
96
+ if (!Array.isArray(runs))
97
+ throw new Error("invalid-run-list");
98
+ return commands.map((commandId) => {
99
+ const run = runs
100
+ .map(record)
101
+ .find((item) => item?.["runId"] === runIdentity(commandId));
102
+ if (run === undefined)
103
+ throw new Error("queue-run-missing");
104
+ return { commandId, run };
105
+ });
106
+ }
107
+ function handedOff(run) {
108
+ const outcome = record(run["outcome"]);
109
+ return (run["state"] === "completed" &&
110
+ outcome?.["kind"] === "handoff_requested" &&
111
+ outcome["issueUpdate"] !== "failed");
112
+ }
113
+ class QueueFailureLimit extends Error {
114
+ }
115
+ function checkFailureLimit(pending) {
116
+ if (pending.failures.length >= 3)
117
+ throw new QueueFailureLimit(`3 runs in a row ended without handoff: ${pending.failures.join(", ")}`);
118
+ }
119
+ async function retired(pending, ending) {
120
+ const { commandId, run } = ending;
121
+ pending.commands.delete(commandId);
122
+ if (handedOff(run))
123
+ pending.failures.length = 0;
124
+ else
125
+ pending.failures.push(runIdentity(commandId));
126
+ await pending.save();
127
+ checkFailureLimit(pending);
128
+ if (handedOff(run))
129
+ return;
130
+ const outcome = record(run["outcome"]);
131
+ process.stderr.write(`Queue run ended ${String(outcome?.["kind"] ?? run["state"])}${outcome?.["issueUpdate"] === "failed" ? "; issueUpdate: failed" : ""}: ${runIdentity(commandId)}; continuing\n`);
132
+ }
133
+ async function completed(settings, pending) {
134
+ checkFailureLimit(pending);
135
+ let pendingRuns = 0;
136
+ const endings = [];
137
+ const tracked = [...pending.commands].filter((commandId) => commandId !== pending.retryCommandId);
138
+ for (const { commandId, run } of await trackedRuns(settings.runtimeDirectory, tracked)) {
139
+ if (terminal(run))
140
+ endings.push({ commandId, run });
141
+ else
142
+ pendingRuns += 1;
143
+ }
144
+ endings.sort((first, second) => Number(first.run["terminalSequence"] ?? first.run["latestSequence"] ?? 0) -
145
+ Number(second.run["terminalSequence"] ?? second.run["latestSequence"] ?? 0));
146
+ for (const ending of endings)
147
+ await retired(pending, ending);
148
+ return pendingRuns;
83
149
  }
84
150
  async function active(runtimeDirectory) {
151
+ let count = 0;
85
152
  let after = "";
86
153
  do {
87
154
  const reply = await send(runtimeDirectory, {
@@ -93,17 +160,16 @@ async function active(runtimeDirectory) {
93
160
  const runs = reply.result["runs"];
94
161
  if (!Array.isArray(runs))
95
162
  throw new Error("invalid-run-list");
96
- if (runs.some((value) => {
163
+ count += runs.filter((value) => {
97
164
  const run = record(value);
98
165
  return run?.["workflowId"] === "develop-issue" && !terminal(run);
99
- }))
100
- return true;
166
+ }).length;
101
167
  const next = reply.result["next"];
102
168
  if (next !== null && (typeof next !== "string" || next <= after))
103
169
  throw new Error("invalid-run-page");
104
170
  after = next;
105
171
  } while (after !== null);
106
- return false;
172
+ return count;
107
173
  }
108
174
  async function submit(settings, pending) {
109
175
  const commandId = pending.retryCommandId ?? randomUUID();
@@ -121,38 +187,115 @@ async function submit(settings, pending) {
121
187
  pending.retryCommandId = undefined;
122
188
  if (reply.kind === "result")
123
189
  return;
124
- if (reply.error.code === "resolver-timeout") {
190
+ if ([
191
+ "resolver-timeout",
192
+ "next-ticket-not-available",
193
+ "ticket-not-available",
194
+ ].includes(reply.error.code)) {
125
195
  pending.retryCommandId = commandId;
126
- process.stderr.write(`Queue resolver-timeout: command ${commandId}; will retry\n`);
196
+ process.stderr.write(`Queue ${reply.error.code}: command ${commandId}; will retry\n`);
127
197
  return;
128
198
  }
129
- if (!["no-ready-ticket", "workflow-active"].includes(reply.error.code))
199
+ if (![
200
+ "no-ready-ticket",
201
+ "workflow-active",
202
+ "ticket-active",
203
+ "ticket-dependency-open",
204
+ ].includes(reply.error.code))
130
205
  throw new Error(reply.error.code);
131
206
  pending.commands.delete(commandId);
132
207
  await pending.save();
133
208
  }
209
+ async function pendingUpdate(directory) {
210
+ return readFile(join(directory, "pending-self-update"), "utf8").catch((error) => {
211
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
212
+ return undefined;
213
+ throw error;
214
+ });
215
+ }
216
+ async function holdStarted(pending, observed) {
217
+ const { mark, activeRuns } = observed;
218
+ const timestamp = Number(mark);
219
+ if (!Number.isFinite(timestamp) || timestamp <= 0)
220
+ throw new Error("invalid-pending-self-update");
221
+ if (pending.selfUpdateHold?.mark !== mark || activeRuns > 0) {
222
+ pending.selfUpdateHold = { mark, startedAt: Date.now() };
223
+ await pending.save();
224
+ }
225
+ return pending.selfUpdateHold.startedAt;
226
+ }
227
+ function updateHold(directory, pending) {
228
+ let notice;
229
+ return async (activeRuns = 0) => {
230
+ const mark = await pendingUpdate(directory);
231
+ if (mark === undefined && pending.selfUpdateHold !== undefined) {
232
+ pending.selfUpdateHold = undefined;
233
+ await pending.save();
234
+ }
235
+ if (mark === undefined) {
236
+ notice = undefined;
237
+ return false;
238
+ }
239
+ const startedAt = await holdStarted(pending, { mark, activeRuns });
240
+ const holding = activeRuns > 0 || Date.now() - startedAt < 30 * 60 * 1000;
241
+ const line = holding
242
+ ? "Queue holding for pending self-update\n"
243
+ : "Queue continuing after pending self-update time limit\n";
244
+ const key = mark + line;
245
+ if (notice !== key)
246
+ process.stderr.write(line);
247
+ notice = key;
248
+ return holding;
249
+ };
250
+ }
251
+ async function admission(settings, attempt) {
252
+ const file = join(settings.directory, "queue-submission");
253
+ await writeFile(file, "", { mode: 0o600 });
254
+ try {
255
+ if (!(await attempt.holding()) && !attempt.signal.aborted)
256
+ await submit(settings, attempt.pending);
257
+ }
258
+ finally {
259
+ await rm(file, { force: true });
260
+ }
261
+ }
134
262
  async function cycle(settings, signal) {
135
263
  const pending = await journal(settings.directory);
264
+ if (settings.resetQueue === true) {
265
+ pending.failures.length = 0;
266
+ await pending.save();
267
+ }
268
+ const holding = updateHold(settings.directory, pending);
269
+ await rm(join(settings.directory, "queue-submission"), { force: true });
136
270
  await recover(settings, pending);
137
271
  const stopped = () => signal.aborted;
138
272
  while (!stopped()) {
139
273
  await delay(settings.intervalMilliseconds, undefined, { signal }).catch(() => undefined);
140
274
  if (stopped())
141
275
  return;
142
- if (await completed(settings, pending))
276
+ const pendingRuns = await completed(settings, pending);
277
+ const lanes = settings.lanes ?? 1;
278
+ if (pendingRuns >= lanes) {
279
+ await holding(pendingRuns);
280
+ continue;
281
+ }
282
+ const activeRuns = await active(settings.runtimeDirectory);
283
+ const held = await holding(activeRuns);
284
+ if (activeRuns >= lanes || activeRuns < pendingRuns)
143
285
  continue;
144
- if (await active(settings.runtimeDirectory))
286
+ if (held)
145
287
  continue;
146
288
  if (stopped())
147
289
  return;
148
- await submit(settings, pending);
290
+ await admission(settings, { pending, holding, signal });
149
291
  }
150
292
  }
151
293
  export function issueQueue(settings) {
152
294
  const stopping = new AbortController();
153
295
  const running = cycle(settings, stopping.signal).catch((error) => {
154
296
  const reason = error instanceof Error ? error.message : String(error);
155
- process.stderr.write(`Queue stopped: ${JSON.stringify(reason)}\n`);
297
+ const diagnostic = error instanceof QueueFailureLimit ? reason : JSON.stringify(reason);
298
+ process.stderr.write(`Queue stopped: ${diagnostic}\n`);
156
299
  });
157
300
  return {
158
301
  stop: async () => {
@@ -0,0 +1,18 @@
1
+ import type { Notification } from "./notify-message.ts";
2
+ export interface DeliveryEntry {
3
+ readonly key: string;
4
+ readonly message: Notification | null;
5
+ readonly state: "pending" | "sent" | "failed";
6
+ readonly attempts: number;
7
+ readonly lastAttemptAt: number | null;
8
+ readonly lastFailure: string | null;
9
+ }
10
+ export interface RecordedNotifications {
11
+ readonly entries: Map<string, DeliveryEntry>;
12
+ readonly retiredKeys: Set<string>;
13
+ }
14
+ export declare function recordedNotifications(file: string): Promise<RecordedNotifications | undefined>;
15
+ export declare function saveNotifications(file: string, journal: {
16
+ readonly entries: ReadonlyMap<string, DeliveryEntry>;
17
+ readonly retiredKeys: ReadonlySet<string>;
18
+ }): Promise<void>;
@@ -0,0 +1,108 @@
1
+ import { open, readFile, rename } from "node:fs/promises";
2
+ import { record } from "../protocol.js";
3
+ export async function recordedNotifications(file) {
4
+ const text = await readFile(file, "utf8").catch((error) => {
5
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
6
+ return undefined;
7
+ throw error;
8
+ });
9
+ if (text === undefined)
10
+ return undefined;
11
+ const value = JSON.parse(text);
12
+ const journal = record(value);
13
+ if (journal?.["version"] === 1 && Array.isArray(journal["entries"])) {
14
+ const entries = journal["entries"].map(deliveryEntry);
15
+ const found = new Map(entries.map((entry) => [entry.key, entry]));
16
+ if (found.size !== entries.length)
17
+ throw new Error("invalid-notify-record");
18
+ return {
19
+ entries: found,
20
+ retiredKeys: recordedKeys(journal["retiredKeys"] ?? []),
21
+ };
22
+ }
23
+ const keys = recordedKeys(value);
24
+ return {
25
+ retiredKeys: new Set(),
26
+ entries: new Map([...keys].map((key) => [
27
+ key,
28
+ {
29
+ key,
30
+ message: null,
31
+ state: "sent",
32
+ attempts: 0,
33
+ lastAttemptAt: Date.now(),
34
+ lastFailure: null,
35
+ },
36
+ ])),
37
+ };
38
+ }
39
+ function recordedKeys(value) {
40
+ if (!Array.isArray(value) ||
41
+ !value.every((key) => typeof key === "string"))
42
+ throw new Error("invalid-notify-record");
43
+ return new Set(value.map(String));
44
+ }
45
+ function notification(value) {
46
+ const message = record(value);
47
+ if (message === undefined)
48
+ return false;
49
+ return (["decision", "blocked", "failed"].includes(String(message["event"])) &&
50
+ ["event", "runId", "workflowId", "subject", "body"].every((field) => typeof message[field] === "string"));
51
+ }
52
+ function deliveryState(value) {
53
+ return value === "pending" || value === "sent" || value === "failed";
54
+ }
55
+ function attemptCount(value) {
56
+ return (typeof value === "number" &&
57
+ Number.isInteger(value) &&
58
+ value >= 0 &&
59
+ value <= 5);
60
+ }
61
+ function attemptTime(value) {
62
+ return (value === null ||
63
+ (typeof value === "number" && Number.isSafeInteger(value) && value >= 0));
64
+ }
65
+ function deliveryEntry(value) {
66
+ const { key, message, state, attempts, lastAttemptAt, lastFailure } = record(value) ?? {};
67
+ if (typeof key !== "string" ||
68
+ !deliveryState(state) ||
69
+ !attemptCount(attempts))
70
+ throw new Error("invalid-notify-record");
71
+ if (!attemptTime(lastAttemptAt) ||
72
+ !(lastFailure === null || typeof lastFailure === "string"))
73
+ throw new Error("invalid-notify-record");
74
+ const content = { message, state };
75
+ if (!entryMessage(content))
76
+ throw new Error("invalid-notify-record");
77
+ return {
78
+ key,
79
+ message: content.message,
80
+ state,
81
+ attempts,
82
+ lastAttemptAt,
83
+ lastFailure,
84
+ };
85
+ }
86
+ function entryMessage(entry) {
87
+ return entry.message === null
88
+ ? entry.state === "sent"
89
+ : notification(entry.message);
90
+ }
91
+ export async function saveNotifications(file, journal) {
92
+ const temporary = await open(`${file}.tmp`, "w", 0o600);
93
+ try {
94
+ await temporary.chmod(0o600);
95
+ await temporary.writeFile(JSON.stringify({
96
+ version: 1,
97
+ entries: [...journal.entries.values()],
98
+ ...(journal.retiredKeys.size > 0
99
+ ? { retiredKeys: [...journal.retiredKeys] }
100
+ : {}),
101
+ }) + "\n");
102
+ await temporary.sync();
103
+ }
104
+ finally {
105
+ await temporary.close();
106
+ }
107
+ await rename(`${file}.tmp`, file);
108
+ }
@@ -1,3 +1,4 @@
1
+ import { operationalDecision } from "../decision-visibility.js";
1
2
  import { decisionLines, runStatus } from "../cli/render.js";
2
3
  import { record } from "../protocol.js";
3
4
  function body(text, commands) {
@@ -17,7 +18,7 @@ function message(run, content) {
17
18
  };
18
19
  }
19
20
  export function decisionNotification(run, decision) {
20
- const lines = decisionLines(decision);
21
+ const lines = decisionLines(operationalDecision(decision, String(run["workflowId"])));
21
22
  const allowed = decision["allowed"];
22
23
  const count = Array.isArray(allowed) ? allowed.length : 0;
23
24
  const split = lines.length - count;