@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 CHANGED
@@ -72,10 +72,17 @@ Inspect the execution plan before running:
72
72
  pipe explain-plan
73
73
  ```
74
74
 
75
- Run the default workflow:
75
+ Generate the default schedule artifact:
76
76
 
77
77
  ```shell
78
- pipe run "Implement PIPE-123 user-facing behavior"
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 routes an epic's child tickets into fixed specialist
100
- tracks, runs those tracks in parallel, merges passing branches, and then runs a
101
- thermo-nuclear code quality review.
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 `--workflow` selection
205
- remains available and takes precedence over `--entrypoint` when both are set.
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`, `.codex/agents/*.toml` | `$pipe <task>`, `$inspect <task>`, `$epic <task>` |
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 ["claude", "codex", "opencode", "kimi", "pi", "command"];
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(config: PipelineConfig, projectRoot?: string, options?: PipelineConfigValidationOptions): PipelineConfig;
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 entrypointSchema = z.object({
262
+ const entrypointBaseSchema = z.object({
264
263
  description: z.string().optional(),
265
- task_context: taskContextResolverSchema.optional(),
266
- workflow: z.string()
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 = JSON.parse(readFileSync(filePath, "utf8"));
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(config, projectRoot, options = {}) {
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 JSON.parse(readFileSync(join(worktreePath, "package.json"), "utf-8")).scripts ?? {};
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: (JSON.parse(output)?.duplicates ?? []).map((dup) => ({
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
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
  interface PipeOptions {
6
6
  entrypoint?: string;
7
7
  pipelineRunner?: typeof runPipelineFromConfig;
8
+ schedule?: string;
8
9
  workflow?: string;
9
10
  }
10
11
  /**
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 { existsSync, realpathSync } from "node:fs";
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(formatWorkflowPlan(config, cwd, resolveWorkflowSelection(config, flags.workflow, flags.entrypoint)));
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
- const plan = compileWorkflowPlan(config, workflowId);
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) {