@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.
Files changed (38) hide show
  1. package/assets/README.md +33 -16
  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 +18 -11
  6. package/assets/examples/agent-then-analysis/plugin.ts +7 -7
  7. package/assets/examples/caller-selected-continuation/caller.ts +3 -3
  8. package/assets/examples/caller-selected-continuation/producer.ts +2 -2
  9. package/assets/examples/coordinating-multiple-agents/README.md +1 -1
  10. package/assets/examples/coordinating-multiple-agents/plugin.ts +9 -9
  11. package/assets/examples/getting-started/README.md +24 -9
  12. package/assets/examples/getting-started/plugin.ts +27 -12
  13. package/assets/examples/minimal-workflow/README.md +2 -2
  14. package/assets/examples/minimal-workflow/plugin.ts +1 -1
  15. package/assets/examples/shared-state/plugin.ts +5 -5
  16. package/assets/examples/worktree-development-loop/scope.ts +1 -1
  17. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +3 -3
  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 +5 -5
  20. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +3 -3
  21. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +5 -5
  22. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +1 -1
  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 +7 -4
  28. package/assets/packages/sdk/src/api.ts +43 -37
  29. package/assets/tests/workflow-ref.test.ts +4 -3
  30. package/dist/generated-build-info.d.ts +2 -2
  31. package/dist/generated-build-info.js +2 -2
  32. package/dist/internal/engine.d.ts +1 -1
  33. package/dist/internal/engine.js +13 -13
  34. package/dist/internal/{run.d.ts → execution-context.d.ts} +8 -13
  35. package/dist/internal/{run.js → execution-context.js} +11 -15
  36. package/dist/internal/workflow-registry.d.ts +2 -2
  37. package/dist/internal/workflow-registry.js +5 -2
  38. 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
- id: "planning",
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, run }): Promise<WorkflowResult> {
14
+ async execute({ args, paths, agents }): Promise<WorkflowResult> {
15
15
  const repositoryPath = resolve(paths.workspace, args.repositoryPath);
16
- const planning = await run.agents.prompt({
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
- id: "review",
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, run }): Promise<WorkflowResult> {
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 run.commands.run({
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 run.logs.read(diff.stdoutLog);
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 run.agents.prompt({
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
- id: "reviewRouter",
10
+ name: "reviewRouter",
11
11
  isEntrypoint: false,
12
12
  instructions: "Route implementation and review iterations based on the latest review decision.",
13
13
  gate: {
@@ -1 +1 @@
1
- {"version":"0.1.0-tip.35436871363.1"}
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.35436871363.1",
6
- "commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
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 { 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;
@@ -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
 
@@ -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
- Id extends string = string,
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 id: Id;
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<Id extends string, ConfigSchema extends TSchema | undefined> = {
108
- readonly id: Id;
110
+ export type NornWorkflowScope<Name extends string, ConfigSchema extends TSchema | undefined> = {
111
+ readonly id: Name;
109
112
  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 }>;
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 Id extends string,
119
+ const Name extends string,
117
120
  ArgsSchema extends TSchema,
118
121
  ConfigSchema extends TSchema | undefined = undefined,
119
- >(definition: NornWorkflowDefinition<Id, ArgsSchema, ConfigSchema>): NornWorkflowDeclaration<Id, ArgsSchema, ConfigSchema, undefined> {
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<Id, ArgsSchema, ConfigSchema, Scope>; readonly scope: Scope },
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
- assertDeclarationId(definition.id);
128
- const callable = (args: StaticEncode<ArgsSchema>): NornRunNext => createWorkflowTransition({ workflowId: definition.id, args });
129
- const result = Object.assign(callable, definition, {
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: ${definition.id}`);
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 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 };
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: input.id,
145
- config: input.config as ConfigSchema,
148
+ id: scope.id,
149
+ config: scope.config,
146
150
  workflow(definition) {
147
- assertDeclarationId(definition.id);
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
- 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
- };
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 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}`);
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 { 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", 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.35436871363.1";
4
- readonly commit: "49b14d0ed0c9633fc6ee530a557ec7babbf52b22";
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.35436871363.1",
5
- "commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
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,
@@ -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;
@@ -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 { NornRunContext } from "./run.js";
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, run: session.run, paths: session.paths, args: currentStep.args, configOverride: session.state.currentState().configOverride });
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, run: session.run, paths: session.paths, args: step.args, configOverride: session.state.currentState().configOverride });
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 run = await this.buildRun({ id, currentRoot, paths, signal: activeRun.controller.signal, logger });
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, run, paths, state, lease, runStore, logger, activeRun };
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 run = await this.buildRun({ id: currentState.id, currentRoot, paths, signal: activeRun.controller.signal, logger });
265
- return { runRoot, cwd: process.cwd(), run, paths, state, lease, runStore, logger, activeRun };
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 buildRun(input) {
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 NornRunContext({
276
+ return new NornExecutionContext({
277
277
  id: input.id,
278
278
  runRoot: input.currentRoot,
279
279
  signal: input.signal,