@oisincoveney/pipeline 2.8.0 → 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.
Files changed (38) hide show
  1. package/dist/argo-submit.d.ts +2 -4
  2. package/dist/argo-submit.js +80 -80
  3. package/dist/cluster-doctor.js +89 -101
  4. package/dist/config/defaults.js +9 -19
  5. package/dist/config/load.js +32 -39
  6. package/dist/config/schemas.d.ts +1 -1
  7. package/dist/gates.js +6 -225
  8. package/dist/mcp/gateway-error.js +15 -0
  9. package/dist/mcp/gateway.js +119 -220
  10. package/dist/moka-global-config.js +20 -20
  11. package/dist/moka-submit.d.ts +1 -1
  12. package/dist/pipeline-runtime.js +580 -371
  13. package/dist/run-state/git-refs.js +124 -94
  14. package/dist/runner-command-contract.d.ts +2 -2
  15. package/dist/runner-event-sink.js +37 -69
  16. package/dist/runtime/agent-node/agent-node.js +214 -173
  17. package/dist/runtime/builtins/builtins.js +344 -57
  18. package/dist/runtime/changed-files/changed-files.js +15 -27
  19. package/dist/runtime/changed-files/index.js +2 -0
  20. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  21. package/dist/runtime/gates/gates.js +46 -28
  22. package/dist/runtime/hooks/hooks.js +74 -29
  23. package/dist/runtime/opencode-server.js +27 -21
  24. package/dist/runtime/opencode-session-executor.js +101 -44
  25. package/dist/runtime/parallel-node/parallel-node.js +24 -5
  26. package/dist/runtime/select-candidate/select-candidate.js +45 -29
  27. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  28. package/dist/runtime/services/command-executor-service.js +8 -0
  29. package/dist/runtime/services/config-io-service.js +42 -0
  30. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  31. package/dist/runtime/services/git-porcelain-service.js +38 -0
  32. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  33. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  34. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  35. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  36. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  37. package/dist/runtime/services/select-candidate-service.js +13 -0
  38. package/package.json +1 -1
@@ -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`];
@@ -1,35 +1,23 @@
1
+ import { GitPorcelainService, GitPorcelainServiceLive } from "../services/git-porcelain-service.js";
2
+ import { Effect } from "effect";
1
3
  import { existsSync, readFileSync } from "node:fs";
2
4
  import { createHash } from "node:crypto";
3
5
  import { join } from "node:path";
4
- import { execFileSync } from "node:child_process";
5
6
  //#region src/runtime/changed-files/changed-files.ts
6
7
  function snapshotChangedFiles(worktreePath) {
7
- try {
8
- const stdout = execFileSync("git", [
9
- "status",
10
- "--porcelain=v1",
11
- "--untracked-files=all",
12
- "-z"
13
- ], {
14
- cwd: worktreePath,
15
- encoding: "utf8",
16
- stdio: [
17
- "ignore",
18
- "pipe",
19
- "ignore"
20
- ]
21
- });
22
- const files = new Set(parsePorcelainStatus(stdout));
23
- return {
24
- files,
25
- fingerprints: new Map([...files].map((file) => [file, fileFingerprint(worktreePath, file)]))
26
- };
27
- } catch {
28
- return {
29
- files: /* @__PURE__ */ new Set(),
30
- fingerprints: /* @__PURE__ */ new Map()
31
- };
32
- }
8
+ return Effect.runSync(Effect.provide(snapshotChangedFilesEffect(worktreePath), GitPorcelainServiceLive));
9
+ }
10
+ function snapshotChangedFilesEffect(worktreePath) {
11
+ return Effect.gen(function* () {
12
+ const stdout = yield* (yield* GitPorcelainService).statusPorcelain(worktreePath).pipe(Effect.catchAll(() => Effect.succeed("")));
13
+ return changedFilesSnapshot(worktreePath, new Set(parsePorcelainStatus(stdout)));
14
+ });
15
+ }
16
+ function changedFilesSnapshot(worktreePath, files) {
17
+ return {
18
+ files,
19
+ fingerprints: new Map([...files].map((file) => [file, fileFingerprint(worktreePath, file)]))
20
+ };
33
21
  }
34
22
  function parsePorcelainStatus(stdout) {
35
23
  const entries = stdout.split("\0").filter(Boolean);
@@ -0,0 +1,2 @@
1
+ import "./changed-files.js";
2
+ export {};