@oisincoveney/pipeline 1.10.1 → 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 +21 -9
- package/dist/config.d.ts +36 -12
- package/dist/config.js +83 -19
- package/dist/gates.js +3 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +63 -13
- package/dist/install-commands.js +193 -297
- package/dist/mcp/bootstrap.js +2 -1
- package/dist/mcp/launch-plan.js +22 -48
- package/dist/pipeline-init.js +102 -254
- package/dist/pipeline-runtime.d.ts +21 -1
- package/dist/pipeline-runtime.js +459 -204
- package/dist/runner-event-sink.js +45 -31
- package/dist/runner-job-contract.js +12 -1
- package/dist/runner.d.ts +2 -2
- package/dist/runner.js +30 -116
- package/dist/runtime-machines/contracts.d.ts +130 -0
- package/dist/runtime-machines/contracts.js +13 -0
- package/dist/runtime-machines/gate-machine.js +138 -0
- package/dist/runtime-machines/hook-machine.js +167 -0
- package/dist/runtime-machines/node-machine.js +268 -0
- package/dist/runtime-machines/workflow-machine.js +350 -0
- package/dist/runtime-observability-inspection.js +49 -0
- package/dist/runtime-observability.js +22 -0
- package/dist/safe-json.js +12 -0
- package/dist/schedule-planner.d.ts +1683 -0
- package/dist/schedule-planner.js +289 -0
- package/docs/adr-xstate-runtime-actor-system.md +38 -0
- package/docs/config-architecture.md +18 -28
- package/docs/mcp-gateway.md +5 -5
- package/docs/mcp-host-isolation.md +19 -13
- package/docs/operator-guide.md +30 -17
- package/docs/slash-command-adapter-contract.md +15 -17
- package/docs/xstate-runtime-actor-model.md +44 -0
- package/package.json +11 -3
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
|
|
@@ -300,7 +308,7 @@ The installer creates one command surface per configured entrypoint.
|
|
|
300
308
|
| Host | Generated files | Invocation |
|
|
301
309
|
| ----------- | ----------------------------------------------------------------- | ---------------------------------- |
|
|
302
310
|
| Claude Code | `.claude/commands/<entrypoint>.md`, `.claude/agents/*.md` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
|
|
303
|
-
| Codex | `.agents/skills/<entrypoint>/SKILL.md`, `.
|
|
311
|
+
| Codex | `.agents/skills/<entrypoint>/SKILL.md`, `.agents/plugins/oisin-pipeline/commands/<entrypoint>.md`, `.agents/plugins/oisin-pipeline/agents/*.md`, `.codex/config.toml` | `$pipe <task>`, `$inspect <task>`, `$epic <task>`, `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
|
|
304
312
|
| OpenCode | `.opencode/commands/<entrypoint>.md`, `.opencode/agents/*.md` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
|
|
305
313
|
| Kimi | `.kimi/commands/<entrypoint>.md`, `.kimi/agents/*.yaml` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
|
|
306
314
|
| Pi | `.pi/prompts/<entrypoint>.md` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
|
|
@@ -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
|
@@ -4,10 +4,11 @@ import { z } from "zod";
|
|
|
4
4
|
declare const PIPELINE_CONFIG_PATH = ".pipeline/pipeline.yaml";
|
|
5
5
|
declare const RUNNERS_CONFIG_PATH = ".pipeline/runners.yaml";
|
|
6
6
|
declare const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
|
|
7
|
-
declare const RUNNER_TYPES: readonly ["
|
|
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>;
|
|
@@ -271,13 +288,19 @@ declare const configSchema: z.ZodObject<{
|
|
|
271
288
|
model: z.ZodOptional<z.ZodString>;
|
|
272
289
|
type: z.ZodEnum<{
|
|
273
290
|
command: "command";
|
|
274
|
-
claude: "claude";
|
|
275
291
|
codex: "codex";
|
|
276
292
|
opencode: "opencode";
|
|
277
|
-
kimi: "kimi";
|
|
278
|
-
pi: "pi";
|
|
279
293
|
}>;
|
|
280
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>>>;
|
|
281
304
|
skills: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
282
305
|
path: z.ZodString;
|
|
283
306
|
}, z.core.$strict>>>;
|
|
@@ -301,6 +324,7 @@ type RunnerType = (typeof RUNNER_TYPES)[number];
|
|
|
301
324
|
type WorkflowNodeKind = (typeof NODE_KINDS)[number];
|
|
302
325
|
type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
303
326
|
type GateKind = (typeof GATE_KINDS)[number];
|
|
327
|
+
type ScheduleBaseline = (typeof SCHEDULE_BASELINES)[number];
|
|
304
328
|
interface PipelineConfigParts {
|
|
305
329
|
pipeline: string;
|
|
306
330
|
profiles: string;
|
|
@@ -313,6 +337,6 @@ declare function loadPipelineConfig(projectRoot: string, options?: PipelineConfi
|
|
|
313
337
|
declare function tryLoadPipelineConfig(projectRoot: string, options?: PipelineConfigValidationOptions): PipelineConfig | null;
|
|
314
338
|
declare function parsePipelineConfigYaml(source: string, sourcePath?: string, projectRoot?: string): PipelineConfig;
|
|
315
339
|
declare function parsePipelineConfigParts(sources: PipelineConfigParts, projectRoot?: string, sourcePaths?: PipelineConfigParts, options?: PipelineConfigValidationOptions): PipelineConfig;
|
|
316
|
-
declare function validatePipelineConfig(
|
|
340
|
+
declare function validatePipelineConfig(rawConfig: PipelineConfig, projectRoot?: string, options?: PipelineConfigValidationOptions): PipelineConfig;
|
|
317
341
|
//#endregion
|
|
318
|
-
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveFileReference } from "./path-refs.js";
|
|
2
|
+
import { parseJson } from "./safe-json.js";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { parseDocument } from "yaml";
|
|
@@ -10,11 +11,8 @@ const PROFILES_CONFIG_PATH = ".pipeline/profiles.yaml";
|
|
|
10
11
|
const LEGACY_CONFIG_PATH = ".pipeline/config.toml";
|
|
11
12
|
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
12
13
|
const RUNNER_TYPES = [
|
|
13
|
-
"claude",
|
|
14
14
|
"codex",
|
|
15
15
|
"opencode",
|
|
16
|
-
"kimi",
|
|
17
|
-
"pi",
|
|
18
16
|
"command"
|
|
19
17
|
];
|
|
20
18
|
const HOOK_EVENTS = [
|
|
@@ -56,6 +54,7 @@ const RETRY_REASONS = [
|
|
|
56
54
|
"gate_failure",
|
|
57
55
|
"timeout"
|
|
58
56
|
];
|
|
57
|
+
const SCHEDULE_BASELINES = ["epic", "pipe"];
|
|
59
58
|
var PipelineConfigError = class extends Error {
|
|
60
59
|
code;
|
|
61
60
|
issues;
|
|
@@ -260,10 +259,16 @@ const hookSchema = z.object({
|
|
|
260
259
|
trusted: z.boolean().optional()
|
|
261
260
|
}).strict();
|
|
262
261
|
const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
|
|
263
|
-
const
|
|
262
|
+
const entrypointBaseSchema = z.object({
|
|
264
263
|
description: z.string().optional(),
|
|
265
|
-
task_context: taskContextResolverSchema.optional()
|
|
266
|
-
|
|
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()
|
|
267
272
|
}).strict();
|
|
268
273
|
const workflowNodeBaseSchema = z.object({
|
|
269
274
|
artifacts: z.array(artifactSchema).optional(),
|
|
@@ -323,11 +328,12 @@ const pipelineFileSchema = z.object({
|
|
|
323
328
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
324
329
|
hooks: strictRecord(hookSchema).default({}),
|
|
325
330
|
orchestrator: orchestratorSchema,
|
|
331
|
+
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
326
332
|
task_context: taskContextResolverSchema.optional(),
|
|
327
333
|
workflows: strictRecord(workflowSchema).default({}),
|
|
328
334
|
version: z.literal(1)
|
|
329
335
|
}).strict();
|
|
330
|
-
z.object({
|
|
336
|
+
const configSchema = z.object({
|
|
331
337
|
default_workflow: z.string(),
|
|
332
338
|
entrypoints: strictRecord(entrypointSchema).default({}),
|
|
333
339
|
hooks: strictRecord(hookSchema).default({}),
|
|
@@ -336,11 +342,70 @@ z.object({
|
|
|
336
342
|
profiles: strictRecord(profileSchema).default({}),
|
|
337
343
|
rules: strictRecord(pathRefSchema).default({}),
|
|
338
344
|
runners: strictRecord(runnerSchema).default({}),
|
|
345
|
+
schedules: strictRecord(schedulePolicySchema).default({}),
|
|
339
346
|
skills: strictRecord(pathRefSchema).default({}),
|
|
340
347
|
task_context: taskContextResolverSchema.optional(),
|
|
341
348
|
version: z.literal(1),
|
|
342
349
|
workflows: strictRecord(workflowSchema).default({})
|
|
343
|
-
}).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
|
+
}
|
|
344
409
|
function loadPipelineConfig(projectRoot, options = {}) {
|
|
345
410
|
const missing = [
|
|
346
411
|
PIPELINE_CONFIG_PATH,
|
|
@@ -399,6 +464,7 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
399
464
|
profiles: profiles.profiles,
|
|
400
465
|
rules: profiles.rules,
|
|
401
466
|
runners: runners.runners,
|
|
467
|
+
schedules: pipeline.schedules,
|
|
402
468
|
skills: profiles.skills,
|
|
403
469
|
...pipeline.task_context ? { task_context: pipeline.task_context } : {},
|
|
404
470
|
version: 1,
|
|
@@ -452,7 +518,7 @@ function resolveMcpServerRef(id, ref, projectRoot) {
|
|
|
452
518
|
function parseMcpJsonFile(id, ref, filePath) {
|
|
453
519
|
let raw;
|
|
454
520
|
try {
|
|
455
|
-
raw =
|
|
521
|
+
raw = parseJson(readFileSync(filePath, "utf8"), `MCP config ${ref.path}`);
|
|
456
522
|
} catch (err) {
|
|
457
523
|
throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse MCP config ${ref.path}`, [{
|
|
458
524
|
path: `mcp_servers.${id}.ref.path`,
|
|
@@ -476,7 +542,13 @@ function normalizeMcpJsonServer(server) {
|
|
|
476
542
|
...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
|
|
477
543
|
};
|
|
478
544
|
}
|
|
479
|
-
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;
|
|
480
552
|
const issues = [];
|
|
481
553
|
validateRegistryIds("runners", config.runners, issues);
|
|
482
554
|
validateRegistryIds("profiles", config.profiles, issues);
|
|
@@ -486,14 +558,6 @@ function validatePipelineConfig(config, projectRoot, options = {}) {
|
|
|
486
558
|
validateRegistryIds("hooks", config.hooks, issues);
|
|
487
559
|
validateRegistryIds("workflows", config.workflows, issues);
|
|
488
560
|
validateRegistryIds("entrypoints", config.entrypoints, issues);
|
|
489
|
-
if (!config.workflows[config.default_workflow]) issues.push({
|
|
490
|
-
path: "default_workflow",
|
|
491
|
-
message: `default workflow '${config.default_workflow}' is not declared`
|
|
492
|
-
});
|
|
493
|
-
for (const [entrypointId, entrypoint] of Object.entries(config.entrypoints)) if (!config.workflows[entrypoint.workflow]) issues.push({
|
|
494
|
-
path: `entrypoints.${entrypointId}.workflow`,
|
|
495
|
-
message: `entrypoint '${entrypointId}' references missing workflow '${entrypoint.workflow}'`
|
|
496
|
-
});
|
|
497
561
|
if (config.profiles[config.orchestrator.profile]) validateReferences("orchestrator.hooks", config.orchestrator.hooks, config.hooks, "hook", issues);
|
|
498
562
|
else issues.push({
|
|
499
563
|
path: "orchestrator.profile",
|
|
@@ -674,4 +738,4 @@ function validationError(issues) {
|
|
|
674
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);
|
|
675
739
|
}
|
|
676
740
|
//#endregion
|
|
677
|
-
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/gates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseJson } from "./safe-json.js";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { execa } from "execa";
|
|
@@ -13,7 +14,7 @@ function parseFailingTests(output) {
|
|
|
13
14
|
}
|
|
14
15
|
function readPackageScripts(worktreePath) {
|
|
15
16
|
try {
|
|
16
|
-
return
|
|
17
|
+
return parseJson(readFileSync(join(worktreePath, "package.json"), "utf-8"), "package.json").scripts ?? {};
|
|
17
18
|
} catch {
|
|
18
19
|
return {};
|
|
19
20
|
}
|
|
@@ -136,19 +137,16 @@ const JSCPD_DEFAULT_IGNORES = [
|
|
|
136
137
|
"**/.next/**",
|
|
137
138
|
"**/.turbo/**",
|
|
138
139
|
"**/.cache/**",
|
|
139
|
-
"**/.claude/**",
|
|
140
140
|
"**/.codex/**",
|
|
141
|
-
"**/.kimi/**",
|
|
142
141
|
"**/.serena/**",
|
|
143
142
|
"**/.opencode/**",
|
|
144
|
-
"**/.pi/**",
|
|
145
143
|
"**/.pipeline/host-resources/**",
|
|
146
144
|
"**/.pipeline/skills/**",
|
|
147
145
|
"**/.agents/skills/**"
|
|
148
146
|
];
|
|
149
147
|
function parseJscpdOutput(output) {
|
|
150
148
|
try {
|
|
151
|
-
return { violations: (
|
|
149
|
+
return { violations: (parseJson(output, "jscpd output")?.duplicates ?? []).map((dup) => ({
|
|
152
150
|
file: dup?.firstFile?.name ?? "unknown",
|
|
153
151
|
line: dup?.firstFile?.start,
|
|
154
152
|
message: `Duplicate code block detected between ${dup?.firstFile?.name} and ${dup?.secondFile?.name}`
|
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,12 +75,18 @@ 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);
|
|
51
87
|
}
|
|
52
88
|
function formatRuntimeProgressMessage(event) {
|
|
53
|
-
return formatWorkflowProgress(event) ?? formatAgentProgress(event) ?? formatCheckProgress(event) ?? formatRepairProgress(event);
|
|
89
|
+
return formatWorkflowProgress(event) ?? formatAgentProgress(event) ?? formatCheckProgress(event) ?? formatObservabilityProgress(event) ?? formatRepairProgress(event);
|
|
54
90
|
}
|
|
55
91
|
function formatWorkflowProgress(event) {
|
|
56
92
|
switch (event.type) {
|
|
@@ -92,6 +128,12 @@ function formatRepairProgress(event) {
|
|
|
92
128
|
default: throw new Error(`Unhandled runtime event: ${event.type}`);
|
|
93
129
|
}
|
|
94
130
|
}
|
|
131
|
+
function formatObservabilityProgress(event) {
|
|
132
|
+
switch (event.type) {
|
|
133
|
+
case "runtime.observability": return `Runtime observed: ${event.name} - ${event.summary}`;
|
|
134
|
+
default: return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
95
137
|
function formatRuntimeResult(result) {
|
|
96
138
|
const lines = [
|
|
97
139
|
`Pipeline complete: ${result.outcome}`,
|
|
@@ -158,24 +200,25 @@ function createCliProgram() {
|
|
|
158
200
|
const runAction = async (descriptionParts, flags) => {
|
|
159
201
|
await pipe(descriptionParts.join(" "), {
|
|
160
202
|
entrypoint: flags.entrypoint,
|
|
203
|
+
schedule: flags.schedule,
|
|
161
204
|
workflow: flags.workflow
|
|
162
205
|
});
|
|
163
206
|
};
|
|
164
|
-
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);
|
|
165
|
-
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);
|
|
166
|
-
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) => {
|
|
167
210
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
168
211
|
const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
|
|
169
|
-
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));
|
|
170
213
|
const warnings = flags.lint === false ? [] : lintPipelineConfig(config, cwd);
|
|
171
214
|
for (const warning of warnings) console.error(formatConfigLintWarning(warning));
|
|
172
215
|
if (flags.strict && warnings.length > 0) throw new Error(`Validation failed with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}.`);
|
|
173
216
|
console.log(`OK: ${plan.workflowId} (${plan.topologicalOrder.length} nodes)`);
|
|
174
217
|
});
|
|
175
|
-
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) => {
|
|
176
219
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
177
220
|
const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
|
|
178
|
-
console.log(
|
|
221
|
+
console.log(formatSelectedWorkflowPlan(config, cwd, flags));
|
|
179
222
|
});
|
|
180
223
|
program.command("doctor").description("Check local prerequisites for pipeline init and execution").action(async () => {
|
|
181
224
|
const result = await runDoctor(process.env.PIPELINE_TARGET_PATH ?? process.cwd());
|
|
@@ -191,11 +234,8 @@ function createCliProgram() {
|
|
|
191
234
|
});
|
|
192
235
|
program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option("--host <host>", "host command set to install").choices([
|
|
193
236
|
"all",
|
|
194
|
-
"claude",
|
|
195
237
|
"opencode",
|
|
196
|
-
"codex"
|
|
197
|
-
"kimi",
|
|
198
|
-
"pi"
|
|
238
|
+
"codex"
|
|
199
239
|
]).default("all").argParser(parseCommandHost)).option("--dry-run", "show planned changes without writing files").option("--check", "fail if generated command files are missing or stale").option("--force", "overwrite manually edited command files").action(async (flags) => {
|
|
200
240
|
const result = await installCommands({
|
|
201
241
|
...flags,
|
|
@@ -411,7 +451,16 @@ function hasExitCode(err) {
|
|
|
411
451
|
return err instanceof Error && "exitCode" in err && typeof err.exitCode === "number";
|
|
412
452
|
}
|
|
413
453
|
function formatWorkflowPlan(config, worktreePath, workflowId) {
|
|
414
|
-
|
|
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) {
|
|
415
464
|
const workflow = config.workflows[plan.workflowId];
|
|
416
465
|
const lines = [`Workflow: ${plan.workflowId}`];
|
|
417
466
|
lines.push(formatOrchestratorPlan(config, worktreePath));
|
|
@@ -446,6 +495,7 @@ function resolveWorkflowSelection(config, workflowId, entrypointId) {
|
|
|
446
495
|
if (!entrypointId) return;
|
|
447
496
|
const entrypoint = config.entrypoints[entrypointId];
|
|
448
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.`);
|
|
449
499
|
return entrypoint.workflow;
|
|
450
500
|
}
|
|
451
501
|
function formatOrchestratorPlan(config, worktreePath) {
|