cloving 1.0.1 → 1.1.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 (177) hide show
  1. package/README.md +145 -14
  2. package/dist/src/authority/commands.js +10 -2
  3. package/dist/src/authority/reconciliation.d.ts +2 -0
  4. package/dist/src/authority/reconciliation.js +10 -1
  5. package/dist/src/authority/transitions.d.ts +1 -1
  6. package/dist/src/authority/transitions.js +2 -0
  7. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  8. package/dist/src/broker/actor/authority-session.js +3 -4
  9. package/dist/src/chats/analysis-batches.d.ts +24 -0
  10. package/dist/src/chats/analysis-batches.js +83 -0
  11. package/dist/src/chats/analysis-contract.d.ts +80 -0
  12. package/dist/src/chats/analysis-contract.js +16 -0
  13. package/dist/src/chats/analysis-data.d.ts +3 -0
  14. package/dist/src/chats/analysis-data.js +85 -0
  15. package/dist/src/chats/analysis-input.d.ts +12 -0
  16. package/dist/src/chats/analysis-input.js +83 -0
  17. package/dist/src/chats/analysis-links.d.ts +2 -0
  18. package/dist/src/chats/analysis-links.js +25 -0
  19. package/dist/src/chats/analysis-prompt.d.ts +10 -0
  20. package/dist/src/chats/analysis-prompt.js +16 -0
  21. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  22. package/dist/src/chats/analysis-proposal.js +32 -0
  23. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  24. package/dist/src/chats/analysis-provenance.js +99 -0
  25. package/dist/src/chats/analysis-shape.d.ts +3 -0
  26. package/dist/src/chats/analysis-shape.js +228 -0
  27. package/dist/src/chats/analysis.d.ts +3 -0
  28. package/dist/src/chats/analysis.js +42 -0
  29. package/dist/src/chats/corpus-input.d.ts +5 -0
  30. package/dist/src/chats/corpus-input.js +288 -0
  31. package/dist/src/chats/destination.js +3 -1
  32. package/dist/src/chats/import.d.ts +8 -6
  33. package/dist/src/chats/import.js +195 -23
  34. package/dist/src/chats/input-file.d.ts +3 -0
  35. package/dist/src/chats/input-file.js +32 -0
  36. package/dist/src/chats/normalize.d.ts +11 -2
  37. package/dist/src/chats/normalize.js +18 -2
  38. package/dist/src/chats/previous-import.d.ts +5 -0
  39. package/dist/src/chats/previous-import.js +34 -0
  40. package/dist/src/chats/reader.d.ts +7 -1
  41. package/dist/src/chats/reader.js +46 -15
  42. package/dist/src/chats/redact-import.d.ts +14 -0
  43. package/dist/src/chats/redact-import.js +141 -0
  44. package/dist/src/chats/redaction.d.ts +41 -0
  45. package/dist/src/chats/redaction.js +161 -0
  46. package/dist/src/chats/revision-input.d.ts +3 -0
  47. package/dist/src/chats/revision-input.js +180 -0
  48. package/dist/src/chats/revision.d.ts +48 -0
  49. package/dist/src/chats/revision.js +94 -0
  50. package/dist/src/chats/selection.d.ts +25 -0
  51. package/dist/src/chats/selection.js +109 -0
  52. package/dist/src/chats/source.d.ts +23 -0
  53. package/dist/src/chats/source.js +135 -0
  54. package/dist/src/chats/structure.d.ts +57 -0
  55. package/dist/src/chats/structure.js +216 -0
  56. package/dist/src/cli/arguments.d.ts +8 -0
  57. package/dist/src/cli/arguments.js +94 -13
  58. package/dist/src/cli/client.d.ts +1 -0
  59. package/dist/src/cli/client.js +38 -1
  60. package/dist/src/cli/commands.d.ts +3 -0
  61. package/dist/src/cli/commands.js +29 -11
  62. package/dist/src/cli/main.d.ts +2 -1
  63. package/dist/src/cli/main.js +61 -14
  64. package/dist/src/cli/render.d.ts +2 -0
  65. package/dist/src/cli/render.js +70 -2
  66. package/dist/src/cli/usage-render.d.ts +3 -0
  67. package/dist/src/cli/usage-render.js +28 -0
  68. package/dist/src/cli/usage.d.ts +3 -2
  69. package/dist/src/cli/usage.js +38 -6
  70. package/dist/src/cli/watch-actions.d.ts +10 -0
  71. package/dist/src/cli/watch-actions.js +58 -0
  72. package/dist/src/cli/watch-board-view.d.ts +32 -0
  73. package/dist/src/cli/watch-board-view.js +88 -15
  74. package/dist/src/cli/watch-board.js +6 -1
  75. package/dist/src/cli/watch-card-client.d.ts +1 -0
  76. package/dist/src/cli/watch-card-client.js +44 -0
  77. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  78. package/dist/src/cli/watch-card-forms.js +33 -0
  79. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  80. package/dist/src/cli/watch-card-reply.js +34 -0
  81. package/dist/src/cli/watch-card-view.d.ts +40 -0
  82. package/dist/src/cli/watch-card-view.js +86 -0
  83. package/dist/src/cli/watch-client.d.ts +1 -0
  84. package/dist/src/cli/watch-client.js +118 -0
  85. package/dist/src/cli/watch-model.js +31 -14
  86. package/dist/src/cli/watch-page.d.ts +1 -1
  87. package/dist/src/cli/watch-page.js +71 -85
  88. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  89. package/dist/src/cli/watch-pr-board.js +26 -0
  90. package/dist/src/cli/watch-pr-command.d.ts +15 -0
  91. package/dist/src/cli/watch-pr-command.js +37 -0
  92. package/dist/src/cli/watch-pr-identity.d.ts +20 -0
  93. package/dist/src/cli/watch-pr-identity.js +62 -0
  94. package/dist/src/cli/watch-pr-observer.d.ts +19 -0
  95. package/dist/src/cli/watch-pr-observer.js +108 -0
  96. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  97. package/dist/src/cli/watch-pr-settings.js +47 -0
  98. package/dist/src/cli/watch-reads.js +4 -0
  99. package/dist/src/cli/watch-render.d.ts +16 -2
  100. package/dist/src/cli/watch-render.js +10 -3
  101. package/dist/src/cli/watch-run-input.d.ts +14 -0
  102. package/dist/src/cli/watch-run-input.js +49 -0
  103. package/dist/src/cli/watch-server.d.ts +3 -0
  104. package/dist/src/cli/watch-server.js +44 -6
  105. package/dist/src/cli/watch-start-view.d.ts +18 -0
  106. package/dist/src/cli/watch-start-view.js +77 -0
  107. package/dist/src/cli/watch-turn.d.ts +1 -0
  108. package/dist/src/cli/watch-turn.js +7 -2
  109. package/dist/src/cli/watch-usage.d.ts +6 -0
  110. package/dist/src/cli/watch-usage.js +65 -0
  111. package/dist/src/cli/watch-view.d.ts +15 -0
  112. package/dist/src/cli/watch-view.js +61 -17
  113. package/dist/src/cli.js +48 -6
  114. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  115. package/dist/src/daemon/analysis-launch.js +81 -0
  116. package/dist/src/daemon/analysis-token.d.ts +6 -0
  117. package/dist/src/daemon/analysis-token.js +88 -0
  118. package/dist/src/daemon/analysis-worker.d.ts +22 -0
  119. package/dist/src/daemon/analysis-worker.js +219 -0
  120. package/dist/src/daemon/chats-files.d.ts +15 -0
  121. package/dist/src/daemon/chats-files.js +113 -0
  122. package/dist/src/daemon/chats-worker.d.ts +12 -0
  123. package/dist/src/daemon/chats-worker.js +200 -0
  124. package/dist/src/daemon/claude-turn.d.ts +10 -0
  125. package/dist/src/daemon/claude-turn.js +96 -15
  126. package/dist/src/daemon/claude-worker.d.ts +4 -1
  127. package/dist/src/daemon/claude-worker.js +99 -47
  128. package/dist/src/daemon/codex-stream.js +16 -0
  129. package/dist/src/daemon/daemon.d.ts +3 -0
  130. package/dist/src/daemon/daemon.js +16 -7
  131. package/dist/src/daemon/github-methods.js +4 -0
  132. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  133. package/dist/src/daemon/github-ticket-status.js +268 -0
  134. package/dist/src/daemon/issue-queue.d.ts +1 -0
  135. package/dist/src/daemon/issue-queue.js +115 -24
  136. package/dist/src/daemon/operation-journal.d.ts +1 -0
  137. package/dist/src/daemon/operation-journal.js +4 -2
  138. package/dist/src/daemon/retrying-turn.d.ts +9 -0
  139. package/dist/src/daemon/retrying-turn.js +185 -0
  140. package/dist/src/host/world-reconciliation.d.ts +2 -0
  141. package/dist/src/host/world-reconciliation.js +6 -0
  142. package/dist/src/host/world-runs.js +9 -1
  143. package/dist/src/service/run-handlers.js +15 -23
  144. package/dist/src/service/worker-totals.d.ts +3 -0
  145. package/dist/src/service/worker-totals.js +28 -0
  146. package/dist/src/ticket-status.d.ts +8 -0
  147. package/dist/src/ticket-status.js +45 -0
  148. package/dist/src/token-prices.d.ts +19 -0
  149. package/dist/src/token-prices.js +76 -0
  150. package/dist/src/worker-usage.d.ts +22 -0
  151. package/dist/src/worker-usage.js +44 -0
  152. package/dist/src/workflow-candidate.d.ts +29 -0
  153. package/dist/src/workflow-candidate.js +59 -0
  154. package/dist/src/workflow-contracts.d.ts +2 -0
  155. package/dist/src/workflow-contracts.js +27 -5
  156. package/dist/src/workflow-declaration.d.ts +1 -1
  157. package/dist/src/workflow-declaration.js +3 -1
  158. package/dist/src/workflows.d.ts +1 -0
  159. package/dist/src/workflows.js +1 -0
  160. package/dist/tools/self-host-processes.js +7 -2
  161. package/dist/tools/self-host-workflows.d.ts +1 -1
  162. package/dist/tools/self-host-workflows.js +2 -2
  163. package/dist/tools/self-host.js +6 -6
  164. package/dist/tools/self-update-host.d.ts +7 -0
  165. package/dist/tools/self-update-host.js +48 -1
  166. package/dist/tools/self-update.d.ts +1 -0
  167. package/dist/tools/self-update.js +85 -13
  168. package/dist/workflows/analyze-chats.d.ts +10 -0
  169. package/dist/workflows/analyze-chats.declaration.json +9 -0
  170. package/dist/workflows/analyze-chats.js +73 -0
  171. package/dist/workflows/develop-ticket.d.ts +1 -7
  172. package/dist/workflows/develop-ticket.js +42 -141
  173. package/dist/workflows/issue-labels.d.ts +3 -1
  174. package/dist/workflows/issue-labels.js +34 -0
  175. package/dist/workflows/ticket-prompts.d.ts +35 -0
  176. package/dist/workflows/ticket-prompts.js +133 -0
  177. package/package.json +5 -3
@@ -0,0 +1,185 @@
1
+ import { join } from "node:path";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { priceOf, sumUsage, usageOf } from "../worker-usage.js";
5
+ import { capacityRefusal, startTurn, } from "./claude-turn.js";
6
+ import { readRecord, writeRecord } from "./operation-journal.js";
7
+ const waits = [60_000, 180_000];
8
+ const continuation = "An earlier attempt stopped because the model was at capacity; continue from the current state of the work tree.\n\n";
9
+ function written(folder, entry) {
10
+ return writeRecord(folder, { ...entry, permissions: "private" });
11
+ }
12
+ function total(values) {
13
+ if (values.includes(null))
14
+ return null;
15
+ return priceOf(values.reduce((sum, value) => sum + (value ?? 0), 0));
16
+ }
17
+ function combined(ends) {
18
+ const last = ends.at(-1) ?? { exitCode: null, result: undefined };
19
+ if (ends.length < 2)
20
+ return last;
21
+ const result = last.result ?? ends.find((end) => end.result)?.result;
22
+ if (result === undefined)
23
+ return last;
24
+ return {
25
+ ...last,
26
+ result: {
27
+ ...result,
28
+ ...(last.result === undefined ? { failed: true, text: "" } : {}),
29
+ usage: sumUsage(ends.map((end) => end.result?.usage ?? usageOf(null))),
30
+ costUsd: total(ends.map((end) => end.result?.costUsd ?? null)),
31
+ turns: total(ends.map((end) => end.result?.turns ?? null)),
32
+ },
33
+ };
34
+ }
35
+ function controlled(attempts, start) {
36
+ const turn = startTurn(start);
37
+ const stop = () => {
38
+ turn.stop(attempts.grace.milliseconds);
39
+ };
40
+ attempts.stopping.signal.addEventListener("abort", stop, { once: true });
41
+ return {
42
+ ...turn,
43
+ ended: turn.ended.finally(() => {
44
+ attempts.stopping.signal.removeEventListener("abort", stop);
45
+ }),
46
+ };
47
+ }
48
+ async function launched(attempts) {
49
+ const { start, options, ends } = attempts;
50
+ const attempt = ends.length + 1;
51
+ const folder = start.mode === "analysis" || attempt === 1
52
+ ? start.folder
53
+ : join(start.folder, `attempt-${String(attempt)}`);
54
+ const entry = {
55
+ ...options.metadata,
56
+ attempt,
57
+ startedAt: attempt === 1 ? attempts.startedAt : new Date().toISOString(),
58
+ };
59
+ await written(start.folder, {
60
+ name: "started.json",
61
+ value: {
62
+ ...entry,
63
+ startedAt: attempts.startedAt,
64
+ phase: "starting",
65
+ processId: null,
66
+ },
67
+ });
68
+ await mkdir(folder, { recursive: true, mode: 0o700 });
69
+ if (attempts.stopping.signal.aborted)
70
+ return undefined;
71
+ const turn = controlled(attempts, {
72
+ ...start,
73
+ folder,
74
+ ...(attempt === 1 ? {} : { activityFolder: start.folder }),
75
+ prompt: attempt === 1 ? start.prompt : continuation + start.prompt,
76
+ });
77
+ try {
78
+ const value = { ...entry, phase: "running", processId: turn.processId };
79
+ await written(start.folder, {
80
+ name: "started.json",
81
+ value: { ...value, startedAt: attempts.startedAt },
82
+ });
83
+ await written(start.folder, {
84
+ name: `attempt-${String(attempt)}.json`,
85
+ value,
86
+ });
87
+ return turn;
88
+ }
89
+ catch (error) {
90
+ turn.stop(0);
91
+ await turn.ended;
92
+ throw error;
93
+ }
94
+ }
95
+ async function recordedEnd(attempts, completion) {
96
+ const { start, ends, options } = attempts;
97
+ const { processId, end } = completion;
98
+ const attempt = ends.length;
99
+ const name = `attempt-${String(attempt)}.json`;
100
+ const started = await readRecord(start.folder, name);
101
+ const startedAt = Reflect.get(Object(started), "startedAt");
102
+ if (typeof startedAt !== "string")
103
+ throw new Error("attempt-start-time-missing");
104
+ await written(start.folder, {
105
+ name,
106
+ value: {
107
+ ...options.metadata,
108
+ attempt,
109
+ processId,
110
+ startedAt,
111
+ endedAt: new Date().toISOString(),
112
+ exitCode: end.exitCode,
113
+ capacity: capacityRefusal(end.result),
114
+ usage: end.result?.usage ?? usageOf(null),
115
+ costUsd: end.result?.costUsd ?? null,
116
+ },
117
+ });
118
+ }
119
+ async function waited(attempts, milliseconds) {
120
+ const { start, options, stopping, ends } = attempts;
121
+ await written(start.folder, {
122
+ name: "started.json",
123
+ value: {
124
+ ...options.metadata,
125
+ startedAt: attempts.startedAt,
126
+ attempt: ends.length,
127
+ phase: "waiting",
128
+ processId: null,
129
+ waitMilliseconds: milliseconds,
130
+ waitingAt: new Date().toISOString(),
131
+ },
132
+ });
133
+ const wait = options.retryDelay ??
134
+ ((duration, signal) => delay(duration, undefined, { signal }));
135
+ await wait(milliseconds, stopping.signal).catch((error) => {
136
+ if (!stopping.signal.aborted)
137
+ throw error;
138
+ });
139
+ }
140
+ async function finished(attempts, first) {
141
+ const { ends } = attempts;
142
+ let turn = first;
143
+ while (turn !== undefined) {
144
+ const end = await turn.ended;
145
+ ends.push(end);
146
+ await recordedEnd(attempts, { processId: turn.processId, end });
147
+ const milliseconds = waits[ends.length - 1];
148
+ if (cancelled(attempts) ||
149
+ !capacityRefusal(end.result) ||
150
+ milliseconds === undefined)
151
+ break;
152
+ await waited(attempts, milliseconds);
153
+ if (cancelled(attempts))
154
+ return { ...combined(ends), stopped: true };
155
+ turn = await launched(attempts);
156
+ }
157
+ return {
158
+ ...combined(ends),
159
+ ...(cancelled(attempts) || turn === undefined ? { stopped: true } : {}),
160
+ };
161
+ }
162
+ function cancelled(attempts) {
163
+ return attempts.stopping.signal.aborted;
164
+ }
165
+ export async function startRetriedTurn(start, options) {
166
+ const attempts = {
167
+ start,
168
+ startedAt: new Date().toISOString(),
169
+ options,
170
+ stopping: new AbortController(),
171
+ ends: [],
172
+ grace: { milliseconds: 2_000 },
173
+ };
174
+ const turn = await launched(attempts);
175
+ if (turn === undefined)
176
+ throw new Error("turn-start-stopped");
177
+ return {
178
+ processId: turn.processId,
179
+ ended: finished(attempts, turn),
180
+ stop: (graceMilliseconds) => {
181
+ attempts.grace.milliseconds = graceMilliseconds;
182
+ attempts.stopping.abort();
183
+ },
184
+ };
185
+ }
@@ -4,6 +4,8 @@ export type ReconciledState = Pick<AuthorityState, "runs" | "intents" | "details
4
4
  export interface WorkflowOutcome {
5
5
  readonly kind: string;
6
6
  readonly reason?: string;
7
+ readonly issueUpdate?: "failed";
8
+ readonly ticketStatus?: "complete" | "partial";
7
9
  }
8
10
  export interface ReconciliationPorts {
9
11
  readonly state: () => Promise<ReconciledState>;
@@ -14,6 +14,12 @@ async function withOutcome(ports, observed) {
14
14
  : {
15
15
  ...world,
16
16
  outcome: read.outcome.kind,
17
+ ...(read.outcome.issueUpdate === "failed"
18
+ ? { issueUpdate: read.outcome.issueUpdate }
19
+ : {}),
20
+ ...(read.outcome.ticketStatus === undefined
21
+ ? {}
22
+ : { ticketStatus: read.outcome.ticketStatus }),
17
23
  ...(read.outcome.reason === undefined
18
24
  ? {}
19
25
  : { outcomeReason: read.outcome.reason }),
@@ -1,3 +1,4 @@
1
+ import { isTicketStatus } from "../ticket-status.js";
1
2
  import { getRun } from "workflow/api";
2
3
  import { getWorld } from "workflow/runtime";
3
4
  export const runTag = "cloving.run";
@@ -44,7 +45,14 @@ export async function workflowOutcome(receipt) {
44
45
  if (typeof kind !== "string")
45
46
  return undefined;
46
47
  const reason = Reflect.get(Object(value), "reason");
47
- return { kind, ...(typeof reason === "string" ? { reason } : {}) };
48
+ const issueUpdate = Reflect.get(Object(value), "issueUpdate");
49
+ const ticketStatus = Reflect.get(Object(value), "ticketStatus");
50
+ return {
51
+ kind,
52
+ ...(typeof reason === "string" ? { reason } : {}),
53
+ ...(issueUpdate === "failed" ? { issueUpdate } : {}),
54
+ ...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
55
+ };
48
56
  }
49
57
  export async function cancelled(receipt) {
50
58
  const run = getRun(receipt);
@@ -1,6 +1,7 @@
1
1
  import { runView, unresolvedEffects, } from "../authority/status.js";
2
2
  import { record } from "../protocol.js";
3
3
  import { committed, failed, invalidRequest, isIdentifier, publicReceipt, rejection, result, wholeNumber, } from "./replies.js";
4
+ import { usageTotals } from "./worker-totals.js";
4
5
  import { feedbackOf } from "./decision-handler.js";
5
6
  async function snapshot(context) {
6
7
  const { authority } = context;
@@ -95,26 +96,6 @@ function summary(view) {
95
96
  pendingDecisions: view.pendingDecisions,
96
97
  };
97
98
  }
98
- function costOf(text) {
99
- try {
100
- const cost = record(JSON.parse(text ?? "null"))?.["costUsd"];
101
- return typeof cost === "number" ? cost : 0;
102
- }
103
- catch {
104
- return 0;
105
- }
106
- }
107
- async function claudeCost(context, runId) {
108
- const { effects, queries } = context.authority;
109
- const digests = (await effects.operations(runId))
110
- .filter((operation) => operation.capability === "claude")
111
- .map((operation) => operation.result)
112
- .filter((digest) => digest !== null);
113
- let total = 0;
114
- for (const digest of digests)
115
- total += costOf(await queries.document(digest));
116
- return Math.round(total * 100) / 100;
117
- }
118
99
  function full(view) {
119
100
  return {
120
101
  ...summary(view),
@@ -123,6 +104,9 @@ function full(view) {
123
104
  lastConfirmedObservation: view.lastConfirmedObservation,
124
105
  recovery: view.recovery,
125
106
  claudeCostUsd: view.claudeCostUsd,
107
+ estimatedCostUsd: view.estimatedCostUsd,
108
+ usage: view.usage,
109
+ workerTurns: view.workerTurns,
126
110
  budget: view.budget,
127
111
  effects: view.effects,
128
112
  nextAction: view.nextAction,
@@ -141,7 +125,15 @@ async function outcomeOf(context, view) {
141
125
  const kind = event?.payload["outcome"];
142
126
  if (kind === undefined)
143
127
  return null;
144
- return { kind, reason: event?.payload["outcomeReason"] ?? null };
128
+ const ticketStatus = event?.payload["ticketStatus"];
129
+ return {
130
+ kind,
131
+ reason: event?.payload["outcomeReason"] ?? null,
132
+ ...(ticketStatus === undefined ? {} : { ticketStatus }),
133
+ ...(event?.payload["issueUpdate"] === "failed"
134
+ ? { issueUpdate: "failed" }
135
+ : {}),
136
+ };
145
137
  }
146
138
  function titleOf(input, workflowId) {
147
139
  const ticket = input?.["ticket"];
@@ -200,10 +192,10 @@ export async function status(context, parameters) {
200
192
  const view = await viewOf(context, { snapshot: current, runId: named });
201
193
  if (view === undefined)
202
194
  return failed("unknown-run", { detail: `No run has the ID ${named}.` });
203
- const claudeCostUsd = await claudeCost(context, view.runId);
195
+ const totals = await usageTotals(context, view.runId);
204
196
  return result("run-status", {
205
197
  run: {
206
- ...full({ ...view, claudeCostUsd }),
198
+ ...full({ ...view, ...totals }),
207
199
  outcome: await outcomeOf(context, view),
208
200
  ...(await metadataOf(context, {
209
201
  workflowId: view.workflowId,
@@ -0,0 +1,3 @@
1
+ import { type WorkerTotals } from "../worker-usage.ts";
2
+ import type { ServiceContext } from "./replies.ts";
3
+ export declare function usageTotals(context: ServiceContext, runId: string): Promise<WorkerTotals>;
@@ -0,0 +1,28 @@
1
+ import { record } from "../protocol.js";
2
+ import { priceOf, usageOf, workerTotals, } from "../worker-usage.js";
3
+ function reportFields(text) {
4
+ try {
5
+ return record(JSON.parse(text ?? "null")) ?? {};
6
+ }
7
+ catch {
8
+ return {};
9
+ }
10
+ }
11
+ export async function usageTotals(context, runId) {
12
+ const { effects, queries } = context.authority;
13
+ const operations = (await effects.operations(runId)).filter((operation) => operation.capability === "claude" &&
14
+ ["completed", "failed", "stopped", "cancelled", "refused"].includes(operation.status));
15
+ const turns = [];
16
+ for (const operation of operations) {
17
+ const report = reportFields(operation.result === null
18
+ ? undefined
19
+ : await queries.document(operation.result));
20
+ turns.push({
21
+ operationId: operation.operationId,
22
+ usage: usageOf(report["usage"]),
23
+ costUsd: priceOf(report["costUsd"]),
24
+ estimatedCostUsd: priceOf(report["estimatedCostUsd"]),
25
+ });
26
+ }
27
+ return workerTotals(turns);
28
+ }
@@ -0,0 +1,8 @@
1
+ export type TicketStatus = {
2
+ readonly kind: "complete";
3
+ } | {
4
+ readonly kind: "partial";
5
+ readonly remaining: readonly string[];
6
+ };
7
+ export declare function ticketStatus(summary: string, preserved?: unknown): TicketStatus;
8
+ export declare function isTicketStatus(value: unknown): value is "complete" | "partial";
@@ -0,0 +1,45 @@
1
+ function parsedStatus(summary) {
2
+ const lines = summary.trimEnd().split(/\r?\n/);
3
+ if (lines.filter((line) => /^Ticket status:/i.test(line)).length !== 1)
4
+ return undefined;
5
+ const status = lines.pop();
6
+ if (status === "Ticket status: complete")
7
+ return { kind: "complete" };
8
+ if (status !== "Ticket status: partial")
9
+ return undefined;
10
+ const heading = lines.lastIndexOf("Remaining:");
11
+ if (heading === -1)
12
+ return undefined;
13
+ const items = lines.slice(heading + 1);
14
+ if (items.length === 0 || items.some((line) => !/^- \S.*$/.test(line)))
15
+ return undefined;
16
+ return { kind: "partial", remaining: items.map((line) => line.slice(2)) };
17
+ }
18
+ function preservedStatus(value) {
19
+ if (typeof value !== "object" || value === null)
20
+ return undefined;
21
+ const kind = Reflect.get(value, "kind");
22
+ if (kind === "complete")
23
+ return { kind };
24
+ const remaining = Reflect.get(value, "remaining");
25
+ if (kind !== "partial" ||
26
+ !Array.isArray(remaining) ||
27
+ remaining.length === 0 ||
28
+ remaining.some((item) => typeof item !== "string" || item.trim() === "" || /[\r\n]/.test(item)))
29
+ return undefined;
30
+ return { kind, remaining: remaining.map(String) };
31
+ }
32
+ export function ticketStatus(summary, preserved) {
33
+ const parsed = preserved === undefined
34
+ ? parsedStatus(summary)
35
+ : preservedStatus(preserved);
36
+ if (parsed !== undefined)
37
+ return parsed;
38
+ return {
39
+ kind: "partial",
40
+ remaining: ["The worker report has no ticket status line."],
41
+ };
42
+ }
43
+ export function isTicketStatus(value) {
44
+ return value === "complete" || value === "partial";
45
+ }
@@ -0,0 +1,19 @@
1
+ import { type WorkerUsage } from "./worker-usage.ts";
2
+ export interface TokenPrice {
3
+ readonly input: number;
4
+ readonly cachedInput: number;
5
+ readonly output: number;
6
+ }
7
+ export type TokenPrices = Readonly<Record<string, TokenPrice>>;
8
+ export declare const builtInPriceTable: {
9
+ readonly date: string;
10
+ readonly source: string;
11
+ readonly prices: TokenPrices;
12
+ };
13
+ export declare function parseTokenPrices(value: string | undefined): TokenPrices;
14
+ export declare function priceForModel(prices: TokenPrices, model: string | null): TokenPrice | undefined;
15
+ export declare function commandModel(command: readonly string[]): string | null;
16
+ export declare function estimatedCostUsd(usage: WorkerUsage, pricing: {
17
+ readonly worker: "claude" | "codex";
18
+ readonly price: TokenPrice | undefined;
19
+ }): number | null;
@@ -0,0 +1,76 @@
1
+ import { priceOf, usageOf } from "./worker-usage.js";
2
+ import { record } from "./protocol.js";
3
+ export const builtInPriceTable = {
4
+ date: "2026-10-18",
5
+ source: "https://openai.com/api/pricing/",
6
+ prices: {
7
+ "gpt-6.1-sol": { input: 2, cachedInput: 0.1, output: 10 },
8
+ "gpt-6-astra": { input: 10, cachedInput: 1, output: 50 },
9
+ },
10
+ };
11
+ function tokenPrice(value) {
12
+ const fields = record(value);
13
+ const input = priceOf(fields?.["input"]);
14
+ const cachedInput = priceOf(fields?.["cachedInput"]);
15
+ const output = priceOf(fields?.["output"]);
16
+ if (input === null || cachedInput === null || output === null)
17
+ throw new Error("Invalid CLOVING_TOKEN_PRICES");
18
+ return { input, cachedInput, output };
19
+ }
20
+ export function parseTokenPrices(value) {
21
+ if (value === undefined)
22
+ return { ...builtInPriceTable.prices };
23
+ try {
24
+ const fields = record(JSON.parse(value));
25
+ if (fields === undefined)
26
+ throw new Error("Invalid CLOVING_TOKEN_PRICES");
27
+ return {
28
+ ...builtInPriceTable.prices,
29
+ ...Object.fromEntries(Object.entries(fields).map(([model, price]) => [
30
+ model,
31
+ tokenPrice(price),
32
+ ])),
33
+ };
34
+ }
35
+ catch {
36
+ throw new Error("Invalid CLOVING_TOKEN_PRICES");
37
+ }
38
+ }
39
+ export function priceForModel(prices, model) {
40
+ return model !== null && Object.hasOwn(prices, model)
41
+ ? prices[model]
42
+ : undefined;
43
+ }
44
+ export function commandModel(command) {
45
+ let model = null;
46
+ for (let index = 1; index < command.length; index += 1) {
47
+ const argument = command[index] ?? "";
48
+ if (argument === "--")
49
+ break;
50
+ if (argument.startsWith("--model="))
51
+ model = argument.slice(8) || null;
52
+ if (argument === "--model" || argument === "-m") {
53
+ const next = command[index + 1];
54
+ model = next && !next.startsWith("-") ? next : null;
55
+ index += 1;
56
+ }
57
+ }
58
+ return model;
59
+ }
60
+ export function estimatedCostUsd(usage, pricing) {
61
+ const { worker, price } = pricing;
62
+ const counts = usageOf(usage);
63
+ const cached = counts.cache_read_input_tokens;
64
+ const output = counts.output_tokens;
65
+ if (price === undefined ||
66
+ counts.input_tokens === null ||
67
+ cached === null ||
68
+ output === null)
69
+ return null;
70
+ const input = counts.input_tokens - (worker === "codex" ? cached : 0);
71
+ if (input < 0)
72
+ return null;
73
+ return priceOf((input / 1_000_000) * price.input +
74
+ (cached / 1_000_000) * price.cachedInput +
75
+ (output / 1_000_000) * price.output);
76
+ }
@@ -0,0 +1,22 @@
1
+ export interface WorkerUsage {
2
+ readonly input_tokens: number | null;
3
+ readonly cache_read_input_tokens: number | null;
4
+ readonly output_tokens: number | null;
5
+ }
6
+ export interface WorkerTurnUsage {
7
+ readonly estimatedCostUsd: number | null;
8
+ readonly operationId: string;
9
+ readonly usage: WorkerUsage;
10
+ readonly costUsd: number | null;
11
+ }
12
+ export interface WorkerTotals {
13
+ readonly estimatedCostUsd: number | null;
14
+ readonly workerTurns: readonly WorkerTurnUsage[];
15
+ readonly usage: WorkerUsage;
16
+ readonly claudeCostUsd: number | null;
17
+ }
18
+ export declare function priceOf(value: unknown): number | null;
19
+ export declare function tokenCount(value: unknown): number | null;
20
+ export declare function usageOf(value: unknown): WorkerUsage;
21
+ export declare function sumUsage(values: readonly WorkerUsage[]): WorkerUsage;
22
+ export declare function workerTotals(workerTurns: readonly WorkerTurnUsage[]): WorkerTotals;
@@ -0,0 +1,44 @@
1
+ export function priceOf(value) {
2
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
3
+ ? value
4
+ : null;
5
+ }
6
+ export function tokenCount(value) {
7
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
8
+ ? value
9
+ : null;
10
+ }
11
+ export function usageOf(value) {
12
+ const fields = typeof value === "object" && value !== null ? value : {};
13
+ return {
14
+ input_tokens: tokenCount(Reflect.get(fields, "input_tokens")),
15
+ cache_read_input_tokens: tokenCount(Reflect.get(fields, "cache_read_input_tokens")),
16
+ output_tokens: tokenCount(Reflect.get(fields, "output_tokens")),
17
+ };
18
+ }
19
+ function total(values) {
20
+ if (values.length === 0 || values.includes(null))
21
+ return null;
22
+ return tokenCount(values.reduce((sum, value) => sum + (value ?? 0), 0));
23
+ }
24
+ export function sumUsage(values) {
25
+ return {
26
+ input_tokens: total(values.map((value) => value.input_tokens)),
27
+ cache_read_input_tokens: total(values.map((value) => value.cache_read_input_tokens)),
28
+ output_tokens: total(values.map((value) => value.output_tokens)),
29
+ };
30
+ }
31
+ function sumPrices(values) {
32
+ const prices = values.map(priceOf);
33
+ if (prices.length === 0 || prices.includes(null))
34
+ return null;
35
+ return priceOf(prices.reduce((sum, price) => sum + (price ?? 0), 0));
36
+ }
37
+ export function workerTotals(workerTurns) {
38
+ return {
39
+ workerTurns,
40
+ usage: sumUsage(workerTurns.map((turn) => turn.usage)),
41
+ claudeCostUsd: sumPrices(workerTurns.map((turn) => turn.costUsd)),
42
+ estimatedCostUsd: sumPrices(workerTurns.map((turn) => turn.estimatedCostUsd)),
43
+ };
44
+ }
@@ -0,0 +1,29 @@
1
+ import { type WorkflowDeclaration } from "./workflow-declaration.ts";
2
+ export interface CandidateDeclaration {
3
+ readonly workflow: WorkflowDeclaration;
4
+ readonly methods: Readonly<Record<string, readonly string[]>>;
5
+ }
6
+ export interface CandidateImport {
7
+ readonly module: string;
8
+ readonly names: readonly string[];
9
+ }
10
+ export interface CandidateEffect {
11
+ readonly capability: string | null;
12
+ readonly method: string | null;
13
+ }
14
+ export interface CandidateAnalysis {
15
+ readonly imports: readonly CandidateImport[];
16
+ readonly effects: readonly CandidateEffect[];
17
+ readonly directEffects: readonly string[];
18
+ readonly unsupportedForms: readonly string[];
19
+ readonly knownCallCount: number;
20
+ readonly knownUnitTotal: number;
21
+ }
22
+ export type InstalledWorkflowExports = Readonly<Record<string, readonly string[]>>;
23
+ export type WorkflowCandidateCode = "invalid-declaration" | "forbidden-import" | "missing-export" | "unsupported-source" | "dynamic-effect" | "undeclared-effect" | "effect-limit" | "direct-effect";
24
+ export interface WorkflowCandidateInput {
25
+ readonly declaration: unknown;
26
+ readonly analysis: CandidateAnalysis;
27
+ readonly installedExports: InstalledWorkflowExports;
28
+ }
29
+ export declare function checkWorkflowCandidate(input: WorkflowCandidateInput): readonly WorkflowCandidateCode[];
@@ -0,0 +1,59 @@
1
+ import { record } from "./protocol.js";
2
+ import { isWorkflowDeclaration, } from "./workflow-declaration.js";
3
+ const workflowModules = [
4
+ "cloving/workflows",
5
+ "cloving/workflows/steps/decision-gate",
6
+ "cloving/workflows/steps/load-cycle",
7
+ "cloving/workflows/steps/protected-effect",
8
+ ];
9
+ function isMethodList(value) {
10
+ return (Array.isArray(value) &&
11
+ value.length > 0 &&
12
+ Array.from(value).every((method) => typeof method === "string" && method.trim().length > 0) &&
13
+ new Set(value).size === value.length);
14
+ }
15
+ function isCandidateDeclaration(value) {
16
+ const fields = record(value);
17
+ if (!fields ||
18
+ Object.keys(fields).some((key) => key !== "workflow" && key !== "methods"))
19
+ return false;
20
+ const workflow = fields["workflow"];
21
+ const methods = record(fields["methods"]);
22
+ if (!isWorkflowDeclaration(workflow) || !methods)
23
+ return false;
24
+ const capabilities = workflow.capabilities;
25
+ return (Object.keys(methods).every((key) => capabilities.includes(key)) &&
26
+ capabilities.every((capability) => isMethodList(methods[capability])));
27
+ }
28
+ export function checkWorkflowCandidate(input) {
29
+ if (!isCandidateDeclaration(input.declaration))
30
+ return ["invalid-declaration"];
31
+ const { analysis, declaration, installedExports } = input;
32
+ const codes = [];
33
+ if (analysis.imports.some((entry) => !workflowModules.includes(entry.module)))
34
+ codes.push("forbidden-import");
35
+ if (analysis.imports.some((entry) => {
36
+ if (!workflowModules.includes(entry.module))
37
+ return false;
38
+ const names = installedExports[entry.module];
39
+ return (names === undefined || entry.names.some((name) => !names.includes(name)));
40
+ }))
41
+ codes.push("missing-export");
42
+ if (analysis.effects.some((effect) => effect.capability !== null &&
43
+ effect.method !== null &&
44
+ (!Object.hasOwn(declaration.methods, effect.capability) ||
45
+ !declaration.methods[effect.capability]?.includes(effect.method))))
46
+ codes.push("undeclared-effect");
47
+ if (analysis.effects.some((effect) => effect.capability === null || effect.method === null))
48
+ codes.push("dynamic-effect");
49
+ const counts = [analysis.knownCallCount, analysis.knownUnitTotal];
50
+ if (counts.some((count) => !Number.isSafeInteger(count) || count < 0))
51
+ codes.push("unsupported-source");
52
+ else if (counts.some((count) => count > declaration.workflow.effectLimit))
53
+ codes.push("effect-limit");
54
+ if (analysis.directEffects.length > 0)
55
+ codes.push("direct-effect");
56
+ if (analysis.unsupportedForms.length > 0)
57
+ codes.push("unsupported-source");
58
+ return [...new Set(codes)].sort();
59
+ }
@@ -14,6 +14,8 @@ export type CycleOutcome = {
14
14
  readonly kind: "handoff_requested";
15
15
  readonly cycleId: string;
16
16
  readonly promotionId: string;
17
+ readonly ticketStatus?: "complete" | "partial";
18
+ readonly issueUpdate?: "failed";
17
19
  };
18
20
  export type Validation<Value> = {
19
21
  readonly kind: "valid";