# Channel adapter  -  PR description

<!-- toc -->
- [Required body structure](#required-body-structure)
- [Markup dialect (per surface, not per pipeline)](#markup-dialect-per-surface-not-per-pipeline)
- [Behaviour by remote](#behaviour-by-remote)
- [Reviewer-preserving Bitbucket payload (required)](#reviewer-preserving-bitbucket-payload-required)
- [Multi-repo cross-links](#multi-repo-cross-links)
- [Version mismatch handling](#version-mismatch-handling)
- [Flags that affect this adapter](#flags-that-affect-this-adapter)
- [Hard rules (must not regress)](#hard-rules-must-not-regress)
<!-- /toc -->

> 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` | `## Geliştirme Özeti` | `## Development Summary` | always |
| 2 | `technical` | `## Teknik Açıklama` | `## Technical Explanation` | always |
| 3 | `architecture` | `## Mimari Kararlar` | `## Architecture Decisions` | when a non-trivial design choice was made |
| 4 | `impact` | `## Etki Analizi` | `## Impact Analysis` | always |
| 5 | `test_scenarios` | `## Test Senaryoları` | `## Test Scenarios` | always |
| 6 | `visuals` | `## Görsel Kanıt` | `## Visual Evidence` | when `state.visualEvidence.required` |
| 7 | `risk` | `## Risk ve Güvenlik` | `## Risk and Security` | when `state.diffRisk.signals` carries a high-stakes signal (`security_path`, `migration`, `public_api`, `no_test_change`, `test_lines_removed`) |
| 8 | `dependencies` | `## Bağımlılıklar` | `## Dependencies` | when deps added/removed/bumped |
| 9 | `build` | `## Build` | `## Build` | always |
| 10 | `related` | `## İlgili` | `## Related` | always (Jira/issue ref; never `Closes/Fixes`) |

`summary`, `impact` and `test_scenarios` carry the same three section keys the
Jira adapter uses, and that pairing is deliberate: the same three questions get
answered on both surfaces, at the register each reader needs. The PR versions name
symbols, files, line counts and build shas. The Jira versions name screens and
behaviour and nothing else (`channels/jira.md`). `technical` has no Jira twin at
all - it is the section whose absence over there is the point.

### Section content rules

**`summary`**  -  2-4 sentences in `outputLanguage`. What broke or what was added, where the user meets it, and the size of the effect when it is measurable (a crash count, a share of a known total, a version range). Past tense, no marketing voice. Code identifiers stay verbatim. This is the one section a reviewer reads before deciding whether to read the rest, so it names the user-visible behaviour before the mechanism.

**`technical`**  -  the account for someone holding the diff: a short paragraph on the mechanism (what the code was actually doing wrong, or what the new code does), then the per-file bullets, then one line of diff stat (`2 files, 8 deletions, 0 insertions`). Where a change is safe for a reason that is not obvious from the diff - an equality relation preserved, an invariant kept, a call site left alone deliberately - that reason belongs here in a sentence, because it is the question the reviewer would otherwise ask in a comment. Tables are welcome when several symbols share a property worth listing side by side.

The bullet list is one item per logically distinct change. Each bullet starts with the touched component and ends with a one-line "what". The source is `$WORKTREE/.pipeline/scope-check.json` `files[].reason` (Phase 2 Step 3.7): a file the dev could not justify there is a file this list cannot describe either, so the bullet quotes the gate output instead of inventing a reason. 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  -  ...` (web), `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.

**`impact`**  -  four fixed numbered parts, each answered, never a placeholder. Same four questions as the Jira `impact` section, answered here with the identifiers and numbers that section is not allowed to carry:

```markdown
## Impact Analysis

**1 - The problem**
<what was wrong, since which version, with the crash/report identifiers and counts>

**2 - What was changed**
<the change, and why behaviour around it is unchanged>

**3 - Affected functions**
<the screens, flows and symbols that must be tested, including shared components the change reaches>

**4 - Effect on other systems**
<none, or which service, contract or channel>
```

Part 3 is the one part of this body that is MEASURED rather than recalled. The
code graph already answers it, so draw the answer instead of re-typing it:

```bash
node "$HOME/.claude/scripts/graph-mermaid.mjs" "<changed symbol[,symbol]>"
```

Append the fenced block it prints under part 3, above the prose. GitHub renders
mermaid natively in pull requests, so this costs no renderer and no plugin. The
prose stays: the diagram says which symbols the change reaches, the sentence says
which screens and flows a tester must open, and neither answers the other.

Exit 1 means the repo has no graph yet (`/multi-agent:graph` builds it) or the
symbol is not in it. That is a gap with a reason, not a failure: write the prose
alone and say the graph was unavailable. Never hand-draw the diagram - a drawn
blast radius nobody measured is worse than none, because a diagram is read as
fact.

The commit line the script prints stays with it. A graph built before the change
draws the radius of an older tree, and the reader has no other way to notice.

This is a GitHub-only section. `channels/jira.md` has no mermaid handling at all:
a fence there converts to a literal `{code:mermaid}` block, so the Jira impact
section keeps its prose. Confluence renders it through the `ac:name="mermaid"`
macro (`md2confluence-v3.py`) when the space has the plugin.

When the change deliberately fixes part of a wider problem, a closing **Risk and remaining scope** paragraph names what is still open and why it was left - a reviewer who can see the rest of the pattern in the repo will ask otherwise, and the honest answer is cheaper written down than defended in a thread.

**`test_scenarios`**  -  the same titled-scenario shape the Jira adapter uses, so the tester reads one list on both surfaces, with symbols allowed here:

```markdown
## Test Scenarios

**1. <what this scenario exercises>**
1. <step>
2. **Expected:** <observable outcome>

**2. <regression scenario>**
1. <step>
2. **Expected:** <what should still behave as before>
```

Reproduction scenarios first, then regressions for whatever the change could have disturbed. When a UI test target covers a scenario, say so on the scenario line and let `## Build` carry the run result - a scenario a machine already ran is not the same request as one a human has to perform, and conflating them wastes the reviewer's time.

**`build`**  -  what was built, on what, and what came out. Not a promise that it builds; the recorded result of the run that happened:

```markdown
## Build

- <build command or scheme> on <base branch>@<sha>: BUILD SUCCEEDED, 0 errors
- Tests: <test command>: <N> passed, <M> failed
- UI tests: <target>: <status> | not run  -  <reason from state.uiTest.notRunReason>
```

The base sha matters because "it builds" is a claim about a merge base, and the reviewer's local tree is usually not that one. `state.uiTest` supplies the third line verbatim, including its `notRunReason` - "no UI test target in this repo" is a result, not a gap, and writing it stops the same question being asked on every PR.

Pick commands for the project's stack  -  the pipeline supports iOS (Swift/Xcode), Android (Gradle), web (npm/pnpm/yarn) and backend (pytest/jest/go test/etc). Multi-repo PRs (one PR per repo) emit the commands for that repo's stack only  -  never mix iOS + Android commands into a single PR body.

**`risk`**  -  only when `state.diffRisk.signals` (Phase 3 Step 1.75) contains a high-stakes signal. Four fixed lines, each answered, never left as a placeholder; the source is Phase 1 `touchedAreas` plus the signals themselves, and when a signal is present the absence of this section is a Phase 4 Step 3 blocker:

```markdown
## Risk and Security

- Auth flow touched: yes | no
- Secret handling changed: yes | no
- Data migration: yes | no
- Rollback: feature flag <name> | git revert <sha> | none, and why
```

**`visuals`**  -  only when `state.visualEvidence.required`. What this section can show depends on where the artefacts are hosted, which Phase 4 resolves into `state.visualEvidence.host`. Render the form for that host and no other.

**`host: jira`.** Filenames, never URLs. A Jira attachment URL is auth-gated and renders as a broken image for anyone reading the PR outside a Jira session, and a broken image is worse than a filename because it looks like the evidence is missing.

```markdown
## Visual Evidence

- Before: `<before-filename>` (attached to PROJ-XXXXX)
- After: `<after-filename>` (attached to PROJ-XXXXX)
- Flow video: `<flow-filename>`, tier <N> (attached to PROJ-XXXXX)
```

**`host: github-public`.** The stills are on the `evidence/<task-id>` branch, so they embed and the reviewer sees them without leaving the PR:

```markdown
## Visual Evidence

**Before**

![before](https://raw.githubusercontent.com/<owner>/<repo>/evidence/<task-id>/<before-filename>)

**After**

![after](https://raw.githubusercontent.com/<owner>/<repo>/evidence/<task-id>/<after-filename>)
```

**`host: github-private`.** Same branch, but a link rather than an embed. GitHub renders markdown images through its own proxy, which has no credentials for a private repo, so an embedded raw URL renders broken for every reader including the author. A blob link opens the image for anyone who can already see the repo:

```markdown
## Visual Evidence

- Before: [<before-filename>](https://github.com/<owner>/<repo>/blob/evidence/<task-id>/<before-filename>)
- After: [<after-filename>](https://github.com/<owner>/<repo>/blob/evidence/<task-id>/<after-filename>)
```

**`host: none`.** Filenames plus the artefact directory, and the reason there is no host:

```markdown
## Visual Evidence

- After: `<after-filename>` (run artefacts: `<artifactsPath>`)
- Not published: <hostReason>
```

**Video is Jira-only.** On a GitHub-hosted run no recording is made and none is published: an mp4 behind a blob link is a download, not something a reviewer opens mid-review, and paying for a recording nobody watches is worse than saying plainly that there is none. The gap line carries that reason.

Every `state.visualEvidence.gaps[]` entry becomes its own line with the reason instead of a filename (`- Before: none  -  the ticket carries no image attachment`). Phase 4 Step 3 blocks on a required artefact that is neither listed nor explained. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.

**`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)

Follow-ups not done in this PR:
- <scope-check.json notDone[].what>  -  <why>
- <deferred triage finding>  -  <triage reason>
```

The follow-up list is present only when `scope-check.json` `notDone[]` or the final triage `deferred[]` is non-empty; the two sources merge into one list.

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. Section order is fixed: `summary` → `technical` → `architecture` (cond.) → `impact` → `test_scenarios` → `visuals` (cond.) → `risk` (cond.) → `dependencies` (cond.) → `build` → `related`.
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 --config <(printf 'user = "%s:%s"\n' "$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 --config <(printf 'user = "%s:%s"\n' "$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` → `technical` → `architecture` (cond.) → `impact` → `test_scenarios` → `visuals` (cond.) → `risk` (cond.) → `dependencies` (cond.) → `build` → `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.
