# Channel adapter  -  PR description

> Detailed contract for the `pr` channel of `/multi-agent:channels`. Split out of `channels.md` in v8.0.0; the parent doc keeps a one-line summary and a link here.

The PR adapter rewrites the pull request description with the body assembled in Step 5 of `channels.md`. Default behaviour is **replace**; `--append` opt-in preserves existing content.

## Required body structure

The PR description targets code reviewers  -  it stays technical. Every adapter run assembles the body from the same fixed set of sections in the same order. Section headings render in `prefs.global.outputLanguage` (the body **text** is bilingual-aware); section keys and order are stable.

| # | Section key | Heading (`tr`) | Heading (`en`) | Required? |
|---|---|---|---|---|
| 1 | `summary` | `## Özet` | `## Summary` | always |
| 2 | `changes` | `## Değişiklikler` | `## Changes` | always |
| 3 | `architecture` | `## Mimari Kararlar` | `## Architecture Decisions` | when a non-trivial design choice was made |
| 4 | `verification` | `## Doğrulama` | `## Verification` | always |
| 5 | `dependencies` | `## Bağımlılıklar` | `## Dependencies` | when deps added/removed/bumped |
| 6 | `related` | `## İlgili` | `## Related` | always (Jira/issue ref; never `Closes/Fixes`) |

### Section content rules

**`summary`**  -  1-3 sentences in `outputLanguage`. The "why" of the change. Past tense, no marketing voice. Code identifiers stay verbatim.

**`changes`**  -  bullet list, one item per logically distinct change. Each bullet starts with the touched component and ends with a one-line "what". Use the stack's native file extensions / module paths  -  the example below shows the **shape**, not a stack lock-in:

```markdown
## Changes

- `<path/to/file.ext>`  -  <one-line description of what changed>
- `<path/to/another.ext>`  -  <one-line description>
- `<path/to/test.ext>`  -  <which scenarios were added/updated>
```

Across stacks the same shape produces, for example: `LoginView.swift  -  ...` (iOS), `LoginScreen.kt  -  ...` (Android), `login-form.tsx  -  ...` (frontend), `auth_service.py  -  ...` (backend). File paths and symbol names are NOT translated  -  only the trailing description sentence follows `outputLanguage`.

**`architecture`**  -  only when the change involves a non-trivial decision (new abstraction, pattern change, data flow shift, dependency direction). Format: short paragraph stating the decision and the alternative considered. Skip the section entirely for mechanical refactors / dependency bumps / formatting passes.

**`verification`**  -  what the reviewer should run to confirm the change works. Commands first, manual steps next. Pick the commands for the project's stack  -  the pipeline supports iOS (Swift/Xcode), Android (Gradle), frontend (npm/pnpm/yarn), and backend (varies: pytest/jest/go test/etc). Do not hardcode one stack in the body; emit only the commands relevant to the repos touched by this PR.

Skeleton (the adapter fills the body with the actual stack-appropriate lines at write-time; the template lists the shape only):

```markdown
## Verification

- Build: <stack-appropriate build command>
- Tests: <stack-appropriate test command, scoped to the touched targets/packages>
- Lint / type-check: <if the project has one>
- Manual: <one or more user-facing steps the reviewer can follow without setup>
```

Multi-repo PRs (one PR per repo) emit verification commands for that repo's stack only  -  never mix iOS + Android commands into a single PR body.

**`dependencies`**  -  only when `Package.swift` / `Podfile` / `build.gradle` / `package.json` changed. Each entry: `package@old → new  -  reason`.

**`related`**  -  flat list, plain text. Examples:

```markdown
## Related

- Jira: PROJ-XXXXX
- Issue: #123
- Confluence: <page-url> (if work referenced a spec)
- Figma: <design-url> (if work referenced a design)
```

Never use `Closes #N`, `Fixes #N`, `Resolves PROJ-X`. Issues require 4-approval close, the auto-close keywords break that contract.

### Assembly order (per run)

```
1. Read agent-state.json (taskId, contextLinks, identity, language).
2. Build section bodies in markdown  -  summary first, then in the table order, skipping conditional sections that don't apply.
3. Run the assembled body through the `humanizer` skill.
4. Apply Multi-repo cross-links (## Related PRs prepend when projects.length > 1).
5. Dispatch per the Behaviour-by-remote table.
```

## Markup dialect (per surface, not per pipeline)

The PR body is **Markdown** on every supported remote  -  GitHub, Bitbucket Server, and GitLab all render Markdown in the description field. Emit the assembled markdown verbatim; there is no conversion step on this adapter.

Jira wiki markup in a PR body is a defect, not a style choice. The Jira adapter's conversion table (`channels/jira.md` "Wiki markup conversion") applies to the **Jira comment only** and must never be reached from here. Concretely, in a PR body:

| Never in a PR body | Renders as | Use instead |
|---|---|---|
| `h2. Title` / `h3. Title` | literal text `h2. Title` | `## Title` / `### Title` |
| `{{identifier}}` | literal braces `{{identifier}}` | `` `identifier` `` |
| `# item` for a numbered list | an H1 heading, one giant line per item | `1. item` |
| `{code:swift} ... {code}` | literal braces | fenced ` ```swift ` block |
| `*bold*` | italic in Markdown, not bold | `**bold**` |

The two adapters run in the same phase over the same source markdown, so the failure mode is a converter applied to the wrong target: the Jira comment is correct and the PR body ships raw wiki markup. Assemble once in markdown, then convert **only** on the Jira branch.

## Behaviour by remote

| Remote | API | Reviewer handling |
|---|---|---|
| GitHub | `gh pr edit --body-file` | Reviewers are independent of the body  -  no extra payload required. |
| Bitbucket | `PUT /rest/api/1.0/projects/{P}/repos/{R}/pull-requests/{id}` | MUST re-send `reviewers`, `fromRef`, `toRef`, `draft`, `version`  -  server-side defaulting wipes any field that is omitted. |

## Reviewer-preserving Bitbucket payload (required)

```bash
PR_JSON=$(curl -s -u "$BB_USER:$BB_TOKEN" "$PR_URL")
REVIEWERS=$(jq '[.reviewers[] | {user: {name: .user.name}, approved, status}]' <<< "$PR_JSON")
VERSION=$(jq -r '.version' <<< "$PR_JSON")
TITLE=$(jq -r '.title' <<< "$PR_JSON")
FROM_REF=$(jq '.fromRef | {id, repository}' <<< "$PR_JSON")
TO_REF=$(jq '.toRef | {id, repository}' <<< "$PR_JSON")
DRAFT=$(jq -r '.draft // false' <<< "$PR_JSON")

jq -n --rawfile body /tmp/channels-$TASK_ID-pr.md \
      --argjson version "$VERSION" --arg title "$TITLE" \
      --argjson reviewers "$REVIEWERS" \
      --argjson fromRef "$FROM_REF" --argjson toRef "$TO_REF" \
      --argjson draft "$DRAFT" \
      '{version: $version, title: $title, description: $body,
        reviewers: $reviewers, fromRef: $fromRef, toRef: $toRef, draft: $draft}' \
  > /tmp/channels-$TASK_ID-pr-payload.json

curl -s -X PUT -u "$BB_USER:$BB_TOKEN" -H "Content-Type: application/json" \
     --data-binary @/tmp/channels-$TASK_ID-pr-payload.json "$PR_URL"
```

Verify after every PUT: refetch the PR and compare `reviewers | length` against the pre-PUT count. A drop means the payload lost the field  -  repair immediately.

`POST /pull-requests/{id}/participants` is **repair-only**, never the primary path. It accepts one user per call, so restoring N reviewers writes N separate "added 1 reviewer" rows into the PR activity feed, and Bitbucket activity entries cannot be deleted. A PR that opened with 22 reviewers in one clean create call and then shows 22 individual re-adds is a visible, permanent record of a dropped-reviewer PUT. Carry `reviewers` through the payload above instead.

## Multi-repo cross-links

When `state.projects[].length > 1`, `channels-multi-repo.sh render-pr <state> <body> <repoName>` prepends a `## Related PRs` block:

- The **primary** target's body lists every extra as `Related: <url>`.
- Each **extra** target's body lists the primary as `Part of: <url>`.

Single-repo tasks return one entry from `channels-multi-repo.sh targets ...` with empty `crossLinks`, so the loop is shape-stable.

## Version mismatch handling

The Bitbucket REST API returns `409 Conflict` if `version` is stale. Adapter behaviour:

1. Refetch PR JSON, retry the PUT once with the new `version`.
2. Second mismatch → fail this adapter, log reason, return `{status: "failed", reason: "version conflict"}`. Other adapters keep running  -  non-blocking by Step 7 contract.

## Flags that affect this adapter

| Flag | Effect |
|---|---|
| `--append` | Body merged with existing description instead of replaced. |
| `--ready` | If PR is draft, promote to READY after the description PUT succeeds (uses `gh pr ready` / Bitbucket `PUT ... {draft: false}` sharing the reviewer-preserving payload). |
| `--dry-run` | Generated body printed to stdout, no PUT issued. |

## Hard rules (must not regress)

- Real newlines, no HTML entities  -  heredoc + `jq --rawfile` + `curl --data-binary @file`. Never embed `\n` literally; Bitbucket stores the literal `\n` characters.
- Section order is fixed: `summary` → `changes` → `architecture` (cond.) → `verification` → `dependencies` (cond.) → `related`. Conditional sections may be omitted but never reordered or inserted between fixed ones.
- Humanizer pass runs **after** body assembly and **before** dispatch  -  every body line carries technical, non-AI tone. References at least one symbol/file/line drawn from the diff or pipeline log.
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, branch names, PR titles, commands, type names, and `Closes/Fixes`-style keywords stay verbatim English. The template file (this doc) is English because `promptLanguage="en"` is locked; the body is rendered in the user's language at write-time.
- No body markers (`<!-- channels:start -->`)  -  channels does a full replace each run.
