@oisincoveney/pipeline 3.3.3 → 3.4.0

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.
@@ -10,8 +10,7 @@ function resolveMokaRun(input) {
10
10
  const effort = flags.effort ?? "normal";
11
11
  const target = flags.target ?? "local";
12
12
  const mode = flags.readOnly ? "read" : "write";
13
- if (flags.command && target !== "remote") throw new Error("--command requires --target remote");
14
- if (flags.detach && target !== "local") throw new Error("--detach requires --target local");
13
+ assertFlagTargetCompatibility(flags, target);
15
14
  return {
16
15
  effort,
17
16
  execution: target === "remote" ? resolveRemoteSubmit(flags, effort) : resolveLocalRuntime(flags, effort),
@@ -19,6 +18,10 @@ function resolveMokaRun(input) {
19
18
  target
20
19
  };
21
20
  }
21
+ function assertFlagTargetCompatibility(flags, target) {
22
+ if (flags.command && target !== "remote") throw new Error("--command requires --target remote");
23
+ if (flags.detach && target !== "local") throw new Error("--detach requires --target local");
24
+ }
22
25
  function resolveRemoteSubmit(flags, effort) {
23
26
  return {
24
27
  command: flags.command,
@@ -27,31 +30,37 @@ function resolveRemoteSubmit(flags, effort) {
27
30
  schedule: flags.schedule
28
31
  };
29
32
  }
30
- function resolveLocalRuntime(flags, effort) {
31
- if (flags.schedule) return {
33
+ const LOCAL_RUNTIME_RESOLVERS = [
34
+ (flags) => flags.schedule ? {
32
35
  kind: "local-runtime",
33
36
  schedule: flags.schedule
34
- };
35
- if (flags.workflow) return {
37
+ } : void 0,
38
+ (flags) => flags.workflow ? {
36
39
  kind: "local-runtime",
37
40
  workflow: flags.workflow
38
- };
39
- if (flags.readOnly) return {
41
+ } : void 0,
42
+ (flags) => flags.readOnly ? {
40
43
  kind: "local-runtime",
41
44
  workflow: "inspect"
42
- };
43
- if (flags.entrypoint) return {
45
+ } : void 0,
46
+ (flags) => flags.entrypoint ? {
44
47
  entrypoint: flags.entrypoint,
45
48
  kind: "local-runtime"
46
- };
47
- if (effort === "quick") return {
49
+ } : void 0,
50
+ (_flags, effort) => effort === "quick" ? {
48
51
  entrypoint: "quick",
49
52
  kind: "local-runtime"
50
- };
51
- if (effort === "thorough") return {
53
+ } : void 0,
54
+ (_flags, effort) => effort === "thorough" ? {
52
55
  entrypoint: "execute",
53
56
  kind: "local-runtime"
54
- };
57
+ } : void 0
58
+ ];
59
+ function resolveLocalRuntime(flags, effort) {
60
+ for (const resolve of LOCAL_RUNTIME_RESOLVERS) {
61
+ const resolved = resolve(flags, effort);
62
+ if (resolved) return resolved;
63
+ }
55
64
  return { kind: "local-runtime" };
56
65
  }
57
66
  //#endregion
@@ -35,6 +35,7 @@ function durabilityField(durability) {
35
35
  function pipe83Fields(pipeline) {
36
36
  const keys = [
37
37
  "context_handoff",
38
+ "delivery",
38
39
  "parallel_worktrees",
39
40
  "repo_map"
40
41
  ];
@@ -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
- deny: "deny";
230
229
  "trusted-only": "trusted-only";
230
+ deny: "deny";
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
- local: "local";
259
258
  hosted: "hosted";
259
+ local: "local";
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";
303
302
  text: "text";
304
303
  json: "json";
305
304
  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";
375
374
  text: "text";
376
375
  json: "json";
377
376
  jsonl: "jsonl";
377
+ json_schema: "json_schema";
378
378
  }>>>;
379
379
  rules: z.ZodOptional<z.ZodBoolean>;
380
380
  skills: z.ZodOptional<z.ZodBoolean>;
@@ -505,6 +505,12 @@ declare const configSchema: z.ZodObject<{
505
505
  enabled: z.ZodDefault<z.ZodBoolean>;
506
506
  model: z.ZodOptional<z.ZodString>;
507
507
  }, z.core.$strict>>;
508
+ delivery: z.ZodOptional<z.ZodObject<{
509
+ pull_request: z.ZodOptional<z.ZodObject<{
510
+ enabled: z.ZodDefault<z.ZodBoolean>;
511
+ label: z.ZodDefault<z.ZodString>;
512
+ }, z.core.$strict>>;
513
+ }, z.core.$strict>>;
508
514
  durability: z.ZodOptional<z.ZodObject<{
509
515
  dir: z.ZodDefault<z.ZodString>;
510
516
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -475,14 +475,23 @@ const repoMapSchema = z.object({
475
475
  enabled: z.boolean().default(false),
476
476
  token_budget: z.number().int().positive().default(2e3)
477
477
  }).strict();
478
+ const deliverySchema = z.object({ pull_request: z.object({
479
+ enabled: z.boolean().default(false),
480
+ label: z.string().min(1).default("preview")
481
+ }).strict().optional() }).strict();
478
482
  const pipelineFileSchema = z.object({
479
483
  default_workflow: z.string(),
484
+ context_handoff: contextHandoffSchema.optional(),
485
+ delivery: deliverySchema.optional(),
486
+ durability: durabilitySchema.optional(),
480
487
  entrypoints: strictRecord(entrypointSchema).default({}),
481
488
  hooks: hooksConfigSchema.default({
482
489
  functions: {},
483
490
  on: {}
484
491
  }),
485
492
  orchestrator: orchestratorSchema.optional(),
493
+ parallel_worktrees: parallelWorktreesSchema.optional(),
494
+ repo_map: repoMapSchema.optional(),
486
495
  runner_command: runnerCommandConfigSchema.default({
487
496
  environment: {
488
497
  setup: [],
@@ -496,10 +505,6 @@ const pipelineFileSchema = z.object({
496
505
  }),
497
506
  schedules: strictRecord(schedulePolicySchema).default({}),
498
507
  task_context: taskContextResolverSchema.optional(),
499
- context_handoff: contextHandoffSchema.optional(),
500
- durability: durabilitySchema.optional(),
501
- parallel_worktrees: parallelWorktreesSchema.optional(),
502
- repo_map: repoMapSchema.optional(),
503
508
  token_budget: tokenBudgetSchema.default(DEFAULT_TOKEN_BUDGET),
504
509
  workflows: strictRecord(workflowSchema).default({}),
505
510
  version: z.literal(1)
@@ -532,6 +537,7 @@ const configSchema = z.object({
532
537
  skills: strictRecord(pathRefSchema).default({}),
533
538
  task_context: taskContextResolverSchema.optional(),
534
539
  context_handoff: contextHandoffSchema.optional(),
540
+ delivery: deliverySchema.optional(),
535
541
  durability: durabilitySchema.optional(),
536
542
  parallel_worktrees: parallelWorktreesSchema.optional(),
537
543
  repo_map: repoMapSchema.optional(),
@@ -175,6 +175,15 @@ function configWithSubmitHooks(config, hooks) {
175
175
  }
176
176
  };
177
177
  }
178
+ function withPullRequestDelivery(config, delivery) {
179
+ return {
180
+ ...config,
181
+ delivery: { pull_request: {
182
+ enabled: delivery.pullRequest === true,
183
+ label: config.delivery?.pull_request?.label ?? "preview"
184
+ } }
185
+ };
186
+ }
178
187
  function submitMoka(rawOptions, dependencies = {}) {
179
188
  const { config, worktreePath, ...schemaOptions } = rawOptions;
180
189
  const options = mokaSubmitOptionsSchema.parse(schemaOptions);
@@ -256,7 +265,7 @@ async function graphScheduleYaml(options, dependencies, runId, task) {
256
265
  if (explicitScheduleYaml) return explicitScheduleYaml;
257
266
  const worktreePath = requireScheduleWorktreePath(options);
258
267
  return readScheduleFile(dependencies, resolve(worktreePath, (await (dependencies.generateSchedule ?? generateScheduleArtifact)({
259
- config: options.config,
268
+ config: withPullRequestDelivery(options.config, options.delivery),
260
269
  entrypointId: options.mode === "quick" ? "quick" : "execute",
261
270
  runId,
262
271
  task,
@@ -598,7 +598,7 @@ function remediateUpstreamImplementationFailure(input) {
598
598
  }
599
599
  function remediatePassedImplementationAncestors(input) {
600
600
  return Effect.gen(function* () {
601
- const implementationNodes = upstreamImplementationNodes(input.context, input.node).filter((candidate) => input.context.nodeStateStore.getNodeState(candidate.id)?.status === "passed");
601
+ const implementationNodes = upstreamImplementationNodes(input.context, input.node);
602
602
  if (implementationNodes.length === 0) return false;
603
603
  let remediated = false;
604
604
  for (const implementationNode of implementationNodes) if (yield* remediateImplementationAncestor(input, implementationNode)) remediated = true;
@@ -740,7 +740,19 @@ function visitImplementationDependencies(candidate, visit) {
740
740
  for (const need of candidate.needs) visit(need);
741
741
  }
742
742
  function appendImplementationNode(context, ordered, candidate) {
743
- if (hasSchedulingRole(context, candidate, "implementation")) ordered.push(candidate);
743
+ if (!nodeStatePassed(context, candidate.id)) return;
744
+ pushIfImplementation(context, ordered, candidate);
745
+ for (const child of candidate.children ?? []) appendPassedImplementationChild(context, ordered, child);
746
+ }
747
+ function appendPassedImplementationChild(context, ordered, child) {
748
+ pushIfImplementation(context, ordered, child);
749
+ for (const grandchild of child.children ?? []) appendPassedImplementationChild(context, ordered, grandchild);
750
+ }
751
+ function pushIfImplementation(context, ordered, node) {
752
+ if (hasSchedulingRole(context, node, "implementation")) ordered.push(node);
753
+ }
754
+ function nodeStatePassed(context, nodeId) {
755
+ return context.nodeStateStore.getNodeState(nodeId)?.status === "passed";
744
756
  }
745
757
  function hasSchedulingRole(context, node, role) {
746
758
  return node.profile ? context.config.profiles[node.profile]?.scheduling_roles?.includes(role) ?? false : false;
@@ -14,6 +14,7 @@ import { integrateParallelWriteFanout } from "../schedule/passes/drain-merge.js"
14
14
  import { canonicalizeGeneratedScheduleIds } from "../schedule/passes/ids.js";
15
15
  import { SCHEDULE_PASS_ORDER } from "../schedule/passes/index.js";
16
16
  import { applyNodeCatalogModelFallbacks } from "../schedule/passes/models.js";
17
+ import { appendPullRequestDelivery } from "../schedule/passes/open-pull-request.js";
17
18
  import { namespaceScheduleWorkflows } from "../schedule/passes/references.js";
18
19
  import { plannerPrompt, plannerRepairPrompt } from "../schedule/prompts.js";
19
20
  import { parseDocument, stringify } from "yaml";
@@ -32,6 +33,7 @@ const SCHEDULE_BUILTINS = [
32
33
  "duplication",
33
34
  "fallow",
34
35
  "lint",
36
+ "open-pull-request",
35
37
  "semgrep",
36
38
  "test",
37
39
  "typecheck"
@@ -94,7 +96,7 @@ async function generateScheduleArtifact(options) {
94
96
  const planningContext = { ...loadBacklogPlanningContext(options.task, options.worktreePath) };
95
97
  const generatedArtifact = await planScheduleArtifact(baseline, policy.planner_profile, options, planningContext);
96
98
  assertSchedulePassOrder();
97
- const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog, integrateParallelWriteFanout(options.config, addGeneratedImplementationCoverage(options.config, generatedArtifact)))), planningContext);
99
+ const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog, appendPullRequestDelivery(options.config, integrateParallelWriteFanout(options.config, addGeneratedImplementationCoverage(options.config, generatedArtifact))))), planningContext);
98
100
  validateScheduleArtifact(options.config, artifact, planningContext);
99
101
  compileScheduleArtifact(options.config, artifact, options.worktreePath);
100
102
  return {
@@ -106,6 +108,7 @@ function assertSchedulePassOrder() {
106
108
  if (SCHEDULE_PASS_ORDER.join("\0") !== [
107
109
  "coverage",
108
110
  "drain-merge",
111
+ "delivery",
109
112
  "models",
110
113
  "ids",
111
114
  "references"
@@ -3,6 +3,8 @@ import { acquireRunStateLock } from "../../run-control/run-state-lock.js";
3
3
  import { executeDrainMergeBuiltin } from "../drain-merge/drain-merge.js";
4
4
  import "../drain-merge/index.js";
5
5
  import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
6
+ import { executeOpenPullRequestBuiltin } from "../open-pull-request/open-pull-request.js";
7
+ import "../open-pull-request/index.js";
6
8
  import { Effect } from "effect";
7
9
  import { existsSync, readFileSync, renameSync } from "node:fs";
8
10
  import { join } from "node:path";
@@ -26,6 +28,7 @@ const JSCPD_DEFAULT_IGNORES = [
26
28
  const WHITESPACE_RE = /\s/;
27
29
  const BUILTIN_HANDLERS = {
28
30
  "drain-merge": (context, node) => Effect.tryPromise(() => executeDrainMergeBuiltin(context, node)),
31
+ "open-pull-request": (context, node) => Effect.tryPromise(() => executeOpenPullRequestBuiltin(context, node)),
29
32
  duplication: (context) => executeDuplicationBuiltinEffect(context),
30
33
  fallow: (context) => executeFallowBuiltinEffect(context),
31
34
  lint: (context) => executeScriptBuiltinEffect(context, "lint"),
@@ -0,0 +1,2 @@
1
+ import "./open-pull-request.js";
2
+ export {};
@@ -0,0 +1,186 @@
1
+ import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
2
+ import { OpenPullRequestGitService, OpenPullRequestGitServiceLive } from "../services/open-pull-request-git-service.js";
3
+ import { Effect, Layer } from "effect";
4
+ //#region src/runtime/open-pull-request/open-pull-request.ts
5
+ const INVALID_REF_CHAR_RE = /[^a-zA-Z0-9/_.-]/g;
6
+ const PR_ALREADY_EXISTS_RE = /already exists/i;
7
+ const NEWLINE_RE = /\r?\n/;
8
+ function executeOpenPullRequestBuiltin(context, _node) {
9
+ const merged = Layer.merge(OpenPullRequestGitServiceLive, CommandExecutorLive);
10
+ return Effect.runPromise(Effect.provide(openPullRequestProgram(context), merged));
11
+ }
12
+ function openPullRequestProgram(context) {
13
+ return Effect.gen(function* () {
14
+ const git = yield* (yield* OpenPullRequestGitService).create(context.worktreePath);
15
+ const prCtx = yield* Effect.either(resolveOpenPrContext(git, context));
16
+ if (prCtx._tag === "Left") return openPrFailure(errorMessage(prCtx.left));
17
+ return yield* executeOpenPr(git, prCtx.right, context);
18
+ });
19
+ }
20
+ function resolveOpenPrContext(git, context) {
21
+ return Effect.gen(function* () {
22
+ const baseBranch = yield* resolveDefaultBranch(git, context);
23
+ const headBranch = resolveHeadBranch(context.runId);
24
+ return {
25
+ baseBranch,
26
+ committer: context.config.runner_command.git.committer,
27
+ headBranch,
28
+ label: context.config.delivery?.pull_request?.label ?? "preview",
29
+ runId: context.runId ?? "local",
30
+ task: context.task
31
+ };
32
+ });
33
+ }
34
+ function resolveDefaultBranch(git, context) {
35
+ return git.raw([
36
+ "symbolic-ref",
37
+ "--short",
38
+ "refs/remotes/origin/HEAD"
39
+ ]).pipe(Effect.map((ref) => stripOriginPrefix(ref.trim())), Effect.catchAll(() => resolveCurrentBranch(git, context)));
40
+ }
41
+ function stripOriginPrefix(ref) {
42
+ return ref.startsWith("origin/") ? ref.slice(7) : ref;
43
+ }
44
+ function resolveCurrentBranch(git, context) {
45
+ return git.raw([
46
+ "rev-parse",
47
+ "--abbrev-ref",
48
+ "HEAD"
49
+ ]).pipe(Effect.map((ref) => ref.trim()), Effect.catchAll(() => Effect.succeed(fallbackBranch(context))));
50
+ }
51
+ function fallbackBranch(context) {
52
+ return context.runId ? `moka/run/${context.runId}` : "main";
53
+ }
54
+ function resolveHeadBranch(runId) {
55
+ return `moka/run/${runId ?? "local"}`.replace(INVALID_REF_CHAR_RE, "-");
56
+ }
57
+ function executeOpenPr(git, prCtx, context) {
58
+ return Effect.gen(function* () {
59
+ const prepareResult = yield* Effect.either(prepareHeadBranch(git, prCtx));
60
+ if (prepareResult._tag === "Left") return openPrFailure(errorMessage(prepareResult.left));
61
+ const pushResult = yield* Effect.either(pushHeadBranch(git, prCtx.headBranch));
62
+ if (pushResult._tag === "Left") return openPrFailure(errorMessage(pushResult.left));
63
+ return yield* submitPullRequest(prCtx, context);
64
+ });
65
+ }
66
+ function prepareHeadBranch(git, prCtx) {
67
+ return checkoutOrCreateHeadBranch(git, prCtx.headBranch).pipe(Effect.flatMap(() => stageAndCommitChanges(git, prCtx)), Effect.asVoid);
68
+ }
69
+ function checkoutOrCreateHeadBranch(git, headBranch) {
70
+ return git.raw([
71
+ "checkout",
72
+ "-B",
73
+ headBranch
74
+ ]).pipe(Effect.asVoid);
75
+ }
76
+ function stageAndCommitChanges(git, prCtx) {
77
+ return git.raw(["status", "--porcelain"]).pipe(Effect.flatMap((status) => commitIfDirty(git, status.trim(), prCtx)), Effect.asVoid);
78
+ }
79
+ function commitIfDirty(git, status, prCtx) {
80
+ if (status.length === 0) return Effect.void;
81
+ return configureCommitter(git, prCtx.committer).pipe(Effect.flatMap(() => git.raw(["add", "-A"])), Effect.flatMap(() => git.raw([
82
+ "commit",
83
+ "-m",
84
+ `open-pull-request: ${prCtx.runId}`
85
+ ])), Effect.asVoid);
86
+ }
87
+ function configureCommitter(git, committer) {
88
+ return git.raw([
89
+ "config",
90
+ "--local",
91
+ "user.name",
92
+ committer.name
93
+ ]).pipe(Effect.flatMap(() => git.raw([
94
+ "config",
95
+ "--local",
96
+ "user.email",
97
+ committer.email
98
+ ])), Effect.asVoid);
99
+ }
100
+ function pushHeadBranch(git, headBranch) {
101
+ return git.raw([
102
+ "push",
103
+ "--force-with-lease",
104
+ "origin",
105
+ `HEAD:refs/heads/${headBranch}`
106
+ ]).pipe(Effect.asVoid);
107
+ }
108
+ function submitPullRequest(prCtx, context) {
109
+ return Effect.gen(function* () {
110
+ const createResult = yield* runGhPrCreate(yield* CommandExecutor, prCtx, extractPrTitle(prCtx.task), context);
111
+ if (createResult.exitCode === 0) return openPrSuccess(extractPrUrl(createResult.output), "opened");
112
+ if (isPrAlreadyExistsError(createResult.output)) return yield* handleExistingPr(prCtx.headBranch, prCtx.label, context);
113
+ return createResult;
114
+ });
115
+ }
116
+ function runGhPrCreate(executor, prCtx, title, context) {
117
+ return executor.execute(buildGhPrCreateArgs(prCtx, title), context).pipe(Effect.catchAll((e) => Effect.succeed(openPrFailure(errorMessage(e)))));
118
+ }
119
+ function handleExistingPr(headBranch, label, context) {
120
+ return Effect.gen(function* () {
121
+ const editResult = yield* runGhPrEdit(yield* CommandExecutor, headBranch, label, context);
122
+ if (editResult.exitCode === 0) return openPrSuccess(headBranch, "updated");
123
+ return openPrFailure(editResult.output || `gh pr edit exited ${editResult.exitCode}`);
124
+ });
125
+ }
126
+ function runGhPrEdit(executor, headBranch, label, context) {
127
+ return executor.execute(buildGhPrEditArgs(headBranch, label), context).pipe(Effect.catchAll((e) => Effect.succeed(openPrFailure(errorMessage(e)))));
128
+ }
129
+ function extractPrTitle(task) {
130
+ return (task.split(NEWLINE_RE)[0] ?? task).trim() || "moka: open pull request";
131
+ }
132
+ function buildGhPrCreateArgs(prCtx, title) {
133
+ return [
134
+ "gh",
135
+ "pr",
136
+ "create",
137
+ "--base",
138
+ prCtx.baseBranch,
139
+ "--head",
140
+ prCtx.headBranch,
141
+ "--title",
142
+ title,
143
+ "--body",
144
+ `Opened by moka run ${prCtx.runId}`,
145
+ "--label",
146
+ prCtx.label
147
+ ];
148
+ }
149
+ function buildGhPrEditArgs(headBranch, label) {
150
+ return [
151
+ "gh",
152
+ "pr",
153
+ "edit",
154
+ headBranch,
155
+ "--add-label",
156
+ label
157
+ ];
158
+ }
159
+ function isPrAlreadyExistsError(output) {
160
+ return PR_ALREADY_EXISTS_RE.test(output);
161
+ }
162
+ function extractPrUrl(output) {
163
+ return output.split(NEWLINE_RE).map((l) => l.trim()).find((l) => l.startsWith("https://")) ?? output.trim();
164
+ }
165
+ function openPrSuccess(url, action) {
166
+ return {
167
+ evidence: [`open-pull-request: PR ${action} — ${url}`],
168
+ exitCode: 0,
169
+ output: JSON.stringify({
170
+ action,
171
+ url
172
+ })
173
+ };
174
+ }
175
+ function openPrFailure(reason) {
176
+ return {
177
+ evidence: [`open-pull-request failed: ${reason}`],
178
+ exitCode: 1,
179
+ output: JSON.stringify({ error: reason })
180
+ };
181
+ }
182
+ function errorMessage(error) {
183
+ return error instanceof Error ? error.message : String(error);
184
+ }
185
+ //#endregion
186
+ export { executeOpenPullRequestBuiltin, openPullRequestProgram };
@@ -169,4 +169,4 @@ function parallelOutput(children, results) {
169
169
  return JSON.stringify({ children: Object.fromEntries(children.filter((child) => outputsByNode.has(child.id)).map((child) => [child.id, outputsByNode.get(child.id)])) });
170
170
  }
171
171
  //#endregion
172
- export { childCategory, executeParallelNode, parallelEvidence, parallelOutput };
172
+ export { executeParallelNode };
@@ -0,0 +1,10 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import simpleGit$1 from "simple-git";
3
+ //#region src/runtime/services/open-pull-request-git-service.ts
4
+ var OpenPullRequestGitService = class extends Context.Tag("OpenPullRequestGitService")() {};
5
+ const OpenPullRequestGitServiceLive = Layer.succeed(OpenPullRequestGitService, { create: (baseDir) => Effect.sync(() => {
6
+ const git = simpleGit$1({ baseDir });
7
+ return { raw: (args) => Effect.tryPromise(() => git.raw(args)) };
8
+ }) });
9
+ //#endregion
10
+ export { OpenPullRequestGitService, OpenPullRequestGitServiceLive };
@@ -2,6 +2,7 @@
2
2
  const SCHEDULE_PASS_ORDER = [
3
3
  "coverage",
4
4
  "drain-merge",
5
+ "delivery",
5
6
  "models",
6
7
  "ids",
7
8
  "references"
@@ -0,0 +1,49 @@
1
+ import { uniqueGeneratedId } from "../../strings.js";
2
+ import { dependentsByNeed } from "../../planning/graph.js";
3
+ //#region src/schedule/passes/open-pull-request.ts
4
+ const OPEN_PR_BUILTIN = "open-pull-request";
5
+ /** True when pull_request delivery is opted in via config. */
6
+ function isPullRequestDeliveryEnabled(config) {
7
+ return config.delivery?.pull_request?.enabled === true;
8
+ }
9
+ /** True when the node list already has an open-pull-request builtin. */
10
+ function hasPullRequestNode(nodes) {
11
+ return nodes.some((node) => node.kind === "builtin" && node.builtin === OPEN_PR_BUILTIN);
12
+ }
13
+ /** Collect top-level node ids that no other top-level node depends on. */
14
+ function terminalNodeIds(nodes) {
15
+ const dependents = dependentsByNeed(nodes);
16
+ return nodes.map((node) => node.id).filter((id) => !dependents.get(id)?.length);
17
+ }
18
+ /** Build a single open-pull-request builtin node depending on all terminals. */
19
+ function buildPrNode(terminalIds, usedIds) {
20
+ return {
21
+ builtin: OPEN_PR_BUILTIN,
22
+ id: uniqueGeneratedId("generated-open-pull-request", usedIds, "generated-open-pull-request"),
23
+ kind: "builtin",
24
+ needs: terminalIds
25
+ };
26
+ }
27
+ /** Append a final open-pull-request node to the root workflow when enabled. */
28
+ function appendPullRequestDelivery(config, artifact) {
29
+ if (!isPullRequestDeliveryEnabled(config)) return artifact;
30
+ const rootWorkflow = artifact.workflows[artifact.root_workflow];
31
+ if (!rootWorkflow) return artifact;
32
+ const nodes = rootWorkflow.nodes;
33
+ if (hasPullRequestNode(nodes)) return artifact;
34
+ const terminals = terminalNodeIds(nodes);
35
+ if (terminals.length === 0) return artifact;
36
+ const prNode = buildPrNode(terminals, new Set(nodes.map((node) => node.id)));
37
+ return {
38
+ ...artifact,
39
+ workflows: {
40
+ ...artifact.workflows,
41
+ [artifact.root_workflow]: {
42
+ ...rootWorkflow,
43
+ nodes: [...nodes, prNode]
44
+ }
45
+ }
46
+ };
47
+ }
48
+ //#endregion
49
+ export { appendPullRequestDelivery };
@@ -5,6 +5,7 @@ const SCHEDULE_BUILTINS = [
5
5
  "duplication",
6
6
  "fallow",
7
7
  "lint",
8
+ "open-pull-request",
8
9
  "semgrep",
9
10
  "test",
10
11
  "typecheck"
package/package.json CHANGED
@@ -126,7 +126,7 @@
126
126
  "prepack": "bun run build:cli"
127
127
  },
128
128
  "type": "module",
129
- "version": "3.3.3",
129
+ "version": "3.4.0",
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",