@oisincoveney/pipeline 2.8.2 → 2.8.4
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/pipeline.yaml +13 -12
- package/dist/config/load.js +0 -1
- package/dist/config/schemas.d.ts +0 -6
- package/dist/config/schemas.js +0 -8
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-runtime.js +23 -5
- package/dist/planning/generate.js +3 -9
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runtime/builtins/builtins.js +0 -2
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +2 -35
- package/dist/schedule/passes/index.js +0 -1
- package/docs/config-architecture.md +4 -26
- package/package.json +2 -1
- package/dist/runtime/select-candidate/select-candidate.js +0 -144
- package/dist/runtime/services/select-candidate-service.js +0 -13
- package/dist/schedule/passes/candidates.js +0 -51
package/defaults/pipeline.yaml
CHANGED
|
@@ -15,18 +15,10 @@ repo_map:
|
|
|
15
15
|
# last passed node without re-running (or re-spending tokens on) finished work.
|
|
16
16
|
durability:
|
|
17
17
|
enabled: true
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# 1. The leased opencode server is rooted at the main worktree and throws
|
|
23
|
-
# "Unexpected server error" when a candidate session runs with directory set
|
|
24
|
-
# to its isolated worktree → candidates exit 70, retries exhaust, green loops.
|
|
25
|
-
# Fix: lease a per-worktree opencode server for each candidate.
|
|
26
|
-
# 2. The winning candidate's file changes live in its worktree and are never
|
|
27
|
-
# merged back to the main tree, so downstream nodes wouldn't see them.
|
|
28
|
-
# Enable explicitly (best_of_n.enabled + n:2 + categories:[green] + parallel_
|
|
29
|
-
# worktrees.enabled) once those land; n=2 also ~doubles green-node spend.
|
|
18
|
+
# parallel_worktrees: opt-in git-worktree isolation for parallel child nodes —
|
|
19
|
+
# each parallel child runs on its own branch with its own per-worktree opencode
|
|
20
|
+
# server so concurrent edits and sessions can't collide. OFF by default; enable
|
|
21
|
+
# explicitly with parallel_worktrees.enabled.
|
|
30
22
|
token_budget:
|
|
31
23
|
default_context_window: 200000
|
|
32
24
|
max_context_pct: 50
|
|
@@ -62,6 +54,15 @@ runner_command:
|
|
|
62
54
|
setup:
|
|
63
55
|
- command: bun
|
|
64
56
|
args: [install, --frozen-lockfile]
|
|
57
|
+
# Set up package-owned pipeline support + the opencode model registration
|
|
58
|
+
# (.opencode/opencode.json, which declares the gpt-5.5-* reasoning selectors)
|
|
59
|
+
# on every run, so opencode-backed agents in the pod resolve their models
|
|
60
|
+
# instead of failing with "Model not found". Both commands are idempotent
|
|
61
|
+
# and write no repo-local pipeline config.
|
|
62
|
+
- command: moka
|
|
63
|
+
args: [init]
|
|
64
|
+
- command: moka
|
|
65
|
+
args: [install-commands]
|
|
65
66
|
scheduler:
|
|
66
67
|
commands:
|
|
67
68
|
quick:
|
package/dist/config/load.js
CHANGED
package/dist/config/schemas.d.ts
CHANGED
|
@@ -501,12 +501,6 @@ declare const configSchema: z.ZodObject<{
|
|
|
501
501
|
task_context: z.ZodOptional<z.ZodObject<{
|
|
502
502
|
type: z.ZodString;
|
|
503
503
|
}, z.core.$loose>>;
|
|
504
|
-
best_of_n: z.ZodOptional<z.ZodObject<{
|
|
505
|
-
categories: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
506
|
-
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
507
|
-
judge_model: z.ZodOptional<z.ZodString>;
|
|
508
|
-
n: z.ZodDefault<z.ZodNumber>;
|
|
509
|
-
}, z.core.$strict>>;
|
|
510
504
|
context_handoff: z.ZodOptional<z.ZodObject<{
|
|
511
505
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
512
506
|
model: z.ZodOptional<z.ZodString>;
|
package/dist/config/schemas.js
CHANGED
|
@@ -471,12 +471,6 @@ const durabilitySchema = z.object({
|
|
|
471
471
|
dir: z.string().min(1).default(".pipeline/journal"),
|
|
472
472
|
enabled: z.boolean().default(false)
|
|
473
473
|
}).strict();
|
|
474
|
-
const bestOfNSchema = z.object({
|
|
475
|
-
categories: z.array(z.string()).default(["green"]),
|
|
476
|
-
enabled: z.boolean().default(false),
|
|
477
|
-
judge_model: z.string().optional(),
|
|
478
|
-
n: z.number().int().positive().default(1)
|
|
479
|
-
}).strict();
|
|
480
474
|
const repoMapSchema = z.object({
|
|
481
475
|
enabled: z.boolean().default(false),
|
|
482
476
|
token_budget: z.number().int().positive().default(2e3)
|
|
@@ -502,7 +496,6 @@ const pipelineFileSchema = z.object({
|
|
|
502
496
|
}),
|
|
503
497
|
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
504
498
|
task_context: taskContextResolverSchema.optional(),
|
|
505
|
-
best_of_n: bestOfNSchema.optional(),
|
|
506
499
|
context_handoff: contextHandoffSchema.optional(),
|
|
507
500
|
durability: durabilitySchema.optional(),
|
|
508
501
|
parallel_worktrees: parallelWorktreesSchema.optional(),
|
|
@@ -538,7 +531,6 @@ const configSchema = z.object({
|
|
|
538
531
|
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
539
532
|
skills: strictRecord(pathRefSchema).default({}),
|
|
540
533
|
task_context: taskContextResolverSchema.optional(),
|
|
541
|
-
best_of_n: bestOfNSchema.optional(),
|
|
542
534
|
context_handoff: contextHandoffSchema.optional(),
|
|
543
535
|
durability: durabilitySchema.optional(),
|
|
544
536
|
parallel_worktrees: parallelWorktreesSchema.optional(),
|
package/dist/moka-submit.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { z } from "zod";
|
|
|
5
5
|
//#region src/moka-submit.d.ts
|
|
6
6
|
declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
|
|
7
7
|
"workflow.start": "workflow.start";
|
|
8
|
-
"node.finish": "node.finish";
|
|
9
|
-
"node.start": "node.start";
|
|
10
8
|
"workflow.success": "workflow.success";
|
|
11
9
|
"workflow.failure": "workflow.failure";
|
|
12
10
|
"workflow.complete": "workflow.complete";
|
|
11
|
+
"node.start": "node.start";
|
|
13
12
|
"node.success": "node.success";
|
|
14
13
|
"node.error": "node.error";
|
|
14
|
+
"node.finish": "node.finish";
|
|
15
15
|
"gate.failure": "gate.failure";
|
|
16
16
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
17
17
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
94
94
|
}, z.core.$strict>>;
|
|
95
95
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
96
96
|
"workflow.start": "workflow.start";
|
|
97
|
-
"node.finish": "node.finish";
|
|
98
|
-
"node.start": "node.start";
|
|
99
97
|
"workflow.success": "workflow.success";
|
|
100
98
|
"workflow.failure": "workflow.failure";
|
|
101
99
|
"workflow.complete": "workflow.complete";
|
|
100
|
+
"node.start": "node.start";
|
|
102
101
|
"node.success": "node.success";
|
|
103
102
|
"node.error": "node.error";
|
|
103
|
+
"node.finish": "node.finish";
|
|
104
104
|
"gate.failure": "gate.failure";
|
|
105
105
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
106
106
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -206,13 +206,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
206
206
|
}, z.core.$strict>>;
|
|
207
207
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
208
208
|
"workflow.start": "workflow.start";
|
|
209
|
-
"node.finish": "node.finish";
|
|
210
|
-
"node.start": "node.start";
|
|
211
209
|
"workflow.success": "workflow.success";
|
|
212
210
|
"workflow.failure": "workflow.failure";
|
|
213
211
|
"workflow.complete": "workflow.complete";
|
|
212
|
+
"node.start": "node.start";
|
|
214
213
|
"node.success": "node.success";
|
|
215
214
|
"node.error": "node.error";
|
|
215
|
+
"node.finish": "node.finish";
|
|
216
216
|
"gate.failure": "gate.failure";
|
|
217
217
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
218
218
|
failure: z.ZodDefault<z.ZodEnum<{
|
package/dist/pipeline-runtime.js
CHANGED
|
@@ -456,13 +456,22 @@ function cancelledRetry(nodeId, attempt, last) {
|
|
|
456
456
|
retryReason: "timeout"
|
|
457
457
|
};
|
|
458
458
|
}
|
|
459
|
+
function unwrapAttemptError(error) {
|
|
460
|
+
const inner = error?.error;
|
|
461
|
+
return inner !== void 0 && inner !== error ? inner : error;
|
|
462
|
+
}
|
|
463
|
+
function attemptErrorMessage(error) {
|
|
464
|
+
const inner = unwrapAttemptError(error);
|
|
465
|
+
if (inner !== error) return attemptErrorMessage(inner);
|
|
466
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
467
|
+
}
|
|
459
468
|
function failedAttemptRetry(nodeId, attempt, last, err) {
|
|
460
|
-
const message =
|
|
469
|
+
const message = attemptErrorMessage(err);
|
|
461
470
|
return {
|
|
462
471
|
attempt,
|
|
463
472
|
evidence: [...last.evidence, message],
|
|
464
473
|
gate: nodeId,
|
|
465
|
-
reason:
|
|
474
|
+
reason: message,
|
|
466
475
|
retryReason: nodeRetryReason(last)
|
|
467
476
|
};
|
|
468
477
|
}
|
|
@@ -970,13 +979,22 @@ const nodeAttemptExecutors = {
|
|
|
970
979
|
parallel: executeParallelAttempt
|
|
971
980
|
};
|
|
972
981
|
function executeAgentAttempt(node, context, attempt) {
|
|
973
|
-
return Effect.tryPromise(
|
|
982
|
+
return Effect.tryPromise({
|
|
983
|
+
catch: (error) => error,
|
|
984
|
+
try: () => executeAgentNode(node, context, attempt)
|
|
985
|
+
});
|
|
974
986
|
}
|
|
975
987
|
function executeCommandAttempt(node, context) {
|
|
976
|
-
return Effect.tryPromise(
|
|
988
|
+
return Effect.tryPromise({
|
|
989
|
+
catch: (error) => error,
|
|
990
|
+
try: () => executeCommand(node.command ?? [], context, { timeout: node.timeoutMs })
|
|
991
|
+
});
|
|
977
992
|
}
|
|
978
993
|
function executeBuiltinAttempt(node, context) {
|
|
979
|
-
return Effect.tryPromise(
|
|
994
|
+
return Effect.tryPromise({
|
|
995
|
+
catch: (error) => error,
|
|
996
|
+
try: () => executeBuiltin(node.builtin ?? "", context, node)
|
|
997
|
+
});
|
|
980
998
|
}
|
|
981
999
|
function executeGroupAttempt(node) {
|
|
982
1000
|
return Effect.succeed({
|
|
@@ -5,7 +5,6 @@ import { createRunnerLaunchPlan, runLaunchPlan } from "../runner.js";
|
|
|
5
5
|
import { normalizeRunnerOutput } from "../runner-output.js";
|
|
6
6
|
import { loadBacklogPlanningContext } from "../schedule/backlog-context.js";
|
|
7
7
|
import { baselineScheduleArtifact } from "../schedule/baseline.js";
|
|
8
|
-
import { expandBestOfNCandidates } from "../schedule/passes/candidates.js";
|
|
9
8
|
import { dependentsByNeed, flattenNodes, hasReachableDependent } from "./graph.js";
|
|
10
9
|
import { isCoverageNode, isImplementationNode } from "../schedule/scheduling-roles.js";
|
|
11
10
|
import { addGeneratedImplementationCoverage } from "../schedule/passes/coverage.js";
|
|
@@ -31,12 +30,11 @@ const SCHEDULE_BUILTINS = [
|
|
|
31
30
|
"duplication",
|
|
32
31
|
"fallow",
|
|
33
32
|
"lint",
|
|
34
|
-
"select-candidate",
|
|
35
33
|
"semgrep",
|
|
36
34
|
"test",
|
|
37
35
|
"typecheck"
|
|
38
36
|
];
|
|
39
|
-
const PARALLEL_MERGE_BUILTINS = new Set(["drain-merge"
|
|
37
|
+
const PARALLEL_MERGE_BUILTINS = new Set(["drain-merge"]);
|
|
40
38
|
const scheduleArtifactSchema = z.object({
|
|
41
39
|
generated_at: z.string().datetime(),
|
|
42
40
|
kind: z.literal(SCHEDULE_KIND),
|
|
@@ -94,7 +92,7 @@ async function generateScheduleArtifact(options) {
|
|
|
94
92
|
const planningContext = { ...loadBacklogPlanningContext(options.task, options.worktreePath) };
|
|
95
93
|
const generatedArtifact = await planScheduleArtifact(baseline, policy.planner_profile, options, planningContext);
|
|
96
94
|
assertSchedulePassOrder();
|
|
97
|
-
const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog,
|
|
95
|
+
const artifact = hydrateScheduleTaskContexts(canonicalizeGeneratedScheduleIds(applyNodeCatalogModelFallbacks(options.config, policy.node_catalog, addGeneratedImplementationCoverage(options.config, generatedArtifact))), planningContext);
|
|
98
96
|
validateScheduleArtifact(options.config, artifact, planningContext);
|
|
99
97
|
compileScheduleArtifact(options.config, artifact, options.worktreePath);
|
|
100
98
|
return {
|
|
@@ -105,7 +103,6 @@ async function generateScheduleArtifact(options) {
|
|
|
105
103
|
function assertSchedulePassOrder() {
|
|
106
104
|
if (SCHEDULE_PASS_ORDER.join("\0") !== [
|
|
107
105
|
"coverage",
|
|
108
|
-
"candidates",
|
|
109
106
|
"models",
|
|
110
107
|
"ids",
|
|
111
108
|
"references"
|
|
@@ -345,7 +342,7 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
|
|
|
345
342
|
const nodes = flattenWorkflowNodes(workflow.nodes);
|
|
346
343
|
const index = dependentsByNeedWithContainment(workflow.nodes, nodes);
|
|
347
344
|
const nodesByWorkUnit = nodesByAssignedWorkUnit(nodes);
|
|
348
|
-
return nodes.filter((node) => isImplementationNode(config, node)
|
|
345
|
+
return nodes.filter((node) => isImplementationNode(config, node)).flatMap((node) => {
|
|
349
346
|
const dependentId = node.task_context?.id;
|
|
350
347
|
if (!dependentId) return [];
|
|
351
348
|
return (dependenciesByUnit.get(dependentId) ?? []).flatMap((prerequisiteId) => {
|
|
@@ -355,9 +352,6 @@ function workUnitDependencyIssues(config, artifact, workUnits) {
|
|
|
355
352
|
});
|
|
356
353
|
});
|
|
357
354
|
}
|
|
358
|
-
function isSelectCandidateNode(node) {
|
|
359
|
-
return node.kind === "builtin" && node.builtin === "select-candidate";
|
|
360
|
-
}
|
|
361
355
|
function nodesByAssignedWorkUnit(nodes) {
|
|
362
356
|
const grouped = /* @__PURE__ */ new Map();
|
|
363
357
|
for (const node of nodes) {
|
|
@@ -10,8 +10,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
10
10
|
at: z.ZodOptional<z.ZodString>;
|
|
11
11
|
sequence: z.ZodNumber;
|
|
12
12
|
type: z.ZodEnum<{
|
|
13
|
-
"workflow.planned": "workflow.planned";
|
|
14
13
|
"workflow.start": "workflow.start";
|
|
14
|
+
"workflow.planned": "workflow.planned";
|
|
15
15
|
}>;
|
|
16
16
|
workflowPlan: z.ZodObject<{
|
|
17
17
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -55,10 +55,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
55
55
|
}>;
|
|
56
56
|
}, z.core.$strip>;
|
|
57
57
|
type: z.ZodEnum<{
|
|
58
|
+
"node.start": "node.start";
|
|
59
|
+
"node.finish": "node.finish";
|
|
58
60
|
"agent.finish": "agent.finish";
|
|
59
61
|
"agent.start": "agent.start";
|
|
60
|
-
"node.finish": "node.finish";
|
|
61
|
-
"node.start": "node.start";
|
|
62
62
|
}>;
|
|
63
63
|
}, z.core.$strip>, z.ZodObject<{
|
|
64
64
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -180,8 +180,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
180
180
|
at: z.ZodOptional<z.ZodString>;
|
|
181
181
|
sequence: z.ZodNumber;
|
|
182
182
|
type: z.ZodEnum<{
|
|
183
|
-
"workflow.planned": "workflow.planned";
|
|
184
183
|
"workflow.start": "workflow.start";
|
|
184
|
+
"workflow.planned": "workflow.planned";
|
|
185
185
|
}>;
|
|
186
186
|
workflowPlan: z.ZodObject<{
|
|
187
187
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -225,10 +225,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
225
225
|
}>;
|
|
226
226
|
}, z.core.$strip>;
|
|
227
227
|
type: z.ZodEnum<{
|
|
228
|
+
"node.start": "node.start";
|
|
229
|
+
"node.finish": "node.finish";
|
|
228
230
|
"agent.finish": "agent.finish";
|
|
229
231
|
"agent.start": "agent.start";
|
|
230
|
-
"node.finish": "node.finish";
|
|
231
|
-
"node.start": "node.start";
|
|
232
232
|
}>;
|
|
233
233
|
}, z.core.$strip>, z.ZodObject<{
|
|
234
234
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -2,7 +2,6 @@ import { parseJson } from "../../safe-json.js";
|
|
|
2
2
|
import { CommandExecutor, CommandExecutorLive } from "../services/command-executor-service.js";
|
|
3
3
|
import { executeDrainMergeBuiltin } from "../drain-merge/drain-merge.js";
|
|
4
4
|
import "../drain-merge/index.js";
|
|
5
|
-
import { executeSelectCandidateBuiltin } from "../select-candidate/select-candidate.js";
|
|
6
5
|
import { Effect } from "effect";
|
|
7
6
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
8
7
|
import { join } from "node:path";
|
|
@@ -29,7 +28,6 @@ const BUILTIN_HANDLERS = {
|
|
|
29
28
|
duplication: (context) => executeDuplicationBuiltinEffect(context),
|
|
30
29
|
fallow: (context) => executeFallowBuiltinEffect(context),
|
|
31
30
|
lint: (context) => executeScriptBuiltinEffect(context, "lint"),
|
|
32
|
-
"select-candidate": (context, node) => Effect.tryPromise(() => executeSelectCandidateBuiltin(context, node)),
|
|
33
31
|
semgrep: (context) => executeSemgrepBuiltinEffect(context),
|
|
34
32
|
test: (context) => executeTestBuiltinEffect(context),
|
|
35
33
|
typecheck: (context) => executeScriptBuiltinEffect(context, "typecheck")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
//#region src/runtime/parallel-worktrees/parallel-worktrees.ts
|
|
5
5
|
/**
|
|
@@ -24,39 +24,6 @@ function provisionGeneratedResources(repoRoot, worktreePath) {
|
|
|
24
24
|
function childWorktreeRelPath(runId, parentNodeId, childNodeId) {
|
|
25
25
|
return join(WORKTREE_ROOT, "trees", sanitize(runId ?? "local"), sanitize(parentNodeId), sanitize(childNodeId));
|
|
26
26
|
}
|
|
27
|
-
function changedWorktreeFiles(worktreePath) {
|
|
28
|
-
const modified = git(worktreePath, [
|
|
29
|
-
"diff",
|
|
30
|
-
"--name-only",
|
|
31
|
-
"HEAD"
|
|
32
|
-
]);
|
|
33
|
-
const untracked = git(worktreePath, [
|
|
34
|
-
"ls-files",
|
|
35
|
-
"--others",
|
|
36
|
-
"--exclude-standard"
|
|
37
|
-
]);
|
|
38
|
-
return [...modified.split("\n"), ...untracked.split("\n")].filter(Boolean);
|
|
39
|
-
}
|
|
40
|
-
function copyFileInto(fromRoot, toRoot, relativePath) {
|
|
41
|
-
const source = join(fromRoot, relativePath);
|
|
42
|
-
if (!existsSync(source)) return;
|
|
43
|
-
const dest = join(toRoot, relativePath);
|
|
44
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
45
|
-
cpSync(source, dest, { recursive: true });
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* PIPE-83.14: promote a best-of-N winner's edits from its (retained) worktree
|
|
49
|
-
* back into the main worktree, so downstream nodes (tests, verification) see the
|
|
50
|
-
* selected candidate's changes. Copies modified + untracked files; no-op if the
|
|
51
|
-
* worktree is gone. Returns the promoted file paths.
|
|
52
|
-
*/
|
|
53
|
-
function promoteWorktreeChanges(repoRoot, runId, parentNodeId, childNodeId) {
|
|
54
|
-
const worktreePath = join(repoRoot, childWorktreeRelPath(runId, parentNodeId, childNodeId));
|
|
55
|
-
if (!existsSync(worktreePath)) return [];
|
|
56
|
-
const files = changedWorktreeFiles(worktreePath);
|
|
57
|
-
for (const relativePath of files) copyFileInto(worktreePath, repoRoot, relativePath);
|
|
58
|
-
return files;
|
|
59
|
-
}
|
|
60
27
|
function git(cwd, args) {
|
|
61
28
|
return execFileSync("git", args, {
|
|
62
29
|
cwd,
|
|
@@ -174,4 +141,4 @@ function gcParallelWorktrees(repoRoot) {
|
|
|
174
141
|
return results;
|
|
175
142
|
}
|
|
176
143
|
//#endregion
|
|
177
|
-
export { createChildWorktree, gcParallelWorktrees
|
|
144
|
+
export { createChildWorktree, gcParallelWorktrees };
|
|
@@ -359,13 +359,11 @@ The toposort uses `@dagrejs/graphlib` for the graph model but an iterative
|
|
|
359
359
|
traversal for the topological sort, because graphlib's recursive topsort can
|
|
360
360
|
overflow the call stack on deep generated workflow chains.
|
|
361
361
|
|
|
362
|
-
## Context
|
|
362
|
+
## Context & durability features (PIPE-83)
|
|
363
363
|
|
|
364
|
-
The shipped `defaults/pipeline.yaml` turns **`context_handoff`, `repo_map`,
|
|
365
|
-
`durability
|
|
366
|
-
|
|
367
|
-
~doubles green-node spend, so lower `n` or set `enabled: false` to spend the
|
|
368
|
-
minimum. Each block can be overridden in `pipeline.yaml`; the per-block
|
|
364
|
+
The shipped `defaults/pipeline.yaml` turns **`context_handoff`, `repo_map`, and
|
|
365
|
+
`durability` ON** — moka uses its architecture by default; `parallel_worktrees`
|
|
366
|
+
is opt-in. Each block can be overridden in `pipeline.yaml`; the per-block
|
|
369
367
|
`# default …` notes below mark the *schema* default that applies when a block is
|
|
370
368
|
omitted entirely.
|
|
371
369
|
|
|
@@ -400,26 +398,6 @@ worktrees are GC'd on the next parallel node under the same guard. A worktree is
|
|
|
400
398
|
*not* a sandbox (node_modules/build state are shared) — real isolation remains
|
|
401
399
|
k8s mode. SDK-runner nodes honour the per-child directory via `plan.cwd`.
|
|
402
400
|
|
|
403
|
-
### `best_of_n` — generate N candidates and select the winner
|
|
404
|
-
|
|
405
|
-
```yaml
|
|
406
|
-
best_of_n:
|
|
407
|
-
enabled: true # default false
|
|
408
|
-
n: 3 # candidates per matching node (default 1 = off)
|
|
409
|
-
categories: [green] # which node categories fan out (id-substring match)
|
|
410
|
-
judge_model: openai/gpt-5.5 # optional LLM judge; omit for status-only
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
When enabled with `n > 1`, the `candidates` schedule pass expands each matching
|
|
414
|
-
agent node into a `kind: parallel` of N candidate children
|
|
415
|
-
(`<node>--candidates`) feeding a `select-candidate` builtin that keeps the
|
|
416
|
-
original node id. Each candidate builds in its own worktree (pair with
|
|
417
|
-
`parallel_worktrees`) and is throttled by its category's
|
|
418
|
-
`token_budget.fan_out_width` cap. The selector scores candidates by a hybrid of
|
|
419
|
-
execution status (PASS/FAIL) and, when `judge_model` is set, a 0..1 LLM judge
|
|
420
|
-
score; it emits the winner's output and **never self-fixes** — if no candidate
|
|
421
|
-
passes, the node fails with evidence.
|
|
422
|
-
|
|
423
401
|
### `durability` — durable crash-resume
|
|
424
402
|
|
|
425
403
|
```yaml
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@biomejs/biome": "2.4.15",
|
|
31
|
+
"@effect/vitest": "^0.29.0",
|
|
31
32
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
32
33
|
"@semantic-release/github": "^12.0.8",
|
|
33
34
|
"@semantic-release/npm": "^13.1.5",
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
"prepack": "bun run build:cli"
|
|
127
128
|
},
|
|
128
129
|
"type": "module",
|
|
129
|
-
"version": "2.8.
|
|
130
|
+
"version": "2.8.4",
|
|
130
131
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
131
132
|
"main": "./dist/index.js",
|
|
132
133
|
"types": "./dist/index.d.ts",
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { createRunnerLaunchPlan } from "../../runner.js";
|
|
2
|
-
import { normalizeRunnerOutput } from "../../runner-output.js";
|
|
3
|
-
import { parseJsonObject } from "../json-validation/json-validation.js";
|
|
4
|
-
import "../json-validation/index.js";
|
|
5
|
-
import { SelectCandidateService, SelectCandidateServiceLive } from "../services/select-candidate-service.js";
|
|
6
|
-
import { Effect } from "effect";
|
|
7
|
-
//#region src/runtime/select-candidate/select-candidate.ts
|
|
8
|
-
const SCORE_RE = /-?\d+(?:\.\d+)?/;
|
|
9
|
-
function selectBestCandidate(candidates) {
|
|
10
|
-
const passing = candidates.filter((candidate) => candidate.status === "PASS");
|
|
11
|
-
if (passing.length === 0) return null;
|
|
12
|
-
return passing.reduce((best, candidate) => (candidate.judgeScore ?? 0) > (best.judgeScore ?? 0) ? candidate : best);
|
|
13
|
-
}
|
|
14
|
-
async function executeSelectCandidateBuiltin(context, node) {
|
|
15
|
-
const program = executeSelectCandidateBuiltinProgram(context, node);
|
|
16
|
-
return await Effect.runPromise(Effect.provide(program, SelectCandidateServiceLive));
|
|
17
|
-
}
|
|
18
|
-
function executeSelectCandidateBuiltinProgram(context, node) {
|
|
19
|
-
return Effect.gen(function* () {
|
|
20
|
-
const candidates = yield* scoreCandidates(context, readCandidates(context, firstNeed(node)));
|
|
21
|
-
const selected = selectBestCandidate(candidates);
|
|
22
|
-
if (!selected) return {
|
|
23
|
-
evidence: [`select-candidate: no passing candidate among ${candidates.length}`, ...candidates.map((candidate) => `- ${candidate.nodeId}: FAIL`)],
|
|
24
|
-
exitCode: 1,
|
|
25
|
-
output: ""
|
|
26
|
-
};
|
|
27
|
-
const promoted = yield* promoteWinner(context, node, selected.nodeId);
|
|
28
|
-
return {
|
|
29
|
-
evidence: selectionEvidence(selected, candidates.length, promoted),
|
|
30
|
-
exitCode: 0,
|
|
31
|
-
output: selected.output
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
function selectionEvidence(selected, candidateCount, promoted) {
|
|
36
|
-
const lines = [`select-candidate: selected '${selected.nodeId}' (judge=${selected.judgeScore ?? "n/a"}) from ${candidateCount} candidates`];
|
|
37
|
-
if (promoted.length > 0) lines.push(`promoted ${promoted.length} file(s) from the winning worktree`);
|
|
38
|
-
return lines;
|
|
39
|
-
}
|
|
40
|
-
function promoteWinner(context, node, winnerNodeId) {
|
|
41
|
-
const parentNodeId = firstNeed(node);
|
|
42
|
-
if (!shouldPromoteWinner(context, parentNodeId)) return Effect.succeed([]);
|
|
43
|
-
return SelectCandidateService.pipe(Effect.flatMap((service) => service.promoteWinner(context.worktreePath, context.runId, parentNodeId, winnerNodeId)));
|
|
44
|
-
}
|
|
45
|
-
function shouldPromoteWinner(context, parentNodeId) {
|
|
46
|
-
const parallelWorktrees = context.config.parallel_worktrees;
|
|
47
|
-
return Boolean(parallelWorktrees?.enabled) && parentNodeId !== null;
|
|
48
|
-
}
|
|
49
|
-
function firstNeed(node) {
|
|
50
|
-
return node?.needs.at(0) ?? null;
|
|
51
|
-
}
|
|
52
|
-
function scoreCandidates(context, candidates) {
|
|
53
|
-
const model = context.config.best_of_n?.judge_model;
|
|
54
|
-
const runner = Object.keys(context.config.runners).at(0);
|
|
55
|
-
if (!(model && runner)) return Effect.succeed(candidates);
|
|
56
|
-
return Effect.forEach(candidates, (candidate) => scoreCandidate(context, candidate, runner, model), { concurrency: "unbounded" });
|
|
57
|
-
}
|
|
58
|
-
function scoreCandidate(context, candidate, runner, model) {
|
|
59
|
-
return Effect.gen(function* () {
|
|
60
|
-
const plan = judgePlan(context, candidate, runner, model);
|
|
61
|
-
context.agentInvocations.push(plan);
|
|
62
|
-
const judgeScore = parseScore(normalizeRunnerOutput(plan, (yield* (yield* SelectCandidateService).executeRunner(context.executor, plan, { signal: context.signal })).stdout).output);
|
|
63
|
-
return judgeScore === null ? candidate : {
|
|
64
|
-
...candidate,
|
|
65
|
-
judgeScore
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function judgePlan(context, candidate, runner, model) {
|
|
70
|
-
const profileId = `select-candidate:judge:${candidate.nodeId}`;
|
|
71
|
-
return createRunnerLaunchPlan({
|
|
72
|
-
...context.config,
|
|
73
|
-
profiles: {
|
|
74
|
-
...context.config.profiles,
|
|
75
|
-
[profileId]: {
|
|
76
|
-
filesystem: { mode: "read-only" },
|
|
77
|
-
instructions: { inline: "Score the candidate implementation." },
|
|
78
|
-
network: { mode: "disabled" },
|
|
79
|
-
output: { format: "text" },
|
|
80
|
-
runner,
|
|
81
|
-
tools: []
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}, {
|
|
85
|
-
model,
|
|
86
|
-
nodeId: profileId,
|
|
87
|
-
profileId,
|
|
88
|
-
prompt: judgePrompt(context.task, candidate.output),
|
|
89
|
-
worktreePath: context.worktreePath
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
function judgePrompt(task, output) {
|
|
93
|
-
return [
|
|
94
|
-
"Score how well this candidate implementation satisfies the task.",
|
|
95
|
-
"Return ONLY a number between 0 and 1 (1 = best). No prose, no fences.",
|
|
96
|
-
"",
|
|
97
|
-
`Task: ${task}`,
|
|
98
|
-
"",
|
|
99
|
-
"Candidate result:",
|
|
100
|
-
output
|
|
101
|
-
].join("\n");
|
|
102
|
-
}
|
|
103
|
-
function parseScore(text) {
|
|
104
|
-
const match = SCORE_RE.exec(text);
|
|
105
|
-
if (!match) return null;
|
|
106
|
-
const value = Number(match[0]);
|
|
107
|
-
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : null;
|
|
108
|
-
}
|
|
109
|
-
function readCandidates(context, upstreamNodeId) {
|
|
110
|
-
if (!upstreamNodeId) return [];
|
|
111
|
-
const upstream = context.plan.graph.node(upstreamNodeId);
|
|
112
|
-
const childrenOutput = parseJsonObject(parseJsonObject(context.nodeStateStore.getOutput(upstreamNodeId)).children);
|
|
113
|
-
return (upstream?.children ?? []).flatMap((child) => {
|
|
114
|
-
const raw = childrenOutput[child.id];
|
|
115
|
-
return raw === void 0 ? [] : [parseCandidate(child.id, raw)];
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function parseCandidate(nodeId, raw) {
|
|
119
|
-
const output = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
120
|
-
const parsed = safeParseObject(output);
|
|
121
|
-
return {
|
|
122
|
-
judgeScore: candidateJudgeScore(parsed),
|
|
123
|
-
nodeId,
|
|
124
|
-
output,
|
|
125
|
-
status: candidateStatus(parsed)
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function candidateStatus(parsed) {
|
|
129
|
-
if (!parsed) return "PASS";
|
|
130
|
-
return parsed.verdict === "FAIL" || parsed.status === "FAIL" ? "FAIL" : "PASS";
|
|
131
|
-
}
|
|
132
|
-
function candidateJudgeScore(parsed) {
|
|
133
|
-
return typeof parsed?.judge_score === "number" ? parsed.judge_score : null;
|
|
134
|
-
}
|
|
135
|
-
function safeParseObject(text) {
|
|
136
|
-
try {
|
|
137
|
-
const value = JSON.parse(text);
|
|
138
|
-
return value && typeof value === "object" ? value : null;
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
//#endregion
|
|
144
|
-
export { executeSelectCandidateBuiltin };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { promoteWorktreeChanges } from "../parallel-worktrees/parallel-worktrees.js";
|
|
2
|
-
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
//#region src/runtime/services/select-candidate-service.ts
|
|
4
|
-
var SelectCandidateService = class extends Context.Tag("SelectCandidateService")() {};
|
|
5
|
-
const SelectCandidateServiceLive = Layer.succeed(SelectCandidateService, {
|
|
6
|
-
executeRunner: (executor, plan, options) => Effect.tryPromise({
|
|
7
|
-
catch: (error) => error,
|
|
8
|
-
try: () => Promise.resolve(executor(plan, options))
|
|
9
|
-
}),
|
|
10
|
-
promoteWinner: (repoRoot, runId, parentNodeId, childNodeId) => Effect.sync(() => promoteWorktreeChanges(repoRoot, runId, parentNodeId, childNodeId))
|
|
11
|
-
});
|
|
12
|
-
//#endregion
|
|
13
|
-
export { SelectCandidateService, SelectCandidateServiceLive };
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
//#region src/schedule/passes/candidates.ts
|
|
2
|
-
/**
|
|
3
|
-
* PIPE-83.7: best-of-N candidate generation. When config.best_of_n is enabled
|
|
4
|
-
* with n > 1, each agent node whose id carries a configured category (e.g.
|
|
5
|
-
* "green") is expanded into a kind:parallel node holding N candidate children
|
|
6
|
-
* (each a full copy with a fresh id and no inter-candidate deps). The wrapper
|
|
7
|
-
* keeps the original id + upstream needs, so downstream consumers and the
|
|
8
|
-
* PIPE-83.9 selector see a single dependency. Default off / n=1 is identity, so
|
|
9
|
-
* generated schedules and the PIPE-57 goldens are unchanged.
|
|
10
|
-
*/
|
|
11
|
-
function expandBestOfNCandidates(config, artifact) {
|
|
12
|
-
const bestOfN = config.best_of_n;
|
|
13
|
-
if (!bestOfN?.enabled || bestOfN.n <= 1) return artifact;
|
|
14
|
-
return {
|
|
15
|
-
...artifact,
|
|
16
|
-
workflows: Object.fromEntries(Object.entries(artifact.workflows).map(([id, workflow]) => [id, {
|
|
17
|
-
...workflow,
|
|
18
|
-
nodes: workflow.nodes.flatMap((node) => expandNode(node, bestOfN.categories, bestOfN.n))
|
|
19
|
-
}]))
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function candidateChild(node, index) {
|
|
23
|
-
const child = {
|
|
24
|
-
...node,
|
|
25
|
-
id: `${node.id}--c${index + 1}`,
|
|
26
|
-
needs: []
|
|
27
|
-
};
|
|
28
|
-
child.task_context = void 0;
|
|
29
|
-
return child;
|
|
30
|
-
}
|
|
31
|
-
function expandNode(node, categories, n) {
|
|
32
|
-
if (node.kind !== "agent" || !categories.some((category) => node.id.includes(category))) return [node];
|
|
33
|
-
const candidatesId = `${node.id}--candidates`;
|
|
34
|
-
return [{
|
|
35
|
-
id: candidatesId,
|
|
36
|
-
kind: "parallel",
|
|
37
|
-
nodes: Array.from({ length: n }, (_, index) => candidateChild(node, index)),
|
|
38
|
-
...node.needs ? { needs: node.needs } : {}
|
|
39
|
-
}, selectCandidateNode(node, candidatesId)];
|
|
40
|
-
}
|
|
41
|
-
function selectCandidateNode(node, candidatesId) {
|
|
42
|
-
return {
|
|
43
|
-
builtin: "select-candidate",
|
|
44
|
-
id: node.id,
|
|
45
|
-
kind: "builtin",
|
|
46
|
-
needs: [candidatesId],
|
|
47
|
-
...node.task_context ? { task_context: node.task_context } : {}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
|
-
export { expandBestOfNCandidates };
|