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,13 +1,18 @@
|
|
|
1
|
+
import { issueIdentity } from "../issue-identity.js";
|
|
2
|
+
import { linksOf } from "../authority/handoff-chain.js";
|
|
1
3
|
import { runView, unresolvedEffects, } from "../authority/status.js";
|
|
2
4
|
import { record } from "../protocol.js";
|
|
3
5
|
import { committed, failed, invalidRequest, isIdentifier, publicReceipt, rejection, result, wholeNumber, } from "./replies.js";
|
|
4
6
|
import { usageTotals } from "./worker-totals.js";
|
|
5
7
|
import { feedbackOf } from "./decision-handler.js";
|
|
8
|
+
import { queueOverview } from "./submission-handler.js";
|
|
9
|
+
import { queueRunStatuses } from "./queue-runs.js";
|
|
6
10
|
async function snapshot(context) {
|
|
7
11
|
const { authority } = context;
|
|
8
12
|
const state = await authority.state();
|
|
9
13
|
const sequences = await authority.queries.sequences();
|
|
10
14
|
return {
|
|
15
|
+
links: await authority.queries.runLinks(),
|
|
11
16
|
state,
|
|
12
17
|
operations: await authority.operations(),
|
|
13
18
|
sequences: sequences.runs,
|
|
@@ -39,9 +44,7 @@ async function effectInput(context, runId) {
|
|
|
39
44
|
const budget = await effects.budget(runId);
|
|
40
45
|
const stored = await effects.operations(runId);
|
|
41
46
|
return {
|
|
42
|
-
budget: budget
|
|
43
|
-
? null
|
|
44
|
-
: { limit: budget.limit, spent: budget.spent },
|
|
47
|
+
budget: budget ? { limit: budget.limit, spent: budget.spent } : null,
|
|
45
48
|
effects: stored.map((operation) => ({
|
|
46
49
|
operationId: operation.operationId,
|
|
47
50
|
status: operation.status,
|
|
@@ -78,6 +81,8 @@ async function viewOf(context, scope) {
|
|
|
78
81
|
};
|
|
79
82
|
return {
|
|
80
83
|
...runView(input),
|
|
84
|
+
...linksOf({ runId: run.runId, ...scope.snapshot.links[run.runId] }),
|
|
85
|
+
input: await submittedInput(context, details?.submission),
|
|
81
86
|
workflowId: scope.snapshot.workflowIds[run.runId] ?? run.workflow,
|
|
82
87
|
latestAt: times[run.runId] ?? null,
|
|
83
88
|
unresolvedEffects: unresolvedEffects(input),
|
|
@@ -87,6 +92,8 @@ async function viewOf(context, scope) {
|
|
|
87
92
|
function summary(view) {
|
|
88
93
|
return {
|
|
89
94
|
runId: view.runId,
|
|
95
|
+
...linksOf(view),
|
|
96
|
+
input: view.input,
|
|
90
97
|
workflowId: view.workflowId,
|
|
91
98
|
state: view.state,
|
|
92
99
|
condition: view.condition,
|
|
@@ -122,17 +129,17 @@ async function outcomeOf(context, view) {
|
|
|
122
129
|
runId: view.runId,
|
|
123
130
|
atOrBefore: view.latestSequence,
|
|
124
131
|
});
|
|
125
|
-
const
|
|
132
|
+
const payload = event?.payload ?? {};
|
|
133
|
+
const kind = payload["outcome"];
|
|
126
134
|
if (kind === undefined)
|
|
127
135
|
return null;
|
|
128
|
-
const ticketStatus =
|
|
136
|
+
const { ticketStatus, promotionId, outcomeReason, issueUpdate } = payload;
|
|
129
137
|
return {
|
|
130
138
|
kind,
|
|
131
|
-
reason:
|
|
139
|
+
reason: outcomeReason ?? null,
|
|
140
|
+
...(promotionId === undefined ? {} : { promotionId }),
|
|
132
141
|
...(ticketStatus === undefined ? {} : { ticketStatus }),
|
|
133
|
-
...(
|
|
134
|
-
? { issueUpdate: "failed" }
|
|
135
|
-
: {}),
|
|
142
|
+
...(issueUpdate === "failed" ? { issueUpdate } : {}),
|
|
136
143
|
};
|
|
137
144
|
}
|
|
138
145
|
function titleOf(input, workflowId) {
|
|
@@ -161,8 +168,18 @@ async function metadataOf(context, source) {
|
|
|
161
168
|
return {
|
|
162
169
|
ticketId: typeof ticketId === "string" && ticketId !== "" ? ticketId : null,
|
|
163
170
|
title: titleOf(input, source.workflowId),
|
|
171
|
+
...(source.workflowId === "develop-issue"
|
|
172
|
+
? { issueIdentity: issueIdentity(input?.["ticket"]) }
|
|
173
|
+
: {}),
|
|
164
174
|
};
|
|
165
175
|
}
|
|
176
|
+
async function submittedInput(context, digest) {
|
|
177
|
+
const document = digest
|
|
178
|
+
? await context.authority.queries.document(digest)
|
|
179
|
+
: undefined;
|
|
180
|
+
const request = record(JSON.parse(document ?? "null"));
|
|
181
|
+
return record(request?.["submission"]) ?? null;
|
|
182
|
+
}
|
|
166
183
|
async function listed(context, page) {
|
|
167
184
|
const identifiers = Object.keys(page.snapshot.state.runs)
|
|
168
185
|
.sort()
|
|
@@ -173,21 +190,36 @@ async function listed(context, page) {
|
|
|
173
190
|
views.push(await viewOf(context, { snapshot: page.snapshot, runId }));
|
|
174
191
|
const runs = views.filter((view) => view !== undefined).map(summary);
|
|
175
192
|
return result("run-list", {
|
|
193
|
+
...(await queueOverview(context)),
|
|
176
194
|
workflows: context.registry.workflowIds,
|
|
195
|
+
workflowRefusals: context.workflowRefusals ?? [],
|
|
196
|
+
...(Object.keys(context.registry.workflowStages).length === 0
|
|
197
|
+
? {}
|
|
198
|
+
: {
|
|
199
|
+
workflowStages: context.registry.workflowStages,
|
|
200
|
+
}),
|
|
177
201
|
runs,
|
|
178
202
|
next: identifiers.length > shown.length ? (shown.at(-1) ?? null) : null,
|
|
179
203
|
worldReports: context.worldReports?.() ?? [],
|
|
180
204
|
});
|
|
181
205
|
}
|
|
182
|
-
|
|
183
|
-
const { runId } = parameters;
|
|
206
|
+
function listParameters(parameters) {
|
|
184
207
|
const after = parameters["after"] ?? "";
|
|
185
208
|
const limit = parameters["limit"] ?? 50;
|
|
186
209
|
if (typeof after !== "string" || !wholeNumber(limit, 1) || limit > 500)
|
|
210
|
+
return undefined;
|
|
211
|
+
return { after, limit };
|
|
212
|
+
}
|
|
213
|
+
export async function status(context, parameters) {
|
|
214
|
+
if (parameters["runIds"] !== undefined)
|
|
215
|
+
return queueRunStatuses(context, parameters["runIds"]);
|
|
216
|
+
const { runId } = parameters;
|
|
217
|
+
const page = listParameters(parameters);
|
|
218
|
+
if (page === undefined)
|
|
187
219
|
return invalidRequest("The status request is not valid.");
|
|
188
220
|
const current = await snapshot(context);
|
|
189
221
|
if (runId === undefined)
|
|
190
|
-
return listed(context, { snapshot: current,
|
|
222
|
+
return listed(context, { snapshot: current, ...page });
|
|
191
223
|
const named = typeof runId === "string" ? runId : "";
|
|
192
224
|
const view = await viewOf(context, { snapshot: current, runId: named });
|
|
193
225
|
if (view === undefined)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Authority, type AuthorityTiming, type Delivery, type EffectSettings, type Refusal } from "../broker/actor/authority.ts";
|
|
2
|
+
import { type WorkflowRefusal } from "../workflow-refusals.ts";
|
|
2
3
|
import { type WorkflowRegistration } from "./registry.ts";
|
|
3
4
|
import { type WorkerPort } from "./worker-port.ts";
|
|
4
5
|
export type { WorkflowRegistration } from "./registry.ts";
|
|
@@ -38,6 +39,8 @@ export interface ServiceOptions {
|
|
|
38
39
|
readonly stateDirectory: string;
|
|
39
40
|
readonly runtimeDirectory: string;
|
|
40
41
|
readonly workflows: readonly WorkflowRegistration[];
|
|
42
|
+
readonly retainedWorkflows?: readonly WorkflowRegistration[];
|
|
43
|
+
readonly workflowRefusals?: readonly WorkflowRefusal[];
|
|
41
44
|
readonly delivery: Delivery;
|
|
42
45
|
readonly host?: (ports: HostPorts) => Promise<ServiceHost>;
|
|
43
46
|
readonly timing?: ServiceTiming;
|
|
@@ -2,6 +2,7 @@ import { lstat, rm } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { authorityTiming, openAuthority, } from "../broker/actor/authority.js";
|
|
4
4
|
import { socketAddress } from "../broker/socket-address.js";
|
|
5
|
+
import { workflowRefusals, } from "../workflow-refusals.js";
|
|
5
6
|
import { changeSignal } from "./changes.js";
|
|
6
7
|
import { decision, questions } from "./decision-handler.js";
|
|
7
8
|
import { events } from "./event-handler.js";
|
|
@@ -87,6 +88,7 @@ async function started(authority, setup) {
|
|
|
87
88
|
handle: dispatcher({
|
|
88
89
|
authority,
|
|
89
90
|
registry,
|
|
91
|
+
workflowRefusals: workflowRefusals(options.workflowRefusals),
|
|
90
92
|
gateway,
|
|
91
93
|
clock,
|
|
92
94
|
changed: changes.next,
|
|
@@ -133,7 +135,7 @@ export async function startService(options) {
|
|
|
133
135
|
if (typeof path !== "string")
|
|
134
136
|
return path;
|
|
135
137
|
const timing = options.timing ?? serviceTiming;
|
|
136
|
-
const registry = workflowRegistry(options.workflows);
|
|
138
|
+
const registry = workflowRegistry(options.workflows, options.retainedWorkflows);
|
|
137
139
|
const opening = await openAuthority(options.stateDirectory, {
|
|
138
140
|
delivery: options.delivery,
|
|
139
141
|
timing: timing.authority,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Fields, type Reply } from "../protocol.ts";
|
|
2
2
|
import { type ServiceContext } from "./replies.ts";
|
|
3
3
|
export declare function runIdentity(commandId: string): string;
|
|
4
|
+
export declare function queueOverview(context: ServiceContext): Promise<Fields>;
|
|
4
5
|
export declare function submissionHandler(context: ServiceContext): (parameters: Fields) => Promise<Reply>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { handoffChain } from "../authority/handoff-chain.js";
|
|
3
|
+
import { executionCapabilities } from "../execution-pin.js";
|
|
2
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
-
import { isIdentifier as isWorkflowIdentifier } from "../authority/commands.js";
|
|
5
|
+
import { isCommandId, isIdentifier as isWorkflowIdentifier, } from "../authority/commands.js";
|
|
4
6
|
import { documentDigest } from "../broker/decision-restore.js";
|
|
5
7
|
import { record } from "../protocol.js";
|
|
6
8
|
import { checkResolution, checkSubmission, normalizedRequest, } from "../submission.js";
|
|
7
|
-
import { committed, failed, invalidRequest,
|
|
9
|
+
import { committed, failed, invalidRequest, publicReceipt, rejection, result, } from "./replies.js";
|
|
8
10
|
export function runIdentity(commandId) {
|
|
9
11
|
return `run-${createHash("sha256").update(commandId).digest("hex").slice(0, 32)}`;
|
|
10
12
|
}
|
|
@@ -49,7 +51,7 @@ function effectBudget(workflow) {
|
|
|
49
51
|
return {};
|
|
50
52
|
return {
|
|
51
53
|
effectLimit: workflow.effectLimit,
|
|
52
|
-
capabilities:
|
|
54
|
+
capabilities: executionCapabilities(workflow.manifest),
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
function usable(workflow, workflowId) {
|
|
@@ -73,7 +75,13 @@ async function resolved(context, admission) {
|
|
|
73
75
|
const { workflow, workflowId, commandId, submission } = admission;
|
|
74
76
|
const limit = new AbortController();
|
|
75
77
|
const timer = new AbortController();
|
|
76
|
-
const request = {
|
|
78
|
+
const request = {
|
|
79
|
+
workflowId,
|
|
80
|
+
submission,
|
|
81
|
+
commandId,
|
|
82
|
+
signal: limit.signal,
|
|
83
|
+
activeTickets: admission.activeTickets,
|
|
84
|
+
};
|
|
77
85
|
const call = Promise.resolve(request)
|
|
78
86
|
.then((given) => workflow.resolver.resolve(given))
|
|
79
87
|
.then((value) => checkResolution(value) ?? "failure")
|
|
@@ -110,6 +118,9 @@ async function admit(context, admission) {
|
|
|
110
118
|
return unresolved(context, { kind: resolution, commandId });
|
|
111
119
|
if (resolution.kind === "rejected")
|
|
112
120
|
return failed(resolution.code, { commandId, detail: resolution.detail });
|
|
121
|
+
const limited = capacityRefusal(workflow, admission);
|
|
122
|
+
if (limited !== undefined)
|
|
123
|
+
return limited;
|
|
113
124
|
const { input } = resolution;
|
|
114
125
|
if (Buffer.byteLength(input) > context.documentBytes)
|
|
115
126
|
return unresolved(context, { kind: "failure", commandId });
|
|
@@ -124,6 +135,12 @@ async function admit(context, admission) {
|
|
|
124
135
|
input: documentDigest(input),
|
|
125
136
|
resolver: `${workflow.resolver.id}:${workflow.resolver.version}`,
|
|
126
137
|
...effectBudget(workflow),
|
|
138
|
+
...(admission.chain.kind === "handoff"
|
|
139
|
+
? {
|
|
140
|
+
parentRunId: admission.chain.parentRunId,
|
|
141
|
+
chainId: admission.chain.chainId,
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
127
144
|
},
|
|
128
145
|
documents: [normalized, input],
|
|
129
146
|
});
|
|
@@ -137,9 +154,32 @@ async function admit(context, admission) {
|
|
|
137
154
|
replayed: receipt.kind === "duplicate",
|
|
138
155
|
});
|
|
139
156
|
}
|
|
157
|
+
function admissionRefusal(workflow, request) {
|
|
158
|
+
const { workflowId, commandId, submission, activeTickets } = request;
|
|
159
|
+
if (workflowId === "develop-issue" &&
|
|
160
|
+
typeof submission["ticket"] === "string" &&
|
|
161
|
+
activeTickets.some((input) => input["ticketId"] === submission["ticket"]))
|
|
162
|
+
return failed("ticket-active", {
|
|
163
|
+
commandId,
|
|
164
|
+
detail: "An active run already works on this ticket.",
|
|
165
|
+
});
|
|
166
|
+
if (workflowId === "develop-issue" && submission["ticket"] !== "next")
|
|
167
|
+
return undefined;
|
|
168
|
+
return capacityRefusal(workflow, request);
|
|
169
|
+
}
|
|
170
|
+
function capacityRefusal(workflow, request) {
|
|
171
|
+
if (workflow.admission !== undefined &&
|
|
172
|
+
request.activeTickets.length >=
|
|
173
|
+
(workflow.admission === "when-idle" ? 1 : workflow.admission))
|
|
174
|
+
return failed("workflow-active", {
|
|
175
|
+
commandId: request.commandId,
|
|
176
|
+
detail: `The workflow ${request.workflowId} already has an active run.`,
|
|
177
|
+
});
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
140
180
|
async function submitted(context, parameters) {
|
|
141
181
|
const { workflowId, commandId } = parameters;
|
|
142
|
-
if (!isWorkflowIdentifier(workflowId) || !
|
|
182
|
+
if (!isWorkflowIdentifier(workflowId) || !isCommandId(commandId))
|
|
143
183
|
return invalidRequest("The workflow ID or the command ID is not valid.");
|
|
144
184
|
const existing = await context.authority.queries.receipt(commandId);
|
|
145
185
|
if (existing !== undefined)
|
|
@@ -147,6 +187,14 @@ async function submitted(context, parameters) {
|
|
|
147
187
|
workflowId,
|
|
148
188
|
submission: parameters["submission"],
|
|
149
189
|
});
|
|
190
|
+
const chain = handoffChain(commandId, commandId.startsWith("handoff:")
|
|
191
|
+
? await context.authority.queries.runOrigins()
|
|
192
|
+
: {});
|
|
193
|
+
if (chain.kind === "refused")
|
|
194
|
+
return failed(chain.reason, {
|
|
195
|
+
commandId,
|
|
196
|
+
detail: "A hand-off chain permits at most five runs and requires a recorded parent.",
|
|
197
|
+
});
|
|
150
198
|
const found = usable(context.registry.byId(workflowId), workflowId);
|
|
151
199
|
if (found.kind === "unusable")
|
|
152
200
|
return found.reply;
|
|
@@ -161,32 +209,63 @@ async function submitted(context, parameters) {
|
|
|
161
209
|
});
|
|
162
210
|
if (check.kind === "rejected")
|
|
163
211
|
return failed(check.code, { commandId, detail: check.detail });
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
const activeTickets = await activeWorkflow(context, workflowId);
|
|
213
|
+
const refusal = admissionRefusal(found.workflow, {
|
|
214
|
+
workflowId,
|
|
215
|
+
commandId,
|
|
216
|
+
submission: check.submission,
|
|
217
|
+
activeTickets,
|
|
218
|
+
});
|
|
219
|
+
if (refusal !== undefined)
|
|
220
|
+
return refusal;
|
|
170
221
|
return admit(context, {
|
|
222
|
+
chain,
|
|
171
223
|
workflow: found.workflow,
|
|
172
224
|
workflowId,
|
|
173
225
|
commandId,
|
|
174
226
|
submission: check.submission,
|
|
175
227
|
normalized: check.normalized,
|
|
228
|
+
activeTickets,
|
|
176
229
|
});
|
|
177
230
|
}
|
|
178
231
|
async function activeWorkflow(context, workflowId) {
|
|
179
232
|
const workflows = await context.authority.queries.workflowIds();
|
|
180
233
|
const state = await context.authority.state();
|
|
181
|
-
|
|
234
|
+
const runs = Object.values(state.runs).filter((run) => workflows[run.runId] === workflowId &&
|
|
182
235
|
!["completed", "failed", "cancelled"].includes(run.status));
|
|
236
|
+
return Promise.all(runs.map(async (run) => {
|
|
237
|
+
const digest = state.details[run.runId]?.input;
|
|
238
|
+
const text = digest
|
|
239
|
+
? await context.authority.queries.document(digest)
|
|
240
|
+
: undefined;
|
|
241
|
+
return record(JSON.parse(text ?? "null")) ?? {};
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
export async function queueOverview(context) {
|
|
245
|
+
const workflow = context.registry.byId("develop-issue");
|
|
246
|
+
if (workflow?.admission === undefined)
|
|
247
|
+
return {};
|
|
248
|
+
const active = await activeWorkflow(context, "develop-issue");
|
|
249
|
+
return {
|
|
250
|
+
queue: {
|
|
251
|
+
lanes: workflow.admission === "when-idle" ? 1 : workflow.admission,
|
|
252
|
+
busy: active.length,
|
|
253
|
+
skipped: [
|
|
254
|
+
...active.flatMap((input) => typeof input["ticketId"] === "string"
|
|
255
|
+
? [{ ticketId: input["ticketId"], reason: "ticket-active" }]
|
|
256
|
+
: []),
|
|
257
|
+
...(workflow.resolver.skippedTickets?.() ?? []),
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
183
261
|
}
|
|
184
262
|
export function submissionHandler(context) {
|
|
185
263
|
const pending = new Map();
|
|
186
264
|
return (parameters) => {
|
|
187
265
|
const keys = [`command:${String(parameters["commandId"])}`];
|
|
188
266
|
const workflowId = String(parameters["workflowId"]);
|
|
189
|
-
if (
|
|
267
|
+
if (workflowId === "develop-issue" ||
|
|
268
|
+
context.registry.byId(workflowId)?.admission !== undefined)
|
|
190
269
|
keys.push(`workflow:${workflowId}`);
|
|
191
270
|
const next = () => submitted(context, parameters);
|
|
192
271
|
const previous = keys
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { record } from "../protocol.js";
|
|
2
|
-
import { priceOf, usageOf, workerTotals, } from "../worker-usage.js";
|
|
2
|
+
import { priceOf, usageOf, tokenCount, workerTotals, } from "../worker-usage.js";
|
|
3
3
|
function reportFields(text) {
|
|
4
4
|
try {
|
|
5
5
|
return record(JSON.parse(text ?? "null")) ?? {};
|
|
@@ -8,6 +8,28 @@ function reportFields(text) {
|
|
|
8
8
|
return {};
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
function attemptsOf(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return undefined;
|
|
14
|
+
return value.flatMap((entry) => {
|
|
15
|
+
const fields = record(entry) ?? {};
|
|
16
|
+
const attempt = tokenCount(fields["attempt"]);
|
|
17
|
+
const model = fields["model"];
|
|
18
|
+
if (attempt === null ||
|
|
19
|
+
attempt === 0 ||
|
|
20
|
+
(model !== null && typeof model !== "string"))
|
|
21
|
+
return [];
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
attempt,
|
|
25
|
+
model,
|
|
26
|
+
usage: usageOf(fields["usage"]),
|
|
27
|
+
costUsd: priceOf(fields["costUsd"]),
|
|
28
|
+
estimatedCostUsd: priceOf(fields["estimatedCostUsd"]),
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
11
33
|
export async function usageTotals(context, runId) {
|
|
12
34
|
const { effects, queries } = context.authority;
|
|
13
35
|
const operations = (await effects.operations(runId)).filter((operation) => operation.capability === "claude" &&
|
|
@@ -17,7 +39,9 @@ export async function usageTotals(context, runId) {
|
|
|
17
39
|
const report = reportFields(operation.result === null
|
|
18
40
|
? undefined
|
|
19
41
|
: await queries.document(operation.result));
|
|
42
|
+
const attempts = attemptsOf(report["attempts"]);
|
|
20
43
|
turns.push({
|
|
44
|
+
...(attempts === undefined ? {} : { attempts }),
|
|
21
45
|
operationId: operation.operationId,
|
|
22
46
|
usage: usageOf(report["usage"]),
|
|
23
47
|
costUsd: priceOf(report["costUsd"]),
|
|
@@ -39,6 +39,14 @@ export interface GateRequest {
|
|
|
39
39
|
readonly gateId: string;
|
|
40
40
|
readonly kind: "question" | "approval";
|
|
41
41
|
readonly prompt: string;
|
|
42
|
+
readonly expiresAt?: string;
|
|
43
|
+
}
|
|
44
|
+
export type HandoffValue = string | number | boolean | null | readonly HandoffValue[] | {
|
|
45
|
+
readonly [key: string]: HandoffValue;
|
|
46
|
+
};
|
|
47
|
+
export interface HandoffRequest {
|
|
48
|
+
readonly workflowId: string;
|
|
49
|
+
readonly input: Readonly<Record<string, HandoffValue>>;
|
|
42
50
|
}
|
|
43
51
|
export type GateResult = {
|
|
44
52
|
readonly kind: "opened";
|
|
@@ -53,5 +61,6 @@ export type GateResult = {
|
|
|
53
61
|
export interface StepBridge {
|
|
54
62
|
readonly effect: (request: ProtectedRequest) => Promise<ProtectedResult>;
|
|
55
63
|
readonly gate: (request: GateRequest) => Promise<GateResult>;
|
|
64
|
+
readonly handoff?: (request: HandoffRequest) => Promise<ProtectedResult>;
|
|
56
65
|
}
|
|
57
66
|
export declare function isStepBridge(value: unknown): value is StepBridge;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isStepBridge, stepBridgeKey, } from "../step-bridge.js";
|
|
2
|
+
export async function handoff(request) {
|
|
3
|
+
"use step";
|
|
4
|
+
const bridge = Reflect.get(globalThis, Symbol.for(stepBridgeKey));
|
|
5
|
+
if (!isStepBridge(bridge) || bridge.handoff === undefined)
|
|
6
|
+
throw new Error("cloving-host-unavailable");
|
|
7
|
+
const result = await bridge.handoff(request);
|
|
8
|
+
if (result.kind !== "completed")
|
|
9
|
+
throw new Error(result.reason ?? result.kind);
|
|
10
|
+
if (result.result === null)
|
|
11
|
+
throw new Error("handoff-result-missing");
|
|
12
|
+
return result.result;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hardDependencies(ticket: string): readonly string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function hardDependencies(ticket) {
|
|
2
|
+
const lines = ticket.split(/\r?\n/);
|
|
3
|
+
const start = lines.findIndex((line) => /^## Dependencies\s*$/.test(line));
|
|
4
|
+
if (start === -1)
|
|
5
|
+
return [];
|
|
6
|
+
const following = lines.slice(start + 1);
|
|
7
|
+
const end = following.findIndex((line) => /^#{1,2} /.test(line));
|
|
8
|
+
const section = (end === -1 ? following : following.slice(0, end)).join("\n");
|
|
9
|
+
const sentences = section
|
|
10
|
+
.replace(/\b(?:e\.g|i\.e)\./g, "example")
|
|
11
|
+
.matchAll(/(?:^|[.!?]\s+|\n)\s*(?:[-*+]\s+|\d+\.\s+)?Needs\b([\s\S]*?)(?=[.!?](?:\s|$)|$)/g);
|
|
12
|
+
return [
|
|
13
|
+
...new Set([...sentences].flatMap((sentence) => [...String(sentence[1]).matchAll(/#([1-9]\d*)\b/g)].map((match) => String(match[1])))),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
@@ -8,11 +8,14 @@ function parsedStatus(summary) {
|
|
|
8
8
|
if (status !== "Ticket status: partial")
|
|
9
9
|
return undefined;
|
|
10
10
|
const heading = lines.lastIndexOf("Remaining:");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const items = heading === -1
|
|
12
|
+
? []
|
|
13
|
+
: lines.slice(heading + 1).filter((line) => line.trim() !== "");
|
|
14
14
|
if (items.length === 0 || items.some((line) => !/^- \S.*$/.test(line)))
|
|
15
|
-
return
|
|
15
|
+
return {
|
|
16
|
+
kind: "partial",
|
|
17
|
+
remaining: ["The worker report has an unreadable Remaining: list."],
|
|
18
|
+
};
|
|
16
19
|
return { kind: "partial", remaining: items.map((line) => line.slice(2)) };
|
|
17
20
|
}
|
|
18
21
|
function preservedStatus(value) {
|
|
@@ -11,8 +11,10 @@ export declare const builtInPriceTable: {
|
|
|
11
11
|
readonly prices: TokenPrices;
|
|
12
12
|
};
|
|
13
13
|
export declare function parseTokenPrices(value: string | undefined): TokenPrices;
|
|
14
|
+
export declare function parseFallbackModels(value: string | undefined): readonly string[];
|
|
14
15
|
export declare function priceForModel(prices: TokenPrices, model: string | null): TokenPrice | undefined;
|
|
15
16
|
export declare function commandModel(command: readonly string[]): string | null;
|
|
17
|
+
export declare function replaceCommandModel(command: readonly string[], model: string): readonly string[];
|
|
16
18
|
export declare function estimatedCostUsd(usage: WorkerUsage, pricing: {
|
|
17
19
|
readonly worker: "claude" | "codex";
|
|
18
20
|
readonly price: TokenPrice | undefined;
|
package/dist/src/token-prices.js
CHANGED
|
@@ -36,26 +36,58 @@ export function parseTokenPrices(value) {
|
|
|
36
36
|
throw new Error("Invalid CLOVING_TOKEN_PRICES");
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
export function parseFallbackModels(value) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
const models = JSON.parse(value);
|
|
44
|
+
if (Array.isArray(models) &&
|
|
45
|
+
models.length <= 3 &&
|
|
46
|
+
models.every((model) => typeof model === "string" && model.trim().length > 0))
|
|
47
|
+
return models.map(String);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error("The daemon did not start: invalid-fallback-models");
|
|
51
|
+
}
|
|
52
|
+
throw new Error("The daemon did not start: invalid-fallback-models");
|
|
53
|
+
}
|
|
39
54
|
export function priceForModel(prices, model) {
|
|
40
55
|
return model !== null && Object.hasOwn(prices, model)
|
|
41
56
|
? prices[model]
|
|
42
57
|
: undefined;
|
|
43
58
|
}
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
|
|
59
|
+
function modelIndex(command) {
|
|
60
|
+
let selected;
|
|
61
|
+
const separator = command.indexOf("--", 1);
|
|
62
|
+
const end = separator === -1 ? command.length : separator;
|
|
63
|
+
for (let index = 1; index < end; index += 1) {
|
|
47
64
|
const argument = command[index] ?? "";
|
|
48
|
-
if (argument === "--")
|
|
49
|
-
break;
|
|
50
65
|
if (argument.startsWith("--model="))
|
|
51
|
-
|
|
52
|
-
if (
|
|
66
|
+
selected = argument.slice(8) ? index : undefined;
|
|
67
|
+
if (["--model", "-m"].includes(argument)) {
|
|
53
68
|
const next = command[index + 1];
|
|
54
|
-
|
|
69
|
+
selected = next && !next.startsWith("-") ? index + 1 : undefined;
|
|
55
70
|
index += 1;
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
|
-
return
|
|
73
|
+
return selected;
|
|
74
|
+
}
|
|
75
|
+
export function commandModel(command) {
|
|
76
|
+
const index = modelIndex(command);
|
|
77
|
+
if (index === undefined)
|
|
78
|
+
return null;
|
|
79
|
+
const argument = command[index] ?? "";
|
|
80
|
+
return argument.startsWith("--model=") ? argument.slice(8) : argument;
|
|
81
|
+
}
|
|
82
|
+
export function replaceCommandModel(command, model) {
|
|
83
|
+
const index = modelIndex(command);
|
|
84
|
+
if (index === undefined)
|
|
85
|
+
return command;
|
|
86
|
+
return command.map((argument, position) => {
|
|
87
|
+
if (position !== index)
|
|
88
|
+
return argument;
|
|
89
|
+
return argument.startsWith("--model=") ? `--model=${model}` : model;
|
|
90
|
+
});
|
|
59
91
|
}
|
|
60
92
|
export function estimatedCostUsd(usage, pricing) {
|
|
61
93
|
const { worker, price } = pricing;
|
|
@@ -4,11 +4,19 @@ export interface WorkerUsage {
|
|
|
4
4
|
readonly output_tokens: number | null;
|
|
5
5
|
}
|
|
6
6
|
export interface WorkerTurnUsage {
|
|
7
|
+
readonly attempts?: readonly WorkerAttemptUsage[];
|
|
7
8
|
readonly estimatedCostUsd: number | null;
|
|
8
9
|
readonly operationId: string;
|
|
9
10
|
readonly usage: WorkerUsage;
|
|
10
11
|
readonly costUsd: number | null;
|
|
11
12
|
}
|
|
13
|
+
export interface WorkerAttemptUsage {
|
|
14
|
+
readonly attempt: number;
|
|
15
|
+
readonly model: string | null;
|
|
16
|
+
readonly usage: WorkerUsage;
|
|
17
|
+
readonly costUsd: number | null;
|
|
18
|
+
readonly estimatedCostUsd: number | null;
|
|
19
|
+
}
|
|
12
20
|
export interface WorkerTotals {
|
|
13
21
|
readonly estimatedCostUsd: number | null;
|
|
14
22
|
readonly workerTurns: readonly WorkerTurnUsage[];
|
|
@@ -19,4 +27,5 @@ export declare function priceOf(value: unknown): number | null;
|
|
|
19
27
|
export declare function tokenCount(value: unknown): number | null;
|
|
20
28
|
export declare function usageOf(value: unknown): WorkerUsage;
|
|
21
29
|
export declare function sumUsage(values: readonly WorkerUsage[]): WorkerUsage;
|
|
30
|
+
export declare function sumPrices(values: readonly (number | null)[]): number | null;
|
|
22
31
|
export declare function workerTotals(workerTurns: readonly WorkerTurnUsage[]): WorkerTotals;
|
package/dist/src/worker-usage.js
CHANGED
|
@@ -28,7 +28,7 @@ export function sumUsage(values) {
|
|
|
28
28
|
output_tokens: total(values.map((value) => value.output_tokens)),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
function sumPrices(values) {
|
|
31
|
+
export function sumPrices(values) {
|
|
32
32
|
const prices = values.map(priceOf);
|
|
33
33
|
if (prices.length === 0 || prices.includes(null))
|
|
34
34
|
return null;
|
|
@@ -27,6 +27,7 @@ export interface ExecutionManifest {
|
|
|
27
27
|
readonly workflowId: string;
|
|
28
28
|
readonly steps: readonly string[];
|
|
29
29
|
readonly capabilities: readonly string[];
|
|
30
|
+
readonly handoffs?: readonly string[];
|
|
30
31
|
readonly artifacts: Readonly<Record<string, string>>;
|
|
31
32
|
readonly resources: Readonly<Record<string, string>>;
|
|
32
33
|
readonly toolchain: ToolchainIdentity;
|
|
@@ -16,6 +16,7 @@ export type CycleOutcome = {
|
|
|
16
16
|
readonly promotionId: string;
|
|
17
17
|
readonly ticketStatus?: "complete" | "partial";
|
|
18
18
|
readonly issueUpdate?: "failed";
|
|
19
|
+
readonly reason?: string;
|
|
19
20
|
};
|
|
20
21
|
export type Validation<Value> = {
|
|
21
22
|
readonly kind: "valid";
|
|
@@ -24,6 +25,7 @@ export type Validation<Value> = {
|
|
|
24
25
|
readonly kind: "invalid";
|
|
25
26
|
readonly reason: string;
|
|
26
27
|
};
|
|
28
|
+
export declare function isPromotionId(value: unknown): value is string;
|
|
27
29
|
export declare function isBlockedReason(value: unknown): value is string;
|
|
28
30
|
export declare function validateDevelopmentInput(value: unknown): Validation<DevelopmentInput>;
|
|
29
31
|
export declare function validateCycleOutcome(value: unknown, input: DevelopmentInput): Validation<CycleOutcome>;
|