@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
@@ -9,8 +9,8 @@ is a single-step starting point.
9
9
  It registers an entrypoint workflow named **Workspace development loop**. The
10
10
  workflow:
11
11
 
12
- 1. clones the configured repository into `run.workspace/repo`;
13
- 2. passes the repository path and retained artifact references through workflow arguments;
12
+ 1. clones the configured repository into `paths.workspace/repo`;
13
+ 2. passes the repository path and retained file paths through workflow arguments;
14
14
  3. passes explicit cwd values to agents and commands;
15
15
  4. plans once, then loops through implementation and automated review;
16
16
  5. routes automated review through a gated review router;
@@ -62,13 +62,14 @@ norn runs inspect "$RUN"
62
62
  After planning, implementation, and automated review succeed, expect
63
63
  `run.status: interrupted` at `worktreeDevelopmentLoop.reviewRouter`, **not** a
64
64
  completed run. Inspection exposes the iteration, proposed decision, summary, and
65
- automated-review artifact in `run.interruption.args`. A command or agent failure
65
+ `automatedReviewPath` in `run.interruption.args`. A command or agent failure
66
66
  can end the run before this gate; inspect the failure instead of attempting approval.
67
67
 
68
68
  ## Review and resume
69
69
 
70
- For the interrupted iteration `N`, inspect these files under
71
- `.norn/runs/$RUN/current/artifacts/`:
70
+ The example's file and repository paths are relative to `run.paths.workspace`,
71
+ reported by run inspection. For the interrupted iteration `N`, inspect these files
72
+ in that directory:
72
73
 
73
74
  - `planning/plan.md` — the saved plan.
74
75
  - `implementation/iteration-N-status.txt` — recorded Git status.
@@ -78,8 +79,9 @@ For the interrupted iteration `N`, inspect these files under
78
79
  Inspect the actual clone as well:
79
80
 
80
81
  ```bash
81
- git -C ".norn/runs/$RUN/current/workspace/repo" status --short
82
- git -C ".norn/runs/$RUN/current/workspace/repo" diff HEAD -- .
82
+ WORKSPACE=<run.paths.workspace-from-inspection>
83
+ git -C "$WORKSPACE/repo" status --short
84
+ git -C "$WORKSPACE/repo" diff HEAD -- .
83
85
  ```
84
86
 
85
87
  Check new files, any commits made since the selected base revision, and evidence
@@ -112,12 +114,12 @@ resume input and protected fields.
112
114
 
113
115
  After acceptance, expect `run.status: completed`, `run.health: healthy`, and
114
116
  `run.outcome.workflowId: worktreeDevelopmentLoop.reviewRouter`. Outcome metadata
115
- includes plan/review artifact refs and data with `status: done`,
116
- `repositoryPath: "repo"`, and the iteration count. The review ref points to
117
- `review/iteration-N-decision.json`, retaining the chosen decision and automated
118
- review ref.
117
+ includes `data.planPath`, `data.reviewPath`, `data.status: done`,
118
+ `data.repositoryPath: "repo"`, and the iteration count. The review file is
119
+ `review/iteration-N-decision.json`, retaining the chosen decision and
120
+ `automatedReviewPath`.
119
121
 
120
- The resulting repository is `.norn/runs/$RUN/current/workspace/repo`. There is no
122
+ The resulting repository is `repo` inside the inspected `run.paths.workspace`. There is no
121
123
  automatic step to merge, push, or copy its changes back to the original repository;
122
124
  retain or transfer the wanted changes before deleting the run. This workspace is
123
125
  [not a filesystem sandbox](../../docs/persistence.md#filesystem-boundaries).
@@ -1,4 +1,4 @@
1
1
  import { workflowScope } from "@vimhead.dev/norn";
2
2
  import { developmentLoopConfigSchema } from "./workflows/development-loop/schema.ts";
3
3
 
4
- export const developmentLoopScope = workflowScope({ id: "worktreeDevelopmentLoop", config: developmentLoopConfigSchema });
4
+ export const developmentLoopScope = workflowScope({ name: "worktreeDevelopmentLoop", config: developmentLoopConfigSchema });
@@ -5,12 +5,12 @@ import { developmentLoopArgsSchema } from "./schema.ts";
5
5
  import { materializeWorkspaceRepository } from "./repository.ts";
6
6
 
7
7
  export const developmentLoopWorkflow = developmentLoopScope.workflow({
8
- id: "developmentLoop",
8
+ name: "developmentLoop",
9
9
  isEntrypoint: true,
10
10
  instructions: "Plan once, then loop implementation and review in a workspace repository copy. Call this when a repository task should run through planning, implementation, and review.",
11
11
  args: developmentLoopArgsSchema,
12
- async execute({ args, scope, run }): Promise<WorkflowResult> {
13
- const repositoryPath = await materializeWorkspaceRepository(run, scope.config.repositoryRoot, args.baseRef);
12
+ async execute({ args, scope, paths, commands }): Promise<WorkflowResult> {
13
+ const repositoryPath = await materializeWorkspaceRepository({ commands, paths, repositoryRoot: scope.config.repositoryRoot, baseRef: args.baseRef });
14
14
 
15
15
  return planningWorkflow({ task: args.task, repositoryPath, maxIterations: args.maxIterations });
16
16
  }
@@ -1,12 +1,14 @@
1
- import type { NornRun } from "@vimhead.dev/norn";
1
+ import { join } from "node:path";
2
+ import type { NornCommands, NornWorkflowPaths } from "@vimhead.dev/norn";
2
3
  import { ensureCommandSucceeded } from "../../shared/commands.ts";
3
4
 
4
5
  const WORKSPACE_REPOSITORY_PATH = "repo";
5
6
 
6
- export async function materializeWorkspaceRepository(run: NornRun, repositoryRoot: string, baseRef: string): Promise<string> {
7
- const repositoryPath = run.path(WORKSPACE_REPOSITORY_PATH);
8
- const result = await run.commands.run({
7
+ export async function materializeWorkspaceRepository({ commands, paths, repositoryRoot, baseRef }: { commands: NornCommands; paths: NornWorkflowPaths; repositoryRoot: string; baseRef: string }): Promise<string> {
8
+ const repositoryPath = join(paths.workspace, WORKSPACE_REPOSITORY_PATH);
9
+ const result = await commands.run({
9
10
  label: "materialize-workspace-repository",
11
+ cwd: paths.workspace,
10
12
  command: [
11
13
  `rm -rf ${shellQuote(repositoryPath)}`,
12
14
  `git clone --no-checkout ${shellQuote(repositoryRoot)} ${shellQuote(repositoryPath)}`,
@@ -1,3 +1,5 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
1
3
  import { reviewWorkflow } from "../review/execute.ts";
2
4
  import type { WorkflowResult } from "@vimhead.dev/norn";
3
5
  import { developmentLoopScope } from "../../scope.ts";
@@ -6,31 +8,30 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
6
8
  import { implementationAgentResponseSchema } from "./schema.ts";
7
9
 
8
10
  export const implementationWorkflow = developmentLoopScope.workflow({
9
- id: "implementation",
11
+ name: "implementation",
10
12
  isEntrypoint: false,
11
13
  instructions: "Apply one implementation pass in the current repository.",
12
14
  args: implementationArgsSchema,
13
- async execute({ args, run }): Promise<WorkflowResult> {
14
- const repositoryPath = args.repositoryPath;
15
- const planArtifact = args.planArtifact;
16
- const plan = await run.artifacts.read(planArtifact);
17
- const previousReviewArtifact = args.previousReviewArtifact;
18
- const previousReview = previousReviewArtifact ? await run.artifacts.read(previousReviewArtifact) : undefined;
19
- const implementation = await run.agents.prompt({
15
+ async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
16
+ const repositoryPath = resolve(paths.workspace, args.repositoryPath);
17
+ const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
18
+ const previousReview = args.previousReviewPath ? await readFile(join(paths.workspace, args.previousReviewPath), "utf8") : undefined;
19
+ const implementation = await agents.prompt({
20
20
  label: `implementation-${args.iteration}`,
21
21
  cwd: repositoryPath,
22
22
  tools: ["read", "grep", "find", "ls", "edit", "write", "bash"],
23
23
  prompt: buildImplementationPrompt(args.task, plan, args.iteration, previousReview),
24
24
  response: implementationAgentResponseSchema,
25
25
  });
26
- const status = await run.commands.run({
26
+ const status = await commands.run({
27
27
  label: `implementation-${args.iteration}-status`,
28
28
  cwd: repositoryPath,
29
29
  command: "git status --short",
30
30
  });
31
31
  await ensureCommandSucceeded(status);
32
- const statusOutput = await run.logs.read(status.stdoutLog);
33
- await run.artifacts.write(`implementation/iteration-${args.iteration}-status.txt`, statusOutput);
32
+ const statusOutput = await logs.read(status.stdoutLog);
33
+ await mkdir(join(paths.workspace, "implementation"), { recursive: true });
34
+ await writeFile(join(paths.workspace, `implementation/iteration-${args.iteration}-status.txt`), statusOutput);
34
35
  return reviewWorkflow({ ...args, implementationSummary: implementation.summary });
35
36
  }
36
37
  });
@@ -1,12 +1,11 @@
1
- import { artifactRefSchema } from "@vimhead.dev/norn";
2
1
  import { planningArgsSchema } from "../planning/schema.ts";
3
2
  import { Type, type StaticDecode } from "typebox";
4
3
 
5
4
  export const implementationArgsSchema = Type.Object({
6
5
  ...planningArgsSchema.properties,
7
6
  iteration: Type.Integer({ minimum: 1 }),
8
- planArtifact: artifactRefSchema,
9
- previousReviewArtifact: Type.Optional(artifactRefSchema),
7
+ planPath: Type.String(),
8
+ previousReviewPath: Type.Optional(Type.String()),
10
9
  });
11
10
 
12
11
  export const implementationAgentResponseSchema = Type.Object({
@@ -1,3 +1,5 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
1
3
  import { implementationWorkflow } from "../implementation/execute.ts";
2
4
  import type { WorkflowResult } from "@vimhead.dev/norn";
3
5
  import { developmentLoopScope } from "../../scope.ts";
@@ -5,21 +7,23 @@ import { planningArgsSchema } from "./schema.ts";
5
7
  import { planningAgentResponseSchema } from "./schema.ts";
6
8
 
7
9
  export const planningWorkflow = developmentLoopScope.workflow({
8
- id: "planning",
10
+ name: "planning",
9
11
  isEntrypoint: false,
10
12
  instructions: "Create an implementation plan for a repository task.",
11
13
  args: planningArgsSchema,
12
- async execute({ args, run }): Promise<WorkflowResult> {
13
- const repositoryPath = args.repositoryPath;
14
- const planning = await run.agents.prompt({
14
+ async execute({ args, paths, agents }): Promise<WorkflowResult> {
15
+ const repositoryPath = resolve(paths.workspace, args.repositoryPath);
16
+ const planning = await agents.prompt({
15
17
  label: "planning",
16
18
  cwd: repositoryPath,
17
19
  tools: ["read", "grep", "find", "ls"],
18
20
  prompt: buildPlanningPrompt(args.task),
19
21
  response: planningAgentResponseSchema,
20
22
  });
21
- const planArtifact = await run.artifacts.write("planning/plan.md", planning.plan);
22
- return implementationWorkflow({ ...args, planArtifact, iteration: 1 });
23
+ const planPath = "planning/plan.md";
24
+ await mkdir(join(paths.workspace, "planning"), { recursive: true });
25
+ await writeFile(join(paths.workspace, planPath), planning.plan);
26
+ return implementationWorkflow({ ...args, planPath, iteration: 1 });
23
27
  }
24
28
  });
25
29
 
@@ -1,3 +1,5 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
1
3
  import { reviewRouterWorkflow } from "../review-router/execute.ts";
2
4
  import type { WorkflowResult } from "@vimhead.dev/norn";
3
5
  import { developmentLoopScope } from "../../scope.ts";
@@ -6,37 +8,38 @@ import { ensureCommandSucceeded } from "../../shared/commands.ts";
6
8
  import { reviewAgentResponseSchema } from "./schema.ts";
7
9
 
8
10
  export const reviewWorkflow = developmentLoopScope.workflow({
9
- id: "review",
11
+ name: "review",
10
12
  isEntrypoint: false,
11
13
  instructions: "Review the current repository boundary changes.",
12
14
  args: reviewArgsSchema,
13
- async execute({ args, run }): Promise<WorkflowResult> {
14
- const repositoryPath = args.repositoryPath;
15
- const planArtifact = args.planArtifact;
15
+ async execute({ args, paths, agents, commands, logs }): Promise<WorkflowResult> {
16
+ const repositoryPath = resolve(paths.workspace, args.repositoryPath);
16
17
  const implementationSummary = args.implementationSummary;
17
- const plan = await run.artifacts.read(planArtifact);
18
- const diff = await run.commands.run({
18
+ const plan = await readFile(join(paths.workspace, args.planPath), "utf8");
19
+ const diff = await commands.run({
19
20
  label: `review-${args.iteration}-diff`,
20
21
  cwd: repositoryPath,
21
22
  command: "git status --short && git diff --stat HEAD -- . && git diff HEAD -- .",
22
23
  });
23
24
  await ensureCommandSucceeded(diff);
24
- const diffOutput = await run.logs.read(diff.stdoutLog);
25
- await run.artifacts.write(`review/iteration-${args.iteration}-diff.txt`, diffOutput);
26
- const review = await run.agents.prompt({
25
+ const diffOutput = await logs.read(diff.stdoutLog);
26
+ await mkdir(join(paths.workspace, "review"), { recursive: true });
27
+ await writeFile(join(paths.workspace, `review/iteration-${args.iteration}-diff.txt`), diffOutput);
28
+ const review = await agents.prompt({
27
29
  label: `review-${args.iteration}`,
28
30
  cwd: repositoryPath,
29
31
  tools: ["read", "grep", "find", "ls", "bash"],
30
32
  prompt: buildReviewPrompt(args.task, plan, implementationSummary, diffOutput),
31
33
  response: reviewAgentResponseSchema,
32
34
  });
33
- const automatedReviewArtifact = await run.artifacts.write(`review/iteration-${args.iteration}-automated.json`, JSON.stringify(review, null, 2));
35
+ const automatedReviewPath = `review/iteration-${args.iteration}-automated.json`;
36
+ await writeFile(join(paths.workspace, automatedReviewPath), JSON.stringify(review, null, 2));
34
37
  return reviewRouterWorkflow({
35
38
  ...args,
36
39
  iteration: args.iteration,
37
40
  decision: review.decision,
38
41
  summary: review.summary,
39
- automatedReviewArtifact,
42
+ automatedReviewPath,
40
43
  });
41
44
  }
42
45
  });
@@ -1,5 +1,4 @@
1
1
  import { implementationArgsSchema } from "../implementation/schema.ts";
2
- import { artifactRefSchema } from "@vimhead.dev/norn";
3
2
  import { Type, type StaticDecode } from "typebox";
4
3
 
5
4
  export const reviewDecisionSchema = Type.Enum(["accept", "revise", "blocked"]);
@@ -17,7 +16,7 @@ export const reviewAgentResponseSchema = Type.Object({
17
16
  export const storedReviewSchema = Type.Object({
18
17
  decision: reviewDecisionSchema,
19
18
  summary: Type.String(),
20
- reviewArtifact: artifactRefSchema,
19
+ reviewPath: Type.String(),
21
20
  });
22
21
 
23
22
  export type ReviewArgs = StaticDecode<typeof reviewArgsSchema>;
@@ -1,3 +1,5 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
1
3
  import { implementationWorkflow } from "../implementation/execute.ts";
2
4
  import type { WorkflowResult } from "@vimhead.dev/norn";
3
5
  import { developmentLoopScope } from "../../scope.ts";
@@ -5,52 +7,51 @@ import { reviewRouterArgsSchema } from "./schema.ts";
5
7
  import type { StoredReview } from "../review/schema.ts";
6
8
 
7
9
  export const reviewRouterWorkflow = developmentLoopScope.workflow({
8
- id: "reviewRouter",
10
+ name: "reviewRouter",
9
11
  isEntrypoint: false,
10
12
  instructions: "Route implementation and review iterations based on the latest review decision.",
11
13
  gate: {
12
14
  enabled: true,
13
- describe: ({ args }) => `Review iteration ${args.iteration}. Confirm or edit the automated decision before continuing. Plan: ${args.planArtifact.path}.`,
15
+ describe: ({ args }) => `Review iteration ${args.iteration}. Confirm or edit the automated decision before continuing. Plan: ${args.planPath}.`,
14
16
  fields: ["decision", "summary"] as const,
15
17
  },
16
18
  args: reviewRouterArgsSchema,
17
- async execute({ args, run }): Promise<WorkflowResult> {
19
+ async execute({ args, paths, run }): Promise<WorkflowResult> {
18
20
  const repositoryPath = args.repositoryPath;
19
- const planArtifact = args.planArtifact;
21
+ const planPath = args.planPath;
20
22
  const currentIteration = args.iteration;
21
23
  const maxIterations = args.maxIterations;
22
24
  const task = args.task;
23
- const reviewArtifact = await run.artifacts.write(
24
- `review/iteration-${args.iteration}-decision.json`,
25
- JSON.stringify({ decision: args.decision, summary: args.summary, automatedReviewArtifact: args.automatedReviewArtifact }, null, 2),
25
+ const reviewPath = `review/iteration-${args.iteration}-decision.json`;
26
+ await mkdir(join(paths.workspace, "review"), { recursive: true });
27
+ await writeFile(
28
+ join(paths.workspace, reviewPath),
29
+ JSON.stringify({ decision: args.decision, summary: args.summary, automatedReviewPath: args.automatedReviewPath }, null, 2),
26
30
  );
27
- const lastReview: StoredReview = { decision: args.decision, summary: args.summary, reviewArtifact };
31
+ const lastReview: StoredReview = { decision: args.decision, summary: args.summary, reviewPath };
28
32
 
29
33
  if (args.decision === "accept") {
30
34
  return run.complete({
31
35
  summary: `Implementation accepted after ${currentIteration} iteration(s).`,
32
- artifacts: { plan: planArtifact, review: reviewArtifact },
33
- data: { status: "done", repositoryPath, iterations: currentIteration, lastReview },
36
+ data: { status: "done", repositoryPath, planPath, reviewPath, iterations: currentIteration, lastReview },
34
37
  });
35
38
  }
36
39
 
37
40
  if (args.decision === "blocked") {
38
41
  return run.fail({
39
42
  summary: args.summary,
40
- artifacts: { plan: planArtifact, review: reviewArtifact },
41
- data: { status: "blocked", repositoryPath, iterations: currentIteration, lastReview },
43
+ data: { status: "blocked", repositoryPath, planPath, reviewPath, iterations: currentIteration, lastReview },
42
44
  });
43
45
  }
44
46
 
45
47
  if (currentIteration >= maxIterations) {
46
48
  return run.fail({
47
49
  summary: `Maximum iteration count reached after ${currentIteration} iteration(s).`,
48
- artifacts: { plan: planArtifact, review: reviewArtifact },
49
- data: { status: "needs-attention", repositoryPath, iterations: currentIteration, lastReview },
50
+ data: { status: "needs-attention", repositoryPath, planPath, reviewPath, iterations: currentIteration, lastReview },
50
51
  });
51
52
  }
52
53
 
53
54
  const nextIteration = currentIteration + 1;
54
- return implementationWorkflow({ task, repositoryPath, maxIterations, planArtifact, iteration: nextIteration, previousReviewArtifact: reviewArtifact });
55
+ return implementationWorkflow({ task, repositoryPath, maxIterations, planPath, iteration: nextIteration, previousReviewPath: reviewPath });
55
56
  }
56
57
  });
@@ -1,5 +1,4 @@
1
1
  import { implementationArgsSchema } from "../implementation/schema.ts";
2
- import { artifactRefSchema } from "@vimhead.dev/norn";
3
2
  import { Type, type StaticDecode } from "typebox";
4
3
  import { reviewDecisionSchema } from "../review/schema.ts";
5
4
 
@@ -7,7 +6,7 @@ export const reviewRouterArgsSchema = Type.Object({
7
6
  ...implementationArgsSchema.properties,
8
7
  decision: reviewDecisionSchema,
9
8
  summary: Type.String(),
10
- automatedReviewArtifact: artifactRefSchema,
9
+ automatedReviewPath: Type.String(),
11
10
  });
12
11
 
13
12
  export type ReviewRouterArgs = StaticDecode<typeof reviewRouterArgsSchema>;
@@ -1 +1 @@
1
- {"version":"0.1.0-tip.35390859630.1"}
1
+ {"version":"0.1.0-tip.35568631545.1"}
@@ -12,6 +12,7 @@ import { Value } from "typebox/value";
12
12
  import { NORN_BUILD_INFO, type NornBuildInfo, type NornGithubReleaseBinaryBuildInfo } from "./build-info.ts";
13
13
  import { renderNornDocumentationIntro, resolveDocumentationCacheRoot, resolveNornDocumentation, type NornDocumentationSource } from "./documentation.ts";
14
14
  import { NornEngine } from "./internal/engine.ts";
15
+ import { resolveNornAgentDirectory } from "./internal/agent-directory.ts";
15
16
  import { errorMessage, NornProjectLoadError, NornRunStoppedError } from "./internal/errors.ts";
16
17
  import { clearRunResumeRequest, readRunLaunchRequest, readRunResumeRequest, writeRunLaunchRequest, writeRunResumeRequest, type NornRunResumeRequest } from "./internal/launch-request.ts";
17
18
  import { readRunMetrics } from "./internal/metrics.ts";
@@ -19,6 +20,7 @@ import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.ts";
19
20
  import { generateRunName } from "./internal/run-names.ts";
20
21
  import { assertRunVersion, getRunInfo, listRuns, mergeInterruptedWorkflowArgs, resolveRunRoot } from "./internal/run-state.ts";
21
22
  import { NornRunStore } from "./internal/run-store.ts";
23
+ import { prepareRunWorkerDirectory } from "./internal/worker-directory.ts";
22
24
  import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./workflow-loader.ts";
23
25
 
24
26
  const RUNS_ROOT = join(".norn", "runs");
@@ -784,7 +786,7 @@ async function startRun(workflowId: string, commandArgs: readonly string[]): Pro
784
786
  const createdAt = new Date().toISOString();
785
787
  await writeRunLaunchRequest(runRoot, { version: 2, type: "run", id, name, workflowId, args, configOverride, createdAt });
786
788
  await startDetachedExecuteRun(id, project.projectRoot);
787
- writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, createdAt }) });
789
+ writeJson({ run: startedRunInfo({ id, name, workflow, runRoot, projectRoot: project.projectRoot, createdAt }) });
788
790
  }
789
791
 
790
792
  async function resumeRun(run: string, args: readonly string[]): Promise<void> {
@@ -899,8 +901,10 @@ async function executeRun(runId: string): Promise<void> {
899
901
  const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
900
902
  const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
901
903
  try {
902
- const project = await loadNornProject(process.cwd());
903
- const engine = new NornEngine({ cwd: project.projectRoot, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
904
+ const agentDir = resolveNornAgentDirectory({ home: homedir(), environment: process.env });
905
+ process.chdir(await prepareRunWorkerDirectory(runRoot));
906
+ const project = await loadNornProject(location.projectRoot);
907
+ const engine = new NornEngine({ cwd: project.projectRoot, agentDir, signal: abortController.signal, gateMode: "pause", config: project.projectConfig });
904
908
  engine.registerWorkflows(project.definitions);
905
909
  if (request.type === "run") {
906
910
  const workflow = project.registry.workflowById(request.workflowId);
@@ -1000,6 +1004,7 @@ function detachedExecuteRunArgs(runId: string): readonly string[] {
1000
1004
  }
1001
1005
 
1002
1006
  function startedRunInfo(input: {
1007
+ readonly projectRoot: string;
1003
1008
  readonly id: string;
1004
1009
  readonly name: string;
1005
1010
  readonly workflow: NornAnyWorkflowDeclaration;
@@ -1011,6 +1016,7 @@ function startedRunInfo(input: {
1011
1016
  id: input.id,
1012
1017
  name: input.name,
1013
1018
  path: input.runRoot,
1019
+ paths: { project: input.projectRoot, workspace: join(input.runRoot, "current", "workspace") },
1014
1020
  entrypointWorkflowId: input.workflow.id,
1015
1021
  currentWorkflowId: input.workflow.id,
1016
1022
  status: "running",
@@ -2,8 +2,8 @@ import type { NornBuildInfo } from "./build-info.ts";
2
2
 
3
3
  export const NORN_GENERATED_BUILD_INFO = {
4
4
  "kind": "npm-registry",
5
- "version": "0.1.0-tip.35390859630.1",
6
- "commit": "7a7976170d62c342c29c6479444394d542c3f22b",
5
+ "version": "0.1.0-tip.35568631545.1",
6
+ "commit": "f16f4b16ede3d325d56cdbd1f21193b3a3d45d38",
7
7
  "packageSpec": "@vimhead.dev/norn-cli@tip",
8
8
  "upgrade": {
9
9
  "supported": false,
@@ -5,6 +5,7 @@ import {
5
5
  createEventBus,
6
6
  type AgentSession,
7
7
  type CreateAgentSessionOptions,
8
+ type ToolDefinition,
8
9
  } from "@earendil-works/pi-coding-agent";
9
10
  import type { NornAgentCreateSessionInput, NornAgentPromptInput, NornAgentRunRawAttempt, NornAgentRunResult, NornAgentSession, NornAgentSessionEvents, NornAgentSinglePromptInput, NornAgentUsage } from "@vimhead.dev/norn";
10
11
  import { AGENT_RESPONSE_TOOL_NAME } from "@vimhead.dev/norn-core/agent-protocol";
@@ -12,7 +13,8 @@ import { inspectSchema } from "@vimhead.dev/norn/schema";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import { mkdir } from "node:fs/promises";
14
15
  import { homedir } from "node:os";
15
- import { isAbsolute, relative, resolve, sep } from "node:path";
16
+ import { resolve } from "node:path";
17
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
16
18
  import { type StaticDecode, type TSchema } from "typebox";
17
19
  import { resolveNornAgentDirectory } from "./agent-directory.ts";
18
20
  import {
@@ -23,19 +25,14 @@ import {
23
25
  import { errorMessage } from "./errors.ts";
24
26
  import { safeFileName } from "./file-names.ts";
25
27
  import type { NornRunLogs } from "./logs.ts";
26
- import { NornSessionResourceBindings } from "./resource-bindings.ts";
27
28
  import type { NornRunLogger } from "./run-log.ts";
28
29
  import { agentUsageFromValue, emptyAgentUsage, totalAgentUsage } from "./usage.ts";
29
30
 
30
31
  const DEFAULT_AGENT_ATTEMPTS = 3;
31
- const DEFAULT_AGENT_TOOL_ALLOWLIST = ["read", "bash", "edit", "write", AGENT_RESPONSE_TOOL_NAME] as const;
32
32
 
33
33
  type NornAgentRunnerInput = {
34
34
  readonly id: string;
35
35
  readonly runRoot: string;
36
- readonly boundaryRoot: string;
37
- readonly boundaryName: string;
38
- readonly cwd: string;
39
36
  readonly signal?: AbortSignal;
40
37
  readonly model?: CreateAgentSessionOptions["model"];
41
38
  readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
@@ -49,7 +46,6 @@ type CreatedNornAgentSessionInput = NornAgentRunnerInput & {
49
46
  readonly label: string;
50
47
  readonly cwd: string;
51
48
  readonly session: AgentSession;
52
- readonly resourceBindings: NornSessionResourceBindings;
53
49
  readonly events: NornAgentSessionEvents;
54
50
  };
55
51
 
@@ -61,7 +57,7 @@ export class NornAgentRunner {
61
57
  }
62
58
 
63
59
  async createSession(agentInput: NornAgentCreateSessionInput): Promise<NornAgentSession> {
64
- const cwd = agentInput.cwd ? this.resolveFromCwd(agentInput.cwd) : this.input.cwd;
60
+ const cwd = requireAbsoluteWorkingDirectory(agentInput.cwd);
65
61
  const sessionDir = resolve(this.input.runRoot, "sessions");
66
62
  await mkdir(sessionDir, { recursive: true });
67
63
 
@@ -82,19 +78,19 @@ export class NornAgentRunner {
82
78
  ...loader.getExtensions().errors.map(error => `Failed to load extension ${error.path}: ${error.error}`),
83
79
  ];
84
80
  if (errors.length > 0) throw new Error(errors.join("\n"));
85
- const resourceBindings = new NornSessionResourceBindings();
81
+ const customTools = agentInput.customTools ?? [];
82
+ assertCustomToolNames({
83
+ tools: customTools,
84
+ reservedNames: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
85
+ ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
86
+ });
86
87
  let session: AgentSession | undefined;
87
88
  try {
88
- await resourceBindings.bind({
89
- adapters: agentInput.resourceAdapters ?? [], runId: this.input.id, label: agentInput.label,
90
- reservedTools: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
91
- ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
92
- });
93
89
  ({ session } = await createAgentSessionFromServices({
94
90
  services,
95
91
  sessionManager: SessionManager.create(cwd, sessionDir),
96
- tools: [...withAgentResponseTool(agentInput.tools), ...resourceBindings.tools.map((tool) => tool.name)],
97
- customTools: [this.responseToolFactory.create(), ...resourceBindings.tools],
92
+ tools: withAgentResponseTool(agentInput.tools),
93
+ customTools: [this.responseToolFactory.create(), ...customTools],
98
94
  model: agentInput.model ?? this.input.model,
99
95
  thinkingLevel: agentInput.thinkingLevel ?? this.input.thinkingLevel,
100
96
  }));
@@ -102,13 +98,12 @@ export class NornAgentRunner {
102
98
  await session.bindExtensions({});
103
99
  await this.input.logger.record({ type: "agent.spawned", label: agentInput.label, cwd });
104
100
  return new CreatedNornAgentSession({
105
- ...this.input, label: agentInput.label, cwd, session, resourceBindings, events: eventBus,
101
+ ...this.input, label: agentInput.label, cwd, session, events: eventBus,
106
102
  });
107
103
  } catch (error) {
108
104
  const errors = [error];
109
105
  try { session?.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
110
- try { await resourceBindings.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
111
- if (errors.length > 1) throw new AggregateError(errors, "Agent creation and resource cleanup failed");
106
+ if (errors.length > 1) throw new AggregateError(errors, "Agent creation and cleanup failed");
112
107
  throw error;
113
108
  }
114
109
  }
@@ -121,15 +116,6 @@ export class NornAgentRunner {
121
116
  await agent.dispose();
122
117
  }
123
118
  }
124
-
125
- private resolveFromCwd(path: string): string {
126
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
127
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
128
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
129
- throw new Error(`Agent cwd escapes ${this.input.boundaryName} isolation: ${path}`);
130
- }
131
- return resolvedPath;
132
- }
133
119
  }
134
120
 
135
121
  function systemPromptOverride(agentInput: NornAgentCreateSessionInput): (() => string) | undefined {
@@ -229,7 +215,6 @@ class CreatedNornAgentSession implements NornAgentSession {
229
215
  const errors: unknown[] = [];
230
216
  try { await this.input.session.abort(); } catch (error) { errors.push(error); }
231
217
  try { this.input.session.dispose(); } catch (error) { errors.push(error); }
232
- try { await this.input.resourceBindings.dispose(); } catch (error) { errors.push(error); }
233
218
  if (errors.length) throw new AggregateError(errors, "Agent disposal failed");
234
219
  await this.input.logger.record({ type: "agent.disposed", label: this.label });
235
220
  }
@@ -360,7 +345,14 @@ function withResponseToolFinalizationInstruction(
360
345
  ].join("\n");
361
346
  }
362
347
 
363
- function withAgentResponseTool(tools: readonly string[] | undefined): string[] {
364
- if (!tools) return [...DEFAULT_AGENT_TOOL_ALLOWLIST];
365
- return Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
348
+ function withAgentResponseTool(tools: readonly string[] | undefined): string[] | undefined {
349
+ return tools === undefined ? undefined : Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
350
+ }
351
+
352
+ function assertCustomToolNames(input: { readonly tools: readonly ToolDefinition[]; readonly reservedNames: readonly string[] }): void {
353
+ const names = new Set(input.reservedNames);
354
+ for (const tool of input.tools) {
355
+ if (names.has(tool.name)) throw new Error(`Custom tool name collision: ${tool.name}`);
356
+ names.add(tool.name);
357
+ }
366
358
  }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { requireAbsoluteWorkingDirectory } from "./working-directory.ts";
4
4
  import type { WriteStream } from "node:fs";
5
5
  import type { NornCommandRunInput, NornCommandRunResult, NornLogRef } from "@vimhead.dev/norn";
6
6
  import { errorMessage } from "./errors.ts";
@@ -9,9 +9,6 @@ import type { NornRunLogs } from "./logs.ts";
9
9
  import type { NornRunLogger } from "./run-log.ts";
10
10
 
11
11
  type NornCommandRunnerInput = {
12
- readonly boundaryRoot: string;
13
- readonly boundaryName: string;
14
- readonly cwd: string;
15
12
  readonly signal?: AbortSignal;
16
13
  readonly logs: NornRunLogs;
17
14
  readonly logger: NornRunLogger;
@@ -21,7 +18,7 @@ export class NornCommandRunner {
21
18
  constructor(private readonly input: NornCommandRunnerInput) {}
22
19
 
23
20
  async run(commandInput: NornCommandRunInput): Promise<NornCommandRunResult> {
24
- const cwd = this.resolveFromCwd(commandInput.cwd ?? this.input.cwd);
21
+ const cwd = requireAbsoluteWorkingDirectory(commandInput.cwd);
25
22
  const startedAtMs = Date.now();
26
23
  const invocationId = randomUUID();
27
24
  const stdoutRef = commandLog({ label: commandInput.label, invocationId, stream: "stdout" });
@@ -67,15 +64,6 @@ export class NornCommandRunner {
67
64
  });
68
65
  return commandResult;
69
66
  }
70
-
71
- private resolveFromCwd(path: string): string {
72
- const resolvedPath = isAbsolute(path) ? path : resolve(this.input.cwd, path);
73
- const pathFromBoundary = relative(this.input.boundaryRoot, resolvedPath);
74
- if (pathFromBoundary === ".." || pathFromBoundary.startsWith(`..${sep}`) || isAbsolute(pathFromBoundary)) {
75
- throw new Error(`Command cwd escapes ${this.input.boundaryName} isolation: ${path}`);
76
- }
77
- return resolvedPath;
78
- }
79
67
  }
80
68
 
81
69
  type SpawnCommandInput = {