# Render, publish, report (Phases 3, 3.5, 4, 5)

> Render, humanize, validate, publish and report. Loaded on demand by `/multi-agent:analysis` and by pipeline Phase 1. The humanizer pass and `validate-analysis-doc.mjs` are required here in every mode: a document that skipped either is not shippable.

### Phase 3 - Humanize & buffer drafts (no side effects)

1. **Language resolution**: read `prefs.global.outputLanguage` (`tr` or `en`, default `tr`). Use the `Output language matrix` table in `$HOME/.claude/multi-agent-refs/analysis-template.md` to swap headings and system strings.

1b. **Template selection** (Locked 31): read `state.analysisSpec.profile`. `global` renders against `$HOME/.claude/multi-agent-refs/analysis-template.md`; `corporate` renders against `$HOME/.claude/multi-agent-refs/analysis-template-corporate.md`. The evidence is the same either way - this step chooses the projection, nothing else. In the corporate profile the Part A backbone and the footer render even with zero evidence, carrying `N/A` or `EKLENECEK`, and each `EKLENECEK` emits its Section 20 row (Locked 32).

2. **Markdown render**: for each platform in `state.analysisSpec.platforms[]`, concatenate the per-platform spec into one markdown file. Tables in pipe-syntax. Each rendered section keeps its canonical number, so omitted sections leave gaps (Locked 2). Corporate Part A and footer never drop; Parts B and C follow the omission table.

   **Missing inputs panel** above Section 1: `build-references.mjs "$STATE" --lang "$LANG" --missing-panel` (prints nothing when every source was read). "Not reachable" is never written as "does not exist".

   **When `platforms[]` is empty** (Locked 34), the loop runs once per channel derived from the evidence (`mobile`, `web`, or one channel-agnostic pass). The development layer (corporate Part C, global Sections 13-15) and the Pass B projection are skipped, Section 20 records that they await a repo selection, and front-matter `platform` reads `none`. Everything else renders in full.

3. **Humanizer pass (required: actually invoke the `ai-common-toolkit:humanizer` skill on the rendered markdown - the punctuation grep alone does NOT satisfy this step)** (`technical-explanatory` tone for the scratch buffer; per-channel re-humanize happens in Phase 4 when actually emitting):
   ```
   ai-common-toolkit:humanizer skill input:
     language: <tr|en>
     tone: technical-explanatory
     stripFancyPunctuation: true
     content: <markdown>
   ```

   **Explicit punctuation policy** (enforced by `stripFancyPunctuation: true`): no em-dash (U+2014), no en-dash (U+2013), no horizontal ellipsis (U+2026), no curly quotes (U+2018, U+2019, U+201C, U+201D), no section sign (U+00A7). The humanizer replaces these with ASCII equivalents (`-`, `:`, `,`, `...`, `'`, `"`, and `bölüm` / `section` for the section sign per `outputLanguage`) before emit. Tables, code blocks, URLs, and front-matter YAML are exempt. Verify with `node $HOME/.claude/scripts/validate-analysis-doc.mjs <file>`, not a Perl-regex grep (BSD grep lacks it, so "zero matches" is trivially true). Readability rules live in the humanizer skill; this paragraph owns only the punctuation policy. **Diacritics are PRESERVED: only the listed codepoints are replaced. Turkish letters (ş, ç, ğ, ı, İ, ö, ü) stay verbatim; never ASCII-fold (`Geliştirme Özeti`, not `Gelistirme Ozeti`). ASCII-folded Turkish fails review.**

3b. **Build Section 21 References** (Locked 33). Fingerprint the sources first (`features/analysis-sources.md`), then emit the table:

   ```bash
   node "$HOME/.claude/scripts/analysis-sources.mjs" fingerprint --state <state.json> --write --sidecar "<draft>.sources.json"
   node "$HOME/.claude/scripts/build-references.mjs" <state.json> --lang "$OUTPUT_LANGUAGE"
   ```

   Paste the table under References; never hand-write it. It projects `state.analysisSpec.evidence.*`, so an unread source cannot appear and a read one cannot go unlisted.

4. **Write scratch drafts**: create `/tmp/analysis-<feature-slug>-<UTC-iso8601>/` with one `<feature>-<platform>.md` per platform; with `platforms[]` empty, one per derived channel (`<feature>-mobile.md`, `<feature>-web.md`; Locked 34), or a single `<feature>.md` when no interface. Set `state.analysisSpec.outputs.draftDir`.

5. **Surface the draft tree to the user**:
   ```
   Drafts ready (3 files, 6.4 KB):
     /tmp/analysis-UserProfile-20260514T1015/UserProfile-ios.md
     /tmp/analysis-UserProfile-20260514T1015/UserProfile-android.md
     /tmp/analysis-UserProfile-20260514T1015/UserProfile-backend.md
   ```
   User can inspect drafts before choosing output destinations in Phase 3.5.

### Phase 3.2 - Draft review (BLOCKING, runs in every mode)

Locked 35 carries the rule. Operationally:

1. Run `phases/phase-3-review.md` Step 0, the analysis-mode branch. It already defines
   the strict validator, the host's three-reviewer set with its model routing, and the
   triage. Do not restate it here; a second definition is the one that rots.
2. Dispatch reviewers as subagents, each given the draft path, the state JSON and
   `analysis/review.md`. Never review from this context: it cannot notice a search it
   never thought to run.
3. Blocking -> back to Phase 2b, dispatch closed, never an open question. Advisory ->
   Phase 3.3. Cap at two returns, then stop and surface what blocks.

Autopilot included. State: `phase = "reviewing_draft"`.

### Phase 3.3 - Close the gaps (BLOCKING)

Phase 3.2 produced the list. Sort every entry and act:

| Bucket | Meaning | Action |
|---|---|---|
| A. Not searched | The evidence is reachable and this run did not look | Run the search; it closes, and never becomes a question |
| B. The user knows | Channel scope, which service backs a screen, what is in scope | Ask, `AskUserQuestion` per `picker-contract.md`, at most 4 per call |
| C. External | Final copy, a legal basis, a contract nobody has written | Record as `AS-NN` in Section 20 |

**Bucket A must be empty before Phase 3.5.** A gap reaches C only with a stamp:
`searched, not found` naming what was searched, or `asked, external`. Unstamped
`EKLENECEK` fails the dispatch gate. A is where most open questions came from in
practice, and none were questions - they were searches nobody ran.

B is asked in every interactive mode; autopilot moves B to C stamped `autopilot: could
not ask`, so an unanswerable gap reads differently from an unasked one. A stays required
under autopilot: a search needs no human.

Re-render affected sections, then report gaps closed by search, by asking, and entered.
State: `phase = "reviewing_gaps"`.

### Phase 3.5 - Output destination picker

AskUserQuestion (multiSelect=true), at least one selection required. `Local file` pre-selected per Locked decision 5.

```
header: "Output"
question: <localized: "Where should the analyses be written?">
options:
  - label: "Local file"
    description: <resolved path, shown literally: "<repo>/analysis/<feature>-<platform>.md" per selected repo, or "~/Desktop/multiAgentAnalysis/<feature-name>/<feature>-<channel>.md" when no repo was selected>
  - label: "Confluence page"
  - label: "Jira issue"
    description: <localized: "As a comment by default; writing the description is a separate, explicit choice">
```

Conditional follow-ups:
- If `Confluence` selected: ask `header="Parent page"`, free-text via Other for the parent page key or URL. One Confluence page per emitted document is created under this parent, each titled `<Feature> - <Platform>` (or `<Feature> - Mobile` / `<Feature> - Web` on a repo-less run, or a bare `<Feature>` for a single channel-agnostic document).
- If `Jira` selected: if the Step 5 Jira question produced Jira IDs, AskUserQuestion (single-select) to pick which one; otherwise ask via Other. Per Locked decision 9 + design choice "single description with platform separators", the chosen issue receives one combined body holding all per-platform sections under `h2. Platform: <X>` separators (wiki markup  -  neither the comment nor the description field renders Markdown; conversion happens at dispatch, see Phase 4).

  Then ask **where in the issue it goes**, because two of the three answers can destroy text somebody else wrote:

  ```
  header: "Jira write"
  question: <localized: "Where should the analysis go on {ISSUE-KEY}?">
  options:
    - label: "Comment"
      description: <localized: "Added as a new comment. Nothing existing is touched.">   # pre-selected
    - label: "Description - append"
      description: <localized: "Kept below the current description, separated by a rule.">
    - label: "Description - replace"
      description: <localized: "Replaces the current description. Its text is backed up first and the run tells you what was there.">
  ```

  `Comment` is pre-selected and is the default on any non-answer, including autopilot. The description of an issue is often the analyst's original requirement text, and an analysis run is not a reason to lose it. Result: `state.analysisSpec.outputs.jira = { key, target: "comment"|"description", mode: "append"|"replace" }`.

Result: `state.analysisSpec.outputs.requested[]`.

### Phase 4 - Dispatch & report

**Pre-dispatch gate (BLOCKING).** Before writing to any destination, run the deterministic doc validator on every per-platform draft:

```bash
for f in /tmp/analysis-<feature-slug>-<ts>/*.md; do
  node "$HOME/.claude/scripts/validate-analysis-doc.mjs" "$f" || GATE_FAILED=1
  node "$HOME/.claude/scripts/build-references.mjs" <state.json> --check "$f" || GATE_FAILED=1
  node "$HOME/.claude/scripts/verify-citations.mjs" "$f" --repo "$REPO_ROOT" || GATE_FAILED=1
done
```

`validate-analysis-doc.mjs` enforces the mechanically-checkable Locked decisions on the emitted markdown itself (front-matter completeness, never-omitted sections per Locked 2, humanizer punctuation per Locked 7, Full-mode business-rule traceability per Locked 30, and in the corporate profile the backbone presence and `EKLENECEK`-to-Section-20 pairing per Locked 32). `build-references.mjs --check` runs the References coverage gate (Locked 33): a source the run consumed but did not list, or a listed row with no evidence behind it, blocks dispatch. `verify-citations.mjs` resolves each claimed `file:line` at HEAD with `git cat-file`, so `Foo.swift:9999` in a repo with no Foo.swift blocks dispatch (Locked 3, Locked 36). Any ERROR blocks dispatch: fix the draft and re-validate. Warnings are advisory (run with `--strict` to treat them as blocking). This turns the "fails the dispatch gate" prose into a real, model-independent check.

Iterate `state.analysisSpec.outputs.requested`. For each target:

| Target | Action |
|--------|--------|
| Local | For each per-platform draft, `cp /tmp/analysis-<feature-slug>-<ts>/<feature>-<platform>.md` into `analysis/<feature>-<platform>.md` in the matching repo's working tree. When multiple repos exist for the same platform, the file is duplicated into each and the dispatch report lists every destination. **When no repo was selected** (Locked 34) there is no working tree to be relative to, so files land in `~/Desktop/multiAgentAnalysis/<feature-name>/`, one per derived channel (`<feature>-mobile.md`, `<feature>-web.md`) or a single `<feature>.md` when none could be derived. The current working directory is never written to: for a repo-less run it is arbitrary, and creating a folder in whatever directory the command happened to be invoked from is the kind of surprise that costs a tool its trust. The user can override the path through the picker's Other input. **No commit.** |
| Confluence | Re-humanize each draft with `formal-stakeholder` tone. One page per draft under the chosen parent, titled `<Feature> - <Platform>` - including a repo-less run, whose drafts are the derived channels (`<Feature> - Mobile` / `<Feature> - Web`, Locked 34). A single channel-agnostic draft becomes one page titled `<Feature>`, with no suffix implying a split that was not made. Cross-link siblings inside each page via `<ac:link><ri:page ri:content-title="<Feature> - <OtherPlatform>"/></ac:link>`; a lone page has no sibling block. Markdown -> storage XML via `$HOME/.claude/multi-agent-refs/channels/confluence.md`. Re-emit on existing pages uses PUT with version bump. |
| Jira | Re-humanize the combined body with `informal-technical` tone. Concatenate the drafts under `h2. Platform: <X>` separators in production order - `iOS`, `Android`, `Web`, `Backend` repo-backed, `Mobile` / `Web` for channels derived on a repo-less run (Locked 34). A single channel-agnostic draft gets no separator: a heading announcing a split of one is noise. then run the whole body through the markdown → Jira wiki conversion table in `$HOME/.claude/multi-agent-refs/channels/jira.md`  -  both the comment body and the `description` field render wiki markup, so raw `##`/`**`/backticks arrive as literal text. Write the converted body to a file and publish it with `$HOME/.claude/lib/jira-publish.sh`, never with a hand-rolled `curl`: <br><br>`bash "$HOME/.claude/lib/jira-publish.sh" --issue "$KEY" --body-file "$F" --target comment` <br>`bash "$HOME/.claude/lib/jira-publish.sh" --issue "$KEY" --body-file "$F" --target description --mode append` <br><br>The script owns the parts that are easy to get wrong: it runs `jira-wiki-escape.mjs` on the body, resolves host + token without putting either in argv, and on the description path it GETs the current value, writes it to a backup under `~/.claude/logs/multi-agent/jira-backups/` and reports the path, appends below a `----` rule by default, and **refuses with exit 3** when `--mode replace` would discard a non-empty description unless `--confirm-overwrite` is passed. Exit 3 is reported to the user with the backup path, never retried with the flag added automatically  -  only the user's explicit "Description - replace" answer from Phase 3.5 supplies it. `--dry-run` previews the exact final body without writing. |

**Output capture**: fill `state.analysisSpec.outputs.localPaths[]` (one entry per per-platform-per-repo write, or one per derived channel on a repo-less run), `outputs.confluencePageUrls[]` (one entry per emitted document), `outputs.confluencePages[]` (the same pages by IDENTITY - `pageId`, `title`, `space`, `channel` - because a write-back needs the id and a `/display/SPACE/Title` URL cannot be parsed for one), `outputs.jiraIssueKey` (single string).

### Phase 5 - Report & stop

After dispatch, print a summary to the user (in `outputLanguage`). Example shape:

**Operational reporting.** Analysis is a pipeline of its own now - its own phases, its own gates, its own report - so it emits a run record like any other run. Without this the panel shows dev runs only, and the command people reach for before writing code is invisible in the usage data.

```bash
STATE="$HOME/.claude/logs/multi-agent/$TASK_ID/agent-state.json"
mkdir -p "$(dirname "$STATE")"
STATUS=complete   # or failed when the run could not emit its document
printf '%s' "{\"taskId\":\"$TASK_ID\",\"command\":\"/multi-agent:analysis\",\"mode\":\"analysis\",\"project\":${PROJECT_JSON:-null},\"status\":\"$STATUS\",\"finishedAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
  | node "$HOME/.claude/scripts/write-state.mjs" "$STATE" >/dev/null 2>&1 || true
node "$HOME/.claude/scripts/usage-report.mjs" --state "$STATE" >/dev/null 2>&1 || true
```

`mode` is `analysis`, one of the modes `schemas/phases.json` declares; the reporter sends only those, and anything else as `unknown`. `status` is `complete` or `failed` (agent-state.schema.json), which the reporter turns into the completed or failed run status. `project` is digested by the reporter before it leaves the machine, and is `null` on a stack-optional run - there is no repository to name. A failure here never affects the run: the reporter is best-effort and the summary above is already printed.

```
Feature: <featureName>
Language: <tr|en>
Platforms: iOS, Android, Backend
Repos: my-ios-app (write), my-android-app (write), my-backend-api (write)

Sources used:
  Figma:     2 frames
  Swagger:   1 file
  Confluence: 1 page (1 fetch error: auth_required)
  Jira:      1 issue
  Firebase:  3 events from schema, 1 from auto-detect
  Standards: 2 local files + 1 wiki page
    - /Users/.../<project>-Standards.md
    - /tmp/<repo>-wiki/Navigation.md
    - ~/.claude/rules/<framework>-qa.md (auto-detected)

Repo evidence (reuse vs new):
  iOS my-ios-app:        4 direct-match, 6 same-domain, 11 cross-cutting
  Android my-android-app: 2 direct-match, 8 same-domain, 14 cross-cutting
  Backend my-backend-api: 1 direct-match, 3 same-domain, 7 cross-cutting

Sections (per platform):
  iOS:      [1 Scope, 2 Design, 3 Localization, 4 API, 5 Deeplink/Push, 6 Business+Tests, 7 Dev Plan]
  Android:  [1 Scope, 2 Design, 3 Localization, 4 API, 5 Deeplink/Push, 6 Business+Tests, 7 Dev Plan]
  Backend:  [1 Scope, 4 API, 6 Business+Tests, 7 Dev Plan]    (Sections 2/3/5 omitted - no UI)

Outputs:
  - analysis/UserProfile-ios.md       (my-ios-app)
  - analysis/UserProfile-android.md   (my-android-app)
  - analysis/UserProfile-backend.md   (my-backend-api)
  - Confluence (3 pages):
      https://{CONFLUENCE_HOST}/pages/viewpage.action?pageId=...    (iOS)
      https://{CONFLUENCE_HOST}/pages/viewpage.action?pageId=...    (Android)
      https://{CONFLUENCE_HOST}/pages/viewpage.action?pageId=...    (Backend)
  - Jira: {JIRA_KEY}-12345 (description updated with 3 platform sections)

Provenance:
  evidence_digest: sha256:1f3a9c2   base_commit: 4c1b2de
```

**Provenance line.** Print the front-matter's `evidence_digest` and `base_commit` in the report too. A timestamp cannot separate two reports made the same day; these two say which side moved.

**Stop. Do not chain into a dev run. Do not open a worktree. Do not create a branch.**

**Open-question follow-up**: when any rendered file's Section 20 (Risks and Open Questions) has rows with status `Acik / Open`, append one line to the report: `<localized: "Section 20 has <N> open rows. Run /multi-agent:analysis-resolve to resolve them interactively before dispatching to dev.">`. This is a suggestion line only - never auto-invoke the resolver. With no open row, the suggested next step is `/multi-agent:analysis-jira`.


## Confluence write (on-request only)

Confluence is NEVER default-selected at Phase 3.5. Only when the user explicitly asks to post does the command:

1. Resolve the token via `~/.claude/lib/credential-store.sh get <key>`, where `<key>` is read from `prefs.global.keychainMapping.confluence` (per-user mapping; never hardcode the service name in this doc - see channel adapter doc `$HOME/.claude/multi-agent-refs/channels/confluence.md` for the lookup contract).
2. Use parent page URL from user input. No default parent is hardcoded here; the user picks one at the prompt (LRU recents come from `prefs.projects[<project>].confluenceUrls`).

   **Corporate profile exception.** With the bindings configured, the prompt is skipped: space `prefs.global.analysisProfile.corporate.confluenceSpaceKey`, parent `prefs.global.analysisProfile.corporate.confluenceParentPageId`, title from `prefs.global.analysisProfile.corporate.titleFormat` with `prefs.global.analysisProfile.corporate.titlePrefix` filling `{prefix}`. Any of the four missing falls back to the prompt; a colliding title becomes an update (PUT with version bump), never a second page.
3. **Publish with `md2confluence-v3.py`, never a hand-rolled conversion + curl.** It
   does the storage-XML conversion (`channels/confluence.md`), uploads every PNG in
   `--attachments-dir`, injects `<ac:image><ri:attachment/>` so a frame gallery renders
   as pictures (Locked 18), and warns on an image reference no attachment matched.

   ```bash
   CONFLUENCE_BASE_URL="https://$(jq -r '.global.hosts.confluence // empty' "$HOME/.claude/multi-agent-preferences.json")"
   export CONFLUENCE_BASE_URL
   python3 "$HOME/.claude/lib/md2confluence-v3.py" create \
     --space "$SPACE" --parent-page-id "$PARENT" --title "$TITLE" \
     --markdown "$DRAFT" --attachments-dir "$(dirname "$DRAFT")"
   # updating an existing page: `update --page-id <ID>` instead of `create --space ...`
   ```

   Screenshots must already sit in `--attachments-dir`: point `figma-screenshot.sh
   --output-dir` at the draft directory during evidence collection, or the gallery
   renders filenames. MCP asset URLs expire after 7 days; cells reference the local file.
   **Draft title:** while `status` is not `final`, prefix `[TASLAK] ` / `[DRAFT] ` (or fill the corporate `titleFormat` `{status}`); the final publish drops it.
4. Read the script's JSON envelope (page id and URL, attachments, `image reference has
   no matching attachment` warnings). A warning means missing pictures: surface it, not a
   clean publish. Surface the page URL in the Phase 4 report.
5. **Read the page back:** `confluence-readback.mjs --page-id "$PAGE_ID" --markdown "$DRAFT"`. Exit 1 names missing headings and unattached images; report it, never republish automatically.

Token miss handling: surface a single line `WARN: Confluence token not configured (prefs.global.keychainMapping.confluence). Run /multi-agent:setup or skip Confluence output.` and continue with local-only output.
