@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
|
@@ -16,14 +16,31 @@ import { dirname, join, resolve } from "node:path";
|
|
|
16
16
|
import { Value } from "typebox/value";
|
|
17
17
|
import { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
18
18
|
import { errorMessage, NornRunStoppedError } from "./errors.ts";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
clearRunResumeRequest,
|
|
21
|
+
matchesRunResumeRequest,
|
|
22
|
+
readOptionalRunResumeRequest,
|
|
23
|
+
RESUME_START_GRACE_MS,
|
|
24
|
+
type NornRunResumeRequest,
|
|
25
|
+
} from "./launch-request.ts";
|
|
20
26
|
import { NornRunLogs } from "./logs.ts";
|
|
21
27
|
import { NornRunLease } from "./run-lease.ts";
|
|
22
28
|
import { NornRunLogger } from "./run-log.ts";
|
|
23
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
assertRunVersion,
|
|
31
|
+
getRunInfo,
|
|
32
|
+
mergeInterruptedWorkflowArgs,
|
|
33
|
+
NornRunStateStore,
|
|
34
|
+
resolveRunRoot,
|
|
35
|
+
type NornRunState,
|
|
36
|
+
} from "./run-state.ts";
|
|
24
37
|
import { NornRunStore, runCurrentRoot } from "./run-store.ts";
|
|
25
38
|
import { NornExecutionContext } from "./execution-context.ts";
|
|
26
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
NornWorkflowRegistry,
|
|
41
|
+
type NornRegisteredWorkflow,
|
|
42
|
+
type NornWorkflowStepResult,
|
|
43
|
+
} from "./workflow-registry.ts";
|
|
27
44
|
|
|
28
45
|
const RUNS_DIR_NAME = ".norn";
|
|
29
46
|
const MANIFEST_FILE_NAME = "manifest.json";
|
|
@@ -68,18 +85,30 @@ export class NornEngine {
|
|
|
68
85
|
private readonly activeRuns = new Map<string, ActiveRun>();
|
|
69
86
|
|
|
70
87
|
constructor(private readonly input: NornEngineInput) {
|
|
71
|
-
this.responseCollector =
|
|
88
|
+
this.responseCollector =
|
|
89
|
+
input.responseCollector ?? new NornAgentResponseCollector();
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
registerWorkflows(
|
|
92
|
+
registerWorkflows(
|
|
93
|
+
workflows: readonly NornAnyWorkflowDeclaration[],
|
|
94
|
+
): NornDispose {
|
|
75
95
|
const disposers: NornDispose[] = [];
|
|
76
96
|
try {
|
|
77
|
-
for (const workflow of workflows)
|
|
97
|
+
for (const workflow of workflows)
|
|
98
|
+
disposers.push(
|
|
99
|
+
this.registry.register({
|
|
100
|
+
workflow,
|
|
101
|
+
config: this.input.config ?? {},
|
|
102
|
+
source: undefined,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
78
105
|
} catch (error) {
|
|
79
106
|
for (const dispose of disposers.reverse()) dispose();
|
|
80
107
|
throw error;
|
|
81
108
|
}
|
|
82
|
-
return () => {
|
|
109
|
+
return () => {
|
|
110
|
+
for (const dispose of disposers) dispose();
|
|
111
|
+
};
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
listWorkflows() {
|
|
@@ -97,7 +126,8 @@ export class NornEngine {
|
|
|
97
126
|
): Promise<NornRunResult> {
|
|
98
127
|
const session = await this.createRunSession(workflow, args, options);
|
|
99
128
|
const current = session.state.currentState().current;
|
|
100
|
-
if (!current)
|
|
129
|
+
if (!current)
|
|
130
|
+
throw new Error("New run is missing its initial workflow step");
|
|
101
131
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
102
132
|
}
|
|
103
133
|
|
|
@@ -112,13 +142,23 @@ export class NornEngine {
|
|
|
112
142
|
const lease = await NornRunLease.acquire(runRoot);
|
|
113
143
|
try {
|
|
114
144
|
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
115
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
pendingRequest &&
|
|
147
|
+
Date.now() - Date.parse(pendingRequest.createdAt) <
|
|
148
|
+
RESUME_START_GRACE_MS
|
|
149
|
+
)
|
|
150
|
+
throw new Error(`Resume request is still pending: ${runRoot}`);
|
|
116
151
|
const runStore = await NornRunStore.open(runRoot);
|
|
117
152
|
await runStore.restoreSnapshot(checkpointId, async (stagedRunRoot) => {
|
|
118
153
|
const state = await NornRunStateStore.load(stagedRunRoot);
|
|
119
154
|
await state.prepareForResumeAfterRollback();
|
|
120
155
|
});
|
|
121
|
-
if (pendingRequest)
|
|
156
|
+
if (pendingRequest)
|
|
157
|
+
await clearRunResumeRequest({
|
|
158
|
+
runRoot,
|
|
159
|
+
request: pendingRequest,
|
|
160
|
+
lease,
|
|
161
|
+
});
|
|
122
162
|
return getRunInfo(runRoot);
|
|
123
163
|
} finally {
|
|
124
164
|
await lease.release();
|
|
@@ -126,46 +166,96 @@ export class NornEngine {
|
|
|
126
166
|
}
|
|
127
167
|
|
|
128
168
|
async resumeWorkflow(path: string, args?: unknown): Promise<NornRunResult> {
|
|
129
|
-
return this.resumeWorkflowExecution({
|
|
169
|
+
return this.resumeWorkflowExecution({
|
|
170
|
+
path,
|
|
171
|
+
args,
|
|
172
|
+
expectedRequest: undefined,
|
|
173
|
+
});
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
async resumeRequestedWorkflow(input: {
|
|
133
|
-
|
|
176
|
+
async resumeRequestedWorkflow(input: {
|
|
177
|
+
readonly runRoot: string;
|
|
178
|
+
readonly request: NornRunResumeRequest;
|
|
179
|
+
}): Promise<NornRunResult> {
|
|
180
|
+
return this.resumeWorkflowExecution({
|
|
181
|
+
path: input.runRoot,
|
|
182
|
+
args: input.request.args,
|
|
183
|
+
expectedRequest: input.request,
|
|
184
|
+
});
|
|
134
185
|
}
|
|
135
186
|
|
|
136
|
-
private async resumeWorkflowExecution(input: {
|
|
187
|
+
private async resumeWorkflowExecution(input: {
|
|
188
|
+
readonly path: string;
|
|
189
|
+
readonly args: unknown;
|
|
190
|
+
readonly expectedRequest: NornRunResumeRequest | undefined;
|
|
191
|
+
}): Promise<NornRunResult> {
|
|
137
192
|
const { args, expectedRequest } = input;
|
|
138
193
|
const runRoot = await resolveRunRoot(this.input.cwd, input.path);
|
|
139
194
|
const initialStateStore = await NornRunStateStore.load(runRoot);
|
|
140
195
|
const initialState = initialStateStore.currentState();
|
|
141
|
-
if (initialState.status === "interrupted" && args === undefined)
|
|
142
|
-
|
|
143
|
-
if (initialState.status
|
|
196
|
+
if (initialState.status === "interrupted" && args === undefined)
|
|
197
|
+
throw new Error(`Interrupted workflow resume requires args: ${runRoot}`);
|
|
198
|
+
if (initialState.status === "pendingResume" && args !== undefined)
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Pending-resume workflows do not accept args: ${runRoot}`,
|
|
201
|
+
);
|
|
202
|
+
if (
|
|
203
|
+
initialState.status !== "interrupted" &&
|
|
204
|
+
initialState.status !== "pendingResume"
|
|
205
|
+
)
|
|
206
|
+
throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
144
207
|
|
|
145
208
|
const lease = await NornRunLease.acquire(runRoot);
|
|
146
209
|
let isLeaseOwnedByScheduler = false;
|
|
147
210
|
let session: RunSession | undefined;
|
|
148
211
|
try {
|
|
149
212
|
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
150
|
-
if (
|
|
213
|
+
if (
|
|
214
|
+
expectedRequest
|
|
215
|
+
? !matchesRunResumeRequest(pendingRequest, expectedRequest)
|
|
216
|
+
: pendingRequest !== undefined
|
|
217
|
+
)
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Resume request changed or belongs to another executor: ${runRoot}`,
|
|
220
|
+
);
|
|
151
221
|
session = await this.openRunSession(runRoot, lease);
|
|
152
222
|
const state = session.state.currentState();
|
|
153
223
|
const current = state.current;
|
|
154
224
|
if (!current) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
155
225
|
const workflow = this.registry.workflowById(current.workflowId);
|
|
156
|
-
if (!workflow)
|
|
226
|
+
if (!workflow)
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Unknown workflow for resumed run: ${current.workflowId}`,
|
|
229
|
+
);
|
|
157
230
|
if (state.status === "interrupted") {
|
|
158
|
-
const rawArgs = mergeInterruptedWorkflowArgs(
|
|
231
|
+
const rawArgs = mergeInterruptedWorkflowArgs(
|
|
232
|
+
current.args,
|
|
233
|
+
args,
|
|
234
|
+
current.interruption?.fields,
|
|
235
|
+
);
|
|
159
236
|
Value.Decode(workflow.args, rawArgs);
|
|
160
237
|
await session.state.replaceCurrentArgs(rawArgs);
|
|
161
|
-
await recordRunEvent(session, {
|
|
238
|
+
await recordRunEvent(session, {
|
|
239
|
+
type: "run.resumed",
|
|
240
|
+
workflowId: workflow.id,
|
|
241
|
+
cwd: session.cwd,
|
|
242
|
+
});
|
|
162
243
|
const resumedCurrent = session.state.currentState().current;
|
|
163
|
-
if (!resumedCurrent)
|
|
244
|
+
if (!resumedCurrent)
|
|
245
|
+
throw new Error(`Run is not resumable: ${runRoot}`);
|
|
164
246
|
isLeaseOwnedByScheduler = true;
|
|
165
|
-
return this.runScheduler(
|
|
247
|
+
return this.runScheduler(
|
|
248
|
+
session,
|
|
249
|
+
toWorkflowStep(workflow, resumedCurrent),
|
|
250
|
+
);
|
|
166
251
|
}
|
|
167
|
-
if (state.status !== "pendingResume")
|
|
168
|
-
|
|
252
|
+
if (state.status !== "pendingResume")
|
|
253
|
+
throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
254
|
+
await recordRunEvent(session, {
|
|
255
|
+
type: "run.resumed",
|
|
256
|
+
workflowId: workflow.id,
|
|
257
|
+
cwd: session.cwd,
|
|
258
|
+
});
|
|
169
259
|
isLeaseOwnedByScheduler = true;
|
|
170
260
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
171
261
|
} finally {
|
|
@@ -179,48 +269,133 @@ export class NornEngine {
|
|
|
179
269
|
}
|
|
180
270
|
}
|
|
181
271
|
|
|
182
|
-
private async runScheduler(
|
|
272
|
+
private async runScheduler(
|
|
273
|
+
session: RunSession,
|
|
274
|
+
initialStep: WorkflowStep,
|
|
275
|
+
): Promise<NornRunResult> {
|
|
183
276
|
let currentStep = initialStep;
|
|
184
277
|
try {
|
|
185
278
|
for (let step = 1; step <= 1_000; step++) {
|
|
186
279
|
throwIfRunAborted(session.activeRun);
|
|
187
280
|
if (shouldPauseForGate(currentStep)) {
|
|
188
|
-
const description = await this.registry.describeGate({
|
|
189
|
-
|
|
281
|
+
const description = await this.registry.describeGate({
|
|
282
|
+
workflow: currentStep.workflow,
|
|
283
|
+
execution: session.execution,
|
|
284
|
+
paths: session.paths,
|
|
285
|
+
args: currentStep.args,
|
|
286
|
+
configOverride: session.state.currentState().configOverride,
|
|
287
|
+
});
|
|
288
|
+
const interruption = {
|
|
289
|
+
description,
|
|
290
|
+
fields: currentStep.workflow.gate?.fields,
|
|
291
|
+
};
|
|
190
292
|
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
191
|
-
await recordRunEvent(session, {
|
|
192
|
-
|
|
193
|
-
|
|
293
|
+
await recordRunEvent(session, {
|
|
294
|
+
type: "run.interrupted",
|
|
295
|
+
workflowId: currentStep.workflow.id,
|
|
296
|
+
});
|
|
297
|
+
await commitRunBoundary(
|
|
298
|
+
session,
|
|
299
|
+
`run interrupted: ${currentStep.workflow.id}`,
|
|
300
|
+
);
|
|
301
|
+
return interruptedLaunchResult(
|
|
302
|
+
{
|
|
303
|
+
id: session.execution.run.id,
|
|
304
|
+
name: session.state.currentState().name,
|
|
305
|
+
workspace: session.paths.workspace,
|
|
306
|
+
cwd: session.cwd,
|
|
307
|
+
},
|
|
308
|
+
currentStep.workflow,
|
|
309
|
+
currentStep.args,
|
|
310
|
+
interruption,
|
|
311
|
+
);
|
|
194
312
|
}
|
|
195
313
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
196
314
|
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
197
315
|
throwIfRunAborted(session.activeRun);
|
|
198
316
|
if (stepResult.type === "complete") {
|
|
199
|
-
await session.state.completeRun(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
317
|
+
await session.state.completeRun(
|
|
318
|
+
stepResult.workflow.id,
|
|
319
|
+
stepResult.metadata,
|
|
320
|
+
);
|
|
321
|
+
await recordRunEvent(session, {
|
|
322
|
+
type: "run.completed",
|
|
323
|
+
workflowId: stepResult.workflow.id,
|
|
324
|
+
metadata: stepResult.metadata,
|
|
325
|
+
});
|
|
326
|
+
await commitRunBoundary(
|
|
327
|
+
session,
|
|
328
|
+
`run completed: ${stepResult.workflow.id}`,
|
|
329
|
+
);
|
|
330
|
+
return {
|
|
331
|
+
status: "completed",
|
|
332
|
+
id: session.execution.run.id,
|
|
333
|
+
name: session.state.currentState().name,
|
|
334
|
+
workspace: session.paths.workspace,
|
|
335
|
+
cwd: session.cwd,
|
|
336
|
+
workflowId: stepResult.workflow.id,
|
|
337
|
+
metadata: stepResult.metadata,
|
|
338
|
+
};
|
|
203
339
|
}
|
|
204
340
|
if (stepResult.type === "fail") {
|
|
205
|
-
await session.state.failRun(
|
|
206
|
-
|
|
207
|
-
|
|
341
|
+
await session.state.failRun(
|
|
342
|
+
stepResult.workflow.id,
|
|
343
|
+
stepResult.metadata,
|
|
344
|
+
);
|
|
345
|
+
await recordRunEvent(session, {
|
|
346
|
+
type: "run.failed",
|
|
347
|
+
workflowId: stepResult.workflow.id,
|
|
348
|
+
metadata: stepResult.metadata,
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
status: "failed",
|
|
352
|
+
id: session.execution.run.id,
|
|
353
|
+
name: session.state.currentState().name,
|
|
354
|
+
workspace: session.paths.workspace,
|
|
355
|
+
cwd: session.cwd,
|
|
356
|
+
workflowId: stepResult.workflow.id,
|
|
357
|
+
metadata: stepResult.metadata,
|
|
358
|
+
};
|
|
208
359
|
}
|
|
209
360
|
const nextStep = this.nextWorkflowStep(stepResult);
|
|
210
|
-
await session.state.completeWithNext(
|
|
211
|
-
|
|
212
|
-
|
|
361
|
+
await session.state.completeWithNext(
|
|
362
|
+
currentStep.workflow.id,
|
|
363
|
+
toRunStateStep(nextStep),
|
|
364
|
+
);
|
|
365
|
+
await recordRunEvent(session, {
|
|
366
|
+
type: "run.transitioned",
|
|
367
|
+
fromWorkflowId: currentStep.workflow.id,
|
|
368
|
+
toWorkflowId: nextStep.workflow.id,
|
|
369
|
+
});
|
|
370
|
+
await commitRunBoundary(
|
|
371
|
+
session,
|
|
372
|
+
`transition: ${currentStep.workflow.id} -> ${nextStep.workflow.id}`,
|
|
373
|
+
);
|
|
213
374
|
currentStep = nextStep;
|
|
214
375
|
}
|
|
215
376
|
throw new Error("Run exceeded 1000 scheduler steps");
|
|
216
377
|
} catch (error) {
|
|
217
378
|
if (isRunStopped(session, error)) {
|
|
218
379
|
await session.state.stopCurrent();
|
|
219
|
-
await recordRunEvent(session, {
|
|
220
|
-
|
|
380
|
+
await recordRunEvent(session, {
|
|
381
|
+
type: "run.stopped",
|
|
382
|
+
workflowId: currentStep.workflow.id,
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
status: "stopped",
|
|
386
|
+
id: session.execution.run.id,
|
|
387
|
+
name: session.state.currentState().name,
|
|
388
|
+
workspace: session.paths.workspace,
|
|
389
|
+
cwd: session.cwd,
|
|
390
|
+
workflowId: currentStep.workflow.id,
|
|
391
|
+
};
|
|
221
392
|
}
|
|
222
393
|
await session.state.failCurrent(errorMessage(error));
|
|
223
|
-
await recordRunEvent(session, {
|
|
394
|
+
await recordRunEvent(session, {
|
|
395
|
+
type: "run.failed",
|
|
396
|
+
workflowId: currentStep.workflow.id,
|
|
397
|
+
error: errorMessage(error),
|
|
398
|
+
});
|
|
224
399
|
throw error;
|
|
225
400
|
} finally {
|
|
226
401
|
await session.lease.release();
|
|
@@ -228,21 +403,53 @@ export class NornEngine {
|
|
|
228
403
|
}
|
|
229
404
|
}
|
|
230
405
|
|
|
231
|
-
private async executeWorkflowStep(
|
|
406
|
+
private async executeWorkflowStep(
|
|
407
|
+
session: RunSession,
|
|
408
|
+
step: WorkflowStep,
|
|
409
|
+
): Promise<NornWorkflowStepResult> {
|
|
232
410
|
const startedAtMs = Date.now();
|
|
233
411
|
try {
|
|
234
412
|
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
235
|
-
await recordRunEvent(session, {
|
|
236
|
-
|
|
413
|
+
await recordRunEvent(session, {
|
|
414
|
+
type: "workflow.started",
|
|
415
|
+
workflowId: step.workflow.id,
|
|
416
|
+
});
|
|
417
|
+
const result = await this.registry.execute({
|
|
418
|
+
workflow: step.workflow,
|
|
419
|
+
execution: session.execution,
|
|
420
|
+
paths: session.paths,
|
|
421
|
+
args: step.args,
|
|
422
|
+
configOverride: session.state.currentState().configOverride,
|
|
423
|
+
});
|
|
237
424
|
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
238
425
|
const durationMs = Date.now() - startedAtMs;
|
|
239
|
-
if (result.type === "complete")
|
|
240
|
-
|
|
241
|
-
|
|
426
|
+
if (result.type === "complete")
|
|
427
|
+
await recordRunEvent(session, {
|
|
428
|
+
type: "workflow.completed",
|
|
429
|
+
workflowId: result.workflow.id,
|
|
430
|
+
durationMs,
|
|
431
|
+
metadata: result.metadata,
|
|
432
|
+
});
|
|
433
|
+
else if (result.type === "fail")
|
|
434
|
+
await recordRunEvent(session, {
|
|
435
|
+
type: "workflow.failed",
|
|
436
|
+
workflowId: result.workflow.id,
|
|
437
|
+
durationMs,
|
|
438
|
+
metadata: result.metadata,
|
|
439
|
+
});
|
|
440
|
+
else
|
|
441
|
+
await recordRunEvent(session, {
|
|
442
|
+
type: "workflow.transitioned",
|
|
443
|
+
fromWorkflowId: step.workflow.id,
|
|
444
|
+
toWorkflowId: result.workflowId,
|
|
445
|
+
durationMs,
|
|
446
|
+
});
|
|
242
447
|
return result;
|
|
243
448
|
} catch (error) {
|
|
244
449
|
await recordRunEvent(session, {
|
|
245
|
-
type: isRunStopped(session, error)
|
|
450
|
+
type: isRunStopped(session, error)
|
|
451
|
+
? "workflow.stopped"
|
|
452
|
+
: "workflow.failed",
|
|
246
453
|
workflowId: step.workflow.id,
|
|
247
454
|
durationMs: Date.now() - startedAtMs,
|
|
248
455
|
...(isRunStopped(session, error) ? {} : { error: errorMessage(error) }),
|
|
@@ -257,7 +464,10 @@ export class NornEngine {
|
|
|
257
464
|
return {
|
|
258
465
|
workflow,
|
|
259
466
|
args: next.args,
|
|
260
|
-
interruption:
|
|
467
|
+
interruption:
|
|
468
|
+
this.input.gateMode === "pause" && workflow.gate
|
|
469
|
+
? { status: "pending" }
|
|
470
|
+
: undefined,
|
|
261
471
|
};
|
|
262
472
|
}
|
|
263
473
|
|
|
@@ -271,7 +481,10 @@ export class NornEngine {
|
|
|
271
481
|
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
272
482
|
const currentRoot = runCurrentRoot(runRoot);
|
|
273
483
|
const workspace = join(currentRoot, "workspace");
|
|
274
|
-
const paths: NornWorkflowPaths = {
|
|
484
|
+
const paths: NornWorkflowPaths = {
|
|
485
|
+
project: resolve(this.input.cwd),
|
|
486
|
+
workspace: workspace,
|
|
487
|
+
};
|
|
275
488
|
const cwd = process.cwd();
|
|
276
489
|
const startedAt = new Date().toISOString();
|
|
277
490
|
await mkdir(runRoot, { recursive: true });
|
|
@@ -280,16 +493,26 @@ export class NornEngine {
|
|
|
280
493
|
try {
|
|
281
494
|
const runStore = await NornRunStore.initialize(runRoot);
|
|
282
495
|
await mkdir(workspace, { recursive: true });
|
|
283
|
-
const logger = new NornRunLogger({
|
|
496
|
+
const logger = new NornRunLogger({
|
|
497
|
+
manifestPath: join(currentRoot, MANIFEST_FILE_NAME),
|
|
498
|
+
files: createRunFileCoordinator(runRoot),
|
|
499
|
+
manifest: {
|
|
500
|
+
id,
|
|
501
|
+
name,
|
|
502
|
+
workflowId: workflow.id,
|
|
503
|
+
runRoot,
|
|
504
|
+
workspace,
|
|
505
|
+
initialCwd: cwd,
|
|
506
|
+
startedAt,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
const execution = await this.buildExecutionContext({
|
|
284
510
|
id,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
startedAt,
|
|
291
|
-
} });
|
|
292
|
-
const execution = await this.buildExecutionContext({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
511
|
+
currentRoot,
|
|
512
|
+
paths,
|
|
513
|
+
signal: activeRun.controller.signal,
|
|
514
|
+
logger,
|
|
515
|
+
});
|
|
293
516
|
const state = await NornRunStateStore.create(runRoot, {
|
|
294
517
|
projectRoot: paths.project,
|
|
295
518
|
id,
|
|
@@ -298,13 +521,34 @@ export class NornEngine {
|
|
|
298
521
|
workspace,
|
|
299
522
|
configOverride: options?.configOverride,
|
|
300
523
|
current: {
|
|
301
|
-
workflowId: workflow.id,
|
|
302
|
-
|
|
524
|
+
workflowId: workflow.id,
|
|
525
|
+
args,
|
|
526
|
+
cwd,
|
|
527
|
+
env: {},
|
|
528
|
+
interruption:
|
|
529
|
+
this.input.gateMode === "pause" && workflow.gate
|
|
530
|
+
? { status: "pending" }
|
|
531
|
+
: undefined,
|
|
303
532
|
},
|
|
304
533
|
startedAt,
|
|
305
534
|
});
|
|
306
|
-
const session = {
|
|
307
|
-
|
|
535
|
+
const session = {
|
|
536
|
+
runRoot,
|
|
537
|
+
cwd,
|
|
538
|
+
execution,
|
|
539
|
+
paths,
|
|
540
|
+
state,
|
|
541
|
+
lease,
|
|
542
|
+
runStore,
|
|
543
|
+
logger,
|
|
544
|
+
activeRun,
|
|
545
|
+
};
|
|
546
|
+
await logger.record({
|
|
547
|
+
type: "run.started",
|
|
548
|
+
workflowId: workflow.id,
|
|
549
|
+
cwd,
|
|
550
|
+
workspace,
|
|
551
|
+
});
|
|
308
552
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
309
553
|
return session;
|
|
310
554
|
} catch (error) {
|
|
@@ -314,18 +558,44 @@ export class NornEngine {
|
|
|
314
558
|
}
|
|
315
559
|
}
|
|
316
560
|
|
|
317
|
-
private async openRunSession(
|
|
561
|
+
private async openRunSession(
|
|
562
|
+
runRoot: string,
|
|
563
|
+
lease: NornRunLease,
|
|
564
|
+
): Promise<RunSession> {
|
|
318
565
|
const activeRun = this.startActiveRun(runRoot);
|
|
319
566
|
try {
|
|
320
567
|
const state = await NornRunStateStore.load(runRoot);
|
|
321
568
|
const currentState = state.currentState();
|
|
322
|
-
if (currentState.status === "completed")
|
|
569
|
+
if (currentState.status === "completed")
|
|
570
|
+
throw new Error(`Run is already completed: ${runRoot}`);
|
|
323
571
|
const runStore = await NornRunStore.open(runRoot);
|
|
324
572
|
const currentRoot = runCurrentRoot(runRoot);
|
|
325
|
-
const logger = await NornRunLogger.load(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
573
|
+
const logger = await NornRunLogger.load(
|
|
574
|
+
join(currentRoot, MANIFEST_FILE_NAME),
|
|
575
|
+
createRunFileCoordinator(runRoot),
|
|
576
|
+
);
|
|
577
|
+
const paths: NornWorkflowPaths = {
|
|
578
|
+
project: currentState.projectRoot ?? resolve(runRoot, "../../.."),
|
|
579
|
+
workspace: currentState.workspace,
|
|
580
|
+
};
|
|
581
|
+
const execution = await this.buildExecutionContext({
|
|
582
|
+
id: currentState.id,
|
|
583
|
+
currentRoot,
|
|
584
|
+
paths,
|
|
585
|
+
signal: activeRun.controller.signal,
|
|
586
|
+
logger,
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
runRoot,
|
|
590
|
+
cwd: process.cwd(),
|
|
591
|
+
execution,
|
|
592
|
+
paths,
|
|
593
|
+
state,
|
|
594
|
+
lease,
|
|
595
|
+
runStore,
|
|
596
|
+
logger,
|
|
597
|
+
activeRun,
|
|
598
|
+
};
|
|
329
599
|
} catch (error) {
|
|
330
600
|
this.failActiveRun(runRoot, activeRun);
|
|
331
601
|
throw error;
|
|
@@ -355,7 +625,8 @@ export class NornEngine {
|
|
|
355
625
|
}
|
|
356
626
|
|
|
357
627
|
private startActiveRun(runRoot: string): ActiveRun {
|
|
358
|
-
if (this.activeRuns.has(runRoot))
|
|
628
|
+
if (this.activeRuns.has(runRoot))
|
|
629
|
+
throw new Error(`Run is already active in this engine: ${runRoot}`);
|
|
359
630
|
const controller = new AbortController();
|
|
360
631
|
let finish: () => void;
|
|
361
632
|
const finished = new Promise<void>((resolvePromise) => {
|
|
@@ -363,12 +634,16 @@ export class NornEngine {
|
|
|
363
634
|
});
|
|
364
635
|
const abortFromParent = () => controller.abort(this.input.signal?.reason);
|
|
365
636
|
if (this.input.signal?.aborted) abortFromParent();
|
|
366
|
-
else
|
|
637
|
+
else
|
|
638
|
+
this.input.signal?.addEventListener("abort", abortFromParent, {
|
|
639
|
+
once: true,
|
|
640
|
+
});
|
|
367
641
|
const activeRun: ActiveRun = {
|
|
368
642
|
controller,
|
|
369
643
|
finished,
|
|
370
644
|
finish: () => finish(),
|
|
371
|
-
dispose: () =>
|
|
645
|
+
dispose: () =>
|
|
646
|
+
this.input.signal?.removeEventListener("abort", abortFromParent),
|
|
372
647
|
};
|
|
373
648
|
this.activeRuns.set(runRoot, activeRun);
|
|
374
649
|
return activeRun;
|
|
@@ -382,18 +657,21 @@ export class NornEngine {
|
|
|
382
657
|
}
|
|
383
658
|
|
|
384
659
|
private failActiveRun(runRoot: string, activeRun: ActiveRun): void {
|
|
385
|
-
if (this.activeRuns.get(runRoot) === activeRun)
|
|
660
|
+
if (this.activeRuns.get(runRoot) === activeRun)
|
|
661
|
+
this.activeRuns.delete(runRoot);
|
|
386
662
|
activeRun.dispose();
|
|
387
663
|
activeRun.finish();
|
|
388
664
|
}
|
|
389
|
-
|
|
390
665
|
}
|
|
391
666
|
|
|
392
667
|
function defaultRunRoot(cwd: string, id: string): string {
|
|
393
668
|
return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
|
|
394
669
|
}
|
|
395
670
|
|
|
396
|
-
function toWorkflowStep(
|
|
671
|
+
function toWorkflowStep(
|
|
672
|
+
workflow: NornAnyWorkflowDeclaration,
|
|
673
|
+
step: NonNullable<NornRunState["current"]>,
|
|
674
|
+
): WorkflowStep {
|
|
397
675
|
return {
|
|
398
676
|
workflow,
|
|
399
677
|
args: step.args,
|
|
@@ -401,7 +679,9 @@ function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<
|
|
|
401
679
|
};
|
|
402
680
|
}
|
|
403
681
|
|
|
404
|
-
function toRunStateStep(
|
|
682
|
+
function toRunStateStep(
|
|
683
|
+
step: WorkflowStep,
|
|
684
|
+
): NonNullable<NornRunState["current"]> {
|
|
405
685
|
return {
|
|
406
686
|
workflowId: step.workflow.id,
|
|
407
687
|
args: step.args,
|
|
@@ -412,23 +692,41 @@ function toRunStateStep(step: WorkflowStep): NonNullable<NornRunState["current"]
|
|
|
412
692
|
}
|
|
413
693
|
|
|
414
694
|
function shouldPauseForGate(step: WorkflowStep): boolean {
|
|
415
|
-
return
|
|
695
|
+
return (
|
|
696
|
+
step.interruption?.status === "pending" && step.workflow.gate !== undefined
|
|
697
|
+
);
|
|
416
698
|
}
|
|
417
699
|
|
|
418
700
|
function throwIfRunAborted(activeRun: ActiveRun): void {
|
|
419
701
|
if (!activeRun.controller.signal.aborted) return;
|
|
420
702
|
const reason: unknown = activeRun.controller.signal.reason;
|
|
421
703
|
if (reason instanceof Error) throw reason;
|
|
422
|
-
throw new Error(
|
|
704
|
+
throw new Error(
|
|
705
|
+
typeof reason === "string" && reason.length > 0 ? reason : "Run aborted",
|
|
706
|
+
);
|
|
423
707
|
}
|
|
424
708
|
|
|
425
709
|
function isRunStopped(session: RunSession, error: unknown): boolean {
|
|
426
|
-
return
|
|
710
|
+
return (
|
|
711
|
+
error instanceof NornRunStoppedError ||
|
|
712
|
+
session.activeRun.controller.signal.reason instanceof NornRunStoppedError
|
|
713
|
+
);
|
|
427
714
|
}
|
|
428
715
|
|
|
429
|
-
function interruptedLaunchResult(
|
|
716
|
+
function interruptedLaunchResult(
|
|
717
|
+
run: RunIdentity,
|
|
718
|
+
workflow: NornAnyWorkflowDeclaration,
|
|
719
|
+
args: unknown,
|
|
720
|
+
interruption: {
|
|
721
|
+
readonly description?: string;
|
|
722
|
+
readonly fields?: readonly string[];
|
|
723
|
+
},
|
|
724
|
+
): NornInterruptedRunResult {
|
|
430
725
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
431
|
-
if (!interruption.description)
|
|
726
|
+
if (!interruption.description)
|
|
727
|
+
throw new Error(
|
|
728
|
+
`Interrupted workflow is missing description: ${workflow.id}`,
|
|
729
|
+
);
|
|
432
730
|
return {
|
|
433
731
|
status: "interrupted",
|
|
434
732
|
id: run.id,
|
|
@@ -436,7 +734,12 @@ function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDecl
|
|
|
436
734
|
workspace: run.workspace,
|
|
437
735
|
cwd: run.cwd,
|
|
438
736
|
workflowId: workflow.id,
|
|
439
|
-
interruption: {
|
|
737
|
+
interruption: {
|
|
738
|
+
workflowId: workflow.id,
|
|
739
|
+
args,
|
|
740
|
+
description: interruption.description,
|
|
741
|
+
fields: interruption.fields,
|
|
742
|
+
},
|
|
440
743
|
};
|
|
441
744
|
}
|
|
442
745
|
|
|
@@ -454,12 +757,18 @@ async function recordRunEvent(
|
|
|
454
757
|
await session.logger.record(event);
|
|
455
758
|
}
|
|
456
759
|
|
|
457
|
-
async function commitRunBoundary(
|
|
760
|
+
async function commitRunBoundary(
|
|
761
|
+
session: RunSession,
|
|
762
|
+
message: string,
|
|
763
|
+
): Promise<void> {
|
|
458
764
|
await session.lease.assertOwned();
|
|
459
765
|
await session.runStore.snapshotCurrent(message);
|
|
460
766
|
}
|
|
461
767
|
|
|
462
|
-
async function assertWorkspaceBoundary(
|
|
768
|
+
async function assertWorkspaceBoundary(
|
|
769
|
+
session: RunSession,
|
|
770
|
+
workspace: string,
|
|
771
|
+
): Promise<void> {
|
|
463
772
|
await session.lease.assertOwned();
|
|
464
773
|
await session.runStore.assertWorkspaceCanBeSnapshotted(workspace);
|
|
465
774
|
}
|