# ADR-0014: Provider-Specific Prompt Engineering

## Status

✅ Accepted

## Context

ADR-0002 established a multi-provider AI abstraction where all providers receive
the same system and user prompt strings through the `AiClient` trait. This works
well for the base prompt structure, but empirical testing revealed a behavioural
difference between model families when project-specific commit guidelines are
included in the system prompt.

When a project supplies commit guidelines — a template showing the exact format
every commit message must follow — Claude models interpret the instructions
correctly: they treat the template as a literal format to reproduce, substituting
only the obvious placeholders (`<type>`, `<scope>`, `<description>`). OpenAI and
Ollama models, given the same instruction phrasing, instead treat the template as
guidance and copy its text verbatim into the generated commit messages, or produce
messages that follow the spirit of the guidelines but ignore the literal structure
required.

The same divergence appears in PR template filling: with minimal instruction Claude
correctly replaces placeholder content with specifics; OpenAI models without
explicit step-by-step guidance reproduce the placeholder text unchanged.

Three approaches were evaluated:

- **Single format for all providers.** The prompt uses one phrasing and all
  providers receive it identically. Claude models continue to work correctly. OpenAI
  and Ollama models misinterpret "reproduce this template literally" as "follow these
  guidelines" and produce incorrectly formatted output whenever a project defines
  custom commit guidelines. This is unacceptable for users who have invested in
  project-specific guidelines expecting them to be enforced.

- **User-configurable prompt templates.** Users write their own prompt text, with
  separate template files for each provider. This gives maximum flexibility but
  imposes configuration burden on every user who switches providers, and it
  externalises a product quality concern — output format accuracy — into user
  configuration.

- **Provider-specific prompt sections.** The core prompt content is identical across
  providers. Only the framing of the guidelines/template section differs, keyed on a
  `PromptStyle` enum derived from `AiClientMetadata`. This is transparent to users
  and requires no configuration.

## Decision

We will vary only the framing of the commit-guidelines and PR-template sections of
system prompts based on a `PromptStyle` enum.

### Provider classification

`PromptStyle` is defined in `src/claude/ai.rs`:

```rust
pub enum PromptStyle {
    /// Claude models handle "literal template" instructions correctly.
    Claude,
    /// OpenAI-compatible models (OpenAI, Ollama) need different formatting.
    OpenAi,
}
```

`AiClientMetadata::prompt_style()` derives the style from the exact provider
strings set by each `AiClient` implementation:

```rust
pub fn prompt_style(&self) -> PromptStyle {
    match self.provider.as_str() {
        "OpenAI" | "Ollama" => PromptStyle::OpenAi,
        _ => PromptStyle::Claude,
    }
}
```

Known strings are `"Anthropic"` (direct API), `"Anthropic Bedrock"` (AWS Bedrock),
`"OpenAI"`, and `"Ollama"`. Bedrock uses the Claude style because it routes to
Claude models. Unrecognised provider strings default to `PromptStyle::Claude`.

### Prompt variants

Two functions in `src/claude/prompts.rs` accept a `PromptStyle` argument:

- `generate_contextual_system_prompt_for_provider(context, provider)` — commit
  message generation system prompt
- `generate_pr_system_prompt_with_context_for_provider(context, provider)` — PR
  description generation system prompt

For commit guidelines, the framing differs as follows:

| Style    | Section header                      | Framing                                                                                                                                          |
|----------|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| `Claude` | `MANDATORY COMMIT MESSAGE TEMPLATE` | "This is a LITERAL TEMPLATE that you must reproduce EXACTLY." Rules emphasise that every element is literal, with explicit wrong/right examples. |
| `OpenAi` | `PROJECT COMMIT GUIDELINES`         | "These are GUIDELINES for how to write commit messages." Rules clarify that the guidelines text itself must not appear in the output.            |

For PR template filling, `PromptStyle::Claude` appends a brief reminder that the
template is to be filled out rather than copied; `PromptStyle::OpenAi` appends
explicit bullet-point instructions with a concrete example.

### Scope of provider-specific behaviour

Provider-specific handling is intentionally limited to the guidelines and template
sections. The base system prompts (`BASIC_SYSTEM_PROMPT`,
`PR_GENERATION_SYSTEM_PROMPT`), all user prompts, check prompts, and coherence
prompts are identical across providers. The observed behavioural difference —
template reproduction versus guideline interpretation — only manifests when a
project-specific guidelines or template block is present. Check prompts embed
guidelines for evaluation rather than reproduction, so the distinction does not
apply.

Both `_for_provider()` functions have convenience wrappers
(`generate_contextual_system_prompt`, `generate_pr_system_prompt_with_context`)
that hardcode `PromptStyle::Claude`. Call sites that hold an `AiClientMetadata`
must use the `_for_provider` variants directly; the wrappers exist for contexts
where no provider is in scope.

### Call sites

`prompt_style()` is called at two points per `twiddle` or `create-pr` request:

1. **Batch planning** (`src/cli/git/twiddle.rs`) — the system prompt is generated
   and its token length estimated to size batches correctly.
2. **Request dispatch** (`src/claude/client.rs`) — the system prompt is generated
   for the actual API call.

Both calls are necessary; the token estimation in step 1 must use the same prompt
that step 2 will send.

## Consequences

**Positive:**

- **Both model families produce correctly formatted output.** When a project defines
  commit guidelines, Claude and OpenAI/Ollama models both apply them as intended.
  Users do not need to know which provider they are using when configuring guidelines.

- **Zero user configuration.** The style is derived automatically from
  `AiClientMetadata`. Switching providers does not require any prompt or config
  adjustment.

- **Minimal code surface.** Only the guidelines framing differs between styles. All
  other prompt logic — verbosity hints, branch context, work-pattern context,
  scope consistency guidance — is shared.

**Negative:**

- **Two code paths to maintain.** Each guidelines framing can drift independently.
  A change to how guidelines are presented in one style may need a corresponding
  update in the other. There are no tests that assert the two paths produce
  equivalent semantic outcomes, only that each path contains the expected section
  header.

- **New providers require a conscious style decision.** Any provider added in future
  will silently receive `PromptStyle::Claude` until a developer audits whether that
  is correct and, if not, adds it to the `PromptStyle::OpenAi` match arm. There is
  no compiler warning or failing test to surface this.

- **The default-to-Claude fallback may be wrong.** A misconfigured or renamed
  provider string falls through to `PromptStyle::Claude`. If that provider actually
  behaves like an OpenAI-compatible model, it will produce incorrectly formatted
  output with no visible error.

**Neutral:**

- **Bedrock uses the Claude style.** AWS Bedrock routes requests to Claude models,
  so `PromptStyle::Claude` is correct. If Bedrock were used to front a non-Claude
  model in future, the provider string `"Anthropic Bedrock"` would need updating or
  a separate variant would be required.

- **Coverage is intentionally narrow.** Check prompts, coherence prompts, and user
  prompts are not provider-specific. This is not an oversight; the behavioural
  difference that motivated this ADR does not manifest in those contexts.
