@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,7 +1,21 @@
|
|
|
1
1
|
// src/internal/run-store.ts
|
|
2
2
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
access,
|
|
6
|
+
chmod as chmod2,
|
|
7
|
+
lstat,
|
|
8
|
+
mkdir as mkdir2,
|
|
9
|
+
mkdtemp,
|
|
10
|
+
readFile,
|
|
11
|
+
readdir,
|
|
12
|
+
readlink,
|
|
13
|
+
rename as rename2,
|
|
14
|
+
rm as rm2,
|
|
15
|
+
symlink,
|
|
16
|
+
utimes,
|
|
17
|
+
writeFile as writeFile2
|
|
18
|
+
} from "node:fs/promises";
|
|
5
19
|
import { dirname as dirname2, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
20
|
import { promisify } from "node:util";
|
|
7
21
|
import { gunzip, gzip } from "node:zlib";
|
|
@@ -76,7 +90,10 @@ var NornRunStore = class _NornRunStore {
|
|
|
76
90
|
}
|
|
77
91
|
async snapshotCurrent(message) {
|
|
78
92
|
const previous = await this.readCheckpointHistory();
|
|
79
|
-
const checkpoint = this.createCheckpoint(
|
|
93
|
+
const checkpoint = this.createCheckpoint(
|
|
94
|
+
message,
|
|
95
|
+
previous.checkpoints.length + 1
|
|
96
|
+
);
|
|
80
97
|
const checkpoints = [...previous.checkpoints, checkpoint];
|
|
81
98
|
await this.createSnapshot(checkpoint, checkpoints);
|
|
82
99
|
await writeJsonAtomically(this.checkpointsPath, checkpoints);
|
|
@@ -84,19 +101,28 @@ var NornRunStore = class _NornRunStore {
|
|
|
84
101
|
await writeTextAtomically(this.currentRefPath, `${checkpoint.id}
|
|
85
102
|
`);
|
|
86
103
|
} catch (error) {
|
|
87
|
-
if (previous.content === void 0)
|
|
104
|
+
if (previous.content === void 0)
|
|
105
|
+
await rm2(this.checkpointsPath, { force: true });
|
|
88
106
|
else await writeTextAtomically(this.checkpointsPath, previous.content);
|
|
89
107
|
throw error;
|
|
90
108
|
}
|
|
91
109
|
return checkpoint;
|
|
92
110
|
}
|
|
93
111
|
async restoreSnapshot(ref, prepare) {
|
|
94
|
-
const checkpoint = (await this.listCheckpoints()).find(
|
|
112
|
+
const checkpoint = (await this.listCheckpoints()).find(
|
|
113
|
+
(entry) => entry.id === ref
|
|
114
|
+
);
|
|
95
115
|
if (!checkpoint) throw new Error(`Unknown active run checkpoint: ${ref}`);
|
|
96
|
-
await this.restoreSnapshotManifest(
|
|
116
|
+
await this.restoreSnapshotManifest(
|
|
117
|
+
await this.readSnapshot(checkpoint.id),
|
|
118
|
+
prepare
|
|
119
|
+
);
|
|
97
120
|
}
|
|
98
121
|
async restoreCurrentSnapshot() {
|
|
99
|
-
await this.restoreSnapshotManifest(
|
|
122
|
+
await this.restoreSnapshotManifest(
|
|
123
|
+
await this.readSnapshot(await this.currentSnapshotRef()),
|
|
124
|
+
void 0
|
|
125
|
+
);
|
|
100
126
|
}
|
|
101
127
|
async listCheckpoints() {
|
|
102
128
|
return (await this.readCheckpointHistory()).checkpoints;
|
|
@@ -105,10 +131,12 @@ var NornRunStore = class _NornRunStore {
|
|
|
105
131
|
try {
|
|
106
132
|
const content = await readFile(this.checkpointsPath, "utf8");
|
|
107
133
|
const checkpoints = parseNornRunCheckpoints(JSON.parse(content));
|
|
108
|
-
for (const checkpoint of checkpoints)
|
|
134
|
+
for (const checkpoint of checkpoints)
|
|
135
|
+
this.assertCheckpointPathMatchesId(checkpoint);
|
|
109
136
|
return { checkpoints, content };
|
|
110
137
|
} catch (error) {
|
|
111
|
-
if (isNodeError(error) && error.code === "ENOENT")
|
|
138
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
139
|
+
return { checkpoints: [] };
|
|
112
140
|
throw error;
|
|
113
141
|
}
|
|
114
142
|
}
|
|
@@ -131,15 +159,33 @@ var NornRunStore = class _NornRunStore {
|
|
|
131
159
|
}
|
|
132
160
|
createCheckpoint(message, index) {
|
|
133
161
|
const id = `cp_${randomUUID2().replaceAll("-", "")}`;
|
|
134
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
path: this.snapshotRelativePath(id),
|
|
165
|
+
index,
|
|
166
|
+
message,
|
|
167
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
168
|
+
};
|
|
135
169
|
}
|
|
136
170
|
async createSnapshot(checkpoint, checkpoints) {
|
|
137
|
-
const entries = (await this.snapshotEntries(this.currentRoot)).filter(
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
const entries = (await this.snapshotEntries(this.currentRoot)).filter(
|
|
172
|
+
(entry) => entry.path !== CHECKPOINTS_FILE_NAME
|
|
173
|
+
);
|
|
174
|
+
const historyBytes = Buffer.from(
|
|
175
|
+
`${JSON.stringify(checkpoints, null, 2)}
|
|
176
|
+
`
|
|
177
|
+
);
|
|
140
178
|
const historyHash = hashBuffer(historyBytes);
|
|
141
179
|
await this.storeObject(historyHash, historyBytes);
|
|
142
|
-
entries.push({
|
|
180
|
+
entries.push({
|
|
181
|
+
path: CHECKPOINTS_FILE_NAME,
|
|
182
|
+
type: "file",
|
|
183
|
+
sha256: historyHash,
|
|
184
|
+
size: historyBytes.length,
|
|
185
|
+
mode: 384,
|
|
186
|
+
mtimeMs: Date.now(),
|
|
187
|
+
compression: "gzip"
|
|
188
|
+
});
|
|
143
189
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
144
190
|
const snapshot = {
|
|
145
191
|
version: SNAPSHOT_VERSION,
|
|
@@ -161,22 +207,45 @@ var NornRunStore = class _NornRunStore {
|
|
|
161
207
|
async collectSnapshotEntries(absolutePath, snapshotPath, entries) {
|
|
162
208
|
const stat2 = await lstat(absolutePath);
|
|
163
209
|
if (stat2.isDirectory()) {
|
|
164
|
-
if (snapshotPath.length > 0)
|
|
210
|
+
if (snapshotPath.length > 0)
|
|
211
|
+
entries.push({
|
|
212
|
+
path: snapshotPath,
|
|
213
|
+
type: "directory",
|
|
214
|
+
mode: stat2.mode
|
|
215
|
+
});
|
|
165
216
|
const children = await readdir(absolutePath, { withFileTypes: true });
|
|
166
|
-
for (const child of children.sort(
|
|
167
|
-
|
|
217
|
+
for (const child of children.sort(
|
|
218
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
219
|
+
)) {
|
|
220
|
+
await this.collectSnapshotEntries(
|
|
221
|
+
join(absolutePath, child.name),
|
|
222
|
+
joinSnapshotPath(snapshotPath, child.name),
|
|
223
|
+
entries
|
|
224
|
+
);
|
|
168
225
|
}
|
|
169
226
|
return;
|
|
170
227
|
}
|
|
171
228
|
if (stat2.isSymbolicLink()) {
|
|
172
|
-
entries.push({
|
|
229
|
+
entries.push({
|
|
230
|
+
path: snapshotPath,
|
|
231
|
+
type: "symlink",
|
|
232
|
+
target: await readlink(absolutePath)
|
|
233
|
+
});
|
|
173
234
|
return;
|
|
174
235
|
}
|
|
175
236
|
if (!stat2.isFile()) return;
|
|
176
237
|
const bytes = await readFile(absolutePath);
|
|
177
238
|
const sha256 = hashBuffer(bytes);
|
|
178
239
|
await this.storeObject(sha256, bytes);
|
|
179
|
-
entries.push({
|
|
240
|
+
entries.push({
|
|
241
|
+
path: snapshotPath,
|
|
242
|
+
type: "file",
|
|
243
|
+
sha256,
|
|
244
|
+
size: stat2.size,
|
|
245
|
+
mode: stat2.mode,
|
|
246
|
+
mtimeMs: stat2.mtimeMs,
|
|
247
|
+
compression: "gzip"
|
|
248
|
+
});
|
|
180
249
|
}
|
|
181
250
|
async storeObject(sha256, bytes) {
|
|
182
251
|
const path = this.objectPath(sha256);
|
|
@@ -214,25 +283,34 @@ var NornRunStore = class _NornRunStore {
|
|
|
214
283
|
await rename2(previousCurrent, this.currentRoot);
|
|
215
284
|
throw error;
|
|
216
285
|
}
|
|
217
|
-
await rm2(previousCurrent, { recursive: true, force: true }).catch(
|
|
286
|
+
await rm2(previousCurrent, { recursive: true, force: true }).catch(
|
|
287
|
+
() => void 0
|
|
288
|
+
);
|
|
218
289
|
} finally {
|
|
219
|
-
await rm2(stagedRunRoot, { recursive: true, force: true }).catch(
|
|
290
|
+
await rm2(stagedRunRoot, { recursive: true, force: true }).catch(
|
|
291
|
+
() => void 0
|
|
292
|
+
);
|
|
220
293
|
}
|
|
221
294
|
}
|
|
222
295
|
async materializeSnapshot(snapshot, destination) {
|
|
223
296
|
const entriesByPath = /* @__PURE__ */ new Map();
|
|
224
297
|
for (const entry of snapshot.entries) {
|
|
225
298
|
this.materializedPath(destination, entry.path);
|
|
226
|
-
if (entriesByPath.has(entry.path))
|
|
299
|
+
if (entriesByPath.has(entry.path))
|
|
300
|
+
throw new Error(`Duplicate snapshot path: ${entry.path}`);
|
|
227
301
|
entriesByPath.set(entry.path, entry);
|
|
228
302
|
}
|
|
229
303
|
for (const entry of snapshot.entries) {
|
|
230
304
|
let parent = dirname2(entry.path);
|
|
231
305
|
while (parent !== ".") {
|
|
232
|
-
if (entriesByPath.get(parent)?.type !== "directory")
|
|
306
|
+
if (entriesByPath.get(parent)?.type !== "directory")
|
|
307
|
+
throw new Error(`Snapshot parent is not a directory: ${parent}`);
|
|
233
308
|
parent = dirname2(parent);
|
|
234
309
|
}
|
|
235
|
-
if (entry.type === "directory")
|
|
310
|
+
if (entry.type === "directory")
|
|
311
|
+
await mkdir2(this.materializedPath(destination, entry.path), {
|
|
312
|
+
recursive: true
|
|
313
|
+
});
|
|
236
314
|
}
|
|
237
315
|
for (const entry of snapshot.entries) {
|
|
238
316
|
if (entry.type === "directory") continue;
|
|
@@ -241,26 +319,36 @@ var NornRunStore = class _NornRunStore {
|
|
|
241
319
|
await symlink(entry.target, path);
|
|
242
320
|
continue;
|
|
243
321
|
}
|
|
244
|
-
const bytes = await gunzipBuffer(
|
|
322
|
+
const bytes = await gunzipBuffer(
|
|
323
|
+
await readFile(this.objectPath(entry.sha256))
|
|
324
|
+
);
|
|
245
325
|
const actualSha256 = hashBuffer(bytes);
|
|
246
|
-
if (actualSha256 !== entry.sha256)
|
|
326
|
+
if (actualSha256 !== entry.sha256)
|
|
327
|
+
throw new Error(`CAS object checksum mismatch: ${entry.sha256}`);
|
|
247
328
|
await writeFile2(path, bytes);
|
|
248
329
|
await chmod2(path, entry.mode);
|
|
249
330
|
await utimes(path, new Date(entry.mtimeMs), new Date(entry.mtimeMs));
|
|
250
331
|
}
|
|
251
332
|
for (const entry of [...snapshot.entries].reverse()) {
|
|
252
|
-
if (entry.type === "directory")
|
|
333
|
+
if (entry.type === "directory")
|
|
334
|
+
await chmod2(this.materializedPath(destination, entry.path), entry.mode);
|
|
253
335
|
}
|
|
254
336
|
}
|
|
255
337
|
materializedPath(destination, snapshotPath) {
|
|
256
|
-
if (snapshotPath.length === 0 || isAbsolute(snapshotPath) || snapshotPath.split(/[\\/]/).some((part) => part === ".." || part === "." || part === ""))
|
|
338
|
+
if (snapshotPath.length === 0 || isAbsolute(snapshotPath) || snapshotPath.split(/[\\/]/).some((part) => part === ".." || part === "." || part === ""))
|
|
339
|
+
throw new Error(`Invalid snapshot path: ${snapshotPath}`);
|
|
257
340
|
const path = resolve(destination, ...snapshotPath.split("/"));
|
|
258
341
|
const pathFromCurrent = relative(destination, path);
|
|
259
|
-
if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`))
|
|
342
|
+
if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`))
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Snapshot path escapes current checkout: ${snapshotPath}`
|
|
345
|
+
);
|
|
260
346
|
return path;
|
|
261
347
|
}
|
|
262
348
|
async readSnapshot(id) {
|
|
263
|
-
return parseRunSnapshotManifest(
|
|
349
|
+
return parseRunSnapshotManifest(
|
|
350
|
+
JSON.parse(await readFile(this.snapshotPath(id), "utf8"))
|
|
351
|
+
);
|
|
264
352
|
}
|
|
265
353
|
snapshotPath(id) {
|
|
266
354
|
return join(this.snapshotsRoot, `${id}.json`);
|
|
@@ -270,10 +358,18 @@ var NornRunStore = class _NornRunStore {
|
|
|
270
358
|
}
|
|
271
359
|
assertCheckpointPathMatchesId(checkpoint) {
|
|
272
360
|
const expectedPath = this.snapshotRelativePath(checkpoint.id);
|
|
273
|
-
if (checkpoint.path !== expectedPath)
|
|
361
|
+
if (checkpoint.path !== expectedPath)
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Run checkpoint path does not match id: ${checkpoint.id}`
|
|
364
|
+
);
|
|
274
365
|
}
|
|
275
366
|
objectPath(sha256) {
|
|
276
|
-
return join(
|
|
367
|
+
return join(
|
|
368
|
+
this.objectsRoot,
|
|
369
|
+
sha256.slice(0, 2),
|
|
370
|
+
sha256.slice(2, 4),
|
|
371
|
+
`${sha256}.gz`
|
|
372
|
+
);
|
|
277
373
|
}
|
|
278
374
|
};
|
|
279
375
|
function hashBuffer(bytes) {
|
|
@@ -283,14 +379,23 @@ function joinSnapshotPath(parent, child) {
|
|
|
283
379
|
return parent.length === 0 ? child : `${parent}/${child}`;
|
|
284
380
|
}
|
|
285
381
|
function parseRunSnapshotManifest(value) {
|
|
286
|
-
if (!value || typeof value !== "object")
|
|
382
|
+
if (!value || typeof value !== "object")
|
|
383
|
+
throw new Error("Invalid run snapshot manifest");
|
|
287
384
|
const snapshot = value;
|
|
288
|
-
if (snapshot.version !== SNAPSHOT_VERSION)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (typeof snapshot.
|
|
293
|
-
|
|
385
|
+
if (snapshot.version !== SNAPSHOT_VERSION)
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Unsupported run snapshot version: ${String(snapshot.version)}`
|
|
388
|
+
);
|
|
389
|
+
if (typeof snapshot.id !== "string" || snapshot.id.length === 0)
|
|
390
|
+
throw new Error("Invalid run snapshot id");
|
|
391
|
+
if (typeof snapshot.index !== "number" || !Number.isInteger(snapshot.index) || snapshot.index < 1)
|
|
392
|
+
throw new Error("Invalid run snapshot index");
|
|
393
|
+
if (typeof snapshot.message !== "string")
|
|
394
|
+
throw new Error("Invalid run snapshot message");
|
|
395
|
+
if (typeof snapshot.createdAt !== "string")
|
|
396
|
+
throw new Error("Invalid run snapshot timestamp");
|
|
397
|
+
if (!Array.isArray(snapshot.entries))
|
|
398
|
+
throw new Error("Invalid run snapshot entries");
|
|
294
399
|
return snapshot;
|
|
295
400
|
}
|
|
296
401
|
function parseNornRunCheckpoints(value) {
|
|
@@ -298,14 +403,21 @@ function parseNornRunCheckpoints(value) {
|
|
|
298
403
|
return value.map(parseNornRunCheckpoint);
|
|
299
404
|
}
|
|
300
405
|
function parseNornRunCheckpoint(value) {
|
|
301
|
-
if (!value || typeof value !== "object")
|
|
406
|
+
if (!value || typeof value !== "object")
|
|
407
|
+
throw new Error("Invalid run checkpoint");
|
|
302
408
|
const checkpoint = value;
|
|
303
|
-
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0)
|
|
304
|
-
|
|
305
|
-
if (checkpoint.path
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
409
|
+
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0)
|
|
410
|
+
throw new Error("Invalid run checkpoint id");
|
|
411
|
+
if (typeof checkpoint.path !== "string" || checkpoint.path.length === 0)
|
|
412
|
+
throw new Error("Invalid run checkpoint path");
|
|
413
|
+
if (checkpoint.path.split(/[\\/]/).includes("..") || resolve(checkpoint.path) === checkpoint.path)
|
|
414
|
+
throw new Error(`Invalid run checkpoint path: ${checkpoint.path}`);
|
|
415
|
+
if (typeof checkpoint.index !== "number" || !Number.isInteger(checkpoint.index) || checkpoint.index < 1)
|
|
416
|
+
throw new Error("Invalid run checkpoint index");
|
|
417
|
+
if (typeof checkpoint.message !== "string")
|
|
418
|
+
throw new Error("Invalid run checkpoint message");
|
|
419
|
+
if (typeof checkpoint.createdAt !== "string")
|
|
420
|
+
throw new Error("Invalid run checkpoint timestamp");
|
|
309
421
|
return checkpoint;
|
|
310
422
|
}
|
|
311
423
|
export {
|
|
@@ -16,8 +16,12 @@ async function prepareRunWorkerDirectory(runRoot) {
|
|
|
16
16
|
if (!isNodeError(error) || error.code !== "EEXIST") throw error;
|
|
17
17
|
}
|
|
18
18
|
const metadata = await lstat(directory);
|
|
19
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
20
|
-
|
|
19
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Run worker cwd must be a directory, not a symlink: ${directory}`
|
|
22
|
+
);
|
|
23
|
+
if ((await readdir(directory)).length !== 0)
|
|
24
|
+
throw new Error(`Run worker cwd must be empty: ${directory}`);
|
|
21
25
|
await chmod(directory, 365);
|
|
22
26
|
return directory;
|
|
23
27
|
}
|
|
@@ -1,25 +1,60 @@
|
|
|
1
1
|
// src/internal/workflow-registry.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
isWorkflowComplete,
|
|
4
|
+
isWorkflowDeclaration,
|
|
5
|
+
isWorkflowFail,
|
|
6
|
+
isWorkflowNext
|
|
7
|
+
} from "@vimhead.dev/norn";
|
|
8
|
+
import {
|
|
9
|
+
assertWorkflowMetadata,
|
|
10
|
+
inspectSchema,
|
|
11
|
+
isPlainObject,
|
|
12
|
+
schemaShape,
|
|
13
|
+
schemaType,
|
|
14
|
+
unwrapSchema
|
|
15
|
+
} from "@vimhead.dev/norn/schema";
|
|
4
16
|
import { Value } from "typebox/value";
|
|
5
17
|
function decodeConfiguration(input) {
|
|
6
18
|
if (!input.schema) {
|
|
7
|
-
if (input.value !== void 0)
|
|
8
|
-
|
|
19
|
+
if (input.value !== void 0)
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Norn config provided without config schema: ${input.key}`
|
|
22
|
+
);
|
|
23
|
+
return {
|
|
24
|
+
key: input.key,
|
|
25
|
+
schema: void 0,
|
|
26
|
+
input: void 0,
|
|
27
|
+
value: void 0
|
|
28
|
+
};
|
|
9
29
|
}
|
|
10
30
|
const value = input.value === void 0 && schemaType(unwrapSchema(input.schema)) === "object" ? {} : input.value;
|
|
11
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
key: input.key,
|
|
33
|
+
schema: input.schema,
|
|
34
|
+
input: value,
|
|
35
|
+
value: Value.Decode(input.schema, value)
|
|
36
|
+
};
|
|
12
37
|
}
|
|
13
38
|
function assertCompatibleConfigurationOwners(workflows) {
|
|
14
39
|
const owners = /* @__PURE__ */ new Map();
|
|
15
40
|
for (const workflow of workflows) {
|
|
16
41
|
for (const owner of [
|
|
17
42
|
{ key: workflow.id, kind: "workflow", definition: workflow },
|
|
18
|
-
...workflow.scope ? [
|
|
43
|
+
...workflow.scope ? [
|
|
44
|
+
{
|
|
45
|
+
key: workflow.scope.id,
|
|
46
|
+
kind: "scope",
|
|
47
|
+
definition: workflow.scope
|
|
48
|
+
}
|
|
49
|
+
] : []
|
|
19
50
|
]) {
|
|
20
51
|
const previous = owners.get(owner.key);
|
|
21
|
-
if (previous && previous.kind !== owner.kind)
|
|
22
|
-
|
|
52
|
+
if (previous && previous.kind !== owner.kind)
|
|
53
|
+
throw new Error(`Ambiguous configuration key: ${owner.key}`);
|
|
54
|
+
if (previous && owner.kind === "scope" && previous.definition !== owner.definition)
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Duplicate workflow scope id: ${owner.key}; import one shared scope definition`
|
|
57
|
+
);
|
|
23
58
|
owners.set(owner.key, owner);
|
|
24
59
|
}
|
|
25
60
|
}
|
|
@@ -30,25 +65,40 @@ var NornWorkflowRegistry = class {
|
|
|
30
65
|
register(input) {
|
|
31
66
|
const { workflow, config, source } = input;
|
|
32
67
|
assertWorkflowDefinition(workflow);
|
|
33
|
-
if (this.entries.has(workflow.id))
|
|
34
|
-
|
|
68
|
+
if (this.entries.has(workflow.id))
|
|
69
|
+
throw new Error(`Workflow already registered: ${workflow.id}`);
|
|
70
|
+
assertCompatibleConfigurationOwners([
|
|
71
|
+
...this.sortedEntries().map((entry2) => entry2.workflow),
|
|
72
|
+
workflow
|
|
73
|
+
]);
|
|
35
74
|
const entry = {
|
|
36
75
|
workflow,
|
|
37
76
|
source,
|
|
38
77
|
scopeId: workflow.scope?.id,
|
|
39
|
-
configuration: decodeConfiguration({
|
|
78
|
+
configuration: decodeConfiguration({
|
|
79
|
+
key: workflow.id,
|
|
80
|
+
schema: workflow.config,
|
|
81
|
+
value: configurationInput(config, workflow.id)
|
|
82
|
+
})
|
|
40
83
|
};
|
|
41
84
|
if (workflow.scope && !this.scopes.has(workflow.scope.id)) {
|
|
42
85
|
this.scopes.set(workflow.scope.id, {
|
|
43
86
|
definition: workflow.scope,
|
|
44
|
-
configuration: decodeConfiguration({
|
|
87
|
+
configuration: decodeConfiguration({
|
|
88
|
+
key: workflow.scope.id,
|
|
89
|
+
schema: workflow.scope.config,
|
|
90
|
+
value: configurationInput(config, workflow.scope.id)
|
|
91
|
+
})
|
|
45
92
|
});
|
|
46
93
|
}
|
|
47
94
|
this.entries.set(workflow.id, entry);
|
|
48
95
|
return () => {
|
|
49
96
|
if (this.entries.get(workflow.id) !== entry) return;
|
|
50
97
|
this.entries.delete(workflow.id);
|
|
51
|
-
if (entry.scopeId && ![...this.entries.values()].some(
|
|
98
|
+
if (entry.scopeId && ![...this.entries.values()].some(
|
|
99
|
+
(other) => other.scopeId === entry.scopeId
|
|
100
|
+
))
|
|
101
|
+
this.scopes.delete(entry.scopeId);
|
|
52
102
|
};
|
|
53
103
|
}
|
|
54
104
|
list(options = {}) {
|
|
@@ -71,12 +121,25 @@ var NornWorkflowRegistry = class {
|
|
|
71
121
|
}
|
|
72
122
|
configurationInfos() {
|
|
73
123
|
return [
|
|
74
|
-
...this.sortedEntries().map((entry) => ({
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
...this.sortedEntries().map((entry) => ({
|
|
125
|
+
configuration: entry.configuration,
|
|
126
|
+
scopeId: null
|
|
127
|
+
})),
|
|
128
|
+
...[...this.scopes.values()].map((scope) => ({
|
|
129
|
+
configuration: scope.configuration,
|
|
130
|
+
scopeId: scope.definition.id
|
|
131
|
+
}))
|
|
132
|
+
].map(({ configuration, scopeId }) => ({
|
|
133
|
+
key: configuration.key,
|
|
134
|
+
scopeId,
|
|
135
|
+
configSchema: configuration.schema ? inspectSchema(configuration.schema) : null,
|
|
136
|
+
config: configuration.value
|
|
137
|
+
}));
|
|
77
138
|
}
|
|
78
139
|
launchableEntries() {
|
|
79
|
-
return this.sortedEntries().filter(
|
|
140
|
+
return this.sortedEntries().filter(
|
|
141
|
+
({ workflow }) => workflow.entrypoint !== false
|
|
142
|
+
);
|
|
80
143
|
}
|
|
81
144
|
workflowById(workflowId) {
|
|
82
145
|
return this.entries.get(workflowId)?.workflow;
|
|
@@ -84,19 +147,27 @@ var NornWorkflowRegistry = class {
|
|
|
84
147
|
async describeGate(input) {
|
|
85
148
|
const { workflow } = input;
|
|
86
149
|
const entry = this.requireEntry(workflow.id);
|
|
87
|
-
if (!workflow.gate)
|
|
150
|
+
if (!workflow.gate)
|
|
151
|
+
throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
88
152
|
const context = this.createExecutionContext({ ...input, entry });
|
|
89
153
|
const description = await entry.workflow.gate?.describe?.(context) ?? workflow.id;
|
|
90
|
-
if (typeof description !== "string" || description.trim().length === 0)
|
|
154
|
+
if (typeof description !== "string" || description.trim().length === 0)
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Workflow gate description must not be empty: ${workflow.id}`
|
|
157
|
+
);
|
|
91
158
|
return description.trim();
|
|
92
159
|
}
|
|
93
160
|
async execute(input) {
|
|
94
161
|
const { workflow } = input;
|
|
95
162
|
const entry = this.requireEntry(workflow.id);
|
|
96
|
-
const result = await entry.workflow.execute(
|
|
163
|
+
const result = await entry.workflow.execute(
|
|
164
|
+
this.createExecutionContext({ ...input, entry })
|
|
165
|
+
);
|
|
97
166
|
if (isWorkflowNext(result)) return result;
|
|
98
|
-
if (isWorkflowComplete(result))
|
|
99
|
-
|
|
167
|
+
if (isWorkflowComplete(result))
|
|
168
|
+
return { type: "complete", workflow, metadata: result.metadata };
|
|
169
|
+
if (isWorkflowFail(result))
|
|
170
|
+
return { type: "fail", workflow, metadata: result.metadata };
|
|
100
171
|
throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
|
|
101
172
|
}
|
|
102
173
|
createExecutionContext(input) {
|
|
@@ -105,7 +176,12 @@ var NornWorkflowRegistry = class {
|
|
|
105
176
|
return {
|
|
106
177
|
args: Value.Decode(entry.workflow.args, input.args),
|
|
107
178
|
config: parseExecutionConfig(entry.configuration, configOverride),
|
|
108
|
-
...scope ? {
|
|
179
|
+
...scope ? {
|
|
180
|
+
scope: {
|
|
181
|
+
id: scope.definition.id,
|
|
182
|
+
config: parseExecutionConfig(scope.configuration, configOverride)
|
|
183
|
+
}
|
|
184
|
+
} : {},
|
|
109
185
|
paths,
|
|
110
186
|
run: execution.run,
|
|
111
187
|
agents: execution.agents,
|
|
@@ -119,39 +195,54 @@ var NornWorkflowRegistry = class {
|
|
|
119
195
|
return entry;
|
|
120
196
|
}
|
|
121
197
|
sortedEntries() {
|
|
122
|
-
return [...this.entries.values()].sort(
|
|
198
|
+
return [...this.entries.values()].sort(
|
|
199
|
+
(left, right) => left.workflow.id.localeCompare(right.workflow.id)
|
|
200
|
+
);
|
|
123
201
|
}
|
|
124
202
|
};
|
|
125
203
|
function configurationInput(config, key) {
|
|
126
204
|
return Object.hasOwn(config, key) ? config[key] : void 0;
|
|
127
205
|
}
|
|
128
206
|
function parseExecutionConfig(configuration, overrides) {
|
|
129
|
-
if (overrides !== void 0 && !isPlainObject(overrides))
|
|
207
|
+
if (overrides !== void 0 && !isPlainObject(overrides))
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Run config override must be an object keyed by workflow or scope id"
|
|
210
|
+
);
|
|
130
211
|
const override = overrides ? configurationInput(overrides, configuration.key) : void 0;
|
|
131
212
|
if (override === void 0) return configuration.value;
|
|
132
|
-
if (!configuration.schema)
|
|
213
|
+
if (!configuration.schema)
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Run config override provided without config schema: ${configuration.key}`
|
|
216
|
+
);
|
|
133
217
|
const input = isPlainObject(configuration.input) && isPlainObject(override) ? deepMerge(configuration.input, override) : override;
|
|
134
218
|
return Value.Decode(configuration.schema, input);
|
|
135
219
|
}
|
|
136
220
|
function deepMerge(base, override) {
|
|
137
221
|
const result = { ...base };
|
|
138
|
-
for (const [key, value] of Object.entries(override))
|
|
222
|
+
for (const [key, value] of Object.entries(override))
|
|
223
|
+
result[key] = isPlainObject(result[key]) && isPlainObject(value) ? deepMerge(result[key], value) : value;
|
|
139
224
|
return result;
|
|
140
225
|
}
|
|
141
226
|
function assertWorkflowDefinition(workflow) {
|
|
142
227
|
assertWorkflowMetadata(workflow);
|
|
143
|
-
if (!isWorkflowDeclaration(workflow))
|
|
228
|
+
if (!isWorkflowDeclaration(workflow))
|
|
229
|
+
throw new Error("Invalid workflow definition");
|
|
144
230
|
assertGateWorkflow(workflow);
|
|
145
231
|
}
|
|
146
232
|
function assertGateWorkflow(workflow) {
|
|
147
233
|
if (!workflow.gate) return;
|
|
148
|
-
if (workflow.gate.enabled !== true)
|
|
149
|
-
|
|
234
|
+
if (workflow.gate.enabled !== true)
|
|
235
|
+
throw new Error(`Workflow gate must be enabled with true: ${workflow.id}`);
|
|
236
|
+
if (workflow.gate.describe !== void 0 && typeof workflow.gate.describe !== "function")
|
|
237
|
+
throw new Error(`Invalid gate description callback: ${workflow.id}`);
|
|
150
238
|
if (!workflow.gate.fields) return;
|
|
151
239
|
const argsSchema = unwrapSchema(workflow.args);
|
|
152
|
-
if (schemaType(argsSchema) !== "object")
|
|
240
|
+
if (schemaType(argsSchema) !== "object")
|
|
241
|
+
throw new Error(`Workflow gate fields require object args: ${workflow.id}`);
|
|
153
242
|
const shape = schemaShape(argsSchema);
|
|
154
|
-
for (const field of workflow.gate.fields)
|
|
243
|
+
for (const field of workflow.gate.fields)
|
|
244
|
+
if (!Object.hasOwn(shape, field))
|
|
245
|
+
throw new Error(`Unknown workflow gate field ${field}: ${workflow.id}`);
|
|
155
246
|
}
|
|
156
247
|
function workflowInfo(entry) {
|
|
157
248
|
return {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/internal/working-directory.ts
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
function requireAbsoluteWorkingDirectory(cwd) {
|
|
4
|
-
if (typeof cwd !== "string" || !isAbsolute(cwd))
|
|
4
|
+
if (typeof cwd !== "string" || !isAbsolute(cwd))
|
|
5
|
+
throw new Error("Working directory (cwd) must be an absolute path");
|
|
5
6
|
return resolve(cwd);
|
|
6
7
|
}
|
|
7
8
|
export {
|