@vimhead.dev/norn-cli 0.1.0-tip.0

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 (171) hide show
  1. package/README.md +13 -0
  2. package/assets/README.md +157 -0
  3. package/assets/docs/README.md +23 -0
  4. package/assets/docs/agents.md +64 -0
  5. package/assets/docs/cli.md +183 -0
  6. package/assets/docs/composition.md +76 -0
  7. package/assets/docs/persistence.md +56 -0
  8. package/assets/docs/projects.md +75 -0
  9. package/assets/docs/recovery.md +56 -0
  10. package/assets/docs/resources.md +61 -0
  11. package/assets/docs/workflows.md +57 -0
  12. package/assets/examples/agent-then-analysis/README.md +103 -0
  13. package/assets/examples/agent-then-analysis/input.json +5 -0
  14. package/assets/examples/agent-then-analysis/norn.project.json +4 -0
  15. package/assets/examples/agent-then-analysis/plugin.ts +89 -0
  16. package/assets/examples/coordinating-multiple-agents/README.md +56 -0
  17. package/assets/examples/coordinating-multiple-agents/input.json +10 -0
  18. package/assets/examples/coordinating-multiple-agents/norn.project.json +4 -0
  19. package/assets/examples/coordinating-multiple-agents/plugin.ts +102 -0
  20. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +52 -0
  21. package/assets/examples/coordinating-multiple-agents/work-queue.ts +153 -0
  22. package/assets/examples/minimal-workflow/README.md +71 -0
  23. package/assets/examples/minimal-workflow/norn.project.json +4 -0
  24. package/assets/examples/minimal-workflow/plugin.ts +29 -0
  25. package/assets/examples/shared-state/README.md +19 -0
  26. package/assets/examples/shared-state/input.json +1 -0
  27. package/assets/examples/shared-state/norn.project.json +4 -0
  28. package/assets/examples/shared-state/plugin.ts +47 -0
  29. package/assets/examples/worktree-development-loop/README.md +66 -0
  30. package/assets/examples/worktree-development-loop/index.ts +1 -0
  31. package/assets/examples/worktree-development-loop/manifest.ts +26 -0
  32. package/assets/examples/worktree-development-loop/norn.project.json +9 -0
  33. package/assets/examples/worktree-development-loop/plugin.ts +27 -0
  34. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -0
  35. package/assets/examples/worktree-development-loop/state.ts +23 -0
  36. package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +8 -0
  37. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -0
  38. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -0
  39. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +22 -0
  40. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +14 -0
  41. package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +8 -0
  42. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +54 -0
  43. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +3 -0
  44. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +12 -0
  45. package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +8 -0
  46. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +28 -0
  47. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +3 -0
  48. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +12 -0
  49. package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +8 -0
  50. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +53 -0
  51. package/assets/examples/worktree-development-loop/workflows/review/index.ts +10 -0
  52. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +23 -0
  53. package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +12 -0
  54. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -0
  55. package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +3 -0
  56. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +12 -0
  57. package/assets/package.json +1 -0
  58. package/assets/packages/cli/src/build-info.ts +36 -0
  59. package/assets/packages/cli/src/bun/cli.ts +16 -0
  60. package/assets/packages/cli/src/cli.ts +1135 -0
  61. package/assets/packages/cli/src/client.ts +167 -0
  62. package/assets/packages/cli/src/documentation-intro.ts +30 -0
  63. package/assets/packages/cli/src/documentation.ts +149 -0
  64. package/assets/packages/cli/src/generated-build-info.ts +12 -0
  65. package/assets/packages/cli/src/internal/agent-directory.ts +5 -0
  66. package/assets/packages/cli/src/internal/agent-response-tool.ts +96 -0
  67. package/assets/packages/cli/src/internal/agents.ts +365 -0
  68. package/assets/packages/cli/src/internal/artifacts.ts +26 -0
  69. package/assets/packages/cli/src/internal/commands.ts +180 -0
  70. package/assets/packages/cli/src/internal/documentation-bundle.ts +49 -0
  71. package/assets/packages/cli/src/internal/engine.ts +501 -0
  72. package/assets/packages/cli/src/internal/errors.ts +39 -0
  73. package/assets/packages/cli/src/internal/file-names.ts +3 -0
  74. package/assets/packages/cli/src/internal/launch-request.ts +94 -0
  75. package/assets/packages/cli/src/internal/logs.ts +41 -0
  76. package/assets/packages/cli/src/internal/metrics.ts +356 -0
  77. package/assets/packages/cli/src/internal/pi-assets.ts +95 -0
  78. package/assets/packages/cli/src/internal/resource-bindings.ts +35 -0
  79. package/assets/packages/cli/src/internal/run-lease.ts +158 -0
  80. package/assets/packages/cli/src/internal/run-log.ts +59 -0
  81. package/assets/packages/cli/src/internal/run-names.ts +36 -0
  82. package/assets/packages/cli/src/internal/run-resources.ts +23 -0
  83. package/assets/packages/cli/src/internal/run-state.ts +380 -0
  84. package/assets/packages/cli/src/internal/run-store.ts +323 -0
  85. package/assets/packages/cli/src/internal/run.ts +133 -0
  86. package/assets/packages/cli/src/internal/state-store.ts +75 -0
  87. package/assets/packages/cli/src/internal/usage.ts +70 -0
  88. package/assets/packages/cli/src/internal/workflow-registry.ts +176 -0
  89. package/assets/packages/cli/src/plugin-loader.ts +412 -0
  90. package/assets/packages/cli/src/resources.ts +67 -0
  91. package/assets/packages/core/src/agent-protocol.ts +1 -0
  92. package/assets/packages/core/src/atomic-files.ts +24 -0
  93. package/assets/packages/core/src/errors.ts +3 -0
  94. package/assets/packages/sdk/src/agent-resource-adapter.ts +11 -0
  95. package/assets/packages/sdk/src/api.ts +821 -0
  96. package/assets/packages/sdk/src/files.ts +136 -0
  97. package/assets/packages/sdk/src/index.ts +6 -0
  98. package/assets/packages/sdk/src/resources.ts +20 -0
  99. package/assets/packages/sdk/src/schema.ts +48 -0
  100. package/assets/packages/sdk/src/seer/config.ts +62 -0
  101. package/assets/packages/sdk/src/seer/index.ts +7 -0
  102. package/assets/packages/sdk/src/state-adapter.ts +75 -0
  103. package/assets/setup/providers.md +128 -0
  104. package/assets/setup/releases.md +76 -0
  105. package/assets/tests/workflow-ref.test.ts +113 -0
  106. package/bin/norn.mjs +10 -0
  107. package/dist/build-info.d.ts +30 -0
  108. package/dist/build-info.js +6 -0
  109. package/dist/cli.d.ts +2 -0
  110. package/dist/cli.js +1032 -0
  111. package/dist/client.d.ts +48 -0
  112. package/dist/client.js +118 -0
  113. package/dist/documentation-intro.d.ts +5 -0
  114. package/dist/documentation-intro.js +29 -0
  115. package/dist/documentation.d.ts +33 -0
  116. package/dist/documentation.js +132 -0
  117. package/dist/generated-build-info.d.ts +10 -0
  118. package/dist/generated-build-info.js +14 -0
  119. package/dist/internal/agent-directory.d.ts +4 -0
  120. package/dist/internal/agent-directory.js +8 -0
  121. package/dist/internal/agent-response-tool.d.ts +21 -0
  122. package/dist/internal/agent-response-tool.js +79 -0
  123. package/dist/internal/agents.d.ts +29 -0
  124. package/dist/internal/agents.js +336 -0
  125. package/dist/internal/artifacts.d.ts +10 -0
  126. package/dist/internal/artifacts.js +29 -0
  127. package/dist/internal/commands.d.ts +18 -0
  128. package/dist/internal/commands.js +147 -0
  129. package/dist/internal/documentation-bundle.d.ts +16 -0
  130. package/dist/internal/documentation-bundle.js +42 -0
  131. package/dist/internal/engine.d.ts +44 -0
  132. package/dist/internal/engine.js +399 -0
  133. package/dist/internal/errors.d.ts +14 -0
  134. package/dist/internal/errors.js +38 -0
  135. package/dist/internal/file-names.d.ts +1 -0
  136. package/dist/internal/file-names.js +7 -0
  137. package/dist/internal/launch-request.d.ts +33 -0
  138. package/dist/internal/launch-request.js +110 -0
  139. package/dist/internal/logs.d.ts +16 -0
  140. package/dist/internal/logs.js +38 -0
  141. package/dist/internal/metrics.d.ts +19 -0
  142. package/dist/internal/metrics.js +282 -0
  143. package/dist/internal/pi-assets.d.ts +13 -0
  144. package/dist/internal/pi-assets.js +94 -0
  145. package/dist/internal/resource-bindings.d.ts +13 -0
  146. package/dist/internal/resource-bindings.js +34 -0
  147. package/dist/internal/run-lease.d.ts +32 -0
  148. package/dist/internal/run-lease.js +166 -0
  149. package/dist/internal/run-log.d.ts +30 -0
  150. package/dist/internal/run-log.js +71 -0
  151. package/dist/internal/run-names.d.ts +1 -0
  152. package/dist/internal/run-names.js +144 -0
  153. package/dist/internal/run-resources.d.ts +6 -0
  154. package/dist/internal/run-resources.js +26 -0
  155. package/dist/internal/run-state.d.ts +95 -0
  156. package/dist/internal/run-state.js +323 -0
  157. package/dist/internal/run-store.d.ts +35 -0
  158. package/dist/internal/run-store.js +314 -0
  159. package/dist/internal/run.d.ts +51 -0
  160. package/dist/internal/run.js +101 -0
  161. package/dist/internal/state-store.d.ts +22 -0
  162. package/dist/internal/state-store.js +97 -0
  163. package/dist/internal/usage.d.ts +5 -0
  164. package/dist/internal/usage.js +70 -0
  165. package/dist/internal/workflow-registry.d.ts +35 -0
  166. package/dist/internal/workflow-registry.js +129 -0
  167. package/dist/plugin-loader.d.ts +55 -0
  168. package/dist/plugin-loader.js +353 -0
  169. package/dist/resources.d.ts +11 -0
  170. package/dist/resources.js +98 -0
  171. package/package.json +52 -0
@@ -0,0 +1,353 @@
1
+ // src/plugin-loader.ts
2
+ import { access, readFile, readdir } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createJiti } from "jiti/static";
6
+ import * as typeboxModule from "typebox";
7
+ import { z } from "zod";
8
+ import * as zodModule from "zod";
9
+ import * as nornModule from "@vimhead.dev/norn";
10
+ import * as nornFilesModule from "@vimhead.dev/norn/files";
11
+ import * as nornSchemaModule from "@vimhead.dev/norn/schema";
12
+ import * as nornSeerModule from "@vimhead.dev/norn/seer";
13
+ import { isWorkflowPlugin } from "@vimhead.dev/norn";
14
+ import { errorMessage, NornProjectLoadError } from "./internal/errors.js";
15
+
16
+ // ../core/src/errors.ts
17
+ function isNodeError(error) {
18
+ return error instanceof Error && "code" in error;
19
+ }
20
+
21
+ // src/plugin-loader.ts
22
+ import { NornMemoryWorkflowState } from "./internal/state-store.js";
23
+ import { NornWorkflowRegistry } from "./internal/workflow-registry.js";
24
+ import { schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
25
+ import { resolveSeerModeConfig } from "@vimhead.dev/norn/seer";
26
+ var NORN_PROJECT_FILE_NAME = "norn.project.json";
27
+ var seerModeConfigSchema = z.object({
28
+ writableRoots: z.array(z.string().min(1)).min(1)
29
+ });
30
+ var nornConfigSchema = z.object({
31
+ plugins: z.array(z.string().min(1)).default([]),
32
+ includes: z.array(z.string().min(1)).default([]),
33
+ config: z.record(z.string(), z.unknown()).default({})
34
+ });
35
+ var nornProjectConfigSchema = nornConfigSchema.extend({
36
+ version: z.literal(1).default(1),
37
+ seerMode: seerModeConfigSchema.optional()
38
+ });
39
+ async function loadNornProject(cwd) {
40
+ const { project, diagnostics } = await collectNornProject(cwd);
41
+ if (diagnostics.length > 0) throw new NornProjectLoadError({ diagnostics });
42
+ return project;
43
+ }
44
+ async function discoverNornProject(cwd) {
45
+ const { project, diagnostics } = await collectNornProject(cwd);
46
+ return { project: buildProjectInfo(project), workflows: project.registry.list(), isComplete: diagnostics.length === 0, diagnostics };
47
+ }
48
+ async function inspectNornWorkflow(input) {
49
+ const { project, diagnostics } = await collectNornProject(input.cwd);
50
+ const summary = project.registry.list().find((workflow) => workflow.id === input.workflowId);
51
+ if (!summary) {
52
+ if (diagnostics.length === 0) throw new Error(`Unknown workflow: ${input.workflowId}`);
53
+ return { workflow: null, isComplete: false, diagnostics };
54
+ }
55
+ const info = summary.plugin;
56
+ if (!info?.path || !info.configPath) throw new Error(`Loaded workflow lacks plugin source: ${input.workflowId}`);
57
+ try {
58
+ return { workflow: project.registry.inspect(input.workflowId) ?? null, isComplete: diagnostics.length === 0, diagnostics };
59
+ } catch (error) {
60
+ return { workflow: null, isComplete: false, diagnostics: [...diagnostics, createPluginDiagnostic({
61
+ source: { configPath: info.configPath, pluginPath: info.path, pluginId: info.id },
62
+ workflowId: input.workflowId,
63
+ stage: "schema",
64
+ error
65
+ })] };
66
+ }
67
+ }
68
+ function buildProjectInfo(project) {
69
+ return {
70
+ cwd: project.cwd,
71
+ projectPath: project.projectPath,
72
+ projectRoot: project.projectRoot,
73
+ configPath: project.configPath,
74
+ configRoot: project.configRoot,
75
+ configFiles: project.configFiles.map((file) => file.path),
76
+ plugins: project.pluginInfos,
77
+ seerMode: project.seerMode ?? null
78
+ };
79
+ }
80
+ async function findNornProject(cwd) {
81
+ const projectPath = await findNearestNornProject(cwd);
82
+ const projectRootConfig = await readNornProjectConfigFile(projectPath);
83
+ const configFiles = await loadIncludedNornConfigFiles(projectRootConfig, /* @__PURE__ */ new Set([projectRootConfig.path]));
84
+ return {
85
+ cwd: resolve(cwd),
86
+ projectPath: projectRootConfig.path,
87
+ projectRoot: projectRootConfig.root,
88
+ configPath: projectRootConfig.path,
89
+ configRoot: projectRootConfig.root,
90
+ config: projectRootConfig.config,
91
+ configFiles,
92
+ projectConfig: mergeProjectConfig(configFiles),
93
+ seerMode: resolveSeerModeConfig({ configPath: projectRootConfig.path, configRoot: projectRootConfig.root, seerMode: projectRootConfig.config.seerMode })
94
+ };
95
+ }
96
+ async function findNearestNornProject(cwd) {
97
+ let current = resolve(cwd);
98
+ while (true) {
99
+ const projectPath = join(current, NORN_PROJECT_FILE_NAME);
100
+ try {
101
+ await access(projectPath);
102
+ return projectPath;
103
+ } catch (error) {
104
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
105
+ }
106
+ const parent = dirname(current);
107
+ if (parent === current) throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
108
+ current = parent;
109
+ }
110
+ }
111
+ async function loadIncludedNornConfigFiles(projectFile, visitedPaths) {
112
+ const includedConfigFiles = await Promise.all(projectFile.config.includes.map(async (includePath) => {
113
+ const configPaths = await expandIncludePath(projectFile.root, includePath);
114
+ return Promise.all(configPaths.map(readNornConfigFile));
115
+ }));
116
+ const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
117
+ return [createProjectConfigEntry(projectFile), ...descendants.flat()];
118
+ }
119
+ async function loadNornConfigTree(configFile, visitedPaths) {
120
+ if (visitedPaths.has(configFile.path)) return [];
121
+ visitedPaths.add(configFile.path);
122
+ const includedConfigFiles = await Promise.all(configFile.config.includes.map(async (includePath) => {
123
+ const configPaths = await expandIncludePath(configFile.root, includePath);
124
+ return Promise.all(configPaths.map(readNornConfigFile));
125
+ }));
126
+ const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
127
+ return [configFile, ...descendants.flat()];
128
+ }
129
+ async function readNornProjectConfigFile(path) {
130
+ const config = nornProjectConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
131
+ return { path, root: dirname(path), config };
132
+ }
133
+ async function readNornConfigFile(path) {
134
+ const config = nornConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
135
+ return { path, root: dirname(path), config };
136
+ }
137
+ function createProjectConfigEntry(projectFile) {
138
+ return {
139
+ path: projectFile.path,
140
+ root: projectFile.root,
141
+ config: {
142
+ plugins: projectFile.config.plugins,
143
+ includes: projectFile.config.includes,
144
+ config: projectFile.config.config
145
+ }
146
+ };
147
+ }
148
+ async function collectNornProject(cwd) {
149
+ const project = await findNornProject(cwd);
150
+ const imported = await importWorkflowPlugins(project);
151
+ const diagnostics = [...imported.diagnostics];
152
+ const registry = new NornWorkflowRegistry();
153
+ const state = new NornMemoryWorkflowState();
154
+ const loaded = [];
155
+ const conflicts = collectPluginConflicts(imported.plugins);
156
+ for (const candidate of imported.plugins) {
157
+ const pluginConflicts = conflicts.get(candidate);
158
+ if (pluginConflicts) {
159
+ diagnostics.push(...pluginConflicts);
160
+ continue;
161
+ }
162
+ const result = registerProjectPlugin({ candidate, project, registry, state });
163
+ diagnostics.push(...result.diagnostics);
164
+ if (result.loaded) loaded.push(result.loaded);
165
+ }
166
+ return {
167
+ project: { ...project, plugins: loaded.map((entry) => entry.plugin), pluginInfos: loaded.map((entry) => entry.info), registry, workflows: registry.launchableEntries(), state },
168
+ diagnostics
169
+ };
170
+ }
171
+ function collectPluginConflicts(plugins) {
172
+ const byPluginId = /* @__PURE__ */ new Map();
173
+ const byWorkflowId = /* @__PURE__ */ new Map();
174
+ for (const candidate of plugins) {
175
+ const id = candidate.plugin.manifest.id;
176
+ byPluginId.set(id, [...byPluginId.get(id) ?? [], candidate]);
177
+ for (const workflowId of new Set(Object.values(candidate.plugin.manifest.workflows).map((workflow) => workflow.id))) {
178
+ byWorkflowId.set(workflowId, [...byWorkflowId.get(workflowId) ?? [], candidate]);
179
+ }
180
+ }
181
+ const conflicts = /* @__PURE__ */ new Map();
182
+ for (const [pluginId, candidates] of byPluginId) {
183
+ if (candidates.length < 2) continue;
184
+ for (const candidate of candidates) conflicts.set(candidate, [createPluginDiagnostic({
185
+ source: candidate.source,
186
+ workflowId: null,
187
+ stage: "duplicate",
188
+ error: new Error(`Duplicate Norn plugin id: ${pluginId} (${candidates.map((other) => other.source.pluginPath).join(", ")})`)
189
+ })]);
190
+ }
191
+ for (const [workflowId, candidates] of byWorkflowId) {
192
+ const distinctPlugins = candidates.filter((candidate) => byPluginId.get(candidate.plugin.manifest.id)?.length === 1);
193
+ if (distinctPlugins.length < 2) continue;
194
+ for (const candidate of distinctPlugins) conflicts.set(candidate, [...conflicts.get(candidate) ?? [], createPluginDiagnostic({
195
+ source: candidate.source,
196
+ workflowId,
197
+ stage: "duplicate",
198
+ error: new Error(`Workflow already registered: ${workflowId} (${distinctPlugins.map((other) => other.source.pluginPath).join(", ")})`)
199
+ })]);
200
+ }
201
+ return conflicts;
202
+ }
203
+ async function importWorkflowPlugins(project) {
204
+ const plugins = [];
205
+ const diagnostics = [];
206
+ for (const configFile of project.configFiles) {
207
+ const jiti = createJiti(pathToFileURL(configFile.path).href, { moduleCache: false, virtualModules: nornWorkflowVirtualModules() });
208
+ for (const pluginPath of configFile.config.plugins) {
209
+ const source = { configPath: configFile.path, pluginPath: resolveConfigPath(configFile.root, pluginPath), pluginId: null };
210
+ let stage = "import";
211
+ try {
212
+ const module = await jiti.import(pathToFileURL(source.pluginPath).href);
213
+ stage = "declaration";
214
+ if (!isWorkflowPlugin(module?.default)) throw new Error(`Norn plugin must be the default export: ${source.pluginPath}`);
215
+ plugins.push({ plugin: module.default, source: { ...source, pluginId: module.default.manifest.id } });
216
+ } catch (error) {
217
+ diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
218
+ }
219
+ }
220
+ }
221
+ return { plugins, diagnostics };
222
+ }
223
+ function registerProjectPlugin(input) {
224
+ const { plugin, source } = input.candidate;
225
+ const diagnostics = [];
226
+ const unregister = [];
227
+ let stage = "config";
228
+ try {
229
+ const configInput = input.project.projectConfig[plugin.manifest.id];
230
+ if (!plugin.manifest.config && configInput !== void 0) throw new Error(`Norn config provided for plugin without config schema: ${plugin.manifest.id}`);
231
+ const config = plugin.manifest.config ? plugin.manifest.config.parse(defaultConfigInput(plugin.manifest.config, configInput)) : void 0;
232
+ stage = "schema";
233
+ const info = {
234
+ id: plugin.manifest.id,
235
+ path: source.pluginPath,
236
+ configPath: source.configPath,
237
+ configSchema: plugin.manifest.config ? z.toJSONSchema(plugin.manifest.config, { io: "input" }) : null,
238
+ config
239
+ };
240
+ stage = "implementation";
241
+ const implementation = typeof plugin.implementation === "function" ? plugin.implementation({ cwd: input.project.cwd, state: input.state }) : plugin.implementation;
242
+ for (const [key, workflow] of Object.entries(plugin.manifest.workflows)) {
243
+ stage = "implementation";
244
+ try {
245
+ const workflowImplementation = implementation.workflows?.[key];
246
+ if (typeof workflowImplementation?.execute !== "function") throw new Error(`Missing implementation for workflow ${workflow.id}`);
247
+ stage = "duplicate";
248
+ if (input.registry.workflowById(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
249
+ stage = "declaration";
250
+ unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configSchema: plugin.manifest.config, config }));
251
+ } catch (error) {
252
+ diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage: error instanceof z.ZodError ? "config" : stage, error }));
253
+ }
254
+ }
255
+ if (diagnostics.length === 0) return { loaded: { plugin, info }, diagnostics };
256
+ } catch (error) {
257
+ diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
258
+ }
259
+ for (const dispose of unregister) dispose();
260
+ return { loaded: null, diagnostics };
261
+ }
262
+ function createPluginDiagnostic(input) {
263
+ return {
264
+ ...input.source,
265
+ workflowId: input.workflowId,
266
+ stage: input.stage,
267
+ message: errorMessage(input.error),
268
+ issues: input.error instanceof z.ZodError ? input.error.issues.map((issue) => ({
269
+ path: issue.path.map((part) => typeof part === "symbol" ? String(part) : part),
270
+ code: issue.code,
271
+ message: issue.message
272
+ })) : []
273
+ };
274
+ }
275
+ function workflowPluginInfo(info) {
276
+ return { id: info.id, path: info.path, configPath: info.configPath };
277
+ }
278
+ function nornWorkflowVirtualModules() {
279
+ return {
280
+ "@vimhead.dev/norn": nornModule,
281
+ "@vimhead.dev/norn/files": nornFilesModule,
282
+ "@vimhead.dev/norn/schema": nornSchemaModule,
283
+ "@vimhead.dev/norn/seer": nornSeerModule,
284
+ typebox: typeboxModule,
285
+ zod: zodModule
286
+ };
287
+ }
288
+ function mergeProjectConfig(configFiles) {
289
+ const [projectFile, ...reusableConfigFiles] = configFiles;
290
+ let reusableConfig = {};
291
+ for (const configFile of reusableConfigFiles) {
292
+ reusableConfig = mergeReusableConfigObjects(reusableConfig, configFile.config.config, ["config"]);
293
+ }
294
+ return mergeProjectConfigObjects(reusableConfig, projectFile?.config.config ?? {});
295
+ }
296
+ function mergeReusableConfigObjects(base, override, path) {
297
+ const result = { ...base };
298
+ for (const [key, overrideValue] of Object.entries(override)) {
299
+ result[key] = Object.prototype.hasOwnProperty.call(result, key) ? mergeReusableConfigValue(result[key], overrideValue, [...path, key]) : overrideValue;
300
+ }
301
+ return result;
302
+ }
303
+ function mergeReusableConfigValue(base, override, path) {
304
+ if (isPlainObject(base) && isPlainObject(override)) return mergeReusableConfigObjects(base, override, path);
305
+ if (JSON.stringify(base) === JSON.stringify(override)) return base;
306
+ throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
307
+ }
308
+ function mergeProjectConfigObjects(base, override) {
309
+ const result = { ...base };
310
+ for (const [key, overrideValue] of Object.entries(override)) {
311
+ result[key] = isPlainObject(result[key]) && isPlainObject(overrideValue) ? mergeProjectConfigObjects(result[key], overrideValue) : overrideValue;
312
+ }
313
+ return result;
314
+ }
315
+ function isPlainObject(value) {
316
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
317
+ }
318
+ function defaultConfigInput(configSchema, config) {
319
+ if (config !== void 0) return config;
320
+ return schemaType(unwrapSchema(configSchema)) === "object" ? {} : void 0;
321
+ }
322
+ async function expandIncludePath(configRoot, includePath) {
323
+ const resolvedIncludePath = resolveConfigPath(configRoot, includePath);
324
+ if (!hasGlobSegment(includePath)) return [resolvedIncludePath];
325
+ return expandGlobSegments(isAbsolute(includePath) ? sep : configRoot, splitPathSegments(includePath));
326
+ }
327
+ async function expandGlobSegments(root, segments) {
328
+ if (segments.length === 0) return [root];
329
+ const [segment, ...remainingSegments] = segments;
330
+ if (segment === "*") {
331
+ const entries = await readdir(root, { withFileTypes: true });
332
+ const expanded = await Promise.all(entries.filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => expandGlobSegments(join(root, entry.name), remainingSegments)));
333
+ return expanded.flat();
334
+ }
335
+ return expandGlobSegments(join(root, segment), remainingSegments);
336
+ }
337
+ function resolveConfigPath(configRoot, path) {
338
+ if (path.length === 0) throw new Error("Norn path must not be empty");
339
+ return isAbsolute(path) ? path : resolve(configRoot, path);
340
+ }
341
+ function hasGlobSegment(path) {
342
+ return splitPathSegments(path).includes("*");
343
+ }
344
+ function splitPathSegments(path) {
345
+ return path.split(/[\\/]+/).filter((segment) => segment.length > 0 && segment !== ".");
346
+ }
347
+ export {
348
+ NORN_PROJECT_FILE_NAME,
349
+ discoverNornProject,
350
+ findNornProject,
351
+ inspectNornWorkflow,
352
+ loadNornProject
353
+ };
@@ -0,0 +1,11 @@
1
+ import { type NornFileCoordinator } from "@vimhead.dev/norn/files";
2
+ import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
3
+ export declare class NornRunResources implements NornResources {
4
+ private readonly runRoot;
5
+ readonly files: NornFileCoordinator;
6
+ private readonly entries;
7
+ private constructor();
8
+ static initialize(runRoot: string): Promise<NornRunResources>;
9
+ ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
10
+ private initializeResource;
11
+ }
@@ -0,0 +1,98 @@
1
+ // src/resources.ts
2
+ import { mkdir as mkdir2, readFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { z } from "zod";
6
+ import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
7
+
8
+ // ../core/src/errors.ts
9
+ function isNodeError(error) {
10
+ return error instanceof Error && "code" in error;
11
+ }
12
+
13
+ // ../core/src/atomic-files.ts
14
+ import { randomUUID } from "node:crypto";
15
+ import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
16
+ import { dirname } from "node:path";
17
+ async function writeJsonAtomically(path, value) {
18
+ await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
19
+ `);
20
+ }
21
+ async function writeTextAtomically(path, content) {
22
+ await mkdir(dirname(path), { recursive: true });
23
+ let existingMode;
24
+ try {
25
+ existingMode = (await stat(path)).mode & 511;
26
+ } catch (error) {
27
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
28
+ }
29
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
30
+ try {
31
+ await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
32
+ if (existingMode !== void 0) await chmod(tmpPath, existingMode);
33
+ await rename(tmpPath, path);
34
+ } catch (error) {
35
+ await rm(tmpPath, { force: true }).catch(() => void 0);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ // src/resources.ts
41
+ var definitionSchema = z.strictObject({ name: z.string(), kind: z.string().min(1), configuration: z.json() });
42
+ var recordSchema = definitionSchema.extend({ isInitialized: z.boolean() });
43
+ var NornRunResources = class _NornRunResources {
44
+ constructor(runRoot) {
45
+ this.runRoot = runRoot;
46
+ this.files = createRunFileCoordinator(runRoot);
47
+ }
48
+ runRoot;
49
+ files;
50
+ entries = /* @__PURE__ */ new Map();
51
+ static async initialize(runRoot) {
52
+ const resources = new _NornRunResources(resolve(runRoot));
53
+ await mkdir2(join(resources.runRoot, "current", "resources"), { recursive: true });
54
+ return resources;
55
+ }
56
+ async ensure(definition) {
57
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
58
+ const identity = definitionSchema.parse({ name: definition.name, kind: definition.kind, configuration: definition.configuration });
59
+ const existing = this.entries.get(definition.name);
60
+ if (existing) {
61
+ if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
62
+ return existing.value;
63
+ }
64
+ const value = this.initializeResource(definition, identity);
65
+ const entry = { definition: identity, value };
66
+ this.entries.set(definition.name, entry);
67
+ try {
68
+ return await value;
69
+ } catch (error) {
70
+ if (this.entries.get(definition.name) === entry) this.entries.delete(definition.name);
71
+ throw error;
72
+ }
73
+ }
74
+ async initializeResource(definition, identity) {
75
+ const directory = join(this.runRoot, "current", "resources", definition.name);
76
+ await mkdir2(directory, { recursive: true });
77
+ return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
78
+ let stored;
79
+ try {
80
+ stored = recordSchema.parse(JSON.parse(await readFile(path, "utf8")));
81
+ } catch (error) {
82
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
83
+ }
84
+ if (stored) {
85
+ const { isInitialized: _isInitialized, ...storedIdentity } = stored;
86
+ if (!isDeepStrictEqual(storedIdentity, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
87
+ } else {
88
+ await writeJsonAtomically(path, { ...identity, isInitialized: false });
89
+ }
90
+ const value = await definition.initialize({ directory, files: this.files, mode: stored?.isInitialized ? "open" : "create" });
91
+ if (!stored?.isInitialized) await writeJsonAtomically(path, { ...identity, isInitialized: true });
92
+ return value;
93
+ });
94
+ }
95
+ };
96
+ export {
97
+ NornRunResources
98
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@vimhead.dev/norn-cli",
3
+ "version": "0.1.0-tip.0",
4
+ "description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/vimhead/norn.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://github.com/vimhead/norn#readme",
13
+ "engines": {
14
+ "node": ">=22.19.0"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "tag": "tip"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "dist",
23
+ "assets",
24
+ "README.md"
25
+ ],
26
+ "bin": {
27
+ "norn": "./bin/norn.mjs"
28
+ },
29
+ "exports": {
30
+ "./client": {
31
+ "types": "./dist/client.d.ts",
32
+ "import": "./dist/client.js"
33
+ },
34
+ "./documentation": {
35
+ "types": "./dist/documentation.d.ts",
36
+ "import": "./dist/documentation.js"
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@vimhead.dev/norn-core": "0.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@earendil-works/pi-coding-agent": "0.85.1",
44
+ "jiti": "^2.7.0",
45
+ "typebox": "^1.3.17",
46
+ "zod": "^4.4.3",
47
+ "@vimhead.dev/norn": "0.1.0-tip.0"
48
+ },
49
+ "scripts": {
50
+ "build": "node ../../scripts/build-package.ts"
51
+ }
52
+ }