---
name: slack-ask-review
description: >
  Ask the team for a code review on Slack by posting (or thread-bumping) a contextual message
  on the repo's tech channel — MR title, link, branches, a one-line why and a diff summary,
  optionally pinging a reviewer or flagging the MR as AI-validated.
  Use when the user says /slack-ask-review, 'demande une review', 'demande une review sur Slack',
  'préviens l équipe que la MR est prête', 'poste la demande de review', 'ping pour review',
  'demande à relire ma MR', 'envoie la MR sur Slack pour review', 'ask for review on slack',
  'request a review on Slack', 'tell the team the MR is ready', 'post a review request',
  'ping someone to review the MR', or wants to notify the team that a merge request is ready
  for review. Distinct from /code-review (which reviews the diff itself) and /glab-mr (which
  opens the MR) — this one only posts the Slack review-request message.
argument-hint: "[optional: additional context or specific reviewer mention] [optional: --approved-by-ai — mark the MR as AI-validated instead of a plain review request]"
allowed-tools:
  - Read
  - Glob
  - Grep
  - Bash
---

Post a review request message on the appropriate Slack channel for the current merge request.

All Slack API calls go through the bundled `pysae-ai-tools slack …` subcommands — no MCP. The commands pick `SLACK_USER_TOKEN` locally (OAuth browser flow) and `SLACK_BOT_TOKEN` in CI automatically.

## Required tools

- `glab`
- `jq`

Run `pysae-ai-tools tools require <tools>` before starting; if it exits non-zero, abort and report the install command from stderr to the user.

## Project configuration

Honour `.pysae-ai-tools.yaml` (this repo's `CLAUDE.md` is NOT in your context at runtime). Check flags with `pysae-ai-tools project flag <path>` (exit 0 = on, 1 = off — no `jq`):

- `slack.enabled` — when `pysae-ai-tools project flag slack.enabled` exits non-zero, **do not post anything**; stop cleanly.
- `slack.notifications.mr_review` — when off, **skip** the review-request message.

## Prerequisite — resolve the Slack token

Every `pysae-ai-tools slack …` call below reads `SLACK_USER_TOKEN` locally or `SLACK_BOT_TOKEN` in CI. Export the right one up-front so the commands never fail for missing auth:

```bash
if [ -n "${CI:-}" ]; then
  # CI: non-interactive bot token, fetched from ai-tools/dev/secrets.
  eval "$(pysae-ai-tools env resolve --set SLACK_BOT_TOKEN)"
  SLACK_TOKEN_KIND=bot
else
  # Local: user token. First run triggers a Slack OAuth browser flow
  # (falls back to SLACK_BOT_TOKEN if the user token isn't available).
  if eval "$(pysae-ai-tools env resolve --set SLACK_USER_TOKEN)"; then
    SLACK_TOKEN_KIND=user
  elif eval "$(pysae-ai-tools env resolve --set SLACK_BOT_TOKEN)"; then
    SLACK_TOKEN_KIND=bot
  fi
fi
```

If neither resolves, stop and tell the user to run `/env-resolve SLACK_USER_TOKEN` (local) or configure the secret in CI.

`SLACK_TOKEN_KIND` matters for the header: a `user` token posts the message **as the MR author**, so the team already sees who's asking; a `bot` token posts anonymously, so the author must be named in the message (see [step 6](#6-compose-the-message)).

## Mode — review request vs AI-validated

By default this skill posts a **review request**. When `$ARGUMENTS` contains `--approved-by-ai` (typically delegated by `/glab-mr-approve` after the AI successfully approved the MR), it posts the **same message with the same content**, only flagging the MR as AI-validated:

```bash
APPROVED_BY_AI=$(echo "$ARGUMENTS" | grep -qE -- "--approved-by-ai" && echo true || echo false)
```

The only difference is a `:white_check_mark: *Validée par IA*` line added under the title (see [step 6](#6-compose-the-message) and [step 4a](#4a-found-reply-in-the-existing-thread)). Everything else — channel resolution, dedup, why/bullets, branch, review env — is identical.

## Process

1. **Resolve context** — detect MR and project in one shot:
   ```bash
   CTX=$(pysae-ai-tools internal detect-context)
   ```
   Extract: `MR_IID`, `MR_TITLE`, `MR_DESCRIPTION`, `MR_AUTHOR`, `PROJECT_URL`, `PROJECT_PATH`, `MR_SOURCE_BRANCH`, `MR_TARGET_BRANCH`.
   If `MR_IID` is empty, inform the user that no MR exists and stop.

   The tech Slack channel is **stable config**, not part of detect-context — resolve it
   from the project's config (current repo):
   ```bash
   { IFS= read -r TECH_SLACK_CHANNEL; IFS= read -r TECH_SLACK_CHANNEL_ID; } < <(
     pysae-ai-tools project show slack.tech_channel slack.tech_channel_id)
   ```

   Also derive the **canonical project name** — the project path **without** the `pysae/` prefix (e.g. `pysae/api` → `api`, `pysae/infra/infra-cluster` → `infra/infra-cluster`), shown at the end of the message, consistent with `/code-review-pre-release`:
   ```bash
   PROJECT_NAME="${PROJECT_PATH#pysae/}"
   ```

2. **Build context summary** — gather two distinct artefacts:
   - **Why summary** (1-2 lines): the *purpose* of the MR — the user value, the problem solved, the bug fixed. Not what changed (that's the bullets), but why a reviewer should care. Pull from `MR_DESCRIPTION` (look for `## Description`, `## Objectif`, `## Contexte` sections, or the first non-checklist paragraph), the linked issue title, or the MR title's intent. Avoid restating the title verbatim.
   - **What changed** (3-5 bullets): summarise the diff. Use `git diff origin/${MR_TARGET_BRANCH}...HEAD --stat` for an overview, then pick the salient changes from the actual files.
   - **Review environment URL**: check if a review environment is deployed for this MR:
     ```bash
     glab api "projects/:id/merge_requests/${MR_IID}/deployments" 2>/dev/null | jq -r '.[].environment.external_url // empty' | head -1
     ```
     If a review URL is found, save it for inclusion in the message.
   - **MR author Slack ID**: resolve `MR_AUTHOR` (GitLab username) into a Slack user ID so the message tags the person responsible for the MR (the merge owner — the one a reviewer might need to ping back). Build a short list of email candidates from `MR_AUTHOR` (e.g. `<author>@pysae.com`, then `<author>` without the `.ext` suffix) and try each via `slack find-user`:
     ```bash
     AUTHOR_ID=""
     for q in "${MR_AUTHOR}@pysae.com" "${MR_AUTHOR%.ext}@pysae.com" "${MR_AUTHOR}"; do
       AUTHOR_ID=$(pysae-ai-tools slack find-user "$q" 2>/dev/null | jq -r 'select(.found) | .id')
       [ -n "$AUTHOR_ID" ] && break
     done
     ```
     If still empty, fall back to plaintext `(par @<MR_AUTHOR>)` so the message stays informative even without a working tag.

3. **Determine the Slack channel** — use `TECH_SLACK_CHANNEL_ID` resolved via `project show` in step 1: the per-repo tech channel declared in the repo's config. There is **no fallback** — if it is empty (the repo declares no tech channel), skip posting and tell the user to set `slack.tech_channel_id` in its `.pysae-ai-tools.yaml`; never invent or hardcode a channel.

4. **Check for existing review request** — search the channel history for a message that already links to this MR:
   ```bash
   RESULT=$(pysae-ai-tools slack ask-review \
     --channel "${TECH_SLACK_CHANNEL_ID}" \
     --project-url "${PROJECT_URL}" \
     --mr-iid "${MR_IID}")
   ```
   The script paginates through the last 30 days and matches the message our tooling posted for this MR by its structured metadata (`ai_tools_review_request` + `{project, mr_iid}`), not by scanning text — so a human's message merely citing the MR link is never mistaken for the review request. (Posting the message with `--review-project` / `--review-mr-iid` in step 7 is what stamps that metadata.)

   Parse the JSON output:
   ```bash
   FOUND=$(echo "$RESULT" | jq -r '.found')
   THREAD_TS=$(echo "$RESULT" | jq -r '.thread_ts // empty')
   ORIGINAL_TEXT=$(echo "$RESULT" | jq -r '.text // empty')
   ```

   - **If `FOUND` is `true`** → thread-reply branch (step 4a).
   - **If `FOUND` is `false`** → compose-and-post branch (steps 5-7).

### 4a. Found: reply in the existing thread

Check whether there are new commits since the original message was posted (compare the message timestamp with `git log --since`):

- **New changes** → post a thread reply summarizing what changed (see [Slack mrkdwn formatting](#slack-mrkdwn-formatting) below — never use Markdown link syntax):
  ```bash
  MSG=$(cat <<EOF
  *MR mise à jour*

  <1-2 lines summarizing what changed>

  Commits :
  - <<project_url>/-/commit/<full_sha>|<short_sha>> <commit message>
  - <<project_url>/-/commit/<full_sha>|<short_sha>> <commit message>
  EOF
  )

  pysae-ai-tools slack post-message \
    --channel "${TECH_SLACK_CHANNEL_ID}" \
    --thread-ts "${THREAD_TS}" \
    --ai-footer \
    --text "$MSG"
  ```

- **No new changes** → post a short bump reply:
  ```bash
  pysae-ai-tools slack post-message \
    --channel "${TECH_SLACK_CHANNEL_ID}" \
    --thread-ts "${THREAD_TS}" \
    --ai-footer \
    --text "*Rappel* : cette MR est toujours en attente de review :pray:"
  ```

If the original message contains a user mention (`<@USER_ID>`), include `cc <@USER_ID>` at the end of the thread reply to re-notify the reviewer. Parse the mention from `$ORIGINAL_TEXT` with a simple grep (`<@U[A-Z0-9]+>`).

**AI-validated mode** (`APPROVED_BY_AI` is `true`): a review thread already exists for this MR, so instead of the update/bump reply above, post a single validation reply in the thread:
```bash
pysae-ai-tools slack post-message \
  --channel "${TECH_SLACK_CHANNEL_ID}" \
  --thread-ts "${THREAD_TS}" \
  --ai-footer \
  --text ":white_check_mark: *Validée par IA*"
```

### 5. Ping a reviewer (optional)

If the user specifies someone in `$ARGUMENTS`, resolve their Slack user ID via the script:
```bash
USER_JSON=$(pysae-ai-tools slack find-user "<name or email>")
USER_ID=$(echo "$USER_JSON" | jq -r 'select(.found) | .id')
```
If `USER_ID` is non-empty, add `cc <@${USER_ID}>` to the message composed in step 6. If no match, mention the query back to the user so they can refine it.

### 6. Compose the message

Build the message body in a shell variable. The structure is fixed: header line (`:eyes:` … `: <project>`), MR title line (`:memo: *<title>*`), MR URL + branch on one line, a one-line "why" preamble, then the bullet list of what changed.

```bash
# Resolved in step 2: AUTHOR_TAG is either "<@U…>" if found, or "@<MR_AUTHOR>" as fallback.
AUTHOR_TAG="${AUTHOR_ID:+<@${AUTHOR_ID}>}"
AUTHOR_TAG="${AUTHOR_TAG:-@${MR_AUTHOR}}"

# With a bot token the message is posted anonymously, so name the MR author in the
# header. With a user token the post already comes from the author — tagging is redundant.
AUTHOR_PREFIX=""
[ "$SLACK_TOKEN_KIND" = "bot" ] && AUTHOR_PREFIX=" par ${AUTHOR_TAG}"

# Status lines always present (fixed layout — the line count never varies; the
# value flips in place). Validation: "Validée par IA" in AI-validated mode, else
# "Non validée". MR state starts "ouverte"; `slack mark-merged` flips it to
# "mergée" on merge. Plain text — only the header (line 1) is bold.
if [ "$APPROVED_BY_AI" = "true" ]; then
  VALIDATION_LINE=":white_check_mark: Validée par IA"
else
  VALIDATION_LINE=":hourglass_flowing_sand: Non validée"
fi

MSG=$(cat <<EOF
:eyes: *MR pour* <${PROJECT_URL}|${PROJECT_NAME}>${AUTHOR_PREFIX} — :large_green_circle: Ouverte
:memo: <MR title>
:seedling: <${PROJECT_URL}/-/tree/<source_branch>|<source_branch>> → <${PROJECT_URL}/-/tree/<target_branch>|<target_branch>>
${VALIDATION_LINE}
:link: <MR web URL>

<1-2 lines explaining the WHY: user value, problem solved, business context. Not what changed.>

• <what changed — point 1>
• <what changed — point 2>
• <what changed — point 3>
EOF
)
```

Header layout, top to bottom (the **line count is stable** — statuses flip in place, never appear/disappear):
- Line 1 — `:eyes: *MR pour* <${PROJECT_URL}|${PROJECT_NAME}>${AUTHOR_PREFIX} — :large_green_circle: Ouverte`: the `:eyes:` marker, the bold label `MR pour`, the project name as a **link to the GitLab project** (label without the `pysae/` prefix), then — after an em dash — the **MR state** (`:large_green_circle: Ouverte` at post time). `${AUTHOR_PREFIX}` appends ` par <@U…>` in bot mode (dropped under a user token). `MR pour` is the only bold text. `slack mark-merged` flips the state segment **in place** to `:twisted_rightwards_arrows: Mergée` (on merge) or `:red_circle: Fermée` (`--state closed`) — the rest of the line is preserved.
- Line 2 — `:memo: <MR title>`: the MR title, **not bold**.
- Line 3 — `:seedling: <src-link> → <target-link>`: the branch routing, each branch a **link to its GitLab tree** (`${PROJECT_URL}/-/tree/<branch>`). No `Branche :` prefix — the `:seedling:` icon carries the meaning. (Links and `` `code` `` don't combine in Slack mrkdwn, so the branches are plain links, not code spans.)
- Line 4 — validation status: `:white_check_mark: Validée par IA` (AI-validated mode) or `:hourglass_flowing_sand: Non validée` otherwise. Plain text.
- Line 5 — `:link: <MR web URL>`: the MR link.

Then the one-line "why", then the bullets. Any `Review env` / `cc` line goes at the very end, after the bullets.

In AI-validated mode, posted via the CI bot token, the header therefore reads:
```
:eyes: *MR pour* <https://gitlab.com/pysae/api|api> par <@U…> — :large_green_circle: Ouverte
:memo: <MR title>
:seedling: <https://gitlab.com/pysae/api/-/tree/feat/x|feat/x> → <https://gitlab.com/pysae/api/-/tree/main|main>
:white_check_mark: Validée par IA
:link: https://gitlab.com/pysae/api/-/merge_requests/42
```
Locally (user token) the `par <@U…>` prefix is dropped. The dedup search (step 4) keys on the message metadata, not the header text, so the wording can evolve freely.

If a review environment URL was found in step 2, append:
```
:globe_with_meridians: Review env : <review_url>
```

If a *reviewer* (different from the author) was resolved in step 5, append `cc <@${USER_ID}>` at the very end. If `$ARGUMENTS` contains additional context, fold it into the bullet list.

The "why" line is plain text (no `>` blockquote): a one-line preamble between the branch line and the bullets, so the reviewer scans title → link/branch → why → diff in one pass.

### 7. Post the message

```bash
pysae-ai-tools slack post-message \
  --channel "${TECH_SLACK_CHANNEL_ID}" \
  --ai-footer \
  --review-project "${PROJECT_URL}" \
  --review-mr-iid "${MR_IID}" \
  --text "$MSG"
```

`--ai-footer` appends a discreet `:robot_face: Rédigé par Claude via pysae-ai-tools` context block (small greyed-out text, with `pysae-ai-tools` linked to its public PyPI page) under the message, so the team always sees it was AI-drafted — even when posted under the author's name with a user token.

`--review-project` + `--review-mr-iid` stamp **structured Slack metadata** (`event_type: ai_tools_review_request`, payload `{project, mr_iid}`) on the message. This is how [step 4](#4-check-for-existing-review-request) re-finds it later (and how `slack mark-merged` locates it): by identity, not by scanning text for the MR URL. **Always pass both** — without them the message is invisible to the dedup search and to `mark-merged`, and a future run would post a duplicate. Use the same `${PROJECT_URL}` / `${MR_IID}` passed to `ask-review` in step 4.

Parse the JSON output to confirm the post succeeded (`ok: true`). On failure, surface the `error` field to the user.

## Slack mrkdwn formatting

All message text posted to Slack **must** be in Slack mrkdwn syntax — never standard Markdown. Slack does not understand `[label](url)`, `**bold**`, or backslash-escaped `\#` / `\!` references; these all render as literal text. Convert before posting:

| Don't (Markdown) | Do (Slack mrkdwn) |
| --- | --- |
| `[label](https://...)` | `<https://...\|label>` |
| Bare URL `https://...` | `<https://...>` (or `<https://...\|label>`) |
| `**bold**` | `*bold*` |
| `*italic*` or `_italic_` | `_italic_` |
| `# Heading`, `## Heading` | `*Heading*` on its own line (no real heading syntax in Slack) |
| `\#2563`, `\!2434` | `#2563`, `!2434` — *or*, when the project URL is known, link them: `<https://gitlab.com/pysae/api/-/issues/2563\|#2563>`, `<https://gitlab.com/pysae/api/-/merge_requests/2434\|!2434>` |
| `@firstname.lastname` | resolve to a real Slack user ID via `pysae-ai-tools slack find-user "<name>"` and use `<@U…>` (see [step 5](#5-ping-a-reviewer-optional)) — otherwise drop the `@` |
| Code fences with language ``` ```python ``` | plain ``` ``` ``` (Slack ignores the language tag) |
| Tables | bullet lists or pre-formatted text inside ``` ``` ``` |

For **real bullet points**, start each line with the bullet character `•` (U+2022) followed by a space — `- ` and `* ` are NOT rendered as bullets by Slack mrkdwn (`*` even turns the line bold). This matches what `code release-content --render slack` already emits. Inline `` `code` `` and blockquotes with `>` work the same in both.

When a section of the message comes from MR descriptions, commit messages, or `git log` output, **rewrite it in mrkdwn**, do not paste raw Markdown.

## Rules

- **French only** for the message content
- **Langue française** : respecter les [règles de rédaction en français](../references/french-writing-rules.md) pour tout texte généré
- **Slack mrkdwn only** — apply the conversion table above to every line before posting (links, bold, GitLab refs, mentions)
- Keep the message short and scannable — no walls of text
- Do not include the full diff or file contents in the message
- If `glab` is not available or fails, inform the user and stop
- **Never use MCP Slack tools** — everything goes through the `slack` subcommands so the same flow works locally (user token) and in CI (bot token)

$ARGUMENTS
