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.
- package/README.md +121 -21
- package/assets/watch-fonts/fira-code-OFL.md +93 -0
- package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-OFL.md +93 -0
- package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
- package/assets/watch-fonts/manifest.json +112 -0
- package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
- package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
- package/dist/src/authority/commands.d.ts +1 -0
- package/dist/src/authority/commands.js +25 -4
- package/dist/src/authority/dispatch-decision.d.ts +1 -0
- package/dist/src/authority/dispatch-decision.js +7 -4
- package/dist/src/authority/events.js +2 -2
- package/dist/src/authority/handoff-chain.d.ts +24 -0
- package/dist/src/authority/handoff-chain.js +46 -0
- package/dist/src/authority/reconciliation.d.ts +1 -0
- package/dist/src/authority/reconciliation.js +18 -11
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +3 -0
- package/dist/src/broker/actor/authority-queries.d.ts +4 -0
- package/dist/src/broker/actor/authority-queries.js +51 -2
- package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
- package/dist/src/broker/actor/effect-delivery.js +9 -0
- package/dist/src/broker/authority-admission.js +13 -5
- package/dist/src/chats/analysis-batches.d.ts +18 -0
- package/dist/src/chats/analysis-batches.js +15 -1
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -0
- package/dist/src/chats/analysis-input.d.ts +7 -1
- package/dist/src/chats/analysis-input.js +20 -6
- package/dist/src/chats/analysis-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +8 -0
- package/dist/src/chats/analysis-prompt.js +27 -1
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +9 -0
- package/dist/src/chats/analysis-shape.d.ts +1 -0
- package/dist/src/chats/analysis-shape.js +11 -4
- package/dist/src/chats/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -0
- package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
- package/dist/src/chats/workflow-generation-projection.js +75 -0
- package/dist/src/chats/workflow-generation-source.d.ts +25 -0
- package/dist/src/chats/workflow-generation-source.js +124 -0
- package/dist/src/chats/workflow-generation-text.d.ts +23 -0
- package/dist/src/chats/workflow-generation-text.js +74 -0
- package/dist/src/chats/workflow-generation.d.ts +69 -0
- package/dist/src/chats/workflow-generation.js +142 -0
- package/dist/src/cli/render.d.ts +1 -0
- package/dist/src/cli/render.js +60 -13
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli/watch-board-view.d.ts +12 -1
- package/dist/src/cli/watch-board-view.js +57 -11
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +42 -10
- package/dist/src/cli/watch-client.d.ts +1 -1
- package/dist/src/cli/watch-client.js +7 -3
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +35 -58
- package/dist/src/cli/watch-page.js +21 -4
- package/dist/src/cli/watch-phases.js +3 -3
- package/dist/src/cli/watch-pr-command.d.ts +5 -2
- package/dist/src/cli/watch-pr-command.js +41 -4
- package/dist/src/cli/watch-pr-identity.d.ts +14 -2
- package/dist/src/cli/watch-pr-identity.js +25 -0
- package/dist/src/cli/watch-pr-observer.d.ts +1 -0
- package/dist/src/cli/watch-pr-observer.js +48 -6
- package/dist/src/cli/watch-pull-request.d.ts +10 -0
- package/dist/src/cli/watch-pull-request.js +130 -0
- package/dist/src/cli/watch-reads.d.ts +7 -1
- package/dist/src/cli/watch-reads.js +42 -20
- package/dist/src/cli/watch-render.d.ts +14 -1
- package/dist/src/cli/watch-render.js +2 -1
- package/dist/src/cli/watch-server.js +33 -2
- package/dist/src/cli/watch-turn.js +1 -1
- package/dist/src/cli/watch-view.d.ts +7 -0
- package/dist/src/cli/watch-view.js +51 -3
- package/dist/src/daemon/analysis-worker.d.ts +0 -2
- package/dist/src/daemon/analysis-worker.js +71 -19
- package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
- package/dist/src/daemon/chats-batch-binding.js +99 -0
- package/dist/src/daemon/chats-capture.d.ts +18 -0
- package/dist/src/daemon/chats-capture.js +194 -0
- package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
- package/dist/src/daemon/chats-checkpoints.js +186 -0
- package/dist/src/daemon/chats-files.d.ts +29 -2
- package/dist/src/daemon/chats-files.js +142 -3
- package/dist/src/daemon/chats-follow-up.d.ts +38 -0
- package/dist/src/daemon/chats-follow-up.js +180 -0
- package/dist/src/daemon/chats-question.d.ts +15 -0
- package/dist/src/daemon/chats-question.js +93 -0
- package/dist/src/daemon/chats-worker.d.ts +1 -0
- package/dist/src/daemon/chats-worker.js +52 -51
- package/dist/src/daemon/claude-turn.d.ts +2 -1
- package/dist/src/daemon/claude-turn.js +1 -1
- package/dist/src/daemon/claude-worker.d.ts +0 -2
- package/dist/src/daemon/claude-worker.js +6 -11
- package/dist/src/daemon/daemon.d.ts +5 -1
- package/dist/src/daemon/daemon.js +38 -10
- package/dist/src/daemon/delivery-router.js +6 -0
- package/dist/src/daemon/github-calls.d.ts +4 -0
- package/dist/src/daemon/github-calls.js +16 -7
- package/dist/src/daemon/github-ci.d.ts +6 -0
- package/dist/src/daemon/github-ci.js +170 -0
- package/dist/src/daemon/github-merge-call.d.ts +14 -0
- package/dist/src/daemon/github-merge-call.js +73 -0
- package/dist/src/daemon/github-merge.d.ts +3 -5
- package/dist/src/daemon/github-merge.js +52 -95
- package/dist/src/daemon/github-methods.d.ts +28 -0
- package/dist/src/daemon/github-methods.js +80 -16
- package/dist/src/daemon/github-operation.d.ts +2 -0
- package/dist/src/daemon/github-retry.d.ts +6 -0
- package/dist/src/daemon/github-retry.js +49 -0
- package/dist/src/daemon/github-ticket-status.js +27 -1
- package/dist/src/daemon/github-worker.js +2 -1
- package/dist/src/daemon/handoff-worker.d.ts +10 -0
- package/dist/src/daemon/handoff-worker.js +232 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +94 -42
- package/dist/src/daemon/notify-journal.d.ts +18 -0
- package/dist/src/daemon/notify-journal.js +108 -0
- package/dist/src/daemon/notify-message.js +2 -1
- package/dist/src/daemon/notify.js +176 -37
- package/dist/src/daemon/retrying-turn.d.ts +3 -0
- package/dist/src/daemon/retrying-turn.js +55 -9
- package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
- package/dist/src/daemon/ticket-dependency-waits.js +60 -0
- package/dist/src/daemon/ticket-registration.d.ts +8 -4
- package/dist/src/daemon/ticket-registration.js +76 -22
- package/dist/src/daemon/ticket-source.d.ts +4 -1
- package/dist/src/daemon/ticket-source.js +8 -2
- package/dist/src/decision-visibility.d.ts +2 -0
- package/dist/src/decision-visibility.js +10 -0
- package/dist/src/execution-pin.d.ts +4 -0
- package/dist/src/execution-pin.js +23 -2
- package/dist/src/host/start-delivery.d.ts +2 -1
- package/dist/src/host/start-delivery.js +1 -1
- package/dist/src/host/step-bridge-host.d.ts +1 -0
- package/dist/src/host/step-bridge-host.js +16 -0
- package/dist/src/host/world-reconciliation.d.ts +1 -0
- package/dist/src/host/world-reconciliation.js +3 -0
- package/dist/src/host/world-runs.js +5 -0
- package/dist/src/issue-identity.d.ts +6 -0
- package/dist/src/issue-identity.js +23 -0
- package/dist/src/service/queue-runs.d.ts +3 -0
- package/dist/src/service/queue-runs.js +39 -0
- package/dist/src/service/registry.d.ts +8 -2
- package/dist/src/service/registry.js +20 -8
- package/dist/src/service/replies.d.ts +2 -0
- package/dist/src/service/run-handlers.js +44 -12
- package/dist/src/service/service.d.ts +3 -0
- package/dist/src/service/service.js +3 -1
- package/dist/src/service/submission-handler.d.ts +1 -0
- package/dist/src/service/submission-handler.js +92 -13
- package/dist/src/service/worker-totals.js +25 -1
- package/dist/src/step-bridge.d.ts +9 -0
- package/dist/src/steps/handoff.d.ts +2 -0
- package/dist/src/steps/handoff.js +13 -0
- package/dist/src/ticket-dependencies.d.ts +1 -0
- package/dist/src/ticket-dependencies.js +15 -0
- package/dist/src/ticket-status.js +7 -4
- package/dist/src/token-prices.d.ts +2 -0
- package/dist/src/token-prices.js +41 -9
- package/dist/src/worker-usage.d.ts +9 -0
- package/dist/src/worker-usage.js +1 -1
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.js +1 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +8 -3
- package/dist/src/workflow-declaration.d.ts +11 -0
- package/dist/src/workflow-declaration.js +71 -15
- package/dist/src/workflow-discovery.d.ts +18 -0
- package/dist/src/workflow-discovery.js +33 -0
- package/dist/src/workflow-refusals.d.ts +6 -0
- package/dist/src/workflow-refusals.js +39 -0
- package/dist/src/workflows.d.ts +2 -1
- package/dist/src/workflows.js +1 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- package/dist/tools/self-host.js +27 -81
- package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
- package/dist/tools/workflow-bundle-dependencies.js +93 -0
- package/dist/tools/workflow-bundles.d.ts +20 -0
- package/dist/tools/workflow-bundles.js +173 -0
- package/dist/tools/workflow-candidate-counts.d.ts +8 -0
- package/dist/tools/workflow-candidate-counts.js +251 -0
- package/dist/tools/workflow-candidate-program.d.ts +11 -0
- package/dist/tools/workflow-candidate-program.js +78 -0
- package/dist/tools/workflow-candidate-source.d.ts +6 -0
- package/dist/tools/workflow-candidate-source.js +107 -0
- package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
- package/dist/tools/workflow-candidate-syntax.js +219 -0
- package/dist/tools/workflow-folder-capture.d.ts +17 -0
- package/dist/tools/workflow-folder-capture.js +216 -0
- package/dist/tools/workflow-folders.d.ts +33 -0
- package/dist/tools/workflow-folders.js +150 -0
- package/dist/tools/workflow-source-effects.d.ts +10 -0
- package/dist/tools/workflow-source-effects.js +213 -0
- package/dist/workflows/analyze-chats.d.ts +6 -1
- package/dist/workflows/analyze-chats.declaration.json +14 -1
- package/dist/workflows/analyze-chats.js +188 -11
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.js +5 -0
- package/dist/workflows/ticket-prompts.js +12 -2
- package/package.json +21 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { recordedNotifications, saveNotifications, } from "./notify-journal.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { send } from "../cli/client.js";
|
|
5
5
|
import { record } from "../protocol.js";
|
|
6
6
|
import { decisionNotification, outcomeNotification, } from "./notify-message.js";
|
|
7
|
+
const retentionMilliseconds = 7 * 24 * 60 * 60_000;
|
|
7
8
|
async function request(runtimeDirectory, request) {
|
|
8
9
|
const reply = await send(runtimeDirectory, request);
|
|
9
10
|
if (reply.kind === "error")
|
|
@@ -20,6 +21,20 @@ function rows(value) {
|
|
|
20
21
|
return row;
|
|
21
22
|
});
|
|
22
23
|
}
|
|
24
|
+
async function flushNotifications(journal) {
|
|
25
|
+
if (journal.unsaved.size === 0)
|
|
26
|
+
return;
|
|
27
|
+
await saveNotifications(journal.file, journal);
|
|
28
|
+
for (const entry of journal.unsaved.values())
|
|
29
|
+
if (entry.state === "failed" && entry.attempts === 5)
|
|
30
|
+
process.stderr.write(`Notify delivery failed: ${entry.key}\n`);
|
|
31
|
+
journal.unsaved.clear();
|
|
32
|
+
}
|
|
33
|
+
async function finishDelivery(journal, entry) {
|
|
34
|
+
journal.entries.set(entry.key, entry);
|
|
35
|
+
journal.unsaved.set(entry.key, entry);
|
|
36
|
+
await flushNotifications(journal);
|
|
37
|
+
}
|
|
23
38
|
async function observedOutcome(summary, source) {
|
|
24
39
|
const { runtimeDirectory, cache } = source;
|
|
25
40
|
const latestSequence = summary["latestSequence"];
|
|
@@ -74,26 +89,6 @@ async function events(runtimeDirectory, cache) {
|
|
|
74
89
|
}
|
|
75
90
|
return found;
|
|
76
91
|
}
|
|
77
|
-
async function recorded(file) {
|
|
78
|
-
const text = await readFile(file, "utf8").catch((error) => {
|
|
79
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
80
|
-
return undefined;
|
|
81
|
-
throw error;
|
|
82
|
-
});
|
|
83
|
-
if (text === undefined)
|
|
84
|
-
return undefined;
|
|
85
|
-
const value = JSON.parse(text);
|
|
86
|
-
if (!Array.isArray(value) || !value.every((key) => typeof key === "string"))
|
|
87
|
-
throw new Error("invalid-notify-record");
|
|
88
|
-
return new Set(value.map(String));
|
|
89
|
-
}
|
|
90
|
-
async function save(file, keys) {
|
|
91
|
-
await writeFile(`${file}.tmp`, JSON.stringify([...keys]) + "\n", {
|
|
92
|
-
mode: 0o600,
|
|
93
|
-
flush: true,
|
|
94
|
-
});
|
|
95
|
-
await rename(`${file}.tmp`, file);
|
|
96
|
-
}
|
|
97
92
|
export function notifyCommand(command, notification) {
|
|
98
93
|
return new Promise((resolve, reject) => {
|
|
99
94
|
const [executable = "", ...argumentsList] = command;
|
|
@@ -117,45 +112,189 @@ function report(label, error) {
|
|
|
117
112
|
const reason = error instanceof Error ? error.message : String(error);
|
|
118
113
|
process.stderr.write(`${label}: ${JSON.stringify(reason)}\n`);
|
|
119
114
|
}
|
|
115
|
+
function failureReason(error) {
|
|
116
|
+
if (!(error instanceof Error))
|
|
117
|
+
return "command error";
|
|
118
|
+
if ("code" in error && (error.code === "ENOENT" || error.code === "EACCES"))
|
|
119
|
+
return `spawn ${error.code}`;
|
|
120
|
+
return /^(?:exit code (?:\d+|null)|signal SIG[A-Z0-9]+)$/.test(error.message)
|
|
121
|
+
? error.message
|
|
122
|
+
: "command error";
|
|
123
|
+
}
|
|
124
|
+
async function prepareDelivery(journal, entry) {
|
|
125
|
+
if (entry.message === null || entry.state === "sent")
|
|
126
|
+
return undefined;
|
|
127
|
+
if (entry.attempts === 5 && entry.state === "pending")
|
|
128
|
+
await finishDelivery(journal, {
|
|
129
|
+
...entry,
|
|
130
|
+
state: "failed",
|
|
131
|
+
lastFailure: "attempt interrupted",
|
|
132
|
+
});
|
|
133
|
+
if (entry.attempts === 5)
|
|
134
|
+
return undefined;
|
|
135
|
+
const backoff = [0, 60_000, 300_000, 900_000, 3_600_000][entry.attempts] ?? 0;
|
|
136
|
+
if (entry.lastAttemptAt !== null &&
|
|
137
|
+
Date.now() - entry.lastAttemptAt < backoff)
|
|
138
|
+
return undefined;
|
|
139
|
+
const pending = {
|
|
140
|
+
...entry,
|
|
141
|
+
message: entry.message,
|
|
142
|
+
state: "pending",
|
|
143
|
+
attempts: entry.attempts + 1,
|
|
144
|
+
lastAttemptAt: Date.now(),
|
|
145
|
+
};
|
|
146
|
+
const next = new Map(journal.entries).set(entry.key, pending);
|
|
147
|
+
await saveNotifications(journal.file, { ...journal, entries: next });
|
|
148
|
+
journal.entries.set(entry.key, pending);
|
|
149
|
+
return pending;
|
|
150
|
+
}
|
|
151
|
+
async function deliver(settings, entry) {
|
|
152
|
+
try {
|
|
153
|
+
await notifyCommand(settings.command, entry.message);
|
|
154
|
+
return { ...entry, state: "sent" };
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
...entry,
|
|
159
|
+
state: "failed",
|
|
160
|
+
lastFailure: failureReason(error),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function settleDelivery(journal, progress) {
|
|
165
|
+
if (progress.completed !== undefined) {
|
|
166
|
+
const entry = progress.completed;
|
|
167
|
+
progress.completed = undefined;
|
|
168
|
+
await finishDelivery(journal, entry);
|
|
169
|
+
}
|
|
170
|
+
await flushNotifications(journal);
|
|
171
|
+
}
|
|
172
|
+
async function scheduleDelivery(settings, delivery) {
|
|
173
|
+
const { journal, progress } = delivery;
|
|
174
|
+
const stopped = () => progress.stopping.aborted;
|
|
175
|
+
if (stopped() ||
|
|
176
|
+
progress.active !== undefined ||
|
|
177
|
+
progress.completed !== undefined ||
|
|
178
|
+
journal.unsaved.size > 0)
|
|
179
|
+
return;
|
|
180
|
+
for (const entry of journal.entries.values()) {
|
|
181
|
+
if (stopped())
|
|
182
|
+
return;
|
|
183
|
+
const pending = await prepareDelivery(journal, entry);
|
|
184
|
+
if (pending === undefined)
|
|
185
|
+
continue;
|
|
186
|
+
if (stopped()) {
|
|
187
|
+
await finishDelivery(journal, entry);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
progress.active = deliver(settings, pending).then((completed) => {
|
|
191
|
+
progress.completed = completed;
|
|
192
|
+
progress.active = undefined;
|
|
193
|
+
delivery.ready();
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
120
198
|
async function check(settings, journal) {
|
|
199
|
+
const retained = new Map([...journal.entries].filter(([, entry]) => entry.state === "pending" ||
|
|
200
|
+
(entry.state === "failed" && entry.attempts < 5) ||
|
|
201
|
+
entry.lastAttemptAt === null ||
|
|
202
|
+
entry.lastAttemptAt >= Date.now() - retentionMilliseconds));
|
|
203
|
+
const removed = [...journal.entries.keys()].filter((key) => !retained.has(key));
|
|
204
|
+
if (removed.length > 0) {
|
|
205
|
+
const retiredKeys = new Set([...journal.retiredKeys, ...removed]);
|
|
206
|
+
await saveNotifications(journal.file, { entries: retained, retiredKeys });
|
|
207
|
+
for (const key of removed) {
|
|
208
|
+
journal.entries.delete(key);
|
|
209
|
+
journal.retiredKeys.add(key);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
121
212
|
for (const event of await events(settings.runtimeDirectory, journal.cache)) {
|
|
122
|
-
if (journal.
|
|
213
|
+
if (journal.entries.has(event.key) || journal.retiredKeys.has(event.key))
|
|
123
214
|
continue;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
215
|
+
const entry = {
|
|
216
|
+
...event,
|
|
217
|
+
state: "pending",
|
|
218
|
+
attempts: 0,
|
|
219
|
+
lastAttemptAt: null,
|
|
220
|
+
lastFailure: null,
|
|
221
|
+
};
|
|
222
|
+
journal.entries.set(event.key, entry);
|
|
223
|
+
journal.unsaved.set(event.key, entry);
|
|
129
224
|
}
|
|
225
|
+
await flushNotifications(journal);
|
|
130
226
|
}
|
|
131
227
|
export async function notifyRuns(settings) {
|
|
132
228
|
const file = join(settings.directory, "notify-events.json");
|
|
133
|
-
const
|
|
229
|
+
const recorded = await recordedNotifications(file);
|
|
134
230
|
const cache = new Map();
|
|
135
|
-
const journal = {
|
|
136
|
-
|
|
231
|
+
const journal = {
|
|
232
|
+
file,
|
|
233
|
+
entries: recorded?.entries ?? new Map(),
|
|
234
|
+
retiredKeys: recorded?.retiredKeys ?? new Set(),
|
|
235
|
+
cache,
|
|
236
|
+
unsaved: new Map(),
|
|
237
|
+
};
|
|
238
|
+
if (recorded === undefined) {
|
|
137
239
|
for (const event of await events(settings.runtimeDirectory, cache))
|
|
138
|
-
journal.
|
|
139
|
-
|
|
240
|
+
journal.entries.set(event.key, {
|
|
241
|
+
...event,
|
|
242
|
+
state: "sent",
|
|
243
|
+
attempts: 0,
|
|
244
|
+
lastAttemptAt: Date.now(),
|
|
245
|
+
lastFailure: null,
|
|
246
|
+
});
|
|
140
247
|
}
|
|
248
|
+
await saveNotifications(file, journal);
|
|
249
|
+
return notificationLoop(settings, journal);
|
|
250
|
+
}
|
|
251
|
+
function notificationLoop(settings, journal) {
|
|
141
252
|
let busy = false;
|
|
142
253
|
let running = Promise.resolve();
|
|
254
|
+
const stopping = new AbortController();
|
|
255
|
+
const progress = {
|
|
256
|
+
stopping: stopping.signal,
|
|
257
|
+
active: undefined,
|
|
258
|
+
completed: undefined,
|
|
259
|
+
};
|
|
260
|
+
const ready = () => {
|
|
261
|
+
if (stopping.signal.aborted)
|
|
262
|
+
return;
|
|
263
|
+
running = running
|
|
264
|
+
.then(() => settleDelivery(journal, progress))
|
|
265
|
+
.then(() => scheduleDelivery(settings, { journal, progress, ready }))
|
|
266
|
+
.catch((error) => {
|
|
267
|
+
report("Notify delivery failed", error);
|
|
268
|
+
});
|
|
269
|
+
};
|
|
143
270
|
const timer = setInterval(() => {
|
|
144
271
|
if (busy)
|
|
145
272
|
return;
|
|
146
273
|
busy = true;
|
|
147
|
-
running =
|
|
274
|
+
running = running
|
|
275
|
+
.then(() => settleDelivery(journal, progress))
|
|
276
|
+
.then(() => check(settings, journal))
|
|
148
277
|
.catch((error) => {
|
|
149
278
|
report("Notify check failed", error);
|
|
150
|
-
})
|
|
151
|
-
|
|
279
|
+
});
|
|
280
|
+
ready();
|
|
281
|
+
running = running.finally(() => {
|
|
152
282
|
busy = false;
|
|
153
283
|
});
|
|
154
284
|
}, settings.intervalMilliseconds ?? 15_000);
|
|
155
285
|
return {
|
|
156
|
-
stop:
|
|
286
|
+
stop: () => {
|
|
287
|
+
stopping.abort();
|
|
157
288
|
clearInterval(timer);
|
|
158
|
-
|
|
289
|
+
running = running
|
|
290
|
+
.then(async () => {
|
|
291
|
+
await progress.active;
|
|
292
|
+
await settleDelivery(journal, progress);
|
|
293
|
+
})
|
|
294
|
+
.catch((error) => {
|
|
295
|
+
report("Notify delivery failed", error);
|
|
296
|
+
});
|
|
297
|
+
return running;
|
|
159
298
|
},
|
|
160
299
|
};
|
|
161
300
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type TokenPrices } from "../token-prices.ts";
|
|
1
2
|
import { type RunningTurn, type TurnStart } from "./claude-turn.ts";
|
|
2
3
|
export interface RetrySettings {
|
|
4
|
+
readonly tokenPrices?: TokenPrices;
|
|
5
|
+
readonly fallbackModels?: readonly string[];
|
|
3
6
|
readonly retryDelay?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
4
7
|
}
|
|
5
8
|
interface RetryOptions extends RetrySettings {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
-
import { priceOf, sumUsage, usageOf } from "../worker-usage.js";
|
|
4
|
+
import { priceOf, sumUsage, usageOf, } from "../worker-usage.js";
|
|
5
|
+
import { commandModel, replaceCommandModel, estimatedCostUsd, priceForModel, } from "../token-prices.js";
|
|
6
|
+
import { analysisCommand } from "./analysis-launch.js";
|
|
5
7
|
import { capacityRefusal, startTurn, } from "./claude-turn.js";
|
|
6
8
|
import { readRecord, writeRecord } from "./operation-journal.js";
|
|
7
9
|
const waits = [60_000, 180_000];
|
|
@@ -14,10 +16,9 @@ function total(values) {
|
|
|
14
16
|
return null;
|
|
15
17
|
return priceOf(values.reduce((sum, value) => sum + (value ?? 0), 0));
|
|
16
18
|
}
|
|
17
|
-
function combined(
|
|
19
|
+
function combined(attempts) {
|
|
20
|
+
const { ends } = attempts;
|
|
18
21
|
const last = ends.at(-1) ?? { exitCode: null, result: undefined };
|
|
19
|
-
if (ends.length < 2)
|
|
20
|
-
return last;
|
|
21
22
|
const result = last.result ?? ends.find((end) => end.result)?.result;
|
|
22
23
|
if (result === undefined)
|
|
23
24
|
return last;
|
|
@@ -25,6 +26,7 @@ function combined(ends) {
|
|
|
25
26
|
...last,
|
|
26
27
|
result: {
|
|
27
28
|
...result,
|
|
29
|
+
attempts: ends.map((end, index) => attemptUsage(attempts, { end, attempt: index + 1 })),
|
|
28
30
|
...(last.result === undefined ? { failed: true, text: "" } : {}),
|
|
29
31
|
usage: sumUsage(ends.map((end) => end.result?.usage ?? usageOf(null))),
|
|
30
32
|
costUsd: total(ends.map((end) => end.result?.costUsd ?? null)),
|
|
@@ -45,14 +47,52 @@ function controlled(attempts, start) {
|
|
|
45
47
|
}),
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
function attemptCommand(attempts, attempt) {
|
|
51
|
+
const { start, options } = attempts;
|
|
52
|
+
const original = start.mode === "analysis" ? analysisCommand() : start.command;
|
|
53
|
+
const fallback = start.mode === "analysis"
|
|
54
|
+
? undefined
|
|
55
|
+
: options.fallbackModels?.[attempt - 3];
|
|
56
|
+
const command = fallback === undefined ? original : replaceCommandModel(original, fallback);
|
|
57
|
+
const model = commandModel(command);
|
|
58
|
+
return {
|
|
59
|
+
command,
|
|
60
|
+
model,
|
|
61
|
+
...(fallback !== undefined && model === null
|
|
62
|
+
? { fallbackReason: "fallback-model-unavailable" }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function attemptUsage(attempts, completion) {
|
|
67
|
+
const { end, attempt } = completion;
|
|
68
|
+
const model = end.result?.model ?? attemptCommand(attempts, attempt).model;
|
|
69
|
+
const usage = end.result?.usage ?? usageOf(null);
|
|
70
|
+
return {
|
|
71
|
+
attempt,
|
|
72
|
+
model,
|
|
73
|
+
usage,
|
|
74
|
+
costUsd: end.result?.costUsd ?? null,
|
|
75
|
+
estimatedCostUsd: estimatedCostUsd(usage, {
|
|
76
|
+
worker: attempts.start.mode === "analysis"
|
|
77
|
+
? "claude"
|
|
78
|
+
: (attempts.start.worker ?? "claude"),
|
|
79
|
+
price: priceForModel(attempts.options.tokenPrices ?? {}, model),
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
48
83
|
async function launched(attempts) {
|
|
49
84
|
const { start, options, ends } = attempts;
|
|
50
85
|
const attempt = ends.length + 1;
|
|
86
|
+
const selected = attemptCommand(attempts, attempt);
|
|
51
87
|
const folder = start.mode === "analysis" || attempt === 1
|
|
52
88
|
? start.folder
|
|
53
89
|
: join(start.folder, `attempt-${String(attempt)}`);
|
|
54
90
|
const entry = {
|
|
55
91
|
...options.metadata,
|
|
92
|
+
model: selected.model,
|
|
93
|
+
...(selected.fallbackReason === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { fallbackReason: selected.fallbackReason }),
|
|
56
96
|
attempt,
|
|
57
97
|
startedAt: attempt === 1 ? attempts.startedAt : new Date().toISOString(),
|
|
58
98
|
};
|
|
@@ -70,9 +110,12 @@ async function launched(attempts) {
|
|
|
70
110
|
return undefined;
|
|
71
111
|
const turn = controlled(attempts, {
|
|
72
112
|
...start,
|
|
113
|
+
command: selected.command,
|
|
73
114
|
folder,
|
|
74
115
|
...(attempt === 1 ? {} : { activityFolder: start.folder }),
|
|
75
|
-
prompt:
|
|
116
|
+
prompt: start.mode === "analysis" || attempt === 1
|
|
117
|
+
? start.prompt
|
|
118
|
+
: continuation + start.prompt,
|
|
76
119
|
});
|
|
77
120
|
try {
|
|
78
121
|
const value = { ...entry, phase: "running", processId: turn.processId };
|
|
@@ -96,6 +139,7 @@ async function recordedEnd(attempts, completion) {
|
|
|
96
139
|
const { start, ends, options } = attempts;
|
|
97
140
|
const { processId, end } = completion;
|
|
98
141
|
const attempt = ends.length;
|
|
142
|
+
const selected = attemptCommand(attempts, attempt);
|
|
99
143
|
const name = `attempt-${String(attempt)}.json`;
|
|
100
144
|
const started = await readRecord(start.folder, name);
|
|
101
145
|
const startedAt = Reflect.get(Object(started), "startedAt");
|
|
@@ -105,14 +149,16 @@ async function recordedEnd(attempts, completion) {
|
|
|
105
149
|
name,
|
|
106
150
|
value: {
|
|
107
151
|
...options.metadata,
|
|
152
|
+
...attemptUsage(attempts, { end, attempt }),
|
|
153
|
+
...(selected.fallbackReason === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { fallbackReason: selected.fallbackReason }),
|
|
108
156
|
attempt,
|
|
109
157
|
processId,
|
|
110
158
|
startedAt,
|
|
111
159
|
endedAt: new Date().toISOString(),
|
|
112
160
|
exitCode: end.exitCode,
|
|
113
161
|
capacity: capacityRefusal(end.result),
|
|
114
|
-
usage: end.result?.usage ?? usageOf(null),
|
|
115
|
-
costUsd: end.result?.costUsd ?? null,
|
|
116
162
|
},
|
|
117
163
|
});
|
|
118
164
|
}
|
|
@@ -151,11 +197,11 @@ async function finished(attempts, first) {
|
|
|
151
197
|
break;
|
|
152
198
|
await waited(attempts, milliseconds);
|
|
153
199
|
if (cancelled(attempts))
|
|
154
|
-
return { ...combined(
|
|
200
|
+
return { ...combined(attempts), stopped: true };
|
|
155
201
|
turn = await launched(attempts);
|
|
156
202
|
}
|
|
157
203
|
return {
|
|
158
|
-
...combined(
|
|
204
|
+
...combined(attempts),
|
|
159
205
|
...(cancelled(attempts) || turn === undefined ? { stopped: true } : {}),
|
|
160
206
|
};
|
|
161
207
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Fields } from "../protocol.ts";
|
|
2
|
+
interface Source {
|
|
3
|
+
readonly repository: string;
|
|
4
|
+
readonly command: readonly string[] | undefined;
|
|
5
|
+
readonly githubCommand?: readonly string[] | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare class TicketDependencyReadError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export declare function ticketDependencyWaits(source: Source): {
|
|
10
|
+
readonly check: (candidate: {
|
|
11
|
+
readonly ticketId: string;
|
|
12
|
+
readonly ticket: string;
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
}) => Promise<readonly string[]>;
|
|
15
|
+
readonly refresh: (signal: AbortSignal) => Promise<readonly string[]>;
|
|
16
|
+
readonly skipped: () => readonly Fields[];
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hardDependencies } from "../ticket-dependencies.js";
|
|
2
|
+
import { issueIdentity } from "../issue-identity.js";
|
|
3
|
+
import { record } from "../protocol.js";
|
|
4
|
+
import { calls } from "./github-calls.js";
|
|
5
|
+
import { ticketText } from "./ticket-source.js";
|
|
6
|
+
export class TicketDependencyReadError extends Error {
|
|
7
|
+
}
|
|
8
|
+
async function openDependencies(source, candidate) {
|
|
9
|
+
const github = calls({ repository: source.repository, gh: source.githubCommand ?? ["gh"] }, candidate.signal);
|
|
10
|
+
const issue = issueIdentity(candidate.ticket);
|
|
11
|
+
const repository = issue === null ? "{owner}/{repo}" : `${issue.owner}/${issue.repository}`;
|
|
12
|
+
const open = [];
|
|
13
|
+
for (const id of hardDependencies(candidate.ticket)) {
|
|
14
|
+
const response = JSON.parse(await github.gh(["api", `repos/${repository}/issues/${id}`]).catch(() => {
|
|
15
|
+
throw new TicketDependencyReadError("github-call-failed");
|
|
16
|
+
}));
|
|
17
|
+
const state = record(response)?.["state"];
|
|
18
|
+
if (state !== "open" && state !== "closed")
|
|
19
|
+
throw new Error("invalid-dependency-state");
|
|
20
|
+
if (state === "open")
|
|
21
|
+
open.push(id);
|
|
22
|
+
}
|
|
23
|
+
return open;
|
|
24
|
+
}
|
|
25
|
+
export function ticketDependencyWaits(source) {
|
|
26
|
+
const waiting = new Map();
|
|
27
|
+
let nextRefresh;
|
|
28
|
+
const check = async (candidate) => {
|
|
29
|
+
const open = await openDependencies(source, candidate);
|
|
30
|
+
if (open.length === 0)
|
|
31
|
+
waiting.delete(candidate.ticketId);
|
|
32
|
+
else
|
|
33
|
+
waiting.set(candidate.ticketId, open);
|
|
34
|
+
return open;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
check,
|
|
38
|
+
refresh: async (signal) => {
|
|
39
|
+
const tickets = [...waiting.keys()];
|
|
40
|
+
const ticketId = nextRefresh !== undefined && waiting.has(nextRefresh)
|
|
41
|
+
? nextRefresh
|
|
42
|
+
: tickets[0];
|
|
43
|
+
if (ticketId === undefined)
|
|
44
|
+
return [];
|
|
45
|
+
nextRefresh = tickets[tickets.indexOf(ticketId) + 1] ?? tickets[0];
|
|
46
|
+
if (source.command === undefined)
|
|
47
|
+
throw new Error("ticket-source-not-configured");
|
|
48
|
+
const read = await ticketText(source.command, { ticketId, signal });
|
|
49
|
+
if (read.kind === "failed")
|
|
50
|
+
throw new TicketDependencyReadError("ticket-not-available");
|
|
51
|
+
await check({ ticketId, ticket: read.text, signal });
|
|
52
|
+
return [...waiting.keys()];
|
|
53
|
+
},
|
|
54
|
+
skipped: () => [...waiting].map(([ticketId, dependencies]) => ({
|
|
55
|
+
ticketId,
|
|
56
|
+
reason: "ticket-dependency-open",
|
|
57
|
+
dependencies,
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { DeclaredWorkflow } from "../workflow-declaration.ts";
|
|
1
|
+
import type { DeclaredWorkflow, WorkflowStage } from "../workflow-declaration.ts";
|
|
2
2
|
import type { Resolver, WorkflowRegistration } from "../service/registry.ts";
|
|
3
|
-
export declare const ticketWorkflow
|
|
4
|
-
export declare const promptWorkflow
|
|
5
|
-
export declare const issueWorkflow
|
|
3
|
+
export declare const ticketWorkflow: "develop-ticket";
|
|
4
|
+
export declare const promptWorkflow: "develop-prompt";
|
|
5
|
+
export declare const issueWorkflow: "develop-issue";
|
|
6
6
|
export declare const ticketCapabilities: string[];
|
|
7
7
|
export type TicketRegistrations = {
|
|
8
8
|
readonly kind: "admitted";
|
|
@@ -15,6 +15,8 @@ export interface TicketSource {
|
|
|
15
15
|
readonly repository: string;
|
|
16
16
|
readonly command: readonly string[] | undefined;
|
|
17
17
|
readonly nextCommand?: readonly string[] | undefined;
|
|
18
|
+
readonly githubCommand?: readonly string[] | undefined;
|
|
19
|
+
readonly skipTickets?: readonly string[];
|
|
18
20
|
}
|
|
19
21
|
export interface TicketRegistration {
|
|
20
22
|
readonly workflowId: string;
|
|
@@ -23,6 +25,8 @@ export interface TicketRegistration {
|
|
|
23
25
|
readonly submission: WorkflowRegistration["submission"];
|
|
24
26
|
readonly effectLimit: number;
|
|
25
27
|
readonly capabilities?: readonly string[];
|
|
28
|
+
readonly stages?: readonly WorkflowStage[];
|
|
29
|
+
readonly handoffs?: readonly string[];
|
|
26
30
|
}
|
|
27
31
|
export declare function ticketResolver(source: TicketSource): Resolver;
|
|
28
32
|
export declare function issueResolver(source: TicketSource): Resolver;
|