@vimhead.dev/norn-cli 0.1.0-tip.35436871363.1 → 0.1.0-tip.35570530940.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 +35 -17
- 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 +22 -16
- package/assets/examples/agent-then-analysis/plugin.ts +9 -10
- package/assets/examples/caller-selected-continuation/caller.ts +5 -5
- package/assets/examples/caller-selected-continuation/producer.ts +3 -4
- package/assets/examples/coordinating-multiple-agents/README.md +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +12 -13
- package/assets/examples/getting-started/README.md +24 -9
- package/assets/examples/getting-started/plugin.ts +27 -13
- package/assets/examples/minimal-workflow/README.md +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +2 -3
- package/assets/examples/shared-state/plugin.ts +6 -7
- package/assets/examples/worktree-development-loop/scope.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +4 -5
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +3 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +6 -7
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +4 -5
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +6 -7
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +2 -3
- 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 +11 -6
- package/assets/packages/sdk/src/api.ts +49 -46
- package/assets/packages/sdk/src/schema.ts +5 -5
- 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 +8 -5
- package/package.json +2 -2
|
@@ -8,28 +8,27 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
|
|
|
8
8
|
import { implementationAgentResponseSchema } from "./schema.ts";
|
|
9
9
|
|
|
10
10
|
export const implementationWorkflow = developmentLoopScope.workflow({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
instructions: "Apply one implementation pass in the current repository.",
|
|
11
|
+
name: "implementation",
|
|
12
|
+
entrypoint: false,
|
|
14
13
|
args: implementationArgsSchema,
|
|
15
|
-
async execute({ args, paths,
|
|
14
|
+
async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
|
|
16
15
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
17
16
|
const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
|
|
18
17
|
const previousReview = args.previousReviewPath ? await readFile(join(paths.workspace, args.previousReviewPath), "utf8") : undefined;
|
|
19
|
-
const implementation = await
|
|
18
|
+
const implementation = await agents.prompt({
|
|
20
19
|
label: `implementation-${args.iteration}`,
|
|
21
20
|
cwd: repositoryPath,
|
|
22
21
|
tools: ["read", "grep", "find", "ls", "edit", "write", "bash"],
|
|
23
22
|
prompt: buildImplementationPrompt(args.task, plan, args.iteration, previousReview),
|
|
24
23
|
response: implementationAgentResponseSchema,
|
|
25
24
|
});
|
|
26
|
-
const status = await
|
|
25
|
+
const status = await commands.run({
|
|
27
26
|
label: `implementation-${args.iteration}-status`,
|
|
28
27
|
cwd: repositoryPath,
|
|
29
28
|
command: "git status --short",
|
|
30
29
|
});
|
|
31
30
|
await ensureCommandSucceeded(status);
|
|
32
|
-
const statusOutput = await
|
|
31
|
+
const statusOutput = await logs.read(status.stdoutLog);
|
|
33
32
|
await mkdir(join(paths.workspace, "implementation"), { recursive: true });
|
|
34
33
|
await writeFile(join(paths.workspace, `implementation/iteration-${args.iteration}-status.txt`), statusOutput);
|
|
35
34
|
return reviewWorkflow({ ...args, implementationSummary: implementation.summary });
|
|
@@ -7,13 +7,12 @@ import { planningArgsSchema } from "./schema.ts";
|
|
|
7
7
|
import { planningAgentResponseSchema } from "./schema.ts";
|
|
8
8
|
|
|
9
9
|
export const planningWorkflow = developmentLoopScope.workflow({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
instructions: "Create an implementation plan for a repository task.",
|
|
10
|
+
name: "planning",
|
|
11
|
+
entrypoint: false,
|
|
13
12
|
args: planningArgsSchema,
|
|
14
|
-
async execute({ args, paths,
|
|
13
|
+
async execute({ args, paths, agents }): Promise<WorkflowResult> {
|
|
15
14
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
16
|
-
const planning = await
|
|
15
|
+
const planning = await agents.prompt({
|
|
17
16
|
label: "planning",
|
|
18
17
|
cwd: repositoryPath,
|
|
19
18
|
tools: ["read", "grep", "find", "ls"],
|
|
@@ -8,24 +8,23 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
|
|
|
8
8
|
import { reviewAgentResponseSchema } from "./schema.ts";
|
|
9
9
|
|
|
10
10
|
export const reviewWorkflow = developmentLoopScope.workflow({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
instructions: "Review the current repository boundary changes.",
|
|
11
|
+
name: "review",
|
|
12
|
+
entrypoint: false,
|
|
14
13
|
args: reviewArgsSchema,
|
|
15
|
-
async execute({ args, paths,
|
|
14
|
+
async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
|
|
16
15
|
const repositoryPath = resolve(paths.workspace, args.repositoryPath);
|
|
17
16
|
const implementationSummary = args.implementationSummary;
|
|
18
17
|
const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
|
|
19
|
-
const diff = await
|
|
18
|
+
const diff = await commands.run({
|
|
20
19
|
label: `review-${args.iteration}-diff`,
|
|
21
20
|
cwd: repositoryPath,
|
|
22
21
|
command: "git status --short && git diff --stat HEAD -- . && git diff HEAD -- .",
|
|
23
22
|
});
|
|
24
23
|
await ensureCommandSucceeded(diff);
|
|
25
|
-
const diffOutput = await
|
|
24
|
+
const diffOutput = await logs.read(diff.stdoutLog);
|
|
26
25
|
await mkdir(join(paths.workspace, "review"), { recursive: true });
|
|
27
26
|
await writeFile(join(paths.workspace, `review/iteration-${args.iteration}-diff.txt`), diffOutput);
|
|
28
|
-
const review = await
|
|
27
|
+
const review = await agents.prompt({
|
|
29
28
|
label: `review-${args.iteration}`,
|
|
30
29
|
cwd: repositoryPath,
|
|
31
30
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
@@ -7,9 +7,8 @@ import { reviewRouterArgsSchema } from "./schema.ts";
|
|
|
7
7
|
import type { StoredReview } from "../review/schema.ts";
|
|
8
8
|
|
|
9
9
|
export const reviewRouterWorkflow = developmentLoopScope.workflow({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
instructions: "Route implementation and review iterations based on the latest review decision.",
|
|
10
|
+
name: "reviewRouter",
|
|
11
|
+
entrypoint: false,
|
|
13
12
|
gate: {
|
|
14
13
|
enabled: true,
|
|
15
14
|
describe: ({ args }) => `Review iteration ${args.iteration}. Confirm or edit the automated decision before continuing. Plan: ${args.planPath}.`,
|
package/assets/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.1.0-tip.
|
|
1
|
+
{"version":"0.1.0-tip.35570530940.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.35570530940.1",
|
|
6
|
+
"commit": "48ecaf47ae0e82e17970db9465497a4e1ad5ff10",
|
|
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;
|
|
@@ -105,7 +105,7 @@ export class NornWorkflowRegistry {
|
|
|
105
105
|
].map(({ configuration, scopeId }) => ({ key: configuration.key, scopeId, configSchema: configuration.schema ? inspectSchema(configuration.schema) : null, config: configuration.value }));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
launchableEntries(): NornRegisteredWorkflow[] { return this.sortedEntries().filter(({ workflow }) => workflow.
|
|
108
|
+
launchableEntries(): NornRegisteredWorkflow[] { return this.sortedEntries().filter(({ workflow }) => workflow.entrypoint !== false); }
|
|
109
109
|
workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined { return this.entries.get(workflowId)?.workflow; }
|
|
110
110
|
|
|
111
111
|
async describeGate(input: NornWorkflowExecutionInput): Promise<string> {
|
|
@@ -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
|
|
|
@@ -186,7 +189,9 @@ function assertGateWorkflow(workflow: NornAnyWorkflowDeclaration): void {
|
|
|
186
189
|
|
|
187
190
|
function workflowInfo(entry: NornRegisteredWorkflow): NornRegisteredWorkflowInfo {
|
|
188
191
|
return {
|
|
189
|
-
id: entry.workflow.id,
|
|
192
|
+
id: entry.workflow.id,
|
|
193
|
+
instructions: entry.workflow.entrypoint === false ? undefined : entry.workflow.entrypoint.instructions,
|
|
194
|
+
isEntrypoint: entry.workflow.entrypoint !== false,
|
|
190
195
|
source: entry.source,
|
|
191
196
|
configKey: entry.workflow.id,
|
|
192
197
|
scope: entry.scopeId ? { id: entry.scopeId } : undefined,
|
|
@@ -32,8 +32,7 @@ export type NornWorkflowDeclaration<
|
|
|
32
32
|
(args: StaticEncode<ArgsSchema>): NornRunNext;
|
|
33
33
|
readonly kind: typeof WORKFLOW_DECLARATION_KIND;
|
|
34
34
|
readonly id: Id;
|
|
35
|
-
readonly
|
|
36
|
-
readonly instructions?: string;
|
|
35
|
+
readonly entrypoint: NornWorkflowEntrypoint;
|
|
37
36
|
readonly args: ArgsSchema;
|
|
38
37
|
readonly gate?: NornWorkflowGate<ArgsSchema> & {
|
|
39
38
|
describe?(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<string>;
|
|
@@ -65,9 +64,7 @@ export type NornWorkflowGate<ArgsSchema extends TSchema> = unknown extends Stati
|
|
|
65
64
|
readonly fields?: never;
|
|
66
65
|
};
|
|
67
66
|
|
|
68
|
-
type
|
|
69
|
-
| { readonly isEntrypoint: true; readonly instructions: string }
|
|
70
|
-
| { readonly isEntrypoint: false; readonly instructions?: string };
|
|
67
|
+
export type NornWorkflowEntrypoint = false | { readonly instructions: string };
|
|
71
68
|
|
|
72
69
|
type WorkflowConfig<Schema extends TSchema | undefined> = Schema extends TSchema ? StaticDecode<Schema> : undefined;
|
|
73
70
|
|
|
@@ -79,73 +76,77 @@ export type NornWorkflowContext<
|
|
|
79
76
|
readonly args: StaticDecode<ArgsSchema>;
|
|
80
77
|
readonly config: WorkflowConfig<ConfigSchema>;
|
|
81
78
|
readonly paths: NornWorkflowPaths;
|
|
79
|
+
readonly agents: NornAgents;
|
|
80
|
+
readonly commands: NornCommands;
|
|
81
|
+
readonly logs: NornLogs;
|
|
82
82
|
readonly run: NornRun;
|
|
83
83
|
} & (Scope extends NornWorkflowScopeInfo ? {
|
|
84
84
|
readonly scope: { readonly id: Scope["id"]; readonly config: WorkflowConfig<Scope["config"]> };
|
|
85
85
|
} : {});
|
|
86
86
|
|
|
87
87
|
export type NornWorkflowDefinition<
|
|
88
|
-
|
|
88
|
+
Name extends string = string,
|
|
89
89
|
ArgsSchema extends TSchema = TSchema,
|
|
90
90
|
ConfigSchema extends TSchema | undefined = undefined,
|
|
91
91
|
Scope extends NornWorkflowScopeInfo | undefined = undefined,
|
|
92
92
|
> = {
|
|
93
|
-
readonly
|
|
93
|
+
readonly name: Name;
|
|
94
|
+
readonly entrypoint: NornWorkflowEntrypoint;
|
|
94
95
|
readonly args: ArgsSchema;
|
|
95
96
|
readonly config?: ConfigSchema;
|
|
96
97
|
readonly gate?: NornWorkflowGate<ArgsSchema> & {
|
|
97
98
|
describe?(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<string>;
|
|
98
99
|
};
|
|
99
100
|
execute(context: NornWorkflowContext<ArgsSchema, ConfigSchema, Scope>): MaybePromise<WorkflowResult>;
|
|
100
|
-
}
|
|
101
|
+
};
|
|
101
102
|
|
|
102
103
|
export type NornWorkflowScopeInfo = {
|
|
103
104
|
readonly id: string;
|
|
104
105
|
readonly config?: TSchema;
|
|
105
106
|
};
|
|
106
107
|
|
|
107
|
-
export type NornWorkflowScope<
|
|
108
|
-
readonly id:
|
|
108
|
+
export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
|
|
109
|
+
readonly id: Name;
|
|
109
110
|
readonly config: ConfigSchema;
|
|
110
|
-
workflow<const
|
|
111
|
-
definition: NornWorkflowDefinition<
|
|
112
|
-
): NornWorkflowDeclaration<`${
|
|
111
|
+
workflow<const LocalName extends string, ArgsSchema extends TSchema, LocalConfig extends TSchema | undefined = undefined>(
|
|
112
|
+
definition: NornWorkflowDefinition<LocalName, ArgsSchema, LocalConfig, { readonly id: Name; readonly config: ConfigSchema }>,
|
|
113
|
+
): NornWorkflowDeclaration<`${Name}.${LocalName}`, ArgsSchema, LocalConfig, { readonly id: Name; readonly config: ConfigSchema }>;
|
|
113
114
|
};
|
|
114
115
|
|
|
115
116
|
export function workflow<
|
|
116
|
-
const
|
|
117
|
+
const Name extends string,
|
|
117
118
|
ArgsSchema extends TSchema,
|
|
118
119
|
ConfigSchema extends TSchema | undefined = undefined,
|
|
119
|
-
>(definition: NornWorkflowDefinition<
|
|
120
|
-
return createWorkflow({ definition, scope: undefined });
|
|
120
|
+
>(definition: NornWorkflowDefinition<Name, ArgsSchema, ConfigSchema>): NornWorkflowDeclaration<Name, ArgsSchema, ConfigSchema, undefined> {
|
|
121
|
+
return createWorkflow({ definition, id: definition.name, scope: undefined });
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function createWorkflow<Id extends string, ArgsSchema extends TSchema, ConfigSchema extends TSchema | undefined, Scope extends NornWorkflowScopeInfo | undefined>(
|
|
124
|
-
input: { readonly definition: NornWorkflowDefinition<
|
|
125
|
+
input: { readonly definition: NornWorkflowDefinition<string, ArgsSchema, ConfigSchema, Scope>; readonly id: Id; readonly scope: Scope },
|
|
125
126
|
): 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,
|
|
127
|
+
const { definition: { name, ...properties }, id, scope } = input;
|
|
128
|
+
assertDeclarationName(name);
|
|
129
|
+
const callable = (args: StaticEncode<ArgsSchema>): NornRunNext => createWorkflowTransition({ workflowId: id, args });
|
|
130
|
+
const result = Object.assign(callable, properties, {
|
|
130
131
|
kind: WORKFLOW_DECLARATION_KIND,
|
|
132
|
+
id,
|
|
131
133
|
scope,
|
|
132
134
|
}) as NornWorkflowDeclaration<Id, ArgsSchema, ConfigSchema, Scope>;
|
|
133
|
-
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${definition.id}`);
|
|
134
135
|
assertWorkflowMetadata(result);
|
|
136
|
+
if (!isWorkflowDeclaration(result)) throw new Error(`Invalid workflow definition: ${id}`);
|
|
135
137
|
return result;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
export function workflowScope<const
|
|
139
|
-
input: { readonly
|
|
140
|
-
): NornWorkflowScope<
|
|
141
|
-
|
|
142
|
-
const scope = { id: input.
|
|
140
|
+
export function workflowScope<const Name extends string, ConfigSchema extends TSchema | undefined = undefined>(
|
|
141
|
+
input: { readonly name: Name; readonly config?: ConfigSchema },
|
|
142
|
+
): NornWorkflowScope<Name, ConfigSchema> {
|
|
143
|
+
assertDeclarationName(input.name);
|
|
144
|
+
const scope = { id: input.name, config: input.config as ConfigSchema };
|
|
143
145
|
return {
|
|
144
|
-
id:
|
|
145
|
-
config:
|
|
146
|
+
id: scope.id,
|
|
147
|
+
config: scope.config,
|
|
146
148
|
workflow(definition) {
|
|
147
|
-
|
|
148
|
-
return createWorkflow({ definition: { ...definition, id: `${input.id}.${definition.id}` }, scope });
|
|
149
|
+
return createWorkflow({ definition, id: `${scope.id}.${definition.name}`, scope });
|
|
149
150
|
},
|
|
150
151
|
};
|
|
151
152
|
}
|
|
@@ -467,16 +468,19 @@ export type NornRun = {
|
|
|
467
468
|
next(workflowId: string, args: unknown): NornRunNext;
|
|
468
469
|
complete(metadata?: NornRunOutcomeMetadata): NornRunComplete;
|
|
469
470
|
fail(metadata: NornRunOutcomeMetadata & { readonly summary: string }): NornRunFail;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
export type NornLogs = {
|
|
474
|
+
read(log: NornLogRef): Promise<string>;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export type NornCommands = {
|
|
478
|
+
run(input: NornCommandRunInput): Promise<NornCommandRunResult>;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export type NornAgents = {
|
|
482
|
+
createSession(input: NornAgentCreateSessionInput): Promise<NornAgentSession>;
|
|
483
|
+
prompt<ResponseSchema extends TSchema>(input: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
|
|
480
484
|
};
|
|
481
485
|
|
|
482
486
|
export type NornWorkflowSource = {
|
|
@@ -552,19 +556,18 @@ export type NornProjectInfo = {
|
|
|
552
556
|
readonly modules: readonly NornWorkflowSource[];
|
|
553
557
|
};
|
|
554
558
|
|
|
555
|
-
function
|
|
556
|
-
if (typeof
|
|
559
|
+
function assertDeclarationName(name: string): void {
|
|
560
|
+
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
561
|
}
|
|
558
562
|
|
|
559
563
|
export function isWorkflowDeclaration(value: unknown): value is NornAnyWorkflowDeclaration {
|
|
560
564
|
if (typeof value !== "function") return false;
|
|
561
|
-
const candidate = value as { kind?: unknown; id?: unknown;
|
|
565
|
+
const candidate = value as { kind?: unknown; id?: unknown; entrypoint?: unknown; args?: unknown; execute?: unknown };
|
|
562
566
|
return (
|
|
563
567
|
candidate.kind === WORKFLOW_DECLARATION_KIND &&
|
|
564
568
|
typeof candidate.id === "string" &&
|
|
565
569
|
candidate.id.length > 0 &&
|
|
566
|
-
(candidate.
|
|
567
|
-
typeof candidate.isEntrypoint === "boolean" &&
|
|
570
|
+
(candidate.entrypoint === false || (isPlainObject(candidate.entrypoint) && typeof candidate.entrypoint.instructions === "string")) &&
|
|
568
571
|
Boolean(candidate.args) &&
|
|
569
572
|
typeof candidate.execute === "function"
|
|
570
573
|
);
|
|
@@ -43,11 +43,11 @@ function assertWorkflowReferenceAnnotations(schema: unknown): void {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function assertWorkflowMetadata(workflow: NornAnyWorkflowDeclaration): void {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (
|
|
50
|
-
throw new Error(`
|
|
46
|
+
const { entrypoint } = workflow;
|
|
47
|
+
if (entrypoint === false) return;
|
|
48
|
+
if (!isPlainObject(entrypoint)) throw new Error(`Workflow entrypoint must be false or an object with instructions: ${workflow.id}`);
|
|
49
|
+
if (typeof entrypoint.instructions !== "string" || entrypoint.instructions.trim().length === 0) {
|
|
50
|
+
throw new Error(`Workflow entrypoint.instructions must be a nonempty string: ${workflow.id}`);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -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", entrypoint: 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.35570530940.1";
|
|
4
|
+
readonly commit: "48ecaf47ae0e82e17970db9465497a4e1ad5ff10";
|
|
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.35570530940.1",
|
|
5
|
+
"commit": "48ecaf47ae0e82e17970db9465497a4e1ad5ff10",
|
|
6
6
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
7
7
|
"upgrade": {
|
|
8
8
|
"supported": false,
|