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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { field, githubTiming } from "./github-operation.js";
|
|
3
|
+
function transient(error) {
|
|
4
|
+
if (!(error instanceof Error) || error.message !== "github-call-failed")
|
|
5
|
+
return false;
|
|
6
|
+
const detail = field(error, "detail");
|
|
7
|
+
if (typeof detail !== "string")
|
|
8
|
+
return false;
|
|
9
|
+
const status = field(error, "status");
|
|
10
|
+
if ([500, 502, 503, 504].includes(Number(status)))
|
|
11
|
+
return true;
|
|
12
|
+
return ((status === undefined ||
|
|
13
|
+
(typeof status === "number" && Number.isNaN(status))) &&
|
|
14
|
+
/TLS handshake timeout|connection reset|i\/o timeout|\bEOF\b/i.test(detail));
|
|
15
|
+
}
|
|
16
|
+
function retryFactor(error, retry) {
|
|
17
|
+
const network = transient(error);
|
|
18
|
+
if (!network && !retry.statuses.includes(Number(field(error, "status"))))
|
|
19
|
+
throw error;
|
|
20
|
+
if (retry.attempt < 2)
|
|
21
|
+
return network ? retry.attempt + 1 : 1;
|
|
22
|
+
if (!network)
|
|
23
|
+
throw error;
|
|
24
|
+
throw Object.assign(new Error("github-network-failed", { cause: error }), {
|
|
25
|
+
detail: field(error, "detail"),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function retryGithub(operation, retry) {
|
|
29
|
+
const timing = operation.settings.timing ?? githubTiming;
|
|
30
|
+
for (let attempt = 0;; attempt += 1) {
|
|
31
|
+
operation.signal.throwIfAborted();
|
|
32
|
+
try {
|
|
33
|
+
return await retry.action(attempt);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
operation.signal.throwIfAborted();
|
|
37
|
+
const factor = retryFactor(error, {
|
|
38
|
+
attempt,
|
|
39
|
+
statuses: retry.statuses ?? [],
|
|
40
|
+
});
|
|
41
|
+
await delay(timing.pollMilliseconds * factor, undefined, {
|
|
42
|
+
signal: operation.signal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function githubRead(operation, parts) {
|
|
48
|
+
return retryGithub(operation, { action: () => operation.calls.gh(parts) });
|
|
49
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { issuePrivacy } from "./github-issue-privacy.js";
|
|
2
|
+
import { field, text } from "./github-operation.js";
|
|
3
|
+
import { ticketLimits } from "./ticket-source.js";
|
|
4
|
+
const privateRemaining = "Remaining work is in the private run report.";
|
|
5
|
+
const issueBodyBytes = ticketLimits.textBytes - 2_048;
|
|
6
|
+
const workflowLabels = [
|
|
7
|
+
"cloving: ready",
|
|
8
|
+
"cloving: blocked",
|
|
9
|
+
"cloving: in progress",
|
|
10
|
+
];
|
|
11
|
+
function positiveInteger(operation, name) {
|
|
12
|
+
const value = operation.payload[name];
|
|
13
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
14
|
+
throw new Error("invalid-payload");
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function issuePath(operation) {
|
|
18
|
+
const repository = `${text(operation, "owner")}/${text(operation, "repository")}`;
|
|
19
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repository))
|
|
20
|
+
throw new Error("invalid-payload");
|
|
21
|
+
return `repos/${repository}/issues/${String(positiveInteger(operation, "number"))}`;
|
|
22
|
+
}
|
|
23
|
+
function labelsOf(issue) {
|
|
24
|
+
const labels = field(issue, "labels");
|
|
25
|
+
if (!Array.isArray(labels))
|
|
26
|
+
throw new Error("github-call-failed");
|
|
27
|
+
return labels.map((item) => {
|
|
28
|
+
const name = typeof item === "string" ? item : field(item, "name");
|
|
29
|
+
if (typeof name !== "string")
|
|
30
|
+
throw new Error("github-call-failed");
|
|
31
|
+
return name;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function readIssue(operation, path) {
|
|
35
|
+
const issue = JSON.parse(await operation.calls.gh(["api", path]));
|
|
36
|
+
const body = field(issue, "body");
|
|
37
|
+
if ((body !== null && typeof body !== "string") ||
|
|
38
|
+
field(issue, "number") !== positiveInteger(operation, "number") ||
|
|
39
|
+
!["open", "closed"].includes(String(field(issue, "state"))) ||
|
|
40
|
+
field(issue, "pull_request") !== undefined)
|
|
41
|
+
throw new Error("github-call-failed");
|
|
42
|
+
return {
|
|
43
|
+
state: field(issue, "state"),
|
|
44
|
+
reason: field(issue, "state_reason"),
|
|
45
|
+
body: typeof body === "string" ? body : "",
|
|
46
|
+
labels: labelsOf(issue),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function isPositiveInteger(value) {
|
|
50
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
51
|
+
}
|
|
52
|
+
function isRemaining(value) {
|
|
53
|
+
return (Array.isArray(value) &&
|
|
54
|
+
value.length > 0 &&
|
|
55
|
+
value.every((item) => typeof item === "string" && item.trim() !== "" && !/[\r\n]/.test(item)));
|
|
56
|
+
}
|
|
57
|
+
function isPartialRun(value) {
|
|
58
|
+
return (isPositiveInteger(field(value, "pullRequest")) &&
|
|
59
|
+
isRemaining(field(value, "remaining")));
|
|
60
|
+
}
|
|
61
|
+
function historyOf(json) {
|
|
62
|
+
try {
|
|
63
|
+
const value = JSON.parse(json);
|
|
64
|
+
const partialRuns = field(value, "partialRuns");
|
|
65
|
+
const completedPullRequest = field(value, "completedPullRequest");
|
|
66
|
+
if (field(value, "version") !== 1 ||
|
|
67
|
+
!Array.isArray(partialRuns) ||
|
|
68
|
+
!partialRuns.every(isPartialRun) ||
|
|
69
|
+
new Set(partialRuns.map((run) => run.pullRequest)).size !==
|
|
70
|
+
partialRuns.length ||
|
|
71
|
+
(completedPullRequest !== undefined &&
|
|
72
|
+
!isPositiveInteger(completedPullRequest)))
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
partialRuns,
|
|
77
|
+
...(completedPullRequest === undefined ? {} : { completedPullRequest }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function statusText(history) {
|
|
85
|
+
const lines = history.partialRuns.map((run) => `- Pull request #${String(run.pullRequest)}: partial. Remaining: ${run.remaining.join("; ")}`);
|
|
86
|
+
if (history.completedPullRequest !== undefined)
|
|
87
|
+
lines.push(`Completed in pull request #${String(history.completedPullRequest)}.`);
|
|
88
|
+
return `## Cloving status\n${lines.join("\n")}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
function legacyFence(line) {
|
|
91
|
+
const marker = /^( {0,3}(?:[-+*]|\d{1,9}[.)])[ \t]+)? {0,3}(`{3,}|~{3,})/.exec(line);
|
|
92
|
+
if (marker === null || marker[2] === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const delimiter = marker[2];
|
|
95
|
+
if (delimiter.startsWith("`") && line.slice(marker[0].length).includes("`"))
|
|
96
|
+
return undefined;
|
|
97
|
+
const indentation = (marker[1]?.length ?? 0) + 3;
|
|
98
|
+
return new RegExp(`^ {0,${String(indentation)}}${delimiter.charAt(0)}{${String(delimiter.length)},}[ \\t]*\\r?\\n?$`);
|
|
99
|
+
}
|
|
100
|
+
function legacyStatusStart(body) {
|
|
101
|
+
let fence;
|
|
102
|
+
let offset = 0;
|
|
103
|
+
for (const line of body.split(/(?<=\n)/)) {
|
|
104
|
+
const start = offset;
|
|
105
|
+
offset += line.length;
|
|
106
|
+
if (fence !== undefined) {
|
|
107
|
+
fence = fence.test(line) ? undefined : fence;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (line.trimEnd() === "## Cloving status")
|
|
111
|
+
return start;
|
|
112
|
+
fence = legacyFence(line);
|
|
113
|
+
}
|
|
114
|
+
return body.length;
|
|
115
|
+
}
|
|
116
|
+
function statusSection(body) {
|
|
117
|
+
const empty = { version: 1, partialRuns: [] };
|
|
118
|
+
const marker = /^<!-- cloving-status(?=\s|-->|$)/gm;
|
|
119
|
+
const start = [...body.matchAll(marker)].at(-1)?.index;
|
|
120
|
+
if (start === undefined)
|
|
121
|
+
return { outside: body.slice(0, legacyStatusStart(body)), history: empty };
|
|
122
|
+
const tail = body.slice(start);
|
|
123
|
+
const end = tail.indexOf("-->");
|
|
124
|
+
if (end !== -1 && tail.slice(end + 3).trim() !== "")
|
|
125
|
+
return { outside: body, history: empty };
|
|
126
|
+
const outside = body.slice(0, start);
|
|
127
|
+
if (end === -1)
|
|
128
|
+
return { outside, history: empty };
|
|
129
|
+
const history = historyOf(tail.slice("<!-- cloving-status".length, end).trim());
|
|
130
|
+
if (history === undefined)
|
|
131
|
+
return { outside, history: empty };
|
|
132
|
+
const section = statusText(history);
|
|
133
|
+
return {
|
|
134
|
+
outside: outside.endsWith(section)
|
|
135
|
+
? outside.slice(0, -section.length)
|
|
136
|
+
: outside,
|
|
137
|
+
history,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function withStatus(outside, history) {
|
|
141
|
+
const separator = "\n".repeat(Math.max(0, 2 - (outside.match(/\n*$/)?.[0].length ?? 0)));
|
|
142
|
+
const json = JSON.stringify(history)
|
|
143
|
+
.replace(/--/g, "\\u002d\\u002d")
|
|
144
|
+
.replace(/>/g, "\\u003e");
|
|
145
|
+
return `${outside}${outside === "" ? "" : separator}${statusText(history)}<!-- cloving-status\n${json}\n-->\n`;
|
|
146
|
+
}
|
|
147
|
+
async function writeIssue(operation, update) {
|
|
148
|
+
const { path, body, labels } = update;
|
|
149
|
+
const latest = await readIssue(operation, path);
|
|
150
|
+
if (latest.body !== update.originalBody)
|
|
151
|
+
throw new Error("github-issue-conflict");
|
|
152
|
+
const fields = update.fields ?? [];
|
|
153
|
+
if (body !== latest.body || fields.length > 0)
|
|
154
|
+
await operation.calls.gh([
|
|
155
|
+
"api",
|
|
156
|
+
"-X",
|
|
157
|
+
"PATCH",
|
|
158
|
+
path,
|
|
159
|
+
"-f",
|
|
160
|
+
`body=${body}`,
|
|
161
|
+
...fields,
|
|
162
|
+
]);
|
|
163
|
+
const add = workflowLabels.filter((name) => labels.includes(name) && !latest.labels.includes(name));
|
|
164
|
+
const remove = workflowLabels.filter((name) => !labels.includes(name) && latest.labels.includes(name));
|
|
165
|
+
if (add.length === 0 && remove.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
await operation.calls.gh([
|
|
168
|
+
"issue",
|
|
169
|
+
"edit",
|
|
170
|
+
String(positiveInteger(operation, "number")),
|
|
171
|
+
"--repo",
|
|
172
|
+
`${text(operation, "owner")}/${text(operation, "repository")}`,
|
|
173
|
+
...add.flatMap((name) => ["--add-label", name]),
|
|
174
|
+
...remove.flatMap((name) => ["--remove-label", name]),
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
function sameLabels(first, second) {
|
|
178
|
+
return JSON.stringify(first.toSorted()) === JSON.stringify(second.toSorted());
|
|
179
|
+
}
|
|
180
|
+
export async function closeIssue(operation) {
|
|
181
|
+
const path = issuePath(operation);
|
|
182
|
+
const pull = positiveInteger(operation, "pullRequest");
|
|
183
|
+
const issue = await readIssue(operation, path);
|
|
184
|
+
const section = statusSection(issue.body);
|
|
185
|
+
const body = withStatus(section.outside, {
|
|
186
|
+
...section.history,
|
|
187
|
+
completedPullRequest: pull,
|
|
188
|
+
});
|
|
189
|
+
const labels = issue.labels.filter((name) => !["cloving: ready", "cloving: blocked", "cloving: in progress"].includes(name));
|
|
190
|
+
if (body === issue.body &&
|
|
191
|
+
sameLabels(issue.labels, labels) &&
|
|
192
|
+
issue.state === "closed" &&
|
|
193
|
+
issue.reason === "completed")
|
|
194
|
+
return {
|
|
195
|
+
number: positiveInteger(operation, "number"),
|
|
196
|
+
ticketStatus: "complete",
|
|
197
|
+
};
|
|
198
|
+
await writeIssue(operation, {
|
|
199
|
+
path,
|
|
200
|
+
originalBody: issue.body,
|
|
201
|
+
body,
|
|
202
|
+
labels,
|
|
203
|
+
fields: issue.state === "closed" && issue.reason === "completed"
|
|
204
|
+
? []
|
|
205
|
+
: ["-f", "state=closed", "-f", "state_reason=completed"],
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
number: positiveInteger(operation, "number"),
|
|
209
|
+
ticketStatus: "complete",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function remainingItems(operation) {
|
|
213
|
+
const items = operation.payload["remaining"];
|
|
214
|
+
if (!Array.isArray(items) ||
|
|
215
|
+
items.length === 0 ||
|
|
216
|
+
items.some((item) => typeof item !== "string" || item.trim() === "" || /[\r\n]/.test(item)))
|
|
217
|
+
throw new Error("invalid-payload");
|
|
218
|
+
return items.map(String);
|
|
219
|
+
}
|
|
220
|
+
function publicRemaining(operation) {
|
|
221
|
+
const remaining = remainingItems(operation);
|
|
222
|
+
try {
|
|
223
|
+
issuePrivacy(remaining.join("; "), {
|
|
224
|
+
settings: operation.settings,
|
|
225
|
+
repository: `${text(operation, "owner")}/${text(operation, "repository")}`,
|
|
226
|
+
});
|
|
227
|
+
return remaining;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return [privateRemaining];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function partialHistory(section, status) {
|
|
234
|
+
const recorded = section.history.partialRuns.some((run) => run.pullRequest === status.pull);
|
|
235
|
+
for (const remaining of [status.remaining, [privateRemaining]]) {
|
|
236
|
+
const history = {
|
|
237
|
+
...section.history,
|
|
238
|
+
partialRuns: recorded
|
|
239
|
+
? section.history.partialRuns
|
|
240
|
+
: [
|
|
241
|
+
...section.history.partialRuns,
|
|
242
|
+
{ pullRequest: status.pull, remaining },
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
if (Buffer.byteLength(withStatus(section.outside, history)) <= issueBodyBytes)
|
|
246
|
+
return history;
|
|
247
|
+
}
|
|
248
|
+
throw new Error("github-issue-too-large");
|
|
249
|
+
}
|
|
250
|
+
export async function partialIssue(operation) {
|
|
251
|
+
const path = issuePath(operation);
|
|
252
|
+
const pull = positiveInteger(operation, "pullRequest");
|
|
253
|
+
const remaining = publicRemaining(operation);
|
|
254
|
+
const issue = await readIssue(operation, path);
|
|
255
|
+
const section = statusSection(issue.body);
|
|
256
|
+
const history = partialHistory(section, { pull, remaining });
|
|
257
|
+
const count = history.partialRuns.length;
|
|
258
|
+
const label = count >= 3 ? "cloving: blocked" : "cloving: ready";
|
|
259
|
+
const body = withStatus(section.outside, history);
|
|
260
|
+
const labels = [
|
|
261
|
+
...issue.labels.filter((name) => ![
|
|
262
|
+
"cloving: ready",
|
|
263
|
+
"cloving: blocked",
|
|
264
|
+
"cloving: in progress",
|
|
265
|
+
].includes(name)),
|
|
266
|
+
label,
|
|
267
|
+
];
|
|
268
|
+
if (body === issue.body && sameLabels(issue.labels, labels))
|
|
269
|
+
return {
|
|
270
|
+
number: positiveInteger(operation, "number"),
|
|
271
|
+
ticketStatus: "partial",
|
|
272
|
+
};
|
|
273
|
+
const repository = `${text(operation, "owner")}/${text(operation, "repository")}`;
|
|
274
|
+
await operation.calls.gh([
|
|
275
|
+
"label",
|
|
276
|
+
"create",
|
|
277
|
+
label,
|
|
278
|
+
"--repo",
|
|
279
|
+
repository,
|
|
280
|
+
"--color",
|
|
281
|
+
"ededed",
|
|
282
|
+
"--force",
|
|
283
|
+
]);
|
|
284
|
+
await writeIssue(operation, {
|
|
285
|
+
path,
|
|
286
|
+
originalBody: issue.body,
|
|
287
|
+
body,
|
|
288
|
+
labels,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
number: positiveInteger(operation, "number"),
|
|
292
|
+
ticketStatus: "partial",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { calls } from "./github-calls.js";
|
|
4
|
-
import { methodReasons, methods, } from "./github-methods.js";
|
|
4
|
+
import { methodReasons, methods, ReviewTimeout, } from "./github-methods.js";
|
|
5
5
|
import { interrupted, located, readRecord, replied, reportOf, writeRecord, } from "./operation-journal.js";
|
|
6
6
|
const worker = "github";
|
|
7
7
|
const closing = "github-worker-closed";
|
|
@@ -62,6 +62,7 @@ async function outcome(parts, run) {
|
|
|
62
62
|
return {
|
|
63
63
|
...interrupted(methodReasons.has(reason) ? reason : "github-call-failed"),
|
|
64
64
|
...(typeof detail === "string" ? { detail } : {}),
|
|
65
|
+
...(error instanceof ReviewTimeout ? { result: error.detail } : {}),
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EffectDelivery } from "./delivery-router.ts";
|
|
2
|
+
interface Settings {
|
|
3
|
+
readonly runtimeDirectory: string;
|
|
4
|
+
readonly journal: string;
|
|
5
|
+
readonly document: (digest: string) => Promise<string | undefined>;
|
|
6
|
+
readonly runStatus: (runId: string) => Promise<string | undefined>;
|
|
7
|
+
readonly retryMilliseconds?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function handoffWorker(settings: Settings): EffectDelivery;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { send } from "../cli/client.js";
|
|
3
|
+
import { record } from "../protocol.js";
|
|
4
|
+
import { runIdentity } from "../service/submission-handler.js";
|
|
5
|
+
import { located, readRecord, writeRecord } from "./operation-journal.js";
|
|
6
|
+
const attempts = 10;
|
|
7
|
+
const uncertain = [
|
|
8
|
+
"outcome-unknown",
|
|
9
|
+
"service-unavailable",
|
|
10
|
+
"service-busy",
|
|
11
|
+
"storage-failure",
|
|
12
|
+
"authority-unavailable",
|
|
13
|
+
"resolver-timeout",
|
|
14
|
+
"resolver-failure",
|
|
15
|
+
];
|
|
16
|
+
function own(intent) {
|
|
17
|
+
return (intent.kind === "protected-effect" &&
|
|
18
|
+
intent.capability === "handoff" &&
|
|
19
|
+
intent.method === "run" &&
|
|
20
|
+
intent.target !== undefined &&
|
|
21
|
+
intent.payload !== undefined &&
|
|
22
|
+
intent.leaseGeneration !== undefined);
|
|
23
|
+
}
|
|
24
|
+
function completed(runId) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "completed",
|
|
27
|
+
worker: "handoff",
|
|
28
|
+
receipt: runId,
|
|
29
|
+
result: runId,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function saved(folder) {
|
|
33
|
+
const stored = record(await readRecord(folder, "result.json"));
|
|
34
|
+
if (stored === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (stored["kind"] === "completed" && typeof stored["result"] === "string")
|
|
37
|
+
return completed(stored["result"]);
|
|
38
|
+
if (stored["kind"] === "failed" && typeof stored["reason"] === "string")
|
|
39
|
+
return { kind: "failed", reason: stored["reason"] };
|
|
40
|
+
throw new Error("invalid-handoff-journal");
|
|
41
|
+
}
|
|
42
|
+
async function retained(folder, outcome) {
|
|
43
|
+
if (["completed", "failed"].includes(outcome.kind))
|
|
44
|
+
await writeRecord(folder, {
|
|
45
|
+
name: "result.json",
|
|
46
|
+
value: outcome,
|
|
47
|
+
permissions: "private",
|
|
48
|
+
});
|
|
49
|
+
return outcome;
|
|
50
|
+
}
|
|
51
|
+
function outcome(reply) {
|
|
52
|
+
if (reply.kind !== "error") {
|
|
53
|
+
const runId = reply.result["runId"];
|
|
54
|
+
return reply.result["kind"] === "run-admitted" && typeof runId === "string"
|
|
55
|
+
? completed(runId)
|
|
56
|
+
: { kind: "unknown" };
|
|
57
|
+
}
|
|
58
|
+
if (reply.error.code === "workflow-active")
|
|
59
|
+
return undefined;
|
|
60
|
+
if (reply.error.code === "chain-limit")
|
|
61
|
+
return { kind: "failed", reason: "chain-limit" };
|
|
62
|
+
return uncertain.includes(reply.error.code)
|
|
63
|
+
? { kind: "unknown" }
|
|
64
|
+
: { kind: "failed", reason: "handoff-input-refused" };
|
|
65
|
+
}
|
|
66
|
+
async function input(settings, intent) {
|
|
67
|
+
const text = await settings.document(intent.payload);
|
|
68
|
+
try {
|
|
69
|
+
return record(JSON.parse(text ?? "null"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function submit(settings, call) {
|
|
76
|
+
const { intent, signal } = call;
|
|
77
|
+
if (!own(intent))
|
|
78
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
79
|
+
const { folder } = located(settings.journal, intent.operationId);
|
|
80
|
+
const known = await saved(folder);
|
|
81
|
+
if (known !== undefined)
|
|
82
|
+
return known;
|
|
83
|
+
const submission = await input(settings, intent);
|
|
84
|
+
if (submission === undefined)
|
|
85
|
+
return retained(folder, {
|
|
86
|
+
kind: "failed",
|
|
87
|
+
reason: "handoff-input-refused",
|
|
88
|
+
});
|
|
89
|
+
const previous = await busyAttempts(folder);
|
|
90
|
+
for (let busy = previous; busy < attempts; busy += 1) {
|
|
91
|
+
if (!(await allowed(settings, { intent, signal })))
|
|
92
|
+
return { kind: "unknown" };
|
|
93
|
+
const reply = await submitted(settings, {
|
|
94
|
+
intent,
|
|
95
|
+
submission,
|
|
96
|
+
signal,
|
|
97
|
+
folder,
|
|
98
|
+
});
|
|
99
|
+
if (reply === undefined)
|
|
100
|
+
return { kind: "unknown" };
|
|
101
|
+
const result = outcome(reply);
|
|
102
|
+
if (result !== undefined)
|
|
103
|
+
return retained(folder, result);
|
|
104
|
+
await writeRecord(folder, {
|
|
105
|
+
name: "attempts.json",
|
|
106
|
+
value: { busy: busy + 1 },
|
|
107
|
+
permissions: "private",
|
|
108
|
+
});
|
|
109
|
+
if (busy + 1 < attempts)
|
|
110
|
+
await delay(settings.retryMilliseconds ?? 1000, undefined, {
|
|
111
|
+
signal,
|
|
112
|
+
}).catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
return retained(folder, { kind: "failed", reason: "handoff-start-timeout" });
|
|
115
|
+
}
|
|
116
|
+
async function submitted(settings, call) {
|
|
117
|
+
const { intent, submission, folder, signal } = call;
|
|
118
|
+
const marker = (status) => writeRecord(folder, {
|
|
119
|
+
name: "submission.json",
|
|
120
|
+
value: { processId: process.pid, pending: status === "pending" },
|
|
121
|
+
permissions: "private",
|
|
122
|
+
});
|
|
123
|
+
await marker("pending");
|
|
124
|
+
if (!(await allowed(settings, { intent, signal }))) {
|
|
125
|
+
await marker("settled");
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const reply = await send(settings.runtimeDirectory, {
|
|
129
|
+
operation: "run",
|
|
130
|
+
parameters: {
|
|
131
|
+
workflowId: intent.target,
|
|
132
|
+
source: "input",
|
|
133
|
+
submission,
|
|
134
|
+
commandId: `handoff:${intent.operationId}`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (reply.kind !== "error" || reply.error.code !== "outcome-unknown")
|
|
138
|
+
await marker("settled");
|
|
139
|
+
return reply;
|
|
140
|
+
}
|
|
141
|
+
async function allowed(settings, call) {
|
|
142
|
+
try {
|
|
143
|
+
call.signal.throwIfAborted();
|
|
144
|
+
const status = await settings.runStatus(call.intent.runId);
|
|
145
|
+
call.signal.throwIfAborted();
|
|
146
|
+
return status === "running";
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function inFlight(folder) {
|
|
153
|
+
const marker = record(await readRecord(folder, "submission.json"));
|
|
154
|
+
if (marker?.["pending"] !== true)
|
|
155
|
+
return false;
|
|
156
|
+
const pid = marker["processId"];
|
|
157
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
|
|
158
|
+
throw new Error("invalid-handoff-journal");
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return !(error instanceof Error &&
|
|
165
|
+
"code" in error &&
|
|
166
|
+
error.code === "ESRCH");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function busyAttempts(folder) {
|
|
170
|
+
const previous = record(await readRecord(folder, "attempts.json"))?.["busy"] ?? 0;
|
|
171
|
+
if (typeof previous !== "number" ||
|
|
172
|
+
!Number.isSafeInteger(previous) ||
|
|
173
|
+
previous < 0 ||
|
|
174
|
+
previous > attempts)
|
|
175
|
+
throw new Error("invalid-handoff-journal");
|
|
176
|
+
return previous;
|
|
177
|
+
}
|
|
178
|
+
async function observed(settings, intent) {
|
|
179
|
+
if (!own(intent))
|
|
180
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
181
|
+
const known = await saved(located(settings.journal, intent.operationId).folder);
|
|
182
|
+
if (known !== undefined)
|
|
183
|
+
return known;
|
|
184
|
+
const reply = await send(settings.runtimeDirectory, {
|
|
185
|
+
operation: "status",
|
|
186
|
+
parameters: { runId: runIdentity(`handoff:${intent.operationId}`) },
|
|
187
|
+
});
|
|
188
|
+
if (reply.kind === "error")
|
|
189
|
+
return reply.error.code === "unknown-run"
|
|
190
|
+
? {
|
|
191
|
+
kind: (await inFlight(located(settings.journal, intent.operationId).folder))
|
|
192
|
+
? "unknown"
|
|
193
|
+
: "absent",
|
|
194
|
+
}
|
|
195
|
+
: { kind: "unknown" };
|
|
196
|
+
const runId = record(reply.result["run"])?.["runId"];
|
|
197
|
+
return typeof runId === "string" ? completed(runId) : { kind: "unknown" };
|
|
198
|
+
}
|
|
199
|
+
export function handoffWorker(settings) {
|
|
200
|
+
const pending = new Map();
|
|
201
|
+
const deliver = (intent, signal) => {
|
|
202
|
+
const known = pending.get(intent.operationId);
|
|
203
|
+
if (known !== undefined)
|
|
204
|
+
return known.result;
|
|
205
|
+
const stopping = new AbortController();
|
|
206
|
+
const result = submit(settings, {
|
|
207
|
+
intent,
|
|
208
|
+
signal: AbortSignal.any([signal, stopping.signal]),
|
|
209
|
+
}).finally(() => pending.delete(intent.operationId));
|
|
210
|
+
pending.set(intent.operationId, { stopping, result });
|
|
211
|
+
return result;
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
deliver,
|
|
215
|
+
reconcile: (intent) => observed(settings, intent),
|
|
216
|
+
retry: async (intent, signal) => {
|
|
217
|
+
if (!own(intent))
|
|
218
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
219
|
+
const { folder } = located(settings.journal, intent.operationId);
|
|
220
|
+
if (!(await inFlight(folder)))
|
|
221
|
+
return { kind: "unknown" };
|
|
222
|
+
return deliver(intent, signal);
|
|
223
|
+
},
|
|
224
|
+
stop: async (intent) => {
|
|
225
|
+
const active = pending.get(intent.operationId);
|
|
226
|
+
active?.stopping.abort();
|
|
227
|
+
await active?.result;
|
|
228
|
+
const known = await observed(settings, intent);
|
|
229
|
+
return known.kind === "absent" ? { kind: "stopped" } : known;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -2,6 +2,8 @@ interface QueueSettings {
|
|
|
2
2
|
readonly directory: string;
|
|
3
3
|
readonly runtimeDirectory: string;
|
|
4
4
|
readonly intervalMilliseconds: number;
|
|
5
|
+
readonly lanes?: number;
|
|
6
|
+
readonly resetQueue?: boolean;
|
|
5
7
|
}
|
|
6
8
|
export declare function issueQueue(settings: QueueSettings): {
|
|
7
9
|
readonly stop: () => Promise<void>;
|