@vimhead.dev/norn-cli 0.1.0-tip.35390859630.1 → 0.1.0-tip.35568631545.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 (97) hide show
  1. package/assets/README.md +36 -15
  2. package/assets/docs/README.md +4 -4
  3. package/assets/docs/agents.md +39 -11
  4. package/assets/docs/cli.md +6 -6
  5. package/assets/docs/composition.md +12 -12
  6. package/assets/docs/persistence.md +31 -17
  7. package/assets/docs/providers.md +2 -2
  8. package/assets/docs/recovery.md +1 -1
  9. package/assets/docs/workflows.md +29 -17
  10. package/assets/examples/agent-then-analysis/README.md +8 -8
  11. package/assets/examples/agent-then-analysis/plugin.ts +23 -21
  12. package/assets/examples/caller-selected-continuation/README.md +11 -10
  13. package/assets/examples/caller-selected-continuation/caller.ts +15 -13
  14. package/assets/examples/caller-selected-continuation/producer.ts +11 -8
  15. package/assets/examples/coordinating-multiple-agents/README.md +27 -17
  16. package/assets/examples/coordinating-multiple-agents/plugin.ts +64 -41
  17. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
  18. package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
  19. package/assets/examples/getting-started/README.md +25 -10
  20. package/assets/examples/getting-started/plugin.ts +30 -11
  21. package/assets/examples/minimal-workflow/README.md +7 -6
  22. package/assets/examples/minimal-workflow/plugin.ts +8 -5
  23. package/assets/examples/shared-state/README.md +5 -5
  24. package/assets/examples/shared-state/plugin.ts +38 -25
  25. package/assets/examples/shared-state/shared-state.ts +30 -49
  26. package/assets/examples/shared-state/state-tools.ts +68 -0
  27. package/assets/examples/worktree-development-loop/README.md +14 -12
  28. package/assets/examples/worktree-development-loop/scope.ts +1 -1
  29. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +3 -3
  30. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +6 -4
  31. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +12 -11
  32. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +2 -3
  33. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +10 -6
  34. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +14 -11
  35. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +1 -2
  36. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +16 -15
  37. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +1 -2
  38. package/assets/package.json +1 -1
  39. package/assets/packages/cli/src/cli.ts +9 -3
  40. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  41. package/assets/packages/cli/src/internal/agents.ts +24 -32
  42. package/assets/packages/cli/src/internal/commands.ts +2 -14
  43. package/assets/packages/cli/src/internal/engine.ts +35 -49
  44. package/assets/packages/cli/src/internal/execution-context.ts +64 -0
  45. package/assets/packages/cli/src/internal/logs.ts +1 -1
  46. package/assets/packages/cli/src/internal/run-log.ts +1 -1
  47. package/assets/packages/cli/src/internal/run-state.ts +7 -2
  48. package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
  49. package/assets/packages/cli/src/internal/workflow-registry.ts +24 -9
  50. package/assets/packages/cli/src/internal/working-directory.ts +6 -0
  51. package/assets/packages/cli/src/workflow-loader.ts +1 -2
  52. package/assets/packages/sdk/src/api.ts +65 -91
  53. package/assets/packages/sdk/src/index.ts +0 -3
  54. package/assets/tests/workflow-ref.test.ts +9 -9
  55. package/dist/cli.js +8 -3
  56. package/dist/generated-build-info.d.ts +2 -2
  57. package/dist/generated-build-info.js +2 -2
  58. package/dist/internal/agents.d.ts +0 -4
  59. package/dist/internal/agents.js +30 -46
  60. package/dist/internal/commands.d.ts +0 -4
  61. package/dist/internal/commands.js +2 -10
  62. package/dist/internal/engine.d.ts +1 -1
  63. package/dist/internal/engine.js +29 -43
  64. package/dist/internal/execution-context.d.ts +26 -0
  65. package/dist/internal/execution-context.js +55 -0
  66. package/dist/internal/file-coordinator.d.ts +17 -0
  67. package/dist/internal/file-coordinator.js +162 -0
  68. package/dist/internal/logs.d.ts +1 -1
  69. package/dist/internal/run-log.d.ts +1 -1
  70. package/dist/internal/run-state.d.ts +2 -0
  71. package/dist/internal/run-state.js +5 -2
  72. package/dist/internal/worker-directory.d.ts +1 -0
  73. package/dist/internal/worker-directory.js +26 -0
  74. package/dist/internal/workflow-registry.d.ts +13 -3
  75. package/dist/internal/workflow-registry.js +12 -7
  76. package/dist/internal/working-directory.d.ts +1 -0
  77. package/dist/internal/working-directory.js +9 -0
  78. package/dist/workflow-loader.js +1 -2
  79. package/package.json +2 -2
  80. package/assets/docs/resources.md +0 -46
  81. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
  82. package/assets/examples/shared-state/state-adapter.ts +0 -76
  83. package/assets/packages/cli/src/internal/artifacts.ts +0 -26
  84. package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
  85. package/assets/packages/cli/src/internal/run.ts +0 -127
  86. package/assets/packages/cli/src/resources.ts +0 -69
  87. package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
  88. package/assets/packages/sdk/src/resources.ts +0 -20
  89. package/dist/internal/artifacts.d.ts +0 -10
  90. package/dist/internal/artifacts.js +0 -29
  91. package/dist/internal/resource-bindings.d.ts +0 -13
  92. package/dist/internal/resource-bindings.js +0 -34
  93. package/dist/internal/run.d.ts +0 -49
  94. package/dist/internal/run.js +0 -103
  95. package/dist/resources.d.ts +0 -11
  96. package/dist/resources.js +0 -100
  97. /package/assets/packages/{sdk/src/files.ts → cli/src/internal/file-coordinator.ts} +0 -0
@@ -0,0 +1,9 @@
1
+ // src/internal/working-directory.ts
2
+ import { isAbsolute, resolve } from "node:path";
3
+ function requireAbsoluteWorkingDirectory(cwd) {
4
+ if (typeof cwd !== "string" || !isAbsolute(cwd)) throw new Error("Working directory (cwd) must be an absolute path");
5
+ return resolve(cwd);
6
+ }
7
+ export {
8
+ requireAbsoluteWorkingDirectory
9
+ };
@@ -8,7 +8,6 @@ function isNodeError(error) {
8
8
  }
9
9
 
10
10
  // src/workflow-loader.ts
11
- import * as nornFilesModule from "@vimhead.dev/norn/files";
12
11
  import * as nornSchemaModule from "@vimhead.dev/norn/schema";
13
12
  import { inspectSchema, isPlainObject } from "@vimhead.dev/norn/schema";
14
13
  import { createJiti } from "jiti/static";
@@ -192,7 +191,7 @@ function createDiagnostic(input) {
192
191
  return { configPath: input.source.configPath, modulePath: input.source.path, scopeId: input.scopeId, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error), issues: input.error instanceof AssertError ? input.error.cause.errors : [] };
193
192
  }
194
193
  function nornWorkflowVirtualModules() {
195
- return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/files": nornFilesModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
194
+ return { "@vimhead.dev/norn": nornModule, "@vimhead.dev/norn/schema": nornSchemaModule, typebox: typeboxModule, "typebox/value": typeboxValueModule, "typebox/compile": typeboxCompileModule, "typebox/schema": typeboxSchemaModule };
196
195
  }
197
196
  function mergeProjectConfig(files) {
198
197
  const [project, ...reusable] = files;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vimhead.dev/norn-cli",
3
- "version": "0.1.0-tip.35390859630.1",
3
+ "version": "0.1.0-tip.35568631545.1",
4
4
  "description": "Harness-agnostic runtime for agent-driven and code-driven workflows, built primarily for agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "0.85.1",
44
44
  "jiti": "^2.7.0",
45
45
  "typebox": "^1.3.34",
46
- "@vimhead.dev/norn": "0.1.0-tip.35390859630.1"
46
+ "@vimhead.dev/norn": "0.1.0-tip.35568631545.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node ../../scripts/build-package.ts"
@@ -1,46 +0,0 @@
1
- # Run resources and exclusive locking
2
-
3
- ## Initialization is separate from attachment
4
-
5
- `run.resources` provides shared resource handles. The [shared-state example](../examples/shared-state/README.md) demonstrates mutable storage as a custom resource.
6
-
7
- `run.resources.ensure(definition)` returns a shared handle for that resource across workflow contexts in the current executor. A definition contains `name`, `kind`, JSON `configuration`, and `initialize({directory, files, mode})`. Names are single alphanumeric/underscore/hyphen identifiers starting with an alphanumeric character. Identity/configuration conflicts fail; `configuration` owns format/version compatibility.
8
-
9
- `mode: "create"` also covers retry of an interrupted initialization; `mode: "open"` means an earlier initialization succeeded. Initializers own their data schema and must reject missing/incompatible data when reopening. Failed initialization remains visible and retryable, not a successful empty resource. Definitions have no filesystem effects until ensured.
10
-
11
- | Decision | GOOD | BAD |
12
- |---|---|---|
13
- | IF implementing an initializer, THEN make create retries preserve existing data and open validate existing storage. ELSE do not register the definition. | Validate a file left by an interrupted create before reusing it. | Truncate the file each time `ensure` calls the initializer. |
14
- | IF a resource format changes incompatibly, THEN change its declared configuration and provide an explicit migration. ELSE reopen the same format. | A mismatching `format` fails. | Reinterpret old bytes under an unchanged format declaration. |
15
-
16
- [Persistence](persistence.md) covers resource lifetimes, storage locations, and recovery. Cross-run storage, queues, ledgers, and scheduler/agent activation are not supplied by this API.
17
-
18
- ## Explicit agent attachment
19
-
20
- The [shared-state example](../examples/shared-state/README.md) supplies its own resource and permission-selected adapter. The [queue example](../examples/coordinating-multiple-agents/README.md) supplies a different resource and tool contract. Neither adapter is a built-in SDK state facility.
21
-
22
- Custom adapters implement `NornAgentResourceAdapter`: a unique name and `bind({runId, label})` returning a `NornAgentResourceBinding` with tools and async `dispose()`. An adapter can expose one or several resource handles; initializing storage does not construct or attach tools. Session creation and one-shot prompting accept adapters through `resourceAdapters`, not through the resource manager or state handle directly. Attached tools are activated with the normal response tool. Duplicate adapter names and collisions with built-ins, the response tool, other adapters or already-loaded extension tools fail. Successful bindings are cleaned up in reverse order on session disposal or later creation failure. An initializer/binder that throws before returning its handle owns cleanup of its partial allocations.
23
-
24
- | Decision | GOOD | BAD |
25
- |---|---|---|
26
- | IF a Norn agent needs resource access, THEN attach an adapter exposing the required operations. ELSE omit the attachment. | A reviewer receives read-only access to a candidate. | Automatically expose every resource to every session. |
27
- | IF disposing a binding, THEN release session-local handles only. ELSE retain the resource for later sessions. | Close a subscription. | Delete workflow state when its agent exits. |
28
-
29
- The attachment never exposes internal scheduler/checkpoint control state. It is a cooperative tool boundary, not a sandbox against unrestricted filesystem tools or trusted extensions. [Agent loading](agents.md#prompts-tools-and-resource-loading) owns those limitations.
30
-
31
- ## Shared storage coordination
32
-
33
- `run.resources.files` supplies `readText`, `writeText`, and `withExclusiveLock(path, async lockedPath => ...)`. Standalone callers can construct `NornFileCoordinator({lockRoot, waitTimeoutMs})` from `@vimhead.dev/norn`. Coordinating callers must use the same lock namespace. Target parents must exist before a raw `withExclusiveLock` call; `writeText` creates them. Existing symbolic links resolve to their canonical target; dangling links fail.
34
-
35
- The lock spans the complete callback, including read/validate/modify/persist. `writeText` replaces complete file contents atomically. A live owner is never expired by a TTL; confirmed dead local owners can be reclaimed. Invalid or foreign-host ownership fails closed, and contention has a bounded wait. PID reuse can delay reclamation rather than permit two owners. Locks support local filesystems on one host, not distributed storage.
36
-
37
- Reads of live command-output logs may return partial streams. File locks do not make multi-file snapshots or external side effects transactional.
38
-
39
- | Decision | GOOD | BAD |
40
- |---|---|---|
41
- | IF updating shared file data, THEN hold one lock around the entire operation and use its canonical `lockedPath`. ELSE ordinary atomic replacement suffices only for independent values. | Read, modify and atomically replace inside one callback. | Lock only the final write after reading stale contents. |
42
- | IF two callers access the same resource, THEN use the same coordinator namespace. ELSE do not claim mutual exclusion. | Reuse the run manager's `files`. | Give each session a different lock root for the same data. |
43
- | IF a callback holds a lock, THEN finish its short storage operation before prompting a model or taking another lock on the same file. ELSE release it first. | Persist a value and return. | Wait for a model turn while holding a file lock. |
44
- | IF restoring a checkpoint, THEN restore resource data but not lock ownership. ELSE follow the owning external resource's recovery contract. | Built-in locks live under the run's `locks/`, outside `current/`. | Restore an old owner's lock directory as live ownership. |
45
-
46
- Sources: [resource contracts](../packages/sdk/src/resources.ts), [resource manager](../packages/cli/src/resources.ts), [adapter contracts](../packages/sdk/src/agent-resource-adapter.ts), [file coordinator](../packages/sdk/src/files.ts).
@@ -1,51 +0,0 @@
1
- import type { NornAgentResourceAdapter } from "@vimhead.dev/norn";
2
- import { randomUUID } from "node:crypto";
3
- import { Type, type Static } from "typebox";
4
- import { summarySchema, type WorkQueue } from "./work-queue.ts";
5
-
6
- const acknowledgeParameters = Type.Object({
7
- id: Type.String({ minLength: 1, maxLength: 128 }),
8
- token: Type.String({ minLength: 36, maxLength: 36 }),
9
- result: summarySchema,
10
- });
11
-
12
- export function QueueAdapter(input: { readonly queue: WorkQueue }): NornAgentResourceAdapter {
13
- return {
14
- name: "example.note-summaries",
15
- async bind() {
16
- const owner = randomUUID();
17
- return {
18
- tools: [
19
- {
20
- name: "queue_status", label: "Note queue status", description: "Read counts of available, leased and acknowledged notes, without exposing other agents' notes or tokens.",
21
- parameters: Type.Object({}),
22
- async execute() {
23
- const { items: _items, ...status } = await input.queue.inspect();
24
- return describeResult(status);
25
- },
26
- },
27
- {
28
- name: "queue_claim", label: "Claim a note", description: "Claim one note for this session, or return its existing live claim. Null means nothing available now, not all work complete. Save its token; expiresAt is Unix time in milliseconds. Note text is bounded to 1000 characters.",
29
- parameters: Type.Object({}),
30
- async execute(_id, _args, signal) {
31
- return describeResult({ claim: await input.queue.claim({ owner, signal }) });
32
- },
33
- },
34
- {
35
- name: "queue_acknowledge", label: "Save a note summary", description: "Save {summary, quote} and acknowledge this session's live claim in one operation. Stale tokens fail; identical successful retries succeed. This records processing, not semantic approval.",
36
- parameters: acknowledgeParameters,
37
- async execute(_id: string, args: Static<typeof acknowledgeParameters>, signal: AbortSignal | undefined) {
38
- await input.queue.acknowledge({ ...args, owner, signal });
39
- return describeResult({ acknowledged: args.id });
40
- },
41
- },
42
- ],
43
- async dispose() {},
44
- };
45
- },
46
- };
47
- }
48
-
49
- function describeResult(value: unknown) {
50
- return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: value };
51
- }
@@ -1,76 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { Type, type TSchema } from "typebox";
3
- import type { NornAgentResourceAdapter, ToolDefinition } from "@vimhead.dev/norn";
4
- import type { SharedStateAccess, SharedStateField } from "./shared-state.ts";
5
- import { inspectSchema } from "@vimhead.dev/norn/schema";
6
-
7
- function defineTool<Schema extends TSchema>(tool: ToolDefinition<Schema>): ToolDefinition<Schema> { return tool; }
8
-
9
- export type NornStateFieldAccess = {
10
- readonly field: SharedStateField;
11
- readonly access: "read" | "write" | "read-write";
12
- };
13
-
14
- const pageParameters = {
15
- offset: Type.Integer({ minimum: 0, description: "Zero-based UTF-16 offset into the serialized JSON. Start at 0." }),
16
- limit: Type.Integer({ minimum: 1, maximum: 10000 }),
17
- };
18
-
19
- export function StateAdapter(input: { readonly state: SharedStateAccess; readonly fields: readonly NornStateFieldAccess[] }): NornAgentResourceAdapter {
20
- const fields = new Map(input.fields.map((grant) => [grant.field.id, grant]));
21
- if (fields.size !== input.fields.length || fields.size === 0) throw new Error("State attachment requires unique, explicitly selected fields");
22
- const selectField = (key: string, access: "read" | "write") => {
23
- const grant = fields.get(key);
24
- if (!grant || (grant.access !== access && grant.access !== "read-write")) throw new Error(`State ${access} is not attached: ${key}`);
25
- return grant.field;
26
- };
27
- return {
28
- name: "example.shared-state",
29
- async bind() {
30
- return {
31
- tools: [
32
- defineTool({
33
- name: "norn_state_list",
34
- label: "Attached workflow state",
35
- description: "List only attached workflow-state field IDs, permissions and value schemas. JSON is paginated; use nextOffset until null.",
36
- parameters: Type.Object(pageParameters),
37
- async execute(_id, args) {
38
- return serializePage({ value: [...fields.values()].map(({ field, access }) => ({ id: field.id, access, schema: inspectSchema(field.schema) })), ...args });
39
- },
40
- }),
41
- defineTool({
42
- name: "norn_state_get",
43
- label: "Read workflow state",
44
- 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.",
45
- parameters: Type.Object({ key: Type.String(), ...pageParameters }),
46
- async execute(_id, args) {
47
- const value = await input.state.getOptional(selectField(args.key, "read"));
48
- return serializePage({ value: value === undefined ? { isSet: false } : { isSet: true, value }, ...args });
49
- },
50
- }),
51
- defineTool({
52
- name: "norn_state_set",
53
- label: "Write workflow state",
54
- 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.",
55
- parameters: Type.Object({ key: Type.String(), value: Type.Unknown() }),
56
- async execute(_id, args, signal) {
57
- signal?.throwIfAborted();
58
- const field = selectField(args.key, "write");
59
- await input.state.set(field, args.value);
60
- return { content: [{ type: "text", text: "Workflow state saved." }], details: {} };
61
- },
62
- }),
63
- ],
64
- async dispose() {},
65
- };
66
- },
67
- };
68
- }
69
-
70
- function serializePage(input: { readonly value: unknown; readonly offset: number; readonly limit: number }) {
71
- const serialized = JSON.stringify(input.value);
72
- 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");
73
- const end = Math.min(serialized.length, input.offset + input.limit);
74
- const details = { text: serialized.slice(input.offset, end), nextOffset: end < serialized.length ? end : null, revision: createHash("sha256").update(serialized).digest("hex") };
75
- return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details };
76
- }
@@ -1,26 +0,0 @@
1
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
3
- import type { NornArtifactRef } from "@vimhead.dev/norn";
4
-
5
- export class NornArtifacts {
6
- constructor(private readonly artifactsRoot: string, private readonly files: NornFileCoordinator) {}
7
-
8
- async write(path: string, content: string): Promise<NornArtifactRef> {
9
- await this.files.writeText(this.resolveArtifactPath(path), content);
10
- return { path };
11
- }
12
-
13
- async read(ref: NornArtifactRef): Promise<string> {
14
- return this.files.readText(this.resolveArtifactPath(ref.path));
15
- }
16
-
17
- private resolveArtifactPath(path: string): string {
18
- if (isAbsolute(path)) throw new Error(`Artifact path must be relative: ${path}`);
19
- const resolvedPath = resolve(this.artifactsRoot, path);
20
- const relativePath = relative(this.artifactsRoot, resolvedPath);
21
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
22
- throw new Error(`Artifact path escapes artifacts directory: ${path}`);
23
- }
24
- return resolvedPath;
25
- }
26
- }
@@ -1,35 +0,0 @@
1
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
- import type { NornAgentResourceBinding, NornAgentResourceAdapter } from "@vimhead.dev/norn";
3
-
4
- export class NornSessionResourceBindings {
5
- readonly tools: ToolDefinition[] = [];
6
- private readonly bindings: NornAgentResourceBinding[] = [];
7
-
8
- async bind(input: { readonly adapters: readonly NornAgentResourceAdapter[]; readonly runId: string; readonly label: string; readonly reservedTools: readonly string[] }): Promise<void> {
9
- const adapterNames = new Set<string>();
10
- const tools = new Set(input.reservedTools);
11
- for (const adapter of input.adapters) {
12
- if (adapterNames.has(adapter.name)) throw new Error(`Duplicate resource adapter: ${adapter.name}`);
13
- adapterNames.add(adapter.name);
14
- const binding = await adapter.bind({ runId: input.runId, label: input.label });
15
- this.bindings.push(binding);
16
- for (const tool of binding.tools) {
17
- if (tools.has(tool.name)) throw new Error(`Resource tool name collision: ${tool.name}`);
18
- tools.add(tool.name);
19
- this.tools.push(tool);
20
- }
21
- }
22
- }
23
-
24
- async dispose(): Promise<void> {
25
- const errors: unknown[] = [];
26
- for (const binding of this.bindings.splice(0).reverse()) {
27
- try {
28
- await binding.dispose();
29
- } catch (error) {
30
- errors.push(error);
31
- }
32
- }
33
- if (errors.length) throw new AggregateError(errors, "Resource binding cleanup failed");
34
- }
35
- }
@@ -1,127 +0,0 @@
1
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
- import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
3
- import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornRun } from "@vimhead.dev/norn";
4
- import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
5
- import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
6
- import type { NornArtifacts } from "./artifacts.ts";
7
- import { NornAgentRunner } from "./agents.ts";
8
- import { NornCommandRunner } from "./commands.ts";
9
- import type { NornRunLogs } from "./logs.ts";
10
- import type { NornRunLogger } from "./run-log.ts";
11
- import type { NornRunResources } from "../resources.ts";
12
-
13
- type DefaultNornRunInput = {
14
- readonly id: string;
15
- readonly runRoot: string;
16
- readonly projectRoot: string;
17
- readonly workspace: string;
18
- readonly cwd: string;
19
- readonly isolationMode: NornWorkflowIsolationMode;
20
- readonly signal?: AbortSignal;
21
- readonly model?: CreateAgentSessionOptions["model"];
22
- readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
23
- readonly agentDir?: string;
24
- readonly responseCollector: NornAgentResponseCollector;
25
- readonly resources: NornRunResources;
26
- readonly logger: NornRunLogger;
27
- readonly artifacts: NornArtifacts;
28
- readonly logs: NornRunLogs;
29
- };
30
-
31
- export class NornRunContext implements NornProjectRun {
32
- readonly resources: NornRunResources;
33
- readonly artifacts: NornRun["artifacts"];
34
- readonly logs: NornRun["logs"];
35
- readonly commands: NornRun["commands"];
36
- readonly agents: NornRun["agents"];
37
- readonly id: string;
38
- readonly workspace: string;
39
- readonly projectRoot: string;
40
- readonly cwd: string;
41
-
42
- constructor(private readonly input: DefaultNornRunInput) {
43
- this.id = input.id;
44
- this.workspace = input.workspace;
45
- this.projectRoot = input.projectRoot;
46
- this.cwd = input.cwd;
47
- this.resources = input.resources;
48
- this.artifacts = input.artifacts;
49
- this.logs = {
50
- read: (log) => this.input.logs.read(log),
51
- };
52
- this.commands = {
53
- run: (commandInput) =>
54
- new NornCommandRunner({
55
- boundaryRoot: this.boundaryRoot,
56
- boundaryName: this.input.isolationMode,
57
- cwd: this.cwd,
58
- signal: this.input.signal,
59
- logs: this.input.logs,
60
- logger: this.input.logger,
61
- }).run(commandInput),
62
- };
63
- this.agents = {
64
- createSession: (agentInput) => this.createAgentRunner().createSession(agentInput),
65
- prompt: (agentInput) => this.createAgentRunner().prompt(agentInput),
66
- };
67
- }
68
-
69
- forWorkflow(workflow: NornAnyWorkflowDeclaration): NornRunContext {
70
- const isolationMode = workflow.isolation.mode;
71
- return new NornRunContext({
72
- ...this.input,
73
- isolationMode,
74
- cwd: isolationMode === "project" ? this.projectRoot : this.workspace,
75
- });
76
- }
77
-
78
- private get boundaryRoot(): string {
79
- return this.input.isolationMode === "project" ? this.projectRoot : this.workspace;
80
- }
81
-
82
- private createAgentRunner(): NornAgentRunner {
83
- return new NornAgentRunner({
84
- id: this.id,
85
- runRoot: this.input.runRoot,
86
- boundaryRoot: this.boundaryRoot,
87
- boundaryName: this.input.isolationMode,
88
- cwd: this.cwd,
89
- signal: this.input.signal,
90
- model: this.input.model,
91
- thinkingLevel: this.input.thinkingLevel,
92
- agentDir: this.input.agentDir,
93
- logs: this.input.logs,
94
- logger: this.input.logger,
95
- responseCollector: this.input.responseCollector,
96
- });
97
- }
98
-
99
- path(relativePath: string): string {
100
- return this.resolveFromRoot(this.cwd, this.boundaryRoot, relativePath);
101
- }
102
-
103
- projectPath(relativePath: string): string {
104
- return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
105
- }
106
-
107
- next(workflowId: string, args: unknown): ReturnType<NornRun["next"]> {
108
- return createWorkflowTransition({ workflowId, args });
109
- }
110
-
111
- complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]> {
112
- return { type: "complete", metadata };
113
- }
114
-
115
- fail(metadata: NornRunOutcomeMetadata & { readonly summary: string }): ReturnType<NornRun["fail"]> {
116
- return { type: "fail", metadata };
117
- }
118
-
119
- private resolveFromRoot(root: string, boundaryRoot: string, path: string): string {
120
- const resolvedPath = isAbsolute(path) ? path : resolve(root, path);
121
- const pathFromBoundary = relative(boundaryRoot, resolvedPath);
122
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
123
- throw new Error(`Workflow path escapes ${this.input.isolationMode} isolation: ${path}`);
124
- }
125
- return resolvedPath;
126
- }
127
- }
@@ -1,69 +0,0 @@
1
- import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
2
- import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
3
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
- import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
5
- import { jsonValueSchema } from "@vimhead.dev/norn/schema";
6
- import { mkdir, readFile } from "node:fs/promises";
7
- import { join, resolve } from "node:path";
8
- import { isDeepStrictEqual } from "node:util";
9
- import { Type, type StaticDecode } from "typebox";
10
- import { Value } from "typebox/value";
11
-
12
- const definitionSchema = Type.Object({ name: Type.String(), kind: Type.String({ minLength: 1 }), configuration: jsonValueSchema }, { additionalProperties: false });
13
- const recordSchema = Type.Object({ ...definitionSchema.properties, isInitialized: Type.Boolean() }, { additionalProperties: false });
14
-
15
- export class NornRunResources implements NornResources {
16
- readonly files: NornFileCoordinator;
17
- private readonly entries = new Map<string, { readonly definition: StaticDecode<typeof definitionSchema>; readonly value: Promise<unknown> }>();
18
-
19
- private constructor(private readonly runRoot: string) {
20
- this.files = createRunFileCoordinator(runRoot);
21
- }
22
-
23
- static async initialize(runRoot: string): Promise<NornRunResources> {
24
- const resources = new NornRunResources(resolve(runRoot));
25
- await mkdir(join(resources.runRoot, "current", "resources"), { recursive: true });
26
- return resources;
27
- }
28
-
29
- async ensure<T>(definition: NornResourceDefinition<T>): Promise<T> {
30
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
31
- const identity = structuredClone(Value.Parse(definitionSchema, { name: definition.name, kind: definition.kind, configuration: definition.configuration }));
32
- const existing = this.entries.get(definition.name);
33
- if (existing) {
34
- if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
35
- return existing.value as Promise<T>;
36
- }
37
- const value = this.initializeResource(definition, identity);
38
- const entry = { definition: identity, value };
39
- this.entries.set(definition.name, entry);
40
- try {
41
- return await value;
42
- } catch (error) {
43
- if (this.entries.get(definition.name) === entry) this.entries.delete(definition.name);
44
- throw error;
45
- }
46
- }
47
-
48
- private async initializeResource<T>(definition: NornResourceDefinition<T>, identity: StaticDecode<typeof definitionSchema>): Promise<T> {
49
- const directory = join(this.runRoot, "current", "resources", definition.name);
50
- await mkdir(directory, { recursive: true });
51
- return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
52
- let stored: StaticDecode<typeof recordSchema> | undefined;
53
- try {
54
- stored = Value.Parse(recordSchema, JSON.parse(await readFile(path, "utf8")));
55
- } catch (error) {
56
- if (!isNodeError(error) || error.code !== "ENOENT") throw error;
57
- }
58
- if (stored) {
59
- const { isInitialized: _isInitialized, ...storedIdentity } = stored;
60
- if (!isDeepStrictEqual(storedIdentity, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
61
- } else {
62
- await writeJsonAtomically(path, { ...identity, isInitialized: false });
63
- }
64
- const value = await definition.initialize({ directory, files: this.files, mode: stored?.isInitialized ? "open" : "create" });
65
- if (!stored?.isInitialized) await writeJsonAtomically(path, { ...identity, isInitialized: true });
66
- return value;
67
- });
68
- }
69
- }
@@ -1,11 +0,0 @@
1
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
-
3
- export type NornAgentResourceBinding = {
4
- readonly tools: readonly ToolDefinition[];
5
- dispose(): Promise<void>;
6
- };
7
-
8
- export type NornAgentResourceAdapter = {
9
- readonly name: string;
10
- bind(context: { readonly runId: string; readonly label: string }): Promise<NornAgentResourceBinding>;
11
- };
@@ -1,20 +0,0 @@
1
- import type { NornFileCoordinator } from "./files.ts";
2
- import type { NornJsonValue } from "./schema.ts";
3
-
4
- export type NornResourceContext = {
5
- readonly mode: "create" | "open";
6
- readonly directory: string;
7
- readonly files: NornFileCoordinator;
8
- };
9
-
10
- export type NornResourceDefinition<T> = {
11
- readonly name: string;
12
- readonly kind: string;
13
- readonly configuration: NornJsonValue;
14
- initialize(context: NornResourceContext): Promise<T>;
15
- };
16
-
17
- export type NornResources = {
18
- readonly files: NornFileCoordinator;
19
- ensure<T>(definition: NornResourceDefinition<T>): Promise<T>;
20
- };
@@ -1,10 +0,0 @@
1
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
2
- import type { NornArtifactRef } from "@vimhead.dev/norn";
3
- export declare class NornArtifacts {
4
- private readonly artifactsRoot;
5
- private readonly files;
6
- constructor(artifactsRoot: string, files: NornFileCoordinator);
7
- write(path: string, content: string): Promise<NornArtifactRef>;
8
- read(ref: NornArtifactRef): Promise<string>;
9
- private resolveArtifactPath;
10
- }
@@ -1,29 +0,0 @@
1
- // src/internal/artifacts.ts
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
- var NornArtifacts = class {
4
- constructor(artifactsRoot, files) {
5
- this.artifactsRoot = artifactsRoot;
6
- this.files = files;
7
- }
8
- artifactsRoot;
9
- files;
10
- async write(path, content) {
11
- await this.files.writeText(this.resolveArtifactPath(path), content);
12
- return { path };
13
- }
14
- async read(ref) {
15
- return this.files.readText(this.resolveArtifactPath(ref.path));
16
- }
17
- resolveArtifactPath(path) {
18
- if (isAbsolute(path)) throw new Error(`Artifact path must be relative: ${path}`);
19
- const resolvedPath = resolve(this.artifactsRoot, path);
20
- const relativePath = relative(this.artifactsRoot, resolvedPath);
21
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
22
- throw new Error(`Artifact path escapes artifacts directory: ${path}`);
23
- }
24
- return resolvedPath;
25
- }
26
- };
27
- export {
28
- NornArtifacts
29
- };
@@ -1,13 +0,0 @@
1
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
- import type { NornAgentResourceAdapter } from "@vimhead.dev/norn";
3
- export declare class NornSessionResourceBindings {
4
- readonly tools: ToolDefinition[];
5
- private readonly bindings;
6
- bind(input: {
7
- readonly adapters: readonly NornAgentResourceAdapter[];
8
- readonly runId: string;
9
- readonly label: string;
10
- readonly reservedTools: readonly string[];
11
- }): Promise<void>;
12
- dispose(): Promise<void>;
13
- }
@@ -1,34 +0,0 @@
1
- // src/internal/resource-bindings.ts
2
- var NornSessionResourceBindings = class {
3
- tools = [];
4
- bindings = [];
5
- async bind(input) {
6
- const adapterNames = /* @__PURE__ */ new Set();
7
- const tools = new Set(input.reservedTools);
8
- for (const adapter of input.adapters) {
9
- if (adapterNames.has(adapter.name)) throw new Error(`Duplicate resource adapter: ${adapter.name}`);
10
- adapterNames.add(adapter.name);
11
- const binding = await adapter.bind({ runId: input.runId, label: input.label });
12
- this.bindings.push(binding);
13
- for (const tool of binding.tools) {
14
- if (tools.has(tool.name)) throw new Error(`Resource tool name collision: ${tool.name}`);
15
- tools.add(tool.name);
16
- this.tools.push(tool);
17
- }
18
- }
19
- }
20
- async dispose() {
21
- const errors = [];
22
- for (const binding of this.bindings.splice(0).reverse()) {
23
- try {
24
- await binding.dispose();
25
- } catch (error) {
26
- errors.push(error);
27
- }
28
- }
29
- if (errors.length) throw new AggregateError(errors, "Resource binding cleanup failed");
30
- }
31
- };
32
- export {
33
- NornSessionResourceBindings
34
- };
@@ -1,49 +0,0 @@
1
- import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
2
- import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornRun } from "@vimhead.dev/norn";
3
- import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
4
- import type { NornArtifacts } from "./artifacts.ts";
5
- import type { NornRunLogs } from "./logs.ts";
6
- import type { NornRunLogger } from "./run-log.ts";
7
- import type { NornRunResources } from "../resources.ts";
8
- type DefaultNornRunInput = {
9
- readonly id: string;
10
- readonly runRoot: string;
11
- readonly projectRoot: string;
12
- readonly workspace: string;
13
- readonly cwd: string;
14
- readonly isolationMode: NornWorkflowIsolationMode;
15
- readonly signal?: AbortSignal;
16
- readonly model?: CreateAgentSessionOptions["model"];
17
- readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
18
- readonly agentDir?: string;
19
- readonly responseCollector: NornAgentResponseCollector;
20
- readonly resources: NornRunResources;
21
- readonly logger: NornRunLogger;
22
- readonly artifacts: NornArtifacts;
23
- readonly logs: NornRunLogs;
24
- };
25
- export declare class NornRunContext implements NornProjectRun {
26
- private readonly input;
27
- readonly resources: NornRunResources;
28
- readonly artifacts: NornRun["artifacts"];
29
- readonly logs: NornRun["logs"];
30
- readonly commands: NornRun["commands"];
31
- readonly agents: NornRun["agents"];
32
- readonly id: string;
33
- readonly workspace: string;
34
- readonly projectRoot: string;
35
- readonly cwd: string;
36
- constructor(input: DefaultNornRunInput);
37
- forWorkflow(workflow: NornAnyWorkflowDeclaration): NornRunContext;
38
- private get boundaryRoot();
39
- private createAgentRunner;
40
- path(relativePath: string): string;
41
- projectPath(relativePath: string): string;
42
- next(workflowId: string, args: unknown): ReturnType<NornRun["next"]>;
43
- complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]>;
44
- fail(metadata: NornRunOutcomeMetadata & {
45
- readonly summary: string;
46
- }): ReturnType<NornRun["fail"]>;
47
- private resolveFromRoot;
48
- }
49
- export {};