@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
@@ -14,17 +14,16 @@ import { inspectSchema } from "@vimhead.dev/norn/schema";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import { mkdir } from "node:fs/promises";
16
16
  import { homedir } from "node:os";
17
- import { isAbsolute, relative, resolve, sep } from "node:path";
17
+ import { resolve } from "node:path";
18
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.js";
18
19
  import { resolveNornAgentDirectory } from "./agent-directory.js";
19
20
  import {
20
21
  NornAgentResponseToolFactory
21
22
  } from "./agent-response-tool.js";
22
23
  import { errorMessage } from "./errors.js";
23
24
  import { safeFileName } from "./file-names.js";
24
- import { NornSessionResourceBindings } from "./resource-bindings.js";
25
25
  import { agentUsageFromValue, emptyAgentUsage, totalAgentUsage } from "./usage.js";
26
26
  var DEFAULT_AGENT_ATTEMPTS = 3;
27
- var DEFAULT_AGENT_TOOL_ALLOWLIST = ["read", "bash", "edit", "write", AGENT_RESPONSE_TOOL_NAME];
28
27
  var NornAgentRunner = class {
29
28
  constructor(input) {
30
29
  this.input = input;
@@ -33,7 +32,7 @@ var NornAgentRunner = class {
33
32
  input;
34
33
  responseToolFactory;
35
34
  async createSession(agentInput) {
36
- const cwd = agentInput.cwd ? this.resolveFromCwd(agentInput.cwd) : this.input.cwd;
35
+ const cwd = requireAbsoluteWorkingDirectory(agentInput.cwd);
37
36
  const sessionDir = resolve(this.input.runRoot, "sessions");
38
37
  await mkdir(sessionDir, { recursive: true });
39
38
  const eventBus = createEventBus();
@@ -53,31 +52,29 @@ var NornAgentRunner = class {
53
52
  ...loader.getExtensions().errors.map((error) => `Failed to load extension ${error.path}: ${error.error}`)
54
53
  ];
55
54
  if (errors.length > 0) throw new Error(errors.join("\n"));
56
- const resourceBindings = new NornSessionResourceBindings();
55
+ const customTools = agentInput.customTools ?? [];
56
+ assertCustomToolNames({
57
+ tools: customTools,
58
+ reservedNames: [
59
+ "read",
60
+ "bash",
61
+ "edit",
62
+ "write",
63
+ "grep",
64
+ "find",
65
+ "ls",
66
+ "powershell",
67
+ AGENT_RESPONSE_TOOL_NAME,
68
+ ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])
69
+ ]
70
+ });
57
71
  let session;
58
72
  try {
59
- await resourceBindings.bind({
60
- adapters: agentInput.resourceAdapters ?? [],
61
- runId: this.input.id,
62
- label: agentInput.label,
63
- reservedTools: [
64
- "read",
65
- "bash",
66
- "edit",
67
- "write",
68
- "grep",
69
- "find",
70
- "ls",
71
- "powershell",
72
- AGENT_RESPONSE_TOOL_NAME,
73
- ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])
74
- ]
75
- });
76
73
  ({ session } = await createAgentSessionFromServices({
77
74
  services,
78
75
  sessionManager: SessionManager.create(cwd, sessionDir),
79
- tools: [...withAgentResponseTool(agentInput.tools), ...resourceBindings.tools.map((tool) => tool.name)],
80
- customTools: [this.responseToolFactory.create(), ...resourceBindings.tools],
76
+ tools: withAgentResponseTool(agentInput.tools),
77
+ customTools: [this.responseToolFactory.create(), ...customTools],
81
78
  model: agentInput.model ?? this.input.model,
82
79
  thinkingLevel: agentInput.thinkingLevel ?? this.input.thinkingLevel
83
80
  }));
@@ -89,7 +86,6 @@ var NornAgentRunner = class {
89
86
  label: agentInput.label,
90
87
  cwd,
91
88
  session,
92
- resourceBindings,
93
89
  events: eventBus
94
90
  });
95
91
  } catch (error) {
@@ -99,12 +95,7 @@ var NornAgentRunner = class {
99
95
  } catch (cleanupError) {
100
96
  errors2.push(cleanupError);
101
97
  }
102
- try {
103
- await resourceBindings.dispose();
104
- } catch (cleanupError) {
105
- errors2.push(cleanupError);
106
- }
107
- if (errors2.length > 1) throw new AggregateError(errors2, "Agent creation and resource cleanup failed");
98
+ if (errors2.length > 1) throw new AggregateError(errors2, "Agent creation and cleanup failed");
108
99
  throw error;
109
100
  }
110
101
  }
@@ -116,14 +107,6 @@ var NornAgentRunner = class {
116
107
  await agent.dispose();
117
108
  }
118
109
  }
119
- resolveFromCwd(path) {
120
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
121
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
122
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
123
- throw new Error(`Agent cwd escapes ${this.input.boundaryName} isolation: ${path}`);
124
- }
125
- return resolvedPath;
126
- }
127
110
  };
128
111
  function systemPromptOverride(agentInput) {
129
112
  const systemPrompt = agentInput.systemPrompt;
@@ -223,11 +206,6 @@ var CreatedNornAgentSession = class {
223
206
  } catch (error) {
224
207
  errors.push(error);
225
208
  }
226
- try {
227
- await this.input.resourceBindings.dispose();
228
- } catch (error) {
229
- errors.push(error);
230
- }
231
209
  if (errors.length) throw new AggregateError(errors, "Agent disposal failed");
232
210
  await this.input.logger.record({ type: "agent.disposed", label: this.label });
233
211
  }
@@ -328,8 +306,14 @@ function withResponseToolFinalizationInstruction(responseSchema, label, response
328
306
  ].join("\n");
329
307
  }
330
308
  function withAgentResponseTool(tools) {
331
- if (!tools) return [...DEFAULT_AGENT_TOOL_ALLOWLIST];
332
- return Array.from(/* @__PURE__ */ new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
309
+ return tools === void 0 ? void 0 : Array.from(/* @__PURE__ */ new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
310
+ }
311
+ function assertCustomToolNames(input) {
312
+ const names = new Set(input.reservedNames);
313
+ for (const tool of input.tools) {
314
+ if (names.has(tool.name)) throw new Error(`Custom tool name collision: ${tool.name}`);
315
+ names.add(tool.name);
316
+ }
333
317
  }
334
318
  export {
335
319
  NornAgentRunner
@@ -2,9 +2,6 @@ import type { NornCommandRunInput, NornCommandRunResult } from "@vimhead.dev/nor
2
2
  import type { NornRunLogs } from "./logs.ts";
3
3
  import type { NornRunLogger } from "./run-log.ts";
4
4
  type NornCommandRunnerInput = {
5
- readonly boundaryRoot: string;
6
- readonly boundaryName: string;
7
- readonly cwd: string;
8
5
  readonly signal?: AbortSignal;
9
6
  readonly logs: NornRunLogs;
10
7
  readonly logger: NornRunLogger;
@@ -13,6 +10,5 @@ export declare class NornCommandRunner {
13
10
  private readonly input;
14
11
  constructor(input: NornCommandRunnerInput);
15
12
  run(commandInput: NornCommandRunInput): Promise<NornCommandRunResult>;
16
- private resolveFromCwd;
17
13
  }
18
14
  export {};
@@ -1,7 +1,7 @@
1
1
  // src/internal/commands.ts
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.js";
5
5
  import { errorMessage } from "./errors.js";
6
6
  import { safeFileName } from "./file-names.js";
7
7
  var NornCommandRunner = class {
@@ -10,7 +10,7 @@ var NornCommandRunner = class {
10
10
  }
11
11
  input;
12
12
  async run(commandInput) {
13
- const cwd = this.resolveFromCwd(commandInput.cwd ?? this.input.cwd);
13
+ const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
14
14
  const startedAtMs = Date.now();
15
15
  const invocationId = randomUUID();
16
16
  const stdoutRef = commandLog({ label: commandInput.label, invocationId, stream: "stdout" });
@@ -56,14 +56,6 @@ var NornCommandRunner = class {
56
56
  });
57
57
  return commandResult;
58
58
  }
59
- resolveFromCwd(path) {
60
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
61
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
62
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
63
- throw new Error(`Command cwd escapes ${this.input.boundaryName} isolation: ${path}`);
64
- }
65
- return resolvedPath;
66
- }
67
59
  };
68
60
  async function spawnCommand(input) {
69
61
  const command = typeof input.command === "string" ? "bash" : input.command[0];
@@ -1,4 +1,4 @@
1
- import { type NornAnyWorkflowDeclaration, type NornAnyWorkflowPluginManifest, type NornDispose, type NornRunCheckpoint, type NornRunInfo, type NornRunResult, type NornRunStartOptions, type NornWorkflowPlugin } from "@vimhead.dev/norn";
1
+ import { type NornAnyWorkflowDeclaration, type NornDispose, type NornRunCheckpoint, type NornRunInfo, type NornRunResult, type NornRunStartOptions } from "@vimhead.dev/norn";
2
2
  import { NornAgentResponseCollector } from "./agent-response-tool.ts";
3
3
  import { type NornRunResumeRequest } from "./launch-request.ts";
4
4
  import { type NornRegisteredWorkflow } from "./workflow-registry.ts";
@@ -13,19 +13,16 @@ export type NornEngineInput = {
13
13
  export declare class NornEngine {
14
14
  private readonly input;
15
15
  private readonly registry;
16
- private readonly registrarState;
17
- private readonly disposersByPlugin;
18
16
  private readonly responseCollector;
19
17
  private readonly activeRuns;
20
18
  constructor(input: NornEngineInput);
21
- registerPlugin<TManifest extends NornAnyWorkflowPluginManifest>(plugin: NornWorkflowPlugin<TManifest>): NornDispose;
19
+ registerWorkflows(workflows: readonly NornAnyWorkflowDeclaration[]): NornDispose;
22
20
  listWorkflows(): import("@vimhead.dev/norn").NornRegisteredWorkflowInfo[];
23
21
  visibleWorkflowEntries(): NornRegisteredWorkflow[];
24
- private resolvePluginImplementation;
25
- runWorkflow<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, params: unknown, options: NornRunStartOptions | undefined): Promise<NornRunResult>;
22
+ runWorkflow<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, args: unknown, options: NornRunStartOptions | undefined): Promise<NornRunResult>;
26
23
  listRunCheckpoints(path: string): Promise<NornRunCheckpoint[]>;
27
24
  rollbackRun(path: string, checkpointId: string): Promise<NornRunInfo>;
28
- resumeWorkflow(path: string, params?: unknown): Promise<NornRunResult>;
25
+ resumeWorkflow(path: string, args?: unknown): Promise<NornRunResult>;
29
26
  resumeRequestedWorkflow(input: {
30
27
  readonly runRoot: string;
31
28
  readonly request: NornRunResumeRequest;
@@ -40,5 +37,4 @@ export declare class NornEngine {
40
37
  private startActiveRun;
41
38
  private finishActiveRun;
42
39
  private failActiveRun;
43
- private disposePlugin;
44
40
  }
@@ -1,22 +1,19 @@
1
1
  // src/internal/engine.ts
2
- import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
2
+ import { createRunFileCoordinator } from "./file-coordinator.js";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { mkdir } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
6
6
  import { Value } from "typebox/value";
7
7
  import { NornAgentResponseCollector } from "./agent-response-tool.js";
8
- import { NornArtifacts } from "./artifacts.js";
9
8
  import { errorMessage, NornRunStoppedError } from "./errors.js";
10
9
  import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.js";
11
10
  import { NornRunLogs } from "./logs.js";
12
11
  import { NornRunLease } from "./run-lease.js";
13
12
  import { NornRunLogger } from "./run-log.js";
14
- import { initializeRunResources } from "./run-resources.js";
15
- import { getRunInfo, mergeInterruptedWorkflowParams, NornRunStateStore, resolveRunRoot } from "./run-state.js";
13
+ import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot } from "./run-state.js";
16
14
  import { NornRunStore, runCurrentRoot } from "./run-store.js";
17
15
  import { NornRunContext } from "./run.js";
18
- import { NornMemoryWorkflowState } from "./state-store.js";
19
- import { decodePluginConfiguration, NornWorkflowRegistry } from "./workflow-registry.js";
16
+ import { NornWorkflowRegistry } from "./workflow-registry.js";
20
17
  var RUNS_DIR_NAME = ".norn";
21
18
  var MANIFEST_FILE_NAME = "manifest.json";
22
19
  var NornEngine = class {
@@ -26,22 +23,18 @@ var NornEngine = class {
26
23
  }
27
24
  input;
28
25
  registry = new NornWorkflowRegistry();
29
- registrarState = new NornMemoryWorkflowState();
30
- disposersByPlugin = /* @__PURE__ */ new Map();
31
26
  responseCollector;
32
27
  activeRuns = /* @__PURE__ */ new Map();
33
- registerPlugin(plugin) {
34
- this.disposePlugin(plugin.manifest.id);
35
- const implementation = this.resolvePluginImplementation(plugin.implementation);
36
- const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: this.input.config?.[plugin.manifest.id] });
37
- const disposers = Object.entries(plugin.manifest.workflows).map(([key, workflow]) => {
38
- const workflowImplementation = implementation.workflows[key];
39
- if (!workflowImplementation) throw new Error(`Missing implementation for workflow ${plugin.manifest.id}.${key}`);
40
- return this.registry.register(workflow, workflowImplementation, { plugin: { id: plugin.manifest.id }, configuration });
41
- });
42
- this.disposersByPlugin.set(plugin.manifest.id, disposers);
28
+ registerWorkflows(workflows) {
29
+ const disposers = [];
30
+ try {
31
+ for (const workflow of workflows) disposers.push(this.registry.register({ workflow, config: this.input.config ?? {}, source: void 0 }));
32
+ } catch (error) {
33
+ for (const dispose of disposers.reverse()) dispose();
34
+ throw error;
35
+ }
43
36
  return () => {
44
- if (this.disposersByPlugin.get(plugin.manifest.id) === disposers) this.disposePlugin(plugin.manifest.id);
37
+ for (const dispose of disposers) dispose();
45
38
  };
46
39
  }
47
40
  listWorkflows() {
@@ -50,11 +43,8 @@ var NornEngine = class {
50
43
  visibleWorkflowEntries() {
51
44
  return this.registry.launchableEntries();
52
45
  }
53
- resolvePluginImplementation(implementationInput) {
54
- return typeof implementationInput === "function" ? implementationInput({ cwd: this.input.cwd, state: this.registrarState }) : implementationInput;
55
- }
56
- async runWorkflow(workflow, params, options) {
57
- const session = await this.createRunSession(workflow, params, options);
46
+ async runWorkflow(workflow, args, options) {
47
+ const session = await this.createRunSession(workflow, args, options);
58
48
  const current = session.state.currentState().current;
59
49
  if (!current) throw new Error("New run is missing its initial workflow step");
60
50
  return this.runScheduler(session, toWorkflowStep(workflow, current));
@@ -65,6 +55,7 @@ var NornEngine = class {
65
55
  }
66
56
  async rollbackRun(path, checkpointId) {
67
57
  const runRoot = await resolveRunRoot(this.input.cwd, path);
58
+ assertRunVersion((await getRunInfo(runRoot)).version);
68
59
  const lease = await NornRunLease.acquire(runRoot);
69
60
  try {
70
61
  const pendingRequest = await readOptionalRunResumeRequest(runRoot);
@@ -80,19 +71,19 @@ var NornEngine = class {
80
71
  await lease.release();
81
72
  }
82
73
  }
83
- async resumeWorkflow(path, params) {
84
- return this.resumeWorkflowExecution({ path, params, expectedRequest: void 0 });
74
+ async resumeWorkflow(path, args) {
75
+ return this.resumeWorkflowExecution({ path, args, expectedRequest: void 0 });
85
76
  }
86
77
  async resumeRequestedWorkflow(input) {
87
- return this.resumeWorkflowExecution({ path: input.runRoot, params: input.request.params, expectedRequest: input.request });
78
+ return this.resumeWorkflowExecution({ path: input.runRoot, args: input.request.args, expectedRequest: input.request });
88
79
  }
89
80
  async resumeWorkflowExecution(input) {
90
- const { params, expectedRequest } = input;
81
+ const { args, expectedRequest } = input;
91
82
  const runRoot = await resolveRunRoot(this.input.cwd, input.path);
92
83
  const initialStateStore = await NornRunStateStore.load(runRoot);
93
84
  const initialState = initialStateStore.currentState();
94
- if (initialState.status === "interrupted" && params === void 0) throw new Error(`Interrupted workflow resume requires params: ${runRoot}`);
95
- if (initialState.status === "pendingResume" && params !== void 0) throw new Error(`Pending-resume workflows do not accept params: ${runRoot}`);
85
+ if (initialState.status === "interrupted" && args === void 0) throw new Error(`Interrupted workflow resume requires args: ${runRoot}`);
86
+ if (initialState.status === "pendingResume" && args !== void 0) throw new Error(`Pending-resume workflows do not accept args: ${runRoot}`);
96
87
  if (initialState.status !== "interrupted" && initialState.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
97
88
  const lease = await NornRunLease.acquire(runRoot);
98
89
  let isLeaseOwnedByScheduler = false;
@@ -107,17 +98,17 @@ var NornEngine = class {
107
98
  const workflow = this.registry.workflowById(current.workflowId);
108
99
  if (!workflow) throw new Error(`Unknown workflow for resumed run: ${current.workflowId}`);
109
100
  if (state.status === "interrupted") {
110
- const rawParams = mergeInterruptedWorkflowParams(current.params, params, current.interruption?.fields);
111
- Value.Decode(workflow.params, rawParams);
112
- await session.state.replaceCurrentParams(rawParams);
113
- await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
101
+ const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
102
+ Value.Decode(workflow.args, rawArgs);
103
+ await session.state.replaceCurrentArgs(rawArgs);
104
+ await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
114
105
  const resumedCurrent = session.state.currentState().current;
115
106
  if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
116
107
  isLeaseOwnedByScheduler = true;
117
108
  return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
118
109
  }
119
110
  if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
120
- await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
111
+ await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
121
112
  isLeaseOwnedByScheduler = true;
122
113
  return this.runScheduler(session, toWorkflowStep(workflow, current));
123
114
  } finally {
@@ -135,28 +126,27 @@ var NornEngine = class {
135
126
  try {
136
127
  for (let step = 1; step <= 1e3; step++) {
137
128
  throwIfRunAborted(session.activeRun);
138
- const stepRuntime = session.run.forWorkflow(currentStep.workflow);
139
129
  if (shouldPauseForGate(currentStep)) {
140
- const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, currentStep.params, session.state.currentState().configOverride);
130
+ const description = await this.registry.describeGate({ workflow: currentStep.workflow, run: session.run, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
141
131
  const interruption = { description, fields: currentStep.workflow.gate?.fields };
142
- await session.state.interruptCurrent(currentStep.params, interruption);
132
+ await session.state.interruptCurrent(currentStep.args, interruption);
143
133
  await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
144
134
  await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
145
- return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, currentStep.params, interruption);
135
+ return interruptedLaunchResult({ id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
146
136
  }
147
137
  await session.state.startStep(toRunStateStep(currentStep));
148
- const stepResult = await this.executeWorkflowStep(session, currentStep, stepRuntime);
138
+ const stepResult = await this.executeWorkflowStep(session, currentStep);
149
139
  throwIfRunAborted(session.activeRun);
150
140
  if (stepResult.type === "complete") {
151
141
  await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
152
142
  await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
153
143
  await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
154
- return { status: "completed", id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
144
+ 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 };
155
145
  }
156
146
  if (stepResult.type === "fail") {
157
147
  await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
158
148
  await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
159
- return { status: "failed", id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
149
+ 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 };
160
150
  }
161
151
  const nextStep = this.nextWorkflowStep(stepResult);
162
152
  await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
@@ -169,7 +159,7 @@ var NornEngine = class {
169
159
  if (isRunStopped(session, error)) {
170
160
  await session.state.stopCurrent();
171
161
  await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
172
- return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.run.workspace, cwd: session.run.cwd, workflowId: currentStep.workflow.id };
162
+ return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
173
163
  }
174
164
  await session.state.failCurrent(errorMessage(error));
175
165
  await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
@@ -179,13 +169,13 @@ var NornEngine = class {
179
169
  this.finishActiveRun(session.runRoot, session.activeRun);
180
170
  }
181
171
  }
182
- async executeWorkflowStep(session, step, run) {
172
+ async executeWorkflowStep(session, step) {
183
173
  const startedAtMs = Date.now();
184
174
  try {
185
- await assertWorkspaceBoundary(session, run.workspace);
175
+ await assertWorkspaceBoundary(session, session.paths.workspace);
186
176
  await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
187
- const result = await this.registry.execute(step.workflow, run, step.params, session.state.currentState().configOverride);
188
- await assertWorkspaceBoundary(session, run.workspace);
177
+ const result = await this.registry.execute({ workflow: step.workflow, run: session.run, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
178
+ await assertWorkspaceBoundary(session, session.paths.workspace);
189
179
  const durationMs = Date.now() - startedAtMs;
190
180
  if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
191
181
  else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
@@ -206,17 +196,18 @@ var NornEngine = class {
206
196
  if (!workflow) throw new Error(`Unknown next workflow: ${next.workflowId}`);
207
197
  return {
208
198
  workflow,
209
- params: next.params,
199
+ args: next.args,
210
200
  interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
211
201
  };
212
202
  }
213
- async createRunSession(workflow, params, options) {
203
+ async createRunSession(workflow, args, options) {
214
204
  const id = options?.id ?? randomUUID();
215
205
  const name = options?.name ?? id;
216
206
  const runRoot = defaultRunRoot(this.input.cwd, id);
217
207
  const currentRoot = runCurrentRoot(runRoot);
218
208
  const workspace = join(currentRoot, "workspace");
219
- const cwd = workflowDefaultCwd(this.input.cwd, workspace, workflow);
209
+ const paths = { project: resolve(this.input.cwd), workspace };
210
+ const cwd = process.cwd();
220
211
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
221
212
  await mkdir(runRoot, { recursive: true });
222
213
  const lease = await NornRunLease.acquire(runRoot);
@@ -233,8 +224,9 @@ var NornEngine = class {
233
224
  initialCwd: cwd,
234
225
  startedAt
235
226
  } });
236
- const run = await this.buildRun({ id, currentRoot, workspace, cwd, isolationMode: workflow.isolation.mode, signal: activeRun.controller.signal, logger });
227
+ const run = await this.buildRun({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
237
228
  const state = await NornRunStateStore.create(runRoot, {
229
+ projectRoot: paths.project,
238
230
  id,
239
231
  name,
240
232
  entrypointWorkflowId: workflow.id,
@@ -242,14 +234,14 @@ var NornEngine = class {
242
234
  configOverride: options?.configOverride,
243
235
  current: {
244
236
  workflowId: workflow.id,
245
- params,
237
+ args,
246
238
  cwd,
247
239
  env: {},
248
240
  interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
249
241
  },
250
242
  startedAt
251
243
  });
252
- const session = { runRoot, run, state, lease, runStore, logger, activeRun };
244
+ const session = { runRoot, cwd, run, paths, state, lease, runStore, logger, activeRun };
253
245
  await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
254
246
  await commitRunBoundary(session, `run started: ${workflow.id}`);
255
247
  return session;
@@ -268,38 +260,27 @@ var NornEngine = class {
268
260
  const runStore = await NornRunStore.open(runRoot);
269
261
  const currentRoot = runCurrentRoot(runRoot);
270
262
  const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
271
- const workflow = currentState.current ? this.registry.workflowById(currentState.current.workflowId) : void 0;
272
- const isolationMode = workflow?.isolation.mode ?? "runWorkspace";
273
- const cwd = workflowDefaultCwd(this.input.cwd, currentState.workspace, workflow);
274
- const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
275
- return { runRoot, run, state, lease, runStore, logger, activeRun };
263
+ const paths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
264
+ const run = await this.buildRun({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
265
+ return { runRoot, cwd: process.cwd(), run, paths, state, lease, runStore, logger, activeRun };
276
266
  } catch (error) {
277
267
  this.failActiveRun(runRoot, activeRun);
278
268
  throw error;
279
269
  }
280
270
  }
281
271
  async buildRun(input) {
282
- const artifactsRoot = join(input.currentRoot, "artifacts");
283
272
  const logsRoot = join(input.currentRoot, "logs");
284
- await mkdir(input.workspace, { recursive: true });
285
- await mkdir(artifactsRoot, { recursive: true });
273
+ await mkdir(input.paths.workspace, { recursive: true });
286
274
  await mkdir(logsRoot, { recursive: true });
287
- const { resources, state } = await initializeRunResources(dirname(input.currentRoot));
275
+ const files = createRunFileCoordinator(dirname(input.currentRoot));
288
276
  return new NornRunContext({
289
277
  id: input.id,
290
278
  runRoot: input.currentRoot,
291
- workspace: input.workspace,
292
- projectRoot: this.input.cwd,
293
- cwd: input.cwd,
294
- isolationMode: input.isolationMode,
295
279
  signal: input.signal,
296
280
  agentDir: this.input.agentDir,
297
281
  responseCollector: this.responseCollector,
298
- resources,
299
- state,
300
282
  logger: input.logger,
301
- artifacts: new NornArtifacts(artifactsRoot, resources.files),
302
- logs: new NornRunLogs(logsRoot, resources.files)
283
+ logs: new NornRunLogs(logsRoot, files)
303
284
  });
304
285
  }
305
286
  startActiveRun(runRoot) {
@@ -332,29 +313,21 @@ var NornEngine = class {
332
313
  activeRun.dispose();
333
314
  activeRun.finish();
334
315
  }
335
- disposePlugin(pluginId) {
336
- const disposers = this.disposersByPlugin.get(pluginId) ?? [];
337
- for (const dispose of [...disposers].reverse()) dispose();
338
- this.disposersByPlugin.delete(pluginId);
339
- }
340
316
  };
341
317
  function defaultRunRoot(cwd, id) {
342
- return join(cwd, RUNS_DIR_NAME, "runs", id);
343
- }
344
- function workflowDefaultCwd(projectRoot, workspace, workflow) {
345
- return workflow?.isolation.mode === "project" ? projectRoot : workspace;
318
+ return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
346
319
  }
347
320
  function toWorkflowStep(workflow, step) {
348
321
  return {
349
322
  workflow,
350
- params: step.params,
323
+ args: step.args,
351
324
  interruption: step.interruption
352
325
  };
353
326
  }
354
327
  function toRunStateStep(step) {
355
328
  return {
356
329
  workflowId: step.workflow.id,
357
- params: step.params,
330
+ args: step.args,
358
331
  cwd: "",
359
332
  env: {},
360
333
  interruption: step.interruption
@@ -372,7 +345,7 @@ function throwIfRunAborted(activeRun) {
372
345
  function isRunStopped(session, error) {
373
346
  return error instanceof NornRunStoppedError || session.activeRun.controller.signal.reason instanceof NornRunStoppedError;
374
347
  }
375
- function interruptedLaunchResult(run, workflow, params, interruption) {
348
+ function interruptedLaunchResult(run, workflow, args, interruption) {
376
349
  if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
377
350
  if (!interruption.description) throw new Error(`Interrupted workflow is missing description: ${workflow.id}`);
378
351
  return {
@@ -382,7 +355,7 @@ function interruptedLaunchResult(run, workflow, params, interruption) {
382
355
  workspace: run.workspace,
383
356
  cwd: run.cwd,
384
357
  workflowId: workflow.id,
385
- interruption: { workflowId: workflow.id, params, description: interruption.description, fields: interruption.fields }
358
+ interruption: { workflowId: workflow.id, args, description: interruption.description, fields: interruption.fields }
386
359
  };
387
360
  }
388
361
  async function recordRunEvent(session, event) {
@@ -1,11 +1,11 @@
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
  export declare class NornProjectLoadError extends Error {
4
4
  readonly code = "NORN_PROJECT_INVALID";
5
5
  readonly isComplete = false;
6
- readonly diagnostics: readonly NornPluginDiagnostic[];
6
+ readonly diagnostics: readonly NornWorkflowDiagnostic[];
7
7
  constructor(input: {
8
- readonly diagnostics: readonly NornPluginDiagnostic[];
8
+ readonly diagnostics: readonly NornWorkflowDiagnostic[];
9
9
  });
10
10
  }
11
11
  export declare class NornRunStoppedError extends Error {
@@ -6,7 +6,7 @@ var NornProjectLoadError = class extends Error {
6
6
  diagnostics;
7
7
  constructor(input) {
8
8
  const details = input.diagnostics.map(
9
- (diagnostic) => `${diagnostic.stage}: ${diagnostic.pluginPath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`
9
+ (diagnostic) => `${diagnostic.stage}: ${diagnostic.modulePath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`
10
10
  );
11
11
  super(`Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.
12
12
  ${details.join("\n")}`);
@@ -0,0 +1,17 @@
1
+ export declare class NornFileCoordinator {
2
+ private readonly input;
3
+ private readonly lockRoot;
4
+ constructor(input: {
5
+ readonly lockRoot: string;
6
+ readonly waitTimeoutMs: number;
7
+ });
8
+ withExclusiveLock<T>(path: string, operation: (lockedPath: string) => Promise<T>): Promise<T>;
9
+ readText(path: string): Promise<string>;
10
+ writeText(path: string, content: string): Promise<void>;
11
+ private withCleanup;
12
+ private resolveTarget;
13
+ private acquire;
14
+ private reclaimDeadOwner;
15
+ private removeEmptyLock;
16
+ }
17
+ export declare function createRunFileCoordinator(runRoot: string): NornFileCoordinator;