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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. package/package.json +2 -2
@@ -1,47 +1,102 @@
1
1
  import { main as runPi } from "@earendil-works/pi-coding-agent";
2
- import { type DeletedNornRunInfo, type NornAnyWorkflowDeclaration, type NornRunInfo } from "@vimhead.dev/norn";
2
+ import {
3
+ type DeletedNornRunInfo,
4
+ type NornAnyWorkflowDeclaration,
5
+ type NornRunInfo,
6
+ } from "@vimhead.dev/norn";
3
7
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
8
  import { spawn } from "node:child_process";
5
9
  import { createHash, randomUUID } from "node:crypto";
6
- import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
+ import {
11
+ chmod,
12
+ mkdir,
13
+ readFile,
14
+ rename,
15
+ rm,
16
+ stat,
17
+ writeFile,
18
+ } from "node:fs/promises";
7
19
  import { homedir } from "node:os";
8
20
  import { dirname, isAbsolute, join, resolve } from "node:path";
9
21
  import { setTimeout as delay } from "node:timers/promises";
10
22
  import { fileURLToPath } from "node:url";
11
23
  import { Value } from "typebox/value";
12
- import { NORN_BUILD_INFO, type NornBuildInfo, type NornGithubReleaseBinaryBuildInfo } from "./build-info.ts";
13
- import { renderNornDocumentationIntro, resolveDocumentationCacheRoot, resolveNornDocumentation, type NornDocumentationSource } from "./documentation.ts";
24
+ import {
25
+ NORN_BUILD_INFO,
26
+ type NornBuildInfo,
27
+ type NornGithubReleaseBinaryBuildInfo,
28
+ } from "./build-info.ts";
29
+ import {
30
+ renderNornDocumentationIntro,
31
+ resolveDocumentationCacheRoot,
32
+ resolveNornDocumentation,
33
+ type NornDocumentationSource,
34
+ } from "./documentation.ts";
14
35
  import { NornEngine } from "./internal/engine.ts";
15
36
  import { resolveNornAgentDirectory } from "./internal/agent-directory.ts";
16
- import { errorMessage, NornProjectLoadError, NornRunStoppedError } from "./internal/errors.ts";
17
- import { clearRunResumeRequest, readRunLaunchRequest, readRunResumeRequest, writeRunLaunchRequest, writeRunResumeRequest, type NornRunResumeRequest } from "./internal/launch-request.ts";
37
+ import {
38
+ errorMessage,
39
+ NornProjectLoadError,
40
+ NornRunStoppedError,
41
+ } from "./internal/errors.ts";
42
+ import {
43
+ clearRunResumeRequest,
44
+ readRunLaunchRequest,
45
+ readRunResumeRequest,
46
+ writeRunLaunchRequest,
47
+ writeRunResumeRequest,
48
+ type NornRunResumeRequest,
49
+ } from "./internal/launch-request.ts";
18
50
  import { readRunMetrics } from "./internal/metrics.ts";
19
51
  import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.ts";
20
52
  import { generateRunName } from "./internal/run-names.ts";
21
- import { assertRunVersion, getRunInfo, listRuns, mergeInterruptedWorkflowArgs, resolveRunRoot } from "./internal/run-state.ts";
53
+ import {
54
+ assertRunVersion,
55
+ getRunInfo,
56
+ listRuns,
57
+ mergeInterruptedWorkflowArgs,
58
+ resolveRunRoot,
59
+ } from "./internal/run-state.ts";
22
60
  import { NornRunStore } from "./internal/run-store.ts";
23
61
  import { prepareRunWorkerDirectory } from "./internal/worker-directory.ts";
24
- import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./workflow-loader.ts";
62
+ import {
63
+ discoverNornProject,
64
+ findNornProject,
65
+ inspectNornWorkflow,
66
+ loadNornProject,
67
+ NORN_PROJECT_FILE_NAME,
68
+ } from "./workflow-loader.ts";
25
69
 
26
70
  const RUNS_ROOT = join(".norn", "runs");
27
71
  const RUN_WAIT_INTERVAL_MS = 1000;
28
- const CLI_DESCRIPTION = "Norn is a harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents. Build workflows with the Norn SDK; use this JSON-native CLI to discover workflows, start or resume runs, inspect evidence, and manage the installed runtime.";
72
+ const CLI_DESCRIPTION =
73
+ "Norn is a harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents. Build workflows with the Norn SDK; use this JSON-native CLI to discover workflows, start or resume runs, inspect evidence, and manage the installed runtime.";
29
74
 
30
75
  const COMMANDS: readonly CliCommand[] = [
31
76
  {
32
77
  id: "pi",
33
78
  path: ["pi"],
34
- description: "Run Norn's bundled Pi CLI for interactive authentication, provider packages, model configuration, or direct Pi use. No Norn project is required.",
79
+ description:
80
+ "Run Norn's bundled Pi CLI for interactive authentication, provider packages, model configuration, or direct Pi use. No Norn project is required.",
35
81
  usage: "norn pi [pi arguments...]",
36
- arguments: ["pi arguments: forwarded unchanged; use norn pi --help for Pi's CLI contract"],
37
- output: "Pi's native terminal, text, JSON, or RPC output and exit status; not wrapped in Norn JSON.",
38
- examples: ["norn pi", "norn pi install npm:pi-cursor-sdk", "norn pi --list-models cursor", "norn pi --help"],
39
- execute: args => runPi([...args]),
82
+ arguments: [
83
+ "pi arguments: forwarded unchanged; use norn pi --help for Pi's CLI contract",
84
+ ],
85
+ output:
86
+ "Pi's native terminal, text, JSON, or RPC output and exit status; not wrapped in Norn JSON.",
87
+ examples: [
88
+ "norn pi",
89
+ "norn pi install npm:pi-cursor-sdk",
90
+ "norn pi --list-models cursor",
91
+ "norn pi --help",
92
+ ],
93
+ execute: (args) => runPi([...args]),
40
94
  },
41
95
  {
42
96
  id: "commands.list",
43
97
  path: ["commands", "list"],
44
- description: "Use when an agent or human needs machine-readable Norn CLI command metadata.",
98
+ description:
99
+ "Use when an agent or human needs machine-readable Norn CLI command metadata.",
45
100
  usage: "norn commands list [--all]",
46
101
  options: ["--all: include hidden internal commands"],
47
102
  output: "JSON object with command metadata under commands.",
@@ -51,7 +106,8 @@ const COMMANDS: readonly CliCommand[] = [
51
106
  {
52
107
  id: "commands.inspect",
53
108
  path: ["commands", "inspect"],
54
- description: "Use when an agent or human needs the usage contract for one Norn CLI command.",
109
+ description:
110
+ "Use when an agent or human needs the usage contract for one Norn CLI command.",
55
111
  usage: "norn commands inspect <command-id>",
56
112
  arguments: ["command-id: command metadata id such as runs.start"],
57
113
  output: "JSON object with one command metadata object under command.",
@@ -61,46 +117,67 @@ const COMMANDS: readonly CliCommand[] = [
61
117
  {
62
118
  id: "help",
63
119
  path: ["help"],
64
- description: "Use when reading JSON help for all Norn commands, one command group, or one command.",
120
+ description:
121
+ "Use when reading JSON help for all Norn commands, one command group, or one command.",
65
122
  usage: "norn help [command-or-group]",
66
- arguments: ["command-or-group: optional command path such as runs or runs start"],
123
+ arguments: [
124
+ "command-or-group: optional command path such as runs or runs start",
125
+ ],
67
126
  output: "JSON object with help metadata under help.",
68
- examples: ["norn help", "norn help runs", "norn help runs start", "norn --help"],
127
+ examples: [
128
+ "norn help",
129
+ "norn help runs",
130
+ "norn help runs start",
131
+ "norn --help",
132
+ ],
69
133
  execute: (args) => writeCliHelp(args),
70
134
  },
71
135
  {
72
136
  id: "docs.inspect",
73
137
  path: ["docs", "inspect"],
74
- description: "Resolve version-matched local documentation and examples. Standalone binaries extract bundled assets into a verified build-specific cache; no project or network is required.",
138
+ description:
139
+ "Resolve version-matched local documentation and examples. Standalone binaries extract bundled assets into a verified build-specific cache; no project or network is required.",
75
140
  usage: "norn docs inspect",
76
- output: "JSON object under documentation with storage, version, commit, assetDigest, and absolute paths. NORN_DOCS_CACHE_DIR overrides the binary cache directory.",
141
+ output:
142
+ "JSON object under documentation with storage, version, commit, assetDigest, and absolute paths. NORN_DOCS_CACHE_DIR overrides the binary cache directory.",
77
143
  examples: ["norn docs inspect"],
78
144
  execute: async (args, documentationSource) => {
79
145
  assertNoExtraArgs("docs inspect", args);
80
- writeJson({ documentation: await resolveCliDocumentation(documentationSource) });
146
+ writeJson({
147
+ documentation: await resolveCliDocumentation(documentationSource),
148
+ });
81
149
  },
82
150
  },
83
151
  {
84
152
  id: "docs.intro",
85
153
  path: ["docs", "intro"],
86
- description: "Produce a compact introduction to Norn authoring with this runtime's invocation and matching local documentation pointers. Does not load a project, list workflows, or deliver context to agents.",
154
+ description:
155
+ "Produce a compact introduction to Norn authoring with this runtime's invocation and matching local documentation pointers. Does not load a project, list workflows, or deliver context to agents.",
87
156
  usage: "norn docs intro",
88
- output: "JSON object with introduction text under intro. Uses the same asset resolution and NORN_DOCS_CACHE_DIR override as docs inspect.",
157
+ output:
158
+ "JSON object with introduction text under intro. Uses the same asset resolution and NORN_DOCS_CACHE_DIR override as docs inspect.",
89
159
  examples: ["norn docs intro"],
90
160
  execute: async (args, documentationSource) => {
91
161
  assertNoExtraArgs("docs intro", args);
92
- writeJson({ intro: renderNornDocumentationIntro({
93
- documentation: await resolveCliDocumentation(documentationSource),
94
- invocation: documentationSource.kind === "embedded"
95
- ? [process.execPath]
96
- : [process.execPath, fileURLToPath(new URL("../bin/norn.mjs", import.meta.url))],
97
- }) });
162
+ writeJson({
163
+ intro: renderNornDocumentationIntro({
164
+ documentation: await resolveCliDocumentation(documentationSource),
165
+ invocation:
166
+ documentationSource.kind === "embedded"
167
+ ? [process.execPath]
168
+ : [
169
+ process.execPath,
170
+ fileURLToPath(new URL("../bin/norn.mjs", import.meta.url)),
171
+ ],
172
+ }),
173
+ });
98
174
  },
99
175
  },
100
176
  {
101
177
  id: "project.init",
102
178
  path: ["project", "init"],
103
- description: "Use when creating a self-contained Norn project config and local run state directory in the current directory.",
179
+ description:
180
+ "Use when creating a self-contained Norn project config and local run state directory in the current directory.",
104
181
  usage: "norn project init",
105
182
  output: "JSON object with initialized project metadata under project.",
106
183
  examples: ["norn project init"],
@@ -112,9 +189,11 @@ const COMMANDS: readonly CliCommand[] = [
112
189
  {
113
190
  id: "project.inspect",
114
191
  path: ["project", "inspect"],
115
- description: "Use when discovering the active Norn project, configuration, and workflow sources.",
192
+ description:
193
+ "Use when discovering the active Norn project, configuration, and workflow sources.",
116
194
  usage: "norn project inspect",
117
- output: "JSON object with project metadata under project, isComplete, and registration diagnostics. Incomplete discovery does not permit execution.",
195
+ output:
196
+ "JSON object with project metadata under project, isComplete, and registration diagnostics. Incomplete discovery does not permit execution.",
118
197
  examples: ["norn project inspect"],
119
198
  execute: async (args) => {
120
199
  assertNoExtraArgs("project inspect", args);
@@ -124,23 +203,35 @@ const COMMANDS: readonly CliCommand[] = [
124
203
  {
125
204
  id: "workflows.list",
126
205
  path: ["workflows", "list"],
127
- description: "Use when selecting a Norn workflow for a user task; defaults to entrypoint workflows.",
206
+ description:
207
+ "Use when selecting a Norn workflow for a user task; defaults to entrypoint workflows.",
128
208
  usage: "norn workflows list [--entrypoints|--all]",
129
- options: ["--entrypoints: list entrypoint workflows", "--all: include internal workflow steps"],
130
- output: "JSON object with workflow summaries under workflows, isComplete, and registration diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
209
+ options: [
210
+ "--entrypoints: list entrypoint workflows",
211
+ "--all: include internal workflow steps",
212
+ ],
213
+ output:
214
+ "JSON object with workflow summaries under workflows, isComplete, and registration diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
131
215
  examples: ["norn workflows list", "norn workflows list --all"],
132
216
  execute: listWorkflows,
133
217
  },
134
218
  {
135
219
  id: "workflows.inspect",
136
220
  path: ["workflows", "inspect"],
137
- description: "Use when reading a workflow's instructions, args schema, gate contract, and registration source before starting or editing it.",
221
+ description:
222
+ "Use when reading a workflow's instructions, args schema, gate contract, and registration source before starting or editing it.",
138
223
  usage: "norn workflows inspect <workflow-id>",
139
224
  arguments: ["workflow-id: fully qualified workflow id"],
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.",
225
+ output:
226
+ "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.",
141
227
  examples: ["norn workflows inspect example.plan"],
142
228
  execute: async (args) => {
143
- const workflowId = requiredArg("workflows inspect", args, 0, "workflow id");
229
+ const workflowId = requiredArg(
230
+ "workflows inspect",
231
+ args,
232
+ 0,
233
+ "workflow id",
234
+ );
144
235
  assertNoExtraArgs("workflows inspect", args.slice(1));
145
236
  await inspectWorkflow(workflowId);
146
237
  },
@@ -148,12 +239,16 @@ const COMMANDS: readonly CliCommand[] = [
148
239
  {
149
240
  id: "runs.start",
150
241
  path: ["runs", "start"],
151
- description: "Use when starting a Norn workflow run after the workflow id and args are known.",
242
+ description:
243
+ "Use when starting a Norn workflow run after the workflow id and args are known.",
152
244
  usage: "norn runs start <workflow-id>",
153
245
  arguments: ["workflow-id: fully qualified workflow id to start"],
154
- stdin: "Optional JSON object: {\"args\":{...},\"config\":{\"workflowOrScopeId\":{...}}}.",
246
+ stdin:
247
+ 'Optional JSON object: {"args":{...},"config":{"workflowOrScopeId":{...}}}.',
155
248
  output: "JSON object with started run info under run.",
156
- examples: ["printf '{\"args\":{\"task\":\"Add tests\"}}' | norn runs start example.plan"],
249
+ examples: [
250
+ 'printf \'{"args":{"task":"Add tests"}}\' | norn runs start example.plan',
251
+ ],
157
252
  execute: async (args) => {
158
253
  const workflowId = requiredArg("runs start", args, 0, "workflow id");
159
254
  await startRun(workflowId, args.slice(1));
@@ -162,12 +257,16 @@ const COMMANDS: readonly CliCommand[] = [
162
257
  {
163
258
  id: "runs.resume",
164
259
  path: ["runs", "resume"],
165
- description: "Use when resuming an interrupted gate or a checkpoint restored for retry.",
260
+ description:
261
+ "Use when resuming an interrupted gate or a checkpoint restored for retry.",
166
262
  usage: "norn runs resume <run>",
167
263
  arguments: ["run: run id, generated name, or run path"],
168
- stdin: "Optional JSON object: {\"args\":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept args.",
264
+ stdin:
265
+ 'Optional JSON object: {"args":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept args.',
169
266
  output: "JSON object with resumed run info under run.",
170
- examples: ["printf '{\"args\":{\"decision\":\"accept\"}}' | norn runs resume quiet-river-lantern"],
267
+ examples: [
268
+ 'printf \'{"args":{"decision":"accept"}}\' | norn runs resume quiet-river-lantern',
269
+ ],
171
270
  execute: async (args) => {
172
271
  const run = requiredArg("runs resume", args, 0, "run");
173
272
  await resumeRun(run, args.slice(1));
@@ -176,7 +275,8 @@ const COMMANDS: readonly CliCommand[] = [
176
275
  {
177
276
  id: "runs.wait",
178
277
  path: ["runs", "wait"],
179
- description: "Use when waiting for a Norn run to finish, fail, interrupt, or become unhealthy.",
278
+ description:
279
+ "Use when waiting for a Norn run to finish, fail, interrupt, or become unhealthy.",
180
280
  usage: "norn runs wait <run>",
181
281
  arguments: ["run: run id, generated name, or run path"],
182
282
  output: "JSON object with final or current run info under run.",
@@ -202,7 +302,8 @@ const COMMANDS: readonly CliCommand[] = [
202
302
  {
203
303
  id: "runs.inspect",
204
304
  path: ["runs", "inspect"],
205
- description: "Use when reading status, health, current workflow, interruption, and outcome details for one Norn run.",
305
+ description:
306
+ "Use when reading status, health, current workflow, interruption, and outcome details for one Norn run.",
206
307
  usage: "norn runs inspect <run>",
207
308
  arguments: ["run: run id, generated name, or run path"],
208
309
  output: "JSON object with run details under run.",
@@ -216,7 +317,8 @@ const COMMANDS: readonly CliCommand[] = [
216
317
  {
217
318
  id: "runs.checkpoints",
218
319
  path: ["runs", "checkpoints"],
219
- description: "Use when finding rollback points before retrying or repairing a Norn run.",
320
+ description:
321
+ "Use when finding rollback points before retrying or repairing a Norn run.",
220
322
  usage: "norn runs checkpoints <run>",
221
323
  arguments: ["run: run id, generated name, or run path"],
222
324
  output: "JSON object with checkpoints under checkpoints.",
@@ -226,13 +328,16 @@ const COMMANDS: readonly CliCommand[] = [
226
328
  assertNoExtraArgs("runs checkpoints", args.slice(1));
227
329
  const project = await findNornProject(process.cwd());
228
330
  const runRoot = await resolveRunRoot(project.projectRoot, run);
229
- writeJson({ checkpoints: await (await NornRunStore.open(runRoot)).listCheckpoints() });
331
+ writeJson({
332
+ checkpoints: await (await NornRunStore.open(runRoot)).listCheckpoints(),
333
+ });
230
334
  },
231
335
  },
232
336
  {
233
337
  id: "runs.metrics",
234
338
  path: ["runs", "metrics"],
235
- description: "Use when measuring workflow, agent, command, token, and cost totals for a Norn run.",
339
+ description:
340
+ "Use when measuring workflow, agent, command, token, and cost totals for a Norn run.",
236
341
  usage: "norn runs metrics <run>",
237
342
  arguments: ["run: run id, generated name, or run path"],
238
343
  output: "JSON object with metrics under metrics.",
@@ -248,12 +353,16 @@ const COMMANDS: readonly CliCommand[] = [
248
353
  {
249
354
  id: "runs.logs",
250
355
  path: ["runs", "logs"],
251
- description: "Use when streaming or reading chronological JSON events for a Norn run.",
356
+ description:
357
+ "Use when streaming or reading chronological JSON events for a Norn run.",
252
358
  usage: "norn runs logs <run> [--follow]",
253
359
  arguments: ["run: run id, generated name, or run path"],
254
360
  options: ["--follow: continue streaming until the run stops"],
255
361
  output: "JSON Lines stream of run events.",
256
- examples: ["norn runs logs quiet-river-lantern", "norn runs logs quiet-river-lantern --follow"],
362
+ examples: [
363
+ "norn runs logs quiet-river-lantern",
364
+ "norn runs logs quiet-river-lantern --follow",
365
+ ],
257
366
  execute: async (args) => {
258
367
  const run = requiredArg("runs logs", args, 0, "run");
259
368
  const logArgs = args.slice(1);
@@ -264,9 +373,13 @@ const COMMANDS: readonly CliCommand[] = [
264
373
  {
265
374
  id: "runs.rollback",
266
375
  path: ["runs", "rollback"],
267
- description: "Use when restoring a failed or stopped run to a checkpoint and marking it pending resume.",
376
+ description:
377
+ "Use when restoring a failed or stopped run to a checkpoint and marking it pending resume.",
268
378
  usage: "norn runs rollback <run> <checkpoint-id>",
269
- arguments: ["run: run id, generated name, or run path", "checkpoint-id: checkpoint id from runs.checkpoints"],
379
+ arguments: [
380
+ "run: run id, generated name, or run path",
381
+ "checkpoint-id: checkpoint id from runs.checkpoints",
382
+ ],
270
383
  output: "JSON object with rolled-back run info under run.",
271
384
  examples: ["norn runs rollback quiet-river-lantern checkpoint-1"],
272
385
  execute: async (args) => {
@@ -277,7 +390,8 @@ const COMMANDS: readonly CliCommand[] = [
277
390
  {
278
391
  id: "runs.stop",
279
392
  path: ["runs", "stop"],
280
- description: "Use when stopping a running Norn execution while preserving its dirty state for inspection or rollback.",
393
+ description:
394
+ "Use when stopping a running Norn execution while preserving its dirty state for inspection or rollback.",
281
395
  usage: "norn runs stop <run>",
282
396
  arguments: ["run: run id, generated name, or run path"],
283
397
  output: "JSON object with updated run info under run.",
@@ -291,7 +405,8 @@ const COMMANDS: readonly CliCommand[] = [
291
405
  {
292
406
  id: "runs.kill",
293
407
  path: ["runs", "kill"],
294
- description: "Use when force-stopping a Norn execution process that did not stop politely.",
408
+ description:
409
+ "Use when force-stopping a Norn execution process that did not stop politely.",
295
410
  usage: "norn runs kill <run>",
296
411
  arguments: ["run: run id, generated name, or run path"],
297
412
  output: "JSON object with updated run info under run.",
@@ -305,7 +420,8 @@ const COMMANDS: readonly CliCommand[] = [
305
420
  {
306
421
  id: "runs.delete",
307
422
  path: ["runs", "delete"],
308
- description: "Use when deleting an inactive Norn run directory after evidence is no longer needed.",
423
+ description:
424
+ "Use when deleting an inactive Norn run directory after evidence is no longer needed.",
309
425
  usage: "norn runs delete <run>",
310
426
  arguments: ["run: run id, generated name, or run path"],
311
427
  output: "JSON object with deleted run identity under deleted.",
@@ -319,10 +435,12 @@ const COMMANDS: readonly CliCommand[] = [
319
435
  {
320
436
  id: "execute-run",
321
437
  path: ["execute-run"],
322
- description: "Use internally when executing a previously launched detached run request.",
438
+ description:
439
+ "Use internally when executing a previously launched detached run request.",
323
440
  usage: "norn execute-run <run-id>",
324
441
  arguments: ["run-id: internal run id"],
325
- output: "No stable stdout contract; execution state is persisted in the run directory.",
442
+ output:
443
+ "No stable stdout contract; execution state is persisted in the run directory.",
326
444
  hidden: true,
327
445
  examples: ["norn execute-run 00000000-0000-0000-0000-000000000000"],
328
446
  execute: async (args) => {
@@ -334,19 +452,25 @@ const COMMANDS: readonly CliCommand[] = [
334
452
  {
335
453
  id: "upgrade",
336
454
  path: ["upgrade"],
337
- description: "Use when upgrading this Norn CLI installation according to explicit build metadata.",
455
+ description:
456
+ "Use when upgrading this Norn CLI installation according to explicit build metadata.",
338
457
  usage: "norn upgrade [--dry-run]",
339
- options: ["--dry-run: report the upgrade plan without changing files or running installers"],
340
- output: "JSON object with upgrade status, plan, or unsupported reason under upgrade.",
458
+ options: [
459
+ "--dry-run: report the upgrade plan without changing files or running installers",
460
+ ],
461
+ output:
462
+ "JSON object with upgrade status, plan, or unsupported reason under upgrade.",
341
463
  examples: ["norn upgrade --dry-run", "norn upgrade"],
342
464
  execute: upgradeNorn,
343
465
  },
344
466
  {
345
467
  id: "version",
346
468
  path: ["version"],
347
- description: "Use when checking the installed Norn CLI version and explicit build metadata.",
469
+ description:
470
+ "Use when checking the installed Norn CLI version and explicit build metadata.",
348
471
  usage: "norn version",
349
- output: "JSON object with package version under version and build metadata under build.",
472
+ output:
473
+ "JSON object with package version under version and build metadata under build.",
350
474
  examples: ["norn version", "norn --version"],
351
475
  execute: async (args) => {
352
476
  assertNoExtraArgs("version", args);
@@ -386,7 +510,8 @@ const HUMAN_COMMAND_SUMMARIES: Readonly<Record<string, string>> = {
386
510
  "commands.list": "List machine-readable command metadata.",
387
511
  "commands.inspect": "Inspect one command's machine-readable contract.",
388
512
  "docs.inspect": "Locate matching documentation and examples offline.",
389
- "docs.intro": "Produce a compact authoring introduction with local documentation pointers.",
513
+ "docs.intro":
514
+ "Produce a compact authoring introduction with local documentation pointers.",
390
515
  "workflows.list": "List Norn workflows.",
391
516
  "workflows.inspect": "Inspect a workflow schema and source.",
392
517
  "runs.start": "Start a workflow run.",
@@ -409,7 +534,10 @@ const HUMAN_COMMAND_SUMMARIES: Readonly<Record<string, string>> = {
409
534
  help: "Show concise command help.",
410
535
  };
411
536
 
412
- export async function main(args: readonly string[], documentationSource: NornDocumentationSource): Promise<void> {
537
+ export async function main(
538
+ args: readonly string[],
539
+ documentationSource: NornDocumentationSource,
540
+ ): Promise<void> {
413
541
  if (args[0] === "pi") {
414
542
  await runPi([...args.slice(1)]);
415
543
  return;
@@ -417,15 +545,23 @@ export async function main(args: readonly string[], documentationSource: NornDoc
417
545
  try {
418
546
  await runCommand(args, documentationSource);
419
547
  } catch (error) {
420
- writeJson({ error: {
421
- code: errorCode(error), message: errorMessage(error),
422
- ...(error instanceof NornProjectLoadError ? { isComplete: error.isComplete, diagnostics: error.diagnostics } : {}),
423
- } });
548
+ writeJson({
549
+ error: {
550
+ code: errorCode(error),
551
+ message: errorMessage(error),
552
+ ...(error instanceof NornProjectLoadError
553
+ ? { isComplete: error.isComplete, diagnostics: error.diagnostics }
554
+ : {}),
555
+ },
556
+ });
424
557
  process.exitCode = 1;
425
558
  }
426
559
  }
427
560
 
428
- async function runCommand(args: readonly string[], documentationSource: NornDocumentationSource): Promise<void> {
561
+ async function runCommand(
562
+ args: readonly string[],
563
+ documentationSource: NornDocumentationSource,
564
+ ): Promise<void> {
429
565
  if (args.length === 0) {
430
566
  writeCliHelp([]);
431
567
  return;
@@ -455,7 +591,10 @@ type CliCommand = {
455
591
  readonly output: string;
456
592
  readonly examples: readonly string[];
457
593
  readonly hidden?: true;
458
- readonly execute: (args: readonly string[], documentationSource: NornDocumentationSource) => Promise<void> | void;
594
+ readonly execute: (
595
+ args: readonly string[],
596
+ documentationSource: NornDocumentationSource,
597
+ ) => Promise<void> | void;
459
598
  };
460
599
 
461
600
  type CliCommandInfo = Omit<CliCommand, "execute">;
@@ -463,7 +602,11 @@ type CliCommandInfo = Omit<CliCommand, "execute">;
463
602
  async function listCliCommands(args: readonly string[]): Promise<void> {
464
603
  assertKnownFlags("commands list", args, ["--all"]);
465
604
  const shouldIncludeHidden = args.includes("--all");
466
- writeJson({ commands: COMMANDS.filter((command) => shouldIncludeHidden || !command.hidden).map(cliCommandInfo) });
605
+ writeJson({
606
+ commands: COMMANDS.filter(
607
+ (command) => shouldIncludeHidden || !command.hidden,
608
+ ).map(cliCommandInfo),
609
+ });
467
610
  }
468
611
 
469
612
  async function inspectCliCommand(args: readonly string[]): Promise<void> {
@@ -505,12 +648,20 @@ function writeCliHelp(path: readonly string[]): void {
505
648
  process.stdout.write(renderCommandHelp(command));
506
649
  return;
507
650
  }
508
- const groupCommands = sortedCommandsForHelp(visibleCommands().filter((candidate) => path.length === 0 || startsWithPath(candidate.path, path)));
509
- if (groupCommands.length === 0) throw new Error(`Unknown norn help topic: ${path.join(" ")}`);
651
+ const groupCommands = sortedCommandsForHelp(
652
+ visibleCommands().filter(
653
+ (candidate) => path.length === 0 || startsWithPath(candidate.path, path),
654
+ ),
655
+ );
656
+ if (groupCommands.length === 0)
657
+ throw new Error(`Unknown norn help topic: ${path.join(" ")}`);
510
658
  process.stdout.write(renderGroupHelp(path, groupCommands));
511
659
  }
512
660
 
513
- function renderGroupHelp(path: readonly string[], commands: readonly CliCommand[]): string {
661
+ function renderGroupHelp(
662
+ path: readonly string[],
663
+ commands: readonly CliCommand[],
664
+ ): string {
514
665
  return [
515
666
  "Norn",
516
667
  "",
@@ -552,33 +703,59 @@ function renderCommandHelp(command: CliCommand): string {
552
703
  function renderCommandSummary(commands: readonly CliCommand[]): string {
553
704
  const commandNames = commands.map((command) => command.path.join(" "));
554
705
  const width = Math.max(...commandNames.map((name) => name.length));
555
- return commands.map((command, index) => ` ${commandNames[index].padEnd(width)} ${commandHumanSummary(command)}`).join("\n");
556
- }
557
-
558
- function helpSection(title: string, lines: readonly string[]): readonly string[] {
706
+ return commands
707
+ .map(
708
+ (command, index) =>
709
+ ` ${commandNames[index].padEnd(width)} ${commandHumanSummary(command)}`,
710
+ )
711
+ .join("\n");
712
+ }
713
+
714
+ function helpSection(
715
+ title: string,
716
+ lines: readonly string[],
717
+ ): readonly string[] {
559
718
  return ["", `${title}:`, ...lines.map((line) => ` ${line}`)];
560
719
  }
561
720
 
562
721
  function groupHelpUsage(path: readonly string[]): readonly string[] {
563
- if (path.length === 0) return ["norn <command> [args]", "norn help <command>", "norn --version"];
722
+ if (path.length === 0)
723
+ return ["norn <command> [args]", "norn help <command>", "norn --version"];
564
724
  const prefix = `norn ${path.join(" ")}`;
565
- return [`${prefix} <command> [args]`, `norn help ${path.join(" ")} <command>`, "norn --version"];
725
+ return [
726
+ `${prefix} <command> [args]`,
727
+ `norn help ${path.join(" ")} <command>`,
728
+ "norn --version",
729
+ ];
566
730
  }
567
731
 
568
732
  function findCliCommand(args: readonly string[]): CliCommand | undefined {
569
- return sortedCommandsByPathLength(COMMANDS).find((command) => startsWithPath(args, command.path));
733
+ return sortedCommandsByPathLength(COMMANDS).find((command) =>
734
+ startsWithPath(args, command.path),
735
+ );
570
736
  }
571
737
 
572
738
  function findExactCliCommand(path: readonly string[]): CliCommand | undefined {
573
- return COMMANDS.find((command) => command.path.length === path.length && startsWithPath(command.path, path));
739
+ return COMMANDS.find(
740
+ (command) =>
741
+ command.path.length === path.length && startsWithPath(command.path, path),
742
+ );
574
743
  }
575
744
 
576
- function sortedCommandsByPathLength(commands: readonly CliCommand[]): readonly CliCommand[] {
577
- return [...commands].sort((left, right) => right.path.length - left.path.length);
745
+ function sortedCommandsByPathLength(
746
+ commands: readonly CliCommand[],
747
+ ): readonly CliCommand[] {
748
+ return [...commands].sort(
749
+ (left, right) => right.path.length - left.path.length,
750
+ );
578
751
  }
579
752
 
580
- function sortedCommandsForHelp(commands: readonly CliCommand[]): readonly CliCommand[] {
581
- return [...commands].sort((left, right) => commandHelpOrder(left) - commandHelpOrder(right));
753
+ function sortedCommandsForHelp(
754
+ commands: readonly CliCommand[],
755
+ ): readonly CliCommand[] {
756
+ return [...commands].sort(
757
+ (left, right) => commandHelpOrder(left) - commandHelpOrder(right),
758
+ );
582
759
  }
583
760
 
584
761
  function commandHelpOrder(command: CliCommand): number {
@@ -594,7 +771,10 @@ function visibleCommands(): readonly CliCommand[] {
594
771
  return COMMANDS.filter((command) => !command.hidden);
595
772
  }
596
773
 
597
- function startsWithPath(value: readonly string[], path: readonly string[]): boolean {
774
+ function startsWithPath(
775
+ value: readonly string[],
776
+ path: readonly string[],
777
+ ): boolean {
598
778
  return path.every((segment, index) => value[index] === segment);
599
779
  }
600
780
 
@@ -602,21 +782,32 @@ async function resolveCliDocumentation(source: NornDocumentationSource) {
602
782
  return resolveNornDocumentation({
603
783
  source,
604
784
  build: { ...NORN_BUILD_INFO, version: await readPackageVersion() },
605
- cacheRoot: resolveDocumentationCacheRoot({ platform: process.platform, home: homedir(), environment: process.env }),
785
+ cacheRoot: resolveDocumentationCacheRoot({
786
+ platform: process.platform,
787
+ home: homedir(),
788
+ environment: process.env,
789
+ }),
606
790
  });
607
791
  }
608
792
 
609
- async function versionInfo(): Promise<{ readonly version: string; readonly build: NornBuildInfo }> {
793
+ async function versionInfo(): Promise<{
794
+ readonly version: string;
795
+ readonly build: NornBuildInfo;
796
+ }> {
610
797
  return { version: await readPackageVersion(), build: NORN_BUILD_INFO };
611
798
  }
612
799
 
613
800
  async function readPackageVersion(): Promise<string> {
614
801
  try {
615
- const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as { version?: unknown };
616
- if (typeof packageJson.version !== "string") throw new Error("Invalid Norn package version");
802
+ const packageJson = JSON.parse(
803
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
804
+ ) as { version?: unknown };
805
+ if (typeof packageJson.version !== "string")
806
+ throw new Error("Invalid Norn package version");
617
807
  return packageJson.version;
618
808
  } catch (error) {
619
- if (isNodeError(error) && error.code === "ENOENT") return NORN_BUILD_INFO.version;
809
+ if (isNodeError(error) && error.code === "ENOENT")
810
+ return NORN_BUILD_INFO.version;
620
811
  throw error;
621
812
  }
622
813
  }
@@ -627,13 +818,21 @@ async function upgradeNorn(args: readonly string[]): Promise<void> {
627
818
  const currentVersion = await readPackageVersion();
628
819
  const build = NORN_BUILD_INFO;
629
820
  if (build.kind !== "github-release-binary") {
630
- writeJson({ upgrade: unsupportedUpgrade(build, currentVersion, build.upgrade.reason) });
821
+ writeJson({
822
+ upgrade: unsupportedUpgrade(build, currentVersion, build.upgrade.reason),
823
+ });
631
824
  return;
632
825
  }
633
- writeJson({ upgrade: await upgradeGithubReleaseBinary(build, currentVersion, dryRun) });
826
+ writeJson({
827
+ upgrade: await upgradeGithubReleaseBinary(build, currentVersion, dryRun),
828
+ });
634
829
  }
635
830
 
636
- function unsupportedUpgrade(build: NornBuildInfo, currentVersion: string, reason: string): Record<string, unknown> {
831
+ function unsupportedUpgrade(
832
+ build: NornBuildInfo,
833
+ currentVersion: string,
834
+ reason: string,
835
+ ): Record<string, unknown> {
637
836
  return {
638
837
  supported: false,
639
838
  kind: build.kind,
@@ -643,16 +842,36 @@ function unsupportedUpgrade(build: NornBuildInfo, currentVersion: string, reason
643
842
  };
644
843
  }
645
844
 
646
- async function upgradeGithubReleaseBinary(build: NornGithubReleaseBinaryBuildInfo, currentVersion: string, dryRun: boolean): Promise<Record<string, unknown>> {
845
+ async function upgradeGithubReleaseBinary(
846
+ build: NornGithubReleaseBinaryBuildInfo,
847
+ currentVersion: string,
848
+ dryRun: boolean,
849
+ ): Promise<Record<string, unknown>> {
647
850
  const plan = githubReleaseBinaryUpgradePlan(build, currentVersion, dryRun);
648
851
  if (dryRun) return { ...plan, status: "planned" };
649
- if (process.platform === "win32") return unsupportedUpgrade(build, currentVersion, "Replacing a running Windows executable is not supported yet.");
852
+ if (process.platform === "win32")
853
+ return unsupportedUpgrade(
854
+ build,
855
+ currentVersion,
856
+ "Replacing a running Windows executable is not supported yet.",
857
+ );
650
858
  const binary = await downloadReleaseAsset(plan.downloadUrl);
651
859
  const checksumText = await downloadReleaseText(plan.checksumUrl);
652
- const expectedChecksum = parseSha256Checksum(checksumText, build.checksumAssetName);
653
- const actualChecksum = createHash("sha256").update(Buffer.from(binary)).digest("hex");
654
- if (actualChecksum !== expectedChecksum) throw new Error(`Downloaded Norn binary checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`);
655
- const tempPath = join(dirname(plan.targetPath), `.norn-upgrade-${process.pid}-${build.assetName}`);
860
+ const expectedChecksum = parseSha256Checksum(
861
+ checksumText,
862
+ build.checksumAssetName,
863
+ );
864
+ const actualChecksum = createHash("sha256")
865
+ .update(Buffer.from(binary))
866
+ .digest("hex");
867
+ if (actualChecksum !== expectedChecksum)
868
+ throw new Error(
869
+ `Downloaded Norn binary checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`,
870
+ );
871
+ const tempPath = join(
872
+ dirname(plan.targetPath),
873
+ `.norn-upgrade-${process.pid}-${build.assetName}`,
874
+ );
656
875
  try {
657
876
  await writeFile(tempPath, binary);
658
877
  await chmod(tempPath, 0o755);
@@ -664,7 +883,11 @@ async function upgradeGithubReleaseBinary(build: NornGithubReleaseBinaryBuildInf
664
883
  return { ...plan, status: "completed", checksum: actualChecksum };
665
884
  }
666
885
 
667
- function githubReleaseBinaryUpgradePlan(build: NornGithubReleaseBinaryBuildInfo, currentVersion: string, dryRun: boolean): {
886
+ function githubReleaseBinaryUpgradePlan(
887
+ build: NornGithubReleaseBinaryBuildInfo,
888
+ currentVersion: string,
889
+ dryRun: boolean,
890
+ ): {
668
891
  readonly supported: true;
669
892
  readonly kind: "github-release-binary";
670
893
  readonly dryRun: boolean;
@@ -694,49 +917,82 @@ function githubReleaseBinaryUpgradePlan(build: NornGithubReleaseBinaryBuildInfo,
694
917
  };
695
918
  }
696
919
 
697
- function githubReleaseDownloadUrl(build: NornGithubReleaseBinaryBuildInfo, assetName: string): string {
920
+ function githubReleaseDownloadUrl(
921
+ build: NornGithubReleaseBinaryBuildInfo,
922
+ assetName: string,
923
+ ): string {
698
924
  return `https://github.com/${build.repository}/releases/download/${build.releaseTag}/${assetName}`;
699
925
  }
700
926
 
701
927
  async function downloadReleaseAsset(url: string): Promise<Uint8Array> {
702
928
  const response = await fetch(url);
703
- if (!response.ok) throw new Error(`Failed to download Norn release asset: ${url} returned ${response.status}`);
929
+ if (!response.ok)
930
+ throw new Error(
931
+ `Failed to download Norn release asset: ${url} returned ${response.status}`,
932
+ );
704
933
  return new Uint8Array(await response.arrayBuffer());
705
934
  }
706
935
 
707
936
  async function downloadReleaseText(url: string): Promise<string> {
708
937
  const response = await fetch(url);
709
- if (!response.ok) throw new Error(`Failed to download Norn release metadata: ${url} returned ${response.status}`);
938
+ if (!response.ok)
939
+ throw new Error(
940
+ `Failed to download Norn release metadata: ${url} returned ${response.status}`,
941
+ );
710
942
  return response.text();
711
943
  }
712
944
 
713
945
  function parseSha256Checksum(text: string, checksumAssetName: string): string {
714
946
  const checksum = text.trim().split(/\s+/)[0] ?? "";
715
- if (!/^[a-f0-9]{64}$/i.test(checksum)) throw new Error(`Invalid Norn checksum asset: ${checksumAssetName}`);
947
+ if (!/^[a-f0-9]{64}$/i.test(checksum))
948
+ throw new Error(`Invalid Norn checksum asset: ${checksumAssetName}`);
716
949
  return checksum.toLowerCase();
717
950
  }
718
951
 
719
- function requiredArg(command: string, args: readonly string[], index: number, label: string): string {
952
+ function requiredArg(
953
+ command: string,
954
+ args: readonly string[],
955
+ index: number,
956
+ label: string,
957
+ ): string {
720
958
  const value = args[index];
721
959
  if (!value) throw new Error(`Missing ${label} for ${command}`);
722
960
  return value;
723
961
  }
724
962
 
725
963
  function assertNoExtraArgs(command: string, args: readonly string[]): void {
726
- if (args.length > 0) throw new Error(`${command} does not accept CLI arguments: ${args.join(" ")}`);
964
+ if (args.length > 0)
965
+ throw new Error(
966
+ `${command} does not accept CLI arguments: ${args.join(" ")}`,
967
+ );
727
968
  }
728
969
 
729
- function assertKnownFlags(command: string, args: readonly string[], flags: readonly string[]): void {
970
+ function assertKnownFlags(
971
+ command: string,
972
+ args: readonly string[],
973
+ flags: readonly string[],
974
+ ): void {
730
975
  const allowedFlags = new Set(flags);
731
976
  const unsupportedFlags = args.filter((arg) => !allowedFlags.has(arg));
732
- if (unsupportedFlags.length > 0) throw new Error(`${command} has unsupported flags or arguments: ${unsupportedFlags.join(" ")}`);
977
+ if (unsupportedFlags.length > 0)
978
+ throw new Error(
979
+ `${command} has unsupported flags or arguments: ${unsupportedFlags.join(" ")}`,
980
+ );
733
981
  }
734
982
 
735
983
  async function listWorkflows(args: readonly string[]): Promise<void> {
736
984
  assertKnownFlags("workflows list", args, ["--entrypoints", "--all"]);
737
985
  const entrypointsOnly = workflowListEntrypointsOnly(args);
738
- const { workflows, isComplete, diagnostics } = await discoverNornProject(process.cwd());
739
- writeJson({ workflows: entrypointsOnly ? workflows.filter(workflow => workflow.isEntrypoint) : workflows, isComplete, diagnostics });
986
+ const { workflows, isComplete, diagnostics } = await discoverNornProject(
987
+ process.cwd(),
988
+ );
989
+ writeJson({
990
+ workflows: entrypointsOnly
991
+ ? workflows.filter((workflow) => workflow.isEntrypoint)
992
+ : workflows,
993
+ isComplete,
994
+ diagnostics,
995
+ });
740
996
  }
741
997
 
742
998
  async function inspectWorkflow(workflowId: string): Promise<void> {
@@ -746,18 +1002,30 @@ async function inspectWorkflow(workflowId: string): Promise<void> {
746
1002
  function workflowListEntrypointsOnly(args: readonly string[]): boolean {
747
1003
  const entrypoints = args.includes("--entrypoints");
748
1004
  const all = args.includes("--all");
749
- if (entrypoints && all) throw new Error("Use either --entrypoints or --all, not both");
1005
+ if (entrypoints && all)
1006
+ throw new Error("Use either --entrypoints or --all, not both");
750
1007
  return !all;
751
1008
  }
752
1009
 
753
1010
  async function initProject(): Promise<void> {
754
1011
  const projectRoot = process.cwd();
755
1012
  const projectPath = resolve(projectRoot, NORN_PROJECT_FILE_NAME);
756
- if (await isFile(projectPath)) throw new Error(`Norn project already exists: ${projectPath}`);
1013
+ if (await isFile(projectPath))
1014
+ throw new Error(`Norn project already exists: ${projectPath}`);
757
1015
  await mkdir(resolve(projectRoot, RUNS_ROOT), { recursive: true });
758
- await writeFile(projectPath, `${JSON.stringify({ version: 1, workflows: [], includes: [], config: {} }, null, 2)}\n`, "utf8");
1016
+ await writeFile(
1017
+ projectPath,
1018
+ `${JSON.stringify({ version: 1, workflows: [], includes: [], config: {} }, null, 2)}\n`,
1019
+ "utf8",
1020
+ );
759
1021
  await ensureGitignoreExcludesRunState(projectRoot);
760
- writeJson({ project: { path: projectPath, root: projectRoot, runsRoot: resolve(projectRoot, RUNS_ROOT) } });
1022
+ writeJson({
1023
+ project: {
1024
+ path: projectPath,
1025
+ root: projectRoot,
1026
+ runsRoot: resolve(projectRoot, RUNS_ROOT),
1027
+ },
1028
+ });
761
1029
  }
762
1030
 
763
1031
  async function listCurrentProjectRuns(): Promise<NornRunInfo[]> {
@@ -766,11 +1034,16 @@ async function listCurrentProjectRuns(): Promise<NornRunInfo[]> {
766
1034
  }
767
1035
 
768
1036
  async function inspectProject(): Promise<void> {
769
- const { project, isComplete, diagnostics } = await discoverNornProject(process.cwd());
1037
+ const { project, isComplete, diagnostics } = await discoverNornProject(
1038
+ process.cwd(),
1039
+ );
770
1040
  writeJson({ project, isComplete, diagnostics });
771
1041
  }
772
1042
 
773
- async function startRun(workflowId: string, commandArgs: readonly string[]): Promise<void> {
1043
+ async function startRun(
1044
+ workflowId: string,
1045
+ commandArgs: readonly string[],
1046
+ ): Promise<void> {
774
1047
  assertNoStructuredInputArgs("runs start", commandArgs);
775
1048
  const project = await loadNornProject(process.cwd());
776
1049
  const workflow = project.registry.workflowById(workflowId);
@@ -780,13 +1053,33 @@ async function startRun(workflowId: string, commandArgs: readonly string[]): Pro
780
1053
  Value.Decode(workflow.args, args);
781
1054
  const configOverride = input.config;
782
1055
  const id = randomUUID();
783
- const name = generateRunName(new Set((await listRuns(project.projectRoot)).map((run) => run.name)));
1056
+ const name = generateRunName(
1057
+ new Set((await listRuns(project.projectRoot)).map((run) => run.name)),
1058
+ );
784
1059
  const runRoot = resolve(project.projectRoot, RUNS_ROOT, id);
785
1060
  await mkdir(runRoot, { recursive: true });
786
1061
  const createdAt = new Date().toISOString();
787
- await writeRunLaunchRequest(runRoot, { version: 2, type: "run", id, name, workflowId, args, configOverride, createdAt });
1062
+ await writeRunLaunchRequest(runRoot, {
1063
+ version: 2,
1064
+ type: "run",
1065
+ id,
1066
+ name,
1067
+ workflowId,
1068
+ args,
1069
+ configOverride,
1070
+ createdAt,
1071
+ });
788
1072
  await startDetachedExecuteRun(id, project.projectRoot);
789
- writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, projectRoot: project.projectRoot, createdAt }) });
1073
+ writeJson({
1074
+ run: startedRunInfo({
1075
+ id,
1076
+ name,
1077
+ workflow,
1078
+ runRoot,
1079
+ projectRoot: project.projectRoot,
1080
+ createdAt,
1081
+ }),
1082
+ });
790
1083
  }
791
1084
 
792
1085
  async function resumeRun(run: string, args: readonly string[]): Promise<void> {
@@ -800,7 +1093,14 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
800
1093
  const runInfo = await getRunInfo(runRoot);
801
1094
  assertRunVersion(runInfo.version);
802
1095
  const args = await parseResumeArgs(runInfo, input.args);
803
- request = { version: 2, type: "resume", id: runInfo.id, requestId: randomUUID(), args, createdAt: new Date().toISOString() };
1096
+ request = {
1097
+ version: 2,
1098
+ type: "resume",
1099
+ id: runInfo.id,
1100
+ requestId: randomUUID(),
1101
+ args,
1102
+ createdAt: new Date().toISOString(),
1103
+ };
804
1104
  await writeRunResumeRequest(runRoot, request);
805
1105
  } finally {
806
1106
  await lease.release();
@@ -814,18 +1114,38 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
814
1114
  writeJson({ run: await getRunInfo(runRoot) });
815
1115
  }
816
1116
 
817
- async function parseResumeArgs(runInfo: NornRunInfo, args: unknown): Promise<unknown> {
1117
+ async function parseResumeArgs(
1118
+ runInfo: NornRunInfo,
1119
+ args: unknown,
1120
+ ): Promise<unknown> {
818
1121
  if (runInfo.status === "interrupted") {
819
- if (args === undefined) throw new Error(`Interrupted workflow resume requires args: ${runInfo.name}`);
820
- if (!runInfo.currentWorkflowId) throw new Error(`Run has no current workflow: ${runInfo.name}`);
1122
+ if (args === undefined)
1123
+ throw new Error(
1124
+ `Interrupted workflow resume requires args: ${runInfo.name}`,
1125
+ );
1126
+ if (!runInfo.currentWorkflowId)
1127
+ throw new Error(`Run has no current workflow: ${runInfo.name}`);
821
1128
  const project = await loadNornProject(process.cwd());
822
1129
  const workflow = project.registry.workflowById(runInfo.currentWorkflowId);
823
- if (!workflow) throw new Error(`Unknown workflow for resumed run: ${runInfo.currentWorkflowId}`);
824
- Value.Decode(workflow.args, mergeInterruptedWorkflowArgs(runInfo.interruption?.args, args, runInfo.interruption?.fields));
1130
+ if (!workflow)
1131
+ throw new Error(
1132
+ `Unknown workflow for resumed run: ${runInfo.currentWorkflowId}`,
1133
+ );
1134
+ Value.Decode(
1135
+ workflow.args,
1136
+ mergeInterruptedWorkflowArgs(
1137
+ runInfo.interruption?.args,
1138
+ args,
1139
+ runInfo.interruption?.fields,
1140
+ ),
1141
+ );
825
1142
  return args;
826
1143
  }
827
1144
  if (runInfo.status === "pendingResume") {
828
- if (args !== undefined) throw new Error(`Pending-resume workflows do not accept args: ${runInfo.name}`);
1145
+ if (args !== undefined)
1146
+ throw new Error(
1147
+ `Pending-resume workflows do not accept args: ${runInfo.name}`,
1148
+ );
829
1149
  return undefined;
830
1150
  }
831
1151
  throw new Error(`Run must be rolled back before resuming: ${runInfo.name}`);
@@ -836,13 +1156,17 @@ async function waitRun(run: string): Promise<void> {
836
1156
  writeJson({ run: await waitForInactiveRun(project.projectRoot, run) });
837
1157
  }
838
1158
 
839
- async function waitForInactiveRun(projectRoot: string, run: string): Promise<NornRunInfo> {
1159
+ async function waitForInactiveRun(
1160
+ projectRoot: string,
1161
+ run: string,
1162
+ ): Promise<NornRunInfo> {
840
1163
  let runRoot: string | undefined;
841
1164
  while (true) {
842
1165
  runRoot ??= await resolveWaitableRunRoot(projectRoot, run);
843
1166
  try {
844
1167
  const runInfo = await readRunInspection(runRoot);
845
- if (runInfo.status !== "running" || runInfo.health === "unhealthy") return runInfo;
1168
+ if (runInfo.status !== "running" || runInfo.health === "unhealthy")
1169
+ return runInfo;
846
1170
  } catch (error) {
847
1171
  if (!isNodeError(error) || error.code !== "ENOENT") throw error;
848
1172
  }
@@ -850,11 +1174,17 @@ async function waitForInactiveRun(projectRoot: string, run: string): Promise<Nor
850
1174
  }
851
1175
  }
852
1176
 
853
- async function resolveWaitableRunRoot(sessionCwd: string, run: string): Promise<string> {
1177
+ async function resolveWaitableRunRoot(
1178
+ sessionCwd: string,
1179
+ run: string,
1180
+ ): Promise<string> {
854
1181
  try {
855
1182
  return await resolveRunRoot(sessionCwd, run);
856
1183
  } catch (error) {
857
- for (const candidate of [resolve(sessionCwd, run), resolve(sessionCwd, RUNS_ROOT, run)]) {
1184
+ for (const candidate of [
1185
+ resolve(sessionCwd, run),
1186
+ resolve(sessionCwd, RUNS_ROOT, run),
1187
+ ]) {
858
1188
  if (await isDirectory(candidate)) return candidate;
859
1189
  }
860
1190
  throw error;
@@ -879,7 +1209,9 @@ async function isFile(path: string): Promise<boolean> {
879
1209
  }
880
1210
  }
881
1211
 
882
- async function ensureGitignoreExcludesRunState(projectRoot: string): Promise<void> {
1212
+ async function ensureGitignoreExcludesRunState(
1213
+ projectRoot: string,
1214
+ ): Promise<void> {
883
1215
  const gitignorePath = resolve(projectRoot, ".gitignore");
884
1216
  const runStatePattern = ".norn/runs/";
885
1217
  let currentText = "";
@@ -889,37 +1221,65 @@ async function ensureGitignoreExcludesRunState(projectRoot: string): Promise<voi
889
1221
  if (!isNodeError(error) || error.code !== "ENOENT") throw error;
890
1222
  }
891
1223
  if (currentText.split(/\r?\n/).includes(runStatePattern)) return;
892
- const separator = currentText.length === 0 || currentText.endsWith("\n") ? "" : "\n";
893
- await writeFile(gitignorePath, `${currentText}${separator}${runStatePattern}\n`, "utf8");
1224
+ const separator =
1225
+ currentText.length === 0 || currentText.endsWith("\n") ? "" : "\n";
1226
+ await writeFile(
1227
+ gitignorePath,
1228
+ `${currentText}${separator}${runStatePattern}\n`,
1229
+ "utf8",
1230
+ );
894
1231
  }
895
1232
 
896
1233
  async function executeRun(runId: string): Promise<void> {
897
1234
  const abortController = new AbortController();
898
- process.once("SIGTERM", () => abortController.abort(new NornRunStoppedError()));
899
- process.once("SIGINT", () => abortController.abort(new NornRunStoppedError()));
1235
+ process.once("SIGTERM", () =>
1236
+ abortController.abort(new NornRunStoppedError()),
1237
+ );
1238
+ process.once("SIGINT", () =>
1239
+ abortController.abort(new NornRunStoppedError()),
1240
+ );
900
1241
  const location = await findNornProject(process.cwd());
901
1242
  const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
902
- const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
1243
+ const request =
1244
+ (await readOptionalRunLaunchRequest(runRoot)) ??
1245
+ (await readRunResumeRequest(runRoot));
903
1246
  try {
904
- const agentDir = resolveNornAgentDirectory({ home: homedir(), environment: process.env });
1247
+ const agentDir = resolveNornAgentDirectory({
1248
+ home: homedir(),
1249
+ environment: process.env,
1250
+ });
905
1251
  process.chdir(await prepareRunWorkerDirectory(runRoot));
906
1252
  const project = await loadNornProject(location.projectRoot);
907
- const engine = new NornEngine({ cwd: project.projectRoot, agentDir, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
1253
+ const engine = new NornEngine({
1254
+ cwd: project.projectRoot,
1255
+ agentDir,
1256
+ signal: abortController.signal,
1257
+ gateMode: "pause",
1258
+ config: project.projectConfig,
1259
+ });
908
1260
  engine.registerWorkflows(project.definitions);
909
1261
  if (request.type === "run") {
910
1262
  const workflow = project.registry.workflowById(request.workflowId);
911
1263
  if (!workflow) throw new Error(`Unknown workflow: ${request.workflowId}`);
912
- await engine.runWorkflow(workflow, request.args, { id: request.id, name: request.name, configOverride: request.configOverride });
1264
+ await engine.runWorkflow(workflow, request.args, {
1265
+ id: request.id,
1266
+ name: request.name,
1267
+ configOverride: request.configOverride,
1268
+ });
913
1269
  } else {
914
1270
  await engine.resumeRequestedWorkflow({ runRoot, request });
915
1271
  }
916
1272
  } finally {
917
- if (request.type === "run") await rm(join(runRoot, "launch-request.json"), { force: true });
1273
+ if (request.type === "run")
1274
+ await rm(join(runRoot, "launch-request.json"), { force: true });
918
1275
  else await removeResumeRequest({ runRoot, request });
919
1276
  }
920
1277
  }
921
1278
 
922
- async function removeResumeRequest(input: { readonly runRoot: string; readonly request: NornRunResumeRequest }): Promise<void> {
1279
+ async function removeResumeRequest(input: {
1280
+ readonly runRoot: string;
1281
+ readonly request: NornRunResumeRequest;
1282
+ }): Promise<void> {
923
1283
  const lease = await NornRunLease.acquire(input.runRoot);
924
1284
  try {
925
1285
  await clearRunResumeRequest({ ...input, lease });
@@ -928,12 +1288,18 @@ async function removeResumeRequest(input: { readonly runRoot: string; readonly r
928
1288
  }
929
1289
  }
930
1290
 
931
- async function rollbackRun(run: string, args: readonly string[]): Promise<void> {
1291
+ async function rollbackRun(
1292
+ run: string,
1293
+ args: readonly string[],
1294
+ ): Promise<void> {
932
1295
  const checkpointId = requiredArg("runs rollback", args, 0, "checkpoint id");
933
1296
  assertNoExtraArgs("runs rollback", args.slice(1));
934
1297
  const project = await findNornProject(process.cwd());
935
1298
  const runRoot = await resolveRunRoot(project.projectRoot, run);
936
- const engine = new NornEngine({ cwd: project.projectRoot, gateMode: "pause" });
1299
+ const engine = new NornEngine({
1300
+ cwd: project.projectRoot,
1301
+ gateMode: "pause",
1302
+ });
937
1303
  writeJson({ run: await engine.rollbackRun(runRoot, checkpointId) });
938
1304
  }
939
1305
 
@@ -957,25 +1323,33 @@ async function deleteRun(run: string): Promise<DeletedNornRunInfo> {
957
1323
  const project = await findNornProject(process.cwd());
958
1324
  const runRoot = await resolveRunRoot(project.projectRoot, run);
959
1325
  const runInfo = await getRunInfo(runRoot);
960
- if (runInfo.status === "running") throw new Error(`Stop run before deleting it: ${runInfo.name}`);
1326
+ if (runInfo.status === "running")
1327
+ throw new Error(`Stop run before deleting it: ${runInfo.name}`);
961
1328
  await rm(runRoot, { recursive: true, force: true });
962
1329
  return { id: runInfo.id, name: runInfo.name, path: runInfo.path };
963
1330
  }
964
1331
 
965
- async function terminateRun(runRoot: string, signal: NodeJS.Signals): Promise<void> {
1332
+ async function terminateRun(
1333
+ runRoot: string,
1334
+ signal: NodeJS.Signals,
1335
+ ): Promise<void> {
966
1336
  const owner = await getRunLeaseOwner(runRoot);
967
1337
  if (!owner) return;
968
1338
  sendSignal(owner.processGroupId, owner.pid, signal);
969
1339
  await delay(signal === "SIGKILL" ? 250 : 1000);
970
1340
  }
971
1341
 
972
- async function writeRunLogs(run: string, options: { readonly follow: boolean }): Promise<void> {
1342
+ async function writeRunLogs(
1343
+ run: string,
1344
+ options: { readonly follow: boolean },
1345
+ ): Promise<void> {
973
1346
  const project = await findNornProject(process.cwd());
974
1347
  const runRoot = await resolveRunRoot(project.projectRoot, run);
975
1348
  let written = 0;
976
1349
  while (true) {
977
1350
  const events = await readRunEvents(runRoot);
978
- for (const event of events.slice(written)) process.stdout.write(`${JSON.stringify(event)}\n`);
1351
+ for (const event of events.slice(written))
1352
+ process.stdout.write(`${JSON.stringify(event)}\n`);
979
1353
  written = events.length;
980
1354
  if (!options.follow) return;
981
1355
  const info = await getRunInfo(runRoot);
@@ -984,7 +1358,10 @@ async function writeRunLogs(run: string, options: { readonly follow: boolean }):
984
1358
  }
985
1359
  }
986
1360
 
987
- async function startDetachedExecuteRun(runId: string, projectRoot: string): Promise<void> {
1361
+ async function startDetachedExecuteRun(
1362
+ runId: string,
1363
+ projectRoot: string,
1364
+ ): Promise<void> {
988
1365
  const child = spawn(process.execPath, detachedExecuteRunArgs(runId), {
989
1366
  cwd: projectRoot,
990
1367
  detached: true,
@@ -998,9 +1375,14 @@ async function startDetachedExecuteRun(runId: string, projectRoot: string): Prom
998
1375
  }
999
1376
 
1000
1377
  function detachedExecuteRunArgs(runId: string): readonly string[] {
1001
- if (NORN_BUILD_INFO.kind === "github-release-binary") return ["execute-run", runId];
1378
+ if (NORN_BUILD_INFO.kind === "github-release-binary")
1379
+ return ["execute-run", runId];
1002
1380
  const scriptPath = process.argv[1];
1003
- return scriptPath && isAbsolute(scriptPath) && /\.(?:c?m?js|ts)$/.test(scriptPath) ? [scriptPath, "execute-run", runId] : ["execute-run", runId];
1381
+ return scriptPath &&
1382
+ isAbsolute(scriptPath) &&
1383
+ /\.(?:c?m?js|ts)$/.test(scriptPath)
1384
+ ? [scriptPath, "execute-run", runId]
1385
+ : ["execute-run", runId];
1004
1386
  }
1005
1387
 
1006
1388
  function startedRunInfo(input: {
@@ -1016,7 +1398,10 @@ function startedRunInfo(input: {
1016
1398
  id: input.id,
1017
1399
  name: input.name,
1018
1400
  path: input.runRoot,
1019
- paths: { project: input.projectRoot, workspace: join(input.runRoot, "current", "workspace") },
1401
+ paths: {
1402
+ project: input.projectRoot,
1403
+ workspace: join(input.runRoot, "current", "workspace"),
1404
+ },
1020
1405
  entrypointWorkflowId: input.workflow.id,
1021
1406
  currentWorkflowId: input.workflow.id,
1022
1407
  status: "running",
@@ -1035,9 +1420,13 @@ async function readOptionalRunLaunchRequest(runRoot: string) {
1035
1420
  }
1036
1421
  }
1037
1422
 
1038
- async function readRunEvents(runRoot: string): Promise<readonly Record<string, unknown>[]> {
1423
+ async function readRunEvents(
1424
+ runRoot: string,
1425
+ ): Promise<readonly Record<string, unknown>[]> {
1039
1426
  try {
1040
- const manifest = JSON.parse(await readFile(join(runRoot, "current", "manifest.json"), "utf8")) as { events?: readonly Record<string, unknown>[] };
1427
+ const manifest = JSON.parse(
1428
+ await readFile(join(runRoot, "current", "manifest.json"), "utf8"),
1429
+ ) as { events?: readonly Record<string, unknown>[] };
1041
1430
  return manifest.events ?? [];
1042
1431
  } catch (error) {
1043
1432
  if (isNodeError(error) && error.code === "ENOENT") return [];
@@ -1065,7 +1454,10 @@ async function readStdin(): Promise<string> {
1065
1454
  });
1066
1455
  }
1067
1456
 
1068
- function parseStartRunInput(value: unknown): { readonly args?: unknown; readonly config?: unknown } {
1457
+ function parseStartRunInput(value: unknown): {
1458
+ readonly args?: unknown;
1459
+ readonly config?: unknown;
1460
+ } {
1069
1461
  if (value === undefined) return {};
1070
1462
  const input = parseStructuredInputObject("runs start", value);
1071
1463
  assertStructuredInputKeys("runs start", input, ["args", "config"]);
@@ -1079,22 +1471,43 @@ function parseResumeRunInput(value: unknown): { readonly args?: unknown } {
1079
1471
  return { args: input.args };
1080
1472
  }
1081
1473
 
1082
- function parseStructuredInputObject(command: string, value: unknown): Record<string, unknown> {
1083
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${command} reads a JSON object from stdin`);
1474
+ function parseStructuredInputObject(
1475
+ command: string,
1476
+ value: unknown,
1477
+ ): Record<string, unknown> {
1478
+ if (!value || typeof value !== "object" || Array.isArray(value))
1479
+ throw new Error(`${command} reads a JSON object from stdin`);
1084
1480
  return value as Record<string, unknown>;
1085
1481
  }
1086
1482
 
1087
- function assertStructuredInputKeys(command: string, input: Record<string, unknown>, allowedKeys: readonly string[]): void {
1483
+ function assertStructuredInputKeys(
1484
+ command: string,
1485
+ input: Record<string, unknown>,
1486
+ allowedKeys: readonly string[],
1487
+ ): void {
1088
1488
  const allowed = new Set(allowedKeys);
1089
1489
  const unexpected = Object.keys(input).filter((key) => !allowed.has(key));
1090
- if (unexpected.length > 0) throw new Error(`${command} stdin JSON has unsupported keys: ${unexpected.join(", ")}`);
1091
- }
1092
-
1093
- function assertNoStructuredInputArgs(command: string, args: readonly string[]): void {
1094
- if (args.length > 0) throw new Error(`${command} reads structured input from stdin, not CLI arguments: ${args.join(" ")}`);
1095
- }
1096
-
1097
- function sendSignal(processGroupId: number, pid: number, signal: NodeJS.Signals): void {
1490
+ if (unexpected.length > 0)
1491
+ throw new Error(
1492
+ `${command} stdin JSON has unsupported keys: ${unexpected.join(", ")}`,
1493
+ );
1494
+ }
1495
+
1496
+ function assertNoStructuredInputArgs(
1497
+ command: string,
1498
+ args: readonly string[],
1499
+ ): void {
1500
+ if (args.length > 0)
1501
+ throw new Error(
1502
+ `${command} reads structured input from stdin, not CLI arguments: ${args.join(" ")}`,
1503
+ );
1504
+ }
1505
+
1506
+ function sendSignal(
1507
+ processGroupId: number,
1508
+ pid: number,
1509
+ signal: NodeJS.Signals,
1510
+ ): void {
1098
1511
  try {
1099
1512
  if (processGroupId > 0 && process.platform !== "win32") {
1100
1513
  process.kill(-processGroupId, signal);
@@ -1111,7 +1524,9 @@ function sendSignal(processGroupId: number, pid: number, signal: NodeJS.Signals)
1111
1524
  }
1112
1525
 
1113
1526
  function isIgnorableSignalError(error: unknown): boolean {
1114
- return isNodeError(error) && (error.code === "ESRCH" || error.code === "EPERM");
1527
+ return (
1528
+ isNodeError(error) && (error.code === "ESRCH" || error.code === "EPERM")
1529
+ );
1115
1530
  }
1116
1531
 
1117
1532
  function writeJson(value: unknown): void {