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,6 +1,9 @@
|
|
|
1
1
|
import { isTicketStatus } from "./ticket-status.js";
|
|
2
2
|
const identifier = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
3
3
|
const reason = /^\S[\s\S]{0,255}$/;
|
|
4
|
+
export function isPromotionId(value) {
|
|
5
|
+
return typeof value === "string" && identifier.test(value);
|
|
6
|
+
}
|
|
4
7
|
export function isBlockedReason(value) {
|
|
5
8
|
return typeof value === "string" && reason.test(value);
|
|
6
9
|
}
|
|
@@ -40,7 +43,7 @@ const outcomes = {
|
|
|
40
43
|
},
|
|
41
44
|
handoff_requested: {
|
|
42
45
|
keys: ["cycleId", "kind", "promotionId"],
|
|
43
|
-
optional: ["ticketStatus", "issueUpdate"],
|
|
46
|
+
optional: ["ticketStatus", "issueUpdate", "reason"],
|
|
44
47
|
parse: (fields) => {
|
|
45
48
|
const cycleId = textField(fields, {
|
|
46
49
|
key: "cycleId",
|
|
@@ -54,16 +57,18 @@ const outcomes = {
|
|
|
54
57
|
return null;
|
|
55
58
|
const ticketStatus = fields["ticketStatus"];
|
|
56
59
|
const issueUpdate = fields["issueUpdate"];
|
|
60
|
+
const reason = fields["reason"];
|
|
57
61
|
if (issueUpdate !== undefined && issueUpdate !== "failed")
|
|
58
62
|
return null;
|
|
59
63
|
if (ticketStatus !== undefined && !isTicketStatus(ticketStatus))
|
|
60
64
|
return null;
|
|
65
|
+
if (reason !== undefined && !isBlockedReason(reason))
|
|
66
|
+
return null;
|
|
61
67
|
return {
|
|
62
68
|
kind: "handoff_requested",
|
|
63
69
|
cycleId,
|
|
64
70
|
promotionId,
|
|
65
|
-
...(issueUpdate
|
|
66
|
-
...(ticketStatus === undefined ? {} : { ticketStatus }),
|
|
71
|
+
...Object.fromEntries(Object.entries({ issueUpdate, ticketStatus, reason }).filter(([, value]) => value !== undefined)),
|
|
67
72
|
};
|
|
68
73
|
},
|
|
69
74
|
},
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { SubmissionContract } from "./submission.ts";
|
|
2
|
+
export interface WorkflowStage {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly operations?: readonly string[];
|
|
6
|
+
readonly gateKinds?: readonly ("approval" | "question")[];
|
|
7
|
+
}
|
|
8
|
+
export type WorkflowStages = Readonly<Record<string, readonly WorkflowStage[]>>;
|
|
2
9
|
export interface WorkflowDeclaration {
|
|
3
10
|
readonly workflowId: string;
|
|
4
11
|
readonly effectLimit: number;
|
|
5
12
|
readonly capabilities: readonly ("claude" | "github" | "chats")[];
|
|
6
13
|
readonly input: SubmissionContract;
|
|
14
|
+
readonly stages?: readonly WorkflowStage[];
|
|
15
|
+
readonly handoffs?: readonly string[];
|
|
7
16
|
}
|
|
8
17
|
export interface DeclaredWorkflow extends WorkflowDeclaration {
|
|
9
18
|
readonly functionName: string;
|
|
10
19
|
}
|
|
20
|
+
export declare function isWorkflowStages(value: unknown): value is readonly WorkflowStage[];
|
|
21
|
+
export declare function isWorkflowHandoffs(value: unknown): value is readonly string[];
|
|
11
22
|
export declare function isWorkflowDeclaration(value: unknown): value is WorkflowDeclaration;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { record } from "./protocol.js";
|
|
2
2
|
function keys(fields, allowed) {
|
|
3
|
-
return
|
|
3
|
+
return (fields !== undefined &&
|
|
4
|
+
Object.keys(fields).every((key) => allowed.includes(key)));
|
|
4
5
|
}
|
|
5
6
|
function integer(value, bounds) {
|
|
6
7
|
return (typeof value === "number" &&
|
|
@@ -20,8 +21,7 @@ function integerBounds(fields) {
|
|
|
20
21
|
}
|
|
21
22
|
function isField(value) {
|
|
22
23
|
const fields = record(value);
|
|
23
|
-
if (fields
|
|
24
|
-
!keys(fields, ["type", "optional", "minimum", "maximum"]))
|
|
24
|
+
if (!keys(fields, ["type", "optional", "minimum", "maximum"]))
|
|
25
25
|
return false;
|
|
26
26
|
if (fields["optional"] !== undefined &&
|
|
27
27
|
typeof fields["optional"] !== "boolean")
|
|
@@ -34,26 +34,82 @@ function isField(value) {
|
|
|
34
34
|
}
|
|
35
35
|
function isInput(value) {
|
|
36
36
|
const input = record(value);
|
|
37
|
-
if (
|
|
37
|
+
if (!keys(input, ["maximumBytes", "fields"]))
|
|
38
38
|
return false;
|
|
39
39
|
const fields = record(input["fields"]);
|
|
40
40
|
return (integer(input["maximumBytes"], { minimum: 1, maximum: 1_048_576 }) &&
|
|
41
41
|
fields !== undefined &&
|
|
42
42
|
Object.values(fields).every(isField));
|
|
43
43
|
}
|
|
44
|
+
function matches(value, pattern) {
|
|
45
|
+
return typeof value === "string" && pattern.test(value);
|
|
46
|
+
}
|
|
47
|
+
function selectors(value, pattern) {
|
|
48
|
+
return (Array.isArray(value) &&
|
|
49
|
+
Array.from(value).every((item) => matches(item, pattern)));
|
|
50
|
+
}
|
|
51
|
+
function isStageLabel(label) {
|
|
52
|
+
return (typeof label === "string" &&
|
|
53
|
+
/^[^\p{C}\p{Zl}\p{Zp}]{1,40}$/u.test(label) &&
|
|
54
|
+
label.trim() === label &&
|
|
55
|
+
!["Other", "Done", "Stopped"].includes(label));
|
|
56
|
+
}
|
|
57
|
+
function isStage(value) {
|
|
58
|
+
const stage = record(value);
|
|
59
|
+
if (!keys(stage, ["name", "label", "operations", "gateKinds"]))
|
|
60
|
+
return false;
|
|
61
|
+
const { name, label } = stage;
|
|
62
|
+
const operations = Object.hasOwn(stage, "operations")
|
|
63
|
+
? stage["operations"]
|
|
64
|
+
: [];
|
|
65
|
+
const gateKinds = Object.hasOwn(stage, "gateKinds") ? stage["gateKinds"] : [];
|
|
66
|
+
return (matches(name, /^[A-Za-z0-9-]{1,64}$/) &&
|
|
67
|
+
isStageLabel(label) &&
|
|
68
|
+
selectors(operations, /^[A-Za-z0-9._:/-]{1,128}$/) &&
|
|
69
|
+
selectors(gateKinds, /^(approval|question)$/) &&
|
|
70
|
+
operations.length + gateKinds.length > 0);
|
|
71
|
+
}
|
|
72
|
+
function unique(values) {
|
|
73
|
+
return new Set(values).size === values.length;
|
|
74
|
+
}
|
|
75
|
+
export function isWorkflowStages(value) {
|
|
76
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 12)
|
|
77
|
+
return false;
|
|
78
|
+
const stages = Array.from(value);
|
|
79
|
+
if (!stages.every(isStage))
|
|
80
|
+
return false;
|
|
81
|
+
return (unique(stages.map((stage) => stage.name)) &&
|
|
82
|
+
unique(stages.map((stage) => stage.label)) &&
|
|
83
|
+
unique(stages.flatMap((stage) => stage.operations ?? [])) &&
|
|
84
|
+
unique(stages.flatMap((stage) => stage.gateKinds ?? [])));
|
|
85
|
+
}
|
|
86
|
+
function isCapabilities(value) {
|
|
87
|
+
return (Array.isArray(value) &&
|
|
88
|
+
value.length > 0 &&
|
|
89
|
+
value.every((capability) => capability === "claude" ||
|
|
90
|
+
capability === "github" ||
|
|
91
|
+
capability === "chats"));
|
|
92
|
+
}
|
|
93
|
+
export function isWorkflowHandoffs(value) {
|
|
94
|
+
return selectors(value, /^[A-Za-z0-9-]{1,64}$/) && unique(value);
|
|
95
|
+
}
|
|
44
96
|
export function isWorkflowDeclaration(value) {
|
|
45
97
|
const declaration = record(value);
|
|
46
|
-
if (declaration
|
|
47
|
-
|
|
98
|
+
if (!keys(declaration, [
|
|
99
|
+
"workflowId",
|
|
100
|
+
"effectLimit",
|
|
101
|
+
"capabilities",
|
|
102
|
+
"input",
|
|
103
|
+
"stages",
|
|
104
|
+
"handoffs",
|
|
105
|
+
]))
|
|
48
106
|
return false;
|
|
49
|
-
const { workflowId, effectLimit, capabilities, input } = declaration;
|
|
50
|
-
return (
|
|
51
|
-
/^[A-Za-z0-9-]{1,64}$/.test(workflowId) &&
|
|
107
|
+
const { workflowId, effectLimit, capabilities, input, handoffs } = declaration;
|
|
108
|
+
return (matches(workflowId, /^[A-Za-z0-9-]{1,64}$/) &&
|
|
52
109
|
integer(effectLimit, { minimum: 1, maximum: 100 }) &&
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isInput(input));
|
|
110
|
+
isCapabilities(capabilities) &&
|
|
111
|
+
isInput(input) &&
|
|
112
|
+
(!Object.hasOwn(declaration, "stages") ||
|
|
113
|
+
isWorkflowStages(declaration["stages"])) &&
|
|
114
|
+
(!Object.hasOwn(declaration, "handoffs") || isWorkflowHandoffs(handoffs)));
|
|
59
115
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface WorkflowReservation {
|
|
2
|
+
readonly baseName?: string;
|
|
3
|
+
readonly workflowId?: string;
|
|
4
|
+
readonly functionName?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkflowDiscoveryIdentity {
|
|
7
|
+
readonly baseName: string;
|
|
8
|
+
readonly workflowId: string | undefined;
|
|
9
|
+
readonly functionNames: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export type WorkflowConflictCode = "workflow-conflict" | "bundled-name-reserved";
|
|
12
|
+
export declare function workflowConflicts(candidates: readonly WorkflowDiscoveryIdentity[], bundled: readonly WorkflowReservation[]): readonly (readonly WorkflowConflictCode[])[];
|
|
13
|
+
export declare const bundledWorkflowIds: {
|
|
14
|
+
readonly ticket: "develop-ticket";
|
|
15
|
+
readonly issue: "develop-issue";
|
|
16
|
+
readonly prompt: "develop-prompt";
|
|
17
|
+
readonly prd: "develop-prd";
|
|
18
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function identityKeys(identity) {
|
|
2
|
+
return [
|
|
3
|
+
...(identity.baseName === undefined ? [] : [`base:${identity.baseName}`]),
|
|
4
|
+
...(identity.workflowId === undefined ? [] : [`id:${identity.workflowId}`]),
|
|
5
|
+
...identity.functionNames.map((name) => `function:${name}`),
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
export function workflowConflicts(candidates, bundled) {
|
|
9
|
+
const keys = candidates.map(identityKeys);
|
|
10
|
+
const counts = new Map();
|
|
11
|
+
for (const names of keys) {
|
|
12
|
+
for (const name of new Set(names))
|
|
13
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
14
|
+
}
|
|
15
|
+
const reserved = new Set(bundled.flatMap((entry) => identityKeys({
|
|
16
|
+
...entry,
|
|
17
|
+
functionNames: entry.functionName === undefined ? [] : [entry.functionName],
|
|
18
|
+
})));
|
|
19
|
+
return keys.map((names) => {
|
|
20
|
+
const codes = [];
|
|
21
|
+
if (names.some((name) => reserved.has(name)))
|
|
22
|
+
codes.push("bundled-name-reserved");
|
|
23
|
+
if (names.some((name) => (counts.get(name) ?? 0) > 1))
|
|
24
|
+
codes.push("workflow-conflict");
|
|
25
|
+
return codes;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export const bundledWorkflowIds = {
|
|
29
|
+
ticket: "develop-ticket",
|
|
30
|
+
issue: "develop-issue",
|
|
31
|
+
prompt: "develop-prompt",
|
|
32
|
+
prd: "develop-prd",
|
|
33
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { record } from "./protocol.js";
|
|
2
|
+
const refusalCodes = new Set([
|
|
3
|
+
"invalid-declaration",
|
|
4
|
+
"forbidden-import",
|
|
5
|
+
"missing-export",
|
|
6
|
+
"unsupported-source",
|
|
7
|
+
"dynamic-effect",
|
|
8
|
+
"undeclared-effect",
|
|
9
|
+
"effect-limit",
|
|
10
|
+
"direct-effect",
|
|
11
|
+
"workflow-conflict",
|
|
12
|
+
"bundled-name-reserved",
|
|
13
|
+
"unsafe-workflow-path",
|
|
14
|
+
"workflow-read-failed",
|
|
15
|
+
"workflow-source-changed",
|
|
16
|
+
"compile-failed",
|
|
17
|
+
]);
|
|
18
|
+
function refusal(value) {
|
|
19
|
+
const { scope, label, codes } = record(value) ?? {};
|
|
20
|
+
if ((scope !== "project" && scope !== "user") ||
|
|
21
|
+
typeof label !== "string" ||
|
|
22
|
+
/^(?:[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.ts|file-[a-f0-9]{16}|workflows\/)$/u.exec(label)?.[0] !== label ||
|
|
23
|
+
!Array.isArray(codes) ||
|
|
24
|
+
codes.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const known = codes.filter((code) => typeof code === "string" && refusalCodes.has(code));
|
|
27
|
+
if (known.length !== codes.length)
|
|
28
|
+
return [];
|
|
29
|
+
return [{ scope, label, codes: [...new Set(known)].sort() }];
|
|
30
|
+
}
|
|
31
|
+
export function workflowRefusals(value) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return [];
|
|
34
|
+
return value.flatMap(refusal).sort((left, right) => {
|
|
35
|
+
const a = `${left.scope}\0${left.label}`;
|
|
36
|
+
const b = `${right.scope}\0${right.label}`;
|
|
37
|
+
return a < b ? -1 : Number(a > b);
|
|
38
|
+
});
|
|
39
|
+
}
|
package/dist/src/workflows.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { loadCycle, type CycleLoad } from "./steps/load-cycle.ts";
|
|
2
2
|
export { protectedEffect } from "./steps/protected-effect.ts";
|
|
3
3
|
export { decisionGate } from "./steps/decision-gate.ts";
|
|
4
|
-
export
|
|
4
|
+
export { handoff } from "./steps/handoff.ts";
|
|
5
|
+
export type { GateRequest, GateResult, HandoffRequest, HandoffValue, ProtectedRequest, ProtectedResult, ProtectedScope, } from "./step-bridge.ts";
|
|
5
6
|
export type { CycleOutcome, DevelopmentInput } from "./workflow-contracts.ts";
|
|
6
7
|
export { ticketStatus, type TicketStatus } from "./ticket-status.ts";
|
package/dist/src/workflows.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { loadCycle } from "./steps/load-cycle.js";
|
|
2
2
|
export { protectedEffect } from "./steps/protected-effect.js";
|
|
3
3
|
export { decisionGate } from "./steps/decision-gate.js";
|
|
4
|
+
export { handoff } from "./steps/handoff.js";
|
|
4
5
|
export { ticketStatus } from "./ticket-status.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type WorkflowCandidateCode } from "../src/workflow-candidate.ts";
|
|
2
|
+
import { type CandidatePackageOptions } from "./workflow-candidate-program.ts";
|
|
3
|
+
export type WorkflowCandidateAdmission = {
|
|
4
|
+
readonly kind: "admitted";
|
|
5
|
+
readonly artifactDigest: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "refused";
|
|
8
|
+
readonly codes: readonly (WorkflowCandidateCode | "compile-failed")[];
|
|
9
|
+
};
|
|
10
|
+
export declare function admitWorkflowCandidate(input: {
|
|
11
|
+
readonly source: string;
|
|
12
|
+
readonly declaration: unknown;
|
|
13
|
+
}, installed?: CandidatePackageOptions): Promise<WorkflowCandidateAdmission>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { findPackageJSON } from "node:module";
|
|
6
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { checkWorkflowCandidate, } from "../src/workflow-candidate.js";
|
|
9
|
+
import { candidatePackageRoot, } from "./workflow-candidate-program.js";
|
|
10
|
+
import { analyzeWorkflowCandidate } from "./workflow-candidate-source.js";
|
|
11
|
+
import { isWorkflowManifest } from "./workflow-manifest.js";
|
|
12
|
+
export async function admitWorkflowCandidate(input, installed = {}) {
|
|
13
|
+
const { source, declaration } = input;
|
|
14
|
+
if (Buffer.from(source, "utf8").toString("utf8") !== source)
|
|
15
|
+
return { kind: "refused", codes: ["unsupported-source"] };
|
|
16
|
+
try {
|
|
17
|
+
const packageRoot = resolve(installed.packageRoot ?? candidatePackageRoot);
|
|
18
|
+
const facts = analyzeWorkflowCandidate(source, { packageRoot });
|
|
19
|
+
const codes = checkWorkflowCandidate({ declaration, ...facts });
|
|
20
|
+
if (codes.length > 0)
|
|
21
|
+
return { kind: "refused", codes };
|
|
22
|
+
return await compiledCandidate(source, packageRoot);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { kind: "refused", codes: ["compile-failed"] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function compiledCandidate(source, packageRoot) {
|
|
29
|
+
const directory = await realpath(await mkdtemp(join(tmpdir(), "cloving-candidate-")));
|
|
30
|
+
try {
|
|
31
|
+
await prepareCandidateProject(directory, { source, packageRoot });
|
|
32
|
+
if (!(await compile(directory)))
|
|
33
|
+
return { kind: "refused", codes: ["compile-failed"] };
|
|
34
|
+
const artifactDirectory = join(directory, "artifacts");
|
|
35
|
+
const manifest = JSON.parse(await readFile(join(artifactDirectory, "manifest.json"), "utf8"));
|
|
36
|
+
if (!isWorkflowManifest(manifest) ||
|
|
37
|
+
Object.values(manifest.workflows ?? {}).flatMap((entries) => Object.values(entries)).length !== 1)
|
|
38
|
+
return { kind: "refused", codes: ["compile-failed"] };
|
|
39
|
+
const digest = createHash("sha256");
|
|
40
|
+
for (const name of [
|
|
41
|
+
"manifest.json",
|
|
42
|
+
"flow.mjs",
|
|
43
|
+
"steps.mjs",
|
|
44
|
+
"client.mjs",
|
|
45
|
+
]) {
|
|
46
|
+
const bytes = await readFile(join(artifactDirectory, name));
|
|
47
|
+
digest.update(JSON.stringify([name, bytes.length]));
|
|
48
|
+
digest.update(bytes);
|
|
49
|
+
}
|
|
50
|
+
return { kind: "admitted", artifactDigest: digest.digest("hex") };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await rm(directory, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function prepareCandidateProject(directory, input) {
|
|
57
|
+
const { source, packageRoot } = input;
|
|
58
|
+
await mkdir(join(directory, "workflows"));
|
|
59
|
+
await mkdir(join(directory, "node_modules"));
|
|
60
|
+
await symlink(packageRoot, join(directory, "node_modules/cloving"));
|
|
61
|
+
const runtime = findPackageJSON("workflow", import.meta.url);
|
|
62
|
+
if (!runtime)
|
|
63
|
+
throw new Error("candidate-runtime-unavailable");
|
|
64
|
+
await symlink(dirname(runtime), join(directory, "node_modules/workflow"));
|
|
65
|
+
await writeFile(join(directory, "package.json"), '{"name":"candidate-project","private":true,"type":"module"}');
|
|
66
|
+
await writeFile(join(directory, "tsconfig.json"), JSON.stringify({
|
|
67
|
+
compilerOptions: {
|
|
68
|
+
target: "ES2024",
|
|
69
|
+
module: "ESNext",
|
|
70
|
+
moduleResolution: "Bundler",
|
|
71
|
+
types: [],
|
|
72
|
+
},
|
|
73
|
+
include: ["workflows/candidate.ts"],
|
|
74
|
+
}));
|
|
75
|
+
await writeFile(join(directory, "workflows/candidate.ts"), source, "utf8");
|
|
76
|
+
}
|
|
77
|
+
function compile(directory) {
|
|
78
|
+
const extension = extname(fileURLToPath(import.meta.url));
|
|
79
|
+
const executable = fileURLToPath(new URL(`./compile-workflows${extension}`, import.meta.url));
|
|
80
|
+
return new Promise((settle) => {
|
|
81
|
+
const child = spawn(process.execPath, [executable, "workflows", "artifacts"], {
|
|
82
|
+
cwd: directory,
|
|
83
|
+
env: { PATH: dirname(process.execPath), TMPDIR: tmpdir() },
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
timeout: 30000,
|
|
86
|
+
});
|
|
87
|
+
child.once("error", () => {
|
|
88
|
+
settle(false);
|
|
89
|
+
});
|
|
90
|
+
child.once("close", (code) => {
|
|
91
|
+
settle(code === 0);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type DeclaredWorkflow } from "../src/workflow-declaration.ts";
|
|
2
|
+
import type { WorkflowRefusal } from "../src/workflow-refusals.ts";
|
|
3
|
+
interface ProjectSettings {
|
|
4
|
+
readonly product: string;
|
|
5
|
+
readonly project: string;
|
|
6
|
+
readonly repository: string;
|
|
7
|
+
readonly home: string;
|
|
8
|
+
readonly xdgConfigHome?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface RegisteredWorkflows {
|
|
11
|
+
readonly repository: string;
|
|
12
|
+
readonly manifests: Readonly<Record<string, string>>;
|
|
13
|
+
readonly declarations: readonly DeclaredWorkflow[];
|
|
14
|
+
readonly workflowRefusals: readonly WorkflowRefusal[];
|
|
15
|
+
}
|
|
16
|
+
export declare function preparedProject(settings: ProjectSettings): Promise<RegisteredWorkflows>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFile, mkdir, readFile, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { findPackageJSON } from "node:module";
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import packageMetadata from "#package" with { type: "json" };
|
|
7
|
+
import { record } from "../src/protocol.js";
|
|
8
|
+
import { isWorkflowDeclaration, } from "../src/workflow-declaration.js";
|
|
9
|
+
import { bundledWorkflowIds, } from "../src/workflow-discovery.js";
|
|
10
|
+
import { workflowDeclarations, workflowFiles, workflowManifests, } from "./self-host-workflows.js";
|
|
11
|
+
import { loadWorkflowFolders, } from "./workflow-folders.js";
|
|
12
|
+
import { isWorkflowManifest } from "./workflow-manifest.js";
|
|
13
|
+
import { retainWorkflowBundle } from "./workflow-bundles.js";
|
|
14
|
+
import { workflowBundleDependencies, workflowPackageRoot, } from "./workflow-bundle-dependencies.js";
|
|
15
|
+
async function initializeProject(settings) {
|
|
16
|
+
const { project, product } = settings;
|
|
17
|
+
await rm(project, { recursive: true, force: true });
|
|
18
|
+
await mkdir(join(project, "workflows"), { recursive: true, mode: 0o700 });
|
|
19
|
+
await writeFile(join(project, "package.json"), '{"name":"cloving-self-host","private":true,"type":"module"}');
|
|
20
|
+
const files = await workflowFiles(join(product, "workflows"));
|
|
21
|
+
for (const file of files)
|
|
22
|
+
await copyWorkflow(settings, file);
|
|
23
|
+
const modules = join(project, "node_modules");
|
|
24
|
+
await mkdir(join(modules, "cloving"), { recursive: true });
|
|
25
|
+
for (const name of Object.keys(packageMetadata.dependencies)) {
|
|
26
|
+
const location = findPackageJSON(name, join(product, "package.json"));
|
|
27
|
+
if (location === undefined)
|
|
28
|
+
throw new Error(`missing-runtime-dependency: ${name}`);
|
|
29
|
+
await mkdir(dirname(join(modules, name)), { recursive: true });
|
|
30
|
+
await symlink(dirname(location), join(modules, name));
|
|
31
|
+
}
|
|
32
|
+
await symlink(join(product, "src"), join(modules, "cloving", "src"));
|
|
33
|
+
await writeFile(join(modules, "cloving", "package.json"), JSON.stringify({
|
|
34
|
+
name: packageMetadata.name,
|
|
35
|
+
version: packageMetadata.version,
|
|
36
|
+
type: "module",
|
|
37
|
+
exports: Object.fromEntries(Object.keys(packageMetadata.exports)
|
|
38
|
+
.filter((subpath) => subpath.startsWith("./workflows"))
|
|
39
|
+
.map((subpath) => [
|
|
40
|
+
subpath,
|
|
41
|
+
`./src/${subpath.slice("./workflows/".length) || "workflows"}${extname(import.meta.filename)}`,
|
|
42
|
+
])),
|
|
43
|
+
}));
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
async function compile(settings) {
|
|
47
|
+
const { product, project } = settings;
|
|
48
|
+
await rm(join(project, "artifacts"), { recursive: true, force: true });
|
|
49
|
+
await new Promise((settle, reject) => {
|
|
50
|
+
const child = spawn(process.execPath, [
|
|
51
|
+
join(product, `tools/compile-workflows${extname(import.meta.filename)}`),
|
|
52
|
+
"workflows",
|
|
53
|
+
"artifacts",
|
|
54
|
+
], { cwd: project, env: process.env, stdio: "ignore", timeout: 30000 });
|
|
55
|
+
child.once("error", reject);
|
|
56
|
+
child.once("close", (code) => {
|
|
57
|
+
if (code === 0)
|
|
58
|
+
settle();
|
|
59
|
+
else
|
|
60
|
+
reject(new Error("The workflows did not compile."));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const compiled = JSON.parse(await readFile(join(project, "artifacts/manifest.json"), "utf8"));
|
|
64
|
+
if (!isWorkflowManifest(compiled))
|
|
65
|
+
throw new Error("The compiled workflow manifest is invalid.");
|
|
66
|
+
return compiled;
|
|
67
|
+
}
|
|
68
|
+
function reservations(input) {
|
|
69
|
+
const { files, manifests, declarations } = input;
|
|
70
|
+
return [
|
|
71
|
+
...files.map((file) => ({
|
|
72
|
+
baseName: basename(file, extname(file)),
|
|
73
|
+
})),
|
|
74
|
+
...Object.entries(manifests).map(([functionName, manifest]) => ({
|
|
75
|
+
baseName: manifest.name,
|
|
76
|
+
workflowId: manifest.workflowId,
|
|
77
|
+
functionName,
|
|
78
|
+
})),
|
|
79
|
+
...[
|
|
80
|
+
...Object.values(bundledWorkflowIds),
|
|
81
|
+
...declarations.map((item) => item.workflowId),
|
|
82
|
+
].map((workflowId) => ({ workflowId })),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
async function stageCandidates(project, candidates) {
|
|
86
|
+
if (candidates.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
const directory = join(project, "workflows", "operators");
|
|
89
|
+
await mkdir(directory, { mode: 0o700 });
|
|
90
|
+
const staged = [];
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const name = createHash("sha256")
|
|
93
|
+
.update(JSON.stringify([
|
|
94
|
+
candidate.scope,
|
|
95
|
+
candidate.baseName,
|
|
96
|
+
candidate.source,
|
|
97
|
+
candidate.declaration,
|
|
98
|
+
]))
|
|
99
|
+
.digest("hex");
|
|
100
|
+
const file = join(directory, `${name}.ts`);
|
|
101
|
+
await writeFile(file, candidate.source);
|
|
102
|
+
await writeFile(join(directory, `${name}.declaration.json`), candidate.declaration);
|
|
103
|
+
staged.push({ candidate, file });
|
|
104
|
+
}
|
|
105
|
+
return staged;
|
|
106
|
+
}
|
|
107
|
+
function operatorDeclaration(staged, compiled) {
|
|
108
|
+
const { candidate, project } = staged;
|
|
109
|
+
const declaration = record(JSON.parse(candidate.declaration))?.["workflow"];
|
|
110
|
+
const entries = Object.entries(compiled.workflows ?? {}).filter(([file]) => resolve(project, file) === staged.file);
|
|
111
|
+
const functions = entries.flatMap(([, functions]) => Object.values(functions));
|
|
112
|
+
const [workflow] = functions;
|
|
113
|
+
if (!isWorkflowDeclaration(declaration) ||
|
|
114
|
+
functions.length !== 1 ||
|
|
115
|
+
workflow === undefined)
|
|
116
|
+
throw new Error("The admitted workflow is missing from the compiled bundle.");
|
|
117
|
+
return {
|
|
118
|
+
...declaration,
|
|
119
|
+
capabilities: [...new Set(declaration.capabilities)],
|
|
120
|
+
functionName: workflow.workflowId.slice(workflow.workflowId.lastIndexOf("//") + 2),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function copyWorkflow(settings, file) {
|
|
124
|
+
const { product, project } = settings;
|
|
125
|
+
await copyFile(join(product, "workflows", file), join(project, "workflows", file));
|
|
126
|
+
const sidecar = `${basename(file, extname(file))}.declaration.json`;
|
|
127
|
+
const declaration = await readFile(join(product, "workflows", sidecar)).catch((error) => {
|
|
128
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
129
|
+
return undefined;
|
|
130
|
+
throw error;
|
|
131
|
+
});
|
|
132
|
+
if (declaration !== undefined)
|
|
133
|
+
await writeFile(join(project, "workflows", sidecar), declaration);
|
|
134
|
+
}
|
|
135
|
+
export async function preparedProject(settings) {
|
|
136
|
+
const { product, project } = settings;
|
|
137
|
+
const files = await initializeProject(settings);
|
|
138
|
+
let compiled = await compile(settings);
|
|
139
|
+
const bundled = workflowManifests(compiled);
|
|
140
|
+
const declarations = await workflowDeclarations(join(project, "workflows"), bundled);
|
|
141
|
+
const folders = await loadWorkflowFolders({
|
|
142
|
+
...settings,
|
|
143
|
+
bundled: reservations({ files, manifests: bundled, declarations }),
|
|
144
|
+
}, { packageRoot: await workflowPackageRoot(product) });
|
|
145
|
+
const staged = await stageCandidates(project, folders.candidates);
|
|
146
|
+
if (staged.length > 0)
|
|
147
|
+
compiled = await compile(settings);
|
|
148
|
+
const combined = [
|
|
149
|
+
...declarations,
|
|
150
|
+
...staged.map((candidate) => operatorDeclaration({ ...candidate, project }, compiled)),
|
|
151
|
+
];
|
|
152
|
+
const manifests = {};
|
|
153
|
+
for (const [functionName, manifest] of Object.entries(workflowManifests(compiled, combined))) {
|
|
154
|
+
const path = join(project, `${manifest.name}.workflow.json`);
|
|
155
|
+
await writeFile(path, JSON.stringify(manifest));
|
|
156
|
+
Object.defineProperty(manifests, functionName, {
|
|
157
|
+
value: path,
|
|
158
|
+
enumerable: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
await writeFile(join(project, "declarations.json"), JSON.stringify(combined));
|
|
162
|
+
await writeFile(join(project, "dependencies.json"), await workflowBundleDependencies({
|
|
163
|
+
product,
|
|
164
|
+
packages: Object.keys(packageMetadata.dependencies),
|
|
165
|
+
}));
|
|
166
|
+
await retainWorkflowBundle({
|
|
167
|
+
project,
|
|
168
|
+
repository: folders.repository,
|
|
169
|
+
directory: join(dirname(project), "workflow-bundles"),
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
repository: folders.repository,
|
|
173
|
+
manifests,
|
|
174
|
+
declarations: combined,
|
|
175
|
+
workflowRefusals: folders.refusals,
|
|
176
|
+
};
|
|
177
|
+
}
|