@vimhead.dev/norn-cli 0.1.0-tip.35359392805.1 → 0.1.0-tip.35436871363.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/README.md +22 -27
- package/assets/docs/README.md +5 -5
- package/assets/docs/agents.md +48 -9
- package/assets/docs/cli.md +7 -7
- package/assets/docs/composition.md +25 -25
- package/assets/docs/persistence.md +34 -23
- package/assets/docs/projects.md +30 -18
- package/assets/docs/providers.md +3 -3
- package/assets/docs/recovery.md +13 -9
- package/assets/docs/schemas.md +1 -1
- package/assets/docs/workflows.md +105 -12
- package/assets/examples/agent-then-analysis/README.md +10 -10
- package/assets/examples/agent-then-analysis/input.json +1 -1
- package/assets/examples/agent-then-analysis/norn.project.json +1 -1
- package/assets/examples/agent-then-analysis/plugin.ts +57 -68
- package/assets/examples/caller-selected-continuation/README.md +15 -14
- package/assets/examples/caller-selected-continuation/caller.ts +37 -38
- package/assets/examples/caller-selected-continuation/input.json +2 -2
- package/assets/examples/caller-selected-continuation/norn.project.json +1 -1
- package/assets/examples/caller-selected-continuation/producer.ts +19 -24
- package/assets/examples/coordinating-multiple-agents/README.md +27 -17
- package/assets/examples/coordinating-multiple-agents/input.json +1 -1
- package/assets/examples/coordinating-multiple-agents/norn.project.json +1 -1
- package/assets/examples/coordinating-multiple-agents/plugin.ts +79 -60
- 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 +2 -2
- package/assets/examples/getting-started/norn.project.json +1 -1
- package/assets/examples/getting-started/plugin.ts +20 -26
- package/assets/examples/minimal-workflow/README.md +13 -12
- package/assets/examples/minimal-workflow/norn.project.json +1 -1
- package/assets/examples/minimal-workflow/plugin.ts +14 -25
- package/assets/examples/shared-state/README.md +12 -4
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +1 -1
- package/assets/examples/shared-state/plugin.ts +49 -41
- package/assets/examples/shared-state/shared-state.ts +56 -0
- package/assets/examples/shared-state/state-tools.ts +68 -0
- package/assets/examples/worktree-development-loop/README.md +18 -17
- package/assets/examples/worktree-development-loop/norn.project.json +1 -1
- package/assets/examples/worktree-development-loop/plugin.ts +6 -26
- package/assets/examples/worktree-development-loop/scope.ts +4 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +14 -15
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +5 -3
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +2 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +35 -33
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +6 -3
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +27 -16
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +4 -2
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +43 -31
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +3 -4
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +6 -6
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +50 -44
- package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +2 -3
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/cli.ts +44 -37
- package/assets/packages/cli/src/client.ts +9 -16
- package/assets/packages/cli/src/documentation-intro.ts +1 -1
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +3 -3
- 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 +63 -101
- package/assets/packages/cli/src/internal/errors.ts +4 -4
- package/assets/packages/cli/src/internal/launch-request.ts +6 -6
- 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 +38 -23
- package/assets/packages/cli/src/internal/run.ts +4 -62
- package/assets/packages/cli/src/internal/worker-directory.ts +17 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +126 -111
- package/assets/packages/cli/src/internal/working-directory.ts +6 -0
- package/assets/packages/cli/src/workflow-loader.ts +213 -0
- package/assets/packages/core/src/workflow-transition.ts +2 -2
- package/assets/packages/sdk/src/api.ts +141 -329
- package/assets/packages/sdk/src/index.ts +1 -4
- package/assets/packages/sdk/src/schema.ts +3 -3
- package/assets/tests/workflow-ref.test.ts +53 -55
- package/dist/cli.js +41 -35
- package/dist/client.d.ts +3 -4
- package/dist/client.js +3 -8
- package/dist/documentation-intro.js +1 -1
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-response-tool.js +3 -3
- 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 +4 -8
- package/dist/internal/engine.js +57 -84
- package/dist/internal/errors.d.ts +3 -3
- package/dist/internal/errors.js +1 -1
- package/dist/internal/file-coordinator.d.ts +17 -0
- package/dist/internal/file-coordinator.js +162 -0
- package/dist/internal/launch-request.d.ts +4 -4
- package/dist/internal/launch-request.js +2 -2
- package/dist/internal/logs.d.ts +1 -1
- package/dist/internal/run-log.d.ts +1 -1
- package/dist/internal/run-state.d.ts +11 -8
- package/dist/internal/run-state.js +34 -22
- package/dist/internal/run.d.ts +3 -23
- package/dist/internal/run.js +4 -50
- package/dist/internal/worker-directory.d.ts +1 -0
- package/dist/internal/worker-directory.js +26 -0
- package/dist/internal/workflow-registry.d.ts +35 -17
- package/dist/internal/workflow-registry.js +110 -73
- package/dist/internal/working-directory.d.ts +1 -0
- package/dist/internal/working-directory.js +9 -0
- package/dist/{plugin-loader.d.ts → workflow-loader.d.ts} +7 -8
- package/dist/workflow-loader.js +234 -0
- package/package.json +2 -2
- package/assets/docs/resources.md +0 -61
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +0 -51
- package/assets/examples/worktree-development-loop/manifest.ts +0 -26
- package/assets/examples/worktree-development-loop/state.ts +0 -23
- package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +0 -8
- package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +0 -12
- 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-resources.ts +0 -23
- package/assets/packages/cli/src/internal/state-store.ts +0 -83
- package/assets/packages/cli/src/plugin-loader.ts +0 -400
- 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/assets/packages/sdk/src/state-adapter.ts +0 -75
- 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-resources.d.ts +0 -6
- package/dist/internal/run-resources.js +0 -26
- package/dist/internal/state-store.d.ts +0 -23
- package/dist/internal/state-store.js +0 -103
- package/dist/plugin-loader.js +0 -341
- 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
|
@@ -12,14 +12,16 @@ 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";
|
|
18
19
|
import { getRunLeaseOwner, NornRunLease } from "./internal/run-lease.ts";
|
|
19
20
|
import { generateRunName } from "./internal/run-names.ts";
|
|
20
|
-
import { getRunInfo, listRuns,
|
|
21
|
+
import { assertRunVersion, getRunInfo, listRuns, mergeInterruptedWorkflowArgs, resolveRunRoot } from "./internal/run-state.ts";
|
|
21
22
|
import { NornRunStore } from "./internal/run-store.ts";
|
|
22
|
-
import {
|
|
23
|
+
import { prepareRunWorkerDirectory } from "./internal/worker-directory.ts";
|
|
24
|
+
import { discoverNornProject, findNornProject, inspectNornWorkflow, loadNornProject, NORN_PROJECT_FILE_NAME } from "./workflow-loader.ts";
|
|
23
25
|
|
|
24
26
|
const RUNS_ROOT = join(".norn", "runs");
|
|
25
27
|
const RUN_WAIT_INTERVAL_MS = 1000;
|
|
@@ -110,9 +112,9 @@ const COMMANDS: readonly CliCommand[] = [
|
|
|
110
112
|
{
|
|
111
113
|
id: "project.inspect",
|
|
112
114
|
path: ["project", "inspect"],
|
|
113
|
-
description: "Use when discovering the active Norn project,
|
|
115
|
+
description: "Use when discovering the active Norn project, configuration, and workflow sources.",
|
|
114
116
|
usage: "norn project inspect",
|
|
115
|
-
output: "JSON object with project metadata under project, isComplete, and
|
|
117
|
+
output: "JSON object with project metadata under project, isComplete, and registration diagnostics. Incomplete discovery does not permit execution.",
|
|
116
118
|
examples: ["norn project inspect"],
|
|
117
119
|
execute: async (args) => {
|
|
118
120
|
assertNoExtraArgs("project inspect", args);
|
|
@@ -125,17 +127,17 @@ const COMMANDS: readonly CliCommand[] = [
|
|
|
125
127
|
description: "Use when selecting a Norn workflow for a user task; defaults to entrypoint workflows.",
|
|
126
128
|
usage: "norn workflows list [--entrypoints|--all]",
|
|
127
129
|
options: ["--entrypoints: list entrypoint workflows", "--all: include internal workflow steps"],
|
|
128
|
-
output: "JSON object with workflow summaries under workflows, isComplete, and
|
|
130
|
+
output: "JSON object with workflow summaries under workflows, isComplete, and registration diagnostics. Successful discovery can be incomplete; start/resume remain strict.",
|
|
129
131
|
examples: ["norn workflows list", "norn workflows list --all"],
|
|
130
132
|
execute: listWorkflows,
|
|
131
133
|
},
|
|
132
134
|
{
|
|
133
135
|
id: "workflows.inspect",
|
|
134
136
|
path: ["workflows", "inspect"],
|
|
135
|
-
description: "Use when reading a workflow's instructions,
|
|
137
|
+
description: "Use when reading a workflow's instructions, args schema, gate contract, and registration source before starting or editing it.",
|
|
136
138
|
usage: "norn workflows inspect <workflow-id>",
|
|
137
139
|
arguments: ["workflow-id: fully qualified workflow id"],
|
|
138
|
-
output: "JSON object with workflow details, isComplete, and
|
|
140
|
+
output: "JSON object with workflow details, isComplete, and registration diagnostics. workflow is null if unavailable in an incomplete catalog or its schema cannot be inspected; an unknown id in a complete catalog is an error.",
|
|
139
141
|
examples: ["norn workflows inspect example.plan"],
|
|
140
142
|
execute: async (args) => {
|
|
141
143
|
const workflowId = requiredArg("workflows inspect", args, 0, "workflow id");
|
|
@@ -146,12 +148,12 @@ const COMMANDS: readonly CliCommand[] = [
|
|
|
146
148
|
{
|
|
147
149
|
id: "runs.start",
|
|
148
150
|
path: ["runs", "start"],
|
|
149
|
-
description: "Use when starting a Norn workflow run after the workflow id and
|
|
151
|
+
description: "Use when starting a Norn workflow run after the workflow id and args are known.",
|
|
150
152
|
usage: "norn runs start <workflow-id>",
|
|
151
153
|
arguments: ["workflow-id: fully qualified workflow id to start"],
|
|
152
|
-
stdin: "Optional JSON object: {\"
|
|
154
|
+
stdin: "Optional JSON object: {\"args\":{...},\"config\":{\"workflowOrScopeId\":{...}}}.",
|
|
153
155
|
output: "JSON object with started run info under run.",
|
|
154
|
-
examples: ["printf '{\"
|
|
156
|
+
examples: ["printf '{\"args\":{\"task\":\"Add tests\"}}' | norn runs start example.plan"],
|
|
155
157
|
execute: async (args) => {
|
|
156
158
|
const workflowId = requiredArg("runs start", args, 0, "workflow id");
|
|
157
159
|
await startRun(workflowId, args.slice(1));
|
|
@@ -163,9 +165,9 @@ const COMMANDS: readonly CliCommand[] = [
|
|
|
163
165
|
description: "Use when resuming an interrupted gate or a checkpoint restored for retry.",
|
|
164
166
|
usage: "norn runs resume <run>",
|
|
165
167
|
arguments: ["run: run id, generated name, or run path"],
|
|
166
|
-
stdin: "Optional JSON object: {\"
|
|
168
|
+
stdin: "Optional JSON object: {\"args\":{...}}. Interrupted runs require a patch containing only editable gate fields; pending-resume runs do not accept args.",
|
|
167
169
|
output: "JSON object with resumed run info under run.",
|
|
168
|
-
examples: ["printf '{\"
|
|
170
|
+
examples: ["printf '{\"args\":{\"decision\":\"accept\"}}' | norn runs resume quiet-river-lantern"],
|
|
169
171
|
execute: async (args) => {
|
|
170
172
|
const run = requiredArg("runs resume", args, 0, "run");
|
|
171
173
|
await resumeRun(run, args.slice(1));
|
|
@@ -753,7 +755,7 @@ async function initProject(): Promise<void> {
|
|
|
753
755
|
const projectPath = resolve(projectRoot, NORN_PROJECT_FILE_NAME);
|
|
754
756
|
if (await isFile(projectPath)) throw new Error(`Norn project already exists: ${projectPath}`);
|
|
755
757
|
await mkdir(resolve(projectRoot, RUNS_ROOT), { recursive: true });
|
|
756
|
-
await writeFile(projectPath, `${JSON.stringify({ version: 1,
|
|
758
|
+
await writeFile(projectPath, `${JSON.stringify({ version: 1, workflows: [], includes: [], config: {} }, null, 2)}\n`, "utf8");
|
|
757
759
|
await ensureGitignoreExcludesRunState(projectRoot);
|
|
758
760
|
writeJson({ project: { path: projectPath, root: projectRoot, runsRoot: resolve(projectRoot, RUNS_ROOT) } });
|
|
759
761
|
}
|
|
@@ -768,23 +770,23 @@ async function inspectProject(): Promise<void> {
|
|
|
768
770
|
writeJson({ project, isComplete, diagnostics });
|
|
769
771
|
}
|
|
770
772
|
|
|
771
|
-
async function startRun(workflowId: string,
|
|
772
|
-
assertNoStructuredInputArgs("runs start",
|
|
773
|
+
async function startRun(workflowId: string, commandArgs: readonly string[]): Promise<void> {
|
|
774
|
+
assertNoStructuredInputArgs("runs start", commandArgs);
|
|
773
775
|
const project = await loadNornProject(process.cwd());
|
|
774
776
|
const workflow = project.registry.workflowById(workflowId);
|
|
775
777
|
if (!workflow) throw new Error(`Unknown workflow: ${workflowId}`);
|
|
776
778
|
const input = parseStartRunInput(await readStdinJson());
|
|
777
|
-
const
|
|
778
|
-
Value.Decode(workflow.
|
|
779
|
+
const args = input.args === undefined ? {} : input.args;
|
|
780
|
+
Value.Decode(workflow.args, args);
|
|
779
781
|
const configOverride = input.config;
|
|
780
782
|
const id = randomUUID();
|
|
781
783
|
const name = generateRunName(new Set((await listRuns(project.projectRoot)).map((run) => run.name)));
|
|
782
784
|
const runRoot = resolve(project.projectRoot, RUNS_ROOT, id);
|
|
783
785
|
await mkdir(runRoot, { recursive: true });
|
|
784
786
|
const createdAt = new Date().toISOString();
|
|
785
|
-
await writeRunLaunchRequest(runRoot, { version:
|
|
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> {
|
|
@@ -796,8 +798,9 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
|
|
|
796
798
|
let request: NornRunResumeRequest;
|
|
797
799
|
try {
|
|
798
800
|
const runInfo = await getRunInfo(runRoot);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
+
assertRunVersion(runInfo.version);
|
|
802
|
+
const args = await parseResumeArgs(runInfo, input.args);
|
|
803
|
+
request = { version: 2, type: "resume", id: runInfo.id, requestId: randomUUID(), args, createdAt: new Date().toISOString() };
|
|
801
804
|
await writeRunResumeRequest(runRoot, request);
|
|
802
805
|
} finally {
|
|
803
806
|
await lease.release();
|
|
@@ -811,18 +814,18 @@ async function resumeRun(run: string, args: readonly string[]): Promise<void> {
|
|
|
811
814
|
writeJson({ run: await getRunInfo(runRoot) });
|
|
812
815
|
}
|
|
813
816
|
|
|
814
|
-
async function
|
|
817
|
+
async function parseResumeArgs(runInfo: NornRunInfo, args: unknown): Promise<unknown> {
|
|
815
818
|
if (runInfo.status === "interrupted") {
|
|
816
|
-
if (
|
|
819
|
+
if (args === undefined) throw new Error(`Interrupted workflow resume requires args: ${runInfo.name}`);
|
|
817
820
|
if (!runInfo.currentWorkflowId) throw new Error(`Run has no current workflow: ${runInfo.name}`);
|
|
818
821
|
const project = await loadNornProject(process.cwd());
|
|
819
822
|
const workflow = project.registry.workflowById(runInfo.currentWorkflowId);
|
|
820
823
|
if (!workflow) throw new Error(`Unknown workflow for resumed run: ${runInfo.currentWorkflowId}`);
|
|
821
|
-
Value.Decode(workflow.
|
|
822
|
-
return
|
|
824
|
+
Value.Decode(workflow.args, mergeInterruptedWorkflowArgs(runInfo.interruption?.args, args, runInfo.interruption?.fields));
|
|
825
|
+
return args;
|
|
823
826
|
}
|
|
824
827
|
if (runInfo.status === "pendingResume") {
|
|
825
|
-
if (
|
|
828
|
+
if (args !== undefined) throw new Error(`Pending-resume workflows do not accept args: ${runInfo.name}`);
|
|
826
829
|
return undefined;
|
|
827
830
|
}
|
|
828
831
|
throw new Error(`Run must be rolled back before resuming: ${runInfo.name}`);
|
|
@@ -898,13 +901,15 @@ async function executeRun(runId: string): Promise<void> {
|
|
|
898
901
|
const runRoot = resolve(location.projectRoot, RUNS_ROOT, runId);
|
|
899
902
|
const request = await readOptionalRunLaunchRequest(runRoot) ?? await readRunResumeRequest(runRoot);
|
|
900
903
|
try {
|
|
901
|
-
const
|
|
902
|
-
|
|
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 });
|
|
908
|
+
engine.registerWorkflows(project.definitions);
|
|
904
909
|
if (request.type === "run") {
|
|
905
910
|
const workflow = project.registry.workflowById(request.workflowId);
|
|
906
911
|
if (!workflow) throw new Error(`Unknown workflow: ${request.workflowId}`);
|
|
907
|
-
await engine.runWorkflow(workflow, request.
|
|
912
|
+
await engine.runWorkflow(workflow, request.args, { id: request.id, name: request.name, configOverride: request.configOverride });
|
|
908
913
|
} else {
|
|
909
914
|
await engine.resumeRequestedWorkflow({ runRoot, request });
|
|
910
915
|
}
|
|
@@ -999,6 +1004,7 @@ function detachedExecuteRunArgs(runId: string): readonly string[] {
|
|
|
999
1004
|
}
|
|
1000
1005
|
|
|
1001
1006
|
function startedRunInfo(input: {
|
|
1007
|
+
readonly projectRoot: string;
|
|
1002
1008
|
readonly id: string;
|
|
1003
1009
|
readonly name: string;
|
|
1004
1010
|
readonly workflow: NornAnyWorkflowDeclaration;
|
|
@@ -1006,10 +1012,11 @@ function startedRunInfo(input: {
|
|
|
1006
1012
|
readonly createdAt: string;
|
|
1007
1013
|
}): NornRunInfo {
|
|
1008
1014
|
return {
|
|
1009
|
-
version:
|
|
1015
|
+
version: 2,
|
|
1010
1016
|
id: input.id,
|
|
1011
1017
|
name: input.name,
|
|
1012
1018
|
path: input.runRoot,
|
|
1019
|
+
paths: { project: input.projectRoot, workspace: join(input.runRoot, "current", "workspace") },
|
|
1013
1020
|
entrypointWorkflowId: input.workflow.id,
|
|
1014
1021
|
currentWorkflowId: input.workflow.id,
|
|
1015
1022
|
status: "running",
|
|
@@ -1058,18 +1065,18 @@ async function readStdin(): Promise<string> {
|
|
|
1058
1065
|
});
|
|
1059
1066
|
}
|
|
1060
1067
|
|
|
1061
|
-
function parseStartRunInput(value: unknown): { readonly
|
|
1068
|
+
function parseStartRunInput(value: unknown): { readonly args?: unknown; readonly config?: unknown } {
|
|
1062
1069
|
if (value === undefined) return {};
|
|
1063
1070
|
const input = parseStructuredInputObject("runs start", value);
|
|
1064
|
-
assertStructuredInputKeys("runs start", input, ["
|
|
1065
|
-
return {
|
|
1071
|
+
assertStructuredInputKeys("runs start", input, ["args", "config"]);
|
|
1072
|
+
return { args: input.args, config: input.config };
|
|
1066
1073
|
}
|
|
1067
1074
|
|
|
1068
|
-
function parseResumeRunInput(value: unknown): { readonly
|
|
1075
|
+
function parseResumeRunInput(value: unknown): { readonly args?: unknown } {
|
|
1069
1076
|
if (value === undefined) return {};
|
|
1070
1077
|
const input = parseStructuredInputObject("runs resume", value);
|
|
1071
|
-
assertStructuredInputKeys("runs resume", input, ["
|
|
1072
|
-
return {
|
|
1078
|
+
assertStructuredInputKeys("runs resume", input, ["args"]);
|
|
1079
|
+
return { args: input.args };
|
|
1073
1080
|
}
|
|
1074
1081
|
|
|
1075
1082
|
function parseStructuredInputObject(command: string, value: unknown): Record<string, unknown> {
|
|
@@ -3,16 +3,15 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import type {
|
|
5
5
|
DeletedNornRunInfo,
|
|
6
|
-
|
|
6
|
+
NornWorkflowDiagnostic,
|
|
7
7
|
NornProjectInspection,
|
|
8
8
|
NornWorkflowCatalogInfo,
|
|
9
9
|
NornWorkflowInspection,
|
|
10
10
|
NornRunCheckpoint,
|
|
11
11
|
NornRunInfo,
|
|
12
12
|
NornRunMetrics,
|
|
13
|
-
NornWorkflowStateReader,
|
|
14
13
|
} from "@vimhead.dev/norn";
|
|
15
|
-
import { loadNornProject } from "./
|
|
14
|
+
import { loadNornProject } from "./workflow-loader.ts";
|
|
16
15
|
import { NornProjectLoadError } from "./internal/errors.ts";
|
|
17
16
|
import type { NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
|
|
18
17
|
|
|
@@ -32,10 +31,9 @@ export type NornClient = {
|
|
|
32
31
|
inspect(workflowId: string): Promise<NornWorkflowInspection>;
|
|
33
32
|
entries(): Promise<readonly NornRegisteredWorkflow[]>;
|
|
34
33
|
};
|
|
35
|
-
readonly state: NornWorkflowStateReader;
|
|
36
34
|
readonly runs: {
|
|
37
|
-
start(input: { readonly workflowId: string; readonly
|
|
38
|
-
resume(input: { readonly run: string; readonly
|
|
35
|
+
start(input: { readonly workflowId: string; readonly args: unknown; readonly configOverride?: unknown }): Promise<NornRunInfo>;
|
|
36
|
+
resume(input: { readonly run: string; readonly args?: unknown }): Promise<NornRunInfo>;
|
|
39
37
|
wait(run: string): Promise<NornRunInfo>;
|
|
40
38
|
list(): Promise<NornRunInfo[]>;
|
|
41
39
|
inspect(run: string): Promise<NornRunInfo>;
|
|
@@ -61,7 +59,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
|
|
|
61
59
|
inspect: async (workflowId) => processRunner.readJson<NornWorkflowInspection>(["workflows", "inspect", workflowId]),
|
|
62
60
|
entries: async () => (await catalog.load()).workflows,
|
|
63
61
|
},
|
|
64
|
-
state: catalog.state,
|
|
65
62
|
runs: {
|
|
66
63
|
start: async (startInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "start", startInput.workflowId], JSON.stringify(runStartInput(startInput)))).run,
|
|
67
64
|
resume: async (resumeInput) => (await processRunner.readJson<{ run: NornRunInfo }>(["runs", "resume", resumeInput.run], runResumeStdin(resumeInput))).run,
|
|
@@ -82,10 +79,6 @@ export function createNornClient(input: NornClientInput = {}): NornClient {
|
|
|
82
79
|
|
|
83
80
|
class NornWorkflowCatalog {
|
|
84
81
|
private loaded: ReturnType<typeof loadNornProject> | undefined;
|
|
85
|
-
readonly state: NornWorkflowStateReader = {
|
|
86
|
-
get: async (state) => (await this.load()).state.get(state),
|
|
87
|
-
getOptional: async (state) => (await this.load()).state.getOptional(state),
|
|
88
|
-
};
|
|
89
82
|
|
|
90
83
|
constructor(private readonly cwd: string) {}
|
|
91
84
|
|
|
@@ -107,7 +100,7 @@ class NornProcessRunner {
|
|
|
107
100
|
|
|
108
101
|
async readJson<T>(args: readonly string[], stdin?: string): Promise<T> {
|
|
109
102
|
const result = await this.spawn(args, stdin);
|
|
110
|
-
const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?:
|
|
103
|
+
const parsed = JSON.parse(result.stdout) as T | { error?: { code?: string; message?: string; diagnostics?: NornWorkflowDiagnostic[] } };
|
|
111
104
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
112
105
|
if (parsed.error?.code === "NORN_PROJECT_INVALID" && Array.isArray(parsed.error.diagnostics)) throw new NornProjectLoadError({ diagnostics: parsed.error.diagnostics });
|
|
113
106
|
throw new Error(parsed.error?.message ?? "Norn command failed");
|
|
@@ -151,10 +144,10 @@ class NornProcessRunner {
|
|
|
151
144
|
}
|
|
152
145
|
}
|
|
153
146
|
|
|
154
|
-
function runStartInput(input: { readonly
|
|
155
|
-
return input.configOverride === undefined ? {
|
|
147
|
+
function runStartInput(input: { readonly args: unknown; readonly configOverride?: unknown }): { readonly args: unknown; readonly config?: unknown } {
|
|
148
|
+
return input.configOverride === undefined ? { args: input.args } : { args: input.args, config: input.configOverride };
|
|
156
149
|
}
|
|
157
150
|
|
|
158
|
-
function runResumeStdin(input: { readonly
|
|
159
|
-
return input.
|
|
151
|
+
function runResumeStdin(input: { readonly args?: unknown }): string | undefined {
|
|
152
|
+
return input.args === undefined ? undefined : JSON.stringify({ args: input.args });
|
|
160
153
|
}
|
|
@@ -24,7 +24,7 @@ export function renderNornDocumentationIntro(input: {
|
|
|
24
24
|
"",
|
|
25
25
|
"Live CLI contracts (argument arrays appended to the runtime argv above):",
|
|
26
26
|
'- ["workflows", "list"] — currently registered entrypoints.',
|
|
27
|
-
'- ["workflows", "inspect", "<workflow-id>"] — current instructions,
|
|
27
|
+
'- ["workflows", "inspect", "<workflow-id>"] — current instructions, args schema, and source.',
|
|
28
28
|
'- ["help"] — command usage.',
|
|
29
29
|
].join("\n");
|
|
30
30
|
}
|
|
@@ -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.35436871363.1",
|
|
6
|
+
"commit": "49b14d0ed0c9633fc6ee530a557ec7babbf52b22",
|
|
7
7
|
"packageSpec": "@vimhead.dev/norn-cli@tip",
|
|
8
8
|
"upgrade": {
|
|
9
9
|
"supported": false,
|
|
@@ -71,12 +71,12 @@ export class NornAgentResponseToolFactory {
|
|
|
71
71
|
label: Type.String({ description: "The exact workflow agent label provided in the prompt." }),
|
|
72
72
|
response: Type.Unknown({ description: "The structured response object requested by the workflow." }),
|
|
73
73
|
}),
|
|
74
|
-
async execute(_toolCallId,
|
|
74
|
+
async execute(_toolCallId, args) {
|
|
75
75
|
try {
|
|
76
|
-
const captured = responseCollector.capture(
|
|
76
|
+
const captured = responseCollector.capture(args.runId, args.label, args.response);
|
|
77
77
|
return {
|
|
78
78
|
content: [{ type: "text", text: `Recorded workflow agent response for ${captured.label}.` }],
|
|
79
|
-
details: { valid: true, runId:
|
|
79
|
+
details: { valid: true, runId: args.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
|
|
80
80
|
terminate: true,
|
|
81
81
|
};
|
|
82
82
|
} catch (error) {
|
|
@@ -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 = {
|