# Channel adapter  -  Jira comment

<!-- toc -->
- [Required body structure](#required-body-structure)
- [Wiki markup conversion](#wiki-markup-conversion)
- [Cross-link injection](#cross-link-injection)
- [Token resolution](#token-resolution)
- [POST contract](#post-contract)
- [Wiki → Jira auto-link triad](#wiki-jira-auto-link-triad)
- [Hard rules (must not regress)](#hard-rules-must-not-regress)
<!-- /toc -->

> Detailed contract for the `jira` 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 Jira adapter posts a comment on the linked issue. The Jira ticket is shared by all repos in a multi-repo task, so the adapter runs **once** per channels invocation regardless of how many PR targets are dispatched in parallel.

## Required body structure

Every Jira comment posted by this adapter follows the same section order. Section headings render in `prefs.global.outputLanguage` (Turkish when `tr`, English when `en`); section keys and the structure itself are stable across languages so downstream tooling and humans both know where to look.

| # | Section key | Heading (`tr`) | Heading (`en`) | Required? |
|---|---|---|---|---|
| 1 | `summary` | `## Geliştirme Özeti` | `## Development Summary` | always |
| 2 | `test_scenarios` | `## Test Senaryoları` | `## Test Scenarios` | always (use " - " placeholder line if truly N/A) |
| 3 | `impact` | `## Etki Analizi` | `## Impact Analysis` | always |
| 4 | `context_refs` | `## Bağlantılar` | `## References` | when any link exists |

> Heading levels in the source markdown are `##`. The wiki-markup converter (next section) rewrites them to `h2.` for Jira rendering.

**Jira is not the PR, and this is the contract that keeps getting that wrong.**
The PR is read by a reviewer holding the diff. The Jira comment is read by the
person who filed the ticket and by the tester who has to verify it, and neither
of them has the diff open. So a Jira body carries no identifiers, no file paths,
no stack frames, no diff hunks and no framework names. Counts in prose are fine
and are often the most useful sentence in the comment ("two files, eight lines
removed, no additions"); a class name is not. A sentence that needs a symbol to
make its point is describing the change at the wrong level for this reader -
restate the behaviour, not the mechanism. The technical account has a home, and
it is the PR body (`channels/pr.md`).

### Section content rules

**`summary`**  -  2-5 sentences in `outputLanguage`. What changed, why, and the user-visible impact. No "we", no marketing tone. Past tense (the work is done at the time the comment goes up).

**Visual evidence inside these sections.** When `state.visualEvidence` carries artefacts, they render INSIDE `summary` and `test_scenarios` - never as a section of their own, which the fixed section order forbids. Phase 4 Step 2.9 has already uploaded them and written the returned name to `visualEvidence.*[].jiraFilename`: reference that name, and call `jira-attach.sh <issue> <file>...` only for an artefact whose `jiraFilename` is absent. Uploading unconditionally here attaches every file a second time whenever the comment is re-rendered - which is exactly what a post-hoc `/multi-agent:channels` run does.

- `summary`, after its sentences: one line naming the pair in `outputLanguage` (`Düzeltme öncesi / Düzeltme sonrası`), then the thumbnails on the next line - `!<file>-before.png|thumbnail! !<file>-after.png|thumbnail!`.
- `test_scenarios`, under the scenario the recording demonstrates: `!<file>-flow.mp4!` plus one line stating the tier used.

A `gaps[]` entry prints its reason on the line where the artefact would have been (`Düzeltme öncesi: ticket'ta görsel yok`). Never an empty thumbnail, never a silent omission. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.

**`test_scenarios`**  -  one titled scenario per acceptance criterion, each a
numbered list of steps ending in the expected result. Not Given/When/Then: it
reads as translated English to the person who actually runs these, and a tester
wants a list they can follow with the app open.
The heading and the text render in `outputLanguage`; this file shows the English
skeleton:

```markdown
## Test Scenarios

**1. <what this scenario exercises>**
1. <step the tester performs, in the app's own words>
2. <step>
3. **Expected:** <what they should see>

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

Order is load-bearing. The scenarios that reproduce the reported behaviour come
first, then the regression scenarios for whatever the change could have disturbed -
anything sharing the component that was touched. A comment that lists only the fix
leaves the tester to guess the blast radius, and that guess is the one thing they
cannot make from the ticket.

Screens, fields and menu items are named the way the app shows them, in
`outputLanguage`. Function names and file paths do not appear at all, per the rule
above. When there are no executable scenarios (pure doc change, dependency bump),
write a single line: `- No manual test required  -  <short reason>`.

**`impact`**  -  four fixed numbered parts, each answered, never left as a
placeholder. This is what a test lead reads before deciding how wide to test and
what a release manager reads before deciding whether it ships this week, so it is
written in the same plain register as the rest of the comment:

```markdown
## Impact Analysis

**1 - The problem**
<what was wrong and since when, in the reader's words>

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

**3 - Affected areas**
<the screens and flows that must be tested, including anything sharing the changed component>

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

Part 4 is answered "none" far more often than it is answered at all, and "none" is
a real answer worth writing: it is what lets the reader stop looking.

**`context_refs`**  -  flat bullet list of external links the work depends on or produced. PR URL is **not** repeated here; it lives on the first line (see *Cross-link injection*). Common entries (only when present in the task):

```markdown
## References

- Confluence: <page-url>
- Swagger: <openapi-url>
- Figma: <design-url>
- Crashlytics: <issue-url>
- Fortify: <findings-url>
```

The links are pulled from `agent-state.json.contextLinks[]` (see Phase 0 link extraction). Empty `contextLinks[]` ⇒ section omitted.

### Assembly order (per run)

```
1. Read agent-state.json (taskId, contextLinks, prUrls, language).
2. Build section bodies in markdown, in table order: summary, test_scenarios, impact, context_refs.
3. Run the assembled body through the `humanizer` skill (see Hard rules below).
4. Apply Cross-link injection (PR URL on line 1).
5. Run Wiki markup conversion.
6. Run the emoticon escape program on the converted body (see *Emoticon escaping*).
7. POST.
```

## Wiki markup conversion

Body markdown is rewritten to Jira wiki markup before POST:

| Markdown | Jira wiki |
|---|---|
| `## Heading` | `h2. Heading` |
| `### Heading` | `h3. Heading` |
| `**bold**` | `*bold*` |
| `1. item` / `2. item` | `# item` (Jira does not render `1.` as an ordered list) |
| `- item` | `* item` |
| `- [ ] task` / `- [x] task` | `# task` (numbered list  -  Jira renders TODO checkboxes inconsistently) |
| `` `code` `` | `{{code}}` |
| `[text](url)` | `[text\|url]` |
| Tables (`\| h \| h \|` / `\| --- \| --- \|`) | `\|\|h\|\|h\|\|` then `\|c\|c\|` rows |
| Code fences ` ```lang ` | `{code:lang} ... {code}` |

The `\|` sequences in the two link/table rows are this table's own escape for a literal pipe  -  the emitted Jira markup carries `|`, never `\|`.

### Emoticon escaping (required)

Jira's wiki renderer turns ASCII sequences into emoticon images. This is not something the pipeline writes; Jira manufactures it at render time, which is why a comment can show a smiley nobody typed. The escape is a program, not a habit  -  run the converted body through it and POST the output:

```bash
node "$HOME/.claude/scripts/jira-wiki-escape.mjs" /tmp/channels-$TASK_ID-jira.txt \
  > /tmp/channels-$TASK_ID-jira-escaped.txt
```

It prefixes a backslash to each sequence below, skips `{code}` / `{noformat}` blocks, and is idempotent, so running it twice is harmless. `--check` is the inverse: it exits 1 and prints `line: sequence` for every unescaped hit, which is what a gate or a pre-POST assertion calls.

The sequences it covers:

| Sequence | Renders as | Escaped |
|---|---|---|
| `:)` `:(` `:P` `:D` `;)` | face icons | `\:)` `\:(` `\:P` `\:D` `\;)` |
| `(y)` `(n)` | thumbs | `\(y)` `\(n)` |
| `(i)` `(?)` `(!)` | info / question / warning icons | `\(i)` `\(?)` `\(!)` |
| `(/)` `(x)` | tick / cross icons | `\(/)` `\(x)` |
| `(+)` `(-)` | plus / minus icons | `\(+)` `\(-)` |
| `(on)` `(off)` | lightbulb icons | `\(on)` `\(off)` |
| `(*)` `(*r)` `(*g)` `(*b)` `(*y)` | star icons | `\(*)` `\(*r)` ... |

The parenthesised forms are the ones that actually bite: `(x)` in a comparison table, `(!)` in a caution note, and `(/)` in a path fragment are ordinary technical prose, and each becomes an image. The face forms bite through code: a Swift selector like `login(source:input:)` ends in `:)`, and the comment renders a smiley in the middle of a file reference.

The program runs AFTER the markdown conversion above and BEFORE the POST. Order is load-bearing in both directions: run it earlier and the conversion re-introduces sequences behind it; skip it and `{{...}}` monospace does not save you, because Jira parses emoticons inside monospace too.

Do not hand-apply this table. A long comment gives the eye no reason to stop on the `:)` at the end of a selector, so the program applies it or nothing does.

Lines outside these patterns pass through verbatim. Multi-paragraph blocks are joined with one blank line.

Every row above is load-bearing, including the ones that look cosmetic. The table must cover each construct the section templates in this file actually emit  -  `##` for the four required headings, `**1. title**` on a scenario and `**Expected:**` inside it, the `**1 - ...**` part labels in the impact section, and `1.`-numbered steps. A missing row does not degrade gracefully: the "pass through verbatim" fallback POSTs `## Test Senaryoları` and `**Expected:**` as literal text, so the comment renders with visible `##` and stray asterisks. Single `*bold*` in Markdown means *italic* in Jira wiki  -  never map `**bold**` to `*bold*` by dropping one asterisk mechanically without checking the source was bold, not italic.

There is no markdown→Jira-wiki converter program in the pipeline (`lib/` ships `md2confluence-v3.py` for Confluence only, and `scripts/jira-wiki-escape.mjs` covers the emoticon step alone). This conversion table is applied by the model, by hand, which is exactly why it has to be complete.

## Cross-link injection

The adapter prepends the linked PR URL on the **first line** so reviewers can jump to the PR with one click:

```
PR: https://github.com/<org>/<repo>/pull/4321

Phase 3 review accepted 2 findings...
```

In multi-repo mode, `channels-multi-repo.sh render-jira <state> <body>` prepends a bulleted `* PR:` list  -  primary first, extras after. Single-repo tasks fall through to the unchanged body.

## Token resolution

Token comes from `prefs.global.keychainMapping.jira` → `~/.claude/lib/credential-store.sh get "$ACCOUNT_JIRA_TOKEN_KEY"`. Resolution path:

1. Read `keychainMapping.jira` from prefs.
2. Resolve via `credential-store.sh get`  -  the driver auto-delegates to `keychain.py`.
3. Missing → adapter prompts the inline Token Save Flow from `setup.md` (clipboard-based); user can save now or skip this run.
4. Expired (401) → same Save Flow with the "expired" trigger; on skip, returns `{status: "skipped", reason: "token expired"}`, does not block other adapters.

## POST contract

```
POST /rest/api/2/issue/{id}/comment
Authorization: Bearer $JIRA_TOKEN
Content-Type: application/json
```

**Post through `jira-publish.sh`. Never hand-roll the `curl`.**

```bash
bash "$HOME/.claude/lib/jira-publish.sh" --issue "$JIRA_ID" \
  --body-file /tmp/channels-$TASK_ID-jira.txt --target comment
```

That script escapes every body unconditionally (`lib/jira-publish.sh:109`) and
passes the token through a `-K` config rather than argv, so neither the escape
nor the token handling depends on an agent remembering a step.

Not a hand-written `jq` + `curl` pair with the escape as a separate `--check`
line above it. In that shape the escaper is present, correct, and simply not run
on the POST path: a Swift selector such as `hash(into:)` renders as `hash(into`
plus a smiley, because `:)` reaches Jira intact and
Jira's wiki renderer turned it into an emoticon, which is what section
*Emoticon escaping* below is about. A defence that has to be invoked by hand is
a defence that is eventually not invoked.

The dispatch summary line includes the comment URL with `?focusedCommentId=...` so the user can paste it into Slack/Teams.

### Writing the issue description (not a comment)

A comment is additive; the `description` field is not. Replacing it destroys whatever was there, which is usually the reporter's own text. There is no hand-rolled `PUT /rest/api/2/issue/{key}` anywhere in the pipeline and there must not be one: the only supported path is

```bash
bash "$HOME/.claude/lib/jira-publish.sh" --issue "$KEY" --body-file "$F" \
  --target description --mode append
```

which escapes the body, GETs the current description, saves it under `~/.claude/logs/multi-agent/jira-backups/`, appends below a `----` rule, and exits 3 rather than replacing a non-empty field without `--confirm-overwrite`. The same script posts comments (`--target comment`), which is what an unattended run should choose when it is not sure. Contract and gate: `analysis/render.md` Phase 4 and `scripts/smoke-jira-publish.sh`.

## Wiki → Jira auto-link triad

When the **Wiki** adapter writes pages on the same run AND `prefs.global.wikiToJiraComment === true`, the Wiki adapter also posts a humanizer-passed Jira comment summarizing wiki pages (component name + variant count + wiki URL). That secondary comment is independent of this adapter  -  both can coexist on the same issue. Full contract: `$HOME/.claude/multi-agent-refs/issue-jira-triad.md`.

## Hard rules (must not regress)

- Real newlines, no HTML entities  -  heredoc + `jq --rawfile` + `curl --data-binary @file`.
- UTF-8 in, UTF-8 out  -  the body file is UTF-8 and `--data-binary` ships its bytes verbatim. Never round-trip the body through `unicode_escape`, `latin-1`, or any re-encode step, and never hand-roll a Python/curl helper that re-decodes it: that mangles Turkish chars (ç ş ı ö ü ğ) into mojibake (`Çözüm` → `ÃÃ¶zÃ¼m`). Use the `jq --rawfile` + `--data-binary @file` path above as-is. Same rule for the PR / Confluence / Wiki adapters.
- Section order is fixed: `summary` → `test_scenarios` → `context_refs`. Never insert sections between them; never reorder.
- Humanizer pass runs **after** body assembly and **before** wiki-markup conversion. Tone target: informal but technical. No marketing voice, no "we are excited", no "I have...".
- **No decorative glyphs anywhere in the comment body**  -  neither emotive (    ) nor status glyphs ([done] [pending] failed skipped active ). The comment is plain technical prose. Status is written in words: `[done]` / `[pending]`, and `done · active · failed · skipped · pending` in the phase strip. This applies to every channel, not only Jira  -  see `channels/README` note in `phase-5-report.md`.
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, branch names, and the wiki-markup syntax stay verbatim. The `promptLanguage="en"` lock means any LLM prompt that produces the body is in English; the body itself is then rendered in the user's language.
- Adapter failures are non-blocking  -  a missing token or 401 returns `skipped`/`failed`, the loop keeps going for PR / Confluence / Wiki.
