@oisincoveney/pipeline 3.3.3 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults/profiles.yaml +2 -2
- package/dist/cli/run-resolver.js +24 -15
- package/dist/config/load.js +1 -0
- package/dist/config/schemas.d.ts +7 -1
- package/dist/config/schemas.js +10 -4
- package/dist/moka-submit.d.ts +1 -1
- package/dist/moka-submit.js +10 -1
- package/dist/pipeline-runtime.js +14 -2
- package/dist/planning/generate.js +4 -1
- package/dist/runner-command-contract.d.ts +2 -2
- package/dist/runtime/builtins/builtins.js +3 -0
- package/dist/runtime/open-pull-request/index.js +2 -0
- package/dist/runtime/open-pull-request/open-pull-request.js +186 -0
- package/dist/runtime/parallel-node/parallel-node.js +1 -1
- package/dist/runtime/services/open-pull-request-git-service.js +10 -0
- package/dist/schedule/passes/index.js +1 -0
- package/dist/schedule/passes/open-pull-request.js +49 -0
- package/dist/schedule/prompts.js +1 -0
- package/package.json +1 -1
package/defaults/profiles.yaml
CHANGED
|
@@ -151,7 +151,7 @@ profiles:
|
|
|
151
151
|
runner: opencode
|
|
152
152
|
scheduling_roles: [implementation]
|
|
153
153
|
description: Add focused failing tests for the requested behavior.
|
|
154
|
-
instructions: { inline: "Add focused failing tests for the requested behavior only. Do not change production code. Only edit files matching test paths such as **/*.test.*, **/*.spec.*, **/*_test.*, **/__tests__/**, test/**, or tests/**. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
|
|
154
|
+
instructions: { inline: "Add focused failing tests for the requested behavior only. Do not change production code. Only edit files matching test paths such as **/*.test.*, **/*.spec.*, **/*_test.*, **/__tests__/**, test/**, or tests/**. NEVER silence lint, type, complexity, or dead-code findings with suppression comments (no // fallow-ignore, // biome-ignore, eslint-disable, oxlint-disable, @ts-ignore, or @ts-expect-error); fix the underlying cause — if a gate flags your test, restructure the test (e.g. move restricted imports into shared support/fixture helpers) rather than suppressing it. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
|
|
155
155
|
skills: [test]
|
|
156
156
|
mcp_servers: [pipeline-gateway]
|
|
157
157
|
tools: [read, list, grep, glob, bash, edit, write]
|
|
@@ -165,7 +165,7 @@ profiles:
|
|
|
165
165
|
runner: opencode
|
|
166
166
|
scheduling_roles: [implementation]
|
|
167
167
|
description: Implement production code until the failing tests pass.
|
|
168
|
-
instructions: { inline: "Implement the smallest production change that satisfies the failing tests. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
|
|
168
|
+
instructions: { inline: "Implement the smallest production change that satisfies the failing tests. NEVER silence lint, type, complexity, or dead-code findings with suppression comments (no // fallow-ignore, // biome-ignore, eslint-disable, oxlint-disable, @ts-ignore, or @ts-expect-error); fix the underlying cause — reduce complexity by extracting helpers, remove genuinely dead code, and migrate off deprecated APIs rather than suppressing the warning. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
|
|
169
169
|
skills: [trace, test, fix, library-first-development]
|
|
170
170
|
mcp_servers: [pipeline-gateway]
|
|
171
171
|
tools: [read, list, grep, glob, bash, edit, write]
|
package/dist/cli/run-resolver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
33
|
+
const LOCAL_RUNTIME_RESOLVERS = [
|
|
34
|
+
(flags) => flags.schedule ? {
|
|
32
35
|
kind: "local-runtime",
|
|
33
36
|
schedule: flags.schedule
|
|
34
|
-
}
|
|
35
|
-
|
|
37
|
+
} : void 0,
|
|
38
|
+
(flags) => flags.workflow ? {
|
|
36
39
|
kind: "local-runtime",
|
|
37
40
|
workflow: flags.workflow
|
|
38
|
-
}
|
|
39
|
-
|
|
41
|
+
} : void 0,
|
|
42
|
+
(flags) => flags.readOnly ? {
|
|
40
43
|
kind: "local-runtime",
|
|
41
44
|
workflow: "inspect"
|
|
42
|
-
}
|
|
43
|
-
|
|
45
|
+
} : void 0,
|
|
46
|
+
(flags) => flags.entrypoint ? {
|
|
44
47
|
entrypoint: flags.entrypoint,
|
|
45
48
|
kind: "local-runtime"
|
|
46
|
-
}
|
|
47
|
-
|
|
49
|
+
} : void 0,
|
|
50
|
+
(_flags, effort) => effort === "quick" ? {
|
|
48
51
|
entrypoint: "quick",
|
|
49
52
|
kind: "local-runtime"
|
|
50
|
-
}
|
|
51
|
-
|
|
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
|
package/dist/config/load.js
CHANGED
package/dist/config/schemas.d.ts
CHANGED
|
@@ -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
|
-
execute: "execute";
|
|
485
484
|
quick: "quick";
|
|
485
|
+
execute: "execute";
|
|
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>;
|
package/dist/config/schemas.js
CHANGED
|
@@ -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/moka-submit.d.ts
CHANGED
|
@@ -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
|
-
quick: "quick";
|
|
164
163
|
full: "full";
|
|
164
|
+
quick: "quick";
|
|
165
165
|
}>;
|
|
166
166
|
schedulePath: z.ZodOptional<z.ZodString>;
|
|
167
167
|
scheduleYaml: z.ZodOptional<z.ZodString>;
|
package/dist/moka-submit.js
CHANGED
|
@@ -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,
|
package/dist/pipeline-runtime.js
CHANGED
|
@@ -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)
|
|
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 (
|
|
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"
|
|
@@ -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
|
-
quick: "quick";
|
|
47
46
|
full: "full";
|
|
47
|
+
quick: "quick";
|
|
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
|
-
quick: "quick";
|
|
108
107
|
full: "full";
|
|
108
|
+
quick: "quick";
|
|
109
109
|
}>;
|
|
110
110
|
}, z.core.$strict>, z.ZodObject<{
|
|
111
111
|
argv: z.ZodArray<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,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 {
|
|
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 };
|
|
@@ -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 };
|
package/dist/schedule/prompts.js
CHANGED
package/package.json
CHANGED
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"prepack": "bun run build:cli"
|
|
127
127
|
},
|
|
128
128
|
"type": "module",
|
|
129
|
-
"version": "3.
|
|
129
|
+
"version": "3.4.1",
|
|
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",
|