cloving 1.0.1 → 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 +251 -20
- 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 +33 -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 +3 -0
- package/dist/src/authority/reconciliation.js +21 -5
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +5 -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/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- 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 +42 -0
- package/dist/src/chats/analysis-batches.js +97 -0
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -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 +18 -0
- package/dist/src/chats/analysis-input.js +97 -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-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +18 -0
- package/dist/src/chats/analysis-prompt.js +42 -0
- package/dist/src/chats/analysis-proposal.d.ts +4 -0
- package/dist/src/chats/analysis-proposal.js +41 -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 +4 -0
- package/dist/src/chats/analysis-shape.js +235 -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/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -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/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/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 +3 -0
- package/dist/src/cli/render.js +122 -7
- 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 +42 -7
- 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 +44 -1
- package/dist/src/cli/watch-board-view.js +137 -18
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +48 -11
- 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 +122 -0
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +61 -67
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +89 -86
- package/dist/src/cli/watch-phases.js +3 -3
- 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 +18 -0
- package/dist/src/cli/watch-pr-command.js +74 -0
- package/dist/src/cli/watch-pr-identity.d.ts +32 -0
- package/dist/src/cli/watch-pr-identity.js +87 -0
- package/dist/src/cli/watch-pr-observer.d.ts +20 -0
- package/dist/src/cli/watch-pr-observer.js +150 -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-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 +44 -18
- package/dist/src/cli/watch-render.d.ts +29 -2
- package/dist/src/cli/watch-render.js +11 -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 +77 -8
- 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 +8 -3
- 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 +22 -0
- package/dist/src/cli/watch-view.js +111 -19
- 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 +20 -0
- package/dist/src/daemon/analysis-worker.js +271 -0
- 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 +42 -0
- package/dist/src/daemon/chats-files.js +252 -0
- 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 +13 -0
- package/dist/src/daemon/chats-worker.js +201 -0
- package/dist/src/daemon/claude-turn.d.ts +11 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +2 -1
- package/dist/src/daemon/claude-worker.js +92 -45
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +8 -1
- package/dist/src/daemon/daemon.js +51 -14
- 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 +84 -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.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +294 -0
- 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 +2 -0
- package/dist/src/daemon/issue-queue.js +186 -43
- 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/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +12 -0
- package/dist/src/daemon/retrying-turn.js +231 -0
- 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 +3 -0
- package/dist/src/host/world-reconciliation.js +9 -0
- package/dist/src/host/world-runs.js +14 -1
- 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 +54 -30
- 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.d.ts +3 -0
- package/dist/src/service/worker-totals.js +52 -0
- 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.d.ts +8 -0
- package/dist/src/ticket-status.js +48 -0
- package/dist/src/token-prices.d.ts +21 -0
- package/dist/src/token-prices.js +108 -0
- package/dist/src/worker-usage.d.ts +31 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +60 -0
- package/dist/src/workflow-contracts.d.ts +4 -0
- package/dist/src/workflow-contracts.js +32 -5
- package/dist/src/workflow-declaration.d.ts +12 -1
- package/dist/src/workflow-declaration.js +71 -13
- 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 +3 -1
- package/dist/src/workflows.js +2 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- 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 +30 -84
- 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/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 +15 -0
- package/dist/workflows/analyze-chats.declaration.json +22 -0
- package/dist/workflows/analyze-chats.js +250 -0
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +47 -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 +143 -0
- package/package.json +25 -5
|
@@ -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
|
}
|
|
@@ -32,6 +32,7 @@ export declare function readRecord(folder: string, name: string): Promise<unknow
|
|
|
32
32
|
export declare function writeRecord(folder: string, record: {
|
|
33
33
|
readonly name: string;
|
|
34
34
|
readonly value: unknown;
|
|
35
|
+
readonly permissions?: "private";
|
|
35
36
|
}): Promise<void>;
|
|
36
37
|
export declare function replied(identity: Identity, recorded: unknown): AdapterOutcome;
|
|
37
38
|
export declare function interrupted(reason: string): Recorded;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export function operationKey(text) {
|
|
5
5
|
return createHash("sha256").update(text).digest("hex").slice(0, 32);
|
|
@@ -16,7 +16,9 @@ export async function readRecord(folder, name) {
|
|
|
16
16
|
export async function writeRecord(folder, record) {
|
|
17
17
|
const path = join(folder, record.name);
|
|
18
18
|
await mkdir(folder, { recursive: true });
|
|
19
|
-
|
|
19
|
+
if (record.permissions === "private")
|
|
20
|
+
await chmod(folder, 0o700);
|
|
21
|
+
await writeFile(`${path}.staged`, JSON.stringify(record.value, null, 2), record.permissions === "private" ? { mode: 0o600 } : {});
|
|
20
22
|
await rename(`${path}.staged`, path);
|
|
21
23
|
}
|
|
22
24
|
export function replied(identity, recorded) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TokenPrices } from "../token-prices.ts";
|
|
2
|
+
import { type RunningTurn, type TurnStart } from "./claude-turn.ts";
|
|
3
|
+
export interface RetrySettings {
|
|
4
|
+
readonly tokenPrices?: TokenPrices;
|
|
5
|
+
readonly fallbackModels?: readonly string[];
|
|
6
|
+
readonly retryDelay?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
interface RetryOptions extends RetrySettings {
|
|
9
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function startRetriedTurn(start: TurnStart, options: RetryOptions): Promise<RunningTurn>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
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 { commandModel, replaceCommandModel, estimatedCostUsd, priceForModel, } from "../token-prices.js";
|
|
6
|
+
import { analysisCommand } from "./analysis-launch.js";
|
|
7
|
+
import { capacityRefusal, startTurn, } from "./claude-turn.js";
|
|
8
|
+
import { readRecord, writeRecord } from "./operation-journal.js";
|
|
9
|
+
const waits = [60_000, 180_000];
|
|
10
|
+
const continuation = "An earlier attempt stopped because the model was at capacity; continue from the current state of the work tree.\n\n";
|
|
11
|
+
function written(folder, entry) {
|
|
12
|
+
return writeRecord(folder, { ...entry, permissions: "private" });
|
|
13
|
+
}
|
|
14
|
+
function total(values) {
|
|
15
|
+
if (values.includes(null))
|
|
16
|
+
return null;
|
|
17
|
+
return priceOf(values.reduce((sum, value) => sum + (value ?? 0), 0));
|
|
18
|
+
}
|
|
19
|
+
function combined(attempts) {
|
|
20
|
+
const { ends } = attempts;
|
|
21
|
+
const last = ends.at(-1) ?? { exitCode: null, result: undefined };
|
|
22
|
+
const result = last.result ?? ends.find((end) => end.result)?.result;
|
|
23
|
+
if (result === undefined)
|
|
24
|
+
return last;
|
|
25
|
+
return {
|
|
26
|
+
...last,
|
|
27
|
+
result: {
|
|
28
|
+
...result,
|
|
29
|
+
attempts: ends.map((end, index) => attemptUsage(attempts, { end, attempt: index + 1 })),
|
|
30
|
+
...(last.result === undefined ? { failed: true, text: "" } : {}),
|
|
31
|
+
usage: sumUsage(ends.map((end) => end.result?.usage ?? usageOf(null))),
|
|
32
|
+
costUsd: total(ends.map((end) => end.result?.costUsd ?? null)),
|
|
33
|
+
turns: total(ends.map((end) => end.result?.turns ?? null)),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function controlled(attempts, start) {
|
|
38
|
+
const turn = startTurn(start);
|
|
39
|
+
const stop = () => {
|
|
40
|
+
turn.stop(attempts.grace.milliseconds);
|
|
41
|
+
};
|
|
42
|
+
attempts.stopping.signal.addEventListener("abort", stop, { once: true });
|
|
43
|
+
return {
|
|
44
|
+
...turn,
|
|
45
|
+
ended: turn.ended.finally(() => {
|
|
46
|
+
attempts.stopping.signal.removeEventListener("abort", stop);
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
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
|
+
}
|
|
83
|
+
async function launched(attempts) {
|
|
84
|
+
const { start, options, ends } = attempts;
|
|
85
|
+
const attempt = ends.length + 1;
|
|
86
|
+
const selected = attemptCommand(attempts, attempt);
|
|
87
|
+
const folder = start.mode === "analysis" || attempt === 1
|
|
88
|
+
? start.folder
|
|
89
|
+
: join(start.folder, `attempt-${String(attempt)}`);
|
|
90
|
+
const entry = {
|
|
91
|
+
...options.metadata,
|
|
92
|
+
model: selected.model,
|
|
93
|
+
...(selected.fallbackReason === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { fallbackReason: selected.fallbackReason }),
|
|
96
|
+
attempt,
|
|
97
|
+
startedAt: attempt === 1 ? attempts.startedAt : new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
await written(start.folder, {
|
|
100
|
+
name: "started.json",
|
|
101
|
+
value: {
|
|
102
|
+
...entry,
|
|
103
|
+
startedAt: attempts.startedAt,
|
|
104
|
+
phase: "starting",
|
|
105
|
+
processId: null,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
109
|
+
if (attempts.stopping.signal.aborted)
|
|
110
|
+
return undefined;
|
|
111
|
+
const turn = controlled(attempts, {
|
|
112
|
+
...start,
|
|
113
|
+
command: selected.command,
|
|
114
|
+
folder,
|
|
115
|
+
...(attempt === 1 ? {} : { activityFolder: start.folder }),
|
|
116
|
+
prompt: start.mode === "analysis" || attempt === 1
|
|
117
|
+
? start.prompt
|
|
118
|
+
: continuation + start.prompt,
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
const value = { ...entry, phase: "running", processId: turn.processId };
|
|
122
|
+
await written(start.folder, {
|
|
123
|
+
name: "started.json",
|
|
124
|
+
value: { ...value, startedAt: attempts.startedAt },
|
|
125
|
+
});
|
|
126
|
+
await written(start.folder, {
|
|
127
|
+
name: `attempt-${String(attempt)}.json`,
|
|
128
|
+
value,
|
|
129
|
+
});
|
|
130
|
+
return turn;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
turn.stop(0);
|
|
134
|
+
await turn.ended;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function recordedEnd(attempts, completion) {
|
|
139
|
+
const { start, ends, options } = attempts;
|
|
140
|
+
const { processId, end } = completion;
|
|
141
|
+
const attempt = ends.length;
|
|
142
|
+
const selected = attemptCommand(attempts, attempt);
|
|
143
|
+
const name = `attempt-${String(attempt)}.json`;
|
|
144
|
+
const started = await readRecord(start.folder, name);
|
|
145
|
+
const startedAt = Reflect.get(Object(started), "startedAt");
|
|
146
|
+
if (typeof startedAt !== "string")
|
|
147
|
+
throw new Error("attempt-start-time-missing");
|
|
148
|
+
await written(start.folder, {
|
|
149
|
+
name,
|
|
150
|
+
value: {
|
|
151
|
+
...options.metadata,
|
|
152
|
+
...attemptUsage(attempts, { end, attempt }),
|
|
153
|
+
...(selected.fallbackReason === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { fallbackReason: selected.fallbackReason }),
|
|
156
|
+
attempt,
|
|
157
|
+
processId,
|
|
158
|
+
startedAt,
|
|
159
|
+
endedAt: new Date().toISOString(),
|
|
160
|
+
exitCode: end.exitCode,
|
|
161
|
+
capacity: capacityRefusal(end.result),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function waited(attempts, milliseconds) {
|
|
166
|
+
const { start, options, stopping, ends } = attempts;
|
|
167
|
+
await written(start.folder, {
|
|
168
|
+
name: "started.json",
|
|
169
|
+
value: {
|
|
170
|
+
...options.metadata,
|
|
171
|
+
startedAt: attempts.startedAt,
|
|
172
|
+
attempt: ends.length,
|
|
173
|
+
phase: "waiting",
|
|
174
|
+
processId: null,
|
|
175
|
+
waitMilliseconds: milliseconds,
|
|
176
|
+
waitingAt: new Date().toISOString(),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const wait = options.retryDelay ??
|
|
180
|
+
((duration, signal) => delay(duration, undefined, { signal }));
|
|
181
|
+
await wait(milliseconds, stopping.signal).catch((error) => {
|
|
182
|
+
if (!stopping.signal.aborted)
|
|
183
|
+
throw error;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function finished(attempts, first) {
|
|
187
|
+
const { ends } = attempts;
|
|
188
|
+
let turn = first;
|
|
189
|
+
while (turn !== undefined) {
|
|
190
|
+
const end = await turn.ended;
|
|
191
|
+
ends.push(end);
|
|
192
|
+
await recordedEnd(attempts, { processId: turn.processId, end });
|
|
193
|
+
const milliseconds = waits[ends.length - 1];
|
|
194
|
+
if (cancelled(attempts) ||
|
|
195
|
+
!capacityRefusal(end.result) ||
|
|
196
|
+
milliseconds === undefined)
|
|
197
|
+
break;
|
|
198
|
+
await waited(attempts, milliseconds);
|
|
199
|
+
if (cancelled(attempts))
|
|
200
|
+
return { ...combined(attempts), stopped: true };
|
|
201
|
+
turn = await launched(attempts);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
...combined(attempts),
|
|
205
|
+
...(cancelled(attempts) || turn === undefined ? { stopped: true } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function cancelled(attempts) {
|
|
209
|
+
return attempts.stopping.signal.aborted;
|
|
210
|
+
}
|
|
211
|
+
export async function startRetriedTurn(start, options) {
|
|
212
|
+
const attempts = {
|
|
213
|
+
start,
|
|
214
|
+
startedAt: new Date().toISOString(),
|
|
215
|
+
options,
|
|
216
|
+
stopping: new AbortController(),
|
|
217
|
+
ends: [],
|
|
218
|
+
grace: { milliseconds: 2_000 },
|
|
219
|
+
};
|
|
220
|
+
const turn = await launched(attempts);
|
|
221
|
+
if (turn === undefined)
|
|
222
|
+
throw new Error("turn-start-stopped");
|
|
223
|
+
return {
|
|
224
|
+
processId: turn.processId,
|
|
225
|
+
ended: finished(attempts, turn),
|
|
226
|
+
stop: (graceMilliseconds) => {
|
|
227
|
+
attempts.grace.milliseconds = graceMilliseconds;
|
|
228
|
+
attempts.stopping.abort();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -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;
|