@vimhead.dev/norn-cli 0.1.0-tip.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/assets/README.md +157 -0
- package/assets/docs/README.md +23 -0
- package/assets/docs/agents.md +64 -0
- package/assets/docs/cli.md +183 -0
- package/assets/docs/composition.md +76 -0
- package/assets/docs/persistence.md +56 -0
- package/assets/docs/projects.md +75 -0
- package/assets/docs/recovery.md +56 -0
- package/assets/docs/resources.md +61 -0
- package/assets/docs/workflows.md +57 -0
- package/assets/examples/agent-then-analysis/README.md +103 -0
- package/assets/examples/agent-then-analysis/input.json +5 -0
- package/assets/examples/agent-then-analysis/norn.project.json +4 -0
- package/assets/examples/agent-then-analysis/plugin.ts +89 -0
- package/assets/examples/coordinating-multiple-agents/README.md +56 -0
- package/assets/examples/coordinating-multiple-agents/input.json +10 -0
- package/assets/examples/coordinating-multiple-agents/norn.project.json +4 -0
- package/assets/examples/coordinating-multiple-agents/plugin.ts +102 -0
- package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +52 -0
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +153 -0
- package/assets/examples/minimal-workflow/README.md +71 -0
- package/assets/examples/minimal-workflow/norn.project.json +4 -0
- package/assets/examples/minimal-workflow/plugin.ts +29 -0
- package/assets/examples/shared-state/README.md +19 -0
- package/assets/examples/shared-state/input.json +1 -0
- package/assets/examples/shared-state/norn.project.json +4 -0
- package/assets/examples/shared-state/plugin.ts +47 -0
- package/assets/examples/worktree-development-loop/README.md +66 -0
- package/assets/examples/worktree-development-loop/index.ts +1 -0
- package/assets/examples/worktree-development-loop/manifest.ts +26 -0
- package/assets/examples/worktree-development-loop/norn.project.json +9 -0
- package/assets/examples/worktree-development-loop/plugin.ts +27 -0
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -0
- package/assets/examples/worktree-development-loop/state.ts +23 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +22 -0
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +14 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +54 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/planning/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +28 -0
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/review/declaration.ts +8 -0
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +53 -0
- package/assets/examples/worktree-development-loop/workflows/review/index.ts +10 -0
- package/assets/examples/worktree-development-loop/workflows/review/schema.ts +23 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/declaration.ts +12 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/index.ts +3 -0
- package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +12 -0
- package/assets/package.json +1 -0
- package/assets/packages/cli/src/build-info.ts +36 -0
- package/assets/packages/cli/src/bun/cli.ts +16 -0
- package/assets/packages/cli/src/cli.ts +1135 -0
- package/assets/packages/cli/src/client.ts +167 -0
- package/assets/packages/cli/src/documentation-intro.ts +30 -0
- package/assets/packages/cli/src/documentation.ts +149 -0
- package/assets/packages/cli/src/generated-build-info.ts +12 -0
- package/assets/packages/cli/src/internal/agent-directory.ts +5 -0
- package/assets/packages/cli/src/internal/agent-response-tool.ts +96 -0
- package/assets/packages/cli/src/internal/agents.ts +365 -0
- package/assets/packages/cli/src/internal/artifacts.ts +26 -0
- package/assets/packages/cli/src/internal/commands.ts +180 -0
- package/assets/packages/cli/src/internal/documentation-bundle.ts +49 -0
- package/assets/packages/cli/src/internal/engine.ts +501 -0
- package/assets/packages/cli/src/internal/errors.ts +39 -0
- package/assets/packages/cli/src/internal/file-names.ts +3 -0
- package/assets/packages/cli/src/internal/launch-request.ts +94 -0
- package/assets/packages/cli/src/internal/logs.ts +41 -0
- package/assets/packages/cli/src/internal/metrics.ts +356 -0
- package/assets/packages/cli/src/internal/pi-assets.ts +95 -0
- package/assets/packages/cli/src/internal/resource-bindings.ts +35 -0
- package/assets/packages/cli/src/internal/run-lease.ts +158 -0
- package/assets/packages/cli/src/internal/run-log.ts +59 -0
- package/assets/packages/cli/src/internal/run-names.ts +36 -0
- package/assets/packages/cli/src/internal/run-resources.ts +23 -0
- package/assets/packages/cli/src/internal/run-state.ts +380 -0
- package/assets/packages/cli/src/internal/run-store.ts +323 -0
- package/assets/packages/cli/src/internal/run.ts +133 -0
- package/assets/packages/cli/src/internal/state-store.ts +75 -0
- package/assets/packages/cli/src/internal/usage.ts +70 -0
- package/assets/packages/cli/src/internal/workflow-registry.ts +176 -0
- package/assets/packages/cli/src/plugin-loader.ts +412 -0
- package/assets/packages/cli/src/resources.ts +67 -0
- package/assets/packages/core/src/agent-protocol.ts +1 -0
- package/assets/packages/core/src/atomic-files.ts +24 -0
- package/assets/packages/core/src/errors.ts +3 -0
- package/assets/packages/sdk/src/agent-resource-adapter.ts +11 -0
- package/assets/packages/sdk/src/api.ts +821 -0
- package/assets/packages/sdk/src/files.ts +136 -0
- package/assets/packages/sdk/src/index.ts +6 -0
- package/assets/packages/sdk/src/resources.ts +20 -0
- package/assets/packages/sdk/src/schema.ts +48 -0
- package/assets/packages/sdk/src/seer/config.ts +62 -0
- package/assets/packages/sdk/src/seer/index.ts +7 -0
- package/assets/packages/sdk/src/state-adapter.ts +75 -0
- package/assets/setup/providers.md +128 -0
- package/assets/setup/releases.md +76 -0
- package/assets/tests/workflow-ref.test.ts +113 -0
- package/bin/norn.mjs +10 -0
- package/dist/build-info.d.ts +30 -0
- package/dist/build-info.js +6 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1032 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +118 -0
- package/dist/documentation-intro.d.ts +5 -0
- package/dist/documentation-intro.js +29 -0
- package/dist/documentation.d.ts +33 -0
- package/dist/documentation.js +132 -0
- package/dist/generated-build-info.d.ts +10 -0
- package/dist/generated-build-info.js +14 -0
- package/dist/internal/agent-directory.d.ts +4 -0
- package/dist/internal/agent-directory.js +8 -0
- package/dist/internal/agent-response-tool.d.ts +21 -0
- package/dist/internal/agent-response-tool.js +79 -0
- package/dist/internal/agents.d.ts +29 -0
- package/dist/internal/agents.js +336 -0
- package/dist/internal/artifacts.d.ts +10 -0
- package/dist/internal/artifacts.js +29 -0
- package/dist/internal/commands.d.ts +18 -0
- package/dist/internal/commands.js +147 -0
- package/dist/internal/documentation-bundle.d.ts +16 -0
- package/dist/internal/documentation-bundle.js +42 -0
- package/dist/internal/engine.d.ts +44 -0
- package/dist/internal/engine.js +399 -0
- package/dist/internal/errors.d.ts +14 -0
- package/dist/internal/errors.js +38 -0
- package/dist/internal/file-names.d.ts +1 -0
- package/dist/internal/file-names.js +7 -0
- package/dist/internal/launch-request.d.ts +33 -0
- package/dist/internal/launch-request.js +110 -0
- package/dist/internal/logs.d.ts +16 -0
- package/dist/internal/logs.js +38 -0
- package/dist/internal/metrics.d.ts +19 -0
- package/dist/internal/metrics.js +282 -0
- package/dist/internal/pi-assets.d.ts +13 -0
- package/dist/internal/pi-assets.js +94 -0
- package/dist/internal/resource-bindings.d.ts +13 -0
- package/dist/internal/resource-bindings.js +34 -0
- package/dist/internal/run-lease.d.ts +32 -0
- package/dist/internal/run-lease.js +166 -0
- package/dist/internal/run-log.d.ts +30 -0
- package/dist/internal/run-log.js +71 -0
- package/dist/internal/run-names.d.ts +1 -0
- package/dist/internal/run-names.js +144 -0
- package/dist/internal/run-resources.d.ts +6 -0
- package/dist/internal/run-resources.js +26 -0
- package/dist/internal/run-state.d.ts +95 -0
- package/dist/internal/run-state.js +323 -0
- package/dist/internal/run-store.d.ts +35 -0
- package/dist/internal/run-store.js +314 -0
- package/dist/internal/run.d.ts +51 -0
- package/dist/internal/run.js +101 -0
- package/dist/internal/state-store.d.ts +22 -0
- package/dist/internal/state-store.js +97 -0
- package/dist/internal/usage.d.ts +5 -0
- package/dist/internal/usage.js +70 -0
- package/dist/internal/workflow-registry.d.ts +35 -0
- package/dist/internal/workflow-registry.js +129 -0
- package/dist/plugin-loader.d.ts +55 -0
- package/dist/plugin-loader.js +353 -0
- package/dist/resources.d.ts +11 -0
- package/dist/resources.js +98 -0
- package/package.json +52 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// src/internal/engine.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { initializeRunResources } from "./run-resources.js";
|
|
6
|
+
import { createRunFileCoordinator } from "@vimhead.dev/norn/files";
|
|
7
|
+
import { NornAgentResponseCollector } from "./agent-response-tool.js";
|
|
8
|
+
import { NornArtifacts } from "./artifacts.js";
|
|
9
|
+
import { errorMessage, NornRunStoppedError } from "./errors.js";
|
|
10
|
+
import { NornRunLogs } from "./logs.js";
|
|
11
|
+
import { clearRunResumeRequest, matchesRunResumeRequest, readOptionalRunResumeRequest, RESUME_START_GRACE_MS } from "./launch-request.js";
|
|
12
|
+
import { NornRunLease } from "./run-lease.js";
|
|
13
|
+
import { NornRunLogger } from "./run-log.js";
|
|
14
|
+
import { NornRunStore, runCurrentRoot } from "./run-store.js";
|
|
15
|
+
import { NornRunStateStore, getRunInfo, mergeInterruptedWorkflowParams, resolveRunRoot } from "./run-state.js";
|
|
16
|
+
import { NornRunContext } from "./run.js";
|
|
17
|
+
import { NornMemoryWorkflowState } from "./state-store.js";
|
|
18
|
+
import { NornWorkflowRegistry } from "./workflow-registry.js";
|
|
19
|
+
var RUNS_DIR_NAME = ".norn";
|
|
20
|
+
var MANIFEST_FILE_NAME = "manifest.json";
|
|
21
|
+
var NornEngine = class {
|
|
22
|
+
constructor(input) {
|
|
23
|
+
this.input = input;
|
|
24
|
+
this.responseCollector = input.responseCollector ?? new NornAgentResponseCollector();
|
|
25
|
+
}
|
|
26
|
+
input;
|
|
27
|
+
registry = new NornWorkflowRegistry();
|
|
28
|
+
registrarState = new NornMemoryWorkflowState();
|
|
29
|
+
disposersByPlugin = /* @__PURE__ */ new Map();
|
|
30
|
+
responseCollector;
|
|
31
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
32
|
+
registerPlugin(plugin) {
|
|
33
|
+
this.disposePlugin(plugin.manifest.id);
|
|
34
|
+
const implementation = this.resolvePluginImplementation(plugin.implementation);
|
|
35
|
+
const disposers = Object.entries(plugin.manifest.workflows).map(([key, workflow]) => {
|
|
36
|
+
const workflowImplementation = implementation.workflows[key];
|
|
37
|
+
if (!workflowImplementation) throw new Error(`Missing implementation for workflow ${plugin.manifest.id}.${key}`);
|
|
38
|
+
return this.registry.register(workflow, workflowImplementation, { plugin: { id: plugin.manifest.id }, configSchema: plugin.manifest.config, config: this.input.config?.[plugin.manifest.id] });
|
|
39
|
+
});
|
|
40
|
+
this.disposersByPlugin.set(plugin.manifest.id, disposers);
|
|
41
|
+
return () => {
|
|
42
|
+
if (this.disposersByPlugin.get(plugin.manifest.id) === disposers) this.disposePlugin(plugin.manifest.id);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
listWorkflows() {
|
|
46
|
+
return this.registry.list();
|
|
47
|
+
}
|
|
48
|
+
visibleWorkflowEntries() {
|
|
49
|
+
return this.registry.launchableEntries();
|
|
50
|
+
}
|
|
51
|
+
resolvePluginImplementation(implementationInput) {
|
|
52
|
+
return typeof implementationInput === "function" ? implementationInput({ cwd: this.input.cwd, state: this.registrarState }) : implementationInput;
|
|
53
|
+
}
|
|
54
|
+
async runWorkflow(workflow, params, options) {
|
|
55
|
+
const session = await this.createRunSession(workflow, params, options);
|
|
56
|
+
const current = session.state.currentState().current;
|
|
57
|
+
if (!current) throw new Error("New run is missing its initial workflow step");
|
|
58
|
+
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
59
|
+
}
|
|
60
|
+
async listRunCheckpoints(path) {
|
|
61
|
+
const runRoot = await resolveRunRoot(this.input.cwd, path);
|
|
62
|
+
return (await NornRunStore.open(runRoot)).listCheckpoints();
|
|
63
|
+
}
|
|
64
|
+
async rollbackRun(path, checkpointId) {
|
|
65
|
+
const runRoot = await resolveRunRoot(this.input.cwd, path);
|
|
66
|
+
const lease = await NornRunLease.acquire(runRoot);
|
|
67
|
+
try {
|
|
68
|
+
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
69
|
+
if (pendingRequest && Date.now() - Date.parse(pendingRequest.createdAt) < RESUME_START_GRACE_MS) throw new Error(`Resume request is still pending: ${runRoot}`);
|
|
70
|
+
const runStore = await NornRunStore.open(runRoot);
|
|
71
|
+
await runStore.restoreSnapshot(checkpointId, async (stagedRunRoot) => {
|
|
72
|
+
const state = await NornRunStateStore.load(stagedRunRoot);
|
|
73
|
+
await state.prepareForResumeAfterRollback();
|
|
74
|
+
});
|
|
75
|
+
if (pendingRequest) await clearRunResumeRequest({ runRoot, request: pendingRequest, lease });
|
|
76
|
+
return getRunInfo(runRoot);
|
|
77
|
+
} finally {
|
|
78
|
+
await lease.release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async resumeWorkflow(path, params) {
|
|
82
|
+
return this.resumeWorkflowExecution({ path, params, expectedRequest: void 0 });
|
|
83
|
+
}
|
|
84
|
+
async resumeRequestedWorkflow(input) {
|
|
85
|
+
return this.resumeWorkflowExecution({ path: input.runRoot, params: input.request.params, expectedRequest: input.request });
|
|
86
|
+
}
|
|
87
|
+
async resumeWorkflowExecution(input) {
|
|
88
|
+
const { params, expectedRequest } = input;
|
|
89
|
+
const runRoot = await resolveRunRoot(this.input.cwd, input.path);
|
|
90
|
+
const initialStateStore = await NornRunStateStore.load(runRoot);
|
|
91
|
+
const initialState = initialStateStore.currentState();
|
|
92
|
+
if (initialState.status === "interrupted" && params === void 0) throw new Error(`Interrupted workflow resume requires params: ${runRoot}`);
|
|
93
|
+
if (initialState.status === "pendingResume" && params !== void 0) throw new Error(`Pending-resume workflows do not accept params: ${runRoot}`);
|
|
94
|
+
if (initialState.status !== "interrupted" && initialState.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
95
|
+
const lease = await NornRunLease.acquire(runRoot);
|
|
96
|
+
let isLeaseOwnedByScheduler = false;
|
|
97
|
+
let session;
|
|
98
|
+
try {
|
|
99
|
+
const pendingRequest = await readOptionalRunResumeRequest(runRoot);
|
|
100
|
+
if (expectedRequest ? !matchesRunResumeRequest(pendingRequest, expectedRequest) : pendingRequest !== void 0) throw new Error(`Resume request changed or belongs to another executor: ${runRoot}`);
|
|
101
|
+
session = await this.openRunSession(runRoot, lease);
|
|
102
|
+
const state = session.state.currentState();
|
|
103
|
+
const current = state.current;
|
|
104
|
+
if (!current) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
105
|
+
const workflow = this.registry.workflowById(current.workflowId);
|
|
106
|
+
if (!workflow) throw new Error(`Unknown workflow for resumed run: ${current.workflowId}`);
|
|
107
|
+
if (state.status === "interrupted") {
|
|
108
|
+
const parsedParams = workflow.params.parse(mergeInterruptedWorkflowParams(current.params, params, current.interruption?.fields));
|
|
109
|
+
await session.state.replaceCurrentParams(parsedParams);
|
|
110
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
|
|
111
|
+
const resumedCurrent = session.state.currentState().current;
|
|
112
|
+
if (!resumedCurrent) throw new Error(`Run is not resumable: ${runRoot}`);
|
|
113
|
+
isLeaseOwnedByScheduler = true;
|
|
114
|
+
return this.runScheduler(session, toWorkflowStep(workflow, resumedCurrent));
|
|
115
|
+
}
|
|
116
|
+
if (state.status !== "pendingResume") throw new Error(`Run must be rolled back before resuming: ${runRoot}`);
|
|
117
|
+
await recordRunEvent(session, { type: "run.resumed", workflowId: workflow.id, cwd: session.run.cwd });
|
|
118
|
+
isLeaseOwnedByScheduler = true;
|
|
119
|
+
return this.runScheduler(session, toWorkflowStep(workflow, current));
|
|
120
|
+
} finally {
|
|
121
|
+
if (!isLeaseOwnedByScheduler) {
|
|
122
|
+
try {
|
|
123
|
+
await lease.release();
|
|
124
|
+
} finally {
|
|
125
|
+
if (session) this.failActiveRun(runRoot, session.activeRun);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async runScheduler(session, initialStep) {
|
|
131
|
+
let currentStep = initialStep;
|
|
132
|
+
try {
|
|
133
|
+
for (let step = 1; step <= 1e3; step++) {
|
|
134
|
+
throwIfRunAborted(session.activeRun);
|
|
135
|
+
const stepRuntime = session.run.forWorkflow(currentStep.workflow);
|
|
136
|
+
if (shouldPauseForGate(currentStep)) {
|
|
137
|
+
const parsedParams = currentStep.workflow.params.parse(currentStep.params);
|
|
138
|
+
const description = await this.registry.describeGate(currentStep.workflow, stepRuntime, parsedParams, session.state.currentState().configOverride);
|
|
139
|
+
const interruption = { description, fields: currentStep.workflow.gate?.fields };
|
|
140
|
+
await session.state.interruptCurrent(parsedParams, interruption);
|
|
141
|
+
await recordRunEvent(session, { type: "run.interrupted", workflowId: currentStep.workflow.id });
|
|
142
|
+
await commitRunBoundary(session, `run interrupted: ${currentStep.workflow.id}`);
|
|
143
|
+
return interruptedLaunchResult({ id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd }, currentStep.workflow, parsedParams, interruption);
|
|
144
|
+
}
|
|
145
|
+
await session.state.startStep(toRunStateStep(currentStep));
|
|
146
|
+
const stepResult = await this.executeWorkflowStep(session, currentStep, stepRuntime);
|
|
147
|
+
throwIfRunAborted(session.activeRun);
|
|
148
|
+
if (stepResult.type === "complete") {
|
|
149
|
+
await session.state.completeRun(stepResult.workflow.id, stepResult.metadata);
|
|
150
|
+
await recordRunEvent(session, { type: "run.completed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
151
|
+
await commitRunBoundary(session, `run completed: ${stepResult.workflow.id}`);
|
|
152
|
+
return { status: "completed", id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
153
|
+
}
|
|
154
|
+
if (stepResult.type === "fail") {
|
|
155
|
+
await session.state.failRun(stepResult.workflow.id, stepResult.metadata);
|
|
156
|
+
await recordRunEvent(session, { type: "run.failed", workflowId: stepResult.workflow.id, metadata: stepResult.metadata });
|
|
157
|
+
return { status: "failed", id: stepRuntime.id, name: session.state.currentState().name, workspace: stepRuntime.workspace, cwd: stepRuntime.cwd, workflowId: stepResult.workflow.id, metadata: stepResult.metadata };
|
|
158
|
+
}
|
|
159
|
+
const nextStep = this.nextWorkflowStep(stepResult);
|
|
160
|
+
await session.state.completeWithNext(currentStep.workflow.id, toRunStateStep(nextStep));
|
|
161
|
+
await recordRunEvent(session, { type: "run.transitioned", fromWorkflowId: currentStep.workflow.id, toWorkflowId: nextStep.workflow.id });
|
|
162
|
+
await commitRunBoundary(session, `transition: ${currentStep.workflow.id} -> ${nextStep.workflow.id}`);
|
|
163
|
+
currentStep = nextStep;
|
|
164
|
+
}
|
|
165
|
+
throw new Error("Run exceeded 1000 scheduler steps");
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (isRunStopped(session, error)) {
|
|
168
|
+
await session.state.stopCurrent();
|
|
169
|
+
await recordRunEvent(session, { type: "run.stopped", workflowId: currentStep.workflow.id });
|
|
170
|
+
return { status: "stopped", id: session.run.id, name: session.state.currentState().name, workspace: session.run.workspace, cwd: session.run.cwd, workflowId: currentStep.workflow.id };
|
|
171
|
+
}
|
|
172
|
+
await session.state.failCurrent(errorMessage(error));
|
|
173
|
+
await recordRunEvent(session, { type: "run.failed", workflowId: currentStep.workflow.id, error: errorMessage(error) });
|
|
174
|
+
throw error;
|
|
175
|
+
} finally {
|
|
176
|
+
await session.lease.release();
|
|
177
|
+
this.finishActiveRun(session.runRoot, session.activeRun);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async executeWorkflowStep(session, step, run) {
|
|
181
|
+
const startedAtMs = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
await assertWorkspaceBoundary(session, run.workspace);
|
|
184
|
+
await recordRunEvent(session, { type: "workflow.started", workflowId: step.workflow.id });
|
|
185
|
+
const result = await this.registry.execute(step.workflow, run, step.params, session.state.currentState().configOverride);
|
|
186
|
+
await assertWorkspaceBoundary(session, run.workspace);
|
|
187
|
+
const durationMs = Date.now() - startedAtMs;
|
|
188
|
+
if (result.type === "complete") await recordRunEvent(session, { type: "workflow.completed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
189
|
+
else if (result.type === "fail") await recordRunEvent(session, { type: "workflow.failed", workflowId: result.workflow.id, durationMs, metadata: result.metadata });
|
|
190
|
+
else await recordRunEvent(session, { type: "workflow.transitioned", fromWorkflowId: step.workflow.id, toWorkflowId: result.workflowId, durationMs });
|
|
191
|
+
return result;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
await recordRunEvent(session, {
|
|
194
|
+
type: isRunStopped(session, error) ? "workflow.stopped" : "workflow.failed",
|
|
195
|
+
workflowId: step.workflow.id,
|
|
196
|
+
durationMs: Date.now() - startedAtMs,
|
|
197
|
+
...isRunStopped(session, error) ? {} : { error: errorMessage(error) }
|
|
198
|
+
});
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
nextWorkflowStep(next) {
|
|
203
|
+
const workflow = this.registry.workflowById(next.workflowId);
|
|
204
|
+
if (!workflow) throw new Error(`Unknown next workflow: ${next.workflowId}`);
|
|
205
|
+
return {
|
|
206
|
+
workflow,
|
|
207
|
+
params: next.params,
|
|
208
|
+
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async createRunSession(workflow, params, options) {
|
|
212
|
+
const id = options?.id ?? randomUUID();
|
|
213
|
+
const name = options?.name ?? id;
|
|
214
|
+
const runRoot = defaultRunRoot(this.input.cwd, id);
|
|
215
|
+
const currentRoot = runCurrentRoot(runRoot);
|
|
216
|
+
const workspace = join(currentRoot, "workspace");
|
|
217
|
+
const cwd = workflowDefaultCwd(this.input.cwd, workspace, workflow);
|
|
218
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
|
+
await mkdir(runRoot, { recursive: true });
|
|
220
|
+
const lease = await NornRunLease.acquire(runRoot);
|
|
221
|
+
const activeRun = this.startActiveRun(runRoot);
|
|
222
|
+
try {
|
|
223
|
+
const runStore = await NornRunStore.initialize(runRoot);
|
|
224
|
+
await mkdir(workspace, { recursive: true });
|
|
225
|
+
const logger = new NornRunLogger({ manifestPath: join(currentRoot, MANIFEST_FILE_NAME), files: createRunFileCoordinator(runRoot), manifest: {
|
|
226
|
+
id,
|
|
227
|
+
name,
|
|
228
|
+
workflowId: workflow.id,
|
|
229
|
+
runRoot,
|
|
230
|
+
workspace,
|
|
231
|
+
initialCwd: cwd,
|
|
232
|
+
startedAt
|
|
233
|
+
} });
|
|
234
|
+
const run = await this.buildRun({ id, currentRoot, workspace, cwd, isolationMode: workflow.isolation.mode, signal: activeRun.controller.signal, logger });
|
|
235
|
+
const state = await NornRunStateStore.create(runRoot, {
|
|
236
|
+
id,
|
|
237
|
+
name,
|
|
238
|
+
entrypointWorkflowId: workflow.id,
|
|
239
|
+
workspace,
|
|
240
|
+
configOverride: options?.configOverride,
|
|
241
|
+
current: {
|
|
242
|
+
workflowId: workflow.id,
|
|
243
|
+
params,
|
|
244
|
+
cwd,
|
|
245
|
+
env: {},
|
|
246
|
+
interruption: this.input.gateMode === "pause" && workflow.gate ? { status: "pending" } : void 0
|
|
247
|
+
},
|
|
248
|
+
startedAt
|
|
249
|
+
});
|
|
250
|
+
const session = { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
251
|
+
await logger.record({ type: "run.started", workflowId: workflow.id, cwd, workspace });
|
|
252
|
+
await commitRunBoundary(session, `run started: ${workflow.id}`);
|
|
253
|
+
return session;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
await lease.release();
|
|
256
|
+
this.failActiveRun(runRoot, activeRun);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async openRunSession(runRoot, lease) {
|
|
261
|
+
const activeRun = this.startActiveRun(runRoot);
|
|
262
|
+
try {
|
|
263
|
+
const state = await NornRunStateStore.load(runRoot);
|
|
264
|
+
const currentState = state.currentState();
|
|
265
|
+
if (currentState.status === "completed") throw new Error(`Run is already completed: ${runRoot}`);
|
|
266
|
+
const runStore = await NornRunStore.open(runRoot);
|
|
267
|
+
const currentRoot = runCurrentRoot(runRoot);
|
|
268
|
+
const logger = await NornRunLogger.load(join(currentRoot, MANIFEST_FILE_NAME), createRunFileCoordinator(runRoot));
|
|
269
|
+
const workflow = currentState.current ? this.registry.workflowById(currentState.current.workflowId) : void 0;
|
|
270
|
+
const isolationMode = workflow?.isolation.mode ?? "runWorkspace";
|
|
271
|
+
const cwd = workflowDefaultCwd(this.input.cwd, currentState.workspace, workflow);
|
|
272
|
+
const run = await this.buildRun({ id: currentState.id, currentRoot, workspace: currentState.workspace, cwd, isolationMode, signal: activeRun.controller.signal, logger });
|
|
273
|
+
return { runRoot, run, state, lease, runStore, logger, activeRun };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.failActiveRun(runRoot, activeRun);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async buildRun(input) {
|
|
280
|
+
const artifactsRoot = join(input.currentRoot, "artifacts");
|
|
281
|
+
const logsRoot = join(input.currentRoot, "logs");
|
|
282
|
+
await mkdir(input.workspace, { recursive: true });
|
|
283
|
+
await mkdir(artifactsRoot, { recursive: true });
|
|
284
|
+
await mkdir(logsRoot, { recursive: true });
|
|
285
|
+
const { resources, state } = await initializeRunResources(dirname(input.currentRoot));
|
|
286
|
+
return new NornRunContext({
|
|
287
|
+
id: input.id,
|
|
288
|
+
runRoot: input.currentRoot,
|
|
289
|
+
workspace: input.workspace,
|
|
290
|
+
projectRoot: this.input.cwd,
|
|
291
|
+
cwd: input.cwd,
|
|
292
|
+
isolationMode: input.isolationMode,
|
|
293
|
+
signal: input.signal,
|
|
294
|
+
agentDir: this.input.agentDir,
|
|
295
|
+
responseCollector: this.responseCollector,
|
|
296
|
+
resources,
|
|
297
|
+
state,
|
|
298
|
+
logger: input.logger,
|
|
299
|
+
artifacts: new NornArtifacts(artifactsRoot, resources.files),
|
|
300
|
+
logs: new NornRunLogs(logsRoot, resources.files)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
startActiveRun(runRoot) {
|
|
304
|
+
if (this.activeRuns.has(runRoot)) throw new Error(`Run is already active in this engine: ${runRoot}`);
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
let finish;
|
|
307
|
+
const finished = new Promise((resolvePromise) => {
|
|
308
|
+
finish = resolvePromise;
|
|
309
|
+
});
|
|
310
|
+
const abortFromParent = () => controller.abort(this.input.signal?.reason);
|
|
311
|
+
if (this.input.signal?.aborted) abortFromParent();
|
|
312
|
+
else this.input.signal?.addEventListener("abort", abortFromParent, { once: true });
|
|
313
|
+
const activeRun = {
|
|
314
|
+
controller,
|
|
315
|
+
finished,
|
|
316
|
+
finish: () => finish(),
|
|
317
|
+
dispose: () => this.input.signal?.removeEventListener("abort", abortFromParent)
|
|
318
|
+
};
|
|
319
|
+
this.activeRuns.set(runRoot, activeRun);
|
|
320
|
+
return activeRun;
|
|
321
|
+
}
|
|
322
|
+
finishActiveRun(runRoot, activeRun) {
|
|
323
|
+
if (this.activeRuns.get(runRoot) !== activeRun) return;
|
|
324
|
+
this.activeRuns.delete(runRoot);
|
|
325
|
+
activeRun.dispose();
|
|
326
|
+
activeRun.finish();
|
|
327
|
+
}
|
|
328
|
+
failActiveRun(runRoot, activeRun) {
|
|
329
|
+
if (this.activeRuns.get(runRoot) === activeRun) this.activeRuns.delete(runRoot);
|
|
330
|
+
activeRun.dispose();
|
|
331
|
+
activeRun.finish();
|
|
332
|
+
}
|
|
333
|
+
disposePlugin(pluginId) {
|
|
334
|
+
const disposers = this.disposersByPlugin.get(pluginId) ?? [];
|
|
335
|
+
for (const dispose of [...disposers].reverse()) dispose();
|
|
336
|
+
this.disposersByPlugin.delete(pluginId);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function defaultRunRoot(cwd, id) {
|
|
340
|
+
return join(cwd, RUNS_DIR_NAME, "runs", id);
|
|
341
|
+
}
|
|
342
|
+
function workflowDefaultCwd(projectRoot, workspace, workflow) {
|
|
343
|
+
return workflow?.isolation.mode === "project" ? projectRoot : workspace;
|
|
344
|
+
}
|
|
345
|
+
function toWorkflowStep(workflow, step) {
|
|
346
|
+
return {
|
|
347
|
+
workflow,
|
|
348
|
+
params: step.params,
|
|
349
|
+
interruption: step.interruption
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function toRunStateStep(step) {
|
|
353
|
+
return {
|
|
354
|
+
workflowId: step.workflow.id,
|
|
355
|
+
params: step.params,
|
|
356
|
+
cwd: "",
|
|
357
|
+
env: {},
|
|
358
|
+
interruption: step.interruption
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function shouldPauseForGate(step) {
|
|
362
|
+
return step.interruption?.status === "pending" && step.workflow.gate !== void 0;
|
|
363
|
+
}
|
|
364
|
+
function throwIfRunAborted(activeRun) {
|
|
365
|
+
if (!activeRun.controller.signal.aborted) return;
|
|
366
|
+
const reason = activeRun.controller.signal.reason;
|
|
367
|
+
if (reason instanceof Error) throw reason;
|
|
368
|
+
throw new Error(typeof reason === "string" && reason.length > 0 ? reason : "Run aborted");
|
|
369
|
+
}
|
|
370
|
+
function isRunStopped(session, error) {
|
|
371
|
+
return error instanceof NornRunStoppedError || session.activeRun.controller.signal.reason instanceof NornRunStoppedError;
|
|
372
|
+
}
|
|
373
|
+
function interruptedLaunchResult(run, workflow, params, interruption) {
|
|
374
|
+
if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
|
|
375
|
+
if (!interruption.description) throw new Error(`Interrupted workflow is missing description: ${workflow.id}`);
|
|
376
|
+
return {
|
|
377
|
+
status: "interrupted",
|
|
378
|
+
id: run.id,
|
|
379
|
+
name: run.name,
|
|
380
|
+
workspace: run.workspace,
|
|
381
|
+
cwd: run.cwd,
|
|
382
|
+
workflowId: workflow.id,
|
|
383
|
+
interruption: { workflowId: workflow.id, params, description: interruption.description, fields: interruption.fields }
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function recordRunEvent(session, event) {
|
|
387
|
+
await session.logger.record(event);
|
|
388
|
+
}
|
|
389
|
+
async function commitRunBoundary(session, message) {
|
|
390
|
+
await session.lease.assertOwned();
|
|
391
|
+
await session.runStore.snapshotCurrent(message);
|
|
392
|
+
}
|
|
393
|
+
async function assertWorkspaceBoundary(session, workspace) {
|
|
394
|
+
await session.lease.assertOwned();
|
|
395
|
+
await session.runStore.assertWorkspaceCanBeSnapshotted(workspace);
|
|
396
|
+
}
|
|
397
|
+
export {
|
|
398
|
+
NornEngine
|
|
399
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NornPluginDiagnostic } from "@vimhead.dev/norn";
|
|
2
|
+
export declare class NornProjectLoadError extends Error {
|
|
3
|
+
readonly code = "NORN_PROJECT_INVALID";
|
|
4
|
+
readonly isComplete = false;
|
|
5
|
+
readonly diagnostics: readonly NornPluginDiagnostic[];
|
|
6
|
+
constructor(input: {
|
|
7
|
+
readonly diagnostics: readonly NornPluginDiagnostic[];
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export declare class NornRunStoppedError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
export declare function zodErrorMessage(error: unknown): string;
|
|
14
|
+
export declare function errorMessage(error: unknown): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/internal/errors.ts
|
|
2
|
+
var NornProjectLoadError = class extends Error {
|
|
3
|
+
code = "NORN_PROJECT_INVALID";
|
|
4
|
+
isComplete = false;
|
|
5
|
+
diagnostics;
|
|
6
|
+
constructor(input) {
|
|
7
|
+
const details = input.diagnostics.map(
|
|
8
|
+
(diagnostic) => `${diagnostic.stage}: ${diagnostic.pluginPath} (declared in ${diagnostic.configPath}${diagnostic.workflowId ? `; workflow ${diagnostic.workflowId}` : ""}): ${diagnostic.message}`
|
|
9
|
+
);
|
|
10
|
+
super(`Norn project is incomplete (${input.diagnostics.length} diagnostics); execution is blocked.
|
|
11
|
+
${details.join("\n")}`);
|
|
12
|
+
this.name = "NornProjectLoadError";
|
|
13
|
+
this.diagnostics = input.diagnostics;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var NornRunStoppedError = class extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("Stopped by user");
|
|
19
|
+
this.name = "NornRunStoppedError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function zodErrorMessage(error) {
|
|
23
|
+
const issues = error.issues;
|
|
24
|
+
if (!Array.isArray(issues) || issues.length === 0) return errorMessage(error);
|
|
25
|
+
return issues.slice(0, 3).map((issue) => {
|
|
26
|
+
const path = Array.isArray(issue.path) && issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
27
|
+
return `${path}${issue.message ?? "Invalid workflow response"}`;
|
|
28
|
+
}).join("; ");
|
|
29
|
+
}
|
|
30
|
+
function errorMessage(error) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
NornProjectLoadError,
|
|
35
|
+
NornRunStoppedError,
|
|
36
|
+
errorMessage,
|
|
37
|
+
zodErrorMessage
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function safeFileName(value: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NornRunLease } from "./run-lease.ts";
|
|
2
|
+
export declare const LAUNCH_REQUEST_FILE_NAME = "launch-request.json";
|
|
3
|
+
export declare const RESUME_REQUEST_FILE_NAME = "resume-request.json";
|
|
4
|
+
export declare const RESUME_START_GRACE_MS = 60000;
|
|
5
|
+
export type NornRunLaunchRequest = {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly type: "run";
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly workflowId: string;
|
|
11
|
+
readonly params: unknown;
|
|
12
|
+
readonly configOverride?: unknown;
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
};
|
|
15
|
+
export type NornRunResumeRequest = {
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly type: "resume";
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly requestId?: string;
|
|
20
|
+
readonly params?: unknown;
|
|
21
|
+
readonly createdAt: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function writeRunLaunchRequest(runRoot: string, request: NornRunLaunchRequest): Promise<void>;
|
|
24
|
+
export declare function readRunLaunchRequest(runRoot: string): Promise<NornRunLaunchRequest>;
|
|
25
|
+
export declare function writeRunResumeRequest(runRoot: string, request: NornRunResumeRequest): Promise<void>;
|
|
26
|
+
export declare function readRunResumeRequest(runRoot: string): Promise<NornRunResumeRequest>;
|
|
27
|
+
export declare function readOptionalRunResumeRequest(runRoot: string): Promise<NornRunResumeRequest | undefined>;
|
|
28
|
+
export declare function matchesRunResumeRequest(actual: NornRunResumeRequest | undefined, expected: NornRunResumeRequest): boolean;
|
|
29
|
+
export declare function clearRunResumeRequest(input: {
|
|
30
|
+
readonly runRoot: string;
|
|
31
|
+
readonly request: NornRunResumeRequest;
|
|
32
|
+
readonly lease: NornRunLease;
|
|
33
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/internal/launch-request.ts
|
|
2
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3
|
+
import { link, readFile, rm as rm2 } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// ../core/src/atomic-files.ts
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
// ../core/src/errors.ts
|
|
12
|
+
function isNodeError(error) {
|
|
13
|
+
return error instanceof Error && "code" in error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../core/src/atomic-files.ts
|
|
17
|
+
async function writeJsonAtomically(path, value) {
|
|
18
|
+
await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
async function writeTextAtomically(path, content) {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
let existingMode;
|
|
24
|
+
try {
|
|
25
|
+
existingMode = (await stat(path)).mode & 511;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
28
|
+
}
|
|
29
|
+
const tmpPath = `${path}.${randomUUID()}.tmp`;
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(tmpPath, content, { encoding: "utf8", mode: existingMode });
|
|
32
|
+
if (existingMode !== void 0) await chmod(tmpPath, existingMode);
|
|
33
|
+
await rename(tmpPath, path);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await rm(tmpPath, { force: true }).catch(() => void 0);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/internal/launch-request.ts
|
|
41
|
+
var LAUNCH_REQUEST_FILE_NAME = "launch-request.json";
|
|
42
|
+
var RESUME_REQUEST_FILE_NAME = "resume-request.json";
|
|
43
|
+
var RESUME_START_GRACE_MS = 6e4;
|
|
44
|
+
async function writeRunLaunchRequest(runRoot, request) {
|
|
45
|
+
await writeJsonAtomically(join(runRoot, LAUNCH_REQUEST_FILE_NAME), request);
|
|
46
|
+
}
|
|
47
|
+
async function readRunLaunchRequest(runRoot) {
|
|
48
|
+
return parseRunLaunchRequest(JSON.parse(await readFile(join(runRoot, LAUNCH_REQUEST_FILE_NAME), "utf8")));
|
|
49
|
+
}
|
|
50
|
+
async function writeRunResumeRequest(runRoot, request) {
|
|
51
|
+
const path = join(runRoot, RESUME_REQUEST_FILE_NAME);
|
|
52
|
+
const stagedPath = `${path}.${randomUUID2()}.tmp`;
|
|
53
|
+
try {
|
|
54
|
+
await writeJsonAtomically(stagedPath, request);
|
|
55
|
+
await link(stagedPath, path);
|
|
56
|
+
} finally {
|
|
57
|
+
await rm2(stagedPath, { force: true }).catch(() => void 0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function readRunResumeRequest(runRoot) {
|
|
61
|
+
return parseRunResumeRequest(JSON.parse(await readFile(join(runRoot, RESUME_REQUEST_FILE_NAME), "utf8")));
|
|
62
|
+
}
|
|
63
|
+
async function readOptionalRunResumeRequest(runRoot) {
|
|
64
|
+
try {
|
|
65
|
+
return await readRunResumeRequest(runRoot);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function matchesRunResumeRequest(actual, expected) {
|
|
72
|
+
return actual?.id === expected.id && (actual.requestId ?? actual.createdAt) === (expected.requestId ?? expected.createdAt);
|
|
73
|
+
}
|
|
74
|
+
async function clearRunResumeRequest(input) {
|
|
75
|
+
await input.lease.assertOwned();
|
|
76
|
+
if (matchesRunResumeRequest(await readOptionalRunResumeRequest(input.runRoot), input.request)) {
|
|
77
|
+
await rm2(join(input.runRoot, RESUME_REQUEST_FILE_NAME), { force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseRunLaunchRequest(value) {
|
|
81
|
+
if (!value || typeof value !== "object") throw new Error("Invalid workflow launch request");
|
|
82
|
+
const request = value;
|
|
83
|
+
if (request.version !== 1 || request.type !== "run") throw new Error("Unsupported workflow launch request");
|
|
84
|
+
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow launch request id");
|
|
85
|
+
if (typeof request.name !== "string" || request.name.length === 0) throw new Error("Invalid workflow launch request name");
|
|
86
|
+
if (typeof request.workflowId !== "string" || request.workflowId.length === 0) throw new Error("Invalid workflow launch request workflow id");
|
|
87
|
+
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow launch request timestamp");
|
|
88
|
+
return request;
|
|
89
|
+
}
|
|
90
|
+
function parseRunResumeRequest(value) {
|
|
91
|
+
if (!value || typeof value !== "object") throw new Error("Invalid workflow resume request");
|
|
92
|
+
const request = value;
|
|
93
|
+
if (request.version !== 1 || request.type !== "resume") throw new Error("Unsupported workflow resume request");
|
|
94
|
+
if (typeof request.id !== "string" || request.id.length === 0) throw new Error("Invalid workflow resume request id");
|
|
95
|
+
if (request.requestId !== void 0 && (typeof request.requestId !== "string" || request.requestId.length === 0)) throw new Error("Invalid workflow resume request token");
|
|
96
|
+
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt))) throw new Error("Invalid workflow resume request timestamp");
|
|
97
|
+
return request;
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
LAUNCH_REQUEST_FILE_NAME,
|
|
101
|
+
RESUME_REQUEST_FILE_NAME,
|
|
102
|
+
RESUME_START_GRACE_MS,
|
|
103
|
+
clearRunResumeRequest,
|
|
104
|
+
matchesRunResumeRequest,
|
|
105
|
+
readOptionalRunResumeRequest,
|
|
106
|
+
readRunLaunchRequest,
|
|
107
|
+
readRunResumeRequest,
|
|
108
|
+
writeRunLaunchRequest,
|
|
109
|
+
writeRunResumeRequest
|
|
110
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type WriteStream } from "node:fs";
|
|
2
|
+
import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
|
|
3
|
+
import type { NornLogRef } from "@vimhead.dev/norn";
|
|
4
|
+
export type NornLogWriteStream = {
|
|
5
|
+
readonly log: NornLogRef;
|
|
6
|
+
readonly stream: WriteStream;
|
|
7
|
+
};
|
|
8
|
+
export declare class NornRunLogs {
|
|
9
|
+
private readonly logsRoot;
|
|
10
|
+
private readonly files;
|
|
11
|
+
constructor(logsRoot: string, files: NornFileCoordinator);
|
|
12
|
+
write(path: string, content: string): Promise<void>;
|
|
13
|
+
read(log: NornLogRef): Promise<string>;
|
|
14
|
+
createWriteStream(log: NornLogRef): Promise<NornLogWriteStream>;
|
|
15
|
+
private resolveLogPath;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/internal/logs.ts
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
var NornRunLogs = class {
|
|
6
|
+
constructor(logsRoot, files) {
|
|
7
|
+
this.logsRoot = logsRoot;
|
|
8
|
+
this.files = files;
|
|
9
|
+
}
|
|
10
|
+
logsRoot;
|
|
11
|
+
files;
|
|
12
|
+
async write(path, content) {
|
|
13
|
+
await this.files.writeText(this.resolveLogPath(path), content);
|
|
14
|
+
}
|
|
15
|
+
async read(log) {
|
|
16
|
+
return this.files.readText(this.resolveLogPath(`${log.id}.log`));
|
|
17
|
+
}
|
|
18
|
+
async createWriteStream(log) {
|
|
19
|
+
const absolutePath = this.resolveLogPath(`${log.id}.log`);
|
|
20
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
21
|
+
return {
|
|
22
|
+
log,
|
|
23
|
+
stream: createWriteStream(absolutePath, { encoding: "utf8" })
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
resolveLogPath(path) {
|
|
27
|
+
if (isAbsolute(path)) throw new Error(`Log path must be relative: ${path}`);
|
|
28
|
+
const resolvedPath = resolve(this.logsRoot, path);
|
|
29
|
+
const relativePath = relative(this.logsRoot, resolvedPath);
|
|
30
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
31
|
+
throw new Error(`Log path escapes logs directory: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
return resolvedPath;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
NornRunLogs
|
|
38
|
+
};
|