docs(skill): add pipeline-for-MR recipe with source filter
Problem
The Orbit skill had no recipe for "pipelines for an MR" and the query language reference didn't document the parent/child pipeline duplication. Asked "how many pipelines have run for gitlab-org/gitlab!235291", Claude Code + skill v0.6.0 followed MergeRequest --TRIGGERED--> Pipeline (or filtered Pipeline.merge_request_id) without a source filter and returned 98 — the graph links every CI pipeline spawned in the context of an MR to that MR, including downstream child pipelines (source = "parent_pipeline"). REST /merge_requests/235291/pipelines, the MR Pipelines tab, and the DAP Orbit panel all return 16.
Solution
Teach the skill the canonical pattern: filter Pipeline.source = "merge_request_event" on every MR-pipeline query. Concretely — a "Common pitfalls" callout in SKILL.md, a leading imperative gotcha in recipes.md ahead of three paste-ready shapes (count by status, list failed, two-node TRIGGERED form), and a matching section in query_language.md. Version bump 0.6.0 → 0.7.0. The deeper fix would be ontology-level (e.g. a dedicated HAS_MR_PIPELINE edge); happy to file a follow-up.
Closes https://gitlab.com/dgruzd/tasks/-/work_items/2659
Validation — opencode-refine smoke tests against the installed branch skill
Each prompt was run twice: once against an interim revision (recipe only, gotcha in a blockquote inside the recipe), once against the final revision (with the SKILL.md callout and the imperative leading line in the recipe). Numbers confirmed by REST API ground truth.
Prompt 1 — "How many pipelines have run for MR !235291 in gitlab-org/gitlab? Break down by status."
| Query shape | source filter? |
Result | |
|---|---|---|---|
| Interim | Multi-node aggregation (Project + MR + Pipeline, TRIGGERED), no filter |
686 (343 / 329 / 14) | |
| Interim + filter applied | Same multi-node aggregation, with source = merge_request_event |
112 (98 / 7 / 7) — see Notes | |
| Final | Canonical single-node aggregation on Pipeline, merge_request_id + source |
16 (14 / 1 / 1) |
|
| REST ground truth | — | — | 16 (14 failed / 1 success / 1 canceled) |
Final-revision agent explicitly quoted the warning back: "Without it, the count would also include downstream child pipelines (source = "parent_pipeline") and over-count by 5-10× compared to what the MR's Pipelines tab and the REST endpoint show."
Prompt 2 — "Show me the failed pipelines for MR !235291 in gitlab-org/gitlab"
| Query shape | source filter? |
Result | |
|---|---|---|---|
| Interim | Multi-node traversal over TRIGGERED, status = failed, no source filter |
8 results, mix of merge_request_event and parent_pipeline (e.g. 2520134257, 2520134222 were children) |
|
| Final | Canonical single-node traversal, merge_request_id + source + status filters |
Only top-level failed pipelines, matching the MR Pipelines tab |
Prompt 3 — "What pipelines ran for MR !1216 (merged) in gitlab-org/orbit/knowledge-graph?"
| Query shape | source filter? |
Result | |
|---|---|---|---|
| Interim | Multi-node traversal over TRIGGERED, no source filter |
3 pipelines — coincidentally correct (no children on this MR) | |
| Final | Canonical single-node traversal, merge_request_id + source filter |
3 pipelines, all merge_request_event, all success — correct by construction |
Notes
- A recipe alone wasn't enough — agents tend to read
SKILL.mdand only optionally pull in references, and they default to multi-node forms even when single-node is canonical. Promoting the gotcha intoSKILL.md+ the leading imperative in the recipe is what got the agent to use the canonical form on the first try. - The 686 → 112 gap on Prompt 1 interim is a separate multi-node
aggregationcross-join behaviour: the same multi-nodetraversalreturns 16 distinct pipeline nodes, butaggregationwith the same nodes/relationships counts joined rows instead. My recipes steer users to the single-node form which avoids it; happy to file the engine quirk as a separate issue. - Linting clean (
markdownlint-cli2,vale --minAlertLevel error,lychee --offline); lefthookorbit-skill-docs-syncpasses. The sync rolls forward four disclaimer lines that previous syncs had left out (pre-existing drift since commit00f5adaa).