@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.1

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