@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
|
@@ -80,7 +80,8 @@ var NornRunLease = class _NornRunLease {
|
|
|
80
80
|
async assertOwned() {
|
|
81
81
|
if (this.isReleased) throw new Error("Run lease has been released");
|
|
82
82
|
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
83
|
-
if (!owner || owner.token !== this.token)
|
|
83
|
+
if (!owner || owner.token !== this.token)
|
|
84
|
+
throw new Error("Run lease was lost");
|
|
84
85
|
}
|
|
85
86
|
async release() {
|
|
86
87
|
if (this.isReleased) return;
|
|
@@ -88,7 +89,8 @@ var NornRunLease = class _NornRunLease {
|
|
|
88
89
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
89
90
|
await this.heartbeatChain.catch(() => void 0);
|
|
90
91
|
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
91
|
-
if (owner?.token === this.token)
|
|
92
|
+
if (owner?.token === this.token)
|
|
93
|
+
await rm2(this.lockRoot, { recursive: true, force: true });
|
|
92
94
|
}
|
|
93
95
|
get ownerPath() {
|
|
94
96
|
return join(this.lockRoot, OWNER_FILE_NAME);
|
|
@@ -105,12 +107,18 @@ var NornRunLease = class _NornRunLease {
|
|
|
105
107
|
async refreshHeartbeat() {
|
|
106
108
|
if (this.isReleased) return;
|
|
107
109
|
const owner = await readRunLeaseOwner(this.ownerPath);
|
|
108
|
-
if (!owner || owner.token !== this.token)
|
|
110
|
+
if (!owner || owner.token !== this.token)
|
|
111
|
+
throw new Error("Run lease was lost");
|
|
109
112
|
await this.writeOwner({ acquiredAt: owner.acquiredAt });
|
|
110
113
|
}
|
|
111
114
|
async writeOwner(input) {
|
|
112
115
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
113
|
-
await writeJsonAtomically(this.ownerPath, {
|
|
116
|
+
await writeJsonAtomically(this.ownerPath, {
|
|
117
|
+
token: this.token,
|
|
118
|
+
acquiredAt: input.acquiredAt,
|
|
119
|
+
heartbeatAt: now,
|
|
120
|
+
...this.processOwner
|
|
121
|
+
});
|
|
114
122
|
}
|
|
115
123
|
};
|
|
116
124
|
async function getRunLeaseHealth(runRoot) {
|
|
@@ -140,13 +148,25 @@ async function readRunLeaseOwner(path) {
|
|
|
140
148
|
function parseRunLeaseOwner(value) {
|
|
141
149
|
if (!value || typeof value !== "object") return void 0;
|
|
142
150
|
const owner = value;
|
|
143
|
-
if (typeof owner.token !== "string" || owner.token.length === 0)
|
|
144
|
-
|
|
145
|
-
if (typeof owner.
|
|
151
|
+
if (typeof owner.token !== "string" || owner.token.length === 0)
|
|
152
|
+
return void 0;
|
|
153
|
+
if (typeof owner.acquiredAt !== "string" || Number.isNaN(Date.parse(owner.acquiredAt)))
|
|
154
|
+
return void 0;
|
|
155
|
+
if (typeof owner.heartbeatAt !== "string" || Number.isNaN(Date.parse(owner.heartbeatAt)))
|
|
156
|
+
return void 0;
|
|
146
157
|
const pid = typeof owner.pid === "number" && Number.isInteger(owner.pid) ? owner.pid : 0;
|
|
147
158
|
const processGroupId = typeof owner.processGroupId === "number" && Number.isInteger(owner.processGroupId) ? owner.processGroupId : pid;
|
|
148
|
-
const command = Array.isArray(owner.command) ? owner.command.filter(
|
|
149
|
-
|
|
159
|
+
const command = Array.isArray(owner.command) ? owner.command.filter(
|
|
160
|
+
(value2) => typeof value2 === "string"
|
|
161
|
+
) : [];
|
|
162
|
+
return {
|
|
163
|
+
token: owner.token,
|
|
164
|
+
acquiredAt: owner.acquiredAt,
|
|
165
|
+
heartbeatAt: owner.heartbeatAt,
|
|
166
|
+
pid,
|
|
167
|
+
processGroupId,
|
|
168
|
+
command
|
|
169
|
+
};
|
|
150
170
|
}
|
|
151
171
|
function isRunLeaseStale(owner) {
|
|
152
172
|
return Date.now() - Date.parse(owner.heartbeatAt) > HEARTBEAT_TTL_MS;
|
package/dist/internal/run-log.js
CHANGED
|
@@ -47,23 +47,35 @@ var NornRunLogger = class _NornRunLogger {
|
|
|
47
47
|
return logger;
|
|
48
48
|
}
|
|
49
49
|
async record(event) {
|
|
50
|
-
await this.input.files.withExclusiveLock(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
manifest
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
await this.input.files.withExclusiveLock(
|
|
51
|
+
this.input.manifestPath,
|
|
52
|
+
async (path) => {
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = parseManifest(await readFile(path, "utf8"));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (this.isInitialized || !isNodeError(error) || error.code !== "ENOENT")
|
|
58
|
+
throw error;
|
|
59
|
+
manifest = { ...this.input.manifest, events: [] };
|
|
60
|
+
}
|
|
61
|
+
if (manifest.id !== this.input.manifest.id)
|
|
62
|
+
throw new Error("Run manifest identity changed");
|
|
63
|
+
await writeJsonAtomically(path, {
|
|
64
|
+
...manifest,
|
|
65
|
+
events: [
|
|
66
|
+
...manifest.events,
|
|
67
|
+
{ ...event, at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
this.isInitialized = true;
|
|
57
71
|
}
|
|
58
|
-
|
|
59
|
-
await writeJsonAtomically(path, { ...manifest, events: [...manifest.events, { ...event, at: (/* @__PURE__ */ new Date()).toISOString() }] });
|
|
60
|
-
this.isInitialized = true;
|
|
61
|
-
});
|
|
72
|
+
);
|
|
62
73
|
}
|
|
63
74
|
};
|
|
64
75
|
function parseManifest(content) {
|
|
65
76
|
const manifest = JSON.parse(content);
|
|
66
|
-
if (!manifest || typeof manifest !== "object" || typeof manifest.id !== "string" || !Array.isArray(manifest.events))
|
|
77
|
+
if (!manifest || typeof manifest !== "object" || typeof manifest.id !== "string" || !Array.isArray(manifest.events))
|
|
78
|
+
throw new Error("Invalid run manifest");
|
|
67
79
|
return manifest;
|
|
68
80
|
}
|
|
69
81
|
export {
|
|
@@ -8,7 +8,11 @@ function isNodeError(error) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// src/internal/run-state.ts
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
readRunLaunchRequest,
|
|
13
|
+
readOptionalRunResumeRequest,
|
|
14
|
+
RESUME_START_GRACE_MS
|
|
15
|
+
} from "./launch-request.js";
|
|
12
16
|
import { getRunLeaseHealth } from "./run-lease.js";
|
|
13
17
|
|
|
14
18
|
// ../core/src/atomic-files.ts
|
|
@@ -39,7 +43,9 @@ async function writeTextAtomically(path, content) {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
// src/internal/run-state.ts
|
|
42
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
createRunFileCoordinator
|
|
48
|
+
} from "./file-coordinator.js";
|
|
43
49
|
import { runCurrentRoot } from "./run-store.js";
|
|
44
50
|
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
45
51
|
import { Value } from "typebox/value";
|
|
@@ -48,8 +54,13 @@ var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
|
48
54
|
function mergeInterruptedWorkflowArgs(currentArgs, args, fields) {
|
|
49
55
|
if (!isRecord(currentArgs) || !isRecord(args)) return args;
|
|
50
56
|
if (fields) {
|
|
51
|
-
const unsupportedFields = Object.keys(args).filter(
|
|
52
|
-
|
|
57
|
+
const unsupportedFields = Object.keys(args).filter(
|
|
58
|
+
(field) => !fields.includes(field)
|
|
59
|
+
);
|
|
60
|
+
if (unsupportedFields.length > 0)
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Interrupted workflow args cannot update non-gate fields: ${unsupportedFields.join(", ")}`
|
|
63
|
+
);
|
|
53
64
|
}
|
|
54
65
|
return { ...currentArgs, ...args };
|
|
55
66
|
}
|
|
@@ -81,27 +92,53 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
81
92
|
startedAt: now,
|
|
82
93
|
updatedAt: now
|
|
83
94
|
};
|
|
84
|
-
const store = new _NornRunStateStore({
|
|
95
|
+
const store = new _NornRunStateStore({
|
|
96
|
+
path: join(runCurrentRoot(runRoot), RUN_STATE_FILE_NAME),
|
|
97
|
+
state,
|
|
98
|
+
files: createRunFileCoordinator(runRoot)
|
|
99
|
+
});
|
|
85
100
|
await store.write();
|
|
86
101
|
return store;
|
|
87
102
|
}
|
|
88
103
|
static async load(runRoot) {
|
|
89
104
|
const currentRoot = runCurrentRoot(runRoot);
|
|
90
105
|
const files = createRunFileCoordinator(runRoot);
|
|
91
|
-
const state = await files.withExclusiveLock(
|
|
92
|
-
|
|
106
|
+
const state = await files.withExclusiveLock(
|
|
107
|
+
join(currentRoot, RUN_STATE_FILE_NAME),
|
|
108
|
+
async (path) => parseNornRunState(
|
|
109
|
+
await readRunStateFile({
|
|
110
|
+
path,
|
|
111
|
+
legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME)
|
|
112
|
+
}),
|
|
113
|
+
"execute"
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
return new _NornRunStateStore({
|
|
117
|
+
path: join(currentRoot, RUN_STATE_FILE_NAME),
|
|
118
|
+
state,
|
|
119
|
+
files
|
|
120
|
+
});
|
|
93
121
|
}
|
|
94
122
|
currentState() {
|
|
95
123
|
return this.state;
|
|
96
124
|
}
|
|
97
125
|
async startStep(step) {
|
|
98
|
-
await this.update({
|
|
126
|
+
await this.update({
|
|
127
|
+
status: "running",
|
|
128
|
+
current: step,
|
|
129
|
+
outcome: null,
|
|
130
|
+
failed: null
|
|
131
|
+
});
|
|
99
132
|
}
|
|
100
133
|
async completeWithNext(completedWorkflowId, next) {
|
|
101
134
|
await this.update({
|
|
102
135
|
status: "running",
|
|
103
136
|
current: next,
|
|
104
|
-
lastCompleted: {
|
|
137
|
+
lastCompleted: {
|
|
138
|
+
workflowId: completedWorkflowId,
|
|
139
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
140
|
+
outcome: { type: "next", workflowId: next.workflowId }
|
|
141
|
+
},
|
|
105
142
|
outcome: null,
|
|
106
143
|
failed: null
|
|
107
144
|
});
|
|
@@ -121,62 +158,115 @@ var NornRunStateStore = class _NornRunStateStore {
|
|
|
121
158
|
await this.update({
|
|
122
159
|
status: "failed",
|
|
123
160
|
current: null,
|
|
124
|
-
lastCompleted: {
|
|
125
|
-
|
|
161
|
+
lastCompleted: {
|
|
162
|
+
workflowId,
|
|
163
|
+
completedAt: failedAt,
|
|
164
|
+
outcome: { type: "fail" }
|
|
165
|
+
},
|
|
166
|
+
outcome: {
|
|
167
|
+
workflowId,
|
|
168
|
+
completedAt: failedAt,
|
|
169
|
+
status: "failed",
|
|
170
|
+
metadata
|
|
171
|
+
},
|
|
126
172
|
failed: { workflowId, error: metadata.summary, metadata, failedAt }
|
|
127
173
|
});
|
|
128
174
|
}
|
|
129
175
|
async interruptCurrent(args, interruption) {
|
|
130
|
-
if (!this.state.current)
|
|
176
|
+
if (!this.state.current)
|
|
177
|
+
throw new Error("Cannot interrupt run without current step");
|
|
131
178
|
await this.update({
|
|
132
179
|
status: "interrupted",
|
|
133
|
-
current: {
|
|
180
|
+
current: {
|
|
181
|
+
...this.state.current,
|
|
182
|
+
args,
|
|
183
|
+
interruption: { status: "pending", ...interruption }
|
|
184
|
+
},
|
|
134
185
|
outcome: null,
|
|
135
186
|
failed: null
|
|
136
187
|
});
|
|
137
188
|
}
|
|
138
189
|
async prepareForResumeAfterRollback() {
|
|
139
|
-
if (!this.state.current)
|
|
190
|
+
if (!this.state.current)
|
|
191
|
+
throw new Error("Cannot resume a checkpoint without a current step");
|
|
140
192
|
if (this.state.status === "interrupted") return;
|
|
141
|
-
if (this.state.status !== "running")
|
|
193
|
+
if (this.state.status !== "running")
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Cannot resume checkpoint with ${this.state.status} status`
|
|
196
|
+
);
|
|
142
197
|
await this.update({ status: "pendingResume", outcome: null, failed: null });
|
|
143
198
|
}
|
|
144
199
|
async stopCurrent() {
|
|
145
|
-
if (!this.state.current)
|
|
200
|
+
if (!this.state.current)
|
|
201
|
+
throw new Error("Cannot stop run without current step");
|
|
146
202
|
await this.update({ status: "stopped", outcome: null, failed: null });
|
|
147
203
|
}
|
|
148
204
|
async replaceCurrentArgs(args) {
|
|
149
|
-
if (!this.state.current)
|
|
205
|
+
if (!this.state.current)
|
|
206
|
+
throw new Error("Cannot resume run without current step");
|
|
150
207
|
await this.update({
|
|
151
208
|
status: "running",
|
|
152
|
-
current: {
|
|
209
|
+
current: {
|
|
210
|
+
...this.state.current,
|
|
211
|
+
args,
|
|
212
|
+
interruption: {
|
|
213
|
+
...this.state.current.interruption,
|
|
214
|
+
status: "satisfied"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
153
217
|
outcome: null,
|
|
154
218
|
failed: null
|
|
155
219
|
});
|
|
156
220
|
}
|
|
157
221
|
async failCurrent(errorOrMetadata) {
|
|
158
|
-
if (!this.state.current)
|
|
222
|
+
if (!this.state.current)
|
|
223
|
+
throw new Error("Cannot fail run without current step");
|
|
159
224
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
160
225
|
const metadata = typeof errorOrMetadata === "string" ? { summary: errorOrMetadata } : errorOrMetadata;
|
|
161
226
|
await this.update({
|
|
162
227
|
status: "failed",
|
|
163
|
-
outcome: {
|
|
164
|
-
|
|
228
|
+
outcome: {
|
|
229
|
+
workflowId: this.state.current.workflowId,
|
|
230
|
+
completedAt: failedAt,
|
|
231
|
+
status: "failed",
|
|
232
|
+
metadata
|
|
233
|
+
},
|
|
234
|
+
failed: {
|
|
235
|
+
workflowId: this.state.current.workflowId,
|
|
236
|
+
error: metadata.summary,
|
|
237
|
+
metadata,
|
|
238
|
+
failedAt
|
|
239
|
+
}
|
|
165
240
|
});
|
|
166
241
|
}
|
|
167
242
|
async update(patch) {
|
|
168
243
|
await this.files.withExclusiveLock(this.path, async (path) => {
|
|
169
|
-
const current = parseNornRunState(
|
|
170
|
-
|
|
171
|
-
|
|
244
|
+
const current = parseNornRunState(
|
|
245
|
+
await readRunStateFile({
|
|
246
|
+
path,
|
|
247
|
+
legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME)
|
|
248
|
+
}),
|
|
249
|
+
"execute"
|
|
250
|
+
);
|
|
251
|
+
const next = {
|
|
252
|
+
...current,
|
|
253
|
+
...patch,
|
|
254
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
255
|
+
};
|
|
256
|
+
if (next.current?.args !== void 0)
|
|
257
|
+
Value.Assert(jsonValueSchema, next.current.args);
|
|
172
258
|
await writeJsonAtomically(path, next);
|
|
173
259
|
this.state = next;
|
|
174
260
|
});
|
|
175
261
|
}
|
|
176
262
|
async write() {
|
|
177
|
-
if (this.state.current?.args !== void 0)
|
|
263
|
+
if (this.state.current?.args !== void 0)
|
|
264
|
+
Value.Assert(jsonValueSchema, this.state.current.args);
|
|
178
265
|
await mkdir2(dirname2(this.path), { recursive: true });
|
|
179
|
-
await this.files.withExclusiveLock(
|
|
266
|
+
await this.files.withExclusiveLock(
|
|
267
|
+
this.path,
|
|
268
|
+
(path) => writeJsonAtomically(path, this.state)
|
|
269
|
+
);
|
|
180
270
|
}
|
|
181
271
|
};
|
|
182
272
|
async function listRuns(sessionCwd) {
|
|
@@ -188,7 +278,9 @@ async function listRuns(sessionCwd) {
|
|
|
188
278
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
189
279
|
throw error;
|
|
190
280
|
}
|
|
191
|
-
const runs = await Promise.all(
|
|
281
|
+
const runs = await Promise.all(
|
|
282
|
+
entries.filter((entry) => entry.isDirectory()).map((entry) => readNornRunInfo(resolve(runsRoot, entry.name)))
|
|
283
|
+
);
|
|
192
284
|
return runs.filter((run) => run !== void 0).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
193
285
|
}
|
|
194
286
|
async function getRunInfo(runRoot) {
|
|
@@ -197,15 +289,22 @@ async function getRunInfo(runRoot) {
|
|
|
197
289
|
(request) => ({ status: "fulfilled", request }),
|
|
198
290
|
(error) => ({ status: "rejected", error })
|
|
199
291
|
);
|
|
200
|
-
const state = parseNornRunState(
|
|
201
|
-
|
|
292
|
+
const state = parseNornRunState(
|
|
293
|
+
await readRunStateJson(runCurrentRoot(runRoot)),
|
|
294
|
+
"inspect"
|
|
295
|
+
);
|
|
296
|
+
if (state.version === RUN_STATE_VERSION && pendingResume.status === "rejected")
|
|
297
|
+
throw pendingResume.error;
|
|
202
298
|
const resumeRequest = state.version === RUN_STATE_VERSION && pendingResume.status === "fulfilled" ? pendingResume.request : void 0;
|
|
203
299
|
return {
|
|
204
300
|
version: state.version,
|
|
205
301
|
id: state.id,
|
|
206
302
|
name: state.name,
|
|
207
303
|
path: runRoot,
|
|
208
|
-
paths: {
|
|
304
|
+
paths: {
|
|
305
|
+
project: state.projectRoot ?? resolve(runRoot, "../../.."),
|
|
306
|
+
workspace: state.workspace
|
|
307
|
+
},
|
|
209
308
|
entrypointWorkflowId: state.entrypointWorkflowId,
|
|
210
309
|
currentWorkflowId: state.current?.workflowId,
|
|
211
310
|
status: resumeRequest ? "running" : state.status,
|
|
@@ -226,17 +325,21 @@ async function resolveRunRoot(sessionCwd, run) {
|
|
|
226
325
|
try {
|
|
227
326
|
if (await readNornRunInfo(candidate)) return candidate;
|
|
228
327
|
} catch (error) {
|
|
229
|
-
if (!isNodeError(error) && (!(error instanceof Error) || !error.message.includes("run state")))
|
|
328
|
+
if (!isNodeError(error) && (!(error instanceof Error) || !error.message.includes("run state")))
|
|
329
|
+
throw error;
|
|
230
330
|
}
|
|
231
331
|
}
|
|
232
332
|
const runs = await listRuns(sessionCwd);
|
|
233
|
-
const match = runs.find(
|
|
333
|
+
const match = runs.find(
|
|
334
|
+
(entry) => entry.id === run || entry.name === run || entry.path === run || entry.path.endsWith(`/${run}`)
|
|
335
|
+
);
|
|
234
336
|
if (!match) throw new Error(`Unknown run: ${run}`);
|
|
235
337
|
return match.path;
|
|
236
338
|
}
|
|
237
339
|
function runRootCandidates(sessionCwd, run) {
|
|
238
340
|
const candidates = [isAbsolute(run) ? run : resolve(sessionCwd, run)];
|
|
239
|
-
if (!isAbsolute(run))
|
|
341
|
+
if (!isAbsolute(run))
|
|
342
|
+
candidates.push(resolve(sessionCwd, ".norn", "runs", run));
|
|
240
343
|
return Array.from(new Set(candidates));
|
|
241
344
|
}
|
|
242
345
|
async function getPendingResumeHealth(runRoot, requestedAt) {
|
|
@@ -262,7 +365,10 @@ async function getLaunchedRunInfo(runRoot) {
|
|
|
262
365
|
id: launchRequest.id,
|
|
263
366
|
name: launchRequest.name,
|
|
264
367
|
path: runRoot,
|
|
265
|
-
paths: {
|
|
368
|
+
paths: {
|
|
369
|
+
project: resolve(runRoot, "../../.."),
|
|
370
|
+
workspace: join(runCurrentRoot(resolve(runRoot)), "workspace")
|
|
371
|
+
},
|
|
266
372
|
entrypointWorkflowId: launchRequest.workflowId,
|
|
267
373
|
currentWorkflowId: launchRequest.workflowId,
|
|
268
374
|
status: "running",
|
|
@@ -272,7 +378,10 @@ async function getLaunchedRunInfo(runRoot) {
|
|
|
272
378
|
};
|
|
273
379
|
}
|
|
274
380
|
async function readRunStateJson(currentRoot) {
|
|
275
|
-
return readRunStateFile({
|
|
381
|
+
return readRunStateFile({
|
|
382
|
+
path: join(currentRoot, RUN_STATE_FILE_NAME),
|
|
383
|
+
legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME)
|
|
384
|
+
});
|
|
276
385
|
}
|
|
277
386
|
async function readRunStateFile(input) {
|
|
278
387
|
try {
|
|
@@ -286,35 +395,50 @@ function isRecord(value) {
|
|
|
286
395
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
287
396
|
}
|
|
288
397
|
function assertRunVersion(version) {
|
|
289
|
-
if (version !== RUN_STATE_VERSION)
|
|
398
|
+
if (version !== RUN_STATE_VERSION)
|
|
399
|
+
throw new Error(
|
|
400
|
+
`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.`
|
|
401
|
+
);
|
|
290
402
|
}
|
|
291
403
|
function parseNornRunState(value, mode) {
|
|
292
404
|
if (!value || typeof value !== "object") throw new Error("Invalid run state");
|
|
293
405
|
const state = value;
|
|
294
|
-
if (mode === "execute" || state.version !== 1)
|
|
295
|
-
|
|
406
|
+
if (mode === "execute" || state.version !== 1)
|
|
407
|
+
assertRunVersion(state.version);
|
|
408
|
+
if (typeof state.id !== "string" || state.id.length === 0)
|
|
409
|
+
throw new Error("Invalid run state id");
|
|
296
410
|
const normalizedState = state;
|
|
297
|
-
if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0)
|
|
298
|
-
|
|
299
|
-
if (typeof
|
|
300
|
-
|
|
301
|
-
if (
|
|
411
|
+
if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0)
|
|
412
|
+
normalizedState.name = state.id;
|
|
413
|
+
if (typeof normalizedState.entrypointWorkflowId !== "string" && typeof normalizedState.rootWorkflowId === "string")
|
|
414
|
+
normalizedState.entrypointWorkflowId = normalizedState.rootWorkflowId;
|
|
415
|
+
if (typeof state.entrypointWorkflowId !== "string" || state.entrypointWorkflowId.length === 0)
|
|
416
|
+
throw new Error("Invalid run state entrypoint workflow id");
|
|
417
|
+
if (typeof state.workspace !== "string" || state.workspace.length === 0)
|
|
418
|
+
throw new Error("Invalid run state workspace");
|
|
419
|
+
if (state.status !== "running" && state.status !== "interrupted" && state.status !== "stopped" && state.status !== "pendingResume" && state.status !== "completed" && state.status !== "failed")
|
|
420
|
+
throw new Error("Invalid run state status");
|
|
302
421
|
if (state.status === "interrupted") assertInterruptedNornRunState(state);
|
|
303
|
-
if (state.status === "stopped" || state.status === "pendingResume")
|
|
304
|
-
|
|
422
|
+
if (state.status === "stopped" || state.status === "pendingResume")
|
|
423
|
+
assertResumableNornRunState(state);
|
|
424
|
+
if (typeof state.startedAt !== "string" || typeof state.updatedAt !== "string")
|
|
425
|
+
throw new Error("Invalid run state timestamps");
|
|
305
426
|
return state;
|
|
306
427
|
}
|
|
307
428
|
function assertInterruptedNornRunState(state) {
|
|
308
429
|
if (!state.current) throw new Error("Invalid interrupted run current step");
|
|
309
430
|
const interruption = state.current.interruption;
|
|
310
|
-
if (!interruption || interruption.status !== "pending")
|
|
311
|
-
|
|
431
|
+
if (!interruption || interruption.status !== "pending")
|
|
432
|
+
throw new Error("Invalid run interruption");
|
|
433
|
+
if (typeof interruption.description !== "string" || interruption.description.length === 0)
|
|
434
|
+
throw new Error("Invalid run interruption description");
|
|
312
435
|
}
|
|
313
436
|
function assertResumableNornRunState(state) {
|
|
314
437
|
if (!state.current) throw new Error("Invalid resumable run current step");
|
|
315
438
|
}
|
|
316
439
|
function runInterruption(state) {
|
|
317
|
-
if (state.status !== "interrupted" || !state.current?.interruption)
|
|
440
|
+
if (state.status !== "interrupted" || !state.current?.interruption)
|
|
441
|
+
return void 0;
|
|
318
442
|
return {
|
|
319
443
|
workflowId: state.current.workflowId,
|
|
320
444
|
args: state.version === 1 ? state.current.params : state.current.args,
|