The pipeline is customized through the private overlay — a gitignored
private/ directory (templated by private.example/) that
the core loads at runtime. No core forking required. See
private.example/README.md for the full injection-point
table; this document covers the three extensibility axes consumers most often ask
about: stages, agents, and agent providers.
Support status at a glance:
| Axis | Status | Mechanism |
|---|---|---|
| Swap / add / remove stages | ✅ Supported | manifest.pipeline declarative edits |
| Customize agents (prompt, rules, skills, model) | ✅ Supported | overlay asset-staging + manifest.agents[name].model |
| Replace a whole agent | ✅ Supported | replace its stage |
| Fine-grained agent override (one field) | — | |
| Swap the agent/LLM provider | ❌ Not yet (Claude-only) | planned AgentRunner seam |
Everything in the pipeline is a Stage (src/types/pipeline.types.ts):
interface Stage {
readonly name: string;
canRun(state: PipelineState): boolean;
execute(state: PipelineState, context: PipelineContext): Promise<PipelineState>;
}The default pipeline is assembled in src/pipeline/pipeline-definition.ts. The
overlay edits its topology declaratively, anchored on stable stage names —
never by array index — via manifest.pipeline:
// private/manifest.ts
const manifest: OverlayManifest = {
pipeline: ({ config, repo }) => [
// insert a custom stage after an existing one
{ op: 'insertAfter', anchor: 'checkpoint:plan-approved', stage: myEnvProvision(config) },
// replace a built-in stage with your own
{ op: 'replace', anchor: 'coding', stage: myCoder(config) },
// drop a stage you don't want
{ op: 'remove', anchor: 'docs-writer' },
],
};Edits apply in order; each anchor resolves against the already-mutated list. A
missing anchor or a resulting duplicate stage name throws (fail-loud — never a
silent misalignment). Build stages with the provided factories — agentStage(),
revisionLoop(), checkpoint() — or implement the Stage interface directly.
Each agent lives in src/agents/<name>/ (config.ts = AgentConfig, schema.ts
= Zod output, CLAUDE.md = instructions, optional .claude/{rules,skills}/).
There are three levels of customization, cheapest first:
Drop files under private/agents/<name>/ and they're merged onto the base agent
at runtime (no code):
private/agents/coder/CLAUDE.append.md # appended to the base CLAUDE.md
private/agents/coder/.claude/rules/<rule>.md # added to the agent's rules
private/agents/coder/.claude/skills/<skill>/ # added to the agent's skills
private/prompts/<fragment>.md # overrides src/prompts/<fragment>.md
This is the right tool for product-specific guidance (naming conventions, an environment CLI workflow, domain rules) without touching public code.
Set the model via manifest.agents[name].model:
// private/manifest.ts
const manifest: OverlayManifest = {
agents: {
coder: { model: 'claude-sonnet-5' },
planner: { model: 'claude-opus-5' },
},
};Model resolution follows this precedence: override → base agent config → pipelineModels.perAgent[name] → pipelineModels.default.
To change an agent's prompt-builder, output schema, or tool set, build your own
AgentConfig + agentStage() and replace the stage (see §1). There is no
finer-grained "override just buildPrompt" hook today — replacing the stage is
the supported path.
Note:
AgentConfig(src/types/agent.types.ts) is currently coupled to the Claude Agent SDK — itsplugins,agents, andhooksfields are SDK types. A custom agent config imports@anthropic-ai/claude-agent-sdk.
Status: not yet pluggable — the pipeline runs on the Claude Agent SDK only.
src/sdk/run-agent.ts calls the SDK's query() directly, and AgentConfig is
SDK-shaped (structured output via tool-use, MCP servers, hooks, subagents are all
Claude-SDK concepts). Switching to a different provider (OpenAI, Gemini, a local
model) today means rewriting run-agent.ts.
A future release will introduce a provider interface so the overlay can supply a non-Claude backend. The intended shape:
// src/sdk/agent-runner.ts (planned)
export interface AgentRunner {
run<T>(req: {
config: AgentConfig<T>;
systemPrompt: string;
userPrompt: string;
cwd: string;
allowedTools: string[];
}): Promise<AgentResult<T>>;
}
// manifest.agentRunner overrides the default Claude runnerAgentResult (src/types/agent.types.ts) is already provider-neutral
(cost / duration / turns / tokens / structured output), so the return contract is
stable. The work is splitting AgentConfig into a provider-neutral core (name,
buildPrompt, outputSchema, allowedTools, model, maxTurns) plus a
Claude-specific extension (plugins/agents/hooks), and routing runAgent()
through config.overlay.agentRunner ?? defaultClaudeRunner.
Until then, Claude is the only supported provider. For an AL / Business Central pipeline this is rarely a constraint — but if non-Claude support matters to you, open an issue so it can be prioritized.