@oisincoveney/pipeline 3.2.0 → 3.3.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.
@@ -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
@@ -4,7 +4,7 @@ import { flattenNodes } from "../planning/graph.js";
4
4
  import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "../mcp/gateway.js";
5
5
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "../runner.js";
6
6
  import { compileWorkflowPlan } from "../planning/compile.js";
7
- import { generateRuntimeRunId } from "../runtime/context/context.js";
7
+ import { generateRuntimeRunId, resolveWorkflowSelection } from "../runtime/context/context.js";
8
8
  import "../runtime/context/index.js";
9
9
  import { runPipelineFromConfig } from "../pipeline-runtime.js";
10
10
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
@@ -567,14 +567,6 @@ function formatWorkflowPlanNode(node, config, worktreePath) {
567
567
  node.artifacts?.length ? `artifacts=${node.artifacts.map((artifact) => artifact.path).join(",")}` : "artifacts=none"
568
568
  ].filter(Boolean).join(" ");
569
569
  }
570
- function resolveWorkflowSelection(config, workflowId, entrypointId) {
571
- if (workflowId) return workflowId;
572
- if (!entrypointId) return;
573
- const entrypoint = config.entrypoints[entrypointId];
574
- if (!entrypoint) throw new Error(`Unknown pipeline entrypoint '${entrypointId}'`);
575
- 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.`);
576
- return entrypoint.workflow;
577
- }
578
570
  function formatOrchestratorPlan(config, worktreePath) {
579
571
  if (!config.orchestrator) return "Orchestrator: not configured";
580
572
  const orchestrator = config.profiles[config.orchestrator.profile];
package/dist/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- fail: "fail";
17
16
  pass: "pass";
17
+ fail: "fail";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
@@ -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";
8
10
  "workflow.success": "workflow.success";
9
11
  "workflow.failure": "workflow.failure";
10
12
  "workflow.complete": "workflow.complete";
11
- "node.start": "node.start";
12
13
  "node.success": "node.success";
13
14
  "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";
97
99
  "workflow.success": "workflow.success";
98
100
  "workflow.failure": "workflow.failure";
99
101
  "workflow.complete": "workflow.complete";
100
- "node.start": "node.start";
101
102
  "node.success": "node.success";
102
103
  "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";
209
211
  "workflow.success": "workflow.success";
210
212
  "workflow.failure": "workflow.failure";
211
213
  "workflow.complete": "workflow.complete";
212
- "node.start": "node.start";
213
214
  "node.success": "node.success";
214
215
  "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<{
@@ -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
  }
@@ -5,6 +5,7 @@ import { dependentsByNeed, flattenNodes, hasReachableDependent } from "./graph.j
5
5
  import { createRunnerLaunchPlan, runLaunchPlan } from "../runner.js";
6
6
  import { normalizeRunnerOutput } from "../runner-output.js";
7
7
  import { compileWorkflowPlan } from "./compile.js";
8
+ import { ensurePipelineWorkspaceIgnore } from "../run-control/workspace.js";
8
9
  import { loadBacklogPlanningContext } from "../schedule/backlog-context.js";
9
10
  import { baselineScheduleArtifact } from "../schedule/baseline.js";
10
11
  import { isCoverageNode, isImplementationNode, isWriteCapableParallelChild } from "../schedule/scheduling-roles.js";
@@ -111,6 +112,7 @@ function assertSchedulePassOrder() {
111
112
  ].join("\0")) throw new ScheduleArtifactError("Schedule pass order is misconfigured");
112
113
  }
113
114
  function persistScheduleArtifact(worktreePath, artifact) {
115
+ ensurePipelineWorkspaceIgnore(worktreePath);
114
116
  const relativePath = join(".pipeline", "runs", artifact.schedule_id, "schedule.yaml");
115
117
  const fullPath = join(worktreePath, relativePath);
116
118
  mkdirSync(join(worktreePath, ".pipeline", "runs", artifact.schedule_id), { recursive: true });
@@ -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 };
@@ -1,3 +1,4 @@
1
+ import { ensurePipelineWorkspaceIgnore } from "./workspace.js";
1
2
  import { DEFAULT_RUN_CONTROL_STALE_DETECTION, parseMokaNodeStatus, parseMokaRunController, parseMokaRunEvent, parseMokaRunManifest, parseMokaRunStatus, parseRunControlStaleDetection, parseRunEffort, parseRunMode, parseRunTarget } from "./contracts.js";
2
3
  import { Effect } from "effect";
3
4
  import { isAbsolute, join } from "node:path";
@@ -15,6 +16,7 @@ function createRunEffect(input) {
15
16
  return Effect.gen(function* () {
16
17
  const { manifest, nodeIds, runId } = yield* Effect.sync(() => createRunManifest(input));
17
18
  const paths = runPaths(input.workspaceRoot, runId);
19
+ yield* Effect.sync(() => ensurePipelineWorkspaceIgnore(input.workspaceRoot));
18
20
  yield* mkdirEffect(paths.runsRoot, { recursive: true });
19
21
  yield* mkdirEffect(paths.runRoot, { recursive: true });
20
22
  yield* mkdirEffect(paths.nodesRoot, { recursive: true });
@@ -0,0 +1,23 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ //#region src/run-control/workspace.ts
4
+ const PIPELINE_DIR = ".pipeline";
5
+ const GITIGNORE_PATH = join(PIPELINE_DIR, ".gitignore");
6
+ const GITIGNORE_CONTENT = "*\n";
7
+ /**
8
+ * Ensures `.pipeline/.gitignore` exists in `worktreePath` so that moka's
9
+ * runtime artifacts are self-ignored and never picked up by lint/format tools
10
+ * running inside the target repo.
11
+ *
12
+ * Idempotent: creates the file only if it does not already exist; never
13
+ * overwrites an existing `.pipeline/.gitignore`.
14
+ */
15
+ function ensurePipelineWorkspaceIgnore(worktreePath) {
16
+ const pipelineDir = join(worktreePath, PIPELINE_DIR);
17
+ const gitignorePath = join(worktreePath, GITIGNORE_PATH);
18
+ if (existsSync(gitignorePath)) return;
19
+ mkdirSync(pipelineDir, { recursive: true });
20
+ writeFileSync(gitignorePath, GITIGNORE_CONTENT);
21
+ }
22
+ //#endregion
23
+ export { ensurePipelineWorkspaceIgnore };
@@ -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.start": "workflow.start";
14
13
  "workflow.planned": "workflow.planned";
14
+ "workflow.start": "workflow.start";
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";
60
58
  "agent.finish": "agent.finish";
61
59
  "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>;
@@ -103,8 +103,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
103
103
  nodeId: z.ZodOptional<z.ZodString>;
104
104
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
105
105
  status: z.ZodEnum<{
106
- fail: "fail";
107
106
  pass: "pass";
107
+ fail: "fail";
108
108
  skip: "skip";
109
109
  }>;
110
110
  summary: 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.start": "workflow.start";
184
183
  "workflow.planned": "workflow.planned";
184
+ "workflow.start": "workflow.start";
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";
230
228
  "agent.finish": "agent.finish";
231
229
  "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>;
@@ -273,8 +273,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
273
273
  nodeId: z.ZodOptional<z.ZodString>;
274
274
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
275
275
  status: z.ZodEnum<{
276
- fail: "fail";
277
276
  pass: "pass";
277
+ fail: "fail";
278
278
  skip: "skip";
279
279
  }>;
280
280
  summary: z.ZodOptional<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>;
@@ -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.2.0",
129
+ "version": "3.3.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",