One Engine, Ten Flows: Getting Sitecore Out of My Own Pipeline

My pipeline had learned Sitecore so thoroughly it could not build anything else. How we measured that, why we did not split the repo, and what packs turned out to be.

Blog8 min read

Welcome back to another blog post. This time I did not build another brand site — I went after something in the pipeline itself that had been quietly annoying me for months.

Short version: my agentic pipeline had learned Sitecore so well that it had forgotten how to build anything else.

The thing that finally got on my nerves

I kicked off a Marketplace app run — a Cloud Portal extension, no head app, no editing host, no publish step anywhere in its life — and watched it inherit assumptions that only make sense for a Content SDK site. It did not crash. It just carried baggage it could not use. And a run that was not Sitecore at all was worse: the "generic" pipeline was really the Content SDK pipeline with the Sitecore bits switched off, which is a very different thing.

So I stopped guessing and counted.

How much does the shared engine actually know about Sitecore?

100 references to platform_target across 32 files, inside commands that were supposed to be platform-agnostic. /implement branched on it. /test branched on it. Five agent identity files carried a hand-maintained "Framework awareness" block that had already drifted out of sync with the commands' own lists.

My founding principle for this workspace is agents are identities, commands are tasks — and it turned out to be exactly one axis short. Domain had no home, so it leaked into both.

The obvious answer, and why I killed it

A repo split. One for Content SDK, one for Marketplace, each free to be as opinionated as it likes. I nearly did it, then measured it instead.

86%
Lines both repos would carry
84%
Of all framework churn, landing twice
0
Marketplace-only files that existed
585
Lines of clone a split cannot place

Two independent measures — static line count, and the last 200 commits — agreed within two points: a split makes about 85% of framework change land twice, forever. The optimistic case does not save it either. If Marketplace grew to match Content SDK exactly, duplication would fall from 86% to 74%, because a shared engine is platform-agnostic by definition and cannot shrink when platforms diverge.

Then the one that actually settled it. I have a clone flow — point it at a public website and it rebuilds that site to a measured parity threshold. A clone flow cannot be a repo: it has no deploy target of its own. You always clone into something. Under a split it would have to be copied into every platform repo, or live in one and be missing from the others.

A split forecloses the merge. Packs preserve the option to split later. So packs first — and if I ever really want two repos, the pack directory is already the boundary.

The probe that made it buildable

Deciding not to split still leaves the hard question: can platform knowledge actually be pulled out, or is it genuinely tangled into the logic?

So, the probe. Walk the nine heaviest commands, find every place they branch on the platform, and classify each one: is it data, is it an ordered step, or is it a genuine fork — control flow that is structurally different per platform and cannot be expressed as either?

27 branch points. Zero genuine forks. Every one reduced to pack data or a hook at a named anchor. If even three had been real forks, packs would have been the wrong shape and I would have found out after building them.

What we decided

Three declarative dimensions, resolved once at intake, injected everywhere:

DimensionThe question it answersValues
platformWhat you build oncontent-sdk · marketplace · none
methodWhere the spec comes fromgreenfield · clone
audienceWho operates the rundeveloper · marketer

The important word is compose, not multiply. Not eleven flows I maintain — three independent choices over one engine. Three by two by two is twelve combinations, of which ten are runnable. The twelfth, marketplace × clone, is declared incompatible and refused by name, because "rebuild this website as a Cloud Portal extension" is not a coherent request and I would rather it be refused than discovered at runtime.

Agents are identities. Commands are tasks. Packs are domains.

What that actually looks like

Start with the engine at every default, then turn one dial at a time. The restrictions are more interesting than the additions.

  1. core

    The engine, with every pack at its default

    No platform, greenfield, developer. Eight stages, five operator gates and a delivery loop that runs per tranche. This is what is left when the domain has been taken out of it — and proving it still runs clean is the test that the domain really left.

    /create-prd (unchanged)/architect (unchanged)/design (unchanged)/task-breakdown (unchanged)/implement (unchanged)/code-review (unchanged)/test (unchanged)/ship (unchanged)
  2. + platform: content-sdk

    Six stages appear, and the release step changes meaning

    The platform is what you build ON, so it owns the beginning and the end: read the content model before designing against it, stand the cloud up before building, and — because authoring happens against preview — put the whole content phase after ship, with one operator-confirmed publish as the only thing that goes live.

    /discover-sitecoreai (added)read the real content model first/create-prd (unchanged)/architect (unchanged)/design (unchanged)/task-breakdown (unchanged)/provision-sitecoreai (added)CM + editing host, idempotent/implement (unchanged)/code-review (unchanged)/test (unchanged)/ship (replaced)code only, deployed against preview/discover-components (added)a visual map for the author/author-content (added)copy and imagery into the tenant/test-content (added)an independent content audit/publish (added)the only step that goes live
  3. + method: clone

    The spec stops being written and starts being measured

    The method is where the specification comes FROM. Point it at a public site and the crawl replaces the interrogation outright — eleven questions, none of them greenfield. Design has nothing to diverge from, so it collapses into a parity phase, and "done" becomes a score per page per viewport instead of a feature list.

    /source-analysis (added)the crawl becomes the blueprint/create-prd (replaced)the greenfield questions are dropped/architect (unchanged)/design (dropped)the source site IS the designparity loop (added)bounded, with its own stop reason/task-breakdown (unchanged)/implement (unchanged)/code-review (unchanged)/test (replaced)+ a 4-criterion parity matrix/ship (unchanged)
  4. + audience: marketer

    The same spine, a different conversation

    Additive only — this dimension may never override a stage body, and a validator enforces that rather than a convention. Nothing is added or removed from the flow. What changes is what reaches the operator: a brief instead of a PRD, plain-language questions, technical answers derived instead of asked, and five dials preset so none of them ever gets surfaced.

    /create-prd (replaced)a brief, and 7 plain questions/architect (unchanged)/design (unchanged)/task-breakdown (unchanged)/implement (unchanged)/code-review (unchanged)/test (replaced)human half + a blocking machine half/ship (replaced)nothing technical surfaced

+ added · ⇄ replaced · − dropped · everything unmarked is the same stage the engine already ran.

The platform owns the beginning and the end, and nothing in the middle. Discover the model, stand the cloud up, and — because authoring happens against preview — the whole content phase sits after ship, with one operator-confirmed publish as the only thing that ever goes live.

The method is the only dimension that removes anything. Clone drops /design outright: when you are rebuilding an existing site there is nothing to diverge from, because the source is the design.

The audience adds and removes nothing at all. Every stage still runs and every artifact is still produced; what changes is which of them a human ever sees. A validator enforces that rather than good intentions — an audience pack that could override a stage body would collide with clone and foreclose marketer × clone, the single most plausible non-developer scenario there is.

Two constraints were locked before any code, and both held: a pack may never add a gate token (it can carry one of my five existing stops, never invent a sixth), and nothing merges off a branch — this is the engine that builds everything else.

The journey, honestly summarized

Eight tranches, each a complete slice: the contract, the two heaviest commands, intake and planning, the tail, then relocation — six stages and three agents physically moving into pack directories while the validator went from advisory to enforcing — then export, then the guided audience.

The first tranche is the one worth naming. Three packs declaring today's behaviour verbatim, and no file moved. An extraction you can diff against a golden snapshot is an extraction you can trust; if the pack says something different from the command it came from, that is a bug in the pack, not an improvement.

None of that is the interesting part. The interesting part is the question I asked once everything was green — validator, twelve suites, seven export self-tests:

Has any of this ever actually been run?

It had not. Zero of 89 real run manifests carried a pack block. The resolver was exhaustively unit-tested and had never once been persisted by anything.

Running it produced a fistful of defects in an afternoon, and I will spare you the list, because they were all the same defect wearing different clothes. A declaration nothing read. An ordered sequence missing a step that every individual declaration got right. A template whose placeholders never rendered. A safety boundary that checked file paths while the thing it guarded lived in file contents. Each one passed every gate it had.

Structural green is not functional green, and the counter-move is not a cleverer gate. It is embarrassingly simple: run the thing. Most of what I found came from execution; almost none of it from inspection.

There was a coda to that, and it is the bit I would most like back. A transitional platform_target field survived the whole refactor behind a ratchet — a count allowed to fall and never rise, which felt responsible and was not. A ratchet made the debt manageable rather than visible, which is exactly how a temporary duplicate becomes a permanent one. It finally came out in what became tranche nine of an eight-tranche plan: roughly eighty-five references down to thirty-five, of which zero are branches or writes.

A ratchet buys you time, and then quietly spends it.

Then I ran it five more times

Five throwaway runs, one per composition, driven from a single prompt per platform so that in each pair the only variable was the audience. For the first time the thesis had numbers rather than a claim:

developermarketer
the first question asked"Current page only, or a site-wide sweep?""Who is this for?"
the auth modelaskedderived
the spec produceda PRD, fifteen sectionsa brief
dials preset by the pack05
scope-dial questions reaching the operatorthe defaultszero, by design

Clone did exactly what it says on the tin: replaced the greenfield question set outright — eleven questions, not one of them greenfield — and presented the skipped design stage with the pack's own reason attached rather than silently omitting it.

So what do I have now

BEFORE
100
platform_target refs
32
Files carrying them
0
Homes for domain knowledge
1
Shape of run it truly knew
AFTER
0
Branches on it anywhere
4
Packs
10
Runnable compositions
7
Published flow exports

Six stages and three agents live inside packs, and every stage-shaped command resolves a generated brief rather than restating a platform's skill list — which is what makes it drift-proof, because two hand-maintained lists diverge by construction.

My favourite number is this one: the Marketplace pack owns zero stages. Its entire behaviour is data. Adding a platform is writing one file, not editing the engine.

And because I did not want to argue the thesis, I ran it. Two cold agents, identical buggy code, identical symptom, one dependency different. The Content-SDK-composed agent cited the relevant Sitecore rule six times and escalated the work. The core agent cited it zero times and wrote a plain fix. That is the whole point, shown rather than claimed.

Where it stands

Merged, validated, published — and one gap stated rather than hidden: subagent isolation inside an export is untested, because the agent registry resolves against the caller's working directory, so a test from my repo would exercise my own agents and prove nothing.

Two things I would do differently, and they are the same thing twice. Run it earlier — every serious defect came from execution, and execution came last. And do the retirement while it is still small, because a ratchet on something you intend to delete is just a polite way of not deleting it.

Is this the right shape forever? No idea. It is the right shape for the next platform, which is the only horizon I trust myself to design for. Ask me again after the third one.

More to come. As always.

Filed under
  • Agentic
  • SitecoreAI
  • Content SDK
  • Marketplace