@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.
@@ -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 { parseJson } from "./safe-json.js";
2
- import { existsSync, readFileSync, renameSync } from "node:fs";
1
+ import "./safe-json.js";
2
+ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { execa } from "execa";
5
- import { resolveCommand } from "package-manager-detector/commands";
6
- import { detect } from "package-manager-detector/detect";
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, runFallow, runJscpd, runLint, runSemgrep, runTests, runTypecheck };
12
+ export { artifactExists };
@@ -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 { runFallow, runJscpd, runLint, runSemgrep, runTests, runTypecheck } from "../../gates.js";
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
- async function executeBuiltin(builtin, context, node) {
7
- switch (builtin) {
8
- case "drain-merge": return executeDrainMergeBuiltin(context, node);
9
- case "select-candidate": return executeSelectCandidateBuiltin(context, node);
10
- case "test": {
11
- const result = await runTests(context.worktreePath, context.signal);
12
- return {
13
- evidence: [...builtinCommandEvidence("test", result), ...result.failingTests],
14
- exitCode: result.exitCode,
15
- output: result.output
16
- };
17
- }
18
- case "typecheck": {
19
- const result = await runTypecheck(context.worktreePath, context.signal);
20
- return {
21
- evidence: builtinCommandEvidence("typecheck", result),
22
- exitCode: result.exitCode,
23
- output: result.output
24
- };
25
- }
26
- case "lint": {
27
- const result = await runLint(context.worktreePath, context.signal);
28
- return {
29
- evidence: builtinCommandEvidence("lint", result),
30
- exitCode: result.exitCode,
31
- output: result.output
32
- };
33
- }
34
- case "fallow": {
35
- const result = await runFallow(context.worktreePath, context.signal);
36
- return {
37
- evidence: builtinCommandEvidence("fallow", result),
38
- exitCode: result.exitCode,
39
- output: result.output
40
- };
41
- }
42
- case "duplication": {
43
- const result = await runJscpd(context.worktreePath, context.signal);
44
- return {
45
- evidence: result.violations.map((violation) => violation.message),
46
- exitCode: result.violations.length === 0 ? 0 : 1,
47
- output: JSON.stringify(result.violations)
48
- };
49
- }
50
- case "semgrep": {
51
- const result = await runSemgrep(context.worktreePath, context.signal, runtimeChangedFiles(context));
52
- return {
53
- evidence: builtinCommandEvidence("semgrep", result),
54
- exitCode: result.exitCode,
55
- output: result.output
56
- };
57
- }
58
- default: return {
59
- evidence: [`unsupported builtin '${builtin}'`],
60
- exitCode: 1,
61
- output: ""
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.1",
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",