@oisincoveney/pipeline 2.7.0 → 2.8.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/defaults/pipeline.yaml +25 -0
- package/dist/argo-graph.js +7 -7
- package/dist/argo-submit.d.ts +2 -4
- package/dist/argo-submit.js +80 -80
- package/dist/bench/eval-report.js +27 -0
- package/dist/cli/program.js +19 -3
- package/dist/cluster-doctor.js +89 -101
- package/dist/commands/bench-command.js +18 -0
- package/dist/config/defaults.js +9 -19
- package/dist/config/load.js +47 -37
- package/dist/config/schemas.d.ts +24 -7
- package/dist/config/schemas.js +20 -7
- package/dist/context/repo-map.js +203 -0
- package/dist/install-commands/opencode.js +10 -1
- package/dist/mcp/gateway-error.js +15 -0
- package/dist/mcp/gateway.js +119 -220
- package/dist/moka-global-config.js +20 -20
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-init.js +18 -12
- package/dist/pipeline-runtime.js +592 -372
- package/dist/planning/compile.d.ts +8 -3
- package/dist/planning/compile.js +7 -7
- package/dist/planning/generate.d.ts +6 -1
- package/dist/planning/generate.js +29 -7
- package/dist/run-state/git-refs.js +124 -94
- package/dist/runner-command-contract.d.ts +6 -1
- package/dist/runner-command-contract.js +6 -5
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runner-event-sink.js +37 -68
- package/dist/runner.d.ts +6 -1
- package/dist/runner.js +3 -3
- package/dist/runtime/agent-node/agent-node.js +218 -159
- package/dist/runtime/changed-files/changed-files.js +15 -27
- package/dist/runtime/changed-files/index.js +2 -0
- package/dist/runtime/drain-merge/drain-merge.js +124 -82
- package/dist/runtime/gates/gates.js +45 -27
- package/dist/runtime/hooks/hooks.js +74 -29
- package/dist/runtime/local-scheduler.js +45 -0
- package/dist/runtime/opencode-server.js +32 -23
- package/dist/runtime/opencode-session-executor.js +101 -44
- package/dist/runtime/parallel-node/parallel-node.js +93 -75
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
- package/dist/runtime/run-journal.js +21 -0
- package/dist/runtime/scheduler.js +122 -93
- package/dist/runtime/select-candidate/select-candidate.js +52 -24
- package/dist/runtime/services/agent-node-runtime-service.js +15 -0
- package/dist/runtime/services/command-executor-service.js +8 -0
- package/dist/runtime/services/config-io-service.js +42 -0
- package/dist/runtime/services/drain-merge-git-service.js +10 -0
- package/dist/runtime/services/git-porcelain-service.js +38 -0
- package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
- package/dist/runtime/services/kubernetes-argo-service.js +81 -0
- package/dist/runtime/services/mcp-gateway-service.js +184 -0
- package/dist/runtime/services/opencode-sdk-service.js +27 -0
- package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
- package/dist/runtime/services/select-candidate-service.js +13 -0
- package/dist/runtime/services/worktree-service.js +18 -0
- package/dist/schedule/passes/candidates.js +17 -8
- package/docs/config-architecture.md +105 -0
- package/package.json +7 -2
|
@@ -7,9 +7,14 @@ interface WorkflowPlannerIssue {
|
|
|
7
7
|
message: string;
|
|
8
8
|
path?: string;
|
|
9
9
|
}
|
|
10
|
-
declare
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
declare const WorkflowPlannerError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
11
|
+
readonly _tag: "WorkflowPlannerError";
|
|
12
|
+
} & Readonly<A>;
|
|
13
|
+
declare class WorkflowPlannerError extends WorkflowPlannerError_base<{
|
|
14
|
+
readonly code: WorkflowPlannerErrorCode;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly issues: WorkflowPlannerIssue[];
|
|
17
|
+
}> {
|
|
13
18
|
constructor(code: WorkflowPlannerErrorCode, message: string, issues?: WorkflowPlannerIssue[]);
|
|
14
19
|
}
|
|
15
20
|
interface PlannedWorkflowNode {
|
package/dist/planning/compile.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { uniqueStrings } from "../strings.js";
|
|
2
2
|
import { findDependencyCycles } from "./graph.js";
|
|
3
|
+
import { Data } from "effect";
|
|
3
4
|
import { Graph } from "@dagrejs/graphlib";
|
|
4
5
|
//#region src/planning/compile.ts
|
|
5
|
-
var WorkflowPlannerError = class extends
|
|
6
|
-
code;
|
|
7
|
-
issues;
|
|
6
|
+
var WorkflowPlannerError = class extends Data.TaggedError("WorkflowPlannerError") {
|
|
8
7
|
constructor(code, message, issues = []) {
|
|
9
|
-
super(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
super({
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
issues
|
|
12
|
+
});
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
function compileWorkflowPlan(config, workflowId = config.default_workflow) {
|
|
@@ -1594,7 +1594,12 @@ interface SchedulePlanningContext {
|
|
|
1594
1594
|
workUnits: BacklogWorkUnit[];
|
|
1595
1595
|
}
|
|
1596
1596
|
declare function parseScheduleArtifact(source: string, sourcePath?: string): ScheduleArtifact;
|
|
1597
|
-
declare
|
|
1597
|
+
declare const ScheduleArtifactError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
1598
|
+
readonly _tag: "ScheduleArtifactError";
|
|
1599
|
+
} & Readonly<A>;
|
|
1600
|
+
declare class ScheduleArtifactError extends ScheduleArtifactError_base<{
|
|
1601
|
+
readonly message: string;
|
|
1602
|
+
}> {
|
|
1598
1603
|
constructor(message: string);
|
|
1599
1604
|
}
|
|
1600
1605
|
declare function compileScheduleArtifact(config: PipelineConfig, artifact: ScheduleArtifact, projectRoot?: string): CompiledScheduleArtifact;
|
|
@@ -17,6 +17,7 @@ import { plannerPrompt, plannerRepairPrompt } from "../schedule/prompts.js";
|
|
|
17
17
|
import { compileWorkflowPlan } from "./compile.js";
|
|
18
18
|
import { parseDocument, stringify } from "yaml";
|
|
19
19
|
import { z } from "zod";
|
|
20
|
+
import { Data } from "effect";
|
|
20
21
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
21
22
|
import { join } from "node:path";
|
|
22
23
|
//#region src/planning/generate.ts
|
|
@@ -30,10 +31,12 @@ const SCHEDULE_BUILTINS = [
|
|
|
30
31
|
"duplication",
|
|
31
32
|
"fallow",
|
|
32
33
|
"lint",
|
|
34
|
+
"select-candidate",
|
|
33
35
|
"semgrep",
|
|
34
36
|
"test",
|
|
35
37
|
"typecheck"
|
|
36
38
|
];
|
|
39
|
+
const PARALLEL_MERGE_BUILTINS = new Set(["drain-merge", "select-candidate"]);
|
|
37
40
|
const scheduleArtifactSchema = z.object({
|
|
38
41
|
generated_at: z.string().datetime(),
|
|
39
42
|
kind: z.literal(SCHEDULE_KIND),
|
|
@@ -54,10 +57,9 @@ function parseScheduleArtifact(source, sourcePath = "schedule.yaml") {
|
|
|
54
57
|
if (!parsed.success) throw new ScheduleArtifactError([`Invalid schedule artifact ${sourcePath}:`, ...parsed.error.issues.map((issue) => issue.path.length > 0 ? `- ${issue.path.join(".")}: ${issue.message}` : `- ${issue.message}`)].join("\n"));
|
|
55
58
|
return parsed.data;
|
|
56
59
|
}
|
|
57
|
-
var ScheduleArtifactError = class extends
|
|
60
|
+
var ScheduleArtifactError = class extends Data.TaggedError("ScheduleArtifactError") {
|
|
58
61
|
constructor(message) {
|
|
59
|
-
super(message);
|
|
60
|
-
this.name = "ScheduleArtifactError";
|
|
62
|
+
super({ message });
|
|
61
63
|
}
|
|
62
64
|
};
|
|
63
65
|
function compileScheduleArtifact(config, artifact, projectRoot) {
|
|
@@ -255,11 +257,28 @@ function unsafeParallelWorktreeIssues(config, artifact) {
|
|
|
255
257
|
return [`parallel node '${workflowId}.${node.id}' has write-capable children sharing a worktree without isolated worktree roots or drain-merge integration`];
|
|
256
258
|
}));
|
|
257
259
|
}
|
|
260
|
+
function registerContainmentEdge(parent, child, index) {
|
|
261
|
+
const dependents = index.get(child.id) ?? [];
|
|
262
|
+
dependents.push(parent);
|
|
263
|
+
index.set(child.id, dependents);
|
|
264
|
+
}
|
|
265
|
+
function addParallelContainmentEdges(nodes, index) {
|
|
266
|
+
for (const node of nodes) {
|
|
267
|
+
if (node.kind !== "parallel") continue;
|
|
268
|
+
for (const child of node.nodes) registerContainmentEdge(node, child, index);
|
|
269
|
+
addParallelContainmentEdges(node.nodes, index);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function dependentsByNeedWithContainment(nested, flat) {
|
|
273
|
+
const index = dependentsByNeed(flat);
|
|
274
|
+
addParallelContainmentEdges(nested, index);
|
|
275
|
+
return index;
|
|
276
|
+
}
|
|
258
277
|
function workflowNodeIssues(artifact, collectIssues) {
|
|
259
278
|
return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
|
|
260
279
|
const nodes = flattenWorkflowNodes(workflow.nodes);
|
|
261
280
|
return collectIssues({
|
|
262
|
-
dependentsByNeed:
|
|
281
|
+
dependentsByNeed: dependentsByNeedWithContainment(workflow.nodes, nodes),
|
|
263
282
|
nodes,
|
|
264
283
|
workflowId
|
|
265
284
|
});
|
|
@@ -272,7 +291,7 @@ function isWriteCapableParallelChild(config, node) {
|
|
|
272
291
|
return false;
|
|
273
292
|
}
|
|
274
293
|
function hasDownstreamDrainMerge(nodeId, index) {
|
|
275
|
-
return hasReachableDependent(nodeId, index, (node) => node.kind === "builtin" && node.builtin
|
|
294
|
+
return hasReachableDependent(nodeId, index, (node) => node.kind === "builtin" && PARALLEL_MERGE_BUILTINS.has(node.builtin));
|
|
276
295
|
}
|
|
277
296
|
function generatedRootWorkflowIssues(artifact) {
|
|
278
297
|
const workflowIds = Object.keys(artifact.workflows);
|
|
@@ -324,9 +343,9 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
|
|
|
324
343
|
const dependenciesByUnit = new Map(workUnits.map((unit) => [unit.id, (unit.dependencies ?? []).filter((id) => workUnitIds.has(id))]));
|
|
325
344
|
return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
|
|
326
345
|
const nodes = flattenWorkflowNodes(workflow.nodes);
|
|
327
|
-
const index =
|
|
346
|
+
const index = dependentsByNeedWithContainment(workflow.nodes, nodes);
|
|
328
347
|
const nodesByWorkUnit = nodesByAssignedWorkUnit(nodes);
|
|
329
|
-
return nodes.filter((node) => isImplementationNode(config, node)).flatMap((node) => {
|
|
348
|
+
return nodes.filter((node) => isImplementationNode(config, node) || isSelectCandidateNode(node)).flatMap((node) => {
|
|
330
349
|
const dependentId = node.task_context?.id;
|
|
331
350
|
if (!dependentId) return [];
|
|
332
351
|
return (dependenciesByUnit.get(dependentId) ?? []).flatMap((prerequisiteId) => {
|
|
@@ -336,6 +355,9 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
|
|
|
336
355
|
});
|
|
337
356
|
});
|
|
338
357
|
}
|
|
358
|
+
function isSelectCandidateNode(node) {
|
|
359
|
+
return node.kind === "builtin" && node.builtin === "select-candidate";
|
|
360
|
+
}
|
|
339
361
|
function nodesByAssignedWorkUnit(nodes) {
|
|
340
362
|
const grouped = /* @__PURE__ */ new Map();
|
|
341
363
|
for (const node of nodes) {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { GitPorcelainService, GitPorcelainServiceLive } from "../runtime/services/git-porcelain-service.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
1
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
4
|
import { dirname, resolve } from "node:path";
|
|
3
5
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
6
|
//#region src/run-state/git-refs.ts
|
|
7
7
|
const DEFAULT_WORKSPACE_PATH = "/workspace";
|
|
8
8
|
const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
|
|
9
9
|
const WRITABLE_GIT_CREDENTIAL_STORE = resolve(tmpdir(), "pipeline-git-credentials");
|
|
10
10
|
const SCP_LIKE_SSH_REMOTE_RE = /^[^@\s]+@[^:\s]+:.+/u;
|
|
11
|
-
const execGit = promisify(execFile);
|
|
12
11
|
let preparedBasicAuthCredentialStore;
|
|
13
12
|
function runnerGitRefs(payload, nodeId) {
|
|
14
13
|
const prefix = `refs/heads/pipeline/runs/${payload.run.id}/${payload.workflow.id}`;
|
|
@@ -19,101 +18,132 @@ function runnerGitRefs(payload, nodeId) {
|
|
|
19
18
|
};
|
|
20
19
|
}
|
|
21
20
|
async function prepareRunnerGitWorkspace(payload, options = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
worktreePath
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
return await Effect.runPromise(Effect.provide(prepareRunnerGitWorkspaceEffect(payload, options), GitPorcelainServiceLive));
|
|
22
|
+
}
|
|
23
|
+
function prepareRunnerGitWorkspaceEffect(payload, options) {
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
if (options.cwd) return resolve(options.cwd);
|
|
26
|
+
const worktreePath = options.workspacePath ?? DEFAULT_WORKSPACE_PATH;
|
|
27
|
+
yield* Effect.sync(() => mkdirSync(dirname(worktreePath), { recursive: true }));
|
|
28
|
+
yield* runGit(dirname(worktreePath), [
|
|
29
|
+
"clone",
|
|
30
|
+
"--no-tags",
|
|
31
|
+
payload.repository.url,
|
|
32
|
+
worktreePath
|
|
33
|
+
]);
|
|
34
|
+
yield* runGit(worktreePath, ["checkout", payload.repository.sha ?? `origin/${payload.repository.baseBranch}`]);
|
|
35
|
+
return worktreePath;
|
|
36
|
+
});
|
|
33
37
|
}
|
|
34
38
|
async function mergeDependencyRefs(input) {
|
|
35
|
-
await
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
return await Effect.runPromise(Effect.provide(mergeDependencyRefsEffect(input), GitPorcelainServiceLive));
|
|
40
|
+
}
|
|
41
|
+
function mergeDependencyRefsEffect(input) {
|
|
42
|
+
return Effect.gen(function* () {
|
|
43
|
+
yield* configureGitCommitter(input.worktreePath, input.committer);
|
|
44
|
+
yield* Effect.forEach(input.dependencyNodeIds, (nodeId) => mergeDependencyRef(input.worktreePath, runnerGitRefs(input.payload, nodeId).nodeRef));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function mergeDependencyRef(worktreePath, ref) {
|
|
48
|
+
return Effect.gen(function* () {
|
|
49
|
+
yield* runGit(worktreePath, [
|
|
39
50
|
"fetch",
|
|
40
51
|
"origin",
|
|
41
52
|
ref
|
|
42
53
|
]);
|
|
43
|
-
|
|
54
|
+
yield* runGit(worktreePath, [
|
|
44
55
|
"merge",
|
|
45
56
|
"--no-ff",
|
|
46
57
|
"--no-edit",
|
|
47
58
|
"FETCH_HEAD"
|
|
48
59
|
]);
|
|
49
|
-
}
|
|
60
|
+
});
|
|
50
61
|
}
|
|
51
62
|
async function commitAndPushNodeRef(input) {
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
return await Effect.runPromise(Effect.provide(commitAndPushNodeRefEffect(input), GitPorcelainServiceLive));
|
|
64
|
+
}
|
|
65
|
+
function commitAndPushNodeRefEffect(input) {
|
|
66
|
+
return Effect.gen(function* () {
|
|
67
|
+
yield* commitChangesIfNeeded(input.worktreePath, input.nodeId, input.committer);
|
|
68
|
+
const sha = yield* headSha(input.worktreePath);
|
|
69
|
+
yield* runGit(input.worktreePath, [
|
|
70
|
+
"push",
|
|
71
|
+
"origin",
|
|
72
|
+
`HEAD:${runnerGitRefs(input.payload, input.nodeId).nodeRef}`
|
|
73
|
+
]);
|
|
74
|
+
return sha;
|
|
75
|
+
});
|
|
60
76
|
}
|
|
61
77
|
async function promoteFinalRef(input) {
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
return await Effect.runPromise(Effect.provide(promoteFinalRefEffect(input), GitPorcelainServiceLive));
|
|
79
|
+
}
|
|
80
|
+
function promoteFinalRefEffect(input) {
|
|
81
|
+
return Effect.gen(function* () {
|
|
82
|
+
yield* mergeDependencyRefsEffect({
|
|
83
|
+
committer: input.committer,
|
|
84
|
+
dependencyNodeIds: input.sourceNodeIds,
|
|
85
|
+
payload: input.payload,
|
|
86
|
+
worktreePath: input.worktreePath
|
|
87
|
+
});
|
|
88
|
+
yield* commitChangesIfNeeded(input.worktreePath, "final", input.committer);
|
|
89
|
+
const sha = yield* headSha(input.worktreePath);
|
|
90
|
+
yield* runGit(input.worktreePath, [
|
|
91
|
+
"push",
|
|
92
|
+
"origin",
|
|
93
|
+
`HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
|
|
94
|
+
]);
|
|
95
|
+
return sha;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function headSha(worktreePath) {
|
|
99
|
+
return Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
|
|
100
|
+
}
|
|
101
|
+
function commitChangesIfNeeded(worktreePath, nodeId, committer) {
|
|
102
|
+
return Effect.gen(function* () {
|
|
103
|
+
if ((yield* runGit(worktreePath, [
|
|
104
|
+
"status",
|
|
105
|
+
"--porcelain",
|
|
106
|
+
"--untracked-files=all"
|
|
107
|
+
])).trim().length === 0) return;
|
|
108
|
+
yield* runGit(worktreePath, ["add", "--all"]);
|
|
109
|
+
yield* configureGitCommitter(worktreePath, committer);
|
|
110
|
+
yield* runGit(worktreePath, [
|
|
111
|
+
"commit",
|
|
112
|
+
"-m",
|
|
113
|
+
`pipeline: ${nodeId}`
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function configureGitCommitter(worktreePath, committer) {
|
|
118
|
+
return Effect.gen(function* () {
|
|
119
|
+
yield* runGit(worktreePath, [
|
|
120
|
+
"config",
|
|
121
|
+
"--local",
|
|
122
|
+
"user.name",
|
|
123
|
+
committer.name
|
|
124
|
+
]);
|
|
125
|
+
yield* runGit(worktreePath, [
|
|
126
|
+
"config",
|
|
127
|
+
"--local",
|
|
128
|
+
"user.email",
|
|
129
|
+
committer.email
|
|
130
|
+
]);
|
|
67
131
|
});
|
|
68
|
-
await commitChangesIfNeeded(input.worktreePath, "final", input.committer);
|
|
69
|
-
const sha = (await runGit(input.worktreePath, ["rev-parse", "HEAD"])).trim();
|
|
70
|
-
await runGit(input.worktreePath, [
|
|
71
|
-
"push",
|
|
72
|
-
"origin",
|
|
73
|
-
`HEAD:${runnerGitRefs(input.payload, "final").finalRef}`
|
|
74
|
-
]);
|
|
75
|
-
return sha;
|
|
76
|
-
}
|
|
77
|
-
async function commitChangesIfNeeded(worktreePath, nodeId, committer) {
|
|
78
|
-
if ((await runGit(worktreePath, [
|
|
79
|
-
"status",
|
|
80
|
-
"--porcelain",
|
|
81
|
-
"--untracked-files=all"
|
|
82
|
-
])).trim().length === 0) return;
|
|
83
|
-
await runGit(worktreePath, ["add", "--all"]);
|
|
84
|
-
await configureGitCommitter(worktreePath, committer);
|
|
85
|
-
await runGit(worktreePath, [
|
|
86
|
-
"commit",
|
|
87
|
-
"-m",
|
|
88
|
-
`pipeline: ${nodeId}`
|
|
89
|
-
]);
|
|
90
|
-
}
|
|
91
|
-
async function configureGitCommitter(worktreePath, committer) {
|
|
92
|
-
await runGit(worktreePath, [
|
|
93
|
-
"config",
|
|
94
|
-
"--local",
|
|
95
|
-
"user.name",
|
|
96
|
-
committer.name
|
|
97
|
-
]);
|
|
98
|
-
await runGit(worktreePath, [
|
|
99
|
-
"config",
|
|
100
|
-
"--local",
|
|
101
|
-
"user.email",
|
|
102
|
-
committer.email
|
|
103
|
-
]);
|
|
104
132
|
}
|
|
105
133
|
function runnerGitCommandArgs(args, remoteUrl) {
|
|
106
134
|
return [...gitCredentialConfigArgs(remoteUrl), ...args];
|
|
107
135
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
function runGit(cwd, args) {
|
|
137
|
+
return Effect.gen(function* () {
|
|
138
|
+
const remoteUrl = yield* remoteUrlFromGitArgs(cwd, args);
|
|
139
|
+
yield* Effect.try({
|
|
140
|
+
try: () => assertSshCredentialsAvailable(remoteUrl),
|
|
141
|
+
catch: (error) => error
|
|
142
|
+
});
|
|
143
|
+
const git = yield* GitPorcelainService;
|
|
144
|
+
const commandArgs = runnerGitCommandArgs(args, remoteUrl);
|
|
145
|
+
return yield* git.run(cwd, commandArgs, runnerGitEnv(remoteUrl));
|
|
115
146
|
});
|
|
116
|
-
return stdout;
|
|
117
147
|
}
|
|
118
148
|
function assertSshCredentialsAvailable(remoteUrl) {
|
|
119
149
|
if (!(remoteUrl && isSshRemote(remoteUrl))) return;
|
|
@@ -227,12 +257,15 @@ function isOwnerReadOnly(path) {
|
|
|
227
257
|
function readCredentialFile(path) {
|
|
228
258
|
return readFileSync(path, "utf8").trim();
|
|
229
259
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
260
|
+
function remoteUrlFromGitArgs(cwd, args) {
|
|
261
|
+
return Effect.gen(function* () {
|
|
262
|
+
const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
|
|
263
|
+
if (literalRemoteUrl) return literalRemoteUrl;
|
|
264
|
+
return yield* remoteUrlForName(cwd, remoteNameFromGitArgs(args));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function remoteUrlForName(cwd, remoteName) {
|
|
268
|
+
return remoteName ? gitRemoteUrl(cwd, remoteName) : Effect.succeed(void 0);
|
|
236
269
|
}
|
|
237
270
|
function literalRemoteUrlFromGitArgs(args) {
|
|
238
271
|
if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
|
|
@@ -247,20 +280,17 @@ function remoteNameFromGitArgs(args) {
|
|
|
247
280
|
if (remoteArg && !remoteArg.startsWith("-") && !isRemoteUrl(remoteArg)) return remoteArg;
|
|
248
281
|
}
|
|
249
282
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
encoding: "utf8",
|
|
258
|
-
env: {
|
|
283
|
+
function gitRemoteUrl(cwd, remoteName) {
|
|
284
|
+
return Effect.gen(function* () {
|
|
285
|
+
return (yield* (yield* GitPorcelainService).run(cwd, [
|
|
286
|
+
"remote",
|
|
287
|
+
"get-url",
|
|
288
|
+
remoteName
|
|
289
|
+
], {
|
|
259
290
|
...process.env,
|
|
260
291
|
GIT_TERMINAL_PROMPT: "0"
|
|
261
|
-
}
|
|
292
|
+
})).trim() || void 0;
|
|
262
293
|
});
|
|
263
|
-
return stdout.trim() || void 0;
|
|
264
294
|
}
|
|
265
295
|
function isRemoteUrl(value) {
|
|
266
296
|
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
|
|
@@ -140,8 +140,13 @@ interface RunnerCommandPayloadValidationIssue {
|
|
|
140
140
|
message: string;
|
|
141
141
|
path: string;
|
|
142
142
|
}
|
|
143
|
-
declare
|
|
143
|
+
declare const RunnerCommandPayloadValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
144
|
+
readonly _tag: "RunnerCommandPayloadValidationError";
|
|
145
|
+
} & Readonly<A>;
|
|
146
|
+
declare class RunnerCommandPayloadValidationError extends RunnerCommandPayloadValidationError_base<{
|
|
147
|
+
readonly message: string;
|
|
144
148
|
readonly issues: RunnerCommandPayloadValidationIssue[];
|
|
149
|
+
}> {
|
|
145
150
|
constructor(message: string, issues: RunnerCommandPayloadValidationIssue[]);
|
|
146
151
|
}
|
|
147
152
|
interface BuildRunnerCommandPayloadOptions {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseJson } from "./safe-json.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Data } from "effect";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
5
|
import parseGitUrl from "git-url-parse";
|
|
5
6
|
//#region src/runner-command-contract.ts
|
|
@@ -71,12 +72,12 @@ const runnerCommandPayloadSchema = z.object({
|
|
|
71
72
|
task: runnerTaskSchema,
|
|
72
73
|
workflow: runnerWorkflowIdentitySchema
|
|
73
74
|
}).strict();
|
|
74
|
-
var RunnerCommandPayloadValidationError = class extends
|
|
75
|
-
issues;
|
|
75
|
+
var RunnerCommandPayloadValidationError = class extends Data.TaggedError("RunnerCommandPayloadValidationError") {
|
|
76
76
|
constructor(message, issues) {
|
|
77
|
-
super(
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
super({
|
|
78
|
+
message,
|
|
79
|
+
issues
|
|
80
|
+
});
|
|
80
81
|
}
|
|
81
82
|
};
|
|
82
83
|
function resolveRunnerEventSinkAuthToken(options) {
|
|
@@ -10,8 +10,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
10
10
|
at: z.ZodOptional<z.ZodString>;
|
|
11
11
|
sequence: z.ZodNumber;
|
|
12
12
|
type: z.ZodEnum<{
|
|
13
|
-
"workflow.planned": "workflow.planned";
|
|
14
13
|
"workflow.start": "workflow.start";
|
|
14
|
+
"workflow.planned": "workflow.planned";
|
|
15
15
|
}>;
|
|
16
16
|
workflowPlan: z.ZodObject<{
|
|
17
17
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -55,10 +55,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
55
55
|
}>;
|
|
56
56
|
}, z.core.$strip>;
|
|
57
57
|
type: z.ZodEnum<{
|
|
58
|
+
"node.start": "node.start";
|
|
59
|
+
"node.finish": "node.finish";
|
|
58
60
|
"agent.finish": "agent.finish";
|
|
59
61
|
"agent.start": "agent.start";
|
|
60
|
-
"node.finish": "node.finish";
|
|
61
|
-
"node.start": "node.start";
|
|
62
62
|
}>;
|
|
63
63
|
}, z.core.$strip>, z.ZodObject<{
|
|
64
64
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -180,8 +180,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
180
180
|
at: z.ZodOptional<z.ZodString>;
|
|
181
181
|
sequence: z.ZodNumber;
|
|
182
182
|
type: z.ZodEnum<{
|
|
183
|
-
"workflow.planned": "workflow.planned";
|
|
184
183
|
"workflow.start": "workflow.start";
|
|
184
|
+
"workflow.planned": "workflow.planned";
|
|
185
185
|
}>;
|
|
186
186
|
workflowPlan: z.ZodObject<{
|
|
187
187
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -225,10 +225,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
225
225
|
}>;
|
|
226
226
|
}, z.core.$strip>;
|
|
227
227
|
type: z.ZodEnum<{
|
|
228
|
+
"node.start": "node.start";
|
|
229
|
+
"node.finish": "node.finish";
|
|
228
230
|
"agent.finish": "agent.finish";
|
|
229
231
|
"agent.start": "agent.start";
|
|
230
|
-
"node.finish": "node.finish";
|
|
231
|
-
"node.start": "node.start";
|
|
232
232
|
}>;
|
|
233
233
|
}, z.core.$strip>, z.ZodObject<{
|
|
234
234
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
import { mapRuntimeEventToRunnerEventRecords } from "./runner-command-contract.js";
|
|
2
|
-
import
|
|
2
|
+
import { RunnerEventSinkHttpService, RunnerEventSinkHttpServiceLive } from "./runtime/services/runner-event-sink-http-service.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
3
4
|
//#region src/runner-event-sink.ts
|
|
4
5
|
const DEFAULT_BATCH_SIZE = 50;
|
|
5
6
|
const DEFAULT_MAX_RETRIES = 0;
|
|
6
7
|
const DEFAULT_RETRY_DELAY_MS = 250;
|
|
7
|
-
const RETRYABLE_STATUS_CODES = [
|
|
8
|
-
408,
|
|
9
|
-
429,
|
|
10
|
-
500,
|
|
11
|
-
501,
|
|
12
|
-
502,
|
|
13
|
-
503,
|
|
14
|
-
504,
|
|
15
|
-
505,
|
|
16
|
-
506,
|
|
17
|
-
507,
|
|
18
|
-
508,
|
|
19
|
-
509,
|
|
20
|
-
510,
|
|
21
|
-
511
|
|
22
|
-
];
|
|
23
|
-
var EventSinkHttpError = class extends Error {
|
|
24
|
-
status;
|
|
25
|
-
constructor(status, message) {
|
|
26
|
-
super(message);
|
|
27
|
-
this.name = "EventSinkHttpError";
|
|
28
|
-
this.status = status;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
8
|
function createRunnerEventSink(options) {
|
|
32
|
-
const batchSize =
|
|
33
|
-
const fetchImpl = options.fetch
|
|
34
|
-
|
|
35
|
-
if (!options.authToken.trim()) throw new Error("Runner event sink requires an auth token");
|
|
9
|
+
const batchSize = positiveOrDefault(options.batchSize, DEFAULT_BATCH_SIZE);
|
|
10
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
11
|
+
assertAuthToken(options.authToken);
|
|
36
12
|
const queue = [];
|
|
37
13
|
let flushChain = Promise.resolve();
|
|
38
14
|
let nextSequence = 1;
|
|
@@ -45,15 +21,17 @@ function createRunnerEventSink(options) {
|
|
|
45
21
|
sequence
|
|
46
22
|
};
|
|
47
23
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
24
|
+
const flushQueueEffect = () => Effect.gen(function* () {
|
|
25
|
+
const service = yield* RunnerEventSinkHttpService;
|
|
26
|
+
while (hasQueuedEvents(queue)) {
|
|
50
27
|
const batch = queue.slice(0, batchSize);
|
|
51
|
-
|
|
28
|
+
yield* service.postBatch(postBatchRequest(options, fetchImpl, batch));
|
|
52
29
|
queue.splice(0, batch.length);
|
|
53
30
|
}
|
|
54
|
-
};
|
|
31
|
+
});
|
|
32
|
+
const runFlush = () => Effect.runPromise(Effect.provide(flushQueueEffect(), RunnerEventSinkHttpServiceLive));
|
|
55
33
|
const runSerializedFlush = () => {
|
|
56
|
-
const nextFlush = flushChain.then(
|
|
34
|
+
const nextFlush = flushChain.then(runFlush, runFlush);
|
|
57
35
|
flushChain = nextFlush.catch(() => void 0);
|
|
58
36
|
return nextFlush;
|
|
59
37
|
};
|
|
@@ -144,41 +122,32 @@ function createRunnerEventSink(options) {
|
|
|
144
122
|
}
|
|
145
123
|
};
|
|
146
124
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const retryDelayMs = Math.max(0, options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
|
|
150
|
-
try {
|
|
151
|
-
await ky.post(options.url, {
|
|
152
|
-
fetch: kyFetchAdapter(fetchImpl),
|
|
153
|
-
headers: { [options.authHeader ?? "Authorization"]: `Bearer ${options.authToken}` },
|
|
154
|
-
json: { events },
|
|
155
|
-
retry: {
|
|
156
|
-
delay: () => retryDelayMs,
|
|
157
|
-
limit: maxRetries,
|
|
158
|
-
methods: ["post"],
|
|
159
|
-
retryOnTimeout: true,
|
|
160
|
-
statusCodes: RETRYABLE_STATUS_CODES
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
} catch (err) {
|
|
164
|
-
if (isHTTPError(err)) {
|
|
165
|
-
let data = "";
|
|
166
|
-
if (typeof err.data === "string") data = err.data;
|
|
167
|
-
else if (err.data !== void 0) data = JSON.stringify(err.data);
|
|
168
|
-
throw new EventSinkHttpError(err.response.status, `Event sink responded with ${err.response.status}${data ? `: ${data}` : ""}`);
|
|
169
|
-
}
|
|
170
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
171
|
-
}
|
|
125
|
+
function hasQueuedEvents(queue) {
|
|
126
|
+
return queue.length > 0;
|
|
172
127
|
}
|
|
173
|
-
function
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
128
|
+
function positiveOrDefault(value, fallback) {
|
|
129
|
+
return Math.max(1, value ?? fallback);
|
|
130
|
+
}
|
|
131
|
+
function nonNegativeOrDefault(value, fallback) {
|
|
132
|
+
return Math.max(0, value ?? fallback);
|
|
133
|
+
}
|
|
134
|
+
function resolveFetch(fetchImpl) {
|
|
135
|
+
const resolved = fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
136
|
+
if (!resolved) throw new Error("Runner event sink requires fetch support");
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
function assertAuthToken(authToken) {
|
|
140
|
+
if (!authToken.trim()) throw new Error("Runner event sink requires an auth token");
|
|
141
|
+
}
|
|
142
|
+
function postBatchRequest(options, fetchImpl, events) {
|
|
143
|
+
return {
|
|
144
|
+
authHeader: options.authHeader,
|
|
145
|
+
authToken: options.authToken,
|
|
146
|
+
events,
|
|
147
|
+
fetch: fetchImpl,
|
|
148
|
+
maxRetries: nonNegativeOrDefault(options.maxRetries, DEFAULT_MAX_RETRIES),
|
|
149
|
+
retryDelayMs: nonNegativeOrDefault(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS),
|
|
150
|
+
url: options.url
|
|
182
151
|
};
|
|
183
152
|
}
|
|
184
153
|
function timestamp(now) {
|
package/dist/runner.d.ts
CHANGED
|
@@ -80,7 +80,12 @@ interface RunnerLaunchInput {
|
|
|
80
80
|
prompt: string;
|
|
81
81
|
worktreePath: string;
|
|
82
82
|
}
|
|
83
|
-
declare
|
|
83
|
+
declare const RunnerCapabilityError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
84
|
+
readonly _tag: "RunnerCapabilityError";
|
|
85
|
+
} & Readonly<A>;
|
|
86
|
+
declare class RunnerCapabilityError extends RunnerCapabilityError_base<{
|
|
87
|
+
readonly message: string;
|
|
88
|
+
}> {
|
|
84
89
|
constructor(message: string);
|
|
85
90
|
}
|
|
86
91
|
/**
|