@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.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 +22 -27
- package/assets/docs/README.md +5 -5
- package/assets/docs/agents.md +48 -9
- package/assets/docs/cli.md +7 -7
- package/assets/docs/composition.md +25 -25
- package/assets/docs/persistence.md +34 -23
- package/assets/docs/projects.md +30 -18
- package/assets/docs/providers.md +3 -3
- package/assets/docs/recovery.md +13 -9
- package/assets/docs/schemas.md +1 -1
- package/assets/docs/workflows.md +105 -12
- package/assets/examples/agent-then-analysis/README.md +10 -10
- package/assets/examples/agent-then-analysis/input.json +1 -1
- package/assets/examples/agent-then-analysis/norn.project.json +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +57 -68
- package/assets/examples/caller-selected-continuation/README.md +15 -14
- package/assets/examples/caller-selected-continuation/caller.ts +37 -38
- package/assets/examples/caller-selected-continuation/input.json +2 -2
- package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
- package/assets/examples/caller-selected-continuation/producer.ts +19 -24
- package/assets/examples/coordinating-multiple-agents/README.md +27 -17
- package/assets/examples/coordinating-multiple-agents/input.json +1 -1
- package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +79 -60
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
- package/assets/examples/getting-started/README.md +2 -2
- package/assets/examples/getting-started/norn.project.json +1 -1
- package/assets/examples/getting-started/plugin.ts +20 -26
- package/assets/examples/minimal-workflow/README.md +13 -12
- package/assets/examples/minimal-workflow/norn.project.json +1 -1
- package/assets/examples/minimal-workflow/plugin.ts +14 -25
- package/assets/examples/shared-state/README.md +12 -4
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +1 -1
- package/assets/examples/shared-state/plugin.ts +49 -41
- package/assets/examples/shared-state/shared-state.ts +56 -0
- package/assets/examples/shared-state/state-tools.ts +68 -0
- package/assets/examples/worktree-development-loop/README.md +18 -17
- package/assets/examples/worktree-development-loop/norn.project.json +1 -1
- package/assets/examples/worktree-development-loop/plugin.ts +6 -26
- package/assets/examples/worktree-development-loop/scope.ts +4 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +35 -33
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +6 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +27 -16
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +43 -31
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +6 -6
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +50 -44
- package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +44 -37
- package/assets/packages/cli/src/client.ts +9 -16
- package/assets/packages/cli/src/documentation-intro.ts +1 -1
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
- package/assets/packages/cli/src/internal/agents.ts +24 -32
- package/assets/packages/cli/src/internal/commands.ts +2 -14
- package/assets/packages/cli/src/internal/engine.ts +63 -101
- package/assets/packages/cli/src/internal/errors.ts +4 -4
- package/assets/packages/cli/src/internal/launch-request.ts +6 -6
- package/assets/packages/cli/src/internal/logs.ts +1 -1
- package/assets/packages/cli/src/internal/run-log.ts +1 -1
- package/assets/packages/cli/src/internal/run-state.ts +38 -23
- package/assets/packages/cli/src/internal/run.ts +4 -62
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +126 -111
- package/assets/packages/cli/src/internal/working-directory.ts +6 -0
- package/assets/packages/cli/src/workflow-loader.ts +213 -0
- package/assets/packages/core/src/workflow-transition.ts +2 -2
- package/assets/packages/sdk/src/api.ts +141 -329
- package/assets/packages/sdk/src/index.ts +1 -4
- package/assets/packages/sdk/src/schema.ts +3 -3
- package/assets/tests/workflow-ref.test.ts +53 -55
- package/dist/cli.js +41 -35
- package/dist/client.d.ts +3 -4
- package/dist/client.js +3 -8
- package/dist/documentation-intro.js +1 -1
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-response-tool.js +3 -3
- package/dist/internal/agents.d.ts +0 -4
- package/dist/internal/agents.js +30 -46
- package/dist/internal/commands.d.ts +0 -4
- package/dist/internal/commands.js +2 -10
- package/dist/internal/engine.d.ts +4 -8
- package/dist/internal/engine.js +57 -84
- package/dist/internal/errors.d.ts +3 -3
- package/dist/internal/errors.js +1 -1
- package/dist/internal/file-coordinator.d.ts +17 -0
- package/dist/internal/file-coordinator.js +162 -0
- package/dist/internal/launch-request.d.ts +4 -4
- package/dist/internal/launch-request.js +2 -2
- package/dist/internal/logs.d.ts +1 -1
- package/dist/internal/run-log.d.ts +1 -1
- package/dist/internal/run-state.d.ts +11 -8
- package/dist/internal/run-state.js +34 -22
- package/dist/internal/run.d.ts +3 -23
- package/dist/internal/run.js +4 -50
- package/dist/internal/worker-directory.d.ts +1 -0
- package/dist/internal/worker-directory.js +26 -0
- package/dist/internal/workflow-registry.d.ts +35 -17
- package/dist/internal/workflow-registry.js +110 -73
- package/dist/internal/working-directory.d.ts +1 -0
- package/dist/internal/working-directory.js +9 -0
- package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
- package/dist/workflow-loader.js +234 -0
- package/package.json +2 -2
- package/assets/docs/resources.md +0 -61
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
- package/assets/examples/worktree-development-loop/manifest.ts +0 -26
- package/assets/examples/worktree-development-loop/state.ts +0 -23
- package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
- package/assets/packages/cli/src/internal/artifacts.ts +0 -26
- package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
- package/assets/packages/cli/src/internal/run-resources.ts +0 -23
- package/assets/packages/cli/src/internal/state-store.ts +0 -83
- package/assets/packages/cli/src/plugin-loader.ts +0 -400
- package/assets/packages/cli/src/resources.ts +0 -69
- package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
- package/assets/packages/sdk/src/resources.ts +0 -20
- package/assets/packages/sdk/src/state-adapter.ts +0 -75
- package/dist/internal/artifacts.d.ts +0 -10
- package/dist/internal/artifacts.js +0 -29
- package/dist/internal/resource-bindings.d.ts +0 -13
- package/dist/internal/resource-bindings.js +0 -34
- package/dist/internal/run-resources.d.ts +0 -6
- package/dist/internal/run-resources.js +0 -26
- package/dist/internal/state-store.d.ts +0 -23
- package/dist/internal/state-store.js +0 -103
- package/dist/plugin-loader.js +0 -341
- package/dist/resources.d.ts +0 -11
- package/dist/resources.js +0 -100
- /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type NornAnyWorkflowDeclaration,
|
|
3
|
-
type NornAnyWorkflowPluginManifest,
|
|
4
3
|
type NornDispose,
|
|
5
4
|
type NornInterruptedRunResult,
|
|
6
5
|
type NornRunCheckpoint,
|
|
@@ -8,28 +7,23 @@ import {
|
|
|
8
7
|
type NornRunNext,
|
|
9
8
|
type NornRunResult,
|
|
10
9
|
type NornRunStartOptions,
|
|
11
|
-
type
|
|
12
|
-
type NornWorkflowPluginImplementation,
|
|
13
|
-
type NornWorkflowPluginImplementationInput,
|
|
10
|
+
type NornWorkflowPaths,
|
|
14
11
|
} from "@vimhead.dev/norn";
|
|
15
|
-
import { createRunFileCoordinator } from "
|
|
12
|
+
import { createRunFileCoordinator } from "./file-coordinator.ts";
|
|
16
13
|
import { randomUUID } from "node:crypto";
|
|
17
14
|
import { mkdir } from "node:fs/promises";
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
16
|
import { Value } from "typebox/value";
|
|
20
17
|
import { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
21
|
-
import { NornArtifacts } from "./artifacts.ts";
|
|
22
18
|
import { errorMessage, NornRunStoppedError } from "./errors.ts";
|
|
23
19
|
import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS, type NornRunResumeRequest } from "./launch-request.ts";
|
|
24
20
|
import { NornRunLogs } from "./logs.ts";
|
|
25
21
|
import { NornRunLease } from "./run-lease.ts";
|
|
26
22
|
import { NornRunLogger } from "./run-log.ts";
|
|
27
|
-
import {
|
|
28
|
-
import { getRunInfo, mergeInterruptedWorkflowParams, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
|
|
23
|
+
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
|
|
29
24
|
import { NornRunStore, runCurrentRoot } from "./run-store.ts";
|
|
30
25
|
import { NornRunContext } from "./run.ts";
|
|
31
|
-
import {
|
|
32
|
-
import { decodePluginConfiguration, NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
|
|
26
|
+
import { NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
|
|
33
27
|
|
|
34
28
|
const RUNS_DIR_NAME = ".norn";
|
|
35
29
|
const MANIFEST_FILE_NAME = "manifest.json";
|
|
@@ -45,7 +39,9 @@ export type NornEngineInput = {
|
|
|
45
39
|
|
|
46
40
|
type RunSession = {
|
|
47
41
|
readonly runRoot: string;
|
|
42
|
+
readonly cwd: string;
|
|
48
43
|
readonly run: NornRunContext;
|
|
44
|
+
readonly paths: NornWorkflowPaths;
|
|
49
45
|
readonly state: NornRunStateStore;
|
|
50
46
|
readonly lease: NornRunLease;
|
|
51
47
|
readonly runStore: NornRunStore;
|
|
@@ -62,14 +58,12 @@ type ActiveRun = {
|
|
|
62
58
|
|
|
63
59
|
type WorkflowStep = {
|
|
64
60
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
65
|
-
readonly
|
|
61
|
+
readonly args: unknown;
|
|
66
62
|
readonly interruption?: NonNullable<NornRunState["current"]>["interruption"];
|
|
67
63
|
};
|
|
68
64
|
|
|
69
65
|
export class NornEngine {
|
|
70
66
|
private readonly registry = new NornWorkflowRegistry();
|
|
71
|
-
private readonly registrarState = new NornMemoryWorkflowState();
|
|
72
|
-
private readonly disposersByPlugin = new Map<string, NornDispose[]>();
|
|
73
67
|
private readonly responseCollector: NornAgentResponseCollector;
|
|
74
68
|
private readonly activeRuns = new Map<string, ActiveRun>();
|
|
75
69
|
|
|
@@ -77,19 +71,15 @@ export class NornEngine {
|
|
|
77
71
|
this.responseCollector = input.responseCollector ?? new NornAgentResponseCollector();
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.disposersByPlugin.set(plugin.manifest.id, disposers);
|
|
90
|
-
return () => {
|
|
91
|
-
if (this.disposersByPlugin.get(plugin.manifest.id) === disposers) this.disposePlugin(plugin.manifest.id);
|
|
92
|
-
};
|
|
74
|
+
registerWorkflows(workflows: readonly NornAnyWorkflowDeclaration[]): NornDispose {
|
|
75
|
+
const disposers: NornDispose[] = [];
|
|
76
|
+
try {
|
|
77
|
+
for (const workflow of workflows) disposers.push(this.registry.register({ workflow, config: this.input.config ?? {}, source: undefined }));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return () => { for (const dispose of disposers) dispose(); };
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
listWorkflows() {
|
|
@@ -100,20 +90,12 @@ export class NornEngine {
|
|
|
100
90
|
return this.registry.launchableEntries();
|
|
101
91
|
}
|
|
102
92
|
|
|
103
|
-
private resolvePluginImplementation<TManifest extends NornAnyWorkflowPluginManifest>(
|
|
104
|
-
implementationInput: NornWorkflowPluginImplementationInput<TManifest>,
|
|
105
|
-
): NornWorkflowPluginImplementation<TManifest> {
|
|
106
|
-
return typeof implementationInput === "function"
|
|
107
|
-
? implementationInput({ cwd: this.input.cwd, state: this.registrarState })
|
|
108
|
-
: implementationInput;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
93
|
async runWorkflow<TWorkflow extends NornAnyWorkflowDeclaration>(
|
|
112
94
|
workflow: TWorkflow,
|
|
113
|
-
|
|
95
|
+
args: unknown,
|
|
114
96
|
options: NornRunStartOptions | undefined,
|
|
115
97
|
): Promise<NornRunResult> {
|
|
116
|
-
const session = await this.createRunSession(workflow,
|
|
98
|
+
const session = await this.createRunSession(workflow, args, options);
|
|
117
99
|
const current = session.state.currentState().current;
|
|
118
100
|
if (!current) throw new Error("New run is missing its initial workflow step");
|
|
119
101
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
@@ -126,6 +108,7 @@ export class NornEngine {
|
|
|
126
108
|
|
|
127
109
|
async rollbackRun(path: string, checkpointId: string): Promise<NornRunInfo> {
|
|
128
110
|
const runRoot = await resolveRunRoot(this.input.cwd, path);
|
|
111
|
+
assertRunVersion((await getRunInfo(runRoot)).version);
|
|
129
112
|
const lease = await NornRunLease.acquire(runRoot);
|
|
130
113
|
try {
|
|
131
114
|
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
@@ -142,21 +125,21 @@ export class NornEngine {
|
|
|
142
125
|
}
|
|
143
126
|
}
|
|
144
127
|
|
|
145
|
-
async resumeWorkflow(path: string,
|
|
146
|
-
return this.resumeWorkflowExecution({ path,
|
|
128
|
+
async resumeWorkflow(path: string, args?: unknown): Promise<NornRunResult> {
|
|
129
|
+
return this.resumeWorkflowExecution({ path, args, expectedRequest: undefined });
|
|
147
130
|
}
|
|
148
131
|
|
|
149
132
|
async resumeRequestedWorkflow(input: { readonly runRoot: string; readonly request: NornRunResumeRequest }): Promise<NornRunResult> {
|
|
150
|
-
return this.resumeWorkflowExecution({ path: input.runRoot,
|
|
133
|
+
return this.resumeWorkflowExecution({ path: input.runRoot, args: input.request.args, expectedRequest: input.request });
|
|
151
134
|
}
|
|
152
135
|
|
|
153
|
-
private async resumeWorkflowExecution(input: { readonly path: string; readonly
|
|
154
|
-
const {
|
|
136
|
+
private async resumeWorkflowExecution(input: { readonly path: string; readonly args: unknown; readonly expectedRequest: NornRunResumeRequest | undefined }): Promise<NornRunResult> {
|
|
137
|
+
const { args, expectedRequest } = input;
|
|
155
138
|
const runRoot = await resolveRunRoot(this.input.cwd, input.path);
|
|
156
139
|
const initialStateStore = await NornRunStateStore.load(runRoot);
|
|
157
140
|
const initialState = initialStateStore.currentState();
|
|
158
|
-
if (initialState.status === "interrupted" &&
|
|
159
|
-
if (initialState.status === "pendingResume" &&
|
|
141
|
+
if (initialState.status === "interrupted" && args === undefined) throw new Error(`Interrupted workflow resume requires args: ${runRoot}`);
|
|
142
|
+
if (initialState.status === "pendingResume" && args !== undefined) throw new Error(`Pending-resume workflows do not accept args: ${runRoot}`);
|
|
160
143
|
if (initialState.status !== "interrupted" && initialState.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
161
144
|
|
|
162
145
|
const lease = await NornRunLease.acquire(runRoot);
|
|
@@ -172,17 +155,17 @@ export class NornEngine {
|
|
|
172
155
|
const workflow = this.registry.workflowById(current.workflowId);
|
|
173
156
|
if (!workflow) throw new Error(`Unknown workflow for resumed run: ${current.workflowId}`);
|
|
174
157
|
if (state.status === "interrupted") {
|
|
175
|
-
const
|
|
176
|
-
Value.Decode(workflow.
|
|
177
|
-
await session.state.
|
|
178
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
158
|
+
const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
|
|
159
|
+
Value.Decode(workflow.args, rawArgs);
|
|
160
|
+
await session.state.replaceCurrentArgs(rawArgs);
|
|
161
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
179
162
|
const resumedCurrent = session.state.currentState().current;
|
|
180
163
|
if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
181
164
|
isLeaseOwnedByScheduler = true;
|
|
182
165
|
return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
|
|
183
166
|
}
|
|
184
167
|
if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
185
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
168
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
186
169
|
isLeaseOwnedByScheduler = true;
|
|
187
170
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
188
171
|
} finally {
|
|
@@ -201,28 +184,27 @@ export class NornEngine {
|
|
|
201
184
|
try {
|
|
202
185
|
for (let step = 1; step <= 1_000; step++) {
|
|
203
186
|
throwIfRunAborted(session.activeRun);
|
|
204
|
-
const stepRuntime = session.run.forWorkflow(currentStep.workflow);
|
|
205
187
|
if (shouldPauseForGate(currentStep)) {
|
|
206
|
-
const description = await this.registry.describeGate(currentStep.workflow,
|
|
188
|
+
const description = await this.registry.describeGate({ workflow: currentStep.workflow, run: session.run, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
|
|
207
189
|
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
208
|
-
await session.state.interruptCurrent(currentStep.
|
|
190
|
+
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
209
191
|
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
210
192
|
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
211
|
-
return interruptedLaunchResult({ id:
|
|
193
|
+
return interruptedLaunchResult({ id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
212
194
|
}
|
|
213
195
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
214
|
-
const stepResult = await this.executeWorkflowStep(session, currentStep
|
|
196
|
+
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
215
197
|
throwIfRunAborted(session.activeRun);
|
|
216
198
|
if (stepResult.type === "complete") {
|
|
217
199
|
await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
|
|
218
200
|
await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
219
201
|
await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
|
|
220
|
-
return { status: "completed", id:
|
|
202
|
+
return { status: "completed", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
221
203
|
}
|
|
222
204
|
if (stepResult.type === "fail") {
|
|
223
205
|
await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
|
|
224
206
|
await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
225
|
-
return { status: "failed", id:
|
|
207
|
+
return { status: "failed", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
226
208
|
}
|
|
227
209
|
const nextStep = this.nextWorkflowStep(stepResult);
|
|
228
210
|
await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
|
|
@@ -235,7 +217,7 @@ export class NornEngine {
|
|
|
235
217
|
if (isRunStopped(session, error)) {
|
|
236
218
|
await session.state.stopCurrent();
|
|
237
219
|
await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
|
|
238
|
-
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.
|
|
220
|
+
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
|
|
239
221
|
}
|
|
240
222
|
await session.state.failCurrent(errorMessage(error));
|
|
241
223
|
await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
|
|
@@ -246,13 +228,13 @@ export class NornEngine {
|
|
|
246
228
|
}
|
|
247
229
|
}
|
|
248
230
|
|
|
249
|
-
private async executeWorkflowStep(session: RunSession, step: WorkflowStep
|
|
231
|
+
private async executeWorkflowStep(session: RunSession, step: WorkflowStep): Promise<NornWorkflowStepResult> {
|
|
250
232
|
const startedAtMs = Date.now();
|
|
251
233
|
try {
|
|
252
|
-
await assertWorkspaceBoundary(session,
|
|
234
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
253
235
|
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
254
|
-
const result = await this.registry.execute(step.workflow, run, step.
|
|
255
|
-
await assertWorkspaceBoundary(session,
|
|
236
|
+
const result = await this.registry.execute({ workflow: step.workflow, run: session.run, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
|
|
237
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
256
238
|
const durationMs = Date.now() - startedAtMs;
|
|
257
239
|
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
258
240
|
else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
@@ -274,14 +256,14 @@ export class NornEngine {
|
|
|
274
256
|
if (!workflow) throw new Error(`Unknown next workflow: ${next.workflowId}`);
|
|
275
257
|
return {
|
|
276
258
|
workflow,
|
|
277
|
-
|
|
259
|
+
args: next.args,
|
|
278
260
|
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : undefined,
|
|
279
261
|
};
|
|
280
262
|
}
|
|
281
263
|
|
|
282
264
|
private async createRunSession<TWorkflow extends NornAnyWorkflowDeclaration>(
|
|
283
265
|
workflow: TWorkflow,
|
|
284
|
-
|
|
266
|
+
args: unknown,
|
|
285
267
|
options: NornRunStartOptions | undefined,
|
|
286
268
|
): Promise<RunSession> {
|
|
287
269
|
const id = options?.id ?? randomUUID();
|
|
@@ -289,7 +271,8 @@ export class NornEngine {
|
|
|
289
271
|
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
290
272
|
const currentRoot = runCurrentRoot(runRoot);
|
|
291
273
|
const workspace = join(currentRoot, "workspace");
|
|
292
|
-
const
|
|
274
|
+
const paths: NornWorkflowPaths = { project: resolve(this.input.cwd), workspace: workspace };
|
|
275
|
+
const cwd = process.cwd();
|
|
293
276
|
const startedAt = new Date().toISOString();
|
|
294
277
|
await mkdir(runRoot, { recursive: true });
|
|
295
278
|
const lease = await NornRunLease.acquire(runRoot);
|
|
@@ -306,20 +289,21 @@ export class NornEngine {
|
|
|
306
289
|
initialCwd: cwd,
|
|
307
290
|
startedAt,
|
|
308
291
|
} });
|
|
309
|
-
const run = await this.buildRun({ id, currentRoot,
|
|
292
|
+
const run = await this.buildRun({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
310
293
|
const state = await NornRunStateStore.create(runRoot, {
|
|
294
|
+
projectRoot: paths.project,
|
|
311
295
|
id,
|
|
312
296
|
name,
|
|
313
297
|
entrypointWorkflowId: workflow.id,
|
|
314
298
|
workspace,
|
|
315
299
|
configOverride: options?.configOverride,
|
|
316
300
|
current: {
|
|
317
|
-
workflowId: workflow.id,
|
|
301
|
+
workflowId: workflow.id, args, cwd, env: {},
|
|
318
302
|
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : undefined,
|
|
319
303
|
},
|
|
320
304
|
startedAt,
|
|
321
305
|
});
|
|
322
|
-
const session = { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
306
|
+
const session = { runRoot, cwd, run, paths, state, lease, runStore, logger, activeRun };
|
|
323
307
|
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
324
308
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
325
309
|
return session;
|
|
@@ -339,11 +323,9 @@ export class NornEngine {
|
|
|
339
323
|
const runStore = await NornRunStore.open(runRoot);
|
|
340
324
|
const currentRoot = runCurrentRoot(runRoot);
|
|
341
325
|
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
|
|
346
|
-
return { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
326
|
+
const paths: NornWorkflowPaths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
|
|
327
|
+
const run = await this.buildRun({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
328
|
+
return { runRoot, cwd: process.cwd(), run, paths, state, lease, runStore, logger, activeRun };
|
|
347
329
|
} catch (error) {
|
|
348
330
|
this.failActiveRun(runRoot, activeRun);
|
|
349
331
|
throw error;
|
|
@@ -353,33 +335,22 @@ export class NornEngine {
|
|
|
353
335
|
private async buildRun(input: {
|
|
354
336
|
readonly id: string;
|
|
355
337
|
readonly currentRoot: string;
|
|
356
|
-
readonly
|
|
357
|
-
readonly cwd: string;
|
|
358
|
-
readonly isolationMode: "runWorkspace" | "project";
|
|
338
|
+
readonly paths: NornWorkflowPaths;
|
|
359
339
|
readonly signal: AbortSignal;
|
|
360
340
|
readonly logger: NornRunLogger;
|
|
361
341
|
}): Promise<NornRunContext> {
|
|
362
|
-
const artifactsRoot = join(input.currentRoot, "artifacts");
|
|
363
342
|
const logsRoot = join(input.currentRoot, "logs");
|
|
364
|
-
await mkdir(input.workspace, { recursive: true });
|
|
365
|
-
await mkdir(artifactsRoot, { recursive: true });
|
|
343
|
+
await mkdir(input.paths.workspace, { recursive: true });
|
|
366
344
|
await mkdir(logsRoot, { recursive: true });
|
|
367
|
-
const
|
|
345
|
+
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
368
346
|
return new NornRunContext({
|
|
369
347
|
id: input.id,
|
|
370
348
|
runRoot: input.currentRoot,
|
|
371
|
-
workspace: input.workspace,
|
|
372
|
-
projectRoot: this.input.cwd,
|
|
373
|
-
cwd: input.cwd,
|
|
374
|
-
isolationMode: input.isolationMode,
|
|
375
349
|
signal: input.signal,
|
|
376
350
|
agentDir: this.input.agentDir,
|
|
377
351
|
responseCollector: this.responseCollector,
|
|
378
|
-
resources,
|
|
379
|
-
state,
|
|
380
352
|
logger: input.logger,
|
|
381
|
-
|
|
382
|
-
logs: new NornRunLogs(logsRoot, resources.files),
|
|
353
|
+
logs: new NornRunLogs(logsRoot, files),
|
|
383
354
|
});
|
|
384
355
|
}
|
|
385
356
|
|
|
@@ -416,25 +387,16 @@ export class NornEngine {
|
|
|
416
387
|
activeRun.finish();
|
|
417
388
|
}
|
|
418
389
|
|
|
419
|
-
private disposePlugin(pluginId: string): void {
|
|
420
|
-
const disposers = this.disposersByPlugin.get(pluginId) ?? [];
|
|
421
|
-
for (const dispose of [...disposers].reverse()) dispose();
|
|
422
|
-
this.disposersByPlugin.delete(pluginId);
|
|
423
|
-
}
|
|
424
390
|
}
|
|
425
391
|
|
|
426
392
|
function defaultRunRoot(cwd: string, id: string): string {
|
|
427
|
-
return join(cwd, RUNS_DIR_NAME, "runs", id);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function workflowDefaultCwd(projectRoot: string, workspace: string, workflow: NornAnyWorkflowDeclaration | undefined): string {
|
|
431
|
-
return workflow?.isolation.mode === "project" ? projectRoot : workspace;
|
|
393
|
+
return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
|
|
432
394
|
}
|
|
433
395
|
|
|
434
396
|
function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<NornRunState["current"]>): WorkflowStep {
|
|
435
397
|
return {
|
|
436
398
|
workflow,
|
|
437
|
-
|
|
399
|
+
args: step.args,
|
|
438
400
|
interruption: step.interruption,
|
|
439
401
|
};
|
|
440
402
|
}
|
|
@@ -442,7 +404,7 @@ function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<
|
|
|
442
404
|
function toRunStateStep(step: WorkflowStep): NonNullable<NornRunState["current"]> {
|
|
443
405
|
return {
|
|
444
406
|
workflowId: step.workflow.id,
|
|
445
|
-
|
|
407
|
+
args: step.args,
|
|
446
408
|
cwd: "",
|
|
447
409
|
env: {},
|
|
448
410
|
interruption: step.interruption,
|
|
@@ -464,7 +426,7 @@ function isRunStopped(session: RunSession, error: unknown): boolean {
|
|
|
464
426
|
return error instanceof NornRunStoppedError || session.activeRun.controller.signal.reason instanceof NornRunStoppedError;
|
|
465
427
|
}
|
|
466
428
|
|
|
467
|
-
function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDeclaration,
|
|
429
|
+
function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDeclaration, args: unknown, interruption: { readonly description?: string; readonly fields?: readonly string[] }): NornInterruptedRunResult {
|
|
468
430
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
469
431
|
if (!interruption.description) throw new Error(`Interrupted workflow is missing description: ${workflow.id}`);
|
|
470
432
|
return {
|
|
@@ -474,7 +436,7 @@ function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDecl
|
|
|
474
436
|
workspace: run.workspace,
|
|
475
437
|
cwd: run.cwd,
|
|
476
438
|
workflowId: workflow.id,
|
|
477
|
-
interruption: { workflowId: workflow.id,
|
|
439
|
+
interruption: { workflowId: workflow.id, args, description: interruption.description, fields: interruption.fields },
|
|
478
440
|
};
|
|
479
441
|
}
|
|
480
442
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NornWorkflowDiagnostic } from "@vimhead.dev/norn";
|
|
2
2
|
import { AssertError } from "typebox/value";
|
|
3
3
|
|
|
4
4
|
export class NornProjectLoadError extends Error {
|
|
5
5
|
readonly code = "NORN_PROJECT_INVALID";
|
|
6
6
|
readonly isComplete = false;
|
|
7
|
-
readonly diagnostics: readonly
|
|
7
|
+
readonly diagnostics: readonly NornWorkflowDiagnostic[];
|
|
8
8
|
|
|
9
|
-
constructor(input: { readonly diagnostics: readonly
|
|
9
|
+
constructor(input: { readonly diagnostics: readonly NornWorkflowDiagnostic[] }) {
|
|
10
10
|
const details = input.diagnostics.map(diagnostic =>
|
|
11
|
-
`${diagnostic.stage}: ${diagnostic.
|
|
11
|
+
`${diagnostic.stage}: ${diagnostic.modulePath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`,
|
|
12
12
|
);
|
|
13
13
|
super(`Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.\n${details.join("\n")}`);
|
|
14
14
|
this.name = "NornProjectLoadError";
|
|
@@ -10,22 +10,22 @@ export const RESUME_REQUEST_FILE_NAME = "resume-request.json";
|
|
|
10
10
|
export const RESUME_START_GRACE_MS = 60_000;
|
|
11
11
|
|
|
12
12
|
export type NornRunLaunchRequest = {
|
|
13
|
-
readonly version:
|
|
13
|
+
readonly version: 2;
|
|
14
14
|
readonly type: "run";
|
|
15
15
|
readonly id: string;
|
|
16
16
|
readonly name: string;
|
|
17
17
|
readonly workflowId: string;
|
|
18
|
-
readonly
|
|
18
|
+
readonly args: unknown;
|
|
19
19
|
readonly configOverride?: unknown;
|
|
20
20
|
readonly createdAt: string;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type NornRunResumeRequest = {
|
|
24
|
-
readonly version:
|
|
24
|
+
readonly version: 2;
|
|
25
25
|
readonly type: "resume";
|
|
26
26
|
readonly id: string;
|
|
27
27
|
readonly requestId?: string;
|
|
28
|
-
readonly
|
|
28
|
+
readonly args?: unknown;
|
|
29
29
|
readonly createdAt: string;
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -75,7 +75,7 @@ export async function clearRunResumeRequest(input: { readonly runRoot: string; r
|
|
|
75
75
|
function parseRunLaunchRequest(value: unknown): NornRunLaunchRequest {
|
|
76
76
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
|
|
77
77
|
const request = value as Partial<NornRunLaunchRequest>;
|
|
78
|
-
if (request.version !==
|
|
78
|
+
if (request.version !== 2 || request.type !== "run") throw new Error("Unsupported workflow launch request");
|
|
79
79
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
|
|
80
80
|
if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
|
|
81
81
|
if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
|
|
@@ -86,7 +86,7 @@ function parseRunLaunchRequest(value: unknown): NornRunLaunchRequest {
|
|
|
86
86
|
function parseRunResumeRequest(value: unknown): NornRunResumeRequest {
|
|
87
87
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
|
|
88
88
|
const request = value as Partial<NornRunResumeRequest>;
|
|
89
|
-
if (request.version !==
|
|
89
|
+
if (request.version !== 2 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
|
|
90
90
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
|
|
91
91
|
if (request.requestId !== undefined && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
|
|
92
92
|
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWriteStream, type WriteStream } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import type { NornFileCoordinator } from "
|
|
3
|
+
import type { NornFileCoordinator } from "./file-coordinator.ts";
|
|
4
4
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import type { NornLogRef } from "@vimhead.dev/norn";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import type { NornFileCoordinator } from "
|
|
2
|
+
import type { NornFileCoordinator } from "./file-coordinator.ts";
|
|
3
3
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
4
|
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
5
5
|
|