@oisincoveney/pipeline 1.11.1 → 1.11.2

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
@@ -11,8 +11,8 @@ artifacts.
11
11
  - Bun 1.1 or newer
12
12
  - Node.js 22.13 or newer
13
13
  - `npx`, `backlog`, `uvx`, and Docker on `PATH` for default skills and MCP setup
14
- - At least one configured runner CLI on `PATH`: `codex`, `claude`,
15
- `opencode`, `kimi`, `pi`, or a declared command runner
14
+ - At least one configured runner CLI on `PATH`: `codex`, `opencode`, `kimi`,
15
+ `pi`, or a declared command runner
16
16
 
17
17
  Install dependencies:
18
18
 
@@ -307,7 +307,6 @@ The installer creates one command surface per configured entrypoint.
307
307
 
308
308
  | Host | Generated files | Invocation |
309
309
  | ----------- | ----------------------------------------------------------------- | ---------------------------------- |
310
- | Claude Code | `.claude/commands/<entrypoint>.md`, `.claude/agents/*.md` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
311
310
  | 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>` |
312
311
  | OpenCode | `.opencode/commands/<entrypoint>.md`, `.opencode/agents/*.md` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
313
312
  | Kimi | `.kimi/commands/<entrypoint>.md`, `.kimi/agents/*.yaml` | `/pipe <task>`, `/inspect <task>`, `/epic <task>` |
package/dist/config.d.ts CHANGED
@@ -101,6 +101,15 @@ declare const workflowNodeBaseSchema: z.ZodObject<{
101
101
  timeout: "timeout";
102
102
  }>>>;
103
103
  }, z.core.$strict>>;
104
+ task_context: z.ZodOptional<z.ZodObject<{
105
+ acceptance_criteria: z.ZodOptional<z.ZodArray<z.ZodObject<{
106
+ id: z.ZodString;
107
+ text: z.ZodString;
108
+ }, z.core.$strict>>>;
109
+ description: z.ZodOptional<z.ZodString>;
110
+ id: z.ZodOptional<z.ZodString>;
111
+ title: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strict>>;
104
113
  timeout_ms: z.ZodOptional<z.ZodNumber>;
105
114
  }, z.core.$strip>;
106
115
  type WorkflowNodeBase = z.infer<typeof workflowNodeBaseSchema>;
package/dist/config.js CHANGED
@@ -259,6 +259,15 @@ const hookSchema = z.object({
259
259
  trusted: z.boolean().optional()
260
260
  }).strict();
261
261
  const taskContextResolverSchema = z.object({ type: z.string().min(1) }).passthrough();
262
+ const nodeTaskContextSchema = z.object({
263
+ acceptance_criteria: z.array(z.object({
264
+ id: z.string().min(1),
265
+ text: z.string().min(1)
266
+ }).strict()).optional(),
267
+ description: z.string().optional(),
268
+ id: z.string().min(1).optional(),
269
+ title: z.string().optional()
270
+ }).strict();
262
271
  const entrypointBaseSchema = z.object({
263
272
  description: z.string().optional(),
264
273
  task_context: taskContextResolverSchema.optional()
@@ -277,6 +286,7 @@ const workflowNodeBaseSchema = z.object({
277
286
  id: z.string(),
278
287
  needs: z.array(z.string()).optional(),
279
288
  retries: retriesSchema.optional(),
289
+ task_context: nodeTaskContextSchema.optional(),
280
290
  timeout_ms: z.number().int().positive().optional()
281
291
  });
282
292
  const workflowNodeSchema = z.lazy(() => z.discriminatedUnion("kind", [
@@ -3,7 +3,7 @@ import { loadPipelineConfig } from "./config.js";
3
3
  import { codexNativeMcpConfig } from "./mcp/native-config.js";
4
4
  import { compileWorkflowPlan } from "./workflow-planner.js";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { dirname, join, relative } from "node:path";
6
+ import { basename, dirname, join, relative } from "node:path";
7
7
  import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
8
8
  import matter from "gray-matter";
9
9
  import { stringify } from "smol-toml";
@@ -17,6 +17,8 @@ const OWNER_YAML_MARKER_PREFIX = "# @oisincoveney/pipeline:";
17
17
  const CODEX_PLUGIN_COMMAND_ROOT = ".agents/plugins/oisin-pipeline/commands";
18
18
  const CODEX_AGENT_CONFIG_START = "# @oisincoveney/pipeline:codex-agents:start";
19
19
  const CODEX_AGENT_CONFIG_END = "# @oisincoveney/pipeline:codex-agents:end";
20
+ const CODEX_AGENTS_MD_START = "<!-- @oisincoveney/pipeline:agents:start -->";
21
+ const CODEX_AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
20
22
  const ENTRYPOINT_PATH_PATTERNS = {
21
23
  codex: [/^\.agents\/skills\/([^/]+)\/SKILL\.md$/, /^\.agents\/plugins\/oisin-pipeline\/commands\/([^/]+)\.md$/],
22
24
  opencode: [/^\.opencode\/commands\/([^/]+)\.md$/]
@@ -261,7 +263,7 @@ function opencodePermission(actor, options = {}) {
261
263
  write: allowed.has("write") ? "allow" : "deny"
262
264
  };
263
265
  }
264
- function opencodeDefinitions(config) {
266
+ function opencodeDefinitions(config, cwd) {
265
267
  return [
266
268
  ...entrypointCommandDefinitions("opencode", config, (id, entrypoint) => ({
267
269
  content: markdown({
@@ -315,7 +317,8 @@ function opencodeDefinitions(config) {
315
317
  host: "opencode",
316
318
  invocation: invocationForHost("opencode"),
317
319
  path: `.opencode/agents/${id}.md`
318
- }))
320
+ })),
321
+ projectAgentsMdDefinition(cwd, "opencode")
319
322
  ];
320
323
  }
321
324
  function codexDefinitions(config, cwd) {
@@ -357,10 +360,49 @@ function codexDefinitions(config, cwd) {
357
360
  invocation: codexPluginInvocation(id),
358
361
  path: `${CODEX_PLUGIN_COMMAND_ROOT}/${id}.md`
359
362
  })),
363
+ projectAgentsMdDefinition(cwd, "codex"),
360
364
  ...nativeCodexProfiles.map(([id, profile]) => codexTomlAgentDefinition(config, cwd, id, profile)),
361
365
  codexProjectConfigAgentDefinition()
362
366
  ];
363
367
  }
368
+ function projectAgentsMdDefinition(cwd, host) {
369
+ const repoName = basename(cwd);
370
+ return {
371
+ block: {
372
+ end: CODEX_AGENTS_MD_END,
373
+ start: CODEX_AGENTS_MD_START
374
+ },
375
+ content: [
376
+ CODEX_AGENTS_MD_START,
377
+ GENERATED_MARKER,
378
+ `${OWNER_MARKER_PREFIX}host=codex -->`,
379
+ `${OWNER_MARKER_PREFIX}host=opencode -->`,
380
+ "",
381
+ "## Pipeline Guidance",
382
+ "",
383
+ "This repository is configured with `@oisincoveney/pipeline`.",
384
+ "",
385
+ "- Use `$pipe`, `$inspect`, or `$epic` for Codex skill entrypoints when the user asks for pipeline workflows.",
386
+ "- Use `/pipe`, `/inspect`, or `/epic` for Codex or OpenCode slash-command entrypoints when available.",
387
+ "- Load and follow the relevant skill from `.agents/skills` before doing specialized work.",
388
+ "- Prefer the generated pipeline profiles and command surfaces over ad hoc subagent prompts.",
389
+ "",
390
+ "## Pipeline Memory",
391
+ "",
392
+ `Use Qdrant collection \`${repoName}\` for this repository.`,
393
+ "",
394
+ `- Call \`qdrant-find\` before research with \`collection_name: ${repoName}\` unless the user explicitly disables memory.`,
395
+ `- Call \`qdrant-store\` during LEARN with \`collection_name: ${repoName}\` for durable lessons worth reusing.`,
396
+ "- Include metadata with at least `repo`, `phase`, `workflow` or `entrypoint`, `task`, and `outcome` when storing lessons.",
397
+ "",
398
+ CODEX_AGENTS_MD_END,
399
+ ""
400
+ ].join("\n"),
401
+ host,
402
+ invocation: invocationForHost(host),
403
+ path: "AGENTS.md"
404
+ };
405
+ }
364
406
  function codexTomlAgentDefinition(config, cwd, id, profile) {
365
407
  const profileWithResolvedModel = {
366
408
  ...profile,
@@ -462,9 +504,16 @@ function instructionsPointer(actor) {
462
504
  function definitionsFor(host, config, cwd) {
463
505
  const definitions = {
464
506
  codex: () => codexDefinitions(config, cwd),
465
- opencode: () => opencodeDefinitions(config)
507
+ opencode: () => opencodeDefinitions(config, cwd)
466
508
  };
467
- return (host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => definitions[name]());
509
+ return dedupeDefinitionsByPath((host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => definitions[name]()));
510
+ }
511
+ function dedupeDefinitionsByPath(definitions) {
512
+ const lastIndexes = /* @__PURE__ */ new Map();
513
+ definitions.forEach((definition, index) => {
514
+ lastIndexes.set(definition.path, index);
515
+ });
516
+ return definitions.filter((definition, index) => lastIndexes.get(definition.path) === index);
468
517
  }
469
518
  function selectedHosts(host) {
470
519
  return host === "all" ? [...COMMAND_HOSTS] : [host];
@@ -313,6 +313,8 @@ rules:
313
313
  path: .pipeline/rules/verification.md
314
314
 
315
315
  skills:
316
+ schedule-graph-shaping:
317
+ path: .pipeline/skills/schedule-graph-shaping/SKILL.md
316
318
  critique:
317
319
  path: .agents/skills/critique/SKILL.md
318
320
  diagnose:
@@ -423,7 +425,7 @@ profiles:
423
425
  description: Refine a baseline schedule into a specialized approved-plan artifact.
424
426
  instructions:
425
427
  path: .pipeline/prompts/schedule-planner.md
426
- skills: [research, scope]
428
+ skills: [schedule-graph-shaping]
427
429
  mcp_servers: [serena, context7, backlog, qdrant, github-readonly]
428
430
  tools: [read, list, grep, glob, bash]
429
431
  filesystem:
@@ -636,6 +638,62 @@ const SCAFFOLD_FILES = {
636
638
  [PIPELINE_CONFIG_PATH]: DEFAULT_PIPELINE_YAML,
637
639
  [PROFILES_CONFIG_PATH]: DEFAULT_PROFILES_YAML,
638
640
  [RUNNERS_CONFIG_PATH]: DEFAULT_RUNNERS_YAML,
641
+ ".pipeline/skills/schedule-graph-shaping/SKILL.md": [
642
+ "---",
643
+ "name: schedule-graph-shaping",
644
+ "description: Use when generating or reviewing pipeline schedule graphs for a task or epic. Shapes explicit root DAGs by grouping related tickets and verification work by goal, dependency, and evidence instead of defaulting to one full RED/GREEN/VERIFY chain per ticket.",
645
+ "---",
646
+ "",
647
+ "# Schedule Graph Shaping",
648
+ "",
649
+ "Use this when producing a `pipeline-schedule` YAML artifact.",
650
+ "",
651
+ "## Contract",
652
+ "",
653
+ "- Return only the artifact requested by the schedule planner. Do not add prose.",
654
+ "- Generate exactly one workflow named `root`.",
655
+ "- Do not use `kind: workflow` or embed configured workflow copies such as `default`, `infra`, `track`, or `epic-drain`.",
656
+ "- Every generated agent node must declare a configured `profile`.",
657
+ "- Node IDs must be stable lowercase kebab-case and match `^[a-z][a-z0-9-]*$`.",
658
+ "- Do not invent profiles, node-level skills, or unconfigured gates.",
659
+ "",
660
+ "## Shaping Procedure",
661
+ "",
662
+ "1. Cluster work units by intent before drawing nodes.",
663
+ " Group tickets that validate the same behavior, touch the same subsystem, share acceptance evidence, or must land in a fixed order.",
664
+ "",
665
+ "2. Use RED nodes for test strategy, not ticket counting.",
666
+ " One RED node can cover several GREEN tickets when they share the same failing test suite or behavior contract. Split RED nodes only when the tests are meaningfully independent or different profiles are needed.",
667
+ "",
668
+ "3. Use GREEN nodes for independently implementable slices.",
669
+ " A GREEN node may cover one ticket or a coherent group of tickets. Split GREEN nodes when the work can run in parallel, has different dependencies, has materially different ownership/risk, or would make one node too broad to review.",
670
+ "",
671
+ "4. Use acceptance nodes for user-visible outcomes.",
672
+ " One acceptance node can cover multiple implementation nodes when they produce the same visible outcome or acceptance checklist.",
673
+ "",
674
+ "5. Use verifier nodes for shared evidence.",
675
+ " One verifier can validate multiple tickets when the same real repository commands and checks prove them. Split verifiers only when evidence differs, one area needs specialized inspection, or a dependency boundary requires earlier proof.",
676
+ "",
677
+ "6. Preserve necessary serial order.",
678
+ " Dependencies from the backlog, shared migrations/schema changes, public API changes, and foundational refactors should gate downstream implementation. Independent clusters should fan out and then fan in to shared acceptance, verifier, merge, or review nodes.",
679
+ "",
680
+ "## Task Context",
681
+ "",
682
+ "- Assign every backlog work unit to at least one explicit generated agent node with `task_context.id`.",
683
+ "- Prefer assigning ticket-specific context to GREEN nodes.",
684
+ "- RED, acceptance, and verifier nodes may omit `task_context` when they cover a group; include it only when the node is genuinely ticket-specific.",
685
+ "",
686
+ "## Efficiency Checks",
687
+ "",
688
+ "Before returning the graph, ask:",
689
+ "",
690
+ "- Did I create a RED/GREEN/VERIFY chain just because a ticket exists?",
691
+ "- Can several GREEN nodes share one RED node without losing test-first behavior?",
692
+ "- Can several GREEN nodes share one verifier because the same commands prove them?",
693
+ "- Are independent implementation slices parallelized?",
694
+ "- Are serial edges based on real dependencies rather than habit?",
695
+ ""
696
+ ].join("\n"),
639
697
  ".pipeline/prompts/orchestrator.md": [
640
698
  "You are the orchestrator for the pipeline.",
641
699
  "Use `.pipeline/pipeline.yaml` as the source of truth for workflow order, profiles, gates, hooks, and artifacts.",
@@ -646,6 +704,9 @@ const SCAFFOLD_FILES = {
646
704
  ].join("\n"),
647
705
  ".pipeline/prompts/researcher.md": [
648
706
  "You are the research phase for the pipeline.",
707
+ "Call `qdrant-find` before local inspection when the qdrant MCP server is available.",
708
+ "Use collection_name equal to the repository directory basename, and skip this only when the user explicitly disables memory.",
709
+ "Surface relevant prior lessons briefly before continuing.",
649
710
  "Inspect first-party source, tests, docs, and task context before proposing changes.",
650
711
  "Write structured findings that identify relevant files, existing patterns, acceptance criteria, and risks.",
651
712
  "Return only valid JSON matching `.pipeline/schemas/research.schema.json`: an object with `findings` and `ac` arrays, plus optional `files`, `risks`, and `target`.",
@@ -665,13 +726,20 @@ const SCAFFOLD_FILES = {
665
726
  ".pipeline/prompts/schedule-planner.md": [
666
727
  "# Schedule planner",
667
728
  "",
668
- "Refine the provided baseline into a specialized `pipeline-schedule` YAML artifact for the user task.",
729
+ "Generate a constrained agent graph as a specialized `pipeline-schedule` YAML artifact for the user task.",
730
+ "",
731
+ "Keep the graph auditable: execution must include research, implementation, and verification.",
732
+ "",
733
+ "Generate exactly one workflow named `root`. Do not embed `default`, `epic-drain`, `infra`, `track`, or other configured workflow copies. Use explicit generated agent, builtin, command, parallel, or group nodes. Do not use `kind: workflow`.",
734
+ "",
735
+ "Use the provided backlog work units as the source of truth when present. Assign each work unit to explicit generated agent nodes with only its `task_context.id`, use only allowed configured profiles, and ensure implementation work has downstream acceptance, verification, or review coverage. Do not invent profiles or node-level skill overrides.",
736
+ "Do not copy backlog descriptions or acceptance criteria into output; the scheduler hydrates them from the assigned `task_context.id` after parsing.",
669
737
  "",
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.",
738
+ "Shape the graph by intent, not by ticket count. Do not create a full RED/GREEN/ACCEPTANCE/VERIFY chain for each backlog ticket unless each step needs ticket-specific evidence. Use one RED node for a group of tickets when they share a test strategy, fan out to parallel GREEN implementation nodes where the work can be implemented independently, and fan back in to shared acceptance or verifier nodes when the same acceptance checklist or real repository commands prove the group. Only serialize ticket nodes when the backlog, a shared migration/schema/API dependency, or implementation risk requires it.",
671
739
  "",
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.",
740
+ "Return exactly one YAML document and nothing else. Do not wrap it in Markdown fences. Do not include commentary, plans, task lists, or explanations. Do not modify files. Do not invoke other agents.",
673
741
  "",
674
- "Return only YAML. Do not wrap it in Markdown fences. Do not modify files. Do not invoke other agents.",
742
+ "Use block-style YAML for objects and arrays. Do not use compact inline mappings like `{ id: PIPE-41.1, title: ... }`. Quote scalar strings that contain punctuation such as `:`, `#`, `[`, `]`, `{`, or `}`.",
675
743
  ""
676
744
  ].join("\n"),
677
745
  ".pipeline/prompts/test-writer.md": [
@@ -744,6 +812,8 @@ const SCAFFOLD_FILES = {
744
812
  ".pipeline/prompts/learner.md": [
745
813
  "You are the LEARN phase for the pipeline.",
746
814
  "Store durable lessons from the run when useful and report qdrant-store evidence.",
815
+ "Call `qdrant-store` with collection_name equal to the repository directory basename.",
816
+ "Include metadata with at least repo, phase, workflow or entrypoint, task, and outcome.",
747
817
  "Do not write local markdown knowledge as the durable sink.",
748
818
  "Return only valid JSON matching `.pipeline/schemas/learn.schema.json`: an object with `qdrant` and `evidence`.",
749
819
  "Do not wrap the JSON in Markdown fences or add prose outside the JSON object.",
@@ -870,7 +870,7 @@ async function executeWorkflowNode(node, context) {
870
870
  runId: context.runId,
871
871
  signal: context.signal,
872
872
  task: context.task,
873
- taskContext: context.taskContext,
873
+ taskContext: effectiveTaskContext(node, context),
874
874
  workflowId: node.workflow,
875
875
  worktreePath: worktree.worktreePath ?? context.worktreePath
876
876
  });
@@ -1155,7 +1155,7 @@ function renderAgentPrompt(node, context) {
1155
1155
  `Workflow: ${context.workflowId}`,
1156
1156
  `Node: ${node.id}`,
1157
1157
  node.profile ? `Profile: ${node.profile}` : "",
1158
- renderTaskContext(context.taskContext),
1158
+ renderTaskContext(effectiveTaskContext(node, context)),
1159
1159
  "",
1160
1160
  "Declared grants:",
1161
1161
  `- tools: ${(profile?.tools ?? []).join(", ") || "none"}`,
@@ -1171,6 +1171,9 @@ function renderAgentPrompt(node, context) {
1171
1171
  ...node.needs.map((need) => `## ${need}\n${context.lastOutputByNode.get(need) ?? ""}`)
1172
1172
  ].filter(Boolean).join("\n");
1173
1173
  }
1174
+ function effectiveTaskContext(node, context) {
1175
+ return node.taskContext ?? context.taskContext;
1176
+ }
1174
1177
  function inheritedOutputSections(node, context) {
1175
1178
  const ownNeeds = new Set(node.needs);
1176
1179
  const inherited = [...context.inheritedOutputNodeIds].filter((id) => !ownNeeds.has(id) && context.lastOutputByNode.has(id));
@@ -1692,12 +1695,13 @@ function emitAgentFinish(context, plan, attempt, result) {
1692
1695
  }
1693
1696
  function evaluateGate(gate, nodeId, context, attempt) {
1694
1697
  const gateId = gate.id ?? `${gate.kind}:${nodeId}`;
1698
+ const node = context.plan.graph.node(nodeId);
1695
1699
  switch (gate.kind) {
1696
1700
  case "command": return evaluateCommandGate(gate, gateId, nodeId, context);
1697
1701
  case "artifact": return evaluateArtifactGate(gate, gateId, nodeId, context);
1698
1702
  case "builtin": return evaluateBuiltinGate(gate, gateId, nodeId, context);
1699
1703
  case "verdict": return evaluateVerdictGate(gate, gateId, nodeId, context, attempt);
1700
- case "acceptance": return evaluateAcceptanceGate(gate, gateId, nodeId, context, attempt);
1704
+ case "acceptance": return evaluateAcceptanceGate(gate, gateId, nodeId, context, attempt, node);
1701
1705
  case "changed_files": return evaluateChangedFilesGate(gate, gateId, nodeId, context);
1702
1706
  case "json_schema": return evaluateJsonSchemaGate(gate, gateId, nodeId, context, attempt);
1703
1707
  default: return assertNever(gate);
@@ -1781,8 +1785,8 @@ function evaluateVerdictGate(gate, gateId, nodeId, context, attempt) {
1781
1785
  reason: passed ? void 0 : "verdict requirement failed"
1782
1786
  };
1783
1787
  }
1784
- function evaluateAcceptanceGate(gate, gateId, nodeId, context, attempt) {
1785
- const expected = context.taskContext?.acceptanceCriteria ?? [];
1788
+ function evaluateAcceptanceGate(gate, gateId, nodeId, context, attempt, node) {
1789
+ const expected = (node ? effectiveTaskContext(node, context) : context.taskContext)?.acceptanceCriteria ?? [];
1786
1790
  if (expected.length === 0) return {
1787
1791
  evidence: ["no acceptance criteria in task context"],
1788
1792
  gateId,
@@ -2076,7 +2080,7 @@ function hookPayload(context, failure, node, gateId) {
2076
2080
  },
2077
2081
  failure,
2078
2082
  task: context.task,
2079
- taskContext: context.taskContext
2083
+ taskContext: node ? effectiveTaskContext(node, context) : context.taskContext
2080
2084
  };
2081
2085
  }
2082
2086
  function renderTemplate(value, context, failure, node, gateId) {