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,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { admitCompiled } from "../cli/validate.js";
|
|
3
3
|
import { ticketCommandText, ticketText } from "./ticket-source.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { bundledWorkflowIds } from "../workflow-discovery.js";
|
|
5
|
+
import { declaredManifest } from "../execution-pin.js";
|
|
6
|
+
import { record } from "../protocol.js";
|
|
7
|
+
import { issueIdentity } from "../issue-identity.js";
|
|
8
|
+
import { ticketDependencyWaits, TicketDependencyReadError, } from "./ticket-dependency-waits.js";
|
|
9
|
+
export const ticketWorkflow = bundledWorkflowIds.ticket;
|
|
10
|
+
export const promptWorkflow = bundledWorkflowIds.prompt;
|
|
11
|
+
export const issueWorkflow = bundledWorkflowIds.issue;
|
|
7
12
|
export const ticketCapabilities = ["claude", "github"];
|
|
8
13
|
const ticketIdentifier = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
9
14
|
function rejected(code, detail) {
|
|
@@ -38,7 +43,15 @@ async function ticket(source, request) {
|
|
|
38
43
|
async function nextTicket(source, request) {
|
|
39
44
|
if (source.nextCommand === undefined)
|
|
40
45
|
return rejected("next-source-not-configured", "The daemon has no next-ticket command. Set CLOVING_NEXT_COMMAND.");
|
|
41
|
-
const selected = await ticketCommandText(source.nextCommand,
|
|
46
|
+
const selected = await ticketCommandText(source.nextCommand, {
|
|
47
|
+
signal: request.signal,
|
|
48
|
+
skipTickets: [
|
|
49
|
+
...new Set([
|
|
50
|
+
...(request.activeTickets ?? []).flatMap((input) => typeof input["ticketId"] === "string" ? [input["ticketId"]] : []),
|
|
51
|
+
...(source.skipTickets ?? []),
|
|
52
|
+
]),
|
|
53
|
+
].sort(),
|
|
54
|
+
});
|
|
42
55
|
if (selected.kind === "failed")
|
|
43
56
|
return rejected("next-ticket-not-available", selected.detail);
|
|
44
57
|
if (selected.text === "")
|
|
@@ -55,18 +68,43 @@ export function ticketResolver(source) {
|
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
export function issueResolver(source) {
|
|
71
|
+
const waits = ticketDependencyWaits(source);
|
|
58
72
|
return {
|
|
59
73
|
id: "issue-command",
|
|
60
74
|
version: "1",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
skippedTickets: waits.skipped,
|
|
76
|
+
resolve: (request) => issueTicket({ source, waits }, request).catch((error) => {
|
|
77
|
+
if (!(error instanceof TicketDependencyReadError))
|
|
78
|
+
throw error;
|
|
79
|
+
return rejected("ticket-not-available", "The ticket dependencies could not be read. Retry this command.");
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function issueTicket(context, request) {
|
|
84
|
+
const { source, waits } = context;
|
|
85
|
+
const skipTickets = request.submission["ticket"] === "next"
|
|
86
|
+
? await waits.refresh(request.signal)
|
|
87
|
+
: [];
|
|
88
|
+
const resolution = await ticket({ ...source, skipTickets }, request);
|
|
89
|
+
if (resolution.kind === "rejected")
|
|
90
|
+
return resolution;
|
|
91
|
+
const input = record(resolution.input) ?? {};
|
|
92
|
+
const identity = issueIdentity(input["ticket"]);
|
|
93
|
+
if ((request.activeTickets ?? []).some((active) => active["ticketId"] === input["ticketId"] ||
|
|
94
|
+
(identity !== null &&
|
|
95
|
+
JSON.stringify(issueIdentity(active["ticket"])) ===
|
|
96
|
+
JSON.stringify(identity))))
|
|
97
|
+
return rejected("ticket-active", "An active run already works on this ticket.");
|
|
98
|
+
const dependencies = await waits.check({
|
|
99
|
+
ticketId: String(input["ticketId"]),
|
|
100
|
+
ticket: String(input["ticket"]),
|
|
101
|
+
signal: request.signal,
|
|
102
|
+
});
|
|
103
|
+
if (dependencies.length > 0)
|
|
104
|
+
return rejected("ticket-dependency-open", `Ticket ${String(input["ticketId"])} needs open issues: ${dependencies.map((id) => `#${id}`).join(", ")}.`);
|
|
105
|
+
return {
|
|
106
|
+
kind: "resolved",
|
|
107
|
+
input: { ...input, delivery: "pull-request" },
|
|
70
108
|
};
|
|
71
109
|
}
|
|
72
110
|
export function promptResolver(repository) {
|
|
@@ -89,6 +127,7 @@ export function ticketRegistrationTable(source, declarations = []) {
|
|
|
89
127
|
workflowId: ticketWorkflow,
|
|
90
128
|
functionName: "developTicket",
|
|
91
129
|
effectLimit: 8,
|
|
130
|
+
handoffs: [],
|
|
92
131
|
submission: { maximumBytes: 4_096, fields: { ticket: "string" } },
|
|
93
132
|
resolver: ticketResolver(source),
|
|
94
133
|
},
|
|
@@ -96,13 +135,15 @@ export function ticketRegistrationTable(source, declarations = []) {
|
|
|
96
135
|
workflowId: issueWorkflow,
|
|
97
136
|
functionName: "developTicket",
|
|
98
137
|
effectLimit: 20,
|
|
138
|
+
handoffs: [],
|
|
99
139
|
submission: { maximumBytes: 4_096, fields: { ticket: "string" } },
|
|
100
140
|
resolver: issueResolver(source),
|
|
101
141
|
},
|
|
102
142
|
{
|
|
103
|
-
workflowId:
|
|
143
|
+
workflowId: bundledWorkflowIds.prd,
|
|
104
144
|
functionName: "developPrd",
|
|
105
145
|
effectLimit: 8,
|
|
146
|
+
handoffs: [issueWorkflow],
|
|
106
147
|
submission: { maximumBytes: 65_536, fields: { prompt: "string" } },
|
|
107
148
|
resolver: promptResolver(source.repository),
|
|
108
149
|
},
|
|
@@ -110,6 +151,7 @@ export function ticketRegistrationTable(source, declarations = []) {
|
|
|
110
151
|
workflowId: promptWorkflow,
|
|
111
152
|
functionName: "developTicket",
|
|
112
153
|
effectLimit: 8,
|
|
154
|
+
handoffs: [],
|
|
113
155
|
submission: { maximumBytes: 65_536, fields: { prompt: "string" } },
|
|
114
156
|
resolver: promptResolver(source.repository),
|
|
115
157
|
},
|
|
@@ -119,6 +161,12 @@ export function ticketRegistrationTable(source, declarations = []) {
|
|
|
119
161
|
effectLimit: declaration.effectLimit,
|
|
120
162
|
capabilities: declaration.capabilities,
|
|
121
163
|
submission: declaration.input,
|
|
164
|
+
...(declaration.stages === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { stages: declaration.stages }),
|
|
167
|
+
...(declaration.handoffs === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { handoffs: declaration.handoffs }),
|
|
122
170
|
resolver: declaredResolver(source.repository),
|
|
123
171
|
})),
|
|
124
172
|
];
|
|
@@ -147,17 +195,26 @@ export async function ticketRegistrations(manifests, options) {
|
|
|
147
195
|
};
|
|
148
196
|
ids.add(row.workflowId);
|
|
149
197
|
}
|
|
198
|
+
for (const row of options.table) {
|
|
199
|
+
const unknown = row.handoffs?.find((target) => !ids.has(target));
|
|
200
|
+
if (unknown !== undefined)
|
|
201
|
+
return {
|
|
202
|
+
kind: "rejected",
|
|
203
|
+
detail: `unknown-handoff-target: ${row.workflowId} -> ${unknown}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
150
206
|
const registrations = [];
|
|
151
207
|
for (const row of options.table) {
|
|
152
|
-
const
|
|
208
|
+
const { functionName, capabilities = ticketCapabilities, ...registration } = row;
|
|
209
|
+
const manifest = manifests[functionName];
|
|
153
210
|
if (manifest === undefined)
|
|
154
211
|
return {
|
|
155
212
|
kind: "rejected",
|
|
156
|
-
detail: `Missing manifest for workflow function ${
|
|
213
|
+
detail: `Missing manifest for workflow function ${functionName}.`,
|
|
157
214
|
};
|
|
158
215
|
const admission = await admitCompiled(manifest, {
|
|
159
216
|
file: manifest,
|
|
160
|
-
capabilities
|
|
217
|
+
capabilities,
|
|
161
218
|
});
|
|
162
219
|
if (admission.kind === "rejected")
|
|
163
220
|
return {
|
|
@@ -165,12 +222,9 @@ export async function ticketRegistrations(manifests, options) {
|
|
|
165
222
|
detail: `${admission.error.code}: ${admission.error.detail}`,
|
|
166
223
|
};
|
|
167
224
|
registrations.push({
|
|
168
|
-
|
|
169
|
-
manifest: admission.manifest,
|
|
225
|
+
...registration,
|
|
226
|
+
manifest: declaredManifest(admission.manifest, row),
|
|
170
227
|
availability: "available",
|
|
171
|
-
effectLimit: row.effectLimit,
|
|
172
|
-
submission: row.submission,
|
|
173
|
-
resolver: row.resolver,
|
|
174
228
|
});
|
|
175
229
|
}
|
|
176
230
|
return { kind: "admitted", registrations };
|
|
@@ -14,4 +14,7 @@ export interface TicketRequest {
|
|
|
14
14
|
readonly signal: AbortSignal;
|
|
15
15
|
}
|
|
16
16
|
export declare function ticketText(command: readonly string[], request: TicketRequest): Promise<TicketRead>;
|
|
17
|
-
export declare function ticketCommandText(command: readonly string[],
|
|
17
|
+
export declare function ticketCommandText(command: readonly string[], request: AbortSignal | {
|
|
18
|
+
readonly signal: AbortSignal;
|
|
19
|
+
readonly skipTickets: readonly string[];
|
|
20
|
+
}): Promise<TicketRead>;
|
|
@@ -26,10 +26,16 @@ export function ticketText(command, request) {
|
|
|
26
26
|
const [executable = "", ...fixed] = command;
|
|
27
27
|
return ticketCommandText([executable, ...fixed, request.ticketId], request.signal).then((result) => result.kind === "failed" ? result : printed(result.text));
|
|
28
28
|
}
|
|
29
|
-
export function ticketCommandText(command,
|
|
29
|
+
export function ticketCommandText(command, request) {
|
|
30
30
|
const [executable = "", ...fixed] = command;
|
|
31
31
|
return executed(executable, fixed, {
|
|
32
|
-
signal,
|
|
32
|
+
signal: request instanceof AbortSignal ? request : request.signal,
|
|
33
|
+
env: request instanceof AbortSignal
|
|
34
|
+
? process.env
|
|
35
|
+
: {
|
|
36
|
+
...process.env,
|
|
37
|
+
CLOVING_SKIP_TICKETS: request.skipTickets.join(" "),
|
|
38
|
+
},
|
|
33
39
|
maxBuffer: 4 * ticketLimits.textBytes,
|
|
34
40
|
encoding: "utf8",
|
|
35
41
|
}).then((output) => ({ kind: "read", text: output.stdout.trim() }), ended);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { AdmissionResult, ExecutionManifest } from "./workflow-admission.ts";
|
|
2
|
+
export declare function executionCapabilities(manifest: ExecutionManifest): string;
|
|
2
3
|
export declare function pinnedIdentity(manifest: ExecutionManifest): string;
|
|
3
4
|
export declare function resumePinned(pinned: ExecutionManifest, available: ExecutionManifest): AdmissionResult;
|
|
4
5
|
export declare function readExecutionManifest(value: unknown): ExecutionManifest;
|
|
6
|
+
export declare function declaredManifest(manifest: ExecutionManifest, declaration: {
|
|
7
|
+
readonly handoffs?: readonly string[];
|
|
8
|
+
}): ExecutionManifest;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { isWorkflowHandoffs } from "./workflow-declaration.js";
|
|
1
2
|
const canonical = {
|
|
2
3
|
name: (manifest) => JSON.stringify(manifest.name),
|
|
3
4
|
workflowId: (manifest) => JSON.stringify(manifest.workflowId),
|
|
4
5
|
steps: (manifest) => JSON.stringify([...manifest.steps].sort()),
|
|
5
6
|
capabilities: (manifest) => JSON.stringify([...manifest.capabilities].sort()),
|
|
7
|
+
handoffs: (manifest) => JSON.stringify([...(manifest.handoffs ?? [])].sort()),
|
|
6
8
|
artifacts: (manifest) => JSON.stringify(Object.keys(manifest.artifacts)
|
|
7
9
|
.sort()
|
|
8
10
|
.map((name) => [name, manifest.artifacts[name]])),
|
|
@@ -20,11 +22,18 @@ const fields = [
|
|
|
20
22
|
"resources",
|
|
21
23
|
"toolchain",
|
|
22
24
|
];
|
|
25
|
+
export function executionCapabilities(manifest) {
|
|
26
|
+
const handoff = manifest.handoffs?.length ? ["handoff"] : [];
|
|
27
|
+
return JSON.stringify([...new Set([...manifest.capabilities, ...handoff])].sort());
|
|
28
|
+
}
|
|
23
29
|
export function pinnedIdentity(manifest) {
|
|
24
|
-
|
|
30
|
+
const identity = fields.map((field) => canonical[field](manifest));
|
|
31
|
+
if (manifest.handoffs?.length)
|
|
32
|
+
identity.push(canonical.handoffs(manifest));
|
|
33
|
+
return JSON.stringify(identity);
|
|
25
34
|
}
|
|
26
35
|
export function resumePinned(pinned, available) {
|
|
27
|
-
const changed = fields.find((field) => canonical[field](pinned) !== canonical[field](available));
|
|
36
|
+
const changed = [...fields, "handoffs"].find((field) => canonical[field](pinned) !== canonical[field](available));
|
|
28
37
|
if (changed !== undefined)
|
|
29
38
|
return {
|
|
30
39
|
kind: "rejected",
|
|
@@ -36,11 +45,15 @@ export function resumePinned(pinned, available) {
|
|
|
36
45
|
export function readExecutionManifest(value) {
|
|
37
46
|
const fields = record(value);
|
|
38
47
|
const toolchain = record(fields["toolchain"]);
|
|
48
|
+
const handoffs = fields["handoffs"];
|
|
49
|
+
if (Object.hasOwn(fields, "handoffs") && !isWorkflowHandoffs(handoffs))
|
|
50
|
+
throw new Error("invalid-handoffs");
|
|
39
51
|
return {
|
|
40
52
|
name: text(fields["name"]),
|
|
41
53
|
workflowId: text(fields["workflowId"]),
|
|
42
54
|
steps: texts(fields["steps"]),
|
|
43
55
|
capabilities: texts(fields["capabilities"]),
|
|
56
|
+
...(isWorkflowHandoffs(handoffs) ? { handoffs: [...handoffs] } : {}),
|
|
44
57
|
artifacts: textRecord(fields["artifacts"]),
|
|
45
58
|
resources: textRecord(fields["resources"]),
|
|
46
59
|
toolchain: {
|
|
@@ -52,6 +65,14 @@ export function readExecutionManifest(value) {
|
|
|
52
65
|
},
|
|
53
66
|
};
|
|
54
67
|
}
|
|
68
|
+
export function declaredManifest(manifest, declaration) {
|
|
69
|
+
return readExecutionManifest({
|
|
70
|
+
...manifest,
|
|
71
|
+
...(declaration.handoffs === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { handoffs: declaration.handoffs }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
55
76
|
function record(value) {
|
|
56
77
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
57
78
|
return {};
|
|
@@ -9,6 +9,7 @@ export interface RunIntent extends Partial<Continuation> {
|
|
|
9
9
|
readonly kind: string;
|
|
10
10
|
readonly runId: string;
|
|
11
11
|
readonly operationId: string;
|
|
12
|
+
readonly workflow: string;
|
|
12
13
|
}
|
|
13
14
|
export type RunOutcome = {
|
|
14
15
|
readonly kind: "accepted";
|
|
@@ -27,7 +28,7 @@ export interface StartParts {
|
|
|
27
28
|
readonly runs: () => Promise<readonly OperationRun[]>;
|
|
28
29
|
readonly input: (runId: string) => Promise<string | undefined>;
|
|
29
30
|
readonly document: (digest: string) => Promise<string | undefined>;
|
|
30
|
-
readonly workflow: (
|
|
31
|
+
readonly workflow: (intent: RunIntent) => Promise<WorkflowFunction | undefined>;
|
|
31
32
|
readonly cancel: (receipt: string) => Promise<string>;
|
|
32
33
|
}
|
|
33
34
|
export interface StartDelivery {
|
|
@@ -31,7 +31,7 @@ async function continuation(parts, intent) {
|
|
|
31
31
|
}
|
|
32
32
|
async function delivered(parts, intent) {
|
|
33
33
|
const continued = await continuation(parts, intent);
|
|
34
|
-
const workflow = await parts.workflow(intent
|
|
34
|
+
const workflow = await parts.workflow(intent);
|
|
35
35
|
const input = await parts.input(intent.runId);
|
|
36
36
|
if (workflow === undefined || input === undefined)
|
|
37
37
|
throw new Error(`start-not-available: ${intent.runId}`);
|
|
@@ -168,6 +168,9 @@ async function opening(hold, subject) {
|
|
|
168
168
|
kind,
|
|
169
169
|
prompt,
|
|
170
170
|
subject: `sha256:${createHash("sha256").update(prompt).digest("hex")}`,
|
|
171
|
+
...(request.expiresAt === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { expiresAt: request.expiresAt }),
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
async function gate(hold, request) {
|
|
@@ -194,6 +197,19 @@ export function stepBridge(ports) {
|
|
|
194
197
|
return {
|
|
195
198
|
effect: (request) => effect(hold, request),
|
|
196
199
|
gate: (request) => gate(hold, request),
|
|
200
|
+
handoff: (request) => effect(hold, {
|
|
201
|
+
scope: {
|
|
202
|
+
phase: "handoff",
|
|
203
|
+
operation: "handoff",
|
|
204
|
+
round: 1,
|
|
205
|
+
key: createHash("sha256").update(ports.call().stepId).digest("hex"),
|
|
206
|
+
},
|
|
207
|
+
capability: "handoff",
|
|
208
|
+
method: "run",
|
|
209
|
+
target: request.workflowId,
|
|
210
|
+
payload: JSON.stringify(request.input),
|
|
211
|
+
units: 1,
|
|
212
|
+
}),
|
|
197
213
|
};
|
|
198
214
|
}
|
|
199
215
|
export function registerStepBridge(bridge) {
|
|
@@ -3,6 +3,7 @@ import type { AuthorityState } from "../authority/transitions.ts";
|
|
|
3
3
|
export type ReconciledState = Pick<AuthorityState, "runs" | "intents" | "details" | "gates" | "commands">;
|
|
4
4
|
export interface WorkflowOutcome {
|
|
5
5
|
readonly kind: string;
|
|
6
|
+
readonly promotionId?: string;
|
|
6
7
|
readonly reason?: string;
|
|
7
8
|
readonly issueUpdate?: "failed";
|
|
8
9
|
readonly ticketStatus?: "complete" | "partial";
|
|
@@ -14,6 +14,9 @@ async function withOutcome(ports, observed) {
|
|
|
14
14
|
: {
|
|
15
15
|
...world,
|
|
16
16
|
outcome: read.outcome.kind,
|
|
17
|
+
...(read.outcome.promotionId === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: { promotionId: read.outcome.promotionId }),
|
|
17
20
|
...(read.outcome.issueUpdate === "failed"
|
|
18
21
|
? { issueUpdate: read.outcome.issueUpdate }
|
|
19
22
|
: {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isTicketStatus } from "../ticket-status.js";
|
|
2
|
+
import { isPromotionId } from "../workflow-contracts.js";
|
|
2
3
|
import { getRun } from "workflow/api";
|
|
3
4
|
import { getWorld } from "workflow/runtime";
|
|
4
5
|
export const runTag = "cloving.run";
|
|
@@ -47,8 +48,12 @@ export async function workflowOutcome(receipt) {
|
|
|
47
48
|
const reason = Reflect.get(Object(value), "reason");
|
|
48
49
|
const issueUpdate = Reflect.get(Object(value), "issueUpdate");
|
|
49
50
|
const ticketStatus = Reflect.get(Object(value), "ticketStatus");
|
|
51
|
+
const promotionId = Reflect.get(Object(value), "promotionId");
|
|
50
52
|
return {
|
|
51
53
|
kind,
|
|
54
|
+
...(kind === "handoff_requested" && isPromotionId(promotionId)
|
|
55
|
+
? { promotionId }
|
|
56
|
+
: {}),
|
|
52
57
|
...(typeof reason === "string" ? { reason } : {}),
|
|
53
58
|
...(issueUpdate === "failed" ? { issueUpdate } : {}),
|
|
54
59
|
...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function issueIdentity(ticket) {
|
|
2
|
+
if (typeof ticket !== "string")
|
|
3
|
+
return null;
|
|
4
|
+
const markers = ticket
|
|
5
|
+
.split(/\r?\n/)
|
|
6
|
+
.filter((line) => line.startsWith("GitHub issue:"));
|
|
7
|
+
if (markers.length !== 1)
|
|
8
|
+
return null;
|
|
9
|
+
const match = /^GitHub issue: ([a-zA-Z0-9][a-zA-Z0-9-]*)\/([a-zA-Z0-9_.-]+)#([1-9][0-9]*)(?: \((?:open|closed)\))?\s*$/.exec(markers[0] ?? "");
|
|
10
|
+
const [, owner, repository, digits] = match ?? [];
|
|
11
|
+
const number = Number(digits);
|
|
12
|
+
if (owner === undefined ||
|
|
13
|
+
repository === undefined ||
|
|
14
|
+
[".", ".."].includes(repository) ||
|
|
15
|
+
!Number.isSafeInteger(number) ||
|
|
16
|
+
number <= 0)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
owner: owner.toLowerCase(),
|
|
20
|
+
repository: repository.toLowerCase(),
|
|
21
|
+
number,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { failed, invalidRequest, isIdentifier, result, } from "./replies.js";
|
|
2
|
+
export async function queueRunStatuses(context, runIds) {
|
|
3
|
+
if (!Array.isArray(runIds) ||
|
|
4
|
+
runIds.length === 0 ||
|
|
5
|
+
runIds.length > 4 ||
|
|
6
|
+
!runIds.every(isIdentifier))
|
|
7
|
+
return invalidRequest("A queue status batch needs one to four run IDs.");
|
|
8
|
+
const state = await context.authority.state();
|
|
9
|
+
const runs = [];
|
|
10
|
+
for (const runId of runIds) {
|
|
11
|
+
const run = state.runs[runId];
|
|
12
|
+
if (run === undefined)
|
|
13
|
+
return failed("unknown-run", { detail: `No run has the ID ${runId}.` });
|
|
14
|
+
runs.push(await queuedRun(context, {
|
|
15
|
+
runId,
|
|
16
|
+
status: run.status,
|
|
17
|
+
sequence: state.sequence,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
return result("queue-run-statuses", { runs });
|
|
21
|
+
}
|
|
22
|
+
async function queuedRun(context, run) {
|
|
23
|
+
const event = await context.authority.queries.latestCompletion({
|
|
24
|
+
runId: run.runId,
|
|
25
|
+
atOrBefore: run.sequence,
|
|
26
|
+
terminal: true,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
runId: run.runId,
|
|
30
|
+
state: run.status,
|
|
31
|
+
terminalSequence: event?.sequence ?? null,
|
|
32
|
+
outcome: event?.type === "run.completed"
|
|
33
|
+
? {
|
|
34
|
+
kind: event.payload["outcome"],
|
|
35
|
+
issueUpdate: event.payload["issueUpdate"],
|
|
36
|
+
}
|
|
37
|
+
: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkflowStage, WorkflowStages } from "../workflow-declaration.ts";
|
|
1
2
|
import type { SubmissionContract } from "../submission.ts";
|
|
2
3
|
import type { ExecutionManifest } from "../workflow-admission.ts";
|
|
3
4
|
export type Resolution = {
|
|
@@ -13,11 +14,13 @@ export interface ResolutionRequest {
|
|
|
13
14
|
readonly submission: Readonly<Record<string, unknown>>;
|
|
14
15
|
readonly commandId: string;
|
|
15
16
|
readonly signal: AbortSignal;
|
|
17
|
+
readonly activeTickets?: readonly Readonly<Record<string, unknown>>[];
|
|
16
18
|
}
|
|
17
19
|
export interface Resolver {
|
|
18
20
|
readonly id: string;
|
|
19
21
|
readonly version: string;
|
|
20
22
|
readonly resolve: (request: ResolutionRequest) => Promise<Resolution>;
|
|
23
|
+
readonly skippedTickets?: () => readonly Readonly<Record<string, unknown>>[];
|
|
21
24
|
}
|
|
22
25
|
export interface WorkflowRegistration {
|
|
23
26
|
readonly workflowId: string;
|
|
@@ -26,15 +29,18 @@ export interface WorkflowRegistration {
|
|
|
26
29
|
readonly submission: SubmissionContract;
|
|
27
30
|
readonly resolver: Resolver;
|
|
28
31
|
readonly effectLimit?: number;
|
|
29
|
-
readonly
|
|
32
|
+
readonly stages?: readonly WorkflowStage[];
|
|
33
|
+
readonly handoffs?: readonly string[];
|
|
34
|
+
readonly admission?: "when-idle" | number;
|
|
30
35
|
}
|
|
31
36
|
export interface Registered extends WorkflowRegistration {
|
|
32
37
|
readonly pin: string;
|
|
33
38
|
}
|
|
34
39
|
export interface Registry {
|
|
35
40
|
readonly workflowIds: readonly string[];
|
|
41
|
+
readonly workflowStages: WorkflowStages;
|
|
36
42
|
readonly byId: (workflowId: string) => Registered | undefined;
|
|
37
43
|
readonly byPin: (pin: string) => Registered | undefined;
|
|
38
44
|
readonly available: (pinned: ExecutionManifest) => ExecutionManifest | undefined;
|
|
39
45
|
}
|
|
40
|
-
export declare function workflowRegistry(workflows: readonly WorkflowRegistration[]): Registry;
|
|
46
|
+
export declare function workflowRegistry(workflows: readonly WorkflowRegistration[], retainedWorkflows?: readonly WorkflowRegistration[]): Registry;
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { workflowPin } from "../broker/workflow-pin.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { declaredManifest } from "../execution-pin.js";
|
|
3
|
+
function registered(workflow) {
|
|
4
|
+
const manifest = declaredManifest(workflow.manifest, workflow);
|
|
5
|
+
return {
|
|
4
6
|
...workflow,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
...(manifest.handoffs === undefined ? {} : { handoffs: manifest.handoffs }),
|
|
8
|
+
manifest,
|
|
9
|
+
pin: workflowPin(manifest),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function workflowRegistry(workflows, retainedWorkflows = []) {
|
|
13
|
+
const current = workflows.map(registered);
|
|
14
|
+
const available = [...current, ...retainedWorkflows.map(registered)].filter((workflow) => workflow.availability === "available");
|
|
15
|
+
const byPin = (pin) => available.find((workflow) => workflow.pin === pin);
|
|
7
16
|
return {
|
|
8
|
-
workflowIds:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
workflowIds: current.map((workflow) => workflow.workflowId),
|
|
18
|
+
workflowStages: Object.fromEntries(current.flatMap((workflow) => workflow.stages === undefined
|
|
19
|
+
? []
|
|
20
|
+
: [[workflow.workflowId, workflow.stages]])),
|
|
21
|
+
byId: (workflowId) => current.find((workflow) => workflow.workflowId === workflowId),
|
|
22
|
+
byPin,
|
|
23
|
+
available: (pinned) => byPin(workflowPin(pinned))?.manifest,
|
|
12
24
|
};
|
|
13
25
|
}
|
|
@@ -3,9 +3,11 @@ import type { Fields, Reply } from "../protocol.ts";
|
|
|
3
3
|
import type { ChangeSignal } from "./changes.ts";
|
|
4
4
|
import type { Gateway } from "./gateway.ts";
|
|
5
5
|
import type { Registry } from "./registry.ts";
|
|
6
|
+
import type { WorkflowRefusal } from "../workflow-refusals.ts";
|
|
6
7
|
export interface ServiceContext {
|
|
7
8
|
readonly authority: Authority;
|
|
8
9
|
readonly registry: Registry;
|
|
10
|
+
readonly workflowRefusals?: readonly WorkflowRefusal[];
|
|
9
11
|
readonly gateway: Gateway;
|
|
10
12
|
readonly clock: () => string;
|
|
11
13
|
readonly livenessMilliseconds: number;
|