@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
@@ -0,0 +1,162 @@
1
+ // ../core/src/atomic-files.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ // ../core/src/errors.ts
7
+ function isNodeError(error) {
8
+ return error instanceof Error && "code" in error;
9
+ }
10
+
11
+ // ../core/src/atomic-files.ts
12
+ async function writeTextAtomically(path, content) {
13
+ await mkdir(dirname(path), { recursive: true });
14
+ let existingMode;
15
+ try {
16
+ existingMode = (await stat(path)).mode & 511;
17
+ } catch (error) {
18
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
19
+ }
20
+ const tmpPath = `${path}.${randomUUID()}.tmp`;
21
+ try {
22
+ await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
23
+ if (existingMode !== void 0) await chmod(tmpPath, existingMode);
24
+ await rename(tmpPath, path);
25
+ } catch (error) {
26
+ await rm(tmpPath, { force: true }).catch(() => void 0);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ // src/internal/file-coordinator.ts
32
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
33
+ import { lstat, mkdir as mkdir2, mkdtemp, readFile, readdir, realpath, rename as rename2, rm as rm2, rmdir, unlink, writeFile as writeFile2 } from "node:fs/promises";
34
+ import { hostname } from "node:os";
35
+ import { basename, dirname as dirname2, join, resolve } from "node:path";
36
+ import { setTimeout as delay } from "node:timers/promises";
37
+ import { Type } from "typebox";
38
+ import { Value } from "typebox/value";
39
+ var ownerSchema = Type.Object({
40
+ token: Type.String({ format: "uuid" }),
41
+ pid: Type.Integer({ exclusiveMinimum: 0 }),
42
+ host: Type.String(),
43
+ target: Type.String()
44
+ }, { additionalProperties: false });
45
+ var NornFileCoordinator = class {
46
+ constructor(input) {
47
+ this.input = input;
48
+ if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0) throw new Error("Lock wait timeout must be a positive integer");
49
+ this.lockRoot = resolve(input.lockRoot);
50
+ }
51
+ input;
52
+ lockRoot;
53
+ async withExclusiveLock(path, operation) {
54
+ const target = await this.resolveTarget(path);
55
+ await mkdir2(this.lockRoot, { recursive: true, mode: 448 });
56
+ const key = createHash("sha256").update(target).digest("hex");
57
+ const lockPath = join(this.lockRoot, `${key}.lock`);
58
+ const stagingPath = await mkdtemp(join(this.lockRoot, `${key}.pending-`));
59
+ const token = randomUUID2();
60
+ const marker = `${token}.json`;
61
+ return this.withCleanup({
62
+ operation: async () => {
63
+ await writeFile2(join(stagingPath, marker), JSON.stringify({ token, pid: process.pid, host: hostname(), target }), { mode: 384 });
64
+ await this.acquire({ stagingPath, lockPath, target });
65
+ return this.withCleanup({
66
+ operation: () => operation(target),
67
+ cleanup: async () => {
68
+ await unlink(join(lockPath, marker));
69
+ await this.removeEmptyLock(lockPath);
70
+ }
71
+ });
72
+ },
73
+ cleanup: () => rm2(stagingPath, { recursive: true, force: true })
74
+ });
75
+ }
76
+ async readText(path) {
77
+ return this.withExclusiveLock(path, (lockedPath) => readFile(lockedPath, "utf8"));
78
+ }
79
+ async writeText(path, content) {
80
+ await mkdir2(dirname2(path), { recursive: true });
81
+ await this.withExclusiveLock(path, (lockedPath) => writeTextAtomically(lockedPath, content));
82
+ }
83
+ async withCleanup(input) {
84
+ let result;
85
+ try {
86
+ result = await input.operation();
87
+ } catch (error) {
88
+ try {
89
+ await input.cleanup();
90
+ } catch (cleanupError) {
91
+ throw new AggregateError([error, cleanupError], "File operation and lock cleanup failed");
92
+ }
93
+ throw error;
94
+ }
95
+ await input.cleanup();
96
+ return result;
97
+ }
98
+ async resolveTarget(path) {
99
+ try {
100
+ return await realpath(path);
101
+ } catch (error) {
102
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
103
+ const entry = await lstat(path).catch((error2) => {
104
+ if (isNodeError(error2) && error2.code === "ENOENT") return void 0;
105
+ throw error2;
106
+ });
107
+ if (entry?.isSymbolicLink()) throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
108
+ return join(await realpath(dirname2(path)), basename(path));
109
+ }
110
+ }
111
+ async acquire(input) {
112
+ const deadline = Date.now() + this.input.waitTimeoutMs;
113
+ while (true) {
114
+ try {
115
+ await rename2(input.stagingPath, input.lockPath);
116
+ return;
117
+ } catch (error) {
118
+ if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
119
+ }
120
+ await this.reclaimDeadOwner(input);
121
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for file lock: ${input.target}`);
122
+ await delay(10);
123
+ }
124
+ }
125
+ async reclaimDeadOwner(input) {
126
+ try {
127
+ const entries = await readdir(input.lockPath);
128
+ if (entries.length === 0) return;
129
+ if (entries.length !== 1) throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
130
+ const marker = entries[0];
131
+ const owner = Value.Parse(ownerSchema, JSON.parse(await readFile(join(input.lockPath, marker), "utf8")));
132
+ if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
133
+ throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
134
+ }
135
+ try {
136
+ process.kill(owner.pid, 0);
137
+ return;
138
+ } catch (error) {
139
+ if (isNodeError(error) && error.code === "EPERM") return;
140
+ if (!isNodeError(error) || error.code !== "ESRCH") throw error;
141
+ }
142
+ await unlink(join(input.lockPath, marker));
143
+ await this.removeEmptyLock(input.lockPath);
144
+ } catch (error) {
145
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
146
+ }
147
+ }
148
+ async removeEmptyLock(path) {
149
+ try {
150
+ await rmdir(path);
151
+ } catch (error) {
152
+ if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
153
+ }
154
+ }
155
+ };
156
+ function createRunFileCoordinator(runRoot) {
157
+ return new NornFileCoordinator({ lockRoot: join(runRoot, "locks"), waitTimeoutMs: 3e4 });
158
+ }
159
+ export {
160
+ NornFileCoordinator,
161
+ createRunFileCoordinator
162
+ };
@@ -3,21 +3,21 @@ export declare const LAUNCH_REQUEST_FILE_NAME = "launch-request.json";
3
3
  export declare const RESUME_REQUEST_FILE_NAME = "resume-request.json";
4
4
  export declare const RESUME_START_GRACE_MS = 60000;
5
5
  export type NornRunLaunchRequest = {
6
- readonly version: 1;
6
+ readonly version: 2;
7
7
  readonly type: "run";
8
8
  readonly id: string;
9
9
  readonly name: string;
10
10
  readonly workflowId: string;
11
- readonly params: unknown;
11
+ readonly args: unknown;
12
12
  readonly configOverride?: unknown;
13
13
  readonly createdAt: string;
14
14
  };
15
15
  export type NornRunResumeRequest = {
16
- readonly version: 1;
16
+ readonly version: 2;
17
17
  readonly type: "resume";
18
18
  readonly id: string;
19
19
  readonly requestId?: string;
20
- readonly params?: unknown;
20
+ readonly args?: unknown;
21
21
  readonly createdAt: string;
22
22
  };
23
23
  export declare function writeRunLaunchRequest(runRoot: string, request: NornRunLaunchRequest): Promise<void>;
@@ -80,7 +80,7 @@ async function clearRunResumeRequest(input) {
80
80
  function parseRunLaunchRequest(value) {
81
81
  if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
82
82
  const request = value;
83
- if (request.version !== 1 || request.type !== "run") throw new Error("Unsupported workflow launch request");
83
+ if (request.version !== 2 || request.type !== "run") throw new Error("Unsupported workflow launch request");
84
84
  if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
85
85
  if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
86
86
  if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
@@ -90,7 +90,7 @@ function parseRunLaunchRequest(value) {
90
90
  function parseRunResumeRequest(value) {
91
91
  if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
92
92
  const request = value;
93
- if (request.version !== 1 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
93
+ if (request.version !== 2 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
94
94
  if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
95
95
  if (request.requestId !== void 0 && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
96
96
  if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
@@ -1,5 +1,5 @@
1
1
  import { type WriteStream } from "node:fs";
2
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
2
+ import type { NornFileCoordinator } from "./file-coordinator.ts";
3
3
  import type { NornLogRef } from "@vimhead.dev/norn";
4
4
  export type NornLogWriteStream = {
5
5
  readonly log: NornLogRef;
@@ -1,4 +1,4 @@
1
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
1
+ import type { NornFileCoordinator } from "./file-coordinator.ts";
2
2
  export type NornRunManifestEvent = {
3
3
  readonly at: string;
4
4
  readonly type: string;
@@ -1,10 +1,10 @@
1
1
  import type { NornRunStatus, NornRunOutcomeMetadata, NornRunInfo } from "@vimhead.dev/norn";
2
2
  export declare const RUN_STATE_FILE_NAME = "run-state.json";
3
- export declare function mergeInterruptedWorkflowParams(currentParams: unknown, params: unknown, fields: readonly string[] | undefined): unknown;
4
- declare const RUN_STATE_VERSION = 1;
3
+ export declare function mergeInterruptedWorkflowArgs(currentArgs: unknown, args: unknown, fields: readonly string[] | undefined): unknown;
4
+ declare const RUN_STATE_VERSION = 2;
5
5
  type NornRunWorkflowStep = {
6
6
  readonly workflowId: string;
7
- readonly params: unknown;
7
+ readonly args: unknown;
8
8
  readonly cwd: string;
9
9
  readonly env: Record<string, string>;
10
10
  readonly interruption?: RuntimeInterruption;
@@ -39,7 +39,8 @@ type NornRunWorkflowOutcome = {
39
39
  readonly metadata?: NornRunOutcomeMetadata;
40
40
  };
41
41
  export type NornRunState = {
42
- readonly version: typeof RUN_STATE_VERSION;
42
+ readonly version: 1 | typeof RUN_STATE_VERSION;
43
+ readonly projectRoot?: string;
43
44
  readonly id: string;
44
45
  readonly name: string;
45
46
  readonly entrypointWorkflowId: string;
@@ -54,6 +55,7 @@ export type NornRunState = {
54
55
  readonly updatedAt: string;
55
56
  };
56
57
  type CreateNornRunStateInput = {
58
+ readonly projectRoot: string;
57
59
  readonly id: string;
58
60
  readonly name: string;
59
61
  readonly entrypointWorkflowId: string;
@@ -76,20 +78,21 @@ export declare class NornRunStateStore {
76
78
  failRun(workflowId: string, metadata: NornRunOutcomeMetadata & {
77
79
  readonly summary: string;
78
80
  }): Promise<void>;
79
- interruptCurrent(params: unknown, interruption: {
81
+ interruptCurrent(args: unknown, interruption: {
80
82
  readonly description: string;
81
83
  readonly fields?: readonly string[];
82
84
  }): Promise<void>;
83
85
  prepareForResumeAfterRollback(): Promise<void>;
84
86
  stopCurrent(): Promise<void>;
85
- replaceCurrentParams(params: unknown): Promise<void>;
86
- failCurrent(errorOrMetadata: string | NornRunOutcomeMetadata & {
87
+ replaceCurrentArgs(args: unknown): Promise<void>;
88
+ failCurrent(errorOrMetadata: string | (NornRunOutcomeMetadata & {
87
89
  readonly summary: string;
88
- }): Promise<void>;
90
+ })): Promise<void>;
89
91
  private update;
90
92
  private write;
91
93
  }
92
94
  export declare function listRuns(sessionCwd: string): Promise<NornRunInfo[]>;
93
95
  export declare function getRunInfo(runRoot: string): Promise<NornRunInfo>;
94
96
  export declare function resolveRunRoot(sessionCwd: string, run: string): Promise<string>;
97
+ export declare function assertRunVersion(version: unknown): void;
95
98
  export {};
@@ -39,21 +39,21 @@ async function writeTextAtomically(path, content) {
39
39
  }
40
40
 
41
41
  // src/internal/run-state.ts
42
- import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
42
+ import { createRunFileCoordinator } from "./file-coordinator.js";
43
43
  import { runCurrentRoot } from "./run-store.js";
44
44
  import { jsonValueSchema } from "@vimhead.dev/norn/schema";
45
45
  import { Value } from "typebox/value";
46
46
  var RUN_STATE_FILE_NAME = "run-state.json";
47
47
  var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
48
- function mergeInterruptedWorkflowParams(currentParams, params, fields) {
49
- if (!isRecord(currentParams) || !isRecord(params)) return params;
48
+ function mergeInterruptedWorkflowArgs(currentArgs, args, fields) {
49
+ if (!isRecord(currentArgs) || !isRecord(args)) return args;
50
50
  if (fields) {
51
- const unsupportedFields = Object.keys(params).filter((field) => !fields.includes(field));
52
- if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow params cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
51
+ const unsupportedFields = Object.keys(args).filter((field) => !fields.includes(field));
52
+ if (unsupportedFields.length > 0) throw new Error(`Interrupted workflow args cannot update non-gate fields: ${unsupportedFields.join(", ")}`);
53
53
  }
54
- return { ...currentParams, ...params };
54
+ return { ...currentArgs, ...args };
55
55
  }
56
- var RUN_STATE_VERSION = 1;
56
+ var RUN_STATE_VERSION = 2;
57
57
  var NornRunStateStore = class _NornRunStateStore {
58
58
  path;
59
59
  state;
@@ -70,7 +70,8 @@ var NornRunStateStore = class _NornRunStateStore {
70
70
  id: input.id,
71
71
  name: input.name,
72
72
  entrypointWorkflowId: input.entrypointWorkflowId,
73
- workspace: input.workspace,
73
+ projectRoot: resolve(input.projectRoot),
74
+ workspace: resolve(input.workspace),
74
75
  configOverride: input.configOverride,
75
76
  status: "running",
76
77
  current: input.current,
@@ -87,7 +88,7 @@ var NornRunStateStore = class _NornRunStateStore {
87
88
  static async load(runRoot) {
88
89
  const currentRoot = runCurrentRoot(runRoot);
89
90
  const files = createRunFileCoordinator(runRoot);
90
- 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) })));
91
+ 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) }), "execute"));
91
92
  return new _NornRunStateStore({ path: join(currentRoot, RUN_STATE_FILE_NAME), state, files });
92
93
  }
93
94
  currentState() {
@@ -125,11 +126,11 @@ var NornRunStateStore = class _NornRunStateStore {
125
126
  failed: { workflowId, error: metadata.summary, metadata, failedAt }
126
127
  });
127
128
  }
128
- async interruptCurrent(params, interruption) {
129
+ async interruptCurrent(args, interruption) {
129
130
  if (!this.state.current) throw new Error("Cannot interrupt run without current step");
130
131
  await this.update({
131
132
  status: "interrupted",
132
- current: { ...this.state.current, params, interruption: { status: "pending", ...interruption } },
133
+ current: { ...this.state.current, args, interruption: { status: "pending", ...interruption } },
133
134
  outcome: null,
134
135
  failed: null
135
136
  });
@@ -144,11 +145,11 @@ var NornRunStateStore = class _NornRunStateStore {
144
145
  if (!this.state.current) throw new Error("Cannot stop run without current step");
145
146
  await this.update({ status: "stopped", outcome: null, failed: null });
146
147
  }
147
- async replaceCurrentParams(params) {
148
+ async replaceCurrentArgs(args) {
148
149
  if (!this.state.current) throw new Error("Cannot resume run without current step");
149
150
  await this.update({
150
151
  status: "running",
151
- current: { ...this.state.current, params, interruption: { ...this.state.current.interruption, status: "satisfied" } },
152
+ current: { ...this.state.current, args, interruption: { ...this.state.current.interruption, status: "satisfied" } },
152
153
  outcome: null,
153
154
  failed: null
154
155
  });
@@ -165,15 +166,15 @@ var NornRunStateStore = class _NornRunStateStore {
165
166
  }
166
167
  async update(patch) {
167
168
  await this.files.withExclusiveLock(this.path, async (path) => {
168
- const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
169
+ const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname2(this.path), LEGACY_RUN_STATE_FILE_NAME) }), "execute");
169
170
  const next = { ...current, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
170
- if (next.current?.params !== void 0) Value.Assert(jsonValueSchema, next.current.params);
171
+ if (next.current?.args !== void 0) Value.Assert(jsonValueSchema, next.current.args);
171
172
  await writeJsonAtomically(path, next);
172
173
  this.state = next;
173
174
  });
174
175
  }
175
176
  async write() {
176
- if (this.state.current?.params !== void 0) Value.Assert(jsonValueSchema, this.state.current.params);
177
+ if (this.state.current?.args !== void 0) Value.Assert(jsonValueSchema, this.state.current.args);
177
178
  await mkdir2(dirname2(this.path), { recursive: true });
178
179
  await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
179
180
  }
@@ -192,13 +193,19 @@ async function listRuns(sessionCwd) {
192
193
  }
193
194
  async function getRunInfo(runRoot) {
194
195
  try {
195
- const resumeRequest = await readOptionalRunResumeRequest(runRoot);
196
- const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)));
196
+ const pendingResume = await readOptionalRunResumeRequest(runRoot).then(
197
+ (request) => ({ status: "fulfilled", request }),
198
+ (error) => ({ status: "rejected", error })
199
+ );
200
+ const state = parseNornRunState(await readRunStateJson(runCurrentRoot(runRoot)), "inspect");
201
+ if (state.version === RUN_STATE_VERSION && pendingResume.status === "rejected") throw pendingResume.error;
202
+ const resumeRequest = state.version === RUN_STATE_VERSION && pendingResume.status === "fulfilled" ? pendingResume.request : void 0;
197
203
  return {
198
204
  version: state.version,
199
205
  id: state.id,
200
206
  name: state.name,
201
207
  path: runRoot,
208
+ paths: { project: state.projectRoot ?? resolve(runRoot, "../../.."), workspace: state.workspace },
202
209
  entrypointWorkflowId: state.entrypointWorkflowId,
203
210
  currentWorkflowId: state.current?.workflowId,
204
211
  status: resumeRequest ? "running" : state.status,
@@ -255,6 +262,7 @@ async function getLaunchedRunInfo(runRoot) {
255
262
  id: launchRequest.id,
256
263
  name: launchRequest.name,
257
264
  path: runRoot,
265
+ paths: { project: resolve(runRoot, "../../.."), workspace: join(runCurrentRoot(resolve(runRoot)), "workspace") },
258
266
  entrypointWorkflowId: launchRequest.workflowId,
259
267
  currentWorkflowId: launchRequest.workflowId,
260
268
  status: "running",
@@ -277,10 +285,13 @@ async function readRunStateFile(input) {
277
285
  function isRecord(value) {
278
286
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
279
287
  }
280
- function parseNornRunState(value) {
288
+ function assertRunVersion(version) {
289
+ if (version !== RUN_STATE_VERSION) throw new Error(`Incompatible saved run state version ${String(version)}; this alpha requires version ${RUN_STATE_VERSION}. Use the original Norn version to resume this run, or start a new run. Saved files have not been migrated.`);
290
+ }
291
+ function parseNornRunState(value, mode) {
281
292
  if (!value || typeof value !== "object") throw new Error("Invalid run state");
282
293
  const state = value;
283
- if (state.version !== RUN_STATE_VERSION) throw new Error(`Unsupported run state version: ${String(state.version)}`);
294
+ if (mode === "execute" || state.version !== 1) assertRunVersion(state.version);
284
295
  if (typeof state.id !== "string" || state.id.length === 0) throw new Error("Invalid run state id");
285
296
  const normalizedState = state;
286
297
  if (typeof normalizedState.name !== "string" || normalizedState.name.length === 0) normalizedState.name = state.id;
@@ -306,7 +317,7 @@ function runInterruption(state) {
306
317
  if (state.status !== "interrupted" || !state.current?.interruption) return void 0;
307
318
  return {
308
319
  workflowId: state.current.workflowId,
309
- params: state.current.params,
320
+ args: state.version === 1 ? state.current.params : state.current.args,
310
321
  description: state.current.interruption.description ?? "",
311
322
  fields: state.current.interruption.fields
312
323
  };
@@ -320,8 +331,9 @@ function runFailure(state) {
320
331
  export {
321
332
  NornRunStateStore,
322
333
  RUN_STATE_FILE_NAME,
334
+ assertRunVersion,
323
335
  getRunInfo,
324
336
  listRuns,
325
- mergeInterruptedWorkflowParams,
337
+ mergeInterruptedWorkflowArgs,
326
338
  resolveRunRoot
327
339
  };
@@ -1,51 +1,31 @@
1
1
  import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
2
- import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornRun } from "@vimhead.dev/norn";
2
+ import type { NornRunOutcomeMetadata, NornRun } from "@vimhead.dev/norn";
3
3
  import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
4
- import type { NornArtifacts } from "./artifacts.ts";
5
4
  import type { NornRunLogs } from "./logs.ts";
6
5
  import type { NornRunLogger } from "./run-log.ts";
7
- import type { NornRunResources } from "../resources.ts";
8
6
  type DefaultNornRunInput = {
9
7
  readonly id: string;
10
8
  readonly runRoot: string;
11
- readonly projectRoot: string;
12
- readonly workspace: string;
13
- readonly cwd: string;
14
- readonly isolationMode: NornWorkflowIsolationMode;
15
9
  readonly signal?: AbortSignal;
16
10
  readonly model?: CreateAgentSessionOptions["model"];
17
11
  readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
18
12
  readonly agentDir?: string;
19
13
  readonly responseCollector: NornAgentResponseCollector;
20
- readonly resources: NornRunResources;
21
- readonly state: NornRun["state"];
22
14
  readonly logger: NornRunLogger;
23
- readonly artifacts: NornArtifacts;
24
15
  readonly logs: NornRunLogs;
25
16
  };
26
- export declare class NornRunContext implements NornProjectRun {
17
+ export declare class NornRunContext implements NornRun {
27
18
  private readonly input;
28
- readonly resources: NornRunResources;
29
- readonly state: NornRun["state"];
30
- readonly artifacts: NornRun["artifacts"];
31
19
  readonly logs: NornRun["logs"];
32
20
  readonly commands: NornRun["commands"];
33
21
  readonly agents: NornRun["agents"];
34
22
  readonly id: string;
35
- readonly workspace: string;
36
- readonly projectRoot: string;
37
- readonly cwd: string;
38
23
  constructor(input: DefaultNornRunInput);
39
- forWorkflow(workflow: NornAnyWorkflowDeclaration): NornRunContext;
40
- private get boundaryRoot();
41
24
  private createAgentRunner;
42
- path(relativePath: string): string;
43
- projectPath(relativePath: string): string;
44
- next(workflowId: string, params: unknown): ReturnType<NornRun["next"]>;
25
+ next(workflowId: string, args: unknown): ReturnType<NornRun["next"]>;
45
26
  complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]>;
46
27
  fail(metadata: NornRunOutcomeMetadata & {
47
28
  readonly summary: string;
48
29
  }): ReturnType<NornRun["fail"]>;
49
- private resolveFromRoot;
50
30
  }
51
31
  export {};
@@ -1,33 +1,21 @@
1
- // src/internal/run.ts
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
-
4
1
  // ../core/src/workflow-transition.ts
5
2
  function createWorkflowTransition(input) {
6
3
  if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
7
- return { type: "next", workflowId: input.workflowId, params: input.params };
4
+ return { type: "next", workflowId: input.workflowId, args: input.args };
8
5
  }
9
6
 
10
7
  // src/internal/run.ts
11
8
  import { NornAgentRunner } from "./agents.js";
12
9
  import { NornCommandRunner } from "./commands.js";
13
- var NornRunContext = class _NornRunContext {
10
+ var NornRunContext = class {
14
11
  constructor(input) {
15
12
  this.input = input;
16
13
  this.id = input.id;
17
- this.workspace = input.workspace;
18
- this.projectRoot = input.projectRoot;
19
- this.cwd = input.cwd;
20
- this.resources = input.resources;
21
- this.state = input.state;
22
- this.artifacts = input.artifacts;
23
14
  this.logs = {
24
15
  read: (log) => this.input.logs.read(log)
25
16
  };
26
17
  this.commands = {
27
18
  run: (commandInput) => new NornCommandRunner({
28
- boundaryRoot: this.boundaryRoot,
29
- boundaryName: this.input.isolationMode,
30
- cwd: this.cwd,
31
19
  signal: this.input.signal,
32
20
  logs: this.input.logs,
33
21
  logger: this.input.logger
@@ -39,34 +27,14 @@ var NornRunContext = class _NornRunContext {
39
27
  };
40
28
  }
41
29
  input;
42
- resources;
43
- state;
44
- artifacts;
45
30
  logs;
46
31
  commands;
47
32
  agents;
48
33
  id;
49
- workspace;
50
- projectRoot;
51
- cwd;
52
- forWorkflow(workflow) {
53
- const isolationMode = workflow.isolation.mode;
54
- return new _NornRunContext({
55
- ...this.input,
56
- isolationMode,
57
- cwd: isolationMode === "project" ? this.projectRoot : this.workspace
58
- });
59
- }
60
- get boundaryRoot() {
61
- return this.input.isolationMode === "project" ? this.projectRoot : this.workspace;
62
- }
63
34
  createAgentRunner() {
64
35
  return new NornAgentRunner({
65
36
  id: this.id,
66
37
  runRoot: this.input.runRoot,
67
- boundaryRoot: this.boundaryRoot,
68
- boundaryName: this.input.isolationMode,
69
- cwd: this.cwd,
70
38
  signal: this.input.signal,
71
39
  model: this.input.model,
72
40
  thinkingLevel: this.input.thinkingLevel,
@@ -76,14 +44,8 @@ var NornRunContext = class _NornRunContext {
76
44
  responseCollector: this.input.responseCollector
77
45
  });
78
46
  }
79
- path(relativePath) {
80
- return this.resolveFromRoot(this.cwd, this.boundaryRoot, relativePath);
81
- }
82
- projectPath(relativePath) {
83
- return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
84
- }
85
- next(workflowId, params) {
86
- return createWorkflowTransition({ workflowId, params });
47
+ next(workflowId, args) {
48
+ return createWorkflowTransition({ workflowId, args });
87
49
  }
88
50
  complete(metadata) {
89
51
  return { type: "complete", metadata };
@@ -91,14 +53,6 @@ var NornRunContext = class _NornRunContext {
91
53
  fail(metadata) {
92
54
  return { type: "fail", metadata };
93
55
  }
94
- resolveFromRoot(root, boundaryRoot, path) {
95
- const resolvedPath = isAbsolute(path) ? path : resolve(root, path);
96
- const pathFromBoundary = relative(boundaryRoot, resolvedPath);
97
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
98
- throw new Error(`Workflow path escapes ${this.input.isolationMode} isolation: ${path}`);
99
- }
100
- return resolvedPath;
101
- }
102
56
  };
103
57
  export {
104
58
  NornRunContext
@@ -0,0 +1 @@
1
+ export declare function prepareRunWorkerDirectory(runRoot: string): Promise<string>;
@@ -0,0 +1,26 @@
1
+ // src/internal/worker-directory.ts
2
+ import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
3
+ import { join } 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/worker-directory.ts
11
+ async function prepareRunWorkerDirectory(runRoot) {
12
+ const directory = join(runRoot, "worker");
13
+ try {
14
+ await mkdir(directory, { mode: 365 });
15
+ } catch (error) {
16
+ if (!isNodeError(error) || error.code !== "EEXIST") throw error;
17
+ }
18
+ const metadata = await lstat(directory);
19
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`Run worker cwd must be a directory, not a symlink: ${directory}`);
20
+ if ((await readdir(directory)).length !== 0) throw new Error(`Run worker cwd must be empty: ${directory}`);
21
+ await chmod(directory, 365);
22
+ return directory;
23
+ }
24
+ export {
25
+ prepareRunWorkerDirectory
26
+ };