@vimhead.dev/norn-cli 0.1.0-tip.35436871363.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 +33 -16
- package/assets/docs/agents.md +4 -4
- package/assets/docs/cli.md +3 -3
- package/assets/docs/recovery.md +1 -1
- package/assets/docs/workflows.md +18 -11
- package/assets/examples/agent-then-analysis/plugin.ts +7 -7
- package/assets/examples/caller-selected-continuation/caller.ts +3 -3
- package/assets/examples/caller-selected-continuation/producer.ts +2 -2
- package/assets/examples/coordinating-multiple-agents/README.md +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +9 -9
- package/assets/examples/getting-started/README.md +24 -9
- package/assets/examples/getting-started/plugin.ts +27 -12
- package/assets/examples/minimal-workflow/README.md +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +1 -1
- package/assets/examples/shared-state/plugin.ts +5 -5
- 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 +3 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +5 -5
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +3 -3
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +5 -5
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +1 -1
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/engine.ts +15 -15
- package/assets/packages/cli/src/internal/{run.ts → execution-context.ts} +23 -31
- package/assets/packages/cli/src/internal/workflow-registry.ts +7 -4
- package/assets/packages/sdk/src/api.ts +43 -37
- package/assets/tests/workflow-ref.test.ts +4 -3
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/engine.d.ts +1 -1
- package/dist/internal/engine.js +13 -13
- package/dist/internal/{run.d.ts → execution-context.d.ts} +8 -13
- package/dist/internal/{run.js → execution-context.js} +11 -15
- package/dist/internal/workflow-registry.d.ts +2 -2
- package/dist/internal/workflow-registry.js +5 -2
- package/package.json +2 -2
|
@@ -7,13 +7,13 @@ import { planningArgsSchema } from "./schema.ts";
|
|
|
7
7
|
import { planningAgentResponseSchema } from "./schema.ts";
|
|
8
8
|
|
|
9
9
|
export const planningWorkflow = developmentLoopScope.workflow({
|
|
10
|
-
|
|
10
|
+
name: "planning",
|
|
11
11
|
isEntrypoint: false,
|
|
12
12
|
instructions: "Create an implementation plan for a repository task.",
|
|
13
13
|
args: planningArgsSchema,
|
|
14
|
-
async execute({ args, paths,
|
|
14
|
+
async execute({ args, paths, agents }): Promise<WorkflowResult> {
|
|
15
15
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
16
|
-
const planning = await
|
|
16
|
+
const planning = await agents.prompt({
|
|
17
17
|
label: "planning",
|
|
18
18
|
cwd: repositoryPath,
|
|
19
19
|
tools: ["read", "grep", "find", "ls"],
|
|
@@ -8,24 +8,24 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
|
|
|
8
8
|
import { reviewAgentResponseSchema } from "./schema.ts";
|
|
9
9
|
|
|
10
10
|
export const reviewWorkflow = developmentLoopScope.workflow({
|
|
11
|
-
|
|
11
|
+
name: "review",
|
|
12
12
|
isEntrypoint: false,
|
|
13
13
|
instructions: "Review the current repository boundary changes.",
|
|
14
14
|
args: reviewArgsSchema,
|
|
15
|
-
async execute({ args, paths,
|
|
15
|
+
async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
|
|
16
16
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
17
17
|
const implementationSummary = args.implementationSummary;
|
|
18
18
|
const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
|
|
19
|
-
const diff = await
|
|
19
|
+
const diff = await commands.run({
|
|
20
20
|
label: `review-${args.iteration}-diff`,
|
|
21
21
|
cwd: repositoryPath,
|
|
22
22
|
command: "git status --short && git diff --stat HEAD -- . && git diff HEAD -- .",
|
|
23
23
|
});
|
|
24
24
|
await ensureCommandSucceeded(diff);
|
|
25
|
-
const diffOutput = await
|
|
25
|
+
const diffOutput = await logs.read(diff.stdoutLog);
|
|
26
26
|
await mkdir(join(paths.workspace, "review"), { recursive: true });
|
|
27
27
|
await writeFile(join(paths.workspace, `review/iteration-${args.iteration}-diff.txt`), diffOutput);
|
|
28
|
-
const review = await
|
|
28
|
+
const review = await agents.prompt({
|
|
29
29
|
label: `review-${args.iteration}`,
|
|
30
30
|
cwd: repositoryPath,
|
|
31
31
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
@@ -7,7 +7,7 @@ import { reviewRouterArgsSchema } from "./schema.ts";
|
|
|
7
7
|
import type { StoredReview } from "../review/schema.ts";
|
|
8
8
|
|
|
9
9
|
export const reviewRouterWorkflow = developmentLoopScope.workflow({
|
|
10
|
-
|
|
10
|
+
name: "reviewRouter",
|
|
11
11
|
isEntrypoint: false,
|
|
12
12
|
instructions: "Route implementation and review iterations based on the latest review decision.",
|
|
13
13
|
gate: {
|
package/assets/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.1.0-tip.
|
|
1
|
+
{"version":"0.1.0-tip.35568631545.1"}
|
|
@@ -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.35568631545.1",
|
|
6
|
+
"commit": "f16f4b16ede3d325d56cdbd1f21193b3a3d45d38",
|
|
7
7
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
8
8
|
"upgrade": {
|
|
9
9
|
"supported": false,
|
|
@@ -22,7 +22,7 @@ import { NornRunLease } from "./run-lease.ts";
|
|
|
22
22
|
import { NornRunLogger } from "./run-log.ts";
|
|
23
23
|
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot, type NornRunState } from "./run-state.ts";
|
|
24
24
|
import { NornRunStore, runCurrentRoot } from "./run-store.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { NornExecutionContext } from "./execution-context.ts";
|
|
26
26
|
import { NornWorkflowRegistry, type NornRegisteredWorkflow, type NornWorkflowStepResult } from "./workflow-registry.ts";
|
|
27
27
|
|
|
28
28
|
const RUNS_DIR_NAME = ".norn";
|
|
@@ -40,7 +40,7 @@ export type NornEngineInput = {
|
|
|
40
40
|
type RunSession = {
|
|
41
41
|
readonly runRoot: string;
|
|
42
42
|
readonly cwd: string;
|
|
43
|
-
readonly
|
|
43
|
+
readonly execution: NornExecutionContext;
|
|
44
44
|
readonly paths: NornWorkflowPaths;
|
|
45
45
|
readonly state: NornRunStateStore;
|
|
46
46
|
readonly lease: NornRunLease;
|
|
@@ -185,12 +185,12 @@ export class NornEngine {
|
|
|
185
185
|
for (let step = 1; step <= 1_000; step++) {
|
|
186
186
|
throwIfRunAborted(session.activeRun);
|
|
187
187
|
if (shouldPauseForGate(currentStep)) {
|
|
188
|
-
const description = await this.registry.describeGate({ workflow: 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: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
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
196
|
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
@@ -199,12 +199,12 @@ export class NornEngine {
|
|
|
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: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
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: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
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.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
|
|
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) });
|
|
@@ -233,7 +233,7 @@ export class NornEngine {
|
|
|
233
233
|
try {
|
|
234
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({ workflow: step.workflow,
|
|
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
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 });
|
|
@@ -289,7 +289,7 @@ export class NornEngine {
|
|
|
289
289
|
initialCwd: cwd,
|
|
290
290
|
startedAt,
|
|
291
291
|
} });
|
|
292
|
-
const
|
|
292
|
+
const execution = await this.buildExecutionContext({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
293
293
|
const state = await NornRunStateStore.create(runRoot, {
|
|
294
294
|
projectRoot: paths.project,
|
|
295
295
|
id,
|
|
@@ -303,7 +303,7 @@ export class NornEngine {
|
|
|
303
303
|
},
|
|
304
304
|
startedAt,
|
|
305
305
|
});
|
|
306
|
-
const session = { runRoot, cwd,
|
|
306
|
+
const session = { runRoot, cwd, execution, paths, state, lease, runStore, logger, activeRun };
|
|
307
307
|
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
308
308
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
309
309
|
return session;
|
|
@@ -324,26 +324,26 @@ export class NornEngine {
|
|
|
324
324
|
const currentRoot = runCurrentRoot(runRoot);
|
|
325
325
|
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
326
326
|
const paths: NornWorkflowPaths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
|
|
327
|
-
const
|
|
328
|
-
return { runRoot, cwd: process.cwd(),
|
|
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
338
|
readonly paths: NornWorkflowPaths;
|
|
339
339
|
readonly signal: AbortSignal;
|
|
340
340
|
readonly logger: NornRunLogger;
|
|
341
|
-
}): Promise<
|
|
341
|
+
}): Promise<NornExecutionContext> {
|
|
342
342
|
const logsRoot = join(input.currentRoot, "logs");
|
|
343
343
|
await mkdir(input.paths.workspace, { recursive: true });
|
|
344
344
|
await mkdir(logsRoot, { recursive: true });
|
|
345
345
|
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
346
|
-
return new
|
|
346
|
+
return new NornExecutionContext({
|
|
347
347
|
id: input.id,
|
|
348
348
|
runRoot: input.currentRoot,
|
|
349
349
|
signal: input.signal,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { NornAgents, NornCommands, NornLogs, NornRun } from "@vimhead.dev/norn";
|
|
3
3
|
import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
|
|
4
4
|
import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
|
|
5
5
|
import { NornAgentRunner } from "./agents.ts";
|
|
@@ -7,7 +7,7 @@ import { NornCommandRunner } from "./commands.ts";
|
|
|
7
7
|
import type { NornRunLogs } from "./logs.ts";
|
|
8
8
|
import type { NornRunLogger } from "./run-log.ts";
|
|
9
9
|
|
|
10
|
-
type
|
|
10
|
+
type NornExecutionContextInput = {
|
|
11
11
|
readonly id: string;
|
|
12
12
|
readonly runRoot: string;
|
|
13
13
|
readonly signal?: AbortSignal;
|
|
@@ -19,34 +19,38 @@ type DefaultNornRunInput = {
|
|
|
19
19
|
readonly logs: NornRunLogs;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export class
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
25
|
-
readonly
|
|
26
|
-
readonly
|
|
22
|
+
export class NornExecutionContext {
|
|
23
|
+
readonly run: NornRun;
|
|
24
|
+
readonly logs: NornLogs;
|
|
25
|
+
readonly commands: NornCommands;
|
|
26
|
+
readonly agents: NornAgents;
|
|
27
27
|
|
|
28
|
-
constructor(private readonly input:
|
|
29
|
-
this.
|
|
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
|
+
};
|
|
30
35
|
this.logs = {
|
|
31
|
-
read:
|
|
36
|
+
read: log => this.input.logs.read(log),
|
|
32
37
|
};
|
|
33
38
|
this.commands = {
|
|
34
|
-
run:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}).run(commandInput),
|
|
39
|
+
run: commandInput => new NornCommandRunner({
|
|
40
|
+
signal: this.input.signal,
|
|
41
|
+
logs: this.input.logs,
|
|
42
|
+
logger: this.input.logger,
|
|
43
|
+
}).run(commandInput),
|
|
40
44
|
};
|
|
41
45
|
this.agents = {
|
|
42
|
-
createSession:
|
|
43
|
-
prompt:
|
|
46
|
+
createSession: agentInput => this.createAgentRunner().createSession(agentInput),
|
|
47
|
+
prompt: agentInput => this.createAgentRunner().prompt(agentInput),
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
private createAgentRunner(): NornAgentRunner {
|
|
48
52
|
return new NornAgentRunner({
|
|
49
|
-
id: this.id,
|
|
53
|
+
id: this.input.id,
|
|
50
54
|
runRoot: this.input.runRoot,
|
|
51
55
|
signal: this.input.signal,
|
|
52
56
|
model: this.input.model,
|
|
@@ -57,16 +61,4 @@ export class NornRunContext implements NornRun {
|
|
|
57
61
|
responseCollector: this.input.responseCollector,
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
next(workflowId: string, args: unknown): ReturnType<NornRun["next"]> {
|
|
62
|
-
return createWorkflowTransition({ workflowId, args });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]> {
|
|
66
|
-
return { type: "complete", metadata };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
fail(metadata: NornRunOutcomeMetadata & { readonly summary: string }): ReturnType<NornRun["fail"]> {
|
|
70
|
-
return { type: "fail", metadata };
|
|
71
|
-
}
|
|
72
64
|
}
|
|
@@ -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";
|
|
@@ -43,7 +43,7 @@ export type NornWorkflowStepResult =
|
|
|
43
43
|
| { readonly type: "fail"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata: NornRunFail["metadata"] };
|
|
44
44
|
|
|
45
45
|
type NornExecutionContextInput = {
|
|
46
|
-
readonly
|
|
46
|
+
readonly execution: Pick<NornWorkflowContext, "run" | "agents" | "commands" | "logs">;
|
|
47
47
|
readonly paths: NornWorkflowPaths;
|
|
48
48
|
readonly args: unknown;
|
|
49
49
|
readonly configOverride: unknown;
|
|
@@ -129,14 +129,17 @@ export class NornWorkflowRegistry {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
private createExecutionContext(input: NornExecutionContextInput & { readonly entry: NornRegisteredWorkflow }): NornWorkflowContext {
|
|
132
|
-
const { entry,
|
|
132
|
+
const { entry, execution, paths, configOverride } = input;
|
|
133
133
|
const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : undefined;
|
|
134
134
|
return {
|
|
135
135
|
args: Value.Decode(entry.workflow.args, input.args),
|
|
136
136
|
config: parseExecutionConfig(entry.configuration, configOverride),
|
|
137
137
|
...(scope ? { scope: { id: scope.definition.id, config: parseExecutionConfig(scope.configuration, configOverride) } } : {}),
|
|
138
138
|
paths,
|
|
139
|
-
run,
|
|
139
|
+
run: execution.run,
|
|
140
|
+
agents: execution.agents,
|
|
141
|
+
commands: execution.commands,
|
|
142
|
+
logs: execution.logs,
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
|
|
@@ -79,18 +79,21 @@ export type NornWorkflowContext<
|
|
|
79
79
|
readonly args: StaticDecode<ArgsSchema>;
|
|
80
80
|
readonly config: WorkflowConfig<ConfigSchema>;
|
|
81
81
|
readonly paths: NornWorkflowPaths;
|
|
82
|
+
readonly agents: NornAgents;
|
|
83
|
+
readonly commands: NornCommands;
|
|
84
|
+
readonly logs: NornLogs;
|
|
82
85
|
readonly run: NornRun;
|
|
83
86
|
} & (Scope extends NornWorkflowScopeInfo ? {
|
|
84
87
|
readonly scope: { readonly id: Scope["id"]; readonly config: WorkflowConfig<Scope["config"]> };
|
|
85
88
|
} : {});
|
|
86
89
|
|
|
87
90
|
export type NornWorkflowDefinition<
|
|
88
|
-
|
|
91
|
+
Name extends string = string,
|
|
89
92
|
ArgsSchema extends TSchema = TSchema,
|
|
90
93
|
ConfigSchema extends TSchema | undefined = undefined,
|
|
91
94
|
Scope extends NornWorkflowScopeInfo | undefined = undefined,
|
|
92
95
|
> = {
|
|
93
|
-
readonly
|
|
96
|
+
readonly name: Name;
|
|
94
97
|
readonly args: ArgsSchema;
|
|
95
98
|
readonly config?: ConfigSchema;
|
|
96
99
|
readonly gate?: NornWorkflowGate<ArgsSchema> & {
|
|
@@ -104,48 +107,48 @@ export type NornWorkflowScopeInfo = {
|
|
|
104
107
|
readonly config?: TSchema;
|
|
105
108
|
};
|
|
106
109
|
|
|
107
|
-
export type NornWorkflowScope<
|
|
108
|
-
readonly id:
|
|
110
|
+
export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
|
|
111
|
+
readonly id: Name;
|
|
109
112
|
readonly config: ConfigSchema;
|
|
110
|
-
workflow<const
|
|
111
|
-
definition: NornWorkflowDefinition<
|
|
112
|
-
): NornWorkflowDeclaration<`${
|
|
113
|
+
workflow<const LocalName extends string, ArgsSchema extends TSchema, LocalConfig extends TSchema | undefined = undefined>(
|
|
114
|
+
definition: NornWorkflowDefinition<LocalName, ArgsSchema, LocalConfig, { readonly id: Name; readonly config: ConfigSchema }>,
|
|
115
|
+
): NornWorkflowDeclaration<`${Name}.${LocalName}`, ArgsSchema, LocalConfig, { readonly id: Name; readonly config: ConfigSchema }>;
|
|
113
116
|
};
|
|
114
117
|
|
|
115
118
|
export function workflow<
|
|
116
|
-
const
|
|
119
|
+
const Name extends string,
|
|
117
120
|
ArgsSchema extends TSchema,
|
|
118
121
|
ConfigSchema extends TSchema | undefined = undefined,
|
|
119
|
-
>(definition: NornWorkflowDefinition<
|
|
120
|
-
return createWorkflow({ definition, scope: undefined });
|
|
122
|
+
>(definition: NornWorkflowDefinition<Name, ArgsSchema, ConfigSchema>): NornWorkflowDeclaration<Name, ArgsSchema, ConfigSchema, undefined> {
|
|
123
|
+
return createWorkflow({ definition, id: definition.name, scope: undefined });
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function createWorkflow<Id extends string, ArgsSchema extends TSchema, ConfigSchema extends TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined>(
|
|
124
|
-
input: { readonly definition: NornWorkflowDefinition<
|
|
127
|
+
input: { readonly definition: NornWorkflowDefinition<string, ArgsSchema, ConfigSchema, Scope>; readonly id: Id; readonly scope: Scope },
|
|
125
128
|
): NornWorkflowDeclaration<Id, ArgsSchema, ConfigSchema, Scope> {
|
|
126
|
-
const { definition, scope } = input;
|
|
127
|
-
|
|
128
|
-
const callable = (args: StaticEncode<ArgsSchema>): NornRunNext => createWorkflowTransition({ workflowId:
|
|
129
|
-
const result = Object.assign(callable,
|
|
129
|
+
const { definition: { name, ...properties }, id, scope } = input;
|
|
130
|
+
assertDeclarationName(name);
|
|
131
|
+
const callable = (args: StaticEncode<ArgsSchema>): NornRunNext => createWorkflowTransition({ workflowId: id, args });
|
|
132
|
+
const result = Object.assign(callable, properties, {
|
|
130
133
|
kind: WORKFLOW_DECLARATION_KIND,
|
|
134
|
+
id,
|
|
131
135
|
scope,
|
|
132
136
|
}) as NornWorkflowDeclaration<Id, ArgsSchema, ConfigSchema, Scope>;
|
|
133
|
-
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${
|
|
137
|
+
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${id}`);
|
|
134
138
|
assertWorkflowMetadata(result);
|
|
135
139
|
return result;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
export function workflowScope<const
|
|
139
|
-
input: { readonly
|
|
140
|
-
): NornWorkflowScope<
|
|
141
|
-
|
|
142
|
-
const scope = { id: input.
|
|
142
|
+
export function workflowScope<const Name extends string, ConfigSchema extends TSchema | undefined = undefined>(
|
|
143
|
+
input: { readonly name: Name; readonly config?: ConfigSchema },
|
|
144
|
+
): NornWorkflowScope<Name, ConfigSchema> {
|
|
145
|
+
assertDeclarationName(input.name);
|
|
146
|
+
const scope = { id: input.name, config: input.config as ConfigSchema };
|
|
143
147
|
return {
|
|
144
|
-
id:
|
|
145
|
-
config:
|
|
148
|
+
id: scope.id,
|
|
149
|
+
config: scope.config,
|
|
146
150
|
workflow(definition) {
|
|
147
|
-
|
|
148
|
-
return createWorkflow({ definition: { ...definition, id: `${input.id}.${definition.id}` }, scope });
|
|
151
|
+
return createWorkflow({ definition, id: `${scope.id}.${definition.name}`, scope });
|
|
149
152
|
},
|
|
150
153
|
};
|
|
151
154
|
}
|
|
@@ -467,16 +470,19 @@ export type NornRun = {
|
|
|
467
470
|
next(workflowId: string, args: unknown): NornRunNext;
|
|
468
471
|
complete(metadata?: NornRunOutcomeMetadata): NornRunComplete;
|
|
469
472
|
fail(metadata: NornRunOutcomeMetadata & { readonly summary: string }): NornRunFail;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export type NornLogs = {
|
|
476
|
+
read(log: NornLogRef): Promise<string>;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
export type NornCommands = {
|
|
480
|
+
run(input: NornCommandRunInput): Promise<NornCommandRunResult>;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export type NornAgents = {
|
|
484
|
+
createSession(input: NornAgentCreateSessionInput): Promise<NornAgentSession>;
|
|
485
|
+
prompt<ResponseSchema extends TSchema>(input: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
480
486
|
};
|
|
481
487
|
|
|
482
488
|
export type NornWorkflowSource = {
|
|
@@ -552,8 +558,8 @@ export type NornProjectInfo = {
|
|
|
552
558
|
readonly modules: readonly NornWorkflowSource[];
|
|
553
559
|
};
|
|
554
560
|
|
|
555
|
-
function
|
|
556
|
-
if (typeof
|
|
561
|
+
function assertDeclarationName(name: string): void {
|
|
562
|
+
if (typeof name !== "string" || name.trim().length === 0 || name.includes(".")) throw new Error(`Invalid workflow or scope name: ${name}; expected a nonempty name without dots`);
|
|
557
563
|
}
|
|
558
564
|
|
|
559
565
|
export function isWorkflowDeclaration(value: unknown): value is NornAnyWorkflowDeclaration {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { workflowScope, isWorkflowDeclaration, workflowRefSchema, type NornWorkflowRefSchemaOptions } from "@vimhead.dev/norn";
|
|
2
2
|
import { inspectSchema } from "@vimhead.dev/norn/schema";
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { Value } from "typebox/value";
|
|
6
6
|
import { expect, expectTypeOf, test } from "vitest";
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const refs = workflowScope({ name: "refs" });
|
|
9
|
+
const target = refs.workflow({
|
|
10
|
+
name: "finish", isEntrypoint: false, args: Type.Object({ report: Type.String() }),
|
|
10
11
|
execute() { throw new Error("A callable must select a transition, not execute it"); },
|
|
11
12
|
});
|
|
12
13
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const NORN_GENERATED_BUILD_INFO: {
|
|
2
2
|
readonly kind: "npm-registry";
|
|
3
|
-
readonly version: "0.1.0-tip.
|
|
4
|
-
readonly commit: "
|
|
3
|
+
readonly version: "0.1.0-tip.35568631545.1";
|
|
4
|
+
readonly commit: "f16f4b16ede3d325d56cdbd1f21193b3a3d45d38";
|
|
5
5
|
readonly packageSpec: "@vimhead.dev/norn-cli@tip";
|
|
6
6
|
readonly upgrade: {
|
|
7
7
|
readonly supported: false;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/generated-build-info.ts
|
|
2
2
|
var NORN_GENERATED_BUILD_INFO = {
|
|
3
3
|
"kind": "npm-registry",
|
|
4
|
-
"version": "0.1.0-tip.
|
|
5
|
-
"commit": "
|
|
4
|
+
"version": "0.1.0-tip.35568631545.1",
|
|
5
|
+
"commit": "f16f4b16ede3d325d56cdbd1f21193b3a3d45d38",
|
|
6
6
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
7
7
|
"upgrade": {
|
|
8
8
|
"supported": false,
|
package/dist/internal/engine.js
CHANGED
|
@@ -12,7 +12,7 @@ import { NornRunLease } from "./run-lease.js";
|
|
|
12
12
|
import { NornRunLogger } from "./run-log.js";
|
|
13
13
|
import { assertRunVersion, getRunInfo, mergeInterruptedWorkflowArgs, NornRunStateStore, resolveRunRoot } from "./run-state.js";
|
|
14
14
|
import { NornRunStore, runCurrentRoot } from "./run-store.js";
|
|
15
|
-
import {
|
|
15
|
+
import { NornExecutionContext } from "./execution-context.js";
|
|
16
16
|
import { NornWorkflowRegistry } from "./workflow-registry.js";
|
|
17
17
|
var RUNS_DIR_NAME = ".norn";
|
|
18
18
|
var MANIFEST_FILE_NAME = "manifest.json";
|
|
@@ -127,12 +127,12 @@ var NornEngine = class {
|
|
|
127
127
|
for (let step = 1; step <= 1e3; step++) {
|
|
128
128
|
throwIfRunAborted(session.activeRun);
|
|
129
129
|
if (shouldPauseForGate(currentStep)) {
|
|
130
|
-
const description = await this.registry.describeGate({ workflow: 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 });
|
|
131
131
|
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
132
132
|
await session.state.interruptCurrent(currentStep.args, interruption);
|
|
133
133
|
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
134
134
|
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
135
|
-
return interruptedLaunchResult({ id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd }, currentStep.workflow, currentStep.args, interruption);
|
|
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);
|
|
136
136
|
}
|
|
137
137
|
await session.state.startStep(toRunStateStep(currentStep));
|
|
138
138
|
const stepResult = await this.executeWorkflowStep(session, currentStep);
|
|
@@ -141,12 +141,12 @@ var NornEngine = class {
|
|
|
141
141
|
await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
|
|
142
142
|
await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
143
143
|
await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
|
|
144
|
-
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 };
|
|
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 };
|
|
145
145
|
}
|
|
146
146
|
if (stepResult.type === "fail") {
|
|
147
147
|
await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
|
|
148
148
|
await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
149
|
-
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 };
|
|
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 };
|
|
150
150
|
}
|
|
151
151
|
const nextStep = this.nextWorkflowStep(stepResult);
|
|
152
152
|
await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
|
|
@@ -159,7 +159,7 @@ var NornEngine = class {
|
|
|
159
159
|
if (isRunStopped(session, error)) {
|
|
160
160
|
await session.state.stopCurrent();
|
|
161
161
|
await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
|
|
162
|
-
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.paths.workspace, cwd: session.cwd, workflowId: currentStep.workflow.id };
|
|
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 };
|
|
163
163
|
}
|
|
164
164
|
await session.state.failCurrent(errorMessage(error));
|
|
165
165
|
await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
|
|
@@ -174,7 +174,7 @@ var NornEngine = class {
|
|
|
174
174
|
try {
|
|
175
175
|
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
176
176
|
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
177
|
-
const result = await this.registry.execute({ workflow: step.workflow,
|
|
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
178
|
await assertWorkspaceBoundary(session, session.paths.workspace);
|
|
179
179
|
const durationMs = Date.now() - startedAtMs;
|
|
180
180
|
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
@@ -224,7 +224,7 @@ var NornEngine = class {
|
|
|
224
224
|
initialCwd: cwd,
|
|
225
225
|
startedAt
|
|
226
226
|
} });
|
|
227
|
-
const
|
|
227
|
+
const execution = await this.buildExecutionContext({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
|
|
228
228
|
const state = await NornRunStateStore.create(runRoot, {
|
|
229
229
|
projectRoot: paths.project,
|
|
230
230
|
id,
|
|
@@ -241,7 +241,7 @@ var NornEngine = class {
|
|
|
241
241
|
},
|
|
242
242
|
startedAt
|
|
243
243
|
});
|
|
244
|
-
const session = { runRoot, cwd,
|
|
244
|
+
const session = { runRoot, cwd, execution, paths, state, lease, runStore, logger, activeRun };
|
|
245
245
|
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
246
246
|
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
247
247
|
return session;
|
|
@@ -261,19 +261,19 @@ var NornEngine = class {
|
|
|
261
261
|
const currentRoot = runCurrentRoot(runRoot);
|
|
262
262
|
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
263
263
|
const paths = { project: currentState.projectRoot ?? resolve(runRoot, "../../.."), workspace: currentState.workspace };
|
|
264
|
-
const
|
|
265
|
-
return { runRoot, cwd: process.cwd(),
|
|
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 };
|
|
266
266
|
} catch (error) {
|
|
267
267
|
this.failActiveRun(runRoot, activeRun);
|
|
268
268
|
throw error;
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
async
|
|
271
|
+
async buildExecutionContext(input) {
|
|
272
272
|
const logsRoot = join(input.currentRoot, "logs");
|
|
273
273
|
await mkdir(input.paths.workspace, { recursive: true });
|
|
274
274
|
await mkdir(logsRoot, { recursive: true });
|
|
275
275
|
const files = createRunFileCoordinator(dirname(input.currentRoot));
|
|
276
|
-
return new
|
|
276
|
+
return new NornExecutionContext({
|
|
277
277
|
id: input.id,
|
|
278
278
|
runRoot: input.currentRoot,
|
|
279
279
|
signal: input.signal,
|