@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
@@ -2,7 +2,11 @@ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
4
4
  import type { WriteStream } from "node:fs";
5
- import type { NornCommandRunInput, NornCommandRunResult, NornLogRef } from "@vimhead.dev/norn";
5
+ import type {
6
+ NornCommandRunInput,
7
+ NornCommandRunResult,
8
+ NornLogRef,
9
+ } from "@vimhead.dev/norn";
6
10
  import { errorMessage } from "./errors.ts";
7
11
  import { safeFileName } from "./file-names.ts";
8
12
  import type { NornRunLogs } from "./logs.ts";
@@ -21,9 +25,25 @@ export class NornCommandRunner {
21
25
  const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
22
26
  const startedAtMs = Date.now();
23
27
  const invocationId = randomUUID();
24
- const stdoutRef = commandLog({ label: commandInput.label, invocationId, stream: "stdout" });
25
- const stderrRef = commandLog({ label: commandInput.label, invocationId, stream: "stderr" });
26
- await this.input.logger.record({ type: "command.started", invocationId, label: commandInput.label, command: commandInput.command, cwd, stdoutLogId: stdoutRef.id, stderrLogId: stderrRef.id });
28
+ const stdoutRef = commandLog({
29
+ label: commandInput.label,
30
+ invocationId,
31
+ stream: "stdout",
32
+ });
33
+ const stderrRef = commandLog({
34
+ label: commandInput.label,
35
+ invocationId,
36
+ stream: "stderr",
37
+ });
38
+ await this.input.logger.record({
39
+ type: "command.started",
40
+ invocationId,
41
+ label: commandInput.label,
42
+ command: commandInput.command,
43
+ cwd,
44
+ stdoutLogId: stdoutRef.id,
45
+ stderrLogId: stderrRef.id,
46
+ });
27
47
  let result: SpawnCommandResult;
28
48
  const stdoutLog = await this.input.logs.createWriteStream(stdoutRef);
29
49
  const stderrLog = await this.input.logs.createWriteStream(stderrRef);
@@ -38,7 +58,13 @@ export class NornCommandRunner {
38
58
  stderrStream: stderrLog.stream,
39
59
  });
40
60
  } catch (error) {
41
- await this.input.logger.record({ type: "command.failed", invocationId, label: commandInput.label, durationMs: Date.now() - startedAtMs, error: errorMessage(error) });
61
+ await this.input.logger.record({
62
+ type: "command.failed",
63
+ invocationId,
64
+ label: commandInput.label,
65
+ durationMs: Date.now() - startedAtMs,
66
+ error: errorMessage(error),
67
+ });
42
68
  throw error;
43
69
  }
44
70
  const commandResult = {
@@ -83,15 +109,24 @@ type SpawnCommandResult = {
83
109
  killed: boolean;
84
110
  };
85
111
 
86
- async function spawnCommand(input: SpawnCommandInput): Promise<SpawnCommandResult> {
112
+ async function spawnCommand(
113
+ input: SpawnCommandInput,
114
+ ): Promise<SpawnCommandResult> {
87
115
  const command = typeof input.command === "string" ? "bash" : input.command[0];
88
- const args = typeof input.command === "string" ? ["-c", input.command] : input.command.slice(1);
116
+ const args =
117
+ typeof input.command === "string"
118
+ ? ["-c", input.command]
119
+ : input.command.slice(1);
89
120
  let killed = false;
90
121
  const stdout = new BoundedTextBuffer();
91
122
  const stderr = new BoundedTextBuffer();
92
123
 
93
124
  return new Promise((resolvePromise, reject) => {
94
- const child = spawn(command, args, { cwd: input.cwd, env: input.env, shell: false });
125
+ const child = spawn(command, args, {
126
+ cwd: input.cwd,
127
+ env: input.env,
128
+ shell: false,
129
+ });
95
130
  let timeout: ReturnType<typeof setTimeout> | undefined;
96
131
  let escalation: ReturnType<typeof setTimeout> | undefined;
97
132
  let isSettled = false;
@@ -104,25 +139,33 @@ async function spawnCommand(input: SpawnCommandInput): Promise<SpawnCommandResul
104
139
  if (isSettled) return;
105
140
  isSettled = true;
106
141
  cleanup();
107
- void closeStreams(input.stdoutStream, input.stderrStream).then(() => resolvePromise(result), reject);
142
+ void closeStreams(input.stdoutStream, input.stderrStream).then(
143
+ () => resolvePromise(result),
144
+ reject,
145
+ );
108
146
  };
109
147
  const fail = (error: Error) => {
110
148
  if (isSettled) return;
111
149
  isSettled = true;
112
150
  cleanup();
113
- void closeStreams(input.stdoutStream, input.stderrStream).then(() => reject(error), reject);
151
+ void closeStreams(input.stdoutStream, input.stderrStream).then(
152
+ () => reject(error),
153
+ reject,
154
+ );
114
155
  };
115
156
  const killChild = () => {
116
157
  if (isSettled || killed) return;
117
158
  killed = true;
118
159
  child.kill("SIGTERM");
119
160
  escalation = setTimeout(() => {
120
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
161
+ if (child.exitCode === null && child.signalCode === null)
162
+ child.kill("SIGKILL");
121
163
  }, 5_000);
122
164
  escalation.unref?.();
123
165
  };
124
166
 
125
- if (input.timeoutMs !== undefined) timeout = setTimeout(killChild, input.timeoutMs);
167
+ if (input.timeoutMs !== undefined)
168
+ timeout = setTimeout(killChild, input.timeoutMs);
126
169
  if (input.signal?.aborted) killChild();
127
170
  input.signal?.addEventListener("abort", killChild, { once: true });
128
171
 
@@ -137,12 +180,25 @@ async function spawnCommand(input: SpawnCommandInput): Promise<SpawnCommandResul
137
180
  input.stderrStream.write(text);
138
181
  });
139
182
  child.on("error", fail);
140
- child.on("close", (code) => finish({ exitCode: code, stdoutTail: stdout.value(), stderrTail: stderr.value(), killed }));
183
+ child.on("close", (code) =>
184
+ finish({
185
+ exitCode: code,
186
+ stdoutTail: stdout.value(),
187
+ stderrTail: stderr.value(),
188
+ killed,
189
+ }),
190
+ );
141
191
  });
142
192
  }
143
193
 
144
- function commandLog(input: { readonly label: string; readonly invocationId: string; readonly stream: "stdout" | "stderr" }): NornLogRef {
145
- return { id: `commands/${safeFileName(input.label)}-${input.invocationId}.${input.stream}` };
194
+ function commandLog(input: {
195
+ readonly label: string;
196
+ readonly invocationId: string;
197
+ readonly stream: "stdout" | "stderr";
198
+ }): NornLogRef {
199
+ return {
200
+ id: `commands/${safeFileName(input.label)}-${input.invocationId}.${input.stream}`,
201
+ };
146
202
  }
147
203
 
148
204
  class BoundedTextBuffer {
@@ -152,7 +208,8 @@ class BoundedTextBuffer {
152
208
 
153
209
  append(value: string): void {
154
210
  this.text += value;
155
- if (this.text.length > this.maxChars) this.text = this.text.slice(this.text.length - this.maxChars);
211
+ if (this.text.length > this.maxChars)
212
+ this.text = this.text.slice(this.text.length - this.maxChars);
156
213
  }
157
214
 
158
215
  value(): string {
@@ -161,8 +218,13 @@ class BoundedTextBuffer {
161
218
  }
162
219
 
163
220
  async function closeStreams(...streams: WriteStream[]): Promise<void> {
164
- await Promise.all(streams.map((stream) => new Promise<void>((resolvePromise, reject) => {
165
- stream.on("error", reject);
166
- stream.end(resolvePromise);
167
- })));
221
+ await Promise.all(
222
+ streams.map(
223
+ (stream) =>
224
+ new Promise<void>((resolvePromise, reject) => {
225
+ stream.on("error", reject);
226
+ stream.end(resolvePromise);
227
+ }),
228
+ ),
229
+ );
168
230
  }
@@ -17,33 +17,64 @@ export const DOCUMENTATION_PATHS = {
17
17
  examples: "examples",
18
18
  } as const;
19
19
 
20
- export function validateDocumentationBundle(bundle: NornDocumentationBundle): void {
21
- if (typeof bundle.version !== "string" || bundle.version.length === 0) throw new Error("Documentation bundle version must not be empty");
20
+ export function validateDocumentationBundle(
21
+ bundle: NornDocumentationBundle,
22
+ ): void {
23
+ if (typeof bundle.version !== "string" || bundle.version.length === 0)
24
+ throw new Error("Documentation bundle version must not be empty");
22
25
  const paths = new Set<string>();
23
26
  for (const file of bundle.files) {
24
- if (typeof file.content !== "string") throw new Error(`Documentation asset must contain text: ${file.path}`);
25
- if (typeof file.path !== "string" || !/^[a-zA-Z0-9._/-]+$/.test(file.path) || file.path.split("/").some(part => part === "" || part === "." || part === ".." || part.endsWith(".") || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
27
+ if (typeof file.content !== "string")
28
+ throw new Error(`Documentation asset must contain text: ${file.path}`);
29
+ if (
30
+ typeof file.path !== "string" ||
31
+ !/^[a-zA-Z0-9._/-]+$/.test(file.path) ||
32
+ file.path
33
+ .split("/")
34
+ .some(
35
+ (part) =>
36
+ part === "" ||
37
+ part === "." ||
38
+ part === ".." ||
39
+ part.endsWith(".") ||
40
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part),
41
+ )
42
+ ) {
26
43
  throw new Error(`Invalid documentation asset path: ${file.path}`);
27
44
  }
28
45
  const portablePath = file.path.toLowerCase();
29
- if (paths.has(portablePath)) throw new Error(`Duplicate documentation asset path: ${file.path}`);
46
+ if (paths.has(portablePath))
47
+ throw new Error(`Duplicate documentation asset path: ${file.path}`);
30
48
  paths.add(portablePath);
31
49
  }
32
50
  for (const path of paths) {
33
51
  const parts = path.split("/");
34
52
  while (parts.pop() && parts.length > 0) {
35
- if (paths.has(parts.join("/"))) throw new Error(`Documentation file/directory collision: ${path}`);
53
+ if (paths.has(parts.join("/")))
54
+ throw new Error(`Documentation file/directory collision: ${path}`);
36
55
  }
37
56
  }
38
57
  for (const path of [DOCUMENTATION_PATHS.readme, DOCUMENTATION_PATHS.index]) {
39
- if (!bundle.files.some(file => file.path === path)) throw new Error(`Missing documentation asset: ${path}`);
58
+ if (!bundle.files.some((file) => file.path === path))
59
+ throw new Error(`Missing documentation asset: ${path}`);
40
60
  }
41
- if (!bundle.files.some(file => file.path.startsWith("examples/"))) throw new Error("Missing documentation examples");
61
+ if (!bundle.files.some((file) => file.path.startsWith("examples/")))
62
+ throw new Error("Missing documentation examples");
42
63
  }
43
64
 
44
- export function hashDocumentationBundle(bundle: NornDocumentationBundle): string {
45
- return createHash("sha256").update(JSON.stringify({
46
- version: bundle.version,
47
- files: bundle.files.map(file => ({ path: file.path, content: file.content })).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
48
- })).digest("hex");
65
+ export function hashDocumentationBundle(
66
+ bundle: NornDocumentationBundle,
67
+ ): string {
68
+ return createHash("sha256")
69
+ .update(
70
+ JSON.stringify({
71
+ version: bundle.version,
72
+ files: bundle.files
73
+ .map((file) => ({ path: file.path, content: file.content }))
74
+ .sort((left, right) =>
75
+ left.path < right.path ? -1 : left.path > right.path ? 1 : 0,
76
+ ),
77
+ }),
78
+ )
79
+ .digest("hex");
49
80
  }