@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.
- package/assets/README.md +36 -15
- package/assets/docs/README.md +4 -4
- package/assets/docs/agents.md +39 -11
- package/assets/docs/cli.md +6 -6
- package/assets/docs/composition.md +12 -12
- package/assets/docs/persistence.md +31 -17
- package/assets/docs/providers.md +2 -2
- package/assets/docs/recovery.md +1 -1
- package/assets/docs/workflows.md +29 -17
- package/assets/examples/agent-then-analysis/README.md +8 -8
- package/assets/examples/agent-then-analysis/plugin.ts +23 -21
- package/assets/examples/caller-selected-continuation/README.md +11 -10
- package/assets/examples/caller-selected-continuation/caller.ts +15 -13
- package/assets/examples/caller-selected-continuation/producer.ts +11 -8
- package/assets/examples/coordinating-multiple-agents/README.md +27 -17
- package/assets/examples/coordinating-multiple-agents/plugin.ts +64 -41
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +43 -0
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +60 -65
- package/assets/examples/getting-started/README.md +25 -10
- package/assets/examples/getting-started/plugin.ts +30 -11
- package/assets/examples/minimal-workflow/README.md +7 -6
- package/assets/examples/minimal-workflow/plugin.ts +8 -5
- package/assets/examples/shared-state/README.md +5 -5
- package/assets/examples/shared-state/plugin.ts +38 -25
- package/assets/examples/shared-state/shared-state.ts +30 -49
- package/assets/examples/shared-state/state-tools.ts +68 -0
- package/assets/examples/worktree-development-loop/README.md +14 -12
- package/assets/examples/worktree-development-loop/scope.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +3 -3
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +6 -4
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +12 -11
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +10 -6
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +14 -11
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +1 -2
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +16 -15
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +1 -2
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +9 -3
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agents.ts +24 -32
- package/assets/packages/cli/src/internal/commands.ts +2 -14
- package/assets/packages/cli/src/internal/engine.ts +35 -49
- package/assets/packages/cli/src/internal/execution-context.ts +64 -0
- package/assets/packages/cli/src/internal/logs.ts +1 -1
- package/assets/packages/cli/src/internal/run-log.ts +1 -1
- package/assets/packages/cli/src/internal/run-state.ts +7 -2
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +24 -9
- package/assets/packages/cli/src/internal/working-directory.ts +6 -0
- package/assets/packages/cli/src/workflow-loader.ts +1 -2
- package/assets/packages/sdk/src/api.ts +65 -91
- package/assets/packages/sdk/src/index.ts +0 -3
- package/assets/tests/workflow-ref.test.ts +9 -9
- package/dist/cli.js +8 -3
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agents.d.ts +0 -4
- package/dist/internal/agents.js +30 -46
- package/dist/internal/commands.d.ts +0 -4
- package/dist/internal/commands.js +2 -10
- package/dist/internal/engine.d.ts +1 -1
- package/dist/internal/engine.js +29 -43
- package/dist/internal/execution-context.d.ts +26 -0
- package/dist/internal/execution-context.js +55 -0
- package/dist/internal/file-coordinator.d.ts +17 -0
- package/dist/internal/file-coordinator.js +162 -0
- package/dist/internal/logs.d.ts +1 -1
- package/dist/internal/run-log.d.ts +1 -1
- package/dist/internal/run-state.d.ts +2 -0
- package/dist/internal/run-state.js +5 -2
- package/dist/internal/worker-directory.d.ts +1 -0
- package/dist/internal/worker-directory.js +26 -0
- package/dist/internal/workflow-registry.d.ts +13 -3
- package/dist/internal/workflow-registry.js +12 -7
- package/dist/internal/working-directory.d.ts +1 -0
- package/dist/internal/working-directory.js +9 -0
- package/dist/workflow-loader.js +1 -2
- package/package.json +2 -2
- package/assets/docs/resources.md +0 -46
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
- package/assets/examples/shared-state/state-adapter.ts +0 -76
- package/assets/packages/cli/src/internal/artifacts.ts +0 -26
- package/assets/packages/cli/src/internal/resource-bindings.ts +0 -35
- package/assets/packages/cli/src/internal/run.ts +0 -127
- package/assets/packages/cli/src/resources.ts +0 -69
- package/assets/packages/sdk/src/agent-resource-adapter.ts +0 -11
- package/assets/packages/sdk/src/resources.ts +0 -20
- package/dist/internal/artifacts.d.ts +0 -10
- package/dist/internal/artifacts.js +0 -29
- package/dist/internal/resource-bindings.d.ts +0 -13
- package/dist/internal/resource-bindings.js +0 -34
- package/dist/internal/run.d.ts +0 -49
- package/dist/internal/run.js +0 -103
- package/dist/resources.d.ts +0 -11
- package/dist/resources.js +0 -100
- /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 `
|
|
13
|
-
2. passes the repository path and retained
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
82
|
-
git -C "
|
|
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
|
|
116
|
-
`repositoryPath: "repo"`, and the iteration count. The review
|
|
117
|
-
`review/iteration-N-decision.json`, retaining the chosen decision and
|
|
118
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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,
|
|
13
|
-
const repositoryPath = await materializeWorkspaceRepository(
|
|
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
|
|
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(
|
|
7
|
-
const repositoryPath =
|
|
8
|
-
const result = await
|
|
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
|
-
|
|
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,
|
|
14
|
-
const repositoryPath = args.repositoryPath;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
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
|
|
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
|
|
33
|
-
await
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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,
|
|
13
|
-
const repositoryPath = args.repositoryPath;
|
|
14
|
-
const planning = await
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
11
|
+
name: "review",
|
|
10
12
|
isEntrypoint: false,
|
|
11
13
|
instructions: "Review the current repository boundary changes.",
|
|
12
14
|
args: reviewArgsSchema,
|
|
13
|
-
async execute({ args,
|
|
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
|
|
18
|
-
const diff = await
|
|
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
|
|
25
|
-
await
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
9
|
+
automatedReviewPath: Type.String(),
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
export type ReviewRouterArgs = StaticDecode<typeof reviewRouterArgsSchema>;
|
package/assets/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.1.0-tip.
|
|
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
|
|
903
|
-
|
|
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.
|
|
6
|
-
"commit": "
|
|
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 {
|
|
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 =
|
|
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
|
|
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:
|
|
97
|
-
customTools: [this.responseToolFactory.create(), ...
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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 {
|
|
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 =
|
|
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 = {
|