@oisincoveney/pipeline 1.10.2 → 1.10.3
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 +20 -8
- package/dist/config.d.ts +35 -8
- package/dist/config.js +81 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -8
- package/dist/install-commands.js +12 -3
- package/dist/mcp/launch-plan.js +22 -14
- package/dist/pipeline-init.js +38 -2
- package/dist/pipeline-runtime.js +1 -0
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +27 -7
- package/dist/schedule-planner.d.ts +1683 -0
- package/dist/schedule-planner.js +289 -0
- package/docs/config-architecture.md +7 -2
- package/docs/mcp-gateway.md +2 -2
- package/docs/mcp-host-isolation.md +19 -13
- package/docs/operator-guide.md +24 -11
- package/docs/slash-command-adapter-contract.md +6 -3
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -72,10 +72,17 @@ Inspect the execution plan before running:
|
|
|
72
72
|
pipe explain-plan
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
Generate the default schedule artifact:
|
|
76
76
|
|
|
77
77
|
```shell
|
|
78
|
-
pipe
|
|
78
|
+
pipe "Implement PIPE-123 user-facing behavior"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This writes `.pipeline/runs/<runId>/schedule.yaml` and stops for approval. Run
|
|
82
|
+
the approved artifact explicitly:
|
|
83
|
+
|
|
84
|
+
```shell
|
|
85
|
+
pipe run --schedule .pipeline/runs/<runId>/schedule.yaml "Implement PIPE-123 user-facing behavior"
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
Run a read-only repository inspection:
|
|
@@ -96,9 +103,9 @@ Run an epic drain workflow:
|
|
|
96
103
|
pipe epic PIPE-31
|
|
97
104
|
```
|
|
98
105
|
|
|
99
|
-
The `epic` entrypoint
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
The `epic` entrypoint generates a specialized schedule for the epic, writes the
|
|
107
|
+
schedule artifact, and stops. Execution uses the same `pipe run --schedule`
|
|
108
|
+
approval boundary as `pipe`.
|
|
102
109
|
|
|
103
110
|
The `pipe` binary also accepts the task directly:
|
|
104
111
|
|
|
@@ -201,8 +208,9 @@ workflows:
|
|
|
201
208
|
```
|
|
202
209
|
|
|
203
210
|
Projects can also declare `entrypoints` in `.pipeline/pipeline.yaml` to expose
|
|
204
|
-
stable app or CLI names that resolve to workflows. Direct
|
|
205
|
-
remains available and takes precedence over `--entrypoint`
|
|
211
|
+
stable app or CLI names that resolve to workflows or schedule policies. Direct
|
|
212
|
+
`--workflow` selection remains available and takes precedence over `--entrypoint`
|
|
213
|
+
when both are set.
|
|
206
214
|
|
|
207
215
|
The default scaffold includes a full research, red, green, verify, learn
|
|
208
216
|
workflow. See `docs/config-architecture.md` for a complete example and the host
|
|
@@ -347,7 +355,7 @@ runners:
|
|
|
347
355
|
|
|
348
356
|
## App-Facing API
|
|
349
357
|
|
|
350
|
-
External apps can import the stable config, planner, and runtime surfaces
|
|
358
|
+
External apps can import the stable config, planner, schedule, and runtime surfaces
|
|
351
359
|
without deep-importing private source paths:
|
|
352
360
|
|
|
353
361
|
```ts
|
|
@@ -356,6 +364,10 @@ import {
|
|
|
356
364
|
parsePipelineConfigParts,
|
|
357
365
|
} from "@oisincoveney/pipeline/config";
|
|
358
366
|
import { compileWorkflowPlan } from "@oisincoveney/pipeline/planner";
|
|
367
|
+
import {
|
|
368
|
+
compileScheduleArtifact,
|
|
369
|
+
parseScheduleArtifact,
|
|
370
|
+
} from "@oisincoveney/pipeline/schedule";
|
|
359
371
|
import {
|
|
360
372
|
runPipelineFromConfig,
|
|
361
373
|
type PipelineRuntimeResult,
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ declare const RUNNER_TYPES: readonly ["codex", "opencode", "command"];
|
|
|
8
8
|
declare const NODE_KINDS: readonly ["agent", "command", "builtin", "group", "parallel", "workflow"];
|
|
9
9
|
declare const HOOK_EVENTS: readonly ["workflow.start", "workflow.success", "workflow.failure", "workflow.complete", "node.start", "node.success", "node.error", "gate.failure"];
|
|
10
10
|
declare const GATE_KINDS: readonly ["acceptance", "artifact", "builtin", "changed_files", "command", "json_schema", "verdict"];
|
|
11
|
+
declare const SCHEDULE_BASELINES: readonly ["epic", "pipe"];
|
|
11
12
|
type PipelineConfigErrorCode = "PIPELINE_CONFIG_LEGACY_UNSUPPORTED" | "PIPELINE_CONFIG_MISSING" | "PIPELINE_CONFIG_PARSE_ERROR" | "PIPELINE_CONFIG_VALIDATION_ERROR";
|
|
12
13
|
interface PipelineConfigIssue {
|
|
13
14
|
message: string;
|
|
@@ -129,15 +130,31 @@ type ChildWorkflowNode = WorkflowNodeBase & {
|
|
|
129
130
|
worktree_root?: string;
|
|
130
131
|
};
|
|
131
132
|
type WorkflowNode = AgentWorkflowNode | CommandWorkflowNode | BuiltinWorkflowNode | GroupWorkflowNode | ParallelWorkflowNode | ChildWorkflowNode;
|
|
133
|
+
declare const workflowSchema: z.ZodObject<{
|
|
134
|
+
description: z.ZodOptional<z.ZodString>;
|
|
135
|
+
execution: z.ZodOptional<z.ZodObject<{
|
|
136
|
+
fail_fast: z.ZodOptional<z.ZodBoolean>;
|
|
137
|
+
max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
|
|
138
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
139
|
+
}, z.core.$strict>>;
|
|
140
|
+
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
141
|
+
nodes: z.ZodArray<z.ZodType<WorkflowNode, unknown, z.core.$ZodTypeInternals<WorkflowNode, unknown>>>;
|
|
142
|
+
}, z.core.$strict>;
|
|
132
143
|
declare const configSchema: z.ZodObject<{
|
|
133
144
|
default_workflow: z.ZodString;
|
|
134
|
-
entrypoints: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
145
|
+
entrypoints: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
135
146
|
description: z.ZodOptional<z.ZodString>;
|
|
136
147
|
task_context: z.ZodOptional<z.ZodObject<{
|
|
137
148
|
type: z.ZodString;
|
|
138
149
|
}, z.core.$loose>>;
|
|
139
150
|
workflow: z.ZodString;
|
|
140
|
-
}, z.core.$strict
|
|
151
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
152
|
+
description: z.ZodOptional<z.ZodString>;
|
|
153
|
+
task_context: z.ZodOptional<z.ZodObject<{
|
|
154
|
+
type: z.ZodString;
|
|
155
|
+
}, z.core.$loose>>;
|
|
156
|
+
schedule: z.ZodString;
|
|
157
|
+
}, z.core.$strict>]>>>;
|
|
141
158
|
hooks: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
142
159
|
builtin: z.ZodOptional<z.ZodString>;
|
|
143
160
|
command: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -205,10 +222,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
205
222
|
}, z.core.$strict>>;
|
|
206
223
|
output: z.ZodOptional<z.ZodObject<{
|
|
207
224
|
format: z.ZodEnum<{
|
|
225
|
+
json_schema: "json_schema";
|
|
208
226
|
text: "text";
|
|
209
227
|
json: "json";
|
|
210
228
|
jsonl: "jsonl";
|
|
211
|
-
json_schema: "json_schema";
|
|
212
229
|
}>;
|
|
213
230
|
repair: z.ZodOptional<z.ZodObject<{
|
|
214
231
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -221,6 +238,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
221
238
|
runner: z.ZodString;
|
|
222
239
|
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
223
240
|
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
241
|
+
task: "task";
|
|
224
242
|
read: "read";
|
|
225
243
|
list: "list";
|
|
226
244
|
grep: "grep";
|
|
@@ -228,7 +246,6 @@ declare const configSchema: z.ZodObject<{
|
|
|
228
246
|
bash: "bash";
|
|
229
247
|
edit: "edit";
|
|
230
248
|
write: "write";
|
|
231
|
-
task: "task";
|
|
232
249
|
}>>>;
|
|
233
250
|
}, z.core.$strict>>>;
|
|
234
251
|
rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -248,14 +265,15 @@ declare const configSchema: z.ZodObject<{
|
|
|
248
265
|
disabled: "disabled";
|
|
249
266
|
}>>>;
|
|
250
267
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
268
|
+
json_schema: "json_schema";
|
|
251
269
|
text: "text";
|
|
252
270
|
json: "json";
|
|
253
271
|
jsonl: "jsonl";
|
|
254
|
-
json_schema: "json_schema";
|
|
255
272
|
}>>>;
|
|
256
273
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
257
274
|
skills: z.ZodOptional<z.ZodBoolean>;
|
|
258
275
|
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
276
|
+
task: "task";
|
|
259
277
|
read: "read";
|
|
260
278
|
list: "list";
|
|
261
279
|
grep: "grep";
|
|
@@ -263,7 +281,6 @@ declare const configSchema: z.ZodObject<{
|
|
|
263
281
|
bash: "bash";
|
|
264
282
|
edit: "edit";
|
|
265
283
|
write: "write";
|
|
266
|
-
task: "task";
|
|
267
284
|
}>>>;
|
|
268
285
|
}, z.core.$strict>;
|
|
269
286
|
command: z.ZodOptional<z.ZodString>;
|
|
@@ -275,6 +292,15 @@ declare const configSchema: z.ZodObject<{
|
|
|
275
292
|
opencode: "opencode";
|
|
276
293
|
}>;
|
|
277
294
|
}, z.core.$strict>>>;
|
|
295
|
+
schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
296
|
+
description: z.ZodOptional<z.ZodString>;
|
|
297
|
+
baseline: z.ZodEnum<{
|
|
298
|
+
pipe: "pipe";
|
|
299
|
+
epic: "epic";
|
|
300
|
+
}>;
|
|
301
|
+
max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
|
|
302
|
+
planner_profile: z.ZodOptional<z.ZodString>;
|
|
303
|
+
}, z.core.$strict>>>;
|
|
278
304
|
skills: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
279
305
|
path: z.ZodString;
|
|
280
306
|
}, z.core.$strict>>>;
|
|
@@ -298,6 +324,7 @@ type RunnerType = (typeof RUNNER_TYPES)[number];
|
|
|
298
324
|
type WorkflowNodeKind = (typeof NODE_KINDS)[number];
|
|
299
325
|
type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
300
326
|
type GateKind = (typeof GATE_KINDS)[number];
|
|
327
|
+
type ScheduleBaseline = (typeof SCHEDULE_BASELINES)[number];
|
|
301
328
|
interface PipelineConfigParts {
|
|
302
329
|
pipeline: string;
|
|
303
330
|
profiles: string;
|
|
@@ -310,6 +337,6 @@ declare function loadPipelineConfig(projectRoot: string, options?: PipelineConfi
|
|
|
310
337
|
declare function tryLoadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig | null;
|
|
311
338
|
declare function parsePipelineConfigYaml(source: string, sourcePath?: string, projectRoot?: string): PipelineConfig;
|
|
312
339
|
declare function parsePipelineConfigParts(sources: PipelineConfigParts, projectRoot?: string, sourcePaths?: PipelineConfigParts, options?: PipelineConfigValidationOptions): PipelineConfig;
|
|
313
|
-
declare function validatePipelineConfig(
|
|
340
|
+
declare function validatePipelineConfig(rawConfig: PipelineConfig, projectRoot?: string, options?: PipelineConfigValidationOptions): PipelineConfig;
|
|
314
341
|
//#endregion
|
|
315
|
-
export { GateKind, HookEvent, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, WorkflowNodeKind, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig };
|
|
342
|
+
export { GateKind, HookEvent, PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfig, PipelineConfigError, PipelineConfigErrorCode, PipelineConfigIssue, PipelineConfigParts, PipelineConfigValidationOptions, RUNNERS_CONFIG_PATH, RunnerType, ScheduleBaseline, WorkflowNodeKind, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
|
package/dist/config.js
CHANGED
|
@@ -54,6 +54,7 @@ const RETRY_REASONS = [
|
|
|
54
54
|
"gate_failure",
|
|
55
55
|
"timeout"
|
|
56
56
|
];
|
|
57
|
+
const SCHEDULE_BASELINES = ["epic", "pipe"];
|
|
57
58
|
var PipelineConfigError = class extends Error {
|
|
58
59
|
code;
|
|
59
60
|
issues;
|
|
@@ -258,10 +259,16 @@ const hookSchema = z.object({
|
|
|
258
259
|
trusted: z.boolean().optional()
|
|
259
260
|
}).strict();
|
|
260
261
|
const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
|
|
261
|
-
const
|
|
262
|
+
const entrypointBaseSchema = z.object({
|
|
262
263
|
description: z.string().optional(),
|
|
263
|
-
task_context: taskContextResolverSchema.optional()
|
|
264
|
-
|
|
264
|
+
task_context: taskContextResolverSchema.optional()
|
|
265
|
+
});
|
|
266
|
+
const entrypointSchema = z.union([entrypointBaseSchema.extend({ workflow: z.string() }).strict(), entrypointBaseSchema.extend({ schedule: z.string() }).strict()]);
|
|
267
|
+
const schedulePolicySchema = z.object({
|
|
268
|
+
description: z.string().optional(),
|
|
269
|
+
baseline: z.enum(SCHEDULE_BASELINES),
|
|
270
|
+
max_parallel_nodes: z.number().int().positive().optional(),
|
|
271
|
+
planner_profile: z.string().optional()
|
|
265
272
|
}).strict();
|
|
266
273
|
const workflowNodeBaseSchema = z.object({
|
|
267
274
|
artifacts: z.array(artifactSchema).optional(),
|
|
@@ -321,11 +328,12 @@ const pipelineFileSchema = z.object({
|
|
|
321
328
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
322
329
|
hooks: strictRecord(hookSchema).default({}),
|
|
323
330
|
orchestrator: orchestratorSchema,
|
|
331
|
+
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
324
332
|
task_context: taskContextResolverSchema.optional(),
|
|
325
333
|
workflows: strictRecord(workflowSchema).default({}),
|
|
326
334
|
version: z.literal(1)
|
|
327
335
|
}).strict();
|
|
328
|
-
z.object({
|
|
336
|
+
const configSchema = z.object({
|
|
329
337
|
default_workflow: z.string(),
|
|
330
338
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
331
339
|
hooks: strictRecord(hookSchema).default({}),
|
|
@@ -334,11 +342,70 @@ z.object({
|
|
|
334
342
|
profiles: strictRecord(profileSchema).default({}),
|
|
335
343
|
rules: strictRecord(pathRefSchema).default({}),
|
|
336
344
|
runners: strictRecord(runnerSchema).default({}),
|
|
345
|
+
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
337
346
|
skills: strictRecord(pathRefSchema).default({}),
|
|
338
347
|
task_context: taskContextResolverSchema.optional(),
|
|
339
348
|
version: z.literal(1),
|
|
340
349
|
workflows: strictRecord(workflowSchema).default({})
|
|
341
|
-
}).strict();
|
|
350
|
+
}).strict().superRefine(validateConfigReferences);
|
|
351
|
+
function validateConfigReferences(config, ctx) {
|
|
352
|
+
addConfigSchemaIssues(ctx, configReferenceIssues(config));
|
|
353
|
+
}
|
|
354
|
+
function configReferenceIssues(config) {
|
|
355
|
+
return [
|
|
356
|
+
...missingRegistryReferenceIssue({
|
|
357
|
+
message: (_field, value) => `default workflow '${value}' is not declared`,
|
|
358
|
+
path: ["default_workflow"],
|
|
359
|
+
registry: config.workflows,
|
|
360
|
+
value: config.default_workflow
|
|
361
|
+
}),
|
|
362
|
+
...registryReferenceIssues("entrypoints", config.entrypoints, [{
|
|
363
|
+
field: "workflow",
|
|
364
|
+
message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing workflow '${value}'`,
|
|
365
|
+
read: (entrypoint) => "workflow" in entrypoint ? entrypoint.workflow : void 0,
|
|
366
|
+
registry: config.workflows
|
|
367
|
+
}, {
|
|
368
|
+
field: "schedule",
|
|
369
|
+
message: (entrypointId, value) => `entrypoint '${entrypointId}' references missing schedule '${value}'`,
|
|
370
|
+
read: (entrypoint) => "schedule" in entrypoint ? entrypoint.schedule : void 0,
|
|
371
|
+
registry: config.schedules
|
|
372
|
+
}]),
|
|
373
|
+
...registryReferenceIssues("schedules", config.schedules, [{
|
|
374
|
+
field: "planner_profile",
|
|
375
|
+
message: (scheduleId, value) => `schedule '${scheduleId}' references missing planner profile '${value}'`,
|
|
376
|
+
read: (schedule) => schedule.planner_profile,
|
|
377
|
+
registry: config.profiles
|
|
378
|
+
}])
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
function registryReferenceIssues(registryPath, records, rules) {
|
|
382
|
+
return Object.entries(records).flatMap(([recordId, record]) => rules.flatMap((rule) => missingRegistryReferenceIssue({
|
|
383
|
+
message: (_field, value) => rule.message(recordId, value),
|
|
384
|
+
path: [
|
|
385
|
+
registryPath,
|
|
386
|
+
recordId,
|
|
387
|
+
rule.field
|
|
388
|
+
],
|
|
389
|
+
registry: rule.registry,
|
|
390
|
+
value: rule.read(record)
|
|
391
|
+
})));
|
|
392
|
+
}
|
|
393
|
+
function missingRegistryReferenceIssue({ message, path, registry, value }) {
|
|
394
|
+
return value && !Object.hasOwn(registry, value) ? [{
|
|
395
|
+
message: message(String(path.at(-1)), value),
|
|
396
|
+
path
|
|
397
|
+
}] : [];
|
|
398
|
+
}
|
|
399
|
+
function addConfigSchemaIssues(ctx, issues) {
|
|
400
|
+
for (const issue of issues) addConfigSchemaIssue(ctx, issue.path, issue.message);
|
|
401
|
+
}
|
|
402
|
+
function addConfigSchemaIssue(ctx, path, message) {
|
|
403
|
+
ctx.addIssue({
|
|
404
|
+
code: "custom",
|
|
405
|
+
path,
|
|
406
|
+
message
|
|
407
|
+
});
|
|
408
|
+
}
|
|
342
409
|
function loadPipelineConfig(projectRoot, options = {}) {
|
|
343
410
|
const missing = [
|
|
344
411
|
PIPELINE_CONFIG_PATH,
|
|
@@ -397,6 +464,7 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
397
464
|
profiles: profiles.profiles,
|
|
398
465
|
rules: profiles.rules,
|
|
399
466
|
runners: runners.runners,
|
|
467
|
+
schedules: pipeline.schedules,
|
|
400
468
|
skills: profiles.skills,
|
|
401
469
|
...pipeline.task_context ? { task_context: pipeline.task_context } : {},
|
|
402
470
|
version: 1,
|
|
@@ -474,7 +542,13 @@ function normalizeMcpJsonServer(server) {
|
|
|
474
542
|
...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
|
|
475
543
|
};
|
|
476
544
|
}
|
|
477
|
-
function validatePipelineConfig(
|
|
545
|
+
function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
|
|
546
|
+
const parsed = configSchema.safeParse(rawConfig);
|
|
547
|
+
if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
|
|
548
|
+
path: issue.path.join("."),
|
|
549
|
+
message: issue.message
|
|
550
|
+
})));
|
|
551
|
+
const config = parsed.data;
|
|
478
552
|
const issues = [];
|
|
479
553
|
validateRegistryIds("runners", config.runners, issues);
|
|
480
554
|
validateRegistryIds("profiles", config.profiles, issues);
|
|
@@ -484,14 +558,6 @@ function validatePipelineConfig(config, projectRoot, options = {}) {
|
|
|
484
558
|
validateRegistryIds("hooks", config.hooks, issues);
|
|
485
559
|
validateRegistryIds("workflows", config.workflows, issues);
|
|
486
560
|
validateRegistryIds("entrypoints", config.entrypoints, issues);
|
|
487
|
-
if (!config.workflows[config.default_workflow]) issues.push({
|
|
488
|
-
path: "default_workflow",
|
|
489
|
-
message: `default workflow '${config.default_workflow}' is not declared`
|
|
490
|
-
});
|
|
491
|
-
for (const [entrypointId, entrypoint] of Object.entries(config.entrypoints)) if (!config.workflows[entrypoint.workflow]) issues.push({
|
|
492
|
-
path: `entrypoints.${entrypointId}.workflow`,
|
|
493
|
-
message: `entrypoint '${entrypointId}' references missing workflow '${entrypoint.workflow}'`
|
|
494
|
-
});
|
|
495
561
|
if (config.profiles[config.orchestrator.profile]) validateReferences("orchestrator.hooks", config.orchestrator.hooks, config.hooks, "hook", issues);
|
|
496
562
|
else issues.push({
|
|
497
563
|
path: "orchestrator.profile",
|
|
@@ -672,4 +738,4 @@ function validationError(issues) {
|
|
|
672
738
|
return new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", ["Invalid pipeline config:", ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
|
|
673
739
|
}
|
|
674
740
|
//#endregion
|
|
675
|
-
export { PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig };
|
|
741
|
+
export { PIPELINE_CONFIG_PATH, PROFILES_CONFIG_PATH, PipelineConfigError, RUNNERS_CONFIG_PATH, loadPipelineConfig, parsePipelineConfigParts, parsePipelineConfigYaml, tryLoadPipelineConfig, validatePipelineConfig, workflowSchema };
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js"
|
|
|
7
7
|
import { runKubernetesRunnerJob } from "./kubernetes-runner.js";
|
|
8
8
|
import { DEFAULT_MCPM_ARGS } from "./mcp/bootstrap.js";
|
|
9
9
|
import { formatPipelineInitResult, initPipelineProject } from "./pipeline-init.js";
|
|
10
|
-
import {
|
|
10
|
+
import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "./schedule-planner.js";
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
11
12
|
import { resolve } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { Command, CommanderError, Help, Option } from "commander";
|
|
@@ -26,6 +27,7 @@ function pipe(description, options = {}) {
|
|
|
26
27
|
return runConfiguredPipeline({
|
|
27
28
|
pipelineRunner: options.pipelineRunner ?? runPipelineFromConfig,
|
|
28
29
|
entrypoint: options.entrypoint,
|
|
30
|
+
schedule: options.schedule,
|
|
29
31
|
task: description,
|
|
30
32
|
workflow: options.workflow,
|
|
31
33
|
worktreePath
|
|
@@ -35,7 +37,35 @@ function pipe(description, options = {}) {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
async function runConfiguredPipeline(inputs) {
|
|
40
|
+
const config = loadPipelineConfig(inputs.worktreePath, { allowMissingLintFileReferences: true });
|
|
41
|
+
if (inputs.schedule) {
|
|
42
|
+
const compiled = compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(inputs.schedule, "utf8"), inputs.schedule), inputs.worktreePath);
|
|
43
|
+
await runAndPrintPipeline({
|
|
44
|
+
...inputs,
|
|
45
|
+
config: compiled.config,
|
|
46
|
+
workflow: compiled.workflowId
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const scheduledEntrypoint = scheduledEntrypointId(config, inputs.workflow, inputs.entrypoint);
|
|
51
|
+
if (scheduledEntrypoint) {
|
|
52
|
+
const result = await generateScheduleArtifact({
|
|
53
|
+
config,
|
|
54
|
+
entrypointId: scheduledEntrypoint,
|
|
55
|
+
task: inputs.task,
|
|
56
|
+
worktreePath: inputs.worktreePath
|
|
57
|
+
});
|
|
58
|
+
console.log([`Schedule generated: ${result.path}`, `Run after approval: pipe run --schedule ${result.path}`].join("\n"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await runAndPrintPipeline({
|
|
62
|
+
...inputs,
|
|
63
|
+
config
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function runAndPrintPipeline(inputs) {
|
|
38
67
|
const result = await (inputs.pipelineRunner ?? runPipelineFromConfig)({
|
|
68
|
+
config: inputs.config,
|
|
39
69
|
reporter: formatRuntimeProgress,
|
|
40
70
|
entrypoint: inputs.entrypoint,
|
|
41
71
|
task: inputs.task,
|
|
@@ -45,6 +75,12 @@ async function runConfiguredPipeline(inputs) {
|
|
|
45
75
|
console.log(formatRuntimeResult(result));
|
|
46
76
|
if (result.outcome !== "PASS") throw new Error(formatRuntimeFailure(result));
|
|
47
77
|
}
|
|
78
|
+
function scheduledEntrypointId(config, workflowId, entrypointId) {
|
|
79
|
+
if (workflowId) return null;
|
|
80
|
+
const id = entrypointId ?? "pipe";
|
|
81
|
+
const entrypoint = config.entrypoints[id];
|
|
82
|
+
return entrypoint && "schedule" in entrypoint ? id : null;
|
|
83
|
+
}
|
|
48
84
|
function formatRuntimeProgress(event) {
|
|
49
85
|
const message = formatRuntimeProgressMessage(event);
|
|
50
86
|
console.error(message);
|
|
@@ -164,24 +200,25 @@ function createCliProgram() {
|
|
|
164
200
|
const runAction = async (descriptionParts, flags) => {
|
|
165
201
|
await pipe(descriptionParts.join(" "), {
|
|
166
202
|
entrypoint: flags.entrypoint,
|
|
203
|
+
schedule: flags.schedule,
|
|
167
204
|
workflow: flags.workflow
|
|
168
205
|
});
|
|
169
206
|
};
|
|
170
|
-
program.command("run").description("Run a workflow from .pipeline/pipeline.yaml").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--workflow <workflow>", "workflow id from pipeline.yaml").action(runAction);
|
|
171
|
-
program.command("pipe").description("Alias for run").argument("<description...>", "task description").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--workflow <workflow>", "workflow id from pipeline.yaml").action(runAction);
|
|
172
|
-
program.command("validate").description("Validate .pipeline/pipeline.yaml and compile the workflow plan").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").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) => {
|
|
207
|
+
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);
|
|
208
|
+
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);
|
|
209
|
+
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) => {
|
|
173
210
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
174
211
|
const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
|
|
175
|
-
const plan = compileWorkflowPlan(config, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint));
|
|
212
|
+
const plan = flags.schedule ? compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(flags.schedule, "utf8"), flags.schedule), cwd).plan : compileWorkflowPlan(config, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint));
|
|
176
213
|
const warnings = flags.lint === false ? [] : lintPipelineConfig(config, cwd);
|
|
177
214
|
for (const warning of warnings) console.error(formatConfigLintWarning(warning));
|
|
178
215
|
if (flags.strict && warnings.length > 0) throw new Error(`Validation failed with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}.`);
|
|
179
216
|
console.log(`OK: ${plan.workflowId} (${plan.topologicalOrder.length} nodes)`);
|
|
180
217
|
});
|
|
181
|
-
program.command("explain-plan").description("Explain workflow nodes, runners, gates, hooks, and artifacts").option("--entrypoint <entrypoint>", "entrypoint alias from pipeline.yaml").option("--workflow <workflow>", "workflow id from pipeline.yaml").action((flags) => {
|
|
218
|
+
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) => {
|
|
182
219
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
183
220
|
const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
|
|
184
|
-
console.log(
|
|
221
|
+
console.log(formatSelectedWorkflowPlan(config, cwd, flags));
|
|
185
222
|
});
|
|
186
223
|
program.command("doctor").description("Check local prerequisites for pipeline init and execution").action(async () => {
|
|
187
224
|
const result = await runDoctor(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
|
|
@@ -414,7 +451,16 @@ function hasExitCode(err) {
|
|
|
414
451
|
return err instanceof Error && "exitCode" in err && typeof err.exitCode === "number";
|
|
415
452
|
}
|
|
416
453
|
function formatWorkflowPlan(config, worktreePath, workflowId) {
|
|
417
|
-
|
|
454
|
+
return formatCompiledWorkflowPlan(config, worktreePath, compileWorkflowPlan(config, workflowId));
|
|
455
|
+
}
|
|
456
|
+
function formatSelectedWorkflowPlan(config, worktreePath, flags) {
|
|
457
|
+
if (flags.schedule) {
|
|
458
|
+
const compiled = compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(flags.schedule, "utf8"), flags.schedule), worktreePath);
|
|
459
|
+
return formatCompiledWorkflowPlan(compiled.config, worktreePath, compiled.plan);
|
|
460
|
+
}
|
|
461
|
+
return formatWorkflowPlan(config, worktreePath, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint));
|
|
462
|
+
}
|
|
463
|
+
function formatCompiledWorkflowPlan(config, worktreePath, plan) {
|
|
418
464
|
const workflow = config.workflows[plan.workflowId];
|
|
419
465
|
const lines = [`Workflow: ${plan.workflowId}`];
|
|
420
466
|
lines.push(formatOrchestratorPlan(config, worktreePath));
|
|
@@ -449,6 +495,7 @@ function resolveWorkflowSelection(config, workflowId, entrypointId) {
|
|
|
449
495
|
if (!entrypointId) return;
|
|
450
496
|
const entrypoint = config.entrypoints[entrypointId];
|
|
451
497
|
if (!entrypoint) throw new Error(`Unknown pipeline entrypoint '${entrypointId}'`);
|
|
498
|
+
if ("schedule" in entrypoint) throw new Error(`Pipeline entrypoint '${entrypointId}' generates schedule '${entrypoint.schedule}'; use the entrypoint to create a schedule, then run with --schedule.`);
|
|
452
499
|
return entrypoint.workflow;
|
|
453
500
|
}
|
|
454
501
|
function formatOrchestratorPlan(config, worktreePath) {
|
package/dist/install-commands.js
CHANGED
|
@@ -169,6 +169,15 @@ function dispatchBlock(host, config, workflowId = config.default_workflow) {
|
|
|
169
169
|
hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes)
|
|
170
170
|
].filter((line) => Boolean(line)).join("\n");
|
|
171
171
|
}
|
|
172
|
+
function entrypointDispatchBlock(host, config, id, entrypoint) {
|
|
173
|
+
if ("workflow" in entrypoint) return dispatchBlock(host, config, entrypoint.workflow);
|
|
174
|
+
return [
|
|
175
|
+
`Generate a schedule for entrypoint \`${id}\` and the user task.`,
|
|
176
|
+
`The schedule policy is \`${entrypoint.schedule}\`.`,
|
|
177
|
+
`Run \`pipe run --entrypoint ${id} <task description>\` to write the schedule artifact, then stop for approval.`,
|
|
178
|
+
"Do not execute workflow nodes until the user runs `pipe run --schedule <schedule.yaml>`."
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
172
181
|
function nativeDispatchBlock(host, routes) {
|
|
173
182
|
if (routes.length === 0) return;
|
|
174
183
|
return [
|
|
@@ -265,7 +274,7 @@ function opencodeDefinitions(config) {
|
|
|
265
274
|
"",
|
|
266
275
|
orchestratorBlock(config),
|
|
267
276
|
"",
|
|
268
|
-
|
|
277
|
+
entrypointDispatchBlock("opencode", config, id, entrypoint)
|
|
269
278
|
]).join("\n")),
|
|
270
279
|
host: "opencode",
|
|
271
280
|
invocation: invocationForHost("opencode", id),
|
|
@@ -323,7 +332,7 @@ function codexDefinitions(config, cwd) {
|
|
|
323
332
|
"",
|
|
324
333
|
orchestratorBlock(config),
|
|
325
334
|
"",
|
|
326
|
-
|
|
335
|
+
entrypointDispatchBlock("codex", config, id, entrypoint)
|
|
327
336
|
]).join("\n")),
|
|
328
337
|
host: "codex",
|
|
329
338
|
invocation: invocationForHost("codex", id),
|
|
@@ -342,7 +351,7 @@ function codexDefinitions(config, cwd) {
|
|
|
342
351
|
"",
|
|
343
352
|
orchestratorBlock(config),
|
|
344
353
|
"",
|
|
345
|
-
|
|
354
|
+
entrypointDispatchBlock("codex", config, id, entrypoint)
|
|
346
355
|
]).join("\n")),
|
|
347
356
|
host: "codex",
|
|
348
357
|
invocation: codexPluginInvocation(id),
|
package/dist/mcp/launch-plan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
//#region src/mcp/launch-plan.ts
|
|
@@ -23,14 +23,22 @@ function mcpArgsFor(runnerType, servers, hasPipelineConfig) {
|
|
|
23
23
|
return [];
|
|
24
24
|
}
|
|
25
25
|
function mcpEnvFor(input, servers) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const path = join(mkdtempSync(join(tmpdir(), "pipeline-opencode-mcp-")), `${input.nodeId}.json`);
|
|
30
|
-
writeFileSync(path, JSON.stringify(config));
|
|
26
|
+
if (input.runnerType !== "opencode" || !input.config) return {};
|
|
27
|
+
const config = toOpenCodeMcpConfig(servers);
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), "pipeline-opencode-runtime-"));
|
|
31
29
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
OPENCODE_AUTH_CONTENT: void 0,
|
|
31
|
+
OPENCODE_CONFIG: void 0,
|
|
32
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
33
|
+
OPENCODE_CONFIG_DIR: void 0,
|
|
34
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
35
|
+
OPENCODE_TEST_HOME: join(dir, "home"),
|
|
36
|
+
PIPELINE_OPENCODE_RUNTIME_DIR: dir,
|
|
37
|
+
PIPELINE_WORKTREE: input.worktreePath,
|
|
38
|
+
XDG_CACHE_HOME: join(dir, "cache"),
|
|
39
|
+
XDG_CONFIG_HOME: join(dir, "config"),
|
|
40
|
+
XDG_DATA_HOME: join(dir, "data"),
|
|
41
|
+
XDG_STATE_HOME: join(dir, "state")
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
function isRemoteMcpServer(server) {
|
|
@@ -41,10 +49,10 @@ function headersWithBearerTokenEnv(server, renderTokenRef) {
|
|
|
41
49
|
if (server.bearer_token_env_var) headers.Authorization = `Bearer ${renderTokenRef(server.bearer_token_env_var)}`;
|
|
42
50
|
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
43
51
|
}
|
|
44
|
-
function toOpenCodeMcpConfig(selectedServers
|
|
45
|
-
return {
|
|
46
|
-
|
|
47
|
-
...Object.fromEntries(Object.entries(selectedServers).map(([id, server]) => {
|
|
52
|
+
function toOpenCodeMcpConfig(selectedServers) {
|
|
53
|
+
return {
|
|
54
|
+
$schema: "https://opencode.ai/config.json",
|
|
55
|
+
mcp: { ...Object.fromEntries(Object.entries(selectedServers).map(([id, server]) => {
|
|
48
56
|
if (isRemoteMcpServer(server)) {
|
|
49
57
|
const headers = headersWithBearerTokenEnv(server, (envVar) => `{env:${envVar}}`);
|
|
50
58
|
return [id, {
|
|
@@ -60,8 +68,8 @@ function toOpenCodeMcpConfig(selectedServers, declaredServers) {
|
|
|
60
68
|
...server.env ? { environment: server.env } : {},
|
|
61
69
|
type: "local"
|
|
62
70
|
}];
|
|
63
|
-
}))
|
|
64
|
-
}
|
|
71
|
+
})) }
|
|
72
|
+
};
|
|
65
73
|
}
|
|
66
74
|
function codexMcpArgs(servers) {
|
|
67
75
|
return Object.entries(servers).flatMap(([id, server]) => {
|
package/dist/pipeline-init.js
CHANGED
|
@@ -23,13 +23,13 @@ default_workflow: default
|
|
|
23
23
|
|
|
24
24
|
entrypoints:
|
|
25
25
|
pipe:
|
|
26
|
-
|
|
26
|
+
schedule: pipe-schedule
|
|
27
27
|
description: Full pipeline
|
|
28
28
|
inspect:
|
|
29
29
|
workflow: inspect
|
|
30
30
|
description: Read-only repository inspection
|
|
31
31
|
epic:
|
|
32
|
-
|
|
32
|
+
schedule: epic-schedule
|
|
33
33
|
description: Route an epic's tickets into specialist tracks, run them in parallel, then thermo-nuclear review.
|
|
34
34
|
|
|
35
35
|
orchestrator:
|
|
@@ -60,6 +60,14 @@ hooks:
|
|
|
60
60
|
timeout_ms: 5000
|
|
61
61
|
output_limit_bytes: 4096
|
|
62
62
|
|
|
63
|
+
schedules:
|
|
64
|
+
pipe-schedule:
|
|
65
|
+
baseline: pipe
|
|
66
|
+
planner_profile: pipeline-schedule-planner
|
|
67
|
+
epic-schedule:
|
|
68
|
+
baseline: epic
|
|
69
|
+
planner_profile: pipeline-schedule-planner
|
|
70
|
+
|
|
63
71
|
workflows:
|
|
64
72
|
inspect:
|
|
65
73
|
description: Read-only repository inspection workflow.
|
|
@@ -410,6 +418,22 @@ profiles:
|
|
|
410
418
|
mode: inherit
|
|
411
419
|
output:
|
|
412
420
|
format: text
|
|
421
|
+
pipeline-schedule-planner:
|
|
422
|
+
runner: codex
|
|
423
|
+
description: Refine a baseline schedule into a specialized approved-plan artifact.
|
|
424
|
+
instructions:
|
|
425
|
+
path: .pipeline/prompts/schedule-planner.md
|
|
426
|
+
skills: [research, scope]
|
|
427
|
+
mcp_servers: [serena, context7, backlog, qdrant, github-readonly]
|
|
428
|
+
tools: [read, list, grep, glob, bash]
|
|
429
|
+
filesystem:
|
|
430
|
+
mode: read-only
|
|
431
|
+
allow: ["**/*"]
|
|
432
|
+
deny: ["node_modules/**", "dist/**", ".git/**"]
|
|
433
|
+
network:
|
|
434
|
+
mode: inherit
|
|
435
|
+
output:
|
|
436
|
+
format: text
|
|
413
437
|
pipeline-test-writer:
|
|
414
438
|
runner: codex
|
|
415
439
|
description: Add focused failing tests for the requested behavior.
|
|
@@ -638,6 +662,18 @@ const SCAFFOLD_FILES = {
|
|
|
638
662
|
"Do not modify files.",
|
|
639
663
|
""
|
|
640
664
|
].join("\n"),
|
|
665
|
+
".pipeline/prompts/schedule-planner.md": [
|
|
666
|
+
"# Schedule planner",
|
|
667
|
+
"",
|
|
668
|
+
"Refine the provided baseline into a specialized `pipeline-schedule` YAML artifact for the user task.",
|
|
669
|
+
"",
|
|
670
|
+
"Keep the graph auditable: every workflow must be embedded in the artifact, every `kind: workflow` reference must point to an embedded workflow, and execution must include research, implementation, and verification.",
|
|
671
|
+
"",
|
|
672
|
+
"Use parallel branches only when they reduce coordination risk. Preserve configured profile ids, gates, hooks, retries, artifacts, and worktree policy unless the task clearly needs a different valid graph.",
|
|
673
|
+
"",
|
|
674
|
+
"Return only YAML. Do not wrap it in Markdown fences. Do not modify files. Do not invoke other agents.",
|
|
675
|
+
""
|
|
676
|
+
].join("\n"),
|
|
641
677
|
".pipeline/prompts/test-writer.md": [
|
|
642
678
|
"You are the RED/test-write phase for the pipeline.",
|
|
643
679
|
"Add focused failing tests for the requested behavior only.",
|
package/dist/pipeline-runtime.js
CHANGED
|
@@ -293,6 +293,7 @@ function resolveWorkflowSelection(config, workflowId, entrypointId) {
|
|
|
293
293
|
if (!entrypointId) return;
|
|
294
294
|
const entrypoint = config.entrypoints[entrypointId];
|
|
295
295
|
if (!entrypoint) throw new Error(`Unknown pipeline entrypoint '${entrypointId}'`);
|
|
296
|
+
if ("schedule" in entrypoint) throw new Error(`Pipeline entrypoint '${entrypointId}' generates schedule '${entrypoint.schedule}'; run an approved schedule artifact instead.`);
|
|
296
297
|
return entrypoint.workflow;
|
|
297
298
|
}
|
|
298
299
|
function failedRuntimeResult(context, nodes, failure) {
|