@oisincoveney/pipeline 1.13.0 → 1.15.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.
package/README.md CHANGED
@@ -188,7 +188,10 @@ default_workflow: default
188
188
 
189
189
  orchestrator:
190
190
  profile: orchestrator
191
- hooks: []
191
+
192
+ hooks:
193
+ functions: {}
194
+ on: {}
192
195
 
193
196
  workflows:
194
197
  default:
@@ -353,7 +356,7 @@ runners:
353
356
  - JSON Schema gates validate structure only. Use `verdict` and `acceptance`
354
357
  gates to enforce semantic pass/fail and per-criterion coverage.
355
358
  - Command hooks support host policy controls, sanitized environments, timeouts,
356
- output limits, and JSON payloads on stdin.
359
+ output limits, and JSON file input/result payloads.
357
360
 
358
361
  ## App-Facing API
359
362
 
@@ -377,6 +380,16 @@ import {
377
380
  } from "@oisincoveney/pipeline/runtime";
378
381
  ```
379
382
 
383
+ Hook modules can import the typed helper and result contract:
384
+
385
+ ```ts
386
+ import {
387
+ defineHook,
388
+ type HookContext,
389
+ type HookResult,
390
+ } from "@oisincoveney/pipeline/hooks";
391
+ ```
392
+
380
393
  Runner Job producers can import the shared payload contract:
381
394
 
382
395
  ```ts
@@ -5,71 +5,5 @@
5
5
  "source": "oisincoveney/skills",
6
6
  "args": ["--agent", "codex", "--skill", "*", "--yes", "--copy"]
7
7
  }
8
- ],
9
- "mcps": [
10
- {
11
- "name": "oisin-pipeline-backlog",
12
- "transport": "stdio",
13
- "command": "backlog",
14
- "args": ["mcp", "start"]
15
- },
16
- {
17
- "name": "oisin-pipeline-context7",
18
- "transport": "remote",
19
- "catalog": "context7"
20
- },
21
- {
22
- "name": "oisin-pipeline-github-readonly",
23
- "transport": "stdio",
24
- "command": "docker",
25
- "args": [
26
- "run",
27
- "-i",
28
- "--rm",
29
- "-e",
30
- "GITHUB_PERSONAL_ACCESS_TOKEN",
31
- "ghcr.io/github/github-mcp-server",
32
- "stdio",
33
- "--read-only"
34
- ],
35
- "env": {
36
- "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
37
- }
38
- },
39
- {
40
- "name": "oisin-pipeline-playwright",
41
- "transport": "stdio",
42
- "catalog": "playwright"
43
- },
44
- {
45
- "name": "oisin-pipeline-qdrant",
46
- "transport": "remote",
47
- "url": "https://memory-mcp.momokaya.ee/mcp/",
48
- "optionalRegistration": true,
49
- "headers": {
50
- "Authorization": {
51
- "sources": [{ "env": "MEMORY_MCP_BASIC_AUTH", "prefix": "Basic " }]
52
- }
53
- }
54
- },
55
- {
56
- "name": "oisin-pipeline-semgrep",
57
- "transport": "stdio",
58
- "command": "docker",
59
- "args": ["run", "-i", "--rm", "ghcr.io/semgrep/mcp", "-t", "stdio"]
60
- },
61
- {
62
- "name": "oisin-pipeline-serena",
63
- "transport": "stdio",
64
- "command": "uvx",
65
- "args": [
66
- "--from",
67
- "git+https://github.com/oraios/serena",
68
- "serena",
69
- "start-mcp-server",
70
- "--project-from-cwd",
71
- "--context=codex"
72
- ]
73
- }
74
8
  ]
75
9
  }
package/dist/config.d.ts CHANGED
@@ -6,7 +6,7 @@ declare const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
6
6
  declare const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
7
7
  declare const RUNNER_TYPES: readonly ["codex", "opencode", "command"];
8
8
  declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel", "workflow"];
9
- declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "gate.failure"];
9
+ declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "node.finish", "gate.failure"];
10
10
  declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
11
11
  declare const SCHEDULE_BASELINES: readonly ["epic", "pipe"];
12
12
  type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
@@ -88,7 +88,6 @@ declare const workflowNodeBaseSchema: z.ZodObject<{
88
88
  field: z.ZodOptional<z.ZodString>;
89
89
  kind: z.ZodLiteral<"verdict">;
90
90
  }, z.core.$strict>], "kind">>>;
91
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
91
  id: z.ZodString;
93
92
  needs: z.ZodOptional<z.ZodArray<z.ZodString>>;
94
93
  retries: z.ZodOptional<z.ZodObject<{
@@ -146,7 +145,6 @@ declare const workflowSchema: z.ZodObject<{
146
145
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
147
146
  timeout_ms: z.ZodOptional<z.ZodNumber>;
148
147
  }, z.core.$strict>>;
149
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
150
148
  nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
151
149
  }, z.core.$strict>;
152
150
  declare const configSchema: z.ZodObject<{
@@ -164,36 +162,85 @@ declare const configSchema: z.ZodObject<{
164
162
  }, z.core.$loose>>;
165
163
  schedule: z.ZodString;
166
164
  }, z.core.$strict>]>>>;
167
- hooks: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
168
- builtin: z.ZodOptional<z.ZodString>;
169
- command: z.ZodOptional<z.ZodArray<z.ZodString>>;
170
- enabled: z.ZodOptional<z.ZodBoolean>;
171
- env: z.ZodOptional<z.ZodObject<{
172
- passthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
173
- set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
165
+ hooks: z.ZodDefault<z.ZodObject<{
166
+ functions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
167
+ kind: z.ZodLiteral<"module">;
168
+ module: z.ZodString;
169
+ permissions: z.ZodOptional<z.ZodObject<{
170
+ filesystem: z.ZodOptional<z.ZodEnum<{
171
+ "read-only": "read-only";
172
+ "workspace-write": "workspace-write";
173
+ }>>;
174
+ network: z.ZodOptional<z.ZodEnum<{
175
+ inherit: "inherit";
176
+ disabled: "disabled";
177
+ }>>;
178
+ }, z.core.$strict>>;
179
+ returns: z.ZodOptional<z.ZodObject<{
180
+ schema: z.ZodOptional<z.ZodString>;
181
+ }, z.core.$strict>>;
182
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
183
+ }, z.core.$strict>, z.ZodObject<{
184
+ command: z.ZodArray<z.ZodString>;
185
+ env: z.ZodOptional<z.ZodObject<{
186
+ passthrough: z.ZodOptional<z.ZodArray<z.ZodString>>;
187
+ set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
188
+ }, z.core.$strict>>;
189
+ kind: z.ZodLiteral<"command">;
190
+ output_limit_bytes: z.ZodOptional<z.ZodNumber>;
191
+ protocol: z.ZodDefault<z.ZodObject<{
192
+ input: z.ZodLiteral<"file">;
193
+ result: z.ZodLiteral<"file">;
194
+ }, z.core.$strict>>;
195
+ returns: z.ZodOptional<z.ZodObject<{
196
+ schema: z.ZodOptional<z.ZodString>;
197
+ }, z.core.$strict>>;
198
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
199
+ trusted: z.ZodOptional<z.ZodBoolean>;
200
+ }, z.core.$strict>], "kind">>>;
201
+ on: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
202
+ failure: z.ZodDefault<z.ZodEnum<{
203
+ fail: "fail";
204
+ ignore: "ignore";
205
+ }>>;
206
+ function: z.ZodString;
207
+ id: z.ZodString;
208
+ result: z.ZodOptional<z.ZodObject<{
209
+ pass_to: z.ZodOptional<z.ZodEnum<{
210
+ downstream: "downstream";
211
+ }>>;
212
+ publish: z.ZodOptional<z.ZodBoolean>;
213
+ save_as: z.ZodOptional<z.ZodString>;
214
+ }, z.core.$strict>>;
215
+ where: z.ZodOptional<z.ZodObject<{
216
+ gate: z.ZodOptional<z.ZodString>;
217
+ node: z.ZodOptional<z.ZodString>;
218
+ workflow: z.ZodOptional<z.ZodString>;
219
+ }, z.core.$strict>>;
220
+ with: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
221
+ }, z.core.$strict>>>>;
222
+ policy: z.ZodOptional<z.ZodObject<{
223
+ commands: z.ZodOptional<z.ZodEnum<{
224
+ allow: "allow";
225
+ deny: "deny";
226
+ "trusted-only": "trusted-only";
227
+ }>>;
228
+ modules: z.ZodOptional<z.ZodEnum<{
229
+ allow: "allow";
230
+ deny: "deny";
231
+ }>>;
174
232
  }, z.core.$strict>>;
175
- event: z.ZodEnum<{
176
- "workflow.start": "workflow.start";
177
- "workflow.success": "workflow.success";
178
- "workflow.failure": "workflow.failure";
179
- "workflow.complete": "workflow.complete";
180
- "node.start": "node.start";
181
- "node.success": "node.success";
182
- "node.error": "node.error";
183
- "gate.failure": "gate.failure";
184
- }>;
185
- kind: z.ZodEnum<{
186
- builtin: "builtin";
187
- command: "command";
233
+ }, z.core.$strict>>;
234
+ mcp_gateway: z.ZodOptional<z.ZodObject<{
235
+ default_profile: z.ZodOptional<z.ZodString>;
236
+ mode: z.ZodEnum<{
237
+ hosted: "hosted";
238
+ local: "local";
188
239
  }>;
189
- output_limit_bytes: z.ZodOptional<z.ZodNumber>;
190
- payload: z.ZodOptional<z.ZodEnum<{
191
- stdin: "stdin";
192
- }>>;
193
- required: z.ZodOptional<z.ZodBoolean>;
194
- timeout_ms: z.ZodOptional<z.ZodNumber>;
195
- trusted: z.ZodOptional<z.ZodBoolean>;
196
- }, z.core.$strict>>>;
240
+ provider: z.ZodLiteral<"toolhive">;
241
+ token_env: z.ZodDefault<z.ZodString>;
242
+ url_env: z.ZodDefault<z.ZodString>;
243
+ }, z.core.$strict>>;
197
244
  mcp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
198
245
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
199
246
  bearer_token_env_var: z.ZodOptional<z.ZodString>;
@@ -203,7 +250,6 @@ declare const configSchema: z.ZodObject<{
203
250
  url: z.ZodOptional<z.ZodString>;
204
251
  }, z.core.$strict>>>;
205
252
  orchestrator: z.ZodObject<{
206
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
207
253
  profile: z.ZodString;
208
254
  }, z.core.$strict>;
209
255
  profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -231,10 +277,10 @@ declare const configSchema: z.ZodObject<{
231
277
  }, z.core.$strict>>;
232
278
  output: z.ZodOptional<z.ZodObject<{
233
279
  format: z.ZodEnum<{
280
+ json_schema: "json_schema";
234
281
  text: "text";
235
282
  json: "json";
236
283
  jsonl: "jsonl";
237
- json_schema: "json_schema";
238
284
  }>;
239
285
  repair: z.ZodOptional<z.ZodObject<{
240
286
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -274,10 +320,10 @@ declare const configSchema: z.ZodObject<{
274
320
  disabled: "disabled";
275
321
  }>>>;
276
322
  output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
323
+ json_schema: "json_schema";
277
324
  text: "text";
278
325
  json: "json";
279
326
  jsonl: "jsonl";
280
- json_schema: "json_schema";
281
327
  }>>>;
282
328
  rules: z.ZodOptional<z.ZodBoolean>;
283
329
  skills: z.ZodOptional<z.ZodBoolean>;
@@ -324,7 +370,6 @@ declare const configSchema: z.ZodObject<{
324
370
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
325
371
  timeout_ms: z.ZodOptional<z.ZodNumber>;
326
372
  }, z.core.$strict>>;
327
- hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
328
373
  nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
329
374
  }, z.core.$strict>>>;
330
375
  }, z.core.$strict>;
package/dist/config.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { resolveFileReference } from "./path-refs.js";
2
- import { parseJson } from "./safe-json.js";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { join } from "node:path";
5
4
  import { parseDocument } from "yaml";
@@ -23,6 +22,7 @@ const HOOK_EVENTS = [
23
22
  "node.start",
24
23
  "node.success",
25
24
  "node.error",
25
+ "node.finish",
26
26
  "gate.failure"
27
27
  ];
28
28
  const TOOL_NAMES = [
@@ -55,6 +55,7 @@ const RETRY_REASONS = [
55
55
  "timeout"
56
56
  ];
57
57
  const SCHEDULE_BASELINES = ["epic", "pipe"];
58
+ const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
58
59
  var PipelineConfigError = class extends Error {
59
60
  code;
60
61
  issues;
@@ -126,21 +127,13 @@ const mcpServerSchema = z.object({
126
127
  path: ["bearer_token_env_var"]
127
128
  });
128
129
  });
129
- const mcpServerRefSchema = z.object({
130
- format: z.literal("mcp-json").default("mcp-json"),
131
- id: z.string().min(1).optional(),
132
- path: z.string().min(1)
130
+ const mcpGatewaySchema = z.object({
131
+ default_profile: z.string().min(1).optional(),
132
+ mode: z.enum(["hosted", "local"]),
133
+ provider: z.literal("toolhive"),
134
+ token_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_TOKEN"),
135
+ url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
133
136
  }).strict();
134
- const mcpServerDefinitionSchema = z.union([mcpServerSchema, z.object({ ref: mcpServerRefSchema }).strict()]);
135
- const mcpJsonServerSchema = z.object({
136
- args: z.array(z.string()).optional(),
137
- bearer_token_env_var: z.string().min(1).optional(),
138
- command: z.string().min(1).optional(),
139
- env: z.record(z.string(), z.string()).optional(),
140
- headers: z.record(z.string(), z.string()).optional(),
141
- url: z.string().min(1).optional()
142
- }).passthrough();
143
- const mcpJsonFileSchema = z.object({ mcpServers: strictRecord(mcpJsonServerSchema) }).passthrough();
144
137
  const instructionsSchema = z.object({
145
138
  inline: z.string().min(1).optional(),
146
139
  path: z.string().min(1).optional()
@@ -237,27 +230,72 @@ const profileSchema = z.object({
237
230
  skills: z.array(z.string()).optional(),
238
231
  tools: z.array(z.enum(TOOL_NAMES)).optional()
239
232
  }).strict();
240
- const orchestratorSchema = z.object({
241
- hooks: z.array(z.string()).optional(),
242
- profile: z.string()
243
- }).strict();
233
+ const orchestratorSchema = z.object({ profile: z.string() }).strict();
244
234
  const hookEnvSchema = z.object({
245
235
  passthrough: z.array(z.string()).optional(),
246
236
  set: z.record(z.string(), z.string()).optional()
247
237
  }).strict();
248
- const hookSchema = z.object({
249
- builtin: z.string().optional(),
250
- command: z.array(z.string()).optional(),
251
- enabled: z.boolean().optional(),
238
+ const hookPermissionsSchema = z.object({
239
+ filesystem: z.enum(FILESYSTEM_MODES).optional(),
240
+ network: z.enum(NETWORK_MODES).optional()
241
+ }).strict();
242
+ const hookReturnsSchema = z.object({ schema: z.string().min(1).optional() }).strict();
243
+ const moduleHookFunctionSchema = z.object({
244
+ kind: z.literal("module"),
245
+ module: z.string().min(1),
246
+ permissions: hookPermissionsSchema.optional(),
247
+ returns: hookReturnsSchema.optional(),
248
+ timeout_ms: z.number().int().positive().optional()
249
+ }).strict();
250
+ const commandHookProtocolSchema = z.object({
251
+ input: z.literal("file"),
252
+ result: z.literal("file")
253
+ }).strict();
254
+ const commandHookFunctionSchema = z.object({
255
+ command: z.array(z.string()).min(1),
252
256
  env: hookEnvSchema.optional(),
253
- event: z.enum(HOOK_EVENTS),
254
- kind: z.enum(["command", "builtin"]),
257
+ kind: z.literal("command"),
255
258
  output_limit_bytes: z.number().int().positive().optional(),
256
- payload: z.enum(["stdin"]).optional(),
257
- required: z.boolean().optional(),
259
+ protocol: commandHookProtocolSchema.default({
260
+ input: "file",
261
+ result: "file"
262
+ }),
263
+ returns: hookReturnsSchema.optional(),
258
264
  timeout_ms: z.number().int().positive().optional(),
259
265
  trusted: z.boolean().optional()
260
266
  }).strict();
267
+ const hookFunctionSchema = z.discriminatedUnion("kind", [moduleHookFunctionSchema, commandHookFunctionSchema]);
268
+ const hookBindingWhereSchema = z.object({
269
+ gate: z.string().optional(),
270
+ node: z.string().optional(),
271
+ workflow: z.string().optional()
272
+ }).strict();
273
+ const hookBindingResultSchema = z.object({
274
+ pass_to: z.enum(["downstream"]).optional(),
275
+ publish: z.boolean().optional(),
276
+ save_as: z.string().min(1).optional()
277
+ }).strict();
278
+ const hookBindingSchema = z.object({
279
+ failure: z.enum(["fail", "ignore"]).default("ignore"),
280
+ function: z.string().min(1),
281
+ id: z.string().min(1),
282
+ result: hookBindingResultSchema.optional(),
283
+ where: hookBindingWhereSchema.optional(),
284
+ with: z.record(z.string(), z.unknown()).optional()
285
+ }).strict();
286
+ const hookPolicySchema = z.object({
287
+ commands: z.enum([
288
+ "allow",
289
+ "trusted-only",
290
+ "deny"
291
+ ]).optional(),
292
+ modules: z.enum(["allow", "deny"]).optional()
293
+ }).strict();
294
+ const hooksConfigSchema = z.object({
295
+ functions: strictRecord(hookFunctionSchema).default({}),
296
+ on: strictRecord(z.array(hookBindingSchema)).default({}),
297
+ policy: hookPolicySchema.optional()
298
+ }).strict();
261
299
  const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
262
300
  const nodeTaskContextSchema = z.object({
263
301
  acceptance_criteria: z.array(z.object({
@@ -282,7 +320,6 @@ const schedulePolicySchema = z.object({
282
320
  const workflowNodeBaseSchema = z.object({
283
321
  artifacts: z.array(artifactSchema).optional(),
284
322
  gates: z.array(gateSchema).optional(),
285
- hooks: z.array(z.string()).optional(),
286
323
  id: z.string(),
287
324
  needs: z.array(z.string()).optional(),
288
325
  retries: retriesSchema.optional(),
@@ -319,7 +356,6 @@ const workflowNodeSchema = z.lazy(() => z.discriminatedUnion("kind", [
319
356
  const workflowSchema = z.object({
320
357
  description: z.string().optional(),
321
358
  execution: workflowExecutionSchema.optional(),
322
- hooks: z.array(z.string()).optional(),
323
359
  nodes: z.array(workflowNodeSchema)
324
360
  }).strict();
325
361
  const runnersFileSchema = z.object({
@@ -327,7 +363,8 @@ const runnersFileSchema = z.object({
327
363
  version: z.literal(1)
328
364
  }).strict();
329
365
  const profilesFileSchema = z.object({
330
- mcp_servers: strictRecord(mcpServerDefinitionSchema).default({}),
366
+ mcp_gateway: mcpGatewaySchema.optional(),
367
+ mcp_servers: strictRecord(z.never()).default({}),
331
368
  profiles: strictRecord(profileSchema).default({}),
332
369
  rules: strictRecord(pathRefSchema).default({}),
333
370
  skills: strictRecord(pathRefSchema).default({}),
@@ -336,7 +373,10 @@ const profilesFileSchema = z.object({
336
373
  const pipelineFileSchema = z.object({
337
374
  default_workflow: z.string(),
338
375
  entrypoints: strictRecord(entrypointSchema).default({}),
339
- hooks: strictRecord(hookSchema).default({}),
376
+ hooks: hooksConfigSchema.default({
377
+ functions: {},
378
+ on: {}
379
+ }),
340
380
  orchestrator: orchestratorSchema,
341
381
  schedules: strictRecord(schedulePolicySchema).default({}),
342
382
  task_context: taskContextResolverSchema.optional(),
@@ -346,7 +386,11 @@ const pipelineFileSchema = z.object({
346
386
  const configSchema = z.object({
347
387
  default_workflow: z.string(),
348
388
  entrypoints: strictRecord(entrypointSchema).default({}),
349
- hooks: strictRecord(hookSchema).default({}),
389
+ hooks: hooksConfigSchema.default({
390
+ functions: {},
391
+ on: {}
392
+ }),
393
+ mcp_gateway: mcpGatewaySchema.optional(),
350
394
  mcp_servers: strictRecord(mcpServerSchema).default({}),
351
395
  orchestrator: orchestratorSchema,
352
396
  profiles: strictRecord(profileSchema).default({}),
@@ -464,12 +508,12 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
464
508
  const runners = parseYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
465
509
  const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
466
510
  const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
467
- const mcpServers = resolveMcpServerDefinitions(profiles.mcp_servers, projectRoot);
468
511
  return validatePipelineConfig({
469
512
  default_workflow: pipeline.default_workflow,
470
513
  entrypoints: pipeline.entrypoints,
471
514
  hooks: pipeline.hooks,
472
- mcp_servers: mcpServers,
515
+ ...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
516
+ mcp_servers: profiles.mcp_servers,
473
517
  orchestrator: pipeline.orchestrator,
474
518
  profiles: profiles.profiles,
475
519
  rules: profiles.rules,
@@ -497,61 +541,6 @@ function parseYamlAs(source, sourcePath, schema) {
497
541
  })));
498
542
  return parsed.data;
499
543
  }
500
- function resolveMcpServerDefinitions(registry, projectRoot) {
501
- return Object.fromEntries(Object.entries(registry).map(([id, definition]) => [id, "ref" in definition ? resolveMcpServerRef(id, definition.ref, projectRoot) : definition]));
502
- }
503
- function resolveMcpServerRef(id, ref, projectRoot) {
504
- if (!projectRoot) throw validationError([{
505
- path: `mcp_servers.${id}.ref.path`,
506
- message: "MCP server refs require a project root"
507
- }]);
508
- const filePath = resolveFileReference(projectRoot, ref.path);
509
- if (!existsSync(filePath)) throw validationError([{
510
- path: `mcp_servers.${id}.ref.path`,
511
- message: `referenced MCP config file '${ref.path}' does not exist`
512
- }]);
513
- const parsed = parseMcpJsonFile(id, ref, filePath);
514
- const importedId = ref.id ?? id;
515
- const imported = parsed.mcpServers[importedId];
516
- if (!imported) throw validationError([{
517
- path: `mcp_servers.${id}.ref.id`,
518
- message: `MCP config '${ref.path}' does not declare server '${importedId}'`
519
- }]);
520
- const normalized = normalizeMcpJsonServer(imported);
521
- const result = mcpServerSchema.safeParse(normalized);
522
- if (!result.success) throw validationError(result.error.issues.map((issue) => ({
523
- path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
524
- message: issue.message
525
- })));
526
- return result.data;
527
- }
528
- function parseMcpJsonFile(id, ref, filePath) {
529
- let raw;
530
- try {
531
- raw = parseJson(readFileSync(filePath, "utf8"), `MCP config ${ref.path}`);
532
- } catch (err) {
533
- throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse MCP config ${ref.path}`, [{
534
- path: `mcp_servers.${id}.ref.path`,
535
- message: err instanceof Error ? err.message : String(err)
536
- }]);
537
- }
538
- const parsed = mcpJsonFileSchema.safeParse(raw);
539
- if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
540
- path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
541
- message: issue.message
542
- })));
543
- return parsed.data;
544
- }
545
- function normalizeMcpJsonServer(server) {
546
- return {
547
- ...server.command ? { command: server.command } : {},
548
- ...server.args ? { args: server.args } : {},
549
- ...server.env ? { env: server.env } : {},
550
- ...server.url ? { url: server.url } : {},
551
- ...server.headers ? { headers: server.headers } : {},
552
- ...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
553
- };
554
- }
555
544
  function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
556
545
  const parsed = configSchema.safeParse(rawConfig);
557
546
  if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
@@ -565,11 +554,10 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
565
554
  validateRegistryIds("rules", config.rules, issues);
566
555
  validateRegistryIds("skills", config.skills, issues);
567
556
  validateRegistryIds("mcp_servers", config.mcp_servers, issues);
568
- validateRegistryIds("hooks", config.hooks, issues);
557
+ validateRegistryIds("hooks.functions", config.hooks.functions, issues);
569
558
  validateRegistryIds("workflows", config.workflows, issues);
570
559
  validateRegistryIds("entrypoints", config.entrypoints, issues);
571
- if (config.profiles[config.orchestrator.profile]) validateReferences("orchestrator.hooks", config.orchestrator.hooks, config.hooks, "hook", issues);
572
- else issues.push({
560
+ if (!config.profiles[config.orchestrator.profile]) issues.push({
573
561
  path: "orchestrator.profile",
574
562
  message: `orchestrator references missing profile '${config.orchestrator.profile}'`
575
563
  });
@@ -584,16 +572,7 @@ function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
584
572
  }
585
573
  validateProfile(profileId, profile, runner, config, issues, projectRoot, options);
586
574
  }
587
- for (const [hookId, hook] of Object.entries(config.hooks)) {
588
- if (hook.kind === "command" && !hook.command) issues.push({
589
- path: `hooks.${hookId}.command`,
590
- message: `command hook '${hookId}' must declare command`
591
- });
592
- if (hook.kind === "builtin" && !hook.builtin) issues.push({
593
- path: `hooks.${hookId}.builtin`,
594
- message: `builtin hook '${hookId}' must declare builtin`
595
- });
596
- }
575
+ validateHookConfig(config, issues, projectRoot, options);
597
576
  for (const [ruleId, rule] of Object.entries(config.rules)) validatePath(`rules.${ruleId}.path`, rule.path, projectRoot, issues, options);
598
577
  for (const [skillId, skill] of Object.entries(config.skills)) validatePath(`skills.${skillId}.path`, skill.path, projectRoot, issues, options);
599
578
  for (const [workflowId, workflow] of Object.entries(config.workflows)) validateWorkflow(workflowId, workflow, config, issues, projectRoot, options);
@@ -606,6 +585,29 @@ function validateRegistryIds(name, registry, issues) {
606
585
  message: `registry id '${id}' must match ${ID_RE.source}`
607
586
  });
608
587
  }
588
+ function validateHookConfig(config, issues, projectRoot, options = {}) {
589
+ const allowedEvents = new Set(HOOK_EVENTS);
590
+ for (const [functionId, hookFunction] of Object.entries(config.hooks.functions)) validatePath(`hooks.functions.${functionId}.returns.schema`, hookFunction.returns?.schema, projectRoot, issues, options);
591
+ for (const [event, bindings] of Object.entries(config.hooks.on)) {
592
+ if (!allowedEvents.has(event)) {
593
+ issues.push({
594
+ path: `hooks.on.${event}`,
595
+ message: `unsupported hook event '${event}'`
596
+ });
597
+ continue;
598
+ }
599
+ for (const [index, binding] of bindings.entries()) {
600
+ if (!ID_RE.test(binding.id)) issues.push({
601
+ path: `hooks.on.${event}.${index}.id`,
602
+ message: `hook binding id '${binding.id}' must match ${ID_RE.source}`
603
+ });
604
+ if (!config.hooks.functions[binding.function]) issues.push({
605
+ path: `hooks.on.${event}.${index}.function`,
606
+ message: `hook binding '${binding.id}' references missing function '${binding.function}'`
607
+ });
608
+ }
609
+ }
610
+ }
609
611
  function validateProfile(profileId, profile, runner, config, issues, projectRoot, options = {}) {
610
612
  validateActor(`profile '${profileId}'`, `profiles.${profileId}`, profile, runner, config, issues, projectRoot, options);
611
613
  validateListCapability(`profiles.${profileId}.output.format`, profile.output?.format ? [profile.output.format] : void 0, runner.capabilities.output_formats, "output format", issues);
@@ -629,7 +631,10 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
629
631
  validatePath(`${path}.instructions.path`, actor.instructions.path, projectRoot, issues, options);
630
632
  validateReferences(`${path}.rules`, actor.rules, config.rules, "rule", issues);
631
633
  validateReferences(`${path}.skills`, actor.skills, config.skills, "skill", issues);
632
- validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_servers, "MCP server", issues);
634
+ validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_gateway ? {
635
+ ...config.mcp_servers,
636
+ [PIPELINE_GATEWAY_SERVER_ID]: {}
637
+ } : config.mcp_servers, "MCP server", issues);
633
638
  validateBooleanCapability(`${path}.rules`, actor.rules, runner.capabilities.rules, "rules", issues);
634
639
  validateBooleanCapability(`${path}.skills`, actor.skills, runner.capabilities.skills, "skills", issues);
635
640
  validateBooleanCapability(`${path}.mcp_servers`, actor.mcp_servers, runner.capabilities.mcp_servers, "MCP servers", issues);
@@ -638,7 +643,6 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
638
643
  validateListCapability(`${path}.network.mode`, actor.network?.mode ? [actor.network.mode] : void 0, runner.capabilities.network, "network mode", issues);
639
644
  }
640
645
  function validateWorkflow(workflowId, workflow, config, issues, projectRoot, options = {}) {
641
- validateReferences(`workflows.${workflowId}.hooks`, workflow.hooks, config.hooks, "hook", issues);
642
646
  const nodeIds = /* @__PURE__ */ new Set();
643
647
  for (const node of workflow.nodes) {
644
648
  if (nodeIds.has(node.id)) issues.push({
@@ -661,7 +665,6 @@ function validateWorkflowNode(workflowId, node, nodeIds, config, issues) {
661
665
  path: `workflows.${workflowId}.nodes.${node.id}.needs`,
662
666
  message: `node '${node.id}' references missing dependency '${need}'`
663
667
  });
664
- validateReferences(`workflows.${workflowId}.nodes.${node.id}.hooks`, node.hooks, config.hooks, "hook", issues);
665
668
  validateWorkflowNodeKind(workflowId, node, config, issues);
666
669
  if (node.kind === "parallel") validateParallelWorkflowNode(workflowId, node, config, issues);
667
670
  }