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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. package/package.json +2 -2
@@ -6,11 +6,16 @@ export class NornProjectLoadError extends Error {
6
6
  readonly isComplete = false;
7
7
  readonly diagnostics: readonly NornWorkflowDiagnostic[];
8
8
 
9
- constructor(input: { readonly diagnostics: readonly NornWorkflowDiagnostic[] }) {
10
- const details = input.diagnostics.map(diagnostic =>
11
- `${diagnostic.stage}: ${diagnostic.modulePath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`,
9
+ constructor(input: {
10
+ readonly diagnostics: readonly NornWorkflowDiagnostic[];
11
+ }) {
12
+ const details = input.diagnostics.map(
13
+ (diagnostic) =>
14
+ `${diagnostic.stage}: ${diagnostic.modulePath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`,
15
+ );
16
+ super(
17
+ `Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.\n${details.join("\n")}`,
12
18
  );
13
- super(`Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.\n${details.join("\n")}`);
14
19
  this.name = "NornProjectLoadError";
15
20
  this.diagnostics = input.diagnostics;
16
21
  }
@@ -24,7 +29,10 @@ export class NornRunStoppedError extends Error {
24
29
  }
25
30
 
26
31
  export function schemaErrorMessage(error: AssertError): string {
27
- return error.cause.errors.slice(0, 3).map(issue => `${issue.instancePath || "/"}: ${issue.message}`).join("; ");
32
+ return error.cause.errors
33
+ .slice(0, 3)
34
+ .map((issue) => `${issue.instancePath || "/"}: ${issue.message}`)
35
+ .join("; ");
28
36
  }
29
37
 
30
38
  export function errorMessage(error: unknown): string {
@@ -1,5 +1,10 @@
1
1
  import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
2
- import type { NornAgents, NornCommands, NornLogs, NornRun } from "@vimhead.dev/norn";
2
+ import type {
3
+ NornAgents,
4
+ NornCommands,
5
+ NornLogs,
6
+ NornRun,
7
+ } from "@vimhead.dev/norn";
3
8
  import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
4
9
  import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
5
10
  import { NornAgentRunner } from "./agents.ts";
@@ -28,23 +33,26 @@ export class NornExecutionContext {
28
33
  constructor(private readonly input: NornExecutionContextInput) {
29
34
  this.run = {
30
35
  id: input.id,
31
- next: (workflowId, args) => createWorkflowTransition({ workflowId, args }),
32
- complete: metadata => ({ type: "complete", metadata }),
33
- fail: metadata => ({ type: "fail", metadata }),
36
+ next: (workflowId, args) =>
37
+ createWorkflowTransition({ workflowId, args }),
38
+ complete: (metadata) => ({ type: "complete", metadata }),
39
+ fail: (metadata) => ({ type: "fail", metadata }),
34
40
  };
35
41
  this.logs = {
36
- read: log => this.input.logs.read(log),
42
+ read: (log) => this.input.logs.read(log),
37
43
  };
38
44
  this.commands = {
39
- run: commandInput => new NornCommandRunner({
40
- signal: this.input.signal,
41
- logs: this.input.logs,
42
- logger: this.input.logger,
43
- }).run(commandInput),
45
+ run: (commandInput) =>
46
+ new NornCommandRunner({
47
+ signal: this.input.signal,
48
+ logs: this.input.logs,
49
+ logger: this.input.logger,
50
+ }).run(commandInput),
44
51
  };
45
52
  this.agents = {
46
- createSession: agentInput => this.createAgentRunner().createSession(agentInput),
47
- prompt: agentInput => this.createAgentRunner().prompt(agentInput),
53
+ createSession: (agentInput) =>
54
+ this.createAgentRunner().createSession(agentInput),
55
+ prompt: (agentInput) => this.createAgentRunner().prompt(agentInput),
48
56
  };
49
57
  }
50
58
 
@@ -1,29 +1,53 @@
1
1
  import { writeTextAtomically } from "@vimhead.dev/norn-core/atomic-files";
2
2
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
4
+ import {
5
+ lstat,
6
+ mkdir,
7
+ mkdtemp,
8
+ readFile,
9
+ readdir,
10
+ realpath,
11
+ rename,
12
+ rm,
13
+ rmdir,
14
+ unlink,
15
+ writeFile,
16
+ } from "node:fs/promises";
5
17
  import { hostname } from "node:os";
6
18
  import { basename, dirname, join, resolve } from "node:path";
7
19
  import { setTimeout as delay } from "node:timers/promises";
8
20
  import { Type } from "typebox";
9
21
  import { Value } from "typebox/value";
10
22
 
11
- const ownerSchema = Type.Object({
12
- token: Type.String({ format: "uuid" }),
13
- pid: Type.Integer({ exclusiveMinimum: 0 }),
14
- host: Type.String(),
15
- target: Type.String(),
16
- }, { additionalProperties: false });
23
+ const ownerSchema = Type.Object(
24
+ {
25
+ token: Type.String({ format: "uuid" }),
26
+ pid: Type.Integer({ exclusiveMinimum: 0 }),
27
+ host: Type.String(),
28
+ target: Type.String(),
29
+ },
30
+ { additionalProperties: false },
31
+ );
17
32
 
18
33
  export class NornFileCoordinator {
19
34
  private readonly lockRoot: string;
20
35
 
21
- constructor(private readonly input: { readonly lockRoot: string; readonly waitTimeoutMs: number }) {
22
- if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0) throw new Error("Lock wait timeout must be a positive integer");
36
+ constructor(
37
+ private readonly input: {
38
+ readonly lockRoot: string;
39
+ readonly waitTimeoutMs: number;
40
+ },
41
+ ) {
42
+ if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0)
43
+ throw new Error("Lock wait timeout must be a positive integer");
23
44
  this.lockRoot = resolve(input.lockRoot);
24
45
  }
25
46
 
26
- async withExclusiveLock<T>(path: string, operation: (lockedPath: string) => Promise<T>): Promise<T> {
47
+ async withExclusiveLock<T>(
48
+ path: string,
49
+ operation: (lockedPath: string) => Promise<T>,
50
+ ): Promise<T> {
27
51
  const target = await this.resolveTarget(path);
28
52
  await mkdir(this.lockRoot, { recursive: true, mode: 0o700 });
29
53
  const key = createHash("sha256").update(target).digest("hex");
@@ -33,7 +57,11 @@ export class NornFileCoordinator {
33
57
  const marker = `${token}.json`;
34
58
  return this.withCleanup({
35
59
  operation: async () => {
36
- await writeFile(join(stagingPath, marker), JSON.stringify({ token, pid: process.pid, host: hostname(), target }), { mode: 0o600 });
60
+ await writeFile(
61
+ join(stagingPath, marker),
62
+ JSON.stringify({ token, pid: process.pid, host: hostname(), target }),
63
+ { mode: 0o600 },
64
+ );
37
65
  await this.acquire({ stagingPath, lockPath, target });
38
66
  return this.withCleanup({
39
67
  operation: () => operation(target),
@@ -48,21 +76,34 @@ export class NornFileCoordinator {
48
76
  }
49
77
 
50
78
  async readText(path: string): Promise<string> {
51
- return this.withExclusiveLock(path, (lockedPath) => readFile(lockedPath, "utf8"));
79
+ return this.withExclusiveLock(path, (lockedPath) =>
80
+ readFile(lockedPath, "utf8"),
81
+ );
52
82
  }
53
83
 
54
84
  async writeText(path: string, content: string): Promise<void> {
55
85
  await mkdir(dirname(path), { recursive: true });
56
- await this.withExclusiveLock(path, (lockedPath) => writeTextAtomically(lockedPath, content));
86
+ await this.withExclusiveLock(path, (lockedPath) =>
87
+ writeTextAtomically(lockedPath, content),
88
+ );
57
89
  }
58
90
 
59
- private async withCleanup<T>(input: { readonly operation: () => Promise<T>; readonly cleanup: () => Promise<void> }): Promise<T> {
91
+ private async withCleanup<T>(input: {
92
+ readonly operation: () => Promise<T>;
93
+ readonly cleanup: () => Promise<void>;
94
+ }): Promise<T> {
60
95
  let result: T;
61
96
  try {
62
97
  result = await input.operation();
63
98
  } catch (error) {
64
- try { await input.cleanup(); }
65
- catch (cleanupError) { throw new AggregateError([error, cleanupError], "File operation and lock cleanup failed"); }
99
+ try {
100
+ await input.cleanup();
101
+ } catch (cleanupError) {
102
+ throw new AggregateError(
103
+ [error, cleanupError],
104
+ "File operation and lock cleanup failed",
105
+ );
106
+ }
66
107
  throw error;
67
108
  }
68
109
  await input.cleanup();
@@ -78,34 +119,55 @@ export class NornFileCoordinator {
78
119
  if (isNodeError(error) && error.code === "ENOENT") return undefined;
79
120
  throw error;
80
121
  });
81
- if (entry?.isSymbolicLink()) throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
122
+ if (entry?.isSymbolicLink())
123
+ throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
82
124
  return join(await realpath(dirname(path)), basename(path));
83
125
  }
84
126
  }
85
127
 
86
- private async acquire(input: { readonly stagingPath: string; readonly lockPath: string; readonly target: string }): Promise<void> {
128
+ private async acquire(input: {
129
+ readonly stagingPath: string;
130
+ readonly lockPath: string;
131
+ readonly target: string;
132
+ }): Promise<void> {
87
133
  const deadline = Date.now() + this.input.waitTimeoutMs;
88
134
  while (true) {
89
135
  try {
90
136
  await rename(input.stagingPath, input.lockPath);
91
137
  return;
92
138
  } catch (error) {
93
- if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
139
+ if (
140
+ !isNodeError(error) ||
141
+ !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")
142
+ )
143
+ throw error;
94
144
  }
95
145
  await this.reclaimDeadOwner(input);
96
- if (Date.now() >= deadline) throw new Error(`Timed out waiting for file lock: ${input.target}`);
146
+ if (Date.now() >= deadline)
147
+ throw new Error(`Timed out waiting for file lock: ${input.target}`);
97
148
  await delay(10);
98
149
  }
99
150
  }
100
151
 
101
- private async reclaimDeadOwner(input: { readonly lockPath: string; readonly target: string }): Promise<void> {
152
+ private async reclaimDeadOwner(input: {
153
+ readonly lockPath: string;
154
+ readonly target: string;
155
+ }): Promise<void> {
102
156
  try {
103
157
  const entries = await readdir(input.lockPath);
104
158
  if (entries.length === 0) return;
105
- if (entries.length !== 1) throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
159
+ if (entries.length !== 1)
160
+ throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
106
161
  const marker = entries[0];
107
- const owner = Value.Parse(ownerSchema, JSON.parse(await readFile(join(input.lockPath, marker), "utf8")));
108
- if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
162
+ const owner = Value.Parse(
163
+ ownerSchema,
164
+ JSON.parse(await readFile(join(input.lockPath, marker), "utf8")),
165
+ );
166
+ if (
167
+ marker !== `${owner.token}.json` ||
168
+ owner.target !== input.target ||
169
+ owner.host !== hostname()
170
+ ) {
109
171
  throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
110
172
  }
111
173
  try {
@@ -127,11 +189,18 @@ export class NornFileCoordinator {
127
189
  try {
128
190
  await rmdir(path);
129
191
  } catch (error) {
130
- if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
192
+ if (
193
+ !isNodeError(error) ||
194
+ !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")
195
+ )
196
+ throw error;
131
197
  }
132
198
  }
133
199
  }
134
200
 
135
201
  export function createRunFileCoordinator(runRoot: string): NornFileCoordinator {
136
- return new NornFileCoordinator({ lockRoot: join(runRoot, "locks"), waitTimeoutMs: 30_000 });
202
+ return new NornFileCoordinator({
203
+ lockRoot: join(runRoot, "locks"),
204
+ waitTimeoutMs: 30_000,
205
+ });
137
206
  }
@@ -1,3 +1,6 @@
1
1
  export function safeFileName(value: string): string {
2
- return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
2
+ return (
3
+ value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") ||
4
+ "workflow"
5
+ );
3
6
  }
@@ -29,15 +29,25 @@ export type NornRunResumeRequest = {
29
29
  readonly createdAt: string;
30
30
  };
31
31
 
32
- export async function writeRunLaunchRequest(runRoot: string, request: NornRunLaunchRequest): Promise<void> {
32
+ export async function writeRunLaunchRequest(
33
+ runRoot: string,
34
+ request: NornRunLaunchRequest,
35
+ ): Promise<void> {
33
36
  await writeJsonAtomically(join(runRoot, LAUNCH_REQUEST_FILE_NAME), request);
34
37
  }
35
38
 
36
- export async function readRunLaunchRequest(runRoot: string): Promise<NornRunLaunchRequest> {
37
- return parseRunLaunchRequest(JSON.parse(await readFile(join(runRoot, LAUNCH_REQUEST_FILE_NAME), "utf8")));
39
+ export async function readRunLaunchRequest(
40
+ runRoot: string,
41
+ ): Promise<NornRunLaunchRequest> {
42
+ return parseRunLaunchRequest(
43
+ JSON.parse(await readFile(join(runRoot, LAUNCH_REQUEST_FILE_NAME), "utf8")),
44
+ );
38
45
  }
39
46
 
40
- export async function writeRunResumeRequest(runRoot: string, request: NornRunResumeRequest): Promise<void> {
47
+ export async function writeRunResumeRequest(
48
+ runRoot: string,
49
+ request: NornRunResumeRequest,
50
+ ): Promise<void> {
41
51
  const path = join(runRoot, RESUME_REQUEST_FILE_NAME);
42
52
  const stagedPath = `${path}.${randomUUID()}.tmp`;
43
53
  try {
@@ -48,11 +58,17 @@ export async function writeRunResumeRequest(runRoot: string, request: NornRunRes
48
58
  }
49
59
  }
50
60
 
51
- export async function readRunResumeRequest(runRoot: string): Promise<NornRunResumeRequest> {
52
- return parseRunResumeRequest(JSON.parse(await readFile(join(runRoot, RESUME_REQUEST_FILE_NAME), "utf8")));
61
+ export async function readRunResumeRequest(
62
+ runRoot: string,
63
+ ): Promise<NornRunResumeRequest> {
64
+ return parseRunResumeRequest(
65
+ JSON.parse(await readFile(join(runRoot, RESUME_REQUEST_FILE_NAME), "utf8")),
66
+ );
53
67
  }
54
68
 
55
- export async function readOptionalRunResumeRequest(runRoot: string): Promise<NornRunResumeRequest | undefined> {
69
+ export async function readOptionalRunResumeRequest(
70
+ runRoot: string,
71
+ ): Promise<NornRunResumeRequest | undefined> {
56
72
  try {
57
73
  return await readRunResumeRequest(runRoot);
58
74
  } catch (error) {
@@ -61,34 +77,70 @@ export async function readOptionalRunResumeRequest(runRoot: string): Promise<Nor
61
77
  }
62
78
  }
63
79
 
64
- export function matchesRunResumeRequest(actual: NornRunResumeRequest | undefined, expected: NornRunResumeRequest): boolean {
65
- return actual?.id === expected.id && (actual.requestId ?? actual.createdAt) === (expected.requestId ?? expected.createdAt);
80
+ export function matchesRunResumeRequest(
81
+ actual: NornRunResumeRequest | undefined,
82
+ expected: NornRunResumeRequest,
83
+ ): boolean {
84
+ return (
85
+ actual?.id === expected.id &&
86
+ (actual.requestId ?? actual.createdAt) ===
87
+ (expected.requestId ?? expected.createdAt)
88
+ );
66
89
  }
67
90
 
68
- export async function clearRunResumeRequest(input: { readonly runRoot: string; readonly request: NornRunResumeRequest; readonly lease: NornRunLease }): Promise<void> {
91
+ export async function clearRunResumeRequest(input: {
92
+ readonly runRoot: string;
93
+ readonly request: NornRunResumeRequest;
94
+ readonly lease: NornRunLease;
95
+ }): Promise<void> {
69
96
  await input.lease.assertOwned();
70
- if (matchesRunResumeRequest(await readOptionalRunResumeRequest(input.runRoot), input.request)) {
97
+ if (
98
+ matchesRunResumeRequest(
99
+ await readOptionalRunResumeRequest(input.runRoot),
100
+ input.request,
101
+ )
102
+ ) {
71
103
  await rm(join(input.runRoot, RESUME_REQUEST_FILE_NAME), { force: true });
72
104
  }
73
105
  }
74
106
 
75
107
  function parseRunLaunchRequest(value: unknown): NornRunLaunchRequest {
76
- if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
108
+ if (!value || typeof value !== "object")
109
+ throw new Error("Invalid workflow launch request");
77
110
  const request = value as Partial<NornRunLaunchRequest>;
78
- if (request.version !== 2 || request.type !== "run") throw new Error("Unsupported workflow launch request");
79
- if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
80
- if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
81
- if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
82
- if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow launch request timestamp");
111
+ if (request.version !== 2 || request.type !== "run")
112
+ throw new Error("Unsupported workflow launch request");
113
+ if (typeof request.id !== "string" || request.id.length === 0)
114
+ throw new Error("Invalid workflow launch request id");
115
+ if (typeof request.name !== "string" || request.name.length === 0)
116
+ throw new Error("Invalid workflow launch request name");
117
+ if (typeof request.workflowId !== "string" || request.workflowId.length === 0)
118
+ throw new Error("Invalid workflow launch request workflow id");
119
+ if (
120
+ typeof request.createdAt !== "string" ||
121
+ Number.isNaN(Date.parse(request.createdAt))
122
+ )
123
+ throw new Error("Invalid workflow launch request timestamp");
83
124
  return request as NornRunLaunchRequest;
84
125
  }
85
126
 
86
127
  function parseRunResumeRequest(value: unknown): NornRunResumeRequest {
87
- if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
128
+ if (!value || typeof value !== "object")
129
+ throw new Error("Invalid workflow resume request");
88
130
  const request = value as Partial<NornRunResumeRequest>;
89
- if (request.version !== 2 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
90
- if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
91
- if (request.requestId !== undefined && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
92
- if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
131
+ if (request.version !== 2 || request.type !== "resume")
132
+ throw new Error("Unsupported workflow resume request");
133
+ if (typeof request.id !== "string" || request.id.length === 0)
134
+ throw new Error("Invalid workflow resume request id");
135
+ if (
136
+ request.requestId !== undefined &&
137
+ (typeof request.requestId !== "string" || request.requestId.length === 0)
138
+ )
139
+ throw new Error("Invalid workflow resume request token");
140
+ if (
141
+ typeof request.createdAt !== "string" ||
142
+ Number.isNaN(Date.parse(request.createdAt))
143
+ )
144
+ throw new Error("Invalid workflow resume request timestamp");
93
145
  return request as NornRunResumeRequest;
94
146
  }
@@ -10,7 +10,10 @@ export type NornLogWriteStream = {
10
10
  };
11
11
 
12
12
  export class NornRunLogs {
13
- constructor(private readonly logsRoot: string, private readonly files: NornFileCoordinator) {}
13
+ constructor(
14
+ private readonly logsRoot: string,
15
+ private readonly files: NornFileCoordinator,
16
+ ) {}
14
17
 
15
18
  async write(path: string, content: string): Promise<void> {
16
19
  await this.files.writeText(this.resolveLogPath(path), content);
@@ -33,7 +36,12 @@ export class NornRunLogs {
33
36
  if (isAbsolute(path)) throw new Error(`Log path must be relative: ${path}`);
34
37
  const resolvedPath = resolve(this.logsRoot, path);
35
38
  const relativePath = relative(this.logsRoot, resolvedPath);
36
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
39
+ if (
40
+ relativePath === "" ||
41
+ relativePath === ".." ||
42
+ relativePath.startsWith(`..${sep}`) ||
43
+ isAbsolute(relativePath)
44
+ ) {
37
45
  throw new Error(`Log path escapes logs directory: ${path}`);
38
46
  }
39
47
  return resolvedPath;