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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. package/package.json +2 -2
@@ -1,12 +1,29 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { access, chmod, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rename, rm, symlink, utimes, writeFile } from "node:fs/promises";
3
+ import {
4
+ access,
5
+ chmod,
6
+ lstat,
7
+ mkdir,
8
+ mkdtemp,
9
+ readFile,
10
+ readdir,
11
+ readlink,
12
+ rename,
13
+ rm,
14
+ symlink,
15
+ utimes,
16
+ writeFile,
17
+ } from "node:fs/promises";
4
18
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
19
  import { promisify } from "node:util";
6
20
  import { gunzip, gzip } from "node:zlib";
7
21
  import type { NornRunCheckpoint } from "@vimhead.dev/norn";
8
22
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
9
- import { writeJsonAtomically, writeTextAtomically } from "@vimhead.dev/norn-core/atomic-files";
23
+ import {
24
+ writeJsonAtomically,
25
+ writeTextAtomically,
26
+ } from "@vimhead.dev/norn-core/atomic-files";
10
27
 
11
28
  const gzipBuffer = promisify(gzip);
12
29
  const gunzipBuffer = promisify(gunzip);
@@ -50,42 +67,62 @@ export class NornRunStore {
50
67
 
51
68
  async snapshotCurrent(message: string): Promise<NornRunCheckpoint> {
52
69
  const previous = await this.readCheckpointHistory();
53
- const checkpoint = this.createCheckpoint(message, previous.checkpoints.length + 1);
70
+ const checkpoint = this.createCheckpoint(
71
+ message,
72
+ previous.checkpoints.length + 1,
73
+ );
54
74
  const checkpoints = [...previous.checkpoints, checkpoint];
55
75
  await this.createSnapshot(checkpoint, checkpoints);
56
76
  await writeJsonAtomically(this.checkpointsPath, checkpoints);
57
77
  try {
58
78
  await writeTextAtomically(this.currentRefPath, `${checkpoint.id}\n`);
59
79
  } catch (error) {
60
- if (previous.content === undefined) await rm(this.checkpointsPath, { force: true });
80
+ if (previous.content === undefined)
81
+ await rm(this.checkpointsPath, { force: true });
61
82
  else await writeTextAtomically(this.checkpointsPath, previous.content);
62
83
  throw error;
63
84
  }
64
85
  return checkpoint;
65
86
  }
66
87
 
67
- async restoreSnapshot(ref: string, prepare: ((stagedRunRoot: string) => Promise<void>) | undefined): Promise<void> {
68
- const checkpoint = (await this.listCheckpoints()).find((entry) => entry.id === ref);
88
+ async restoreSnapshot(
89
+ ref: string,
90
+ prepare: ((stagedRunRoot: string) => Promise<void>) | undefined,
91
+ ): Promise<void> {
92
+ const checkpoint = (await this.listCheckpoints()).find(
93
+ (entry) => entry.id === ref,
94
+ );
69
95
  if (!checkpoint) throw new Error(`Unknown active run checkpoint: ${ref}`);
70
- await this.restoreSnapshotManifest(await this.readSnapshot(checkpoint.id), prepare);
96
+ await this.restoreSnapshotManifest(
97
+ await this.readSnapshot(checkpoint.id),
98
+ prepare,
99
+ );
71
100
  }
72
101
 
73
102
  async restoreCurrentSnapshot(): Promise<void> {
74
- await this.restoreSnapshotManifest(await this.readSnapshot(await this.currentSnapshotRef()), undefined);
103
+ await this.restoreSnapshotManifest(
104
+ await this.readSnapshot(await this.currentSnapshotRef()),
105
+ undefined,
106
+ );
75
107
  }
76
108
 
77
109
  async listCheckpoints(): Promise<NornRunCheckpoint[]> {
78
110
  return (await this.readCheckpointHistory()).checkpoints;
79
111
  }
80
112
 
81
- private async readCheckpointHistory(): Promise<{ readonly checkpoints: NornRunCheckpoint[]; readonly content?: string }> {
113
+ private async readCheckpointHistory(): Promise<{
114
+ readonly checkpoints: NornRunCheckpoint[];
115
+ readonly content?: string;
116
+ }> {
82
117
  try {
83
118
  const content = await readFile(this.checkpointsPath, "utf8");
84
119
  const checkpoints = parseNornRunCheckpoints(JSON.parse(content));
85
- for (const checkpoint of checkpoints) this.assertCheckpointPathMatchesId(checkpoint);
120
+ for (const checkpoint of checkpoints)
121
+ this.assertCheckpointPathMatchesId(checkpoint);
86
122
  return { checkpoints, content };
87
123
  } catch (error) {
88
- if (isNodeError(error) && error.code === "ENOENT") return { checkpoints: [] };
124
+ if (isNodeError(error) && error.code === "ENOENT")
125
+ return { checkpoints: [] };
89
126
  throw error;
90
127
  }
91
128
  }
@@ -114,15 +151,36 @@ export class NornRunStore {
114
151
 
115
152
  private createCheckpoint(message: string, index: number): NornRunCheckpoint {
116
153
  const id = `cp_${randomUUID().replaceAll("-", "")}`;
117
- return { id, path: this.snapshotRelativePath(id), index, message, createdAt: new Date().toISOString() };
154
+ return {
155
+ id,
156
+ path: this.snapshotRelativePath(id),
157
+ index,
158
+ message,
159
+ createdAt: new Date().toISOString(),
160
+ };
118
161
  }
119
162
 
120
- private async createSnapshot(checkpoint: NornRunCheckpoint, checkpoints: readonly NornRunCheckpoint[]): Promise<RunSnapshotManifest> {
121
- const entries = (await this.snapshotEntries(this.currentRoot)).filter((entry) => entry.path !== CHECKPOINTS_FILE_NAME);
122
- const historyBytes = Buffer.from(`${JSON.stringify(checkpoints, null, 2)}\n`);
163
+ private async createSnapshot(
164
+ checkpoint: NornRunCheckpoint,
165
+ checkpoints: readonly NornRunCheckpoint[],
166
+ ): Promise<RunSnapshotManifest> {
167
+ const entries = (await this.snapshotEntries(this.currentRoot)).filter(
168
+ (entry) => entry.path !== CHECKPOINTS_FILE_NAME,
169
+ );
170
+ const historyBytes = Buffer.from(
171
+ `${JSON.stringify(checkpoints, null, 2)}\n`,
172
+ );
123
173
  const historyHash = hashBuffer(historyBytes);
124
174
  await this.storeObject(historyHash, historyBytes);
125
- entries.push({ path: CHECKPOINTS_FILE_NAME, type: "file", sha256: historyHash, size: historyBytes.length, mode: 0o600, mtimeMs: Date.now(), compression: "gzip" });
175
+ entries.push({
176
+ path: CHECKPOINTS_FILE_NAME,
177
+ type: "file",
178
+ sha256: historyHash,
179
+ size: historyBytes.length,
180
+ mode: 0o600,
181
+ mtimeMs: Date.now(),
182
+ compression: "gzip",
183
+ });
126
184
  entries.sort((left, right) => left.path.localeCompare(right.path));
127
185
  const snapshot: RunSnapshotManifest = {
128
186
  version: SNAPSHOT_VERSION,
@@ -143,25 +201,52 @@ export class NornRunStore {
143
201
  return entries.sort((left, right) => left.path.localeCompare(right.path));
144
202
  }
145
203
 
146
- private async collectSnapshotEntries(absolutePath: string, snapshotPath: string, entries: RunSnapshotEntry[]): Promise<void> {
204
+ private async collectSnapshotEntries(
205
+ absolutePath: string,
206
+ snapshotPath: string,
207
+ entries: RunSnapshotEntry[],
208
+ ): Promise<void> {
147
209
  const stat = await lstat(absolutePath);
148
210
  if (stat.isDirectory()) {
149
- if (snapshotPath.length > 0) entries.push({ path: snapshotPath, type: "directory", mode: stat.mode });
211
+ if (snapshotPath.length > 0)
212
+ entries.push({
213
+ path: snapshotPath,
214
+ type: "directory",
215
+ mode: stat.mode,
216
+ });
150
217
  const children = await readdir(absolutePath, { withFileTypes: true });
151
- for (const child of children.sort((left, right) => left.name.localeCompare(right.name))) {
152
- await this.collectSnapshotEntries(join(absolutePath, child.name), joinSnapshotPath(snapshotPath, child.name), entries);
218
+ for (const child of children.sort((left, right) =>
219
+ left.name.localeCompare(right.name),
220
+ )) {
221
+ await this.collectSnapshotEntries(
222
+ join(absolutePath, child.name),
223
+ joinSnapshotPath(snapshotPath, child.name),
224
+ entries,
225
+ );
153
226
  }
154
227
  return;
155
228
  }
156
229
  if (stat.isSymbolicLink()) {
157
- entries.push({ path: snapshotPath, type: "symlink", target: await readlink(absolutePath) });
230
+ entries.push({
231
+ path: snapshotPath,
232
+ type: "symlink",
233
+ target: await readlink(absolutePath),
234
+ });
158
235
  return;
159
236
  }
160
237
  if (!stat.isFile()) return;
161
238
  const bytes = await readFile(absolutePath);
162
239
  const sha256 = hashBuffer(bytes);
163
240
  await this.storeObject(sha256, bytes);
164
- entries.push({ path: snapshotPath, type: "file", sha256, size: stat.size, mode: stat.mode, mtimeMs: stat.mtimeMs, compression: "gzip" });
241
+ entries.push({
242
+ path: snapshotPath,
243
+ type: "file",
244
+ sha256,
245
+ size: stat.size,
246
+ mode: stat.mode,
247
+ mtimeMs: stat.mtimeMs,
248
+ compression: "gzip",
249
+ });
165
250
  }
166
251
 
167
252
  private async storeObject(sha256: string, bytes: Buffer): Promise<void> {
@@ -178,7 +263,10 @@ export class NornRunStore {
178
263
  await rename(tmpPath, path);
179
264
  }
180
265
 
181
- private async restoreSnapshotManifest(snapshot: RunSnapshotManifest, prepare: ((stagedRunRoot: string) => Promise<void>) | undefined): Promise<void> {
266
+ private async restoreSnapshotManifest(
267
+ snapshot: RunSnapshotManifest,
268
+ prepare: ((stagedRunRoot: string) => Promise<void>) | undefined,
269
+ ): Promise<void> {
182
270
  const stagedRunRoot = await mkdtemp(join(this.runRoot, ".restore-"));
183
271
  const stagedCurrent = runCurrentRoot(stagedRunRoot);
184
272
  const previousCurrent = join(this.runRoot, `.previous-${randomUUID()}`);
@@ -200,26 +288,38 @@ export class NornRunStore {
200
288
  await rename(previousCurrent, this.currentRoot);
201
289
  throw error;
202
290
  }
203
- await rm(previousCurrent, { recursive: true, force: true }).catch(() => undefined);
291
+ await rm(previousCurrent, { recursive: true, force: true }).catch(
292
+ () => undefined,
293
+ );
204
294
  } finally {
205
- await rm(stagedRunRoot, { recursive: true, force: true }).catch(() => undefined);
295
+ await rm(stagedRunRoot, { recursive: true, force: true }).catch(
296
+ () => undefined,
297
+ );
206
298
  }
207
299
  }
208
300
 
209
- private async materializeSnapshot(snapshot: RunSnapshotManifest, destination: string): Promise<void> {
301
+ private async materializeSnapshot(
302
+ snapshot: RunSnapshotManifest,
303
+ destination: string,
304
+ ): Promise<void> {
210
305
  const entriesByPath = new Map<string, RunSnapshotEntry>();
211
306
  for (const entry of snapshot.entries) {
212
307
  this.materializedPath(destination, entry.path);
213
- if (entriesByPath.has(entry.path)) throw new Error(`Duplicate snapshot path: ${entry.path}`);
308
+ if (entriesByPath.has(entry.path))
309
+ throw new Error(`Duplicate snapshot path: ${entry.path}`);
214
310
  entriesByPath.set(entry.path, entry);
215
311
  }
216
312
  for (const entry of snapshot.entries) {
217
313
  let parent = dirname(entry.path);
218
314
  while (parent !== ".") {
219
- if (entriesByPath.get(parent)?.type !== "directory") throw new Error(`Snapshot parent is not a directory: ${parent}`);
315
+ if (entriesByPath.get(parent)?.type !== "directory")
316
+ throw new Error(`Snapshot parent is not a directory: ${parent}`);
220
317
  parent = dirname(parent);
221
318
  }
222
- if (entry.type === "directory") await mkdir(this.materializedPath(destination, entry.path), { recursive: true });
319
+ if (entry.type === "directory")
320
+ await mkdir(this.materializedPath(destination, entry.path), {
321
+ recursive: true,
322
+ });
223
323
  }
224
324
  for (const entry of snapshot.entries) {
225
325
  if (entry.type === "directory") continue;
@@ -228,28 +328,44 @@ export class NornRunStore {
228
328
  await symlink(entry.target, path);
229
329
  continue;
230
330
  }
231
- const bytes = await gunzipBuffer(await readFile(this.objectPath(entry.sha256)));
331
+ const bytes = await gunzipBuffer(
332
+ await readFile(this.objectPath(entry.sha256)),
333
+ );
232
334
  const actualSha256 = hashBuffer(bytes);
233
- if (actualSha256 !== entry.sha256) throw new Error(`CAS object checksum mismatch: ${entry.sha256}`);
335
+ if (actualSha256 !== entry.sha256)
336
+ throw new Error(`CAS object checksum mismatch: ${entry.sha256}`);
234
337
  await writeFile(path, bytes);
235
338
  await chmod(path, entry.mode);
236
339
  await utimes(path, new Date(entry.mtimeMs), new Date(entry.mtimeMs));
237
340
  }
238
341
  for (const entry of [...snapshot.entries].reverse()) {
239
- if (entry.type === "directory") await chmod(this.materializedPath(destination, entry.path), entry.mode);
342
+ if (entry.type === "directory")
343
+ await chmod(this.materializedPath(destination, entry.path), entry.mode);
240
344
  }
241
345
  }
242
346
 
243
347
  private materializedPath(destination: string, snapshotPath: string): string {
244
- if (snapshotPath.length === 0 || isAbsolute(snapshotPath) || snapshotPath.split(/[\\/]/).some((part) => part === ".." || part === "." || part === "")) throw new Error(`Invalid snapshot path: ${snapshotPath}`);
348
+ if (
349
+ snapshotPath.length === 0 ||
350
+ isAbsolute(snapshotPath) ||
351
+ snapshotPath
352
+ .split(/[\\/]/)
353
+ .some((part) => part === ".." || part === "." || part === "")
354
+ )
355
+ throw new Error(`Invalid snapshot path: ${snapshotPath}`);
245
356
  const path = resolve(destination, ...snapshotPath.split("/"));
246
357
  const pathFromCurrent = relative(destination, path);
247
- if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`)) throw new Error(`Snapshot path escapes current checkout: ${snapshotPath}`);
358
+ if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`))
359
+ throw new Error(
360
+ `Snapshot path escapes current checkout: ${snapshotPath}`,
361
+ );
248
362
  return path;
249
363
  }
250
364
 
251
365
  private async readSnapshot(id: string): Promise<RunSnapshotManifest> {
252
- return parseRunSnapshotManifest(JSON.parse(await readFile(this.snapshotPath(id), "utf8")));
366
+ return parseRunSnapshotManifest(
367
+ JSON.parse(await readFile(this.snapshotPath(id), "utf8")),
368
+ );
253
369
  }
254
370
 
255
371
  private snapshotPath(id: string): string {
@@ -262,11 +378,19 @@ export class NornRunStore {
262
378
 
263
379
  private assertCheckpointPathMatchesId(checkpoint: NornRunCheckpoint): void {
264
380
  const expectedPath = this.snapshotRelativePath(checkpoint.id);
265
- if (checkpoint.path !== expectedPath) throw new Error(`Run checkpoint path does not match id: ${checkpoint.id}`);
381
+ if (checkpoint.path !== expectedPath)
382
+ throw new Error(
383
+ `Run checkpoint path does not match id: ${checkpoint.id}`,
384
+ );
266
385
  }
267
386
 
268
387
  private objectPath(sha256: string): string {
269
- return join(this.objectsRoot, sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.gz`);
388
+ return join(
389
+ this.objectsRoot,
390
+ sha256.slice(0, 2),
391
+ sha256.slice(2, 4),
392
+ `${sha256}.gz`,
393
+ );
270
394
  }
271
395
  }
272
396
 
@@ -282,8 +406,20 @@ type RunSnapshotManifest = {
282
406
 
283
407
  type RunSnapshotEntry =
284
408
  | { readonly path: string; readonly type: "directory"; readonly mode: number }
285
- | { readonly path: string; readonly type: "file"; readonly sha256: string; readonly size: number; readonly mode: number; readonly mtimeMs: number; readonly compression: "gzip" }
286
- | { readonly path: string; readonly type: "symlink"; readonly target: string };
409
+ | {
410
+ readonly path: string;
411
+ readonly type: "file";
412
+ readonly sha256: string;
413
+ readonly size: number;
414
+ readonly mode: number;
415
+ readonly mtimeMs: number;
416
+ readonly compression: "gzip";
417
+ }
418
+ | {
419
+ readonly path: string;
420
+ readonly type: "symlink";
421
+ readonly target: string;
422
+ };
287
423
 
288
424
  function hashBuffer(bytes: Buffer): string {
289
425
  return createHash("sha256").update(bytes).digest("hex");
@@ -294,14 +430,27 @@ function joinSnapshotPath(parent: string, child: string): string {
294
430
  }
295
431
 
296
432
  function parseRunSnapshotManifest(value: unknown): RunSnapshotManifest {
297
- if (!value || typeof value !== "object") throw new Error("Invalid run snapshot manifest");
433
+ if (!value || typeof value !== "object")
434
+ throw new Error("Invalid run snapshot manifest");
298
435
  const snapshot = value as Partial<RunSnapshotManifest>;
299
- if (snapshot.version !== SNAPSHOT_VERSION) throw new Error(`Unsupported run snapshot version: ${String(snapshot.version)}`);
300
- if (typeof snapshot.id !== "string" || snapshot.id.length === 0) throw new Error("Invalid run snapshot id");
301
- if (typeof snapshot.index !== "number" || !Number.isInteger(snapshot.index) || snapshot.index < 1) throw new Error("Invalid run snapshot index");
302
- if (typeof snapshot.message !== "string") throw new Error("Invalid run snapshot message");
303
- if (typeof snapshot.createdAt !== "string") throw new Error("Invalid run snapshot timestamp");
304
- if (!Array.isArray(snapshot.entries)) throw new Error("Invalid run snapshot entries");
436
+ if (snapshot.version !== SNAPSHOT_VERSION)
437
+ throw new Error(
438
+ `Unsupported run snapshot version: ${String(snapshot.version)}`,
439
+ );
440
+ if (typeof snapshot.id !== "string" || snapshot.id.length === 0)
441
+ throw new Error("Invalid run snapshot id");
442
+ if (
443
+ typeof snapshot.index !== "number" ||
444
+ !Number.isInteger(snapshot.index) ||
445
+ snapshot.index < 1
446
+ )
447
+ throw new Error("Invalid run snapshot index");
448
+ if (typeof snapshot.message !== "string")
449
+ throw new Error("Invalid run snapshot message");
450
+ if (typeof snapshot.createdAt !== "string")
451
+ throw new Error("Invalid run snapshot timestamp");
452
+ if (!Array.isArray(snapshot.entries))
453
+ throw new Error("Invalid run snapshot entries");
305
454
  return snapshot as RunSnapshotManifest;
306
455
  }
307
456
 
@@ -311,13 +460,27 @@ function parseNornRunCheckpoints(value: unknown): NornRunCheckpoint[] {
311
460
  }
312
461
 
313
462
  function parseNornRunCheckpoint(value: unknown): NornRunCheckpoint {
314
- if (!value || typeof value !== "object") throw new Error("Invalid run checkpoint");
463
+ if (!value || typeof value !== "object")
464
+ throw new Error("Invalid run checkpoint");
315
465
  const checkpoint = value as Partial<NornRunCheckpoint>;
316
- if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0) throw new Error("Invalid run checkpoint id");
317
- if (typeof checkpoint.path !== "string" || checkpoint.path.length === 0) throw new Error("Invalid run checkpoint path");
318
- if (checkpoint.path.split(/[\\/]/).includes("..") || resolve(checkpoint.path) === checkpoint.path) throw new Error(`Invalid run checkpoint path: ${checkpoint.path}`);
319
- if (typeof checkpoint.index !== "number" || !Number.isInteger(checkpoint.index) || checkpoint.index < 1) throw new Error("Invalid run checkpoint index");
320
- if (typeof checkpoint.message !== "string") throw new Error("Invalid run checkpoint message");
321
- if (typeof checkpoint.createdAt !== "string") throw new Error("Invalid run checkpoint timestamp");
466
+ if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0)
467
+ throw new Error("Invalid run checkpoint id");
468
+ if (typeof checkpoint.path !== "string" || checkpoint.path.length === 0)
469
+ throw new Error("Invalid run checkpoint path");
470
+ if (
471
+ checkpoint.path.split(/[\\/]/).includes("..") ||
472
+ resolve(checkpoint.path) === checkpoint.path
473
+ )
474
+ throw new Error(`Invalid run checkpoint path: ${checkpoint.path}`);
475
+ if (
476
+ typeof checkpoint.index !== "number" ||
477
+ !Number.isInteger(checkpoint.index) ||
478
+ checkpoint.index < 1
479
+ )
480
+ throw new Error("Invalid run checkpoint index");
481
+ if (typeof checkpoint.message !== "string")
482
+ throw new Error("Invalid run checkpoint message");
483
+ if (typeof checkpoint.createdAt !== "string")
484
+ throw new Error("Invalid run checkpoint timestamp");
322
485
  return checkpoint as NornRunCheckpoint;
323
486
  }
@@ -1,6 +1,8 @@
1
1
  import type { NornAgentUsage } from "@vimhead.dev/norn";
2
2
 
3
- export function agentUsageFromValue(value: unknown): NornAgentUsage | undefined {
3
+ export function agentUsageFromValue(
4
+ value: unknown,
5
+ ): NornAgentUsage | undefined {
4
6
  if (!value || typeof value !== "object") return undefined;
5
7
  const usage = value as Record<string, unknown>;
6
8
  const input = numberField(usage.input);
@@ -8,8 +10,12 @@ export function agentUsageFromValue(value: unknown): NornAgentUsage | undefined
8
10
  const cacheRead = numberField(usage.cacheRead);
9
11
  const cacheWrite = numberField(usage.cacheWrite);
10
12
  const reasoning = optionalNumberField(usage.reasoning);
11
- const totalTokens = numberField(usage.totalTokens) || input + output + cacheRead + cacheWrite;
12
- const costValue = usage.cost && typeof usage.cost === "object" ? usage.cost as Record<string, unknown> : {};
13
+ const totalTokens =
14
+ numberField(usage.totalTokens) || input + output + cacheRead + cacheWrite;
15
+ const costValue =
16
+ usage.cost && typeof usage.cost === "object"
17
+ ? (usage.cost as Record<string, unknown>)
18
+ : {};
13
19
  return {
14
20
  input,
15
21
  output,
@@ -27,12 +33,20 @@ export function agentUsageFromValue(value: unknown): NornAgentUsage | undefined
27
33
  };
28
34
  }
29
35
 
30
- export function totalAgentUsage(usages: readonly NornAgentUsage[]): NornAgentUsage {
36
+ export function totalAgentUsage(
37
+ usages: readonly NornAgentUsage[],
38
+ ): NornAgentUsage {
31
39
  return usages.reduce(addAgentUsage, emptyAgentUsage());
32
40
  }
33
41
 
34
- export function addAgentUsage(left: NornAgentUsage, right: NornAgentUsage): NornAgentUsage {
35
- const reasoning = left.reasoning === undefined && right.reasoning === undefined ? undefined : (left.reasoning ?? 0) + (right.reasoning ?? 0);
42
+ export function addAgentUsage(
43
+ left: NornAgentUsage,
44
+ right: NornAgentUsage,
45
+ ): NornAgentUsage {
46
+ const reasoning =
47
+ left.reasoning === undefined && right.reasoning === undefined
48
+ ? undefined
49
+ : (left.reasoning ?? 0) + (right.reasoning ?? 0);
36
50
  return {
37
51
  input: left.input + right.input,
38
52
  output: left.output + right.output,
@@ -66,5 +80,7 @@ function numberField(value: unknown): number {
66
80
  }
67
81
 
68
82
  function optionalNumberField(value: unknown): number | undefined {
69
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
83
+ return typeof value === "number" && Number.isFinite(value)
84
+ ? value
85
+ : undefined;
70
86
  }
@@ -2,7 +2,9 @@ import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
4
 
5
- export async function prepareRunWorkerDirectory(runRoot: string): Promise<string> {
5
+ export async function prepareRunWorkerDirectory(
6
+ runRoot: string,
7
+ ): Promise<string> {
6
8
  const directory = join(runRoot, "worker");
7
9
  try {
8
10
  await mkdir(directory, { mode: 0o555 });
@@ -10,8 +12,12 @@ export async function prepareRunWorkerDirectory(runRoot: string): Promise<string
10
12
  if (!isNodeError(error) || error.code !== "EEXIST") throw error;
11
13
  }
12
14
  const metadata = await lstat(directory);
13
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Run worker cwd must be a directory, not a symlink: ${directory}`);
14
- if ((await readdir(directory)).length !== 0) throw new Error(`Run worker cwd must be empty: ${directory}`);
15
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
16
+ throw new Error(
17
+ `Run worker cwd must be a directory, not a symlink: ${directory}`,
18
+ );
19
+ if ((await readdir(directory)).length !== 0)
20
+ throw new Error(`Run worker cwd must be empty: ${directory}`);
15
21
  await chmod(directory, 0o555);
16
22
  return directory;
17
23
  }