@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.1
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/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -1,9 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
isWorkflowComplete,
|
|
3
|
+
isWorkflowDeclaration,
|
|
4
|
+
isWorkflowFail,
|
|
5
|
+
isWorkflowNext,
|
|
6
|
+
type NornAnyWorkflowDeclaration,
|
|
7
|
+
type NornDispose,
|
|
8
|
+
type NornInspectedWorkflowInfo,
|
|
9
|
+
type NornRegisteredWorkflowInfo,
|
|
10
|
+
type NornRunComplete,
|
|
11
|
+
type NornRunFail,
|
|
12
|
+
type NornRunNext,
|
|
13
|
+
type NornWorkflowPaths,
|
|
14
|
+
type NornWorkflowContext,
|
|
15
|
+
type NornWorkflowGateInfo,
|
|
16
|
+
type NornWorkflowSource,
|
|
17
|
+
type NornWorkflowScopeInfo,
|
|
18
|
+
type NornProjectConfigurationInfo,
|
|
19
|
+
} from "@vimhead.dev/norn";
|
|
20
|
+
import {
|
|
21
|
+
assertWorkflowMetadata,
|
|
22
|
+
inspectSchema,
|
|
23
|
+
isPlainObject,
|
|
24
|
+
schemaShape,
|
|
25
|
+
schemaType,
|
|
26
|
+
unwrapSchema,
|
|
27
|
+
} from "@vimhead.dev/norn/schema";
|
|
3
28
|
import type { TSchema } from "typebox";
|
|
4
29
|
import { Value } from "typebox/value";
|
|
5
30
|
|
|
6
|
-
export type NornRegisteredConfiguration = {
|
|
31
|
+
export type NornRegisteredConfiguration = {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly schema: TSchema | undefined;
|
|
34
|
+
readonly input: unknown;
|
|
35
|
+
readonly value: unknown;
|
|
36
|
+
};
|
|
7
37
|
export type NornRegisteredWorkflow = {
|
|
8
38
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
9
39
|
readonly configuration: NornRegisteredConfiguration;
|
|
@@ -11,27 +41,72 @@ export type NornRegisteredWorkflow = {
|
|
|
11
41
|
readonly source: NornWorkflowSource | undefined;
|
|
12
42
|
};
|
|
13
43
|
|
|
14
|
-
type NornRegisteredScope = {
|
|
44
|
+
type NornRegisteredScope = {
|
|
45
|
+
readonly definition: NornWorkflowScopeInfo;
|
|
46
|
+
readonly configuration: NornRegisteredConfiguration;
|
|
47
|
+
};
|
|
15
48
|
|
|
16
|
-
export function decodeConfiguration(input: {
|
|
49
|
+
export function decodeConfiguration(input: {
|
|
50
|
+
readonly key: string;
|
|
51
|
+
readonly schema: TSchema | undefined;
|
|
52
|
+
readonly value: unknown;
|
|
53
|
+
}): NornRegisteredConfiguration {
|
|
17
54
|
if (!input.schema) {
|
|
18
|
-
if (input.value !== undefined)
|
|
19
|
-
|
|
55
|
+
if (input.value !== undefined)
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Norn config provided without config schema: ${input.key}`,
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
key: input.key,
|
|
61
|
+
schema: undefined,
|
|
62
|
+
input: undefined,
|
|
63
|
+
value: undefined,
|
|
64
|
+
};
|
|
20
65
|
}
|
|
21
|
-
const value =
|
|
22
|
-
|
|
66
|
+
const value =
|
|
67
|
+
input.value === undefined &&
|
|
68
|
+
schemaType(unwrapSchema(input.schema)) === "object"
|
|
69
|
+
? {}
|
|
70
|
+
: input.value;
|
|
71
|
+
return {
|
|
72
|
+
key: input.key,
|
|
73
|
+
schema: input.schema,
|
|
74
|
+
input: value,
|
|
75
|
+
value: Value.Decode(input.schema, value),
|
|
76
|
+
};
|
|
23
77
|
}
|
|
24
78
|
|
|
25
|
-
export function assertCompatibleConfigurationOwners(
|
|
26
|
-
|
|
79
|
+
export function assertCompatibleConfigurationOwners(
|
|
80
|
+
workflows: readonly NornAnyWorkflowDeclaration[],
|
|
81
|
+
): void {
|
|
82
|
+
const owners = new Map<
|
|
83
|
+
string,
|
|
84
|
+
{ readonly kind: "workflow" | "scope"; readonly definition: object }
|
|
85
|
+
>();
|
|
27
86
|
for (const workflow of workflows) {
|
|
28
87
|
for (const owner of [
|
|
29
88
|
{ key: workflow.id, kind: "workflow" as const, definition: workflow },
|
|
30
|
-
...(workflow.scope
|
|
89
|
+
...(workflow.scope
|
|
90
|
+
? [
|
|
91
|
+
{
|
|
92
|
+
key: workflow.scope.id,
|
|
93
|
+
kind: "scope" as const,
|
|
94
|
+
definition: workflow.scope,
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
31
98
|
]) {
|
|
32
99
|
const previous = owners.get(owner.key);
|
|
33
|
-
if (previous && previous.kind !== owner.kind)
|
|
34
|
-
|
|
100
|
+
if (previous && previous.kind !== owner.kind)
|
|
101
|
+
throw new Error(`Ambiguous configuration key: ${owner.key}`);
|
|
102
|
+
if (
|
|
103
|
+
previous &&
|
|
104
|
+
owner.kind === "scope" &&
|
|
105
|
+
previous.definition !== owner.definition
|
|
106
|
+
)
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Duplicate workflow scope id: ${owner.key}; import one shared scope definition`,
|
|
109
|
+
);
|
|
35
110
|
owners.set(owner.key, owner);
|
|
36
111
|
}
|
|
37
112
|
}
|
|
@@ -39,102 +114,192 @@ export function assertCompatibleConfigurationOwners(workflows: readonly NornAnyW
|
|
|
39
114
|
|
|
40
115
|
export type NornWorkflowStepResult =
|
|
41
116
|
| NornRunNext
|
|
42
|
-
| {
|
|
43
|
-
|
|
117
|
+
| {
|
|
118
|
+
readonly type: "complete";
|
|
119
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
120
|
+
readonly metadata?: NornRunComplete["metadata"];
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
readonly type: "fail";
|
|
124
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
125
|
+
readonly metadata: NornRunFail["metadata"];
|
|
126
|
+
};
|
|
44
127
|
|
|
45
128
|
type NornExecutionContextInput = {
|
|
46
|
-
readonly execution: Pick<
|
|
129
|
+
readonly execution: Pick<
|
|
130
|
+
NornWorkflowContext,
|
|
131
|
+
"run" | "agents" | "commands" | "logs"
|
|
132
|
+
>;
|
|
47
133
|
readonly paths: NornWorkflowPaths;
|
|
48
134
|
readonly args: unknown;
|
|
49
135
|
readonly configOverride: unknown;
|
|
50
136
|
};
|
|
51
137
|
|
|
52
|
-
type NornWorkflowExecutionInput = NornExecutionContextInput & {
|
|
138
|
+
type NornWorkflowExecutionInput = NornExecutionContextInput & {
|
|
139
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
140
|
+
};
|
|
53
141
|
|
|
54
142
|
export class NornWorkflowRegistry {
|
|
55
143
|
private readonly entries = new Map<string, NornRegisteredWorkflow>();
|
|
56
144
|
private readonly scopes = new Map<string, NornRegisteredScope>();
|
|
57
145
|
|
|
58
|
-
register(input: {
|
|
146
|
+
register(input: {
|
|
147
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
148
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
149
|
+
readonly source: NornWorkflowSource | undefined;
|
|
150
|
+
}): NornDispose {
|
|
59
151
|
const { workflow, config, source } = input;
|
|
60
152
|
assertWorkflowDefinition(workflow);
|
|
61
|
-
if (this.entries.has(workflow.id))
|
|
62
|
-
|
|
153
|
+
if (this.entries.has(workflow.id))
|
|
154
|
+
throw new Error(`Workflow already registered: ${workflow.id}`);
|
|
155
|
+
assertCompatibleConfigurationOwners([
|
|
156
|
+
...this.sortedEntries().map((entry) => entry.workflow),
|
|
157
|
+
workflow,
|
|
158
|
+
]);
|
|
63
159
|
const entry: NornRegisteredWorkflow = {
|
|
64
|
-
workflow,
|
|
65
|
-
|
|
160
|
+
workflow,
|
|
161
|
+
source,
|
|
162
|
+
scopeId: workflow.scope?.id,
|
|
163
|
+
configuration: decodeConfiguration({
|
|
164
|
+
key: workflow.id,
|
|
165
|
+
schema: workflow.config,
|
|
166
|
+
value: configurationInput(config, workflow.id),
|
|
167
|
+
}),
|
|
66
168
|
};
|
|
67
169
|
if (workflow.scope && !this.scopes.has(workflow.scope.id)) {
|
|
68
170
|
this.scopes.set(workflow.scope.id, {
|
|
69
171
|
definition: workflow.scope,
|
|
70
|
-
configuration: decodeConfiguration({
|
|
172
|
+
configuration: decodeConfiguration({
|
|
173
|
+
key: workflow.scope.id,
|
|
174
|
+
schema: workflow.scope.config,
|
|
175
|
+
value: configurationInput(config, workflow.scope.id),
|
|
176
|
+
}),
|
|
71
177
|
});
|
|
72
178
|
}
|
|
73
179
|
this.entries.set(workflow.id, entry);
|
|
74
180
|
return () => {
|
|
75
181
|
if (this.entries.get(workflow.id) !== entry) return;
|
|
76
182
|
this.entries.delete(workflow.id);
|
|
77
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
entry.scopeId &&
|
|
185
|
+
![...this.entries.values()].some(
|
|
186
|
+
(other) => other.scopeId === entry.scopeId,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
this.scopes.delete(entry.scopeId);
|
|
78
190
|
};
|
|
79
191
|
}
|
|
80
192
|
|
|
81
|
-
list(
|
|
82
|
-
|
|
193
|
+
list(
|
|
194
|
+
options: { readonly entrypointsOnly?: boolean } = {},
|
|
195
|
+
): NornRegisteredWorkflowInfo[] {
|
|
196
|
+
return (
|
|
197
|
+
options.entrypointsOnly ? this.launchableEntries() : this.sortedEntries()
|
|
198
|
+
).map(workflowInfo);
|
|
83
199
|
}
|
|
84
200
|
|
|
85
201
|
inspect(workflowId: string): NornInspectedWorkflowInfo | undefined {
|
|
86
202
|
const entry = this.entries.get(workflowId);
|
|
87
203
|
const scope = entry?.scopeId ? this.scopes.get(entry.scopeId) : undefined;
|
|
88
|
-
return entry
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
204
|
+
return entry
|
|
205
|
+
? {
|
|
206
|
+
...workflowInfo(entry),
|
|
207
|
+
argsSchema: inspectSchema(entry.workflow.args),
|
|
208
|
+
configSchema: entry.configuration.schema
|
|
209
|
+
? inspectSchema(entry.configuration.schema)
|
|
210
|
+
: null,
|
|
211
|
+
scope: scope
|
|
212
|
+
? {
|
|
213
|
+
id: scope.definition.id,
|
|
214
|
+
configKey: scope.configuration.key,
|
|
215
|
+
configSchema: scope.configuration.schema
|
|
216
|
+
? inspectSchema(scope.configuration.schema)
|
|
217
|
+
: null,
|
|
218
|
+
}
|
|
219
|
+
: undefined,
|
|
220
|
+
gate: gateInfo(entry.workflow),
|
|
221
|
+
}
|
|
222
|
+
: undefined;
|
|
99
223
|
}
|
|
100
224
|
|
|
101
225
|
configurationInfos(): NornProjectConfigurationInfo[] {
|
|
102
226
|
return [
|
|
103
|
-
...this.sortedEntries().map(entry => ({
|
|
104
|
-
|
|
105
|
-
|
|
227
|
+
...this.sortedEntries().map((entry) => ({
|
|
228
|
+
configuration: entry.configuration,
|
|
229
|
+
scopeId: null,
|
|
230
|
+
})),
|
|
231
|
+
...[...this.scopes.values()].map((scope) => ({
|
|
232
|
+
configuration: scope.configuration,
|
|
233
|
+
scopeId: scope.definition.id,
|
|
234
|
+
})),
|
|
235
|
+
].map(({ configuration, scopeId }) => ({
|
|
236
|
+
key: configuration.key,
|
|
237
|
+
scopeId,
|
|
238
|
+
configSchema: configuration.schema
|
|
239
|
+
? inspectSchema(configuration.schema)
|
|
240
|
+
: null,
|
|
241
|
+
config: configuration.value,
|
|
242
|
+
}));
|
|
106
243
|
}
|
|
107
244
|
|
|
108
|
-
launchableEntries(): NornRegisteredWorkflow[] {
|
|
109
|
-
|
|
245
|
+
launchableEntries(): NornRegisteredWorkflow[] {
|
|
246
|
+
return this.sortedEntries().filter(
|
|
247
|
+
({ workflow }) => workflow.entrypoint !== false,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined {
|
|
251
|
+
return this.entries.get(workflowId)?.workflow;
|
|
252
|
+
}
|
|
110
253
|
|
|
111
254
|
async describeGate(input: NornWorkflowExecutionInput): Promise<string> {
|
|
112
255
|
const { workflow } = input;
|
|
113
256
|
const entry = this.requireEntry(workflow.id);
|
|
114
|
-
if (!workflow.gate)
|
|
257
|
+
if (!workflow.gate)
|
|
258
|
+
throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
115
259
|
const context = this.createExecutionContext({ ...input, entry });
|
|
116
|
-
const description =
|
|
117
|
-
|
|
260
|
+
const description =
|
|
261
|
+
(await entry.workflow.gate?.describe?.(context)) ?? workflow.id;
|
|
262
|
+
if (typeof description !== "string" || description.trim().length === 0)
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Workflow gate description must not be empty: ${workflow.id}`,
|
|
265
|
+
);
|
|
118
266
|
return description.trim();
|
|
119
267
|
}
|
|
120
268
|
|
|
121
|
-
async execute(
|
|
269
|
+
async execute(
|
|
270
|
+
input: NornWorkflowExecutionInput,
|
|
271
|
+
): Promise<NornWorkflowStepResult> {
|
|
122
272
|
const { workflow } = input;
|
|
123
273
|
const entry = this.requireEntry(workflow.id);
|
|
124
|
-
const result = await entry.workflow.execute(
|
|
274
|
+
const result = await entry.workflow.execute(
|
|
275
|
+
this.createExecutionContext({ ...input, entry }),
|
|
276
|
+
);
|
|
125
277
|
if (isWorkflowNext(result)) return result;
|
|
126
|
-
if (isWorkflowComplete(result))
|
|
127
|
-
|
|
278
|
+
if (isWorkflowComplete(result))
|
|
279
|
+
return { type: "complete", workflow, metadata: result.metadata };
|
|
280
|
+
if (isWorkflowFail(result))
|
|
281
|
+
return { type: "fail", workflow, metadata: result.metadata };
|
|
128
282
|
throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
|
|
129
283
|
}
|
|
130
284
|
|
|
131
|
-
private createExecutionContext(
|
|
285
|
+
private createExecutionContext(
|
|
286
|
+
input: NornExecutionContextInput & {
|
|
287
|
+
readonly entry: NornRegisteredWorkflow;
|
|
288
|
+
},
|
|
289
|
+
): NornWorkflowContext {
|
|
132
290
|
const { entry, execution, paths, configOverride } = input;
|
|
133
291
|
const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : undefined;
|
|
134
292
|
return {
|
|
135
293
|
args: Value.Decode(entry.workflow.args, input.args),
|
|
136
294
|
config: parseExecutionConfig(entry.configuration, configOverride),
|
|
137
|
-
...(scope
|
|
295
|
+
...(scope
|
|
296
|
+
? {
|
|
297
|
+
scope: {
|
|
298
|
+
id: scope.definition.id,
|
|
299
|
+
config: parseExecutionConfig(scope.configuration, configOverride),
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
138
303
|
paths,
|
|
139
304
|
run: execution.run,
|
|
140
305
|
agents: execution.agents,
|
|
@@ -148,55 +313,101 @@ export class NornWorkflowRegistry {
|
|
|
148
313
|
if (!entry) throw new Error(`Unknown workflow: ${id}`);
|
|
149
314
|
return entry;
|
|
150
315
|
}
|
|
151
|
-
private sortedEntries(): NornRegisteredWorkflow[] {
|
|
316
|
+
private sortedEntries(): NornRegisteredWorkflow[] {
|
|
317
|
+
return [...this.entries.values()].sort((left, right) =>
|
|
318
|
+
left.workflow.id.localeCompare(right.workflow.id),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
152
321
|
}
|
|
153
322
|
|
|
154
|
-
function configurationInput(
|
|
323
|
+
function configurationInput(
|
|
324
|
+
config: Readonly<Record<string, unknown>>,
|
|
325
|
+
key: string,
|
|
326
|
+
): unknown {
|
|
155
327
|
return Object.hasOwn(config, key) ? config[key] : undefined;
|
|
156
328
|
}
|
|
157
329
|
|
|
158
|
-
function parseExecutionConfig(
|
|
159
|
-
|
|
160
|
-
|
|
330
|
+
function parseExecutionConfig(
|
|
331
|
+
configuration: NornRegisteredConfiguration,
|
|
332
|
+
overrides: unknown,
|
|
333
|
+
): unknown {
|
|
334
|
+
if (overrides !== undefined && !isPlainObject(overrides))
|
|
335
|
+
throw new Error(
|
|
336
|
+
"Run config override must be an object keyed by workflow or scope id",
|
|
337
|
+
);
|
|
338
|
+
const override = overrides
|
|
339
|
+
? configurationInput(overrides, configuration.key)
|
|
340
|
+
: undefined;
|
|
161
341
|
if (override === undefined) return configuration.value;
|
|
162
|
-
if (!configuration.schema)
|
|
163
|
-
|
|
342
|
+
if (!configuration.schema)
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Run config override provided without config schema: ${configuration.key}`,
|
|
345
|
+
);
|
|
346
|
+
const input =
|
|
347
|
+
isPlainObject(configuration.input) && isPlainObject(override)
|
|
348
|
+
? deepMerge(configuration.input, override)
|
|
349
|
+
: override;
|
|
164
350
|
return Value.Decode(configuration.schema, input);
|
|
165
351
|
}
|
|
166
352
|
|
|
167
|
-
function deepMerge(
|
|
353
|
+
function deepMerge(
|
|
354
|
+
base: Record<string, unknown>,
|
|
355
|
+
override: Record<string, unknown>,
|
|
356
|
+
): Record<string, unknown> {
|
|
168
357
|
const result = { ...base };
|
|
169
|
-
for (const [key, value] of Object.entries(override))
|
|
358
|
+
for (const [key, value] of Object.entries(override))
|
|
359
|
+
result[key] =
|
|
360
|
+
isPlainObject(result[key]) && isPlainObject(value)
|
|
361
|
+
? deepMerge(result[key], value)
|
|
362
|
+
: value;
|
|
170
363
|
return result;
|
|
171
364
|
}
|
|
172
365
|
|
|
173
|
-
export function assertWorkflowDefinition(
|
|
366
|
+
export function assertWorkflowDefinition(
|
|
367
|
+
workflow: NornAnyWorkflowDeclaration,
|
|
368
|
+
): void {
|
|
174
369
|
assertWorkflowMetadata(workflow);
|
|
175
|
-
if (!isWorkflowDeclaration(workflow))
|
|
370
|
+
if (!isWorkflowDeclaration(workflow))
|
|
371
|
+
throw new Error("Invalid workflow definition");
|
|
176
372
|
assertGateWorkflow(workflow);
|
|
177
373
|
}
|
|
178
374
|
|
|
179
375
|
function assertGateWorkflow(workflow: NornAnyWorkflowDeclaration): void {
|
|
180
376
|
if (!workflow.gate) return;
|
|
181
|
-
if (workflow.gate.enabled !== true)
|
|
182
|
-
|
|
377
|
+
if (workflow.gate.enabled !== true)
|
|
378
|
+
throw new Error(`Workflow gate must be enabled with true: ${workflow.id}`);
|
|
379
|
+
if (
|
|
380
|
+
workflow.gate.describe !== undefined &&
|
|
381
|
+
typeof workflow.gate.describe !== "function"
|
|
382
|
+
)
|
|
383
|
+
throw new Error(`Invalid gate description callback: ${workflow.id}`);
|
|
183
384
|
if (!workflow.gate.fields) return;
|
|
184
385
|
const argsSchema = unwrapSchema(workflow.args);
|
|
185
|
-
if (schemaType(argsSchema) !== "object")
|
|
386
|
+
if (schemaType(argsSchema) !== "object")
|
|
387
|
+
throw new Error(`Workflow gate fields require object args: ${workflow.id}`);
|
|
186
388
|
const shape = schemaShape(argsSchema);
|
|
187
|
-
for (const field of workflow.gate.fields)
|
|
389
|
+
for (const field of workflow.gate.fields)
|
|
390
|
+
if (!Object.hasOwn(shape, field))
|
|
391
|
+
throw new Error(`Unknown workflow gate field ${field}: ${workflow.id}`);
|
|
188
392
|
}
|
|
189
393
|
|
|
190
|
-
function workflowInfo(
|
|
394
|
+
function workflowInfo(
|
|
395
|
+
entry: NornRegisteredWorkflow,
|
|
396
|
+
): NornRegisteredWorkflowInfo {
|
|
191
397
|
return {
|
|
192
398
|
id: entry.workflow.id,
|
|
193
|
-
instructions:
|
|
399
|
+
instructions:
|
|
400
|
+
entry.workflow.entrypoint === false
|
|
401
|
+
? undefined
|
|
402
|
+
: entry.workflow.entrypoint.instructions,
|
|
194
403
|
isEntrypoint: entry.workflow.entrypoint !== false,
|
|
195
404
|
source: entry.source,
|
|
196
405
|
configKey: entry.workflow.id,
|
|
197
406
|
scope: entry.scopeId ? { id: entry.scopeId } : undefined,
|
|
198
407
|
};
|
|
199
408
|
}
|
|
200
|
-
function gateInfo(
|
|
409
|
+
function gateInfo(
|
|
410
|
+
workflow: NornAnyWorkflowDeclaration,
|
|
411
|
+
): NornWorkflowGateInfo | null {
|
|
201
412
|
return workflow.gate ? { enabled: true, fields: workflow.gate.fields } : null;
|
|
202
413
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from "node:path";
|
|
2
2
|
|
|
3
3
|
export function requireAbsoluteWorkingDirectory(cwd: string): string {
|
|
4
|
-
if (typeof cwd !== "string" || !isAbsolute(cwd))
|
|
4
|
+
if (typeof cwd !== "string" || !isAbsolute(cwd))
|
|
5
|
+
throw new Error("Working directory (cwd) must be an absolute path");
|
|
5
6
|
return resolve(cwd);
|
|
6
7
|
}
|