@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.
- package/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -1,47 +1,102 @@
|
|
|
1
1
|
import { main as runPi } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
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 {
|
|
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 {
|
|
13
|
-
|
|
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 {
|
|
17
|
-
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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:
|
|
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: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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: [
|
|
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: [
|
|
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:
|
|
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:
|
|
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({
|
|
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:
|
|
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:
|
|
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({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
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:
|
|
192
|
+
description:
|
|
193
|
+
"Use when discovering the active Norn project, configuration, and workflow sources.",
|
|
116
194
|
usage: "norn project inspect",
|
|
117
|
-
output:
|
|
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:
|
|
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: [
|
|
130
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
246
|
+
stdin:
|
|
247
|
+
'Optional JSON object: {"args":{...},"config":{"workflowOrScopeId":{...}}}.',
|
|
155
248
|
output: "JSON object with started run info under run.",
|
|
156
|
-
examples: [
|
|
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:
|
|
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:
|
|
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: [
|
|
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:
|
|
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:
|
|
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:
|
|
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({
|
|
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:
|
|
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:
|
|
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: [
|
|
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:
|
|
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: [
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
455
|
+
description:
|
|
456
|
+
"Use when upgrading this Norn CLI installation according to explicit build metadata.",
|
|
338
457
|
usage: "norn upgrade [--dry-run]",
|
|
339
|
-
options: [
|
|
340
|
-
|
|
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:
|
|
469
|
+
description:
|
|
470
|
+
"Use when checking the installed Norn CLI version and explicit build metadata.",
|
|
348
471
|
usage: "norn version",
|
|
349
|
-
output:
|
|
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":
|
|
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(
|
|
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({
|
|
421
|
-
|
|
422
|
-
|
|
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(
|
|
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: (
|
|
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({
|
|
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(
|
|
509
|
-
|
|
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(
|
|
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
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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)
|
|
722
|
+
if (path.length === 0)
|
|
723
|
+
return ["norn <command> [args]", "norn help <command>", "norn --version"];
|
|
564
724
|
const prefix = `norn ${path.join(" ")}`;
|
|
565
|
-
return [
|
|
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) =>
|
|
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(
|
|
739
|
+
return COMMANDS.find(
|
|
740
|
+
(command) =>
|
|
741
|
+
command.path.length === path.length && startsWithPath(command.path, path),
|
|
742
|
+
);
|
|
574
743
|
}
|
|
575
744
|
|
|
576
|
-
function sortedCommandsByPathLength(
|
|
577
|
-
|
|
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(
|
|
581
|
-
|
|
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(
|
|
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({
|
|
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<{
|
|
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(
|
|
616
|
-
|
|
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")
|
|
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({
|
|
821
|
+
writeJson({
|
|
822
|
+
upgrade: unsupportedUpgrade(build, currentVersion, build.upgrade.reason),
|
|
823
|
+
});
|
|
631
824
|
return;
|
|
632
825
|
}
|
|
633
|
-
writeJson({
|
|
826
|
+
writeJson({
|
|
827
|
+
upgrade: await upgradeGithubReleaseBinary(build, currentVersion, dryRun),
|
|
828
|
+
});
|
|
634
829
|
}
|
|
635
830
|
|
|
636
|
-
function unsupportedUpgrade(
|
|
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(
|
|
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")
|
|
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(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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)
|
|
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))
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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(
|
|
739
|
-
|
|
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)
|
|
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))
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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, {
|
|
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({
|
|
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 = {
|
|
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(
|
|
1117
|
+
async function parseResumeArgs(
|
|
1118
|
+
runInfo: NornRunInfo,
|
|
1119
|
+
args: unknown,
|
|
1120
|
+
): Promise<unknown> {
|
|
818
1121
|
if (runInfo.status === "interrupted") {
|
|
819
|
-
if (args === undefined)
|
|
820
|
-
|
|
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)
|
|
824
|
-
|
|
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)
|
|
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(
|
|
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")
|
|
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(
|
|
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 [
|
|
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(
|
|
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 =
|
|
893
|
-
|
|
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", () =>
|
|
899
|
-
|
|
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 =
|
|
1243
|
+
const request =
|
|
1244
|
+
(await readOptionalRunLaunchRequest(runRoot)) ??
|
|
1245
|
+
(await readRunResumeRequest(runRoot));
|
|
903
1246
|
try {
|
|
904
|
-
const agentDir = resolveNornAgentDirectory({
|
|
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({
|
|
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, {
|
|
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")
|
|
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: {
|
|
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(
|
|
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({
|
|
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")
|
|
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(
|
|
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(
|
|
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))
|
|
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(
|
|
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")
|
|
1378
|
+
if (NORN_BUILD_INFO.kind === "github-release-binary")
|
|
1379
|
+
return ["execute-run", runId];
|
|
1002
1380
|
const scriptPath = process.argv[1];
|
|
1003
|
-
return scriptPath &&
|
|
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: {
|
|
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(
|
|
1423
|
+
async function readRunEvents(
|
|
1424
|
+
runRoot: string,
|
|
1425
|
+
): Promise<readonly Record<string, unknown>[]> {
|
|
1039
1426
|
try {
|
|
1040
|
-
const manifest = JSON.parse(
|
|
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): {
|
|
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(
|
|
1083
|
-
|
|
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(
|
|
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)
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
|
1527
|
+
return (
|
|
1528
|
+
isNodeError(error) && (error.code === "ESRCH" || error.code === "EPERM")
|
|
1529
|
+
);
|
|
1115
1530
|
}
|
|
1116
1531
|
|
|
1117
1532
|
function writeJson(value: unknown): void {
|