@vimhead.dev/norn-cli 0.1.0-tip.0

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 (171) hide show
  1. package/README.md +13 -0
  2. package/assets/README.md +157 -0
  3. package/assets/docs/README.md +23 -0
  4. package/assets/docs/agents.md +64 -0
  5. package/assets/docs/cli.md +183 -0
  6. package/assets/docs/composition.md +76 -0
  7. package/assets/docs/persistence.md +56 -0
  8. package/assets/docs/projects.md +75 -0
  9. package/assets/docs/recovery.md +56 -0
  10. package/assets/docs/resources.md +61 -0
  11. package/assets/docs/workflows.md +57 -0
  12. package/assets/examples/agent-then-analysis/README.md +103 -0
  13. package/assets/examples/agent-then-analysis/input.json +5 -0
  14. package/assets/examples/agent-then-analysis/norn.project.json +4 -0
  15. package/assets/examples/agent-then-analysis/plugin.ts +89 -0
  16. package/assets/examples/coordinating-multiple-agents/README.md +56 -0
  17. package/assets/examples/coordinating-multiple-agents/input.json +10 -0
  18. package/assets/examples/coordinating-multiple-agents/norn.project.json +4 -0
  19. package/assets/examples/coordinating-multiple-agents/plugin.ts +102 -0
  20. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +52 -0
  21. package/assets/examples/coordinating-multiple-agents/work-queue.ts +153 -0
  22. package/assets/examples/minimal-workflow/README.md +71 -0
  23. package/assets/examples/minimal-workflow/norn.project.json +4 -0
  24. package/assets/examples/minimal-workflow/plugin.ts +29 -0
  25. package/assets/examples/shared-state/README.md +19 -0
  26. package/assets/examples/shared-state/input.json +1 -0
  27. package/assets/examples/shared-state/norn.project.json +4 -0
  28. package/assets/examples/shared-state/plugin.ts +47 -0
  29. package/assets/examples/worktree-development-loop/README.md +66 -0
  30. package/assets/examples/worktree-development-loop/index.ts +1 -0
  31. package/assets/examples/worktree-development-loop/manifest.ts +26 -0
  32. package/assets/examples/worktree-development-loop/norn.project.json +9 -0
  33. package/assets/examples/worktree-development-loop/plugin.ts +27 -0
  34. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -0
  35. package/assets/examples/worktree-development-loop/state.ts +23 -0
  36. package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +8 -0
  37. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -0
  38. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -0
  39. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +22 -0
  40. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +14 -0
  41. package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +8 -0
  42. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +54 -0
  43. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +3 -0
  44. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +12 -0
  45. package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +8 -0
  46. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +28 -0
  47. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +3 -0
  48. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +12 -0
  49. package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +8 -0
  50. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +53 -0
  51. package/assets/examples/worktree-development-loop/workflows/review/index.ts +10 -0
  52. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +23 -0
  53. package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +12 -0
  54. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -0
  55. package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +3 -0
  56. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +12 -0
  57. package/assets/package.json +1 -0
  58. package/assets/packages/cli/src/build-info.ts +36 -0
  59. package/assets/packages/cli/src/bun/cli.ts +16 -0
  60. package/assets/packages/cli/src/cli.ts +1135 -0
  61. package/assets/packages/cli/src/client.ts +167 -0
  62. package/assets/packages/cli/src/documentation-intro.ts +30 -0
  63. package/assets/packages/cli/src/documentation.ts +149 -0
  64. package/assets/packages/cli/src/generated-build-info.ts +12 -0
  65. package/assets/packages/cli/src/internal/agent-directory.ts +5 -0
  66. package/assets/packages/cli/src/internal/agent-response-tool.ts +96 -0
  67. package/assets/packages/cli/src/internal/agents.ts +365 -0
  68. package/assets/packages/cli/src/internal/artifacts.ts +26 -0
  69. package/assets/packages/cli/src/internal/commands.ts +180 -0
  70. package/assets/packages/cli/src/internal/documentation-bundle.ts +49 -0
  71. package/assets/packages/cli/src/internal/engine.ts +501 -0
  72. package/assets/packages/cli/src/internal/errors.ts +39 -0
  73. package/assets/packages/cli/src/internal/file-names.ts +3 -0
  74. package/assets/packages/cli/src/internal/launch-request.ts +94 -0
  75. package/assets/packages/cli/src/internal/logs.ts +41 -0
  76. package/assets/packages/cli/src/internal/metrics.ts +356 -0
  77. package/assets/packages/cli/src/internal/pi-assets.ts +95 -0
  78. package/assets/packages/cli/src/internal/resource-bindings.ts +35 -0
  79. package/assets/packages/cli/src/internal/run-lease.ts +158 -0
  80. package/assets/packages/cli/src/internal/run-log.ts +59 -0
  81. package/assets/packages/cli/src/internal/run-names.ts +36 -0
  82. package/assets/packages/cli/src/internal/run-resources.ts +23 -0
  83. package/assets/packages/cli/src/internal/run-state.ts +380 -0
  84. package/assets/packages/cli/src/internal/run-store.ts +323 -0
  85. package/assets/packages/cli/src/internal/run.ts +133 -0
  86. package/assets/packages/cli/src/internal/state-store.ts +75 -0
  87. package/assets/packages/cli/src/internal/usage.ts +70 -0
  88. package/assets/packages/cli/src/internal/workflow-registry.ts +176 -0
  89. package/assets/packages/cli/src/plugin-loader.ts +412 -0
  90. package/assets/packages/cli/src/resources.ts +67 -0
  91. package/assets/packages/core/src/agent-protocol.ts +1 -0
  92. package/assets/packages/core/src/atomic-files.ts +24 -0
  93. package/assets/packages/core/src/errors.ts +3 -0
  94. package/assets/packages/sdk/src/agent-resource-adapter.ts +11 -0
  95. package/assets/packages/sdk/src/api.ts +821 -0
  96. package/assets/packages/sdk/src/files.ts +136 -0
  97. package/assets/packages/sdk/src/index.ts +6 -0
  98. package/assets/packages/sdk/src/resources.ts +20 -0
  99. package/assets/packages/sdk/src/schema.ts +48 -0
  100. package/assets/packages/sdk/src/seer/config.ts +62 -0
  101. package/assets/packages/sdk/src/seer/index.ts +7 -0
  102. package/assets/packages/sdk/src/state-adapter.ts +75 -0
  103. package/assets/setup/providers.md +128 -0
  104. package/assets/setup/releases.md +76 -0
  105. package/assets/tests/workflow-ref.test.ts +113 -0
  106. package/bin/norn.mjs +10 -0
  107. package/dist/build-info.d.ts +30 -0
  108. package/dist/build-info.js +6 -0
  109. package/dist/cli.d.ts +2 -0
  110. package/dist/cli.js +1032 -0
  111. package/dist/client.d.ts +48 -0
  112. package/dist/client.js +118 -0
  113. package/dist/documentation-intro.d.ts +5 -0
  114. package/dist/documentation-intro.js +29 -0
  115. package/dist/documentation.d.ts +33 -0
  116. package/dist/documentation.js +132 -0
  117. package/dist/generated-build-info.d.ts +10 -0
  118. package/dist/generated-build-info.js +14 -0
  119. package/dist/internal/agent-directory.d.ts +4 -0
  120. package/dist/internal/agent-directory.js +8 -0
  121. package/dist/internal/agent-response-tool.d.ts +21 -0
  122. package/dist/internal/agent-response-tool.js +79 -0
  123. package/dist/internal/agents.d.ts +29 -0
  124. package/dist/internal/agents.js +336 -0
  125. package/dist/internal/artifacts.d.ts +10 -0
  126. package/dist/internal/artifacts.js +29 -0
  127. package/dist/internal/commands.d.ts +18 -0
  128. package/dist/internal/commands.js +147 -0
  129. package/dist/internal/documentation-bundle.d.ts +16 -0
  130. package/dist/internal/documentation-bundle.js +42 -0
  131. package/dist/internal/engine.d.ts +44 -0
  132. package/dist/internal/engine.js +399 -0
  133. package/dist/internal/errors.d.ts +14 -0
  134. package/dist/internal/errors.js +38 -0
  135. package/dist/internal/file-names.d.ts +1 -0
  136. package/dist/internal/file-names.js +7 -0
  137. package/dist/internal/launch-request.d.ts +33 -0
  138. package/dist/internal/launch-request.js +110 -0
  139. package/dist/internal/logs.d.ts +16 -0
  140. package/dist/internal/logs.js +38 -0
  141. package/dist/internal/metrics.d.ts +19 -0
  142. package/dist/internal/metrics.js +282 -0
  143. package/dist/internal/pi-assets.d.ts +13 -0
  144. package/dist/internal/pi-assets.js +94 -0
  145. package/dist/internal/resource-bindings.d.ts +13 -0
  146. package/dist/internal/resource-bindings.js +34 -0
  147. package/dist/internal/run-lease.d.ts +32 -0
  148. package/dist/internal/run-lease.js +166 -0
  149. package/dist/internal/run-log.d.ts +30 -0
  150. package/dist/internal/run-log.js +71 -0
  151. package/dist/internal/run-names.d.ts +1 -0
  152. package/dist/internal/run-names.js +144 -0
  153. package/dist/internal/run-resources.d.ts +6 -0
  154. package/dist/internal/run-resources.js +26 -0
  155. package/dist/internal/run-state.d.ts +95 -0
  156. package/dist/internal/run-state.js +323 -0
  157. package/dist/internal/run-store.d.ts +35 -0
  158. package/dist/internal/run-store.js +314 -0
  159. package/dist/internal/run.d.ts +51 -0
  160. package/dist/internal/run.js +101 -0
  161. package/dist/internal/state-store.d.ts +22 -0
  162. package/dist/internal/state-store.js +97 -0
  163. package/dist/internal/usage.d.ts +5 -0
  164. package/dist/internal/usage.js +70 -0
  165. package/dist/internal/workflow-registry.d.ts +35 -0
  166. package/dist/internal/workflow-registry.js +129 -0
  167. package/dist/plugin-loader.d.ts +55 -0
  168. package/dist/plugin-loader.js +353 -0
  169. package/dist/resources.d.ts +11 -0
  170. package/dist/resources.js +98 -0
  171. package/package.json +52 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1032 @@
1
+ // src/cli.ts
2
+ import { spawn } from "node:child_process";
3
+ import { main as runPi } from "@earendil-works/pi-coding-agent";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import { renderNornDocumentationIntro, resolveDocumentationCacheRoot, resolveNornDocumentation } from "./documentation.js";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./plugin-loader.js";
12
+ import { NornEngine } from "./internal/engine.js";
13
+ import { errorMessage, NornProjectLoadError, NornRunStoppedError } from "./internal/errors.js";
14
+
15
+ // ../core/src/errors.ts
16
+ function isNodeError(error) {
17
+ return error instanceof Error && "code" in error;
18
+ }
19
+
20
+ // src/cli.ts
21
+ import { clearRunResumeRequest, readRunLaunchRequest, readRunResumeRequest, writeRunLaunchRequest, writeRunResumeRequest } from "./internal/launch-request.js";
22
+ import { generateRunName } from "./internal/run-names.js";
23
+ import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.js";
24
+ import { NornRunStore } from "./internal/run-store.js";
25
+ import { readRunMetrics } from "./internal/metrics.js";
26
+ import { getRunInfo, listRuns, mergeInterruptedWorkflowParams, resolveRunRoot } from "./internal/run-state.js";
27
+ import { NORN_BUILD_INFO } from "./build-info.js";
28
+ var RUNS_ROOT = join(".norn", "runs");
29
+ var RUN_WAIT_INTERVAL_MS = 1e3;
30
+ var 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.";
31
+ var COMMANDS = [
32
+ {
33
+ id: "pi",
34
+ path: ["pi"],
35
+ description: "Run Norn's bundled Pi CLI for interactive authentication, provider packages, model configuration, or direct Pi use. No Norn project is required.",
36
+ usage: "norn pi [pi arguments...]",
37
+ arguments: ["pi arguments: forwarded unchanged; use norn pi --help for Pi's CLI contract"],
38
+ output: "Pi's native terminal, text, JSON, or RPC output and exit status; not wrapped in Norn JSON.",
39
+ examples: ["norn pi", "norn pi install npm:pi-cursor-sdk", "norn pi --list-models cursor", "norn pi --help"],
40
+ execute: (args) => runPi([...args])
41
+ },
42
+ {
43
+ id: "commands.list",
44
+ path: ["commands", "list"],
45
+ description: "Use when an agent or human needs machine-readable Norn CLI command metadata.",
46
+ usage: "norn commands list [--all]",
47
+ options: ["--all: include hidden internal commands"],
48
+ output: "JSON object with command metadata under commands.",
49
+ examples: ["norn commands list", "norn commands list --all"],
50
+ execute: listCliCommands
51
+ },
52
+ {
53
+ id: "commands.inspect",
54
+ path: ["commands", "inspect"],
55
+ description: "Use when an agent or human needs the usage contract for one Norn CLI command.",
56
+ usage: "norn commands inspect <command-id>",
57
+ arguments: ["command-id: command metadata id such as runs.start"],
58
+ output: "JSON object with one command metadata object under command.",
59
+ examples: ["norn commands inspect runs.start"],
60
+ execute: inspectCliCommand
61
+ },
62
+ {
63
+ id: "help",
64
+ path: ["help"],
65
+ description: "Use when reading JSON help for all Norn commands, one command group, or one command.",
66
+ usage: "norn help [command-or-group]",
67
+ arguments: ["command-or-group: optional command path such as runs or runs start"],
68
+ output: "JSON object with help metadata under help.",
69
+ examples: ["norn help", "norn help runs", "norn help runs start", "norn --help"],
70
+ execute: (args) => writeCliHelp(args)
71
+ },
72
+ {
73
+ id: "docs.inspect",
74
+ path: ["docs", "inspect"],
75
+ 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.",
76
+ usage: "norn docs inspect",
77
+ output: "JSON object under documentation with storage, version, commit, assetDigest, and absolute paths. NORN_DOCS_CACHE_DIR overrides the binary cache directory.",
78
+ examples: ["norn docs inspect"],
79
+ execute: async (args, documentationSource) => {
80
+ assertNoExtraArgs("docs inspect", args);
81
+ writeJson({ documentation: await resolveCliDocumentation(documentationSource) });
82
+ }
83
+ },
84
+ {
85
+ id: "docs.intro",
86
+ path: ["docs", "intro"],
87
+ 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.",
88
+ usage: "norn docs intro",
89
+ output: "JSON object with introduction text under intro. Uses the same asset resolution and NORN_DOCS_CACHE_DIR override as docs inspect.",
90
+ examples: ["norn docs intro"],
91
+ execute: async (args, documentationSource) => {
92
+ assertNoExtraArgs("docs intro", args);
93
+ writeJson({ intro: renderNornDocumentationIntro({
94
+ documentation: await resolveCliDocumentation(documentationSource),
95
+ invocation: documentationSource.kind === "embedded" ? [process.execPath] : [process.execPath, fileURLToPath(new URL("../bin/norn.mjs", import.meta.url))]
96
+ }) });
97
+ }
98
+ },
99
+ {
100
+ id: "project.init",
101
+ path: ["project", "init"],
102
+ description: "Use when creating a self-contained Norn project config and local run state directory in the current directory.",
103
+ usage: "norn project init",
104
+ output: "JSON object with initialized project metadata under project.",
105
+ examples: ["norn project init"],
106
+ execute: async (args) => {
107
+ assertNoExtraArgs("project init", args);
108
+ await initProject();
109
+ }
110
+ },
111
+ {
112
+ id: "project.inspect",
113
+ path: ["project", "inspect"],
114
+ description: "Use when discovering the active Norn project, plugins, workflow sources, and Seer mode.",
115
+ usage: "norn project inspect",
116
+ output: "JSON object with project metadata under project, isComplete, and plugin diagnostics. Incomplete discovery does not permit execution.",
117
+ examples: ["norn project inspect"],
118
+ execute: async (args) => {
119
+ assertNoExtraArgs("project inspect", args);
120
+ await inspectProject();
121
+ }
122
+ },
123
+ {
124
+ id: "seer.inspect",
125
+ path: ["seer", "inspect"],
126
+ description: "Use when checking the current project's resolved Seer mode before agent execution.",
127
+ usage: "norn seer inspect",
128
+ output: "JSON object with resolved Seer mode under seerMode.",
129
+ examples: ["norn seer inspect"],
130
+ execute: async (args) => {
131
+ assertNoExtraArgs("seer inspect", args);
132
+ await inspectSeerMode();
133
+ }
134
+ },
135
+ {
136
+ id: "workflows.list",
137
+ path: ["workflows", "list"],
138
+ description: "Use when selecting a Norn workflow for a user task; defaults to entrypoint workflows.",
139
+ usage: "norn workflows list [--entrypoints|--all]",
140
+ options: ["--entrypoints: list entrypoint workflows", "--all: include internal workflow steps"],
141
+ output: "JSON object with workflow summaries under workflows, isComplete, and plugin diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
142
+ examples: ["norn workflows list", "norn workflows list --all"],
143
+ execute: listWorkflows
144
+ },
145
+ {
146
+ id: "workflows.inspect",
147
+ path: ["workflows", "inspect"],
148
+ description: "Use when reading a workflow's instructions, params schema, gate contract, and source plugin before starting or editing it.",
149
+ usage: "norn workflows inspect <workflow-id>",
150
+ arguments: ["workflow-id: fully qualified workflow id"],
151
+ 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.",
152
+ examples: ["norn workflows inspect example.plan"],
153
+ execute: async (args) => {
154
+ const workflowId = requiredArg("workflows inspect", args, 0, "workflow id");
155
+ assertNoExtraArgs("workflows inspect", args.slice(1));
156
+ await inspectWorkflow(workflowId);
157
+ }
158
+ },
159
+ {
160
+ id: "runs.start",
161
+ path: ["runs", "start"],
162
+ description: "Use when starting a Norn workflow run after the workflow id and params are known.",
163
+ usage: "norn runs start <workflow-id>",
164
+ arguments: ["workflow-id: fully qualified workflow id to start"],
165
+ stdin: 'Optional JSON object: {"params":{...},"config":{"pluginId":{...}}}.',
166
+ output: "JSON object with started run info under run.",
167
+ examples: [`printf '{"params":{"task":"Add tests"}}' | norn runs start example.plan`],
168
+ execute: async (args) => {
169
+ const workflowId = requiredArg("runs start", args, 0, "workflow id");
170
+ await startRun(workflowId, args.slice(1));
171
+ }
172
+ },
173
+ {
174
+ id: "runs.resume",
175
+ path: ["runs", "resume"],
176
+ description: "Use when resuming an interrupted gate or a checkpoint restored for retry.",
177
+ usage: "norn runs resume <run>",
178
+ arguments: ["run: run id, generated name, or run path"],
179
+ stdin: 'Optional JSON object: {"params":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept params.',
180
+ output: "JSON object with resumed run info under run.",
181
+ examples: [`printf '{"params":{"decision":"accept"}}' | norn runs resume quiet-river-lantern`],
182
+ execute: async (args) => {
183
+ const run = requiredArg("runs resume", args, 0, "run");
184
+ await resumeRun(run, args.slice(1));
185
+ }
186
+ },
187
+ {
188
+ id: "runs.wait",
189
+ path: ["runs", "wait"],
190
+ description: "Use when waiting for a Norn run to finish, fail, interrupt, or become unhealthy.",
191
+ usage: "norn runs wait <run>",
192
+ arguments: ["run: run id, generated name, or run path"],
193
+ output: "JSON object with final or current run info under run.",
194
+ examples: ["norn runs wait quiet-river-lantern"],
195
+ execute: async (args) => {
196
+ const run = requiredArg("runs wait", args, 0, "run");
197
+ assertNoExtraArgs("runs wait", args.slice(1));
198
+ await waitRun(run);
199
+ }
200
+ },
201
+ {
202
+ id: "runs.list",
203
+ path: ["runs", "list"],
204
+ description: "Use when listing known Norn runs in the current project.",
205
+ usage: "norn runs list",
206
+ output: "JSON object with run summaries under runs.",
207
+ examples: ["norn runs list"],
208
+ execute: async (args) => {
209
+ assertNoExtraArgs("runs list", args);
210
+ writeJson({ runs: await listCurrentProjectRuns() });
211
+ }
212
+ },
213
+ {
214
+ id: "runs.inspect",
215
+ path: ["runs", "inspect"],
216
+ description: "Use when reading status, health, current workflow, interruption, and outcome details for one Norn run.",
217
+ usage: "norn runs inspect <run>",
218
+ arguments: ["run: run id, generated name, or run path"],
219
+ output: "JSON object with run details under run.",
220
+ examples: ["norn runs inspect quiet-river-lantern"],
221
+ execute: async (args) => {
222
+ const run = requiredArg("runs inspect", args, 0, "run");
223
+ assertNoExtraArgs("runs inspect", args.slice(1));
224
+ writeJson({ run: await inspectRun(run) });
225
+ }
226
+ },
227
+ {
228
+ id: "runs.checkpoints",
229
+ path: ["runs", "checkpoints"],
230
+ description: "Use when finding rollback points before retrying or repairing a Norn run.",
231
+ usage: "norn runs checkpoints <run>",
232
+ arguments: ["run: run id, generated name, or run path"],
233
+ output: "JSON object with checkpoints under checkpoints.",
234
+ examples: ["norn runs checkpoints quiet-river-lantern"],
235
+ execute: async (args) => {
236
+ const run = requiredArg("runs checkpoints", args, 0, "run");
237
+ assertNoExtraArgs("runs checkpoints", args.slice(1));
238
+ const project = await findNornProject(process.cwd());
239
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
240
+ writeJson({ checkpoints: await (await NornRunStore.open(runRoot)).listCheckpoints() });
241
+ }
242
+ },
243
+ {
244
+ id: "runs.metrics",
245
+ path: ["runs", "metrics"],
246
+ description: "Use when measuring workflow, agent, command, token, and cost totals for a Norn run.",
247
+ usage: "norn runs metrics <run>",
248
+ arguments: ["run: run id, generated name, or run path"],
249
+ output: "JSON object with metrics under metrics.",
250
+ examples: ["norn runs metrics quiet-river-lantern"],
251
+ execute: async (args) => {
252
+ const run = requiredArg("runs metrics", args, 0, "run");
253
+ assertNoExtraArgs("runs metrics", args.slice(1));
254
+ const project = await findNornProject(process.cwd());
255
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
256
+ writeJson({ metrics: await readRunMetrics(runRoot) });
257
+ }
258
+ },
259
+ {
260
+ id: "runs.logs",
261
+ path: ["runs", "logs"],
262
+ description: "Use when streaming or reading chronological JSON events for a Norn run.",
263
+ usage: "norn runs logs <run> [--follow]",
264
+ arguments: ["run: run id, generated name, or run path"],
265
+ options: ["--follow: continue streaming until the run stops"],
266
+ output: "JSON Lines stream of run events.",
267
+ examples: ["norn runs logs quiet-river-lantern", "norn runs logs quiet-river-lantern --follow"],
268
+ execute: async (args) => {
269
+ const run = requiredArg("runs logs", args, 0, "run");
270
+ const logArgs = args.slice(1);
271
+ assertKnownFlags("runs logs", logArgs, ["--follow"]);
272
+ await writeRunLogs(run, { follow: logArgs.includes("--follow") });
273
+ }
274
+ },
275
+ {
276
+ id: "runs.rollback",
277
+ path: ["runs", "rollback"],
278
+ description: "Use when restoring a failed or stopped run to a checkpoint and marking it pending resume.",
279
+ usage: "norn runs rollback <run> <checkpoint-id>",
280
+ arguments: ["run: run id, generated name, or run path", "checkpoint-id: checkpoint id from runs.checkpoints"],
281
+ output: "JSON object with rolled-back run info under run.",
282
+ examples: ["norn runs rollback quiet-river-lantern checkpoint-1"],
283
+ execute: async (args) => {
284
+ const run = requiredArg("runs rollback", args, 0, "run");
285
+ await rollbackRun(run, args.slice(1));
286
+ }
287
+ },
288
+ {
289
+ id: "runs.stop",
290
+ path: ["runs", "stop"],
291
+ description: "Use when stopping a running Norn execution while preserving its dirty state for inspection or rollback.",
292
+ usage: "norn runs stop <run>",
293
+ arguments: ["run: run id, generated name, or run path"],
294
+ output: "JSON object with updated run info under run.",
295
+ examples: ["norn runs stop quiet-river-lantern"],
296
+ execute: async (args) => {
297
+ const run = requiredArg("runs stop", args, 0, "run");
298
+ assertNoExtraArgs("runs stop", args.slice(1));
299
+ await signalRun(run, "SIGTERM");
300
+ }
301
+ },
302
+ {
303
+ id: "runs.kill",
304
+ path: ["runs", "kill"],
305
+ description: "Use when force-stopping a Norn execution process that did not stop politely.",
306
+ usage: "norn runs kill <run>",
307
+ arguments: ["run: run id, generated name, or run path"],
308
+ output: "JSON object with updated run info under run.",
309
+ examples: ["norn runs kill quiet-river-lantern"],
310
+ execute: async (args) => {
311
+ const run = requiredArg("runs kill", args, 0, "run");
312
+ assertNoExtraArgs("runs kill", args.slice(1));
313
+ await signalRun(run, "SIGKILL");
314
+ }
315
+ },
316
+ {
317
+ id: "runs.delete",
318
+ path: ["runs", "delete"],
319
+ description: "Use when deleting an inactive Norn run directory after evidence is no longer needed.",
320
+ usage: "norn runs delete <run>",
321
+ arguments: ["run: run id, generated name, or run path"],
322
+ output: "JSON object with deleted run identity under deleted.",
323
+ examples: ["norn runs delete quiet-river-lantern"],
324
+ execute: async (args) => {
325
+ const run = requiredArg("runs delete", args, 0, "run");
326
+ assertNoExtraArgs("runs delete", args.slice(1));
327
+ writeJson({ deleted: await deleteRun(run) });
328
+ }
329
+ },
330
+ {
331
+ id: "execute-run",
332
+ path: ["execute-run"],
333
+ description: "Use internally when executing a previously launched detached run request.",
334
+ usage: "norn execute-run <run-id>",
335
+ arguments: ["run-id: internal run id"],
336
+ output: "No stable stdout contract; execution state is persisted in the run directory.",
337
+ hidden: true,
338
+ examples: ["norn execute-run 00000000-0000-0000-0000-000000000000"],
339
+ execute: async (args) => {
340
+ const runId = requiredArg("execute-run", args, 0, "run id");
341
+ assertNoExtraArgs("execute-run", args.slice(1));
342
+ await executeRun(runId);
343
+ }
344
+ },
345
+ {
346
+ id: "upgrade",
347
+ path: ["upgrade"],
348
+ description: "Use when upgrading this Norn CLI installation according to explicit build metadata.",
349
+ usage: "norn upgrade [--dry-run]",
350
+ options: ["--dry-run: report the upgrade plan without changing files or running installers"],
351
+ output: "JSON object with upgrade status, plan, or unsupported reason under upgrade.",
352
+ examples: ["norn upgrade --dry-run", "norn upgrade"],
353
+ execute: upgradeNorn
354
+ },
355
+ {
356
+ id: "version",
357
+ path: ["version"],
358
+ description: "Use when checking the installed Norn CLI version and explicit build metadata.",
359
+ usage: "norn version",
360
+ output: "JSON object with package version under version and build metadata under build.",
361
+ examples: ["norn version", "norn --version"],
362
+ execute: async (args) => {
363
+ assertNoExtraArgs("version", args);
364
+ writeJson(await versionInfo());
365
+ }
366
+ }
367
+ ];
368
+ var HELP_COMMAND_ORDER = [
369
+ "commands.list",
370
+ "commands.inspect",
371
+ "docs.inspect",
372
+ "docs.intro",
373
+ "workflows.list",
374
+ "workflows.inspect",
375
+ "runs.start",
376
+ "runs.resume",
377
+ "runs.wait",
378
+ "runs.list",
379
+ "runs.inspect",
380
+ "runs.logs",
381
+ "runs.checkpoints",
382
+ "runs.rollback",
383
+ "runs.metrics",
384
+ "runs.stop",
385
+ "runs.kill",
386
+ "runs.delete",
387
+ "project.init",
388
+ "project.inspect",
389
+ "seer.inspect",
390
+ "pi",
391
+ "upgrade",
392
+ "version",
393
+ "help"
394
+ ];
395
+ var HUMAN_COMMAND_SUMMARIES = {
396
+ "commands.list": "List machine-readable command metadata.",
397
+ "commands.inspect": "Inspect one command's machine-readable contract.",
398
+ "docs.inspect": "Locate matching documentation and examples offline.",
399
+ "docs.intro": "Produce a compact authoring introduction with local documentation pointers.",
400
+ "workflows.list": "List Norn workflows.",
401
+ "workflows.inspect": "Inspect a workflow schema and source.",
402
+ "runs.start": "Start a workflow run.",
403
+ "runs.resume": "Resume an interrupted gate or a restored checkpoint.",
404
+ "runs.wait": "Wait for a run to stop running.",
405
+ "runs.list": "List known runs.",
406
+ "runs.inspect": "Inspect a run.",
407
+ "runs.logs": "Read run event logs.",
408
+ "runs.checkpoints": "List run rollback checkpoints.",
409
+ "runs.rollback": "Restore a run checkpoint for a manual resume.",
410
+ "runs.metrics": "Inspect run metrics.",
411
+ "runs.stop": "Stop a running run.",
412
+ "runs.kill": "Force-stop a running run.",
413
+ "runs.delete": "Delete an inactive run.",
414
+ "project.init": "Create a Norn project in the current directory.",
415
+ "project.inspect": "Inspect the Norn project.",
416
+ "seer.inspect": "Inspect resolved Seer mode.",
417
+ pi: "Run bundled Pi for authentication, providers, and model setup.",
418
+ upgrade: "Upgrade the installed Norn CLI.",
419
+ version: "Print version/build info.",
420
+ help: "Show concise command help."
421
+ };
422
+ async function main(args, documentationSource) {
423
+ if (args[0] === "pi") {
424
+ await runPi([...args.slice(1)]);
425
+ return;
426
+ }
427
+ try {
428
+ await runCommand(args, documentationSource);
429
+ } catch (error) {
430
+ writeJson({ error: {
431
+ code: errorCode(error),
432
+ message: errorMessage(error),
433
+ ...error instanceof NornProjectLoadError ? { isComplete: error.isComplete, diagnostics: error.diagnostics } : {}
434
+ } });
435
+ process.exitCode = 1;
436
+ }
437
+ }
438
+ async function runCommand(args, documentationSource) {
439
+ if (args.length === 0) {
440
+ writeCliHelp([]);
441
+ return;
442
+ }
443
+ if (isVersionRequest(args)) {
444
+ writeJson(await versionInfo());
445
+ return;
446
+ }
447
+ const helpPath = cliHelpPath(args);
448
+ if (helpPath) {
449
+ writeCliHelp(helpPath);
450
+ return;
451
+ }
452
+ const command = findCliCommand(args);
453
+ if (!command) throw new Error(`Unknown norn command: ${args.join(" ")}`);
454
+ await command.execute(args.slice(command.path.length), documentationSource);
455
+ }
456
+ async function listCliCommands(args) {
457
+ assertKnownFlags("commands list", args, ["--all"]);
458
+ const shouldIncludeHidden = args.includes("--all");
459
+ writeJson({ commands: COMMANDS.filter((command) => shouldIncludeHidden || !command.hidden).map(cliCommandInfo) });
460
+ }
461
+ async function inspectCliCommand(args) {
462
+ const commandId = requiredArg("commands inspect", args, 0, "command id");
463
+ assertNoExtraArgs("commands inspect", args.slice(1));
464
+ const command = COMMANDS.find((candidate) => candidate.id === commandId);
465
+ if (!command) throw new Error(`Unknown norn command id: ${commandId}`);
466
+ writeJson({ command: cliCommandInfo(command) });
467
+ }
468
+ function cliCommandInfo(command) {
469
+ return {
470
+ id: command.id,
471
+ path: command.path,
472
+ description: command.description,
473
+ usage: command.usage,
474
+ arguments: command.arguments,
475
+ options: command.options,
476
+ stdin: command.stdin,
477
+ output: command.output,
478
+ examples: command.examples,
479
+ hidden: command.hidden
480
+ };
481
+ }
482
+ function isVersionRequest(args) {
483
+ return args.length === 1 && (args[0] === "--version" || args[0] === "-v");
484
+ }
485
+ function cliHelpPath(args) {
486
+ if (args[0] === "help") return args.slice(1);
487
+ if (!args.includes("--help") && !args.includes("-h")) return void 0;
488
+ return args.filter((arg) => arg !== "--help" && arg !== "-h");
489
+ }
490
+ function writeCliHelp(path) {
491
+ const command = findExactCliCommand(path);
492
+ if (command) {
493
+ process.stdout.write(renderCommandHelp(command));
494
+ return;
495
+ }
496
+ const groupCommands = sortedCommandsForHelp(visibleCommands().filter((candidate) => path.length === 0 || startsWithPath(candidate.path, path)));
497
+ if (groupCommands.length === 0) throw new Error(`Unknown norn help topic: ${path.join(" ")}`);
498
+ process.stdout.write(renderGroupHelp(path, groupCommands));
499
+ }
500
+ function renderGroupHelp(path, commands) {
501
+ return [
502
+ "Norn",
503
+ "",
504
+ CLI_DESCRIPTION,
505
+ "",
506
+ "Usage:",
507
+ ...groupHelpUsage(path).map((line) => ` ${line}`),
508
+ "",
509
+ "Commands:",
510
+ renderCommandSummary(commands),
511
+ "",
512
+ "Machine-readable metadata:",
513
+ " norn commands list",
514
+ " norn commands inspect <command-id>",
515
+ ""
516
+ ].join("\n");
517
+ }
518
+ function renderCommandHelp(command) {
519
+ return [
520
+ commandHumanSummary(command),
521
+ "",
522
+ "Usage:",
523
+ ` ${command.usage}`,
524
+ "",
525
+ "Description:",
526
+ ` ${command.description}`,
527
+ ...command.arguments ? helpSection("Arguments", command.arguments) : [],
528
+ ...command.options ? helpSection("Options", command.options) : [],
529
+ ...command.stdin ? ["", "Stdin:", ` ${command.stdin}`] : [],
530
+ "",
531
+ "Output:",
532
+ ` ${command.output}`,
533
+ ...helpSection("Examples", command.examples),
534
+ ""
535
+ ].join("\n");
536
+ }
537
+ function renderCommandSummary(commands) {
538
+ const commandNames = commands.map((command) => command.path.join(" "));
539
+ const width = Math.max(...commandNames.map((name) => name.length));
540
+ return commands.map((command, index) => ` ${commandNames[index].padEnd(width)} ${commandHumanSummary(command)}`).join("\n");
541
+ }
542
+ function helpSection(title, lines) {
543
+ return ["", `${title}:`, ...lines.map((line) => ` ${line}`)];
544
+ }
545
+ function groupHelpUsage(path) {
546
+ if (path.length === 0) return ["norn <command> [args]", "norn help <command>", "norn --version"];
547
+ const prefix = `norn ${path.join(" ")}`;
548
+ return [`${prefix} <command> [args]`, `norn help ${path.join(" ")} <command>`, "norn --version"];
549
+ }
550
+ function findCliCommand(args) {
551
+ return sortedCommandsByPathLength(COMMANDS).find((command) => startsWithPath(args, command.path));
552
+ }
553
+ function findExactCliCommand(path) {
554
+ return COMMANDS.find((command) => command.path.length === path.length && startsWithPath(command.path, path));
555
+ }
556
+ function sortedCommandsByPathLength(commands) {
557
+ return [...commands].sort((left, right) => right.path.length - left.path.length);
558
+ }
559
+ function sortedCommandsForHelp(commands) {
560
+ return [...commands].sort((left, right) => commandHelpOrder(left) - commandHelpOrder(right));
561
+ }
562
+ function commandHelpOrder(command) {
563
+ const index = HELP_COMMAND_ORDER.indexOf(command.id);
564
+ return index === -1 ? HELP_COMMAND_ORDER.length : index;
565
+ }
566
+ function commandHumanSummary(command) {
567
+ return HUMAN_COMMAND_SUMMARIES[command.id] ?? command.description;
568
+ }
569
+ function visibleCommands() {
570
+ return COMMANDS.filter((command) => !command.hidden);
571
+ }
572
+ function startsWithPath(value, path) {
573
+ return path.every((segment, index) => value[index] === segment);
574
+ }
575
+ async function resolveCliDocumentation(source) {
576
+ return resolveNornDocumentation({
577
+ source,
578
+ build: { ...NORN_BUILD_INFO, version: await readPackageVersion() },
579
+ cacheRoot: resolveDocumentationCacheRoot({ platform: process.platform, home: homedir(), environment: process.env })
580
+ });
581
+ }
582
+ async function versionInfo() {
583
+ return { version: await readPackageVersion(), build: NORN_BUILD_INFO };
584
+ }
585
+ async function readPackageVersion() {
586
+ try {
587
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
588
+ if (typeof packageJson.version !== "string") throw new Error("Invalid Norn package version");
589
+ return packageJson.version;
590
+ } catch (error) {
591
+ if (isNodeError(error) && error.code === "ENOENT") return NORN_BUILD_INFO.version;
592
+ throw error;
593
+ }
594
+ }
595
+ async function upgradeNorn(args) {
596
+ assertKnownFlags("upgrade", args, ["--dry-run"]);
597
+ const dryRun = args.includes("--dry-run");
598
+ const currentVersion = await readPackageVersion();
599
+ const build = NORN_BUILD_INFO;
600
+ if (build.kind !== "github-release-binary") {
601
+ writeJson({ upgrade: unsupportedUpgrade(build, currentVersion, build.upgrade.reason) });
602
+ return;
603
+ }
604
+ writeJson({ upgrade: await upgradeGithubReleaseBinary(build, currentVersion, dryRun) });
605
+ }
606
+ function unsupportedUpgrade(build, currentVersion, reason) {
607
+ return {
608
+ supported: false,
609
+ kind: build.kind,
610
+ currentVersion,
611
+ buildVersion: build.version,
612
+ reason
613
+ };
614
+ }
615
+ async function upgradeGithubReleaseBinary(build, currentVersion, dryRun) {
616
+ const plan = githubReleaseBinaryUpgradePlan(build, currentVersion, dryRun);
617
+ if (dryRun) return { ...plan, status: "planned" };
618
+ if (process.platform === "win32") return unsupportedUpgrade(build, currentVersion, "Replacing a running Windows executable is not supported yet.");
619
+ const binary = await downloadReleaseAsset(plan.downloadUrl);
620
+ const checksumText = await downloadReleaseText(plan.checksumUrl);
621
+ const expectedChecksum = parseSha256Checksum(checksumText, build.checksumAssetName);
622
+ const actualChecksum = createHash("sha256").update(Buffer.from(binary)).digest("hex");
623
+ if (actualChecksum !== expectedChecksum) throw new Error(`Downloaded Norn binary checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`);
624
+ const tempPath = join(dirname(plan.targetPath), `.norn-upgrade-${process.pid}-${build.assetName}`);
625
+ try {
626
+ await writeFile(tempPath, binary);
627
+ await chmod(tempPath, 493);
628
+ await rename(tempPath, plan.targetPath);
629
+ } catch (error) {
630
+ await rm(tempPath, { force: true });
631
+ throw error;
632
+ }
633
+ return { ...plan, status: "completed", checksum: actualChecksum };
634
+ }
635
+ function githubReleaseBinaryUpgradePlan(build, currentVersion, dryRun) {
636
+ return {
637
+ supported: true,
638
+ kind: build.kind,
639
+ dryRun,
640
+ currentVersion,
641
+ buildVersion: build.version,
642
+ repository: build.repository,
643
+ releaseTag: build.releaseTag,
644
+ assetName: build.assetName,
645
+ checksumAssetName: build.checksumAssetName,
646
+ downloadUrl: githubReleaseDownloadUrl(build, build.assetName),
647
+ checksumUrl: githubReleaseDownloadUrl(build, build.checksumAssetName),
648
+ targetPath: process.execPath
649
+ };
650
+ }
651
+ function githubReleaseDownloadUrl(build, assetName) {
652
+ return `https://github.com/${build.repository}/releases/download/${build.releaseTag}/${assetName}`;
653
+ }
654
+ async function downloadReleaseAsset(url) {
655
+ const response = await fetch(url);
656
+ if (!response.ok) throw new Error(`Failed to download Norn release asset: ${url} returned ${response.status}`);
657
+ return new Uint8Array(await response.arrayBuffer());
658
+ }
659
+ async function downloadReleaseText(url) {
660
+ const response = await fetch(url);
661
+ if (!response.ok) throw new Error(`Failed to download Norn release metadata: ${url} returned ${response.status}`);
662
+ return response.text();
663
+ }
664
+ function parseSha256Checksum(text, checksumAssetName) {
665
+ const checksum = text.trim().split(/\s+/)[0] ?? "";
666
+ if (!/^[a-f0-9]{64}$/i.test(checksum)) throw new Error(`Invalid Norn checksum asset: ${checksumAssetName}`);
667
+ return checksum.toLowerCase();
668
+ }
669
+ function requiredArg(command, args, index, label) {
670
+ const value = args[index];
671
+ if (!value) throw new Error(`Missing ${label} for ${command}`);
672
+ return value;
673
+ }
674
+ function assertNoExtraArgs(command, args) {
675
+ if (args.length > 0) throw new Error(`${command} does not accept CLI arguments: ${args.join(" ")}`);
676
+ }
677
+ function assertKnownFlags(command, args, flags) {
678
+ const allowedFlags = new Set(flags);
679
+ const unsupportedFlags = args.filter((arg) => !allowedFlags.has(arg));
680
+ if (unsupportedFlags.length > 0) throw new Error(`${command} has unsupported flags or arguments: ${unsupportedFlags.join(" ")}`);
681
+ }
682
+ async function listWorkflows(args) {
683
+ assertKnownFlags("workflows list", args, ["--entrypoints", "--all"]);
684
+ const entrypointsOnly = workflowListEntrypointsOnly(args);
685
+ const { workflows, isComplete, diagnostics } = await discoverNornProject(process.cwd());
686
+ writeJson({ workflows: entrypointsOnly ? workflows.filter((workflow) => workflow.isEntrypoint) : workflows, isComplete, diagnostics });
687
+ }
688
+ async function inspectWorkflow(workflowId) {
689
+ writeJson(await inspectNornWorkflow({ cwd: process.cwd(), workflowId }));
690
+ }
691
+ function workflowListEntrypointsOnly(args) {
692
+ const entrypoints = args.includes("--entrypoints");
693
+ const all = args.includes("--all");
694
+ if (entrypoints && all) throw new Error("Use either --entrypoints or --all, not both");
695
+ return !all;
696
+ }
697
+ async function initProject() {
698
+ const projectRoot = process.cwd();
699
+ const projectPath = resolve(projectRoot, NORN_PROJECT_FILE_NAME);
700
+ if (await isFile(projectPath)) throw new Error(`Norn project already exists: ${projectPath}`);
701
+ await mkdir(resolve(projectRoot, RUNS_ROOT), { recursive: true });
702
+ await writeFile(projectPath, `${JSON.stringify({ version: 1, plugins: [], includes: [], config: {} }, null, 2)}
703
+ `, "utf8");
704
+ await ensureGitignoreExcludesRunState(projectRoot);
705
+ writeJson({ project: { path: projectPath, root: projectRoot, runsRoot: resolve(projectRoot, RUNS_ROOT) } });
706
+ }
707
+ async function listCurrentProjectRuns() {
708
+ const project = await findNornProject(process.cwd());
709
+ return listRuns(project.projectRoot);
710
+ }
711
+ async function inspectProject() {
712
+ const { project, isComplete, diagnostics } = await discoverNornProject(process.cwd());
713
+ writeJson({ project, isComplete, diagnostics });
714
+ }
715
+ async function inspectSeerMode() {
716
+ const project = await findNornProject(process.cwd());
717
+ writeJson({ seerMode: project.seerMode ?? null });
718
+ }
719
+ async function startRun(workflowId, args) {
720
+ assertNoStructuredInputArgs("runs start", args);
721
+ const project = await loadNornProject(process.cwd());
722
+ const workflow = project.registry.workflowById(workflowId);
723
+ if (!workflow) throw new Error(`Unknown workflow: ${workflowId}`);
724
+ const input = parseStartRunInput(await readStdinJson());
725
+ const params = workflow.params.parse(input.params ?? {});
726
+ const configOverride = input.config;
727
+ const id = randomUUID();
728
+ const name = generateRunName(new Set((await listRuns(project.projectRoot)).map((run) => run.name)));
729
+ const runRoot = resolve(project.projectRoot, RUNS_ROOT, id);
730
+ await mkdir(runRoot, { recursive: true });
731
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
732
+ await writeRunLaunchRequest(runRoot, { version: 1, type: "run", id, name, workflowId, params, configOverride, createdAt });
733
+ await startDetachedExecuteRun(id, project.projectRoot);
734
+ writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, createdAt }) });
735
+ }
736
+ async function resumeRun(run, args) {
737
+ assertNoStructuredInputArgs("runs resume", args);
738
+ const project = await loadNornProject(process.cwd());
739
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
740
+ const input = parseResumeRunInput(await readStdinJson());
741
+ const lease = await NornRunLease.acquire(runRoot);
742
+ let request;
743
+ try {
744
+ const runInfo = await getRunInfo(runRoot);
745
+ const params = await parseResumeParams(runInfo, input.params);
746
+ request = { version: 1, type: "resume", id: runInfo.id, requestId: randomUUID(), params, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
747
+ await writeRunResumeRequest(runRoot, request);
748
+ } finally {
749
+ await lease.release();
750
+ }
751
+ try {
752
+ await startDetachedExecuteRun(request.id, project.projectRoot);
753
+ } catch (error) {
754
+ await removeResumeRequest({ runRoot, request });
755
+ throw error;
756
+ }
757
+ writeJson({ run: await getRunInfo(runRoot) });
758
+ }
759
+ async function parseResumeParams(runInfo, params) {
760
+ if (runInfo.status === "interrupted") {
761
+ if (params === void 0) throw new Error(`Interrupted workflow resume requires params: ${runInfo.name}`);
762
+ if (!runInfo.currentWorkflowId) throw new Error(`Run has no current workflow: ${runInfo.name}`);
763
+ const project = await loadNornProject(process.cwd());
764
+ const workflow = project.registry.workflowById(runInfo.currentWorkflowId);
765
+ if (!workflow) throw new Error(`Unknown workflow for resumed run: ${runInfo.currentWorkflowId}`);
766
+ workflow.params.parse(mergeInterruptedWorkflowParams(runInfo.interruption?.params, params, runInfo.interruption?.fields));
767
+ return params;
768
+ }
769
+ if (runInfo.status === "pendingResume") {
770
+ if (params !== void 0) throw new Error(`Pending-resume workflows do not accept params: ${runInfo.name}`);
771
+ return void 0;
772
+ }
773
+ throw new Error(`Run must be rolled back before resuming: ${runInfo.name}`);
774
+ }
775
+ async function waitRun(run) {
776
+ const project = await findNornProject(process.cwd());
777
+ writeJson({ run: await waitForInactiveRun(project.projectRoot, run) });
778
+ }
779
+ async function waitForInactiveRun(projectRoot, run) {
780
+ let runRoot;
781
+ while (true) {
782
+ runRoot ??= await resolveWaitableRunRoot(projectRoot, run);
783
+ try {
784
+ const runInfo = await readRunInspection(runRoot);
785
+ if (runInfo.status !== "running" || runInfo.health === "unhealthy") return runInfo;
786
+ } catch (error) {
787
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
788
+ }
789
+ await delay(RUN_WAIT_INTERVAL_MS);
790
+ }
791
+ }
792
+ async function resolveWaitableRunRoot(sessionCwd, run) {
793
+ try {
794
+ return await resolveRunRoot(sessionCwd, run);
795
+ } catch (error) {
796
+ for (const candidate of [resolve(sessionCwd, run), resolve(sessionCwd, RUNS_ROOT, run)]) {
797
+ if (await isDirectory(candidate)) return candidate;
798
+ }
799
+ throw error;
800
+ }
801
+ }
802
+ async function isDirectory(path) {
803
+ try {
804
+ return (await stat(path)).isDirectory();
805
+ } catch (error) {
806
+ if (isNodeError(error) && error.code === "ENOENT") return false;
807
+ throw error;
808
+ }
809
+ }
810
+ async function isFile(path) {
811
+ try {
812
+ return (await stat(path)).isFile();
813
+ } catch (error) {
814
+ if (isNodeError(error) && error.code === "ENOENT") return false;
815
+ throw error;
816
+ }
817
+ }
818
+ async function ensureGitignoreExcludesRunState(projectRoot) {
819
+ const gitignorePath = resolve(projectRoot, ".gitignore");
820
+ const runStatePattern = ".norn/runs/";
821
+ let currentText = "";
822
+ try {
823
+ currentText = await readFile(gitignorePath, "utf8");
824
+ } catch (error) {
825
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
826
+ }
827
+ if (currentText.split(/\r?\n/).includes(runStatePattern)) return;
828
+ const separator = currentText.length === 0 || currentText.endsWith("\n") ? "" : "\n";
829
+ await writeFile(gitignorePath, `${currentText}${separator}${runStatePattern}
830
+ `, "utf8");
831
+ }
832
+ async function executeRun(runId) {
833
+ const abortController = new AbortController();
834
+ process.once("SIGTERM", () => abortController.abort(new NornRunStoppedError()));
835
+ process.once("SIGINT", () => abortController.abort(new NornRunStoppedError()));
836
+ const location = await findNornProject(process.cwd());
837
+ const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
838
+ const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
839
+ try {
840
+ const project = await loadNornProject(process.cwd());
841
+ const engine = new NornEngine({ cwd: project.projectRoot, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
842
+ for (const plugin of project.plugins) engine.registerPlugin(plugin);
843
+ if (request.type === "run") {
844
+ const workflow = project.registry.workflowById(request.workflowId);
845
+ if (!workflow) throw new Error(`Unknown workflow: ${request.workflowId}`);
846
+ await engine.runWorkflow(workflow, request.params, { id: request.id, name: request.name, configOverride: request.configOverride });
847
+ } else {
848
+ await engine.resumeRequestedWorkflow({ runRoot, request });
849
+ }
850
+ } finally {
851
+ if (request.type === "run") await rm(join(runRoot, "launch-request.json"), { force: true });
852
+ else await removeResumeRequest({ runRoot, request });
853
+ }
854
+ }
855
+ async function removeResumeRequest(input) {
856
+ const lease = await NornRunLease.acquire(input.runRoot);
857
+ try {
858
+ await clearRunResumeRequest({ ...input, lease });
859
+ } finally {
860
+ await lease.release();
861
+ }
862
+ }
863
+ async function rollbackRun(run, args) {
864
+ const checkpointId = requiredArg("runs rollback", args, 0, "checkpoint id");
865
+ assertNoExtraArgs("runs rollback", args.slice(1));
866
+ const project = await findNornProject(process.cwd());
867
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
868
+ const engine = new NornEngine({ cwd: project.projectRoot, gateMode: "pause" });
869
+ writeJson({ run: await engine.rollbackRun(runRoot, checkpointId) });
870
+ }
871
+ async function inspectRun(run) {
872
+ const project = await findNornProject(process.cwd());
873
+ return readRunInspection(await resolveRunRoot(project.projectRoot, run));
874
+ }
875
+ async function readRunInspection(runRoot) {
876
+ return getRunInfo(runRoot);
877
+ }
878
+ async function signalRun(run, signal) {
879
+ const project = await findNornProject(process.cwd());
880
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
881
+ await terminateRun(runRoot, signal);
882
+ writeJson({ run: await getRunInfo(runRoot) });
883
+ }
884
+ async function deleteRun(run) {
885
+ const project = await findNornProject(process.cwd());
886
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
887
+ const runInfo = await getRunInfo(runRoot);
888
+ if (runInfo.status === "running") throw new Error(`Stop run before deleting it: ${runInfo.name}`);
889
+ await rm(runRoot, { recursive: true, force: true });
890
+ return { id: runInfo.id, name: runInfo.name, path: runInfo.path };
891
+ }
892
+ async function terminateRun(runRoot, signal) {
893
+ const owner = await getRunLeaseOwner(runRoot);
894
+ if (!owner) return;
895
+ sendSignal(owner.processGroupId, owner.pid, signal);
896
+ await delay(signal === "SIGKILL" ? 250 : 1e3);
897
+ }
898
+ async function writeRunLogs(run, options) {
899
+ const project = await findNornProject(process.cwd());
900
+ const runRoot = await resolveRunRoot(project.projectRoot, run);
901
+ let written = 0;
902
+ while (true) {
903
+ const events = await readRunEvents(runRoot);
904
+ for (const event of events.slice(written)) process.stdout.write(`${JSON.stringify(event)}
905
+ `);
906
+ written = events.length;
907
+ if (!options.follow) return;
908
+ const info = await getRunInfo(runRoot);
909
+ if (info.status !== "running" && written >= events.length) return;
910
+ await delay(1e3);
911
+ }
912
+ }
913
+ async function startDetachedExecuteRun(runId, projectRoot) {
914
+ const child = spawn(process.execPath, detachedExecuteRunArgs(runId), {
915
+ cwd: projectRoot,
916
+ detached: true,
917
+ stdio: "ignore"
918
+ });
919
+ await new Promise((resolveSpawn, reject) => {
920
+ child.once("spawn", resolveSpawn);
921
+ child.once("error", reject);
922
+ });
923
+ child.unref();
924
+ }
925
+ function detachedExecuteRunArgs(runId) {
926
+ if (NORN_BUILD_INFO.kind === "github-release-binary") return ["execute-run", runId];
927
+ const scriptPath = process.argv[1];
928
+ return scriptPath && isAbsolute(scriptPath) && /\.(?:c?m?js|ts)$/.test(scriptPath) ? [scriptPath, "execute-run", runId] : ["execute-run", runId];
929
+ }
930
+ function startedRunInfo(input) {
931
+ return {
932
+ version: 1,
933
+ id: input.id,
934
+ name: input.name,
935
+ path: input.runRoot,
936
+ entrypointWorkflowId: input.workflow.id,
937
+ currentWorkflowId: input.workflow.id,
938
+ status: "running",
939
+ health: "healthy",
940
+ startedAt: input.createdAt,
941
+ updatedAt: input.createdAt
942
+ };
943
+ }
944
+ async function readOptionalRunLaunchRequest(runRoot) {
945
+ try {
946
+ return await readRunLaunchRequest(runRoot);
947
+ } catch (error) {
948
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
949
+ throw error;
950
+ }
951
+ }
952
+ async function readRunEvents(runRoot) {
953
+ try {
954
+ const manifest = JSON.parse(await readFile(join(runRoot, "current", "manifest.json"), "utf8"));
955
+ return manifest.events ?? [];
956
+ } catch (error) {
957
+ if (isNodeError(error) && error.code === "ENOENT") return [];
958
+ throw error;
959
+ }
960
+ }
961
+ async function readStdinJson() {
962
+ const text = await readStdin();
963
+ const trimmed = text.trim();
964
+ return trimmed.length === 0 ? void 0 : JSON.parse(trimmed);
965
+ }
966
+ async function readStdin() {
967
+ if (process.stdin.isTTY) return "";
968
+ return new Promise((resolvePromise, reject) => {
969
+ let text = "";
970
+ process.stdin.setEncoding("utf8");
971
+ process.stdin.on("data", (chunk) => {
972
+ text += chunk;
973
+ });
974
+ process.stdin.on("error", reject);
975
+ process.stdin.on("end", () => resolvePromise(text));
976
+ process.stdin.resume();
977
+ });
978
+ }
979
+ function parseStartRunInput(value) {
980
+ if (value === void 0) return {};
981
+ const input = parseStructuredInputObject("runs start", value);
982
+ assertStructuredInputKeys("runs start", input, ["params", "config"]);
983
+ return { params: input.params, config: input.config };
984
+ }
985
+ function parseResumeRunInput(value) {
986
+ if (value === void 0) return {};
987
+ const input = parseStructuredInputObject("runs resume", value);
988
+ assertStructuredInputKeys("runs resume", input, ["params"]);
989
+ return { params: input.params };
990
+ }
991
+ function parseStructuredInputObject(command, value) {
992
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${command} reads a JSON object from stdin`);
993
+ return value;
994
+ }
995
+ function assertStructuredInputKeys(command, input, allowedKeys) {
996
+ const allowed = new Set(allowedKeys);
997
+ const unexpected = Object.keys(input).filter((key) => !allowed.has(key));
998
+ if (unexpected.length > 0) throw new Error(`${command} stdin JSON has unsupported keys: ${unexpected.join(", ")}`);
999
+ }
1000
+ function assertNoStructuredInputArgs(command, args) {
1001
+ if (args.length > 0) throw new Error(`${command} reads structured input from stdin, not CLI arguments: ${args.join(" ")}`);
1002
+ }
1003
+ function sendSignal(processGroupId, pid, signal) {
1004
+ try {
1005
+ if (processGroupId > 0 && process.platform !== "win32") {
1006
+ process.kill(-processGroupId, signal);
1007
+ return;
1008
+ }
1009
+ } catch (error) {
1010
+ if (!isIgnorableSignalError(error)) throw error;
1011
+ }
1012
+ try {
1013
+ process.kill(pid, signal);
1014
+ } catch (error) {
1015
+ if (!isIgnorableSignalError(error)) throw error;
1016
+ }
1017
+ }
1018
+ function isIgnorableSignalError(error) {
1019
+ return isNodeError(error) && (error.code === "ESRCH" || error.code === "EPERM");
1020
+ }
1021
+ function writeJson(value) {
1022
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
1023
+ `);
1024
+ }
1025
+ function errorCode(error) {
1026
+ if (error instanceof NornProjectLoadError) return error.code;
1027
+ if (error instanceof SyntaxError) return "INVALID_JSON";
1028
+ return "NORN_ERROR";
1029
+ }
1030
+ export {
1031
+ main
1032
+ };