@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.
Files changed (111) hide show
  1. package/assets/README.md +17 -26
  2. package/assets/docs/README.md +3 -3
  3. package/assets/docs/agents.md +12 -1
  4. package/assets/docs/cli.md +5 -5
  5. package/assets/docs/composition.md +21 -21
  6. package/assets/docs/persistence.md +7 -10
  7. package/assets/docs/projects.md +30 -18
  8. package/assets/docs/providers.md +1 -1
  9. package/assets/docs/recovery.md +13 -9
  10. package/assets/docs/resources.md +4 -19
  11. package/assets/docs/schemas.md +1 -1
  12. package/assets/docs/workflows.md +95 -7
  13. package/assets/examples/agent-then-analysis/README.md +4 -4
  14. package/assets/examples/agent-then-analysis/input.json +1 -1
  15. package/assets/examples/agent-then-analysis/norn.project.json +1 -1
  16. package/assets/examples/agent-then-analysis/plugin.ts +55 -68
  17. package/assets/examples/caller-selected-continuation/README.md +5 -5
  18. package/assets/examples/caller-selected-continuation/caller.ts +35 -38
  19. package/assets/examples/caller-selected-continuation/input.json +2 -2
  20. package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
  21. package/assets/examples/caller-selected-continuation/producer.ts +15 -23
  22. package/assets/examples/coordinating-multiple-agents/README.md +2 -2
  23. package/assets/examples/coordinating-multiple-agents/input.json +1 -1
  24. package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
  25. package/assets/examples/coordinating-multiple-agents/plugin.ts +52 -56
  26. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +4 -4
  27. package/assets/examples/getting-started/norn.project.json +1 -1
  28. package/assets/examples/getting-started/plugin.ts +16 -26
  29. package/assets/examples/minimal-workflow/README.md +8 -8
  30. package/assets/examples/minimal-workflow/norn.project.json +1 -1
  31. package/assets/examples/minimal-workflow/plugin.ts +11 -25
  32. package/assets/examples/shared-state/README.md +10 -2
  33. package/assets/examples/shared-state/input.json +1 -1
  34. package/assets/examples/shared-state/norn.project.json +1 -1
  35. package/assets/examples/shared-state/plugin.ts +36 -41
  36. package/assets/examples/shared-state/shared-state.ts +75 -0
  37. package/assets/{packages/sdk/src → examples/shared-state}/state-adapter.ts +17 -16
  38. package/assets/examples/worktree-development-loop/README.md +6 -7
  39. package/assets/examples/worktree-development-loop/norn.project.json +1 -1
  40. package/assets/examples/worktree-development-loop/plugin.ts +6 -26
  41. package/assets/examples/worktree-development-loop/scope.ts +4 -0
  42. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
  43. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
  44. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
  45. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +34 -33
  46. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
  47. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +7 -3
  48. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +23 -16
  49. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
  50. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
  51. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +40 -31
  52. package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
  53. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +5 -4
  54. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +49 -44
  55. package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
  56. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +4 -3
  57. package/assets/package.json +1 -1
  58. package/assets/packages/cli/src/cli.ts +35 -34
  59. package/assets/packages/cli/src/client.ts +9 -16
  60. package/assets/packages/cli/src/documentation-intro.ts +1 -1
  61. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  62. package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
  63. package/assets/packages/cli/src/internal/engine.ts +38 -62
  64. package/assets/packages/cli/src/internal/errors.ts +4 -4
  65. package/assets/packages/cli/src/internal/launch-request.ts +6 -6
  66. package/assets/packages/cli/src/internal/run-state.ts +31 -21
  67. package/assets/packages/cli/src/internal/run.ts +2 -5
  68. package/assets/packages/cli/src/internal/workflow-registry.ts +114 -111
  69. package/assets/packages/cli/src/workflow-loader.ts +214 -0
  70. package/assets/packages/core/src/workflow-transition.ts +2 -2
  71. package/assets/packages/sdk/src/api.ts +133 -289
  72. package/assets/packages/sdk/src/index.ts +1 -1
  73. package/assets/packages/sdk/src/schema.ts +3 -3
  74. package/assets/tests/workflow-ref.test.ts +50 -51
  75. package/dist/cli.js +33 -32
  76. package/dist/client.d.ts +3 -4
  77. package/dist/client.js +3 -8
  78. package/dist/documentation-intro.js +1 -1
  79. package/dist/generated-build-info.d.ts +2 -2
  80. package/dist/generated-build-info.js +2 -2
  81. package/dist/internal/agent-response-tool.js +3 -3
  82. package/dist/internal/engine.d.ts +4 -8
  83. package/dist/internal/engine.js +36 -49
  84. package/dist/internal/errors.d.ts +3 -3
  85. package/dist/internal/errors.js +1 -1
  86. package/dist/internal/launch-request.d.ts +4 -4
  87. package/dist/internal/launch-request.js +2 -2
  88. package/dist/internal/run-state.d.ts +9 -8
  89. package/dist/internal/run-state.js +29 -20
  90. package/dist/internal/run.d.ts +1 -3
  91. package/dist/internal/run.js +3 -5
  92. package/dist/internal/workflow-registry.d.ts +25 -17
  93. package/dist/internal/workflow-registry.js +107 -72
  94. package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
  95. package/dist/workflow-loader.js +235 -0
  96. package/package.json +2 -2
  97. package/assets/examples/worktree-development-loop/manifest.ts +0 -26
  98. package/assets/examples/worktree-development-loop/state.ts +0 -23
  99. package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
  100. package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
  101. package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
  102. package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
  103. package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
  104. package/assets/packages/cli/src/internal/run-resources.ts +0 -23
  105. package/assets/packages/cli/src/internal/state-store.ts +0 -83
  106. package/assets/packages/cli/src/plugin-loader.ts +0 -400
  107. package/dist/internal/run-resources.d.ts +0 -6
  108. package/dist/internal/run-resources.js +0 -26
  109. package/dist/internal/state-store.d.ts +0 -23
  110. package/dist/internal/state-store.js +0 -103
  111. package/dist/plugin-loader.js +0 -341
@@ -3,16 +3,15 @@ import { fileURLToPath } from "node:url";
3
3
  import { createInterface } from "node:readline";
4
4
  import type {
5
5
  DeletedNornRunInfo,
6
- NornPluginDiagnostic,
6
+ NornWorkflowDiagnostic,
7
7
  NornProjectInspection,
8
8
  NornWorkflowCatalogInfo,
9
9
  NornWorkflowInspection,
10
10
  NornRunCheckpoint,
11
11
  NornRunInfo,
12
12
  NornRunMetrics,
13
- NornWorkflowStateReader,
14
13
  } from "@vimhead.dev/norn";
15
- import { loadNornProject } from "./plugin-loader.ts";
14
+ import { loadNornProject } from "./workflow-loader.ts";
16
15
  import { NornProjectLoadError } from "./internal/errors.ts";
17
16
  import type { NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
18
17
 
@@ -32,10 +31,9 @@ export type NornClient = {
32
31
  inspect(workflowId: string): Promise<NornWorkflowInspection>;
33
32
  entries(): Promise<readonly NornRegisteredWorkflow[]>;
34
33
  };
35
- readonly state: NornWorkflowStateReader;
36
34
  readonly runs: {
37
- start(input: { readonly workflowId: string; readonly params: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
38
- resume(input: { readonly run: string; readonly params?: unknown }): Promise<NornRunInfo>;
35
+ start(input: { readonly workflowId: string; readonly args: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
36
+ resume(input: { readonly run: string; readonly args?: unknown }): Promise<NornRunInfo>;
39
37
  wait(run: string): Promise<NornRunInfo>;
40
38
  list(): Promise<NornRunInfo[]>;
41
39
  inspect(run: string): Promise<NornRunInfo>;
@@ -61,7 +59,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
61
59
  inspect: async (workflowId) => processRunner.readJson<NornWorkflowInspection>(["workflows", "inspect", workflowId]),
62
60
  entries: async () => (await catalog.load()).workflows,
63
61
  },
64
- state: catalog.state,
65
62
  runs: {
66
63
  start: async (startInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "start", startInput.workflowId], JSON.stringify(runStartInput(startInput)))).run,
67
64
  resume: async (resumeInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "resume", resumeInput.run], runResumeStdin(resumeInput))).run,
@@ -82,10 +79,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
82
79
 
83
80
  class NornWorkflowCatalog {
84
81
  private loaded: ReturnType<typeof loadNornProject> | undefined;
85
- readonly state: NornWorkflowStateReader = {
86
- get: async (state) => (await this.load()).state.get(state),
87
- getOptional: async (state) => (await this.load()).state.getOptional(state),
88
- };
89
82
 
90
83
  constructor(private readonly cwd: string) {}
91
84
 
@@ -107,7 +100,7 @@ class NornProcessRunner {
107
100
 
108
101
  async readJson<T>(args: readonly string[], stdin?: string): Promise<T> {
109
102
  const result = await this.spawn(args, stdin);
110
- const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?: NornPluginDiagnostic[] } };
103
+ const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?: NornWorkflowDiagnostic[] } };
111
104
  if (parsed && typeof parsed === "object" && "error" in parsed) {
112
105
  if (parsed.error?.code === "NORN_PROJECT_INVALID" && Array.isArray(parsed.error.diagnostics)) throw new NornProjectLoadError({ diagnostics: parsed.error.diagnostics });
113
106
  throw new Error(parsed.error?.message ?? "Norn command failed");
@@ -151,10 +144,10 @@ class NornProcessRunner {
151
144
  }
152
145
  }
153
146
 
154
- function runStartInput(input: { readonly params: unknown; readonly configOverride?: unknown }): { readonly params: unknown; readonly config?: unknown } {
155
- return input.configOverride === undefined ? { params: input.params } : { params: input.params, config: input.configOverride };
147
+ function runStartInput(input: { readonly args: unknown; readonly configOverride?: unknown }): { readonly args: unknown; readonly config?: unknown } {
148
+ return input.configOverride === undefined ? { args: input.args } : { args: input.args, config: input.configOverride };
156
149
  }
157
150
 
158
- function runResumeStdin(input: { readonly params?: unknown }): string | undefined {
159
- return input.params === undefined ? undefined : JSON.stringify({ params: input.params });
151
+ function runResumeStdin(input: { readonly args?: unknown }): string | undefined {
152
+ return input.args === undefined ? undefined : JSON.stringify({ args: input.args });
160
153
  }
@@ -24,7 +24,7 @@ export function renderNornDocumentationIntro(input: {
24
24
  "",
25
25
  "Live CLI contracts (argument arrays appended to the runtime argv above):",
26
26
  '- ["workflows", "list"] — currently registered entrypoints.',
27
- '- ["workflows", "inspect", "<workflow-id>"] — current instructions, params schema, and source.',
27
+ '- ["workflows", "inspect", "<workflow-id>"] — current instructions, args schema, and source.',
28
28
  '- ["help"] — command usage.',
29
29
  ].join("\n");
30
30
  }
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
2
2
 
3
3
  export const NORN_GENERATED_BUILD_INFO = {
4
4
  "kind": "npm-registry",
5
- "version": "0.1.0-tip.35359392805.1",
6
- "commit": "f958006ca0ae3752060727c5b76b2ac1a008b103",
5
+ "version": "0.1.0-tip.35390859630.1",
6
+ "commit": "7a7976170d62c342c29c6479444394d542c3f22b",
7
7
  "packageSpec": "@vimhead.dev/norn-cli@tip",
8
8
  "upgrade": {
9
9
  "supported": false,
@@ -71,12 +71,12 @@ export class NornAgentResponseToolFactory {
71
71
  label: Type.String({ description: "The exact workflow agent label provided in the prompt." }),
72
72
  response: Type.Unknown({ description: "The structured response object requested by the workflow." }),
73
73
  }),
74
- async execute(_toolCallId, params) {
74
+ async execute(_toolCallId, args) {
75
75
  try {
76
- const captured = responseCollector.capture(params.runId, params.label, params.response);
76
+ const captured = responseCollector.capture(args.runId, args.label, args.response);
77
77
  return {
78
78
  content: [{ type: "text", text: `Recorded workflow agent response for ${captured.label}.` }],
79
- details: { valid: true, runId: params.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
79
+ details: { valid: true, runId: args.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
80
80
  terminate: true,
81
81
  };
82
82
  } catch (error) {
@@ -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,9 +7,6 @@ import {
8
7
  type NornRunNext,
9
8
  type NornRunResult,
10
9
  type NornRunStartOptions,
11
- type NornWorkflowPlugin,
12
- type NornWorkflowPluginImplementation,
13
- type NornWorkflowPluginImplementationInput,
14
10
  } from "@vimhead.dev/norn";
15
11
  import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
16
12
  import { randomUUID } from "node:crypto";
@@ -24,12 +20,11 @@ import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRe
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 { initializeRunResources } from "./run-resources.ts";
28
- import { getRunInfo, mergeInterruptedWorkflowParams, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
23
+ import { NornRunResources } from "../resources.ts";
24
+ import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
29
25
  import { NornRunStore, runCurrentRoot } from "./run-store.ts";
30
26
  import { NornRunContext } from "./run.ts";
31
- import { NornMemoryWorkflowState } from "./state-store.ts";
32
- import { decodePluginConfiguration, NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
27
+ import { NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
33
28
 
34
29
  const RUNS_DIR_NAME = ".norn";
35
30
  const MANIFEST_FILE_NAME = "manifest.json";
@@ -62,14 +57,12 @@ type ActiveRun = {
62
57
 
63
58
  type WorkflowStep = {
64
59
  readonly workflow: NornAnyWorkflowDeclaration;
65
- readonly params: unknown;
60
+ readonly args: unknown;
66
61
  readonly interruption?: NonNullable<NornRunState["current"]>["interruption"];
67
62
  };
68
63
 
69
64
  export class NornEngine {
70
65
  private readonly registry = new NornWorkflowRegistry();
71
- private readonly registrarState = new NornMemoryWorkflowState();
72
- private readonly disposersByPlugin = new Map<string, NornDispose[]>();
73
66
  private readonly responseCollector: NornAgentResponseCollector;
74
67
  private readonly activeRuns = new Map<string, ActiveRun>();
75
68
 
@@ -77,19 +70,15 @@ export class NornEngine {
77
70
  this.responseCollector = input.responseCollector ?? new NornAgentResponseCollector();
78
71
  }
79
72
 
80
- registerPlugin<TManifest extends NornAnyWorkflowPluginManifest>(plugin: NornWorkflowPlugin<TManifest>): NornDispose {
81
- this.disposePlugin(plugin.manifest.id);
82
- const implementation = this.resolvePluginImplementation(plugin.implementation);
83
- const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: this.input.config?.[plugin.manifest.id] });
84
- const disposers = Object.entries(plugin.manifest.workflows).map(([key, workflow]) => {
85
- const workflowImplementation = implementation.workflows[key];
86
- if (!workflowImplementation) throw new Error(`Missing implementation for workflow ${plugin.manifest.id}.${key}`);
87
- return this.registry.register(workflow, workflowImplementation, { plugin: { id: plugin.manifest.id }, configuration });
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
- };
73
+ registerWorkflows(workflows: readonly NornAnyWorkflowDeclaration[]): NornDispose {
74
+ const disposers: NornDispose[] = [];
75
+ try {
76
+ for (const workflow of workflows) disposers.push(this.registry.register({ workflow, config: this.input.config ?? {}, source: undefined }));
77
+ } catch (error) {
78
+ for (const dispose of disposers.reverse()) dispose();
79
+ throw error;
80
+ }
81
+ return () => { for (const dispose of disposers) dispose(); };
93
82
  }
94
83
 
95
84
  listWorkflows() {
@@ -100,20 +89,12 @@ export class NornEngine {
100
89
  return this.registry.launchableEntries();
101
90
  }
102
91
 
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
92
  async runWorkflow<TWorkflow extends NornAnyWorkflowDeclaration>(
112
93
  workflow: TWorkflow,
113
- params: unknown,
94
+ args: unknown,
114
95
  options: NornRunStartOptions | undefined,
115
96
  ): Promise<NornRunResult> {
116
- const session = await this.createRunSession(workflow, params, options);
97
+ const session = await this.createRunSession(workflow, args, options);
117
98
  const current = session.state.currentState().current;
118
99
  if (!current) throw new Error("New run is missing its initial workflow step");
119
100
  return this.runScheduler(session, toWorkflowStep(workflow, current));
@@ -126,6 +107,7 @@ export class NornEngine {
126
107
 
127
108
  async rollbackRun(path: string, checkpointId: string): Promise<NornRunInfo> {
128
109
  const runRoot = await resolveRunRoot(this.input.cwd, path);
110
+ assertRunVersion((await getRunInfo(runRoot)).version);
129
111
  const lease = await NornRunLease.acquire(runRoot);
130
112
  try {
131
113
  const pendingRequest = await readOptionalRunResumeRequest(runRoot);
@@ -142,21 +124,21 @@ export class NornEngine {
142
124
  }
143
125
  }
144
126
 
145
- async resumeWorkflow(path: string, params?: unknown): Promise<NornRunResult> {
146
- return this.resumeWorkflowExecution({ path, params, expectedRequest: undefined });
127
+ async resumeWorkflow(path: string, args?: unknown): Promise<NornRunResult> {
128
+ return this.resumeWorkflowExecution({ path, args, expectedRequest: undefined });
147
129
  }
148
130
 
149
131
  async resumeRequestedWorkflow(input: { readonly runRoot: string; readonly request: NornRunResumeRequest }): Promise<NornRunResult> {
150
- return this.resumeWorkflowExecution({ path: input.runRoot, params: input.request.params, expectedRequest: input.request });
132
+ return this.resumeWorkflowExecution({ path: input.runRoot, args: input.request.args, expectedRequest: input.request });
151
133
  }
152
134
 
153
- private async resumeWorkflowExecution(input: { readonly path: string; readonly params: unknown; readonly expectedRequest: NornRunResumeRequest | undefined }): Promise<NornRunResult> {
154
- const { params, expectedRequest } = input;
135
+ private async resumeWorkflowExecution(input: { readonly path: string; readonly args: unknown; readonly expectedRequest: NornRunResumeRequest | undefined }): Promise<NornRunResult> {
136
+ const { args, expectedRequest } = input;
155
137
  const runRoot = await resolveRunRoot(this.input.cwd, input.path);
156
138
  const initialStateStore = await NornRunStateStore.load(runRoot);
157
139
  const initialState = initialStateStore.currentState();
158
- if (initialState.status === "interrupted" && params === undefined) throw new Error(`Interrupted workflow resume requires params: ${runRoot}`);
159
- if (initialState.status === "pendingResume" && params !== undefined) throw new Error(`Pending-resume workflows do not accept params: ${runRoot}`);
140
+ if (initialState.status === "interrupted" && args === undefined) throw new Error(`Interrupted workflow resume requires args: ${runRoot}`);
141
+ if (initialState.status === "pendingResume" && args !== undefined) throw new Error(`Pending-resume workflows do not accept args: ${runRoot}`);
160
142
  if (initialState.status !== "interrupted" && initialState.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
161
143
 
162
144
  const lease = await NornRunLease.acquire(runRoot);
@@ -172,9 +154,9 @@ export class NornEngine {
172
154
  const workflow = this.registry.workflowById(current.workflowId);
173
155
  if (!workflow) throw new Error(`Unknown workflow for resumed run: ${current.workflowId}`);
174
156
  if (state.status === "interrupted") {
175
- const rawParams = mergeInterruptedWorkflowParams(current.params, params, current.interruption?.fields);
176
- Value.Decode(workflow.params, rawParams);
177
- await session.state.replaceCurrentParams(rawParams);
157
+ const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
158
+ Value.Decode(workflow.args, rawArgs);
159
+ await session.state.replaceCurrentArgs(rawArgs);
178
160
  await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
179
161
  const resumedCurrent = session.state.currentState().current;
180
162
  if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
@@ -203,12 +185,12 @@ export class NornEngine {
203
185
  throwIfRunAborted(session.activeRun);
204
186
  const stepRuntime = session.run.forWorkflow(currentStep.workflow);
205
187
  if (shouldPauseForGate(currentStep)) {
206
- const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.params, session.state.currentState().configOverride);
188
+ const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.args, session.state.currentState().configOverride);
207
189
  const interruption = { description, fields: currentStep.workflow.gate?.fields };
208
- await session.state.interruptCurrent(currentStep.params, interruption);
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: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.params, interruption);
193
+ return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.args, interruption);
212
194
  }
213
195
  await session.state.startStep(toRunStateStep(currentStep));
214
196
  const stepResult = await this.executeWorkflowStep(session, currentStep, stepRuntime);
@@ -251,7 +233,7 @@ export class NornEngine {
251
233
  try {
252
234
  await assertWorkspaceBoundary(session, run.workspace);
253
235
  await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
254
- const result = await this.registry.execute(step.workflow, run, step.params, session.state.currentState().configOverride);
236
+ const result = await this.registry.execute(step.workflow, run, step.args, session.state.currentState().configOverride);
255
237
  await assertWorkspaceBoundary(session, run.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 });
@@ -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
- params: next.params,
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
- params: unknown,
266
+ args: unknown,
285
267
  options: NornRunStartOptions | undefined,
286
268
  ): Promise<RunSession> {
287
269
  const id = options?.id ?? randomUUID();
@@ -314,7 +296,7 @@ export class NornEngine {
314
296
  workspace,
315
297
  configOverride: options?.configOverride,
316
298
  current: {
317
- workflowId: workflow.id, params, cwd, env: {},
299
+ workflowId: workflow.id, args, cwd, env: {},
318
300
  interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : undefined,
319
301
  },
320
302
  startedAt,
@@ -364,7 +346,7 @@ export class NornEngine {
364
346
  await mkdir(input.workspace, { recursive: true });
365
347
  await mkdir(artifactsRoot, { recursive: true });
366
348
  await mkdir(logsRoot, { recursive: true });
367
- const { resources, state } = await initializeRunResources(dirname(input.currentRoot));
349
+ const resources = await NornRunResources.initialize(dirname(input.currentRoot));
368
350
  return new NornRunContext({
369
351
  id: input.id,
370
352
  runRoot: input.currentRoot,
@@ -376,7 +358,6 @@ export class NornEngine {
376
358
  agentDir: this.input.agentDir,
377
359
  responseCollector: this.responseCollector,
378
360
  resources,
379
- state,
380
361
  logger: input.logger,
381
362
  artifacts: new NornArtifacts(artifactsRoot, resources.files),
382
363
  logs: new NornRunLogs(logsRoot, resources.files),
@@ -416,11 +397,6 @@ export class NornEngine {
416
397
  activeRun.finish();
417
398
  }
418
399
 
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
400
  }
425
401
 
426
402
  function defaultRunRoot(cwd: string, id: string): string {
@@ -434,7 +410,7 @@ function workflowDefaultCwd(projectRoot: string, workspace: string, workflow: No
434
410
  function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<NornRunState["current"]>): WorkflowStep {
435
411
  return {
436
412
  workflow,
437
- params: step.params,
413
+ args: step.args,
438
414
  interruption: step.interruption,
439
415
  };
440
416
  }
@@ -442,7 +418,7 @@ function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<
442
418
  function toRunStateStep(step: WorkflowStep): NonNullable<NornRunState["current"]> {
443
419
  return {
444
420
  workflowId: step.workflow.id,
445
- params: step.params,
421
+ args: step.args,
446
422
  cwd: "",
447
423
  env: {},
448
424
  interruption: step.interruption,
@@ -464,7 +440,7 @@ function isRunStopped(session: RunSession, error: unknown): boolean {
464
440
  return error instanceof NornRunStoppedError || session.activeRun.controller.signal.reason instanceof NornRunStoppedError;
465
441
  }
466
442
 
467
- function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDeclaration, params: unknown, interruption: { readonly description?: string; readonly fields?: readonly string[] }): NornInterruptedRunResult {
443
+ function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDeclaration, args: unknown, interruption: { readonly description?: string; readonly fields?: readonly string[] }): NornInterruptedRunResult {
468
444
  if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
469
445
  if (!interruption.description) throw new Error(`Interrupted workflow is missing description: ${workflow.id}`);
470
446
  return {
@@ -474,7 +450,7 @@ function interruptedLaunchResult(run: RunIdentity, workflow: NornAnyWorkflowDecl
474
450
  workspace: run.workspace,
475
451
  cwd: run.cwd,
476
452
  workflowId: workflow.id,
477
- interruption: { workflowId: workflow.id, params, description: interruption.description, fields: interruption.fields },
453
+ interruption: { workflowId: workflow.id, args, description: interruption.description, fields: interruption.fields },
478
454
  };
479
455
  }
480
456
 
@@ -1,14 +1,14 @@
1
- import type { NornPluginDiagnostic } from "@vimhead.dev/norn";
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 NornPluginDiagnostic[];
7
+ readonly diagnostics: readonly NornWorkflowDiagnostic[];
8
8
 
9
- constructor(input: { readonly diagnostics: readonly NornPluginDiagnostic[] }) {
9
+ constructor(input: { readonly diagnostics: readonly NornWorkflowDiagnostic[] }) {
10
10
  const details = input.diagnostics.map(diagnostic =>
11
- `${diagnostic.stage}: ${diagnostic.pluginPath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`,
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: 1;
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 params: unknown;
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: 1;
24
+ readonly version: 2;
25
25
  readonly type: "resume";
26
26
  readonly id: string;
27
27
  readonly requestId?: string;
28
- readonly params?: unknown;
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 !== 1 || request.type !== "run") throw new Error("Unsupported workflow launch request");
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 !== 1 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
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");
@@ -13,19 +13,19 @@ import { Value } from "typebox/value";
13
13
  export const RUN_STATE_FILE_NAME = "run-state.json";
14
14
  const LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
15
15
 
16
- export function mergeInterruptedWorkflowParams(currentParams: unknown, params: unknown, fields: readonly string[] | undefined): unknown {
17
- if (!isRecord(currentParams) || !isRecord(params)) return params;
16
+ export function mergeInterruptedWorkflowArgs(currentArgs: unknown, args: unknown, fields: readonly string[] | undefined): unknown {
17
+ if (!isRecord(currentArgs) || !isRecord(args)) return args;
18
18
  if (fields) {
19
- const unsupportedFields = Object.keys(params).filter((field) => !fields.includes(field));
20
- if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow params cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
19
+ const unsupportedFields = Object.keys(args).filter((field) => !fields.includes(field));
20
+ if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow args cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
21
21
  }
22
- return { ...currentParams, ...params };
22
+ return { ...currentArgs, ...args };
23
23
  }
24
- const RUN_STATE_VERSION = 1;
24
+ const RUN_STATE_VERSION = 2;
25
25
 
26
26
  type NornRunWorkflowStep = {
27
27
  readonly workflowId: string;
28
- readonly params: unknown;
28
+ readonly args: unknown;
29
29
  readonly cwd: string;
30
30
  readonly env: Record<string, string>;
31
31
  readonly interruption?: RuntimeInterruption;
@@ -58,7 +58,7 @@ type NornRunWorkflowOutcome = {
58
58
  };
59
59
 
60
60
  export type NornRunState = {
61
- readonly version: typeof RUN_STATE_VERSION;
61
+ readonly version: 1 | typeof RUN_STATE_VERSION;
62
62
  readonly id: string;
63
63
  readonly name: string;
64
64
  readonly entrypointWorkflowId: string;
@@ -119,7 +119,7 @@ export class NornRunStateStore {
119
119
  static async load(runRoot: string): Promise<NornRunStateStore> {
120
120
  const currentRoot = runCurrentRoot(runRoot);
121
121
  const files = createRunFileCoordinator(runRoot);
122
- 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) })));
122
+ 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"));
123
123
  return new NornRunStateStore({ path: join(currentRoot, RUN_STATE_FILE_NAME), state, files });
124
124
  }
125
125
 
@@ -163,11 +163,11 @@ export class NornRunStateStore {
163
163
  });
164
164
  }
165
165
 
166
- async interruptCurrent(params: unknown, interruption: { readonly description: string; readonly fields?: readonly string[] }): Promise<void> {
166
+ async interruptCurrent(args: unknown, interruption: { readonly description: string; readonly fields?: readonly string[] }): Promise<void> {
167
167
  if (!this.state.current) throw new Error("Cannot interrupt run without current step");
168
168
  await this.update({
169
169
  status: "interrupted",
170
- current: { ...this.state.current, params, interruption: { status: "pending", ...interruption } },
170
+ current: { ...this.state.current, args, interruption: { status: "pending", ...interruption } },
171
171
  outcome: null,
172
172
  failed: null,
173
173
  });
@@ -185,11 +185,11 @@ export class NornRunStateStore {
185
185
  await this.update({ status: "stopped", outcome: null, failed: null });
186
186
  }
187
187
 
188
- async replaceCurrentParams(params: unknown): Promise<void> {
188
+ async replaceCurrentArgs(args: unknown): Promise<void> {
189
189
  if (!this.state.current) throw new Error("Cannot resume run without current step");
190
190
  await this.update({
191
191
  status: "running",
192
- current: { ...this.state.current, params, interruption: { ...this.state.current.interruption, status: "satisfied" } },
192
+ current: { ...this.state.current, args, interruption: { ...this.state.current.interruption, status: "satisfied" } },
193
193
  outcome: null,
194
194
  failed: null,
195
195
  });
@@ -208,16 +208,16 @@ export class NornRunStateStore {
208
208
 
209
209
  private async update(patch: Partial<NornRunState>): Promise<void> {
210
210
  await this.files.withExclusiveLock(this.path, async (path) => {
211
- const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
211
+ const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname(this.path), LEGACY_RUN_STATE_FILE_NAME) }), "execute");
212
212
  const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
213
- if (next.current?.params !== undefined) Value.Assert(jsonValueSchema, next.current.params);
213
+ if (next.current?.args !== undefined) Value.Assert(jsonValueSchema, next.current.args);
214
214
  await writeJsonAtomically(path, next);
215
215
  this.state = next;
216
216
  });
217
217
  }
218
218
 
219
219
  private async write(): Promise<void> {
220
- if (this.state.current?.params !== undefined) Value.Assert(jsonValueSchema, this.state.current.params);
220
+ if (this.state.current?.args !== undefined) Value.Assert(jsonValueSchema, this.state.current.args);
221
221
  await mkdir(dirname(this.path), { recursive: true });
222
222
  await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
223
223
  }
@@ -239,8 +239,14 @@ export async function listRuns(sessionCwd: string): Promise<NornRunInfo[]> {
239
239
 
240
240
  export async function getRunInfo(runRoot: string): Promise<NornRunInfo> {
241
241
  try {
242
- const resumeRequest = await readOptionalRunResumeRequest(runRoot);
243
- const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)));
242
+ // Read the request first so a completed handoff cannot revive an old interruption.
243
+ const pendingResume = await readOptionalRunResumeRequest(runRoot).then(
244
+ request => ({ status: "fulfilled" as const, request }),
245
+ (error: unknown) => ({ status: "rejected" as const, error }),
246
+ );
247
+ const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)), "inspect");
248
+ if (state.version === RUN_STATE_VERSION && pendingResume.status === "rejected") throw pendingResume.error;
249
+ const resumeRequest = state.version === RUN_STATE_VERSION && pendingResume.status === "fulfilled" ? pendingResume.request : undefined;
244
250
  return {
245
251
  version: state.version,
246
252
  id: state.id,
@@ -337,10 +343,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
337
343
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
338
344
  }
339
345
 
340
- function parseNornRunState(value: unknown): NornRunState {
346
+ export function assertRunVersion(version: unknown): void {
347
+ 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.`);
348
+ }
349
+
350
+ function parseNornRunState(value: unknown, mode: "execute" | "inspect"): NornRunState {
341
351
  if (!value || typeof value !== "object") throw new Error("Invalid run state");
342
352
  const state = value as Partial<NornRunState> & { rootWorkflowId?: unknown };
343
- if (state.version !== RUN_STATE_VERSION) throw new Error(`Unsupported run state version: ${String(state.version)}`);
353
+ if (mode === "execute" || state.version !== 1) assertRunVersion(state.version);
344
354
  if (typeof state.id !== "string" || state.id.length === 0) throw new Error("Invalid run state id");
345
355
  const normalizedState = state as { name?: string; entrypointWorkflowId?: unknown; rootWorkflowId?: unknown };
346
356
  if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0) normalizedState.name = state.id;
@@ -369,7 +379,7 @@ function runInterruption(state: NornRunState): NornRunInterruption | undefined {
369
379
  if (state.status !== "interrupted" || !state.current?.interruption) return undefined;
370
380
  return {
371
381
  workflowId: state.current.workflowId,
372
- params: state.current.params,
382
+ args: state.version === 1 ? (state.current as { readonly params?: unknown }).params : state.current.args,
373
383
  description: state.current.interruption.description ?? "",
374
384
  fields: state.current.interruption.fields,
375
385
  };
@@ -23,7 +23,6 @@ type DefaultNornRunInput = {
23
23
  readonly agentDir?: string;
24
24
  readonly responseCollector: NornAgentResponseCollector;
25
25
  readonly resources: NornRunResources;
26
- readonly state: NornRun["state"];
27
26
  readonly logger: NornRunLogger;
28
27
  readonly artifacts: NornArtifacts;
29
28
  readonly logs: NornRunLogs;
@@ -31,7 +30,6 @@ type DefaultNornRunInput = {
31
30
 
32
31
  export class NornRunContext implements NornProjectRun {
33
32
  readonly resources: NornRunResources;
34
- readonly state: NornRun["state"];
35
33
  readonly artifacts: NornRun["artifacts"];
36
34
  readonly logs: NornRun["logs"];
37
35
  readonly commands: NornRun["commands"];
@@ -47,7 +45,6 @@ export class NornRunContext implements NornProjectRun {
47
45
  this.projectRoot = input.projectRoot;
48
46
  this.cwd = input.cwd;
49
47
  this.resources = input.resources;
50
- this.state = input.state;
51
48
  this.artifacts = input.artifacts;
52
49
  this.logs = {
53
50
  read: (log) => this.input.logs.read(log),
@@ -107,8 +104,8 @@ export class NornRunContext implements NornProjectRun {
107
104
  return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
108
105
  }
109
106
 
110
- next(workflowId: string, params: unknown): ReturnType<NornRun["next"]> {
111
- return createWorkflowTransition({ workflowId, params });
107
+ next(workflowId: string, args: unknown): ReturnType<NornRun["next"]> {
108
+ return createWorkflowTransition({ workflowId, args });
112
109
  }
113
110
 
114
111
  complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]> {