@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.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 (146) hide show
  1. package/assets/README.md +22 -27
  2. package/assets/docs/README.md +5 -5
  3. package/assets/docs/agents.md +48 -9
  4. package/assets/docs/cli.md +7 -7
  5. package/assets/docs/composition.md +25 -25
  6. package/assets/docs/persistence.md +34 -23
  7. package/assets/docs/projects.md +30 -18
  8. package/assets/docs/providers.md +3 -3
  9. package/assets/docs/recovery.md +13 -9
  10. package/assets/docs/schemas.md +1 -1
  11. package/assets/docs/workflows.md +105 -12
  12. package/assets/examples/agent-then-analysis/README.md +10 -10
  13. package/assets/examples/agent-then-analysis/input.json +1 -1
  14. package/assets/examples/agent-then-analysis/norn.project.json +1 -1
  15. package/assets/examples/agent-then-analysis/plugin.ts +57 -68
  16. package/assets/examples/caller-selected-continuation/README.md +15 -14
  17. package/assets/examples/caller-selected-continuation/caller.ts +37 -38
  18. package/assets/examples/caller-selected-continuation/input.json +2 -2
  19. package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
  20. package/assets/examples/caller-selected-continuation/producer.ts +19 -24
  21. package/assets/examples/coordinating-multiple-agents/README.md +27 -17
  22. package/assets/examples/coordinating-multiple-agents/input.json +1 -1
  23. package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
  24. package/assets/examples/coordinating-multiple-agents/plugin.ts +79 -60
  25. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
  26. package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
  27. package/assets/examples/getting-started/README.md +2 -2
  28. package/assets/examples/getting-started/norn.project.json +1 -1
  29. package/assets/examples/getting-started/plugin.ts +20 -26
  30. package/assets/examples/minimal-workflow/README.md +13 -12
  31. package/assets/examples/minimal-workflow/norn.project.json +1 -1
  32. package/assets/examples/minimal-workflow/plugin.ts +14 -25
  33. package/assets/examples/shared-state/README.md +12 -4
  34. package/assets/examples/shared-state/input.json +1 -1
  35. package/assets/examples/shared-state/norn.project.json +1 -1
  36. package/assets/examples/shared-state/plugin.ts +49 -41
  37. package/assets/examples/shared-state/shared-state.ts +56 -0
  38. package/assets/examples/shared-state/state-tools.ts +68 -0
  39. package/assets/examples/worktree-development-loop/README.md +18 -17
  40. package/assets/examples/worktree-development-loop/norn.project.json +1 -1
  41. package/assets/examples/worktree-development-loop/plugin.ts +6 -26
  42. package/assets/examples/worktree-development-loop/scope.ts +4 -0
  43. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
  44. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
  45. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
  47. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +35 -33
  48. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
  49. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +6 -3
  50. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +27 -16
  51. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
  52. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
  53. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +43 -31
  54. package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
  55. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +6 -6
  56. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +50 -44
  57. package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
  58. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
  59. package/assets/package.json +1 -1
  60. package/assets/packages/cli/src/cli.ts +44 -37
  61. package/assets/packages/cli/src/client.ts +9 -16
  62. package/assets/packages/cli/src/documentation-intro.ts +1 -1
  63. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
  65. package/assets/packages/cli/src/internal/agents.ts +24 -32
  66. package/assets/packages/cli/src/internal/commands.ts +2 -14
  67. package/assets/packages/cli/src/internal/engine.ts +63 -101
  68. package/assets/packages/cli/src/internal/errors.ts +4 -4
  69. package/assets/packages/cli/src/internal/launch-request.ts +6 -6
  70. package/assets/packages/cli/src/internal/logs.ts +1 -1
  71. package/assets/packages/cli/src/internal/run-log.ts +1 -1
  72. package/assets/packages/cli/src/internal/run-state.ts +38 -23
  73. package/assets/packages/cli/src/internal/run.ts +4 -62
  74. package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
  75. package/assets/packages/cli/src/internal/workflow-registry.ts +126 -111
  76. package/assets/packages/cli/src/internal/working-directory.ts +6 -0
  77. package/assets/packages/cli/src/workflow-loader.ts +213 -0
  78. package/assets/packages/core/src/workflow-transition.ts +2 -2
  79. package/assets/packages/sdk/src/api.ts +141 -329
  80. package/assets/packages/sdk/src/index.ts +1 -4
  81. package/assets/packages/sdk/src/schema.ts +3 -3
  82. package/assets/tests/workflow-ref.test.ts +53 -55
  83. package/dist/cli.js +41 -35
  84. package/dist/client.d.ts +3 -4
  85. package/dist/client.js +3 -8
  86. package/dist/documentation-intro.js +1 -1
  87. package/dist/generated-build-info.d.ts +2 -2
  88. package/dist/generated-build-info.js +2 -2
  89. package/dist/internal/agent-response-tool.js +3 -3
  90. package/dist/internal/agents.d.ts +0 -4
  91. package/dist/internal/agents.js +30 -46
  92. package/dist/internal/commands.d.ts +0 -4
  93. package/dist/internal/commands.js +2 -10
  94. package/dist/internal/engine.d.ts +4 -8
  95. package/dist/internal/engine.js +57 -84
  96. package/dist/internal/errors.d.ts +3 -3
  97. package/dist/internal/errors.js +1 -1
  98. package/dist/internal/file-coordinator.d.ts +17 -0
  99. package/dist/internal/file-coordinator.js +162 -0
  100. package/dist/internal/launch-request.d.ts +4 -4
  101. package/dist/internal/launch-request.js +2 -2
  102. package/dist/internal/logs.d.ts +1 -1
  103. package/dist/internal/run-log.d.ts +1 -1
  104. package/dist/internal/run-state.d.ts +11 -8
  105. package/dist/internal/run-state.js +34 -22
  106. package/dist/internal/run.d.ts +3 -23
  107. package/dist/internal/run.js +4 -50
  108. package/dist/internal/worker-directory.d.ts +1 -0
  109. package/dist/internal/worker-directory.js +26 -0
  110. package/dist/internal/workflow-registry.d.ts +35 -17
  111. package/dist/internal/workflow-registry.js +110 -73
  112. package/dist/internal/working-directory.d.ts +1 -0
  113. package/dist/internal/working-directory.js +9 -0
  114. package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
  115. package/dist/workflow-loader.js +234 -0
  116. package/package.json +2 -2
  117. package/assets/docs/resources.md +0 -61
  118. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
  119. package/assets/examples/worktree-development-loop/manifest.ts +0 -26
  120. package/assets/examples/worktree-development-loop/state.ts +0 -23
  121. package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
  122. package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
  123. package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
  124. package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
  125. package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
  126. package/assets/packages/cli/src/internal/artifacts.ts +0 -26
  127. package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
  128. package/assets/packages/cli/src/internal/run-resources.ts +0 -23
  129. package/assets/packages/cli/src/internal/state-store.ts +0 -83
  130. package/assets/packages/cli/src/plugin-loader.ts +0 -400
  131. package/assets/packages/cli/src/resources.ts +0 -69
  132. package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
  133. package/assets/packages/sdk/src/resources.ts +0 -20
  134. package/assets/packages/sdk/src/state-adapter.ts +0 -75
  135. package/dist/internal/artifacts.d.ts +0 -10
  136. package/dist/internal/artifacts.js +0 -29
  137. package/dist/internal/resource-bindings.d.ts +0 -13
  138. package/dist/internal/resource-bindings.js +0 -34
  139. package/dist/internal/run-resources.d.ts +0 -6
  140. package/dist/internal/run-resources.js +0 -26
  141. package/dist/internal/state-store.d.ts +0 -23
  142. package/dist/internal/state-store.js +0 -103
  143. package/dist/plugin-loader.js +0 -341
  144. package/dist/resources.d.ts +0 -11
  145. package/dist/resources.js +0 -100
  146. /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
@@ -1,21 +1,23 @@
1
- import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type NornWorkflowImplementation, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
2
- import { type TSchema } from "typebox";
3
- export type NornRegisteredWorkflow = {
4
- workflow: NornAnyWorkflowDeclaration;
5
- implementation: NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>;
6
- configuration?: NornRegisteredConfiguration;
7
- plugin?: NornWorkflowPluginInfo;
8
- };
1
+ import { type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRun, type NornRunNext, type NornWorkflowPaths, type NornWorkflowSource, type NornProjectConfigurationInfo } from "@vimhead.dev/norn";
2
+ import type { TSchema } from "typebox";
9
3
  export type NornRegisteredConfiguration = {
10
- readonly schema: TSchema;
4
+ readonly key: string;
5
+ readonly schema: TSchema | undefined;
11
6
  readonly input: unknown;
12
7
  readonly value: unknown;
13
8
  };
14
- export declare function decodePluginConfiguration(input: {
15
- readonly pluginId: string;
9
+ export type NornRegisteredWorkflow = {
10
+ readonly workflow: NornAnyWorkflowDeclaration;
11
+ readonly configuration: NornRegisteredConfiguration;
12
+ readonly scopeId: string | undefined;
13
+ readonly source: NornWorkflowSource | undefined;
14
+ };
15
+ export declare function decodeConfiguration(input: {
16
+ readonly key: string;
16
17
  readonly schema: TSchema | undefined;
17
18
  readonly value: unknown;
18
- }): NornRegisteredConfiguration | undefined;
19
+ }): NornRegisteredConfiguration;
20
+ export declare function assertCompatibleConfigurationOwners(workflows: readonly NornAnyWorkflowDeclaration[]): void;
19
21
  export type NornWorkflowStepResult = NornRunNext | {
20
22
  readonly type: "complete";
21
23
  readonly workflow: NornAnyWorkflowDeclaration;
@@ -25,19 +27,35 @@ export type NornWorkflowStepResult = NornRunNext | {
25
27
  readonly workflow: NornAnyWorkflowDeclaration;
26
28
  readonly metadata: NornRunFail["metadata"];
27
29
  };
30
+ type NornExecutionContextInput = {
31
+ readonly run: NornRun;
32
+ readonly paths: NornWorkflowPaths;
33
+ readonly args: unknown;
34
+ readonly configOverride: unknown;
35
+ };
36
+ type NornWorkflowExecutionInput = NornExecutionContextInput & {
37
+ readonly workflow: NornAnyWorkflowDeclaration;
38
+ };
28
39
  export declare class NornWorkflowRegistry {
29
40
  private readonly entries;
30
- register<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, implementation: NornWorkflowImplementation<TWorkflow, unknown>, metadata?: {
31
- readonly plugin?: NornWorkflowPluginInfo;
32
- readonly configuration?: NornRegisteredConfiguration;
41
+ private readonly scopes;
42
+ register(input: {
43
+ readonly workflow: NornAnyWorkflowDeclaration;
44
+ readonly config: Readonly<Record<string, unknown>>;
45
+ readonly source: NornWorkflowSource | undefined;
33
46
  }): NornDispose;
34
47
  list(options?: {
35
48
  readonly entrypointsOnly?: boolean;
36
49
  }): NornRegisteredWorkflowInfo[];
37
50
  inspect(workflowId: string): NornInspectedWorkflowInfo | undefined;
51
+ configurationInfos(): NornProjectConfigurationInfo[];
38
52
  launchableEntries(): NornRegisteredWorkflow[];
39
53
  workflowById(workflowId: string): NornAnyWorkflowDeclaration | undefined;
40
- describeGate<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, params: unknown, configOverride?: unknown): Promise<string>;
41
- execute<TWorkflow extends NornAnyWorkflowDeclaration>(workflow: TWorkflow, run: NornRunFor<TWorkflow>, params: unknown, configOverride?: unknown): Promise<NornWorkflowStepResult>;
54
+ describeGate(input: NornWorkflowExecutionInput): Promise<string>;
55
+ execute(input: NornWorkflowExecutionInput): Promise<NornWorkflowStepResult>;
56
+ private createExecutionContext;
57
+ private requireEntry;
42
58
  private sortedEntries;
43
59
  }
60
+ export declare function assertWorkflowDefinition(workflow: NornAnyWorkflowDeclaration): void;
61
+ export {};
@@ -1,39 +1,79 @@
1
1
  // src/internal/workflow-registry.ts
2
- import { isWorkflowComplete, isWorkflowFail, isWorkflowNext } from "@vimhead.dev/norn";
2
+ import { isWorkflowComplete, isWorkflowDeclaration, isWorkflowFail, isWorkflowNext } from "@vimhead.dev/norn";
3
3
  import { assertWorkflowMetadata, inspectSchema, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
4
4
  import { Value } from "typebox/value";
5
- function decodePluginConfiguration(input) {
5
+ function decodeConfiguration(input) {
6
6
  if (!input.schema) {
7
- if (input.value !== void 0) throw new Error(`Norn config provided for plugin without config schema: ${input.pluginId}`);
8
- return void 0;
7
+ if (input.value !== void 0) throw new Error(`Norn config provided without config schema: ${input.key}`);
8
+ return { key: input.key, schema: void 0, input: void 0, value: void 0 };
9
+ }
10
+ const value = input.value === void 0 && schemaType(unwrapSchema(input.schema)) === "object" ? {} : input.value;
11
+ return { key: input.key, schema: input.schema, input: value, value: Value.Decode(input.schema, value) };
12
+ }
13
+ function assertCompatibleConfigurationOwners(workflows) {
14
+ const owners = /* @__PURE__ */ new Map();
15
+ for (const workflow of workflows) {
16
+ for (const owner of [
17
+ { key: workflow.id, kind: "workflow", definition: workflow },
18
+ ...workflow.scope ? [{ key: workflow.scope.id, kind: "scope", definition: workflow.scope }] : []
19
+ ]) {
20
+ const previous = owners.get(owner.key);
21
+ if (previous && previous.kind !== owner.kind) throw new Error(`Ambiguous configuration key: ${owner.key}`);
22
+ if (previous && owner.kind === "scope" && previous.definition !== owner.definition) throw new Error(`Duplicate workflow scope id: ${owner.key}; import one shared scope definition`);
23
+ owners.set(owner.key, owner);
24
+ }
9
25
  }
10
- const value = defaultConfigInput(input.schema, input.value);
11
- return { schema: input.schema, input: value, value: Value.Decode(input.schema, value) };
12
26
  }
13
27
  var NornWorkflowRegistry = class {
14
28
  entries = /* @__PURE__ */ new Map();
15
- register(workflow, implementation, metadata = {}) {
29
+ scopes = /* @__PURE__ */ new Map();
30
+ register(input) {
31
+ const { workflow, config, source } = input;
32
+ assertWorkflowDefinition(workflow);
16
33
  if (this.entries.has(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
34
+ assertCompatibleConfigurationOwners([...this.sortedEntries().map((entry2) => entry2.workflow), workflow]);
17
35
  const entry = {
18
36
  workflow,
19
- implementation,
20
- configuration: metadata.configuration,
21
- plugin: metadata.plugin
37
+ source,
38
+ scopeId: workflow.scope?.id,
39
+ configuration: decodeConfiguration({ key: workflow.id, schema: workflow.config, value: configurationInput(config, workflow.id) })
22
40
  };
23
- assertWorkflowMetadata(workflow);
24
- assertGateWorkflow(workflow);
41
+ if (workflow.scope && !this.scopes.has(workflow.scope.id)) {
42
+ this.scopes.set(workflow.scope.id, {
43
+ definition: workflow.scope,
44
+ configuration: decodeConfiguration({ key: workflow.scope.id, schema: workflow.scope.config, value: configurationInput(config, workflow.scope.id) })
45
+ });
46
+ }
25
47
  this.entries.set(workflow.id, entry);
26
48
  return () => {
27
- if (this.entries.get(workflow.id) === entry) this.entries.delete(workflow.id);
49
+ if (this.entries.get(workflow.id) !== entry) return;
50
+ this.entries.delete(workflow.id);
51
+ if (entry.scopeId && ![...this.entries.values()].some((other) => other.scopeId === entry.scopeId)) this.scopes.delete(entry.scopeId);
28
52
  };
29
53
  }
30
54
  list(options = {}) {
31
- const entries = options.entrypointsOnly ? this.launchableEntries() : this.sortedEntries();
32
- return entries.map((entry) => workflowInfo(entry));
55
+ return (options.entrypointsOnly ? this.launchableEntries() : this.sortedEntries()).map(workflowInfo);
33
56
  }
34
57
  inspect(workflowId) {
35
58
  const entry = this.entries.get(workflowId);
36
- return entry ? inspectedWorkflowInfo(entry) : void 0;
59
+ const scope = entry?.scopeId ? this.scopes.get(entry.scopeId) : void 0;
60
+ return entry ? {
61
+ ...workflowInfo(entry),
62
+ argsSchema: inspectSchema(entry.workflow.args),
63
+ configSchema: entry.configuration.schema ? inspectSchema(entry.configuration.schema) : null,
64
+ scope: scope ? {
65
+ id: scope.definition.id,
66
+ configKey: scope.configuration.key,
67
+ configSchema: scope.configuration.schema ? inspectSchema(scope.configuration.schema) : null
68
+ } : void 0,
69
+ gate: gateInfo(entry.workflow)
70
+ } : void 0;
71
+ }
72
+ configurationInfos() {
73
+ return [
74
+ ...this.sortedEntries().map((entry) => ({ configuration: entry.configuration, scopeId: null })),
75
+ ...[...this.scopes.values()].map((scope) => ({ configuration: scope.configuration, scopeId: scope.definition.id }))
76
+ ].map(({ configuration, scopeId }) => ({ key: configuration.key, scopeId, configSchema: configuration.schema ? inspectSchema(configuration.schema) : null, config: configuration.value }));
37
77
  }
38
78
  launchableEntries() {
39
79
  return this.sortedEntries().filter(({ workflow }) => workflow.isEntrypoint);
@@ -41,88 +81,83 @@ var NornWorkflowRegistry = class {
41
81
  workflowById(workflowId) {
42
82
  return this.entries.get(workflowId)?.workflow;
43
83
  }
44
- async describeGate(workflow, run, params, configOverride) {
45
- const entry = this.entries.get(workflow.id);
46
- if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
84
+ async describeGate(input) {
85
+ const { workflow } = input;
86
+ const entry = this.requireEntry(workflow.id);
47
87
  if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
48
- const parsedParams = Value.Decode(workflow.params, params);
49
- const parsedConfig = parseExecutionConfig(entry, configOverride);
50
- const description = await entry.implementation.gate?.describe(run, parsedParams, parsedConfig);
51
- return validateGateDescription(description ?? workflow.id, workflow.id);
88
+ const context = this.createExecutionContext({ ...input, entry });
89
+ const description = await entry.workflow.gate?.describe?.(context) ?? workflow.id;
90
+ if (typeof description !== "string" || description.trim().length === 0) throw new Error(`Workflow gate description must not be empty: ${workflow.id}`);
91
+ return description.trim();
52
92
  }
53
- async execute(workflow, run, params, configOverride) {
54
- const entry = this.entries.get(workflow.id);
55
- if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
56
- const parsedParams = Value.Decode(workflow.params, params);
57
- const parsedConfig = parseExecutionConfig(entry, configOverride);
58
- const result = await entry.implementation.execute(run, parsedParams, parsedConfig);
93
+ async execute(input) {
94
+ const { workflow } = input;
95
+ const entry = this.requireEntry(workflow.id);
96
+ const result = await entry.workflow.execute(this.createExecutionContext({ ...input, entry }));
59
97
  if (isWorkflowNext(result)) return result;
60
98
  if (isWorkflowComplete(result)) return { type: "complete", workflow, metadata: result.metadata };
61
99
  if (isWorkflowFail(result)) return { type: "fail", workflow, metadata: result.metadata };
62
100
  throw new Error(`Workflow returned invalid control result: ${workflow.id}`);
63
101
  }
102
+ createExecutionContext(input) {
103
+ const { entry, run, paths, configOverride } = input;
104
+ const scope = entry.scopeId ? this.scopes.get(entry.scopeId) : void 0;
105
+ return {
106
+ args: Value.Decode(entry.workflow.args, input.args),
107
+ config: parseExecutionConfig(entry.configuration, configOverride),
108
+ ...scope ? { scope: { id: scope.definition.id, config: parseExecutionConfig(scope.configuration, configOverride) } } : {},
109
+ paths,
110
+ run
111
+ };
112
+ }
113
+ requireEntry(id) {
114
+ const entry = this.entries.get(id);
115
+ if (!entry) throw new Error(`Unknown workflow: ${id}`);
116
+ return entry;
117
+ }
64
118
  sortedEntries() {
65
- return Array.from(this.entries.values()).sort((left, right) => left.workflow.id.localeCompare(right.workflow.id));
119
+ return [...this.entries.values()].sort((left, right) => left.workflow.id.localeCompare(right.workflow.id));
66
120
  }
67
121
  };
68
- function defaultConfigInput(configSchema, config) {
69
- if (config !== void 0) return config;
70
- return schemaType(unwrapSchema(configSchema)) === "object" ? {} : void 0;
71
- }
72
- function parseExecutionConfig(entry, configOverride) {
73
- const pluginConfigOverride = pluginConfigOverrideInput(entry, configOverride);
74
- if (!entry.configuration) {
75
- if (pluginConfigOverride !== void 0) throw new Error(`Run config override provided for plugin without config schema: ${entry.plugin?.id ?? entry.workflow.id}`);
76
- return void 0;
77
- }
78
- if (pluginConfigOverride === void 0) return entry.configuration.value;
79
- const rawConfig = isPlainObject(entry.configuration.input) && isPlainObject(pluginConfigOverride) ? deepMerge(entry.configuration.input, pluginConfigOverride) : pluginConfigOverride;
80
- return Value.Decode(entry.configuration.schema, rawConfig);
122
+ function configurationInput(config, key) {
123
+ return Object.hasOwn(config, key) ? config[key] : void 0;
81
124
  }
82
- function pluginConfigOverrideInput(entry, configOverride) {
83
- if (configOverride === void 0) return void 0;
84
- if (!isPlainObject(configOverride)) throw new Error("Run config override must be an object keyed by plugin id");
85
- const pluginId = entry.plugin?.id;
86
- return pluginId ? configOverride[pluginId] : void 0;
125
+ function parseExecutionConfig(configuration, overrides) {
126
+ if (overrides !== void 0 && !isPlainObject(overrides)) throw new Error("Run config override must be an object keyed by workflow or scope id");
127
+ const override = overrides ? configurationInput(overrides, configuration.key) : void 0;
128
+ if (override === void 0) return configuration.value;
129
+ if (!configuration.schema) throw new Error(`Run config override provided without config schema: ${configuration.key}`);
130
+ const input = isPlainObject(configuration.input) && isPlainObject(override) ? deepMerge(configuration.input, override) : override;
131
+ return Value.Decode(configuration.schema, input);
87
132
  }
88
133
  function deepMerge(base, override) {
89
134
  const result = { ...base };
90
- for (const [key, value] of Object.entries(override)) {
91
- const baseValue = result[key];
92
- result[key] = isPlainObject(baseValue) && isPlainObject(value) ? deepMerge(baseValue, value) : value;
93
- }
135
+ for (const [key, value] of Object.entries(override)) result[key] = isPlainObject(result[key]) && isPlainObject(value) ? deepMerge(result[key], value) : value;
94
136
  return result;
95
137
  }
138
+ function assertWorkflowDefinition(workflow) {
139
+ assertWorkflowMetadata(workflow);
140
+ if (!isWorkflowDeclaration(workflow)) throw new Error("Invalid workflow definition");
141
+ assertGateWorkflow(workflow);
142
+ }
96
143
  function assertGateWorkflow(workflow) {
97
144
  if (!workflow.gate) return;
98
145
  if (workflow.gate.enabled !== true) throw new Error(`Workflow gate must be enabled with true: ${workflow.id}`);
146
+ if (workflow.gate.describe !== void 0 && typeof workflow.gate.describe !== "function") throw new Error(`Invalid gate description callback: ${workflow.id}`);
99
147
  if (!workflow.gate.fields) return;
100
- const paramsSchema = unwrapSchema(workflow.params);
101
- if (schemaType(paramsSchema) !== "object") throw new Error(`Workflow gate fields require object params: ${workflow.id}`);
102
- const paramsShape = schemaShape(paramsSchema);
103
- for (const field of workflow.gate.fields) {
104
- if (!Object.prototype.hasOwnProperty.call(paramsShape, field)) throw new Error(`Unknown workflow gate field ${field}: ${workflow.id}`);
105
- }
106
- }
107
- function validateGateDescription(description, workflowId) {
108
- const trimmed = description.trim();
109
- if (trimmed.length === 0) throw new Error(`Workflow gate description must not be empty: ${workflowId}`);
110
- return trimmed;
111
- }
112
- function inspectedWorkflowInfo(entry) {
113
- return {
114
- ...workflowInfo(entry),
115
- paramsSchema: inspectSchema(entry.workflow.params),
116
- gate: gateInfo(entry.workflow)
117
- };
148
+ const argsSchema = unwrapSchema(workflow.args);
149
+ if (schemaType(argsSchema) !== "object") throw new Error(`Workflow gate fields require object args: ${workflow.id}`);
150
+ const shape = schemaShape(argsSchema);
151
+ for (const field of workflow.gate.fields) if (!Object.hasOwn(shape, field)) throw new Error(`Unknown workflow gate field ${field}: ${workflow.id}`);
118
152
  }
119
153
  function workflowInfo(entry) {
120
154
  return {
121
155
  id: entry.workflow.id,
122
156
  instructions: entry.workflow.instructions,
123
157
  isEntrypoint: entry.workflow.isEntrypoint,
124
- isolation: entry.workflow.isolation,
125
- plugin: entry.plugin
158
+ source: entry.source,
159
+ configKey: entry.workflow.id,
160
+ scope: entry.scopeId ? { id: entry.scopeId } : void 0
126
161
  };
127
162
  }
128
163
  function gateInfo(workflow) {
@@ -130,5 +165,7 @@ function gateInfo(workflow) {
130
165
  }
131
166
  export {
132
167
  NornWorkflowRegistry,
133
- decodePluginConfiguration
168
+ assertCompatibleConfigurationOwners,
169
+ assertWorkflowDefinition,
170
+ decodeConfiguration
134
171
  };
@@ -0,0 +1 @@
1
+ export declare function requireAbsoluteWorkingDirectory(cwd: string): string;
@@ -0,0 +1,9 @@
1
+ // src/internal/working-directory.ts
2
+ import { isAbsolute, resolve } from "node:path";
3
+ function requireAbsoluteWorkingDirectory(cwd) {
4
+ if (typeof cwd !== "string" || !isAbsolute(cwd)) throw new Error("Working directory (cwd) must be an absolute path");
5
+ return resolve(cwd);
6
+ }
7
+ export {
8
+ requireAbsoluteWorkingDirectory
9
+ };
@@ -1,19 +1,18 @@
1
- import { type NornProjectInspection, type NornProjectPluginInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowPlugin } from "@vimhead.dev/norn";
1
+ import { type NornAnyWorkflowDeclaration, type NornProjectInspection, type NornProjectConfigurationInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowSource } from "@vimhead.dev/norn";
2
2
  import * as typeboxModule from "typebox";
3
3
  import { type StaticDecode } from "typebox";
4
- import { NornMemoryWorkflowState } from "./internal/state-store.ts";
5
4
  import { NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
6
5
  export declare const NORN_PROJECT_FILE_NAME = "norn.project.json";
7
6
  declare const nornConfigSchema: typeboxModule.TObject<{
8
- plugins: typeboxModule.TArray<typeboxModule.TString>;
7
+ workflows: typeboxModule.TArray<typeboxModule.TString>;
9
8
  includes: typeboxModule.TArray<typeboxModule.TString>;
10
9
  config: typeboxModule.TRecord<"^.*$", typeboxModule.TUnknown>;
11
10
  }>;
12
11
  declare const nornProjectConfigSchema: typeboxModule.TObject<{
13
- version: typeboxModule.TLiteral<1>;
14
- plugins: typeboxModule.TArray<typeboxModule.TString>;
12
+ workflows: typeboxModule.TArray<typeboxModule.TString>;
15
13
  includes: typeboxModule.TArray<typeboxModule.TString>;
16
14
  config: typeboxModule.TRecord<"^.*$", typeboxModule.TUnknown>;
15
+ version: typeboxModule.TLiteral<1>;
17
16
  }>;
18
17
  type NornConfig = StaticDecode<typeof nornConfigSchema>;
19
18
  type NornProjectConfig = StaticDecode<typeof nornProjectConfigSchema>;
@@ -33,11 +32,11 @@ export type NornProject = {
33
32
  readonly projectConfig: Record<string, unknown>;
34
33
  };
35
34
  export type NornLoadedProject = NornProject & {
36
- readonly plugins: readonly NornWorkflowPlugin[];
37
- readonly pluginInfos: readonly NornProjectPluginInfo[];
35
+ readonly definitions: readonly NornAnyWorkflowDeclaration[];
36
+ readonly modules: readonly NornWorkflowSource[];
37
+ readonly configurations: readonly NornProjectConfigurationInfo[];
38
38
  readonly registry: NornWorkflowRegistry;
39
39
  readonly workflows: readonly NornRegisteredWorkflow[];
40
- readonly state: NornMemoryWorkflowState;
41
40
  };
42
41
  export declare function loadNornProject(cwd: string): Promise<NornLoadedProject>;
43
42
  export declare function discoverNornProject(cwd: string): Promise<NornProjectInspection & NornWorkflowCatalogInfo>;
@@ -0,0 +1,234 @@
1
+ // src/workflow-loader.ts
2
+ import * as nornModule from "@vimhead.dev/norn";
3
+ import { isWorkflowDeclaration } from "@vimhead.dev/norn";
4
+
5
+ // ../core/src/errors.ts
6
+ function isNodeError(error) {
7
+ return error instanceof Error && "code" in error;
8
+ }
9
+
10
+ // src/workflow-loader.ts
11
+ import * as nornSchemaModule from "@vimhead.dev/norn/schema";
12
+ import { inspectSchema, isPlainObject } from "@vimhead.dev/norn/schema";
13
+ import { createJiti } from "jiti/static";
14
+ import { access, readdir, readFile } from "node:fs/promises";
15
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ import * as typeboxModule from "typebox";
18
+ import { Type } from "typebox";
19
+ import * as typeboxCompileModule from "typebox/compile";
20
+ import * as typeboxSchemaModule from "typebox/schema";
21
+ import * as typeboxValueModule from "typebox/value";
22
+ import { AssertError, Value } from "typebox/value";
23
+ import { errorMessage, NornProjectLoadError } from "./internal/errors.js";
24
+ import { assertCompatibleConfigurationOwners, assertWorkflowDefinition, NornWorkflowRegistry } from "./internal/workflow-registry.js";
25
+ var NORN_PROJECT_FILE_NAME = "norn.project.json";
26
+ var nornConfigSchema = Type.Object({
27
+ workflows: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
28
+ includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
29
+ config: Type.Record(Type.String(), Type.Unknown(), { default: {} })
30
+ });
31
+ var nornProjectConfigSchema = Type.Object({ ...nornConfigSchema.properties, version: Type.Literal(1, { default: 1 }) });
32
+ async function loadNornProject(cwd) {
33
+ const { project, diagnostics } = await collectNornProject(cwd);
34
+ if (diagnostics.length) throw new NornProjectLoadError({ diagnostics });
35
+ return project;
36
+ }
37
+ async function discoverNornProject(cwd) {
38
+ const { project, diagnostics } = await collectNornProject(cwd);
39
+ return { project: buildProjectInfo(project), workflows: project.registry.list(), isComplete: diagnostics.length === 0, diagnostics };
40
+ }
41
+ async function inspectNornWorkflow(input) {
42
+ const { project, diagnostics } = await collectNornProject(input.cwd);
43
+ const summary = project.registry.list().find((workflow) => workflow.id === input.workflowId);
44
+ if (!summary) {
45
+ if (!diagnostics.length) throw new Error(`Unknown workflow: ${input.workflowId}`);
46
+ return { workflow: null, isComplete: false, diagnostics };
47
+ }
48
+ try {
49
+ return { workflow: project.registry.inspect(input.workflowId) ?? null, isComplete: diagnostics.length === 0, diagnostics };
50
+ } catch (error) {
51
+ if (!summary.source) throw error;
52
+ return { workflow: null, isComplete: false, diagnostics: [...diagnostics, createDiagnostic({ source: summary.source, workflowId: summary.id, scopeId: summary.scope?.id ?? null, stage: "schema", error })] };
53
+ }
54
+ }
55
+ function buildProjectInfo(project) {
56
+ return {
57
+ cwd: project.cwd,
58
+ projectPath: project.projectPath,
59
+ projectRoot: project.projectRoot,
60
+ configPath: project.configPath,
61
+ configRoot: project.configRoot,
62
+ configFiles: project.configFiles.map((file) => file.path),
63
+ modules: project.modules,
64
+ configurations: project.configurations
65
+ };
66
+ }
67
+ async function findNornProject(cwd) {
68
+ const projectPath = await findNearestNornProject(cwd);
69
+ const projectFile = await readConfigFile({ path: projectPath, isProject: true });
70
+ const configFiles = await loadConfigTree(projectFile, /* @__PURE__ */ new Set());
71
+ return {
72
+ cwd: resolve(cwd),
73
+ projectPath,
74
+ projectRoot: projectFile.root,
75
+ configPath: projectPath,
76
+ configRoot: projectFile.root,
77
+ config: projectFile.config,
78
+ configFiles,
79
+ projectConfig: mergeProjectConfig(configFiles)
80
+ };
81
+ }
82
+ async function findNearestNornProject(cwd) {
83
+ let current = resolve(cwd);
84
+ while (true) {
85
+ const path = join(current, NORN_PROJECT_FILE_NAME);
86
+ try {
87
+ await access(path);
88
+ return path;
89
+ } catch (error) {
90
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
91
+ }
92
+ const parent = dirname(current);
93
+ if (parent === current) throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
94
+ current = parent;
95
+ }
96
+ }
97
+ async function loadConfigTree(file, visited) {
98
+ if (visited.has(file.path)) return [];
99
+ visited.add(file.path);
100
+ const files = [file];
101
+ for (const include of file.config.includes) {
102
+ for (const path of await expandIncludePath(file.root, include)) files.push(...await loadConfigTree(await readConfigFile({ path, isProject: false }), visited));
103
+ }
104
+ return files;
105
+ }
106
+ async function readConfigFile(input) {
107
+ const raw = JSON.parse(await readFile(input.path, "utf8"));
108
+ if (isPlainObject(raw) && Object.hasOwn(raw, "plugins")) throw new Error(`Replace plugins with workflows in ${input.path}; registration modules must export workflow arrays`);
109
+ const schema = input.isProject ? nornProjectConfigSchema : nornConfigSchema;
110
+ const config = Value.Parse(schema, Value.Default(schema, raw));
111
+ return { path: input.path, root: dirname(input.path), config: { ...config, version: 1 } };
112
+ }
113
+ async function collectNornProject(cwd) {
114
+ const project = await findNornProject(cwd);
115
+ const imported = await importWorkflowModules(project);
116
+ const diagnostics = [...imported.diagnostics];
117
+ const registry = new NornWorkflowRegistry();
118
+ const loaded = [];
119
+ const conflicts = collectModuleConflicts(imported.modules);
120
+ for (const candidate of imported.modules) {
121
+ const conflict = conflicts.get(candidate);
122
+ if (conflict) {
123
+ diagnostics.push(...conflict);
124
+ continue;
125
+ }
126
+ const unregister = [];
127
+ const errors = [];
128
+ for (const workflow of candidate.definitions) {
129
+ let stage = "schema";
130
+ try {
131
+ if (workflow.config) inspectSchema(workflow.config);
132
+ if (workflow.scope?.config) inspectSchema(workflow.scope.config);
133
+ stage = "declaration";
134
+ assertWorkflowDefinition(workflow);
135
+ stage = "config";
136
+ unregister.push(registry.register({ workflow, config: project.projectConfig, source: candidate.source }));
137
+ } catch (error) {
138
+ errors.push(createDiagnostic({ source: candidate.source, workflowId: workflow.id, scopeId: workflow.scope?.id ?? null, stage, error }));
139
+ }
140
+ }
141
+ if (errors.length) {
142
+ for (const dispose of unregister) dispose();
143
+ diagnostics.push(...errors);
144
+ } else loaded.push(candidate);
145
+ }
146
+ const definitions = loaded.flatMap((module) => module.definitions);
147
+ return { project: { ...project, definitions, modules: loaded.map((module) => module.source), configurations: registry.configurationInfos(), registry, workflows: registry.launchableEntries() }, diagnostics };
148
+ }
149
+ function collectModuleConflicts(modules) {
150
+ const conflicts = /* @__PURE__ */ new Map();
151
+ const byId = /* @__PURE__ */ new Map();
152
+ for (const module of modules) for (const workflow of module.definitions) byId.set(workflow.id, [...byId.get(workflow.id) ?? [], module]);
153
+ for (const [id, owners] of byId) {
154
+ if (owners.length < 2) continue;
155
+ for (const module of new Set(owners)) conflicts.set(module, [...conflicts.get(module) ?? [], createDiagnostic({ source: module.source, workflowId: id, scopeId: null, stage: "duplicate", error: new Error(`Workflow already registered: ${id}`) })]);
156
+ }
157
+ for (let index = 0; index < modules.length; index++) {
158
+ for (let other = index; other < modules.length; other++) {
159
+ const candidates = [.../* @__PURE__ */ new Set([modules[index], modules[other]])];
160
+ try {
161
+ assertCompatibleConfigurationOwners(candidates.flatMap((module) => module.definitions));
162
+ } catch (error) {
163
+ for (const module of candidates) conflicts.set(module, [...conflicts.get(module) ?? [], createDiagnostic({ source: module.source, workflowId: null, scopeId: null, stage: "config", error })]);
164
+ }
165
+ }
166
+ }
167
+ return conflicts;
168
+ }
169
+ async function importWorkflowModules(project) {
170
+ const modules = [];
171
+ const diagnostics = [];
172
+ const jiti = createJiti(pathToFileURL(project.configPath).href, { moduleCache: false, tryNative: false, virtualModules: nornWorkflowVirtualModules() });
173
+ const cache = /* @__PURE__ */ Object.create(null);
174
+ for (const file of project.configFiles) for (const path of file.config.workflows) {
175
+ const source = { configPath: file.path, path: resolveConfigPath(file.root, path) };
176
+ let stage = "import";
177
+ try {
178
+ const cached = cache[source.path];
179
+ const module = cached?.loaded ? cached.exports : await jiti.evalModule(await readFile(source.path, "utf8"), { filename: source.path, cache, async: true });
180
+ stage = "declaration";
181
+ if (!Array.isArray(module?.default) || !Array.from(module.default).every(isWorkflowDeclaration)) throw new Error(`Workflow module must default-export an array of complete workflow definitions: ${source.path}`);
182
+ modules.push({ definitions: module.default, source });
183
+ } catch (error) {
184
+ for (const [path2, module] of Object.entries(cache)) if (!module.loaded) delete cache[path2];
185
+ diagnostics.push(createDiagnostic({ source, workflowId: null, scopeId: null, stage, error }));
186
+ }
187
+ }
188
+ return { modules, diagnostics };
189
+ }
190
+ function createDiagnostic(input) {
191
+ return { configPath: input.source.configPath, modulePath: input.source.path, scopeId: input.scopeId, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error), issues: input.error instanceof AssertError ? input.error.cause.errors : [] };
192
+ }
193
+ function nornWorkflowVirtualModules() {
194
+ return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
195
+ }
196
+ function mergeProjectConfig(files) {
197
+ const [project, ...reusable] = files;
198
+ let config = {};
199
+ for (const file of reusable) config = mergeConfig({ base: config, override: file.config.config, path: ["config"], rejectConflicts: true });
200
+ return mergeConfig({ base: config, override: project?.config.config ?? {}, path: ["config"], rejectConflicts: false });
201
+ }
202
+ function mergeConfig(input) {
203
+ const result = { ...input.base };
204
+ for (const [key, value] of Object.entries(input.override)) {
205
+ const previous = result[key];
206
+ const path = [...input.path, key];
207
+ if (isPlainObject(previous) && isPlainObject(value)) result[key] = mergeConfig({ base: previous, override: value, path, rejectConflicts: input.rejectConflicts });
208
+ else if (input.rejectConflicts && Object.hasOwn(result, key) && JSON.stringify(previous) !== JSON.stringify(value)) throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
209
+ else result[key] = value;
210
+ }
211
+ return result;
212
+ }
213
+ async function expandIncludePath(root, path) {
214
+ const segments = path.split(/[\\/]+/).filter((segment) => segment.length && segment !== ".");
215
+ return segments.includes("*") ? expandGlobSegments(isAbsolute(path) ? sep : root, segments) : [resolveConfigPath(root, path)];
216
+ }
217
+ async function expandGlobSegments(root, segments) {
218
+ if (!segments.length) return [root];
219
+ const [segment, ...remaining] = segments;
220
+ if (segment !== "*") return expandGlobSegments(join(root, segment), remaining);
221
+ const entries = await readdir(root, { withFileTypes: true });
222
+ return (await Promise.all(entries.filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => expandGlobSegments(join(root, entry.name), remaining)))).flat();
223
+ }
224
+ function resolveConfigPath(root, path) {
225
+ if (!path.length) throw new Error("Norn path must not be empty");
226
+ return isAbsolute(path) ? path : resolve(root, path);
227
+ }
228
+ export {
229
+ NORN_PROJECT_FILE_NAME,
230
+ discoverNornProject,
231
+ findNornProject,
232
+ inspectNornWorkflow,
233
+ loadNornProject
234
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vimhead.dev/norn-cli",
3
- "version": "0.1.0-tip.35359392805.1",
3
+ "version": "0.1.0-tip.35436871363.1",
4
4
  "description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "0.85.1",
44
44
  "jiti": "^2.7.0",
45
45
  "typebox": "^1.3.34",
46
- "@vimhead.dev/norn": "0.1.0-tip.35359392805.1"
46
+ "@vimhead.dev/norn": "0.1.0-tip.35436871363.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node ../../scripts/build-package.ts"