@oisincoveney/pipeline 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/defaults/pipeline.yaml +25 -0
  2. package/dist/argo-graph.js +7 -7
  3. package/dist/argo-submit.d.ts +2 -4
  4. package/dist/argo-submit.js +80 -80
  5. package/dist/bench/eval-report.js +27 -0
  6. package/dist/cli/program.js +19 -3
  7. package/dist/cluster-doctor.js +89 -101
  8. package/dist/commands/bench-command.js +18 -0
  9. package/dist/config/defaults.js +9 -19
  10. package/dist/config/load.js +47 -37
  11. package/dist/config/schemas.d.ts +24 -7
  12. package/dist/config/schemas.js +20 -7
  13. package/dist/context/repo-map.js +203 -0
  14. package/dist/install-commands/opencode.js +10 -1
  15. package/dist/mcp/gateway-error.js +15 -0
  16. package/dist/mcp/gateway.js +119 -220
  17. package/dist/moka-global-config.js +20 -20
  18. package/dist/moka-submit.d.ts +6 -6
  19. package/dist/pipeline-init.js +18 -12
  20. package/dist/pipeline-runtime.js +592 -372
  21. package/dist/planning/compile.d.ts +8 -3
  22. package/dist/planning/compile.js +7 -7
  23. package/dist/planning/generate.d.ts +6 -1
  24. package/dist/planning/generate.js +29 -7
  25. package/dist/run-state/git-refs.js +124 -94
  26. package/dist/runner-command-contract.d.ts +6 -1
  27. package/dist/runner-command-contract.js +6 -5
  28. package/dist/runner-event-schema.d.ts +6 -6
  29. package/dist/runner-event-sink.js +37 -68
  30. package/dist/runner.d.ts +6 -1
  31. package/dist/runner.js +3 -3
  32. package/dist/runtime/agent-node/agent-node.js +218 -159
  33. package/dist/runtime/changed-files/changed-files.js +15 -27
  34. package/dist/runtime/changed-files/index.js +2 -0
  35. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  36. package/dist/runtime/gates/gates.js +45 -27
  37. package/dist/runtime/hooks/hooks.js +74 -29
  38. package/dist/runtime/local-scheduler.js +45 -0
  39. package/dist/runtime/opencode-server.js +32 -23
  40. package/dist/runtime/opencode-session-executor.js +101 -44
  41. package/dist/runtime/parallel-node/parallel-node.js +93 -75
  42. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
  43. package/dist/runtime/run-journal.js +21 -0
  44. package/dist/runtime/scheduler.js +122 -93
  45. package/dist/runtime/select-candidate/select-candidate.js +52 -24
  46. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  47. package/dist/runtime/services/command-executor-service.js +8 -0
  48. package/dist/runtime/services/config-io-service.js +42 -0
  49. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  50. package/dist/runtime/services/git-porcelain-service.js +38 -0
  51. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  52. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  53. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  54. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  55. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  56. package/dist/runtime/services/select-candidate-service.js +13 -0
  57. package/dist/runtime/services/worktree-service.js +18 -0
  58. package/dist/schedule/passes/candidates.js +17 -8
  59. package/docs/config-architecture.md +105 -0
  60. package/package.json +7 -2
@@ -1,23 +1,22 @@
1
+ import { PipelineMcpGatewayError } from "./gateway-error.js";
2
+ import { McpGatewayService, McpGatewayServiceLive } from "../runtime/services/mcp-gateway-service.js";
1
3
  import { resolveRepoLocalBackendSpecs } from "./repo-local-backends.js";
2
4
  import { renderToolHiveVmcpInventory } from "./toolhive-vmcp.js";
5
+ import { Effect } from "effect";
3
6
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
7
  import { dirname, join } from "node:path";
5
8
  import { homedir } from "node:os";
6
- import { execa } from "execa";
7
9
  //#region src/mcp/gateway.ts
8
10
  const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
9
11
  const DEFAULT_LOCAL_GATEWAY_URL = "http://127.0.0.1:4483/mcp";
10
12
  const LEGACY_OPENCODE_MCP_RE = /"mcp"\s*:\s*{(?!\s*"pipeline-gateway")/s;
11
13
  const LEGACY_PIPELINE_MCP_RE = /path:\s*\.mcp\.json|uvx\s+mcpm|mcpm\s+run/;
12
- var PipelineMcpGatewayError = class extends Error {
13
- constructor(message) {
14
- super(message);
15
- this.name = "PipelineMcpGatewayError";
16
- }
17
- };
18
14
  function profileNeedsMcpGateway(actor) {
19
15
  return (actor?.mcp_servers ?? []).length > 0;
20
16
  }
17
+ function runMcpGatewayEffect(program) {
18
+ return Effect.runPromise(Effect.provide(program, McpGatewayServiceLive));
19
+ }
21
20
  function gatewayServerForProfile(config, actor, env = process.env) {
22
21
  if (!(config && profileNeedsMcpGateway(actor))) return {};
23
22
  return { [PIPELINE_GATEWAY_SERVER_ID]: gatewayServer(config, env) };
@@ -88,128 +87,79 @@ function configureGatewayHosts(config, options) {
88
87
  };
89
88
  });
90
89
  }
91
- async function runGatewayDoctor(config, cwd) {
92
- const gateway = configuredGateway(config);
93
- const checks = [
94
- {
95
- detail: `${gateway.provider}/${gateway.mode}`,
96
- name: "gateway-config",
97
- passed: true
98
- },
99
- checkGatewayUrl(gateway),
100
- checkGatewayToken(gateway),
101
- ...gateway.mode === "local" ? [await checkThv(cwd)] : [],
102
- await checkGatewayHealth(gateway),
103
- await checkGatewayRequiredTools(gateway),
104
- checkLegacyDirectMcp(cwd)
105
- ];
106
- return {
107
- checks,
108
- passed: checks.every((check) => check.passed)
109
- };
90
+ function runGatewayDoctor(config, cwd) {
91
+ return runMcpGatewayEffect(Effect.gen(function* () {
92
+ const gateway = configuredGateway(config);
93
+ const checks = [
94
+ {
95
+ detail: `${gateway.provider}/${gateway.mode}`,
96
+ name: "gateway-config",
97
+ passed: true
98
+ },
99
+ checkGatewayUrl(gateway),
100
+ checkGatewayToken(gateway),
101
+ ...gateway.mode === "local" ? [yield* checkThv(cwd)] : [],
102
+ yield* checkGatewayHealth(gateway),
103
+ yield* checkGatewayRequiredTools(gateway),
104
+ checkLegacyDirectMcp(cwd)
105
+ ];
106
+ return {
107
+ checks,
108
+ passed: checks.every((check) => check.passed)
109
+ };
110
+ }));
110
111
  }
111
- async function startLocalGateway(config, cwd) {
112
- if (configuredGateway(config).mode !== "local") throw new PipelineMcpGatewayError("mcp gateway local-start is only valid when mcp_gateway.mode is local.");
113
- const result = await reconcileGateway(config, cwd);
114
- if (result.readinessFailures.length > 0) throw new PipelineMcpGatewayError(`Cannot start local MCP gateway; readiness failures: ${result.readinessFailures.join("; ")}`);
115
- await execa("thv", [
116
- "vmcp",
117
- "serve",
118
- "--config",
119
- result.configPath,
120
- "--host",
121
- "127.0.0.1",
122
- "--port",
123
- "4483"
124
- ], {
125
- cwd,
126
- env: await toolhiveEnv(cwd),
127
- stderr: "inherit",
128
- stdout: "inherit"
129
- });
112
+ function startLocalGateway(config, cwd) {
113
+ return runMcpGatewayEffect(Effect.gen(function* () {
114
+ if (configuredGateway(config).mode !== "local") return yield* Effect.fail(new PipelineMcpGatewayError("mcp gateway local-start is only valid when mcp_gateway.mode is local."));
115
+ const result = yield* reconcileGatewayEffect(config, cwd, process.env);
116
+ if (result.readinessFailures.length > 0) return yield* Effect.fail(new PipelineMcpGatewayError(`Cannot start local MCP gateway; readiness failures: ${result.readinessFailures.join("; ")}`));
117
+ yield* (yield* McpGatewayService).serveToolHiveVmcp(result.configPath, cwd);
118
+ }));
130
119
  }
131
- async function reconcileGateway(config, cwd, env = process.env) {
132
- const gateway = configuredGateway(config);
133
- if (gateway.provider !== "toolhive") throw new PipelineMcpGatewayError(`Unsupported MCP gateway provider '${gateway.provider}'.`);
134
- const workspacePath = env.PIPELINE_TARGET_PATH || cwd;
135
- const repoLocalBackends = resolveRepoLocalBackendSpecs(config, {
136
- cwd: workspacePath,
137
- env
138
- });
139
- const inventory = renderToolHiveVmcpInventory(config, {
140
- repoLocalBackends,
141
- toolHiveWorkloads: await listToolHiveGroupWorkloads(gateway.default_profile ?? "default", workspacePath)
142
- });
143
- const configPath = join(workspacePath, ".pipeline", "mcp-gateway", "vmcp.yaml");
144
- mkdirSync(dirname(configPath), { recursive: true });
145
- writeFileSync(configPath, inventory.yaml);
146
- await execa("thv", [
147
- "vmcp",
148
- "validate",
149
- "--config",
150
- configPath
151
- ], {
152
- cwd: workspacePath,
153
- env: await toolhiveEnv(workspacePath),
154
- stdin: "ignore"
155
- });
156
- return {
157
- backendCount: inventory.backends.length,
158
- configPath,
159
- readinessFailures: [...repoLocalBackends.filter((backend) => backend.enabled && !backend.readiness.ok).map((backend) => `${backend.id}: ${backend.readiness.reason}`), ...inventory.backends.filter((backend) => backend.enabled && backend.required && !backend.url).map((backend) => `${backend.name}: missing ToolHive workload`)],
160
- workspacePath
161
- };
120
+ function reconcileGateway(config, cwd, env = process.env) {
121
+ return runMcpGatewayEffect(reconcileGatewayEffect(config, cwd, env));
162
122
  }
163
- async function listToolHiveGroupWorkloads(group, cwd) {
164
- const result = await execa("thv", [
165
- "list",
166
- "--group",
167
- group,
168
- "--format",
169
- "json"
170
- ], {
171
- cwd,
172
- env: await toolhiveEnv(cwd),
173
- stdin: "ignore"
174
- });
175
- let parsed;
176
- const stdout = result.stdout.trim();
177
- if (!stdout) return [];
178
- try {
179
- parsed = JSON.parse(stdout);
180
- } catch {
181
- throw new PipelineMcpGatewayError("ToolHive list returned malformed JSON while reconciling MCP gateway workloads.");
182
- }
183
- if (!Array.isArray(parsed)) throw new PipelineMcpGatewayError("ToolHive list returned a non-array payload while reconciling MCP gateway workloads.");
184
- return parsed.flatMap((item) => {
185
- if (!item || typeof item.name !== "string") return [];
186
- return [{
187
- name: item.name,
188
- status: typeof item.status === "string" ? item.status : void 0,
189
- transport: toolHiveWorkloadTransport(item),
190
- url: typeof item.url === "string" ? item.url : void 0
191
- }];
123
+ function reconcileGatewayEffect(config, cwd, env) {
124
+ const gateway = configuredGateway(config);
125
+ if (gateway.provider !== "toolhive") return Effect.fail(new PipelineMcpGatewayError(`Unsupported MCP gateway provider '${gateway.provider}'.`));
126
+ return Effect.gen(function* () {
127
+ const service = yield* McpGatewayService;
128
+ const workspacePath = env.PIPELINE_TARGET_PATH || cwd;
129
+ const repoLocalBackends = resolveRepoLocalBackendSpecs(config, {
130
+ cwd: workspacePath,
131
+ env
132
+ });
133
+ const inventory = renderToolHiveVmcpInventory(config, {
134
+ repoLocalBackends,
135
+ toolHiveWorkloads: yield* service.listToolHiveGroupWorkloads(gateway.default_profile ?? "default", workspacePath)
136
+ });
137
+ const configPath = join(workspacePath, ".pipeline", "mcp-gateway", "vmcp.yaml");
138
+ mkdirSync(dirname(configPath), { recursive: true });
139
+ writeFileSync(configPath, inventory.yaml);
140
+ yield* service.validateToolHiveVmcp(configPath, workspacePath);
141
+ return {
142
+ backendCount: inventory.backends.length,
143
+ configPath,
144
+ readinessFailures: [...repoLocalBackends.filter((backend) => backend.enabled && !backend.readiness.ok).map((backend) => `${backend.id}: ${backend.readiness.reason}`), ...inventory.backends.filter((backend) => backend.enabled && backend.required && !backend.url).map((backend) => `${backend.name}: missing ToolHive workload`)],
145
+ workspacePath
146
+ };
192
147
  });
193
148
  }
194
- function toolHiveWorkloadTransport(item) {
195
- if (typeof item.transport_type === "string") return item.transport_type;
196
- if (typeof item.transport === "string") return item.transport;
197
- }
198
- async function localGatewayStatus(cwd) {
199
- return (await execa("thv", ["list"], {
200
- cwd,
201
- env: await toolhiveEnv(cwd)
202
- })).stdout.trim();
149
+ function localGatewayStatus(cwd) {
150
+ return runMcpGatewayEffect(Effect.gen(function* () {
151
+ return yield* (yield* McpGatewayService).localGatewayStatus(cwd);
152
+ }));
203
153
  }
204
- async function checkGatewayRequiredTools(gateway) {
154
+ function checkGatewayRequiredTools(gateway) {
205
155
  const requiredPrefixes = requiredGatewayToolPrefixes(gateway);
206
- if (requiredPrefixes.length === 0) return {
156
+ if (requiredPrefixes.length === 0) return Effect.succeed({
207
157
  detail: "no required tools declared",
208
158
  name: "gateway-required-tools",
209
159
  passed: true
210
- };
211
- try {
212
- const tools = await listGatewayTools(gateway);
160
+ });
161
+ return Effect.gen(function* () {
162
+ const tools = yield* listGatewayTools(gateway);
213
163
  const missing = requiredPrefixes.filter((prefix) => !tools.some((tool) => tool === prefix || tool.startsWith(`${prefix}_`)));
214
164
  return missing.length === 0 ? {
215
165
  detail: `found: ${requiredPrefixes.join(", ")}`,
@@ -220,54 +170,45 @@ async function checkGatewayRequiredTools(gateway) {
220
170
  name: "gateway-required-tools",
221
171
  passed: false
222
172
  };
223
- } catch (err) {
224
- return {
225
- detail: err instanceof Error ? err.message : String(err),
226
- name: "gateway-required-tools",
227
- passed: false
228
- };
229
- }
173
+ }).pipe(Effect.catchAll((error) => Effect.succeed({
174
+ detail: error instanceof Error ? error.message : String(error),
175
+ name: "gateway-required-tools",
176
+ passed: false
177
+ })));
230
178
  }
231
179
  function requiredGatewayToolPrefixes(gateway) {
232
180
  return [...new Set(Object.values(gateway.backends).filter((backend) => backend.required).flatMap((backend) => backend.tool_prefixes))].sort();
233
181
  }
234
- async function listGatewayTools(gateway) {
182
+ function listGatewayTools(gateway) {
235
183
  const url = gatewayUrl(gateway);
236
- await callGatewayRpc(gateway, url, {
237
- id: 1,
238
- jsonrpc: "2.0",
239
- method: "initialize",
240
- params: {
241
- capabilities: {},
242
- clientInfo: {
243
- name: "@oisincoveney/pipeline",
244
- version: "1"
245
- },
246
- protocolVersion: "2025-06-18"
247
- }
184
+ return Effect.gen(function* () {
185
+ yield* callGatewayRpc(gateway, url, {
186
+ id: 1,
187
+ jsonrpc: "2.0",
188
+ method: "initialize",
189
+ params: {
190
+ capabilities: {},
191
+ clientInfo: {
192
+ name: "@oisincoveney/pipeline",
193
+ version: "1"
194
+ },
195
+ protocolVersion: "2025-06-18"
196
+ }
197
+ });
198
+ const tools = (yield* callGatewayRpc(gateway, url, {
199
+ id: 2,
200
+ jsonrpc: "2.0",
201
+ method: "tools/list",
202
+ params: {}
203
+ })).result?.tools;
204
+ if (!Array.isArray(tools)) return yield* Effect.fail(new PipelineMcpGatewayError("Malformed tools/list response."));
205
+ return tools.flatMap((tool) => tool && typeof tool.name === "string" ? [tool.name] : []);
248
206
  });
249
- const tools = (await callGatewayRpc(gateway, url, {
250
- id: 2,
251
- jsonrpc: "2.0",
252
- method: "tools/list",
253
- params: {}
254
- })).result?.tools;
255
- if (!Array.isArray(tools)) throw new PipelineMcpGatewayError("Malformed tools/list response.");
256
- return tools.flatMap((tool) => tool && typeof tool.name === "string" ? [tool.name] : []);
257
- }
258
- async function callGatewayRpc(gateway, url, body) {
259
- const authorization = process.env[gateway.authorization_env];
260
- const response = await fetch(url, {
261
- body: JSON.stringify(body),
262
- headers: {
263
- Accept: "application/json",
264
- "Content-Type": "application/json",
265
- ...authorization ? { Authorization: authorization } : {}
266
- },
267
- method: "POST"
207
+ }
208
+ function callGatewayRpc(gateway, url, body) {
209
+ return Effect.gen(function* () {
210
+ return yield* (yield* McpGatewayService).callGatewayRpc(url, body, process.env[gateway.authorization_env]);
268
211
  });
269
- if (!response.ok) throw new PipelineMcpGatewayError(`Gateway MCP request failed: HTTP ${response.status}.`);
270
- return await response.json();
271
212
  }
272
213
  function selectedGatewayHosts(host) {
273
214
  return host === "all" ? ["opencode"] : [host];
@@ -314,86 +255,44 @@ function gatewayAuthorizationHeader(gateway) {
314
255
  function gatewayOpenCodeHeaders(gateway) {
315
256
  return { Authorization: gatewayAuthorizationHeader(gateway) };
316
257
  }
317
- async function checkThv(cwd) {
318
- try {
319
- await execa("thv", ["version"], {
320
- cwd,
321
- env: await toolhiveEnv(cwd),
322
- stdin: "ignore"
323
- });
258
+ function checkThv(cwd) {
259
+ return Effect.gen(function* () {
260
+ yield* (yield* McpGatewayService).runToolHiveVersion(cwd);
324
261
  return {
325
262
  detail: "available",
326
263
  name: "toolhive",
327
264
  passed: true
328
265
  };
329
- } catch (err) {
330
- const error = err;
331
- return {
332
- detail: (error.shortMessage || error.stderr || "not available").trim(),
333
- name: "toolhive",
334
- passed: false
335
- };
336
- }
337
- }
338
- async function toolhiveEnv(cwd) {
339
- if (process.env.DOCKER_HOST) return process.env;
340
- const dockerHost = await activeDockerHost(cwd);
341
- return dockerHost ? {
342
- ...process.env,
343
- DOCKER_HOST: dockerHost
344
- } : process.env;
345
- }
346
- async function activeDockerHost(cwd) {
347
- try {
348
- const result = await execa("docker", ["context", "inspect"], {
349
- cwd,
350
- stdin: "ignore"
351
- });
352
- const host = JSON.parse(result.stdout)[0]?.Endpoints?.docker?.Host;
353
- return typeof host === "string" && host.length > 0 ? host : void 0;
354
- } catch {
355
- return;
356
- }
266
+ }).pipe(Effect.catchAll((error) => Effect.succeed({
267
+ detail: error.message || "not available",
268
+ name: "toolhive",
269
+ passed: false
270
+ })));
357
271
  }
358
- async function checkGatewayHealth(gateway) {
272
+ function checkGatewayHealth(gateway) {
359
273
  let url;
360
274
  try {
361
275
  url = gatewayUrl(gateway);
362
276
  } catch (err) {
363
- return {
277
+ return Effect.succeed({
364
278
  detail: err instanceof Error ? err.message : String(err),
365
279
  name: "gateway-health",
366
280
  passed: false
367
- };
281
+ });
368
282
  }
369
- try {
370
- const response = await firstHealthyGatewayResponse(url, gateway);
283
+ return Effect.gen(function* () {
284
+ const response = yield* (yield* McpGatewayService).firstHealthyGatewayResponse(gatewayHealthUrls(url), process.env[gateway.authorization_env]);
371
285
  const passed = Boolean(response);
372
286
  return {
373
287
  detail: response ? `HTTP ${response.status} ${response.url}` : "gateway endpoint did not report healthy",
374
288
  name: "gateway-health",
375
289
  passed
376
290
  };
377
- } catch (err) {
378
- return {
379
- detail: err instanceof Error ? err.message : String(err),
380
- name: "gateway-health",
381
- passed: false
382
- };
383
- }
384
- }
385
- async function firstHealthyGatewayResponse(url, gateway) {
386
- const authorization = process.env[gateway.authorization_env];
387
- for (const healthUrl of gatewayHealthUrls(url)) {
388
- const response = await fetch(healthUrl, {
389
- headers: {
390
- Accept: "application/json, text/event-stream",
391
- ...authorization ? { Authorization: authorization } : {}
392
- },
393
- method: "GET"
394
- });
395
- if (response.status >= 200 && response.status < 300 || response.status === 405) return response;
396
- }
291
+ }).pipe(Effect.catchAll((error) => Effect.succeed({
292
+ detail: error instanceof Error ? error.message : String(error),
293
+ name: "gateway-health",
294
+ passed: false
295
+ })));
397
296
  }
398
297
  function gatewayHealthUrls(url) {
399
298
  const urls = [];
@@ -1,8 +1,8 @@
1
1
  import { PipelineConfigError } from "./config/schemas.js";
2
+ import { ConfigIoService, runConfigIoSync } from "./runtime/services/config-io-service.js";
2
3
  import "./config.js";
3
- import { parseDocument } from "yaml";
4
4
  import { z } from "zod";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { Effect } from "effect";
6
6
  import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  //#region src/moka-global-config.ts
@@ -30,27 +30,27 @@ function mokaGlobalConfigPath(homeDir = homedir()) {
30
30
  }
31
31
  function loadMokaGlobalConfig() {
32
32
  const configPath = mokaGlobalConfigPath();
33
- if (!existsSync(configPath)) return null;
34
- return parseMokaGlobalConfig(readFileSync(configPath, "utf8"), configPath);
33
+ return runConfigIoSync(Effect.gen(function* () {
34
+ const source = yield* (yield* ConfigIoService).readOptionalText(configPath);
35
+ return source === null ? null : yield* parseMokaGlobalConfigEffect(source, configPath);
36
+ }));
35
37
  }
36
38
  function parseMokaGlobalConfig(source, sourcePath) {
37
- const document = parseDocument(source, {
38
- prettyErrors: false,
39
- uniqueKeys: true
39
+ return runConfigIoSync(parseMokaGlobalConfigEffect(source, sourcePath));
40
+ }
41
+ function parseMokaGlobalConfigEffect(source, sourcePath) {
42
+ return Effect.gen(function* () {
43
+ const yaml = yield* (yield* ConfigIoService).parseYaml(source, sourcePath);
44
+ const parsed = mokaGlobalConfigSchema.safeParse(yaml);
45
+ if (!parsed.success) {
46
+ const issues = parsed.error.issues.map((issue) => ({
47
+ path: issue.path.join("."),
48
+ message: issue.message
49
+ }));
50
+ return yield* Effect.fail(new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", [`Invalid ${sourcePath}:`, ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues));
51
+ }
52
+ return parsed.data;
40
53
  });
41
- if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
42
- message: err.message,
43
- path: sourcePath
44
- })));
45
- const parsed = mokaGlobalConfigSchema.safeParse(document.toJS());
46
- if (!parsed.success) {
47
- const issues = parsed.error.issues.map((issue) => ({
48
- path: issue.path.join("."),
49
- message: issue.message
50
- }));
51
- throw new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", [`Invalid ${sourcePath}:`, ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
52
- }
53
- return parsed.data;
54
54
  }
55
55
  //#endregion
56
56
  export { MOKA_GLOBAL_CONFIG_PATH, loadMokaGlobalConfig, mokaGlobalConfigPath, mokaGlobalConfigSchema, parseMokaGlobalConfig };
@@ -5,13 +5,13 @@ import { z } from "zod";
5
5
  //#region src/moka-submit.d.ts
6
6
  declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
7
7
  "workflow.start": "workflow.start";
8
- "node.finish": "node.finish";
9
- "node.start": "node.start";
10
8
  "workflow.success": "workflow.success";
11
9
  "workflow.failure": "workflow.failure";
12
10
  "workflow.complete": "workflow.complete";
11
+ "node.start": "node.start";
13
12
  "node.success": "node.success";
14
13
  "node.error": "node.error";
14
+ "node.finish": "node.finish";
15
15
  "gate.failure": "gate.failure";
16
16
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
17
17
  failure: z.ZodDefault<z.ZodEnum<{
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
94
94
  }, z.core.$strict>>;
95
95
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
96
96
  "workflow.start": "workflow.start";
97
- "node.finish": "node.finish";
98
- "node.start": "node.start";
99
97
  "workflow.success": "workflow.success";
100
98
  "workflow.failure": "workflow.failure";
101
99
  "workflow.complete": "workflow.complete";
100
+ "node.start": "node.start";
102
101
  "node.success": "node.success";
103
102
  "node.error": "node.error";
103
+ "node.finish": "node.finish";
104
104
  "gate.failure": "gate.failure";
105
105
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
106
106
  failure: z.ZodDefault<z.ZodEnum<{
@@ -206,13 +206,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
206
206
  }, z.core.$strict>>;
207
207
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
208
208
  "workflow.start": "workflow.start";
209
- "node.finish": "node.finish";
210
- "node.start": "node.start";
211
209
  "workflow.success": "workflow.success";
212
210
  "workflow.failure": "workflow.failure";
213
211
  "workflow.complete": "workflow.complete";
212
+ "node.start": "node.start";
214
213
  "node.success": "node.success";
215
214
  "node.error": "node.error";
215
+ "node.finish": "node.finish";
216
216
  "gate.failure": "gate.failure";
217
217
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
218
218
  failure: z.ZodDefault<z.ZodEnum<{
@@ -2,7 +2,7 @@ import { installCommands } from "./install-commands.js";
2
2
  import { execa } from "execa";
3
3
  //#region src/pipeline-init.ts
4
4
  const DEFAULT_SKILL_INSTALL_SOURCE = "oisin-ee/skills";
5
- const DEFAULT_SKILL_INSTALL_ARGS = [
5
+ const SKILL_INSTALL_AGENT_ARGS = [
6
6
  "--agent",
7
7
  "opencode",
8
8
  "--agent",
@@ -11,17 +11,19 @@ const DEFAULT_SKILL_INSTALL_ARGS = [
11
11
  "claude-code",
12
12
  "--skill",
13
13
  "*",
14
- "--yes",
15
- "--copy"
14
+ "--yes"
16
15
  ];
17
- async function installDefaultSkills(cwd) {
16
+ function skillInstallArgs(scope) {
17
+ return scope === "personal" ? [...SKILL_INSTALL_AGENT_ARGS, "--global"] : [...SKILL_INSTALL_AGENT_ARGS, "--copy"];
18
+ }
19
+ async function installDefaultSkills(cwd, scope) {
18
20
  try {
19
21
  await execa("npx", [
20
22
  "--yes",
21
23
  "skills",
22
24
  "add",
23
25
  DEFAULT_SKILL_INSTALL_SOURCE,
24
- ...DEFAULT_SKILL_INSTALL_ARGS
26
+ ...skillInstallArgs(scope)
25
27
  ], {
26
28
  cwd,
27
29
  stdio: "inherit"
@@ -33,17 +35,21 @@ async function installDefaultSkills(cwd) {
33
35
  }
34
36
  async function initPipelineProject(options = {}) {
35
37
  const cwd = options.cwd ?? process.cwd();
36
- await (options.skillInstaller ?? installDefaultSkills)(cwd);
37
- return { files: (await installCommands({
38
- cwd,
39
- force: true,
40
- host: "all"
41
- })).items.map((item) => item.path) };
38
+ const scope = options.scope ?? "project";
39
+ await (options.skillInstaller ?? ((target) => installDefaultSkills(target, scope)))(cwd);
40
+ return {
41
+ files: (await installCommands({
42
+ cwd,
43
+ force: true,
44
+ host: "all"
45
+ })).items.map((item) => item.path),
46
+ scope
47
+ };
42
48
  }
43
49
  function formatPipelineInitResult(result) {
44
50
  return [
45
51
  "Initialized package-owned pipeline support:",
46
- "installed default skills",
52
+ result.scope === "personal" ? "installed default skills at user/global scope (inherited by every repo, no per-repo copy)" : "installed default skills (repo-local copy)",
47
53
  ...result.files.map((path) => `generated ${path}`),
48
54
  "no repo-local pipeline config files were created"
49
55
  ].join("\n");