@oisincoveney/pipeline 3.3.2 → 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>;
@@ -481,8 +481,8 @@ declare const configSchema: z.ZodObject<{
481
481
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
482
482
  description: z.ZodOptional<z.ZodString>;
483
483
  baseline: z.ZodEnum<{
484
- quick: "quick";
485
484
  execute: "execute";
485
+ quick: "quick";
486
486
  }>;
487
487
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
488
488
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -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(),
package/dist/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- pass: "pass";
17
16
  fail: "fail";
17
+ pass: "pass";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
@@ -160,8 +160,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
160
160
  }, z.core.$strict>>;
161
161
  serviceAccountName: z.ZodOptional<z.ZodString>;
162
162
  mode: z.ZodEnum<{
163
- full: "full";
164
163
  quick: "quick";
164
+ full: "full";
165
165
  }>;
166
166
  schedulePath: z.ZodOptional<z.ZodString>;
167
167
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -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,10 +598,11 @@ 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
- for (const implementationNode of implementationNodes) if (!(yield* remediateImplementationAncestor(input, implementationNode))) return false;
604
- return true;
603
+ let remediated = false;
604
+ for (const implementationNode of implementationNodes) if (yield* remediateImplementationAncestor(input, implementationNode)) remediated = true;
605
+ return remediated;
605
606
  });
606
607
  }
607
608
  function remediateImplementationAncestor(input, implementationNode) {
@@ -739,7 +740,19 @@ function visitImplementationDependencies(candidate, visit) {
739
740
  for (const need of candidate.needs) visit(need);
740
741
  }
741
742
  function appendImplementationNode(context, ordered, candidate) {
742
- 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";
743
756
  }
744
757
  function hasSchedulingRole(context, node, role) {
745
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"
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- full: "full";
47
46
  quick: "quick";
47
+ full: "full";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- full: "full";
108
107
  quick: "quick";
108
+ full: "full";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;
@@ -103,8 +103,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
103
103
  nodeId: z.ZodOptional<z.ZodString>;
104
104
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
105
105
  status: z.ZodEnum<{
106
- pass: "pass";
107
106
  fail: "fail";
107
+ pass: "pass";
108
108
  skip: "skip";
109
109
  }>;
110
110
  summary: z.ZodOptional<z.ZodString>;
@@ -273,8 +273,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
273
273
  nodeId: z.ZodOptional<z.ZodString>;
274
274
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
275
275
  status: z.ZodEnum<{
276
- pass: "pass";
277
276
  fail: "fail";
277
+ pass: "pass";
278
278
  skip: "skip";
279
279
  }>;
280
280
  summary: z.ZodOptional<z.ZodString>;
@@ -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.2",
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",