@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
@@ -12,14 +12,16 @@ import { Value } from "typebox/value";
12
12
  import { NORN_BUILD_INFO, type NornBuildInfo, type NornGithubReleaseBinaryBuildInfo } from "./build-info.ts";
13
13
  import { renderNornDocumentationIntro, resolveDocumentationCacheRoot, resolveNornDocumentation, type NornDocumentationSource } from "./documentation.ts";
14
14
  import { NornEngine } from "./internal/engine.ts";
15
+ import { resolveNornAgentDirectory } from "./internal/agent-directory.ts";
15
16
  import { errorMessage, NornProjectLoadError, NornRunStoppedError } from "./internal/errors.ts";
16
17
  import { clearRunResumeRequest, readRunLaunchRequest, readRunResumeRequest, writeRunLaunchRequest, writeRunResumeRequest, type NornRunResumeRequest } from "./internal/launch-request.ts";
17
18
  import { readRunMetrics } from "./internal/metrics.ts";
18
19
  import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.ts";
19
20
  import { generateRunName } from "./internal/run-names.ts";
20
- import { getRunInfo, listRuns, mergeInterruptedWorkflowParams, resolveRunRoot } from "./internal/run-state.ts";
21
+ import { assertRunVersion, getRunInfo, listRuns, mergeInterruptedWorkflowArgs, resolveRunRoot } from "./internal/run-state.ts";
21
22
  import { NornRunStore } from "./internal/run-store.ts";
22
- import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./plugin-loader.ts";
23
+ import { prepareRunWorkerDirectory } from "./internal/worker-directory.ts";
24
+ import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./workflow-loader.ts";
23
25
 
24
26
  const RUNS_ROOT = join(".norn", "runs");
25
27
  const RUN_WAIT_INTERVAL_MS = 1000;
@@ -110,9 +112,9 @@ const COMMANDS: readonly CliCommand[] = [
110
112
  {
111
113
  id: "project.inspect",
112
114
  path: ["project", "inspect"],
113
- description: "Use when discovering the active Norn project, plugins, and workflow sources.",
115
+ description: "Use when discovering the active Norn project, configuration, and workflow sources.",
114
116
  usage: "norn project inspect",
115
- output: "JSON object with project metadata under project, isComplete, and plugin diagnostics. Incomplete discovery does not permit execution.",
117
+ output: "JSON object with project metadata under project, isComplete, and registration diagnostics. Incomplete discovery does not permit execution.",
116
118
  examples: ["norn project inspect"],
117
119
  execute: async (args) => {
118
120
  assertNoExtraArgs("project inspect", args);
@@ -125,17 +127,17 @@ const COMMANDS: readonly CliCommand[] = [
125
127
  description: "Use when selecting a Norn workflow for a user task; defaults to entrypoint workflows.",
126
128
  usage: "norn workflows list [--entrypoints|--all]",
127
129
  options: ["--entrypoints: list entrypoint workflows", "--all: include internal workflow steps"],
128
- output: "JSON object with workflow summaries under workflows, isComplete, and plugin diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
130
+ output: "JSON object with workflow summaries under workflows, isComplete, and registration diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
129
131
  examples: ["norn workflows list", "norn workflows list --all"],
130
132
  execute: listWorkflows,
131
133
  },
132
134
  {
133
135
  id: "workflows.inspect",
134
136
  path: ["workflows", "inspect"],
135
- description: "Use when reading a workflow's instructions, params schema, gate contract, and source plugin before starting or editing it.",
137
+ description: "Use when reading a workflow's instructions, args schema, gate contract, and registration source before starting or editing it.",
136
138
  usage: "norn workflows inspect <workflow-id>",
137
139
  arguments: ["workflow-id: fully qualified workflow id"],
138
- output: "JSON object with workflow details, isComplete, and plugin diagnostics. workflow is null if unavailable in an incomplete catalog or its schema cannot be inspected; an unknown id in a complete catalog is an error.",
140
+ output: "JSON object with workflow details, isComplete, and registration diagnostics. workflow is null if unavailable in an incomplete catalog or its schema cannot be inspected; an unknown id in a complete catalog is an error.",
139
141
  examples: ["norn workflows inspect example.plan"],
140
142
  execute: async (args) => {
141
143
  const workflowId = requiredArg("workflows inspect", args, 0, "workflow id");
@@ -146,12 +148,12 @@ const COMMANDS: readonly CliCommand[] = [
146
148
  {
147
149
  id: "runs.start",
148
150
  path: ["runs", "start"],
149
- description: "Use when starting a Norn workflow run after the workflow id and params are known.",
151
+ description: "Use when starting a Norn workflow run after the workflow id and args are known.",
150
152
  usage: "norn runs start <workflow-id>",
151
153
  arguments: ["workflow-id: fully qualified workflow id to start"],
152
- stdin: "Optional JSON object: {\"params\":{...},\"config\":{\"pluginId\":{...}}}.",
154
+ stdin: "Optional JSON object: {\"args\":{...},\"config\":{\"workflowOrScopeId\":{...}}}.",
153
155
  output: "JSON object with started run info under run.",
154
- examples: ["printf '{\"params\":{\"task\":\"Add tests\"}}' | norn runs start example.plan"],
156
+ examples: ["printf '{\"args\":{\"task\":\"Add tests\"}}' | norn runs start example.plan"],
155
157
  execute: async (args) => {
156
158
  const workflowId = requiredArg("runs start", args, 0, "workflow id");
157
159
  await startRun(workflowId, args.slice(1));
@@ -163,9 +165,9 @@ const COMMANDS: readonly CliCommand[] = [
163
165
  description: "Use when resuming an interrupted gate or a checkpoint restored for retry.",
164
166
  usage: "norn runs resume <run>",
165
167
  arguments: ["run: run id, generated name, or run path"],
166
- stdin: "Optional JSON object: {\"params\":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept params.",
168
+ stdin: "Optional JSON object: {\"args\":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept args.",
167
169
  output: "JSON object with resumed run info under run.",
168
- examples: ["printf '{\"params\":{\"decision\":\"accept\"}}' | norn runs resume quiet-river-lantern"],
170
+ examples: ["printf '{\"args\":{\"decision\":\"accept\"}}' | norn runs resume quiet-river-lantern"],
169
171
  execute: async (args) => {
170
172
  const run = requiredArg("runs resume", args, 0, "run");
171
173
  await resumeRun(run, args.slice(1));
@@ -753,7 +755,7 @@ async function initProject(): Promise<void> {
753
755
  const projectPath = resolve(projectRoot, NORN_PROJECT_FILE_NAME);
754
756
  if (await isFile(projectPath)) throw new Error(`Norn project already exists: ${projectPath}`);
755
757
  await mkdir(resolve(projectRoot, RUNS_ROOT), { recursive: true });
756
- await writeFile(projectPath, `${JSON.stringify({ version: 1, plugins: [], includes: [], config: {} }, null, 2)}\n`, "utf8");
758
+ await writeFile(projectPath, `${JSON.stringify({ version: 1, workflows: [], includes: [], config: {} }, null, 2)}\n`, "utf8");
757
759
  await ensureGitignoreExcludesRunState(projectRoot);
758
760
  writeJson({ project: { path: projectPath, root: projectRoot, runsRoot: resolve(projectRoot, RUNS_ROOT) } });
759
761
  }
@@ -768,23 +770,23 @@ async function inspectProject(): Promise<void> {
768
770
  writeJson({ project, isComplete, diagnostics });
769
771
  }
770
772
 
771
- async function startRun(workflowId: string, args: readonly string[]): Promise<void> {
772
- assertNoStructuredInputArgs("runs start", args);
773
+ async function startRun(workflowId: string, commandArgs: readonly string[]): Promise<void> {
774
+ assertNoStructuredInputArgs("runs start", commandArgs);
773
775
  const project = await loadNornProject(process.cwd());
774
776
  const workflow = project.registry.workflowById(workflowId);
775
777
  if (!workflow) throw new Error(`Unknown workflow: ${workflowId}`);
776
778
  const input = parseStartRunInput(await readStdinJson());
777
- const params = input.params === undefined ? {} : input.params;
778
- Value.Decode(workflow.params, params);
779
+ const args = input.args === undefined ? {} : input.args;
780
+ Value.Decode(workflow.args, args);
779
781
  const configOverride = input.config;
780
782
  const id = randomUUID();
781
783
  const name = generateRunName(new Set((await listRuns(project.projectRoot)).map((run) => run.name)));
782
784
  const runRoot = resolve(project.projectRoot, RUNS_ROOT, id);
783
785
  await mkdir(runRoot, { recursive: true });
784
786
  const createdAt = new Date().toISOString();
785
- await writeRunLaunchRequest(runRoot, { version: 1, type: "run", id, name, workflowId, params, configOverride, createdAt });
787
+ await writeRunLaunchRequest(runRoot, { version: 2, type: "run", id, name, workflowId, args, configOverride, createdAt });
786
788
  await startDetachedExecuteRun(id, project.projectRoot);
787
- writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, createdAt }) });
789
+ writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, projectRoot: project.projectRoot, createdAt }) });
788
790
  }
789
791
 
790
792
  async function resumeRun(run: string, args: readonly string[]): Promise<void> {
@@ -796,8 +798,9 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
796
798
  let request: NornRunResumeRequest;
797
799
  try {
798
800
  const runInfo = await getRunInfo(runRoot);
799
- const params = await parseResumeParams(runInfo, input.params);
800
- request = { version: 1, type: "resume", id: runInfo.id, requestId: randomUUID(), params, createdAt: new Date().toISOString() };
801
+ assertRunVersion(runInfo.version);
802
+ const args = await parseResumeArgs(runInfo, input.args);
803
+ request = { version: 2, type: "resume", id: runInfo.id, requestId: randomUUID(), args, createdAt: new Date().toISOString() };
801
804
  await writeRunResumeRequest(runRoot, request);
802
805
  } finally {
803
806
  await lease.release();
@@ -811,18 +814,18 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
811
814
  writeJson({ run: await getRunInfo(runRoot) });
812
815
  }
813
816
 
814
- async function parseResumeParams(runInfo: NornRunInfo, params: unknown): Promise<unknown> {
817
+ async function parseResumeArgs(runInfo: NornRunInfo, args: unknown): Promise<unknown> {
815
818
  if (runInfo.status === "interrupted") {
816
- if (params === undefined) throw new Error(`Interrupted workflow resume requires params: ${runInfo.name}`);
819
+ if (args === undefined) throw new Error(`Interrupted workflow resume requires args: ${runInfo.name}`);
817
820
  if (!runInfo.currentWorkflowId) throw new Error(`Run has no current workflow: ${runInfo.name}`);
818
821
  const project = await loadNornProject(process.cwd());
819
822
  const workflow = project.registry.workflowById(runInfo.currentWorkflowId);
820
823
  if (!workflow) throw new Error(`Unknown workflow for resumed run: ${runInfo.currentWorkflowId}`);
821
- Value.Decode(workflow.params, mergeInterruptedWorkflowParams(runInfo.interruption?.params, params, runInfo.interruption?.fields));
822
- return params;
824
+ Value.Decode(workflow.args, mergeInterruptedWorkflowArgs(runInfo.interruption?.args, args, runInfo.interruption?.fields));
825
+ return args;
823
826
  }
824
827
  if (runInfo.status === "pendingResume") {
825
- if (params !== undefined) throw new Error(`Pending-resume workflows do not accept params: ${runInfo.name}`);
828
+ if (args !== undefined) throw new Error(`Pending-resume workflows do not accept args: ${runInfo.name}`);
826
829
  return undefined;
827
830
  }
828
831
  throw new Error(`Run must be rolled back before resuming: ${runInfo.name}`);
@@ -898,13 +901,15 @@ async function executeRun(runId: string): Promise<void> {
898
901
  const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
899
902
  const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
900
903
  try {
901
- const project = await loadNornProject(process.cwd());
902
- const engine = new NornEngine({ cwd: project.projectRoot, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
903
- for (const plugin of project.plugins) engine.registerPlugin(plugin);
904
+ const agentDir = resolveNornAgentDirectory({ home: homedir(), environment: process.env });
905
+ process.chdir(await prepareRunWorkerDirectory(runRoot));
906
+ const project = await loadNornProject(location.projectRoot);
907
+ const engine = new NornEngine({ cwd: project.projectRoot, agentDir, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
908
+ engine.registerWorkflows(project.definitions);
904
909
  if (request.type === "run") {
905
910
  const workflow = project.registry.workflowById(request.workflowId);
906
911
  if (!workflow) throw new Error(`Unknown workflow: ${request.workflowId}`);
907
- await engine.runWorkflow(workflow, request.params, { id: request.id, name: request.name, configOverride: request.configOverride });
912
+ await engine.runWorkflow(workflow, request.args, { id: request.id, name: request.name, configOverride: request.configOverride });
908
913
  } else {
909
914
  await engine.resumeRequestedWorkflow({ runRoot, request });
910
915
  }
@@ -999,6 +1004,7 @@ function detachedExecuteRunArgs(runId: string): readonly string[] {
999
1004
  }
1000
1005
 
1001
1006
  function startedRunInfo(input: {
1007
+ readonly projectRoot: string;
1002
1008
  readonly id: string;
1003
1009
  readonly name: string;
1004
1010
  readonly workflow: NornAnyWorkflowDeclaration;
@@ -1006,10 +1012,11 @@ function startedRunInfo(input: {
1006
1012
  readonly createdAt: string;
1007
1013
  }): NornRunInfo {
1008
1014
  return {
1009
- version: 1,
1015
+ version: 2,
1010
1016
  id: input.id,
1011
1017
  name: input.name,
1012
1018
  path: input.runRoot,
1019
+ paths: { project: input.projectRoot, workspace: join(input.runRoot, "current", "workspace") },
1013
1020
  entrypointWorkflowId: input.workflow.id,
1014
1021
  currentWorkflowId: input.workflow.id,
1015
1022
  status: "running",
@@ -1058,18 +1065,18 @@ async function readStdin(): Promise<string> {
1058
1065
  });
1059
1066
  }
1060
1067
 
1061
- function parseStartRunInput(value: unknown): { readonly params?: unknown; readonly config?: unknown } {
1068
+ function parseStartRunInput(value: unknown): { readonly args?: unknown; readonly config?: unknown } {
1062
1069
  if (value === undefined) return {};
1063
1070
  const input = parseStructuredInputObject("runs start", value);
1064
- assertStructuredInputKeys("runs start", input, ["params", "config"]);
1065
- return { params: input.params, config: input.config };
1071
+ assertStructuredInputKeys("runs start", input, ["args", "config"]);
1072
+ return { args: input.args, config: input.config };
1066
1073
  }
1067
1074
 
1068
- function parseResumeRunInput(value: unknown): { readonly params?: unknown } {
1075
+ function parseResumeRunInput(value: unknown): { readonly args?: unknown } {
1069
1076
  if (value === undefined) return {};
1070
1077
  const input = parseStructuredInputObject("runs resume", value);
1071
- assertStructuredInputKeys("runs resume", input, ["params"]);
1072
- return { params: input.params };
1078
+ assertStructuredInputKeys("runs resume", input, ["args"]);
1079
+ return { args: input.args };
1073
1080
  }
1074
1081
 
1075
1082
  function parseStructuredInputObject(command: string, value: unknown): Record<string, unknown> {
@@ -3,16 +3,15 @@ import { fileURLToPath } from "node:url";
3
3
  import { createInterface } from "node:readline";
4
4
  import type {
5
5
  DeletedNornRunInfo,
6
- NornPluginDiagnostic,
6
+ NornWorkflowDiagnostic,
7
7
  NornProjectInspection,
8
8
  NornWorkflowCatalogInfo,
9
9
  NornWorkflowInspection,
10
10
  NornRunCheckpoint,
11
11
  NornRunInfo,
12
12
  NornRunMetrics,
13
- NornWorkflowStateReader,
14
13
  } from "@vimhead.dev/norn";
15
- import { loadNornProject } from "./plugin-loader.ts";
14
+ import { loadNornProject } from "./workflow-loader.ts";
16
15
  import { NornProjectLoadError } from "./internal/errors.ts";
17
16
  import type { NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
18
17
 
@@ -32,10 +31,9 @@ export type NornClient = {
32
31
  inspect(workflowId: string): Promise<NornWorkflowInspection>;
33
32
  entries(): Promise<readonly NornRegisteredWorkflow[]>;
34
33
  };
35
- readonly state: NornWorkflowStateReader;
36
34
  readonly runs: {
37
- start(input: { readonly workflowId: string; readonly params: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
38
- resume(input: { readonly run: string; readonly params?: unknown }): Promise<NornRunInfo>;
35
+ start(input: { readonly workflowId: string; readonly args: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
36
+ resume(input: { readonly run: string; readonly args?: unknown }): Promise<NornRunInfo>;
39
37
  wait(run: string): Promise<NornRunInfo>;
40
38
  list(): Promise<NornRunInfo[]>;
41
39
  inspect(run: string): Promise<NornRunInfo>;
@@ -61,7 +59,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
61
59
  inspect: async (workflowId) => processRunner.readJson<NornWorkflowInspection>(["workflows", "inspect", workflowId]),
62
60
  entries: async () => (await catalog.load()).workflows,
63
61
  },
64
- state: catalog.state,
65
62
  runs: {
66
63
  start: async (startInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "start", startInput.workflowId], JSON.stringify(runStartInput(startInput)))).run,
67
64
  resume: async (resumeInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "resume", resumeInput.run], runResumeStdin(resumeInput))).run,
@@ -82,10 +79,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
82
79
 
83
80
  class NornWorkflowCatalog {
84
81
  private loaded: ReturnType<typeof loadNornProject> | undefined;
85
- readonly state: NornWorkflowStateReader = {
86
- get: async (state) => (await this.load()).state.get(state),
87
- getOptional: async (state) => (await this.load()).state.getOptional(state),
88
- };
89
82
 
90
83
  constructor(private readonly cwd: string) {}
91
84
 
@@ -107,7 +100,7 @@ class NornProcessRunner {
107
100
 
108
101
  async readJson<T>(args: readonly string[], stdin?: string): Promise<T> {
109
102
  const result = await this.spawn(args, stdin);
110
- const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?: NornPluginDiagnostic[] } };
103
+ const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?: NornWorkflowDiagnostic[] } };
111
104
  if (parsed && typeof parsed === "object" && "error" in parsed) {
112
105
  if (parsed.error?.code === "NORN_PROJECT_INVALID" && Array.isArray(parsed.error.diagnostics)) throw new NornProjectLoadError({ diagnostics: parsed.error.diagnostics });
113
106
  throw new Error(parsed.error?.message ?? "Norn command failed");
@@ -151,10 +144,10 @@ class NornProcessRunner {
151
144
  }
152
145
  }
153
146
 
154
- function runStartInput(input: { readonly params: unknown; readonly configOverride?: unknown }): { readonly params: unknown; readonly config?: unknown } {
155
- return input.configOverride === undefined ? { params: input.params } : { params: input.params, config: input.configOverride };
147
+ function runStartInput(input: { readonly args: unknown; readonly configOverride?: unknown }): { readonly args: unknown; readonly config?: unknown } {
148
+ return input.configOverride === undefined ? { args: input.args } : { args: input.args, config: input.configOverride };
156
149
  }
157
150
 
158
- function runResumeStdin(input: { readonly params?: unknown }): string | undefined {
159
- return input.params === undefined ? undefined : JSON.stringify({ params: input.params });
151
+ function runResumeStdin(input: { readonly args?: unknown }): string | undefined {
152
+ return input.args === undefined ? undefined : JSON.stringify({ args: input.args });
160
153
  }
@@ -24,7 +24,7 @@ export function renderNornDocumentationIntro(input: {
24
24
  "",
25
25
  "Live CLI contracts (argument arrays appended to the runtime argv above):",
26
26
  '- ["workflows", "list"] — currently registered entrypoints.',
27
- '- ["workflows", "inspect", "<workflow-id>"] — current instructions, params schema, and source.',
27
+ '- ["workflows", "inspect", "<workflow-id>"] — current instructions, args schema, and source.',
28
28
  '- ["help"] — command usage.',
29
29
  ].join("\n");
30
30
  }
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
2
2
 
3
3
  export const NORN_GENERATED_BUILD_INFO = {
4
4
  "kind": "npm-registry",
5
- "version": "0.1.0-tip.35359392805.1",
6
- "commit": "f958006ca0ae3752060727c5b76b2ac1a008b103",
5
+ "version": "0.1.0-tip.35436871363.1",
6
+ "commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
7
7
  "packageSpec": "@vimhead.dev/norn-cli@tip",
8
8
  "upgrade": {
9
9
  "supported": false,
@@ -71,12 +71,12 @@ export class NornAgentResponseToolFactory {
71
71
  label: Type.String({ description: "The exact workflow agent label provided in the prompt." }),
72
72
  response: Type.Unknown({ description: "The structured response object requested by the workflow." }),
73
73
  }),
74
- async execute(_toolCallId, params) {
74
+ async execute(_toolCallId, args) {
75
75
  try {
76
- const captured = responseCollector.capture(params.runId, params.label, params.response);
76
+ const captured = responseCollector.capture(args.runId, args.label, args.response);
77
77
  return {
78
78
  content: [{ type: "text", text: `Recorded workflow agent response for ${captured.label}.` }],
79
- details: { valid: true, runId: params.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
79
+ details: { valid: true, runId: args.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
80
80
  terminate: true,
81
81
  };
82
82
  } catch (error) {
@@ -5,6 +5,7 @@ import {
5
5
  createEventBus,
6
6
  type AgentSession,
7
7
  type CreateAgentSessionOptions,
8
+ type ToolDefinition,
8
9
  } from "@earendil-works/pi-coding-agent";
9
10
  import type { NornAgentCreateSessionInput, NornAgentPromptInput, NornAgentRunRawAttempt, NornAgentRunResult, NornAgentSession, NornAgentSessionEvents, NornAgentSinglePromptInput, NornAgentUsage } from "@vimhead.dev/norn";
10
11
  import { AGENT_RESPONSE_TOOL_NAME } from "@vimhead.dev/norn-core/agent-protocol";
@@ -12,7 +13,8 @@ import { inspectSchema } from "@vimhead.dev/norn/schema";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import { mkdir } from "node:fs/promises";
14
15
  import { homedir } from "node:os";
15
- import { isAbsolute, relative, resolve, sep } from "node:path";
16
+ import { resolve } from "node:path";
17
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
16
18
  import { type StaticDecode, type TSchema } from "typebox";
17
19
  import { resolveNornAgentDirectory } from "./agent-directory.ts";
18
20
  import {
@@ -23,19 +25,14 @@ import {
23
25
  import { errorMessage } from "./errors.ts";
24
26
  import { safeFileName } from "./file-names.ts";
25
27
  import type { NornRunLogs } from "./logs.ts";
26
- import { NornSessionResourceBindings } from "./resource-bindings.ts";
27
28
  import type { NornRunLogger } from "./run-log.ts";
28
29
  import { agentUsageFromValue, emptyAgentUsage, totalAgentUsage } from "./usage.ts";
29
30
 
30
31
  const DEFAULT_AGENT_ATTEMPTS = 3;
31
- const DEFAULT_AGENT_TOOL_ALLOWLIST = ["read", "bash", "edit", "write", AGENT_RESPONSE_TOOL_NAME] as const;
32
32
 
33
33
  type NornAgentRunnerInput = {
34
34
  readonly id: string;
35
35
  readonly runRoot: string;
36
- readonly boundaryRoot: string;
37
- readonly boundaryName: string;
38
- readonly cwd: string;
39
36
  readonly signal?: AbortSignal;
40
37
  readonly model?: CreateAgentSessionOptions["model"];
41
38
  readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
@@ -49,7 +46,6 @@ type CreatedNornAgentSessionInput = NornAgentRunnerInput & {
49
46
  readonly label: string;
50
47
  readonly cwd: string;
51
48
  readonly session: AgentSession;
52
- readonly resourceBindings: NornSessionResourceBindings;
53
49
  readonly events: NornAgentSessionEvents;
54
50
  };
55
51
 
@@ -61,7 +57,7 @@ export class NornAgentRunner {
61
57
  }
62
58
 
63
59
  async createSession(agentInput: NornAgentCreateSessionInput): Promise<NornAgentSession> {
64
- const cwd = agentInput.cwd ? this.resolveFromCwd(agentInput.cwd) : this.input.cwd;
60
+ const cwd = requireAbsoluteWorkingDirectory(agentInput.cwd);
65
61
  const sessionDir = resolve(this.input.runRoot, "sessions");
66
62
  await mkdir(sessionDir, { recursive: true });
67
63
 
@@ -82,19 +78,19 @@ export class NornAgentRunner {
82
78
  ...loader.getExtensions().errors.map(error => `Failed to load extension ${error.path}: ${error.error}`),
83
79
  ];
84
80
  if (errors.length > 0) throw new Error(errors.join("\n"));
85
- const resourceBindings = new NornSessionResourceBindings();
81
+ const customTools = agentInput.customTools ?? [];
82
+ assertCustomToolNames({
83
+ tools: customTools,
84
+ reservedNames: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
85
+ ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
86
+ });
86
87
  let session: AgentSession | undefined;
87
88
  try {
88
- await resourceBindings.bind({
89
- adapters: agentInput.resourceAdapters ?? [], runId: this.input.id, label: agentInput.label,
90
- reservedTools: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
91
- ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
92
- });
93
89
  ({ session } = await createAgentSessionFromServices({
94
90
  services,
95
91
  sessionManager: SessionManager.create(cwd, sessionDir),
96
- tools: [...withAgentResponseTool(agentInput.tools), ...resourceBindings.tools.map((tool) => tool.name)],
97
- customTools: [this.responseToolFactory.create(), ...resourceBindings.tools],
92
+ tools: withAgentResponseTool(agentInput.tools),
93
+ customTools: [this.responseToolFactory.create(), ...customTools],
98
94
  model: agentInput.model ?? this.input.model,
99
95
  thinkingLevel: agentInput.thinkingLevel ?? this.input.thinkingLevel,
100
96
  }));
@@ -102,13 +98,12 @@ export class NornAgentRunner {
102
98
  await session.bindExtensions({});
103
99
  await this.input.logger.record({ type: "agent.spawned", label: agentInput.label, cwd });
104
100
  return new CreatedNornAgentSession({
105
- ...this.input, label: agentInput.label, cwd, session, resourceBindings, events: eventBus,
101
+ ...this.input, label: agentInput.label, cwd, session, events: eventBus,
106
102
  });
107
103
  } catch (error) {
108
104
  const errors = [error];
109
105
  try { session?.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
110
- try { await resourceBindings.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
111
- if (errors.length > 1) throw new AggregateError(errors, "Agent creation and resource cleanup failed");
106
+ if (errors.length > 1) throw new AggregateError(errors, "Agent creation and cleanup failed");
112
107
  throw error;
113
108
  }
114
109
  }
@@ -121,15 +116,6 @@ export class NornAgentRunner {
121
116
  await agent.dispose();
122
117
  }
123
118
  }
124
-
125
- private resolveFromCwd(path: string): string {
126
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
127
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
128
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
129
- throw new Error(`Agent cwd escapes ${this.input.boundaryName} isolation: ${path}`);
130
- }
131
- return resolvedPath;
132
- }
133
119
  }
134
120
 
135
121
  function systemPromptOverride(agentInput: NornAgentCreateSessionInput): (() => string) | undefined {
@@ -229,7 +215,6 @@ class CreatedNornAgentSession implements NornAgentSession {
229
215
  const errors: unknown[] = [];
230
216
  try { await this.input.session.abort(); } catch (error) { errors.push(error); }
231
217
  try { this.input.session.dispose(); } catch (error) { errors.push(error); }
232
- try { await this.input.resourceBindings.dispose(); } catch (error) { errors.push(error); }
233
218
  if (errors.length) throw new AggregateError(errors, "Agent disposal failed");
234
219
  await this.input.logger.record({ type: "agent.disposed", label: this.label });
235
220
  }
@@ -360,7 +345,14 @@ function withResponseToolFinalizationInstruction(
360
345
  ].join("\n");
361
346
  }
362
347
 
363
- function withAgentResponseTool(tools: readonly string[] | undefined): string[] {
364
- if (!tools) return [...DEFAULT_AGENT_TOOL_ALLOWLIST];
365
- return Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
348
+ function withAgentResponseTool(tools: readonly string[] | undefined): string[] | undefined {
349
+ return tools === undefined ? undefined : Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
350
+ }
351
+
352
+ function assertCustomToolNames(input: { readonly tools: readonly ToolDefinition[]; readonly reservedNames: readonly string[] }): void {
353
+ const names = new Set(input.reservedNames);
354
+ for (const tool of input.tools) {
355
+ if (names.has(tool.name)) throw new Error(`Custom tool name collision: ${tool.name}`);
356
+ names.add(tool.name);
357
+ }
366
358
  }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
4
4
  import type { WriteStream } from "node:fs";
5
5
  import type { NornCommandRunInput, NornCommandRunResult, NornLogRef } from "@vimhead.dev/norn";
6
6
  import { errorMessage } from "./errors.ts";
@@ -9,9 +9,6 @@ import type { NornRunLogs } from "./logs.ts";
9
9
  import type { NornRunLogger } from "./run-log.ts";
10
10
 
11
11
  type NornCommandRunnerInput = {
12
- readonly boundaryRoot: string;
13
- readonly boundaryName: string;
14
- readonly cwd: string;
15
12
  readonly signal?: AbortSignal;
16
13
  readonly logs: NornRunLogs;
17
14
  readonly logger: NornRunLogger;
@@ -21,7 +18,7 @@ export class NornCommandRunner {
21
18
  constructor(private readonly input: NornCommandRunnerInput) {}
22
19
 
23
20
  async run(commandInput: NornCommandRunInput): Promise<NornCommandRunResult> {
24
- const cwd = this.resolveFromCwd(commandInput.cwd ?? this.input.cwd);
21
+ const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
25
22
  const startedAtMs = Date.now();
26
23
  const invocationId = randomUUID();
27
24
  const stdoutRef = commandLog({ label: commandInput.label, invocationId, stream: "stdout" });
@@ -67,15 +64,6 @@ export class NornCommandRunner {
67
64
  });
68
65
  return commandResult;
69
66
  }
70
-
71
- private resolveFromCwd(path: string): string {
72
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
73
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
74
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
75
- throw new Error(`Command cwd escapes ${this.input.boundaryName} isolation: ${path}`);
76
- }
77
- return resolvedPath;
78
- }
79
67
  }
80
68
 
81
69
  type SpawnCommandInput = {