cloving 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +121 -21
  2. package/assets/watch-fonts/fira-code-OFL.md +93 -0
  3. package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
  4. package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
  5. package/assets/watch-fonts/heebo-OFL.md +93 -0
  6. package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
  7. package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
  8. package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
  9. package/assets/watch-fonts/manifest.json +112 -0
  10. package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
  11. package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
  12. package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
  13. package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
  14. package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
  15. package/dist/src/authority/commands.d.ts +1 -0
  16. package/dist/src/authority/commands.js +25 -4
  17. package/dist/src/authority/dispatch-decision.d.ts +1 -0
  18. package/dist/src/authority/dispatch-decision.js +7 -4
  19. package/dist/src/authority/events.js +2 -2
  20. package/dist/src/authority/handoff-chain.d.ts +24 -0
  21. package/dist/src/authority/handoff-chain.js +46 -0
  22. package/dist/src/authority/reconciliation.d.ts +1 -0
  23. package/dist/src/authority/reconciliation.js +18 -11
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +3 -0
  26. package/dist/src/broker/actor/authority-queries.d.ts +4 -0
  27. package/dist/src/broker/actor/authority-queries.js +51 -2
  28. package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
  29. package/dist/src/broker/actor/effect-delivery.js +9 -0
  30. package/dist/src/broker/authority-admission.js +13 -5
  31. package/dist/src/chats/analysis-batches.d.ts +18 -0
  32. package/dist/src/chats/analysis-batches.js +15 -1
  33. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  34. package/dist/src/chats/analysis-checkpoint.js +31 -0
  35. package/dist/src/chats/analysis-input.d.ts +7 -1
  36. package/dist/src/chats/analysis-input.js +20 -6
  37. package/dist/src/chats/analysis-merge.d.ts +16 -0
  38. package/dist/src/chats/analysis-merge.js +134 -0
  39. package/dist/src/chats/analysis-prompt.d.ts +8 -0
  40. package/dist/src/chats/analysis-prompt.js +27 -1
  41. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  42. package/dist/src/chats/analysis-proposal.js +9 -0
  43. package/dist/src/chats/analysis-shape.d.ts +1 -0
  44. package/dist/src/chats/analysis-shape.js +11 -4
  45. package/dist/src/chats/reference-mapping.d.ts +16 -0
  46. package/dist/src/chats/reference-mapping.js +153 -0
  47. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  48. package/dist/src/chats/workflow-generation-projection.js +75 -0
  49. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  50. package/dist/src/chats/workflow-generation-source.js +124 -0
  51. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  52. package/dist/src/chats/workflow-generation-text.js +74 -0
  53. package/dist/src/chats/workflow-generation.d.ts +69 -0
  54. package/dist/src/chats/workflow-generation.js +142 -0
  55. package/dist/src/cli/render.d.ts +1 -0
  56. package/dist/src/cli/render.js +60 -13
  57. package/dist/src/cli/usage.js +5 -2
  58. package/dist/src/cli/watch-board-view.d.ts +12 -1
  59. package/dist/src/cli/watch-board-view.js +57 -11
  60. package/dist/src/cli/watch-board.d.ts +7 -1
  61. package/dist/src/cli/watch-board.js +42 -10
  62. package/dist/src/cli/watch-client.d.ts +1 -1
  63. package/dist/src/cli/watch-client.js +7 -3
  64. package/dist/src/cli/watch-model.d.ts +10 -4
  65. package/dist/src/cli/watch-model.js +35 -58
  66. package/dist/src/cli/watch-page.js +21 -4
  67. package/dist/src/cli/watch-phases.js +3 -3
  68. package/dist/src/cli/watch-pr-command.d.ts +5 -2
  69. package/dist/src/cli/watch-pr-command.js +41 -4
  70. package/dist/src/cli/watch-pr-identity.d.ts +14 -2
  71. package/dist/src/cli/watch-pr-identity.js +25 -0
  72. package/dist/src/cli/watch-pr-observer.d.ts +1 -0
  73. package/dist/src/cli/watch-pr-observer.js +48 -6
  74. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  75. package/dist/src/cli/watch-pull-request.js +130 -0
  76. package/dist/src/cli/watch-reads.d.ts +7 -1
  77. package/dist/src/cli/watch-reads.js +42 -20
  78. package/dist/src/cli/watch-render.d.ts +14 -1
  79. package/dist/src/cli/watch-render.js +2 -1
  80. package/dist/src/cli/watch-server.js +33 -2
  81. package/dist/src/cli/watch-turn.js +1 -1
  82. package/dist/src/cli/watch-view.d.ts +7 -0
  83. package/dist/src/cli/watch-view.js +51 -3
  84. package/dist/src/daemon/analysis-worker.d.ts +0 -2
  85. package/dist/src/daemon/analysis-worker.js +71 -19
  86. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  87. package/dist/src/daemon/chats-batch-binding.js +99 -0
  88. package/dist/src/daemon/chats-capture.d.ts +18 -0
  89. package/dist/src/daemon/chats-capture.js +194 -0
  90. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  91. package/dist/src/daemon/chats-checkpoints.js +186 -0
  92. package/dist/src/daemon/chats-files.d.ts +29 -2
  93. package/dist/src/daemon/chats-files.js +142 -3
  94. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  95. package/dist/src/daemon/chats-follow-up.js +180 -0
  96. package/dist/src/daemon/chats-question.d.ts +15 -0
  97. package/dist/src/daemon/chats-question.js +93 -0
  98. package/dist/src/daemon/chats-worker.d.ts +1 -0
  99. package/dist/src/daemon/chats-worker.js +52 -51
  100. package/dist/src/daemon/claude-turn.d.ts +2 -1
  101. package/dist/src/daemon/claude-turn.js +1 -1
  102. package/dist/src/daemon/claude-worker.d.ts +0 -2
  103. package/dist/src/daemon/claude-worker.js +6 -11
  104. package/dist/src/daemon/daemon.d.ts +5 -1
  105. package/dist/src/daemon/daemon.js +38 -10
  106. package/dist/src/daemon/delivery-router.js +6 -0
  107. package/dist/src/daemon/github-calls.d.ts +4 -0
  108. package/dist/src/daemon/github-calls.js +16 -7
  109. package/dist/src/daemon/github-ci.d.ts +6 -0
  110. package/dist/src/daemon/github-ci.js +170 -0
  111. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  112. package/dist/src/daemon/github-merge-call.js +73 -0
  113. package/dist/src/daemon/github-merge.d.ts +3 -5
  114. package/dist/src/daemon/github-merge.js +52 -95
  115. package/dist/src/daemon/github-methods.d.ts +28 -0
  116. package/dist/src/daemon/github-methods.js +80 -16
  117. package/dist/src/daemon/github-operation.d.ts +2 -0
  118. package/dist/src/daemon/github-retry.d.ts +6 -0
  119. package/dist/src/daemon/github-retry.js +49 -0
  120. package/dist/src/daemon/github-ticket-status.js +27 -1
  121. package/dist/src/daemon/github-worker.js +2 -1
  122. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  123. package/dist/src/daemon/handoff-worker.js +232 -0
  124. package/dist/src/daemon/issue-queue.d.ts +1 -0
  125. package/dist/src/daemon/issue-queue.js +94 -42
  126. package/dist/src/daemon/notify-journal.d.ts +18 -0
  127. package/dist/src/daemon/notify-journal.js +108 -0
  128. package/dist/src/daemon/notify-message.js +2 -1
  129. package/dist/src/daemon/notify.js +176 -37
  130. package/dist/src/daemon/retrying-turn.d.ts +3 -0
  131. package/dist/src/daemon/retrying-turn.js +55 -9
  132. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  133. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  134. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  135. package/dist/src/daemon/ticket-registration.js +76 -22
  136. package/dist/src/daemon/ticket-source.d.ts +4 -1
  137. package/dist/src/daemon/ticket-source.js +8 -2
  138. package/dist/src/decision-visibility.d.ts +2 -0
  139. package/dist/src/decision-visibility.js +10 -0
  140. package/dist/src/execution-pin.d.ts +4 -0
  141. package/dist/src/execution-pin.js +23 -2
  142. package/dist/src/host/start-delivery.d.ts +2 -1
  143. package/dist/src/host/start-delivery.js +1 -1
  144. package/dist/src/host/step-bridge-host.d.ts +1 -0
  145. package/dist/src/host/step-bridge-host.js +16 -0
  146. package/dist/src/host/world-reconciliation.d.ts +1 -0
  147. package/dist/src/host/world-reconciliation.js +3 -0
  148. package/dist/src/host/world-runs.js +5 -0
  149. package/dist/src/issue-identity.d.ts +6 -0
  150. package/dist/src/issue-identity.js +23 -0
  151. package/dist/src/service/queue-runs.d.ts +3 -0
  152. package/dist/src/service/queue-runs.js +39 -0
  153. package/dist/src/service/registry.d.ts +8 -2
  154. package/dist/src/service/registry.js +20 -8
  155. package/dist/src/service/replies.d.ts +2 -0
  156. package/dist/src/service/run-handlers.js +44 -12
  157. package/dist/src/service/service.d.ts +3 -0
  158. package/dist/src/service/service.js +3 -1
  159. package/dist/src/service/submission-handler.d.ts +1 -0
  160. package/dist/src/service/submission-handler.js +92 -13
  161. package/dist/src/service/worker-totals.js +25 -1
  162. package/dist/src/step-bridge.d.ts +9 -0
  163. package/dist/src/steps/handoff.d.ts +2 -0
  164. package/dist/src/steps/handoff.js +13 -0
  165. package/dist/src/ticket-dependencies.d.ts +1 -0
  166. package/dist/src/ticket-dependencies.js +15 -0
  167. package/dist/src/ticket-status.js +7 -4
  168. package/dist/src/token-prices.d.ts +2 -0
  169. package/dist/src/token-prices.js +41 -9
  170. package/dist/src/worker-usage.d.ts +9 -0
  171. package/dist/src/worker-usage.js +1 -1
  172. package/dist/src/workflow-admission.d.ts +1 -0
  173. package/dist/src/workflow-candidate.js +1 -0
  174. package/dist/src/workflow-contracts.d.ts +2 -0
  175. package/dist/src/workflow-contracts.js +8 -3
  176. package/dist/src/workflow-declaration.d.ts +11 -0
  177. package/dist/src/workflow-declaration.js +71 -15
  178. package/dist/src/workflow-discovery.d.ts +18 -0
  179. package/dist/src/workflow-discovery.js +33 -0
  180. package/dist/src/workflow-refusals.d.ts +6 -0
  181. package/dist/src/workflow-refusals.js +39 -0
  182. package/dist/src/workflows.d.ts +2 -1
  183. package/dist/src/workflows.js +1 -0
  184. package/dist/tools/admit-workflow.d.ts +13 -0
  185. package/dist/tools/admit-workflow.js +94 -0
  186. package/dist/tools/self-host-project.d.ts +17 -0
  187. package/dist/tools/self-host-project.js +177 -0
  188. package/dist/tools/self-host.js +27 -81
  189. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  190. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  191. package/dist/tools/workflow-bundles.d.ts +20 -0
  192. package/dist/tools/workflow-bundles.js +173 -0
  193. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  194. package/dist/tools/workflow-candidate-counts.js +251 -0
  195. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  196. package/dist/tools/workflow-candidate-program.js +78 -0
  197. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  198. package/dist/tools/workflow-candidate-source.js +107 -0
  199. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  200. package/dist/tools/workflow-candidate-syntax.js +219 -0
  201. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  202. package/dist/tools/workflow-folder-capture.js +216 -0
  203. package/dist/tools/workflow-folders.d.ts +33 -0
  204. package/dist/tools/workflow-folders.js +150 -0
  205. package/dist/tools/workflow-source-effects.d.ts +10 -0
  206. package/dist/tools/workflow-source-effects.js +213 -0
  207. package/dist/workflows/analyze-chats.d.ts +6 -1
  208. package/dist/workflows/analyze-chats.declaration.json +14 -1
  209. package/dist/workflows/analyze-chats.js +188 -11
  210. package/dist/workflows/develop-prd.js +71 -3
  211. package/dist/workflows/develop-ticket.js +5 -0
  212. package/dist/workflows/ticket-prompts.js +12 -2
  213. package/package.json +21 -3
@@ -1,28 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
- import { setTimeout as delay } from "node:timers/promises";
3
2
  import { readRecord, writeRecord } from "./operation-journal.js";
4
- import { changeChecked, defaultBranch, existingTree, field, githubTiming, removeWorktree, repositoryPath, } from "./github-operation.js";
3
+ import { changeChecked, defaultBranch, existingTree, field, removeWorktree, repositoryPath, } from "./github-operation.js";
5
4
  import { treePlace } from "./work-tree.js";
6
- function commitOf(value, name) {
7
- if (typeof value !== "object" || value === null)
8
- throw new Error("github-call-failed");
9
- const commit = Reflect.get(value, name);
10
- if (typeof commit !== "string")
11
- throw new Error("github-call-failed");
12
- return commit;
13
- }
14
- async function knownMerge(operation, expected) {
15
- const pull = JSON.parse(await operation.calls.gh([
16
- "api",
17
- `${repositoryPath}/pulls/${String(expected.number)}`,
18
- ]));
19
- const head = commitOf(field(pull, "head"), "sha");
20
- if (field(pull, "merged") !== true)
21
- return { head, merged: undefined };
22
- if (head !== expected.commit && head !== expected.updated)
23
- throw new Error("merged-other-commit");
24
- return { head, merged: commitOf(pull, "merge_commit_sha") };
25
- }
5
+ import { waitForCi } from "./github-ci.js";
6
+ import { githubText } from "./github-calls.js";
7
+ import { knownMerge, mergePull, } from "./github-merge-call.js";
26
8
  async function abortMerge(operation, tree) {
27
9
  const path = (await operation.calls.git(tree.path, [
28
10
  "rev-parse",
@@ -69,14 +51,19 @@ async function updatedTree(operation, place) {
69
51
  place.base,
70
52
  ]);
71
53
  }
72
- catch {
54
+ catch (error) {
73
55
  const conflicts = await calls.git(place.tree.path, [
74
56
  "diff",
75
57
  "--name-only",
76
58
  "--diff-filter=U",
77
59
  ]);
78
60
  await calls.git(place.tree.path, ["merge", "--abort"]);
79
- throw new Error("merge-conflict", { cause: conflicts.trim() });
61
+ const output = commandOutput(error instanceof Error ? error.cause : error);
62
+ throw Object.assign(new Error("merge-conflict", { cause: error }), {
63
+ output: githubText(output + conflicts, operation.settings)
64
+ .slice(-6_000)
65
+ .trim(),
66
+ });
80
67
  }
81
68
  }
82
69
  async function checked(operation, place) {
@@ -115,52 +102,6 @@ async function refreshed(operation, place) {
115
102
  }
116
103
  throw new Error("base-moved");
117
104
  }
118
- async function mergedNow(operation, pull) {
119
- const reply = JSON.parse(await operation.calls.gh([
120
- "api",
121
- "-X",
122
- "PUT",
123
- `${repositoryPath}/pulls/${String(pull.number)}/merge`,
124
- "-f",
125
- "merge_method=merge",
126
- "-f",
127
- `sha=${pull.commit}`,
128
- ]));
129
- return commitOf(reply, "sha");
130
- }
131
- async function waitForHead(operation, pull) {
132
- const timing = operation.settings.timing ?? githubTiming;
133
- for (let attempt = 0; attempt < 30; attempt += 1) {
134
- if (attempt > 0)
135
- await delay(timing.pollMilliseconds, undefined, {
136
- signal: operation.signal,
137
- });
138
- const current = JSON.parse(await operation.calls.gh([
139
- "api",
140
- `${repositoryPath}/pulls/${String(pull.number)}`,
141
- ]));
142
- if (commitOf(field(current, "head"), "sha") === pull.commit)
143
- return;
144
- }
145
- throw new Error("head-not-updated");
146
- }
147
- async function mergeUpdated(operation, pull) {
148
- const timing = operation.settings.timing ?? githubTiming;
149
- for (let attempt = 0;; attempt += 1) {
150
- await waitForHead(operation, pull);
151
- try {
152
- return await mergedNow(operation, pull);
153
- }
154
- catch (error) {
155
- const status = field(error, "status");
156
- if (attempt === 2 || (status !== 405 && status !== 409))
157
- throw error;
158
- await delay(timing.pollMilliseconds, undefined, {
159
- signal: operation.signal,
160
- });
161
- }
162
- }
163
- }
164
105
  async function removed(operation, place) {
165
106
  const { settings, calls } = operation;
166
107
  await calls
@@ -191,35 +132,47 @@ async function recordedHead(operation, commit) {
191
132
  }
192
133
  async function readyHead(operation, pull) {
193
134
  const tree = existingTree(operation);
194
- const updated = await refreshed(operation, {
195
- tree,
196
- commit: pull.commit,
197
- recorded: pull.recorded,
198
- });
199
- if (updated === pull.commit)
200
- return updated;
201
- if (pull.recorded !== updated)
202
- await writeRecord(operation.folder, {
203
- name: "updated-head.json",
204
- value: { commit: pull.commit, updated },
135
+ let recorded = pull.recorded;
136
+ let remote = pull.remote;
137
+ for (let attempt = 0; attempt < 3; attempt += 1) {
138
+ const updated = await refreshed(operation, {
139
+ tree,
140
+ commit: pull.commit,
141
+ recorded,
205
142
  });
206
- if (pull.remote !== updated)
207
- await operation.calls.git(tree.path, [
208
- "push",
209
- "--quiet",
210
- "origin",
211
- `${updated}:refs/heads/${tree.branch}`,
212
- ]);
213
- return updated;
143
+ if (updated !== pull.commit && recorded !== updated)
144
+ await writeRecord(operation.folder, {
145
+ name: "updated-head.json",
146
+ value: { commit: pull.commit, updated },
147
+ });
148
+ if (updated !== pull.commit && remote !== updated)
149
+ await operation.calls.git(tree.path, [
150
+ "push",
151
+ "--quiet",
152
+ "origin",
153
+ `${updated}:refs/heads/${tree.branch}`,
154
+ ]);
155
+ const ci = await waitForCi(operation, updated);
156
+ if (ci !== undefined)
157
+ return ci;
158
+ const newest = await fetched(operation, await defaultBranch(operation));
159
+ if ((await operation.calls.git(tree.path, ["merge-base", updated, newest])).trim() === newest)
160
+ return updated;
161
+ recorded = updated;
162
+ remote = updated;
163
+ }
164
+ throw new Error("base-moved");
214
165
  }
215
166
  function checkOutput(cause) {
167
+ return commandOutput(cause).slice(-6_000);
168
+ }
169
+ function commandOutput(cause) {
216
170
  return ["stdout", "stderr"]
217
171
  .map((name) => {
218
172
  const value = field(cause, name);
219
173
  return typeof value === "string" ? value : "";
220
174
  })
221
- .join("")
222
- .slice(-6_000);
175
+ .join("");
223
176
  }
224
177
  export async function merge(operation, pull) {
225
178
  try {
@@ -232,10 +185,13 @@ export async function merge(operation, pull) {
232
185
  const reason = error.message;
233
186
  if (reason !== "check-failed" && reason !== "merge-conflict")
234
187
  throw error;
235
- const conflicts = typeof error.cause === "string" ? error.cause : "";
188
+ const detail = field(error, "output");
189
+ const conflicts = typeof detail === "string" ? detail : "";
236
190
  return {
237
191
  reason,
238
- output: reason === "check-failed" ? checkOutput(error.cause) : conflicts,
192
+ output: reason === "check-failed"
193
+ ? checkOutput(error.cause)
194
+ : githubText(conflicts, operation.settings),
239
195
  };
240
196
  }
241
197
  }
@@ -252,8 +208,9 @@ async function completedMerge(operation, pull) {
252
208
  const updated = known.merged === undefined
253
209
  ? await readyHead(operation, { ...pull, recorded, remote: known.head })
254
210
  : (recorded ?? pull.commit);
255
- const mergeCall = updated === pull.commit ? mergedNow : mergeUpdated;
256
- const merged = known.merged ?? (await mergeCall(operation, { ...pull, commit: updated }));
211
+ if (typeof updated !== "string")
212
+ return updated;
213
+ const merged = known.merged ?? (await mergePull(operation, { ...pull, updated }));
257
214
  await removed(operation, place);
258
215
  return { number: pull.number, commit: pull.commit, updated, merged };
259
216
  }
@@ -1,5 +1,33 @@
1
+ import { type Finding, type Scored } from "./github-calls.ts";
1
2
  import { type Operation } from "./github-operation.ts";
2
3
  export type { GithubTiming, MethodSettings } from "./github-operation.ts";
3
4
  export declare const methodReasons: ReadonlySet<string>;
5
+ export interface ReviewResult extends Scored {
6
+ readonly number: number;
7
+ readonly commit: string;
8
+ readonly findings: readonly Finding[];
9
+ }
10
+ interface ReviewFailure {
11
+ readonly call: "read-review" | "request-review";
12
+ readonly reason: "github-call-failed" | "invalid-review-response";
13
+ readonly status?: number;
14
+ }
15
+ interface ReviewClock {
16
+ readonly now: () => number;
17
+ readonly sleep: (milliseconds: number, signal: AbortSignal) => Promise<void>;
18
+ }
19
+ interface ReviewWait {
20
+ readonly clock: ReviewClock;
21
+ failedReads: number;
22
+ requests: number;
23
+ acceptedRequests: number;
24
+ requestedAt: number;
25
+ lastFailure: ReviewFailure | null;
26
+ }
27
+ export declare class ReviewTimeout extends Error {
28
+ readonly detail: string;
29
+ constructor(wait: ReviewWait);
30
+ }
31
+ export declare function waitReview(operation: Operation, clock?: ReviewClock): Promise<ReviewResult>;
4
32
  export type Method = (operation: Operation) => Promise<unknown>;
5
33
  export declare const methods: ReadonlyMap<string, Method>;
@@ -1,17 +1,21 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
- import { findingsOf, scoreOf } from "./github-calls.js";
2
+ import { appendFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { findingsOf, scoreOf, } from "./github-calls.js";
3
5
  import { closeIssue, partialIssue } from "./github-ticket-status.js";
4
6
  import { merge } from "./github-merge.js";
5
7
  import { createIssue, discard } from "./github-issue-methods.js";
6
8
  import { existingTree, defaultBranch, changeChecked, field, githubTiming, repositoryPath, text, } from "./github-operation.js";
7
9
  import { workTree } from "./work-tree.js";
8
10
  const triggerText = "@greptileai";
11
+ const maximumReviewRequests = 3;
9
12
  export const methodReasons = new Set([
10
13
  "draft-not-available",
11
14
  "invalid-draft",
12
15
  "post-uncertain",
13
16
  "private-texts-not-set",
14
17
  "github-call-failed",
18
+ "github-network-failed",
15
19
  "github-issue-conflict",
16
20
  "git-call-failed",
17
21
  "check-failed",
@@ -157,30 +161,90 @@ async function reviewed(operation, pull) {
157
161
  const lines = await read(`pulls/${String(number)}/comments?per_page=100`);
158
162
  return { number, commit, ...scored, findings: findingsOf(lines, commit) };
159
163
  }
160
- async function waitReview(operation) {
164
+ const reviewClock = {
165
+ now: () => Date.now(),
166
+ sleep: async (milliseconds, signal) => {
167
+ await delay(milliseconds, undefined, { signal });
168
+ },
169
+ };
170
+ export class ReviewTimeout extends Error {
171
+ detail;
172
+ constructor(wait) {
173
+ super("review-not-received");
174
+ this.detail = JSON.stringify({
175
+ failedReads: wait.failedReads,
176
+ requests: wait.requests,
177
+ acceptedRequests: wait.acceptedRequests,
178
+ lastFailure: wait.lastFailure,
179
+ });
180
+ }
181
+ }
182
+ async function reviewFailure(operation, failed) {
183
+ const { wait, call, error } = failed;
184
+ const status = field(error, "status");
185
+ const failure = {
186
+ call,
187
+ reason: error instanceof SyntaxError
188
+ ? "invalid-review-response"
189
+ : "github-call-failed",
190
+ ...(typeof status === "number" &&
191
+ Number.isInteger(status) &&
192
+ status >= 400 &&
193
+ status <= 599
194
+ ? { status }
195
+ : {}),
196
+ };
197
+ wait.lastFailure = failure;
198
+ await appendFile(join(operation.folder, "review-failures.jsonl"), JSON.stringify(failure) + "\n", { mode: 0o600 }).catch(() => undefined);
199
+ }
200
+ async function requestReview(operation, request) {
201
+ const { wait, number } = request;
202
+ wait.requests++;
203
+ try {
204
+ await operation.calls.gh([
205
+ "pr",
206
+ "comment",
207
+ String(number),
208
+ "--body",
209
+ triggerText,
210
+ ]);
211
+ wait.acceptedRequests++;
212
+ wait.requestedAt = wait.clock.now();
213
+ }
214
+ catch (error) {
215
+ await reviewFailure(operation, { wait, call: "request-review", error });
216
+ }
217
+ }
218
+ export async function waitReview(operation, clock = reviewClock) {
161
219
  const pull = {
162
220
  number: integer(operation, "number"),
163
221
  commit: text(operation, "commit"),
164
222
  };
165
223
  const timing = operation.settings.timing ?? githubTiming;
166
- const startedAt = Date.now();
167
- let triggered = false;
224
+ const startedAt = clock.now();
225
+ const wait = {
226
+ clock,
227
+ failedReads: 0,
228
+ requests: 0,
229
+ acceptedRequests: 0,
230
+ requestedAt: startedAt,
231
+ lastFailure: null,
232
+ };
168
233
  for (;;) {
169
- const review = await reviewed(operation, pull).catch(() => undefined);
234
+ const review = await reviewed(operation, pull).catch(async (error) => {
235
+ wait.failedReads++;
236
+ await reviewFailure(operation, { wait, call: "read-review", error });
237
+ return undefined;
238
+ });
170
239
  if (review !== undefined)
171
240
  return review;
172
- const waited = Date.now() - startedAt;
241
+ const waited = clock.now() - startedAt;
173
242
  if (waited >= timing.limitMilliseconds)
174
- throw new Error("review-not-received");
175
- if (!triggered && waited >= timing.triggerMilliseconds) {
176
- triggered = true;
177
- await operation.calls
178
- .gh(["pr", "comment", String(pull.number), "--body", triggerText])
179
- .catch(() => undefined);
180
- }
181
- await delay(timing.pollMilliseconds, undefined, {
182
- signal: operation.signal,
183
- });
243
+ throw new ReviewTimeout(wait);
244
+ if (wait.acceptedRequests < maximumReviewRequests &&
245
+ clock.now() - wait.requestedAt >= timing.triggerMilliseconds)
246
+ await requestReview(operation, { wait, number: pull.number });
247
+ await clock.sleep(timing.pollMilliseconds, operation.signal);
184
248
  }
185
249
  }
186
250
  export const methods = new Map([
@@ -3,6 +3,8 @@ export interface GithubTiming {
3
3
  readonly pollMilliseconds: number;
4
4
  readonly triggerMilliseconds: number;
5
5
  readonly limitMilliseconds: number;
6
+ readonly ciLimitMilliseconds?: number;
7
+ readonly ciGraceMilliseconds?: number;
6
8
  }
7
9
  export declare const githubTiming: GithubTiming;
8
10
  export interface MethodSettings extends CallSettings {
@@ -0,0 +1,6 @@
1
+ import { type Operation } from "./github-operation.ts";
2
+ export declare function retryGithub<Result>(operation: Operation, retry: {
3
+ readonly action: (attempt: number) => Promise<Result>;
4
+ readonly statuses?: readonly number[];
5
+ }): Promise<Result>;
6
+ export declare function githubRead(operation: Operation, parts: readonly string[]): Promise<string>;
@@ -0,0 +1,49 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { field, githubTiming } from "./github-operation.js";
3
+ function transient(error) {
4
+ if (!(error instanceof Error) || error.message !== "github-call-failed")
5
+ return false;
6
+ const detail = field(error, "detail");
7
+ if (typeof detail !== "string")
8
+ return false;
9
+ const status = field(error, "status");
10
+ if ([500, 502, 503, 504].includes(Number(status)))
11
+ return true;
12
+ return ((status === undefined ||
13
+ (typeof status === "number" && Number.isNaN(status))) &&
14
+ /TLS handshake timeout|connection reset|i\/o timeout|\bEOF\b/i.test(detail));
15
+ }
16
+ function retryFactor(error, retry) {
17
+ const network = transient(error);
18
+ if (!network && !retry.statuses.includes(Number(field(error, "status"))))
19
+ throw error;
20
+ if (retry.attempt < 2)
21
+ return network ? retry.attempt + 1 : 1;
22
+ if (!network)
23
+ throw error;
24
+ throw Object.assign(new Error("github-network-failed", { cause: error }), {
25
+ detail: field(error, "detail"),
26
+ });
27
+ }
28
+ export async function retryGithub(operation, retry) {
29
+ const timing = operation.settings.timing ?? githubTiming;
30
+ for (let attempt = 0;; attempt += 1) {
31
+ operation.signal.throwIfAborted();
32
+ try {
33
+ return await retry.action(attempt);
34
+ }
35
+ catch (error) {
36
+ operation.signal.throwIfAborted();
37
+ const factor = retryFactor(error, {
38
+ attempt,
39
+ statuses: retry.statuses ?? [],
40
+ });
41
+ await delay(timing.pollMilliseconds * factor, undefined, {
42
+ signal: operation.signal,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ export function githubRead(operation, parts) {
48
+ return retryGithub(operation, { action: () => operation.calls.gh(parts) });
49
+ }
@@ -87,12 +87,38 @@ function statusText(history) {
87
87
  lines.push(`Completed in pull request #${String(history.completedPullRequest)}.`);
88
88
  return `## Cloving status\n${lines.join("\n")}\n\n`;
89
89
  }
90
+ function legacyFence(line) {
91
+ const marker = /^( {0,3}(?:[-+*]|\d{1,9}[.)])[ \t]+)? {0,3}(`{3,}|~{3,})/.exec(line);
92
+ if (marker === null || marker[2] === undefined)
93
+ return undefined;
94
+ const delimiter = marker[2];
95
+ if (delimiter.startsWith("`") && line.slice(marker[0].length).includes("`"))
96
+ return undefined;
97
+ const indentation = (marker[1]?.length ?? 0) + 3;
98
+ return new RegExp(`^ {0,${String(indentation)}}${delimiter.charAt(0)}{${String(delimiter.length)},}[ \\t]*\\r?\\n?$`);
99
+ }
100
+ function legacyStatusStart(body) {
101
+ let fence;
102
+ let offset = 0;
103
+ for (const line of body.split(/(?<=\n)/)) {
104
+ const start = offset;
105
+ offset += line.length;
106
+ if (fence !== undefined) {
107
+ fence = fence.test(line) ? undefined : fence;
108
+ continue;
109
+ }
110
+ if (line.trimEnd() === "## Cloving status")
111
+ return start;
112
+ fence = legacyFence(line);
113
+ }
114
+ return body.length;
115
+ }
90
116
  function statusSection(body) {
91
117
  const empty = { version: 1, partialRuns: [] };
92
118
  const marker = /^<!-- cloving-status(?=\s|-->|$)/gm;
93
119
  const start = [...body.matchAll(marker)].at(-1)?.index;
94
120
  if (start === undefined)
95
- return { outside: body, history: empty };
121
+ return { outside: body.slice(0, legacyStatusStart(body)), history: empty };
96
122
  const tail = body.slice(start);
97
123
  const end = tail.indexOf("-->");
98
124
  if (end !== -1 && tail.slice(end + 3).trim() !== "")
@@ -1,7 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { calls } from "./github-calls.js";
4
- import { methodReasons, methods, } from "./github-methods.js";
4
+ import { methodReasons, methods, ReviewTimeout, } from "./github-methods.js";
5
5
  import { interrupted, located, readRecord, replied, reportOf, writeRecord, } from "./operation-journal.js";
6
6
  const worker = "github";
7
7
  const closing = "github-worker-closed";
@@ -62,6 +62,7 @@ async function outcome(parts, run) {
62
62
  return {
63
63
  ...interrupted(methodReasons.has(reason) ? reason : "github-call-failed"),
64
64
  ...(typeof detail === "string" ? { detail } : {}),
65
+ ...(error instanceof ReviewTimeout ? { result: error.detail } : {}),
65
66
  };
66
67
  }
67
68
  }
@@ -0,0 +1,10 @@
1
+ import type { EffectDelivery } from "./delivery-router.ts";
2
+ interface Settings {
3
+ readonly runtimeDirectory: string;
4
+ readonly journal: string;
5
+ readonly document: (digest: string) => Promise<string | undefined>;
6
+ readonly runStatus: (runId: string) => Promise<string | undefined>;
7
+ readonly retryMilliseconds?: number;
8
+ }
9
+ export declare function handoffWorker(settings: Settings): EffectDelivery;
10
+ export {};