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,13 +1,18 @@
1
+ import { issueIdentity } from "../issue-identity.js";
2
+ import { linksOf } from "../authority/handoff-chain.js";
1
3
  import { runView, unresolvedEffects, } from "../authority/status.js";
2
4
  import { record } from "../protocol.js";
3
5
  import { committed, failed, invalidRequest, isIdentifier, publicReceipt, rejection, result, wholeNumber, } from "./replies.js";
4
6
  import { usageTotals } from "./worker-totals.js";
5
7
  import { feedbackOf } from "./decision-handler.js";
8
+ import { queueOverview } from "./submission-handler.js";
9
+ import { queueRunStatuses } from "./queue-runs.js";
6
10
  async function snapshot(context) {
7
11
  const { authority } = context;
8
12
  const state = await authority.state();
9
13
  const sequences = await authority.queries.sequences();
10
14
  return {
15
+ links: await authority.queries.runLinks(),
11
16
  state,
12
17
  operations: await authority.operations(),
13
18
  sequences: sequences.runs,
@@ -39,9 +44,7 @@ async function effectInput(context, runId) {
39
44
  const budget = await effects.budget(runId);
40
45
  const stored = await effects.operations(runId);
41
46
  return {
42
- budget: budget === undefined
43
- ? null
44
- : { limit: budget.limit, spent: budget.spent },
47
+ budget: budget ? { limit: budget.limit, spent: budget.spent } : null,
45
48
  effects: stored.map((operation) => ({
46
49
  operationId: operation.operationId,
47
50
  status: operation.status,
@@ -78,6 +81,8 @@ async function viewOf(context, scope) {
78
81
  };
79
82
  return {
80
83
  ...runView(input),
84
+ ...linksOf({ runId: run.runId, ...scope.snapshot.links[run.runId] }),
85
+ input: await submittedInput(context, details?.submission),
81
86
  workflowId: scope.snapshot.workflowIds[run.runId] ?? run.workflow,
82
87
  latestAt: times[run.runId] ?? null,
83
88
  unresolvedEffects: unresolvedEffects(input),
@@ -87,6 +92,8 @@ async function viewOf(context, scope) {
87
92
  function summary(view) {
88
93
  return {
89
94
  runId: view.runId,
95
+ ...linksOf(view),
96
+ input: view.input,
90
97
  workflowId: view.workflowId,
91
98
  state: view.state,
92
99
  condition: view.condition,
@@ -122,17 +129,17 @@ async function outcomeOf(context, view) {
122
129
  runId: view.runId,
123
130
  atOrBefore: view.latestSequence,
124
131
  });
125
- const kind = event?.payload["outcome"];
132
+ const payload = event?.payload ?? {};
133
+ const kind = payload["outcome"];
126
134
  if (kind === undefined)
127
135
  return null;
128
- const ticketStatus = event?.payload["ticketStatus"];
136
+ const { ticketStatus, promotionId, outcomeReason, issueUpdate } = payload;
129
137
  return {
130
138
  kind,
131
- reason: event?.payload["outcomeReason"] ?? null,
139
+ reason: outcomeReason ?? null,
140
+ ...(promotionId === undefined ? {} : { promotionId }),
132
141
  ...(ticketStatus === undefined ? {} : { ticketStatus }),
133
- ...(event?.payload["issueUpdate"] === "failed"
134
- ? { issueUpdate: "failed" }
135
- : {}),
142
+ ...(issueUpdate === "failed" ? { issueUpdate } : {}),
136
143
  };
137
144
  }
138
145
  function titleOf(input, workflowId) {
@@ -161,8 +168,18 @@ async function metadataOf(context, source) {
161
168
  return {
162
169
  ticketId: typeof ticketId === "string" && ticketId !== "" ? ticketId : null,
163
170
  title: titleOf(input, source.workflowId),
171
+ ...(source.workflowId === "develop-issue"
172
+ ? { issueIdentity: issueIdentity(input?.["ticket"]) }
173
+ : {}),
164
174
  };
165
175
  }
176
+ async function submittedInput(context, digest) {
177
+ const document = digest
178
+ ? await context.authority.queries.document(digest)
179
+ : undefined;
180
+ const request = record(JSON.parse(document ?? "null"));
181
+ return record(request?.["submission"]) ?? null;
182
+ }
166
183
  async function listed(context, page) {
167
184
  const identifiers = Object.keys(page.snapshot.state.runs)
168
185
  .sort()
@@ -173,21 +190,36 @@ async function listed(context, page) {
173
190
  views.push(await viewOf(context, { snapshot: page.snapshot, runId }));
174
191
  const runs = views.filter((view) => view !== undefined).map(summary);
175
192
  return result("run-list", {
193
+ ...(await queueOverview(context)),
176
194
  workflows: context.registry.workflowIds,
195
+ workflowRefusals: context.workflowRefusals ?? [],
196
+ ...(Object.keys(context.registry.workflowStages).length === 0
197
+ ? {}
198
+ : {
199
+ workflowStages: context.registry.workflowStages,
200
+ }),
177
201
  runs,
178
202
  next: identifiers.length > shown.length ? (shown.at(-1) ?? null) : null,
179
203
  worldReports: context.worldReports?.() ?? [],
180
204
  });
181
205
  }
182
- export async function status(context, parameters) {
183
- const { runId } = parameters;
206
+ function listParameters(parameters) {
184
207
  const after = parameters["after"] ?? "";
185
208
  const limit = parameters["limit"] ?? 50;
186
209
  if (typeof after !== "string" || !wholeNumber(limit, 1) || limit > 500)
210
+ return undefined;
211
+ return { after, limit };
212
+ }
213
+ export async function status(context, parameters) {
214
+ if (parameters["runIds"] !== undefined)
215
+ return queueRunStatuses(context, parameters["runIds"]);
216
+ const { runId } = parameters;
217
+ const page = listParameters(parameters);
218
+ if (page === undefined)
187
219
  return invalidRequest("The status request is not valid.");
188
220
  const current = await snapshot(context);
189
221
  if (runId === undefined)
190
- return listed(context, { snapshot: current, after, limit });
222
+ return listed(context, { snapshot: current, ...page });
191
223
  const named = typeof runId === "string" ? runId : "";
192
224
  const view = await viewOf(context, { snapshot: current, runId: named });
193
225
  if (view === undefined)
@@ -1,4 +1,5 @@
1
1
  import { type Authority, type AuthorityTiming, type Delivery, type EffectSettings, type Refusal } from "../broker/actor/authority.ts";
2
+ import { type WorkflowRefusal } from "../workflow-refusals.ts";
2
3
  import { type WorkflowRegistration } from "./registry.ts";
3
4
  import { type WorkerPort } from "./worker-port.ts";
4
5
  export type { WorkflowRegistration } from "./registry.ts";
@@ -38,6 +39,8 @@ export interface ServiceOptions {
38
39
  readonly stateDirectory: string;
39
40
  readonly runtimeDirectory: string;
40
41
  readonly workflows: readonly WorkflowRegistration[];
42
+ readonly retainedWorkflows?: readonly WorkflowRegistration[];
43
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
41
44
  readonly delivery: Delivery;
42
45
  readonly host?: (ports: HostPorts) => Promise<ServiceHost>;
43
46
  readonly timing?: ServiceTiming;
@@ -2,6 +2,7 @@ import { lstat, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { authorityTiming, openAuthority, } from "../broker/actor/authority.js";
4
4
  import { socketAddress } from "../broker/socket-address.js";
5
+ import { workflowRefusals, } from "../workflow-refusals.js";
5
6
  import { changeSignal } from "./changes.js";
6
7
  import { decision, questions } from "./decision-handler.js";
7
8
  import { events } from "./event-handler.js";
@@ -87,6 +88,7 @@ async function started(authority, setup) {
87
88
  handle: dispatcher({
88
89
  authority,
89
90
  registry,
91
+ workflowRefusals: workflowRefusals(options.workflowRefusals),
90
92
  gateway,
91
93
  clock,
92
94
  changed: changes.next,
@@ -133,7 +135,7 @@ export async function startService(options) {
133
135
  if (typeof path !== "string")
134
136
  return path;
135
137
  const timing = options.timing ?? serviceTiming;
136
- const registry = workflowRegistry(options.workflows);
138
+ const registry = workflowRegistry(options.workflows, options.retainedWorkflows);
137
139
  const opening = await openAuthority(options.stateDirectory, {
138
140
  delivery: options.delivery,
139
141
  timing: timing.authority,
@@ -1,4 +1,5 @@
1
1
  import { type Fields, type Reply } from "../protocol.ts";
2
2
  import { type ServiceContext } from "./replies.ts";
3
3
  export declare function runIdentity(commandId: string): string;
4
+ export declare function queueOverview(context: ServiceContext): Promise<Fields>;
4
5
  export declare function submissionHandler(context: ServiceContext): (parameters: Fields) => Promise<Reply>;
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { handoffChain } from "../authority/handoff-chain.js";
3
+ import { executionCapabilities } from "../execution-pin.js";
2
4
  import { setTimeout as delay } from "node:timers/promises";
3
- import { isIdentifier as isWorkflowIdentifier } from "../authority/commands.js";
5
+ import { isCommandId, isIdentifier as isWorkflowIdentifier, } from "../authority/commands.js";
4
6
  import { documentDigest } from "../broker/decision-restore.js";
5
7
  import { record } from "../protocol.js";
6
8
  import { checkResolution, checkSubmission, normalizedRequest, } from "../submission.js";
7
- import { committed, failed, invalidRequest, isIdentifier, publicReceipt, rejection, result, } from "./replies.js";
9
+ import { committed, failed, invalidRequest, publicReceipt, rejection, result, } from "./replies.js";
8
10
  export function runIdentity(commandId) {
9
11
  return `run-${createHash("sha256").update(commandId).digest("hex").slice(0, 32)}`;
10
12
  }
@@ -49,7 +51,7 @@ function effectBudget(workflow) {
49
51
  return {};
50
52
  return {
51
53
  effectLimit: workflow.effectLimit,
52
- capabilities: JSON.stringify([...workflow.manifest.capabilities].sort()),
54
+ capabilities: executionCapabilities(workflow.manifest),
53
55
  };
54
56
  }
55
57
  function usable(workflow, workflowId) {
@@ -73,7 +75,13 @@ async function resolved(context, admission) {
73
75
  const { workflow, workflowId, commandId, submission } = admission;
74
76
  const limit = new AbortController();
75
77
  const timer = new AbortController();
76
- const request = { workflowId, submission, commandId, signal: limit.signal };
78
+ const request = {
79
+ workflowId,
80
+ submission,
81
+ commandId,
82
+ signal: limit.signal,
83
+ activeTickets: admission.activeTickets,
84
+ };
77
85
  const call = Promise.resolve(request)
78
86
  .then((given) => workflow.resolver.resolve(given))
79
87
  .then((value) => checkResolution(value) ?? "failure")
@@ -110,6 +118,9 @@ async function admit(context, admission) {
110
118
  return unresolved(context, { kind: resolution, commandId });
111
119
  if (resolution.kind === "rejected")
112
120
  return failed(resolution.code, { commandId, detail: resolution.detail });
121
+ const limited = capacityRefusal(workflow, admission);
122
+ if (limited !== undefined)
123
+ return limited;
113
124
  const { input } = resolution;
114
125
  if (Buffer.byteLength(input) > context.documentBytes)
115
126
  return unresolved(context, { kind: "failure", commandId });
@@ -124,6 +135,12 @@ async function admit(context, admission) {
124
135
  input: documentDigest(input),
125
136
  resolver: `${workflow.resolver.id}:${workflow.resolver.version}`,
126
137
  ...effectBudget(workflow),
138
+ ...(admission.chain.kind === "handoff"
139
+ ? {
140
+ parentRunId: admission.chain.parentRunId,
141
+ chainId: admission.chain.chainId,
142
+ }
143
+ : {}),
127
144
  },
128
145
  documents: [normalized, input],
129
146
  });
@@ -137,9 +154,32 @@ async function admit(context, admission) {
137
154
  replayed: receipt.kind === "duplicate",
138
155
  });
139
156
  }
157
+ function admissionRefusal(workflow, request) {
158
+ const { workflowId, commandId, submission, activeTickets } = request;
159
+ if (workflowId === "develop-issue" &&
160
+ typeof submission["ticket"] === "string" &&
161
+ activeTickets.some((input) => input["ticketId"] === submission["ticket"]))
162
+ return failed("ticket-active", {
163
+ commandId,
164
+ detail: "An active run already works on this ticket.",
165
+ });
166
+ if (workflowId === "develop-issue" && submission["ticket"] !== "next")
167
+ return undefined;
168
+ return capacityRefusal(workflow, request);
169
+ }
170
+ function capacityRefusal(workflow, request) {
171
+ if (workflow.admission !== undefined &&
172
+ request.activeTickets.length >=
173
+ (workflow.admission === "when-idle" ? 1 : workflow.admission))
174
+ return failed("workflow-active", {
175
+ commandId: request.commandId,
176
+ detail: `The workflow ${request.workflowId} already has an active run.`,
177
+ });
178
+ return undefined;
179
+ }
140
180
  async function submitted(context, parameters) {
141
181
  const { workflowId, commandId } = parameters;
142
- if (!isWorkflowIdentifier(workflowId) || !isIdentifier(commandId))
182
+ if (!isWorkflowIdentifier(workflowId) || !isCommandId(commandId))
143
183
  return invalidRequest("The workflow ID or the command ID is not valid.");
144
184
  const existing = await context.authority.queries.receipt(commandId);
145
185
  if (existing !== undefined)
@@ -147,6 +187,14 @@ async function submitted(context, parameters) {
147
187
  workflowId,
148
188
  submission: parameters["submission"],
149
189
  });
190
+ const chain = handoffChain(commandId, commandId.startsWith("handoff:")
191
+ ? await context.authority.queries.runOrigins()
192
+ : {});
193
+ if (chain.kind === "refused")
194
+ return failed(chain.reason, {
195
+ commandId,
196
+ detail: "A hand-off chain permits at most five runs and requires a recorded parent.",
197
+ });
150
198
  const found = usable(context.registry.byId(workflowId), workflowId);
151
199
  if (found.kind === "unusable")
152
200
  return found.reply;
@@ -161,32 +209,63 @@ async function submitted(context, parameters) {
161
209
  });
162
210
  if (check.kind === "rejected")
163
211
  return failed(check.code, { commandId, detail: check.detail });
164
- if (found.workflow.admission === "when-idle" &&
165
- (await activeWorkflow(context, workflowId)))
166
- return failed("workflow-active", {
167
- commandId,
168
- detail: `The workflow ${workflowId} already has an active run.`,
169
- });
212
+ const activeTickets = await activeWorkflow(context, workflowId);
213
+ const refusal = admissionRefusal(found.workflow, {
214
+ workflowId,
215
+ commandId,
216
+ submission: check.submission,
217
+ activeTickets,
218
+ });
219
+ if (refusal !== undefined)
220
+ return refusal;
170
221
  return admit(context, {
222
+ chain,
171
223
  workflow: found.workflow,
172
224
  workflowId,
173
225
  commandId,
174
226
  submission: check.submission,
175
227
  normalized: check.normalized,
228
+ activeTickets,
176
229
  });
177
230
  }
178
231
  async function activeWorkflow(context, workflowId) {
179
232
  const workflows = await context.authority.queries.workflowIds();
180
233
  const state = await context.authority.state();
181
- return Object.values(state.runs).some((run) => workflows[run.runId] === workflowId &&
234
+ const runs = Object.values(state.runs).filter((run) => workflows[run.runId] === workflowId &&
182
235
  !["completed", "failed", "cancelled"].includes(run.status));
236
+ return Promise.all(runs.map(async (run) => {
237
+ const digest = state.details[run.runId]?.input;
238
+ const text = digest
239
+ ? await context.authority.queries.document(digest)
240
+ : undefined;
241
+ return record(JSON.parse(text ?? "null")) ?? {};
242
+ }));
243
+ }
244
+ export async function queueOverview(context) {
245
+ const workflow = context.registry.byId("develop-issue");
246
+ if (workflow?.admission === undefined)
247
+ return {};
248
+ const active = await activeWorkflow(context, "develop-issue");
249
+ return {
250
+ queue: {
251
+ lanes: workflow.admission === "when-idle" ? 1 : workflow.admission,
252
+ busy: active.length,
253
+ skipped: [
254
+ ...active.flatMap((input) => typeof input["ticketId"] === "string"
255
+ ? [{ ticketId: input["ticketId"], reason: "ticket-active" }]
256
+ : []),
257
+ ...(workflow.resolver.skippedTickets?.() ?? []),
258
+ ],
259
+ },
260
+ };
183
261
  }
184
262
  export function submissionHandler(context) {
185
263
  const pending = new Map();
186
264
  return (parameters) => {
187
265
  const keys = [`command:${String(parameters["commandId"])}`];
188
266
  const workflowId = String(parameters["workflowId"]);
189
- if (context.registry.byId(workflowId)?.admission === "when-idle")
267
+ if (workflowId === "develop-issue" ||
268
+ context.registry.byId(workflowId)?.admission !== undefined)
190
269
  keys.push(`workflow:${workflowId}`);
191
270
  const next = () => submitted(context, parameters);
192
271
  const previous = keys
@@ -1,5 +1,5 @@
1
1
  import { record } from "../protocol.js";
2
- import { priceOf, usageOf, workerTotals, } from "../worker-usage.js";
2
+ import { priceOf, usageOf, tokenCount, workerTotals, } from "../worker-usage.js";
3
3
  function reportFields(text) {
4
4
  try {
5
5
  return record(JSON.parse(text ?? "null")) ?? {};
@@ -8,6 +8,28 @@ function reportFields(text) {
8
8
  return {};
9
9
  }
10
10
  }
11
+ function attemptsOf(value) {
12
+ if (!Array.isArray(value))
13
+ return undefined;
14
+ return value.flatMap((entry) => {
15
+ const fields = record(entry) ?? {};
16
+ const attempt = tokenCount(fields["attempt"]);
17
+ const model = fields["model"];
18
+ if (attempt === null ||
19
+ attempt === 0 ||
20
+ (model !== null && typeof model !== "string"))
21
+ return [];
22
+ return [
23
+ {
24
+ attempt,
25
+ model,
26
+ usage: usageOf(fields["usage"]),
27
+ costUsd: priceOf(fields["costUsd"]),
28
+ estimatedCostUsd: priceOf(fields["estimatedCostUsd"]),
29
+ },
30
+ ];
31
+ });
32
+ }
11
33
  export async function usageTotals(context, runId) {
12
34
  const { effects, queries } = context.authority;
13
35
  const operations = (await effects.operations(runId)).filter((operation) => operation.capability === "claude" &&
@@ -17,7 +39,9 @@ export async function usageTotals(context, runId) {
17
39
  const report = reportFields(operation.result === null
18
40
  ? undefined
19
41
  : await queries.document(operation.result));
42
+ const attempts = attemptsOf(report["attempts"]);
20
43
  turns.push({
44
+ ...(attempts === undefined ? {} : { attempts }),
21
45
  operationId: operation.operationId,
22
46
  usage: usageOf(report["usage"]),
23
47
  costUsd: priceOf(report["costUsd"]),
@@ -39,6 +39,14 @@ export interface GateRequest {
39
39
  readonly gateId: string;
40
40
  readonly kind: "question" | "approval";
41
41
  readonly prompt: string;
42
+ readonly expiresAt?: string;
43
+ }
44
+ export type HandoffValue = string | number | boolean | null | readonly HandoffValue[] | {
45
+ readonly [key: string]: HandoffValue;
46
+ };
47
+ export interface HandoffRequest {
48
+ readonly workflowId: string;
49
+ readonly input: Readonly<Record<string, HandoffValue>>;
42
50
  }
43
51
  export type GateResult = {
44
52
  readonly kind: "opened";
@@ -53,5 +61,6 @@ export type GateResult = {
53
61
  export interface StepBridge {
54
62
  readonly effect: (request: ProtectedRequest) => Promise<ProtectedResult>;
55
63
  readonly gate: (request: GateRequest) => Promise<GateResult>;
64
+ readonly handoff?: (request: HandoffRequest) => Promise<ProtectedResult>;
56
65
  }
57
66
  export declare function isStepBridge(value: unknown): value is StepBridge;
@@ -0,0 +1,2 @@
1
+ import { type HandoffRequest } from "../step-bridge.ts";
2
+ export declare function handoff(request: HandoffRequest): Promise<string>;
@@ -0,0 +1,13 @@
1
+ import { isStepBridge, stepBridgeKey, } from "../step-bridge.js";
2
+ export async function handoff(request) {
3
+ "use step";
4
+ const bridge = Reflect.get(globalThis, Symbol.for(stepBridgeKey));
5
+ if (!isStepBridge(bridge) || bridge.handoff === undefined)
6
+ throw new Error("cloving-host-unavailable");
7
+ const result = await bridge.handoff(request);
8
+ if (result.kind !== "completed")
9
+ throw new Error(result.reason ?? result.kind);
10
+ if (result.result === null)
11
+ throw new Error("handoff-result-missing");
12
+ return result.result;
13
+ }
@@ -0,0 +1 @@
1
+ export declare function hardDependencies(ticket: string): readonly string[];
@@ -0,0 +1,15 @@
1
+ export function hardDependencies(ticket) {
2
+ const lines = ticket.split(/\r?\n/);
3
+ const start = lines.findIndex((line) => /^## Dependencies\s*$/.test(line));
4
+ if (start === -1)
5
+ return [];
6
+ const following = lines.slice(start + 1);
7
+ const end = following.findIndex((line) => /^#{1,2} /.test(line));
8
+ const section = (end === -1 ? following : following.slice(0, end)).join("\n");
9
+ const sentences = section
10
+ .replace(/\b(?:e\.g|i\.e)\./g, "example")
11
+ .matchAll(/(?:^|[.!?]\s+|\n)\s*(?:[-*+]\s+|\d+\.\s+)?Needs\b([\s\S]*?)(?=[.!?](?:\s|$)|$)/g);
12
+ return [
13
+ ...new Set([...sentences].flatMap((sentence) => [...String(sentence[1]).matchAll(/#([1-9]\d*)\b/g)].map((match) => String(match[1])))),
14
+ ];
15
+ }
@@ -8,11 +8,14 @@ function parsedStatus(summary) {
8
8
  if (status !== "Ticket status: partial")
9
9
  return undefined;
10
10
  const heading = lines.lastIndexOf("Remaining:");
11
- if (heading === -1)
12
- return undefined;
13
- const items = lines.slice(heading + 1);
11
+ const items = heading === -1
12
+ ? []
13
+ : lines.slice(heading + 1).filter((line) => line.trim() !== "");
14
14
  if (items.length === 0 || items.some((line) => !/^- \S.*$/.test(line)))
15
- return undefined;
15
+ return {
16
+ kind: "partial",
17
+ remaining: ["The worker report has an unreadable Remaining: list."],
18
+ };
16
19
  return { kind: "partial", remaining: items.map((line) => line.slice(2)) };
17
20
  }
18
21
  function preservedStatus(value) {
@@ -11,8 +11,10 @@ export declare const builtInPriceTable: {
11
11
  readonly prices: TokenPrices;
12
12
  };
13
13
  export declare function parseTokenPrices(value: string | undefined): TokenPrices;
14
+ export declare function parseFallbackModels(value: string | undefined): readonly string[];
14
15
  export declare function priceForModel(prices: TokenPrices, model: string | null): TokenPrice | undefined;
15
16
  export declare function commandModel(command: readonly string[]): string | null;
17
+ export declare function replaceCommandModel(command: readonly string[], model: string): readonly string[];
16
18
  export declare function estimatedCostUsd(usage: WorkerUsage, pricing: {
17
19
  readonly worker: "claude" | "codex";
18
20
  readonly price: TokenPrice | undefined;
@@ -36,26 +36,58 @@ export function parseTokenPrices(value) {
36
36
  throw new Error("Invalid CLOVING_TOKEN_PRICES");
37
37
  }
38
38
  }
39
+ export function parseFallbackModels(value) {
40
+ if (value === undefined)
41
+ return [];
42
+ try {
43
+ const models = JSON.parse(value);
44
+ if (Array.isArray(models) &&
45
+ models.length <= 3 &&
46
+ models.every((model) => typeof model === "string" && model.trim().length > 0))
47
+ return models.map(String);
48
+ }
49
+ catch {
50
+ throw new Error("The daemon did not start: invalid-fallback-models");
51
+ }
52
+ throw new Error("The daemon did not start: invalid-fallback-models");
53
+ }
39
54
  export function priceForModel(prices, model) {
40
55
  return model !== null && Object.hasOwn(prices, model)
41
56
  ? prices[model]
42
57
  : undefined;
43
58
  }
44
- export function commandModel(command) {
45
- let model = null;
46
- for (let index = 1; index < command.length; index += 1) {
59
+ function modelIndex(command) {
60
+ let selected;
61
+ const separator = command.indexOf("--", 1);
62
+ const end = separator === -1 ? command.length : separator;
63
+ for (let index = 1; index < end; index += 1) {
47
64
  const argument = command[index] ?? "";
48
- if (argument === "--")
49
- break;
50
65
  if (argument.startsWith("--model="))
51
- model = argument.slice(8) || null;
52
- if (argument === "--model" || argument === "-m") {
66
+ selected = argument.slice(8) ? index : undefined;
67
+ if (["--model", "-m"].includes(argument)) {
53
68
  const next = command[index + 1];
54
- model = next && !next.startsWith("-") ? next : null;
69
+ selected = next && !next.startsWith("-") ? index + 1 : undefined;
55
70
  index += 1;
56
71
  }
57
72
  }
58
- return model;
73
+ return selected;
74
+ }
75
+ export function commandModel(command) {
76
+ const index = modelIndex(command);
77
+ if (index === undefined)
78
+ return null;
79
+ const argument = command[index] ?? "";
80
+ return argument.startsWith("--model=") ? argument.slice(8) : argument;
81
+ }
82
+ export function replaceCommandModel(command, model) {
83
+ const index = modelIndex(command);
84
+ if (index === undefined)
85
+ return command;
86
+ return command.map((argument, position) => {
87
+ if (position !== index)
88
+ return argument;
89
+ return argument.startsWith("--model=") ? `--model=${model}` : model;
90
+ });
59
91
  }
60
92
  export function estimatedCostUsd(usage, pricing) {
61
93
  const { worker, price } = pricing;
@@ -4,11 +4,19 @@ export interface WorkerUsage {
4
4
  readonly output_tokens: number | null;
5
5
  }
6
6
  export interface WorkerTurnUsage {
7
+ readonly attempts?: readonly WorkerAttemptUsage[];
7
8
  readonly estimatedCostUsd: number | null;
8
9
  readonly operationId: string;
9
10
  readonly usage: WorkerUsage;
10
11
  readonly costUsd: number | null;
11
12
  }
13
+ export interface WorkerAttemptUsage {
14
+ readonly attempt: number;
15
+ readonly model: string | null;
16
+ readonly usage: WorkerUsage;
17
+ readonly costUsd: number | null;
18
+ readonly estimatedCostUsd: number | null;
19
+ }
12
20
  export interface WorkerTotals {
13
21
  readonly estimatedCostUsd: number | null;
14
22
  readonly workerTurns: readonly WorkerTurnUsage[];
@@ -19,4 +27,5 @@ export declare function priceOf(value: unknown): number | null;
19
27
  export declare function tokenCount(value: unknown): number | null;
20
28
  export declare function usageOf(value: unknown): WorkerUsage;
21
29
  export declare function sumUsage(values: readonly WorkerUsage[]): WorkerUsage;
30
+ export declare function sumPrices(values: readonly (number | null)[]): number | null;
22
31
  export declare function workerTotals(workerTurns: readonly WorkerTurnUsage[]): WorkerTotals;
@@ -28,7 +28,7 @@ export function sumUsage(values) {
28
28
  output_tokens: total(values.map((value) => value.output_tokens)),
29
29
  };
30
30
  }
31
- function sumPrices(values) {
31
+ export function sumPrices(values) {
32
32
  const prices = values.map(priceOf);
33
33
  if (prices.length === 0 || prices.includes(null))
34
34
  return null;
@@ -27,6 +27,7 @@ export interface ExecutionManifest {
27
27
  readonly workflowId: string;
28
28
  readonly steps: readonly string[];
29
29
  readonly capabilities: readonly string[];
30
+ readonly handoffs?: readonly string[];
30
31
  readonly artifacts: Readonly<Record<string, string>>;
31
32
  readonly resources: Readonly<Record<string, string>>;
32
33
  readonly toolchain: ToolchainIdentity;
@@ -5,6 +5,7 @@ const workflowModules = [
5
5
  "cloving/workflows/steps/decision-gate",
6
6
  "cloving/workflows/steps/load-cycle",
7
7
  "cloving/workflows/steps/protected-effect",
8
+ "cloving/workflows/steps/handoff",
8
9
  ];
9
10
  function isMethodList(value) {
10
11
  return (Array.isArray(value) &&
@@ -16,6 +16,7 @@ export type CycleOutcome = {
16
16
  readonly promotionId: string;
17
17
  readonly ticketStatus?: "complete" | "partial";
18
18
  readonly issueUpdate?: "failed";
19
+ readonly reason?: string;
19
20
  };
20
21
  export type Validation<Value> = {
21
22
  readonly kind: "valid";
@@ -24,6 +25,7 @@ export type Validation<Value> = {
24
25
  readonly kind: "invalid";
25
26
  readonly reason: string;
26
27
  };
28
+ export declare function isPromotionId(value: unknown): value is string;
27
29
  export declare function isBlockedReason(value: unknown): value is string;
28
30
  export declare function validateDevelopmentInput(value: unknown): Validation<DevelopmentInput>;
29
31
  export declare function validateCycleOutcome(value: unknown, input: DevelopmentInput): Validation<CycleOutcome>;