@oisincoveney/pipeline 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,7 +86,7 @@ scheduler:
86
86
  green-implementation:
87
87
  category: green
88
88
  profile: moka-code-writer
89
- models: [openai/gpt-5.5-high, kimi-for-coding/k2p6, opencode-go/qwen3.7-max]
89
+ models: [opencode-go/qwen3.7-max, openai/gpt-5.5-high, kimi-for-coding/k2p6]
90
90
  verification:
91
91
  category: verification
92
92
  profile: moka-verifier
@@ -109,7 +109,7 @@ scheduler:
109
109
  green-backend:
110
110
  category: green
111
111
  profile: moka-code-writer
112
- models: [openai/gpt-5.5-high, kimi-for-coding/k2p6, opencode-go/qwen3.7-max]
112
+ models: [opencode-go/qwen3.7-max, openai/gpt-5.5-high, kimi-for-coding/k2p6]
113
113
  green-frontend:
114
114
  category: green
115
115
  profile: moka-code-writer
@@ -1,9 +1,10 @@
1
1
  import { loadPipelineConfig } from "../config/load.js";
2
2
  import "../config.js";
3
+ import { flattenNodes } from "../planning/graph.js";
3
4
  import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "../mcp/gateway.js";
4
5
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "../runner.js";
5
6
  import { compileWorkflowPlan } from "../planning/compile.js";
6
- import { generateRuntimeRunId } from "../runtime/context/context.js";
7
+ import { generateRuntimeRunId, resolveWorkflowSelection } from "../runtime/context/context.js";
7
8
  import "../runtime/context/index.js";
8
9
  import { runPipelineFromConfig } from "../pipeline-runtime.js";
9
10
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
@@ -192,7 +193,7 @@ function resolvedRunControlOptions(input) {
192
193
  function plannedRunStoreNodeIds(inputs) {
193
194
  if (inputs.pipelineRunner) return [];
194
195
  const workflowId = resolveWorkflowSelection(inputs.config, inputs.workflow, inputs.entrypoint);
195
- return compileWorkflowPlan(inputs.config, workflowId).topologicalOrder.map((node) => node.id);
196
+ return flattenNodes(compileWorkflowPlan(inputs.config, workflowId).topologicalOrder, (node) => node.children).map((node) => node.id);
196
197
  }
197
198
  function formatSupervisedRunFollowUp(runId) {
198
199
  return [
@@ -566,14 +567,6 @@ function formatWorkflowPlanNode(node, config, worktreePath) {
566
567
  node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none"
567
568
  ].filter(Boolean).join(" ");
568
569
  }
569
- function resolveWorkflowSelection(config, workflowId, entrypointId) {
570
- if (workflowId) return workflowId;
571
- if (!entrypointId) return;
572
- const entrypoint = config.entrypoints[entrypointId];
573
- if (!entrypoint) throw new Error(`Unknown pipeline entrypoint '${entrypointId}'`);
574
- if ("schedule" in entrypoint) throw new Error(`Pipeline entrypoint '${entrypointId}' generates schedule '${entrypoint.schedule}'; use the entrypoint to create a schedule, then run with --schedule.`);
575
- return entrypoint.workflow;
576
- }
577
570
  function formatOrchestratorPlan(config, worktreePath) {
578
571
  if (!config.orchestrator) return "Orchestrator: not configured";
579
572
  const orchestrator = config.profiles[config.orchestrator.profile];
@@ -226,8 +226,8 @@ declare const configSchema: z.ZodObject<{
226
226
  policy: z.ZodOptional<z.ZodObject<{
227
227
  commands: z.ZodOptional<z.ZodEnum<{
228
228
  allow: "allow";
229
- "trusted-only": "trusted-only";
230
229
  deny: "deny";
230
+ "trusted-only": "trusted-only";
231
231
  }>>;
232
232
  modules: z.ZodOptional<z.ZodEnum<{
233
233
  allow: "allow";
@@ -255,8 +255,8 @@ declare const configSchema: z.ZodObject<{
255
255
  global: "global";
256
256
  }>>;
257
257
  mode: z.ZodEnum<{
258
- hosted: "hosted";
259
258
  local: "local";
259
+ hosted: "hosted";
260
260
  }>;
261
261
  provider: z.ZodLiteral<"toolhive">;
262
262
  authorization_env: z.ZodDefault<z.ZodString>;
@@ -299,10 +299,10 @@ declare const configSchema: z.ZodObject<{
299
299
  }, z.core.$strict>>;
300
300
  output: z.ZodOptional<z.ZodObject<{
301
301
  format: z.ZodEnum<{
302
+ json_schema: "json_schema";
302
303
  text: "text";
303
304
  json: "json";
304
305
  jsonl: "jsonl";
305
- json_schema: "json_schema";
306
306
  }>;
307
307
  repair: z.ZodOptional<z.ZodObject<{
308
308
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -371,10 +371,10 @@ declare const configSchema: z.ZodObject<{
371
371
  disabled: "disabled";
372
372
  }>>>;
373
373
  output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
374
+ json_schema: "json_schema";
374
375
  text: "text";
375
376
  json: "json";
376
377
  jsonl: "jsonl";
377
- json_schema: "json_schema";
378
378
  }>>>;
379
379
  rules: z.ZodOptional<z.ZodBoolean>;
380
380
  skills: z.ZodOptional<z.ZodBoolean>;
@@ -481,8 +481,8 @@ declare const configSchema: z.ZodObject<{
481
481
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
482
482
  description: z.ZodOptional<z.ZodString>;
483
483
  baseline: z.ZodEnum<{
484
- quick: "quick";
485
484
  execute: "execute";
485
+ quick: "quick";
486
486
  }>;
487
487
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
488
488
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -9,17 +9,28 @@ function fallbackModelSelection(models, options) {
9
9
  skipped: []
10
10
  };
11
11
  const disabled = disabledModels();
12
- const enabled = models.filter((candidate) => !disabled.has(candidate));
13
- const disabledSkipped = models.filter((candidate) => disabled.has(candidate));
14
- if (!options) {
12
+ const available = options?.available;
13
+ const enabled = models.filter((candidate) => !disabled.has(candidate) && isAvailable(candidate, available));
14
+ const skipped = models.filter((candidate) => disabled.has(candidate) || !isAvailable(candidate, available));
15
+ const sizing = sizingFromOptions(options);
16
+ if (!sizing) {
15
17
  const model = enabled[0];
16
18
  return {
17
19
  model,
18
20
  reason: selectionReason(model),
19
- skipped: disabledSkipped
21
+ skipped
20
22
  };
21
23
  }
22
- return sizedSelection(enabled, disabledSkipped, options);
24
+ return sizedSelection(enabled, skipped, sizing);
25
+ }
26
+ function isAvailable(candidate, available) {
27
+ return available ? available.has(candidate) : true;
28
+ }
29
+ function sizingFromOptions(options) {
30
+ if (options?.budget && typeof options.estimatedTokens === "number") return {
31
+ budget: options.budget,
32
+ estimatedTokens: options.estimatedTokens
33
+ };
23
34
  }
24
35
  function sizedSelection(enabled, disabledSkipped, options) {
25
36
  const { estimatedTokens, budget } = options;
@@ -160,8 +160,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
160
160
  }, z.core.$strict>>;
161
161
  serviceAccountName: z.ZodOptional<z.ZodString>;
162
162
  mode: z.ZodEnum<{
163
- full: "full";
164
163
  quick: "quick";
164
+ full: "full";
165
165
  }>;
166
166
  schedulePath: z.ZodOptional<z.ZodString>;
167
167
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -77,10 +77,12 @@ function runWithLeasedOpencode(options, config, worktreePath, run) {
77
77
  ...options.signal ? { signal: options.signal } : {},
78
78
  worktreePath
79
79
  })), (lease) => Effect.promise(() => lease.release()));
80
+ const availableModels = yield* Effect.promise(() => lease.availableModels());
80
81
  return yield* run({
81
82
  ...options,
82
83
  config,
83
- executor: lease.executor
84
+ executor: lease.executor,
85
+ ...availableModels ? { availableModels } : {}
84
86
  });
85
87
  }));
86
88
  }
@@ -20,11 +20,11 @@ function createRunStoreRuntimeReporterRuntime(input) {
20
20
  const activeHookPreviousStatuses = /* @__PURE__ */ new Map();
21
21
  let writeChain = Promise.resolve();
22
22
  const enqueue = (event) => {
23
- const projection = projectRuntimeEvent(event, {
23
+ const persisted = persistRuntimeEventEffect(input, event, projectRuntimeEvent(event, {
24
24
  activeHookPreviousStatuses,
25
25
  observedNodeStatuses
26
- });
27
- writeChain = writeChain.then(() => withRunStateLock(() => Effect.runPromise(persistRuntimeEventEffect(input, event, projection, now))));
26
+ }), now).pipe(Effect.catchAll((error) => warnPersistSkipped(input, event, error)));
27
+ writeChain = writeChain.then(() => withRunStateLock(() => Effect.runPromise(persisted)));
28
28
  };
29
29
  const flushEffect = () => Effect.tryPromise({
30
30
  catch: (error) => error,
@@ -184,5 +184,14 @@ function timestamp(now) {
184
184
  function assertNever(value) {
185
185
  throw new Error(`Unhandled runtime reporter value: ${String(value)}`);
186
186
  }
187
+ function warnPersistSkipped(input, event, error) {
188
+ return Effect.sync(() => {
189
+ const nodeId = "nodeId" in event && typeof event.nodeId === "string" ? ` node=${event.nodeId}` : "";
190
+ process.stderr.write(`run-control: skipped persisting ${event.type}${nodeId} for run ${input.runId}: ${errorMessage(error)}\n`);
191
+ });
192
+ }
193
+ function errorMessage(error) {
194
+ return error instanceof Error ? error.message : String(error);
195
+ }
187
196
  //#endregion
188
197
  export { createRunStoreRuntimeReporter };
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- full: "full";
47
46
  quick: "quick";
47
+ full: "full";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- full: "full";
108
107
  quick: "quick";
108
+ full: "full";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;
@@ -25,7 +25,7 @@ function executeAgentNodeEffect(node, context, attempt) {
25
25
  output: ""
26
26
  };
27
27
  const prompt = yield* renderAgentPromptEffect(node, context);
28
- const decision = decideNodeModel(prompt, node, context.config.token_budget);
28
+ const decision = decideNodeModel(prompt, node, context.config.token_budget, context.availableModels);
29
29
  if (decision.overBudget) return {
30
30
  evidence: [
31
31
  `agent boundary node=${node.id} profile=${node.profile}`,
@@ -155,14 +155,15 @@ function createHandoffFinalizerPlan(context, node, runner, rawOutput) {
155
155
  * with a fallback array but no fitting model is `overBudget` — the caller fails
156
156
  * it fast rather than truncating.
157
157
  */
158
- function decideNodeModel(prompt, node, budget) {
158
+ function decideNodeModel(prompt, node, budget, availableModels) {
159
159
  const estimatedTokens = estimateTokens(prompt);
160
160
  if (!(budget && node.models?.length)) return {
161
161
  estimatedTokens,
162
162
  overBudget: false,
163
- selection: selectNodeModel(node)
163
+ selection: selectNodeModel(node, { available: availableModels })
164
164
  };
165
165
  const selection = selectNodeModel(node, {
166
+ available: availableModels,
166
167
  budget,
167
168
  estimatedTokens
168
169
  });
@@ -18,6 +18,7 @@ function createRuntimeContext(options) {
18
18
  const observability = options.reporter ? createPublicRuntimeObservabilityEmitter(options.reporter, workflowId) : void 0;
19
19
  return {
20
20
  agentInvocations: [],
21
+ ...options.availableModels ? { availableModels: options.availableModels } : {},
21
22
  ...runId ? { runId } : {},
22
23
  config,
23
24
  executor: options.executor ?? runLaunchPlan,
@@ -70,4 +71,4 @@ function generateRuntimeRunId() {
70
71
  return `run-${randomUUID()}`;
71
72
  }
72
73
  //#endregion
73
- export { createRuntimeContext, generateRuntimeRunId, normalizeMaxParallelNodes, resolveWorkflowSelection };
74
+ export { createRuntimeContext, generateRuntimeRunId, resolveWorkflowSelection };
@@ -248,6 +248,7 @@ type PipelineRuntimeEvent = {
248
248
  * single-node, schedule-driven execution path.
249
249
  */
250
250
  interface PipelineRuntimeOptions {
251
+ availableModels?: ReadonlySet<string>;
251
252
  config?: PipelineConfig;
252
253
  entrypoint?: string;
253
254
  executor?: (plan: RunnerLaunchPlan, options: RunnerExecutionOptions) => AgentResult | Promise<AgentResult>;
@@ -1,3 +1,4 @@
1
+ import { flattenNodes } from "../../planning/graph.js";
1
2
  import { isRecord } from "../../safe-json.js";
2
3
  import { runtimeActorId } from "../actor-ids.js";
3
4
  import { parseRuntimeOutput, validateJsonSchemaSource } from "../json-validation/json-validation.js";
@@ -125,7 +126,7 @@ function emitWorkflowPlanned(context) {
125
126
  }
126
127
  function emitWorkflowStarted(context) {
127
128
  emit(context, {
128
- nodeIds: context.plan.topologicalOrder.map((node) => node.id),
129
+ nodeIds: flattenNodes(context.plan.topologicalOrder, (node) => node.children).map((node) => node.id),
129
130
  type: "workflow.start",
130
131
  workflowId: context.workflowId
131
132
  });
@@ -27,6 +27,7 @@ function leaseOpencodeRuntimeEffect(input) {
27
27
  const registry = createOpencodeSessionRegistry();
28
28
  let handle;
29
29
  let pending;
30
+ let availableModelsPending;
30
31
  const ensureExecutor = () => {
31
32
  pending ??= Effect.runPromise(Effect.provide(ensureExecutorEffect(input, registry), OpencodeRuntimeServerServiceLive)).then((executor) => {
32
33
  handle = executor.handle;
@@ -34,7 +35,12 @@ function leaseOpencodeRuntimeEffect(input) {
34
35
  });
35
36
  return pending;
36
37
  };
38
+ const resolveAvailableModels = () => {
39
+ availableModelsPending ??= ensureExecutor().then(() => handle ? queryAvailableOpencodeModels(handle.client) : void 0).catch(() => void 0);
40
+ return availableModelsPending;
41
+ };
37
42
  return Effect.succeed({
43
+ availableModels: resolveAvailableModels,
38
44
  executor: async (plan, options) => {
39
45
  return await (await ensureExecutor())(plan, options);
40
46
  },
@@ -46,6 +52,15 @@ function leaseOpencodeRuntimeEffect(input) {
46
52
  }
47
53
  });
48
54
  }
55
+ /**
56
+ * Collect every model the leased server can resolve (each authenticated
57
+ * provider's models as `provider/model`) from the opencode `/config/providers`
58
+ * endpoint, for availability-aware model selection.
59
+ */
60
+ async function queryAvailableOpencodeModels(client) {
61
+ const providers = (await client.config.providers()).data?.providers ?? [];
62
+ return new Set(providers.flatMap((provider) => Object.keys(provider.models ?? {}).map((modelId) => `${provider.id}/${modelId}`)));
63
+ }
49
64
  function ensureExecutorEffect(input, registry) {
50
65
  return Effect.gen(function* () {
51
66
  const handle = yield* (yield* OpencodeRuntimeServerService).open(input);
@@ -22,15 +22,11 @@ function addWorkflowImplementationCoverage(config, workflow, coverageProfileId)
22
22
  };
23
23
  }
24
24
  function addNodeScopeImplementationCoverage(config, nodes, coverageProfileId) {
25
- const scopedNodes = nodes.map((node) => node.kind === "parallel" ? {
26
- ...node,
27
- nodes: addNodeScopeImplementationCoverage(config, node.nodes, coverageProfileId)
28
- } : node);
29
- const index = dependentsByNeed(scopedNodes);
30
- const uncovered = scopedNodes.filter((node) => isImplementationNode(config, node)).filter((node) => !hasDownstreamCoverage(config, node.id, index));
31
- if (uncovered.length === 0) return scopedNodes;
32
- const coverageNodeId = uniqueGeneratedId("generated-coverage", new Set(scopedNodes.map((node) => node.id)), "generated-coverage");
33
- return [...scopedNodes, {
25
+ const index = dependentsByNeed(nodes);
26
+ const uncovered = nodes.filter((node) => nodeNeedsImplementationCoverage(config, node)).filter((node) => !hasDownstreamCoverage(config, node.id, index));
27
+ if (uncovered.length === 0) return nodes;
28
+ const coverageNodeId = uniqueGeneratedId("generated-coverage", new Set(nodes.map((node) => node.id)), "generated-coverage");
29
+ return [...nodes, {
34
30
  gates: generatedCoverageGates(coverageNodeId),
35
31
  id: coverageNodeId,
36
32
  kind: "agent",
@@ -38,6 +34,10 @@ function addNodeScopeImplementationCoverage(config, nodes, coverageProfileId) {
38
34
  profile: coverageProfileId
39
35
  }];
40
36
  }
37
+ function nodeNeedsImplementationCoverage(config, node) {
38
+ if (isImplementationNode(config, node)) return true;
39
+ return node.kind === "parallel" && node.nodes.some((child) => nodeNeedsImplementationCoverage(config, child));
40
+ }
41
41
  function generatedCoverageProfileId(config) {
42
42
  const coverageProfiles = Object.entries(config.profiles).filter(([, profile]) => profile.scheduling_roles?.includes("coverage")).map(([id]) => id);
43
43
  if (coverageProfiles.length === 0) return null;
package/package.json CHANGED
@@ -126,7 +126,7 @@
126
126
  "prepack": "bun run build:cli"
127
127
  },
128
128
  "type": "module",
129
- "version": "3.1.1",
129
+ "version": "3.3.0",
130
130
  "description": "Config-driven multi-agent pipeline runner for repository work",
131
131
  "main": "./dist/index.js",
132
132
  "types": "./dist/index.d.ts",