@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
|
@@ -7,23 +7,22 @@ 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
|
-
import {
|
|
25
|
+
import { NornExecutionContext } from "./execution-context.ts";
|
|
27
26
|
import { NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
|
|
28
27
|
|
|
29
28
|
const RUNS_DIR_NAME = ".norn";
|
|
@@ -40,7 +39,9 @@ export type NornEngineInput = {
|
|
|
40
39
|
|
|
41
40
|
type RunSession = {
|
|
42
41
|
readonly runRoot: string;
|
|
43
|
-
readonly
|
|
42
|
+
readonly cwd: string;
|
|
43
|
+
readonly execution: NornExecutionContext;
|
|
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, execution: session.execution, 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.execution.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.execution.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.execution.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.execution.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,
|
|
237
|
-
await assertWorkspaceBoundary(session,
|
|
236
|
+
const result = await this.registry.execute({ workflow: step.workflow, execution: session.execution, 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
|
|
292
|
+
const execution = await this.buildExecutionContext({ 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,
|
|
306
|
+
const session = { runRoot, cwd, execution, 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,46 +323,34 @@ 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 execution = await this.buildExecutionContext({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
328
|
+
return { runRoot, cwd: process.cwd(), execution, paths, state, lease, runStore, logger, activeRun };
|
|
329
329
|
} catch (error) {
|
|
330
330
|
this.failActiveRun(runRoot, activeRun);
|
|
331
331
|
throw error;
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
private async
|
|
335
|
+
private async buildExecutionContext(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
|
-
}): Promise<
|
|
344
|
-
const artifactsRoot = join(input.currentRoot, "artifacts");
|
|
341
|
+
}): Promise<NornExecutionContext> {
|
|
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
|
|
350
|
-
return new
|
|
345
|
+
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
346
|
+
return new NornExecutionContext({
|
|
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 {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { NornAgents, NornCommands, NornLogs, NornRun } from "@vimhead.dev/norn";
|
|
3
|
+
import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
|
|
4
|
+
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
5
|
+
import { NornAgentRunner } from "./agents.ts";
|
|
6
|
+
import { NornCommandRunner } from "./commands.ts";
|
|
7
|
+
import type { NornRunLogs } from "./logs.ts";
|
|
8
|
+
import type { NornRunLogger } from "./run-log.ts";
|
|
9
|
+
|
|
10
|
+
type NornExecutionContextInput = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly runRoot: string;
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
readonly model?: CreateAgentSessionOptions["model"];
|
|
15
|
+
readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
16
|
+
readonly agentDir?: string;
|
|
17
|
+
readonly responseCollector: NornAgentResponseCollector;
|
|
18
|
+
readonly logger: NornRunLogger;
|
|
19
|
+
readonly logs: NornRunLogs;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class NornExecutionContext {
|
|
23
|
+
readonly run: NornRun;
|
|
24
|
+
readonly logs: NornLogs;
|
|
25
|
+
readonly commands: NornCommands;
|
|
26
|
+
readonly agents: NornAgents;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly input: NornExecutionContextInput) {
|
|
29
|
+
this.run = {
|
|
30
|
+
id: input.id,
|
|
31
|
+
next: (workflowId, args) => createWorkflowTransition({ workflowId, args }),
|
|
32
|
+
complete: metadata => ({ type: "complete", metadata }),
|
|
33
|
+
fail: metadata => ({ type: "fail", metadata }),
|
|
34
|
+
};
|
|
35
|
+
this.logs = {
|
|
36
|
+
read: log => this.input.logs.read(log),
|
|
37
|
+
};
|
|
38
|
+
this.commands = {
|
|
39
|
+
run: commandInput => new NornCommandRunner({
|
|
40
|
+
signal: this.input.signal,
|
|
41
|
+
logs: this.input.logs,
|
|
42
|
+
logger: this.input.logger,
|
|
43
|
+
}).run(commandInput),
|
|
44
|
+
};
|
|
45
|
+
this.agents = {
|
|
46
|
+
createSession: agentInput => this.createAgentRunner().createSession(agentInput),
|
|
47
|
+
prompt: agentInput => this.createAgentRunner().prompt(agentInput),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private createAgentRunner(): NornAgentRunner {
|
|
52
|
+
return new NornAgentRunner({
|
|
53
|
+
id: this.input.id,
|
|
54
|
+
runRoot: this.input.runRoot,
|
|
55
|
+
signal: this.input.signal,
|
|
56
|
+
model: this.input.model,
|
|
57
|
+
thinkingLevel: this.input.thinkingLevel,
|
|
58
|
+
agentDir: this.input.agentDir,
|
|
59
|
+
logs: this.input.logs,
|
|
60
|
+
logger: this.input.logger,
|
|
61
|
+
responseCollector: this.input.responseCollector,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -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",
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isWorkflowComplete, isWorkflowDeclaration, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type
|
|
1
|
+
import { isWorkflowComplete, isWorkflowDeclaration, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunNext, type NornWorkflowPaths, type NornWorkflowContext, type NornWorkflowGateInfo, type NornWorkflowSource, type NornWorkflowScopeInfo, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
|
|
2
2
|
import { assertWorkflowMetadata, inspectSchema, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
|
|
3
3
|
import type { TSchema } from "typebox";
|
|
4
4
|
import { Value } from "typebox/value";
|
|
@@ -42,6 +42,15 @@ export type NornWorkflowStepResult =
|
|
|
42
42
|
| { readonly type: "complete"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata?: NornRunComplete["metadata"] }
|
|
43
43
|
| { readonly type: "fail"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata: NornRunFail["metadata"] };
|
|
44
44
|
|
|
45
|
+
type NornExecutionContextInput = {
|
|
46
|
+
readonly execution: Pick<NornWorkflowContext, "run" | "agents" | "commands" | "logs">;
|
|
47
|
+
readonly paths: NornWorkflowPaths;
|
|
48
|
+
readonly args: unknown;
|
|
49
|
+
readonly configOverride: unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type NornWorkflowExecutionInput = NornExecutionContextInput & { readonly workflow: NornAnyWorkflowDeclaration };
|
|
53
|
+
|
|
45
54
|
export class NornWorkflowRegistry {
|
|
46
55
|
private readonly entries = new Map<string, NornRegisteredWorkflow>();
|
|
47
56
|
private readonly scopes = new Map<string, NornRegisteredScope>();
|
|
@@ -99,32 +108,38 @@ export class NornWorkflowRegistry {
|
|
|
99
108
|
launchableEntries(): NornRegisteredWorkflow[] { return this.sortedEntries().filter(({ workflow }) => workflow.isEntrypoint); }
|
|
100
109
|
workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined { return this.entries.get(workflowId)?.workflow; }
|
|
101
110
|
|
|
102
|
-
async describeGate
|
|
111
|
+
async describeGate(input: NornWorkflowExecutionInput): Promise<string> {
|
|
112
|
+
const { workflow } = input;
|
|
103
113
|
const entry = this.requireEntry(workflow.id);
|
|
104
114
|
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
105
|
-
const context = this.createExecutionContext({
|
|
115
|
+
const context = this.createExecutionContext({ ...input, entry });
|
|
106
116
|
const description = await entry.workflow.gate?.describe?.(context) ?? workflow.id;
|
|
107
117
|
if (typeof description !== "string" || description.trim().length === 0) throw new Error(`Workflow gate description must not be empty: ${workflow.id}`);
|
|
108
118
|
return description.trim();
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
async execute
|
|
121
|
+
async execute(input: NornWorkflowExecutionInput): Promise<NornWorkflowStepResult> {
|
|
122
|
+
const { workflow } = input;
|
|
112
123
|
const entry = this.requireEntry(workflow.id);
|
|
113
|
-
const result = await entry.workflow.execute(this.createExecutionContext({
|
|
124
|
+
const result = await entry.workflow.execute(this.createExecutionContext({ ...input, entry }));
|
|
114
125
|
if (isWorkflowNext(result)) return result;
|
|
115
126
|
if (isWorkflowComplete(result)) return { type: "complete", workflow, metadata: result.metadata };
|
|
116
127
|
if (isWorkflowFail(result)) return { type: "fail", workflow, metadata: result.metadata };
|
|
117
128
|
throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
private createExecutionContext(input: { readonly entry: NornRegisteredWorkflow
|
|
121
|
-
const { entry,
|
|
131
|
+
private createExecutionContext(input: NornExecutionContextInput & { readonly entry: NornRegisteredWorkflow }): NornWorkflowContext {
|
|
132
|
+
const { entry, execution, paths, configOverride } = input;
|
|
122
133
|
const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : undefined;
|
|
123
134
|
return {
|
|
124
135
|
args: Value.Decode(entry.workflow.args, input.args),
|
|
125
136
|
config: parseExecutionConfig(entry.configuration, configOverride),
|
|
126
137
|
...(scope ? { scope: { id: scope.definition.id, config: parseExecutionConfig(scope.configuration, configOverride) } } : {}),
|
|
127
|
-
|
|
138
|
+
paths,
|
|
139
|
+
run: execution.run,
|
|
140
|
+
agents: execution.agents,
|
|
141
|
+
commands: execution.commands,
|
|
142
|
+
logs: execution.logs,
|
|
128
143
|
};
|
|
129
144
|
}
|
|
130
145
|
|
|
@@ -175,7 +190,7 @@ function assertGateWorkflow(workflow: NornAnyWorkflowDeclaration): void {
|
|
|
175
190
|
function workflowInfo(entry: NornRegisteredWorkflow): NornRegisteredWorkflowInfo {
|
|
176
191
|
return {
|
|
177
192
|
id: entry.workflow.id, instructions: entry.workflow.instructions, isEntrypoint: entry.workflow.isEntrypoint,
|
|
178
|
-
|
|
193
|
+
source: entry.source,
|
|
179
194
|
configKey: entry.workflow.id,
|
|
180
195
|
scope: entry.scopeId ? { id: entry.scopeId } : undefined,
|
|
181
196
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function requireAbsoluteWorkingDirectory(cwd: string): string {
|
|
4
|
+
if (typeof cwd !== "string" || !isAbsolute(cwd)) throw new Error("Working directory (cwd) must be an absolute path");
|
|
5
|
+
return resolve(cwd);
|
|
6
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as nornModule from "@vimhead.dev/norn";
|
|
2
2
|
import { isWorkflowDeclaration, type NornAnyWorkflowDeclaration, type NornDispose, type NornWorkflowDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectConfigurationInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowSource } from "@vimhead.dev/norn";
|
|
3
3
|
import { isNodeError } from "@vimhead.dev/norn-core/errors";
|
|
4
|
-
import * as nornFilesModule from "@vimhead.dev/norn/files";
|
|
5
4
|
import * as nornSchemaModule from "@vimhead.dev/norn/schema";
|
|
6
5
|
import { inspectSchema, isPlainObject } from "@vimhead.dev/norn/schema";
|
|
7
6
|
import { createJiti, type ModuleCache } from "jiti/static";
|
|
@@ -178,7 +177,7 @@ function createDiagnostic(input: { readonly source: NornWorkflowSource; readonly
|
|
|
178
177
|
return { configPath: input.source.configPath, modulePath: input.source.path, scopeId: input.scopeId, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error), issues: input.error instanceof AssertError ? input.error.cause.errors : [] };
|
|
179
178
|
}
|
|
180
179
|
function nornWorkflowVirtualModules(): Record<string, unknown> {
|
|
181
|
-
return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/
|
|
180
|
+
return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
|
|
182
181
|
}
|
|
183
182
|
function mergeProjectConfig(files: readonly NornConfigFile[]): Record<string, unknown> {
|
|
184
183
|
const [project, ...reusable] = files;
|