@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,136 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
3
+ import { hostname } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { z } from "zod";
7
+ import { isNodeError } from "@vimhead.dev/norn-core/errors";
8
+ import { writeTextAtomically } from "@vimhead.dev/norn-core/atomic-files";
9
+
10
+ const ownerSchema = z.strictObject({
11
+ token: z.uuid(),
12
+ pid: z.number().int().positive(),
13
+ host: z.string(),
14
+ target: z.string(),
15
+ });
16
+
17
+ export class NornFileCoordinator {
18
+ private readonly lockRoot: string;
19
+
20
+ constructor(private readonly input: { readonly lockRoot: string; readonly waitTimeoutMs: number }) {
21
+ if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0) throw new Error("Lock wait timeout must be a positive integer");
22
+ this.lockRoot = resolve(input.lockRoot);
23
+ }
24
+
25
+ async withExclusiveLock<T>(path: string, operation: (lockedPath: string) => Promise<T>): Promise<T> {
26
+ const target = await this.resolveTarget(path);
27
+ await mkdir(this.lockRoot, { recursive: true, mode: 0o700 });
28
+ const key = createHash("sha256").update(target).digest("hex");
29
+ const lockPath = join(this.lockRoot, `${key}.lock`);
30
+ const stagingPath = await mkdtemp(join(this.lockRoot, `${key}.pending-`));
31
+ const token = randomUUID();
32
+ const marker = `${token}.json`;
33
+ return this.withCleanup({
34
+ operation: async () => {
35
+ await writeFile(join(stagingPath, marker), JSON.stringify({ token, pid: process.pid, host: hostname(), target }), { mode: 0o600 });
36
+ await this.acquire({ stagingPath, lockPath, target });
37
+ return this.withCleanup({
38
+ operation: () => operation(target),
39
+ cleanup: async () => {
40
+ await unlink(join(lockPath, marker));
41
+ await this.removeEmptyLock(lockPath);
42
+ },
43
+ });
44
+ },
45
+ cleanup: () => rm(stagingPath, { recursive: true, force: true }),
46
+ });
47
+ }
48
+
49
+ async readText(path: string): Promise<string> {
50
+ return this.withExclusiveLock(path, (lockedPath) => readFile(lockedPath, "utf8"));
51
+ }
52
+
53
+ async writeText(path: string, content: string): Promise<void> {
54
+ await mkdir(dirname(path), { recursive: true });
55
+ await this.withExclusiveLock(path, (lockedPath) => writeTextAtomically(lockedPath, content));
56
+ }
57
+
58
+ private async withCleanup<T>(input: { readonly operation: () => Promise<T>; readonly cleanup: () => Promise<void> }): Promise<T> {
59
+ let result: T;
60
+ try {
61
+ result = await input.operation();
62
+ } catch (error) {
63
+ try { await input.cleanup(); }
64
+ catch (cleanupError) { throw new AggregateError([error, cleanupError], "File operation and lock cleanup failed"); }
65
+ throw error;
66
+ }
67
+ await input.cleanup();
68
+ return result;
69
+ }
70
+
71
+ private async resolveTarget(path: string): Promise<string> {
72
+ try {
73
+ return await realpath(path);
74
+ } catch (error) {
75
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
76
+ const entry = await lstat(path).catch((error: unknown) => {
77
+ if (isNodeError(error) && error.code === "ENOENT") return undefined;
78
+ throw error;
79
+ });
80
+ if (entry?.isSymbolicLink()) throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
81
+ return join(await realpath(dirname(path)), basename(path));
82
+ }
83
+ }
84
+
85
+ private async acquire(input: { readonly stagingPath: string; readonly lockPath: string; readonly target: string }): Promise<void> {
86
+ const deadline = Date.now() + this.input.waitTimeoutMs;
87
+ while (true) {
88
+ try {
89
+ await rename(input.stagingPath, input.lockPath);
90
+ return;
91
+ } catch (error) {
92
+ if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
93
+ }
94
+ await this.reclaimDeadOwner(input);
95
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for file lock: ${input.target}`);
96
+ await delay(10);
97
+ }
98
+ }
99
+
100
+ private async reclaimDeadOwner(input: { readonly lockPath: string; readonly target: string }): Promise<void> {
101
+ try {
102
+ const entries = await readdir(input.lockPath);
103
+ if (entries.length === 0) return;
104
+ if (entries.length !== 1) throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
105
+ const marker = entries[0];
106
+ const owner = ownerSchema.parse(JSON.parse(await readFile(join(input.lockPath, marker), "utf8")));
107
+ if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
108
+ throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
109
+ }
110
+ try {
111
+ process.kill(owner.pid, 0);
112
+ return;
113
+ } catch (error) {
114
+ if (isNodeError(error) && error.code === "EPERM") return;
115
+ if (!isNodeError(error) || error.code !== "ESRCH") throw error;
116
+ }
117
+ // A delayed reaper must never remove a successor's differently named marker.
118
+ await unlink(join(input.lockPath, marker));
119
+ await this.removeEmptyLock(input.lockPath);
120
+ } catch (error) {
121
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
122
+ }
123
+ }
124
+
125
+ private async removeEmptyLock(path: string): Promise<void> {
126
+ try {
127
+ await rmdir(path);
128
+ } catch (error) {
129
+ if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? "")) throw error;
130
+ }
131
+ }
132
+ }
133
+
134
+ export function createRunFileCoordinator(runRoot: string): NornFileCoordinator {
135
+ return new NornFileCoordinator({ lockRoot: join(runRoot, "locks"), waitTimeoutMs: 30_000 });
136
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./api.ts";
2
+ export { NornFileCoordinator } from "./files.ts";
3
+ export * from "./resources.ts";
4
+ export * from "./agent-resource-adapter.ts";
5
+ export * from "./state-adapter.ts";
6
+ export * from "./seer/index.ts";
@@ -0,0 +1,20 @@
1
+ import type { z } from "zod";
2
+ import type { NornFileCoordinator } from "./files.ts";
3
+
4
+ export type NornResourceContext = {
5
+ readonly mode: "create" | "open";
6
+ readonly directory: string;
7
+ readonly files: NornFileCoordinator;
8
+ };
9
+
10
+ export type NornResourceDefinition<T> = {
11
+ readonly name: string;
12
+ readonly kind: string;
13
+ readonly configuration: z.infer<ReturnType<typeof z.json>>;
14
+ initialize(context: NornResourceContext): Promise<T>;
15
+ };
16
+
17
+ export type NornResources = {
18
+ readonly files: NornFileCoordinator;
19
+ ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
20
+ };
@@ -0,0 +1,48 @@
1
+ import type { NornAnyWorkflowDeclaration } from "./api.ts";
2
+
3
+ export function assertWorkflowMetadata(workflow: NornAnyWorkflowDeclaration): void {
4
+ if (workflow.instructions !== undefined && (typeof workflow.instructions !== "string" || workflow.instructions.trim().length === 0)) {
5
+ throw new Error(`Workflow instructions must be a nonempty string: ${workflow.id}`);
6
+ }
7
+ if (workflow.isEntrypoint && workflow.instructions === undefined) {
8
+ throw new Error(`Entrypoint workflow requires instructions: ${workflow.id}`);
9
+ }
10
+ }
11
+
12
+ export function unwrapSchema(schema: unknown): unknown {
13
+ let current = schema;
14
+ while (true) {
15
+ const def = schemaDef(current);
16
+ if (["optional", "nullable", "default", "catch", "readonly", "prefault"].includes(def.type ?? "") && def.innerType) {
17
+ current = def.innerType;
18
+ continue;
19
+ }
20
+ return current;
21
+ }
22
+ }
23
+
24
+ export function schemaShape(schema: unknown): Record<string, unknown> {
25
+ const shape = schemaDef(unwrapSchema(schema)).shape;
26
+ if (!shape) return {};
27
+ return typeof shape === "function" ? shape() as Record<string, unknown> : shape as Record<string, unknown>;
28
+ }
29
+
30
+ export function schemaType(schema: unknown): string | undefined {
31
+ return schemaDef(unwrapSchema(schema)).type;
32
+ }
33
+
34
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
35
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+
38
+ type ZodDef = {
39
+ readonly type?: string;
40
+ readonly innerType?: unknown;
41
+ readonly shape?: unknown;
42
+ };
43
+
44
+ function schemaDef(schema: unknown): ZodDef {
45
+ if (!schema || typeof schema !== "object") return {};
46
+ const candidate = schema as { _def?: ZodDef; def?: ZodDef };
47
+ return candidate._def ?? candidate.def ?? {};
48
+ }
@@ -0,0 +1,62 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+
3
+ export type NornSeerModeConfig = {
4
+ readonly writableRoots: readonly string[];
5
+ };
6
+
7
+ export type NornResolvedSeerModeConfig = {
8
+ readonly configPath: string;
9
+ readonly projectRoot: string;
10
+ readonly writableRoots: readonly string[];
11
+ };
12
+
13
+ type ResolveSeerModeConfigInput = {
14
+ readonly configPath: string;
15
+ readonly configRoot: string;
16
+ readonly seerMode: NornSeerModeConfig | undefined;
17
+ };
18
+
19
+ export function resolveSeerModeConfig(input: ResolveSeerModeConfigInput): NornResolvedSeerModeConfig | undefined {
20
+ if (!input.seerMode) return undefined;
21
+ if (input.seerMode.writableRoots.length === 0) throw new Error("Norn seerMode.writableRoots must not be empty");
22
+ const projectRoot = resolve(input.configRoot);
23
+ const writableRoots = Array.from(new Set(input.seerMode.writableRoots.map((path) => resolveWritableRoot(projectRoot, path))));
24
+ return { configPath: input.configPath, projectRoot, writableRoots };
25
+ }
26
+
27
+ export function assertSeerModeWritablePath(seerMode: NornResolvedSeerModeConfig, cwd: string, path: string): string {
28
+ const resolvedPath = resolveSeerModePath(seerMode, cwd, path);
29
+ if (!isSeerModeWritableResolvedPath(seerMode, resolvedPath)) {
30
+ throw new Error(`Path is outside Norn seerMode writable roots: ${path}`);
31
+ }
32
+ return resolvedPath;
33
+ }
34
+
35
+ export function isSeerModeWritablePath(seerMode: NornResolvedSeerModeConfig, cwd: string, path: string): boolean {
36
+ return isSeerModeWritableResolvedPath(seerMode, resolveSeerModePath(seerMode, cwd, path));
37
+ }
38
+
39
+ function resolveWritableRoot(projectRoot: string, path: string): string {
40
+ if (path.length === 0) throw new Error("Norn seerMode writable root must not be empty");
41
+ const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(projectRoot, path);
42
+ if (!isInsideOrEqual(projectRoot, resolvedPath)) throw new Error(`Norn seerMode writable root escapes project root: ${path}`);
43
+ return resolvedPath;
44
+ }
45
+
46
+ function resolveSeerModePath(seerMode: NornResolvedSeerModeConfig, cwd: string, path: string): string {
47
+ if (path.length === 0) throw new Error("Norn seerMode path must not be empty");
48
+ const resolvedCwd = resolve(cwd);
49
+ if (!isInsideOrEqual(seerMode.projectRoot, resolvedCwd)) throw new Error(`Norn seerMode cwd escapes project root: ${cwd}`);
50
+ const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedCwd, path);
51
+ if (!isInsideOrEqual(seerMode.projectRoot, resolvedPath)) return resolvedPath;
52
+ return resolvedPath;
53
+ }
54
+
55
+ function isSeerModeWritableResolvedPath(seerMode: NornResolvedSeerModeConfig, path: string): boolean {
56
+ return seerMode.writableRoots.some((root) => isInsideOrEqual(root, path));
57
+ }
58
+
59
+ function isInsideOrEqual(root: string, path: string): boolean {
60
+ const pathFromRoot = relative(root, path);
61
+ return pathFromRoot === "" || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== ".." && !isAbsolute(pathFromRoot));
62
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ assertSeerModeWritablePath,
3
+ isSeerModeWritablePath,
4
+ resolveSeerModeConfig,
5
+ type NornResolvedSeerModeConfig,
6
+ type NornSeerModeConfig,
7
+ } from "./config.ts";
@@ -0,0 +1,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { defineTool } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { z } from "zod";
5
+ import type { NornWorkflowState, NornWorkflowStateDefinition } from "./api.ts";
6
+ import type { NornAgentResourceAdapter } from "./agent-resource-adapter.ts";
7
+
8
+ export type NornStateFieldAccess = {
9
+ readonly field: NornWorkflowStateDefinition;
10
+ readonly access: "read" | "write" | "read-write";
11
+ };
12
+
13
+ const pageParameters = {
14
+ offset: Type.Integer({ minimum: 0, description: "Zero-based UTF-16 offset into the serialized JSON. Start at 0." }),
15
+ limit: Type.Integer({ minimum: 1, maximum: 10000 }),
16
+ };
17
+
18
+ export function StateAdapter(input: { readonly state: NornWorkflowState; readonly fields: readonly NornStateFieldAccess[] }): NornAgentResourceAdapter {
19
+ const fields = new Map(input.fields.map((grant) => [grant.field.id, grant]));
20
+ if (fields.size !== input.fields.length || fields.size === 0) throw new Error("State attachment requires unique, explicitly selected fields");
21
+ const selectField = (key: string, access: "read" | "write") => {
22
+ const grant = fields.get(key);
23
+ if (!grant || (grant.access !== access && grant.access !== "read-write")) throw new Error(`State ${access} is not attached: ${key}`);
24
+ return grant.field;
25
+ };
26
+ return {
27
+ name: "norn.state",
28
+ async bind() {
29
+ return {
30
+ tools: [
31
+ defineTool({
32
+ name: "norn_state_list",
33
+ label: "Attached workflow state",
34
+ description: "List only attached workflow-state field IDs, permissions and value schemas. JSON is paginated; use nextOffset until null.",
35
+ parameters: Type.Object(pageParameters),
36
+ async execute(_id, params) {
37
+ return serializePage({ value: [...fields.values()].map(({ field, access }) => ({ id: field.id, access, schema: z.toJSONSchema(field.schema, { io: "input" }) })), ...params });
38
+ },
39
+ }),
40
+ defineTool({
41
+ name: "norn_state_get",
42
+ label: "Read workflow state",
43
+ description: "Read a selected workflow-state field. Unset fields return isSet:false. JSON is paginated; concurrent writes can change later pages, so compare revision before combining pages.",
44
+ parameters: Type.Object({ key: Type.String(), ...pageParameters }),
45
+ async execute(_id, params) {
46
+ const value = await input.state.getOptional(selectField(params.key, "read"));
47
+ return serializePage({ value: value === undefined ? { isSet: false } : { isSet: true, value }, ...params });
48
+ },
49
+ }),
50
+ defineTool({
51
+ name: "norn_state_set",
52
+ label: "Write workflow state",
53
+ description: "Set an explicitly writable workflow-state field. Validate the value against its schema from norn_state_list. A get followed by set is not a transaction.",
54
+ parameters: Type.Object({ key: Type.String(), value: Type.Unknown() }),
55
+ async execute(_id, params, signal) {
56
+ signal?.throwIfAborted();
57
+ const field = selectField(params.key, "write");
58
+ await input.state.set(field, params.value);
59
+ return { content: [{ type: "text", text: "Workflow state saved." }], details: {} };
60
+ },
61
+ }),
62
+ ],
63
+ async dispose() {},
64
+ };
65
+ },
66
+ };
67
+ }
68
+
69
+ function serializePage(input: { readonly value: unknown; readonly offset: number; readonly limit: number }) {
70
+ const serialized = JSON.stringify(input.value);
71
+ if (!Number.isInteger(input.offset) || input.offset < 0 || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 10000) throw new Error("Invalid state output page");
72
+ const end = Math.min(serialized.length, input.offset + input.limit);
73
+ const details = { text: serialized.slice(input.offset, end), nextOffset: end < serialized.length ? end : null, revision: createHash("sha256").update(serialized).digest("hex") };
74
+ return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
75
+ }
@@ -0,0 +1,128 @@
1
+ # Providers and authentication
2
+
3
+ `norn pi` runs the Pi CLI bundled with the selected Norn installation. It needs
4
+ neither a separate Pi installation nor a Norn project. Everything after `pi` is
5
+ forwarded to Pi, including its terminal UI, package commands, and output modes:
6
+
7
+ ```bash
8
+ norn pi --version
9
+ norn pi --help
10
+ norn pi
11
+ ```
12
+
13
+ In the interactive session, use `/login` to authenticate. Open `/model`, highlight
14
+ the desired model, and press **Ctrl+S** to save it as the startup default. Selecting
15
+ a model is not the same as saving a startup default. Use `/quit` to exit.
16
+ Configure these before launching Norn agents: detached execution cannot conduct
17
+ interactive login. Authentication in the outer harness does not automatically
18
+ authenticate a Norn agent's provider.
19
+
20
+ ## Norn configuration
21
+
22
+ The proxy and Norn agents share Norn's agent directory, normally `~/.norn/agent/`.
23
+ It is independent of the outer Pi harness's global configuration:
24
+
25
+ - `settings.json` — installed packages and default provider/model
26
+ - `auth.json` — saved API keys and OAuth credentials
27
+ - `models.json` — custom endpoints, models, and authentication configuration
28
+
29
+ `NORN_AGENT_DIR` selects another directory. Set it consistently for both
30
+ `norn pi` and workflow execution. A programmatic runtime caller supplying `agentDir`
31
+ must point the setup command at that same directory. Norn does not use an inherited
32
+ `PI_CODING_AGENT_DIR` to select its global configuration. Existing Pi packages and
33
+ credentials are not imported or linked: install providers and authenticate through
34
+ `norn pi`, even when Pi is the outer harness.
35
+
36
+ Project-local `.pi` settings and extension discovery still depend on the working
37
+ directory; installing a provider globally avoids making
38
+ it available only in one project or worktree.
39
+
40
+ Login is optional when credentials are supplied another way. Norn agents inherit
41
+ provider environment variables such as `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`.
42
+ Custom providers can resolve environment variables or secret-manager commands
43
+ through `models.json`. Keep credentials out of project files and version control.
44
+ Finding a configured key or listing a model is not proof that a provider accepts it.
45
+
46
+ ## Third-party provider packages
47
+
48
+ Pi provider extensions are separate from Norn workflow plugins and outer-harness
49
+ adapters. They register providers through Pi's APIs; do not put them in
50
+ `norn.project.json` or install them only in Cursor's plugin marketplace.
51
+
52
+ Use `norn pi install` with the package's published npm spec or Git URL. For example:
53
+
54
+ ```bash
55
+ norn pi install npm:pi-cursor-sdk@0.3.6
56
+ norn pi --list-models cursor
57
+ norn pi
58
+ ```
59
+
60
+ This installs into Norn's global configuration. Select the new provider using
61
+ `/model`. Agent resource discovery and model precedence are covered in
62
+ [Norn agents](../docs/agents.md#prompts-tools-and-resource-loading).
63
+ Use `norn pi list` to inspect installations and `norn pi remove <source>` to remove
64
+ one. Upgrade bundled Pi by upgrading Norn, not by using Pi's self-update command.
65
+ `norn pi update --extensions` updates unpinned extension packages.
66
+
67
+ Review third-party packages before installation: extensions and dependency install
68
+ scripts execute code with the user's permissions. npm packages require Node/npm;
69
+ Git sources additionally require Git. A standalone Norn binary does not bundle
70
+ those installers or every third-party package's native dependencies. Check package
71
+ requirements against `norn pi --version` and the local platform.
72
+
73
+ ### Cursor SDK example
74
+
75
+ [pi-cursor-sdk](https://pi.dev/packages/pi-cursor-sdk) registers the `cursor`
76
+ provider. Its API key must be a **Cursor SDK API key** (user or service account),
77
+ not a Team Admin key. It does not reuse Cursor Desktop or Agent CLI login.
78
+
79
+ After installation, run `/login cursor` inside `norn pi` and enter the SDK key,
80
+ or supply `CURSOR_API_KEY` to Norn's launching environment. Select a Cursor model
81
+ with `/model`. A newly started session reloads the provider; its model listing can
82
+ contain fallback models even without working credentials.
83
+
84
+ Keep the provider's **local runtime and Pi tool bridge enabled** for Norn agents.
85
+ Norn requires its structured-response tool, and attached resources also expose Pi
86
+ tools. The provider's cloud mode does not expose that local bridge. Cursor-native
87
+ tools are a separate surface: restricting Norn's `tools` list does not disable
88
+ Cursor's own tools or ambient configuration. See the package's documentation for
89
+ its runtime and isolation controls.
90
+
91
+ Installation and fallback-model discovery for version 0.3.6 were exercised through
92
+ a standalone Norn binary without credentials. Authenticated Cursor inference and
93
+ its Norn response-tool bridge require a separate live check; model discovery alone
94
+ does not establish end-to-end compatibility.
95
+
96
+ ## Custom endpoints without an extension
97
+
98
+ For a provider using an existing API protocol, add its configuration under
99
+ `providers` in the shared `models.json`, preserving existing entries:
100
+
101
+ ```json
102
+ {
103
+ "providers": {
104
+ "team-gateway": {
105
+ "baseUrl": "https://gateway.example.com/v1",
106
+ "api": "openai-completions",
107
+ "apiKey": "$TEAM_GATEWAY_API_KEY",
108
+ "models": [{ "id": "team-model" }]
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ Set `TEAM_GATEWAY_API_KEY` in the launching environment, then select the model
115
+ through `norn pi`. A provider with a different protocol or custom OAuth flow needs
116
+ a compatible Pi provider extension, not just an `auth.json` entry. Use that
117
+ extension's documented authentication method; Norn does not duplicate its login
118
+ or token-refresh implementation.
119
+
120
+ ## Standalone assets
121
+
122
+ Compiled Norn includes the bundled Pi version metadata, themes, documentation,
123
+ examples, and export assets. It extracts and verifies them in a content-specific
124
+ cache before loading Pi. `NORN_PI_CACHE_DIR` overrides its root; otherwise it uses
125
+ `~/Library/Caches/norn/pi` on macOS, `$XDG_CACHE_HOME/norn/pi` (or
126
+ `~/.cache/norn/pi`) on Linux, and `%LOCALAPPDATA%/norn/pi` on Windows.
127
+ The asset cache is not the credential directory. A modified or incomplete entry
128
+ causes an error naming the entry to remove before retrying.
@@ -0,0 +1,76 @@
1
+ # Rolling releases
2
+
3
+ The public npm packages are `@vimhead.dev/norn`, `@vimhead.dev/norn-cli`, and
4
+ `@vimhead.dev/pi-norn`. The root and core workspaces are private.
5
+
6
+ ## Local preparation
7
+
8
+ ```bash
9
+ pnpm install --frozen-lockfile
10
+ pnpm check
11
+ pnpm test
12
+ pnpm pack:dry
13
+ ```
14
+
15
+ `pack:dry` writes real pnpm tarballs and a checksum manifest into `dist/npm/`;
16
+ it does not publish. pnpm rewrites the CLI's workspace dependency to the exact
17
+ Norn library version. Consumer builds inline private core code.
18
+
19
+ ## One-time npm bootstrap
20
+
21
+ An npm owner with access to the `vimhead.dev` organization must publish each new
22
+ package before configuring its trusted publisher. Use a disposable checkout so
23
+ release version stamping does not modify the development checkout:
24
+
25
+ ```bash
26
+ npm login --registry=https://registry.npmjs.org/
27
+ git worktree add --detach /tmp/norn-bootstrap HEAD
28
+ cd /tmp/norn-bootstrap
29
+ pnpm install --frozen-lockfile
30
+ pnpm release:prepare 0.1.0-tip.0 "$(git rev-parse HEAD)"
31
+ pnpm release:pack
32
+ pnpm test
33
+ pnpm release:publish --interactive
34
+ ```
35
+
36
+ Choose an unused prerelease version if `0.1.0-tip.0` already exists. The publication
37
+ command prompts through npm for any required authentication/2FA; never put
38
+ credentials in repository files. It publishes the library before the CLI and
39
+ adapter, with public access and the `tip` tag, never `latest`.
40
+
41
+ For **each** package, configure an npm trusted publisher:
42
+
43
+ - Provider: GitHub Actions
44
+ - Organization/user: `vimhead`
45
+ - Repository: `norn`
46
+ - Workflow filename: `release.yml`
47
+ - Environment: unset (the workflow does not name one)
48
+ - Allowed action: enable direct `npm publish`, not only staged publishing
49
+
50
+ Use npm's [trusted-publisher settings](https://docs.npmjs.com/trusted-publishers/).
51
+ GitHub-hosted runners request OIDC credentials through `id-token: write`; no npm
52
+ publishing token is stored in GitHub secrets. Public releases include provenance.
53
+
54
+ ## Continuous tip publication and recovery
55
+
56
+ After a successful `Test` run on `main`, `Release Tip` stamps all three packages
57
+ with `0.1.0-tip.<GitHub-run-id>.<attempt>` and the tested commit. It validates and
58
+ packs npm artifacts, builds standalone binaries, then publishes only if the
59
+ selected commit is still `main`. Manual dispatch performs the same checks.
60
+ Only after all npm packages are verified does it update the GitHub `tip` release.
61
+
62
+ npm versions are immutable. A workflow rerun gets a new attempt/version, so a
63
+ partial prior publication is retained rather than overwritten. Repeating the
64
+ publication command for the same artifacts skips an existing package only when
65
+ its integrity and `tip` version match; conflicting bytes or a moved tag fail
66
+ closed and require a new version. Completed npm publication followed by a GitHub
67
+ failure can likewise be repaired by rerunning the workflow.
68
+
69
+ npm tags move package by package; npm and GitHub do not provide a cross-registry
70
+ transaction. The CLI always uses its exact library dependency, even during a
71
+ partial rollout. Install that version of `@vimhead.dev/norn` for matching editor
72
+ types rather than assuming independently read `tip` tags are atomic.
73
+
74
+ Trusted publishing authenticates `npm publish`, not general tag-management
75
+ commands. The workflow sets `tip` through publication itself; it does not rely on
76
+ `npm dist-tag` or a long-lived token for a separate promotion step.