@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,400 +0,0 @@
1
- import * as nornModule from "@vimhead.dev/norn";
2
- import { isWorkflowPlugin, type NornDispose, type NornPluginDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectPluginInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowPlugin, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
3
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
- import * as nornFilesModule from "@vimhead.dev/norn/files";
5
- import * as nornSchemaModule from "@vimhead.dev/norn/schema";
6
- import { inspectSchema } from "@vimhead.dev/norn/schema";
7
- import { createJiti } from "jiti/static";
8
- import { access, readdir, readFile } from "node:fs/promises";
9
- import { dirname, isAbsolute, join, resolve, sep } from "node:path";
10
- import { pathToFileURL } from "node:url";
11
- import * as typeboxModule from "typebox";
12
- import { Type, type StaticDecode } from "typebox";
13
- import * as typeboxCompileModule from "typebox/compile";
14
- import * as typeboxSchemaModule from "typebox/schema";
15
- import * as typeboxValueModule from "typebox/value";
16
- import { AssertError, Value } from "typebox/value";
17
- import { errorMessage, NornProjectLoadError } from "./internal/errors.ts";
18
- import { NornMemoryWorkflowState } from "./internal/state-store.ts";
19
- import { decodePluginConfiguration, NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
20
-
21
- export const NORN_PROJECT_FILE_NAME = "norn.project.json";
22
-
23
- const nornConfigSchema = Type.Object({
24
- plugins: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
25
- includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
26
- config: Type.Record(Type.String(), Type.Unknown(), { default: {} }),
27
- });
28
- const nornProjectConfigSchema = Type.Object({
29
- ...nornConfigSchema.properties,
30
- version: Type.Literal(1, { default: 1 }),
31
- });
32
-
33
- type NornConfig = StaticDecode<typeof nornConfigSchema>;
34
- type NornProjectConfig = StaticDecode<typeof nornProjectConfigSchema>;
35
-
36
- type NornConfigFile = {
37
- readonly path: string;
38
- readonly root: string;
39
- readonly config: NornConfig;
40
- };
41
-
42
- export type NornProject = {
43
- readonly cwd: string;
44
- readonly projectPath: string;
45
- readonly projectRoot: string;
46
- readonly configPath: string;
47
- readonly configRoot: string;
48
- readonly config: NornProjectConfig;
49
- readonly configFiles: readonly NornConfigFile[];
50
- readonly projectConfig: Record<string, unknown>;
51
- };
52
-
53
- export type NornLoadedProject = NornProject & {
54
- readonly plugins: readonly NornWorkflowPlugin[];
55
- readonly pluginInfos: readonly NornProjectPluginInfo[];
56
- readonly registry: NornWorkflowRegistry;
57
- readonly workflows: readonly NornRegisteredWorkflow[];
58
- readonly state: NornMemoryWorkflowState;
59
- };
60
-
61
- type LoadedNornWorkflowPlugin = {
62
- readonly plugin: NornWorkflowPlugin;
63
- readonly info: NornProjectPluginInfo;
64
- };
65
-
66
- export async function loadNornProject(cwd: string): Promise<NornLoadedProject> {
67
- const { project, diagnostics } = await collectNornProject(cwd);
68
- if (diagnostics.length > 0) throw new NornProjectLoadError({ diagnostics });
69
- return project;
70
- }
71
-
72
- export async function discoverNornProject(cwd: string): Promise<NornProjectInspection & NornWorkflowCatalogInfo> {
73
- const { project, diagnostics } = await collectNornProject(cwd);
74
- return { project: buildProjectInfo(project), workflows: project.registry.list(), isComplete: diagnostics.length === 0, diagnostics };
75
- }
76
-
77
- export async function inspectNornWorkflow(input: { readonly cwd: string; readonly workflowId: string }): Promise<NornWorkflowInspection> {
78
- const { project, diagnostics } = await collectNornProject(input.cwd);
79
- const summary = project.registry.list().find(workflow => workflow.id === input.workflowId);
80
- if (!summary) {
81
- if (diagnostics.length === 0) throw new Error(`Unknown workflow: ${input.workflowId}`);
82
- return { workflow: null, isComplete: false, diagnostics };
83
- }
84
- const info = summary.plugin;
85
- if (!info?.path || !info.configPath) throw new Error(`Loaded workflow lacks plugin source: ${input.workflowId}`);
86
- try {
87
- return { workflow: project.registry.inspect(input.workflowId) ?? null, isComplete: diagnostics.length === 0, diagnostics };
88
- } catch (error) {
89
- return { workflow: null, isComplete: false, diagnostics: [...diagnostics, createPluginDiagnostic({
90
- source: { configPath: info.configPath, pluginPath: info.path, pluginId: info.id },
91
- workflowId: input.workflowId, stage: "schema", error,
92
- })] };
93
- }
94
- }
95
-
96
- function buildProjectInfo(project: NornLoadedProject): NornProjectInfo {
97
- return {
98
- cwd: project.cwd, projectPath: project.projectPath, projectRoot: project.projectRoot,
99
- configPath: project.configPath, configRoot: project.configRoot,
100
- configFiles: project.configFiles.map(file => file.path), plugins: project.pluginInfos,
101
- };
102
- }
103
-
104
- export async function findNornProject(cwd: string): Promise<NornProject> {
105
- const projectPath = await findNearestNornProject(cwd);
106
- const projectRootConfig = await readNornProjectConfigFile(projectPath);
107
- const configFiles = await loadIncludedNornConfigFiles(projectRootConfig, new Set([projectRootConfig.path]));
108
- return {
109
- cwd: resolve(cwd),
110
- projectPath: projectRootConfig.path,
111
- projectRoot: projectRootConfig.root,
112
- configPath: projectRootConfig.path,
113
- configRoot: projectRootConfig.root,
114
- config: projectRootConfig.config,
115
- configFiles,
116
- projectConfig: mergeProjectConfig(configFiles),
117
- };
118
- }
119
-
120
- async function findNearestNornProject(cwd: string): Promise<string> {
121
- let current = resolve(cwd);
122
- while (true) {
123
- const projectPath = join(current, NORN_PROJECT_FILE_NAME);
124
- try {
125
- await access(projectPath);
126
- return projectPath;
127
- } catch (error) {
128
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
129
- }
130
- const parent = dirname(current);
131
- if (parent === current) throw new Error(`Could not find ${NORN_PROJECT_FILE_NAME} from ${cwd}`);
132
- current = parent;
133
- }
134
- }
135
-
136
- type NornProjectConfigFile = {
137
- readonly path: string;
138
- readonly root: string;
139
- readonly config: NornProjectConfig;
140
- };
141
-
142
- async function loadIncludedNornConfigFiles(projectFile: NornProjectConfigFile, visitedPaths: Set<string>): Promise<NornConfigFile[]> {
143
- const includedConfigFiles = await Promise.all(projectFile.config.includes.map(async (includePath) => {
144
- const configPaths = await expandIncludePath(projectFile.root, includePath);
145
- return Promise.all(configPaths.map(readNornConfigFile));
146
- }));
147
- const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
148
- return [createProjectConfigEntry(projectFile), ...descendants.flat()];
149
- }
150
-
151
- async function loadNornConfigTree(configFile: NornConfigFile, visitedPaths: Set<string>): Promise<NornConfigFile[]> {
152
- if (visitedPaths.has(configFile.path)) return [];
153
- visitedPaths.add(configFile.path);
154
- const includedConfigFiles = await Promise.all(configFile.config.includes.map(async (includePath) => {
155
- const configPaths = await expandIncludePath(configFile.root, includePath);
156
- return Promise.all(configPaths.map(readNornConfigFile));
157
- }));
158
- const descendants = await Promise.all(includedConfigFiles.flat().map((includedConfigFile) => loadNornConfigTree(includedConfigFile, visitedPaths)));
159
- return [configFile, ...descendants.flat()];
160
- }
161
-
162
- async function readNornProjectConfigFile(path: string): Promise<NornProjectConfigFile> {
163
- const config = Value.Parse(nornProjectConfigSchema, Value.Default(nornProjectConfigSchema, JSON.parse(await readFile(path, "utf8"))));
164
- return { path, root: dirname(path), config };
165
- }
166
-
167
- async function readNornConfigFile(path: string): Promise<NornConfigFile> {
168
- const config = Value.Parse(nornConfigSchema, Value.Default(nornConfigSchema, JSON.parse(await readFile(path, "utf8"))));
169
- return { path, root: dirname(path), config };
170
- }
171
-
172
- function createProjectConfigEntry(projectFile: NornProjectConfigFile): NornConfigFile {
173
- return {
174
- path: projectFile.path,
175
- root: projectFile.root,
176
- config: {
177
- plugins: projectFile.config.plugins,
178
- includes: projectFile.config.includes,
179
- config: projectFile.config.config,
180
- },
181
- };
182
- }
183
-
184
- type NornPluginSource = Pick<NornPluginDiagnostic, "configPath" | "pluginPath" | "pluginId">;
185
- type ImportedNornPlugin = { readonly plugin: NornWorkflowPlugin; readonly source: NornPluginSource };
186
-
187
- async function collectNornProject(cwd: string): Promise<{ readonly project: NornLoadedProject; readonly diagnostics: NornPluginDiagnostic[] }> {
188
- const project = await findNornProject(cwd);
189
- const imported = await importWorkflowPlugins(project);
190
- const diagnostics = [...imported.diagnostics];
191
- const registry = new NornWorkflowRegistry();
192
- const state = new NornMemoryWorkflowState();
193
- const loaded: LoadedNornWorkflowPlugin[] = [];
194
- const conflicts = collectPluginConflicts(imported.plugins);
195
- for (const candidate of imported.plugins) {
196
- const pluginConflicts = conflicts.get(candidate);
197
- if (pluginConflicts) {
198
- diagnostics.push(...pluginConflicts);
199
- continue;
200
- }
201
- const result = registerProjectPlugin({ candidate, project, registry, state });
202
- diagnostics.push(...result.diagnostics);
203
- if (result.loaded) loaded.push(result.loaded);
204
- }
205
- return {
206
- project: { ...project, plugins: loaded.map(entry => entry.plugin), pluginInfos: loaded.map(entry => entry.info), registry, workflows: registry.launchableEntries(), state },
207
- diagnostics,
208
- };
209
- }
210
-
211
- function collectPluginConflicts(plugins: readonly ImportedNornPlugin[]): Map<ImportedNornPlugin, NornPluginDiagnostic[]> {
212
- const byPluginId = new Map<string, ImportedNornPlugin[]>();
213
- const byWorkflowId = new Map<string, ImportedNornPlugin[]>();
214
- for (const candidate of plugins) {
215
- const id = candidate.plugin.manifest.id;
216
- byPluginId.set(id, [...(byPluginId.get(id) ?? []), candidate]);
217
- for (const workflowId of new Set(Object.values(candidate.plugin.manifest.workflows).map(workflow => workflow.id))) {
218
- byWorkflowId.set(workflowId, [...(byWorkflowId.get(workflowId) ?? []), candidate]);
219
- }
220
- }
221
- const conflicts = new Map<ImportedNornPlugin, NornPluginDiagnostic[]>();
222
- for (const [pluginId, candidates] of byPluginId) {
223
- if (candidates.length < 2) continue;
224
- for (const candidate of candidates) conflicts.set(candidate, [createPluginDiagnostic({
225
- source: candidate.source, workflowId: null, stage: "duplicate",
226
- error: new Error(`Duplicate Norn plugin id: ${pluginId} (${candidates.map(other => other.source.pluginPath).join(", ")})`),
227
- })]);
228
- }
229
- for (const [workflowId, candidates] of byWorkflowId) {
230
- const distinctPlugins = candidates.filter(candidate => byPluginId.get(candidate.plugin.manifest.id)?.length === 1);
231
- if (distinctPlugins.length < 2) continue;
232
- for (const candidate of distinctPlugins) conflicts.set(candidate, [...(conflicts.get(candidate) ?? []), createPluginDiagnostic({
233
- source: candidate.source, workflowId, stage: "duplicate",
234
- error: new Error(`Workflow already registered: ${workflowId} (${distinctPlugins.map(other => other.source.pluginPath).join(", ")})`),
235
- })]);
236
- }
237
- return conflicts;
238
- }
239
-
240
- async function importWorkflowPlugins(project: NornProject): Promise<{ readonly plugins: ImportedNornPlugin[]; readonly diagnostics: NornPluginDiagnostic[] }> {
241
- const plugins: ImportedNornPlugin[] = [];
242
- const diagnostics: NornPluginDiagnostic[] = [];
243
- for (const configFile of project.configFiles) {
244
- const jiti = createJiti(pathToFileURL(configFile.path).href, { moduleCache: false, virtualModules: nornWorkflowVirtualModules() });
245
- for (const pluginPath of configFile.config.plugins) {
246
- const source: NornPluginSource = { configPath: configFile.path, pluginPath: resolveConfigPath(configFile.root, pluginPath), pluginId: null };
247
- let stage: NornPluginDiagnostic["stage"] = "import";
248
- try {
249
- const module = await jiti.import(pathToFileURL(source.pluginPath).href) as { default?: unknown };
250
- stage = "declaration";
251
- if (!isWorkflowPlugin(module?.default)) throw new Error(`Norn plugin must be the default export: ${source.pluginPath}`);
252
- plugins.push({ plugin: module.default, source: { ...source, pluginId: module.default.manifest.id } });
253
- } catch (error) {
254
- diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
255
- }
256
- }
257
- }
258
- return { plugins, diagnostics };
259
- }
260
-
261
- function registerProjectPlugin(input: {
262
- readonly candidate: ImportedNornPlugin;
263
- readonly project: NornProject;
264
- readonly registry: NornWorkflowRegistry;
265
- readonly state: NornMemoryWorkflowState;
266
- }): { readonly loaded: LoadedNornWorkflowPlugin | null; readonly diagnostics: NornPluginDiagnostic[] } {
267
- const { plugin, source } = input.candidate;
268
- const diagnostics: NornPluginDiagnostic[] = [];
269
- const unregister: NornDispose[] = [];
270
- let stage: NornPluginDiagnostic["stage"] = "config";
271
- try {
272
- const configInput = input.project.projectConfig[plugin.manifest.id];
273
- const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: configInput });
274
- stage = "schema";
275
- const info: NornProjectPluginInfo = {
276
- id: plugin.manifest.id, path: source.pluginPath, configPath: source.configPath,
277
- configSchema: plugin.manifest.config ? inspectSchema(plugin.manifest.config) : null, config: configuration?.value,
278
- };
279
- stage = "implementation";
280
- const implementation = typeof plugin.implementation === "function" ? plugin.implementation({ cwd: input.project.cwd, state: input.state }) : plugin.implementation;
281
- for (const [key, workflow] of Object.entries(plugin.manifest.workflows)) {
282
- stage = "implementation";
283
- try {
284
- const workflowImplementation = implementation.workflows?.[key];
285
- if (typeof workflowImplementation?.execute !== "function") throw new Error(`Missing implementation for workflow ${workflow.id}`);
286
- stage = "duplicate";
287
- if (input.registry.workflowById(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
288
- stage = "declaration";
289
- unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configuration }));
290
- } catch (error) {
291
- diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage, error }));
292
- }
293
- }
294
- if (diagnostics.length === 0) return { loaded: { plugin, info }, diagnostics };
295
- } catch (error) {
296
- diagnostics.push(createPluginDiagnostic({ source, workflowId: null, stage, error }));
297
- }
298
- for (const dispose of unregister) dispose();
299
- return { loaded: null, diagnostics };
300
- }
301
-
302
- function createPluginDiagnostic(input: {
303
- readonly source: NornPluginSource;
304
- readonly workflowId: string | null;
305
- readonly stage: NornPluginDiagnostic["stage"];
306
- readonly error: unknown;
307
- }): NornPluginDiagnostic {
308
- return {
309
- ...input.source, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error),
310
- issues: input.error instanceof AssertError ? input.error.cause.errors : [],
311
- };
312
- }
313
-
314
- function workflowPluginInfo(info: NornProjectPluginInfo): NornWorkflowPluginInfo {
315
- return { id: info.id, path: info.path, configPath: info.configPath };
316
- }
317
-
318
- function nornWorkflowVirtualModules(): Record<string, unknown> {
319
- return {
320
- "@vimhead.dev/norn": nornModule,
321
- "@vimhead.dev/norn/files": nornFilesModule,
322
- "@vimhead.dev/norn/schema": nornSchemaModule,
323
- typebox: typeboxModule,
324
- "typebox/value": typeboxValueModule,
325
- "typebox/compile": typeboxCompileModule,
326
- "typebox/schema": typeboxSchemaModule,
327
- };
328
- }
329
-
330
- function mergeProjectConfig(configFiles: readonly NornConfigFile[]): Record<string, unknown> {
331
- const [projectFile, ...reusableConfigFiles] = configFiles;
332
- let reusableConfig: Record<string, unknown> = {};
333
- for (const configFile of reusableConfigFiles) {
334
- reusableConfig = mergeReusableConfigObjects(reusableConfig, configFile.config.config, ["config"]);
335
- }
336
- return mergeProjectConfigObjects(reusableConfig, projectFile?.config.config ?? {});
337
- }
338
-
339
- function mergeReusableConfigObjects(base: Record<string, unknown>, override: Record<string, unknown>, path: readonly string[]): Record<string, unknown> {
340
- const result: Record<string, unknown> = { ...base };
341
- for (const [key, overrideValue] of Object.entries(override)) {
342
- result[key] = Object.prototype.hasOwnProperty.call(result, key)
343
- ? mergeReusableConfigValue(result[key], overrideValue, [...path, key])
344
- : overrideValue;
345
- }
346
- return result;
347
- }
348
-
349
- function mergeReusableConfigValue(base: unknown, override: unknown, path: readonly string[]): unknown {
350
- if (isPlainObject(base) && isPlainObject(override)) return mergeReusableConfigObjects(base, override, path);
351
- if (JSON.stringify(base) === JSON.stringify(override)) return base;
352
- throw new Error(`Conflicting Norn reusable config at ${path.join(".")}`);
353
- }
354
-
355
- function mergeProjectConfigObjects(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
356
- const result: Record<string, unknown> = { ...base };
357
- for (const [key, overrideValue] of Object.entries(override)) {
358
- result[key] = isPlainObject(result[key]) && isPlainObject(overrideValue)
359
- ? mergeProjectConfigObjects(result[key], overrideValue)
360
- : overrideValue;
361
- }
362
- return result;
363
- }
364
-
365
- function isPlainObject(value: unknown): value is Record<string, unknown> {
366
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
367
- }
368
-
369
- async function expandIncludePath(configRoot: string, includePath: string): Promise<string[]> {
370
- const resolvedIncludePath = resolveConfigPath(configRoot, includePath);
371
- if (!hasGlobSegment(includePath)) return [resolvedIncludePath];
372
- return expandGlobSegments(isAbsolute(includePath) ? sep : configRoot, splitPathSegments(includePath));
373
- }
374
-
375
- async function expandGlobSegments(root: string, segments: readonly string[]): Promise<string[]> {
376
- if (segments.length === 0) return [root];
377
- const [segment, ...remainingSegments] = segments;
378
- if (segment === "*") {
379
- const entries = await readdir(root, { withFileTypes: true });
380
- const expanded = await Promise.all(entries
381
- .filter((entry) => entry.isDirectory())
382
- .sort((left, right) => left.name.localeCompare(right.name))
383
- .map((entry) => expandGlobSegments(join(root, entry.name), remainingSegments)));
384
- return expanded.flat();
385
- }
386
- return expandGlobSegments(join(root, segment), remainingSegments);
387
- }
388
-
389
- function resolveConfigPath(configRoot: string, path: string): string {
390
- if (path.length === 0) throw new Error("Norn path must not be empty");
391
- return isAbsolute(path) ? path : resolve(configRoot, path);
392
- }
393
-
394
- function hasGlobSegment(path: string): boolean {
395
- return splitPathSegments(path).includes("*");
396
- }
397
-
398
- function splitPathSegments(path: string): string[] {
399
- return path.split(/[\\/]+/).filter((segment) => segment.length > 0 && segment !== ".");
400
- }
@@ -1,69 +0,0 @@
1
- import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
2
- import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
3
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
- import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
5
- import { jsonValueSchema } from "@vimhead.dev/norn/schema";
6
- import { mkdir, readFile } from "node:fs/promises";
7
- import { join, resolve } from "node:path";
8
- import { isDeepStrictEqual } from "node:util";
9
- import { Type, type StaticDecode } from "typebox";
10
- import { Value } from "typebox/value";
11
-
12
- const definitionSchema = Type.Object({ name: Type.String(), kind: Type.String({ minLength: 1 }), configuration: jsonValueSchema }, { additionalProperties: false });
13
- const recordSchema = Type.Object({ ...definitionSchema.properties, isInitialized: Type.Boolean() }, { additionalProperties: false });
14
-
15
- export class NornRunResources implements NornResources {
16
- readonly files: NornFileCoordinator;
17
- private readonly entries = new Map<string, { readonly definition: StaticDecode<typeof definitionSchema>; readonly value: Promise<unknown> }>();
18
-
19
- private constructor(private readonly runRoot: string) {
20
- this.files = createRunFileCoordinator(runRoot);
21
- }
22
-
23
- static async initialize(runRoot: string): Promise<NornRunResources> {
24
- const resources = new NornRunResources(resolve(runRoot));
25
- await mkdir(join(resources.runRoot, "current", "resources"), { recursive: true });
26
- return resources;
27
- }
28
-
29
- async ensure<T>(definition: NornResourceDefinition<T>): Promise<T> {
30
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
31
- const identity = structuredClone(Value.Parse(definitionSchema, { name: definition.name, kind: definition.kind, configuration: definition.configuration }));
32
- const existing = this.entries.get(definition.name);
33
- if (existing) {
34
- if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
35
- return existing.value as Promise<T>;
36
- }
37
- const value = this.initializeResource(definition, identity);
38
- const entry = { definition: identity, value };
39
- this.entries.set(definition.name, entry);
40
- try {
41
- return await value;
42
- } catch (error) {
43
- if (this.entries.get(definition.name) === entry) this.entries.delete(definition.name);
44
- throw error;
45
- }
46
- }
47
-
48
- private async initializeResource<T>(definition: NornResourceDefinition<T>, identity: StaticDecode<typeof definitionSchema>): Promise<T> {
49
- const directory = join(this.runRoot, "current", "resources", definition.name);
50
- await mkdir(directory, { recursive: true });
51
- return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
52
- let stored: StaticDecode<typeof recordSchema> | undefined;
53
- try {
54
- stored = Value.Parse(recordSchema, JSON.parse(await readFile(path, "utf8")));
55
- } catch (error) {
56
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
57
- }
58
- if (stored) {
59
- const { isInitialized: _isInitialized, ...storedIdentity } = stored;
60
- if (!isDeepStrictEqual(storedIdentity, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
61
- } else {
62
- await writeJsonAtomically(path, { ...identity, isInitialized: false });
63
- }
64
- const value = await definition.initialize({ directory, files: this.files, mode: stored?.isInitialized ? "open" : "create" });
65
- if (!stored?.isInitialized) await writeJsonAtomically(path, { ...identity, isInitialized: true });
66
- return value;
67
- });
68
- }
69
- }
@@ -1,11 +0,0 @@
1
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
-
3
- export type NornAgentResourceBinding = {
4
- readonly tools: readonly ToolDefinition[];
5
- dispose(): Promise<void>;
6
- };
7
-
8
- export type NornAgentResourceAdapter = {
9
- readonly name: string;
10
- bind(context: { readonly runId: string; readonly label: string }): Promise<NornAgentResourceBinding>;
11
- };
@@ -1,20 +0,0 @@
1
- import type { NornFileCoordinator } from "./files.ts";
2
- import type { NornJsonValue } from "./schema.ts";
3
-
4
- export type NornResourceContext = {
5
- readonly mode: "create" | "open";
6
- readonly directory: string;
7
- readonly files: NornFileCoordinator;
8
- };
9
-
10
- export type NornResourceDefinition<T> = {
11
- readonly name: string;
12
- readonly kind: string;
13
- readonly configuration: NornJsonValue;
14
- initialize(context: NornResourceContext): Promise<T>;
15
- };
16
-
17
- export type NornResources = {
18
- readonly files: NornFileCoordinator;
19
- ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
20
- };
@@ -1,75 +0,0 @@
1
- import { defineTool } from "@earendil-works/pi-coding-agent";
2
- import { createHash } from "node:crypto";
3
- import { Type } from "typebox";
4
- import type { NornAgentResourceAdapter } from "./agent-resource-adapter.ts";
5
- import type { NornWorkflowState, NornWorkflowStateDefinition } from "./api.ts";
6
- import { inspectSchema } from "./schema.ts";
7
-
8
- export type NornStateFieldAccess = {
9
- readonly field: NornWorkflowStateDefinition;
10
- readonly access: "read" | "write" | "read-write";
11
- };
12
-
13
- const pageParameters = {
14
- offset: Type.Integer({ minimum: 0, description: "Zero-based UTF-16 offset into the serialized JSON. Start at 0." }),
15
- limit: Type.Integer({ minimum: 1, maximum: 10000 }),
16
- };
17
-
18
- export function StateAdapter(input: { readonly state: NornWorkflowState; readonly fields: readonly NornStateFieldAccess[] }): NornAgentResourceAdapter {
19
- const fields = new Map(input.fields.map((grant) => [grant.field.id, grant]));
20
- if (fields.size !== input.fields.length || fields.size === 0) throw new Error("State attachment requires unique, explicitly selected fields");
21
- const selectField = (key: string, access: "read" | "write") => {
22
- const grant = fields.get(key);
23
- if (!grant || (grant.access !== access && grant.access !== "read-write")) throw new Error(`State ${access} is not attached: ${key}`);
24
- return grant.field;
25
- };
26
- return {
27
- name: "norn.state",
28
- async bind() {
29
- return {
30
- tools: [
31
- defineTool({
32
- name: "norn_state_list",
33
- label: "Attached workflow state",
34
- description: "List only attached workflow-state field IDs, permissions and value schemas. JSON is paginated; use nextOffset until null.",
35
- parameters: Type.Object(pageParameters),
36
- async execute(_id, params) {
37
- return serializePage({ value: [...fields.values()].map(({ field, access }) => ({ id: field.id, access, schema: inspectSchema(field.schema) })), ...params });
38
- },
39
- }),
40
- defineTool({
41
- name: "norn_state_get",
42
- label: "Read workflow state",
43
- description: "Read a selected workflow-state field. Unset fields return isSet:false. JSON is paginated; concurrent writes can change later pages, so compare revision before combining pages.",
44
- parameters: Type.Object({ key: Type.String(), ...pageParameters }),
45
- async execute(_id, params) {
46
- const value = await input.state.getOptional(selectField(params.key, "read"));
47
- return serializePage({ value: value === undefined ? { isSet: false } : { isSet: true, value }, ...params });
48
- },
49
- }),
50
- defineTool({
51
- name: "norn_state_set",
52
- label: "Write workflow state",
53
- description: "Set an explicitly writable workflow-state field. Validate the value against its schema from norn_state_list. A get followed by set is not a transaction.",
54
- parameters: Type.Object({ key: Type.String(), value: Type.Unknown() }),
55
- async execute(_id, params, signal) {
56
- signal?.throwIfAborted();
57
- const field = selectField(params.key, "write");
58
- await input.state.set(field, params.value);
59
- return { content: [{ type: "text", text: "Workflow state saved." }], details: {} };
60
- },
61
- }),
62
- ],
63
- async dispose() {},
64
- };
65
- },
66
- };
67
- }
68
-
69
- function serializePage(input: { readonly value: unknown; readonly offset: number; readonly limit: number }) {
70
- const serialized = JSON.stringify(input.value);
71
- if (!Number.isInteger(input.offset) || input.offset < 0 || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 10000) throw new Error("Invalid state output page");
72
- const end = Math.min(serialized.length, input.offset + input.limit);
73
- const details = { text: serialized.slice(input.offset, end), nextOffset: end < serialized.length ? end : null, revision: createHash("sha256").update(serialized).digest("hex") };
74
- return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
75
- }
@@ -1,10 +0,0 @@
1
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
2
- import type { NornArtifactRef } from "@vimhead.dev/norn";
3
- export declare class NornArtifacts {
4
- private readonly artifactsRoot;
5
- private readonly files;
6
- constructor(artifactsRoot: string, files: NornFileCoordinator);
7
- write(path: string, content: string): Promise<NornArtifactRef>;
8
- read(ref: NornArtifactRef): Promise<string>;
9
- private resolveArtifactPath;
10
- }
@@ -1,29 +0,0 @@
1
- // src/internal/artifacts.ts
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
- var NornArtifacts = class {
4
- constructor(artifactsRoot, files) {
5
- this.artifactsRoot = artifactsRoot;
6
- this.files = files;
7
- }
8
- artifactsRoot;
9
- files;
10
- async write(path, content) {
11
- await this.files.writeText(this.resolveArtifactPath(path), content);
12
- return { path };
13
- }
14
- async read(ref) {
15
- return this.files.readText(this.resolveArtifactPath(ref.path));
16
- }
17
- resolveArtifactPath(path) {
18
- if (isAbsolute(path)) throw new Error(`Artifact path must be relative: ${path}`);
19
- const resolvedPath = resolve(this.artifactsRoot, path);
20
- const relativePath = relative(this.artifactsRoot, resolvedPath);
21
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
22
- throw new Error(`Artifact path escapes artifacts directory: ${path}`);
23
- }
24
- return resolvedPath;
25
- }
26
- };
27
- export {
28
- NornArtifacts
29
- };
@@ -1,13 +0,0 @@
1
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
- import type { NornAgentResourceAdapter } from "@vimhead.dev/norn";
3
- export declare class NornSessionResourceBindings {
4
- readonly tools: ToolDefinition[];
5
- private readonly bindings;
6
- bind(input: {
7
- readonly adapters: readonly NornAgentResourceAdapter[];
8
- readonly runId: string;
9
- readonly label: string;
10
- readonly reservedTools: readonly string[];
11
- }): Promise<void>;
12
- dispose(): Promise<void>;
13
- }
@@ -1,34 +0,0 @@
1
- // src/internal/resource-bindings.ts
2
- var NornSessionResourceBindings = class {
3
- tools = [];
4
- bindings = [];
5
- async bind(input) {
6
- const adapterNames = /* @__PURE__ */ new Set();
7
- const tools = new Set(input.reservedTools);
8
- for (const adapter of input.adapters) {
9
- if (adapterNames.has(adapter.name)) throw new Error(`Duplicate resource adapter: ${adapter.name}`);
10
- adapterNames.add(adapter.name);
11
- const binding = await adapter.bind({ runId: input.runId, label: input.label });
12
- this.bindings.push(binding);
13
- for (const tool of binding.tools) {
14
- if (tools.has(tool.name)) throw new Error(`Resource tool name collision: ${tool.name}`);
15
- tools.add(tool.name);
16
- this.tools.push(tool);
17
- }
18
- }
19
- }
20
- async dispose() {
21
- const errors = [];
22
- for (const binding of this.bindings.splice(0).reverse()) {
23
- try {
24
- await binding.dispose();
25
- } catch (error) {
26
- errors.push(error);
27
- }
28
- }
29
- if (errors.length) throw new AggregateError(errors, "Resource binding cleanup failed");
30
- }
31
- };
32
- export {
33
- NornSessionResourceBindings
34
- };
@@ -1,6 +0,0 @@
1
- import { NornRunResources } from "../resources.ts";
2
- import { NornJsonWorkflowState } from "./state-store.ts";
3
- export declare function initializeRunResources(runRoot: string): Promise<{
4
- readonly resources: NornRunResources;
5
- readonly state: NornJsonWorkflowState;
6
- }>;