@oisincoveney/pipeline 2.8.1 → 2.8.2
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/dist/config/schemas.d.ts +4 -4
- package/dist/gates.js +6 -225
- package/dist/moka-submit.d.ts +6 -6
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runtime/agent-node/agent-node.js +1 -1
- package/dist/runtime/builtins/builtins.js +344 -57
- package/dist/runtime/gates/gates.js +1 -1
- package/package.json +1 -1
package/dist/config/schemas.d.ts
CHANGED
|
@@ -226,8 +226,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
226
226
|
policy: z.ZodOptional<z.ZodObject<{
|
|
227
227
|
commands: z.ZodOptional<z.ZodEnum<{
|
|
228
228
|
allow: "allow";
|
|
229
|
-
"trusted-only": "trusted-only";
|
|
230
229
|
deny: "deny";
|
|
230
|
+
"trusted-only": "trusted-only";
|
|
231
231
|
}>>;
|
|
232
232
|
modules: z.ZodOptional<z.ZodEnum<{
|
|
233
233
|
allow: "allow";
|
|
@@ -255,8 +255,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
255
255
|
global: "global";
|
|
256
256
|
}>>;
|
|
257
257
|
mode: z.ZodEnum<{
|
|
258
|
-
hosted: "hosted";
|
|
259
258
|
local: "local";
|
|
259
|
+
hosted: "hosted";
|
|
260
260
|
}>;
|
|
261
261
|
provider: z.ZodLiteral<"toolhive">;
|
|
262
262
|
authorization_env: z.ZodDefault<z.ZodString>;
|
|
@@ -299,10 +299,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
299
299
|
}, z.core.$strict>>;
|
|
300
300
|
output: z.ZodOptional<z.ZodObject<{
|
|
301
301
|
format: z.ZodEnum<{
|
|
302
|
+
json_schema: "json_schema";
|
|
302
303
|
text: "text";
|
|
303
304
|
json: "json";
|
|
304
305
|
jsonl: "jsonl";
|
|
305
|
-
json_schema: "json_schema";
|
|
306
306
|
}>;
|
|
307
307
|
repair: z.ZodOptional<z.ZodObject<{
|
|
308
308
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -371,10 +371,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
371
371
|
disabled: "disabled";
|
|
372
372
|
}>>>;
|
|
373
373
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
374
|
+
json_schema: "json_schema";
|
|
374
375
|
text: "text";
|
|
375
376
|
json: "json";
|
|
376
377
|
jsonl: "jsonl";
|
|
377
|
-
json_schema: "json_schema";
|
|
378
378
|
}>>>;
|
|
379
379
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
380
380
|
skills: z.ZodOptional<z.ZodBoolean>;
|
package/dist/gates.js
CHANGED
|
@@ -1,231 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { existsSync
|
|
1
|
+
import "./safe-json.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import "execa";
|
|
5
|
+
import "package-manager-detector/commands";
|
|
6
|
+
import "package-manager-detector/detect";
|
|
7
7
|
//#region src/gates.ts
|
|
8
|
-
const FAILING_TEST_RE = /^[✗×✕●]\s+(.+)$/;
|
|
9
|
-
function parseFailingTests(output) {
|
|
10
|
-
return output.split("\n").flatMap((line) => {
|
|
11
|
-
const m = FAILING_TEST_RE.exec(line);
|
|
12
|
-
return m ? [m[1].trim()] : [];
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
function displayCommand(command) {
|
|
16
|
-
return [command.command, ...command.args].join(" ");
|
|
17
|
-
}
|
|
18
|
-
function readPackageScripts(worktreePath) {
|
|
19
|
-
try {
|
|
20
|
-
return parseJson(readFileSync(join(worktreePath, "package.json"), "utf-8"), "package.json").scripts ?? {};
|
|
21
|
-
} catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function envCommand(envName) {
|
|
26
|
-
const raw = process.env[envName]?.trim();
|
|
27
|
-
if (!raw) return null;
|
|
28
|
-
return {
|
|
29
|
-
command: raw,
|
|
30
|
-
args: [],
|
|
31
|
-
shell: true
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
async function resolvePackageScript(worktreePath, scriptName) {
|
|
35
|
-
if (!readPackageScripts(worktreePath)[scriptName]) return null;
|
|
36
|
-
const resolved = resolveCommand(await detectPackageManagerAgent(worktreePath), "run", [scriptName]);
|
|
37
|
-
if (!resolved) return null;
|
|
38
|
-
return {
|
|
39
|
-
command: resolved.command,
|
|
40
|
-
args: resolved.args
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
async function detectPackageManagerAgent(worktreePath) {
|
|
44
|
-
return (await detect({
|
|
45
|
-
cwd: worktreePath,
|
|
46
|
-
stopDir: worktreePath
|
|
47
|
-
}))?.agent ?? "npm";
|
|
48
|
-
}
|
|
49
|
-
async function resolvePackageBinaryCommand(worktreePath, binary, args) {
|
|
50
|
-
if (!existsSync(join(worktreePath, "package.json"))) return null;
|
|
51
|
-
switch (await detectPackageManagerAgent(worktreePath)) {
|
|
52
|
-
case "bun": return {
|
|
53
|
-
command: "bun",
|
|
54
|
-
args: [
|
|
55
|
-
"x",
|
|
56
|
-
binary,
|
|
57
|
-
...args
|
|
58
|
-
]
|
|
59
|
-
};
|
|
60
|
-
case "pnpm": return {
|
|
61
|
-
command: "pnpm",
|
|
62
|
-
args: [
|
|
63
|
-
"exec",
|
|
64
|
-
binary,
|
|
65
|
-
...args
|
|
66
|
-
]
|
|
67
|
-
};
|
|
68
|
-
case "yarn": return {
|
|
69
|
-
command: "yarn",
|
|
70
|
-
args: [
|
|
71
|
-
"exec",
|
|
72
|
-
binary,
|
|
73
|
-
...args
|
|
74
|
-
]
|
|
75
|
-
};
|
|
76
|
-
default: return {
|
|
77
|
-
command: "npx",
|
|
78
|
-
args: [
|
|
79
|
-
"--yes",
|
|
80
|
-
binary,
|
|
81
|
-
...args
|
|
82
|
-
]
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async function runTests(worktreePath, signal) {
|
|
87
|
-
const projectCommand = envCommand("PIPELINE_TEST_COMMAND") ?? await resolvePackageScript(worktreePath, "test");
|
|
88
|
-
if (!projectCommand) return {
|
|
89
|
-
exitCode: 1,
|
|
90
|
-
failingTests: [],
|
|
91
|
-
output: "No test command found. Set PIPELINE_TEST_COMMAND or define a package test script."
|
|
92
|
-
};
|
|
93
|
-
const result = await runProjectCommand(projectCommand, worktreePath, signal);
|
|
94
|
-
return {
|
|
95
|
-
...result,
|
|
96
|
-
failingTests: result.exitCode === 0 ? [] : parseFailingTests(result.output)
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
async function runTypecheck(worktreePath, signal) {
|
|
100
|
-
const projectCommand = envCommand("PIPELINE_TYPECHECK_COMMAND") ?? await resolvePackageScript(worktreePath, "typecheck");
|
|
101
|
-
if (!projectCommand) return {
|
|
102
|
-
exitCode: 0,
|
|
103
|
-
output: "skipped"
|
|
104
|
-
};
|
|
105
|
-
return await runProjectCommand(projectCommand, worktreePath, signal);
|
|
106
|
-
}
|
|
107
|
-
async function runLint(worktreePath, signal) {
|
|
108
|
-
const projectCommand = envCommand("PIPELINE_LINT_COMMAND") ?? await resolvePackageScript(worktreePath, "lint");
|
|
109
|
-
if (!projectCommand) return {
|
|
110
|
-
exitCode: 0,
|
|
111
|
-
output: "skipped"
|
|
112
|
-
};
|
|
113
|
-
return await runProjectCommand(projectCommand, worktreePath, signal, { hidePipelineRuns: true });
|
|
114
|
-
}
|
|
115
|
-
async function runFallow(worktreePath, signal) {
|
|
116
|
-
return runProjectCommand(envCommand("PIPELINE_FALLOW_COMMAND") ?? await resolvePackageScript(worktreePath, "fallow") ?? await resolvePackageBinaryCommand(worktreePath, "fallow", ["audit"]) ?? {
|
|
117
|
-
args: ["audit"],
|
|
118
|
-
command: "fallow"
|
|
119
|
-
}, worktreePath, signal, { hidePipelineRuns: true });
|
|
120
|
-
}
|
|
121
|
-
async function runProjectCommand(projectCommand, worktreePath, signal, options) {
|
|
122
|
-
const hiddenRuns = options?.hidePipelineRuns ? hidePipelineRunsDirectory(worktreePath) : null;
|
|
123
|
-
try {
|
|
124
|
-
const result = await execa(projectCommand.command, projectCommand.args, {
|
|
125
|
-
cancelSignal: signal,
|
|
126
|
-
cwd: worktreePath,
|
|
127
|
-
shell: projectCommand.shell
|
|
128
|
-
});
|
|
129
|
-
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
130
|
-
return {
|
|
131
|
-
command: displayCommand(projectCommand),
|
|
132
|
-
exitCode: result.exitCode ?? 0,
|
|
133
|
-
output
|
|
134
|
-
};
|
|
135
|
-
} catch (err) {
|
|
136
|
-
const e = commandError(err);
|
|
137
|
-
return {
|
|
138
|
-
command: displayCommand(projectCommand),
|
|
139
|
-
exitCode: e.exitCode ?? 1,
|
|
140
|
-
output: commandErrorOutput(e)
|
|
141
|
-
};
|
|
142
|
-
} finally {
|
|
143
|
-
hiddenRuns?.restore();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function hidePipelineRunsDirectory(worktreePath) {
|
|
147
|
-
const pipelineDir = join(worktreePath, ".pipeline");
|
|
148
|
-
const runsDir = join(pipelineDir, "runs");
|
|
149
|
-
if (!existsSync(runsDir)) return null;
|
|
150
|
-
const hiddenRunsDir = join(pipelineDir, `.runs-hidden-${process.pid}-${Date.now()}`);
|
|
151
|
-
renameSync(runsDir, hiddenRunsDir);
|
|
152
|
-
return { restore: () => {
|
|
153
|
-
if (!existsSync(hiddenRunsDir)) return;
|
|
154
|
-
renameSync(hiddenRunsDir, runsDir);
|
|
155
|
-
} };
|
|
156
|
-
}
|
|
157
|
-
function commandError(err) {
|
|
158
|
-
return err;
|
|
159
|
-
}
|
|
160
|
-
function commandErrorOutput(err) {
|
|
161
|
-
return [err.stdout, err.stderr].filter(Boolean).join("\n") || [err.shortMessage, err.message].filter(Boolean).join("\n");
|
|
162
|
-
}
|
|
163
|
-
async function runSemgrep(worktreePath, signal, changedFiles) {
|
|
164
|
-
const overrideCommand = envCommand("PIPELINE_SEMGREP_COMMAND");
|
|
165
|
-
const targets = changedFiles ? [...new Set(changedFiles)].filter((file) => existsSync(join(worktreePath, file))) : void 0;
|
|
166
|
-
if (!overrideCommand && targets && targets.length === 0) return {
|
|
167
|
-
command: "uvx semgrep scan --config=p/ci --error",
|
|
168
|
-
exitCode: 0,
|
|
169
|
-
output: "skipped: no changed files to scan"
|
|
170
|
-
};
|
|
171
|
-
return await runProjectCommand(overrideCommand ?? {
|
|
172
|
-
args: [
|
|
173
|
-
"semgrep",
|
|
174
|
-
"scan",
|
|
175
|
-
"--config=p/ci",
|
|
176
|
-
"--error",
|
|
177
|
-
...targets ? ["--", ...targets] : ["."]
|
|
178
|
-
],
|
|
179
|
-
command: "uvx"
|
|
180
|
-
}, worktreePath, signal);
|
|
181
|
-
}
|
|
182
8
|
function artifactExists(worktreePath, filename) {
|
|
183
9
|
return existsSync(join(worktreePath, filename));
|
|
184
10
|
}
|
|
185
|
-
const JSCPD_DEFAULT_IGNORES = [
|
|
186
|
-
"**/node_modules/**",
|
|
187
|
-
"**/.git/**",
|
|
188
|
-
"**/dist/**",
|
|
189
|
-
"**/coverage/**",
|
|
190
|
-
"**/.next/**",
|
|
191
|
-
"**/.turbo/**",
|
|
192
|
-
"**/.cache/**",
|
|
193
|
-
"**/.serena/**",
|
|
194
|
-
"**/.opencode/**",
|
|
195
|
-
"**/.pipeline/host-resources/**",
|
|
196
|
-
"**/.pipeline/skills/**",
|
|
197
|
-
"**/.agents/skills/**"
|
|
198
|
-
];
|
|
199
|
-
function parseJscpdOutput(output) {
|
|
200
|
-
try {
|
|
201
|
-
return { violations: (parseJson(output, "jscpd output")?.duplicates ?? []).map((dup) => ({
|
|
202
|
-
file: dup?.firstFile?.name ?? "unknown",
|
|
203
|
-
line: dup?.firstFile?.start,
|
|
204
|
-
message: `Duplicate code block detected between ${dup?.firstFile?.name} and ${dup?.secondFile?.name}`
|
|
205
|
-
})) };
|
|
206
|
-
} catch {
|
|
207
|
-
return { violations: [] };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function runJscpd(worktreePath, signal) {
|
|
211
|
-
try {
|
|
212
|
-
return parseJscpdOutput((await execa("bunx", [
|
|
213
|
-
"jscpd",
|
|
214
|
-
"--min-tokens",
|
|
215
|
-
"50",
|
|
216
|
-
"--reporters",
|
|
217
|
-
"json",
|
|
218
|
-
"--gitignore",
|
|
219
|
-
"--ignore",
|
|
220
|
-
JSCPD_DEFAULT_IGNORES.join(","),
|
|
221
|
-
"."
|
|
222
|
-
], {
|
|
223
|
-
cancelSignal: signal,
|
|
224
|
-
cwd: worktreePath
|
|
225
|
-
})).stdout ?? "");
|
|
226
|
-
} catch (err) {
|
|
227
|
-
return parseJscpdOutput(err.stdout ?? "");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
11
|
//#endregion
|
|
231
|
-
export { artifactExists
|
|
12
|
+
export { artifactExists };
|
package/dist/moka-submit.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { z } from "zod";
|
|
|
5
5
|
//#region src/moka-submit.d.ts
|
|
6
6
|
declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
|
|
7
7
|
"workflow.start": "workflow.start";
|
|
8
|
+
"node.finish": "node.finish";
|
|
9
|
+
"node.start": "node.start";
|
|
8
10
|
"workflow.success": "workflow.success";
|
|
9
11
|
"workflow.failure": "workflow.failure";
|
|
10
12
|
"workflow.complete": "workflow.complete";
|
|
11
|
-
"node.start": "node.start";
|
|
12
13
|
"node.success": "node.success";
|
|
13
14
|
"node.error": "node.error";
|
|
14
|
-
"node.finish": "node.finish";
|
|
15
15
|
"gate.failure": "gate.failure";
|
|
16
16
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
17
17
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
94
94
|
}, z.core.$strict>>;
|
|
95
95
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
96
96
|
"workflow.start": "workflow.start";
|
|
97
|
+
"node.finish": "node.finish";
|
|
98
|
+
"node.start": "node.start";
|
|
97
99
|
"workflow.success": "workflow.success";
|
|
98
100
|
"workflow.failure": "workflow.failure";
|
|
99
101
|
"workflow.complete": "workflow.complete";
|
|
100
|
-
"node.start": "node.start";
|
|
101
102
|
"node.success": "node.success";
|
|
102
103
|
"node.error": "node.error";
|
|
103
|
-
"node.finish": "node.finish";
|
|
104
104
|
"gate.failure": "gate.failure";
|
|
105
105
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
106
106
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -206,13 +206,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
206
206
|
}, z.core.$strict>>;
|
|
207
207
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
208
208
|
"workflow.start": "workflow.start";
|
|
209
|
+
"node.finish": "node.finish";
|
|
210
|
+
"node.start": "node.start";
|
|
209
211
|
"workflow.success": "workflow.success";
|
|
210
212
|
"workflow.failure": "workflow.failure";
|
|
211
213
|
"workflow.complete": "workflow.complete";
|
|
212
|
-
"node.start": "node.start";
|
|
213
214
|
"node.success": "node.success";
|
|
214
215
|
"node.error": "node.error";
|
|
215
|
-
"node.finish": "node.finish";
|
|
216
216
|
"gate.failure": "gate.failure";
|
|
217
217
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
218
218
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -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.start": "workflow.start";
|
|
14
13
|
"workflow.planned": "workflow.planned";
|
|
14
|
+
"workflow.start": "workflow.start";
|
|
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";
|
|
60
58
|
"agent.finish": "agent.finish";
|
|
61
59
|
"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.start": "workflow.start";
|
|
184
183
|
"workflow.planned": "workflow.planned";
|
|
184
|
+
"workflow.start": "workflow.start";
|
|
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";
|
|
230
228
|
"agent.finish": "agent.finish";
|
|
231
229
|
"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>;
|
|
@@ -121,7 +121,7 @@ function runHandoffFinalizerEffect(context, node, rawOutput, attempt) {
|
|
|
121
121
|
const result = yield* (yield* AgentNodeRuntimeService).executeRunner(context.executor, plan, { signal: context.signal });
|
|
122
122
|
emitAgentFinish(context, plan, attempt, result);
|
|
123
123
|
return parseHandoff(normalizeAgentOutput(plan, result.stdout).output) ?? synthesizeMinimalHandoff(rawOutput);
|
|
124
|
-
});
|
|
124
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(synthesizeMinimalHandoff(rawOutput))));
|
|
125
125
|
}
|
|
126
126
|
function createHandoffFinalizerPlan(context, node, runner, rawOutput) {
|
|
127
127
|
const finalizerProfileId = `${node.id}:handoff`;
|
|
@@ -1,70 +1,357 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseJson } from "../../safe-json.js";
|
|
2
|
+
import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
|
|
2
3
|
import { executeDrainMergeBuiltin } from "../drain-merge/drain-merge.js";
|
|
3
4
|
import "../drain-merge/index.js";
|
|
4
5
|
import { executeSelectCandidateBuiltin } from "../select-candidate/select-candidate.js";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { resolveCommand } from "package-manager-detector/commands";
|
|
10
|
+
import { detect } from "package-manager-detector/detect";
|
|
5
11
|
//#region src/runtime/builtins/builtins.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
12
|
+
const JSCPD_DEFAULT_IGNORES = [
|
|
13
|
+
"**/node_modules/**",
|
|
14
|
+
"**/.git/**",
|
|
15
|
+
"**/dist/**",
|
|
16
|
+
"**/coverage/**",
|
|
17
|
+
"**/.next/**",
|
|
18
|
+
"**/.turbo/**",
|
|
19
|
+
"**/.cache/**",
|
|
20
|
+
"**/.serena/**",
|
|
21
|
+
"**/.opencode/**",
|
|
22
|
+
"**/.pipeline/host-resources/**",
|
|
23
|
+
"**/.pipeline/skills/**",
|
|
24
|
+
"**/.agents/skills/**"
|
|
25
|
+
];
|
|
26
|
+
const WHITESPACE_RE = /\s/;
|
|
27
|
+
const BUILTIN_HANDLERS = {
|
|
28
|
+
"drain-merge": (context, node) => Effect.tryPromise(() => executeDrainMergeBuiltin(context, node)),
|
|
29
|
+
duplication: (context) => executeDuplicationBuiltinEffect(context),
|
|
30
|
+
fallow: (context) => executeFallowBuiltinEffect(context),
|
|
31
|
+
lint: (context) => executeScriptBuiltinEffect(context, "lint"),
|
|
32
|
+
"select-candidate": (context, node) => Effect.tryPromise(() => executeSelectCandidateBuiltin(context, node)),
|
|
33
|
+
semgrep: (context) => executeSemgrepBuiltinEffect(context),
|
|
34
|
+
test: (context) => executeTestBuiltinEffect(context),
|
|
35
|
+
typecheck: (context) => executeScriptBuiltinEffect(context, "typecheck")
|
|
36
|
+
};
|
|
37
|
+
function executeBuiltin(builtin, context, node) {
|
|
38
|
+
const program = executeBuiltinEffect(builtin, context, node);
|
|
39
|
+
return Effect.runPromise(Effect.provide(program, CommandExecutorLive));
|
|
40
|
+
}
|
|
41
|
+
function executeBuiltinEffect(builtin, context, node) {
|
|
42
|
+
const handler = BUILTIN_HANDLERS[builtin];
|
|
43
|
+
return handler ? handler(context, node) : Effect.succeed(unsupportedBuiltin(builtin));
|
|
44
|
+
}
|
|
45
|
+
function unsupportedBuiltin(builtin) {
|
|
46
|
+
return {
|
|
47
|
+
evidence: [`unsupported builtin '${builtin}'`],
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
output: ""
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function executeTestBuiltinEffect(context) {
|
|
53
|
+
return Effect.gen(function* () {
|
|
54
|
+
const command = yield* resolveRequiredScriptCommand(context.worktreePath, "PIPELINE_TEST_COMMAND", "test");
|
|
55
|
+
if (!command) return missingTestCommandResult();
|
|
56
|
+
const result = yield* executeProjectCommand(command, context);
|
|
57
|
+
return commandBuiltinResult("test", result, result.exitCode === 0 ? [] : parseFailingTests(result.output));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function executeScriptBuiltinEffect(context, builtin) {
|
|
61
|
+
return Effect.gen(function* () {
|
|
62
|
+
const command = yield* resolveRequiredScriptCommand(context.worktreePath, builtinEnvName(builtin), builtin);
|
|
63
|
+
return command ? yield* executeScriptCommandBuiltin(builtin, command, context) : skippedBuiltinResult(builtin);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function executeScriptCommandBuiltin(builtin, command, context) {
|
|
67
|
+
return executeProjectCommand(command, context, builtin === "lint").pipe(Effect.map((result) => commandBuiltinResult(builtin, result)));
|
|
68
|
+
}
|
|
69
|
+
function executeFallowBuiltinEffect(context) {
|
|
70
|
+
return Effect.gen(function* () {
|
|
71
|
+
return commandBuiltinResult("fallow", yield* executeProjectCommand(yield* resolveFallowCommand(context.worktreePath), context, true));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function executeSemgrepBuiltinEffect(context) {
|
|
75
|
+
return Effect.gen(function* () {
|
|
76
|
+
const command = yield* resolveSemgrepCommand(context);
|
|
77
|
+
if (!command) return semgrepNoChangedFilesResult();
|
|
78
|
+
return commandBuiltinResult("semgrep", yield* executeProjectCommand(command, context));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function executeDuplicationBuiltinEffect(context) {
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
return duplicationBuiltinResult(parseJscpdOutput((yield* executeProjectCommand(jscpdCommand(), context)).output));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function executeProjectCommand(command, context, hidePipelineRuns = false) {
|
|
87
|
+
const run = executeVisibleProjectCommand(command, context);
|
|
88
|
+
return hidePipelineRuns ? withHiddenPipelineRuns(context.worktreePath, run) : run;
|
|
89
|
+
}
|
|
90
|
+
function executeVisibleProjectCommand(command, context) {
|
|
91
|
+
return Effect.gen(function* () {
|
|
92
|
+
const result = yield* (yield* CommandExecutor).execute(projectCommandArray(command), context);
|
|
93
|
+
return {
|
|
94
|
+
command: displayCommand(command),
|
|
95
|
+
...result
|
|
62
96
|
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function withHiddenPipelineRuns(worktreePath, effect) {
|
|
100
|
+
return Effect.acquireUseRelease(Effect.sync(() => hidePipelineRunsDirectory(worktreePath)), () => effect, (hiddenRuns) => Effect.sync(() => hiddenRuns?.restore()));
|
|
101
|
+
}
|
|
102
|
+
function resolveRequiredScriptCommand(worktreePath, envName, scriptName) {
|
|
103
|
+
return Effect.gen(function* () {
|
|
104
|
+
return envCommand(envName) ?? (yield* resolvePackageScript(worktreePath, scriptName));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function resolveFallowCommand(worktreePath) {
|
|
108
|
+
return Effect.gen(function* () {
|
|
109
|
+
const script = yield* resolvePackageScript(worktreePath, "fallow");
|
|
110
|
+
const binary = yield* resolvePackageBinaryCommand(worktreePath, "fallow", ["audit"]);
|
|
111
|
+
return envCommand("PIPELINE_FALLOW_COMMAND") ?? script ?? binary ?? fallowCommand();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function resolveSemgrepCommand(context) {
|
|
115
|
+
return Effect.sync(() => {
|
|
116
|
+
const override = envCommand("PIPELINE_SEMGREP_COMMAND");
|
|
117
|
+
const targets = semgrepTargets(context);
|
|
118
|
+
return override ?? semgrepScanCommand(targets);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function resolvePackageScript(worktreePath, scriptName) {
|
|
122
|
+
return Effect.gen(function* () {
|
|
123
|
+
if (!readPackageScripts(worktreePath)[scriptName]) return null;
|
|
124
|
+
return packageManagerCommand(yield* detectPackageManagerAgent(worktreePath), [scriptName]);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function resolvePackageBinaryCommand(worktreePath, binary, args) {
|
|
128
|
+
return Effect.gen(function* () {
|
|
129
|
+
if (!existsSync(join(worktreePath, "package.json"))) return null;
|
|
130
|
+
return packageBinaryCommand(yield* detectPackageManagerAgent(worktreePath), binary, args);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function detectPackageManagerAgent(worktreePath) {
|
|
134
|
+
return Effect.tryPromise(async () => {
|
|
135
|
+
return (await detect({
|
|
136
|
+
cwd: worktreePath,
|
|
137
|
+
stopDir: worktreePath
|
|
138
|
+
}))?.agent ?? "npm";
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function packageManagerCommand(agent, args) {
|
|
142
|
+
const resolved = resolveCommand(agent, "run", args);
|
|
143
|
+
return resolved ? {
|
|
144
|
+
args: resolved.args,
|
|
145
|
+
command: resolved.command
|
|
146
|
+
} : null;
|
|
147
|
+
}
|
|
148
|
+
function packageBinaryCommand(agent, binary, args) {
|
|
149
|
+
return {
|
|
150
|
+
bun: {
|
|
151
|
+
args: [
|
|
152
|
+
"x",
|
|
153
|
+
binary,
|
|
154
|
+
...args
|
|
155
|
+
],
|
|
156
|
+
command: "bun"
|
|
157
|
+
},
|
|
158
|
+
pnpm: {
|
|
159
|
+
args: [
|
|
160
|
+
"exec",
|
|
161
|
+
binary,
|
|
162
|
+
...args
|
|
163
|
+
],
|
|
164
|
+
command: "pnpm"
|
|
165
|
+
},
|
|
166
|
+
yarn: {
|
|
167
|
+
args: [
|
|
168
|
+
"exec",
|
|
169
|
+
binary,
|
|
170
|
+
...args
|
|
171
|
+
],
|
|
172
|
+
command: "yarn"
|
|
173
|
+
}
|
|
174
|
+
}[String(agent)] ?? {
|
|
175
|
+
args: [
|
|
176
|
+
"--yes",
|
|
177
|
+
binary,
|
|
178
|
+
...args
|
|
179
|
+
],
|
|
180
|
+
command: "npx"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function envCommand(envName) {
|
|
184
|
+
const raw = process.env[envName]?.trim();
|
|
185
|
+
return raw ? envProjectCommand(raw) : null;
|
|
186
|
+
}
|
|
187
|
+
function envProjectCommand(raw) {
|
|
188
|
+
return WHITESPACE_RE.test(raw) ? shellProjectCommand(raw) : executableProjectCommand(raw);
|
|
189
|
+
}
|
|
190
|
+
function executableProjectCommand(command) {
|
|
191
|
+
return {
|
|
192
|
+
args: [],
|
|
193
|
+
command
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function shellProjectCommand(command) {
|
|
197
|
+
return {
|
|
198
|
+
args: ["-c", command],
|
|
199
|
+
command: "sh",
|
|
200
|
+
display: command
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function readPackageScripts(worktreePath) {
|
|
204
|
+
const text = readPackageJsonText(worktreePath);
|
|
205
|
+
return text ? packageScriptsFromJson(text) : {};
|
|
206
|
+
}
|
|
207
|
+
function readPackageJsonText(worktreePath) {
|
|
208
|
+
try {
|
|
209
|
+
return readFileSync(join(worktreePath, "package.json"), "utf-8");
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
63
212
|
}
|
|
64
213
|
}
|
|
214
|
+
function packageScriptsFromJson(text) {
|
|
215
|
+
return parseJson(text, "package.json").scripts ?? {};
|
|
216
|
+
}
|
|
65
217
|
function runtimeChangedFiles(context) {
|
|
66
218
|
return [...new Set(context.nodeStateStore.changedFilesForAllNodes())].sort();
|
|
67
219
|
}
|
|
220
|
+
function semgrepTargets(context) {
|
|
221
|
+
const targets = runtimeChangedFiles(context).filter((file) => existsSync(join(context.worktreePath, file)));
|
|
222
|
+
return targets.length === 0 ? void 0 : targets;
|
|
223
|
+
}
|
|
224
|
+
function semgrepScanCommand(targets) {
|
|
225
|
+
if (!targets) return null;
|
|
226
|
+
return {
|
|
227
|
+
args: [
|
|
228
|
+
"semgrep",
|
|
229
|
+
"scan",
|
|
230
|
+
"--config=p/ci",
|
|
231
|
+
"--error",
|
|
232
|
+
"--",
|
|
233
|
+
...targets
|
|
234
|
+
],
|
|
235
|
+
command: "uvx"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function jscpdCommand() {
|
|
239
|
+
return {
|
|
240
|
+
args: [
|
|
241
|
+
"jscpd",
|
|
242
|
+
"--min-tokens",
|
|
243
|
+
"50",
|
|
244
|
+
"--reporters",
|
|
245
|
+
"json",
|
|
246
|
+
"--gitignore",
|
|
247
|
+
"--ignore",
|
|
248
|
+
JSCPD_DEFAULT_IGNORES.join(","),
|
|
249
|
+
"."
|
|
250
|
+
],
|
|
251
|
+
command: "bunx"
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function fallowCommand() {
|
|
255
|
+
return {
|
|
256
|
+
args: ["audit"],
|
|
257
|
+
command: "fallow"
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function projectCommandArray(command) {
|
|
261
|
+
return [command.command, ...command.args];
|
|
262
|
+
}
|
|
263
|
+
function displayCommand(command) {
|
|
264
|
+
return command.display ?? projectCommandArray(command).join(" ");
|
|
265
|
+
}
|
|
266
|
+
function builtinEnvName(builtin) {
|
|
267
|
+
return `PIPELINE_${builtin.toUpperCase()}_COMMAND`;
|
|
268
|
+
}
|
|
269
|
+
function missingTestCommandResult() {
|
|
270
|
+
return {
|
|
271
|
+
evidence: ["No test command found. Set PIPELINE_TEST_COMMAND or define a package test script."],
|
|
272
|
+
exitCode: 1,
|
|
273
|
+
output: "No test command found. Set PIPELINE_TEST_COMMAND or define a package test script."
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function skippedBuiltinResult(builtin) {
|
|
277
|
+
return {
|
|
278
|
+
evidence: builtinCommandEvidence(builtin, {
|
|
279
|
+
exitCode: 0,
|
|
280
|
+
output: "skipped"
|
|
281
|
+
}),
|
|
282
|
+
exitCode: 0,
|
|
283
|
+
output: "skipped"
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function semgrepNoChangedFilesResult() {
|
|
287
|
+
return commandBuiltinResult("semgrep", {
|
|
288
|
+
command: "uvx semgrep scan --config=p/ci --error",
|
|
289
|
+
exitCode: 0,
|
|
290
|
+
output: "skipped: no changed files to scan"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function duplicationBuiltinResult(violations) {
|
|
294
|
+
return {
|
|
295
|
+
evidence: violations.map((violation) => violation.message),
|
|
296
|
+
exitCode: violations.length === 0 ? 0 : 1,
|
|
297
|
+
output: JSON.stringify(violations)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function commandBuiltinResult(builtin, result, extraEvidence = []) {
|
|
301
|
+
return {
|
|
302
|
+
evidence: [...builtinCommandEvidence(builtin, result), ...extraEvidence],
|
|
303
|
+
exitCode: result.exitCode,
|
|
304
|
+
output: result.output
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function hidePipelineRunsDirectory(worktreePath) {
|
|
308
|
+
const paths = pipelineRunsPaths(worktreePath);
|
|
309
|
+
if (!existsSync(paths.runs)) return null;
|
|
310
|
+
renameSync(paths.runs, paths.hidden);
|
|
311
|
+
return { restore: () => restoreHiddenRuns(paths) };
|
|
312
|
+
}
|
|
313
|
+
function pipelineRunsPaths(worktreePath) {
|
|
314
|
+
const pipelineDir = join(worktreePath, ".pipeline");
|
|
315
|
+
return {
|
|
316
|
+
hidden: join(pipelineDir, `.runs-hidden-${process.pid}-${Date.now()}`),
|
|
317
|
+
runs: join(pipelineDir, "runs")
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function restoreHiddenRuns(paths) {
|
|
321
|
+
if (existsSync(paths.hidden)) renameSync(paths.hidden, paths.runs);
|
|
322
|
+
}
|
|
323
|
+
const FAILING_TEST_RE = /^[✗×✕●]\s+(.+)$/;
|
|
324
|
+
function parseFailingTests(output) {
|
|
325
|
+
return output.split("\n").flatMap(parseFailingTestLine);
|
|
326
|
+
}
|
|
327
|
+
function parseFailingTestLine(line) {
|
|
328
|
+
const match = FAILING_TEST_RE.exec(line);
|
|
329
|
+
return match ? [match[1].trim()] : [];
|
|
330
|
+
}
|
|
331
|
+
function parseJscpdOutput(output) {
|
|
332
|
+
try {
|
|
333
|
+
return (parseJson(output, "jscpd output").duplicates ?? []).map(jscpdDuplicateViolation);
|
|
334
|
+
} catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function jscpdDuplicateViolation(dup) {
|
|
339
|
+
const firstFile = jscpdFirstFileName(dup);
|
|
340
|
+
return {
|
|
341
|
+
file: firstFile ?? "unknown",
|
|
342
|
+
line: jscpdFirstFileStart(dup),
|
|
343
|
+
message: `Duplicate code block detected between ${firstFile} and ${jscpdSecondFileName(dup)}`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function jscpdFirstFileName(dup) {
|
|
347
|
+
return dup.firstFile?.name;
|
|
348
|
+
}
|
|
349
|
+
function jscpdFirstFileStart(dup) {
|
|
350
|
+
return dup.firstFile?.start;
|
|
351
|
+
}
|
|
352
|
+
function jscpdSecondFileName(dup) {
|
|
353
|
+
return dup.secondFile?.name;
|
|
354
|
+
}
|
|
68
355
|
function builtinCommandEvidence(builtin, result) {
|
|
69
356
|
const command = result.command ? `: ${result.command}` : "";
|
|
70
357
|
return [`builtin '${builtin}' exited ${result.exitCode}${command}`, result.output || `builtin '${builtin}' produced no output`];
|
|
@@ -5,9 +5,9 @@ import "../json-validation/index.js";
|
|
|
5
5
|
import { emitGateFinish, emitGateStart, runtimeSystemId } from "../events/events.js";
|
|
6
6
|
import "../events/index.js";
|
|
7
7
|
import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
|
|
8
|
-
import { artifactExists } from "../../gates.js";
|
|
9
8
|
import { executeBuiltin } from "../builtins/builtins.js";
|
|
10
9
|
import "../builtins/index.js";
|
|
10
|
+
import { artifactExists } from "../../gates.js";
|
|
11
11
|
import { Effect } from "effect";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import micromatch from "micromatch";
|
package/package.json
CHANGED
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"prepack": "bun run build:cli"
|
|
127
127
|
},
|
|
128
128
|
"type": "module",
|
|
129
|
-
"version": "2.8.
|
|
129
|
+
"version": "2.8.2",
|
|
130
130
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
131
131
|
"main": "./dist/index.js",
|
|
132
132
|
"types": "./dist/index.d.ts",
|