@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.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 +22 -27
- package/assets/docs/README.md +5 -5
- package/assets/docs/agents.md +48 -9
- package/assets/docs/cli.md +7 -7
- package/assets/docs/composition.md +25 -25
- package/assets/docs/persistence.md +34 -23
- package/assets/docs/projects.md +30 -18
- package/assets/docs/providers.md +3 -3
- package/assets/docs/recovery.md +13 -9
- package/assets/docs/schemas.md +1 -1
- package/assets/docs/workflows.md +105 -12
- package/assets/examples/agent-then-analysis/README.md +10 -10
- package/assets/examples/agent-then-analysis/input.json +1 -1
- package/assets/examples/agent-then-analysis/norn.project.json +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +57 -68
- package/assets/examples/caller-selected-continuation/README.md +15 -14
- package/assets/examples/caller-selected-continuation/caller.ts +37 -38
- package/assets/examples/caller-selected-continuation/input.json +2 -2
- package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
- package/assets/examples/caller-selected-continuation/producer.ts +19 -24
- package/assets/examples/coordinating-multiple-agents/README.md +27 -17
- package/assets/examples/coordinating-multiple-agents/input.json +1 -1
- package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +79 -60
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
- package/assets/examples/getting-started/README.md +2 -2
- package/assets/examples/getting-started/norn.project.json +1 -1
- package/assets/examples/getting-started/plugin.ts +20 -26
- package/assets/examples/minimal-workflow/README.md +13 -12
- package/assets/examples/minimal-workflow/norn.project.json +1 -1
- package/assets/examples/minimal-workflow/plugin.ts +14 -25
- package/assets/examples/shared-state/README.md +12 -4
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +1 -1
- package/assets/examples/shared-state/plugin.ts +49 -41
- package/assets/examples/shared-state/shared-state.ts +56 -0
- package/assets/examples/shared-state/state-tools.ts +68 -0
- package/assets/examples/worktree-development-loop/README.md +18 -17
- package/assets/examples/worktree-development-loop/norn.project.json +1 -1
- package/assets/examples/worktree-development-loop/plugin.ts +6 -26
- package/assets/examples/worktree-development-loop/scope.ts +4 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +35 -33
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +6 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +27 -16
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +43 -31
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +6 -6
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +50 -44
- package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +44 -37
- package/assets/packages/cli/src/client.ts +9 -16
- package/assets/packages/cli/src/documentation-intro.ts +1 -1
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
- package/assets/packages/cli/src/internal/agents.ts +24 -32
- package/assets/packages/cli/src/internal/commands.ts +2 -14
- package/assets/packages/cli/src/internal/engine.ts +63 -101
- package/assets/packages/cli/src/internal/errors.ts +4 -4
- package/assets/packages/cli/src/internal/launch-request.ts +6 -6
- package/assets/packages/cli/src/internal/logs.ts +1 -1
- package/assets/packages/cli/src/internal/run-log.ts +1 -1
- package/assets/packages/cli/src/internal/run-state.ts +38 -23
- package/assets/packages/cli/src/internal/run.ts +4 -62
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +126 -111
- package/assets/packages/cli/src/internal/working-directory.ts +6 -0
- package/assets/packages/cli/src/workflow-loader.ts +213 -0
- package/assets/packages/core/src/workflow-transition.ts +2 -2
- package/assets/packages/sdk/src/api.ts +141 -329
- package/assets/packages/sdk/src/index.ts +1 -4
- package/assets/packages/sdk/src/schema.ts +3 -3
- package/assets/tests/workflow-ref.test.ts +53 -55
- package/dist/cli.js +41 -35
- package/dist/client.d.ts +3 -4
- package/dist/client.js +3 -8
- package/dist/documentation-intro.js +1 -1
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-response-tool.js +3 -3
- package/dist/internal/agents.d.ts +0 -4
- package/dist/internal/agents.js +30 -46
- package/dist/internal/commands.d.ts +0 -4
- package/dist/internal/commands.js +2 -10
- package/dist/internal/engine.d.ts +4 -8
- package/dist/internal/engine.js +57 -84
- package/dist/internal/errors.d.ts +3 -3
- package/dist/internal/errors.js +1 -1
- package/dist/internal/file-coordinator.d.ts +17 -0
- package/dist/internal/file-coordinator.js +162 -0
- package/dist/internal/launch-request.d.ts +4 -4
- package/dist/internal/launch-request.js +2 -2
- package/dist/internal/logs.d.ts +1 -1
- package/dist/internal/run-log.d.ts +1 -1
- package/dist/internal/run-state.d.ts +11 -8
- package/dist/internal/run-state.js +34 -22
- package/dist/internal/run.d.ts +3 -23
- package/dist/internal/run.js +4 -50
- package/dist/internal/worker-directory.d.ts +1 -0
- package/dist/internal/worker-directory.js +26 -0
- package/dist/internal/workflow-registry.d.ts +35 -17
- package/dist/internal/workflow-registry.js +110 -73
- package/dist/internal/working-directory.d.ts +1 -0
- package/dist/internal/working-directory.js +9 -0
- package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
- package/dist/workflow-loader.js +234 -0
- package/package.json +2 -2
- package/assets/docs/resources.md +0 -61
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
- package/assets/examples/worktree-development-loop/manifest.ts +0 -26
- package/assets/examples/worktree-development-loop/state.ts +0 -23
- package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
- package/assets/packages/cli/src/internal/artifacts.ts +0 -26
- package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
- package/assets/packages/cli/src/internal/run-resources.ts +0 -23
- package/assets/packages/cli/src/internal/state-store.ts +0 -83
- package/assets/packages/cli/src/plugin-loader.ts +0 -400
- package/assets/packages/cli/src/resources.ts +0 -69
- package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
- package/assets/packages/sdk/src/resources.ts +0 -20
- package/assets/packages/sdk/src/state-adapter.ts +0 -75
- package/dist/internal/artifacts.d.ts +0 -10
- package/dist/internal/artifacts.js +0 -29
- package/dist/internal/resource-bindings.d.ts +0 -13
- package/dist/internal/resource-bindings.js +0 -34
- package/dist/internal/run-resources.d.ts +0 -6
- package/dist/internal/run-resources.js +0 -26
- package/dist/internal/state-store.d.ts +0 -23
- package/dist/internal/state-store.js +0 -103
- package/dist/plugin-loader.js +0 -341
- package/dist/resources.d.ts +0 -11
- package/dist/resources.js +0 -100
- /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// ../core/src/atomic-files.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
// ../core/src/errors.ts
|
|
7
|
+
function isNodeError(error) {
|
|
8
|
+
return error instanceof Error && "code" in error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../core/src/atomic-files.ts
|
|
12
|
+
async function writeTextAtomically(path, content) {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
let existingMode;
|
|
15
|
+
try {
|
|
16
|
+
existingMode = (await stat(path)).mode & 511;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
19
|
+
}
|
|
20
|
+
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
21
|
+
try {
|
|
22
|
+
await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
|
|
23
|
+
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
24
|
+
await rename(tmpPath, path);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
await rm(tmpPath, { force: true }).catch(() => void 0);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/internal/file-coordinator.ts
|
|
32
|
+
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
33
|
+
import { lstat, mkdir as mkdir2, mkdtemp, readFile, readdir, realpath, rename as rename2, rm as rm2, rmdir, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
34
|
+
import { hostname } from "node:os";
|
|
35
|
+
import { basename, dirname as dirname2, join, resolve } from "node:path";
|
|
36
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
37
|
+
import { Type } from "typebox";
|
|
38
|
+
import { Value } from "typebox/value";
|
|
39
|
+
var ownerSchema = Type.Object({
|
|
40
|
+
token: Type.String({ format: "uuid" }),
|
|
41
|
+
pid: Type.Integer({ exclusiveMinimum: 0 }),
|
|
42
|
+
host: Type.String(),
|
|
43
|
+
target: Type.String()
|
|
44
|
+
}, { additionalProperties: false });
|
|
45
|
+
var NornFileCoordinator = class {
|
|
46
|
+
constructor(input) {
|
|
47
|
+
this.input = input;
|
|
48
|
+
if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0) throw new Error("Lock wait timeout must be a positive integer");
|
|
49
|
+
this.lockRoot = resolve(input.lockRoot);
|
|
50
|
+
}
|
|
51
|
+
input;
|
|
52
|
+
lockRoot;
|
|
53
|
+
async withExclusiveLock(path, operation) {
|
|
54
|
+
const target = await this.resolveTarget(path);
|
|
55
|
+
await mkdir2(this.lockRoot, { recursive: true, mode: 448 });
|
|
56
|
+
const key = createHash("sha256").update(target).digest("hex");
|
|
57
|
+
const lockPath = join(this.lockRoot, `${key}.lock`);
|
|
58
|
+
const stagingPath = await mkdtemp(join(this.lockRoot, `${key}.pending-`));
|
|
59
|
+
const token = randomUUID2();
|
|
60
|
+
const marker = `${token}.json`;
|
|
61
|
+
return this.withCleanup({
|
|
62
|
+
operation: async () => {
|
|
63
|
+
await writeFile2(join(stagingPath, marker), JSON.stringify({ token, pid: process.pid, host: hostname(), target }), { mode: 384 });
|
|
64
|
+
await this.acquire({ stagingPath, lockPath, target });
|
|
65
|
+
return this.withCleanup({
|
|
66
|
+
operation: () => operation(target),
|
|
67
|
+
cleanup: async () => {
|
|
68
|
+
await unlink(join(lockPath, marker));
|
|
69
|
+
await this.removeEmptyLock(lockPath);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
cleanup: () => rm2(stagingPath, { recursive: true, force: true })
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async readText(path) {
|
|
77
|
+
return this.withExclusiveLock(path, (lockedPath) => readFile(lockedPath, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
async writeText(path, content) {
|
|
80
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
81
|
+
await this.withExclusiveLock(path, (lockedPath) => writeTextAtomically(lockedPath, content));
|
|
82
|
+
}
|
|
83
|
+
async withCleanup(input) {
|
|
84
|
+
let result;
|
|
85
|
+
try {
|
|
86
|
+
result = await input.operation();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
try {
|
|
89
|
+
await input.cleanup();
|
|
90
|
+
} catch (cleanupError) {
|
|
91
|
+
throw new AggregateError([error, cleanupError], "File operation and lock cleanup failed");
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
await input.cleanup();
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
async resolveTarget(path) {
|
|
99
|
+
try {
|
|
100
|
+
return await realpath(path);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
103
|
+
const entry = await lstat(path).catch((error2) => {
|
|
104
|
+
if (isNodeError(error2) && error2.code === "ENOENT") return void 0;
|
|
105
|
+
throw error2;
|
|
106
|
+
});
|
|
107
|
+
if (entry?.isSymbolicLink()) throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
|
|
108
|
+
return join(await realpath(dirname2(path)), basename(path));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async acquire(input) {
|
|
112
|
+
const deadline = Date.now() + this.input.waitTimeoutMs;
|
|
113
|
+
while (true) {
|
|
114
|
+
try {
|
|
115
|
+
await rename2(input.stagingPath, input.lockPath);
|
|
116
|
+
return;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
119
|
+
}
|
|
120
|
+
await this.reclaimDeadOwner(input);
|
|
121
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for file lock: ${input.target}`);
|
|
122
|
+
await delay(10);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async reclaimDeadOwner(input) {
|
|
126
|
+
try {
|
|
127
|
+
const entries = await readdir(input.lockPath);
|
|
128
|
+
if (entries.length === 0) return;
|
|
129
|
+
if (entries.length !== 1) throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
|
|
130
|
+
const marker = entries[0];
|
|
131
|
+
const owner = Value.Parse(ownerSchema, JSON.parse(await readFile(join(input.lockPath, marker), "utf8")));
|
|
132
|
+
if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
|
|
133
|
+
throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
process.kill(owner.pid, 0);
|
|
137
|
+
return;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (isNodeError(error) && error.code === "EPERM") return;
|
|
140
|
+
if (!isNodeError(error) || error.code !== "ESRCH") throw error;
|
|
141
|
+
}
|
|
142
|
+
await unlink(join(input.lockPath, marker));
|
|
143
|
+
await this.removeEmptyLock(input.lockPath);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async removeEmptyLock(path) {
|
|
149
|
+
try {
|
|
150
|
+
await rmdir(path);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function createRunFileCoordinator(runRoot) {
|
|
157
|
+
return new NornFileCoordinator({ lockRoot: join(runRoot, "locks"), waitTimeoutMs: 3e4 });
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
NornFileCoordinator,
|
|
161
|
+
createRunFileCoordinator
|
|
162
|
+
};
|
|
@@ -3,21 +3,21 @@ export declare const LAUNCH_REQUEST_FILE_NAME = "launch-request.json";
|
|
|
3
3
|
export declare const RESUME_REQUEST_FILE_NAME = "resume-request.json";
|
|
4
4
|
export declare const RESUME_START_GRACE_MS = 60000;
|
|
5
5
|
export type NornRunLaunchRequest = {
|
|
6
|
-
readonly version:
|
|
6
|
+
readonly version: 2;
|
|
7
7
|
readonly type: "run";
|
|
8
8
|
readonly id: string;
|
|
9
9
|
readonly name: string;
|
|
10
10
|
readonly workflowId: string;
|
|
11
|
-
readonly
|
|
11
|
+
readonly args: unknown;
|
|
12
12
|
readonly configOverride?: unknown;
|
|
13
13
|
readonly createdAt: string;
|
|
14
14
|
};
|
|
15
15
|
export type NornRunResumeRequest = {
|
|
16
|
-
readonly version:
|
|
16
|
+
readonly version: 2;
|
|
17
17
|
readonly type: "resume";
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly requestId?: string;
|
|
20
|
-
readonly
|
|
20
|
+
readonly args?: unknown;
|
|
21
21
|
readonly createdAt: string;
|
|
22
22
|
};
|
|
23
23
|
export declare function writeRunLaunchRequest(runRoot: string, request: NornRunLaunchRequest): Promise<void>;
|
|
@@ -80,7 +80,7 @@ async function clearRunResumeRequest(input) {
|
|
|
80
80
|
function parseRunLaunchRequest(value) {
|
|
81
81
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
|
|
82
82
|
const request = value;
|
|
83
|
-
if (request.version !==
|
|
83
|
+
if (request.version !== 2 || request.type !== "run") throw new Error("Unsupported workflow launch request");
|
|
84
84
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
|
|
85
85
|
if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
|
|
86
86
|
if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
|
|
@@ -90,7 +90,7 @@ function parseRunLaunchRequest(value) {
|
|
|
90
90
|
function parseRunResumeRequest(value) {
|
|
91
91
|
if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
|
|
92
92
|
const request = value;
|
|
93
|
-
if (request.version !==
|
|
93
|
+
if (request.version !== 2 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
|
|
94
94
|
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
|
|
95
95
|
if (request.requestId !== void 0 && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
|
|
96
96
|
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
|
package/dist/internal/logs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type WriteStream } from "node:fs";
|
|
2
|
-
import type { NornFileCoordinator } from "
|
|
2
|
+
import type { NornFileCoordinator } from "./file-coordinator.ts";
|
|
3
3
|
import type { NornLogRef } from "@vimhead.dev/norn";
|
|
4
4
|
export type NornLogWriteStream = {
|
|
5
5
|
readonly log: NornLogRef;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { NornRunStatus, NornRunOutcomeMetadata, NornRunInfo } from "@vimhead.dev/norn";
|
|
2
2
|
export declare const RUN_STATE_FILE_NAME = "run-state.json";
|
|
3
|
-
export declare function
|
|
4
|
-
declare const RUN_STATE_VERSION =
|
|
3
|
+
export declare function mergeInterruptedWorkflowArgs(currentArgs: unknown, args: unknown, fields: readonly string[] | undefined): unknown;
|
|
4
|
+
declare const RUN_STATE_VERSION = 2;
|
|
5
5
|
type NornRunWorkflowStep = {
|
|
6
6
|
readonly workflowId: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly args: unknown;
|
|
8
8
|
readonly cwd: string;
|
|
9
9
|
readonly env: Record<string, string>;
|
|
10
10
|
readonly interruption?: RuntimeInterruption;
|
|
@@ -39,7 +39,8 @@ type NornRunWorkflowOutcome = {
|
|
|
39
39
|
readonly metadata?: NornRunOutcomeMetadata;
|
|
40
40
|
};
|
|
41
41
|
export type NornRunState = {
|
|
42
|
-
readonly version: typeof RUN_STATE_VERSION;
|
|
42
|
+
readonly version: 1 | typeof RUN_STATE_VERSION;
|
|
43
|
+
readonly projectRoot?: string;
|
|
43
44
|
readonly id: string;
|
|
44
45
|
readonly name: string;
|
|
45
46
|
readonly entrypointWorkflowId: string;
|
|
@@ -54,6 +55,7 @@ export type NornRunState = {
|
|
|
54
55
|
readonly updatedAt: string;
|
|
55
56
|
};
|
|
56
57
|
type CreateNornRunStateInput = {
|
|
58
|
+
readonly projectRoot: string;
|
|
57
59
|
readonly id: string;
|
|
58
60
|
readonly name: string;
|
|
59
61
|
readonly entrypointWorkflowId: string;
|
|
@@ -76,20 +78,21 @@ export declare class NornRunStateStore {
|
|
|
76
78
|
failRun(workflowId: string, metadata: NornRunOutcomeMetadata & {
|
|
77
79
|
readonly summary: string;
|
|
78
80
|
}): Promise<void>;
|
|
79
|
-
interruptCurrent(
|
|
81
|
+
interruptCurrent(args: unknown, interruption: {
|
|
80
82
|
readonly description: string;
|
|
81
83
|
readonly fields?: readonly string[];
|
|
82
84
|
}): Promise<void>;
|
|
83
85
|
prepareForResumeAfterRollback(): Promise<void>;
|
|
84
86
|
stopCurrent(): Promise<void>;
|
|
85
|
-
|
|
86
|
-
failCurrent(errorOrMetadata: string | NornRunOutcomeMetadata & {
|
|
87
|
+
replaceCurrentArgs(args: unknown): Promise<void>;
|
|
88
|
+
failCurrent(errorOrMetadata: string | (NornRunOutcomeMetadata & {
|
|
87
89
|
readonly summary: string;
|
|
88
|
-
}): Promise<void>;
|
|
90
|
+
})): Promise<void>;
|
|
89
91
|
private update;
|
|
90
92
|
private write;
|
|
91
93
|
}
|
|
92
94
|
export declare function listRuns(sessionCwd: string): Promise<NornRunInfo[]>;
|
|
93
95
|
export declare function getRunInfo(runRoot: string): Promise<NornRunInfo>;
|
|
94
96
|
export declare function resolveRunRoot(sessionCwd: string, run: string): Promise<string>;
|
|
97
|
+
export declare function assertRunVersion(version: unknown): void;
|
|
95
98
|
export {};
|
|
@@ -39,21 +39,21 @@ async function writeTextAtomically(path, content) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// src/internal/run-state.ts
|
|
42
|
-
import { createRunFileCoordinator } from "
|
|
42
|
+
import { createRunFileCoordinator } from "./file-coordinator.js";
|
|
43
43
|
import { runCurrentRoot } from "./run-store.js";
|
|
44
44
|
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
45
45
|
import { Value } from "typebox/value";
|
|
46
46
|
var RUN_STATE_FILE_NAME = "run-state.json";
|
|
47
47
|
var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
48
|
-
function
|
|
49
|
-
if (!isRecord(
|
|
48
|
+
function mergeInterruptedWorkflowArgs(currentArgs, args, fields) {
|
|
49
|
+
if (!isRecord(currentArgs) || !isRecord(args)) return args;
|
|
50
50
|
if (fields) {
|
|
51
|
-
const unsupportedFields = Object.keys(
|
|
52
|
-
if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow
|
|
51
|
+
const unsupportedFields = Object.keys(args).filter((field) => !fields.includes(field));
|
|
52
|
+
if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow args cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
|
|
53
53
|
}
|
|
54
|
-
return { ...
|
|
54
|
+
return { ...currentArgs, ...args };
|
|
55
55
|
}
|
|
56
|
-
var RUN_STATE_VERSION =
|
|
56
|
+
var RUN_STATE_VERSION = 2;
|
|
57
57
|
var NornRunStateStore = class _NornRunStateStore {
|
|
58
58
|
path;
|
|
59
59
|
state;
|
|
@@ -70,7 +70,8 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
70
70
|
id: input.id,
|
|
71
71
|
name: input.name,
|
|
72
72
|
entrypointWorkflowId: input.entrypointWorkflowId,
|
|
73
|
-
|
|
73
|
+
projectRoot: resolve(input.projectRoot),
|
|
74
|
+
workspace: resolve(input.workspace),
|
|
74
75
|
configOverride: input.configOverride,
|
|
75
76
|
status: "running",
|
|
76
77
|
current: input.current,
|
|
@@ -87,7 +88,7 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
87
88
|
static async load(runRoot) {
|
|
88
89
|
const currentRoot = runCurrentRoot(runRoot);
|
|
89
90
|
const files = createRunFileCoordinator(runRoot);
|
|
90
|
-
const state = await files.withExclusiveLock(join(currentRoot, RUN_STATE_FILE_NAME), async (path) => parseNornRunState(await readRunStateFile({ path, legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) })));
|
|
91
|
+
const state = await files.withExclusiveLock(join(currentRoot, RUN_STATE_FILE_NAME), async (path) => parseNornRunState(await readRunStateFile({ path, legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) }), "execute"));
|
|
91
92
|
return new _NornRunStateStore({ path: join(currentRoot, RUN_STATE_FILE_NAME), state, files });
|
|
92
93
|
}
|
|
93
94
|
currentState() {
|
|
@@ -125,11 +126,11 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
125
126
|
failed: { workflowId, error: metadata.summary, metadata, failedAt }
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
|
-
async interruptCurrent(
|
|
129
|
+
async interruptCurrent(args, interruption) {
|
|
129
130
|
if (!this.state.current) throw new Error("Cannot interrupt run without current step");
|
|
130
131
|
await this.update({
|
|
131
132
|
status: "interrupted",
|
|
132
|
-
current: { ...this.state.current,
|
|
133
|
+
current: { ...this.state.current, args, interruption: { status: "pending", ...interruption } },
|
|
133
134
|
outcome: null,
|
|
134
135
|
failed: null
|
|
135
136
|
});
|
|
@@ -144,11 +145,11 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
144
145
|
if (!this.state.current) throw new Error("Cannot stop run without current step");
|
|
145
146
|
await this.update({ status: "stopped", outcome: null, failed: null });
|
|
146
147
|
}
|
|
147
|
-
async
|
|
148
|
+
async replaceCurrentArgs(args) {
|
|
148
149
|
if (!this.state.current) throw new Error("Cannot resume run without current step");
|
|
149
150
|
await this.update({
|
|
150
151
|
status: "running",
|
|
151
|
-
current: { ...this.state.current,
|
|
152
|
+
current: { ...this.state.current, args, interruption: { ...this.state.current.interruption, status: "satisfied" } },
|
|
152
153
|
outcome: null,
|
|
153
154
|
failed: null
|
|
154
155
|
});
|
|
@@ -165,15 +166,15 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
165
166
|
}
|
|
166
167
|
async update(patch) {
|
|
167
168
|
await this.files.withExclusiveLock(this.path, async (path) => {
|
|
168
|
-
const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
|
|
169
|
+
const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }), "execute");
|
|
169
170
|
const next = { ...current, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
170
|
-
if (next.current?.
|
|
171
|
+
if (next.current?.args !== void 0) Value.Assert(jsonValueSchema, next.current.args);
|
|
171
172
|
await writeJsonAtomically(path, next);
|
|
172
173
|
this.state = next;
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
async write() {
|
|
176
|
-
if (this.state.current?.
|
|
177
|
+
if (this.state.current?.args !== void 0) Value.Assert(jsonValueSchema, this.state.current.args);
|
|
177
178
|
await mkdir2(dirname2(this.path), { recursive: true });
|
|
178
179
|
await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
|
|
179
180
|
}
|
|
@@ -192,13 +193,19 @@ async function listRuns(sessionCwd) {
|
|
|
192
193
|
}
|
|
193
194
|
async function getRunInfo(runRoot) {
|
|
194
195
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
196
|
+
const pendingResume = await readOptionalRunResumeRequest(runRoot).then(
|
|
197
|
+
(request) => ({ status: "fulfilled", request }),
|
|
198
|
+
(error) => ({ status: "rejected", error })
|
|
199
|
+
);
|
|
200
|
+
const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)), "inspect");
|
|
201
|
+
if (state.version === RUN_STATE_VERSION && pendingResume.status === "rejected") throw pendingResume.error;
|
|
202
|
+
const resumeRequest = state.version === RUN_STATE_VERSION && pendingResume.status === "fulfilled" ? pendingResume.request : void 0;
|
|
197
203
|
return {
|
|
198
204
|
version: state.version,
|
|
199
205
|
id: state.id,
|
|
200
206
|
name: state.name,
|
|
201
207
|
path: runRoot,
|
|
208
|
+
paths: { project: state.projectRoot ?? resolve(runRoot, "../../.."), workspace: state.workspace },
|
|
202
209
|
entrypointWorkflowId: state.entrypointWorkflowId,
|
|
203
210
|
currentWorkflowId: state.current?.workflowId,
|
|
204
211
|
status: resumeRequest ? "running" : state.status,
|
|
@@ -255,6 +262,7 @@ async function getLaunchedRunInfo(runRoot) {
|
|
|
255
262
|
id: launchRequest.id,
|
|
256
263
|
name: launchRequest.name,
|
|
257
264
|
path: runRoot,
|
|
265
|
+
paths: { project: resolve(runRoot, "../../.."), workspace: join(runCurrentRoot(resolve(runRoot)), "workspace") },
|
|
258
266
|
entrypointWorkflowId: launchRequest.workflowId,
|
|
259
267
|
currentWorkflowId: launchRequest.workflowId,
|
|
260
268
|
status: "running",
|
|
@@ -277,10 +285,13 @@ async function readRunStateFile(input) {
|
|
|
277
285
|
function isRecord(value) {
|
|
278
286
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
279
287
|
}
|
|
280
|
-
function
|
|
288
|
+
function assertRunVersion(version) {
|
|
289
|
+
if (version !== RUN_STATE_VERSION) throw new Error(`Incompatible saved run state version ${String(version)}; this alpha requires version ${RUN_STATE_VERSION}. Use the original Norn version to resume this run, or start a new run. Saved files have not been migrated.`);
|
|
290
|
+
}
|
|
291
|
+
function parseNornRunState(value, mode) {
|
|
281
292
|
if (!value || typeof value !== "object") throw new Error("Invalid run state");
|
|
282
293
|
const state = value;
|
|
283
|
-
if (state.version !==
|
|
294
|
+
if (mode === "execute" || state.version !== 1) assertRunVersion(state.version);
|
|
284
295
|
if (typeof state.id !== "string" || state.id.length === 0) throw new Error("Invalid run state id");
|
|
285
296
|
const normalizedState = state;
|
|
286
297
|
if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0) normalizedState.name = state.id;
|
|
@@ -306,7 +317,7 @@ function runInterruption(state) {
|
|
|
306
317
|
if (state.status !== "interrupted" || !state.current?.interruption) return void 0;
|
|
307
318
|
return {
|
|
308
319
|
workflowId: state.current.workflowId,
|
|
309
|
-
params: state.current.
|
|
320
|
+
args: state.version === 1 ? state.current.params : state.current.args,
|
|
310
321
|
description: state.current.interruption.description ?? "",
|
|
311
322
|
fields: state.current.interruption.fields
|
|
312
323
|
};
|
|
@@ -320,8 +331,9 @@ function runFailure(state) {
|
|
|
320
331
|
export {
|
|
321
332
|
NornRunStateStore,
|
|
322
333
|
RUN_STATE_FILE_NAME,
|
|
334
|
+
assertRunVersion,
|
|
323
335
|
getRunInfo,
|
|
324
336
|
listRuns,
|
|
325
|
-
|
|
337
|
+
mergeInterruptedWorkflowArgs,
|
|
326
338
|
resolveRunRoot
|
|
327
339
|
};
|
package/dist/internal/run.d.ts
CHANGED
|
@@ -1,51 +1,31 @@
|
|
|
1
1
|
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { NornRunOutcomeMetadata, NornRun } from "@vimhead.dev/norn";
|
|
3
3
|
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
4
|
-
import type { NornArtifacts } from "./artifacts.ts";
|
|
5
4
|
import type { NornRunLogs } from "./logs.ts";
|
|
6
5
|
import type { NornRunLogger } from "./run-log.ts";
|
|
7
|
-
import type { NornRunResources } from "../resources.ts";
|
|
8
6
|
type DefaultNornRunInput = {
|
|
9
7
|
readonly id: string;
|
|
10
8
|
readonly runRoot: string;
|
|
11
|
-
readonly projectRoot: string;
|
|
12
|
-
readonly workspace: string;
|
|
13
|
-
readonly cwd: string;
|
|
14
|
-
readonly isolationMode: NornWorkflowIsolationMode;
|
|
15
9
|
readonly signal?: AbortSignal;
|
|
16
10
|
readonly model?: CreateAgentSessionOptions["model"];
|
|
17
11
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
18
12
|
readonly agentDir?: string;
|
|
19
13
|
readonly responseCollector: NornAgentResponseCollector;
|
|
20
|
-
readonly resources: NornRunResources;
|
|
21
|
-
readonly state: NornRun["state"];
|
|
22
14
|
readonly logger: NornRunLogger;
|
|
23
|
-
readonly artifacts: NornArtifacts;
|
|
24
15
|
readonly logs: NornRunLogs;
|
|
25
16
|
};
|
|
26
|
-
export declare class NornRunContext implements
|
|
17
|
+
export declare class NornRunContext implements NornRun {
|
|
27
18
|
private readonly input;
|
|
28
|
-
readonly resources: NornRunResources;
|
|
29
|
-
readonly state: NornRun["state"];
|
|
30
|
-
readonly artifacts: NornRun["artifacts"];
|
|
31
19
|
readonly logs: NornRun["logs"];
|
|
32
20
|
readonly commands: NornRun["commands"];
|
|
33
21
|
readonly agents: NornRun["agents"];
|
|
34
22
|
readonly id: string;
|
|
35
|
-
readonly workspace: string;
|
|
36
|
-
readonly projectRoot: string;
|
|
37
|
-
readonly cwd: string;
|
|
38
23
|
constructor(input: DefaultNornRunInput);
|
|
39
|
-
forWorkflow(workflow: NornAnyWorkflowDeclaration): NornRunContext;
|
|
40
|
-
private get boundaryRoot();
|
|
41
24
|
private createAgentRunner;
|
|
42
|
-
|
|
43
|
-
projectPath(relativePath: string): string;
|
|
44
|
-
next(workflowId: string, params: unknown): ReturnType<NornRun["next"]>;
|
|
25
|
+
next(workflowId: string, args: unknown): ReturnType<NornRun["next"]>;
|
|
45
26
|
complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]>;
|
|
46
27
|
fail(metadata: NornRunOutcomeMetadata & {
|
|
47
28
|
readonly summary: string;
|
|
48
29
|
}): ReturnType<NornRun["fail"]>;
|
|
49
|
-
private resolveFromRoot;
|
|
50
30
|
}
|
|
51
31
|
export {};
|
package/dist/internal/run.js
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
// src/internal/run.ts
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
-
|
|
4
1
|
// ../core/src/workflow-transition.ts
|
|
5
2
|
function createWorkflowTransition(input) {
|
|
6
3
|
if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
|
|
7
|
-
return { type: "next", workflowId: input.workflowId,
|
|
4
|
+
return { type: "next", workflowId: input.workflowId, args: input.args };
|
|
8
5
|
}
|
|
9
6
|
|
|
10
7
|
// src/internal/run.ts
|
|
11
8
|
import { NornAgentRunner } from "./agents.js";
|
|
12
9
|
import { NornCommandRunner } from "./commands.js";
|
|
13
|
-
var NornRunContext = class
|
|
10
|
+
var NornRunContext = class {
|
|
14
11
|
constructor(input) {
|
|
15
12
|
this.input = input;
|
|
16
13
|
this.id = input.id;
|
|
17
|
-
this.workspace = input.workspace;
|
|
18
|
-
this.projectRoot = input.projectRoot;
|
|
19
|
-
this.cwd = input.cwd;
|
|
20
|
-
this.resources = input.resources;
|
|
21
|
-
this.state = input.state;
|
|
22
|
-
this.artifacts = input.artifacts;
|
|
23
14
|
this.logs = {
|
|
24
15
|
read: (log) => this.input.logs.read(log)
|
|
25
16
|
};
|
|
26
17
|
this.commands = {
|
|
27
18
|
run: (commandInput) => new NornCommandRunner({
|
|
28
|
-
boundaryRoot: this.boundaryRoot,
|
|
29
|
-
boundaryName: this.input.isolationMode,
|
|
30
|
-
cwd: this.cwd,
|
|
31
19
|
signal: this.input.signal,
|
|
32
20
|
logs: this.input.logs,
|
|
33
21
|
logger: this.input.logger
|
|
@@ -39,34 +27,14 @@ var NornRunContext = class _NornRunContext {
|
|
|
39
27
|
};
|
|
40
28
|
}
|
|
41
29
|
input;
|
|
42
|
-
resources;
|
|
43
|
-
state;
|
|
44
|
-
artifacts;
|
|
45
30
|
logs;
|
|
46
31
|
commands;
|
|
47
32
|
agents;
|
|
48
33
|
id;
|
|
49
|
-
workspace;
|
|
50
|
-
projectRoot;
|
|
51
|
-
cwd;
|
|
52
|
-
forWorkflow(workflow) {
|
|
53
|
-
const isolationMode = workflow.isolation.mode;
|
|
54
|
-
return new _NornRunContext({
|
|
55
|
-
...this.input,
|
|
56
|
-
isolationMode,
|
|
57
|
-
cwd: isolationMode === "project" ? this.projectRoot : this.workspace
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
get boundaryRoot() {
|
|
61
|
-
return this.input.isolationMode === "project" ? this.projectRoot : this.workspace;
|
|
62
|
-
}
|
|
63
34
|
createAgentRunner() {
|
|
64
35
|
return new NornAgentRunner({
|
|
65
36
|
id: this.id,
|
|
66
37
|
runRoot: this.input.runRoot,
|
|
67
|
-
boundaryRoot: this.boundaryRoot,
|
|
68
|
-
boundaryName: this.input.isolationMode,
|
|
69
|
-
cwd: this.cwd,
|
|
70
38
|
signal: this.input.signal,
|
|
71
39
|
model: this.input.model,
|
|
72
40
|
thinkingLevel: this.input.thinkingLevel,
|
|
@@ -76,14 +44,8 @@ var NornRunContext = class _NornRunContext {
|
|
|
76
44
|
responseCollector: this.input.responseCollector
|
|
77
45
|
});
|
|
78
46
|
}
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
projectPath(relativePath) {
|
|
83
|
-
return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
|
|
84
|
-
}
|
|
85
|
-
next(workflowId, params) {
|
|
86
|
-
return createWorkflowTransition({ workflowId, params });
|
|
47
|
+
next(workflowId, args) {
|
|
48
|
+
return createWorkflowTransition({ workflowId, args });
|
|
87
49
|
}
|
|
88
50
|
complete(metadata) {
|
|
89
51
|
return { type: "complete", metadata };
|
|
@@ -91,14 +53,6 @@ var NornRunContext = class _NornRunContext {
|
|
|
91
53
|
fail(metadata) {
|
|
92
54
|
return { type: "fail", metadata };
|
|
93
55
|
}
|
|
94
|
-
resolveFromRoot(root, boundaryRoot, path) {
|
|
95
|
-
const resolvedPath = isAbsolute(path) ? path : resolve(root, path);
|
|
96
|
-
const pathFromBoundary = relative(boundaryRoot, resolvedPath);
|
|
97
|
-
if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
|
|
98
|
-
throw new Error(`Workflow path escapes ${this.input.isolationMode} isolation: ${path}`);
|
|
99
|
-
}
|
|
100
|
-
return resolvedPath;
|
|
101
|
-
}
|
|
102
56
|
};
|
|
103
57
|
export {
|
|
104
58
|
NornRunContext
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function prepareRunWorkerDirectory(runRoot: string): Promise<string>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/internal/worker-directory.ts
|
|
2
|
+
import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// ../core/src/errors.ts
|
|
6
|
+
function isNodeError(error) {
|
|
7
|
+
return error instanceof Error && "code" in error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/internal/worker-directory.ts
|
|
11
|
+
async function prepareRunWorkerDirectory(runRoot) {
|
|
12
|
+
const directory = join(runRoot, "worker");
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(directory, { mode: 365 });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (!isNodeError(error) || error.code !== "EEXIST") throw error;
|
|
17
|
+
}
|
|
18
|
+
const metadata = await lstat(directory);
|
|
19
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Run worker cwd must be a directory, not a symlink: ${directory}`);
|
|
20
|
+
if ((await readdir(directory)).length !== 0) throw new Error(`Run worker cwd must be empty: ${directory}`);
|
|
21
|
+
await chmod(directory, 365);
|
|
22
|
+
return directory;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
prepareRunWorkerDirectory
|
|
26
|
+
};
|