---
description: "Customer-complaint triage. Ingests complaints (paste, csv/xlsx/txt/json file, Jira issue, Confluence URL), fetches Graylog evidence per trx/conversation id, correlates read-only against the selected client + BFF repos; per complaint: client/bff root cause + fix plan + dev prompt, or core routing recommendation, or insufficient-evidence. Report only, no dev chaining. Use when customer-reported errors need layer triage."
description-tr: "Müşteri şikayeti / müşteri kaynaklı hata triyajı. Şikayetleri alır (serbest metin, csv/xlsx/txt/json dosya, Jira issue, Confluence URL), trxId/conversationId ile Graylog kanıtı çeker, seçilen client + BFF repolarıyla salt-okunur eşleştirir ve her şikayeti sınıflandırır: client / bff (bizim sorumluluğumuz, kök neden analizi) veya core (core ekibine yönlendirme önerisi) veya insufficient-evidence. Rapor üretip durur - branch, worktree, commit, PR veya dev zinciri yok."
argument-hint: "[\"<run-name>\"] [--file <path>] [<jira-id | jira-url | confluence-url> ...]"
---

# multi-agent complaint-analysis - Customer Complaint Triage

**Input**: $ARGUMENTS (optional run name, optional `--file <path>`, optional Jira ids / Jira URLs / Confluence URLs; remaining free text is treated as pasted complaints)

Triage of customer-reported errors across the layers the team owns (client apps + BFFs). Per complaint: Graylog log evidence by trx/conversation id, read-only repo correlation, and a verdict. **Core failures get a routing recommendation, never a fix analysis.**

**Side-effect contract**: may write a local markdown report, post a Confluence page, or add a Jira comment/description - but **never** creates branches, worktrees, commits, or PRs. Stops at the report.

> **Language**: Per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix - instruction prose stays English. `AskUserQuestion.question`, `.options[].label` and `.options[].description` follow `outputLanguage`; only `header` stays English (<=12-char chip). The report body follows `outputLanguage`; Graylog excerpts, verdict tokens, and external payload metadata stay English.

## Locked decisions (do not re-ask)

1. **Graylog is primary evidence (deliberate departure).** `features/external-context-injection.md` declares Graylog advisory-only for the dev pipeline; in THIS command Graylog IS the evidence backbone. A per-complaint fetch failure still never halts the run, but that complaint's verdict is capped at `insufficient-evidence` - never fabricated.
2. **Ids are never guessed.** A complaint with no trxId and no convId enters the Phase 0 Step 5 confirmation loop: the user supplies the id(s) or explicitly marks the complaint `skip-graylog`. Empty submit is not consent (`feedback_no-inferred-defaults-from-empty-answer`); re-ask.
3. **Read-only ops command.** No worktree, branch, commit, PR, or dev chaining. Report + Stop.
4. **Redaction before any output.** Complaint text is redacted at intake (`parse-complaints.sh`, default on). Jira / Confluence-sourced complaint text passes through the same redaction (`--stdin` mode) after fetch. Raw PII never enters state, drafts, or any dispatch target.
5. **Core is a residual classification, not a repo.** Verdict `core` produces a routing recommendation only - no fix analysis, no core-repo grepping, no core code speculation.
6. **Layer mapping is user-confirmed.** Name-based layer inference (Step 3b) is a proposal; the confirmation table must be approved before Phase 1.
7. **Output default = Local file.** The Phase 4.5 picker keeps `Local file` pre-selected; Confluence and Jira are never default-selected.
8. **Humanizer punctuation policy is non-negotiable.** No em-dash, en-dash, ellipsis, curly quotes, or section sign in any emitted text. Turkish diacritics are preserved verbatim - never ASCII-fold the prose.
9. **Language split.** Report body follows `outputLanguage`; verdict tokens (`client:ios`, `bff:mobile-bff`, `core`, `insufficient-evidence`), Graylog evidence excerpts, and Confluence/Jira payload metadata stay English.
10. **Verdict citation discipline.** Every `client`/`bff` verdict cites at least one Graylog message (timestamp + source) AND one repo evidence row (`file:line`). Anything less is `insufficient-evidence`. Every `core` verdict cites the Graylog message that names the upstream service.
11. **One complaint batch per run.** Mixed batches spanning unrelated products are user error: surface it and ask to split.
12. **No auto-commit.** The local report is written to the working tree; the user commits it themselves if they want.
13. **Client/bff verdicts carry a development handoff.** Every `client`/`bff` verdict renders a fix plan grounded in the EXISTING architecture (the `repoEvidence[]` files are the reference: name the concrete files/components to touch, reuse-first, no invented structures) plus a ready-to-run dev prompt (English, one fenced block, `/multi-agent`-compatible). The handoff is part of the report - this command still never runs dev itself (Locked 3). Core and insufficient-evidence verdicts never get a fix plan (Locked 5).

## Steps

### Phase 0 - Intake

Sequential `AskUserQuestion` chain, answers land under `state.complaintSpec.*` (schema: `$HOME/.claude/schemas/complaint-analysis-spec.schema.json`). Step narration per `$HOME/.claude/multi-agent-refs/picker-contract.md`: print `<localized: "Step <i>/<n>: <what this step decides>">` before each picker; auto-resolved steps still print their breadcrumb.

#### Step 0 - Language resolution (BLOCKING)

Read `prefs.global.outputLanguage` (`tr` or `en`, default `tr`) before the first picker. Every `<localized: "...">` marker in this file is rendered in that language, never emitted literally.

#### Step 1 - Run name

From `$ARGUMENTS` quoted string if present, else default `complaints-<YYYYMMDD>` (announce, do not ask). Result: `state.complaintSpec.runName`.

#### Step 2 - Account picker

Reuse `$HOME/.claude/multi-agent-refs/_account-picker.md`. Needed for Graylog-adjacent Jira/Confluence fetches and dispatch; skipped only when the run is fully local (no Jira/Confluence input, local output only) - then `account: null`.

#### Step 3 - Repo multi-select

Reuse `$HOME/.claude/multi-agent-refs/_repo-picker.md` (multi-select via `~/.claude/lib/repo-cache.sh`). Guidance line in the question: <localized: "Select the client and BFF repos your team owns (e.g. ios / android / web / mobile-bff / web-bff). Core services are NOT selected - core is a triage outcome, not a repo.">

#### Step 3b - Layer tagging (single confirmation table)

Infer a layer per selected repo from its name (`ios|iphone` -> ios, `android` -> android, `web(?!.*bff)` -> web, `mobile.?bff|bff.?mobile` -> mobile-bff, `web.?bff|bff.?web` -> web-bff, else other). Present ONE AskUserQuestion with the full `repo -> layer` table in the question body: options `Approve mapping` / `Override rows`. On override, one follow-up per rejected row with the 6 layer options. Result: `state.complaintSpec.repos[] = {name, layer, localPath?, provider?}` (Locked 6).

#### Step 4 - Complaint input (multi-source)

Classify every `$ARGUMENTS` remainder and any pasted input via `~/.claude/lib/context-link-extractor.sh`, then collect per source type. If nothing was supplied, ask: <localized: "Paste the complaints, or give a file path (csv / xlsx / txt / json), a Jira issue, or a Confluence URL. Mixed input is fine.">

| Source | Ingestion |
|---|---|
| File path (`--file` or detected) | `~/.claude/lib/parse-complaints.sh --file <path>` (format auto-detected; exit 5 = xlsx degrade -> surface `degradeReason`, ask for a CSV export path, re-run) |
| Pasted free text | `parse-complaints.sh --stdin` (splits blocks, pre-fills ids via the shared label set) |
| Jira id / URL | Fetch issue summary + description + comments via Jira REST (account token); concatenate as text blocks, pipe through `parse-complaints.sh --stdin` (Locked 4) |
| Confluence URL | `~/.claude/lib/fetch-confluence.sh <url>`; page body paragraphs/table rows become text blocks, piped through `parse-complaints.sh --stdin` |

Merge all outputs into one list, re-numbering ids `C-01..C-NN`. Result: `state.complaintSpec.input` + `state.complaintSpec.complaints[]`.

#### Step 5 - Id confirmation loop (Locked 2)

Render the parsed table (id, redacted excerpt <= 80 chars, trxId, convId, platformHint) to the user. For every complaint missing BOTH ids, one AskUserQuestion: <localized: "Complaint <id> has no trx/conversation id. Supply one, or skip Graylog for it?"> with options `Skip Graylog for this complaint` (-> `skipGraylog: true`) and Other for the id (`trx:<value>` / `conv:<value>`). Empty submit re-asks. Set `phase: "fetching_graylog"`.

### Phase 1 - Graylog evidence fan-out

For each complaint with at least one id and `skipGraylog: false`:

```bash
~/.claude/lib/fetch-graylog.sh --trx <trxId> --conv <convId>   # pass whichever exist
```

Environment defaults to `auto`: production first, the test instance (`hosts.graylogTest`) when production returns nothing or is unreachable. A customer complaint is normally a production event, but a tester-minted id only exists on test, and prod-only search reports "no logs" for both cases identically. When the intake itself says which environment a complaint came from, pin it with `--env prod` / `--env test` rather than letting the fallback decide.

Record per complaint: `graylog: {status: ok|degraded|skipped, environment, totalResults, degradeReason}` plus the top messages (timestamp, source, level, message excerpt) kept in working context for Phase 2/3. Failure handling:

- Exit 0 with `degraded: true` -> `status: degraded`, keep the reason, continue.
- Exit 2 / 3 / 6 (credential / auth / host) -> per `$HOME/.claude/multi-agent-refs/keychain.md` non-critical rule: warn ONCE (`WARN: Graylog unavailable (<reason>); remaining complaints proceed without log evidence.`), mark this and all remaining fetches `status: degraded`, continue. Never halt (Locked 1).
- `skipGraylog: true` -> `status: skipped`.

**Always name the environment when citing log evidence.** `source.environment` in the fetcher payload says which instance answered; the same lines mean different things depending on whether a production complaint was corroborated by production logs or only by test ones. A complaint whose only evidence came from test is `insufficient-evidence` for a production claim, not `bff`.

Set `phase: "correlating_repos"`.

### Phase 2 - Repo evidence correlation (read-only)

From each complaint's Graylog messages extract candidate signals: endpoint paths, error codes, exception class names, distinctive message templates, and the `source` service name. Then grep the selected repos (skip dirs: `.build`, `DerivedData`, `Pods`, `node_modules`, `.next`, `build/`, `.gradle`, `vendor/`):

```bash
grep -rn --include='*.swift' --include='*.kt' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.java' -E "<signal>" "$REPO_PATH"
```

Bucket hits per complaint as `repoEvidence[] = {repo, layer, file, line, signal, matchKind: direct|partial}` (`direct` = exact endpoint/error-code/template match; `partial` = fuzzy/name-only). Grep only - no convention extraction, no Figma, no Swagger. Complaints with `status: degraded|skipped` still get a text-similarity pass (grep the complaint's distinctive nouns/error phrases), tagged `partial`. Set `phase: "triaging"`.

### Phase 3 - Triage classification

Per complaint, in order:

| Condition | Verdict |
|---|---|
| Graylog error originates in an owned layer: signal matched `direct` in a selected repo, OR the failing `source` maps to an owned BFF | `client:<ios|android|web>` or `bff:<mobile-bff|web-bff>` + root-cause rationale + citations (Locked 10) |
| Graylog shows the failure downstream of owned layers: `source` is an unowned core service, 5xx from an upstream nobody selected owns, no repo match | `core` + routing recommendation (below) |
| No/degraded Graylog AND no direct repo signal | `insufficient-evidence` + open question row |

**Routing recommendation** (core only, Locked 5): `{suspectedService: <Graylog source>, endpoint, errorCode, evidenceExcerpt (EN, redacted), suggestedQueue: prefs.projects[<project>].routing.coreTeamLabel ?? null, confidence}`.

Verdict shape: `verdict: {category, layer, confidence: high|medium|low, rationale}`. Ambiguous client-vs-bff attribution lowers `confidence`, never invents evidence.

**Development handoff (client/bff only, Locked 13)**: for each `client`/`bff` verdict, derive from the `repoEvidence[]` rows:

- **Fix plan**: 2-5 numbered steps referencing the existing architecture by `file:line` - which service/view/handler to change, what to reuse (reuse-first: prefer extending the cited components over adding new ones), which tests to add. No speculative rewrites.
- **Dev prompt**: one fenced English block the user can paste into `/multi-agent` (or a Jira description): complaint summary, root cause, the cited files, the fix plan steps, and the acceptance check. Include the complaint id (`[C-NN]`) for traceability.

Set `phase: "drafting"`.

### Phase 4 - Draft, humanize, buffer

1. Render the report per `$HOME/.claude/multi-agent-refs/complaint-analysis-template.md` (8 fixed sections; single-language body in `outputLanguage`; verdict tokens English per Locked 9) to `/tmp/complaint-analysis-<run-slug>-<UTC-iso8601>/report.md`. Store `outputs.draftDir`.
2. **Humanizer pass (required: actually invoke the `ai-common-toolkit:humanizer` skill; the punctuation grep alone does NOT satisfy this)** with `language: <tr|en>`, `tone: technical-explanatory`, `stripFancyPunctuation: true`. Diacritics preserved (Locked 8).
3. Punctuation gate: `node $HOME/.claude/scripts/validate-complaint-doc.mjs <draft>` reports no banned-punctuation error. It checks the policy in Node, so the same result holds on macOS, Linux and Windows; `grep -P` is absent from BSD grep and would never run there.
4. Show the draft path + size to the user. Set `phase: "awaiting_output_decision"`.

### Phase 4.5 - Output destination picker

AskUserQuestion (multiSelect=true), `Local file` pre-selected (Locked 7):

```
header: "Output"
question: <localized: "Where should the triage report be written?">
options:
  - label: "Local file"        (description: complaints/<run-name>.md in the primary repo's working tree)
  - label: "Confluence page"
  - label: "Jira"
```

Follow-ups: Confluence -> ask parent page (Other, LRU recents from `prefs.projects[<project>].confluenceUrls`); Jira -> pick from Step 4 Jira ids if any, else ask via Other. Result: `outputs.destinations[]`. Set `phase: "dispatching"`.

### Phase 5 - Validate, dispatch, report. Stop.

**Pre-dispatch gate (BLOCKING)**: `node $HOME/.claude/scripts/validate-complaint-doc.mjs <draft>` - front-matter, required sections, verdict tokens per triage row, routing entry per core verdict, punctuation, redaction-leak scan. Any ERROR blocks dispatch: fix the draft, re-validate.

| Target | Action |
|---|---|
| Local | `cp` the draft to `complaints/<run-name>.md` in the primary repo's working tree. **No commit** (Locked 12). |
| Confluence | Re-humanize with `formal-stakeholder` tone; post one page under the chosen parent via `$HOME/.claude/multi-agent-refs/channels/confluence.md` + `~/.claude/lib/md2confluence-v3.py`. |
| Jira | Re-humanize with `informal-technical` tone; markdown -> wiki markup per `$HOME/.claude/multi-agent-refs/channels/jira.md`; post as a comment on the chosen issue (never close/transition the issue). |

Then print the summary in `outputLanguage`: complaint count, verdict counts (`X client / Y bff / Z core / W insufficient-evidence`), degraded services, output paths/URLs. When any `core` verdict exists, add: <localized: "N complaint(s) route to the core team - see the routing section before forwarding.">

**Stop. Do not chain into a dev run. Do not open a worktree. Do not create a branch.** Set `phase: "done"`.

### Resume contract

`state.complaintSpec.phase`: `intake | fetching_graylog | correlating_repos | triaging | drafting | awaiting_output_decision | dispatching | reporting | done`.

`/multi-agent:resume` at `awaiting_output_decision`: if `outputs.draftDir` still holds `report.md`, jump to Phase 4.5; if gone, re-render Phase 4 from state (evidence is retained). Earlier phases resume at their own boundary; Graylog results already in state are never re-fetched.

## Reusable refs

| Path | Reason |
|---|---|
| `~/.claude/lib/parse-complaints.sh` | Phase 0 Step 4 normalization + redaction (all formats + stdin) |
| `~/.claude/lib/context-link-extractor.sh` | Phase 0 Step 4 input classifier (jira / confluence / graylog id labels) |
| `~/.claude/lib/fetch-graylog.sh` | Phase 1 log evidence (`--trx` / `--conv`) |
| `~/.claude/lib/fetch-confluence.sh` | Phase 0 Step 4 Confluence-sourced complaints |
| `$HOME/.claude/multi-agent-refs/_account-picker.md`, `_repo-picker.md`, `picker-contract.md` | Phase 0 pickers |
| `$HOME/.claude/multi-agent-refs/keychain.md` | Phase 1 non-critical credential handling |
| `$HOME/.claude/multi-agent-refs/complaint-analysis-template.md` | Phase 4 report template (8 sections) |
| `ai-common-toolkit:humanizer` | Phase 4 tone pass |
| `ai-analyst-toolkit:signal-community` | Phase 1 corroboration, optional. Advisory only: a matching report outside the company shows the complaint is not one user's device, and lands in the risks section with its link, never as a root cause. |
| `$HOME/.claude/scripts/validate-complaint-doc.mjs` | Phase 5 pre-dispatch gate |
| `$HOME/.claude/multi-agent-refs/channels/confluence.md`, `channels/jira.md`, `~/.claude/lib/md2confluence-v3.py` | Phase 5 dispatch |
| `$HOME/.claude/schemas/complaint-analysis-spec.schema.json` | State contract |

## Notes

- Fully generic: hosts come from `prefs.global.hosts.*`, tokens from `prefs.global.keychainMapping.*`; no company name, host, or real repo name in this file.
- `prefs.projects[<project>].routing.coreTeamLabel` is optional; when unset, `suggestedQueue` renders as `-` and the routing entry still stands.
- If Confluence / Jira dispatch returns 401 / 403, surface the error and offer the Local fallback (the draft stays on disk).
- The `complaints/` directory is not gitignored; the user commits manually if desired.
