@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.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 (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. package/package.json +2 -2
@@ -1,5 +1,16 @@
1
1
  import * as nornModule from "@vimhead.dev/norn";
2
- import { isWorkflowDeclaration, type NornAnyWorkflowDeclaration, type NornDispose, type NornWorkflowDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectConfigurationInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowSource } from "@vimhead.dev/norn";
2
+ import {
3
+ isWorkflowDeclaration,
4
+ type NornAnyWorkflowDeclaration,
5
+ type NornDispose,
6
+ type NornWorkflowDiagnostic,
7
+ type NornProjectInfo,
8
+ type NornProjectInspection,
9
+ type NornProjectConfigurationInfo,
10
+ type NornWorkflowCatalogInfo,
11
+ type NornWorkflowInspection,
12
+ type NornWorkflowSource,
13
+ } from "@vimhead.dev/norn";
3
14
  import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
15
  import * as nornSchemaModule from "@vimhead.dev/norn/schema";
5
16
  import { inspectSchema, isPlainObject } from "@vimhead.dev/norn/schema";
@@ -14,7 +25,12 @@ import * as typeboxSchemaModule from "typebox/schema";
14
25
  import * as typeboxValueModule from "typebox/value";
15
26
  import { AssertError, Value } from "typebox/value";
16
27
  import { errorMessage, NornProjectLoadError } from "./internal/errors.ts";
17
- import { assertCompatibleConfigurationOwners, assertWorkflowDefinition, NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
28
+ import {
29
+ assertCompatibleConfigurationOwners,
30
+ assertWorkflowDefinition,
31
+ NornWorkflowRegistry,
32
+ type NornRegisteredWorkflow,
33
+ } from "./internal/workflow-registry.ts";
18
34
 
19
35
  export const NORN_PROJECT_FILE_NAME = "norn.project.json";
20
36
  const nornConfigSchema = Type.Object({
@@ -22,15 +38,29 @@ const nornConfigSchema = Type.Object({
22
38
  includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
23
39
  config: Type.Record(Type.String(), Type.Unknown(), { default: {} }),
24
40
  });
25
- const nornProjectConfigSchema = Type.Object({ ...nornConfigSchema.properties, version: Type.Literal(1, { default: 1 }) });
41
+ const nornProjectConfigSchema = Type.Object({
42
+ ...nornConfigSchema.properties,
43
+ version: Type.Literal(1, { default: 1 }),
44
+ });
26
45
  type NornConfig = StaticDecode<typeof nornConfigSchema>;
27
46
  type NornProjectConfig = StaticDecode<typeof nornProjectConfigSchema>;
28
- type NornConfigFile = { readonly path: string; readonly root: string; readonly config: NornConfig };
29
- type NornProjectConfigFile = Omit<NornConfigFile, "config"> & { readonly config: NornProjectConfig };
47
+ type NornConfigFile = {
48
+ readonly path: string;
49
+ readonly root: string;
50
+ readonly config: NornConfig;
51
+ };
52
+ type NornProjectConfigFile = Omit<NornConfigFile, "config"> & {
53
+ readonly config: NornProjectConfig;
54
+ };
30
55
  export type NornProject = {
31
- readonly cwd: string; readonly projectPath: string; readonly projectRoot: string;
32
- readonly configPath: string; readonly configRoot: string; readonly config: NornProjectConfig;
33
- readonly configFiles: readonly NornConfigFile[]; readonly projectConfig: Record<string, unknown>;
56
+ readonly cwd: string;
57
+ readonly projectPath: string;
58
+ readonly projectRoot: string;
59
+ readonly configPath: string;
60
+ readonly configRoot: string;
61
+ readonly config: NornProjectConfig;
62
+ readonly configFiles: readonly NornConfigFile[];
63
+ readonly projectConfig: Record<string, unknown>;
34
64
  };
35
65
  export type NornLoadedProject = NornProject & {
36
66
  readonly definitions: readonly NornAnyWorkflowDeclaration[];
@@ -39,74 +69,149 @@ export type NornLoadedProject = NornProject & {
39
69
  readonly registry: NornWorkflowRegistry;
40
70
  readonly workflows: readonly NornRegisteredWorkflow[];
41
71
  };
42
- type ImportedWorkflowModule = { readonly definitions: readonly NornAnyWorkflowDeclaration[]; readonly source: NornWorkflowSource };
72
+ type ImportedWorkflowModule = {
73
+ readonly definitions: readonly NornAnyWorkflowDeclaration[];
74
+ readonly source: NornWorkflowSource;
75
+ };
43
76
 
44
77
  export async function loadNornProject(cwd: string): Promise<NornLoadedProject> {
45
78
  const { project, diagnostics } = await collectNornProject(cwd);
46
79
  if (diagnostics.length) throw new NornProjectLoadError({ diagnostics });
47
80
  return project;
48
81
  }
49
- export async function discoverNornProject(cwd: string): Promise<NornProjectInspection & NornWorkflowCatalogInfo> {
82
+ export async function discoverNornProject(
83
+ cwd: string,
84
+ ): Promise<NornProjectInspection & NornWorkflowCatalogInfo> {
50
85
  const { project, diagnostics } = await collectNornProject(cwd);
51
- return { project: buildProjectInfo(project), workflows: project.registry.list(), isComplete: diagnostics.length === 0, diagnostics };
86
+ return {
87
+ project: buildProjectInfo(project),
88
+ workflows: project.registry.list(),
89
+ isComplete: diagnostics.length === 0,
90
+ diagnostics,
91
+ };
52
92
  }
53
- export async function inspectNornWorkflow(input: { readonly cwd: string; readonly workflowId: string }): Promise<NornWorkflowInspection> {
93
+ export async function inspectNornWorkflow(input: {
94
+ readonly cwd: string;
95
+ readonly workflowId: string;
96
+ }): Promise<NornWorkflowInspection> {
54
97
  const { project, diagnostics } = await collectNornProject(input.cwd);
55
- const summary = project.registry.list().find(workflow => workflow.id === input.workflowId);
98
+ const summary = project.registry
99
+ .list()
100
+ .find((workflow) => workflow.id === input.workflowId);
56
101
  if (!summary) {
57
- if (!diagnostics.length) throw new Error(`Unknown workflow: ${input.workflowId}`);
102
+ if (!diagnostics.length)
103
+ throw new Error(`Unknown workflow: ${input.workflowId}`);
58
104
  return { workflow: null, isComplete: false, diagnostics };
59
105
  }
60
- try { return { workflow: project.registry.inspect(input.workflowId) ?? null, isComplete: diagnostics.length === 0, diagnostics }; }
61
- catch (error) {
106
+ try {
107
+ return {
108
+ workflow: project.registry.inspect(input.workflowId) ?? null,
109
+ isComplete: diagnostics.length === 0,
110
+ diagnostics,
111
+ };
112
+ } catch (error) {
62
113
  if (!summary.source) throw error;
63
- return { workflow: null, isComplete: false, diagnostics: [...diagnostics, createDiagnostic({ source: summary.source, workflowId: summary.id, scopeId: summary.scope?.id ?? null, stage: "schema", error })] };
114
+ return {
115
+ workflow: null,
116
+ isComplete: false,
117
+ diagnostics: [
118
+ ...diagnostics,
119
+ createDiagnostic({
120
+ source: summary.source,
121
+ workflowId: summary.id,
122
+ scopeId: summary.scope?.id ?? null,
123
+ stage: "schema",
124
+ error,
125
+ }),
126
+ ],
127
+ };
64
128
  }
65
129
  }
66
130
  function buildProjectInfo(project: NornLoadedProject): NornProjectInfo {
67
131
  return {
68
- cwd: project.cwd, projectPath: project.projectPath, projectRoot: project.projectRoot,
69
- configPath: project.configPath, configRoot: project.configRoot, configFiles: project.configFiles.map(file => file.path),
70
- modules: project.modules, configurations: project.configurations,
132
+ cwd: project.cwd,
133
+ projectPath: project.projectPath,
134
+ projectRoot: project.projectRoot,
135
+ configPath: project.configPath,
136
+ configRoot: project.configRoot,
137
+ configFiles: project.configFiles.map((file) => file.path),
138
+ modules: project.modules,
139
+ configurations: project.configurations,
71
140
  };
72
141
  }
73
142
  export async function findNornProject(cwd: string): Promise<NornProject> {
74
143
  const projectPath = await findNearestNornProject(cwd);
75
- const projectFile = await readConfigFile({ path: projectPath, isProject: true });
144
+ const projectFile = await readConfigFile({
145
+ path: projectPath,
146
+ isProject: true,
147
+ });
76
148
  const configFiles = await loadConfigTree(projectFile, new Set());
77
149
  return {
78
- cwd: resolve(cwd), projectPath, projectRoot: projectFile.root, configPath: projectPath, configRoot: projectFile.root,
79
- config: projectFile.config, configFiles, projectConfig: mergeProjectConfig(configFiles),
150
+ cwd: resolve(cwd),
151
+ projectPath,
152
+ projectRoot: projectFile.root,
153
+ configPath: projectPath,
154
+ configRoot: projectFile.root,
155
+ config: projectFile.config,
156
+ configFiles,
157
+ projectConfig: mergeProjectConfig(configFiles),
80
158
  };
81
159
  }
82
160
  async function findNearestNornProject(cwd: string): Promise<string> {
83
161
  let current = resolve(cwd);
84
162
  while (true) {
85
163
  const path = join(current, NORN_PROJECT_FILE_NAME);
86
- try { await access(path); return path; }
87
- catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") throw error; }
164
+ try {
165
+ await access(path);
166
+ return path;
167
+ } catch (error) {
168
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
169
+ }
88
170
  const parent = dirname(current);
89
- if (parent === current) throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
171
+ if (parent === current)
172
+ throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
90
173
  current = parent;
91
174
  }
92
175
  }
93
- async function loadConfigTree(file: NornConfigFile, visited: Set<string>): Promise<NornConfigFile[]> {
176
+ async function loadConfigTree(
177
+ file: NornConfigFile,
178
+ visited: Set<string>,
179
+ ): Promise<NornConfigFile[]> {
94
180
  if (visited.has(file.path)) return [];
95
181
  visited.add(file.path);
96
182
  const files: NornConfigFile[] = [file];
97
183
  for (const include of file.config.includes) {
98
- for (const path of await expandIncludePath(file.root, include)) files.push(...await loadConfigTree(await readConfigFile({ path, isProject: false }), visited));
184
+ for (const path of await expandIncludePath(file.root, include))
185
+ files.push(
186
+ ...(await loadConfigTree(
187
+ await readConfigFile({ path, isProject: false }),
188
+ visited,
189
+ )),
190
+ );
99
191
  }
100
192
  return files;
101
193
  }
102
- async function readConfigFile(input: { readonly path: string; readonly isProject: boolean }): Promise<NornProjectConfigFile> {
194
+ async function readConfigFile(input: {
195
+ readonly path: string;
196
+ readonly isProject: boolean;
197
+ }): Promise<NornProjectConfigFile> {
103
198
  const raw: unknown = JSON.parse(await readFile(input.path, "utf8"));
104
- if (isPlainObject(raw) && Object.hasOwn(raw, "plugins")) throw new Error(`Replace plugins with workflows in ${input.path}; registration modules must export workflow arrays`);
199
+ if (isPlainObject(raw) && Object.hasOwn(raw, "plugins"))
200
+ throw new Error(
201
+ `Replace plugins with workflows in ${input.path}; registration modules must export workflow arrays`,
202
+ );
105
203
  const schema = input.isProject ? nornProjectConfigSchema : nornConfigSchema;
106
204
  const config = Value.Parse(schema, Value.Default(schema, raw));
107
- return { path: input.path, root: dirname(input.path), config: { ...config, version: 1 } };
205
+ return {
206
+ path: input.path,
207
+ root: dirname(input.path),
208
+ config: { ...config, version: 1 },
209
+ };
108
210
  }
109
- async function collectNornProject(cwd: string): Promise<{ readonly project: NornLoadedProject; readonly diagnostics: NornWorkflowDiagnostic[] }> {
211
+ async function collectNornProject(cwd: string): Promise<{
212
+ readonly project: NornLoadedProject;
213
+ readonly diagnostics: NornWorkflowDiagnostic[];
214
+ }> {
110
215
  const project = await findNornProject(cwd);
111
216
  const imported = await importWorkflowModules(project);
112
217
  const diagnostics = [...imported.diagnostics];
@@ -115,7 +220,10 @@ async function collectNornProject(cwd: string): Promise<{ readonly project: Norn
115
220
  const conflicts = collectModuleConflicts(imported.modules);
116
221
  for (const candidate of imported.modules) {
117
222
  const conflict = conflicts.get(candidate);
118
- if (conflict) { diagnostics.push(...conflict); continue; }
223
+ if (conflict) {
224
+ diagnostics.push(...conflict);
225
+ continue;
226
+ }
119
227
  const unregister: NornDispose[] = [];
120
228
  const errors: NornWorkflowDiagnostic[] = [];
121
229
  for (const workflow of candidate.definitions) {
@@ -126,86 +234,245 @@ async function collectNornProject(cwd: string): Promise<{ readonly project: Norn
126
234
  stage = "declaration";
127
235
  assertWorkflowDefinition(workflow);
128
236
  stage = "config";
129
- unregister.push(registry.register({ workflow, config: project.projectConfig, source: candidate.source }));
130
- } catch (error) { errors.push(createDiagnostic({ source: candidate.source, workflowId: workflow.id, scopeId: workflow.scope?.id ?? null, stage, error })); }
237
+ unregister.push(
238
+ registry.register({
239
+ workflow,
240
+ config: project.projectConfig,
241
+ source: candidate.source,
242
+ }),
243
+ );
244
+ } catch (error) {
245
+ errors.push(
246
+ createDiagnostic({
247
+ source: candidate.source,
248
+ workflowId: workflow.id,
249
+ scopeId: workflow.scope?.id ?? null,
250
+ stage,
251
+ error,
252
+ }),
253
+ );
254
+ }
131
255
  }
132
- if (errors.length) { for (const dispose of unregister) dispose(); diagnostics.push(...errors); }
133
- else loaded.push(candidate);
256
+ if (errors.length) {
257
+ for (const dispose of unregister) dispose();
258
+ diagnostics.push(...errors);
259
+ } else loaded.push(candidate);
134
260
  }
135
- const definitions = loaded.flatMap(module => module.definitions);
136
- return { project: { ...project, definitions, modules: loaded.map(module => module.source), configurations: registry.configurationInfos(), registry, workflows: registry.launchableEntries() }, diagnostics };
261
+ const definitions = loaded.flatMap((module) => module.definitions);
262
+ return {
263
+ project: {
264
+ ...project,
265
+ definitions,
266
+ modules: loaded.map((module) => module.source),
267
+ configurations: registry.configurationInfos(),
268
+ registry,
269
+ workflows: registry.launchableEntries(),
270
+ },
271
+ diagnostics,
272
+ };
137
273
  }
138
- function collectModuleConflicts(modules: readonly ImportedWorkflowModule[]): Map<ImportedWorkflowModule, NornWorkflowDiagnostic[]> {
274
+ function collectModuleConflicts(
275
+ modules: readonly ImportedWorkflowModule[],
276
+ ): Map<ImportedWorkflowModule, NornWorkflowDiagnostic[]> {
139
277
  const conflicts = new Map<ImportedWorkflowModule, NornWorkflowDiagnostic[]>();
140
278
  const byId = new Map<string, ImportedWorkflowModule[]>();
141
- for (const module of modules) for (const workflow of module.definitions) byId.set(workflow.id, [...byId.get(workflow.id) ?? [], module]);
279
+ for (const module of modules)
280
+ for (const workflow of module.definitions)
281
+ byId.set(workflow.id, [...(byId.get(workflow.id) ?? []), module]);
142
282
  for (const [id, owners] of byId) {
143
283
  if (owners.length < 2) continue;
144
- 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}`) })]);
284
+ for (const module of new Set(owners))
285
+ conflicts.set(module, [
286
+ ...(conflicts.get(module) ?? []),
287
+ createDiagnostic({
288
+ source: module.source,
289
+ workflowId: id,
290
+ scopeId: null,
291
+ stage: "duplicate",
292
+ error: new Error(`Workflow already registered: ${id}`),
293
+ }),
294
+ ]);
145
295
  }
146
296
  for (let index = 0; index < modules.length; index++) {
147
297
  for (let other = index; other < modules.length; other++) {
148
298
  const candidates = [...new Set([modules[index], modules[other]])];
149
- try { assertCompatibleConfigurationOwners(candidates.flatMap(module => module.definitions)); }
150
- catch (error) { for (const module of candidates) conflicts.set(module, [...conflicts.get(module) ?? [], createDiagnostic({ source: module.source, workflowId: null, scopeId: null, stage: "config", error })]); }
299
+ try {
300
+ assertCompatibleConfigurationOwners(
301
+ candidates.flatMap((module) => module.definitions),
302
+ );
303
+ } catch (error) {
304
+ for (const module of candidates)
305
+ conflicts.set(module, [
306
+ ...(conflicts.get(module) ?? []),
307
+ createDiagnostic({
308
+ source: module.source,
309
+ workflowId: null,
310
+ scopeId: null,
311
+ stage: "config",
312
+ error,
313
+ }),
314
+ ]);
315
+ }
151
316
  }
152
317
  }
153
318
  return conflicts;
154
319
  }
155
- async function importWorkflowModules(project: NornProject): Promise<{ readonly modules: ImportedWorkflowModule[]; readonly diagnostics: NornWorkflowDiagnostic[] }> {
320
+ async function importWorkflowModules(project: NornProject): Promise<{
321
+ readonly modules: ImportedWorkflowModule[];
322
+ readonly diagnostics: NornWorkflowDiagnostic[];
323
+ }> {
156
324
  const modules: ImportedWorkflowModule[] = [];
157
325
  const diagnostics: NornWorkflowDiagnostic[] = [];
158
- const jiti = createJiti(pathToFileURL(project.configPath).href, { moduleCache: false, tryNative: false, virtualModules: nornWorkflowVirtualModules() });
326
+ const jiti = createJiti(pathToFileURL(project.configPath).href, {
327
+ moduleCache: false,
328
+ tryNative: false,
329
+ virtualModules: nornWorkflowVirtualModules(),
330
+ });
159
331
  const cache: ModuleCache = Object.create(null);
160
- for (const file of project.configFiles) for (const path of file.config.workflows) {
161
- const source = { configPath: file.path, path: resolveConfigPath(file.root, path) };
162
- let stage: NornWorkflowDiagnostic["stage"] = "import";
163
- try {
164
- const cached = cache[source.path];
165
- const module = (cached?.loaded ? cached.exports : await jiti.evalModule(await readFile(source.path, "utf8"), { filename: source.path, cache, async: true })) as { default?: unknown };
166
- stage = "declaration";
167
- 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}`);
168
- modules.push({ definitions: module.default, source });
169
- } catch (error) {
170
- for (const [path, module] of Object.entries(cache)) if (!module.loaded) delete cache[path];
171
- diagnostics.push(createDiagnostic({ source, workflowId: null, scopeId: null, stage, error }));
332
+ for (const file of project.configFiles)
333
+ for (const path of file.config.workflows) {
334
+ const source = {
335
+ configPath: file.path,
336
+ path: resolveConfigPath(file.root, path),
337
+ };
338
+ let stage: NornWorkflowDiagnostic["stage"] = "import";
339
+ try {
340
+ const cached = cache[source.path];
341
+ const module = (
342
+ cached?.loaded
343
+ ? cached.exports
344
+ : await jiti.evalModule(await readFile(source.path, "utf8"), {
345
+ filename: source.path,
346
+ cache,
347
+ async: true,
348
+ })
349
+ ) as { default?: unknown };
350
+ stage = "declaration";
351
+ if (
352
+ !Array.isArray(module?.default) ||
353
+ !Array.from(module.default).every(isWorkflowDeclaration)
354
+ )
355
+ throw new Error(
356
+ `Workflow module must default-export an array of complete workflow definitions: ${source.path}`,
357
+ );
358
+ modules.push({ definitions: module.default, source });
359
+ } catch (error) {
360
+ for (const [path, module] of Object.entries(cache))
361
+ if (!module.loaded) delete cache[path];
362
+ diagnostics.push(
363
+ createDiagnostic({
364
+ source,
365
+ workflowId: null,
366
+ scopeId: null,
367
+ stage,
368
+ error,
369
+ }),
370
+ );
371
+ }
172
372
  }
173
- }
174
373
  return { modules, diagnostics };
175
374
  }
176
- function createDiagnostic(input: { readonly source: NornWorkflowSource; readonly workflowId: string | null; readonly scopeId: string | null; readonly stage: NornWorkflowDiagnostic["stage"]; readonly error: unknown }): NornWorkflowDiagnostic {
177
- 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 : [] };
375
+ function createDiagnostic(input: {
376
+ readonly source: NornWorkflowSource;
377
+ readonly workflowId: string | null;
378
+ readonly scopeId: string | null;
379
+ readonly stage: NornWorkflowDiagnostic["stage"];
380
+ readonly error: unknown;
381
+ }): NornWorkflowDiagnostic {
382
+ return {
383
+ configPath: input.source.configPath,
384
+ modulePath: input.source.path,
385
+ scopeId: input.scopeId,
386
+ workflowId: input.workflowId,
387
+ stage: input.stage,
388
+ message: errorMessage(input.error),
389
+ issues: input.error instanceof AssertError ? input.error.cause.errors : [],
390
+ };
178
391
  }
179
392
  function nornWorkflowVirtualModules(): Record<string, unknown> {
180
- return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
393
+ return {
394
+ "@vimhead.dev/norn": nornModule,
395
+ "@vimhead.dev/norn/schema": nornSchemaModule,
396
+ typebox: typeboxModule,
397
+ "typebox/value": typeboxValueModule,
398
+ "typebox/compile": typeboxCompileModule,
399
+ "typebox/schema": typeboxSchemaModule,
400
+ };
181
401
  }
182
- function mergeProjectConfig(files: readonly NornConfigFile[]): Record<string, unknown> {
402
+ function mergeProjectConfig(
403
+ files: readonly NornConfigFile[],
404
+ ): Record<string, unknown> {
183
405
  const [project, ...reusable] = files;
184
406
  let config: Record<string, unknown> = {};
185
- for (const file of reusable) config = mergeConfig({ base: config, override: file.config.config, path: ["config"], rejectConflicts: true });
186
- return mergeConfig({ base: config, override: project?.config.config ?? {}, path: ["config"], rejectConflicts: false });
407
+ for (const file of reusable)
408
+ config = mergeConfig({
409
+ base: config,
410
+ override: file.config.config,
411
+ path: ["config"],
412
+ rejectConflicts: true,
413
+ });
414
+ return mergeConfig({
415
+ base: config,
416
+ override: project?.config.config ?? {},
417
+ path: ["config"],
418
+ rejectConflicts: false,
419
+ });
187
420
  }
188
- function mergeConfig(input: { readonly base: Record<string, unknown>; readonly override: Record<string, unknown>; readonly path: readonly string[]; readonly rejectConflicts: boolean }): Record<string, unknown> {
421
+ function mergeConfig(input: {
422
+ readonly base: Record<string, unknown>;
423
+ readonly override: Record<string, unknown>;
424
+ readonly path: readonly string[];
425
+ readonly rejectConflicts: boolean;
426
+ }): Record<string, unknown> {
189
427
  const result = { ...input.base };
190
428
  for (const [key, value] of Object.entries(input.override)) {
191
429
  const previous = result[key];
192
430
  const path = [...input.path, key];
193
- if (isPlainObject(previous) && isPlainObject(value)) result[key] = mergeConfig({ base: previous, override: value, path, rejectConflicts: input.rejectConflicts });
194
- else if (input.rejectConflicts && Object.hasOwn(result, key) && JSON.stringify(previous) !== JSON.stringify(value)) throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
431
+ if (isPlainObject(previous) && isPlainObject(value))
432
+ result[key] = mergeConfig({
433
+ base: previous,
434
+ override: value,
435
+ path,
436
+ rejectConflicts: input.rejectConflicts,
437
+ });
438
+ else if (
439
+ input.rejectConflicts &&
440
+ Object.hasOwn(result, key) &&
441
+ JSON.stringify(previous) !== JSON.stringify(value)
442
+ )
443
+ throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
195
444
  else result[key] = value;
196
445
  }
197
446
  return result;
198
447
  }
199
- async function expandIncludePath(root: string, path: string): Promise<string[]> {
200
- const segments = path.split(/[\\/]+/).filter(segment => segment.length && segment !== ".");
201
- return segments.includes("*") ? expandGlobSegments(isAbsolute(path) ? sep : root, segments) : [resolveConfigPath(root, path)];
448
+ async function expandIncludePath(
449
+ root: string,
450
+ path: string,
451
+ ): Promise<string[]> {
452
+ const segments = path
453
+ .split(/[\\/]+/)
454
+ .filter((segment) => segment.length && segment !== ".");
455
+ return segments.includes("*")
456
+ ? expandGlobSegments(isAbsolute(path) ? sep : root, segments)
457
+ : [resolveConfigPath(root, path)];
202
458
  }
203
- async function expandGlobSegments(root: string, segments: readonly string[]): Promise<string[]> {
459
+ async function expandGlobSegments(
460
+ root: string,
461
+ segments: readonly string[],
462
+ ): Promise<string[]> {
204
463
  if (!segments.length) return [root];
205
464
  const [segment, ...remaining] = segments;
206
- if (segment !== "*") return expandGlobSegments(join(root, segment), remaining);
465
+ if (segment !== "*")
466
+ return expandGlobSegments(join(root, segment), remaining);
207
467
  const entries = await readdir(root, { withFileTypes: true });
208
- 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();
468
+ return (
469
+ await Promise.all(
470
+ entries
471
+ .filter((entry) => entry.isDirectory())
472
+ .sort((left, right) => left.name.localeCompare(right.name))
473
+ .map((entry) => expandGlobSegments(join(root, entry.name), remaining)),
474
+ )
475
+ ).flat();
209
476
  }
210
477
  function resolveConfigPath(root: string, path: string): string {
211
478
  if (!path.length) throw new Error("Norn path must not be empty");
@@ -3,15 +3,24 @@ import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { isNodeError } from "./errors.ts";
5
5
 
6
- export async function writeJsonAtomically(path: string, value: unknown): Promise<void> {
6
+ export async function writeJsonAtomically(
7
+ path: string,
8
+ value: unknown,
9
+ ): Promise<void> {
7
10
  await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}\n`);
8
11
  }
9
12
 
10
- export async function writeTextAtomically(path: string, content: string): Promise<void> {
13
+ export async function writeTextAtomically(
14
+ path: string,
15
+ content: string,
16
+ ): Promise<void> {
11
17
  await mkdir(dirname(path), { recursive: true });
12
18
  let existingMode: number | undefined;
13
- try { existingMode = (await stat(path)).mode & 0o777; }
14
- catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") throw error; }
19
+ try {
20
+ existingMode = (await stat(path)).mode & 0o777;
21
+ } catch (error) {
22
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
23
+ }
15
24
  const tmpPath = `${path}.${randomUUID()}.tmp`;
16
25
  try {
17
26
  await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
@@ -1,4 +1,15 @@
1
- export function createWorkflowTransition(input: { readonly workflowId: string; readonly args: unknown }): { readonly type: "next"; readonly workflowId: string; readonly args: unknown } {
2
- if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
1
+ export function createWorkflowTransition(input: {
2
+ readonly workflowId: string;
3
+ readonly args: unknown;
4
+ }): {
5
+ readonly type: "next";
6
+ readonly workflowId: string;
7
+ readonly args: unknown;
8
+ } {
9
+ if (
10
+ typeof input.workflowId !== "string" ||
11
+ input.workflowId.trim().length === 0
12
+ )
13
+ throw new Error("Workflow transition requires a nonempty workflow ID");
3
14
  return { type: "next", workflowId: input.workflowId, args: input.args };
4
15
  }