# Channel adapter  -  Jira comment

> 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` | `## Yapılan Çalışma Özeti` | `## Work Summary` | always |
| 2 | `test_scenarios` | `## Test Senaryoları` | `## Test Scenarios` | always (use " - " placeholder line if truly N/A) |
| 3 | `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.

### 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).

**`test_scenarios`**  -  Given/When/Then numbered list. One scenario per acceptance criterion. The heading and scenario text are rendered in `outputLanguage` at write-time; the template itself (this file) shows the English skeleton:

```markdown
## Test Scenarios

1. **Given** a signed-in user
   **When** they navigate to the Profile screen
   **Then** their saved preferences appear in the configured order
2. **Given** ...
```

Code identifiers (function names, file paths) stay verbatim  -  they are not translated. When there are no executable scenarios (pure doc change, dependency bump), write a single line: `- No manual test required  -  <short reason>`.

**`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  -  summary first, test_scenarios next, context_refs last.
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. It was hand-applied for several releases and a smiley reached a ticket anyway  -  a long comment gives the eye no reason to stop on the `:)` at the end of a selector.

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 three required headings, `**Given**` / `**When**` / `**Then**` in the test-scenario skeleton, and `1.`-numbered scenarios. A missing row does not degrade gracefully: the "pass through verbatim" fallback POSTs `## Test Senaryoları` and `**Given**` 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 4 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` on macOS / Linux and PowerShell `CredentialManager` on Windows.
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
```

Body assembled with `jq --rawfile` + `curl --data-binary @file`, from the escaped file that *Emoticon escaping* produced:

```bash
node "$HOME/.claude/scripts/jira-wiki-escape.mjs" --check /tmp/channels-$TASK_ID-jira-escaped.txt \
  || { echo "unescaped Jira emoticon in the body - do not POST" >&2; exit 1; }
jq -n --rawfile body /tmp/channels-$TASK_ID-jira-escaped.txt '{body: $body}' \
  > /tmp/channels-$TASK_ID-jira-payload.json
curl -s -X POST -H "Authorization: Bearer $JIRA_TOKEN" \
     -H "Content-Type: application/json" \
     --data-binary @/tmp/channels-$TASK_ID-jira-payload.json \
     "$JIRA_BASE/rest/api/2/issue/$JIRA_ID/comment"
```

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-7-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.
