@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.1

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