@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35390859630.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 +17 -26
- package/assets/docs/README.md +3 -3
- package/assets/docs/agents.md +12 -1
- package/assets/docs/cli.md +5 -5
- package/assets/docs/composition.md +21 -21
- package/assets/docs/persistence.md +7 -10
- package/assets/docs/projects.md +30 -18
- package/assets/docs/providers.md +1 -1
- package/assets/docs/recovery.md +13 -9
- package/assets/docs/resources.md +4 -19
- package/assets/docs/schemas.md +1 -1
- package/assets/docs/workflows.md +95 -7
- package/assets/examples/agent-then-analysis/README.md +4 -4
- 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 +55 -68
- package/assets/examples/caller-selected-continuation/README.md +5 -5
- package/assets/examples/caller-selected-continuation/caller.ts +35 -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 +15 -23
- package/assets/examples/coordinating-multiple-agents/README.md +2 -2
- 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 +52 -56
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +4 -4
- package/assets/examples/getting-started/norn.project.json +1 -1
- package/assets/examples/getting-started/plugin.ts +16 -26
- package/assets/examples/minimal-workflow/README.md +8 -8
- package/assets/examples/minimal-workflow/norn.project.json +1 -1
- package/assets/examples/minimal-workflow/plugin.ts +11 -25
- package/assets/examples/shared-state/README.md +10 -2
- 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 +36 -41
- package/assets/examples/shared-state/shared-state.ts +75 -0
- package/assets/{packages/sdk/src → examples/shared-state}/state-adapter.ts +17 -16
- package/assets/examples/worktree-development-loop/README.md +6 -7
- 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/schema.ts +2 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +34 -33
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +7 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +23 -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 +40 -31
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +5 -4
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +49 -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 +4 -3
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +35 -34
- 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/engine.ts +38 -62
- 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/run-state.ts +31 -21
- package/assets/packages/cli/src/internal/run.ts +2 -5
- package/assets/packages/cli/src/internal/workflow-registry.ts +114 -111
- package/assets/packages/cli/src/workflow-loader.ts +214 -0
- package/assets/packages/core/src/workflow-transition.ts +2 -2
- package/assets/packages/sdk/src/api.ts +133 -289
- package/assets/packages/sdk/src/index.ts +1 -1
- package/assets/packages/sdk/src/schema.ts +3 -3
- package/assets/tests/workflow-ref.test.ts +50 -51
- package/dist/cli.js +33 -32
- 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/engine.d.ts +4 -8
- package/dist/internal/engine.js +36 -49
- package/dist/internal/errors.d.ts +3 -3
- package/dist/internal/errors.js +1 -1
- package/dist/internal/launch-request.d.ts +4 -4
- package/dist/internal/launch-request.js +2 -2
- package/dist/internal/run-state.d.ts +9 -8
- package/dist/internal/run-state.js +29 -20
- package/dist/internal/run.d.ts +1 -3
- package/dist/internal/run.js +3 -5
- package/dist/internal/workflow-registry.d.ts +25 -17
- package/dist/internal/workflow-registry.js +107 -72
- package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
- package/dist/workflow-loader.js +235 -0
- package/package.json +2 -2
- 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/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/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/internal/engine.js
CHANGED
|
@@ -11,12 +11,11 @@ import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRe
|
|
|
11
11
|
import { NornRunLogs } from "./logs.js";
|
|
12
12
|
import { NornRunLease } from "./run-lease.js";
|
|
13
13
|
import { NornRunLogger } from "./run-log.js";
|
|
14
|
-
import {
|
|
15
|
-
import { getRunInfo,
|
|
14
|
+
import { NornRunResources } from "../resources.js";
|
|
15
|
+
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot } from "./run-state.js";
|
|
16
16
|
import { NornRunStore, runCurrentRoot } from "./run-store.js";
|
|
17
17
|
import { NornRunContext } from "./run.js";
|
|
18
|
-
import {
|
|
19
|
-
import { decodePluginConfiguration, NornWorkflowRegistry } from "./workflow-registry.js";
|
|
18
|
+
import { NornWorkflowRegistry } from "./workflow-registry.js";
|
|
20
19
|
var RUNS_DIR_NAME = ".norn";
|
|
21
20
|
var MANIFEST_FILE_NAME = "manifest.json";
|
|
22
21
|
var NornEngine = class {
|
|
@@ -26,22 +25,18 @@ var NornEngine = class {
|
|
|
26
25
|
}
|
|
27
26
|
input;
|
|
28
27
|
registry = new NornWorkflowRegistry();
|
|
29
|
-
registrarState = new NornMemoryWorkflowState();
|
|
30
|
-
disposersByPlugin = /* @__PURE__ */ new Map();
|
|
31
28
|
responseCollector;
|
|
32
29
|
activeRuns = /* @__PURE__ */ new Map();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
this.disposersByPlugin.set(plugin.manifest.id, disposers);
|
|
30
|
+
registerWorkflows(workflows) {
|
|
31
|
+
const disposers = [];
|
|
32
|
+
try {
|
|
33
|
+
for (const workflow of workflows) disposers.push(this.registry.register({ workflow, config: this.input.config ?? {}, source: void 0 }));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
43
38
|
return () => {
|
|
44
|
-
|
|
39
|
+
for (const dispose of disposers) dispose();
|
|
45
40
|
};
|
|
46
41
|
}
|
|
47
42
|
listWorkflows() {
|
|
@@ -50,11 +45,8 @@ var NornEngine = class {
|
|
|
50
45
|
visibleWorkflowEntries() {
|
|
51
46
|
return this.registry.launchableEntries();
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
async runWorkflow(workflow, params, options) {
|
|
57
|
-
const session = await this.createRunSession(workflow, params, options);
|
|
48
|
+
async runWorkflow(workflow, args, options) {
|
|
49
|
+
const session = await this.createRunSession(workflow, args, options);
|
|
58
50
|
const current = session.state.currentState().current;
|
|
59
51
|
if (!current) throw new Error("New run is missing its initial workflow step");
|
|
60
52
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
@@ -65,6 +57,7 @@ var NornEngine = class {
|
|
|
65
57
|
}
|
|
66
58
|
async rollbackRun(path, checkpointId) {
|
|
67
59
|
const runRoot = await resolveRunRoot(this.input.cwd, path);
|
|
60
|
+
assertRunVersion((await getRunInfo(runRoot)).version);
|
|
68
61
|
const lease = await NornRunLease.acquire(runRoot);
|
|
69
62
|
try {
|
|
70
63
|
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
@@ -80,19 +73,19 @@ var NornEngine = class {
|
|
|
80
73
|
await lease.release();
|
|
81
74
|
}
|
|
82
75
|
}
|
|
83
|
-
async resumeWorkflow(path,
|
|
84
|
-
return this.resumeWorkflowExecution({ path,
|
|
76
|
+
async resumeWorkflow(path, args) {
|
|
77
|
+
return this.resumeWorkflowExecution({ path, args, expectedRequest: void 0 });
|
|
85
78
|
}
|
|
86
79
|
async resumeRequestedWorkflow(input) {
|
|
87
|
-
return this.resumeWorkflowExecution({ path: input.runRoot,
|
|
80
|
+
return this.resumeWorkflowExecution({ path: input.runRoot, args: input.request.args, expectedRequest: input.request });
|
|
88
81
|
}
|
|
89
82
|
async resumeWorkflowExecution(input) {
|
|
90
|
-
const {
|
|
83
|
+
const { args, expectedRequest } = input;
|
|
91
84
|
const runRoot = await resolveRunRoot(this.input.cwd, input.path);
|
|
92
85
|
const initialStateStore = await NornRunStateStore.load(runRoot);
|
|
93
86
|
const initialState = initialStateStore.currentState();
|
|
94
|
-
if (initialState.status === "interrupted" &&
|
|
95
|
-
if (initialState.status === "pendingResume" &&
|
|
87
|
+
if (initialState.status === "interrupted" && args === void 0) throw new Error(`Interrupted workflow resume requires args: ${runRoot}`);
|
|
88
|
+
if (initialState.status === "pendingResume" && args !== void 0) throw new Error(`Pending-resume workflows do not accept args: ${runRoot}`);
|
|
96
89
|
if (initialState.status !== "interrupted" && initialState.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
97
90
|
const lease = await NornRunLease.acquire(runRoot);
|
|
98
91
|
let isLeaseOwnedByScheduler = false;
|
|
@@ -107,9 +100,9 @@ var NornEngine = class {
|
|
|
107
100
|
const workflow = this.registry.workflowById(current.workflowId);
|
|
108
101
|
if (!workflow) throw new Error(`Unknown workflow for resumed run: ${current.workflowId}`);
|
|
109
102
|
if (state.status === "interrupted") {
|
|
110
|
-
const
|
|
111
|
-
Value.Decode(workflow.
|
|
112
|
-
await session.state.
|
|
103
|
+
const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
|
|
104
|
+
Value.Decode(workflow.args, rawArgs);
|
|
105
|
+
await session.state.replaceCurrentArgs(rawArgs);
|
|
113
106
|
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
|
|
114
107
|
const resumedCurrent = session.state.currentState().current;
|
|
115
108
|
if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
@@ -137,12 +130,12 @@ var NornEngine = class {
|
|
|
137
130
|
throwIfRunAborted(session.activeRun);
|
|
138
131
|
const stepRuntime = session.run.forWorkflow(currentStep.workflow);
|
|
139
132
|
if (shouldPauseForGate(currentStep)) {
|
|
140
|
-
const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.
|
|
133
|
+
const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.args, session.state.currentState().configOverride);
|
|
141
134
|
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
142
|
-
await session.state.interruptCurrent(currentStep.
|
|
135
|
+
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
143
136
|
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
144
137
|
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
145
|
-
return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.
|
|
138
|
+
return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
146
139
|
}
|
|
147
140
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
148
141
|
const stepResult = await this.executeWorkflowStep(session, currentStep, stepRuntime);
|
|
@@ -184,7 +177,7 @@ var NornEngine = class {
|
|
|
184
177
|
try {
|
|
185
178
|
await assertWorkspaceBoundary(session, run.workspace);
|
|
186
179
|
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
187
|
-
const result = await this.registry.execute(step.workflow, run, step.
|
|
180
|
+
const result = await this.registry.execute(step.workflow, run, step.args, session.state.currentState().configOverride);
|
|
188
181
|
await assertWorkspaceBoundary(session, run.workspace);
|
|
189
182
|
const durationMs = Date.now() - startedAtMs;
|
|
190
183
|
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
@@ -206,11 +199,11 @@ var NornEngine = class {
|
|
|
206
199
|
if (!workflow) throw new Error(`Unknown next workflow: ${next.workflowId}`);
|
|
207
200
|
return {
|
|
208
201
|
workflow,
|
|
209
|
-
|
|
202
|
+
args: next.args,
|
|
210
203
|
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
|
|
211
204
|
};
|
|
212
205
|
}
|
|
213
|
-
async createRunSession(workflow,
|
|
206
|
+
async createRunSession(workflow, args, options) {
|
|
214
207
|
const id = options?.id ?? randomUUID();
|
|
215
208
|
const name = options?.name ?? id;
|
|
216
209
|
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
@@ -242,7 +235,7 @@ var NornEngine = class {
|
|
|
242
235
|
configOverride: options?.configOverride,
|
|
243
236
|
current: {
|
|
244
237
|
workflowId: workflow.id,
|
|
245
|
-
|
|
238
|
+
args,
|
|
246
239
|
cwd,
|
|
247
240
|
env: {},
|
|
248
241
|
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
|
|
@@ -284,7 +277,7 @@ var NornEngine = class {
|
|
|
284
277
|
await mkdir(input.workspace, { recursive: true });
|
|
285
278
|
await mkdir(artifactsRoot, { recursive: true });
|
|
286
279
|
await mkdir(logsRoot, { recursive: true });
|
|
287
|
-
const
|
|
280
|
+
const resources = await NornRunResources.initialize(dirname(input.currentRoot));
|
|
288
281
|
return new NornRunContext({
|
|
289
282
|
id: input.id,
|
|
290
283
|
runRoot: input.currentRoot,
|
|
@@ -296,7 +289,6 @@ var NornEngine = class {
|
|
|
296
289
|
agentDir: this.input.agentDir,
|
|
297
290
|
responseCollector: this.responseCollector,
|
|
298
291
|
resources,
|
|
299
|
-
state,
|
|
300
292
|
logger: input.logger,
|
|
301
293
|
artifacts: new NornArtifacts(artifactsRoot, resources.files),
|
|
302
294
|
logs: new NornRunLogs(logsRoot, resources.files)
|
|
@@ -332,11 +324,6 @@ var NornEngine = class {
|
|
|
332
324
|
activeRun.dispose();
|
|
333
325
|
activeRun.finish();
|
|
334
326
|
}
|
|
335
|
-
disposePlugin(pluginId) {
|
|
336
|
-
const disposers = this.disposersByPlugin.get(pluginId) ?? [];
|
|
337
|
-
for (const dispose of [...disposers].reverse()) dispose();
|
|
338
|
-
this.disposersByPlugin.delete(pluginId);
|
|
339
|
-
}
|
|
340
327
|
};
|
|
341
328
|
function defaultRunRoot(cwd, id) {
|
|
342
329
|
return join(cwd, RUNS_DIR_NAME, "runs", id);
|
|
@@ -347,14 +334,14 @@ function workflowDefaultCwd(projectRoot, workspace, workflow) {
|
|
|
347
334
|
function toWorkflowStep(workflow, step) {
|
|
348
335
|
return {
|
|
349
336
|
workflow,
|
|
350
|
-
|
|
337
|
+
args: step.args,
|
|
351
338
|
interruption: step.interruption
|
|
352
339
|
};
|
|
353
340
|
}
|
|
354
341
|
function toRunStateStep(step) {
|
|
355
342
|
return {
|
|
356
343
|
workflowId: step.workflow.id,
|
|
357
|
-
|
|
344
|
+
args: step.args,
|
|
358
345
|
cwd: "",
|
|
359
346
|
env: {},
|
|
360
347
|
interruption: step.interruption
|
|
@@ -372,7 +359,7 @@ function throwIfRunAborted(activeRun) {
|
|
|
372
359
|
function isRunStopped(session, error) {
|
|
373
360
|
return error instanceof NornRunStoppedError || session.activeRun.controller.signal.reason instanceof NornRunStoppedError;
|
|
374
361
|
}
|
|
375
|
-
function interruptedLaunchResult(run, workflow,
|
|
362
|
+
function interruptedLaunchResult(run, workflow, args, interruption) {
|
|
376
363
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
377
364
|
if (!interruption.description) throw new Error(`Interrupted workflow is missing description: ${workflow.id}`);
|
|
378
365
|
return {
|
|
@@ -382,7 +369,7 @@ function interruptedLaunchResult(run, workflow, params, interruption) {
|
|
|
382
369
|
workspace: run.workspace,
|
|
383
370
|
cwd: run.cwd,
|
|
384
371
|
workflowId: workflow.id,
|
|
385
|
-
interruption: { workflowId: workflow.id,
|
|
372
|
+
interruption: { workflowId: workflow.id, args, description: interruption.description, fields: interruption.fields }
|
|
386
373
|
};
|
|
387
374
|
}
|
|
388
375
|
async function recordRunEvent(session, event) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NornWorkflowDiagnostic } from "@vimhead.dev/norn";
|
|
2
2
|
import { AssertError } from "typebox/value";
|
|
3
3
|
export declare class NornProjectLoadError extends Error {
|
|
4
4
|
readonly code = "NORN_PROJECT_INVALID";
|
|
5
5
|
readonly isComplete = false;
|
|
6
|
-
readonly diagnostics: readonly
|
|
6
|
+
readonly diagnostics: readonly NornWorkflowDiagnostic[];
|
|
7
7
|
constructor(input: {
|
|
8
|
-
readonly diagnostics: readonly
|
|
8
|
+
readonly diagnostics: readonly NornWorkflowDiagnostic[];
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
export declare class NornRunStoppedError extends Error {
|
package/dist/internal/errors.js
CHANGED
|
@@ -6,7 +6,7 @@ var NornProjectLoadError = class extends Error {
|
|
|
6
6
|
diagnostics;
|
|
7
7
|
constructor(input) {
|
|
8
8
|
const details = input.diagnostics.map(
|
|
9
|
-
(diagnostic) => `${diagnostic.stage}: ${diagnostic.
|
|
9
|
+
(diagnostic) => `${diagnostic.stage}: ${diagnostic.modulePath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`
|
|
10
10
|
);
|
|
11
11
|
super(`Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.
|
|
12
12
|
${details.join("\n")}`);
|
|
@@ -3,21 +3,21 @@ export declare const LAUNCH_REQUEST_FILE_NAME = "launch-request.json";
|
|
|
3
3
|
export declare const RESUME_REQUEST_FILE_NAME = "resume-request.json";
|
|
4
4
|
export declare const RESUME_START_GRACE_MS = 60000;
|
|
5
5
|
export type NornRunLaunchRequest = {
|
|
6
|
-
readonly version:
|
|
6
|
+
readonly version: 2;
|
|
7
7
|
readonly type: "run";
|
|
8
8
|
readonly id: string;
|
|
9
9
|
readonly name: string;
|
|
10
10
|
readonly workflowId: string;
|
|
11
|
-
readonly
|
|
11
|
+
readonly args: unknown;
|
|
12
12
|
readonly configOverride?: unknown;
|
|
13
13
|
readonly createdAt: string;
|
|
14
14
|
};
|
|
15
15
|
export type NornRunResumeRequest = {
|
|
16
|
-
readonly version:
|
|
16
|
+
readonly version: 2;
|
|
17
17
|
readonly type: "resume";
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly requestId?: string;
|
|
20
|
-
readonly
|
|
20
|
+
readonly args?: unknown;
|
|
21
21
|
readonly createdAt: string;
|
|
22
22
|
};
|
|
23
23
|
export declare function writeRunLaunchRequest(runRoot: string, request: NornRunLaunchRequest): Promise<void>;
|
|
@@ -80,7 +80,7 @@ async function clearRunResumeRequest(input) {
|
|
|
80
80
|
function parseRunLaunchRequest(value) {
|
|
81
81
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
|
|
82
82
|
const request = value;
|
|
83
|
-
if (request.version !==
|
|
83
|
+
if (request.version !== 2 || request.type !== "run") throw new Error("Unsupported workflow launch request");
|
|
84
84
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
|
|
85
85
|
if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
|
|
86
86
|
if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
|
|
@@ -90,7 +90,7 @@ function parseRunLaunchRequest(value) {
|
|
|
90
90
|
function parseRunResumeRequest(value) {
|
|
91
91
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
|
|
92
92
|
const request = value;
|
|
93
|
-
if (request.version !==
|
|
93
|
+
if (request.version !== 2 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
|
|
94
94
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
|
|
95
95
|
if (request.requestId !== void 0 && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
|
|
96
96
|
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { NornRunStatus, NornRunOutcomeMetadata, NornRunInfo } from "@vimhead.dev/norn";
|
|
2
2
|
export declare const RUN_STATE_FILE_NAME = "run-state.json";
|
|
3
|
-
export declare function
|
|
4
|
-
declare const RUN_STATE_VERSION =
|
|
3
|
+
export declare function mergeInterruptedWorkflowArgs(currentArgs: unknown, args: unknown, fields: readonly string[] | undefined): unknown;
|
|
4
|
+
declare const RUN_STATE_VERSION = 2;
|
|
5
5
|
type NornRunWorkflowStep = {
|
|
6
6
|
readonly workflowId: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly args: unknown;
|
|
8
8
|
readonly cwd: string;
|
|
9
9
|
readonly env: Record<string, string>;
|
|
10
10
|
readonly interruption?: RuntimeInterruption;
|
|
@@ -39,7 +39,7 @@ type NornRunWorkflowOutcome = {
|
|
|
39
39
|
readonly metadata?: NornRunOutcomeMetadata;
|
|
40
40
|
};
|
|
41
41
|
export type NornRunState = {
|
|
42
|
-
readonly version: typeof RUN_STATE_VERSION;
|
|
42
|
+
readonly version: 1 | typeof RUN_STATE_VERSION;
|
|
43
43
|
readonly id: string;
|
|
44
44
|
readonly name: string;
|
|
45
45
|
readonly entrypointWorkflowId: string;
|
|
@@ -76,20 +76,21 @@ export declare class NornRunStateStore {
|
|
|
76
76
|
failRun(workflowId: string, metadata: NornRunOutcomeMetadata & {
|
|
77
77
|
readonly summary: string;
|
|
78
78
|
}): Promise<void>;
|
|
79
|
-
interruptCurrent(
|
|
79
|
+
interruptCurrent(args: unknown, interruption: {
|
|
80
80
|
readonly description: string;
|
|
81
81
|
readonly fields?: readonly string[];
|
|
82
82
|
}): Promise<void>;
|
|
83
83
|
prepareForResumeAfterRollback(): Promise<void>;
|
|
84
84
|
stopCurrent(): Promise<void>;
|
|
85
|
-
|
|
86
|
-
failCurrent(errorOrMetadata: string | NornRunOutcomeMetadata & {
|
|
85
|
+
replaceCurrentArgs(args: unknown): Promise<void>;
|
|
86
|
+
failCurrent(errorOrMetadata: string | (NornRunOutcomeMetadata & {
|
|
87
87
|
readonly summary: string;
|
|
88
|
-
}): Promise<void>;
|
|
88
|
+
})): Promise<void>;
|
|
89
89
|
private update;
|
|
90
90
|
private write;
|
|
91
91
|
}
|
|
92
92
|
export declare function listRuns(sessionCwd: string): Promise<NornRunInfo[]>;
|
|
93
93
|
export declare function getRunInfo(runRoot: string): Promise<NornRunInfo>;
|
|
94
94
|
export declare function resolveRunRoot(sessionCwd: string, run: string): Promise<string>;
|
|
95
|
+
export declare function assertRunVersion(version: unknown): void;
|
|
95
96
|
export {};
|
|
@@ -45,15 +45,15 @@ import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
|
45
45
|
import { Value } from "typebox/value";
|
|
46
46
|
var RUN_STATE_FILE_NAME = "run-state.json";
|
|
47
47
|
var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
48
|
-
function
|
|
49
|
-
if (!isRecord(
|
|
48
|
+
function mergeInterruptedWorkflowArgs(currentArgs, args, fields) {
|
|
49
|
+
if (!isRecord(currentArgs) || !isRecord(args)) return args;
|
|
50
50
|
if (fields) {
|
|
51
|
-
const unsupportedFields = Object.keys(
|
|
52
|
-
if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow
|
|
51
|
+
const unsupportedFields = Object.keys(args).filter((field) => !fields.includes(field));
|
|
52
|
+
if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow args cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
|
|
53
53
|
}
|
|
54
|
-
return { ...
|
|
54
|
+
return { ...currentArgs, ...args };
|
|
55
55
|
}
|
|
56
|
-
var RUN_STATE_VERSION =
|
|
56
|
+
var RUN_STATE_VERSION = 2;
|
|
57
57
|
var NornRunStateStore = class _NornRunStateStore {
|
|
58
58
|
path;
|
|
59
59
|
state;
|
|
@@ -87,7 +87,7 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
87
87
|
static async load(runRoot) {
|
|
88
88
|
const currentRoot = runCurrentRoot(runRoot);
|
|
89
89
|
const files = createRunFileCoordinator(runRoot);
|
|
90
|
-
const state = await files.withExclusiveLock(join(currentRoot, RUN_STATE_FILE_NAME), async (path) => parseNornRunState(await readRunStateFile({ path, legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) })));
|
|
90
|
+
const state = await files.withExclusiveLock(join(currentRoot, RUN_STATE_FILE_NAME), async (path) => parseNornRunState(await readRunStateFile({ path, legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) }), "execute"));
|
|
91
91
|
return new _NornRunStateStore({ path: join(currentRoot, RUN_STATE_FILE_NAME), state, files });
|
|
92
92
|
}
|
|
93
93
|
currentState() {
|
|
@@ -125,11 +125,11 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
125
125
|
failed: { workflowId, error: metadata.summary, metadata, failedAt }
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
|
-
async interruptCurrent(
|
|
128
|
+
async interruptCurrent(args, interruption) {
|
|
129
129
|
if (!this.state.current) throw new Error("Cannot interrupt run without current step");
|
|
130
130
|
await this.update({
|
|
131
131
|
status: "interrupted",
|
|
132
|
-
current: { ...this.state.current,
|
|
132
|
+
current: { ...this.state.current, args, interruption: { status: "pending", ...interruption } },
|
|
133
133
|
outcome: null,
|
|
134
134
|
failed: null
|
|
135
135
|
});
|
|
@@ -144,11 +144,11 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
144
144
|
if (!this.state.current) throw new Error("Cannot stop run without current step");
|
|
145
145
|
await this.update({ status: "stopped", outcome: null, failed: null });
|
|
146
146
|
}
|
|
147
|
-
async
|
|
147
|
+
async replaceCurrentArgs(args) {
|
|
148
148
|
if (!this.state.current) throw new Error("Cannot resume run without current step");
|
|
149
149
|
await this.update({
|
|
150
150
|
status: "running",
|
|
151
|
-
current: { ...this.state.current,
|
|
151
|
+
current: { ...this.state.current, args, interruption: { ...this.state.current.interruption, status: "satisfied" } },
|
|
152
152
|
outcome: null,
|
|
153
153
|
failed: null
|
|
154
154
|
});
|
|
@@ -165,15 +165,15 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
165
165
|
}
|
|
166
166
|
async update(patch) {
|
|
167
167
|
await this.files.withExclusiveLock(this.path, async (path) => {
|
|
168
|
-
const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
|
|
168
|
+
const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }), "execute");
|
|
169
169
|
const next = { ...current, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
170
|
-
if (next.current?.
|
|
170
|
+
if (next.current?.args !== void 0) Value.Assert(jsonValueSchema, next.current.args);
|
|
171
171
|
await writeJsonAtomically(path, next);
|
|
172
172
|
this.state = next;
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
async write() {
|
|
176
|
-
if (this.state.current?.
|
|
176
|
+
if (this.state.current?.args !== void 0) Value.Assert(jsonValueSchema, this.state.current.args);
|
|
177
177
|
await mkdir2(dirname2(this.path), { recursive: true });
|
|
178
178
|
await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
|
|
179
179
|
}
|
|
@@ -192,8 +192,13 @@ async function listRuns(sessionCwd) {
|
|
|
192
192
|
}
|
|
193
193
|
async function getRunInfo(runRoot) {
|
|
194
194
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
195
|
+
const pendingResume = await readOptionalRunResumeRequest(runRoot).then(
|
|
196
|
+
(request) => ({ status: "fulfilled", request }),
|
|
197
|
+
(error) => ({ status: "rejected", error })
|
|
198
|
+
);
|
|
199
|
+
const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)), "inspect");
|
|
200
|
+
if (state.version === RUN_STATE_VERSION && pendingResume.status === "rejected") throw pendingResume.error;
|
|
201
|
+
const resumeRequest = state.version === RUN_STATE_VERSION && pendingResume.status === "fulfilled" ? pendingResume.request : void 0;
|
|
197
202
|
return {
|
|
198
203
|
version: state.version,
|
|
199
204
|
id: state.id,
|
|
@@ -277,10 +282,13 @@ async function readRunStateFile(input) {
|
|
|
277
282
|
function isRecord(value) {
|
|
278
283
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
279
284
|
}
|
|
280
|
-
function
|
|
285
|
+
function assertRunVersion(version) {
|
|
286
|
+
if (version !== RUN_STATE_VERSION) throw new Error(`Incompatible saved run state version ${String(version)}; this alpha requires version ${RUN_STATE_VERSION}. Use the original Norn version to resume this run, or start a new run. Saved files have not been migrated.`);
|
|
287
|
+
}
|
|
288
|
+
function parseNornRunState(value, mode) {
|
|
281
289
|
if (!value || typeof value !== "object") throw new Error("Invalid run state");
|
|
282
290
|
const state = value;
|
|
283
|
-
if (state.version !==
|
|
291
|
+
if (mode === "execute" || state.version !== 1) assertRunVersion(state.version);
|
|
284
292
|
if (typeof state.id !== "string" || state.id.length === 0) throw new Error("Invalid run state id");
|
|
285
293
|
const normalizedState = state;
|
|
286
294
|
if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0) normalizedState.name = state.id;
|
|
@@ -306,7 +314,7 @@ function runInterruption(state) {
|
|
|
306
314
|
if (state.status !== "interrupted" || !state.current?.interruption) return void 0;
|
|
307
315
|
return {
|
|
308
316
|
workflowId: state.current.workflowId,
|
|
309
|
-
params: state.current.
|
|
317
|
+
args: state.version === 1 ? state.current.params : state.current.args,
|
|
310
318
|
description: state.current.interruption.description ?? "",
|
|
311
319
|
fields: state.current.interruption.fields
|
|
312
320
|
};
|
|
@@ -320,8 +328,9 @@ function runFailure(state) {
|
|
|
320
328
|
export {
|
|
321
329
|
NornRunStateStore,
|
|
322
330
|
RUN_STATE_FILE_NAME,
|
|
331
|
+
assertRunVersion,
|
|
323
332
|
getRunInfo,
|
|
324
333
|
listRuns,
|
|
325
|
-
|
|
334
|
+
mergeInterruptedWorkflowArgs,
|
|
326
335
|
resolveRunRoot
|
|
327
336
|
};
|
package/dist/internal/run.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ type DefaultNornRunInput = {
|
|
|
18
18
|
readonly agentDir?: string;
|
|
19
19
|
readonly responseCollector: NornAgentResponseCollector;
|
|
20
20
|
readonly resources: NornRunResources;
|
|
21
|
-
readonly state: NornRun["state"];
|
|
22
21
|
readonly logger: NornRunLogger;
|
|
23
22
|
readonly artifacts: NornArtifacts;
|
|
24
23
|
readonly logs: NornRunLogs;
|
|
@@ -26,7 +25,6 @@ type DefaultNornRunInput = {
|
|
|
26
25
|
export declare class NornRunContext implements NornProjectRun {
|
|
27
26
|
private readonly input;
|
|
28
27
|
readonly resources: NornRunResources;
|
|
29
|
-
readonly state: NornRun["state"];
|
|
30
28
|
readonly artifacts: NornRun["artifacts"];
|
|
31
29
|
readonly logs: NornRun["logs"];
|
|
32
30
|
readonly commands: NornRun["commands"];
|
|
@@ -41,7 +39,7 @@ export declare class NornRunContext implements NornProjectRun {
|
|
|
41
39
|
private createAgentRunner;
|
|
42
40
|
path(relativePath: string): string;
|
|
43
41
|
projectPath(relativePath: string): string;
|
|
44
|
-
next(workflowId: string,
|
|
42
|
+
next(workflowId: string, args: unknown): ReturnType<NornRun["next"]>;
|
|
45
43
|
complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]>;
|
|
46
44
|
fail(metadata: NornRunOutcomeMetadata & {
|
|
47
45
|
readonly summary: string;
|
package/dist/internal/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
// ../core/src/workflow-transition.ts
|
|
5
5
|
function createWorkflowTransition(input) {
|
|
6
6
|
if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
|
|
7
|
-
return { type: "next", workflowId: input.workflowId,
|
|
7
|
+
return { type: "next", workflowId: input.workflowId, args: input.args };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// src/internal/run.ts
|
|
@@ -18,7 +18,6 @@ var NornRunContext = class _NornRunContext {
|
|
|
18
18
|
this.projectRoot = input.projectRoot;
|
|
19
19
|
this.cwd = input.cwd;
|
|
20
20
|
this.resources = input.resources;
|
|
21
|
-
this.state = input.state;
|
|
22
21
|
this.artifacts = input.artifacts;
|
|
23
22
|
this.logs = {
|
|
24
23
|
read: (log) => this.input.logs.read(log)
|
|
@@ -40,7 +39,6 @@ var NornRunContext = class _NornRunContext {
|
|
|
40
39
|
}
|
|
41
40
|
input;
|
|
42
41
|
resources;
|
|
43
|
-
state;
|
|
44
42
|
artifacts;
|
|
45
43
|
logs;
|
|
46
44
|
commands;
|
|
@@ -82,8 +80,8 @@ var NornRunContext = class _NornRunContext {
|
|
|
82
80
|
projectPath(relativePath) {
|
|
83
81
|
return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
|
|
84
82
|
}
|
|
85
|
-
next(workflowId,
|
|
86
|
-
return createWorkflowTransition({ workflowId,
|
|
83
|
+
next(workflowId, args) {
|
|
84
|
+
return createWorkflowTransition({ workflowId, args });
|
|
87
85
|
}
|
|
88
86
|
complete(metadata) {
|
|
89
87
|
return { type: "complete", metadata };
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type
|
|
2
|
-
import {
|
|
3
|
-
export type NornRegisteredWorkflow = {
|
|
4
|
-
workflow: NornAnyWorkflowDeclaration;
|
|
5
|
-
implementation: NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>;
|
|
6
|
-
configuration?: NornRegisteredConfiguration;
|
|
7
|
-
plugin?: NornWorkflowPluginInfo;
|
|
8
|
-
};
|
|
1
|
+
import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type NornWorkflowSource, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
9
3
|
export type NornRegisteredConfiguration = {
|
|
10
|
-
readonly
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly schema: TSchema | undefined;
|
|
11
6
|
readonly input: unknown;
|
|
12
7
|
readonly value: unknown;
|
|
13
8
|
};
|
|
14
|
-
export
|
|
15
|
-
readonly
|
|
9
|
+
export type NornRegisteredWorkflow = {
|
|
10
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
11
|
+
readonly configuration: NornRegisteredConfiguration;
|
|
12
|
+
readonly scopeId: string | undefined;
|
|
13
|
+
readonly source: NornWorkflowSource | undefined;
|
|
14
|
+
};
|
|
15
|
+
export declare function decodeConfiguration(input: {
|
|
16
|
+
readonly key: string;
|
|
16
17
|
readonly schema: TSchema | undefined;
|
|
17
18
|
readonly value: unknown;
|
|
18
|
-
}): NornRegisteredConfiguration
|
|
19
|
+
}): NornRegisteredConfiguration;
|
|
20
|
+
export declare function assertCompatibleConfigurationOwners(workflows: readonly NornAnyWorkflowDeclaration[]): void;
|
|
19
21
|
export type NornWorkflowStepResult = NornRunNext | {
|
|
20
22
|
readonly type: "complete";
|
|
21
23
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
@@ -27,17 +29,23 @@ export type NornWorkflowStepResult = NornRunNext | {
|
|
|
27
29
|
};
|
|
28
30
|
export declare class NornWorkflowRegistry {
|
|
29
31
|
private readonly entries;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly
|
|
32
|
+
private readonly scopes;
|
|
33
|
+
register(input: {
|
|
34
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
35
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
36
|
+
readonly source: NornWorkflowSource | undefined;
|
|
33
37
|
}): NornDispose;
|
|
34
38
|
list(options?: {
|
|
35
39
|
readonly entrypointsOnly?: boolean;
|
|
36
40
|
}): NornRegisteredWorkflowInfo[];
|
|
37
41
|
inspect(workflowId: string): NornInspectedWorkflowInfo | undefined;
|
|
42
|
+
configurationInfos(): NornProjectConfigurationInfo[];
|
|
38
43
|
launchableEntries(): NornRegisteredWorkflow[];
|
|
39
44
|
workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined;
|
|
40
|
-
describeGate<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>,
|
|
41
|
-
execute<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>,
|
|
45
|
+
describeGate<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, args: unknown, configOverride?: unknown): Promise<string>;
|
|
46
|
+
execute<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, args: unknown, configOverride?: unknown): Promise<NornWorkflowStepResult>;
|
|
47
|
+
private createExecutionContext;
|
|
48
|
+
private requireEntry;
|
|
42
49
|
private sortedEntries;
|
|
43
50
|
}
|
|
51
|
+
export declare function assertWorkflowDefinition(workflow: NornAnyWorkflowDeclaration): void;
|