@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,323 @@
1
+ // src/internal/run-state.ts
2
+ import { mkdir as mkdir2, readdir, readFile } from "node:fs/promises";
3
+ import { dirname as dirname2, isAbsolute, join, resolve } from "node:path";
4
+
5
+ // ../core/src/errors.ts
6
+ function isNodeError(error) {
7
+ return error instanceof Error && "code" in error;
8
+ }
9
+
10
+ // src/internal/run-state.ts
11
+ import { readRunLaunchRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.js";
12
+ import { getRunLeaseHealth } from "./run-lease.js";
13
+
14
+ // ../core/src/atomic-files.ts
15
+ import { randomUUID } from "node:crypto";
16
+ import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
17
+ import { dirname } from "node:path";
18
+ async function writeJsonAtomically(path, value) {
19
+ await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
20
+ `);
21
+ }
22
+ async function writeTextAtomically(path, content) {
23
+ await mkdir(dirname(path), { recursive: true });
24
+ let existingMode;
25
+ try {
26
+ existingMode = (await stat(path)).mode & 511;
27
+ } catch (error) {
28
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
29
+ }
30
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
31
+ try {
32
+ await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
33
+ if (existingMode !== void 0) await chmod(tmpPath, existingMode);
34
+ await rename(tmpPath, path);
35
+ } catch (error) {
36
+ await rm(tmpPath, { force: true }).catch(() => void 0);
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ // src/internal/run-state.ts
42
+ import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
43
+ import { runCurrentRoot } from "./run-store.js";
44
+ var RUN_STATE_FILE_NAME = "run-state.json";
45
+ var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
46
+ function mergeInterruptedWorkflowParams(currentParams, params, fields) {
47
+ if (!isRecord(currentParams) || !isRecord(params)) return params;
48
+ if (fields) {
49
+ const unsupportedFields = Object.keys(params).filter((field) => !fields.includes(field));
50
+ if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow params cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
51
+ }
52
+ return { ...currentParams, ...params };
53
+ }
54
+ var RUN_STATE_VERSION = 1;
55
+ var NornRunStateStore = class _NornRunStateStore {
56
+ path;
57
+ state;
58
+ files;
59
+ constructor(input) {
60
+ this.path = input.path;
61
+ this.state = input.state;
62
+ this.files = input.files;
63
+ }
64
+ static async create(runRoot, input) {
65
+ const now = input.startedAt;
66
+ const state = {
67
+ version: RUN_STATE_VERSION,
68
+ id: input.id,
69
+ name: input.name,
70
+ entrypointWorkflowId: input.entrypointWorkflowId,
71
+ workspace: input.workspace,
72
+ configOverride: input.configOverride,
73
+ status: "running",
74
+ current: input.current,
75
+ lastCompleted: null,
76
+ outcome: null,
77
+ failed: null,
78
+ startedAt: now,
79
+ updatedAt: now
80
+ };
81
+ const store = new _NornRunStateStore({ path: join(runCurrentRoot(runRoot), RUN_STATE_FILE_NAME), state, files: createRunFileCoordinator(runRoot) });
82
+ await store.write();
83
+ return store;
84
+ }
85
+ static async load(runRoot) {
86
+ const currentRoot = runCurrentRoot(runRoot);
87
+ const files = createRunFileCoordinator(runRoot);
88
+ const state = await files.withExclusiveLock(join(currentRoot, RUN_STATE_FILE_NAME), async (path) => parseNornRunState(await readRunStateFile({ path, legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) })));
89
+ return new _NornRunStateStore({ path: join(currentRoot, RUN_STATE_FILE_NAME), state, files });
90
+ }
91
+ currentState() {
92
+ return this.state;
93
+ }
94
+ async startStep(step) {
95
+ await this.update({ status: "running", current: step, outcome: null, failed: null });
96
+ }
97
+ async completeWithNext(completedWorkflowId, next) {
98
+ await this.update({
99
+ status: "running",
100
+ current: next,
101
+ lastCompleted: { workflowId: completedWorkflowId, completedAt: (/* @__PURE__ */ new Date()).toISOString(), outcome: { type: "next", workflowId: next.workflowId } },
102
+ outcome: null,
103
+ failed: null
104
+ });
105
+ }
106
+ async completeRun(workflowId, metadata) {
107
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
108
+ await this.update({
109
+ status: "completed",
110
+ current: null,
111
+ lastCompleted: { workflowId, completedAt, outcome: { type: "complete" } },
112
+ outcome: { workflowId, completedAt, status: "completed", metadata },
113
+ failed: null
114
+ });
115
+ }
116
+ async failRun(workflowId, metadata) {
117
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
118
+ await this.update({
119
+ status: "failed",
120
+ current: null,
121
+ lastCompleted: { workflowId, completedAt: failedAt, outcome: { type: "fail" } },
122
+ outcome: { workflowId, completedAt: failedAt, status: "failed", metadata },
123
+ failed: { workflowId, error: metadata.summary, metadata, failedAt }
124
+ });
125
+ }
126
+ async interruptCurrent(params, interruption) {
127
+ if (!this.state.current) throw new Error("Cannot interrupt run without current step");
128
+ await this.update({
129
+ status: "interrupted",
130
+ current: { ...this.state.current, params, interruption: { status: "pending", ...interruption } },
131
+ outcome: null,
132
+ failed: null
133
+ });
134
+ }
135
+ async prepareForResumeAfterRollback() {
136
+ if (!this.state.current) throw new Error("Cannot resume a checkpoint without a current step");
137
+ if (this.state.status === "interrupted") return;
138
+ if (this.state.status !== "running") throw new Error(`Cannot resume checkpoint with ${this.state.status} status`);
139
+ await this.update({ status: "pendingResume", outcome: null, failed: null });
140
+ }
141
+ async stopCurrent() {
142
+ if (!this.state.current) throw new Error("Cannot stop run without current step");
143
+ await this.update({ status: "stopped", outcome: null, failed: null });
144
+ }
145
+ async replaceCurrentParams(params) {
146
+ if (!this.state.current) throw new Error("Cannot resume run without current step");
147
+ await this.update({
148
+ status: "running",
149
+ current: { ...this.state.current, params, interruption: { ...this.state.current.interruption, status: "satisfied" } },
150
+ outcome: null,
151
+ failed: null
152
+ });
153
+ }
154
+ async failCurrent(errorOrMetadata) {
155
+ if (!this.state.current) throw new Error("Cannot fail run without current step");
156
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
157
+ const metadata = typeof errorOrMetadata === "string" ? { summary: errorOrMetadata } : errorOrMetadata;
158
+ await this.update({
159
+ status: "failed",
160
+ outcome: { workflowId: this.state.current.workflowId, completedAt: failedAt, status: "failed", metadata },
161
+ failed: { workflowId: this.state.current.workflowId, error: metadata.summary, metadata, failedAt }
162
+ });
163
+ }
164
+ async update(patch) {
165
+ await this.files.withExclusiveLock(this.path, async (path) => {
166
+ const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
167
+ const next = { ...current, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
168
+ await writeJsonAtomically(path, next);
169
+ this.state = next;
170
+ });
171
+ }
172
+ async write() {
173
+ await mkdir2(dirname2(this.path), { recursive: true });
174
+ await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
175
+ }
176
+ };
177
+ async function listRuns(sessionCwd) {
178
+ const runsRoot = join(sessionCwd, ".norn", "runs");
179
+ let entries;
180
+ try {
181
+ entries = await readdir(runsRoot, { withFileTypes: true });
182
+ } catch (error) {
183
+ if (isNodeError(error) && error.code === "ENOENT") return [];
184
+ throw error;
185
+ }
186
+ const runs = await Promise.all(entries.filter((entry) => entry.isDirectory()).map((entry) => readNornRunInfo(resolve(runsRoot, entry.name))));
187
+ return runs.filter((run) => run !== void 0).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
188
+ }
189
+ async function getRunInfo(runRoot) {
190
+ try {
191
+ const resumeRequest = await readOptionalRunResumeRequest(runRoot);
192
+ const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)));
193
+ return {
194
+ version: state.version,
195
+ id: state.id,
196
+ name: state.name,
197
+ path: runRoot,
198
+ entrypointWorkflowId: state.entrypointWorkflowId,
199
+ currentWorkflowId: state.current?.workflowId,
200
+ status: resumeRequest ? "running" : state.status,
201
+ health: resumeRequest ? await getPendingResumeHealth(runRoot, resumeRequest.createdAt) : await runHealth(runRoot, state.status),
202
+ interruption: resumeRequest ? void 0 : runInterruption(state),
203
+ outcome: runOutcome(state),
204
+ failed: runFailure(state),
205
+ startedAt: state.startedAt,
206
+ updatedAt: state.updatedAt
207
+ };
208
+ } catch (error) {
209
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
210
+ return getLaunchedRunInfo(runRoot);
211
+ }
212
+ }
213
+ async function resolveRunRoot(sessionCwd, run) {
214
+ for (const candidate of runRootCandidates(sessionCwd, run)) {
215
+ try {
216
+ if (await readNornRunInfo(candidate)) return candidate;
217
+ } catch (error) {
218
+ if (!isNodeError(error) && (!(error instanceof Error) || !error.message.includes("run state"))) throw error;
219
+ }
220
+ }
221
+ const runs = await listRuns(sessionCwd);
222
+ const match = runs.find((entry) => entry.id === run || entry.name === run || entry.path === run || entry.path.endsWith(`/${run}`));
223
+ if (!match) throw new Error(`Unknown run: ${run}`);
224
+ return match.path;
225
+ }
226
+ function runRootCandidates(sessionCwd, run) {
227
+ const candidates = [isAbsolute(run) ? run : resolve(sessionCwd, run)];
228
+ if (!isAbsolute(run)) candidates.push(resolve(sessionCwd, ".norn", "runs", run));
229
+ return Array.from(new Set(candidates));
230
+ }
231
+ async function getPendingResumeHealth(runRoot, requestedAt) {
232
+ const health = await getRunLeaseHealth(runRoot);
233
+ return health === "healthy" || Date.now() - Date.parse(requestedAt) < RESUME_START_GRACE_MS ? "healthy" : "unhealthy";
234
+ }
235
+ async function runHealth(runRoot, status) {
236
+ if (status !== "running") return "healthy";
237
+ return getRunLeaseHealth(runRoot);
238
+ }
239
+ async function readNornRunInfo(runRoot) {
240
+ try {
241
+ return await getRunInfo(runRoot);
242
+ } catch (error) {
243
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
244
+ throw error;
245
+ }
246
+ }
247
+ async function getLaunchedRunInfo(runRoot) {
248
+ const launchRequest = await readRunLaunchRequest(runRoot);
249
+ return {
250
+ version: launchRequest.version,
251
+ id: launchRequest.id,
252
+ name: launchRequest.name,
253
+ path: runRoot,
254
+ entrypointWorkflowId: launchRequest.workflowId,
255
+ currentWorkflowId: launchRequest.workflowId,
256
+ status: "running",
257
+ health: "healthy",
258
+ startedAt: launchRequest.createdAt,
259
+ updatedAt: launchRequest.createdAt
260
+ };
261
+ }
262
+ async function readRunStateJson(currentRoot) {
263
+ return readRunStateFile({ path: join(currentRoot, RUN_STATE_FILE_NAME), legacyPath: join(currentRoot, LEGACY_RUN_STATE_FILE_NAME) });
264
+ }
265
+ async function readRunStateFile(input) {
266
+ try {
267
+ return JSON.parse(await readFile(input.path, "utf8"));
268
+ } catch (error) {
269
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
270
+ return JSON.parse(await readFile(input.legacyPath, "utf8"));
271
+ }
272
+ }
273
+ function isRecord(value) {
274
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
275
+ }
276
+ function parseNornRunState(value) {
277
+ if (!value || typeof value !== "object") throw new Error("Invalid run state");
278
+ const state = value;
279
+ if (state.version !== RUN_STATE_VERSION) throw new Error(`Unsupported run state version: ${String(state.version)}`);
280
+ if (typeof state.id !== "string" || state.id.length === 0) throw new Error("Invalid run state id");
281
+ const normalizedState = state;
282
+ if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0) normalizedState.name = state.id;
283
+ if (typeof normalizedState.entrypointWorkflowId !== "string" && typeof normalizedState.rootWorkflowId === "string") normalizedState.entrypointWorkflowId = normalizedState.rootWorkflowId;
284
+ if (typeof state.entrypointWorkflowId !== "string" || state.entrypointWorkflowId.length === 0) throw new Error("Invalid run state entrypoint workflow id");
285
+ if (typeof state.workspace !== "string" || state.workspace.length === 0) throw new Error("Invalid run state workspace");
286
+ if (state.status !== "running" && state.status !== "interrupted" && state.status !== "stopped" && state.status !== "pendingResume" && state.status !== "completed" && state.status !== "failed") throw new Error("Invalid run state status");
287
+ if (state.status === "interrupted") assertInterruptedNornRunState(state);
288
+ if (state.status === "stopped" || state.status === "pendingResume") assertResumableNornRunState(state);
289
+ if (typeof state.startedAt !== "string" || typeof state.updatedAt !== "string") throw new Error("Invalid run state timestamps");
290
+ return state;
291
+ }
292
+ function assertInterruptedNornRunState(state) {
293
+ if (!state.current) throw new Error("Invalid interrupted run current step");
294
+ const interruption = state.current.interruption;
295
+ if (!interruption || interruption.status !== "pending") throw new Error("Invalid run interruption");
296
+ if (typeof interruption.description !== "string" || interruption.description.length === 0) throw new Error("Invalid run interruption description");
297
+ }
298
+ function assertResumableNornRunState(state) {
299
+ if (!state.current) throw new Error("Invalid resumable run current step");
300
+ }
301
+ function runInterruption(state) {
302
+ if (state.status !== "interrupted" || !state.current?.interruption) return void 0;
303
+ return {
304
+ workflowId: state.current.workflowId,
305
+ params: state.current.params,
306
+ description: state.current.interruption.description ?? "",
307
+ fields: state.current.interruption.fields
308
+ };
309
+ }
310
+ function runOutcome(state) {
311
+ return state.outcome ?? void 0;
312
+ }
313
+ function runFailure(state) {
314
+ return state.failed ?? void 0;
315
+ }
316
+ export {
317
+ NornRunStateStore,
318
+ RUN_STATE_FILE_NAME,
319
+ getRunInfo,
320
+ listRuns,
321
+ mergeInterruptedWorkflowParams,
322
+ resolveRunRoot
323
+ };
@@ -0,0 +1,35 @@
1
+ import type { NornRunCheckpoint } from "@vimhead.dev/norn";
2
+ export declare function runCurrentRoot(runRoot: string): string;
3
+ export declare class NornRunStore {
4
+ private readonly runRoot;
5
+ private readonly currentRoot;
6
+ private readonly storeRoot;
7
+ private constructor();
8
+ static initialize(root: string): Promise<NornRunStore>;
9
+ static open(root: string): Promise<NornRunStore>;
10
+ currentSnapshotRef(): Promise<string>;
11
+ snapshotCurrent(message: string): Promise<NornRunCheckpoint>;
12
+ restoreSnapshot(ref: string, prepare: ((stagedRunRoot: string) => Promise<void>) | undefined): Promise<void>;
13
+ restoreCurrentSnapshot(): Promise<void>;
14
+ listCheckpoints(): Promise<NornRunCheckpoint[]>;
15
+ private readCheckpointHistory;
16
+ assertWorkspaceCanBeSnapshotted(_workspace: string): Promise<void>;
17
+ private get objectsRoot();
18
+ private get snapshotsRoot();
19
+ private get refsRoot();
20
+ private get currentRefPath();
21
+ private get checkpointsPath();
22
+ private createCheckpoint;
23
+ private createSnapshot;
24
+ private snapshotEntries;
25
+ private collectSnapshotEntries;
26
+ private storeObject;
27
+ private restoreSnapshotManifest;
28
+ private materializeSnapshot;
29
+ private materializedPath;
30
+ private readSnapshot;
31
+ private snapshotPath;
32
+ private snapshotRelativePath;
33
+ private assertCheckpointPathMatchesId;
34
+ private objectPath;
35
+ }
@@ -0,0 +1,314 @@
1
+ // src/internal/run-store.ts
2
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { access, chmod as chmod2, lstat, mkdir as mkdir2, mkdtemp, readFile, readdir, readlink, rename as rename2, rm as rm2, symlink, utimes, writeFile as writeFile2 } from "node:fs/promises";
5
+ import { dirname as dirname2, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { gunzip, gzip } from "node:zlib";
8
+
9
+ // ../core/src/errors.ts
10
+ function isNodeError(error) {
11
+ return error instanceof Error && "code" in error;
12
+ }
13
+
14
+ // ../core/src/atomic-files.ts
15
+ import { randomUUID } from "node:crypto";
16
+ import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
17
+ import { dirname } from "node:path";
18
+ async function writeJsonAtomically(path, value) {
19
+ await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
20
+ `);
21
+ }
22
+ async function writeTextAtomically(path, content) {
23
+ await mkdir(dirname(path), { recursive: true });
24
+ let existingMode;
25
+ try {
26
+ existingMode = (await stat(path)).mode & 511;
27
+ } catch (error) {
28
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
29
+ }
30
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
31
+ try {
32
+ await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
33
+ if (existingMode !== void 0) await chmod(tmpPath, existingMode);
34
+ await rename(tmpPath, path);
35
+ } catch (error) {
36
+ await rm(tmpPath, { force: true }).catch(() => void 0);
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ // src/internal/run-store.ts
42
+ var gzipBuffer = promisify(gzip);
43
+ var gunzipBuffer = promisify(gunzip);
44
+ var SNAPSHOT_VERSION = 1;
45
+ var CURRENT_DIR_NAME = "current";
46
+ var STORE_DIR_NAME = "store";
47
+ var CHECKPOINTS_FILE_NAME = "checkpoints.json";
48
+ function runCurrentRoot(runRoot) {
49
+ return join(runRoot, CURRENT_DIR_NAME);
50
+ }
51
+ var NornRunStore = class _NornRunStore {
52
+ constructor(runRoot) {
53
+ this.runRoot = runRoot;
54
+ this.currentRoot = runCurrentRoot(runRoot);
55
+ this.storeRoot = join(runRoot, STORE_DIR_NAME);
56
+ }
57
+ runRoot;
58
+ currentRoot;
59
+ storeRoot;
60
+ static async initialize(root) {
61
+ const runStore = new _NornRunStore(root);
62
+ await mkdir2(runStore.currentRoot, { recursive: true });
63
+ await mkdir2(runStore.objectsRoot, { recursive: true });
64
+ await mkdir2(runStore.snapshotsRoot, { recursive: true });
65
+ await mkdir2(runStore.refsRoot, { recursive: true });
66
+ return runStore;
67
+ }
68
+ static async open(root) {
69
+ const runStore = new _NornRunStore(root);
70
+ await access(runStore.currentRoot, constants.R_OK | constants.W_OK);
71
+ await access(runStore.storeRoot, constants.R_OK | constants.W_OK);
72
+ return runStore;
73
+ }
74
+ async currentSnapshotRef() {
75
+ return readFile(this.currentRefPath, "utf8").then((value) => value.trim());
76
+ }
77
+ async snapshotCurrent(message) {
78
+ const previous = await this.readCheckpointHistory();
79
+ const checkpoint = this.createCheckpoint(message, previous.checkpoints.length + 1);
80
+ const checkpoints = [...previous.checkpoints, checkpoint];
81
+ await this.createSnapshot(checkpoint, checkpoints);
82
+ await writeJsonAtomically(this.checkpointsPath, checkpoints);
83
+ try {
84
+ await writeTextAtomically(this.currentRefPath, `${checkpoint.id}
85
+ `);
86
+ } catch (error) {
87
+ if (previous.content === void 0) await rm2(this.checkpointsPath, { force: true });
88
+ else await writeTextAtomically(this.checkpointsPath, previous.content);
89
+ throw error;
90
+ }
91
+ return checkpoint;
92
+ }
93
+ async restoreSnapshot(ref, prepare) {
94
+ const checkpoint = (await this.listCheckpoints()).find((entry) => entry.id === ref);
95
+ if (!checkpoint) throw new Error(`Unknown active run checkpoint: ${ref}`);
96
+ await this.restoreSnapshotManifest(await this.readSnapshot(checkpoint.id), prepare);
97
+ }
98
+ async restoreCurrentSnapshot() {
99
+ await this.restoreSnapshotManifest(await this.readSnapshot(await this.currentSnapshotRef()), void 0);
100
+ }
101
+ async listCheckpoints() {
102
+ return (await this.readCheckpointHistory()).checkpoints;
103
+ }
104
+ async readCheckpointHistory() {
105
+ try {
106
+ const content = await readFile(this.checkpointsPath, "utf8");
107
+ const checkpoints = parseNornRunCheckpoints(JSON.parse(content));
108
+ for (const checkpoint of checkpoints) this.assertCheckpointPathMatchesId(checkpoint);
109
+ return { checkpoints, content };
110
+ } catch (error) {
111
+ if (isNodeError(error) && error.code === "ENOENT") return { checkpoints: [] };
112
+ throw error;
113
+ }
114
+ }
115
+ async assertWorkspaceCanBeSnapshotted(_workspace) {
116
+ }
117
+ get objectsRoot() {
118
+ return join(this.storeRoot, "objects", "sha256");
119
+ }
120
+ get snapshotsRoot() {
121
+ return join(this.storeRoot, "snapshots");
122
+ }
123
+ get refsRoot() {
124
+ return join(this.storeRoot, "refs");
125
+ }
126
+ get currentRefPath() {
127
+ return join(this.refsRoot, "current");
128
+ }
129
+ get checkpointsPath() {
130
+ return join(this.currentRoot, CHECKPOINTS_FILE_NAME);
131
+ }
132
+ createCheckpoint(message, index) {
133
+ const id = `cp_${randomUUID2().replaceAll("-", "")}`;
134
+ return { id, path: this.snapshotRelativePath(id), index, message, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
135
+ }
136
+ async createSnapshot(checkpoint, checkpoints) {
137
+ const entries = (await this.snapshotEntries(this.currentRoot)).filter((entry) => entry.path !== CHECKPOINTS_FILE_NAME);
138
+ const historyBytes = Buffer.from(`${JSON.stringify(checkpoints, null, 2)}
139
+ `);
140
+ const historyHash = hashBuffer(historyBytes);
141
+ await this.storeObject(historyHash, historyBytes);
142
+ entries.push({ path: CHECKPOINTS_FILE_NAME, type: "file", sha256: historyHash, size: historyBytes.length, mode: 384, mtimeMs: Date.now(), compression: "gzip" });
143
+ entries.sort((left, right) => left.path.localeCompare(right.path));
144
+ const snapshot = {
145
+ version: SNAPSHOT_VERSION,
146
+ id: checkpoint.id,
147
+ path: checkpoint.path,
148
+ index: checkpoint.index,
149
+ message: checkpoint.message,
150
+ createdAt: checkpoint.createdAt,
151
+ entries
152
+ };
153
+ await writeJsonAtomically(this.snapshotPath(snapshot.id), snapshot);
154
+ return snapshot;
155
+ }
156
+ async snapshotEntries(root) {
157
+ const entries = [];
158
+ await this.collectSnapshotEntries(root, "", entries);
159
+ return entries.sort((left, right) => left.path.localeCompare(right.path));
160
+ }
161
+ async collectSnapshotEntries(absolutePath, snapshotPath, entries) {
162
+ const stat2 = await lstat(absolutePath);
163
+ if (stat2.isDirectory()) {
164
+ if (snapshotPath.length > 0) entries.push({ path: snapshotPath, type: "directory", mode: stat2.mode });
165
+ const children = await readdir(absolutePath, { withFileTypes: true });
166
+ for (const child of children.sort((left, right) => left.name.localeCompare(right.name))) {
167
+ await this.collectSnapshotEntries(join(absolutePath, child.name), joinSnapshotPath(snapshotPath, child.name), entries);
168
+ }
169
+ return;
170
+ }
171
+ if (stat2.isSymbolicLink()) {
172
+ entries.push({ path: snapshotPath, type: "symlink", target: await readlink(absolutePath) });
173
+ return;
174
+ }
175
+ if (!stat2.isFile()) return;
176
+ const bytes = await readFile(absolutePath);
177
+ const sha256 = hashBuffer(bytes);
178
+ await this.storeObject(sha256, bytes);
179
+ entries.push({ path: snapshotPath, type: "file", sha256, size: stat2.size, mode: stat2.mode, mtimeMs: stat2.mtimeMs, compression: "gzip" });
180
+ }
181
+ async storeObject(sha256, bytes) {
182
+ const path = this.objectPath(sha256);
183
+ try {
184
+ await access(path, constants.R_OK);
185
+ return;
186
+ } catch (error) {
187
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
188
+ }
189
+ await mkdir2(dirname2(path), { recursive: true });
190
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
191
+ await writeFile2(tmpPath, await gzipBuffer(bytes));
192
+ await rename2(tmpPath, path);
193
+ }
194
+ async restoreSnapshotManifest(snapshot, prepare) {
195
+ const stagedRunRoot = await mkdtemp(join(this.runRoot, ".restore-"));
196
+ const stagedCurrent = runCurrentRoot(stagedRunRoot);
197
+ const previousCurrent = join(this.runRoot, `.previous-${randomUUID2()}`);
198
+ try {
199
+ await mkdir2(stagedCurrent);
200
+ await this.materializeSnapshot(snapshot, stagedCurrent);
201
+ await prepare?.(stagedRunRoot);
202
+ await rename2(this.currentRoot, previousCurrent);
203
+ try {
204
+ await rename2(stagedCurrent, this.currentRoot);
205
+ } catch (error) {
206
+ await rename2(previousCurrent, this.currentRoot);
207
+ throw error;
208
+ }
209
+ try {
210
+ await writeTextAtomically(this.currentRefPath, `${snapshot.id}
211
+ `);
212
+ } catch (error) {
213
+ await rename2(this.currentRoot, stagedCurrent);
214
+ await rename2(previousCurrent, this.currentRoot);
215
+ throw error;
216
+ }
217
+ await rm2(previousCurrent, { recursive: true, force: true }).catch(() => void 0);
218
+ } finally {
219
+ await rm2(stagedRunRoot, { recursive: true, force: true }).catch(() => void 0);
220
+ }
221
+ }
222
+ async materializeSnapshot(snapshot, destination) {
223
+ const entriesByPath = /* @__PURE__ */ new Map();
224
+ for (const entry of snapshot.entries) {
225
+ this.materializedPath(destination, entry.path);
226
+ if (entriesByPath.has(entry.path)) throw new Error(`Duplicate snapshot path: ${entry.path}`);
227
+ entriesByPath.set(entry.path, entry);
228
+ }
229
+ for (const entry of snapshot.entries) {
230
+ let parent = dirname2(entry.path);
231
+ while (parent !== ".") {
232
+ if (entriesByPath.get(parent)?.type !== "directory") throw new Error(`Snapshot parent is not a directory: ${parent}`);
233
+ parent = dirname2(parent);
234
+ }
235
+ if (entry.type === "directory") await mkdir2(this.materializedPath(destination, entry.path), { recursive: true });
236
+ }
237
+ for (const entry of snapshot.entries) {
238
+ if (entry.type === "directory") continue;
239
+ const path = this.materializedPath(destination, entry.path);
240
+ if (entry.type === "symlink") {
241
+ await symlink(entry.target, path);
242
+ continue;
243
+ }
244
+ const bytes = await gunzipBuffer(await readFile(this.objectPath(entry.sha256)));
245
+ const actualSha256 = hashBuffer(bytes);
246
+ if (actualSha256 !== entry.sha256) throw new Error(`CAS object checksum mismatch: ${entry.sha256}`);
247
+ await writeFile2(path, bytes);
248
+ await chmod2(path, entry.mode);
249
+ await utimes(path, new Date(entry.mtimeMs), new Date(entry.mtimeMs));
250
+ }
251
+ for (const entry of [...snapshot.entries].reverse()) {
252
+ if (entry.type === "directory") await chmod2(this.materializedPath(destination, entry.path), entry.mode);
253
+ }
254
+ }
255
+ materializedPath(destination, snapshotPath) {
256
+ if (snapshotPath.length === 0 || isAbsolute(snapshotPath) || snapshotPath.split(/[\\/]/).some((part) => part === ".." || part === "." || part === "")) throw new Error(`Invalid snapshot path: ${snapshotPath}`);
257
+ const path = resolve(destination, ...snapshotPath.split("/"));
258
+ const pathFromCurrent = relative(destination, path);
259
+ if (pathFromCurrent === ".." || pathFromCurrent.startsWith(`..${sep}`)) throw new Error(`Snapshot path escapes current checkout: ${snapshotPath}`);
260
+ return path;
261
+ }
262
+ async readSnapshot(id) {
263
+ return parseRunSnapshotManifest(JSON.parse(await readFile(this.snapshotPath(id), "utf8")));
264
+ }
265
+ snapshotPath(id) {
266
+ return join(this.snapshotsRoot, `${id}.json`);
267
+ }
268
+ snapshotRelativePath(id) {
269
+ return relative(this.runRoot, this.snapshotPath(id));
270
+ }
271
+ assertCheckpointPathMatchesId(checkpoint) {
272
+ const expectedPath = this.snapshotRelativePath(checkpoint.id);
273
+ if (checkpoint.path !== expectedPath) throw new Error(`Run checkpoint path does not match id: ${checkpoint.id}`);
274
+ }
275
+ objectPath(sha256) {
276
+ return join(this.objectsRoot, sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.gz`);
277
+ }
278
+ };
279
+ function hashBuffer(bytes) {
280
+ return createHash("sha256").update(bytes).digest("hex");
281
+ }
282
+ function joinSnapshotPath(parent, child) {
283
+ return parent.length === 0 ? child : `${parent}/${child}`;
284
+ }
285
+ function parseRunSnapshotManifest(value) {
286
+ if (!value || typeof value !== "object") throw new Error("Invalid run snapshot manifest");
287
+ const snapshot = value;
288
+ if (snapshot.version !== SNAPSHOT_VERSION) throw new Error(`Unsupported run snapshot version: ${String(snapshot.version)}`);
289
+ if (typeof snapshot.id !== "string" || snapshot.id.length === 0) throw new Error("Invalid run snapshot id");
290
+ if (typeof snapshot.index !== "number" || !Number.isInteger(snapshot.index) || snapshot.index < 1) throw new Error("Invalid run snapshot index");
291
+ if (typeof snapshot.message !== "string") throw new Error("Invalid run snapshot message");
292
+ if (typeof snapshot.createdAt !== "string") throw new Error("Invalid run snapshot timestamp");
293
+ if (!Array.isArray(snapshot.entries)) throw new Error("Invalid run snapshot entries");
294
+ return snapshot;
295
+ }
296
+ function parseNornRunCheckpoints(value) {
297
+ if (!Array.isArray(value)) throw new Error("Invalid run checkpoints");
298
+ return value.map(parseNornRunCheckpoint);
299
+ }
300
+ function parseNornRunCheckpoint(value) {
301
+ if (!value || typeof value !== "object") throw new Error("Invalid run checkpoint");
302
+ const checkpoint = value;
303
+ if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0) throw new Error("Invalid run checkpoint id");
304
+ if (typeof checkpoint.path !== "string" || checkpoint.path.length === 0) throw new Error("Invalid run checkpoint path");
305
+ if (checkpoint.path.split(/[\\/]/).includes("..") || resolve(checkpoint.path) === checkpoint.path) throw new Error(`Invalid run checkpoint path: ${checkpoint.path}`);
306
+ if (typeof checkpoint.index !== "number" || !Number.isInteger(checkpoint.index) || checkpoint.index < 1) throw new Error("Invalid run checkpoint index");
307
+ if (typeof checkpoint.message !== "string") throw new Error("Invalid run checkpoint message");
308
+ if (typeof checkpoint.createdAt !== "string") throw new Error("Invalid run checkpoint timestamp");
309
+ return checkpoint;
310
+ }
311
+ export {
312
+ NornRunStore,
313
+ runCurrentRoot
314
+ };