@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.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 +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -30,22 +30,38 @@ async function writeTextAtomically(path, content) {
|
|
|
30
30
|
|
|
31
31
|
// src/internal/file-coordinator.ts
|
|
32
32
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
lstat,
|
|
35
|
+
mkdir as mkdir2,
|
|
36
|
+
mkdtemp,
|
|
37
|
+
readFile,
|
|
38
|
+
readdir,
|
|
39
|
+
realpath,
|
|
40
|
+
rename as rename2,
|
|
41
|
+
rm as rm2,
|
|
42
|
+
rmdir,
|
|
43
|
+
unlink,
|
|
44
|
+
writeFile as writeFile2
|
|
45
|
+
} from "node:fs/promises";
|
|
34
46
|
import { hostname } from "node:os";
|
|
35
47
|
import { basename, dirname as dirname2, join, resolve } from "node:path";
|
|
36
48
|
import { setTimeout as delay } from "node:timers/promises";
|
|
37
49
|
import { Type } from "typebox";
|
|
38
50
|
import { Value } from "typebox/value";
|
|
39
|
-
var ownerSchema = Type.Object(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
var ownerSchema = Type.Object(
|
|
52
|
+
{
|
|
53
|
+
token: Type.String({ format: "uuid" }),
|
|
54
|
+
pid: Type.Integer({ exclusiveMinimum: 0 }),
|
|
55
|
+
host: Type.String(),
|
|
56
|
+
target: Type.String()
|
|
57
|
+
},
|
|
58
|
+
{ additionalProperties: false }
|
|
59
|
+
);
|
|
45
60
|
var NornFileCoordinator = class {
|
|
46
61
|
constructor(input) {
|
|
47
62
|
this.input = input;
|
|
48
|
-
if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0)
|
|
63
|
+
if (!Number.isSafeInteger(input.waitTimeoutMs) || input.waitTimeoutMs <= 0)
|
|
64
|
+
throw new Error("Lock wait timeout must be a positive integer");
|
|
49
65
|
this.lockRoot = resolve(input.lockRoot);
|
|
50
66
|
}
|
|
51
67
|
input;
|
|
@@ -60,7 +76,11 @@ var NornFileCoordinator = class {
|
|
|
60
76
|
const marker = `${token}.json`;
|
|
61
77
|
return this.withCleanup({
|
|
62
78
|
operation: async () => {
|
|
63
|
-
await writeFile2(
|
|
79
|
+
await writeFile2(
|
|
80
|
+
join(stagingPath, marker),
|
|
81
|
+
JSON.stringify({ token, pid: process.pid, host: hostname(), target }),
|
|
82
|
+
{ mode: 384 }
|
|
83
|
+
);
|
|
64
84
|
await this.acquire({ stagingPath, lockPath, target });
|
|
65
85
|
return this.withCleanup({
|
|
66
86
|
operation: () => operation(target),
|
|
@@ -74,11 +94,17 @@ var NornFileCoordinator = class {
|
|
|
74
94
|
});
|
|
75
95
|
}
|
|
76
96
|
async readText(path) {
|
|
77
|
-
return this.withExclusiveLock(
|
|
97
|
+
return this.withExclusiveLock(
|
|
98
|
+
path,
|
|
99
|
+
(lockedPath) => readFile(lockedPath, "utf8")
|
|
100
|
+
);
|
|
78
101
|
}
|
|
79
102
|
async writeText(path, content) {
|
|
80
103
|
await mkdir2(dirname2(path), { recursive: true });
|
|
81
|
-
await this.withExclusiveLock(
|
|
104
|
+
await this.withExclusiveLock(
|
|
105
|
+
path,
|
|
106
|
+
(lockedPath) => writeTextAtomically(lockedPath, content)
|
|
107
|
+
);
|
|
82
108
|
}
|
|
83
109
|
async withCleanup(input) {
|
|
84
110
|
let result;
|
|
@@ -88,7 +114,10 @@ var NornFileCoordinator = class {
|
|
|
88
114
|
try {
|
|
89
115
|
await input.cleanup();
|
|
90
116
|
} catch (cleanupError) {
|
|
91
|
-
throw new AggregateError(
|
|
117
|
+
throw new AggregateError(
|
|
118
|
+
[error, cleanupError],
|
|
119
|
+
"File operation and lock cleanup failed"
|
|
120
|
+
);
|
|
92
121
|
}
|
|
93
122
|
throw error;
|
|
94
123
|
}
|
|
@@ -104,7 +133,8 @@ var NornFileCoordinator = class {
|
|
|
104
133
|
if (isNodeError(error2) && error2.code === "ENOENT") return void 0;
|
|
105
134
|
throw error2;
|
|
106
135
|
});
|
|
107
|
-
if (entry?.isSymbolicLink())
|
|
136
|
+
if (entry?.isSymbolicLink())
|
|
137
|
+
throw new Error(`Cannot lock a dangling symbolic link: ${path}`);
|
|
108
138
|
return join(await realpath(dirname2(path)), basename(path));
|
|
109
139
|
}
|
|
110
140
|
}
|
|
@@ -115,10 +145,12 @@ var NornFileCoordinator = class {
|
|
|
115
145
|
await rename2(input.stagingPath, input.lockPath);
|
|
116
146
|
return;
|
|
117
147
|
} catch (error) {
|
|
118
|
-
if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
148
|
+
if (!isNodeError(error) || !["ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
149
|
+
throw error;
|
|
119
150
|
}
|
|
120
151
|
await this.reclaimDeadOwner(input);
|
|
121
|
-
if (Date.now() >= deadline)
|
|
152
|
+
if (Date.now() >= deadline)
|
|
153
|
+
throw new Error(`Timed out waiting for file lock: ${input.target}`);
|
|
122
154
|
await delay(10);
|
|
123
155
|
}
|
|
124
156
|
}
|
|
@@ -126,9 +158,13 @@ var NornFileCoordinator = class {
|
|
|
126
158
|
try {
|
|
127
159
|
const entries = await readdir(input.lockPath);
|
|
128
160
|
if (entries.length === 0) return;
|
|
129
|
-
if (entries.length !== 1)
|
|
161
|
+
if (entries.length !== 1)
|
|
162
|
+
throw new Error(`Invalid file lock ownership: ${input.lockPath}`);
|
|
130
163
|
const marker = entries[0];
|
|
131
|
-
const owner = Value.Parse(
|
|
164
|
+
const owner = Value.Parse(
|
|
165
|
+
ownerSchema,
|
|
166
|
+
JSON.parse(await readFile(join(input.lockPath, marker), "utf8"))
|
|
167
|
+
);
|
|
132
168
|
if (marker !== `${owner.token}.json` || owner.target !== input.target || owner.host !== hostname()) {
|
|
133
169
|
throw new Error(`Incompatible file lock ownership: ${input.lockPath}`);
|
|
134
170
|
}
|
|
@@ -149,12 +185,16 @@ var NornFileCoordinator = class {
|
|
|
149
185
|
try {
|
|
150
186
|
await rmdir(path);
|
|
151
187
|
} catch (error) {
|
|
152
|
-
if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
188
|
+
if (!isNodeError(error) || !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
189
|
+
throw error;
|
|
153
190
|
}
|
|
154
191
|
}
|
|
155
192
|
};
|
|
156
193
|
function createRunFileCoordinator(runRoot) {
|
|
157
|
-
return new NornFileCoordinator({
|
|
194
|
+
return new NornFileCoordinator({
|
|
195
|
+
lockRoot: join(runRoot, "locks"),
|
|
196
|
+
waitTimeoutMs: 3e4
|
|
197
|
+
});
|
|
158
198
|
}
|
|
159
199
|
export {
|
|
160
200
|
NornFileCoordinator,
|
|
@@ -45,7 +45,9 @@ async function writeRunLaunchRequest(runRoot, request) {
|
|
|
45
45
|
await writeJsonAtomically(join(runRoot, LAUNCH_REQUEST_FILE_NAME), request);
|
|
46
46
|
}
|
|
47
47
|
async function readRunLaunchRequest(runRoot) {
|
|
48
|
-
return parseRunLaunchRequest(
|
|
48
|
+
return parseRunLaunchRequest(
|
|
49
|
+
JSON.parse(await readFile(join(runRoot, LAUNCH_REQUEST_FILE_NAME), "utf8"))
|
|
50
|
+
);
|
|
49
51
|
}
|
|
50
52
|
async function writeRunResumeRequest(runRoot, request) {
|
|
51
53
|
const path = join(runRoot, RESUME_REQUEST_FILE_NAME);
|
|
@@ -58,7 +60,9 @@ async function writeRunResumeRequest(runRoot, request) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
async function readRunResumeRequest(runRoot) {
|
|
61
|
-
return parseRunResumeRequest(
|
|
63
|
+
return parseRunResumeRequest(
|
|
64
|
+
JSON.parse(await readFile(join(runRoot, RESUME_REQUEST_FILE_NAME), "utf8"))
|
|
65
|
+
);
|
|
62
66
|
}
|
|
63
67
|
async function readOptionalRunResumeRequest(runRoot) {
|
|
64
68
|
try {
|
|
@@ -73,27 +77,41 @@ function matchesRunResumeRequest(actual, expected) {
|
|
|
73
77
|
}
|
|
74
78
|
async function clearRunResumeRequest(input) {
|
|
75
79
|
await input.lease.assertOwned();
|
|
76
|
-
if (matchesRunResumeRequest(
|
|
80
|
+
if (matchesRunResumeRequest(
|
|
81
|
+
await readOptionalRunResumeRequest(input.runRoot),
|
|
82
|
+
input.request
|
|
83
|
+
)) {
|
|
77
84
|
await rm2(join(input.runRoot, RESUME_REQUEST_FILE_NAME), { force: true });
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
function parseRunLaunchRequest(value) {
|
|
81
|
-
if (!value || typeof value !== "object")
|
|
88
|
+
if (!value || typeof value !== "object")
|
|
89
|
+
throw new Error("Invalid workflow launch request");
|
|
82
90
|
const request = value;
|
|
83
|
-
if (request.version !== 2 || request.type !== "run")
|
|
84
|
-
|
|
85
|
-
if (typeof request.
|
|
86
|
-
|
|
87
|
-
if (typeof request.
|
|
91
|
+
if (request.version !== 2 || request.type !== "run")
|
|
92
|
+
throw new Error("Unsupported workflow launch request");
|
|
93
|
+
if (typeof request.id !== "string" || request.id.length === 0)
|
|
94
|
+
throw new Error("Invalid workflow launch request id");
|
|
95
|
+
if (typeof request.name !== "string" || request.name.length === 0)
|
|
96
|
+
throw new Error("Invalid workflow launch request name");
|
|
97
|
+
if (typeof request.workflowId !== "string" || request.workflowId.length === 0)
|
|
98
|
+
throw new Error("Invalid workflow launch request workflow id");
|
|
99
|
+
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt)))
|
|
100
|
+
throw new Error("Invalid workflow launch request timestamp");
|
|
88
101
|
return request;
|
|
89
102
|
}
|
|
90
103
|
function parseRunResumeRequest(value) {
|
|
91
|
-
if (!value || typeof value !== "object")
|
|
104
|
+
if (!value || typeof value !== "object")
|
|
105
|
+
throw new Error("Invalid workflow resume request");
|
|
92
106
|
const request = value;
|
|
93
|
-
if (request.version !== 2 || request.type !== "resume")
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
107
|
+
if (request.version !== 2 || request.type !== "resume")
|
|
108
|
+
throw new Error("Unsupported workflow resume request");
|
|
109
|
+
if (typeof request.id !== "string" || request.id.length === 0)
|
|
110
|
+
throw new Error("Invalid workflow resume request id");
|
|
111
|
+
if (request.requestId !== void 0 && (typeof request.requestId !== "string" || request.requestId.length === 0))
|
|
112
|
+
throw new Error("Invalid workflow resume request token");
|
|
113
|
+
if (typeof request.createdAt !== "string" || Number.isNaN(Date.parse(request.createdAt)))
|
|
114
|
+
throw new Error("Invalid workflow resume request timestamp");
|
|
97
115
|
return request;
|
|
98
116
|
}
|
|
99
117
|
export {
|
package/dist/internal/metrics.js
CHANGED
|
@@ -10,7 +10,11 @@ function isNodeError(error) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
// src/internal/metrics.ts
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
addAgentUsage,
|
|
15
|
+
agentUsageFromValue,
|
|
16
|
+
emptyAgentUsage
|
|
17
|
+
} from "./usage.js";
|
|
14
18
|
var LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
|
|
15
19
|
async function readRunMetrics(runRoot) {
|
|
16
20
|
const currentRoot = runCurrentRoot(runRoot);
|
|
@@ -32,7 +36,8 @@ function calculateRunMetrics(events, state, now) {
|
|
|
32
36
|
let agentUsage = emptyAgentUsage();
|
|
33
37
|
for (const event of events) {
|
|
34
38
|
const eventAtMs = timestampMs(event.at);
|
|
35
|
-
if (event.type === "run.interrupted" && eventAtMs !== void 0)
|
|
39
|
+
if (event.type === "run.interrupted" && eventAtMs !== void 0)
|
|
40
|
+
gateStartedAtMs = eventAtMs;
|
|
36
41
|
else if (event.type === "run.resumed" && eventAtMs !== void 0 && gateStartedAtMs !== void 0) {
|
|
37
42
|
gateWaitMs += Math.max(0, eventAtMs - gateStartedAtMs);
|
|
38
43
|
gateStartedAtMs = void 0;
|
|
@@ -50,40 +55,87 @@ function calculateRunMetrics(events, state, now) {
|
|
|
50
55
|
continue;
|
|
51
56
|
}
|
|
52
57
|
if (event.type === "agent.started") {
|
|
53
|
-
if (openWorkflow)
|
|
58
|
+
if (openWorkflow)
|
|
59
|
+
openWorkflow = startAgentMetrics(openWorkflow, event, now);
|
|
54
60
|
continue;
|
|
55
61
|
}
|
|
56
62
|
if (event.type === "agent.completed" || event.type === "agent.failed") {
|
|
57
63
|
const usage = agentUsageFromEvent(event);
|
|
58
64
|
agentUsage = addAgentUsage(agentUsage, usage);
|
|
59
|
-
if (openWorkflow)
|
|
65
|
+
if (openWorkflow)
|
|
66
|
+
openWorkflow = finishAgentMetrics(openWorkflow, event, usage, now);
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
if (event.type === "command.started") {
|
|
63
|
-
if (openWorkflow)
|
|
70
|
+
if (openWorkflow)
|
|
71
|
+
openWorkflow = startCommandMetrics(openWorkflow, event, now);
|
|
64
72
|
continue;
|
|
65
73
|
}
|
|
66
74
|
if (event.type === "command.completed" || event.type === "command.failed") {
|
|
67
|
-
if (openWorkflow)
|
|
75
|
+
if (openWorkflow)
|
|
76
|
+
openWorkflow = finishCommandMetrics(openWorkflow, event, now);
|
|
68
77
|
continue;
|
|
69
78
|
}
|
|
70
79
|
const status = workflowTerminalStatus(event.type);
|
|
71
80
|
if (status) {
|
|
72
81
|
const durationMs = numberField(event.durationMs) || (openWorkflow && eventAtMs !== void 0 ? Math.max(0, eventAtMs - openWorkflow.startedAtMs) : 0);
|
|
73
|
-
const workflowId = status === "transitioned" ? stringField(
|
|
74
|
-
|
|
82
|
+
const workflowId = status === "transitioned" ? stringField(
|
|
83
|
+
event.fromWorkflowId,
|
|
84
|
+
openWorkflow?.workflowId ?? "unknown"
|
|
85
|
+
) : stringField(
|
|
86
|
+
event.workflowId,
|
|
87
|
+
openWorkflow?.workflowId ?? "unknown"
|
|
88
|
+
);
|
|
89
|
+
const workflow = openWorkflow ?? createSyntheticWorkflowMetrics(
|
|
90
|
+
workflowId,
|
|
91
|
+
startedAtMs,
|
|
92
|
+
endedAtMs,
|
|
93
|
+
eventAtMs,
|
|
94
|
+
durationMs
|
|
95
|
+
);
|
|
75
96
|
const childStatus = status === "failed" ? "failed" : "completed";
|
|
76
|
-
workflows.push(
|
|
97
|
+
workflows.push(
|
|
98
|
+
closeWorkflowMetrics(
|
|
99
|
+
closeOpenChildMetrics(
|
|
100
|
+
workflow,
|
|
101
|
+
eventAtMs ?? now.getTime(),
|
|
102
|
+
event.at,
|
|
103
|
+
childStatus
|
|
104
|
+
),
|
|
105
|
+
workflows.length + 1,
|
|
106
|
+
status,
|
|
107
|
+
durationMs,
|
|
108
|
+
event.at
|
|
109
|
+
)
|
|
110
|
+
);
|
|
77
111
|
openWorkflow = void 0;
|
|
78
112
|
}
|
|
79
113
|
}
|
|
80
|
-
if (gateStartedAtMs !== void 0)
|
|
114
|
+
if (gateStartedAtMs !== void 0)
|
|
115
|
+
gateWaitMs += Math.max(0, endedAtMs - gateStartedAtMs);
|
|
81
116
|
if (openWorkflow) {
|
|
82
|
-
workflows.push(
|
|
117
|
+
workflows.push(
|
|
118
|
+
closeWorkflowMetrics(
|
|
119
|
+
closeOpenChildMetrics(openWorkflow, endedAtMs, endedAt, "running"),
|
|
120
|
+
workflows.length + 1,
|
|
121
|
+
"running",
|
|
122
|
+
Math.max(0, endedAtMs - openWorkflow.startedAtMs),
|
|
123
|
+
endedAt
|
|
124
|
+
)
|
|
125
|
+
);
|
|
83
126
|
}
|
|
84
|
-
const workflowsMs = workflows.reduce(
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
const workflowsMs = workflows.reduce(
|
|
128
|
+
(sum, workflow) => sum + workflow.wallMs,
|
|
129
|
+
0
|
|
130
|
+
);
|
|
131
|
+
const agentsMs = workflows.reduce(
|
|
132
|
+
(sum, workflow) => sum + workflow.agentsMs,
|
|
133
|
+
0
|
|
134
|
+
);
|
|
135
|
+
const commandsMs = workflows.reduce(
|
|
136
|
+
(sum, workflow) => sum + workflow.commandsMs,
|
|
137
|
+
0
|
|
138
|
+
);
|
|
87
139
|
const wallMs = Math.max(0, endedAtMs - startedAtMs);
|
|
88
140
|
return {
|
|
89
141
|
status: state.status ?? "running",
|
|
@@ -102,10 +154,14 @@ function calculateRunMetrics(events, state, now) {
|
|
|
102
154
|
}
|
|
103
155
|
async function readRunStateSnapshot(currentRoot) {
|
|
104
156
|
try {
|
|
105
|
-
return JSON.parse(
|
|
157
|
+
return JSON.parse(
|
|
158
|
+
await readFile(join(currentRoot, RUN_STATE_FILE_NAME), "utf8")
|
|
159
|
+
);
|
|
106
160
|
} catch (error) {
|
|
107
161
|
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
108
|
-
return JSON.parse(
|
|
162
|
+
return JSON.parse(
|
|
163
|
+
await readFile(join(currentRoot, LEGACY_RUN_STATE_FILE_NAME), "utf8")
|
|
164
|
+
);
|
|
109
165
|
}
|
|
110
166
|
}
|
|
111
167
|
async function readManifestEvents(path) {
|
|
@@ -127,32 +183,59 @@ function createSyntheticWorkflowMetrics(workflowId, runStartedAtMs, runEndedAtMs
|
|
|
127
183
|
function startAgentMetrics(workflow, event, now) {
|
|
128
184
|
return {
|
|
129
185
|
...workflow,
|
|
130
|
-
openAgents: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
186
|
+
openAgents: [
|
|
187
|
+
...workflow.openAgents,
|
|
188
|
+
{
|
|
189
|
+
label: stringField(event.label, "unknown"),
|
|
190
|
+
startedAt: isoTime(event.at, now),
|
|
191
|
+
startedAtMs: timestampMs(event.at) ?? now.getTime()
|
|
192
|
+
}
|
|
193
|
+
]
|
|
135
194
|
};
|
|
136
195
|
}
|
|
137
196
|
function finishAgentMetrics(workflow, event, usage, now) {
|
|
138
|
-
const [agent, openAgents] = takeOpenChild(
|
|
139
|
-
|
|
197
|
+
const [agent, openAgents] = takeOpenChild(
|
|
198
|
+
workflow.openAgents,
|
|
199
|
+
stringField(event.label, "unknown")
|
|
200
|
+
);
|
|
201
|
+
const closedAgent = closeAgentMetrics(
|
|
202
|
+
event,
|
|
203
|
+
workflow.agents.length + 1,
|
|
204
|
+
usage,
|
|
205
|
+
now,
|
|
206
|
+
agent
|
|
207
|
+
);
|
|
140
208
|
return { ...workflow, agents: [...workflow.agents, closedAgent], openAgents };
|
|
141
209
|
}
|
|
142
210
|
function startCommandMetrics(workflow, event, now) {
|
|
143
211
|
return {
|
|
144
212
|
...workflow,
|
|
145
|
-
openCommands: [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
213
|
+
openCommands: [
|
|
214
|
+
...workflow.openCommands,
|
|
215
|
+
{
|
|
216
|
+
label: stringField(event.label, "unknown"),
|
|
217
|
+
startedAt: isoTime(event.at, now),
|
|
218
|
+
startedAtMs: timestampMs(event.at) ?? now.getTime()
|
|
219
|
+
}
|
|
220
|
+
]
|
|
150
221
|
};
|
|
151
222
|
}
|
|
152
223
|
function finishCommandMetrics(workflow, event, now) {
|
|
153
|
-
const [command, openCommands] = takeOpenChild(
|
|
154
|
-
|
|
155
|
-
|
|
224
|
+
const [command, openCommands] = takeOpenChild(
|
|
225
|
+
workflow.openCommands,
|
|
226
|
+
stringField(event.label, "unknown")
|
|
227
|
+
);
|
|
228
|
+
const closedCommand = closeCommandMetrics(
|
|
229
|
+
event,
|
|
230
|
+
workflow.commands.length + 1,
|
|
231
|
+
now,
|
|
232
|
+
command
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
...workflow,
|
|
236
|
+
commands: [...workflow.commands, closedCommand],
|
|
237
|
+
openCommands
|
|
238
|
+
};
|
|
156
239
|
}
|
|
157
240
|
function closeOpenChildMetrics(workflow, endedAtMs, endedAt, status) {
|
|
158
241
|
return {
|
|
@@ -185,8 +268,14 @@ function closeOpenChildMetrics(workflow, endedAtMs, endedAt, status) {
|
|
|
185
268
|
};
|
|
186
269
|
}
|
|
187
270
|
function closeWorkflowMetrics(workflow, index, status, wallMs, endedAt) {
|
|
188
|
-
const agentsMs = workflow.agents.reduce(
|
|
189
|
-
|
|
271
|
+
const agentsMs = workflow.agents.reduce(
|
|
272
|
+
(sum, agent) => sum + agent.wallMs,
|
|
273
|
+
0
|
|
274
|
+
);
|
|
275
|
+
const commandsMs = workflow.commands.reduce(
|
|
276
|
+
(sum, command) => sum + command.wallMs,
|
|
277
|
+
0
|
|
278
|
+
);
|
|
190
279
|
return {
|
|
191
280
|
index,
|
|
192
281
|
workflowId: workflow.workflowId,
|
|
@@ -197,7 +286,10 @@ function closeWorkflowMetrics(workflow, index, status, wallMs, endedAt) {
|
|
|
197
286
|
ownMs: Math.max(0, wallMs - agentsMs - commandsMs),
|
|
198
287
|
agentsMs,
|
|
199
288
|
commandsMs,
|
|
200
|
-
agentUsage: workflow.agents.reduce(
|
|
289
|
+
agentUsage: workflow.agents.reduce(
|
|
290
|
+
(sum, agent) => addAgentUsage(sum, agent.usage),
|
|
291
|
+
emptyAgentUsage()
|
|
292
|
+
),
|
|
201
293
|
agents: workflow.agents,
|
|
202
294
|
commands: workflow.commands
|
|
203
295
|
};
|
|
@@ -226,13 +318,19 @@ function closeCommandMetrics(event, index, now, openCommand) {
|
|
|
226
318
|
startedAt: openCommand?.startedAt ?? isoTimeFromMs(Math.max(0, endedAtMs - wallMs)),
|
|
227
319
|
endedAt: isoTime(event.at, now),
|
|
228
320
|
wallMs,
|
|
229
|
-
...event.type === "command.completed" ? {
|
|
321
|
+
...event.type === "command.completed" ? {
|
|
322
|
+
exitCode: nullableNumberField(event.exitCode),
|
|
323
|
+
killed: booleanField(event.killed)
|
|
324
|
+
} : {}
|
|
230
325
|
};
|
|
231
326
|
}
|
|
232
327
|
function takeOpenChild(children, label) {
|
|
233
328
|
const index = children.findIndex((child) => child.label === label);
|
|
234
329
|
if (index === -1) return [void 0, children];
|
|
235
|
-
return [
|
|
330
|
+
return [
|
|
331
|
+
children[index],
|
|
332
|
+
[...children.slice(0, index), ...children.slice(index + 1)]
|
|
333
|
+
];
|
|
236
334
|
}
|
|
237
335
|
function workflowTerminalStatus(type) {
|
|
238
336
|
if (type === "workflow.completed") return "completed";
|
|
@@ -241,8 +339,10 @@ function workflowTerminalStatus(type) {
|
|
|
241
339
|
return void 0;
|
|
242
340
|
}
|
|
243
341
|
function runEndedAt(state) {
|
|
244
|
-
if (state.status === "completed")
|
|
245
|
-
|
|
342
|
+
if (state.status === "completed")
|
|
343
|
+
return state.outcome?.completedAt ?? state.updatedAt;
|
|
344
|
+
if (state.status === "failed")
|
|
345
|
+
return state.failed?.failedAt ?? state.outcome?.completedAt ?? state.updatedAt;
|
|
246
346
|
return void 0;
|
|
247
347
|
}
|
|
248
348
|
function agentUsageFromEvent(event) {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// src/internal/pi-assets.ts
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
readFile,
|
|
8
|
+
readdir,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "node:fs/promises";
|
|
4
13
|
import { join, resolve } from "node:path";
|
|
5
14
|
import { gunzipSync } from "node:zlib";
|
|
6
15
|
|
|
@@ -11,15 +20,28 @@ function isNodeError(error) {
|
|
|
11
20
|
|
|
12
21
|
// src/internal/pi-assets.ts
|
|
13
22
|
function resolvePiAssetsCacheRoot(input) {
|
|
14
|
-
if (input.environment.NORN_PI_CACHE_DIR)
|
|
15
|
-
|
|
16
|
-
if (input.platform === "
|
|
23
|
+
if (input.environment.NORN_PI_CACHE_DIR)
|
|
24
|
+
return resolve(input.environment.NORN_PI_CACHE_DIR);
|
|
25
|
+
if (input.platform === "darwin")
|
|
26
|
+
return join(input.home, "Library", "Caches", "norn", "pi");
|
|
27
|
+
if (input.platform === "win32")
|
|
28
|
+
return join(
|
|
29
|
+
input.environment.LOCALAPPDATA || join(input.home, "AppData", "Local"),
|
|
30
|
+
"norn",
|
|
31
|
+
"pi"
|
|
32
|
+
);
|
|
17
33
|
const xdgCache = input.environment.XDG_CACHE_HOME;
|
|
18
|
-
return join(
|
|
34
|
+
return join(
|
|
35
|
+
xdgCache?.startsWith("/") ? xdgCache : join(input.home, ".cache"),
|
|
36
|
+
"norn",
|
|
37
|
+
"pi"
|
|
38
|
+
);
|
|
19
39
|
}
|
|
20
40
|
async function materializePiAssets(input) {
|
|
21
41
|
const digest = createHash("sha256").update(input.archive).digest("hex");
|
|
22
|
-
const files = JSON.parse(
|
|
42
|
+
const files = JSON.parse(
|
|
43
|
+
gunzipSync(Buffer.from(input.archive, "base64")).toString("utf8")
|
|
44
|
+
);
|
|
23
45
|
const expectedFiles = /* @__PURE__ */ new Map();
|
|
24
46
|
for (const file of files) {
|
|
25
47
|
if (!file.path || file.path.includes("\\") || file.path.includes(":") || file.path.split("/").some((part) => !part || part === "." || part === "..") || expectedFiles.has(file.path)) {
|
|
@@ -27,7 +49,11 @@ async function materializePiAssets(input) {
|
|
|
27
49
|
}
|
|
28
50
|
expectedFiles.set(file.path, Buffer.from(file.content, "base64"));
|
|
29
51
|
}
|
|
30
|
-
return new PiAssetCache({
|
|
52
|
+
return new PiAssetCache({
|
|
53
|
+
cacheRoot: input.cacheRoot,
|
|
54
|
+
digest,
|
|
55
|
+
expectedFiles
|
|
56
|
+
}).materialize();
|
|
31
57
|
}
|
|
32
58
|
var PiAssetCache = class {
|
|
33
59
|
constructor(input) {
|
|
@@ -38,21 +64,32 @@ var PiAssetCache = class {
|
|
|
38
64
|
root;
|
|
39
65
|
async materialize() {
|
|
40
66
|
await mkdir(this.input.cacheRoot, { recursive: true, mode: 448 });
|
|
41
|
-
if (!(await lstat(this.input.cacheRoot)).isDirectory())
|
|
67
|
+
if (!(await lstat(this.input.cacheRoot)).isDirectory())
|
|
68
|
+
throw new Error("Pi asset cache root must be a real directory");
|
|
42
69
|
if (await this.validateExistingCache()) return this.root;
|
|
43
70
|
const staging = await mkdtemp(join(this.input.cacheRoot, ".extract-"));
|
|
44
71
|
try {
|
|
45
72
|
for (const [path, content] of this.input.expectedFiles) {
|
|
46
73
|
const parts = path.split("/");
|
|
47
|
-
await mkdir(join(staging, ...parts.slice(0, -1)), {
|
|
48
|
-
|
|
74
|
+
await mkdir(join(staging, ...parts.slice(0, -1)), {
|
|
75
|
+
recursive: true,
|
|
76
|
+
mode: 448
|
|
77
|
+
});
|
|
78
|
+
await writeFile(join(staging, ...parts), content, {
|
|
79
|
+
flag: "wx",
|
|
80
|
+
mode: 384
|
|
81
|
+
});
|
|
49
82
|
}
|
|
50
83
|
try {
|
|
51
84
|
await rename(staging, this.root);
|
|
52
85
|
} catch (error) {
|
|
53
|
-
if (!isNodeError(error) || !["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(
|
|
86
|
+
if (!isNodeError(error) || !["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(
|
|
87
|
+
error.code ?? ""
|
|
88
|
+
) || !await this.validateExistingCache())
|
|
89
|
+
throw error;
|
|
54
90
|
}
|
|
55
|
-
if (!await this.validateExistingCache())
|
|
91
|
+
if (!await this.validateExistingCache())
|
|
92
|
+
throw new Error("Pi assets disappeared after extraction");
|
|
56
93
|
return this.root;
|
|
57
94
|
} finally {
|
|
58
95
|
await rm(staging, { recursive: true, force: true });
|
|
@@ -60,32 +97,46 @@ var PiAssetCache = class {
|
|
|
60
97
|
}
|
|
61
98
|
async validateExistingCache() {
|
|
62
99
|
try {
|
|
63
|
-
if (!(await lstat(this.root)).isDirectory())
|
|
100
|
+
if (!(await lstat(this.root)).isDirectory())
|
|
101
|
+
throw this.invalidCacheError();
|
|
64
102
|
} catch (error) {
|
|
65
103
|
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
66
104
|
throw error;
|
|
67
105
|
}
|
|
68
106
|
const seen = /* @__PURE__ */ new Set();
|
|
69
107
|
await this.validateDirectory({ directory: this.root, prefix: "", seen });
|
|
70
|
-
if (seen.size !== this.input.expectedFiles.size)
|
|
108
|
+
if (seen.size !== this.input.expectedFiles.size)
|
|
109
|
+
throw this.invalidCacheError();
|
|
71
110
|
return true;
|
|
72
111
|
}
|
|
73
112
|
async validateDirectory(input) {
|
|
74
|
-
for (const entry of await readdir(input.directory, {
|
|
113
|
+
for (const entry of await readdir(input.directory, {
|
|
114
|
+
withFileTypes: true
|
|
115
|
+
})) {
|
|
75
116
|
const path = input.prefix ? `${input.prefix}/${entry.name}` : entry.name;
|
|
76
117
|
const absolutePath = join(input.directory, entry.name);
|
|
77
118
|
if (entry.isDirectory()) {
|
|
78
|
-
if (![...this.input.expectedFiles.keys()].some(
|
|
79
|
-
|
|
119
|
+
if (![...this.input.expectedFiles.keys()].some(
|
|
120
|
+
(file) => file.startsWith(`${path}/`)
|
|
121
|
+
))
|
|
122
|
+
throw this.invalidCacheError();
|
|
123
|
+
await this.validateDirectory({
|
|
124
|
+
directory: absolutePath,
|
|
125
|
+
prefix: path,
|
|
126
|
+
seen: input.seen
|
|
127
|
+
});
|
|
80
128
|
} else {
|
|
81
129
|
const expected = this.input.expectedFiles.get(path);
|
|
82
|
-
if (!entry.isFile() || !expected || !(await readFile(absolutePath)).equals(expected))
|
|
130
|
+
if (!entry.isFile() || !expected || !(await readFile(absolutePath)).equals(expected))
|
|
131
|
+
throw this.invalidCacheError();
|
|
83
132
|
input.seen.add(path);
|
|
84
133
|
}
|
|
85
134
|
}
|
|
86
135
|
}
|
|
87
136
|
invalidCacheError() {
|
|
88
|
-
return new Error(
|
|
137
|
+
return new Error(
|
|
138
|
+
`Bundled Pi assets are incomplete or modified: ${this.root}. Remove this cache entry and retry.`
|
|
139
|
+
);
|
|
89
140
|
}
|
|
90
141
|
};
|
|
91
142
|
export {
|