@vimhead.dev/norn-cli 0.1.0-tip.35390859630.1 → 0.1.0-tip.35568631545.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 +36 -15
- package/assets/docs/README.md +4 -4
- package/assets/docs/agents.md +39 -11
- package/assets/docs/cli.md +6 -6
- package/assets/docs/composition.md +12 -12
- package/assets/docs/persistence.md +31 -17
- package/assets/docs/providers.md +2 -2
- package/assets/docs/recovery.md +1 -1
- package/assets/docs/workflows.md +29 -17
- package/assets/examples/agent-then-analysis/README.md +8 -8
- package/assets/examples/agent-then-analysis/plugin.ts +23 -21
- package/assets/examples/caller-selected-continuation/README.md +11 -10
- package/assets/examples/caller-selected-continuation/caller.ts +15 -13
- package/assets/examples/caller-selected-continuation/producer.ts +11 -8
- package/assets/examples/coordinating-multiple-agents/README.md +27 -17
- package/assets/examples/coordinating-multiple-agents/plugin.ts +64 -41
- 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 +25 -10
- package/assets/examples/getting-started/plugin.ts +30 -11
- package/assets/examples/minimal-workflow/README.md +7 -6
- package/assets/examples/minimal-workflow/plugin.ts +8 -5
- package/assets/examples/shared-state/README.md +5 -5
- package/assets/examples/shared-state/plugin.ts +38 -25
- package/assets/examples/shared-state/shared-state.ts +30 -49
- package/assets/examples/shared-state/state-tools.ts +68 -0
- package/assets/examples/worktree-development-loop/README.md +14 -12
- package/assets/examples/worktree-development-loop/scope.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +3 -3
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +6 -4
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +12 -11
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +10 -6
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +14 -11
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +1 -2
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +16 -15
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +1 -2
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +9 -3
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- 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 +35 -49
- package/assets/packages/cli/src/internal/execution-context.ts +64 -0
- 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 +7 -2
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +24 -9
- package/assets/packages/cli/src/internal/working-directory.ts +6 -0
- package/assets/packages/cli/src/workflow-loader.ts +1 -2
- package/assets/packages/sdk/src/api.ts +65 -91
- package/assets/packages/sdk/src/index.ts +0 -3
- package/assets/tests/workflow-ref.test.ts +9 -9
- package/dist/cli.js +8 -3
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- 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 +1 -1
- package/dist/internal/engine.js +29 -43
- package/dist/internal/execution-context.d.ts +26 -0
- package/dist/internal/execution-context.js +55 -0
- package/dist/internal/file-coordinator.d.ts +17 -0
- package/dist/internal/file-coordinator.js +162 -0
- package/dist/internal/logs.d.ts +1 -1
- package/dist/internal/run-log.d.ts +1 -1
- package/dist/internal/run-state.d.ts +2 -0
- package/dist/internal/run-state.js +5 -2
- package/dist/internal/worker-directory.d.ts +1 -0
- package/dist/internal/worker-directory.js +26 -0
- package/dist/internal/workflow-registry.d.ts +13 -3
- package/dist/internal/workflow-registry.js +12 -7
- package/dist/internal/working-directory.d.ts +1 -0
- package/dist/internal/working-directory.js +9 -0
- package/dist/workflow-loader.js +1 -2
- package/package.json +2 -2
- package/assets/docs/resources.md +0 -46
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
- package/assets/examples/shared-state/state-adapter.ts +0 -76
- 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.ts +0 -127
- 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/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.d.ts +0 -49
- package/dist/internal/run.js +0 -103
- 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
package/dist/internal/engine.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
// src/internal/engine.ts
|
|
2
|
-
import { createRunFileCoordinator } from "
|
|
2
|
+
import { createRunFileCoordinator } from "./file-coordinator.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { mkdir } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { Value } from "typebox/value";
|
|
7
7
|
import { NornAgentResponseCollector } from "./agent-response-tool.js";
|
|
8
|
-
import { NornArtifacts } from "./artifacts.js";
|
|
9
8
|
import { errorMessage, NornRunStoppedError } from "./errors.js";
|
|
10
9
|
import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.js";
|
|
11
10
|
import { NornRunLogs } from "./logs.js";
|
|
12
11
|
import { NornRunLease } from "./run-lease.js";
|
|
13
12
|
import { NornRunLogger } from "./run-log.js";
|
|
14
|
-
import { NornRunResources } from "../resources.js";
|
|
15
13
|
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot } from "./run-state.js";
|
|
16
14
|
import { NornRunStore, runCurrentRoot } from "./run-store.js";
|
|
17
|
-
import {
|
|
15
|
+
import { NornExecutionContext } from "./execution-context.js";
|
|
18
16
|
import { NornWorkflowRegistry } from "./workflow-registry.js";
|
|
19
17
|
var RUNS_DIR_NAME = ".norn";
|
|
20
18
|
var MANIFEST_FILE_NAME = "manifest.json";
|
|
@@ -103,14 +101,14 @@ var NornEngine = class {
|
|
|
103
101
|
const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
|
|
104
102
|
Value.Decode(workflow.args, rawArgs);
|
|
105
103
|
await session.state.replaceCurrentArgs(rawArgs);
|
|
106
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
104
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
107
105
|
const resumedCurrent = session.state.currentState().current;
|
|
108
106
|
if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
109
107
|
isLeaseOwnedByScheduler = true;
|
|
110
108
|
return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
|
|
111
109
|
}
|
|
112
110
|
if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
113
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
111
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
114
112
|
isLeaseOwnedByScheduler = true;
|
|
115
113
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
116
114
|
} finally {
|
|
@@ -128,28 +126,27 @@ var NornEngine = class {
|
|
|
128
126
|
try {
|
|
129
127
|
for (let step = 1; step <= 1e3; step++) {
|
|
130
128
|
throwIfRunAborted(session.activeRun);
|
|
131
|
-
const stepRuntime = session.run.forWorkflow(currentStep.workflow);
|
|
132
129
|
if (shouldPauseForGate(currentStep)) {
|
|
133
|
-
const description = await this.registry.describeGate(currentStep.workflow,
|
|
130
|
+
const description = await this.registry.describeGate({ workflow: currentStep.workflow, execution: session.execution, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
|
|
134
131
|
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
135
132
|
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
136
133
|
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
137
134
|
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
138
|
-
return interruptedLaunchResult({ id:
|
|
135
|
+
return interruptedLaunchResult({ id: session.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
139
136
|
}
|
|
140
137
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
141
|
-
const stepResult = await this.executeWorkflowStep(session, currentStep
|
|
138
|
+
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
142
139
|
throwIfRunAborted(session.activeRun);
|
|
143
140
|
if (stepResult.type === "complete") {
|
|
144
141
|
await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
|
|
145
142
|
await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
146
143
|
await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
|
|
147
|
-
return { status: "completed", id:
|
|
144
|
+
return { status: "completed", id: session.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
148
145
|
}
|
|
149
146
|
if (stepResult.type === "fail") {
|
|
150
147
|
await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
|
|
151
148
|
await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
152
|
-
return { status: "failed", id:
|
|
149
|
+
return { status: "failed", id: session.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
153
150
|
}
|
|
154
151
|
const nextStep = this.nextWorkflowStep(stepResult);
|
|
155
152
|
await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
|
|
@@ -162,7 +159,7 @@ var NornEngine = class {
|
|
|
162
159
|
if (isRunStopped(session, error)) {
|
|
163
160
|
await session.state.stopCurrent();
|
|
164
161
|
await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
|
|
165
|
-
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.
|
|
162
|
+
return { status: "stopped", id: session.execution.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
|
|
166
163
|
}
|
|
167
164
|
await session.state.failCurrent(errorMessage(error));
|
|
168
165
|
await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
|
|
@@ -172,13 +169,13 @@ var NornEngine = class {
|
|
|
172
169
|
this.finishActiveRun(session.runRoot, session.activeRun);
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
|
-
async executeWorkflowStep(session, step
|
|
172
|
+
async executeWorkflowStep(session, step) {
|
|
176
173
|
const startedAtMs = Date.now();
|
|
177
174
|
try {
|
|
178
|
-
await assertWorkspaceBoundary(session,
|
|
175
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
179
176
|
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
180
|
-
const result = await this.registry.execute(step.workflow,
|
|
181
|
-
await assertWorkspaceBoundary(session,
|
|
177
|
+
const result = await this.registry.execute({ workflow: step.workflow, execution: session.execution, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
|
|
178
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
182
179
|
const durationMs = Date.now() - startedAtMs;
|
|
183
180
|
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
184
181
|
else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
@@ -209,7 +206,8 @@ var NornEngine = class {
|
|
|
209
206
|
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
210
207
|
const currentRoot = runCurrentRoot(runRoot);
|
|
211
208
|
const workspace = join(currentRoot, "workspace");
|
|
212
|
-
const
|
|
209
|
+
const paths = { project: resolve(this.input.cwd), workspace };
|
|
210
|
+
const cwd = process.cwd();
|
|
213
211
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
214
212
|
await mkdir(runRoot, { recursive: true });
|
|
215
213
|
const lease = await NornRunLease.acquire(runRoot);
|
|
@@ -226,8 +224,9 @@ var NornEngine = class {
|
|
|
226
224
|
initialCwd: cwd,
|
|
227
225
|
startedAt
|
|
228
226
|
} });
|
|
229
|
-
const
|
|
227
|
+
const execution = await this.buildExecutionContext({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
230
228
|
const state = await NornRunStateStore.create(runRoot, {
|
|
229
|
+
projectRoot: paths.project,
|
|
231
230
|
id,
|
|
232
231
|
name,
|
|
233
232
|
entrypointWorkflowId: workflow.id,
|
|
@@ -242,7 +241,7 @@ var NornEngine = class {
|
|
|
242
241
|
},
|
|
243
242
|
startedAt
|
|
244
243
|
});
|
|
245
|
-
const session = { runRoot,
|
|
244
|
+
const session = { runRoot, cwd, execution, paths, state, lease, runStore, logger, activeRun };
|
|
246
245
|
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
247
246
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
248
247
|
return session;
|
|
@@ -261,37 +260,27 @@ var NornEngine = class {
|
|
|
261
260
|
const runStore = await NornRunStore.open(runRoot);
|
|
262
261
|
const currentRoot = runCurrentRoot(runRoot);
|
|
263
262
|
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
|
|
268
|
-
return { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
263
|
+
const paths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
|
|
264
|
+
const execution = await this.buildExecutionContext({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
265
|
+
return { runRoot, cwd: process.cwd(), execution, paths, state, lease, runStore, logger, activeRun };
|
|
269
266
|
} catch (error) {
|
|
270
267
|
this.failActiveRun(runRoot, activeRun);
|
|
271
268
|
throw error;
|
|
272
269
|
}
|
|
273
270
|
}
|
|
274
|
-
async
|
|
275
|
-
const artifactsRoot = join(input.currentRoot, "artifacts");
|
|
271
|
+
async buildExecutionContext(input) {
|
|
276
272
|
const logsRoot = join(input.currentRoot, "logs");
|
|
277
|
-
await mkdir(input.workspace, { recursive: true });
|
|
278
|
-
await mkdir(artifactsRoot, { recursive: true });
|
|
273
|
+
await mkdir(input.paths.workspace, { recursive: true });
|
|
279
274
|
await mkdir(logsRoot, { recursive: true });
|
|
280
|
-
const
|
|
281
|
-
return new
|
|
275
|
+
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
276
|
+
return new NornExecutionContext({
|
|
282
277
|
id: input.id,
|
|
283
278
|
runRoot: input.currentRoot,
|
|
284
|
-
workspace: input.workspace,
|
|
285
|
-
projectRoot: this.input.cwd,
|
|
286
|
-
cwd: input.cwd,
|
|
287
|
-
isolationMode: input.isolationMode,
|
|
288
279
|
signal: input.signal,
|
|
289
280
|
agentDir: this.input.agentDir,
|
|
290
281
|
responseCollector: this.responseCollector,
|
|
291
|
-
resources,
|
|
292
282
|
logger: input.logger,
|
|
293
|
-
|
|
294
|
-
logs: new NornRunLogs(logsRoot, resources.files)
|
|
283
|
+
logs: new NornRunLogs(logsRoot, files)
|
|
295
284
|
});
|
|
296
285
|
}
|
|
297
286
|
startActiveRun(runRoot) {
|
|
@@ -326,10 +315,7 @@ var NornEngine = class {
|
|
|
326
315
|
}
|
|
327
316
|
};
|
|
328
317
|
function defaultRunRoot(cwd, id) {
|
|
329
|
-
return join(cwd, RUNS_DIR_NAME, "runs", id);
|
|
330
|
-
}
|
|
331
|
-
function workflowDefaultCwd(projectRoot, workspace, workflow) {
|
|
332
|
-
return workflow?.isolation.mode === "project" ? projectRoot : workspace;
|
|
318
|
+
return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
|
|
333
319
|
}
|
|
334
320
|
function toWorkflowStep(workflow, step) {
|
|
335
321
|
return {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { NornAgents, NornCommands, NornLogs, NornRun } from "@vimhead.dev/norn";
|
|
3
|
+
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
4
|
+
import type { NornRunLogs } from "./logs.ts";
|
|
5
|
+
import type { NornRunLogger } from "./run-log.ts";
|
|
6
|
+
type NornExecutionContextInput = {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly runRoot: string;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
readonly model?: CreateAgentSessionOptions["model"];
|
|
11
|
+
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
12
|
+
readonly agentDir?: string;
|
|
13
|
+
readonly responseCollector: NornAgentResponseCollector;
|
|
14
|
+
readonly logger: NornRunLogger;
|
|
15
|
+
readonly logs: NornRunLogs;
|
|
16
|
+
};
|
|
17
|
+
export declare class NornExecutionContext {
|
|
18
|
+
private readonly input;
|
|
19
|
+
readonly run: NornRun;
|
|
20
|
+
readonly logs: NornLogs;
|
|
21
|
+
readonly commands: NornCommands;
|
|
22
|
+
readonly agents: NornAgents;
|
|
23
|
+
constructor(input: NornExecutionContextInput);
|
|
24
|
+
private createAgentRunner;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ../core/src/workflow-transition.ts
|
|
2
|
+
function createWorkflowTransition(input) {
|
|
3
|
+
if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
|
|
4
|
+
return { type: "next", workflowId: input.workflowId, args: input.args };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/internal/execution-context.ts
|
|
8
|
+
import { NornAgentRunner } from "./agents.js";
|
|
9
|
+
import { NornCommandRunner } from "./commands.js";
|
|
10
|
+
var NornExecutionContext = class {
|
|
11
|
+
constructor(input) {
|
|
12
|
+
this.input = input;
|
|
13
|
+
this.run = {
|
|
14
|
+
id: input.id,
|
|
15
|
+
next: (workflowId, args) => createWorkflowTransition({ workflowId, args }),
|
|
16
|
+
complete: (metadata) => ({ type: "complete", metadata }),
|
|
17
|
+
fail: (metadata) => ({ type: "fail", metadata })
|
|
18
|
+
};
|
|
19
|
+
this.logs = {
|
|
20
|
+
read: (log) => this.input.logs.read(log)
|
|
21
|
+
};
|
|
22
|
+
this.commands = {
|
|
23
|
+
run: (commandInput) => new NornCommandRunner({
|
|
24
|
+
signal: this.input.signal,
|
|
25
|
+
logs: this.input.logs,
|
|
26
|
+
logger: this.input.logger
|
|
27
|
+
}).run(commandInput)
|
|
28
|
+
};
|
|
29
|
+
this.agents = {
|
|
30
|
+
createSession: (agentInput) => this.createAgentRunner().createSession(agentInput),
|
|
31
|
+
prompt: (agentInput) => this.createAgentRunner().prompt(agentInput)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
input;
|
|
35
|
+
run;
|
|
36
|
+
logs;
|
|
37
|
+
commands;
|
|
38
|
+
agents;
|
|
39
|
+
createAgentRunner() {
|
|
40
|
+
return new NornAgentRunner({
|
|
41
|
+
id: this.input.id,
|
|
42
|
+
runRoot: this.input.runRoot,
|
|
43
|
+
signal: this.input.signal,
|
|
44
|
+
model: this.input.model,
|
|
45
|
+
thinkingLevel: this.input.thinkingLevel,
|
|
46
|
+
agentDir: this.input.agentDir,
|
|
47
|
+
logs: this.input.logs,
|
|
48
|
+
logger: this.input.logger,
|
|
49
|
+
responseCollector: this.input.responseCollector
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
NornExecutionContext
|
|
55
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class NornFileCoordinator {
|
|
2
|
+
private readonly input;
|
|
3
|
+
private readonly lockRoot;
|
|
4
|
+
constructor(input: {
|
|
5
|
+
readonly lockRoot: string;
|
|
6
|
+
readonly waitTimeoutMs: number;
|
|
7
|
+
});
|
|
8
|
+
withExclusiveLock<T>(path: string, operation: (lockedPath: string) => Promise<T>): Promise<T>;
|
|
9
|
+
readText(path: string): Promise<string>;
|
|
10
|
+
writeText(path: string, content: string): Promise<void>;
|
|
11
|
+
private withCleanup;
|
|
12
|
+
private resolveTarget;
|
|
13
|
+
private acquire;
|
|
14
|
+
private reclaimDeadOwner;
|
|
15
|
+
private removeEmptyLock;
|
|
16
|
+
}
|
|
17
|
+
export declare function createRunFileCoordinator(runRoot: string): NornFileCoordinator;
|
|
@@ -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
|
+
};
|
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;
|
|
@@ -40,6 +40,7 @@ type NornRunWorkflowOutcome = {
|
|
|
40
40
|
};
|
|
41
41
|
export type NornRunState = {
|
|
42
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;
|
|
@@ -39,7 +39,7 @@ 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";
|
|
@@ -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,
|
|
@@ -204,6 +205,7 @@ async function getRunInfo(runRoot) {
|
|
|
204
205
|
id: state.id,
|
|
205
206
|
name: state.name,
|
|
206
207
|
path: runRoot,
|
|
208
|
+
paths: { project: state.projectRoot ?? resolve(runRoot, "../../.."), workspace: state.workspace },
|
|
207
209
|
entrypointWorkflowId: state.entrypointWorkflowId,
|
|
208
210
|
currentWorkflowId: state.current?.workflowId,
|
|
209
211
|
status: resumeRequest ? "running" : state.status,
|
|
@@ -260,6 +262,7 @@ async function getLaunchedRunInfo(runRoot) {
|
|
|
260
262
|
id: launchRequest.id,
|
|
261
263
|
name: launchRequest.name,
|
|
262
264
|
path: runRoot,
|
|
265
|
+
paths: { project: resolve(runRoot, "../../.."), workspace: join(runCurrentRoot(resolve(runRoot)), "workspace") },
|
|
263
266
|
entrypointWorkflowId: launchRequest.workflowId,
|
|
264
267
|
currentWorkflowId: launchRequest.workflowId,
|
|
265
268
|
status: "running",
|
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type
|
|
1
|
+
import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunNext, type NornWorkflowPaths, type NornWorkflowContext, type NornWorkflowSource, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
|
|
2
2
|
import type { TSchema } from "typebox";
|
|
3
3
|
export type NornRegisteredConfiguration = {
|
|
4
4
|
readonly key: string;
|
|
@@ -27,6 +27,15 @@ export type NornWorkflowStepResult = NornRunNext | {
|
|
|
27
27
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
28
28
|
readonly metadata: NornRunFail["metadata"];
|
|
29
29
|
};
|
|
30
|
+
type NornExecutionContextInput = {
|
|
31
|
+
readonly execution: Pick<NornWorkflowContext, "run" | "agents" | "commands" | "logs">;
|
|
32
|
+
readonly paths: NornWorkflowPaths;
|
|
33
|
+
readonly args: unknown;
|
|
34
|
+
readonly configOverride: unknown;
|
|
35
|
+
};
|
|
36
|
+
type NornWorkflowExecutionInput = NornExecutionContextInput & {
|
|
37
|
+
readonly workflow: NornAnyWorkflowDeclaration;
|
|
38
|
+
};
|
|
30
39
|
export declare class NornWorkflowRegistry {
|
|
31
40
|
private readonly entries;
|
|
32
41
|
private readonly scopes;
|
|
@@ -42,10 +51,11 @@ export declare class NornWorkflowRegistry {
|
|
|
42
51
|
configurationInfos(): NornProjectConfigurationInfo[];
|
|
43
52
|
launchableEntries(): NornRegisteredWorkflow[];
|
|
44
53
|
workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined;
|
|
45
|
-
describeGate
|
|
46
|
-
execute
|
|
54
|
+
describeGate(input: NornWorkflowExecutionInput): Promise<string>;
|
|
55
|
+
execute(input: NornWorkflowExecutionInput): Promise<NornWorkflowStepResult>;
|
|
47
56
|
private createExecutionContext;
|
|
48
57
|
private requireEntry;
|
|
49
58
|
private sortedEntries;
|
|
50
59
|
}
|
|
51
60
|
export declare function assertWorkflowDefinition(workflow: NornAnyWorkflowDeclaration): void;
|
|
61
|
+
export {};
|
|
@@ -81,30 +81,36 @@ var NornWorkflowRegistry = class {
|
|
|
81
81
|
workflowById(workflowId) {
|
|
82
82
|
return this.entries.get(workflowId)?.workflow;
|
|
83
83
|
}
|
|
84
|
-
async describeGate(
|
|
84
|
+
async describeGate(input) {
|
|
85
|
+
const { workflow } = input;
|
|
85
86
|
const entry = this.requireEntry(workflow.id);
|
|
86
87
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
87
|
-
const context = this.createExecutionContext({
|
|
88
|
+
const context = this.createExecutionContext({ ...input, entry });
|
|
88
89
|
const description = await entry.workflow.gate?.describe?.(context) ?? workflow.id;
|
|
89
90
|
if (typeof description !== "string" || description.trim().length === 0) throw new Error(`Workflow gate description must not be empty: ${workflow.id}`);
|
|
90
91
|
return description.trim();
|
|
91
92
|
}
|
|
92
|
-
async execute(
|
|
93
|
+
async execute(input) {
|
|
94
|
+
const { workflow } = input;
|
|
93
95
|
const entry = this.requireEntry(workflow.id);
|
|
94
|
-
const result = await entry.workflow.execute(this.createExecutionContext({
|
|
96
|
+
const result = await entry.workflow.execute(this.createExecutionContext({ ...input, entry }));
|
|
95
97
|
if (isWorkflowNext(result)) return result;
|
|
96
98
|
if (isWorkflowComplete(result)) return { type: "complete", workflow, metadata: result.metadata };
|
|
97
99
|
if (isWorkflowFail(result)) return { type: "fail", workflow, metadata: result.metadata };
|
|
98
100
|
throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
|
|
99
101
|
}
|
|
100
102
|
createExecutionContext(input) {
|
|
101
|
-
const { entry,
|
|
103
|
+
const { entry, execution, paths, configOverride } = input;
|
|
102
104
|
const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : void 0;
|
|
103
105
|
return {
|
|
104
106
|
args: Value.Decode(entry.workflow.args, input.args),
|
|
105
107
|
config: parseExecutionConfig(entry.configuration, configOverride),
|
|
106
108
|
...scope ? { scope: { id: scope.definition.id, config: parseExecutionConfig(scope.configuration, configOverride) } } : {},
|
|
107
|
-
|
|
109
|
+
paths,
|
|
110
|
+
run: execution.run,
|
|
111
|
+
agents: execution.agents,
|
|
112
|
+
commands: execution.commands,
|
|
113
|
+
logs: execution.logs
|
|
108
114
|
};
|
|
109
115
|
}
|
|
110
116
|
requireEntry(id) {
|
|
@@ -152,7 +158,6 @@ function workflowInfo(entry) {
|
|
|
152
158
|
id: entry.workflow.id,
|
|
153
159
|
instructions: entry.workflow.instructions,
|
|
154
160
|
isEntrypoint: entry.workflow.isEntrypoint,
|
|
155
|
-
isolation: entry.workflow.isolation,
|
|
156
161
|
source: entry.source,
|
|
157
162
|
configKey: entry.workflow.id,
|
|
158
163
|
scope: entry.scopeId ? { id: entry.scopeId } : void 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function requireAbsoluteWorkingDirectory(cwd: string): string;
|