@oisincoveney/pipeline 1.20.0 → 1.21.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 (41) hide show
  1. package/README.md +41 -43
  2. package/dist/commands/pipeline-command.js +0 -1
  3. package/dist/config.d.ts +19 -3
  4. package/dist/config.js +91 -10
  5. package/dist/gates.js +22 -6
  6. package/dist/hooks.d.ts +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +25 -22
  9. package/dist/install-commands.js +8 -2
  10. package/dist/mcp/gateway.js +101 -1
  11. package/dist/mcp/repo-local-backends.js +70 -0
  12. package/dist/mcp/toolhive-vmcp.js +57 -0
  13. package/dist/pipeline-init.js +10 -256
  14. package/dist/pipeline-runtime.d.ts +2 -2
  15. package/dist/pipeline-runtime.js +183 -4
  16. package/dist/runner-event-sink.js +27 -7
  17. package/dist/runner-job/delivery.js +64 -15
  18. package/dist/runner-job/pr-summary.js +117 -0
  19. package/dist/runner-job/run.js +63 -19
  20. package/dist/runner-job/workspace.js +39 -5
  21. package/dist/runner-output.js +21 -19
  22. package/dist/runner.d.ts +1 -1
  23. package/dist/runner.js +15 -3
  24. package/dist/runtime/agent-node/agent-node.js +31 -5
  25. package/dist/runtime/builtins/builtins.js +11 -4
  26. package/dist/runtime/context/context.js +1 -0
  27. package/dist/runtime/contracts/contracts.d.ts +17 -1
  28. package/dist/runtime/contracts/index.d.ts +1 -1
  29. package/dist/runtime/events/events.js +45 -1
  30. package/dist/runtime/json-validation/json-validation.js +8 -2
  31. package/dist/runtime/parallel-node/parallel-node.js +1 -0
  32. package/dist/schedule-planner.js +168 -5
  33. package/dist/standard-output-schemas.js +80 -0
  34. package/docs/config-architecture.md +13 -15
  35. package/docs/mcp-gateway.md +30 -9
  36. package/docs/mcp-host-isolation.md +2 -1
  37. package/docs/operator-guide.md +60 -18
  38. package/docs/pipeline-console-runner-contract.md +14 -14
  39. package/docs/pipeline-smoke-recovery-plan.md +7 -8
  40. package/docs/slash-command-adapter-contract.md +5 -6
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # @oisincoveney/pipeline
2
2
 
3
- Config-driven multi-agent pipeline runner for repository work. The source of
4
- truth is three YAML files: `.pipeline/runners.yaml` declares runner adapters,
5
- `.pipeline/profiles.yaml` declares reusable profiles and their grants, and
6
- `.pipeline/pipeline.yaml` declares orchestration, workflows, gates, hooks, and
7
- artifacts.
3
+ Config-driven multi-agent pipeline runner for repository work. Runtime config is
4
+ owned by the installed `@oisincoveney/pipeline` package. Repo-local `.pipeline`
5
+ paths are runtime artifact locations only; they are not the runtime config
6
+ source.
8
7
 
9
8
  ## Requirements
10
9
 
11
10
  - Bun 1.1 or newer
12
11
  - Node.js 22.13 or newer
13
- - `npx`, `backlog`, `uvx`, and Docker on `PATH` for default skills and MCP setup
12
+ - `npx`, `backlog`, `uvx`, and Docker on `PATH` for default skills and MCP gateway setup
14
13
  - At least one configured runner CLI on `PATH`: `codex`, `opencode`, `kimi`,
15
14
  `pi`, or a declared command runner
16
15
 
@@ -22,33 +21,31 @@ bun install --frozen-lockfile
22
21
 
23
22
  ## Start A Repository
24
23
 
25
- Scaffold the default YAML workflow:
24
+ Initialize package-owned pipeline support:
26
25
 
27
26
  ```shell
28
27
  pipe init
29
28
  ```
30
29
 
31
30
  `pipe init` installs default project skills with
32
- `npx skills add oisincoveney/skills` and registers default MCP servers with the
33
- MCPM CLI from https://mcpm.sh/. Default profiles point at the installed
34
- `.agents/skills/<skill>/SKILL.md` files in the target repository. The package
35
- invokes MCPM through `uvx --python 3.12 mcpm`, so generated `.mcp.json` entries
36
- do not depend on a globally installed `mcpm` binary. The default Qdrant/memory
37
- MCP is the Momokaya remote endpoint
38
- `https://memory-mcp.momokaya.ee/mcp/`.
39
-
40
- The default GitHub MCP registration uses GitHub's official container in
41
- read-only mode and reads `GITHUB_PERSONAL_ACCESS_TOKEN` from the environment.
42
- The Momokaya gateway endpoint is protected by Traefik HTTP Basic auth. Set
43
- `PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
44
- value before starting Codex or OpenCode:
31
+ `npx skills add oisincoveney/skills`, then writes generated Codex and OpenCode
32
+ command surfaces plus their singleton `pipeline-gateway` MCP entries. It does
33
+ not create repo-local `.pipeline` config files.
34
+
35
+ The default MCP gateway can run locally or point at the hosted Momokaya
36
+ gateway. Set `PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP
37
+ `Authorization` header value before starting Codex or OpenCode when using a
38
+ protected gateway:
45
39
 
46
40
  ```shell
47
41
  export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' | base64)"
48
42
  ```
49
43
 
50
- `pipe init` writes project-level Codex and OpenCode config that points at the
51
- singleton `pipeline-gateway` MCP server.
44
+ To refresh or check generated host files later, use:
45
+
46
+ ```shell
47
+ pipe install-commands --host all --check
48
+ ```
52
49
 
53
50
  Check local prerequisites and config health:
54
51
 
@@ -136,9 +133,12 @@ validation, contract-version checks, and JSON Schema generation.
136
133
  Use `PIPELINE_TARGET_PATH=/path/to/worktree` when the checked-out target repo is
137
134
  mounted somewhere other than the process working directory.
138
135
 
139
- ## Minimal YAML
136
+ ## Custom YAML Parts
137
+
138
+ Runtime execution uses package-owned defaults. Tests and advanced embedding code
139
+ can still parse explicit YAML parts with `parsePipelineConfigParts()`.
140
140
 
141
- `.pipeline/runners.yaml`:
141
+ `runners`:
142
142
 
143
143
  ```yaml
144
144
  version: 1
@@ -156,7 +156,7 @@ runners:
156
156
  output_formats: [text, json, jsonl, json_schema]
157
157
  ```
158
158
 
159
- `.pipeline/profiles.yaml`:
159
+ `profiles`:
160
160
 
161
161
  ```yaml
162
162
  version: 1
@@ -182,7 +182,7 @@ profiles:
182
182
  format: text
183
183
  ```
184
184
 
185
- `.pipeline/pipeline.yaml`:
185
+ Example workflow shape:
186
186
 
187
187
  ```yaml
188
188
  version: 1
@@ -215,14 +215,12 @@ workflows:
215
215
  builtin: typecheck
216
216
  ```
217
217
 
218
- Projects can also declare `entrypoints` in `.pipeline/pipeline.yaml` to expose
219
- stable app or CLI names that resolve to workflows or schedule policies. Direct
220
- `--workflow` selection remains available and takes precedence over `--entrypoint`
221
- when both are set.
218
+ Package-owned defaults declare `entrypoints` that expose stable app or CLI names
219
+ resolving to workflows or schedule policies. Direct `--workflow` selection
220
+ remains available and takes precedence over `--entrypoint` when both are set.
222
221
 
223
- The default scaffold includes a full research, red, green, verify, learn
224
- workflow. See `docs/config-architecture.md` for a complete example and the host
225
- support matrix.
222
+ The package defaults include a full research, red, green, verify, learn
223
+ workflow. See `docs/config-architecture.md` for the host support matrix.
226
224
 
227
225
  ### Structural Parallelism
228
226
 
@@ -292,22 +290,22 @@ branches share a base SHA, and merges passing branches into an integration
292
290
  branch in declaration order. It reports merge conflicts; it does not resolve
293
291
  them automatically.
294
292
 
295
- Default profile skills are installed into `.agents/skills` by `pipe init`.
296
- Runtime MCP projection and host-specific isolation policy live in `src/mcp`; see
297
- [`docs/mcp-host-isolation.md`](docs/mcp-host-isolation.md) and
293
+ Default profile skills and generated host resources are installed by
294
+ `pipe init`. Runtime MCP projection and host-specific isolation policy live in
295
+ `src/mcp`; see [`docs/mcp-host-isolation.md`](docs/mcp-host-isolation.md) and
298
296
  [`docs/mcp-gateway.md`](docs/mcp-gateway.md).
299
297
 
300
298
  ## Generated Host Resources
301
299
 
302
- Generate native host files from the YAML config:
300
+ Generate native host files during setup:
303
301
 
304
302
  ```shell
305
- pipe install-commands --host all
303
+ pipe init
306
304
  ```
307
305
 
308
- Generated resources are derived from the three config files; they are not
309
- separate sources of truth. Host resources use exact native agents when the node
310
- runner matches the host. OpenCode also uses native subagents for cross-runner
306
+ Generated resources are derived from package-owned config; they are not separate
307
+ sources of truth. Host resources use exact native agents when the node runner
308
+ matches the host. OpenCode also uses native subagents for cross-runner
311
309
  model-backed nodes when the runner/profile provides an OpenCode-compatible
312
310
  `model` or `host_models.opencode` value. Otherwise generated instructions
313
311
  dispatch to that runner's CLI instead of inventing a host model.
@@ -336,8 +334,8 @@ runners:
336
334
 
337
335
  ## Runtime Guarantees
338
336
 
339
- - `pipe run` fails without `.pipeline/pipeline.yaml`,
340
- `.pipeline/profiles.yaml`, and `.pipeline/runners.yaml`.
337
+ - `pipe run` loads package-owned `@oisincoveney/pipeline` config even when the
338
+ repository has no repo-local `.pipeline` config files.
341
339
  - Multi-agent workflows execute as separate agent boundaries; nodes are not
342
340
  merged into one prompt.
343
341
  - Native subagent strategy is preferred when the selected runner can represent
@@ -12,7 +12,6 @@ const BUILTIN_PIPE_COMMANDS = new Set([
12
12
  ]);
13
13
  function registerConfiguredEntrypointCommands(program, config, runEntrypoint) {
14
14
  const registered = /* @__PURE__ */ new Set();
15
- if (!config) return registered;
16
15
  const reservedCommands = new Set(program.commands.map((command) => command.name()));
17
16
  for (const [id, entrypoint] of Object.entries(config.entrypoints)) {
18
17
  if (reservedCommands.has(id)) continue;
package/dist/config.d.ts CHANGED
@@ -10,11 +10,13 @@ declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "work
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
  declare const SCHEDULING_ROLES: readonly ["coverage", "implementation"];
13
+ declare const MCP_GATEWAY_BACKEND_LOCALITIES: readonly ["repo-local", "repo-scoped-remote", "shared-remote"];
14
+ declare const MCP_GATEWAY_WORKSPACE_PATH_SOURCES: readonly ["PIPELINE_TARGET_PATH", "cwd"];
13
15
  declare const DEFAULT_RUNNER_JOB_GIT_COMMITTER: {
14
16
  readonly email: "git@oisin.ee";
15
17
  readonly name: "oisin-bot";
16
18
  };
17
- type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
19
+ type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
18
20
  interface PipelineConfigIssue {
19
21
  message: string;
20
22
  path?: string;
@@ -237,6 +239,19 @@ declare const configSchema: z.ZodObject<{
237
239
  }, z.core.$strict>>;
238
240
  }, z.core.$strict>>;
239
241
  mcp_gateway: z.ZodOptional<z.ZodObject<{
242
+ backends: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
243
+ locality: z.ZodEnum<{
244
+ "repo-local": "repo-local";
245
+ "repo-scoped-remote": "repo-scoped-remote";
246
+ "shared-remote": "shared-remote";
247
+ }>;
248
+ required: z.ZodDefault<z.ZodBoolean>;
249
+ tool_prefixes: z.ZodArray<z.ZodString>;
250
+ workspace_path_source: z.ZodOptional<z.ZodEnum<{
251
+ PIPELINE_TARGET_PATH: "PIPELINE_TARGET_PATH";
252
+ cwd: "cwd";
253
+ }>>;
254
+ }, z.core.$strict>>>;
240
255
  default_profile: z.ZodOptional<z.ZodString>;
241
256
  mode: z.ZodEnum<{
242
257
  hosted: "hosted";
@@ -411,6 +426,8 @@ type HookEvent = (typeof HOOK_EVENTS)[number];
411
426
  type GateKind = (typeof GATE_KINDS)[number];
412
427
  type ScheduleBaseline = (typeof SCHEDULE_BASELINES)[number];
413
428
  type SchedulingRole = (typeof SCHEDULING_ROLES)[number];
429
+ type McpGatewayBackendLocality = (typeof MCP_GATEWAY_BACKEND_LOCALITIES)[number];
430
+ type McpGatewayWorkspacePathSource = (typeof MCP_GATEWAY_WORKSPACE_PATH_SOURCES)[number];
414
431
  interface PipelineConfigParts {
415
432
  pipeline: string;
416
433
  profiles: string;
@@ -421,9 +438,8 @@ interface PipelineConfigValidationOptions {
421
438
  }
422
439
  declare function loadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig;
423
440
  declare function loadPackagePipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig;
424
- declare function tryLoadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig | null;
425
441
  declare function parsePipelineConfigYaml(source: string, sourcePath?: string, projectRoot?: string): PipelineConfig;
426
442
  declare function parsePipelineConfigParts(sources: PipelineConfigParts, projectRoot?: string, sourcePaths?: PipelineConfigParts, options?: PipelineConfigValidationOptions): PipelineConfig;
427
443
  declare function validatePipelineConfig(rawConfig: PipelineConfig, projectRoot?: string, options?: PipelineConfigValidationOptions): PipelineConfig;
428
444
  //#endregion
429
- export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, GateKind, HookEvent, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, ScheduleBaseline, SchedulingRole, WorkflowNodeKind, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
445
+ export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, GateKind, HookEvent, McpGatewayBackendLocality, McpGatewayWorkspacePathSource, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, ScheduleBaseline, SchedulingRole, WorkflowNodeKind, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, validatePipelineConfig, workflowSchema };
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolveFileReference } from "./path-refs.js";
2
+ import { standardOutputSchemaNameFromPath } from "./standard-output-schemas.js";
2
3
  import { existsSync } from "node:fs";
3
4
  import { parseDocument } from "yaml";
4
5
  import { z } from "zod";
@@ -54,6 +55,12 @@ const RETRY_REASONS = [
54
55
  ];
55
56
  const SCHEDULE_BASELINES = ["epic", "pipe"];
56
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"];
57
64
  const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
58
65
  const DEFAULT_RUNNER_JOB_GIT_COMMITTER = {
59
66
  email: "git@oisin.ee",
@@ -104,6 +111,29 @@ mcp_gateway:
104
111
  url_env: PIPELINE_MCP_GATEWAY_URL
105
112
  authorization_env: PIPELINE_MCP_GATEWAY_AUTHORIZATION
106
113
  default_profile: default
114
+ backends:
115
+ context7:
116
+ locality: shared-remote
117
+ tool_prefixes: [context7]
118
+ uidotsh:
119
+ locality: shared-remote
120
+ tool_prefixes: [uidotsh]
121
+ qdrant:
122
+ locality: repo-scoped-remote
123
+ tool_prefixes: [qdrant]
124
+ fallow:
125
+ locality: repo-local
126
+ workspace_path_source: PIPELINE_TARGET_PATH
127
+ required: false
128
+ tool_prefixes: [fallow]
129
+ serena:
130
+ locality: repo-local
131
+ workspace_path_source: PIPELINE_TARGET_PATH
132
+ tool_prefixes: [serena]
133
+ backlog:
134
+ locality: repo-local
135
+ workspace_path_source: PIPELINE_TARGET_PATH
136
+ tool_prefixes: [backlog]
107
137
  profiles:
108
138
  orchestrator:
109
139
  runner: codex
@@ -121,6 +151,10 @@ profiles:
121
151
  tools: [read, list, grep, glob, bash]
122
152
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
123
153
  network: { mode: inherit }
154
+ output:
155
+ format: json_schema
156
+ schema_path: .pipeline/schemas/research.schema.json
157
+ repair: { enabled: true, max_attempts: 1 }
124
158
  pipeline-inspector:
125
159
  runner: codex
126
160
  description: Inspect the repository without modifying files.
@@ -139,12 +173,17 @@ profiles:
139
173
  network: { mode: inherit }
140
174
  pipeline-test-writer:
141
175
  runner: codex
176
+ scheduling_roles: [implementation]
142
177
  description: Add focused failing tests for the requested behavior.
143
- instructions: { inline: "Add focused failing tests for the requested behavior only. Do not change production code." }
178
+ instructions: { inline: "Add focused failing tests for the requested behavior only. Do not change production code. 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." }
144
179
  mcp_servers: [pipeline-gateway]
145
180
  tools: [read, list, grep, glob, bash, edit, write]
146
181
  filesystem: { mode: workspace-write, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
147
182
  network: { mode: inherit }
183
+ output:
184
+ format: json_schema
185
+ schema_path: .pipeline/schemas/implementation.schema.json
186
+ repair: { enabled: true, max_attempts: 1 }
148
187
  pipeline-epic-router:
149
188
  runner: codex
150
189
  description: Route epic sub-tickets into fixed implementation tracks.
@@ -157,11 +196,15 @@ profiles:
157
196
  runner: codex
158
197
  scheduling_roles: [implementation]
159
198
  description: Implement production code until the failing tests pass.
160
- instructions: { inline: "Implement the smallest production change that satisfies the failing tests." }
199
+ instructions: { inline: "Implement the smallest production change that satisfies the failing tests. 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." }
161
200
  mcp_servers: [pipeline-gateway]
162
201
  tools: [read, list, grep, glob, bash, edit, write]
163
202
  filesystem: { mode: workspace-write, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
164
203
  network: { mode: inherit }
204
+ output:
205
+ format: json_schema
206
+ schema_path: .pipeline/schemas/implementation.schema.json
207
+ repair: { enabled: true, max_attempts: 1 }
165
208
  pipeline-acceptance-reviewer:
166
209
  runner: codex
167
210
  scheduling_roles: [coverage]
@@ -171,6 +214,10 @@ profiles:
171
214
  tools: [read, list, grep, glob, bash]
172
215
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
173
216
  network: { mode: inherit }
217
+ output:
218
+ format: json_schema
219
+ schema_path: .pipeline/schemas/acceptance.schema.json
220
+ repair: { enabled: true, max_attempts: 1 }
174
221
  pipeline-thermo-nuclear-reviewer:
175
222
  runner: codex
176
223
  scheduling_roles: [coverage]
@@ -180,6 +227,10 @@ profiles:
180
227
  tools: [read, list, grep, glob, bash]
181
228
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
182
229
  network: { mode: inherit }
230
+ output:
231
+ format: json_schema
232
+ schema_path: .pipeline/schemas/review.schema.json
233
+ repair: { enabled: true, max_attempts: 1 }
183
234
  pipeline-verifier:
184
235
  runner: codex
185
236
  scheduling_roles: [coverage]
@@ -189,6 +240,10 @@ profiles:
189
240
  tools: [read, list, grep, glob, bash]
190
241
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
191
242
  network: { mode: inherit }
243
+ output:
244
+ format: json_schema
245
+ schema_path: .pipeline/schemas/verify.schema.json
246
+ repair: { enabled: true, max_attempts: 1 }
192
247
  pipeline-learner:
193
248
  runner: codex
194
249
  description: Store durable lessons from the completed run.
@@ -197,6 +252,10 @@ profiles:
197
252
  tools: [read, list, grep, glob, bash]
198
253
  filesystem: { mode: read-only, allow: ["**/*"], deny: ["node_modules/**", "dist/**", ".git/**"] }
199
254
  network: { mode: inherit }
255
+ output:
256
+ format: json_schema
257
+ schema_path: .pipeline/schemas/learn.schema.json
258
+ repair: { enabled: true, max_attempts: 1 }
200
259
  `;
201
260
  const PACKAGE_DEFAULT_PIPELINE_YAML = `version: 1
202
261
  default_workflow: default
@@ -419,7 +478,28 @@ const mcpServerSchema = z.object({
419
478
  path: ["bearer_token_env_var"]
420
479
  });
421
480
  });
481
+ const mcpGatewayBackendSchema = z.object({
482
+ locality: z.enum(MCP_GATEWAY_BACKEND_LOCALITIES),
483
+ required: z.boolean().default(true),
484
+ tool_prefixes: z.array(z.string().min(1)).min(1),
485
+ workspace_path_source: z.enum(MCP_GATEWAY_WORKSPACE_PATH_SOURCES).optional()
486
+ }).strict().superRefine((backend, ctx) => {
487
+ if (backend.locality === "repo-local") {
488
+ if (!backend.workspace_path_source) ctx.addIssue({
489
+ code: "custom",
490
+ message: "repo-local gateway backend must declare workspace_path_source as PIPELINE_TARGET_PATH or cwd",
491
+ path: ["workspace_path_source"]
492
+ });
493
+ return;
494
+ }
495
+ if (backend.workspace_path_source) ctx.addIssue({
496
+ code: "custom",
497
+ message: "workspace_path_source is only valid for repo-local gateway backends",
498
+ path: ["workspace_path_source"]
499
+ });
500
+ });
422
501
  const mcpGatewaySchema = z.object({
502
+ backends: strictRecord(mcpGatewayBackendSchema).default({}),
423
503
  default_profile: z.string().min(1).optional(),
424
504
  mode: z.enum(["hosted", "local"]),
425
505
  provider: z.literal("toolhive"),
@@ -804,9 +884,6 @@ function loadPackagePipelineConfig(projectRoot, options = {}) {
804
884
  runners: "@oisincoveney/pipeline/defaults/runners.yaml"
805
885
  }, options);
806
886
  }
807
- function tryLoadPipelineConfig(projectRoot, options = {}) {
808
- return loadPipelineConfig(projectRoot, options);
809
- }
810
887
  function parsePipelineConfigYaml(source, sourcePath = PIPELINE_CONFIG_PATH, projectRoot) {
811
888
  return parsePipelineConfigParts({
812
889
  pipeline: source,
@@ -950,10 +1027,13 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
950
1027
  validatePath(`${path}.instructions.path`, actor.instructions.path, projectRoot, issues, options);
951
1028
  validateReferences(`${path}.rules`, actor.rules, config.rules, "rule", issues);
952
1029
  validateReferences(`${path}.skills`, actor.skills, config.skills, "skill", issues);
953
- validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_gateway ? {
954
- ...config.mcp_servers,
955
- [PIPELINE_GATEWAY_SERVER_ID]: {}
956
- } : config.mcp_servers, "MCP server", issues);
1030
+ validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_gateway ? { [PIPELINE_GATEWAY_SERVER_ID]: {} } : config.mcp_servers, "MCP server", issues);
1031
+ if (config.mcp_gateway) {
1032
+ for (const serverId of actor.mcp_servers ?? []) if (serverId !== PIPELINE_GATEWAY_SERVER_ID) issues.push({
1033
+ path: `${path}.mcp_servers`,
1034
+ message: `${path}.mcp_servers must only reference ${PIPELINE_GATEWAY_SERVER_ID} when mcp_gateway is configured`
1035
+ });
1036
+ }
957
1037
  validateBooleanCapability(`${path}.rules`, actor.rules, runner.capabilities.rules, "rules", issues);
958
1038
  validateBooleanCapability(`${path}.skills`, actor.skills, runner.capabilities.skills, "skills", issues);
959
1039
  validateBooleanCapability(`${path}.mcp_servers`, actor.mcp_servers, runner.capabilities.mcp_servers, "MCP servers", issues);
@@ -1052,6 +1132,7 @@ function validateListCapability(path, requested, supported, label, issues) {
1052
1132
  }
1053
1133
  function validatePath(path, value, projectRoot, issues, options = {}) {
1054
1134
  if (!(value && projectRoot)) return;
1135
+ if (standardOutputSchemaNameFromPath(value)) return;
1055
1136
  if (!existsSync(resolveFileReference(projectRoot, value))) {
1056
1137
  if (options.allowMissingLintFileReferences && isLintableMissingFileReferencePath(path)) return;
1057
1138
  issues.push({
@@ -1070,4 +1151,4 @@ function validationError(issues) {
1070
1151
  return new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", ["Invalid pipeline config:", ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
1071
1152
  }
1072
1153
  //#endregion
1073
- export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
1154
+ export { DEFAULT_RUNNER_JOB_GIT_COMMITTER, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPackagePipelineConfig, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, validatePipelineConfig, workflowSchema };
package/dist/gates.js CHANGED
@@ -12,6 +12,9 @@ function parseFailingTests(output) {
12
12
  return m ? [m[1].trim()] : [];
13
13
  });
14
14
  }
15
+ function displayCommand(command) {
16
+ return [command.command, ...command.args].join(" ");
17
+ }
15
18
  function readPackageScripts(worktreePath) {
16
19
  try {
17
20
  return parseJson(readFileSync(join(worktreePath, "package.json"), "utf-8"), "package.json").scripts ?? {};
@@ -55,14 +58,16 @@ async function runTests(worktreePath, signal) {
55
58
  });
56
59
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
57
60
  return {
61
+ command: displayCommand(projectCommand),
58
62
  exitCode: result.exitCode ?? 0,
59
63
  output,
60
64
  failingTests: []
61
65
  };
62
66
  } catch (err) {
63
67
  const e = err;
64
- const output = [e.stdout, e.stderr].filter(Boolean).join("\n");
68
+ const output = [e.stdout, e.stderr].filter(Boolean).join("\n") || [e.shortMessage, e.message].filter(Boolean).join("\n");
65
69
  return {
70
+ command: displayCommand(projectCommand),
66
71
  exitCode: e.exitCode ?? 1,
67
72
  output,
68
73
  failingTests: parseFailingTests(output)
@@ -83,26 +88,35 @@ async function runTypecheck(worktreePath, signal) {
83
88
  });
84
89
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
85
90
  return {
91
+ command: displayCommand(projectCommand),
86
92
  exitCode: result.exitCode ?? 0,
87
93
  output
88
94
  };
89
95
  } catch (err) {
90
96
  const e = err;
91
- const output = [e.stdout, e.stderr].filter(Boolean).join("\n");
97
+ const output = [e.stdout, e.stderr].filter(Boolean).join("\n") || [e.shortMessage, e.message].filter(Boolean).join("\n");
92
98
  return {
99
+ command: displayCommand(projectCommand),
93
100
  exitCode: e.exitCode ?? 1,
94
101
  output
95
102
  };
96
103
  }
97
104
  }
98
- async function runSemgrep(worktreePath, signal) {
99
- const projectCommand = envCommand("PIPELINE_SEMGREP_COMMAND") ?? {
105
+ async function runSemgrep(worktreePath, signal, changedFiles) {
106
+ const overrideCommand = envCommand("PIPELINE_SEMGREP_COMMAND");
107
+ const targets = changedFiles ? [...new Set(changedFiles)].filter((file) => existsSync(join(worktreePath, file))) : void 0;
108
+ if (!overrideCommand && targets && targets.length === 0) return {
109
+ command: "uvx semgrep scan --config=p/ci --error",
110
+ exitCode: 0,
111
+ output: "skipped: no changed files to scan"
112
+ };
113
+ const projectCommand = overrideCommand ?? {
100
114
  args: [
101
115
  "semgrep",
102
116
  "scan",
103
117
  "--config=p/ci",
104
118
  "--error",
105
- "."
119
+ ...targets ? ["--", ...targets] : ["."]
106
120
  ],
107
121
  command: "uvx"
108
122
  };
@@ -114,13 +128,15 @@ async function runSemgrep(worktreePath, signal) {
114
128
  });
115
129
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
116
130
  return {
131
+ command: displayCommand(projectCommand),
117
132
  exitCode: result.exitCode ?? 0,
118
133
  output
119
134
  };
120
135
  } catch (err) {
121
136
  const e = err;
122
- const output = [e.stdout, e.stderr].filter(Boolean).join("\n");
137
+ const output = [e.stdout, e.stderr].filter(Boolean).join("\n") || [e.shortMessage, e.message].filter(Boolean).join("\n");
123
138
  return {
139
+ command: displayCommand(projectCommand),
124
140
  exitCode: e.exitCode ?? 1,
125
141
  output
126
142
  };
package/dist/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- pass: "pass";
17
16
  fail: "fail";
17
+ pass: "pass";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ interface PipeOptions {
10
10
  }
11
11
  /**
12
12
  * Config-driven `pipe` entrypoint. Package-owned defaults are the source of
13
- * truth; repo-local pipeline files are only scaffolding fixtures.
13
+ * truth; repo-local pipeline files are ignored by runtime loading.
14
14
  */
15
15
  declare function pipe(description: string, options?: PipeOptions): Promise<void>;
16
16
  interface DoctorCheck {
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { PipelineConfigError, loadPipelineConfig, tryLoadPipelineConfig } from "./config.js";
2
+ import { standardOutputSchemaNameFromPath } from "./standard-output-schemas.js";
3
+ import { PipelineConfigError, loadPipelineConfig } from "./config.js";
3
4
  import { BUILTIN_PIPE_COMMANDS, registerConfiguredEntrypointCommands } from "./commands/pipeline-command.js";
4
- import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
5
+ import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
5
6
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "./runner.js";
6
7
  import { compileWorkflowPlan } from "./workflow-planner.js";
7
8
  import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js";
@@ -19,7 +20,7 @@ const PATH_SEPARATOR_RE = /[\\/]/;
19
20
  const LINE_RE = /\r?\n/;
20
21
  /**
21
22
  * Config-driven `pipe` entrypoint. Package-owned defaults are the source of
22
- * truth; repo-local pipeline files are only scaffolding fixtures.
23
+ * truth; repo-local pipeline files are ignored by runtime loading.
23
24
  */
24
25
  function pipe(description, options = {}) {
25
26
  try {
@@ -192,9 +193,9 @@ function truncateMiddle(text, maxLength) {
192
193
  return `${text.slice(0, keep)}\n... truncated ...\n${text.slice(-keep)}`;
193
194
  }
194
195
  function createCliProgram() {
195
- const configuredPipeline = tryLoadConfiguredEntrypoints(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
196
+ const configuredPipeline = loadConfiguredEntrypoints(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
196
197
  const program = new Command();
197
- program.name("@oisincoveney/pipeline").description("Run and install the oisin pipeline").exitOverride();
198
+ program.name("@oisincoveney/pipeline").description("Run package-owned @oisincoveney/pipeline config").exitOverride();
198
199
  const runAction = async (descriptionParts, flags) => {
199
200
  await pipe(descriptionParts.join(" "), {
200
201
  entrypoint: flags.entrypoint,
@@ -202,9 +203,9 @@ function createCliProgram() {
202
203
  workflow: flags.workflow
203
204
  });
204
205
  };
205
- program.command("run").description("Run a workflow from .pipeline/pipeline.yaml").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--schedule <schedule>", "approved schedule YAML to execute").option("--workflow <workflow>", "workflow id from pipeline.yaml").action(runAction);
206
- program.command("pipe").description("Alias for run").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--schedule <schedule>", "approved schedule YAML to execute").option("--workflow <workflow>", "workflow id from pipeline.yaml").action(runAction);
207
- program.command("validate").description("Validate .pipeline/pipeline.yaml and compile the workflow plan").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--schedule <schedule>", "approved schedule YAML to validate").option("--strict", "fail when validation lint warnings are emitted").option("--no-lint", "skip validation lint warnings").option("--workflow <workflow>", "workflow id from pipeline.yaml").action((flags) => {
206
+ program.command("run").description("Run a workflow from package-owned @oisincoveney/pipeline config").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--schedule <schedule>", "approved schedule YAML to execute").option("--workflow <workflow>", "workflow id from package config").action(runAction);
207
+ program.command("pipe").description("Alias for run").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--schedule <schedule>", "approved schedule YAML to execute").option("--workflow <workflow>", "workflow id from package config").action(runAction);
208
+ program.command("validate").description("Validate package-owned @oisincoveney/pipeline config and compile the workflow plan").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--schedule <schedule>", "approved schedule YAML to validate").option("--strict", "fail when validation lint warnings are emitted").option("--no-lint", "skip validation lint warnings").option("--workflow <workflow>", "workflow id from package config").action((flags) => {
208
209
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
209
210
  const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
210
211
  const plan = flags.schedule ? compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(flags.schedule, "utf8"), flags.schedule), cwd).plan : compileWorkflowPlan(config, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint));
@@ -213,7 +214,7 @@ function createCliProgram() {
213
214
  if (flags.strict && warnings.length > 0) throw new Error(`Validation failed with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}.`);
214
215
  console.log(`OK: ${plan.workflowId} (${plan.topologicalOrder.length} nodes)`);
215
216
  });
216
- program.command("explain-plan").description("Explain workflow nodes, runners, gates, hooks, and artifacts").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--schedule <schedule>", "approved schedule YAML to explain").option("--workflow <workflow>", "workflow id from pipeline.yaml").action((flags) => {
217
+ program.command("explain-plan").description("Explain workflow nodes, runners, gates, hooks, and artifacts").option("--entrypoint <entrypoint>", "entrypoint alias from package config").option("--schedule <schedule>", "approved schedule YAML to explain").option("--workflow <workflow>", "workflow id from package config").action((flags) => {
217
218
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
218
219
  const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
219
220
  console.log(formatSelectedWorkflowPlan(config, cwd, flags));
@@ -247,6 +248,16 @@ function createCliProgram() {
247
248
  });
248
249
  console.log(result.map((item) => [`${item.host}: ${item.path}`, item.backupPath ? `backup=${item.backupPath}` : "backup=none"].join(" ")).join("\n"));
249
250
  });
251
+ gatewayCommand.command("reconcile").description("Apply the current workspace gateway backend inventory").action(async () => {
252
+ const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
253
+ const result = await reconcileGateway(loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd);
254
+ console.log([
255
+ `workspace=${result.workspacePath}`,
256
+ `config=${result.configPath}`,
257
+ `backends=${result.backendCount}`,
258
+ result.readinessFailures.length > 0 ? `readiness_failures=${result.readinessFailures.join("; ")}` : "readiness_failures=none"
259
+ ].join("\n"));
260
+ });
250
261
  gatewayCommand.command("local-start").description("Start a local ToolHive vMCP gateway for local mode").option("--detach", "reserved for future background startup", false).action(async (flags) => {
251
262
  if (flags.detach) throw new Error("Detached local gateway startup is not implemented.");
252
263
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
@@ -256,11 +267,8 @@ function createCliProgram() {
256
267
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
257
268
  console.log(await localGatewayStatus(cwd));
258
269
  });
259
- program.command("init").description("Initialize package-owned pipeline support without repo-local config").option("--overwrite", "replace existing pipeline scaffold files", false).action(async (flags) => {
260
- const result = await initPipelineProject({
261
- cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
262
- overwrite: flags.overwrite ?? false
263
- });
270
+ program.command("init").description("Initialize package-owned pipeline support without repo-local config").action(async () => {
271
+ const result = await initPipelineProject({ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd() });
264
272
  console.log(formatPipelineInitResult(result));
265
273
  });
266
274
  program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option("--host <host>", "host command set to install").choices([
@@ -282,13 +290,8 @@ function createCliProgram() {
282
290
  } });
283
291
  return program;
284
292
  }
285
- function tryLoadConfiguredEntrypoints(cwd) {
286
- try {
287
- return tryLoadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
288
- } catch (err) {
289
- if (err instanceof PipelineConfigError) return null;
290
- throw err;
291
- }
293
+ function loadConfiguredEntrypoints(cwd) {
294
+ return loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
292
295
  }
293
296
  function parseGatewayHostScope(value) {
294
297
  if (value === "project" || value === "global") return value;
@@ -324,7 +327,7 @@ function lintMissingFileReferences(config, projectRoot) {
324
327
  });
325
328
  }
326
329
  return refs.flatMap((ref) => {
327
- if (!ref.value || existsSync(resolve(projectRoot, ref.value))) return [];
330
+ if (!ref.value || standardOutputSchemaNameFromPath(ref.value) || existsSync(resolve(projectRoot, ref.value))) return [];
328
331
  return [{
329
332
  ruleId: "missing-file-reference",
330
333
  message: `${ref.path} references missing file '${ref.value}'`