@oisincoveney/pipeline 2.1.1 → 2.2.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.
Files changed (37) hide show
  1. package/defaults/pipeline.yaml +111 -0
  2. package/defaults/profiles.yaml +212 -0
  3. package/defaults/runners.yaml +24 -0
  4. package/dist/argo-graph.js +59 -27
  5. package/dist/argo-submit.js +10 -2
  6. package/dist/claude-settings-config.js +44 -0
  7. package/dist/cli/program.js +10 -2
  8. package/dist/config/defaults.js +7 -350
  9. package/dist/config/schemas.d.ts +5 -5
  10. package/dist/install-commands/claude-code.js +160 -0
  11. package/dist/install-commands/opencode.js +39 -32
  12. package/dist/install-commands/shared.js +32 -5
  13. package/dist/install-commands.js +26 -15
  14. package/dist/json-config-merge.js +47 -0
  15. package/dist/mcp/gateway.js +9 -1
  16. package/dist/moka-submit.d.ts +7 -7
  17. package/dist/opencode-project-config.js +2 -45
  18. package/dist/pipeline-runtime.js +50 -8
  19. package/dist/runner-command-contract.d.ts +2 -2
  20. package/dist/runner-event-schema.d.ts +349 -0
  21. package/dist/runner-event-schema.js +185 -0
  22. package/dist/runner-output.js +2 -2
  23. package/dist/runner.d.ts +2 -0
  24. package/dist/runtime/agent-node/agent-node.js +1 -0
  25. package/dist/runtime/contracts/contracts.d.ts +2 -0
  26. package/dist/runtime/node-state-store.js +7 -0
  27. package/dist/runtime/opencode-adapter.js +28 -8
  28. package/dist/runtime/opencode-agent-name.js +18 -0
  29. package/dist/runtime/opencode-runtime.js +62 -0
  30. package/dist/runtime/opencode-server.js +67 -0
  31. package/dist/runtime/opencode-session-executor.js +206 -0
  32. package/docs/adr-opencode-first-goal-loop-runtime.md +1 -1
  33. package/docs/config-architecture.md +11 -6
  34. package/docs/mcp-gateway.md +4 -4
  35. package/docs/operator-guide.md +7 -5
  36. package/docs/slash-command-adapter-contract.md +1 -0
  37. package/package.json +6 -1
@@ -2,14 +2,15 @@ import { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST } from "../config/defaults.js";
2
2
  import { resolvePackageAssetPath } from "../package-assets.js";
3
3
  import "../config.js";
4
4
  import { compileWorkflowPlan } from "../workflow-planner.js";
5
+ import { mergeOpenCodeProjectConfig } from "../opencode-project-config.js";
5
6
  import { renderOpenCodeGatewayConfig } from "../mcp/gateway.js";
6
- import { AGENTS_MD_END, AGENTS_MD_START, COMMAND_HOSTS, GENERATED_MARKER, GENERATED_TS_MARKER, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, invocationForHost } from "./shared.js";
7
+ import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
8
+ import { AGENTS_MD_END, AGENTS_MD_START, COMMAND_HOSTS, GENERATED_MARKER, GENERATED_TS_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries } from "./shared.js";
7
9
  import { readFileSync } from "node:fs";
8
10
  import { basename } from "node:path";
9
11
  import matter from "gray-matter";
10
12
  //#region src/install-commands/opencode.ts
11
13
  const OPENCODE_ORCHESTRATOR_AGENT_ID = "MoKa Orchestrator";
12
- const MOKA_PROFILE_PREFIX = "moka-";
13
14
  function header(host) {
14
15
  return [
15
16
  GENERATED_MARKER,
@@ -20,19 +21,6 @@ function header(host) {
20
21
  function markdown(data, body) {
21
22
  return `${matter.stringify(body.trimEnd(), data).trimEnd()}\n`;
22
23
  }
23
- function profileEntries(config) {
24
- return Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b));
25
- }
26
- function entrypointEntries(config) {
27
- const entries = Object.entries(config.entrypoints);
28
- return entries.length > 0 ? entries : [["execute", {
29
- description: "Run the configured pipeline workflow",
30
- workflow: config.default_workflow
31
- }]];
32
- }
33
- function entrypointDescription(id, entrypoint) {
34
- return entrypoint.description ?? `Run the ${id} workflow`;
35
- }
36
24
  function entrypointCommandDefinitions(_host, config, makeDefinition) {
37
25
  return entrypointEntries(config).map(([id, entrypoint]) => makeDefinition(id, entrypoint));
38
26
  }
@@ -104,14 +92,6 @@ function dispatchRouteForAgent(host, config, route) {
104
92
  function nativeAgentIdForHost(host, profileId) {
105
93
  return host === "opencode" ? opencodeAgentName(profileId) : profileId;
106
94
  }
107
- function opencodeAgentName(profileId) {
108
- if (!profileId.startsWith(MOKA_PROFILE_PREFIX)) return profileId;
109
- return `MoKa ${profileId.slice(5).split("-").map(opencodeAgentNamePart).join(" ")}`;
110
- }
111
- function opencodeAgentNamePart(part) {
112
- if (part === "opencode") return "OpenCode";
113
- return `${part.charAt(0).toUpperCase()}${part.slice(1)}`;
114
- }
115
95
  function grants(actor) {
116
96
  return [
117
97
  `model: ${actor.model ?? "default"}`,
@@ -222,14 +202,14 @@ function hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes) {
222
202
  if (cliRoutes.length > 0 && nativeRoutes.length === 0) return `Do not claim these nodes are ${hostDisplayName(host)} subagents.`;
223
203
  }
224
204
  function hostDisplayName(host) {
225
- return { opencode: "OpenCode" }[host];
205
+ return {
206
+ opencode: "OpenCode",
207
+ "claude-code": "Claude Code"
208
+ }[host];
226
209
  }
227
210
  function needsSummary(needs) {
228
211
  return needs.length > 0 ? needs.join(",") : "none";
229
212
  }
230
- function compactLines(lines) {
231
- return lines.filter((line) => line !== void 0);
232
- }
233
213
  const OPENCODE_PERMISSION_TOOLS = [
234
214
  "bash",
235
215
  "edit",
@@ -423,9 +403,36 @@ function opencodeModelProjection(config, profile) {
423
403
  const model = resolvedHostModel(config, "opencode", profile);
424
404
  return model ? { model } : {};
425
405
  }
426
- function instructionsPointer(actor) {
427
- if (actor.instructions.path) return `Instructions: ${actor.instructions.path}`;
428
- return `Instructions:\n${actor.instructions.inline ?? ""}`;
429
- }
406
+ /**
407
+ * The opencode HostAdapter. Encapsulates all opencode-specific command
408
+ * generation, resource roots, and config-merge behaviour.
409
+ */
410
+ const opencodeAdapter = {
411
+ host: "opencode",
412
+ resourceRoots: [
413
+ ".opencode/commands",
414
+ ".opencode/agents",
415
+ ".opencode/plugins",
416
+ ".opencode/skills"
417
+ ],
418
+ definitions(config, cwd) {
419
+ return opencodeDefinitions(config, cwd);
420
+ },
421
+ mergeDefinition(definition, existingContent) {
422
+ if (definition.path !== ".opencode/opencode.json") return;
423
+ const merged = mergeOpenCodeProjectConfig(existingContent, JSON.parse(definition.content));
424
+ if (!merged.ok) return {
425
+ ok: false,
426
+ content: definition.content
427
+ };
428
+ return {
429
+ ok: true,
430
+ content: merged.content
431
+ };
432
+ },
433
+ isAlwaysForced(definition) {
434
+ return definition.path === OPENCODE_PROJECT_CONFIG_PATH;
435
+ }
436
+ };
430
437
  //#endregion
431
- export { opencodeDefinitions };
438
+ export { agentDispatchRoutes, entrypointDispatchBlock, grants, header, markdown, opencodeAdapter, projectAgentsMdDefinition, resolvedHostModel, scheduledEntrypointK8sNote };
@@ -9,15 +9,42 @@ const AGENTS_MD_START = "<!-- @oisincoveney/pipeline:agents:start -->";
9
9
  const AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
10
10
  const SINGLE_OPENCODE_PLUGIN_ARRAY_RE = /\n {2}"plugin": \[\n {4}("[^"]+")\n {2}\]/;
11
11
  const OPENCODE_PROJECT_CONFIG_PATH = ".opencode/opencode.json";
12
+ const CLAUDE_PROJECT_CONFIG_PATH = ".claude/settings.json";
12
13
  const OPENCODE_COMMAND_PREFIX = "moka-";
13
- const ENTRYPOINT_PATH_PATTERNS = { opencode: [/^\.opencode\/commands\/(?:moka-)?([^/]+)\.md$/] };
14
- const COMMAND_HOSTS = ["opencode"];
14
+ const ENTRYPOINT_PATH_PATTERNS = {
15
+ opencode: [/^\.opencode\/commands\/(?:moka-)?([^/]+)\.md$/],
16
+ "claude-code": [/^\.claude\/commands\/(?:moka-)?([^/]+)\.md$/]
17
+ };
18
+ const COMMAND_HOSTS = ["opencode", "claude-code"];
19
+ function profileEntries(config) {
20
+ return Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b));
21
+ }
22
+ function entrypointEntries(config) {
23
+ const entries = Object.entries(config.entrypoints);
24
+ return entries.length > 0 ? entries : [["execute", {
25
+ description: "Run the configured pipeline workflow",
26
+ workflow: config.default_workflow
27
+ }]];
28
+ }
29
+ function entrypointDescription(id, entrypoint) {
30
+ return entrypoint.description ?? `Run the ${id} workflow`;
31
+ }
32
+ function instructionsPointer(actor) {
33
+ if (actor.instructions.path) return `Instructions: ${actor.instructions.path}`;
34
+ return `Instructions:\n${actor.instructions.inline ?? ""}`;
35
+ }
36
+ function compactLines(lines) {
37
+ return lines.filter((line) => line !== void 0);
38
+ }
15
39
  function invocationForHost(host, entrypointId = "execute") {
16
- return `${{ opencode: "/" }[host]}${commandIdForHost(host, entrypointId)} <task description>`;
40
+ return `${{
41
+ opencode: "/",
42
+ "claude-code": "/"
43
+ }[host]}${commandIdForHost(host, entrypointId)} <task description>`;
17
44
  }
18
45
  function commandIdForHost(host, entrypointId) {
19
- if (host === "opencode") return `${OPENCODE_COMMAND_PREFIX}${entrypointId}`;
46
+ if (host === "opencode" || host === "claude-code") return `${OPENCODE_COMMAND_PREFIX}${entrypointId}`;
20
47
  return entrypointId;
21
48
  }
22
49
  //#endregion
23
- export { AGENTS_MD_END, AGENTS_MD_START, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, invocationForHost };
50
+ export { AGENTS_MD_END, AGENTS_MD_START, CLAUDE_PROJECT_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries };
@@ -1,15 +1,18 @@
1
1
  import { loadPipelineConfig } from "./config/load.js";
2
2
  import "./config.js";
3
- import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
4
3
  import { COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, invocationForHost } from "./install-commands/shared.js";
5
- import { opencodeDefinitions } from "./install-commands/opencode.js";
4
+ import { opencodeAdapter } from "./install-commands/opencode.js";
5
+ import { claudeCodeAdapter } from "./install-commands/claude-code.js";
6
6
  import { existsSync, readFileSync, statSync } from "node:fs";
7
7
  import { dirname, join, relative } from "node:path";
8
8
  import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
9
9
  //#region src/install-commands.ts
10
+ const ADAPTERS = {
11
+ opencode: opencodeAdapter,
12
+ "claude-code": claudeCodeAdapter
13
+ };
10
14
  function definitionsFor(host, config, cwd) {
11
- const definitions = { opencode: () => opencodeDefinitions(config, cwd) };
12
- return dedupeDefinitionsByPath((host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => definitions[name]()));
15
+ return dedupeDefinitionsByPath((host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => ADAPTERS[name].definitions(config, cwd)));
13
16
  }
14
17
  function dedupeDefinitionsByPath(definitions) {
15
18
  const lastIndexes = /* @__PURE__ */ new Map();
@@ -21,12 +24,9 @@ function dedupeDefinitionsByPath(definitions) {
21
24
  function selectedHosts(host) {
22
25
  return host === "all" ? [...COMMAND_HOSTS] : [host];
23
26
  }
24
- const GENERATED_RESOURCE_ROOTS = { opencode: [
25
- ".opencode/commands",
26
- ".opencode/agents",
27
- ".opencode/plugins",
28
- ".opencode/skills"
29
- ] };
27
+ function resourceRootsFor(host) {
28
+ return ADAPTERS[host].resourceRoots;
29
+ }
30
30
  async function listFiles(root) {
31
31
  if (!existsSync(root)) return [];
32
32
  if (statSync(root).isFile()) return [root];
@@ -42,7 +42,7 @@ function generatedHostFor(content) {
42
42
  }
43
43
  async function obsoleteGeneratedItems(cwd, host, wantedPaths) {
44
44
  const hosts = new Set(selectedHosts(host));
45
- const roots = selectedHosts(host).flatMap((selectedHost) => GENERATED_RESOURCE_ROOTS[selectedHost]);
45
+ const roots = selectedHosts(host).flatMap((selectedHost) => resourceRootsFor(selectedHost));
46
46
  return (await Promise.all(roots.map((root) => listFiles(join(cwd, root))))).flat().flatMap((absolutePath) => {
47
47
  const generatedHost = generatedHostFor(readFileSync(absolutePath, "utf8"));
48
48
  if (!(generatedHost && hosts.has(generatedHost))) return [];
@@ -63,12 +63,19 @@ function entrypointIdFromGeneratedPath(host, path) {
63
63
  }
64
64
  }
65
65
  function resolveDefinitionContent(definition, target) {
66
- if (definition.path !== ".opencode/opencode.json" || !existsSync(target)) return {
66
+ const adapter = ADAPTERS[definition.host];
67
+ if (!(adapter.mergeDefinition && existsSync(target))) return {
68
+ conflict: false,
69
+ content: definition.content
70
+ };
71
+ return applyMergeDefinition(adapter.mergeDefinition.bind(adapter), definition, target);
72
+ }
73
+ function applyMergeDefinition(merge, definition, target) {
74
+ const merged = merge(definition, readFileSync(target, "utf8"));
75
+ if (!merged) return {
67
76
  conflict: false,
68
77
  content: definition.content
69
78
  };
70
- const projection = JSON.parse(definition.content);
71
- const merged = mergeOpenCodeProjectConfig(readFileSync(target, "utf8"), projection);
72
79
  if (!merged.ok) return {
73
80
  conflict: true,
74
81
  content: definition.content
@@ -101,9 +108,13 @@ function upsertGeneratedBlock(current, content, block) {
101
108
  const separator = current.trimEnd().length > 0 ? "\n\n" : "";
102
109
  return `${current.trimEnd()}${separator}${content}`;
103
110
  }
111
+ function adapterForcesDefinition(definition) {
112
+ const fn = ADAPTERS[definition.host].isAlwaysForced;
113
+ return fn ? fn(definition) : false;
114
+ }
104
115
  function installActionForDefinition(definition, target, resolved, force) {
105
116
  if (resolved.conflict) return "conflict";
106
- return actionFor(target, resolved.content, force || definition.path === ".opencode/opencode.json", definition.block);
117
+ return actionFor(target, resolved.content, force || adapterForcesDefinition(definition), definition.block);
107
118
  }
108
119
  async function writeDefinition(definition, target, content) {
109
120
  await mkdir(dirname(target), { recursive: true });
@@ -0,0 +1,47 @@
1
+ import { applyEdits, modify, parse } from "jsonc-parser";
2
+ //#region src/json-config-merge.ts
3
+ const JSON_FORMAT_OPTIONS = {
4
+ insertSpaces: true,
5
+ tabSize: 2
6
+ };
7
+ function parseJsonRecord(currentText) {
8
+ const errors = [];
9
+ const value = parse(currentText, errors, {
10
+ allowTrailingComma: true,
11
+ disallowComments: false
12
+ });
13
+ if (errors.length > 0 || !isRecord(value)) return {
14
+ errors,
15
+ ok: false
16
+ };
17
+ return {
18
+ ok: true,
19
+ value
20
+ };
21
+ }
22
+ function setIfMissing(content, parsed, path, value) {
23
+ if (value === void 0 || hasPath(parsed, path)) return content;
24
+ return applyJsonEdit(content, path, value);
25
+ }
26
+ function hasPath(value, path) {
27
+ let cursor = value;
28
+ for (const segment of path) {
29
+ if (!(isRecord(cursor) && segment in cursor)) return false;
30
+ cursor = cursor[segment];
31
+ }
32
+ return true;
33
+ }
34
+ function applyJsonEdit(content, path, value) {
35
+ return applyEdits(content, modify(content, path, value, { formattingOptions: JSON_FORMAT_OPTIONS }));
36
+ }
37
+ function formatJson(value) {
38
+ return `${JSON.stringify(value, null, 2)}\n`;
39
+ }
40
+ function ensureTrailingNewline(value) {
41
+ return value.endsWith("\n") ? value : `${value}\n`;
42
+ }
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ //#endregion
47
+ export { applyJsonEdit, ensureTrailingNewline, formatJson, isRecord, parseJsonRecord, setIfMissing };
@@ -66,6 +66,14 @@ function renderOpenCodeGatewayConfig(config) {
66
66
  } }
67
67
  }, null, 2)}\n`;
68
68
  }
69
+ function renderClaudeGatewayMcpServers(config) {
70
+ const gateway = configuredGateway(config);
71
+ return { [PIPELINE_GATEWAY_SERVER_ID]: {
72
+ headers: gatewayOpenCodeHeaders(gateway),
73
+ type: "http",
74
+ url: gatewayUrl(gateway)
75
+ } };
76
+ }
69
77
  function configureGatewayHosts(config, options) {
70
78
  return selectedGatewayHosts(options.host).map((host) => {
71
79
  const path = gatewayHostConfigPath(options.scope, options.cwd);
@@ -422,4 +430,4 @@ function legacyContentHit(cwd, path, pattern) {
422
430
  return pattern.test(readFileSync(fullPath, "utf8")) ? path : void 0;
423
431
  }
424
432
  //#endregion
425
- export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, reconcileGateway, renderGatewayConfig, renderOpenCodeGatewayConfig, runGatewayDoctor, startLocalGateway };
433
+ export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, reconcileGateway, renderClaudeGatewayMcpServers, renderGatewayConfig, renderOpenCodeGatewayConfig, runGatewayDoctor, startLocalGateway };
@@ -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<{
@@ -161,8 +161,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
161
161
  }, z.core.$strict>>;
162
162
  serviceAccountName: z.ZodOptional<z.ZodString>;
163
163
  mode: z.ZodEnum<{
164
- quick: "quick";
165
164
  full: "full";
165
+ quick: "quick";
166
166
  }>;
167
167
  schedulePath: z.ZodOptional<z.ZodString>;
168
168
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -207,13 +207,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
207
207
  }, z.core.$strict>>;
208
208
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
209
209
  "workflow.start": "workflow.start";
210
+ "node.finish": "node.finish";
211
+ "node.start": "node.start";
210
212
  "workflow.success": "workflow.success";
211
213
  "workflow.failure": "workflow.failure";
212
214
  "workflow.complete": "workflow.complete";
213
- "node.start": "node.start";
214
215
  "node.success": "node.success";
215
216
  "node.error": "node.error";
216
- "node.finish": "node.finish";
217
217
  "gate.failure": "gate.failure";
218
218
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
219
219
  failure: z.ZodDefault<z.ZodEnum<{
@@ -1,36 +1,17 @@
1
- import { applyEdits, modify, parse } from "jsonc-parser";
1
+ import { applyJsonEdit, ensureTrailingNewline, formatJson, parseJsonRecord, setIfMissing } from "./json-config-merge.js";
2
2
  //#region src/opencode-project-config.ts
3
- const JSON_FORMAT_OPTIONS = {
4
- insertSpaces: true,
5
- tabSize: 2
6
- };
7
3
  function mergeOpenCodeProjectConfig(currentText, projection) {
8
4
  if (currentText === void 0) return {
9
5
  content: formatJson(projection),
10
6
  ok: true
11
7
  };
12
- const parsed = parseOpenCodeProjectConfig(currentText);
8
+ const parsed = parseJsonRecord(currentText);
13
9
  if (!parsed.ok) return parsed;
14
10
  return {
15
11
  content: ensureTrailingNewline(applyOpenCodeProjection(currentText, parsed.value, projection)),
16
12
  ok: true
17
13
  };
18
14
  }
19
- function parseOpenCodeProjectConfig(currentText) {
20
- const errors = [];
21
- const value = parse(currentText, errors, {
22
- allowTrailingComma: true,
23
- disallowComments: false
24
- });
25
- if (errors.length > 0 || !isRecord(value)) return {
26
- errors,
27
- ok: false
28
- };
29
- return {
30
- ok: true,
31
- value
32
- };
33
- }
34
15
  function applyOpenCodeProjection(currentText, parsed, projection) {
35
16
  return applyProviderProjection(applyPluginProjection(applyMcpProjection(setIfMissing(setIfMissing(currentText, parsed, ["$schema"], projection.$schema), parsed, ["lsp"], projection.lsp), parsed, projection), parsed, projection), parsed, projection);
36
17
  }
@@ -49,18 +30,6 @@ function applyPluginProjection(content, parsed, projection) {
49
30
  const plugins = mergePluginEntries(parsed.plugin, projection.plugin ?? []);
50
31
  return plugins.length > 0 ? applyJsonEdit(content, ["plugin"], plugins) : content;
51
32
  }
52
- function setIfMissing(content, parsed, path, value) {
53
- if (value === void 0 || hasPath(parsed, path)) return content;
54
- return applyJsonEdit(content, path, value);
55
- }
56
- function hasPath(value, path) {
57
- let cursor = value;
58
- for (const segment of path) {
59
- if (!(isRecord(cursor) && segment in cursor)) return false;
60
- cursor = cursor[segment];
61
- }
62
- return true;
63
- }
64
33
  function mergePluginEntries(existing, projected) {
65
34
  const projectedByKey = new Map(projected.map((plugin) => [pluginKey(plugin), plugin]));
66
35
  const merged = [];
@@ -86,17 +55,5 @@ function pluginName(specifier) {
86
55
  const versionSeparator = specifier.indexOf("@", 1);
87
56
  return versionSeparator === -1 ? specifier : specifier.slice(0, versionSeparator);
88
57
  }
89
- function applyJsonEdit(content, path, value) {
90
- return applyEdits(content, modify(content, path, value, { formattingOptions: JSON_FORMAT_OPTIONS }));
91
- }
92
- function formatJson(value) {
93
- return `${JSON.stringify(value, null, 2)}\n`;
94
- }
95
- function ensureTrailingNewline(value) {
96
- return value.endsWith("\n") ? value : `${value}\n`;
97
- }
98
- function isRecord(value) {
99
- return typeof value === "object" && value !== null && !Array.isArray(value);
100
- }
101
58
  //#endregion
102
59
  export { mergeOpenCodeProjectConfig };
@@ -1,3 +1,5 @@
1
+ import { loadPipelineConfig } from "./config/load.js";
2
+ import "./config.js";
1
3
  import { executeCommand } from "./runtime/command-executor/command-executor.js";
2
4
  import "./runtime/command-executor/index.js";
3
5
  import { parseJsonObject } from "./runtime/json-validation/json-validation.js";
@@ -17,24 +19,64 @@ import { diffChangedFiles, snapshotChangedFiles } from "./runtime/changed-files/
17
19
  import { evaluateNodeGates } from "./runtime/gates/gates.js";
18
20
  import "./runtime/gates/index.js";
19
21
  import { NodeStateTracker } from "./runtime/node-state-tracker.js";
22
+ import { configUsesOpencode, leaseOpencodeRuntime } from "./runtime/opencode-runtime.js";
20
23
  import { executeParallelNode } from "./runtime/parallel-node/parallel-node.js";
21
24
  import "./runtime/parallel-node/index.js";
22
25
  import { decideNodeRetry, nodeRetryPolicy } from "./runtime/retry.js";
23
26
  import { LocalScheduler } from "./runtime/scheduler.js";
24
27
  //#region src/pipeline-runtime.ts
25
28
  function runPipelineFromConfig(options) {
26
- return runPipelineWithContext(createRuntimeContext(options));
29
+ return withOpencodeRuntime(options, (resolved) => runPipelineWithContext(createRuntimeContext(resolved)));
27
30
  }
28
31
  function runScheduledWorkflowTask(options) {
29
32
  const { dependencyOutputs, nodeId, ...runtimeOptions } = options;
30
- const context = createRuntimeContext(runtimeOptions);
31
- hydrateScheduledDependencyStates(context, nodeId);
32
- hydrateDependencyOutputs(context, dependencyOutputs);
33
- recordNodeEvent(context, nodeId, {
34
- at: now(),
35
- type: "READY"
33
+ return withOpencodeRuntime(runtimeOptions, (resolved) => {
34
+ const context = createRuntimeContext(resolved);
35
+ hydrateScheduledDependencyStates(context, nodeId);
36
+ hydrateDependencyOutputs(context, dependencyOutputs);
37
+ recordNodeEvent(context, nodeId, {
38
+ at: now(),
39
+ type: "READY"
40
+ });
41
+ return executePlannedNode(nodeId, context);
42
+ });
43
+ }
44
+ /**
45
+ * When the config uses opencode and the caller did not inject an executor,
46
+ * open one opencode server for the run, drive nodes through the SDK executor,
47
+ * and tear the server down afterward. Command-only configs and callers that
48
+ * supply their own executor (tests, embedders) are passed through untouched.
49
+ */
50
+ async function withOpencodeRuntime(options, run) {
51
+ if (options.executor) return await run(options);
52
+ const { config, worktreePath } = resolveConfigForRun(options);
53
+ return configUsesOpencode(config) ? await runWithLeasedOpencode(options, config, worktreePath, run) : await run({
54
+ ...options,
55
+ config
56
+ });
57
+ }
58
+ function resolveConfigForRun(options) {
59
+ const worktreePath = options.worktreePath ?? process.cwd();
60
+ return {
61
+ config: options.config ?? loadPipelineConfig(worktreePath),
62
+ worktreePath
63
+ };
64
+ }
65
+ async function runWithLeasedOpencode(options, config, worktreePath, run) {
66
+ const lease = await leaseOpencodeRuntime({
67
+ config,
68
+ ...options.signal ? { signal: options.signal } : {},
69
+ worktreePath
36
70
  });
37
- return executePlannedNode(nodeId, context);
71
+ try {
72
+ return await run({
73
+ ...options,
74
+ config,
75
+ executor: lease.executor
76
+ });
77
+ } finally {
78
+ await lease.release();
79
+ }
38
80
  }
39
81
  async function runPipelineWithContext(context) {
40
82
  return finishRuntime(context, await new LocalScheduler({
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- quick: "quick";
47
46
  full: "full";
47
+ quick: "quick";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- quick: "quick";
108
107
  full: "full";
108
+ quick: "quick";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;