@vimhead.dev/norn-cli 0.1.0-tip.35390859630.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 +8 -4
- package/assets/docs/README.md +4 -4
- package/assets/docs/agents.md +37 -9
- package/assets/docs/cli.md +3 -3
- package/assets/docs/composition.md +12 -12
- package/assets/docs/persistence.md +31 -17
- package/assets/docs/providers.md +2 -2
- package/assets/docs/workflows.md +15 -10
- package/assets/examples/agent-then-analysis/README.md +8 -8
- package/assets/examples/agent-then-analysis/plugin.ts +18 -16
- package/assets/examples/caller-selected-continuation/README.md +11 -10
- package/assets/examples/caller-selected-continuation/caller.ts +12 -10
- package/assets/examples/caller-selected-continuation/producer.ts +9 -6
- package/assets/examples/coordinating-multiple-agents/README.md +26 -16
- package/assets/examples/coordinating-multiple-agents/plugin.ts +58 -35
- 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/plugin.ts +7 -3
- package/assets/examples/minimal-workflow/README.md +5 -4
- package/assets/examples/minimal-workflow/plugin.ts +7 -4
- package/assets/examples/shared-state/README.md +5 -5
- package/assets/examples/shared-state/plugin.ts +35 -22
- 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/workflows/development-loop/execute.ts +2 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +8 -7
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +8 -4
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +10 -7
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +1 -2
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +15 -14
- 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 +30 -44
- 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/run.ts +2 -57
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +20 -8
- 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 +27 -59
- package/assets/packages/sdk/src/index.ts +0 -3
- package/assets/tests/workflow-ref.test.ts +6 -7
- 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.js +26 -40
- 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/run.d.ts +2 -20
- package/dist/internal/run.js +1 -45
- 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 +8 -6
- 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/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/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
|
@@ -12,6 +12,7 @@ import { Value } from "typebox/value";
|
|
|
12
12
|
import { NORN_BUILD_INFO, type NornBuildInfo, type NornGithubReleaseBinaryBuildInfo } from "./build-info.ts";
|
|
13
13
|
import { renderNornDocumentationIntro, resolveDocumentationCacheRoot, resolveNornDocumentation, type NornDocumentationSource } from "./documentation.ts";
|
|
14
14
|
import { NornEngine } from "./internal/engine.ts";
|
|
15
|
+
import { resolveNornAgentDirectory } from "./internal/agent-directory.ts";
|
|
15
16
|
import { errorMessage, NornProjectLoadError, NornRunStoppedError } from "./internal/errors.ts";
|
|
16
17
|
import { clearRunResumeRequest, readRunLaunchRequest, readRunResumeRequest, writeRunLaunchRequest, writeRunResumeRequest, type NornRunResumeRequest } from "./internal/launch-request.ts";
|
|
17
18
|
import { readRunMetrics } from "./internal/metrics.ts";
|
|
@@ -19,6 +20,7 @@ import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.ts";
|
|
|
19
20
|
import { generateRunName } from "./internal/run-names.ts";
|
|
20
21
|
import { assertRunVersion, getRunInfo, listRuns, mergeInterruptedWorkflowArgs, resolveRunRoot } from "./internal/run-state.ts";
|
|
21
22
|
import { NornRunStore } from "./internal/run-store.ts";
|
|
23
|
+
import { prepareRunWorkerDirectory } from "./internal/worker-directory.ts";
|
|
22
24
|
import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./workflow-loader.ts";
|
|
23
25
|
|
|
24
26
|
const RUNS_ROOT = join(".norn", "runs");
|
|
@@ -784,7 +786,7 @@ async function startRun(workflowId: string, commandArgs: readonly string[]): Pro
|
|
|
784
786
|
const createdAt = new Date().toISOString();
|
|
785
787
|
await writeRunLaunchRequest(runRoot, { version: 2, type: "run", id, name, workflowId, args, configOverride, createdAt });
|
|
786
788
|
await startDetachedExecuteRun(id, project.projectRoot);
|
|
787
|
-
writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, createdAt }) });
|
|
789
|
+
writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, projectRoot: project.projectRoot, createdAt }) });
|
|
788
790
|
}
|
|
789
791
|
|
|
790
792
|
async function resumeRun(run: string, args: readonly string[]): Promise<void> {
|
|
@@ -899,8 +901,10 @@ async function executeRun(runId: string): Promise<void> {
|
|
|
899
901
|
const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
|
|
900
902
|
const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
|
|
901
903
|
try {
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
+
const agentDir = resolveNornAgentDirectory({ home: homedir(), environment: process.env });
|
|
905
|
+
process.chdir(await prepareRunWorkerDirectory(runRoot));
|
|
906
|
+
const project = await loadNornProject(location.projectRoot);
|
|
907
|
+
const engine = new NornEngine({ cwd: project.projectRoot, agentDir, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
|
|
904
908
|
engine.registerWorkflows(project.definitions);
|
|
905
909
|
if (request.type === "run") {
|
|
906
910
|
const workflow = project.registry.workflowById(request.workflowId);
|
|
@@ -1000,6 +1004,7 @@ function detachedExecuteRunArgs(runId: string): readonly string[] {
|
|
|
1000
1004
|
}
|
|
1001
1005
|
|
|
1002
1006
|
function startedRunInfo(input: {
|
|
1007
|
+
readonly projectRoot: string;
|
|
1003
1008
|
readonly id: string;
|
|
1004
1009
|
readonly name: string;
|
|
1005
1010
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
@@ -1011,6 +1016,7 @@ function startedRunInfo(input: {
|
|
|
1011
1016
|
id: input.id,
|
|
1012
1017
|
name: input.name,
|
|
1013
1018
|
path: input.runRoot,
|
|
1019
|
+
paths: { project: input.projectRoot, workspace: join(input.runRoot, "current", "workspace") },
|
|
1014
1020
|
entrypointWorkflowId: input.workflow.id,
|
|
1015
1021
|
currentWorkflowId: input.workflow.id,
|
|
1016
1022
|
status: "running",
|
|
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
|
|
|
2
2
|
|
|
3
3
|
export const NORN_GENERATED_BUILD_INFO = {
|
|
4
4
|
"kind": "npm-registry",
|
|
5
|
-
"version": "0.1.0-tip.
|
|
6
|
-
"commit": "
|
|
5
|
+
"version": "0.1.0-tip.35436871363.1",
|
|
6
|
+
"commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
|
|
7
7
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
8
8
|
"upgrade": {
|
|
9
9
|
"supported": false,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createEventBus,
|
|
6
6
|
type AgentSession,
|
|
7
7
|
type CreateAgentSessionOptions,
|
|
8
|
+
type ToolDefinition,
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import type { NornAgentCreateSessionInput, NornAgentPromptInput, NornAgentRunRawAttempt, NornAgentRunResult, NornAgentSession, NornAgentSessionEvents, NornAgentSinglePromptInput, NornAgentUsage } from "@vimhead.dev/norn";
|
|
10
11
|
import { AGENT_RESPONSE_TOOL_NAME } from "@vimhead.dev/norn-core/agent-protocol";
|
|
@@ -12,7 +13,8 @@ import { inspectSchema } from "@vimhead.dev/norn/schema";
|
|
|
12
13
|
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { mkdir } from "node:fs/promises";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
|
-
import {
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
|
|
16
18
|
import { type StaticDecode, type TSchema } from "typebox";
|
|
17
19
|
import { resolveNornAgentDirectory } from "./agent-directory.ts";
|
|
18
20
|
import {
|
|
@@ -23,19 +25,14 @@ import {
|
|
|
23
25
|
import { errorMessage } from "./errors.ts";
|
|
24
26
|
import { safeFileName } from "./file-names.ts";
|
|
25
27
|
import type { NornRunLogs } from "./logs.ts";
|
|
26
|
-
import { NornSessionResourceBindings } from "./resource-bindings.ts";
|
|
27
28
|
import type { NornRunLogger } from "./run-log.ts";
|
|
28
29
|
import { agentUsageFromValue, emptyAgentUsage, totalAgentUsage } from "./usage.ts";
|
|
29
30
|
|
|
30
31
|
const DEFAULT_AGENT_ATTEMPTS = 3;
|
|
31
|
-
const DEFAULT_AGENT_TOOL_ALLOWLIST = ["read", "bash", "edit", "write", AGENT_RESPONSE_TOOL_NAME] as const;
|
|
32
32
|
|
|
33
33
|
type NornAgentRunnerInput = {
|
|
34
34
|
readonly id: string;
|
|
35
35
|
readonly runRoot: string;
|
|
36
|
-
readonly boundaryRoot: string;
|
|
37
|
-
readonly boundaryName: string;
|
|
38
|
-
readonly cwd: string;
|
|
39
36
|
readonly signal?: AbortSignal;
|
|
40
37
|
readonly model?: CreateAgentSessionOptions["model"];
|
|
41
38
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
@@ -49,7 +46,6 @@ type CreatedNornAgentSessionInput = NornAgentRunnerInput & {
|
|
|
49
46
|
readonly label: string;
|
|
50
47
|
readonly cwd: string;
|
|
51
48
|
readonly session: AgentSession;
|
|
52
|
-
readonly resourceBindings: NornSessionResourceBindings;
|
|
53
49
|
readonly events: NornAgentSessionEvents;
|
|
54
50
|
};
|
|
55
51
|
|
|
@@ -61,7 +57,7 @@ export class NornAgentRunner {
|
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
async createSession(agentInput: NornAgentCreateSessionInput): Promise<NornAgentSession> {
|
|
64
|
-
const cwd =
|
|
60
|
+
const cwd = requireAbsoluteWorkingDirectory(agentInput.cwd);
|
|
65
61
|
const sessionDir = resolve(this.input.runRoot, "sessions");
|
|
66
62
|
await mkdir(sessionDir, { recursive: true });
|
|
67
63
|
|
|
@@ -82,19 +78,19 @@ export class NornAgentRunner {
|
|
|
82
78
|
...loader.getExtensions().errors.map(error => `Failed to load extension ${error.path}: ${error.error}`),
|
|
83
79
|
];
|
|
84
80
|
if (errors.length > 0) throw new Error(errors.join("\n"));
|
|
85
|
-
const
|
|
81
|
+
const customTools = agentInput.customTools ?? [];
|
|
82
|
+
assertCustomToolNames({
|
|
83
|
+
tools: customTools,
|
|
84
|
+
reservedNames: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
|
|
85
|
+
...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
|
|
86
|
+
});
|
|
86
87
|
let session: AgentSession | undefined;
|
|
87
88
|
try {
|
|
88
|
-
await resourceBindings.bind({
|
|
89
|
-
adapters: agentInput.resourceAdapters ?? [], runId: this.input.id, label: agentInput.label,
|
|
90
|
-
reservedTools: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
|
|
91
|
-
...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
|
|
92
|
-
});
|
|
93
89
|
({ session } = await createAgentSessionFromServices({
|
|
94
90
|
services,
|
|
95
91
|
sessionManager: SessionManager.create(cwd, sessionDir),
|
|
96
|
-
tools:
|
|
97
|
-
customTools: [this.responseToolFactory.create(), ...
|
|
92
|
+
tools: withAgentResponseTool(agentInput.tools),
|
|
93
|
+
customTools: [this.responseToolFactory.create(), ...customTools],
|
|
98
94
|
model: agentInput.model ?? this.input.model,
|
|
99
95
|
thinkingLevel: agentInput.thinkingLevel ?? this.input.thinkingLevel,
|
|
100
96
|
}));
|
|
@@ -102,13 +98,12 @@ export class NornAgentRunner {
|
|
|
102
98
|
await session.bindExtensions({});
|
|
103
99
|
await this.input.logger.record({ type: "agent.spawned", label: agentInput.label, cwd });
|
|
104
100
|
return new CreatedNornAgentSession({
|
|
105
|
-
...this.input, label: agentInput.label, cwd, session,
|
|
101
|
+
...this.input, label: agentInput.label, cwd, session, events: eventBus,
|
|
106
102
|
});
|
|
107
103
|
} catch (error) {
|
|
108
104
|
const errors = [error];
|
|
109
105
|
try { session?.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
|
|
110
|
-
|
|
111
|
-
if (errors.length > 1) throw new AggregateError(errors, "Agent creation and resource cleanup failed");
|
|
106
|
+
if (errors.length > 1) throw new AggregateError(errors, "Agent creation and cleanup failed");
|
|
112
107
|
throw error;
|
|
113
108
|
}
|
|
114
109
|
}
|
|
@@ -121,15 +116,6 @@ export class NornAgentRunner {
|
|
|
121
116
|
await agent.dispose();
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
|
-
|
|
125
|
-
private resolveFromCwd(path: string): string {
|
|
126
|
-
const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
|
|
127
|
-
const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
|
|
128
|
-
if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
|
|
129
|
-
throw new Error(`Agent cwd escapes ${this.input.boundaryName} isolation: ${path}`);
|
|
130
|
-
}
|
|
131
|
-
return resolvedPath;
|
|
132
|
-
}
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
function systemPromptOverride(agentInput: NornAgentCreateSessionInput): (() => string) | undefined {
|
|
@@ -229,7 +215,6 @@ class CreatedNornAgentSession implements NornAgentSession {
|
|
|
229
215
|
const errors: unknown[] = [];
|
|
230
216
|
try { await this.input.session.abort(); } catch (error) { errors.push(error); }
|
|
231
217
|
try { this.input.session.dispose(); } catch (error) { errors.push(error); }
|
|
232
|
-
try { await this.input.resourceBindings.dispose(); } catch (error) { errors.push(error); }
|
|
233
218
|
if (errors.length) throw new AggregateError(errors, "Agent disposal failed");
|
|
234
219
|
await this.input.logger.record({ type: "agent.disposed", label: this.label });
|
|
235
220
|
}
|
|
@@ -360,7 +345,14 @@ function withResponseToolFinalizationInstruction(
|
|
|
360
345
|
].join("\n");
|
|
361
346
|
}
|
|
362
347
|
|
|
363
|
-
function withAgentResponseTool(tools: readonly string[] | undefined): string[] {
|
|
364
|
-
|
|
365
|
-
|
|
348
|
+
function withAgentResponseTool(tools: readonly string[] | undefined): string[] | undefined {
|
|
349
|
+
return tools === undefined ? undefined : Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function assertCustomToolNames(input: { readonly tools: readonly ToolDefinition[]; readonly reservedNames: readonly string[] }): void {
|
|
353
|
+
const names = new Set(input.reservedNames);
|
|
354
|
+
for (const tool of input.tools) {
|
|
355
|
+
if (names.has(tool.name)) throw new Error(`Custom tool name collision: ${tool.name}`);
|
|
356
|
+
names.add(tool.name);
|
|
357
|
+
}
|
|
366
358
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
|
|
4
4
|
import type { WriteStream } from "node:fs";
|
|
5
5
|
import type { NornCommandRunInput, NornCommandRunResult, NornLogRef } from "@vimhead.dev/norn";
|
|
6
6
|
import { errorMessage } from "./errors.ts";
|
|
@@ -9,9 +9,6 @@ import type { NornRunLogs } from "./logs.ts";
|
|
|
9
9
|
import type { NornRunLogger } from "./run-log.ts";
|
|
10
10
|
|
|
11
11
|
type NornCommandRunnerInput = {
|
|
12
|
-
readonly boundaryRoot: string;
|
|
13
|
-
readonly boundaryName: string;
|
|
14
|
-
readonly cwd: string;
|
|
15
12
|
readonly signal?: AbortSignal;
|
|
16
13
|
readonly logs: NornRunLogs;
|
|
17
14
|
readonly logger: NornRunLogger;
|
|
@@ -21,7 +18,7 @@ export class NornCommandRunner {
|
|
|
21
18
|
constructor(private readonly input: NornCommandRunnerInput) {}
|
|
22
19
|
|
|
23
20
|
async run(commandInput: NornCommandRunInput): Promise<NornCommandRunResult> {
|
|
24
|
-
const cwd =
|
|
21
|
+
const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
|
|
25
22
|
const startedAtMs = Date.now();
|
|
26
23
|
const invocationId = randomUUID();
|
|
27
24
|
const stdoutRef = commandLog({ label: commandInput.label, invocationId, stream: "stdout" });
|
|
@@ -67,15 +64,6 @@ export class NornCommandRunner {
|
|
|
67
64
|
});
|
|
68
65
|
return commandResult;
|
|
69
66
|
}
|
|
70
|
-
|
|
71
|
-
private resolveFromCwd(path: string): string {
|
|
72
|
-
const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
|
|
73
|
-
const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
|
|
74
|
-
if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
|
|
75
|
-
throw new Error(`Command cwd escapes ${this.input.boundaryName} isolation: ${path}`);
|
|
76
|
-
}
|
|
77
|
-
return resolvedPath;
|
|
78
|
-
}
|
|
79
67
|
}
|
|
80
68
|
|
|
81
69
|
type SpawnCommandInput = {
|
|
@@ -7,20 +7,19 @@ import {
|
|
|
7
7
|
type NornRunNext,
|
|
8
8
|
type NornRunResult,
|
|
9
9
|
type NornRunStartOptions,
|
|
10
|
+
type NornWorkflowPaths,
|
|
10
11
|
} from "@vimhead.dev/norn";
|
|
11
|
-
import { createRunFileCoordinator } from "
|
|
12
|
+
import { createRunFileCoordinator } from "./file-coordinator.ts";
|
|
12
13
|
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { mkdir } from "node:fs/promises";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
16
|
import { Value } from "typebox/value";
|
|
16
17
|
import { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
17
|
-
import { NornArtifacts } from "./artifacts.ts";
|
|
18
18
|
import { errorMessage, NornRunStoppedError } from "./errors.ts";
|
|
19
19
|
import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS, type NornRunResumeRequest } from "./launch-request.ts";
|
|
20
20
|
import { NornRunLogs } from "./logs.ts";
|
|
21
21
|
import { NornRunLease } from "./run-lease.ts";
|
|
22
22
|
import { NornRunLogger } from "./run-log.ts";
|
|
23
|
-
import { NornRunResources } from "../resources.ts";
|
|
24
23
|
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
|
|
25
24
|
import { NornRunStore, runCurrentRoot } from "./run-store.ts";
|
|
26
25
|
import { NornRunContext } from "./run.ts";
|
|
@@ -40,7 +39,9 @@ export type NornEngineInput = {
|
|
|
40
39
|
|
|
41
40
|
type RunSession = {
|
|
42
41
|
readonly runRoot: string;
|
|
42
|
+
readonly cwd: string;
|
|
43
43
|
readonly run: NornRunContext;
|
|
44
|
+
readonly paths: NornWorkflowPaths;
|
|
44
45
|
readonly state: NornRunStateStore;
|
|
45
46
|
readonly lease: NornRunLease;
|
|
46
47
|
readonly runStore: NornRunStore;
|
|
@@ -157,14 +158,14 @@ export class NornEngine {
|
|
|
157
158
|
const rawArgs = mergeInterruptedWorkflowArgs(current.args, args, current.interruption?.fields);
|
|
158
159
|
Value.Decode(workflow.args, rawArgs);
|
|
159
160
|
await session.state.replaceCurrentArgs(rawArgs);
|
|
160
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
161
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
161
162
|
const resumedCurrent = session.state.currentState().current;
|
|
162
163
|
if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
163
164
|
isLeaseOwnedByScheduler = true;
|
|
164
165
|
return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
|
|
165
166
|
}
|
|
166
167
|
if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
167
|
-
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.
|
|
168
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.cwd });
|
|
168
169
|
isLeaseOwnedByScheduler = true;
|
|
169
170
|
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
170
171
|
} finally {
|
|
@@ -183,28 +184,27 @@ export class NornEngine {
|
|
|
183
184
|
try {
|
|
184
185
|
for (let step = 1; step <= 1_000; step++) {
|
|
185
186
|
throwIfRunAborted(session.activeRun);
|
|
186
|
-
const stepRuntime = session.run.forWorkflow(currentStep.workflow);
|
|
187
187
|
if (shouldPauseForGate(currentStep)) {
|
|
188
|
-
const description = await this.registry.describeGate(currentStep.workflow,
|
|
188
|
+
const description = await this.registry.describeGate({ workflow: currentStep.workflow, run: session.run, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
|
|
189
189
|
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
190
190
|
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
191
191
|
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
192
192
|
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
193
|
-
return interruptedLaunchResult({ id:
|
|
193
|
+
return interruptedLaunchResult({ id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
194
194
|
}
|
|
195
195
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
196
|
-
const stepResult = await this.executeWorkflowStep(session, currentStep
|
|
196
|
+
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
197
197
|
throwIfRunAborted(session.activeRun);
|
|
198
198
|
if (stepResult.type === "complete") {
|
|
199
199
|
await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
|
|
200
200
|
await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
201
201
|
await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
|
|
202
|
-
return { status: "completed", id:
|
|
202
|
+
return { status: "completed", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
203
203
|
}
|
|
204
204
|
if (stepResult.type === "fail") {
|
|
205
205
|
await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
|
|
206
206
|
await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
207
|
-
return { status: "failed", id:
|
|
207
|
+
return { status: "failed", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
208
208
|
}
|
|
209
209
|
const nextStep = this.nextWorkflowStep(stepResult);
|
|
210
210
|
await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
|
|
@@ -217,7 +217,7 @@ export class NornEngine {
|
|
|
217
217
|
if (isRunStopped(session, error)) {
|
|
218
218
|
await session.state.stopCurrent();
|
|
219
219
|
await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
|
|
220
|
-
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.
|
|
220
|
+
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
|
|
221
221
|
}
|
|
222
222
|
await session.state.failCurrent(errorMessage(error));
|
|
223
223
|
await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
|
|
@@ -228,13 +228,13 @@ export class NornEngine {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
private async executeWorkflowStep(session: RunSession, step: WorkflowStep
|
|
231
|
+
private async executeWorkflowStep(session: RunSession, step: WorkflowStep): Promise<NornWorkflowStepResult> {
|
|
232
232
|
const startedAtMs = Date.now();
|
|
233
233
|
try {
|
|
234
|
-
await assertWorkspaceBoundary(session,
|
|
234
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
235
235
|
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
236
|
-
const result = await this.registry.execute(step.workflow, run, step.args, session.state.currentState().configOverride);
|
|
237
|
-
await assertWorkspaceBoundary(session,
|
|
236
|
+
const result = await this.registry.execute({ workflow: step.workflow, run: session.run, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
|
|
237
|
+
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
238
238
|
const durationMs = Date.now() - startedAtMs;
|
|
239
239
|
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
240
240
|
else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
@@ -271,7 +271,8 @@ export class NornEngine {
|
|
|
271
271
|
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
272
272
|
const currentRoot = runCurrentRoot(runRoot);
|
|
273
273
|
const workspace = join(currentRoot, "workspace");
|
|
274
|
-
const
|
|
274
|
+
const paths: NornWorkflowPaths = { project: resolve(this.input.cwd), workspace: workspace };
|
|
275
|
+
const cwd = process.cwd();
|
|
275
276
|
const startedAt = new Date().toISOString();
|
|
276
277
|
await mkdir(runRoot, { recursive: true });
|
|
277
278
|
const lease = await NornRunLease.acquire(runRoot);
|
|
@@ -288,8 +289,9 @@ export class NornEngine {
|
|
|
288
289
|
initialCwd: cwd,
|
|
289
290
|
startedAt,
|
|
290
291
|
} });
|
|
291
|
-
const run = await this.buildRun({ id, currentRoot,
|
|
292
|
+
const run = await this.buildRun({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
292
293
|
const state = await NornRunStateStore.create(runRoot, {
|
|
294
|
+
projectRoot: paths.project,
|
|
293
295
|
id,
|
|
294
296
|
name,
|
|
295
297
|
entrypointWorkflowId: workflow.id,
|
|
@@ -301,7 +303,7 @@ export class NornEngine {
|
|
|
301
303
|
},
|
|
302
304
|
startedAt,
|
|
303
305
|
});
|
|
304
|
-
const session = { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
306
|
+
const session = { runRoot, cwd, run, paths, state, lease, runStore, logger, activeRun };
|
|
305
307
|
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
306
308
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
307
309
|
return session;
|
|
@@ -321,11 +323,9 @@ export class NornEngine {
|
|
|
321
323
|
const runStore = await NornRunStore.open(runRoot);
|
|
322
324
|
const currentRoot = runCurrentRoot(runRoot);
|
|
323
325
|
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
|
|
328
|
-
return { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
326
|
+
const paths: NornWorkflowPaths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
|
|
327
|
+
const run = await this.buildRun({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
328
|
+
return { runRoot, cwd: process.cwd(), run, paths, state, lease, runStore, logger, activeRun };
|
|
329
329
|
} catch (error) {
|
|
330
330
|
this.failActiveRun(runRoot, activeRun);
|
|
331
331
|
throw error;
|
|
@@ -335,32 +335,22 @@ export class NornEngine {
|
|
|
335
335
|
private async buildRun(input: {
|
|
336
336
|
readonly id: string;
|
|
337
337
|
readonly currentRoot: string;
|
|
338
|
-
readonly
|
|
339
|
-
readonly cwd: string;
|
|
340
|
-
readonly isolationMode: "runWorkspace" | "project";
|
|
338
|
+
readonly paths: NornWorkflowPaths;
|
|
341
339
|
readonly signal: AbortSignal;
|
|
342
340
|
readonly logger: NornRunLogger;
|
|
343
341
|
}): Promise<NornRunContext> {
|
|
344
|
-
const artifactsRoot = join(input.currentRoot, "artifacts");
|
|
345
342
|
const logsRoot = join(input.currentRoot, "logs");
|
|
346
|
-
await mkdir(input.workspace, { recursive: true });
|
|
347
|
-
await mkdir(artifactsRoot, { recursive: true });
|
|
343
|
+
await mkdir(input.paths.workspace, { recursive: true });
|
|
348
344
|
await mkdir(logsRoot, { recursive: true });
|
|
349
|
-
const
|
|
345
|
+
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
350
346
|
return new NornRunContext({
|
|
351
347
|
id: input.id,
|
|
352
348
|
runRoot: input.currentRoot,
|
|
353
|
-
workspace: input.workspace,
|
|
354
|
-
projectRoot: this.input.cwd,
|
|
355
|
-
cwd: input.cwd,
|
|
356
|
-
isolationMode: input.isolationMode,
|
|
357
349
|
signal: input.signal,
|
|
358
350
|
agentDir: this.input.agentDir,
|
|
359
351
|
responseCollector: this.responseCollector,
|
|
360
|
-
resources,
|
|
361
352
|
logger: input.logger,
|
|
362
|
-
|
|
363
|
-
logs: new NornRunLogs(logsRoot, resources.files),
|
|
353
|
+
logs: new NornRunLogs(logsRoot, files),
|
|
364
354
|
});
|
|
365
355
|
}
|
|
366
356
|
|
|
@@ -400,11 +390,7 @@ export class NornEngine {
|
|
|
400
390
|
}
|
|
401
391
|
|
|
402
392
|
function defaultRunRoot(cwd: string, id: string): string {
|
|
403
|
-
return join(cwd, RUNS_DIR_NAME, "runs", id);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function workflowDefaultCwd(projectRoot: string, workspace: string, workflow: NornAnyWorkflowDeclaration | undefined): string {
|
|
407
|
-
return workflow?.isolation.mode === "project" ? projectRoot : workspace;
|
|
393
|
+
return join(resolve(cwd), RUNS_DIR_NAME, "runs", id);
|
|
408
394
|
}
|
|
409
395
|
|
|
410
396
|
function toWorkflowStep(workflow: NornAnyWorkflowDeclaration, step: NonNullable<NornRunState["current"]>): WorkflowStep {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWriteStream, type WriteStream } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import type { NornFileCoordinator } from "
|
|
3
|
+
import type { NornFileCoordinator } from "./file-coordinator.ts";
|
|
4
4
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import type { NornLogRef } from "@vimhead.dev/norn";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import type { NornFileCoordinator } from "
|
|
2
|
+
import type { NornFileCoordinator } from "./file-coordinator.ts";
|
|
3
3
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
4
|
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
5
5
|
|
|
@@ -5,7 +5,7 @@ import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
|
5
5
|
import { readRunLaunchRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.ts";
|
|
6
6
|
import { getRunLeaseHealth } from "./run-lease.ts";
|
|
7
7
|
import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
|
|
8
|
-
import { createRunFileCoordinator, type NornFileCoordinator } from "
|
|
8
|
+
import { createRunFileCoordinator, type NornFileCoordinator } from "./file-coordinator.ts";
|
|
9
9
|
import { runCurrentRoot } from "./run-store.ts";
|
|
10
10
|
import { jsonValueSchema } from "@vimhead.dev/norn/schema";
|
|
11
11
|
import { Value } from "typebox/value";
|
|
@@ -59,6 +59,7 @@ type NornRunWorkflowOutcome = {
|
|
|
59
59
|
|
|
60
60
|
export type NornRunState = {
|
|
61
61
|
readonly version: 1 | typeof RUN_STATE_VERSION;
|
|
62
|
+
readonly projectRoot?: string;
|
|
62
63
|
readonly id: string;
|
|
63
64
|
readonly name: string;
|
|
64
65
|
readonly entrypointWorkflowId: string;
|
|
@@ -74,6 +75,7 @@ export type NornRunState = {
|
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
type CreateNornRunStateInput = {
|
|
78
|
+
readonly projectRoot: string;
|
|
77
79
|
readonly id: string;
|
|
78
80
|
readonly name: string;
|
|
79
81
|
readonly entrypointWorkflowId: string;
|
|
@@ -101,7 +103,8 @@ export class NornRunStateStore {
|
|
|
101
103
|
id: input.id,
|
|
102
104
|
name: input.name,
|
|
103
105
|
entrypointWorkflowId: input.entrypointWorkflowId,
|
|
104
|
-
|
|
106
|
+
projectRoot: resolve(input.projectRoot),
|
|
107
|
+
workspace: resolve(input.workspace),
|
|
105
108
|
configOverride: input.configOverride,
|
|
106
109
|
status: "running",
|
|
107
110
|
current: input.current,
|
|
@@ -252,6 +255,7 @@ export async function getRunInfo(runRoot: string): Promise<NornRunInfo> {
|
|
|
252
255
|
id: state.id,
|
|
253
256
|
name: state.name,
|
|
254
257
|
path: runRoot,
|
|
258
|
+
paths: { project: state.projectRoot ?? resolve(runRoot, "../../.."), workspace: state.workspace },
|
|
255
259
|
entrypointWorkflowId: state.entrypointWorkflowId,
|
|
256
260
|
currentWorkflowId: state.current?.workflowId,
|
|
257
261
|
status: resumeRequest ? "running" : state.status,
|
|
@@ -317,6 +321,7 @@ async function getLaunchedRunInfo(runRoot: string): Promise<NornRunInfo> {
|
|
|
317
321
|
id: launchRequest.id,
|
|
318
322
|
name: launchRequest.name,
|
|
319
323
|
path: runRoot,
|
|
324
|
+
paths: { project: resolve(runRoot, "../../.."), workspace: join(runCurrentRoot(resolve(runRoot)), "workspace") },
|
|
320
325
|
entrypointWorkflowId: launchRequest.workflowId,
|
|
321
326
|
currentWorkflowId: launchRequest.workflowId,
|
|
322
327
|
status: "running",
|
|
@@ -1,60 +1,38 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
1
|
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type {
|
|
2
|
+
import type { NornRunOutcomeMetadata, NornRun } from "@vimhead.dev/norn";
|
|
4
3
|
import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
|
|
5
4
|
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
6
|
-
import type { NornArtifacts } from "./artifacts.ts";
|
|
7
5
|
import { NornAgentRunner } from "./agents.ts";
|
|
8
6
|
import { NornCommandRunner } from "./commands.ts";
|
|
9
7
|
import type { NornRunLogs } from "./logs.ts";
|
|
10
8
|
import type { NornRunLogger } from "./run-log.ts";
|
|
11
|
-
import type { NornRunResources } from "../resources.ts";
|
|
12
9
|
|
|
13
10
|
type DefaultNornRunInput = {
|
|
14
11
|
readonly id: string;
|
|
15
12
|
readonly runRoot: string;
|
|
16
|
-
readonly projectRoot: string;
|
|
17
|
-
readonly workspace: string;
|
|
18
|
-
readonly cwd: string;
|
|
19
|
-
readonly isolationMode: NornWorkflowIsolationMode;
|
|
20
13
|
readonly signal?: AbortSignal;
|
|
21
14
|
readonly model?: CreateAgentSessionOptions["model"];
|
|
22
15
|
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
23
16
|
readonly agentDir?: string;
|
|
24
17
|
readonly responseCollector: NornAgentResponseCollector;
|
|
25
|
-
readonly resources: NornRunResources;
|
|
26
18
|
readonly logger: NornRunLogger;
|
|
27
|
-
readonly artifacts: NornArtifacts;
|
|
28
19
|
readonly logs: NornRunLogs;
|
|
29
20
|
};
|
|
30
21
|
|
|
31
|
-
export class NornRunContext implements
|
|
32
|
-
readonly resources: NornRunResources;
|
|
33
|
-
readonly artifacts: NornRun["artifacts"];
|
|
22
|
+
export class NornRunContext implements NornRun {
|
|
34
23
|
readonly logs: NornRun["logs"];
|
|
35
24
|
readonly commands: NornRun["commands"];
|
|
36
25
|
readonly agents: NornRun["agents"];
|
|
37
26
|
readonly id: string;
|
|
38
|
-
readonly workspace: string;
|
|
39
|
-
readonly projectRoot: string;
|
|
40
|
-
readonly cwd: string;
|
|
41
27
|
|
|
42
28
|
constructor(private readonly input: DefaultNornRunInput) {
|
|
43
29
|
this.id = input.id;
|
|
44
|
-
this.workspace = input.workspace;
|
|
45
|
-
this.projectRoot = input.projectRoot;
|
|
46
|
-
this.cwd = input.cwd;
|
|
47
|
-
this.resources = input.resources;
|
|
48
|
-
this.artifacts = input.artifacts;
|
|
49
30
|
this.logs = {
|
|
50
31
|
read: (log) => this.input.logs.read(log),
|
|
51
32
|
};
|
|
52
33
|
this.commands = {
|
|
53
34
|
run: (commandInput) =>
|
|
54
35
|
new NornCommandRunner({
|
|
55
|
-
boundaryRoot: this.boundaryRoot,
|
|
56
|
-
boundaryName: this.input.isolationMode,
|
|
57
|
-
cwd: this.cwd,
|
|
58
36
|
signal: this.input.signal,
|
|
59
37
|
logs: this.input.logs,
|
|
60
38
|
logger: this.input.logger,
|
|
@@ -66,26 +44,10 @@ export class NornRunContext implements NornProjectRun {
|
|
|
66
44
|
};
|
|
67
45
|
}
|
|
68
46
|
|
|
69
|
-
forWorkflow(workflow: NornAnyWorkflowDeclaration): NornRunContext {
|
|
70
|
-
const isolationMode = workflow.isolation.mode;
|
|
71
|
-
return new NornRunContext({
|
|
72
|
-
...this.input,
|
|
73
|
-
isolationMode,
|
|
74
|
-
cwd: isolationMode === "project" ? this.projectRoot : this.workspace,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private get boundaryRoot(): string {
|
|
79
|
-
return this.input.isolationMode === "project" ? this.projectRoot : this.workspace;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
47
|
private createAgentRunner(): NornAgentRunner {
|
|
83
48
|
return new NornAgentRunner({
|
|
84
49
|
id: this.id,
|
|
85
50
|
runRoot: this.input.runRoot,
|
|
86
|
-
boundaryRoot: this.boundaryRoot,
|
|
87
|
-
boundaryName: this.input.isolationMode,
|
|
88
|
-
cwd: this.cwd,
|
|
89
51
|
signal: this.input.signal,
|
|
90
52
|
model: this.input.model,
|
|
91
53
|
thinkingLevel: this.input.thinkingLevel,
|
|
@@ -96,14 +58,6 @@ export class NornRunContext implements NornProjectRun {
|
|
|
96
58
|
});
|
|
97
59
|
}
|
|
98
60
|
|
|
99
|
-
path(relativePath: string): string {
|
|
100
|
-
return this.resolveFromRoot(this.cwd, this.boundaryRoot, relativePath);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
projectPath(relativePath: string): string {
|
|
104
|
-
return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
61
|
next(workflowId: string, args: unknown): ReturnType<NornRun["next"]> {
|
|
108
62
|
return createWorkflowTransition({ workflowId, args });
|
|
109
63
|
}
|
|
@@ -115,13 +69,4 @@ export class NornRunContext implements NornProjectRun {
|
|
|
115
69
|
fail(metadata: NornRunOutcomeMetadata & { readonly summary: string }): ReturnType<NornRun["fail"]> {
|
|
116
70
|
return { type: "fail", metadata };
|
|
117
71
|
}
|
|
118
|
-
|
|
119
|
-
private resolveFromRoot(root: string, boundaryRoot: string, path: string): string {
|
|
120
|
-
const resolvedPath = isAbsolute(path) ? path : resolve(root, path);
|
|
121
|
-
const pathFromBoundary = relative(boundaryRoot, resolvedPath);
|
|
122
|
-
if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
|
|
123
|
-
throw new Error(`Workflow path escapes ${this.input.isolationMode} isolation: ${path}`);
|
|
124
|
-
}
|
|
125
|
-
return resolvedPath;
|
|
126
|
-
}
|
|
127
72
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
|
+
|
|
5
|
+
export async function prepareRunWorkerDirectory(runRoot: string): Promise<string> {
|
|
6
|
+
const directory = join(runRoot, "worker");
|
|
7
|
+
try {
|
|
8
|
+
await mkdir(directory, { mode: 0o555 });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (!isNodeError(error) || error.code !== "EEXIST") throw error;
|
|
11
|
+
}
|
|
12
|
+
const metadata = await lstat(directory);
|
|
13
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Run worker cwd must be a directory, not a symlink: ${directory}`);
|
|
14
|
+
if ((await readdir(directory)).length !== 0) throw new Error(`Run worker cwd must be empty: ${directory}`);
|
|
15
|
+
await chmod(directory, 0o555);
|
|
16
|
+
return directory;
|
|
17
|
+
}
|