@vimhead.dev/norn-cli 0.1.0-tip.35390859630.1 → 0.1.0-tip.35568631545.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 (97) hide show
  1. package/assets/README.md +36 -15
  2. package/assets/docs/README.md +4 -4
  3. package/assets/docs/agents.md +39 -11
  4. package/assets/docs/cli.md +6 -6
  5. package/assets/docs/composition.md +12 -12
  6. package/assets/docs/persistence.md +31 -17
  7. package/assets/docs/providers.md +2 -2
  8. package/assets/docs/recovery.md +1 -1
  9. package/assets/docs/workflows.md +29 -17
  10. package/assets/examples/agent-then-analysis/README.md +8 -8
  11. package/assets/examples/agent-then-analysis/plugin.ts +23 -21
  12. package/assets/examples/caller-selected-continuation/README.md +11 -10
  13. package/assets/examples/caller-selected-continuation/caller.ts +15 -13
  14. package/assets/examples/caller-selected-continuation/producer.ts +11 -8
  15. package/assets/examples/coordinating-multiple-agents/README.md +27 -17
  16. package/assets/examples/coordinating-multiple-agents/plugin.ts +64 -41
  17. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
  18. package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
  19. package/assets/examples/getting-started/README.md +25 -10
  20. package/assets/examples/getting-started/plugin.ts +30 -11
  21. package/assets/examples/minimal-workflow/README.md +7 -6
  22. package/assets/examples/minimal-workflow/plugin.ts +8 -5
  23. package/assets/examples/shared-state/README.md +5 -5
  24. package/assets/examples/shared-state/plugin.ts +38 -25
  25. package/assets/examples/shared-state/shared-state.ts +30 -49
  26. package/assets/examples/shared-state/state-tools.ts +68 -0
  27. package/assets/examples/worktree-development-loop/README.md +14 -12
  28. package/assets/examples/worktree-development-loop/scope.ts +1 -1
  29. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +3 -3
  30. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +6 -4
  31. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +12 -11
  32. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +2 -3
  33. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +10 -6
  34. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +14 -11
  35. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +1 -2
  36. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +16 -15
  37. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +1 -2
  38. package/assets/package.json +1 -1
  39. package/assets/packages/cli/src/cli.ts +9 -3
  40. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  41. package/assets/packages/cli/src/internal/agents.ts +24 -32
  42. package/assets/packages/cli/src/internal/commands.ts +2 -14
  43. package/assets/packages/cli/src/internal/engine.ts +35 -49
  44. package/assets/packages/cli/src/internal/execution-context.ts +64 -0
  45. package/assets/packages/cli/src/internal/logs.ts +1 -1
  46. package/assets/packages/cli/src/internal/run-log.ts +1 -1
  47. package/assets/packages/cli/src/internal/run-state.ts +7 -2
  48. package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
  49. package/assets/packages/cli/src/internal/workflow-registry.ts +24 -9
  50. package/assets/packages/cli/src/internal/working-directory.ts +6 -0
  51. package/assets/packages/cli/src/workflow-loader.ts +1 -2
  52. package/assets/packages/sdk/src/api.ts +65 -91
  53. package/assets/packages/sdk/src/index.ts +0 -3
  54. package/assets/tests/workflow-ref.test.ts +9 -9
  55. package/dist/cli.js +8 -3
  56. package/dist/generated-build-info.d.ts +2 -2
  57. package/dist/generated-build-info.js +2 -2
  58. package/dist/internal/agents.d.ts +0 -4
  59. package/dist/internal/agents.js +30 -46
  60. package/dist/internal/commands.d.ts +0 -4
  61. package/dist/internal/commands.js +2 -10
  62. package/dist/internal/engine.d.ts +1 -1
  63. package/dist/internal/engine.js +29 -43
  64. package/dist/internal/execution-context.d.ts +26 -0
  65. package/dist/internal/execution-context.js +55 -0
  66. package/dist/internal/file-coordinator.d.ts +17 -0
  67. package/dist/internal/file-coordinator.js +162 -0
  68. package/dist/internal/logs.d.ts +1 -1
  69. package/dist/internal/run-log.d.ts +1 -1
  70. package/dist/internal/run-state.d.ts +2 -0
  71. package/dist/internal/run-state.js +5 -2
  72. package/dist/internal/worker-directory.d.ts +1 -0
  73. package/dist/internal/worker-directory.js +26 -0
  74. package/dist/internal/workflow-registry.d.ts +13 -3
  75. package/dist/internal/workflow-registry.js +12 -7
  76. package/dist/internal/working-directory.d.ts +1 -0
  77. package/dist/internal/working-directory.js +9 -0
  78. package/dist/workflow-loader.js +1 -2
  79. package/package.json +2 -2
  80. package/assets/docs/resources.md +0 -46
  81. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
  82. package/assets/examples/shared-state/state-adapter.ts +0 -76
  83. package/assets/packages/cli/src/internal/artifacts.ts +0 -26
  84. package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
  85. package/assets/packages/cli/src/internal/run.ts +0 -127
  86. package/assets/packages/cli/src/resources.ts +0 -69
  87. package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
  88. package/assets/packages/sdk/src/resources.ts +0 -20
  89. package/dist/internal/artifacts.d.ts +0 -10
  90. package/dist/internal/artifacts.js +0 -29
  91. package/dist/internal/resource-bindings.d.ts +0 -13
  92. package/dist/internal/resource-bindings.js +0 -34
  93. package/dist/internal/run.d.ts +0 -49
  94. package/dist/internal/run.js +0 -103
  95. package/dist/resources.d.ts +0 -11
  96. package/dist/resources.js +0 -100
  97. /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
@@ -7,23 +7,22 @@ import {
7
7
  type NornRunNext,
8
8
  type NornRunResult,
9
9
  type NornRunStartOptions,
10
+ type NornWorkflowPaths,
10
11
  } from "@vimhead.dev/norn";
11
- import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
12
+ import { createRunFileCoordinator } from "./file-coordinator.ts";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import { mkdir } from "node:fs/promises";
14
- import { dirname, join } from "node:path";
15
+ import { dirname, join, resolve } from "node:path";
15
16
  import { Value } from "typebox/value";
16
17
  import { NornAgentResponseCollector } from "./agent-response-tool.ts";
17
- import { NornArtifacts } from "./artifacts.ts";
18
18
  import { errorMessage, NornRunStoppedError } from "./errors.ts";
19
19
  import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS, type NornRunResumeRequest } from "./launch-request.ts";
20
20
  import { NornRunLogs } from "./logs.ts";
21
21
  import { NornRunLease } from "./run-lease.ts";
22
22
  import { NornRunLogger } from "./run-log.ts";
23
- import { NornRunResources } from "../resources.ts";
24
23
  import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
25
24
  import { NornRunStore, runCurrentRoot } from "./run-store.ts";
26
- import { NornRunContext } from "./run.ts";
25
+ import { NornExecutionContext } from "./execution-context.ts";
27
26
  import { NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
28
27
 
29
28
  const RUNS_DIR_NAME = ".norn";
@@ -40,7 +39,9 @@ export type NornEngineInput = {
40
39
 
41
40
  type RunSession = {
42
41
  readonly runRoot: string;
43
- readonly run: NornRunContext;
42
+ readonly cwd: string;
43
+ readonly execution: NornExecutionContext;
44
+ readonly paths: NornWorkflowPaths;
44
45
  readonly state: NornRunStateStore;
45
46
  readonly lease: NornRunLease;
46
47
  readonly runStore: NornRunStore;
@@ -157,14 +158,14 @@ export class NornEngine {
157
158
  const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
158
159
  Value.Decode(workflow.args, rawArgs);
159
160
  await session.state.replaceCurrentArgs(rawArgs);
160
- await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
161
+ await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
161
162
  const resumedCurrent = session.state.currentState().current;
162
163
  if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
163
164
  isLeaseOwnedByScheduler = true;
164
165
  return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
165
166
  }
166
167
  if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
167
- 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 });
168
169
  isLeaseOwnedByScheduler = true;
169
170
  return this.runScheduler(session, toWorkflowStep(workflow, current));
170
171
  } finally {
@@ -183,28 +184,27 @@ export class NornEngine {
183
184
  try {
184
185
  for (let step = 1; step <= 1_000; step++) {
185
186
  throwIfRunAborted(session.activeRun);
186
- const stepRuntime = session.run.forWorkflow(currentStep.workflow);
187
187
  if (shouldPauseForGate(currentStep)) {
188
- const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.args, session.state.currentState().configOverride);
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
189
  const interruption = { description, fields: currentStep.workflow.gate?.fields };
190
190
  await session.state.interruptCurrent(currentStep.args, interruption);
191
191
  await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
192
192
  await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
193
- return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.args, interruption);
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);
194
194
  }
195
195
  await session.state.startStep(toRunStateStep(currentStep));
196
- const stepResult = await this.executeWorkflowStep(session, currentStep, stepRuntime);
196
+ const stepResult = await this.executeWorkflowStep(session, currentStep);
197
197
  throwIfRunAborted(session.activeRun);
198
198
  if (stepResult.type === "complete") {
199
199
  await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
200
200
  await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
201
201
  await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
202
- 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.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
203
203
  }
204
204
  if (stepResult.type === "fail") {
205
205
  await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
206
206
  await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
207
- 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.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
208
208
  }
209
209
  const nextStep = this.nextWorkflowStep(stepResult);
210
210
  await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
@@ -217,7 +217,7 @@ export class NornEngine {
217
217
  if (isRunStopped(session, error)) {
218
218
  await session.state.stopCurrent();
219
219
  await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
220
- 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.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
221
221
  }
222
222
  await session.state.failCurrent(errorMessage(error));
223
223
  await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
@@ -228,13 +228,13 @@ export class NornEngine {
228
228
  }
229
229
  }
230
230
 
231
- private async executeWorkflowStep(session: RunSession, step: WorkflowStep, run: NornRunContext): Promise<NornWorkflowStepResult> {
231
+ private async executeWorkflowStep(session: RunSession, step: WorkflowStep): Promise<NornWorkflowStepResult> {
232
232
  const startedAtMs = Date.now();
233
233
  try {
234
- await assertWorkspaceBoundary(session, run.workspace);
234
+ await assertWorkspaceBoundary(session, session.paths.workspace);
235
235
  await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
236
- const result = await this.registry.execute(step.workflow, run, step.args, session.state.currentState().configOverride);
237
- await assertWorkspaceBoundary(session, run.workspace);
236
+ const result = await this.registry.execute({ workflow: step.workflow, execution: session.execution, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
237
+ await assertWorkspaceBoundary(session, session.paths.workspace);
238
238
  const durationMs = Date.now() - startedAtMs;
239
239
  if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
240
240
  else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
@@ -271,7 +271,8 @@ export class NornEngine {
271
271
  const runRoot = defaultRunRoot(this.input.cwd, id);
272
272
  const currentRoot = runCurrentRoot(runRoot);
273
273
  const workspace = join(currentRoot, "workspace");
274
- const cwd = workflowDefaultCwd(this.input.cwd, workspace, workflow);
274
+ const paths: NornWorkflowPaths = { project: resolve(this.input.cwd), workspace: workspace };
275
+ const cwd = process.cwd();
275
276
  const startedAt = new Date().toISOString();
276
277
  await mkdir(runRoot, { recursive: true });
277
278
  const lease = await NornRunLease.acquire(runRoot);
@@ -288,8 +289,9 @@ export class NornEngine {
288
289
  initialCwd: cwd,
289
290
  startedAt,
290
291
  } });
291
- const run = await this.buildRun({ id, currentRoot, workspace, cwd, isolationMode: workflow.isolation.mode, signal: activeRun.controller.signal, logger });
292
+ const execution = await this.buildExecutionContext({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
292
293
  const state = await NornRunStateStore.create(runRoot, {
294
+ projectRoot: paths.project,
293
295
  id,
294
296
  name,
295
297
  entrypointWorkflowId: workflow.id,
@@ -301,7 +303,7 @@ export class NornEngine {
301
303
  },
302
304
  startedAt,
303
305
  });
304
- const session = { runRoot, run, state, lease, runStore, logger, activeRun };
306
+ const session = { runRoot, cwd, execution, paths, state, lease, runStore, logger, activeRun };
305
307
  await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
306
308
  await commitRunBoundary(session, `run started: ${workflow.id}`);
307
309
  return session;
@@ -321,46 +323,34 @@ export class NornEngine {
321
323
  const runStore = await NornRunStore.open(runRoot);
322
324
  const currentRoot = runCurrentRoot(runRoot);
323
325
  const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
324
- const workflow = currentState.current ? this.registry.workflowById(currentState.current.workflowId) : undefined;
325
- const isolationMode = workflow?.isolation.mode ?? "runWorkspace";
326
- const cwd = workflowDefaultCwd(this.input.cwd, currentState.workspace, workflow);
327
- const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
328
- return { runRoot, run, state, lease, runStore, logger, activeRun };
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 };
329
329
  } catch (error) {
330
330
  this.failActiveRun(runRoot, activeRun);
331
331
  throw error;
332
332
  }
333
333
  }
334
334
 
335
- private async buildRun(input: {
335
+ private async buildExecutionContext(input: {
336
336
  readonly id: string;
337
337
  readonly currentRoot: string;
338
- readonly workspace: string;
339
- readonly cwd: string;
340
- readonly isolationMode: "runWorkspace" | "project";
338
+ readonly paths: NornWorkflowPaths;
341
339
  readonly signal: AbortSignal;
342
340
  readonly logger: NornRunLogger;
343
- }): Promise<NornRunContext> {
344
- const artifactsRoot = join(input.currentRoot, "artifacts");
341
+ }): Promise<NornExecutionContext> {
345
342
  const logsRoot = join(input.currentRoot, "logs");
346
- await mkdir(input.workspace, { recursive: true });
347
- await mkdir(artifactsRoot, { recursive: true });
343
+ await mkdir(input.paths.workspace, { recursive: true });
348
344
  await mkdir(logsRoot, { recursive: true });
349
- const resources = await NornRunResources.initialize(dirname(input.currentRoot));
350
- return new NornRunContext({
345
+ const files = createRunFileCoordinator(dirname(input.currentRoot));
346
+ return new NornExecutionContext({
351
347
  id: input.id,
352
348
  runRoot: input.currentRoot,
353
- workspace: input.workspace,
354
- projectRoot: this.input.cwd,
355
- cwd: input.cwd,
356
- isolationMode: input.isolationMode,
357
349
  signal: input.signal,
358
350
  agentDir: this.input.agentDir,
359
351
  responseCollector: this.responseCollector,
360
- resources,
361
352
  logger: input.logger,
362
- artifacts: new NornArtifacts(artifactsRoot, resources.files),
363
- logs: new NornRunLogs(logsRoot, resources.files),
353
+ logs: new NornRunLogs(logsRoot, files),
364
354
  });
365
355
  }
366
356
 
@@ -400,11 +390,7 @@ export class NornEngine {
400
390
  }
401
391
 
402
392
  function defaultRunRoot(cwd: string, id: string): string {
403
- return join(cwd, RUNS_DIR_NAME, "runs", id);
404
- }
405
-
406
- function workflowDefaultCwd(projectRoot: string, workspace: string, workflow: NornAnyWorkflowDeclaration | undefined): string {
407
- return workflow?.isolation.mode === "project" ? projectRoot : workspace;
393
+ return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
408
394
  }
409
395
 
410
396
  function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<NornRunState["current"]>): WorkflowStep {
@@ -0,0 +1,64 @@
1
+ import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
2
+ import type { NornAgents, NornCommands, NornLogs, NornRun } from "@vimhead.dev/norn";
3
+ import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
4
+ import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
5
+ import { NornAgentRunner } from "./agents.ts";
6
+ import { NornCommandRunner } from "./commands.ts";
7
+ import type { NornRunLogs } from "./logs.ts";
8
+ import type { NornRunLogger } from "./run-log.ts";
9
+
10
+ type NornExecutionContextInput = {
11
+ readonly id: string;
12
+ readonly runRoot: string;
13
+ readonly signal?: AbortSignal;
14
+ readonly model?: CreateAgentSessionOptions["model"];
15
+ readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
16
+ readonly agentDir?: string;
17
+ readonly responseCollector: NornAgentResponseCollector;
18
+ readonly logger: NornRunLogger;
19
+ readonly logs: NornRunLogs;
20
+ };
21
+
22
+ export class NornExecutionContext {
23
+ readonly run: NornRun;
24
+ readonly logs: NornLogs;
25
+ readonly commands: NornCommands;
26
+ readonly agents: NornAgents;
27
+
28
+ constructor(private readonly input: NornExecutionContextInput) {
29
+ this.run = {
30
+ id: input.id,
31
+ next: (workflowId, args) => createWorkflowTransition({ workflowId, args }),
32
+ complete: metadata => ({ type: "complete", metadata }),
33
+ fail: metadata => ({ type: "fail", metadata }),
34
+ };
35
+ this.logs = {
36
+ read: log => this.input.logs.read(log),
37
+ };
38
+ this.commands = {
39
+ run: commandInput => new NornCommandRunner({
40
+ signal: this.input.signal,
41
+ logs: this.input.logs,
42
+ logger: this.input.logger,
43
+ }).run(commandInput),
44
+ };
45
+ this.agents = {
46
+ createSession: agentInput => this.createAgentRunner().createSession(agentInput),
47
+ prompt: agentInput => this.createAgentRunner().prompt(agentInput),
48
+ };
49
+ }
50
+
51
+ private createAgentRunner(): NornAgentRunner {
52
+ return new NornAgentRunner({
53
+ id: this.input.id,
54
+ runRoot: this.input.runRoot,
55
+ signal: this.input.signal,
56
+ model: this.input.model,
57
+ thinkingLevel: this.input.thinkingLevel,
58
+ agentDir: this.input.agentDir,
59
+ logs: this.input.logs,
60
+ logger: this.input.logger,
61
+ responseCollector: this.input.responseCollector,
62
+ });
63
+ }
64
+ }
@@ -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
 
@@ -5,7 +5,7 @@ import { isNodeError } from "@vimhead.dev/norn-core/errors";
5
5
  import { readRunLaunchRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.ts";
6
6
  import { getRunLeaseHealth } from "./run-lease.ts";
7
7
  import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
8
- import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
8
+ import { createRunFileCoordinator, type NornFileCoordinator } from "./file-coordinator.ts";
9
9
  import { runCurrentRoot } from "./run-store.ts";
10
10
  import { jsonValueSchema } from "@vimhead.dev/norn/schema";
11
11
  import { Value } from "typebox/value";
@@ -59,6 +59,7 @@ type NornRunWorkflowOutcome = {
59
59
 
60
60
  export type NornRunState = {
61
61
  readonly version: 1 | typeof RUN_STATE_VERSION;
62
+ readonly projectRoot?: string;
62
63
  readonly id: string;
63
64
  readonly name: string;
64
65
  readonly entrypointWorkflowId: string;
@@ -74,6 +75,7 @@ export type NornRunState = {
74
75
  };
75
76
 
76
77
  type CreateNornRunStateInput = {
78
+ readonly projectRoot: string;
77
79
  readonly id: string;
78
80
  readonly name: string;
79
81
  readonly entrypointWorkflowId: string;
@@ -101,7 +103,8 @@ export class NornRunStateStore {
101
103
  id: input.id,
102
104
  name: input.name,
103
105
  entrypointWorkflowId: input.entrypointWorkflowId,
104
- workspace: input.workspace,
106
+ projectRoot: resolve(input.projectRoot),
107
+ workspace: resolve(input.workspace),
105
108
  configOverride: input.configOverride,
106
109
  status: "running",
107
110
  current: input.current,
@@ -252,6 +255,7 @@ export async function getRunInfo(runRoot: string): Promise<NornRunInfo> {
252
255
  id: state.id,
253
256
  name: state.name,
254
257
  path: runRoot,
258
+ paths: { project: state.projectRoot ?? resolve(runRoot, "../../.."), workspace: state.workspace },
255
259
  entrypointWorkflowId: state.entrypointWorkflowId,
256
260
  currentWorkflowId: state.current?.workflowId,
257
261
  status: resumeRequest ? "running" : state.status,
@@ -317,6 +321,7 @@ async function getLaunchedRunInfo(runRoot: string): Promise<NornRunInfo> {
317
321
  id: launchRequest.id,
318
322
  name: launchRequest.name,
319
323
  path: runRoot,
324
+ paths: { project: resolve(runRoot, "../../.."), workspace: join(runCurrentRoot(resolve(runRoot)), "workspace") },
320
325
  entrypointWorkflowId: launchRequest.workflowId,
321
326
  currentWorkflowId: launchRequest.workflowId,
322
327
  status: "running",
@@ -0,0 +1,17 @@
1
+ import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
+
5
+ export async function prepareRunWorkerDirectory(runRoot: string): Promise<string> {
6
+ const directory = join(runRoot, "worker");
7
+ try {
8
+ await mkdir(directory, { mode: 0o555 });
9
+ } catch (error) {
10
+ if (!isNodeError(error) || error.code !== "EEXIST") throw error;
11
+ }
12
+ const metadata = await lstat(directory);
13
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Run worker cwd must be a directory, not a symlink: ${directory}`);
14
+ if ((await readdir(directory)).length !== 0) throw new Error(`Run worker cwd must be empty: ${directory}`);
15
+ await chmod(directory, 0o555);
16
+ return directory;
17
+ }
@@ -1,4 +1,4 @@
1
- import { isWorkflowComplete, isWorkflowDeclaration, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type NornWorkflowContext, type NornWorkflowGateInfo, type NornWorkflowSource, type NornWorkflowScopeInfo, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
1
+ import { isWorkflowComplete, isWorkflowDeclaration, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunNext, type NornWorkflowPaths, type NornWorkflowContext, type NornWorkflowGateInfo, type NornWorkflowSource, type NornWorkflowScopeInfo, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
2
2
  import { assertWorkflowMetadata, inspectSchema, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
3
3
  import type { TSchema } from "typebox";
4
4
  import { Value } from "typebox/value";
@@ -42,6 +42,15 @@ export type NornWorkflowStepResult =
42
42
  | { readonly type: "complete"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata?: NornRunComplete["metadata"] }
43
43
  | { readonly type: "fail"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata: NornRunFail["metadata"] };
44
44
 
45
+ type NornExecutionContextInput = {
46
+ readonly execution: Pick<NornWorkflowContext, "run" | "agents" | "commands" | "logs">;
47
+ readonly paths: NornWorkflowPaths;
48
+ readonly args: unknown;
49
+ readonly configOverride: unknown;
50
+ };
51
+
52
+ type NornWorkflowExecutionInput = NornExecutionContextInput & { readonly workflow: NornAnyWorkflowDeclaration };
53
+
45
54
  export class NornWorkflowRegistry {
46
55
  private readonly entries = new Map<string, NornRegisteredWorkflow>();
47
56
  private readonly scopes = new Map<string, NornRegisteredScope>();
@@ -99,32 +108,38 @@ export class NornWorkflowRegistry {
99
108
  launchableEntries(): NornRegisteredWorkflow[] { return this.sortedEntries().filter(({ workflow }) => workflow.isEntrypoint); }
100
109
  workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined { return this.entries.get(workflowId)?.workflow; }
101
110
 
102
- async describeGate<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, args: unknown, configOverride?: unknown): Promise<string> {
111
+ async describeGate(input: NornWorkflowExecutionInput): Promise<string> {
112
+ const { workflow } = input;
103
113
  const entry = this.requireEntry(workflow.id);
104
114
  if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
105
- const context = this.createExecutionContext({ entry, run, args, configOverride });
115
+ const context = this.createExecutionContext({ ...input, entry });
106
116
  const description = await entry.workflow.gate?.describe?.(context) ?? workflow.id;
107
117
  if (typeof description !== "string" || description.trim().length === 0) throw new Error(`Workflow gate description must not be empty: ${workflow.id}`);
108
118
  return description.trim();
109
119
  }
110
120
 
111
- async execute<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, args: unknown, configOverride?: unknown): Promise<NornWorkflowStepResult> {
121
+ async execute(input: NornWorkflowExecutionInput): Promise<NornWorkflowStepResult> {
122
+ const { workflow } = input;
112
123
  const entry = this.requireEntry(workflow.id);
113
- const result = await entry.workflow.execute(this.createExecutionContext({ entry, run, args, configOverride }));
124
+ const result = await entry.workflow.execute(this.createExecutionContext({ ...input, entry }));
114
125
  if (isWorkflowNext(result)) return result;
115
126
  if (isWorkflowComplete(result)) return { type: "complete", workflow, metadata: result.metadata };
116
127
  if (isWorkflowFail(result)) return { type: "fail", workflow, metadata: result.metadata };
117
128
  throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
118
129
  }
119
130
 
120
- private createExecutionContext(input: { readonly entry: NornRegisteredWorkflow; readonly run: NornRunFor<NornAnyWorkflowDeclaration>; readonly args: unknown; readonly configOverride: unknown }): NornWorkflowContext {
121
- const { entry, run, configOverride } = input;
131
+ private createExecutionContext(input: NornExecutionContextInput & { readonly entry: NornRegisteredWorkflow }): NornWorkflowContext {
132
+ const { entry, execution, paths, configOverride } = input;
122
133
  const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : undefined;
123
134
  return {
124
135
  args: Value.Decode(entry.workflow.args, input.args),
125
136
  config: parseExecutionConfig(entry.configuration, configOverride),
126
137
  ...(scope ? { scope: { id: scope.definition.id, config: parseExecutionConfig(scope.configuration, configOverride) } } : {}),
127
- run,
138
+ paths,
139
+ run: execution.run,
140
+ agents: execution.agents,
141
+ commands: execution.commands,
142
+ logs: execution.logs,
128
143
  };
129
144
  }
130
145
 
@@ -175,7 +190,7 @@ function assertGateWorkflow(workflow: NornAnyWorkflowDeclaration): void {
175
190
  function workflowInfo(entry: NornRegisteredWorkflow): NornRegisteredWorkflowInfo {
176
191
  return {
177
192
  id: entry.workflow.id, instructions: entry.workflow.instructions, isEntrypoint: entry.workflow.isEntrypoint,
178
- isolation: entry.workflow.isolation, source: entry.source,
193
+ source: entry.source,
179
194
  configKey: entry.workflow.id,
180
195
  scope: entry.scopeId ? { id: entry.scopeId } : undefined,
181
196
  };
@@ -0,0 +1,6 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+
3
+ export function requireAbsoluteWorkingDirectory(cwd: string): string {
4
+ if (typeof cwd !== "string" || !isAbsolute(cwd)) throw new Error("Working directory (cwd) must be an absolute path");
5
+ return resolve(cwd);
6
+ }
@@ -1,7 +1,6 @@
1
1
  import * as nornModule from "@vimhead.dev/norn";
2
2
  import { isWorkflowDeclaration, type NornAnyWorkflowDeclaration, type NornDispose, type NornWorkflowDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectConfigurationInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowSource } from "@vimhead.dev/norn";
3
3
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
- import * as nornFilesModule from "@vimhead.dev/norn/files";
5
4
  import * as nornSchemaModule from "@vimhead.dev/norn/schema";
6
5
  import { inspectSchema, isPlainObject } from "@vimhead.dev/norn/schema";
7
6
  import { createJiti, type ModuleCache } from "jiti/static";
@@ -178,7 +177,7 @@ function createDiagnostic(input: { readonly source: NornWorkflowSource; readonly
178
177
  return { configPath: input.source.configPath, modulePath: input.source.path, scopeId: input.scopeId, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error), issues: input.error instanceof AssertError ? input.error.cause.errors : [] };
179
178
  }
180
179
  function nornWorkflowVirtualModules(): Record<string, unknown> {
181
- return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/files": nornFilesModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
180
+ return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
182
181
  }
183
182
  function mergeProjectConfig(files: readonly NornConfigFile[]): Record<string, unknown> {
184
183
  const [project, ...reusable] = files;