@oisincoveney/pipeline 2.8.3 → 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.
@@ -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
- # best_of_n / parallel_worktrees: the verifier-pattern dial. Schedule generation
19
- # + selection are validated/tested, BUT a live end-to-end run (2026-06-16) proved
20
- # the EXECUTION is not yet production-ready, so it stays OFF by default (on-by-
21
- # default made real runs hang). Two runtime gaps remain — see PIPE-83.14:
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:
@@ -34,7 +34,6 @@ function durabilityField(durability) {
34
34
  }
35
35
  function pipe83Fields(pipeline) {
36
36
  const keys = [
37
- "best_of_n",
38
37
  "context_handoff",
39
38
  "parallel_worktrees",
40
39
  "repo_map"
@@ -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>;
@@ -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(),
@@ -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<{
@@ -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", "select-candidate"]);
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, expandBestOfNCandidates(options.config, addGeneratedImplementationCoverage(options.config, generatedArtifact)))), planningContext);
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) || isSelectCandidateNode(node)).flatMap((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 { dirname, join } from "node:path";
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, promoteWorktreeChanges };
144
+ export { createChildWorktree, gcParallelWorktrees };
@@ -1,7 +1,6 @@
1
1
  //#region src/schedule/passes/index.ts
2
2
  const SCHEDULE_PASS_ORDER = [
3
3
  "coverage",
4
- "candidates",
5
4
  "models",
6
5
  "ids",
7
6
  "references"
@@ -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, durability & best-of-N features (PIPE-83)
362
+ ## Context & durability features (PIPE-83)
363
363
 
364
- The shipped `defaults/pipeline.yaml` turns **`context_handoff`, `repo_map`,
365
- `durability`, `best_of_n` (n=2 on green), and `parallel_worktrees` ON** — moka
366
- uses its architecture by default. `best_of_n` is the cost/quality dial: n=2
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.3",
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 };