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
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createWriteStream, mkdtempSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { codexStream } from "./codex-stream.js";
5
- import { priceOf, usageOf } from "../worker-usage.js";
5
+ import { priceOf, usageOf, } from "../worker-usage.js";
6
6
  import { commandModel } from "../token-prices.js";
7
7
  import { analysisCommand, analysisEnvironment } from "./analysis-launch.js";
8
8
  function parsed(line) {
@@ -1,11 +1,9 @@
1
- import { type TokenPrices } from "../token-prices.ts";
2
1
  import { type TurnProfiles } from "../turn-profile.ts";
3
2
  import type { Delivery } from "../broker/actor/delivery-intents.ts";
4
3
  import { type Worker } from "./claude-turn.ts";
5
4
  import { type RetrySettings } from "./retrying-turn.ts";
6
5
  import { type WorkerReport } from "./operation-journal.ts";
7
6
  export interface ClaudeWorkerSettings extends RetrySettings {
8
- readonly tokenPrices?: TokenPrices;
9
7
  readonly worker?: Worker;
10
8
  readonly profiles?: TurnProfiles;
11
9
  readonly workflowId?: (runId: string) => Promise<string | undefined>;
@@ -1,8 +1,8 @@
1
1
  import { join } from "node:path";
2
2
  import { ticketStatus } from "../ticket-status.js";
3
+ import { sumPrices } from "../worker-usage.js";
3
4
  import { analysisWorker } from "./analysis-worker.js";
4
5
  import { analysisMethod } from "./analysis-launch.js";
5
- import { estimatedCostUsd, priceForModel, } from "../token-prices.js";
6
6
  import { profileCommand, } from "../turn-profile.js";
7
7
  import {} from "./claude-turn.js";
8
8
  import { startRetriedTurn } from "./retrying-turn.js";
@@ -41,8 +41,7 @@ async function reconciled(parts, intent) {
41
41
  await written(folder, { name: "result.json", value: ending });
42
42
  return replied({ receipt: key, worker }, ending);
43
43
  }
44
- async function resultText(tree, turn) {
45
- const { result, settings } = turn;
44
+ async function resultText(tree, result) {
46
45
  const fields = {
47
46
  ticketStatus: ticketStatus(result.text),
48
47
  summary: result.text.slice(0, claudeLimits.summaryCharacters),
@@ -51,10 +50,8 @@ async function resultText(tree, turn) {
51
50
  costUsd: result.costUsd,
52
51
  usage: result.usage,
53
52
  model: result.model,
54
- estimatedCostUsd: estimatedCostUsd(result.usage, {
55
- worker: settings.worker ?? "claude",
56
- price: priceForModel(settings.tokenPrices ?? {}, result.model),
57
- }),
53
+ ...(result.attempts === undefined ? {} : { attempts: result.attempts }),
54
+ estimatedCostUsd: sumPrices((result.attempts ?? []).map((attempt) => attempt.estimatedCostUsd)),
58
55
  sessionId: result.sessionId,
59
56
  turns: result.turns,
60
57
  };
@@ -84,9 +81,7 @@ async function resultText(tree, turn) {
84
81
  async function outcome(running, end) {
85
82
  const endedAt = new Date().toISOString();
86
83
  const { result } = end;
87
- const report = result === undefined
88
- ? {}
89
- : await resultText(running.tree, { result, settings: running.settings });
84
+ const report = result === undefined ? {} : await resultText(running.tree, result);
90
85
  const { text, reason } = report;
91
86
  if (reason === undefined &&
92
87
  text !== undefined &&
@@ -104,7 +99,7 @@ async function outcome(running, end) {
104
99
  }
105
100
  async function concluded(parts, running) {
106
101
  const { intent, key } = running;
107
- const recorded = await outcome({ ...running, settings: parts.settings }, await running.turn.ended);
102
+ const recorded = await outcome(running, await running.turn.ended);
108
103
  await written(running.folder, { name: "result.json", value: recorded });
109
104
  await parts.settings.report(reportOf({ intent, commandId: `claude-turn:${key}`, worker }, recorded));
110
105
  }
@@ -3,8 +3,10 @@ import { type WorkflowHandler } from "../host/world-host.ts";
3
3
  import type { TurnProfiles } from "../turn-profile.ts";
4
4
  import type { TokenPrices } from "../token-prices.ts";
5
5
  import type { DeclaredWorkflow } from "../workflow-declaration.ts";
6
+ import type { WorkflowRefusal } from "../workflow-refusals.ts";
6
7
  import type { Worker } from "./claude-turn.ts";
7
8
  export interface DaemonSettings {
9
+ readonly fallbackModels?: readonly string[];
8
10
  readonly tokenPrices?: TokenPrices;
9
11
  readonly worker?: Worker;
10
12
  readonly profiles?: TurnProfiles;
@@ -13,6 +15,7 @@ export interface DaemonSettings {
13
15
  readonly repository: string;
14
16
  readonly manifests: Readonly<Record<string, string | undefined>>;
15
17
  readonly declarations?: readonly DeclaredWorkflow[];
18
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
16
19
  readonly workflows: Readonly<Record<string, WorkflowFunction | undefined>>;
17
20
  readonly handler: WorkflowHandler;
18
21
  readonly command: readonly string[];
@@ -21,6 +24,7 @@ export interface DaemonSettings {
21
24
  readonly notifyCommand?: readonly string[];
22
25
  readonly notifyMilliseconds?: number;
23
26
  readonly queueMilliseconds?: number;
27
+ readonly queueLanes?: number;
24
28
  readonly resetQueue?: boolean;
25
29
  readonly githubCommand?: readonly string[];
26
30
  readonly checkCommand?: readonly string[];
@@ -46,4 +50,4 @@ export declare const bridgeTiming: {
46
50
  pollMilliseconds: number;
47
51
  retryMilliseconds: number;
48
52
  };
49
- export declare function startDaemon(settings: DaemonSettings): Promise<DaemonOpening>;
53
+ export declare function startDaemon(givenSettings: DaemonSettings): Promise<DaemonOpening>;
@@ -1,4 +1,4 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { mkdir, realpath } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { startDelivery, } from "../host/start-delivery.js";
4
4
  import { registerStepBridge, sdkStepCall, stepBridge, } from "../host/step-bridge-host.js";
@@ -6,8 +6,10 @@ import { startHost } from "../host/world-host.js";
6
6
  import { worldLoop } from "../host/world-reconciliation.js";
7
7
  import { cancelled, workflowOutcome, taggedRun, taggedRuns, } from "../host/world-runs.js";
8
8
  import { startService, } from "../service/service.js";
9
+ import { workflowPin } from "../broker/workflow-pin.js";
9
10
  import { claudeWorker } from "./claude-worker.js";
10
11
  import { deliveryRouter } from "./delivery-router.js";
12
+ import { handoffWorker } from "./handoff-worker.js";
11
13
  import { githubWorker } from "./github-worker.js";
12
14
  import { chatsWorker } from "./chats-worker.js";
13
15
  import { worldDirectory } from "./host-environment.js";
@@ -72,11 +74,16 @@ function githubOf(settings, given) {
72
74
  }
73
75
  function adapters(settings, options) {
74
76
  const { directory, repository, command } = settings;
75
- const { given, table } = options;
76
- const workflows = Object.fromEntries(table.map((row) => [row.workflowId, settings.workflows[row.functionName]]));
77
+ const { given, table, registrations } = options;
78
+ const functions = Object.fromEntries(table.map((row) => [row.workflowId, settings.workflows[row.functionName]]));
79
+ const workflows = Object.fromEntries(registrations.map((registration) => [
80
+ workflowPin(registration.manifest),
81
+ functions[registration.workflowId],
82
+ ]));
77
83
  const document = async (digest) => (await given).document(digest);
78
84
  const report = async (effect) => (await given).reportEffect(effect);
79
85
  const worker = claudeWorker({
86
+ fallbackModels: settings.fallbackModels ?? [],
80
87
  tokenPrices: settings.tokenPrices ?? {},
81
88
  worker: settings.worker ?? "claude",
82
89
  profiles: settings.profiles ?? {},
@@ -99,18 +106,36 @@ function adapters(settings, options) {
99
106
  runs: taggedRuns,
100
107
  cancel: cancelled,
101
108
  input: async (runId) => (await given).input(runId),
102
- workflow: async (runId) => workflows[String(await (await given).workflowId(runId))],
109
+ workflow: ({ workflow }) => Promise.resolve(workflows[workflow]),
103
110
  });
104
- return { worker, github, runs, chats };
111
+ const handoff = handoffOf(settings, given);
112
+ return { worker, github, runs, chats, handoff };
105
113
  }
106
- export async function startDaemon(settings) {
107
- const { directory } = settings;
114
+ function handoffOf(settings, given) {
115
+ return handoffWorker({
116
+ runtimeDirectory: settings.runtimeDirectory,
117
+ journal: join(settings.directory, "handoffs"),
118
+ document: async (digest) => (await given).document(digest),
119
+ runStatus: async (runId) => (await (await given).state()).runs[runId]?.status,
120
+ });
121
+ }
122
+ async function daemonRegistrations(settings) {
108
123
  const table = ticketRegistrationTable({
109
124
  repository: settings.repository,
110
125
  command: settings.ticketCommand,
111
126
  nextCommand: settings.nextCommand,
127
+ githubCommand: settings.githubCommand,
112
128
  }, settings.declarations);
113
129
  const admitted = await ticketRegistrations(settings.manifests, { table });
130
+ return { table, admitted };
131
+ }
132
+ export async function startDaemon(givenSettings) {
133
+ const settings = {
134
+ ...givenSettings,
135
+ repository: await realpath(givenSettings.repository),
136
+ };
137
+ const { directory } = settings;
138
+ const { table, admitted } = await daemonRegistrations(settings);
114
139
  if (admitted.kind === "rejected")
115
140
  return {
116
141
  kind: "refused",
@@ -118,25 +143,27 @@ export async function startDaemon(settings) {
118
143
  detail: admitted.detail,
119
144
  };
120
145
  const given = Promise.withResolvers();
121
- const { worker, github, runs, chats } = adapters(settings, {
146
+ const { worker, github, runs, chats, handoff } = adapters(settings, {
122
147
  given: given.promise,
123
148
  table,
149
+ registrations: admitted.registrations,
124
150
  });
125
151
  await mkdir(join(directory, "state"), { recursive: true });
126
152
  await mkdir(settings.runtimeDirectory, { recursive: true, mode: 0o700 });
127
153
  const opening = await startService({
128
154
  stateDirectory: join(directory, "state"),
129
155
  runtimeDirectory: settings.runtimeDirectory,
156
+ workflowRefusals: settings.workflowRefusals ?? [],
130
157
  workflows: admitted.registrations.map((registration) => ({
131
158
  ...registration,
132
159
  ...(settings.queueMilliseconds !== undefined &&
133
160
  registration.workflowId === "develop-issue"
134
- ? { admission: "when-idle" }
161
+ ? { admission: settings.queueLanes ?? 1 }
135
162
  : {}),
136
163
  })),
137
164
  delivery: deliveryRouter({
138
165
  runs,
139
- effects: { claude: worker, github, chats },
166
+ effects: { claude: worker, github, chats, handoff },
140
167
  }),
141
168
  effects: { leaseMilliseconds: turnLeaseMilliseconds },
142
169
  host: (ports) => {
@@ -181,6 +208,7 @@ async function runningDaemon(settings, resources) {
181
208
  directory: settings.directory,
182
209
  runtimeDirectory: settings.runtimeDirectory,
183
210
  intervalMilliseconds: settings.queueMilliseconds,
211
+ lanes: settings.queueLanes ?? 1,
184
212
  resetQueue: settings.resetQueue ?? false,
185
213
  });
186
214
  return {
@@ -25,6 +25,12 @@ export function deliveryRouter(routes) {
25
25
  return runs.reconcile(intent);
26
26
  return workerOf(routes, intent)?.reconcile(intent, signal) ?? refusal();
27
27
  },
28
+ retry: (intent, signal) => {
29
+ if (!protectedEffect(intent))
30
+ return refusal();
31
+ return (workerOf(routes, intent)?.retry?.(intent, signal) ??
32
+ Promise.resolve({ kind: "unknown" }));
33
+ },
28
34
  stop: (intent, signal) => {
29
35
  if (!protectedEffect(intent))
30
36
  return runs.stop(intent);
@@ -24,6 +24,10 @@ export interface Finding {
24
24
  readonly line: number | null;
25
25
  readonly body: string;
26
26
  }
27
+ export declare function githubText(text: string, settings: {
28
+ readonly environment?: NodeJS.ProcessEnv;
29
+ readonly privateTexts?: readonly string[];
30
+ }): string;
27
31
  export declare function calls(settings: CallSettings, signal: AbortSignal): Calls;
28
32
  export declare function scoreOf(comments: unknown, commit: string): Scored | undefined;
29
33
  export declare function findingsOf(comments: unknown, commit: string): readonly Finding[];
@@ -22,17 +22,26 @@ function privateEnvironment(environment) {
22
22
  function failureDetail(error, place) {
23
23
  const stderr = field(error, "stderr");
24
24
  const text = typeof stderr === "string" ? stderr : "";
25
- const secrets = Object.entries(place.environment)
25
+ return {
26
+ detail: githubText(text, place).slice(-2_000),
27
+ status: Number(/\(HTTP (\d{3})\)/.exec(text)?.[1]),
28
+ };
29
+ }
30
+ export function githubText(text, settings) {
31
+ const environment = { ...process.env, ...settings.environment };
32
+ const secrets = Object.entries(environment)
26
33
  .filter(([name]) => /TOKEN|SECRET|PASSWORD|KEY/i.test(name))
27
34
  .map(([, value]) => value);
28
- const hidden = [...secrets, ...place.privateTexts]
35
+ const privateTexts = [
36
+ ...(settings.privateTexts ?? []),
37
+ ...privateEnvironment(environment),
38
+ ];
39
+ const hidden = [...secrets, ...privateTexts]
29
40
  .filter((value) => typeof value === "string" && value !== "")
30
41
  .sort((left, right) => right.length - left.length);
31
- const detail = hidden.reduce((redacted, value) => redacted.replaceAll(value, "[hidden]"), text);
32
- return {
33
- detail: detail.slice(-2_000),
34
- status: Number(/\(HTTP (\d{3})\)/.exec(text)?.[1]),
35
- };
42
+ return hidden.reduce((redacted, value) => redacted.replaceAll(privateTexts.includes(value)
43
+ ? new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi")
44
+ : value, "[hidden]"), text);
36
45
  }
37
46
  async function output(command, place) {
38
47
  try {
@@ -0,0 +1,6 @@
1
+ import { type Operation } from "./github-operation.ts";
2
+ export interface CiResult {
3
+ readonly reason: "ci-failed" | "ci-pending";
4
+ readonly output: string;
5
+ }
6
+ export declare function waitForCi(operation: Operation, commit: string): Promise<CiResult | undefined>;
@@ -0,0 +1,170 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { githubText } from "./github-calls.js";
3
+ import { githubRead } from "./github-retry.js";
4
+ import { field, githubTiming, repositoryPath, } from "./github-operation.js";
5
+ function isCheckRun(value) {
6
+ const conclusion = field(value, "conclusion");
7
+ const details = field(value, "details_url");
8
+ const app = field(value, "app");
9
+ return (typeof field(value, "id") === "number" &&
10
+ typeof field(value, "name") === "string" &&
11
+ typeof field(value, "status") === "string" &&
12
+ (conclusion === null || typeof conclusion === "string") &&
13
+ (details === null || typeof details === "string") &&
14
+ (app === null || typeof field(app, "slug") === "string"));
15
+ }
16
+ const passing = new Set(["success", "neutral", "skipped"]);
17
+ function latestRuns(checks, required) {
18
+ const latest = new Map();
19
+ for (const check of checks) {
20
+ const name = check.name;
21
+ if (required === undefined
22
+ ? check.app?.slug !== "github-actions"
23
+ : !required.has(name))
24
+ continue;
25
+ const previous = latest.get(name);
26
+ if (previous === undefined || check.id > previous.id)
27
+ latest.set(name, check);
28
+ }
29
+ return [...latest.values()];
30
+ }
31
+ function list(value) {
32
+ return Array.isArray(value) ? value.map((item) => item) : [];
33
+ }
34
+ async function checkRuns(operation, commit) {
35
+ const pages = JSON.parse(await githubRead(operation, [
36
+ "api",
37
+ "--paginate",
38
+ "--slurp",
39
+ `${repositoryPath}/commits/${commit}/check-runs?filter=all&per_page=100`,
40
+ ]));
41
+ if (!Array.isArray(pages) || pages.length === 0)
42
+ throw new Error("github-call-failed");
43
+ return list(pages).flatMap((page) => {
44
+ const checks = field(page, "check_runs");
45
+ if (!Array.isArray(checks) || !checks.every(isCheckRun))
46
+ throw new Error("github-call-failed");
47
+ return checks;
48
+ });
49
+ }
50
+ function unavailable(operation, error) {
51
+ operation.signal.throwIfAborted();
52
+ const detail = field(error, "detail");
53
+ return `CI diagnostics unavailable: ${githubText(typeof detail === "string" ? detail : "GitHub logs could not be read", operation.settings)}`;
54
+ }
55
+ async function failedSteps(operation, place) {
56
+ try {
57
+ const view = JSON.parse(await operation.calls.gh(["run", "view", place.run, "--json", "jobs"]));
58
+ return list(field(view, "jobs"))
59
+ .filter((job) => String(field(job, "databaseId")) === place.job)
60
+ .flatMap((job) => list(field(job, "steps")))
61
+ .filter((step) => typeof field(step, "conclusion") === "string" &&
62
+ !passing.has(String(field(step, "conclusion"))))
63
+ .map((step) => `Failed step: ${String(field(step, "name"))}`)
64
+ .join("\n");
65
+ }
66
+ catch (error) {
67
+ return unavailable(operation, error);
68
+ }
69
+ }
70
+ async function failedCheck(operation, check) {
71
+ const summary = [
72
+ `Failed check: ${check.name} (${check.conclusion ?? "null"})`,
73
+ ];
74
+ const details = check.details_url ?? "";
75
+ const run = /\/actions\/runs\/(\d+)/.exec(details)?.[1];
76
+ let log = "Failed log unavailable.";
77
+ if (run !== undefined) {
78
+ const job = /\/job\/(\d+)/.exec(details)?.[1];
79
+ summary.push(await failedSteps(operation, { run, job }));
80
+ log = await failedLog(operation, { run, job });
81
+ }
82
+ return {
83
+ summary: githubText(summary.join("\n"), operation.settings),
84
+ log: githubText(log, operation.settings),
85
+ };
86
+ }
87
+ async function failedLog(operation, place) {
88
+ try {
89
+ return await operation.calls.gh([
90
+ "run",
91
+ "view",
92
+ place.run,
93
+ "--log-failed",
94
+ ...(place.job === undefined ? [] : ["--job", place.job]),
95
+ ]);
96
+ }
97
+ catch (error) {
98
+ operation.signal.throwIfAborted();
99
+ if (place.job === undefined)
100
+ return unavailable(operation, error);
101
+ return operation.calls
102
+ .gh(["api", `${repositoryPath}/actions/jobs/${place.job}/logs`])
103
+ .catch((failure) => unavailable(operation, failure));
104
+ }
105
+ }
106
+ async function failedOutput(operation, checks) {
107
+ const reports = [];
108
+ for (const check of checks)
109
+ reports.push(await failedCheck(operation, check));
110
+ const summary = reports.map((report) => report.summary).join("\n");
111
+ const available = Math.max(0, 6_000 - summary.length - reports.length);
112
+ const allowance = Math.floor(available / reports.length);
113
+ return [
114
+ summary,
115
+ ...reports.map((report) => allowance === 0 ? "" : report.log.slice(-allowance)),
116
+ ]
117
+ .join("\n")
118
+ .slice(0, 6_000);
119
+ }
120
+ function requiredChecks(operation) {
121
+ const setting = operation.settings.environment?.["CLOVING_REQUIRED_CHECKS"] ??
122
+ process.env["CLOVING_REQUIRED_CHECKS"];
123
+ if (setting === undefined)
124
+ return undefined;
125
+ const names = JSON.parse(setting);
126
+ if (!Array.isArray(names) ||
127
+ !names.every((name) => typeof name === "string" && name.trim() !== ""))
128
+ throw new Error("invalid-payload");
129
+ return new Set(names);
130
+ }
131
+ function pendingChecks(checks, names) {
132
+ return [...names].filter((name) => !checks.some((check) => check.name === name && check.status === "completed"));
133
+ }
134
+ export async function waitForCi(operation, commit) {
135
+ const { pollMilliseconds: poll, ciGraceMilliseconds: grace, ciLimitMilliseconds: limit, } = {
136
+ ...githubTiming,
137
+ ciGraceMilliseconds: 60_000,
138
+ ciLimitMilliseconds: 5_400_000,
139
+ ...operation.settings.timing,
140
+ };
141
+ const started = Date.now();
142
+ const required = requiredChecks(operation);
143
+ const names = new Set(required);
144
+ for (;;) {
145
+ const checks = latestRuns(await checkRuns(operation, commit), required);
146
+ for (const check of checks)
147
+ names.add(check.name);
148
+ const failed = checks.filter((check) => check.status === "completed" && !passing.has(check.conclusion ?? ""));
149
+ if (failed.length > 0)
150
+ return {
151
+ reason: "ci-failed",
152
+ output: await failedOutput(operation, failed),
153
+ };
154
+ const pending = pendingChecks(checks, names);
155
+ if (pending.length === 0 &&
156
+ (required !== undefined ||
157
+ names.size > 0 ||
158
+ Date.now() - started >= grace))
159
+ return undefined;
160
+ const remaining = limit - (Date.now() - started);
161
+ if (remaining <= 0)
162
+ return {
163
+ reason: "ci-pending",
164
+ output: `CI wait limit reached for ${commit}. Pending checks: ${pending.join(", ") || "waiting for GitHub Actions checks"}`,
165
+ };
166
+ await delay(Math.min(poll, remaining), undefined, {
167
+ signal: operation.signal,
168
+ });
169
+ }
170
+ }
@@ -0,0 +1,14 @@
1
+ import { type Operation } from "./github-operation.ts";
2
+ export interface ReviewedPull {
3
+ readonly number: number;
4
+ readonly commit: string;
5
+ }
6
+ export declare function knownMerge(operation: Operation, expected: ReviewedPull & {
7
+ readonly updated: string;
8
+ }): Promise<{
9
+ readonly head: string;
10
+ readonly merged: string | undefined;
11
+ }>;
12
+ export declare function mergePull(operation: Operation, pull: ReviewedPull & {
13
+ readonly updated: string;
14
+ }): Promise<string>;
@@ -0,0 +1,73 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { field, githubTiming, repositoryPath, } from "./github-operation.js";
3
+ import { githubRead, retryGithub } from "./github-retry.js";
4
+ function commitOf(value, name) {
5
+ if (typeof value !== "object" || value === null)
6
+ throw new Error("github-call-failed");
7
+ const commit = Reflect.get(value, name);
8
+ if (typeof commit !== "string")
9
+ throw new Error("github-call-failed");
10
+ return commit;
11
+ }
12
+ export async function knownMerge(operation, expected) {
13
+ const pull = JSON.parse(await githubRead(operation, [
14
+ "api",
15
+ `${repositoryPath}/pulls/${String(expected.number)}`,
16
+ ]));
17
+ const head = commitOf(field(pull, "head"), "sha");
18
+ if (field(pull, "merged") !== true)
19
+ return { head, merged: undefined };
20
+ if (head !== expected.commit && head !== expected.updated)
21
+ throw new Error("merged-other-commit");
22
+ return { head, merged: commitOf(pull, "merge_commit_sha") };
23
+ }
24
+ async function mergedNow(operation, pull) {
25
+ const reply = JSON.parse(await operation.calls.gh([
26
+ "api",
27
+ "-X",
28
+ "PUT",
29
+ `${repositoryPath}/pulls/${String(pull.number)}/merge`,
30
+ "-f",
31
+ "merge_method=merge",
32
+ "-f",
33
+ `sha=${pull.commit}`,
34
+ ]));
35
+ return commitOf(reply, "sha");
36
+ }
37
+ async function waitForHead(operation, pull) {
38
+ const timing = operation.settings.timing ?? githubTiming;
39
+ for (let attempt = 0; attempt < 30; attempt += 1) {
40
+ if (attempt > 0)
41
+ await delay(timing.pollMilliseconds, undefined, {
42
+ signal: operation.signal,
43
+ });
44
+ const current = JSON.parse(await githubRead(operation, [
45
+ "api",
46
+ `${repositoryPath}/pulls/${String(pull.number)}`,
47
+ ]));
48
+ if (commitOf(field(current, "head"), "sha") === pull.commit)
49
+ return;
50
+ }
51
+ throw new Error("head-not-updated");
52
+ }
53
+ export function mergePull(operation, pull) {
54
+ return retryGithub(operation, {
55
+ action: async (attempt) => {
56
+ const known = attempt > 0 ? await knownMerge(operation, pull) : undefined;
57
+ if (known?.merged !== undefined)
58
+ return known.merged;
59
+ const updated = { number: pull.number, commit: pull.updated };
60
+ if (pull.updated !== pull.commit)
61
+ await waitForHead(operation, updated);
62
+ return mergedNow(operation, updated);
63
+ },
64
+ statuses: pull.updated === pull.commit ? [] : [405, 409],
65
+ }).catch(async (error) => {
66
+ if (!(error instanceof Error) || error.message !== "github-network-failed")
67
+ throw error;
68
+ const known = await knownMerge(operation, pull);
69
+ if (known.merged !== undefined)
70
+ return known.merged;
71
+ throw error;
72
+ });
73
+ }
@@ -1,8 +1,6 @@
1
1
  import { type Operation } from "./github-operation.ts";
2
- interface ReviewedPull {
3
- readonly number: number;
4
- readonly commit: string;
5
- }
2
+ import { type CiResult } from "./github-ci.ts";
3
+ import { type ReviewedPull } from "./github-merge-call.ts";
6
4
  interface SuccessfulMerge {
7
5
  readonly number: number;
8
6
  readonly commit: string;
@@ -13,6 +11,6 @@ interface MergeRepair {
13
11
  readonly reason: "check-failed" | "merge-conflict";
14
12
  readonly output: string;
15
13
  }
16
- export type MergeResult = SuccessfulMerge | MergeRepair;
14
+ export type MergeResult = SuccessfulMerge | MergeRepair | CiResult;
17
15
  export declare function merge(operation: Operation, pull: ReviewedPull): Promise<MergeResult>;
18
16
  export {};