@oisincoveney/pipeline 3.15.0 → 3.15.2

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 (102) hide show
  1. package/defaults/profiles.yaml +2 -0
  2. package/dist/argo-graph.js +5 -4
  3. package/dist/cli/program.js +5 -2
  4. package/dist/commands/bench-command.js +1 -1
  5. package/dist/commands/pipeline-command.js +1 -1
  6. package/dist/commands/runner-command-command.js +1 -1
  7. package/dist/commands/ticket/create.js +2 -1
  8. package/dist/config/schema/catalog.d.ts +11 -0
  9. package/dist/config/schema/catalog.js +68 -0
  10. package/dist/config/schema/mcp.js +90 -0
  11. package/dist/config/schema/reference-validation.js +83 -0
  12. package/dist/config/schemas.d.ts +1 -8
  13. package/dist/config/schemas.js +5 -217
  14. package/dist/config/validate.js +2 -1
  15. package/dist/install-commands/opencode.js +1 -1
  16. package/dist/install-commands.js +2 -2
  17. package/dist/loop/argo-poll.js +3 -3
  18. package/dist/loop/merge.js +2 -2
  19. package/dist/mcp/gateway-config.js +47 -0
  20. package/dist/mcp/gateway-doctor.js +192 -0
  21. package/dist/mcp/gateway-error.js +2 -3
  22. package/dist/mcp/gateway-reconcile.js +54 -0
  23. package/dist/mcp/gateway-runtime.js +8 -0
  24. package/dist/mcp/host-config.js +69 -0
  25. package/dist/mcp/host-renderers.js +48 -0
  26. package/dist/pipeline-runtime.js +32 -232
  27. package/dist/planning/compile.d.ts +2 -2
  28. package/dist/planning/compile.js +18 -74
  29. package/dist/planning/generate.js +3 -2
  30. package/dist/planning/graph.d.ts +6 -0
  31. package/dist/planning/graph.js +108 -1
  32. package/dist/run-control/command-context.js +15 -0
  33. package/dist/run-control/commands.js +15 -334
  34. package/dist/run-control/contracts.js +1 -1
  35. package/dist/run-control/detach.js +1 -1
  36. package/dist/run-control/file-errors.js +6 -0
  37. package/dist/run-control/logical-segment.js +15 -0
  38. package/dist/run-control/resume-command.js +39 -0
  39. package/dist/run-control/run-artifacts-command.js +138 -0
  40. package/dist/run-control/run-command-domain.js +31 -0
  41. package/dist/run-control/run-control-store.js +2 -1
  42. package/dist/run-control/run-query-command.js +129 -0
  43. package/dist/run-control/run-state-lock.js +4 -0
  44. package/dist/run-control/runtime-event-projection.js +98 -0
  45. package/dist/run-control/runtime-reporter.js +26 -89
  46. package/dist/run-control/stop-command.js +60 -0
  47. package/dist/run-control/store-fs-effects.js +48 -0
  48. package/dist/run-control/store-manifest.js +82 -0
  49. package/dist/run-control/store-paths.js +47 -0
  50. package/dist/run-control/store.js +22 -175
  51. package/dist/run-control/supervisor.js +7 -6
  52. package/dist/runner/opencode-excludes.js +23 -0
  53. package/dist/runner/subprocess-result.js +62 -0
  54. package/dist/runner/subprocess.js +72 -0
  55. package/dist/runner/timeouts.js +19 -0
  56. package/dist/runner-command/finalize.js +1 -1
  57. package/dist/runner-command/lifecycle.js +1 -1
  58. package/dist/runner-command/run.js +4 -4
  59. package/dist/runner.d.ts +14 -32
  60. package/dist/runner.js +102 -263
  61. package/dist/runtime/agent-node/agent-node.js +4 -4
  62. package/dist/runtime/changed-files/changed-files.js +1 -1
  63. package/dist/runtime/context/context.js +1 -1
  64. package/dist/runtime/detached-race.js +27 -0
  65. package/dist/runtime/drain-merge/drain-merge.js +6 -6
  66. package/dist/runtime/durable-store/postgres/postgres-store.js +4 -3
  67. package/dist/runtime/json-validation/json-validation.js +1 -1
  68. package/dist/runtime/local-scheduler.js +1 -1
  69. package/dist/runtime/node-state-tracker.js +133 -58
  70. package/dist/runtime/open-pull-request/open-pull-request.js +11 -11
  71. package/dist/runtime/opencode-server.js +1 -1
  72. package/dist/runtime/opencode-session-executor.js +16 -15
  73. package/dist/runtime/parallel-node/parallel-node.js +2 -2
  74. package/dist/runtime/remediation/remediation.js +246 -0
  75. package/dist/runtime/scheduler.js +1 -1
  76. package/dist/runtime/services/agent-node-runtime-service.js +1 -1
  77. package/dist/runtime/services/backlog-service.d.ts +1 -1
  78. package/dist/runtime/services/backlog-service.js +1 -1
  79. package/dist/runtime/services/command-executor-service.js +1 -1
  80. package/dist/runtime/services/config-io-service.js +2 -2
  81. package/dist/runtime/services/drain-merge-git-service.js +1 -1
  82. package/dist/runtime/services/file-system-service.js +2 -2
  83. package/dist/runtime/services/git-porcelain-service.js +1 -1
  84. package/dist/runtime/services/kubernetes-argo-service.js +2 -2
  85. package/dist/runtime/services/mcp-gateway-service.js +2 -2
  86. package/dist/runtime/services/open-pull-request-git-service.js +1 -1
  87. package/dist/runtime/services/opencode-runtime-server-service.js +1 -1
  88. package/dist/runtime/services/opencode-sdk-service.js +1 -1
  89. package/dist/runtime/services/repo-io-service.js +2 -2
  90. package/dist/runtime/services/runner-command-io-service.js +4 -4
  91. package/dist/runtime/services/runner-event-sink-http-service.js +1 -1
  92. package/dist/runtime/services/worktree-service.js +2 -2
  93. package/dist/runtime/workflow-lifecycle.js +2 -2
  94. package/dist/schedule/backlog-context.js +6 -8
  95. package/dist/serialized-write-queue.js +35 -0
  96. package/dist/tickets/ticket-graph-dto.js +3 -5
  97. package/dist/tickets/ticket-graph.d.ts +1 -2
  98. package/dist/tickets/ticket-graph.js +13 -30
  99. package/dist/tickets/ticket-selection.js +1 -1
  100. package/docs/runtime-actor-model.md +30 -0
  101. package/package.json +3 -3
  102. package/dist/mcp/gateway.js +0 -386
@@ -154,6 +154,7 @@ profiles:
154
154
  runner: opencode
155
155
  scheduling_roles: [implementation]
156
156
  description: Add focused failing tests for the requested behavior.
157
+ timeout_ms: 900000
157
158
  instructions: { inline: "Add focused failing tests for the requested behavior only. Do not change production code. Only edit files matching test paths such as **/*.test.*, **/*.spec.*, **/*_test.*, **/__tests__/**, test/**, or tests/**. NEVER silence lint, type, complexity, or dead-code findings with suppression comments (no // fallow-ignore, // biome-ignore, eslint-disable, oxlint-disable, @ts-ignore, or @ts-expect-error); fix the underlying cause — if a gate flags your test, restructure the test (e.g. move restricted imports into shared support/fixture helpers) rather than suppressing it. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
158
159
  skills: [test]
159
160
  mcp_servers: [pipeline-gateway]
@@ -168,6 +169,7 @@ profiles:
168
169
  runner: opencode
169
170
  scheduling_roles: [implementation]
170
171
  description: Implement production code until the failing tests pass.
172
+ timeout_ms: 900000
171
173
  instructions: { inline: "Implement the smallest production change that satisfies the failing tests. NEVER silence lint, type, complexity, or dead-code findings with suppression comments (no // fallow-ignore, // biome-ignore, eslint-disable, oxlint-disable, @ts-ignore, or @ts-expect-error); fix the underlying cause — reduce complexity by extracting helpers, remove genuinely dead code, and migrate off deprecated APIs rather than suppressing the warning. Return only valid JSON with top-level changes and verification. Every changes entry must include summary, why, and files. Include risks, followups, and lessons when present. Do not use Markdown fences or prose outside the JSON object." }
172
174
  skills: [trace, test, fix, library-first-development]
173
175
  mcp_servers: [pipeline-gateway]
@@ -1,5 +1,6 @@
1
1
  import { uniqueStrings } from "./strings.js";
2
2
  import { resolveExecutableDependencyIds } from "./planning/dependency-refs.js";
3
+ import { terminalDependencyItems } from "./planning/graph.js";
3
4
  import { z } from "zod";
4
5
  import { Data } from "effect";
5
6
  //#region src/argo-graph.ts
@@ -43,10 +44,11 @@ var ArgoGraphCompiler = class {
43
44
  }
44
45
  compile() {
45
46
  this.compileNodes(this.plan.topologicalOrder, []);
47
+ const terminalTasks = this.terminalTasks();
46
48
  return {
47
- terminalNodeIds: this.terminalTasks().map((task) => task.nodeId),
49
+ terminalNodeIds: terminalTasks.map((task) => task.nodeId),
48
50
  tasks: this.tasks,
49
- terminalTaskNames: this.terminalTasks().map((task) => task.taskName),
51
+ terminalTaskNames: terminalTasks.map((task) => task.taskName),
50
52
  workflowId: this.plan.workflowId
51
53
  };
52
54
  }
@@ -92,8 +94,7 @@ var ArgoGraphCompiler = class {
92
94
  return uniqueStrings(resolveExecutableDependencyIds(this.nodeById, nodeIds).map((id) => argoTaskName(id)));
93
95
  }
94
96
  terminalTasks() {
95
- const dependedOn = new Set(this.tasks.flatMap((task) => task.dependencies));
96
- return this.tasks.filter((task) => !dependedOn.has(task.taskName));
97
+ return terminalDependencyItems(this.tasks, (task) => task.taskName, (task) => task.dependencies);
97
98
  }
98
99
  };
99
100
  function argoTaskName(nodeId) {
@@ -1,12 +1,12 @@
1
+ import { flattenNodes } from "../planning/graph.js";
1
2
  import { loadPipelineConfig } from "../config/load.js";
2
3
  import "../config.js";
3
4
  import { loadMokaGlobalConfig } from "../moka-global-config.js";
4
- import { flattenNodes } from "../planning/graph.js";
5
5
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "../runner.js";
6
6
  import { compileWorkflowPlan } from "../planning/compile.js";
7
7
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "../planning/generate.js";
8
8
  import { withRunControlStoreScoped } from "../run-control/run-control-store.js";
9
- import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "../mcp/gateway.js";
9
+ import { renderGatewayConfig } from "../mcp/gateway-config.js";
10
10
  import { generateRuntimeRunId, resolveWorkflowSelection } from "../runtime/context/context.js";
11
11
  import "../runtime/context/index.js";
12
12
  import { runPipelineFromConfig } from "../pipeline-runtime.js";
@@ -19,6 +19,9 @@ import { registerTicketCommand } from "../commands/ticket-command.js";
19
19
  import { formatConfigLintWarning, lintPipelineConfig } from "../config/lint.js";
20
20
  import { parseLoopFlags, runLoopSubmit } from "../loop/loop-command.js";
21
21
  import { runLoopControllerEntrypoint } from "../loop/loop-controller-entrypoint.js";
22
+ import { runGatewayDoctor } from "../mcp/gateway-doctor.js";
23
+ import { localGatewayStatus, reconcileGateway, startLocalGateway } from "../mcp/gateway-reconcile.js";
24
+ import { configureGatewayHosts } from "../mcp/host-config.js";
22
25
  import { formatPipelineInitResult, initPipelineProject } from "../pipeline-init.js";
23
26
  import { registerRunControlCommands } from "../run-control/commands.js";
24
27
  import { startDetachedRunController } from "../run-control/detach.js";
@@ -2,7 +2,7 @@ import { buildEvalReport, renderEvalReport } from "../bench/eval-report.js";
2
2
  import { Context, Effect, Layer } from "effect";
3
3
  import { readFileSync } from "node:fs";
4
4
  //#region src/commands/bench-command.ts
5
- var BenchCommandService = class extends Context.Tag("BenchCommandService")() {};
5
+ var BenchCommandService = class extends Context.Service()("BenchCommandService") {};
6
6
  const BenchCommandServiceLive = Layer.succeed(BenchCommandService, {
7
7
  readResults: (path) => Effect.try(() => JSON.parse(readFileSync(path, "utf8"))),
8
8
  writeReport: (report) => Effect.try(() => process.stdout.write(`${report}\n`))
@@ -12,7 +12,7 @@ const BUILTIN_PIPE_COMMANDS = new Set([
12
12
  "runner-command",
13
13
  "ticket"
14
14
  ]);
15
- var EntrypointCommandService = class extends Context.Tag("EntrypointCommandService")() {};
15
+ var EntrypointCommandService = class extends Context.Service()("EntrypointCommandService") {};
16
16
  const createEntrypointCommandServiceLive = (runEntrypoint) => Layer.succeed(EntrypointCommandService, { runEntrypoint: (entrypoint, task, opts) => Effect.tryPromise({
17
17
  catch: (error) => error,
18
18
  try: () => runEntrypoint(entrypoint, task, opts)
@@ -3,7 +3,7 @@ import { runRunnerFinalize } from "../runner-command/finalize.js";
3
3
  import { runRunnerLifecycle } from "../runner-command/lifecycle.js";
4
4
  import { Context, Effect, Layer } from "effect";
5
5
  //#region src/commands/runner-command-command.ts
6
- var RunnerCommandService = class extends Context.Tag("RunnerCommandService")() {};
6
+ var RunnerCommandService = class extends Context.Service()("RunnerCommandService") {};
7
7
  const RunnerCommandServiceLive = Layer.succeed(RunnerCommandService, {
8
8
  finalize: (options) => Effect.tryPromise({
9
9
  catch: (error) => error,
@@ -1,7 +1,8 @@
1
1
  import { parseTicketPlanEffect } from "../../tickets/ticket-plan.js";
2
2
  import { loadPipelineConfig } from "../../config/load.js";
3
3
  import "../../config.js";
4
- import { createRunnerLaunchPlan, runLaunchPlan } from "../../runner.js";
4
+ import { createRunnerLaunchPlan } from "../../runner.js";
5
+ import { runLaunchPlan } from "../../runner/subprocess.js";
5
6
  import { normalizeRunnerOutput } from "../../runner-output.js";
6
7
  import { BacklogServiceLive } from "../../runtime/services/backlog-service.js";
7
8
  import { TicketCommandError, currentWorktreePath, errorMessage, writeLineEffect } from "./shared.js";
@@ -0,0 +1,11 @@
1
+ //#region src/config/schema/catalog.d.ts
2
+ declare const RUNNER_TYPES: readonly ["opencode", "command"];
3
+ declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel"];
4
+ declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "node.finish", "gate.failure"];
5
+ declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
6
+ declare const SCHEDULE_BASELINES: readonly ["execute", "quick"];
7
+ declare const SCHEDULING_ROLES: readonly ["coverage", "implementation"];
8
+ declare const MCP_GATEWAY_BACKEND_LOCALITIES: readonly ["repo-local", "repo-scoped-remote", "shared-remote"];
9
+ declare const MCP_GATEWAY_WORKSPACE_PATH_SOURCES: readonly ["PIPELINE_TARGET_PATH", "cwd"];
10
+ //#endregion
11
+ export { GATE_KINDS, HOOK_EVENTS, MCP_GATEWAY_BACKEND_LOCALITIES, MCP_GATEWAY_WORKSPACE_PATH_SOURCES, NODE_KINDS, RUNNER_TYPES, SCHEDULE_BASELINES, SCHEDULING_ROLES };
@@ -0,0 +1,68 @@
1
+ //#region src/config/schema/catalog.ts
2
+ const ID_RE = /^[a-z][a-z0-9-]*$/;
3
+ const REASONING_EFFORTS = [
4
+ "none",
5
+ "low",
6
+ "medium",
7
+ "high",
8
+ "xhigh"
9
+ ];
10
+ const RUNNER_TYPES = ["opencode", "command"];
11
+ const HOOK_EVENTS = [
12
+ "workflow.start",
13
+ "workflow.success",
14
+ "workflow.failure",
15
+ "workflow.complete",
16
+ "node.start",
17
+ "node.success",
18
+ "node.error",
19
+ "node.finish",
20
+ "gate.failure"
21
+ ];
22
+ const TOOL_NAMES = [
23
+ "read",
24
+ "list",
25
+ "grep",
26
+ "glob",
27
+ "bash",
28
+ "edit",
29
+ "write",
30
+ "task"
31
+ ];
32
+ const FILESYSTEM_MODES = ["read-only", "workspace-write"];
33
+ const NETWORK_MODES = ["inherit", "disabled"];
34
+ const OUTPUT_FORMATS = [
35
+ "text",
36
+ "json",
37
+ "jsonl",
38
+ "json_schema"
39
+ ];
40
+ const BUILTIN_GATES = [
41
+ "duplication",
42
+ "fallow",
43
+ "lint",
44
+ "semgrep",
45
+ "test",
46
+ "typecheck"
47
+ ];
48
+ const RETRY_REASONS = [
49
+ "exit_nonzero",
50
+ "gate_failure",
51
+ "timeout"
52
+ ];
53
+ const SCHEDULE_BASELINES = ["execute", "quick"];
54
+ const SCHEDULE_STRATEGIES = ["planner"];
55
+ const SCHEDULING_ROLES = ["coverage", "implementation"];
56
+ const MCP_GATEWAY_BACKEND_LOCALITIES = [
57
+ "repo-local",
58
+ "repo-scoped-remote",
59
+ "shared-remote"
60
+ ];
61
+ const MCP_GATEWAY_WORKSPACE_PATH_SOURCES = ["PIPELINE_TARGET_PATH", "cwd"];
62
+ const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
63
+ const DEFAULT_RUNNER_COMMAND_GIT_COMMITTER = {
64
+ email: "git@oisin.ee",
65
+ name: "oisin-bot"
66
+ };
67
+ //#endregion
68
+ export { BUILTIN_GATES, DEFAULT_RUNNER_COMMAND_GIT_COMMITTER, FILESYSTEM_MODES, HOOK_EVENTS, ID_RE, MCP_GATEWAY_BACKEND_LOCALITIES, MCP_GATEWAY_WORKSPACE_PATH_SOURCES, NETWORK_MODES, OUTPUT_FORMATS, PIPELINE_GATEWAY_SERVER_ID, REASONING_EFFORTS, RETRY_REASONS, RUNNER_TYPES, SCHEDULE_BASELINES, SCHEDULE_STRATEGIES, SCHEDULING_ROLES, TOOL_NAMES };
@@ -0,0 +1,90 @@
1
+ import { MCP_GATEWAY_BACKEND_LOCALITIES, MCP_GATEWAY_WORKSPACE_PATH_SOURCES } from "./catalog.js";
2
+ import { z } from "zod";
3
+ //#region src/config/schema/mcp.ts
4
+ const REPO_LOCAL_WORKSPACE_PATH_SOURCE_MESSAGE = [
5
+ "repo-local gateway backend must declare workspace_path_source",
6
+ "as",
7
+ "PIPELINE_TARGET_PATH or cwd"
8
+ ].join(" ");
9
+ const mcpServerBaseSchema = z.object({
10
+ args: z.array(z.string()).optional(),
11
+ bearer_token_env_var: z.string().min(1).optional(),
12
+ command: z.string().min(1).optional(),
13
+ env: z.record(z.string(), z.string()).optional(),
14
+ headers: z.record(z.string(), z.string()).optional(),
15
+ url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP server url must use http or https" }).optional()
16
+ }).strict();
17
+ const hasCommand = (server) => Boolean(server.command);
18
+ const hasUrl = (server) => Boolean(server.url);
19
+ const hasAuthorizationHeader = (server) => Object.keys(server.headers ?? {}).some((key) => key.toLowerCase() === "authorization");
20
+ const mcpServerRefinements = [
21
+ {
22
+ message: "MCP server must declare exactly one of command or url",
23
+ path: (server) => hasCommand(server) ? ["url"] : ["command"],
24
+ violates: (server) => hasCommand(server) === hasUrl(server)
25
+ },
26
+ {
27
+ message: "args are only valid for command MCP servers",
28
+ path: () => ["args"],
29
+ violates: (server) => hasUrl(server) && Boolean(server.args)
30
+ },
31
+ {
32
+ message: "env is only valid for command MCP servers",
33
+ path: () => ["env"],
34
+ violates: (server) => hasUrl(server) && Boolean(server.env)
35
+ },
36
+ {
37
+ message: "headers are only valid for url MCP servers",
38
+ path: () => ["headers"],
39
+ violates: (server) => hasCommand(server) && Boolean(server.headers)
40
+ },
41
+ {
42
+ message: "bearer_token_env_var is only valid for url MCP servers",
43
+ path: () => ["bearer_token_env_var"],
44
+ violates: (server) => hasCommand(server) && Boolean(server.bearer_token_env_var)
45
+ },
46
+ {
47
+ message: "headers.Authorization cannot be combined with bearer_token_env_var",
48
+ path: () => ["bearer_token_env_var"],
49
+ violates: (server) => hasUrl(server) && Boolean(server.bearer_token_env_var) && hasAuthorizationHeader(server)
50
+ }
51
+ ];
52
+ const mcpServerSchema = mcpServerBaseSchema.superRefine((server, ctx) => {
53
+ for (const refinement of mcpServerRefinements) if (refinement.violates(server)) ctx.addIssue({
54
+ code: "custom",
55
+ message: refinement.message,
56
+ path: refinement.path(server)
57
+ });
58
+ });
59
+ const mcpGatewayBackendSchema = z.object({
60
+ locality: z.enum(MCP_GATEWAY_BACKEND_LOCALITIES),
61
+ required: z.boolean().default(true),
62
+ tool_prefixes: z.array(z.string().min(1)).min(1),
63
+ workspace_path_source: z.enum(MCP_GATEWAY_WORKSPACE_PATH_SOURCES).optional()
64
+ }).strict().superRefine((backend, ctx) => {
65
+ if (backend.locality === "repo-local") {
66
+ if (!backend.workspace_path_source) ctx.addIssue({
67
+ code: "custom",
68
+ message: REPO_LOCAL_WORKSPACE_PATH_SOURCE_MESSAGE,
69
+ path: ["workspace_path_source"]
70
+ });
71
+ return;
72
+ }
73
+ if (backend.workspace_path_source) ctx.addIssue({
74
+ code: "custom",
75
+ message: "workspace_path_source is only valid for repo-local gateway backends",
76
+ path: ["workspace_path_source"]
77
+ });
78
+ });
79
+ const mcpGatewaySchema = z.object({
80
+ backends: z.record(z.string(), mcpGatewayBackendSchema).default({}),
81
+ default_profile: z.string().min(1).optional(),
82
+ host_scope: z.enum(["project", "global"]).default("project"),
83
+ mode: z.enum(["hosted", "local"]),
84
+ provider: z.literal("toolhive"),
85
+ authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
86
+ url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP gateway url must use http or https" }).optional(),
87
+ url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
88
+ }).strict();
89
+ //#endregion
90
+ export { mcpGatewaySchema, mcpServerSchema };
@@ -0,0 +1,83 @@
1
+ //#region src/config/schema/reference-validation.ts
2
+ function validateConfigReferences(config, ctx) {
3
+ addConfigSchemaIssues(ctx, configReferenceIssues(config));
4
+ }
5
+ function configReferenceIssues(config) {
6
+ return [
7
+ ...missingRegistryReferenceIssue({
8
+ message: (_field, value) => `default workflow '${value}' is not declared`,
9
+ path: ["default_workflow"],
10
+ registry: config.workflows,
11
+ value: config.default_workflow
12
+ }),
13
+ ...registryReferenceIssues("entrypoints", config.entrypoints, [{
14
+ field: "workflow",
15
+ message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing workflow '${value}'`,
16
+ read: (entrypoint) => entrypoint.workflow,
17
+ registry: config.workflows
18
+ }, {
19
+ field: "schedule",
20
+ message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing schedule '${value}'`,
21
+ read: (entrypoint) => entrypoint.schedule,
22
+ registry: config.schedules
23
+ }]),
24
+ ...registryReferenceIssues("schedules", config.schedules, [{
25
+ field: "planner_profile",
26
+ message: (scheduleId, value) => `schedule '${scheduleId}' references missing planner profile '${value}'`,
27
+ read: (schedule) => schedule.planner_profile,
28
+ registry: config.profiles
29
+ }, {
30
+ field: "node_catalog",
31
+ message: (scheduleId, value) => `schedule '${scheduleId}' references missing scheduler node catalog '${value}'`,
32
+ read: (schedule) => schedule.node_catalog,
33
+ registry: config.scheduler.node_catalogs
34
+ }]),
35
+ ...registryReferenceIssues("scheduler.commands", config.scheduler.commands, [{
36
+ field: "catalog",
37
+ message: (commandId, value) => `scheduler command '${commandId}' references missing node catalog '${value}'`,
38
+ read: (command) => command.catalog,
39
+ registry: config.scheduler.node_catalogs
40
+ }, {
41
+ field: "schedule",
42
+ message: (commandId, value) => `scheduler command '${commandId}' references missing schedule '${value}'`,
43
+ read: (command) => command.schedule,
44
+ registry: config.schedules
45
+ }]),
46
+ ...Object.entries(config.scheduler.node_catalogs).flatMap(([catalogId, catalog]) => registryReferenceIssues(`scheduler.node_catalogs.${catalogId}.nodes`, catalog.nodes, [{
47
+ field: "profile",
48
+ message: (nodeId, value) => `scheduler node '${catalogId}.${nodeId}' references missing profile '${value}'`,
49
+ read: (node) => node.profile,
50
+ registry: config.profiles
51
+ }]))
52
+ ];
53
+ }
54
+ function registryReferenceIssues(registryPath, records, rules) {
55
+ return Object.entries(records).flatMap(([recordId, record]) => rules.flatMap((rule) => missingRegistryReferenceIssue({
56
+ message: (_field, value) => rule.message(recordId, value),
57
+ path: [
58
+ registryPath,
59
+ recordId,
60
+ rule.field
61
+ ],
62
+ registry: rule.registry,
63
+ value: rule.read(record)
64
+ })));
65
+ }
66
+ function missingRegistryReferenceIssue({ message, path, registry, value }) {
67
+ return value && !Object.hasOwn(registry, value) ? [{
68
+ message: message(String(path.at(-1)), value),
69
+ path
70
+ }] : [];
71
+ }
72
+ function addConfigSchemaIssues(ctx, issues) {
73
+ for (const issue of issues) addConfigSchemaIssue(ctx, issue.path, issue.message);
74
+ }
75
+ function addConfigSchemaIssue(ctx, path, message) {
76
+ ctx.addIssue({
77
+ code: "custom",
78
+ path,
79
+ message
80
+ });
81
+ }
82
+ //#endregion
83
+ export { validateConfigReferences };
@@ -1,14 +1,7 @@
1
+ import { GATE_KINDS, HOOK_EVENTS, MCP_GATEWAY_BACKEND_LOCALITIES, MCP_GATEWAY_WORKSPACE_PATH_SOURCES, NODE_KINDS, RUNNER_TYPES, SCHEDULE_BASELINES, SCHEDULING_ROLES } from "./schema/catalog.js";
1
2
  import { z } from "zod";
2
3
 
3
4
  //#region src/config/schemas.d.ts
4
- declare const RUNNER_TYPES: readonly ["opencode", "command"];
5
- declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel"];
6
- declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "node.finish", "gate.failure"];
7
- declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
8
- declare const SCHEDULE_BASELINES: readonly ["execute", "quick"];
9
- declare const SCHEDULING_ROLES: readonly ["coverage", "implementation"];
10
- declare const MCP_GATEWAY_BACKEND_LOCALITIES: readonly ["repo-local", "repo-scoped-remote", "shared-remote"];
11
- declare const MCP_GATEWAY_WORKSPACE_PATH_SOURCES: readonly ["PIPELINE_TARGET_PATH", "cwd"];
12
5
  type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
13
6
  interface PipelineConfigIssue {
14
7
  message: string;
@@ -1,71 +1,10 @@
1
+ import { BUILTIN_GATES, DEFAULT_RUNNER_COMMAND_GIT_COMMITTER, FILESYSTEM_MODES, NETWORK_MODES, OUTPUT_FORMATS, REASONING_EFFORTS, RETRY_REASONS, RUNNER_TYPES, SCHEDULE_BASELINES, SCHEDULE_STRATEGIES, SCHEDULING_ROLES, TOOL_NAMES } from "./schema/catalog.js";
2
+ import { mcpGatewaySchema, mcpServerSchema } from "./schema/mcp.js";
3
+ import { validateConfigReferences } from "./schema/reference-validation.js";
1
4
  import { z } from "zod";
2
5
  import { Data } from "effect";
3
6
  //#region src/config/schemas.ts
4
- const ID_RE = /^[a-z][a-z0-9-]*$/;
5
- const reasoningEffortSchema = z.enum([
6
- "none",
7
- "low",
8
- "medium",
9
- "high",
10
- "xhigh"
11
- ]);
12
- const RUNNER_TYPES = ["opencode", "command"];
13
- const HOOK_EVENTS = [
14
- "workflow.start",
15
- "workflow.success",
16
- "workflow.failure",
17
- "workflow.complete",
18
- "node.start",
19
- "node.success",
20
- "node.error",
21
- "node.finish",
22
- "gate.failure"
23
- ];
24
- const TOOL_NAMES = [
25
- "read",
26
- "list",
27
- "grep",
28
- "glob",
29
- "bash",
30
- "edit",
31
- "write",
32
- "task"
33
- ];
34
- const FILESYSTEM_MODES = ["read-only", "workspace-write"];
35
- const NETWORK_MODES = ["inherit", "disabled"];
36
- const OUTPUT_FORMATS = [
37
- "text",
38
- "json",
39
- "jsonl",
40
- "json_schema"
41
- ];
42
- const BUILTIN_GATES = [
43
- "duplication",
44
- "fallow",
45
- "lint",
46
- "semgrep",
47
- "test",
48
- "typecheck"
49
- ];
50
- const RETRY_REASONS = [
51
- "exit_nonzero",
52
- "gate_failure",
53
- "timeout"
54
- ];
55
- const SCHEDULE_BASELINES = ["execute", "quick"];
56
- const SCHEDULE_STRATEGIES = ["planner"];
57
- const SCHEDULING_ROLES = ["coverage", "implementation"];
58
- const MCP_GATEWAY_BACKEND_LOCALITIES = [
59
- "repo-local",
60
- "repo-scoped-remote",
61
- "shared-remote"
62
- ];
63
- const MCP_GATEWAY_WORKSPACE_PATH_SOURCES = ["PIPELINE_TARGET_PATH", "cwd"];
64
- const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
65
- const DEFAULT_RUNNER_COMMAND_GIT_COMMITTER = {
66
- email: "git@oisin.ee",
67
- name: "oisin-bot"
68
- };
7
+ const reasoningEffortSchema = z.enum(REASONING_EFFORTS);
69
8
  var PipelineConfigError = class extends Data.TaggedError("PipelineConfigError") {
70
9
  constructor(code, message, issues = []) {
71
10
  super({
@@ -99,77 +38,6 @@ const pathRefSchema = z.object({
99
38
  path: z.string().min(1),
100
39
  source_root: z.enum(["package", "project"]).default("project")
101
40
  }).strict();
102
- const mcpServerSchema = z.object({
103
- args: z.array(z.string()).optional(),
104
- bearer_token_env_var: z.string().min(1).optional(),
105
- command: z.string().min(1).optional(),
106
- env: z.record(z.string(), z.string()).optional(),
107
- headers: z.record(z.string(), z.string()).optional(),
108
- url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP server url must use http or https" }).optional()
109
- }).strict().superRefine((server, ctx) => {
110
- const hasCommand = Boolean(server.command);
111
- const hasUrl = Boolean(server.url);
112
- if (hasCommand === hasUrl) ctx.addIssue({
113
- code: "custom",
114
- message: "MCP server must declare exactly one of command or url",
115
- path: hasCommand ? ["url"] : ["command"]
116
- });
117
- if (hasUrl && server.args) ctx.addIssue({
118
- code: "custom",
119
- message: "args are only valid for command MCP servers",
120
- path: ["args"]
121
- });
122
- if (hasUrl && server.env) ctx.addIssue({
123
- code: "custom",
124
- message: "env is only valid for command MCP servers",
125
- path: ["env"]
126
- });
127
- if (hasCommand && server.headers) ctx.addIssue({
128
- code: "custom",
129
- message: "headers are only valid for url MCP servers",
130
- path: ["headers"]
131
- });
132
- if (hasCommand && server.bearer_token_env_var) ctx.addIssue({
133
- code: "custom",
134
- message: "bearer_token_env_var is only valid for url MCP servers",
135
- path: ["bearer_token_env_var"]
136
- });
137
- if (hasUrl && server.bearer_token_env_var && Object.keys(server.headers ?? {}).some((key) => key.toLowerCase() === "authorization")) ctx.addIssue({
138
- code: "custom",
139
- message: "headers.Authorization cannot be combined with bearer_token_env_var",
140
- path: ["bearer_token_env_var"]
141
- });
142
- });
143
- const mcpGatewayBackendSchema = z.object({
144
- locality: z.enum(MCP_GATEWAY_BACKEND_LOCALITIES),
145
- required: z.boolean().default(true),
146
- tool_prefixes: z.array(z.string().min(1)).min(1),
147
- workspace_path_source: z.enum(MCP_GATEWAY_WORKSPACE_PATH_SOURCES).optional()
148
- }).strict().superRefine((backend, ctx) => {
149
- if (backend.locality === "repo-local") {
150
- if (!backend.workspace_path_source) ctx.addIssue({
151
- code: "custom",
152
- message: "repo-local gateway backend must declare workspace_path_source as PIPELINE_TARGET_PATH or cwd",
153
- path: ["workspace_path_source"]
154
- });
155
- return;
156
- }
157
- if (backend.workspace_path_source) ctx.addIssue({
158
- code: "custom",
159
- message: "workspace_path_source is only valid for repo-local gateway backends",
160
- path: ["workspace_path_source"]
161
- });
162
- });
163
- const mcpGatewaySchema = z.object({
164
- backends: strictRecord(mcpGatewayBackendSchema).default({}),
165
- default_profile: z.string().min(1).optional(),
166
- host_scope: z.enum(["project", "global"]).default("project"),
167
- mode: z.enum(["hosted", "local"]),
168
- provider: z.literal("toolhive"),
169
- authorization_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_AUTHORIZATION"),
170
- url: z.string().url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol), { message: "MCP gateway url must use http or https" }).optional(),
171
- url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
172
- }).strict();
173
41
  const instructionsSchema = z.object({
174
42
  inline: z.string().min(1).optional(),
175
43
  path: z.string().min(1).optional()
@@ -553,86 +421,6 @@ const configSchema = z.object({
553
421
  version: z.literal(1),
554
422
  workflows: strictRecord(workflowSchema).default({})
555
423
  }).strict().superRefine(validateConfigReferences);
556
- function validateConfigReferences(config, ctx) {
557
- addConfigSchemaIssues(ctx, configReferenceIssues(config));
558
- }
559
- function configReferenceIssues(config) {
560
- return [
561
- ...missingRegistryReferenceIssue({
562
- message: (_field, value) => `default workflow '${value}' is not declared`,
563
- path: ["default_workflow"],
564
- registry: config.workflows,
565
- value: config.default_workflow
566
- }),
567
- ...registryReferenceIssues("entrypoints", config.entrypoints, [{
568
- field: "workflow",
569
- message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing workflow '${value}'`,
570
- read: (entrypoint) => "workflow" in entrypoint ? entrypoint.workflow : void 0,
571
- registry: config.workflows
572
- }, {
573
- field: "schedule",
574
- message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing schedule '${value}'`,
575
- read: (entrypoint) => "schedule" in entrypoint ? entrypoint.schedule : void 0,
576
- registry: config.schedules
577
- }]),
578
- ...registryReferenceIssues("schedules", config.schedules, [{
579
- field: "planner_profile",
580
- message: (scheduleId, value) => `schedule '${scheduleId}' references missing planner profile '${value}'`,
581
- read: (schedule) => schedule.planner_profile,
582
- registry: config.profiles
583
- }, {
584
- field: "node_catalog",
585
- message: (scheduleId, value) => `schedule '${scheduleId}' references missing scheduler node catalog '${value}'`,
586
- read: (schedule) => schedule.node_catalog,
587
- registry: config.scheduler.node_catalogs
588
- }]),
589
- ...registryReferenceIssues("scheduler.commands", config.scheduler.commands, [{
590
- field: "catalog",
591
- message: (commandId, value) => `scheduler command '${commandId}' references missing node catalog '${value}'`,
592
- read: (command) => command.catalog,
593
- registry: config.scheduler.node_catalogs
594
- }, {
595
- field: "schedule",
596
- message: (commandId, value) => `scheduler command '${commandId}' references missing schedule '${value}'`,
597
- read: (command) => command.schedule,
598
- registry: config.schedules
599
- }]),
600
- ...Object.entries(config.scheduler.node_catalogs).flatMap(([catalogId, catalog]) => registryReferenceIssues(`scheduler.node_catalogs.${catalogId}.nodes`, catalog.nodes, [{
601
- field: "profile",
602
- message: (nodeId, value) => `scheduler node '${catalogId}.${nodeId}' references missing profile '${value}'`,
603
- read: (node) => node.profile,
604
- registry: config.profiles
605
- }]))
606
- ];
607
- }
608
- function registryReferenceIssues(registryPath, records, rules) {
609
- return Object.entries(records).flatMap(([recordId, record]) => rules.flatMap((rule) => missingRegistryReferenceIssue({
610
- message: (_field, value) => rule.message(recordId, value),
611
- path: [
612
- registryPath,
613
- recordId,
614
- rule.field
615
- ],
616
- registry: rule.registry,
617
- value: rule.read(record)
618
- })));
619
- }
620
- function missingRegistryReferenceIssue({ message, path, registry, value }) {
621
- return value && !Object.hasOwn(registry, value) ? [{
622
- message: message(String(path.at(-1)), value),
623
- path
624
- }] : [];
625
- }
626
- function addConfigSchemaIssues(ctx, issues) {
627
- for (const issue of issues) addConfigSchemaIssue(ctx, issue.path, issue.message);
628
- }
629
- function addConfigSchemaIssue(ctx, path, message) {
630
- ctx.addIssue({
631
- code: "custom",
632
- path,
633
- message
634
- });
635
- }
636
424
  function validationError(issues) {
637
425
  return new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", ["Invalid pipeline config:", ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
638
426
  }
@@ -643,4 +431,4 @@ function configIssuesFromZodError(error) {
643
431
  }));
644
432
  }
645
433
  //#endregion
646
- export { HOOK_EVENTS, ID_RE, PIPELINE_GATEWAY_SERVER_ID, PipelineConfigError, configIssuesFromZodError, configSchema, pipelineFileSchema, profilesFileSchema, runnersFileSchema, validationError, workflowSchema };
434
+ export { PipelineConfigError, configIssuesFromZodError, configSchema, pipelineFileSchema, profilesFileSchema, runnersFileSchema, validationError, workflowSchema };
@@ -1,4 +1,5 @@
1
- import { HOOK_EVENTS, ID_RE, PIPELINE_GATEWAY_SERVER_ID, configIssuesFromZodError, configSchema, validationError } from "./schemas.js";
1
+ import { HOOK_EVENTS, ID_RE, PIPELINE_GATEWAY_SERVER_ID } from "./schema/catalog.js";
2
+ import { configIssuesFromZodError, configSchema, validationError } from "./schemas.js";
2
3
  import { PACKAGE_ASSET_ROOT } from "../package-assets.js";
3
4
  import { resolveFileReference } from "../path-refs.js";
4
5
  import { standardOutputSchemaNameFromPath } from "../standard-output-schemas.js";
@@ -4,9 +4,9 @@ import "../config.js";
4
4
  import { protectedPermissionOverlay } from "../runtime/protected-paths/protected-paths.js";
5
5
  import { RepoIoService, runRepoIoSync } from "../runtime/services/repo-io-service.js";
6
6
  import { compileWorkflowPlan } from "../planning/compile.js";
7
- import { renderOpenCodeGatewayConfig } from "../mcp/gateway.js";
8
7
  import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
9
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";
9
+ import { renderOpenCodeGatewayConfig } from "../mcp/host-renderers.js";
10
10
  import { mergeOpenCodeProjectConfig } from "../opencode-project-config.js";
11
11
  import { Effect } from "effect";
12
12
  import { basename } from "node:path";