@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.
Files changed (39) hide show
  1. package/assets/README.md +35 -17
  2. package/assets/docs/agents.md +4 -4
  3. package/assets/docs/cli.md +3 -3
  4. package/assets/docs/recovery.md +1 -1
  5. package/assets/docs/workflows.md +22 -16
  6. package/assets/examples/agent-then-analysis/plugin.ts +9 -10
  7. package/assets/examples/caller-selected-continuation/caller.ts +5 -5
  8. package/assets/examples/caller-selected-continuation/producer.ts +3 -4
  9. package/assets/examples/coordinating-multiple-agents/README.md +1 -1
  10. package/assets/examples/coordinating-multiple-agents/plugin.ts +12 -13
  11. package/assets/examples/getting-started/README.md +24 -9
  12. package/assets/examples/getting-started/plugin.ts +27 -13
  13. package/assets/examples/minimal-workflow/README.md +2 -2
  14. package/assets/examples/minimal-workflow/plugin.ts +2 -3
  15. package/assets/examples/shared-state/plugin.ts +6 -7
  16. package/assets/examples/worktree-development-loop/scope.ts +1 -1
  17. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +4 -5
  18. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +3 -3
  19. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +6 -7
  20. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +4 -5
  21. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +6 -7
  22. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +2 -3
  23. package/assets/package.json +1 -1
  24. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  25. package/assets/packages/cli/src/internal/engine.ts +15 -15
  26. package/assets/packages/cli/src/internal/{run.ts → execution-context.ts} +23 -31
  27. package/assets/packages/cli/src/internal/workflow-registry.ts +11 -6
  28. package/assets/packages/sdk/src/api.ts +49 -46
  29. package/assets/packages/sdk/src/schema.ts +5 -5
  30. package/assets/tests/workflow-ref.test.ts +4 -3
  31. package/dist/generated-build-info.d.ts +2 -2
  32. package/dist/generated-build-info.js +2 -2
  33. package/dist/internal/engine.d.ts +1 -1
  34. package/dist/internal/engine.js +13 -13
  35. package/dist/internal/{run.d.ts → execution-context.d.ts} +8 -13
  36. package/dist/internal/{run.js → execution-context.js} +11 -15
  37. package/dist/internal/workflow-registry.d.ts +2 -2
  38. package/dist/internal/workflow-registry.js +8 -5
  39. 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
- id: "implementation",
12
- isEntrypoint: false,
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, run }): Promise<WorkflowResult> {
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 run.agents.prompt({
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 run.commands.run({
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 run.logs.read(status.stdoutLog);
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
- id: "planning",
11
- isEntrypoint: false,
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, run }): Promise<WorkflowResult> {
13
+ async execute({ args, paths, agents }): Promise<WorkflowResult> {
15
14
  const repositoryPath = resolve(paths.workspace, args.repositoryPath);
16
- const planning = await run.agents.prompt({
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
- id: "review",
12
- isEntrypoint: false,
13
- instructions: "Review the current repository boundary changes.",
11
+ name: "review",
12
+ entrypoint: false,
14
13
  args: reviewArgsSchema,
15
- async execute({ args, paths, run }): Promise<WorkflowResult> {
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 run.commands.run({
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 run.logs.read(diff.stdoutLog);
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 run.agents.prompt({
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
- id: "reviewRouter",
11
- isEntrypoint: false,
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}.`,
@@ -1 +1 @@
1
- {"version":"0.1.0-tip.35436871363.1"}
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.35436871363.1",
6
- "commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
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 { NornRunContext } from "./run.ts";
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 run: NornRunContext;
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, run: session.run, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
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, run: session.run, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
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 run = await this.buildRun({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
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, run, paths, state, lease, runStore, logger, activeRun };
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 run = await this.buildRun({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
328
- return { runRoot, cwd: process.cwd(), run, paths, state, lease, runStore, logger, activeRun };
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 buildRun(input: {
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<NornRunContext> {
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 NornRunContext({
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 { NornRunOutcomeMetadata, NornRun } from "@vimhead.dev/norn";
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 DefaultNornRunInput = {
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 NornRunContext implements NornRun {
23
- readonly logs: NornRun["logs"];
24
- readonly commands: NornRun["commands"];
25
- readonly agents: NornRun["agents"];
26
- readonly id: string;
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: DefaultNornRunInput) {
29
- this.id = input.id;
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: (log) => this.input.logs.read(log),
36
+ read: log => this.input.logs.read(log),
32
37
  };
33
38
  this.commands = {
34
- run: (commandInput) =>
35
- new NornCommandRunner({
36
- signal: this.input.signal,
37
- logs: this.input.logs,
38
- logger: this.input.logger,
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: (agentInput) => this.createAgentRunner().createSession(agentInput),
43
- prompt: (agentInput) => this.createAgentRunner().prompt(agentInput),
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 NornRun, type NornRunNext, type NornWorkflowPaths, type NornWorkflowContext, type NornWorkflowGateInfo, type NornWorkflowSource, type NornWorkflowScopeInfo, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
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 run: NornRun;
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.isEntrypoint); }
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, run, paths, configOverride } = input;
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, instructions: entry.workflow.instructions, isEntrypoint: entry.workflow.isEntrypoint,
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 isEntrypoint: boolean;
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 NornWorkflowInstructions =
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
- Id extends string = string,
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 id: Id;
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
- } & NornWorkflowInstructions;
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<Id extends string, ConfigSchema extends TSchema | undefined> = {
108
- readonly id: Id;
108
+ export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
109
+ readonly id: Name;
109
110
  readonly config: ConfigSchema;
110
- workflow<const LocalId extends string, ArgsSchema extends TSchema, LocalConfig extends TSchema | undefined = undefined>(
111
- definition: NornWorkflowDefinition<LocalId, ArgsSchema, LocalConfig, { readonly id: Id; readonly config: ConfigSchema }>,
112
- ): NornWorkflowDeclaration<`${Id}.${LocalId}`, ArgsSchema, LocalConfig, { readonly id: Id; readonly config: ConfigSchema }>;
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 Id extends string,
117
+ const Name extends string,
117
118
  ArgsSchema extends TSchema,
118
119
  ConfigSchema extends TSchema | undefined = undefined,
119
- >(definition: NornWorkflowDefinition<Id, ArgsSchema, ConfigSchema>): NornWorkflowDeclaration<Id, ArgsSchema, ConfigSchema, undefined> {
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<Id, ArgsSchema, ConfigSchema, Scope>; readonly scope: Scope },
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
- assertDeclarationId(definition.id);
128
- const callable = (args: StaticEncode<ArgsSchema>): NornRunNext => createWorkflowTransition({ workflowId: definition.id, args });
129
- const result = Object.assign(callable, definition, {
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 Id extends string, ConfigSchema extends TSchema | undefined = undefined>(
139
- input: { readonly id: Id; readonly config?: ConfigSchema },
140
- ): NornWorkflowScope<Id, ConfigSchema> {
141
- assertDeclarationId(input.id);
142
- const scope = { id: input.id, config: input.config as ConfigSchema };
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: input.id,
145
- config: input.config as ConfigSchema,
146
+ id: scope.id,
147
+ config: scope.config,
146
148
  workflow(definition) {
147
- assertDeclarationId(definition.id);
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
- logs: {
471
- read(log: NornLogRef): Promise<string>;
472
- };
473
- commands: {
474
- run(input: NornCommandRunInput): Promise<NornCommandRunResult>;
475
- };
476
- agents: {
477
- createSession(input: NornAgentCreateSessionInput): Promise<NornAgentSession>;
478
- prompt<ResponseSchema extends TSchema>(input: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>>;
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 assertDeclarationId(id: string): void {
556
- if (typeof id !== "string" || id.trim().length === 0 || id.split(".").some(segment => segment.length === 0)) throw new Error(`Invalid workflow or scope id: ${id}`);
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; instructions?: unknown; isEntrypoint?: unknown; args?: unknown; execute?: 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.instructions === undefined || typeof candidate.instructions === "string") &&
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
- if (workflow.instructions !== undefined && (typeof workflow.instructions !== "string" || workflow.instructions.trim().length === 0)) {
47
- throw new Error(`Workflow instructions must be a nonempty string: ${workflow.id}`);
48
- }
49
- if (workflow.isEntrypoint && workflow.instructions === undefined) {
50
- throw new Error(`Entrypoint workflow requires instructions: ${workflow.id}`);
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 { workflow, isWorkflowDeclaration, workflowRefSchema, type NornWorkflowRefSchemaOptions } from "@vimhead.dev/norn";
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 target = workflow({
9
- id: "refs.finish", isEntrypoint: false, args: Type.Object({ report: Type.String() }),
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.35436871363.1";
4
- readonly commit: "49b14d0ed0c9633fc6ee530a557ec7babbf52b22";
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.35436871363.1",
5
- "commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
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,
@@ -33,7 +33,7 @@ export declare class NornEngine {
33
33
  private nextWorkflowStep;
34
34
  private createRunSession;
35
35
  private openRunSession;
36
- private buildRun;
36
+ private buildExecutionContext;
37
37
  private startActiveRun;
38
38
  private finishActiveRun;
39
39
  private failActiveRun;