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.
- package/README.md +145 -14
- package/dist/src/authority/commands.js +10 -2
- package/dist/src/authority/reconciliation.d.ts +2 -0
- package/dist/src/authority/reconciliation.js +10 -1
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +2 -0
- package/dist/src/broker/actor/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- package/dist/src/chats/analysis-batches.d.ts +24 -0
- package/dist/src/chats/analysis-batches.js +83 -0
- package/dist/src/chats/analysis-contract.d.ts +80 -0
- package/dist/src/chats/analysis-contract.js +16 -0
- package/dist/src/chats/analysis-data.d.ts +3 -0
- package/dist/src/chats/analysis-data.js +85 -0
- package/dist/src/chats/analysis-input.d.ts +12 -0
- package/dist/src/chats/analysis-input.js +83 -0
- package/dist/src/chats/analysis-links.d.ts +2 -0
- package/dist/src/chats/analysis-links.js +25 -0
- package/dist/src/chats/analysis-prompt.d.ts +10 -0
- package/dist/src/chats/analysis-prompt.js +16 -0
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +32 -0
- package/dist/src/chats/analysis-provenance.d.ts +4 -0
- package/dist/src/chats/analysis-provenance.js +99 -0
- package/dist/src/chats/analysis-shape.d.ts +3 -0
- package/dist/src/chats/analysis-shape.js +228 -0
- package/dist/src/chats/analysis.d.ts +3 -0
- package/dist/src/chats/analysis.js +42 -0
- package/dist/src/chats/corpus-input.d.ts +5 -0
- package/dist/src/chats/corpus-input.js +288 -0
- package/dist/src/chats/destination.js +3 -1
- package/dist/src/chats/import.d.ts +8 -6
- package/dist/src/chats/import.js +195 -23
- package/dist/src/chats/input-file.d.ts +3 -0
- package/dist/src/chats/input-file.js +32 -0
- package/dist/src/chats/normalize.d.ts +11 -2
- package/dist/src/chats/normalize.js +18 -2
- package/dist/src/chats/previous-import.d.ts +5 -0
- package/dist/src/chats/previous-import.js +34 -0
- package/dist/src/chats/reader.d.ts +7 -1
- package/dist/src/chats/reader.js +46 -15
- package/dist/src/chats/redact-import.d.ts +14 -0
- package/dist/src/chats/redact-import.js +141 -0
- package/dist/src/chats/redaction.d.ts +41 -0
- package/dist/src/chats/redaction.js +161 -0
- package/dist/src/chats/revision-input.d.ts +3 -0
- package/dist/src/chats/revision-input.js +180 -0
- package/dist/src/chats/revision.d.ts +48 -0
- package/dist/src/chats/revision.js +94 -0
- package/dist/src/chats/selection.d.ts +25 -0
- package/dist/src/chats/selection.js +109 -0
- package/dist/src/chats/source.d.ts +23 -0
- package/dist/src/chats/source.js +135 -0
- package/dist/src/chats/structure.d.ts +57 -0
- package/dist/src/chats/structure.js +216 -0
- package/dist/src/cli/arguments.d.ts +8 -0
- package/dist/src/cli/arguments.js +94 -13
- package/dist/src/cli/client.d.ts +1 -0
- package/dist/src/cli/client.js +38 -1
- package/dist/src/cli/commands.d.ts +3 -0
- package/dist/src/cli/commands.js +29 -11
- package/dist/src/cli/main.d.ts +2 -1
- package/dist/src/cli/main.js +61 -14
- package/dist/src/cli/render.d.ts +2 -0
- package/dist/src/cli/render.js +70 -2
- package/dist/src/cli/usage-render.d.ts +3 -0
- package/dist/src/cli/usage-render.js +28 -0
- package/dist/src/cli/usage.d.ts +3 -2
- package/dist/src/cli/usage.js +38 -6
- package/dist/src/cli/watch-actions.d.ts +10 -0
- package/dist/src/cli/watch-actions.js +58 -0
- package/dist/src/cli/watch-board-view.d.ts +32 -0
- package/dist/src/cli/watch-board-view.js +88 -15
- package/dist/src/cli/watch-board.js +6 -1
- package/dist/src/cli/watch-card-client.d.ts +1 -0
- package/dist/src/cli/watch-card-client.js +44 -0
- package/dist/src/cli/watch-card-forms.d.ts +2 -0
- package/dist/src/cli/watch-card-forms.js +33 -0
- package/dist/src/cli/watch-card-reply.d.ts +5 -0
- package/dist/src/cli/watch-card-reply.js +34 -0
- package/dist/src/cli/watch-card-view.d.ts +40 -0
- package/dist/src/cli/watch-card-view.js +86 -0
- package/dist/src/cli/watch-client.d.ts +1 -0
- package/dist/src/cli/watch-client.js +118 -0
- package/dist/src/cli/watch-model.js +31 -14
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +71 -85
- package/dist/src/cli/watch-pr-board.d.ts +3 -0
- package/dist/src/cli/watch-pr-board.js +26 -0
- package/dist/src/cli/watch-pr-command.d.ts +15 -0
- package/dist/src/cli/watch-pr-command.js +37 -0
- package/dist/src/cli/watch-pr-identity.d.ts +20 -0
- package/dist/src/cli/watch-pr-identity.js +62 -0
- package/dist/src/cli/watch-pr-observer.d.ts +19 -0
- package/dist/src/cli/watch-pr-observer.js +108 -0
- package/dist/src/cli/watch-pr-settings.d.ts +8 -0
- package/dist/src/cli/watch-pr-settings.js +47 -0
- package/dist/src/cli/watch-reads.js +4 -0
- package/dist/src/cli/watch-render.d.ts +16 -2
- package/dist/src/cli/watch-render.js +10 -3
- package/dist/src/cli/watch-run-input.d.ts +14 -0
- package/dist/src/cli/watch-run-input.js +49 -0
- package/dist/src/cli/watch-server.d.ts +3 -0
- package/dist/src/cli/watch-server.js +44 -6
- package/dist/src/cli/watch-start-view.d.ts +18 -0
- package/dist/src/cli/watch-start-view.js +77 -0
- package/dist/src/cli/watch-turn.d.ts +1 -0
- package/dist/src/cli/watch-turn.js +7 -2
- package/dist/src/cli/watch-usage.d.ts +6 -0
- package/dist/src/cli/watch-usage.js +65 -0
- package/dist/src/cli/watch-view.d.ts +15 -0
- package/dist/src/cli/watch-view.js +61 -17
- package/dist/src/cli.js +48 -6
- package/dist/src/daemon/analysis-launch.d.ts +5 -0
- package/dist/src/daemon/analysis-launch.js +81 -0
- package/dist/src/daemon/analysis-token.d.ts +6 -0
- package/dist/src/daemon/analysis-token.js +88 -0
- package/dist/src/daemon/analysis-worker.d.ts +22 -0
- package/dist/src/daemon/analysis-worker.js +219 -0
- package/dist/src/daemon/chats-files.d.ts +15 -0
- package/dist/src/daemon/chats-files.js +113 -0
- package/dist/src/daemon/chats-worker.d.ts +12 -0
- package/dist/src/daemon/chats-worker.js +200 -0
- package/dist/src/daemon/claude-turn.d.ts +10 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +4 -1
- package/dist/src/daemon/claude-worker.js +99 -47
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +3 -0
- package/dist/src/daemon/daemon.js +16 -7
- package/dist/src/daemon/github-methods.js +4 -0
- package/dist/src/daemon/github-ticket-status.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +268 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +115 -24
- package/dist/src/daemon/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +9 -0
- package/dist/src/daemon/retrying-turn.js +185 -0
- package/dist/src/host/world-reconciliation.d.ts +2 -0
- package/dist/src/host/world-reconciliation.js +6 -0
- package/dist/src/host/world-runs.js +9 -1
- package/dist/src/service/run-handlers.js +15 -23
- package/dist/src/service/worker-totals.d.ts +3 -0
- package/dist/src/service/worker-totals.js +28 -0
- package/dist/src/ticket-status.d.ts +8 -0
- package/dist/src/ticket-status.js +45 -0
- package/dist/src/token-prices.d.ts +19 -0
- package/dist/src/token-prices.js +76 -0
- package/dist/src/worker-usage.d.ts +22 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +59 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +27 -5
- package/dist/src/workflow-declaration.d.ts +1 -1
- package/dist/src/workflow-declaration.js +3 -1
- package/dist/src/workflows.d.ts +1 -0
- package/dist/src/workflows.js +1 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-workflows.d.ts +1 -1
- package/dist/tools/self-host-workflows.js +2 -2
- package/dist/tools/self-host.js +6 -6
- package/dist/tools/self-update-host.d.ts +7 -0
- package/dist/tools/self-update-host.js +48 -1
- package/dist/tools/self-update.d.ts +1 -0
- package/dist/tools/self-update.js +85 -13
- package/dist/workflows/analyze-chats.d.ts +10 -0
- package/dist/workflows/analyze-chats.declaration.json +9 -0
- package/dist/workflows/analyze-chats.js +73 -0
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +42 -141
- package/dist/workflows/issue-labels.d.ts +3 -1
- package/dist/workflows/issue-labels.js +34 -0
- package/dist/workflows/ticket-prompts.d.ts +35 -0
- package/dist/workflows/ticket-prompts.js +133 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
195
|
+
const totals = await usageTotals(context, view.runId);
|
|
204
196
|
return result("run-status", {
|
|
205
197
|
run: {
|
|
206
|
-
...full({ ...view,
|
|
198
|
+
...full({ ...view, ...totals }),
|
|
207
199
|
outcome: await outcomeOf(context, view),
|
|
208
200
|
...(await metadataOf(context, {
|
|
209
201
|
workflowId: view.workflowId,
|
|
@@ -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";
|