cloving 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +121 -21
  2. package/assets/watch-fonts/fira-code-OFL.md +93 -0
  3. package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
  4. package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
  5. package/assets/watch-fonts/heebo-OFL.md +93 -0
  6. package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
  7. package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
  8. package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
  9. package/assets/watch-fonts/manifest.json +112 -0
  10. package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
  11. package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
  12. package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
  13. package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
  14. package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
  15. package/dist/src/authority/commands.d.ts +1 -0
  16. package/dist/src/authority/commands.js +25 -4
  17. package/dist/src/authority/dispatch-decision.d.ts +1 -0
  18. package/dist/src/authority/dispatch-decision.js +7 -4
  19. package/dist/src/authority/events.js +2 -2
  20. package/dist/src/authority/handoff-chain.d.ts +24 -0
  21. package/dist/src/authority/handoff-chain.js +46 -0
  22. package/dist/src/authority/reconciliation.d.ts +1 -0
  23. package/dist/src/authority/reconciliation.js +18 -11
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +3 -0
  26. package/dist/src/broker/actor/authority-queries.d.ts +4 -0
  27. package/dist/src/broker/actor/authority-queries.js +51 -2
  28. package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
  29. package/dist/src/broker/actor/effect-delivery.js +9 -0
  30. package/dist/src/broker/authority-admission.js +13 -5
  31. package/dist/src/chats/analysis-batches.d.ts +18 -0
  32. package/dist/src/chats/analysis-batches.js +15 -1
  33. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  34. package/dist/src/chats/analysis-checkpoint.js +31 -0
  35. package/dist/src/chats/analysis-input.d.ts +7 -1
  36. package/dist/src/chats/analysis-input.js +20 -6
  37. package/dist/src/chats/analysis-merge.d.ts +16 -0
  38. package/dist/src/chats/analysis-merge.js +134 -0
  39. package/dist/src/chats/analysis-prompt.d.ts +8 -0
  40. package/dist/src/chats/analysis-prompt.js +27 -1
  41. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  42. package/dist/src/chats/analysis-proposal.js +9 -0
  43. package/dist/src/chats/analysis-shape.d.ts +1 -0
  44. package/dist/src/chats/analysis-shape.js +11 -4
  45. package/dist/src/chats/reference-mapping.d.ts +16 -0
  46. package/dist/src/chats/reference-mapping.js +153 -0
  47. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  48. package/dist/src/chats/workflow-generation-projection.js +75 -0
  49. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  50. package/dist/src/chats/workflow-generation-source.js +124 -0
  51. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  52. package/dist/src/chats/workflow-generation-text.js +74 -0
  53. package/dist/src/chats/workflow-generation.d.ts +69 -0
  54. package/dist/src/chats/workflow-generation.js +142 -0
  55. package/dist/src/cli/render.d.ts +1 -0
  56. package/dist/src/cli/render.js +60 -13
  57. package/dist/src/cli/usage.js +5 -2
  58. package/dist/src/cli/watch-board-view.d.ts +12 -1
  59. package/dist/src/cli/watch-board-view.js +57 -11
  60. package/dist/src/cli/watch-board.d.ts +7 -1
  61. package/dist/src/cli/watch-board.js +42 -10
  62. package/dist/src/cli/watch-client.d.ts +1 -1
  63. package/dist/src/cli/watch-client.js +7 -3
  64. package/dist/src/cli/watch-model.d.ts +10 -4
  65. package/dist/src/cli/watch-model.js +35 -58
  66. package/dist/src/cli/watch-page.js +21 -4
  67. package/dist/src/cli/watch-phases.js +3 -3
  68. package/dist/src/cli/watch-pr-command.d.ts +5 -2
  69. package/dist/src/cli/watch-pr-command.js +41 -4
  70. package/dist/src/cli/watch-pr-identity.d.ts +14 -2
  71. package/dist/src/cli/watch-pr-identity.js +25 -0
  72. package/dist/src/cli/watch-pr-observer.d.ts +1 -0
  73. package/dist/src/cli/watch-pr-observer.js +48 -6
  74. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  75. package/dist/src/cli/watch-pull-request.js +130 -0
  76. package/dist/src/cli/watch-reads.d.ts +7 -1
  77. package/dist/src/cli/watch-reads.js +42 -20
  78. package/dist/src/cli/watch-render.d.ts +14 -1
  79. package/dist/src/cli/watch-render.js +2 -1
  80. package/dist/src/cli/watch-server.js +33 -2
  81. package/dist/src/cli/watch-turn.js +1 -1
  82. package/dist/src/cli/watch-view.d.ts +7 -0
  83. package/dist/src/cli/watch-view.js +51 -3
  84. package/dist/src/daemon/analysis-worker.d.ts +0 -2
  85. package/dist/src/daemon/analysis-worker.js +71 -19
  86. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  87. package/dist/src/daemon/chats-batch-binding.js +99 -0
  88. package/dist/src/daemon/chats-capture.d.ts +18 -0
  89. package/dist/src/daemon/chats-capture.js +194 -0
  90. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  91. package/dist/src/daemon/chats-checkpoints.js +186 -0
  92. package/dist/src/daemon/chats-files.d.ts +29 -2
  93. package/dist/src/daemon/chats-files.js +142 -3
  94. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  95. package/dist/src/daemon/chats-follow-up.js +180 -0
  96. package/dist/src/daemon/chats-question.d.ts +15 -0
  97. package/dist/src/daemon/chats-question.js +93 -0
  98. package/dist/src/daemon/chats-worker.d.ts +1 -0
  99. package/dist/src/daemon/chats-worker.js +52 -51
  100. package/dist/src/daemon/claude-turn.d.ts +2 -1
  101. package/dist/src/daemon/claude-turn.js +1 -1
  102. package/dist/src/daemon/claude-worker.d.ts +0 -2
  103. package/dist/src/daemon/claude-worker.js +6 -11
  104. package/dist/src/daemon/daemon.d.ts +5 -1
  105. package/dist/src/daemon/daemon.js +38 -10
  106. package/dist/src/daemon/delivery-router.js +6 -0
  107. package/dist/src/daemon/github-calls.d.ts +4 -0
  108. package/dist/src/daemon/github-calls.js +16 -7
  109. package/dist/src/daemon/github-ci.d.ts +6 -0
  110. package/dist/src/daemon/github-ci.js +170 -0
  111. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  112. package/dist/src/daemon/github-merge-call.js +73 -0
  113. package/dist/src/daemon/github-merge.d.ts +3 -5
  114. package/dist/src/daemon/github-merge.js +52 -95
  115. package/dist/src/daemon/github-methods.d.ts +28 -0
  116. package/dist/src/daemon/github-methods.js +80 -16
  117. package/dist/src/daemon/github-operation.d.ts +2 -0
  118. package/dist/src/daemon/github-retry.d.ts +6 -0
  119. package/dist/src/daemon/github-retry.js +49 -0
  120. package/dist/src/daemon/github-ticket-status.js +27 -1
  121. package/dist/src/daemon/github-worker.js +2 -1
  122. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  123. package/dist/src/daemon/handoff-worker.js +232 -0
  124. package/dist/src/daemon/issue-queue.d.ts +1 -0
  125. package/dist/src/daemon/issue-queue.js +94 -42
  126. package/dist/src/daemon/notify-journal.d.ts +18 -0
  127. package/dist/src/daemon/notify-journal.js +108 -0
  128. package/dist/src/daemon/notify-message.js +2 -1
  129. package/dist/src/daemon/notify.js +176 -37
  130. package/dist/src/daemon/retrying-turn.d.ts +3 -0
  131. package/dist/src/daemon/retrying-turn.js +55 -9
  132. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  133. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  134. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  135. package/dist/src/daemon/ticket-registration.js +76 -22
  136. package/dist/src/daemon/ticket-source.d.ts +4 -1
  137. package/dist/src/daemon/ticket-source.js +8 -2
  138. package/dist/src/decision-visibility.d.ts +2 -0
  139. package/dist/src/decision-visibility.js +10 -0
  140. package/dist/src/execution-pin.d.ts +4 -0
  141. package/dist/src/execution-pin.js +23 -2
  142. package/dist/src/host/start-delivery.d.ts +2 -1
  143. package/dist/src/host/start-delivery.js +1 -1
  144. package/dist/src/host/step-bridge-host.d.ts +1 -0
  145. package/dist/src/host/step-bridge-host.js +16 -0
  146. package/dist/src/host/world-reconciliation.d.ts +1 -0
  147. package/dist/src/host/world-reconciliation.js +3 -0
  148. package/dist/src/host/world-runs.js +5 -0
  149. package/dist/src/issue-identity.d.ts +6 -0
  150. package/dist/src/issue-identity.js +23 -0
  151. package/dist/src/service/queue-runs.d.ts +3 -0
  152. package/dist/src/service/queue-runs.js +39 -0
  153. package/dist/src/service/registry.d.ts +8 -2
  154. package/dist/src/service/registry.js +20 -8
  155. package/dist/src/service/replies.d.ts +2 -0
  156. package/dist/src/service/run-handlers.js +44 -12
  157. package/dist/src/service/service.d.ts +3 -0
  158. package/dist/src/service/service.js +3 -1
  159. package/dist/src/service/submission-handler.d.ts +1 -0
  160. package/dist/src/service/submission-handler.js +92 -13
  161. package/dist/src/service/worker-totals.js +25 -1
  162. package/dist/src/step-bridge.d.ts +9 -0
  163. package/dist/src/steps/handoff.d.ts +2 -0
  164. package/dist/src/steps/handoff.js +13 -0
  165. package/dist/src/ticket-dependencies.d.ts +1 -0
  166. package/dist/src/ticket-dependencies.js +15 -0
  167. package/dist/src/ticket-status.js +7 -4
  168. package/dist/src/token-prices.d.ts +2 -0
  169. package/dist/src/token-prices.js +41 -9
  170. package/dist/src/worker-usage.d.ts +9 -0
  171. package/dist/src/worker-usage.js +1 -1
  172. package/dist/src/workflow-admission.d.ts +1 -0
  173. package/dist/src/workflow-candidate.js +1 -0
  174. package/dist/src/workflow-contracts.d.ts +2 -0
  175. package/dist/src/workflow-contracts.js +8 -3
  176. package/dist/src/workflow-declaration.d.ts +11 -0
  177. package/dist/src/workflow-declaration.js +71 -15
  178. package/dist/src/workflow-discovery.d.ts +18 -0
  179. package/dist/src/workflow-discovery.js +33 -0
  180. package/dist/src/workflow-refusals.d.ts +6 -0
  181. package/dist/src/workflow-refusals.js +39 -0
  182. package/dist/src/workflows.d.ts +2 -1
  183. package/dist/src/workflows.js +1 -0
  184. package/dist/tools/admit-workflow.d.ts +13 -0
  185. package/dist/tools/admit-workflow.js +94 -0
  186. package/dist/tools/self-host-project.d.ts +17 -0
  187. package/dist/tools/self-host-project.js +177 -0
  188. package/dist/tools/self-host.js +27 -81
  189. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  190. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  191. package/dist/tools/workflow-bundles.d.ts +20 -0
  192. package/dist/tools/workflow-bundles.js +173 -0
  193. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  194. package/dist/tools/workflow-candidate-counts.js +251 -0
  195. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  196. package/dist/tools/workflow-candidate-program.js +78 -0
  197. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  198. package/dist/tools/workflow-candidate-source.js +107 -0
  199. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  200. package/dist/tools/workflow-candidate-syntax.js +219 -0
  201. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  202. package/dist/tools/workflow-folder-capture.js +216 -0
  203. package/dist/tools/workflow-folders.d.ts +33 -0
  204. package/dist/tools/workflow-folders.js +150 -0
  205. package/dist/tools/workflow-source-effects.d.ts +10 -0
  206. package/dist/tools/workflow-source-effects.js +213 -0
  207. package/dist/workflows/analyze-chats.d.ts +6 -1
  208. package/dist/workflows/analyze-chats.declaration.json +14 -1
  209. package/dist/workflows/analyze-chats.js +188 -11
  210. package/dist/workflows/develop-prd.js +71 -3
  211. package/dist/workflows/develop-ticket.js +5 -0
  212. package/dist/workflows/ticket-prompts.js +12 -2
  213. package/package.json +21 -3
@@ -1,9 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { admitCompiled } from "../cli/validate.js";
3
3
  import { ticketCommandText, ticketText } from "./ticket-source.js";
4
- export const ticketWorkflow = "develop-ticket";
5
- export const promptWorkflow = "develop-prompt";
6
- export const issueWorkflow = "develop-issue";
4
+ import { bundledWorkflowIds } from "../workflow-discovery.js";
5
+ import { declaredManifest } from "../execution-pin.js";
6
+ import { record } from "../protocol.js";
7
+ import { issueIdentity } from "../issue-identity.js";
8
+ import { ticketDependencyWaits, TicketDependencyReadError, } from "./ticket-dependency-waits.js";
9
+ export const ticketWorkflow = bundledWorkflowIds.ticket;
10
+ export const promptWorkflow = bundledWorkflowIds.prompt;
11
+ export const issueWorkflow = bundledWorkflowIds.issue;
7
12
  export const ticketCapabilities = ["claude", "github"];
8
13
  const ticketIdentifier = /^[A-Za-z0-9._:-]{1,64}$/;
9
14
  function rejected(code, detail) {
@@ -38,7 +43,15 @@ async function ticket(source, request) {
38
43
  async function nextTicket(source, request) {
39
44
  if (source.nextCommand === undefined)
40
45
  return rejected("next-source-not-configured", "The daemon has no next-ticket command. Set CLOVING_NEXT_COMMAND.");
41
- const selected = await ticketCommandText(source.nextCommand, request.signal);
46
+ const selected = await ticketCommandText(source.nextCommand, {
47
+ signal: request.signal,
48
+ skipTickets: [
49
+ ...new Set([
50
+ ...(request.activeTickets ?? []).flatMap((input) => typeof input["ticketId"] === "string" ? [input["ticketId"]] : []),
51
+ ...(source.skipTickets ?? []),
52
+ ]),
53
+ ].sort(),
54
+ });
42
55
  if (selected.kind === "failed")
43
56
  return rejected("next-ticket-not-available", selected.detail);
44
57
  if (selected.text === "")
@@ -55,18 +68,43 @@ export function ticketResolver(source) {
55
68
  };
56
69
  }
57
70
  export function issueResolver(source) {
71
+ const waits = ticketDependencyWaits(source);
58
72
  return {
59
73
  id: "issue-command",
60
74
  version: "1",
61
- resolve: async (request) => {
62
- const resolution = await ticket(source, request);
63
- if (resolution.kind === "rejected")
64
- return resolution;
65
- return {
66
- kind: "resolved",
67
- input: { ...Object(resolution.input), delivery: "pull-request" },
68
- };
69
- },
75
+ skippedTickets: waits.skipped,
76
+ resolve: (request) => issueTicket({ source, waits }, request).catch((error) => {
77
+ if (!(error instanceof TicketDependencyReadError))
78
+ throw error;
79
+ return rejected("ticket-not-available", "The ticket dependencies could not be read. Retry this command.");
80
+ }),
81
+ };
82
+ }
83
+ async function issueTicket(context, request) {
84
+ const { source, waits } = context;
85
+ const skipTickets = request.submission["ticket"] === "next"
86
+ ? await waits.refresh(request.signal)
87
+ : [];
88
+ const resolution = await ticket({ ...source, skipTickets }, request);
89
+ if (resolution.kind === "rejected")
90
+ return resolution;
91
+ const input = record(resolution.input) ?? {};
92
+ const identity = issueIdentity(input["ticket"]);
93
+ if ((request.activeTickets ?? []).some((active) => active["ticketId"] === input["ticketId"] ||
94
+ (identity !== null &&
95
+ JSON.stringify(issueIdentity(active["ticket"])) ===
96
+ JSON.stringify(identity))))
97
+ return rejected("ticket-active", "An active run already works on this ticket.");
98
+ const dependencies = await waits.check({
99
+ ticketId: String(input["ticketId"]),
100
+ ticket: String(input["ticket"]),
101
+ signal: request.signal,
102
+ });
103
+ if (dependencies.length > 0)
104
+ return rejected("ticket-dependency-open", `Ticket ${String(input["ticketId"])} needs open issues: ${dependencies.map((id) => `#${id}`).join(", ")}.`);
105
+ return {
106
+ kind: "resolved",
107
+ input: { ...input, delivery: "pull-request" },
70
108
  };
71
109
  }
72
110
  export function promptResolver(repository) {
@@ -89,6 +127,7 @@ export function ticketRegistrationTable(source, declarations = []) {
89
127
  workflowId: ticketWorkflow,
90
128
  functionName: "developTicket",
91
129
  effectLimit: 8,
130
+ handoffs: [],
92
131
  submission: { maximumBytes: 4_096, fields: { ticket: "string" } },
93
132
  resolver: ticketResolver(source),
94
133
  },
@@ -96,13 +135,15 @@ export function ticketRegistrationTable(source, declarations = []) {
96
135
  workflowId: issueWorkflow,
97
136
  functionName: "developTicket",
98
137
  effectLimit: 20,
138
+ handoffs: [],
99
139
  submission: { maximumBytes: 4_096, fields: { ticket: "string" } },
100
140
  resolver: issueResolver(source),
101
141
  },
102
142
  {
103
- workflowId: "develop-prd",
143
+ workflowId: bundledWorkflowIds.prd,
104
144
  functionName: "developPrd",
105
145
  effectLimit: 8,
146
+ handoffs: [issueWorkflow],
106
147
  submission: { maximumBytes: 65_536, fields: { prompt: "string" } },
107
148
  resolver: promptResolver(source.repository),
108
149
  },
@@ -110,6 +151,7 @@ export function ticketRegistrationTable(source, declarations = []) {
110
151
  workflowId: promptWorkflow,
111
152
  functionName: "developTicket",
112
153
  effectLimit: 8,
154
+ handoffs: [],
113
155
  submission: { maximumBytes: 65_536, fields: { prompt: "string" } },
114
156
  resolver: promptResolver(source.repository),
115
157
  },
@@ -119,6 +161,12 @@ export function ticketRegistrationTable(source, declarations = []) {
119
161
  effectLimit: declaration.effectLimit,
120
162
  capabilities: declaration.capabilities,
121
163
  submission: declaration.input,
164
+ ...(declaration.stages === undefined
165
+ ? {}
166
+ : { stages: declaration.stages }),
167
+ ...(declaration.handoffs === undefined
168
+ ? {}
169
+ : { handoffs: declaration.handoffs }),
122
170
  resolver: declaredResolver(source.repository),
123
171
  })),
124
172
  ];
@@ -147,17 +195,26 @@ export async function ticketRegistrations(manifests, options) {
147
195
  };
148
196
  ids.add(row.workflowId);
149
197
  }
198
+ for (const row of options.table) {
199
+ const unknown = row.handoffs?.find((target) => !ids.has(target));
200
+ if (unknown !== undefined)
201
+ return {
202
+ kind: "rejected",
203
+ detail: `unknown-handoff-target: ${row.workflowId} -> ${unknown}`,
204
+ };
205
+ }
150
206
  const registrations = [];
151
207
  for (const row of options.table) {
152
- const manifest = manifests[row.functionName];
208
+ const { functionName, capabilities = ticketCapabilities, ...registration } = row;
209
+ const manifest = manifests[functionName];
153
210
  if (manifest === undefined)
154
211
  return {
155
212
  kind: "rejected",
156
- detail: `Missing manifest for workflow function ${row.functionName}.`,
213
+ detail: `Missing manifest for workflow function ${functionName}.`,
157
214
  };
158
215
  const admission = await admitCompiled(manifest, {
159
216
  file: manifest,
160
- capabilities: row.capabilities ?? ticketCapabilities,
217
+ capabilities,
161
218
  });
162
219
  if (admission.kind === "rejected")
163
220
  return {
@@ -165,12 +222,9 @@ export async function ticketRegistrations(manifests, options) {
165
222
  detail: `${admission.error.code}: ${admission.error.detail}`,
166
223
  };
167
224
  registrations.push({
168
- workflowId: row.workflowId,
169
- manifest: admission.manifest,
225
+ ...registration,
226
+ manifest: declaredManifest(admission.manifest, row),
170
227
  availability: "available",
171
- effectLimit: row.effectLimit,
172
- submission: row.submission,
173
- resolver: row.resolver,
174
228
  });
175
229
  }
176
230
  return { kind: "admitted", registrations };
@@ -14,4 +14,7 @@ export interface TicketRequest {
14
14
  readonly signal: AbortSignal;
15
15
  }
16
16
  export declare function ticketText(command: readonly string[], request: TicketRequest): Promise<TicketRead>;
17
- export declare function ticketCommandText(command: readonly string[], signal: AbortSignal): Promise<TicketRead>;
17
+ export declare function ticketCommandText(command: readonly string[], request: AbortSignal | {
18
+ readonly signal: AbortSignal;
19
+ readonly skipTickets: readonly string[];
20
+ }): Promise<TicketRead>;
@@ -26,10 +26,16 @@ export function ticketText(command, request) {
26
26
  const [executable = "", ...fixed] = command;
27
27
  return ticketCommandText([executable, ...fixed, request.ticketId], request.signal).then((result) => result.kind === "failed" ? result : printed(result.text));
28
28
  }
29
- export function ticketCommandText(command, signal) {
29
+ export function ticketCommandText(command, request) {
30
30
  const [executable = "", ...fixed] = command;
31
31
  return executed(executable, fixed, {
32
- signal,
32
+ signal: request instanceof AbortSignal ? request : request.signal,
33
+ env: request instanceof AbortSignal
34
+ ? process.env
35
+ : {
36
+ ...process.env,
37
+ CLOVING_SKIP_TICKETS: request.skipTickets.join(" "),
38
+ },
33
39
  maxBuffer: 4 * ticketLimits.textBytes,
34
40
  encoding: "utf8",
35
41
  }).then((output) => ({ kind: "read", text: output.stdout.trim() }), ended);
@@ -0,0 +1,2 @@
1
+ import type { Fields } from "./protocol.ts";
2
+ export declare function operationalDecision(decision: Fields, workflowId: string): Fields;
@@ -0,0 +1,10 @@
1
+ export function operationalDecision(decision, workflowId) {
2
+ if (workflowId !== "analyze-chats")
3
+ return decision;
4
+ return {
5
+ ...decision,
6
+ prompt: "Private analysis question. Run cloving questions to view it.",
7
+ context: [],
8
+ feedback: [],
9
+ };
10
+ }
@@ -1,4 +1,8 @@
1
1
  import type { AdmissionResult, ExecutionManifest } from "./workflow-admission.ts";
2
+ export declare function executionCapabilities(manifest: ExecutionManifest): string;
2
3
  export declare function pinnedIdentity(manifest: ExecutionManifest): string;
3
4
  export declare function resumePinned(pinned: ExecutionManifest, available: ExecutionManifest): AdmissionResult;
4
5
  export declare function readExecutionManifest(value: unknown): ExecutionManifest;
6
+ export declare function declaredManifest(manifest: ExecutionManifest, declaration: {
7
+ readonly handoffs?: readonly string[];
8
+ }): ExecutionManifest;
@@ -1,8 +1,10 @@
1
+ import { isWorkflowHandoffs } from "./workflow-declaration.js";
1
2
  const canonical = {
2
3
  name: (manifest) => JSON.stringify(manifest.name),
3
4
  workflowId: (manifest) => JSON.stringify(manifest.workflowId),
4
5
  steps: (manifest) => JSON.stringify([...manifest.steps].sort()),
5
6
  capabilities: (manifest) => JSON.stringify([...manifest.capabilities].sort()),
7
+ handoffs: (manifest) => JSON.stringify([...(manifest.handoffs ?? [])].sort()),
6
8
  artifacts: (manifest) => JSON.stringify(Object.keys(manifest.artifacts)
7
9
  .sort()
8
10
  .map((name) => [name, manifest.artifacts[name]])),
@@ -20,11 +22,18 @@ const fields = [
20
22
  "resources",
21
23
  "toolchain",
22
24
  ];
25
+ export function executionCapabilities(manifest) {
26
+ const handoff = manifest.handoffs?.length ? ["handoff"] : [];
27
+ return JSON.stringify([...new Set([...manifest.capabilities, ...handoff])].sort());
28
+ }
23
29
  export function pinnedIdentity(manifest) {
24
- return JSON.stringify(fields.map((field) => canonical[field](manifest)));
30
+ const identity = fields.map((field) => canonical[field](manifest));
31
+ if (manifest.handoffs?.length)
32
+ identity.push(canonical.handoffs(manifest));
33
+ return JSON.stringify(identity);
25
34
  }
26
35
  export function resumePinned(pinned, available) {
27
- const changed = fields.find((field) => canonical[field](pinned) !== canonical[field](available));
36
+ const changed = [...fields, "handoffs"].find((field) => canonical[field](pinned) !== canonical[field](available));
28
37
  if (changed !== undefined)
29
38
  return {
30
39
  kind: "rejected",
@@ -36,11 +45,15 @@ export function resumePinned(pinned, available) {
36
45
  export function readExecutionManifest(value) {
37
46
  const fields = record(value);
38
47
  const toolchain = record(fields["toolchain"]);
48
+ const handoffs = fields["handoffs"];
49
+ if (Object.hasOwn(fields, "handoffs") && !isWorkflowHandoffs(handoffs))
50
+ throw new Error("invalid-handoffs");
39
51
  return {
40
52
  name: text(fields["name"]),
41
53
  workflowId: text(fields["workflowId"]),
42
54
  steps: texts(fields["steps"]),
43
55
  capabilities: texts(fields["capabilities"]),
56
+ ...(isWorkflowHandoffs(handoffs) ? { handoffs: [...handoffs] } : {}),
44
57
  artifacts: textRecord(fields["artifacts"]),
45
58
  resources: textRecord(fields["resources"]),
46
59
  toolchain: {
@@ -52,6 +65,14 @@ export function readExecutionManifest(value) {
52
65
  },
53
66
  };
54
67
  }
68
+ export function declaredManifest(manifest, declaration) {
69
+ return readExecutionManifest({
70
+ ...manifest,
71
+ ...(declaration.handoffs === undefined
72
+ ? {}
73
+ : { handoffs: declaration.handoffs }),
74
+ });
75
+ }
55
76
  function record(value) {
56
77
  if (!value || typeof value !== "object" || Array.isArray(value))
57
78
  return {};
@@ -9,6 +9,7 @@ export interface RunIntent extends Partial<Continuation> {
9
9
  readonly kind: string;
10
10
  readonly runId: string;
11
11
  readonly operationId: string;
12
+ readonly workflow: string;
12
13
  }
13
14
  export type RunOutcome = {
14
15
  readonly kind: "accepted";
@@ -27,7 +28,7 @@ export interface StartParts {
27
28
  readonly runs: () => Promise<readonly OperationRun[]>;
28
29
  readonly input: (runId: string) => Promise<string | undefined>;
29
30
  readonly document: (digest: string) => Promise<string | undefined>;
30
- readonly workflow: (runId: string) => Promise<WorkflowFunction | undefined>;
31
+ readonly workflow: (intent: RunIntent) => Promise<WorkflowFunction | undefined>;
31
32
  readonly cancel: (receipt: string) => Promise<string>;
32
33
  }
33
34
  export interface StartDelivery {
@@ -31,7 +31,7 @@ async function continuation(parts, intent) {
31
31
  }
32
32
  async function delivered(parts, intent) {
33
33
  const continued = await continuation(parts, intent);
34
- const workflow = await parts.workflow(intent.runId);
34
+ const workflow = await parts.workflow(intent);
35
35
  const input = await parts.input(intent.runId);
36
36
  if (workflow === undefined || input === undefined)
37
37
  throw new Error(`start-not-available: ${intent.runId}`);
@@ -42,6 +42,7 @@ export interface GateOpening {
42
42
  readonly kind: "question" | "approval";
43
43
  readonly prompt: string;
44
44
  readonly subject: string;
45
+ readonly expiresAt?: string;
45
46
  }
46
47
  export interface WaitRegistration {
47
48
  readonly runId: string;
@@ -168,6 +168,9 @@ async function opening(hold, subject) {
168
168
  kind,
169
169
  prompt,
170
170
  subject: `sha256:${createHash("sha256").update(prompt).digest("hex")}`,
171
+ ...(request.expiresAt === undefined
172
+ ? {}
173
+ : { expiresAt: request.expiresAt }),
171
174
  });
172
175
  }
173
176
  async function gate(hold, request) {
@@ -194,6 +197,19 @@ export function stepBridge(ports) {
194
197
  return {
195
198
  effect: (request) => effect(hold, request),
196
199
  gate: (request) => gate(hold, request),
200
+ handoff: (request) => effect(hold, {
201
+ scope: {
202
+ phase: "handoff",
203
+ operation: "handoff",
204
+ round: 1,
205
+ key: createHash("sha256").update(ports.call().stepId).digest("hex"),
206
+ },
207
+ capability: "handoff",
208
+ method: "run",
209
+ target: request.workflowId,
210
+ payload: JSON.stringify(request.input),
211
+ units: 1,
212
+ }),
197
213
  };
198
214
  }
199
215
  export function registerStepBridge(bridge) {
@@ -3,6 +3,7 @@ import type { AuthorityState } from "../authority/transitions.ts";
3
3
  export type ReconciledState = Pick<AuthorityState, "runs" | "intents" | "details" | "gates" | "commands">;
4
4
  export interface WorkflowOutcome {
5
5
  readonly kind: string;
6
+ readonly promotionId?: string;
6
7
  readonly reason?: string;
7
8
  readonly issueUpdate?: "failed";
8
9
  readonly ticketStatus?: "complete" | "partial";
@@ -14,6 +14,9 @@ async function withOutcome(ports, observed) {
14
14
  : {
15
15
  ...world,
16
16
  outcome: read.outcome.kind,
17
+ ...(read.outcome.promotionId === undefined
18
+ ? {}
19
+ : { promotionId: read.outcome.promotionId }),
17
20
  ...(read.outcome.issueUpdate === "failed"
18
21
  ? { issueUpdate: read.outcome.issueUpdate }
19
22
  : {}),
@@ -1,4 +1,5 @@
1
1
  import { isTicketStatus } from "../ticket-status.js";
2
+ import { isPromotionId } from "../workflow-contracts.js";
2
3
  import { getRun } from "workflow/api";
3
4
  import { getWorld } from "workflow/runtime";
4
5
  export const runTag = "cloving.run";
@@ -47,8 +48,12 @@ export async function workflowOutcome(receipt) {
47
48
  const reason = Reflect.get(Object(value), "reason");
48
49
  const issueUpdate = Reflect.get(Object(value), "issueUpdate");
49
50
  const ticketStatus = Reflect.get(Object(value), "ticketStatus");
51
+ const promotionId = Reflect.get(Object(value), "promotionId");
50
52
  return {
51
53
  kind,
54
+ ...(kind === "handoff_requested" && isPromotionId(promotionId)
55
+ ? { promotionId }
56
+ : {}),
52
57
  ...(typeof reason === "string" ? { reason } : {}),
53
58
  ...(issueUpdate === "failed" ? { issueUpdate } : {}),
54
59
  ...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
@@ -0,0 +1,6 @@
1
+ export interface IssueIdentity {
2
+ readonly owner: string;
3
+ readonly repository: string;
4
+ readonly number: number;
5
+ }
6
+ export declare function issueIdentity(ticket: unknown): IssueIdentity | null;
@@ -0,0 +1,23 @@
1
+ export function issueIdentity(ticket) {
2
+ if (typeof ticket !== "string")
3
+ return null;
4
+ const markers = ticket
5
+ .split(/\r?\n/)
6
+ .filter((line) => line.startsWith("GitHub issue:"));
7
+ if (markers.length !== 1)
8
+ return null;
9
+ const match = /^GitHub issue: ([a-zA-Z0-9][a-zA-Z0-9-]*)\/([a-zA-Z0-9_.-]+)#([1-9][0-9]*)(?: \((?:open|closed)\))?\s*$/.exec(markers[0] ?? "");
10
+ const [, owner, repository, digits] = match ?? [];
11
+ const number = Number(digits);
12
+ if (owner === undefined ||
13
+ repository === undefined ||
14
+ [".", ".."].includes(repository) ||
15
+ !Number.isSafeInteger(number) ||
16
+ number <= 0)
17
+ return null;
18
+ return {
19
+ owner: owner.toLowerCase(),
20
+ repository: repository.toLowerCase(),
21
+ number,
22
+ };
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { Reply } from "../protocol.ts";
2
+ import { type ServiceContext } from "./replies.ts";
3
+ export declare function queueRunStatuses(context: ServiceContext, runIds: unknown): Promise<Reply>;
@@ -0,0 +1,39 @@
1
+ import { failed, invalidRequest, isIdentifier, result, } from "./replies.js";
2
+ export async function queueRunStatuses(context, runIds) {
3
+ if (!Array.isArray(runIds) ||
4
+ runIds.length === 0 ||
5
+ runIds.length > 4 ||
6
+ !runIds.every(isIdentifier))
7
+ return invalidRequest("A queue status batch needs one to four run IDs.");
8
+ const state = await context.authority.state();
9
+ const runs = [];
10
+ for (const runId of runIds) {
11
+ const run = state.runs[runId];
12
+ if (run === undefined)
13
+ return failed("unknown-run", { detail: `No run has the ID ${runId}.` });
14
+ runs.push(await queuedRun(context, {
15
+ runId,
16
+ status: run.status,
17
+ sequence: state.sequence,
18
+ }));
19
+ }
20
+ return result("queue-run-statuses", { runs });
21
+ }
22
+ async function queuedRun(context, run) {
23
+ const event = await context.authority.queries.latestCompletion({
24
+ runId: run.runId,
25
+ atOrBefore: run.sequence,
26
+ terminal: true,
27
+ });
28
+ return {
29
+ runId: run.runId,
30
+ state: run.status,
31
+ terminalSequence: event?.sequence ?? null,
32
+ outcome: event?.type === "run.completed"
33
+ ? {
34
+ kind: event.payload["outcome"],
35
+ issueUpdate: event.payload["issueUpdate"],
36
+ }
37
+ : null,
38
+ };
39
+ }
@@ -1,3 +1,4 @@
1
+ import type { WorkflowStage, WorkflowStages } from "../workflow-declaration.ts";
1
2
  import type { SubmissionContract } from "../submission.ts";
2
3
  import type { ExecutionManifest } from "../workflow-admission.ts";
3
4
  export type Resolution = {
@@ -13,11 +14,13 @@ export interface ResolutionRequest {
13
14
  readonly submission: Readonly<Record<string, unknown>>;
14
15
  readonly commandId: string;
15
16
  readonly signal: AbortSignal;
17
+ readonly activeTickets?: readonly Readonly<Record<string, unknown>>[];
16
18
  }
17
19
  export interface Resolver {
18
20
  readonly id: string;
19
21
  readonly version: string;
20
22
  readonly resolve: (request: ResolutionRequest) => Promise<Resolution>;
23
+ readonly skippedTickets?: () => readonly Readonly<Record<string, unknown>>[];
21
24
  }
22
25
  export interface WorkflowRegistration {
23
26
  readonly workflowId: string;
@@ -26,15 +29,18 @@ export interface WorkflowRegistration {
26
29
  readonly submission: SubmissionContract;
27
30
  readonly resolver: Resolver;
28
31
  readonly effectLimit?: number;
29
- readonly admission?: "when-idle";
32
+ readonly stages?: readonly WorkflowStage[];
33
+ readonly handoffs?: readonly string[];
34
+ readonly admission?: "when-idle" | number;
30
35
  }
31
36
  export interface Registered extends WorkflowRegistration {
32
37
  readonly pin: string;
33
38
  }
34
39
  export interface Registry {
35
40
  readonly workflowIds: readonly string[];
41
+ readonly workflowStages: WorkflowStages;
36
42
  readonly byId: (workflowId: string) => Registered | undefined;
37
43
  readonly byPin: (pin: string) => Registered | undefined;
38
44
  readonly available: (pinned: ExecutionManifest) => ExecutionManifest | undefined;
39
45
  }
40
- export declare function workflowRegistry(workflows: readonly WorkflowRegistration[]): Registry;
46
+ export declare function workflowRegistry(workflows: readonly WorkflowRegistration[], retainedWorkflows?: readonly WorkflowRegistration[]): Registry;
@@ -1,13 +1,25 @@
1
1
  import { workflowPin } from "../broker/workflow-pin.js";
2
- export function workflowRegistry(workflows) {
3
- const registered = workflows.map((workflow) => ({
2
+ import { declaredManifest } from "../execution-pin.js";
3
+ function registered(workflow) {
4
+ const manifest = declaredManifest(workflow.manifest, workflow);
5
+ return {
4
6
  ...workflow,
5
- pin: workflowPin(workflow.manifest),
6
- }));
7
+ ...(manifest.handoffs === undefined ? {} : { handoffs: manifest.handoffs }),
8
+ manifest,
9
+ pin: workflowPin(manifest),
10
+ };
11
+ }
12
+ export function workflowRegistry(workflows, retainedWorkflows = []) {
13
+ const current = workflows.map(registered);
14
+ const available = [...current, ...retainedWorkflows.map(registered)].filter((workflow) => workflow.availability === "available");
15
+ const byPin = (pin) => available.find((workflow) => workflow.pin === pin);
7
16
  return {
8
- workflowIds: registered.map((workflow) => workflow.workflowId),
9
- byId: (workflowId) => registered.find((workflow) => workflow.workflowId === workflowId),
10
- byPin: (pin) => registered.find((workflow) => workflow.pin === pin),
11
- available: (pinned) => registered.find((workflow) => workflow.manifest.workflowId === pinned.workflowId)?.manifest,
17
+ workflowIds: current.map((workflow) => workflow.workflowId),
18
+ workflowStages: Object.fromEntries(current.flatMap((workflow) => workflow.stages === undefined
19
+ ? []
20
+ : [[workflow.workflowId, workflow.stages]])),
21
+ byId: (workflowId) => current.find((workflow) => workflow.workflowId === workflowId),
22
+ byPin,
23
+ available: (pinned) => byPin(workflowPin(pinned))?.manifest,
12
24
  };
13
25
  }
@@ -3,9 +3,11 @@ import type { Fields, Reply } from "../protocol.ts";
3
3
  import type { ChangeSignal } from "./changes.ts";
4
4
  import type { Gateway } from "./gateway.ts";
5
5
  import type { Registry } from "./registry.ts";
6
+ import type { WorkflowRefusal } from "../workflow-refusals.ts";
6
7
  export interface ServiceContext {
7
8
  readonly authority: Authority;
8
9
  readonly registry: Registry;
10
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
9
11
  readonly gateway: Gateway;
10
12
  readonly clock: () => string;
11
13
  readonly livenessMilliseconds: number;