---
name: glab-issue-workflow-update
description: >
  Reconcile GitLab ticket board statuses with merge-request reality across
  every project in the group at once (use --project to filter to one). Bumps
  tickets with an open MR to at least `workflow::In progress` (or `Under review`
  if that MR is approved), tickets whose MRs are all merged but not yet shipped
  to `workflow::To deploy`, closes tickets whose merge commit is on a deploy
  branch (deploy/prod or deploy/support/*), and closes tickets carrying the
  ANNULE (cancelled) label.
  Use when the user says /glab-issue-workflow-update, 'mets à jour le statut des tickets',
  'mets à jour les tickets', 'mets à jour tous les tickets', 'corrige les statuts des tickets',
  'répare le workflow du board', 'fix ticket statuses', 'update ticket statuses',
  'sync le board', 'les tickets sont pas à jour sur le board', 'mets à jour mes tickets',
  'mets à jour les tickets de tout le monde', or wants to clean
  up stale board columns across projects after merges and deploys. By default it
  audits only the current user's tickets (assigned to me, or authored by me with
  no one else assigned); pass --anyone for everyone's, or --assignee <user> /
  --author <user> to target a specific person. With --reopen it also reopens
  recently-closed tickets closed by mistake ('rouvre les tickets fermés à tort',
  'reopen wrongly closed tickets'); off by default.
argument-hint: "[--project <path|name>] [--anyone] [--reopen] [--plan-out <file>] [--apply-plan <file>]"
allowed-tools:
  - Bash
  - AskUserQuestion
---

Thin gateway over `pysae-ai-tools glab issue-workflow-update`. The CLI does all the work — resolve what's in prod, fetch issues + related MRs, decide per ticket by commit ancestry — and prints a JSON plan. This skill **saves the plan to a file first** (`--plan-out`), shows it, gets confirmation, then applies **that exact file** (`--apply-plan`) — so what is applied is precisely what was reviewed (no re-audit between the two steps).

## Required tools

- `glab`

Run `pysae-ai-tools tools require glab || exit 1` before starting; the command exits non-zero with the install command on stderr if it is missing or out of date.

## Rules applied

Evaluated in this precedence; a ticket already at or beyond the target column is left untouched. All bumps reuse `glab workflow-transition` (incremental — other labels like `review::release` / `version::*` are preserved).

0. **Cancelled label (highest)** → an *open* ticket carrying a cancelled label (`ANNULE` by default, `--cancelled-label`, repeatable) is **closed** whatever its MRs say — a cancelled ticket does not belong on the board. The `ANNULE` label itself is kept (only board/workflow labels are stripped on close). A *closed* cancelled ticket is never reopened.
1. **Open MR** → a ticket with an *open* MR belongs in `workflow::In progress` — or `workflow::Under review` if that open MR is already **approved**. An open MR always wins: a ticket is never closed while work is still open, even if another of its MRs already shipped. This is the one rule that can move a ticket **backward**: a ticket parked in `workflow::To deploy` with a still-open MR is pulled back to its work column (an open MR means the work isn't done). No backward churn within the work columns themselves (an unapproved open MR does not demote `Under review` → `In progress`).
2. **Merged & shipped** → a ticket whose fixing MRs are **all merged, none open**, and whose merge commit(s) are contained in a deploy branch, is **closed** — and its board/workflow column label is stripped at the same time (a closed ticket owns no column). **Special case — no deploy branch at all**: the project has no deployment step, so `To deploy` is meaningless and a merged ticket is **closed directly**.
3. **Merged, not yet shipped** → all merged, none open, but no merge commit is on a deploy branch yet → bumped to at least `workflow::To deploy`.
4. **Reopen (opt-in, `--reopen`)** → a **closed** ticket that looks closed by mistake — it still has an *open* MR, or (on a deploying project) a *merged* MR **not yet shipped** — is **reopened** and set to the column its MR state implies (`Under review`/`In progress` for an open MR, `To deploy` for a merged-not-shipped MR). Closed tickets with no MR, only abandoned MRs, already-shipped MRs, or the `ANNULE` label are left closed. On a project with no deploy branch, only a still-open MR reopens.

Board ordering used for « at least »: Refinement < Ready < To Do < In progress < Under review < To deploy.

### How "shipped" is determined (no changelog)

"Shipped" is decided **per line** from a **source→deploy branch mapping**, not by the changelog. Each project maps source branches to their deploy branch via `.pysae-ai-tools.yaml` `board.deploy_branches` (a dict; a `*` on both sides must correspond, the source capture fills the deploy side), defaulting to `main: deploy/prod` + `support/*: deploy/support/*`. Wildcards are expanded against the repo branches to concrete `(source, deploy)` pairs whose deploy branch exists. For each pair one `compare?from=deploy&to=source` call gives the commits on that source line **not yet** on its deploy branch; a merged MR (merged into `source`) whose merge/squash commit is **absent** from that set has shipped → the ticket is closed. Pairing source to deploy is what makes diverging lines correct (`main`→`deploy/prod`, `support/5.2.x`→`deploy/support/5.2.x`). Everything is API-only (no clone, no local git). When a project has **no deploy branch at all**, `has_deploy` is `false` and merged tickets are closed directly (no `To deploy`). `--deploy-branch source=deploy` overrides the mapping for a run (repeatable, globs).

### How the ticket ↔ MR link is detected

The MR(s) that address a ticket are resolved from GitLab's native **`closed_by`** field in priority — the MRs whose `Closes #N` ties them to the issue (auto-close is disabled project-wide, so `Closes` records the link without closing on merge). It falls back to `related_merge_requests` only when `closed_by` is empty. When several candidates come back, the right one is the MR whose **branch encodes the ticket IID** (`<type>/<IID>-…`); if the branch doesn't disambiguate, all candidates are kept. The MR **description is never parsed**, and a bare `#N` mention never drives status. The merge/squash commit SHA comes straight from that MR payload.

## Workflow

Everything is fetched via the GitLab API — **no checkout or local git is required**, so you can run it from anywhere. By default it sweeps **every non-archived project in the `pysae` group** (subgroups included), resolving each project's deploy branches and fanning MR lookups across a thread pool (`--workers`, default 8). `--project` is a **filter** (repeatable: path, id, or short name like `api`), not a separate mode.

**Assignment scope** — by default only **my tickets** are audited within the scanned projects: tickets **assigned to me**, or that **I authored with no one else assigned** (me = current `glab` user). Opt out or retarget:

- `--anyone` — audit **everyone's** tickets (the previous whole-group behaviour).
- `--assignee <user>` — only tickets assigned to that user (username, display name, or `@me`). Overrides the default "mine".
- `--author <user>` — only tickets authored by that user (username, display name, or `@me`). Overrides the default "mine".

`--assignee` and `--author` combine as AND. When a scope is active the JSON output carries an `assignment_filter` block with the resolved usernames (it is `null` under `--anyone`). Use `--anyone` when the user asks to update *all* tickets / the whole board; keep the default when they mean their own.

**Reopen scope** — `--reopen` (off by default) adds a pass over **recently-closed** tickets (closed within the last 30 days, `--closed-since N`) to catch closings made by mistake. It reopens only those with a still-open MR or a merged-but-not-shipped MR, never those with no MR / only abandoned MRs / already shipped, and never those carrying the cancelled label (`--cancelled-label`, default `ANNULE`, repeatable). The same assignment scope applies (my closed tickets by default; `--anyone` for everyone's).

1. **Plan → file** — compute the plan and save it (never mutate yet). Use a unique temp path (`mktemp`), never a fixed one:
   ```bash
   PLAN=$(mktemp -t wf-plan-XXXXXX.json)
   pysae-ai-tools glab issue-workflow-update --plan-out "$PLAN" --format report   # save JSON, print report
   pysae-ai-tools glab issue-workflow-update --project api --plan-out "$PLAN" --format report
   pysae-ai-tools glab issue-workflow-update --anyone --plan-out "$PLAN"    # everyone's tickets
   pysae-ai-tools glab issue-workflow-update --reopen --plan-out "$PLAN"    # also reopen wrongly-closed tickets
   pysae-ai-tools glab issue-workflow-update --closed-since 90 --plan-out "$PLAN"
   ```
   `--plan-out` writes the machine JSON to `$PLAN`; `--format report` renders it (from the same plan) as one block per ticket — `project #iid — title`, `owner`, `current status → corrected status`, `reason`. Default stdout is JSON: `{ "group", "filtered", "applied", "reopen", "projects_scanned", "actionable", "results": [<per-project blocks>] }`; each block has `project`, `project_id`, `deployed_version`, `deploy_branches`, `has_deploy`, `open_issues_scanned`, `closed_issues_scanned` (only with `--reopen`), and `decisions[]` (`iid`, `title`, `owner` — first assignee, else author —, `current_board`, `action`, `reason`, and `target` for reopens).

2. **Present the plan** to the user — the `--format report` output is already a per-ticket, per-project breakdown; relay it (or summarise counts by action: `set_in_progress`, `set_under_review`, `set_to_deploy`, `close`, `reopen`). If `actionable` is 0, report « board déjà à jour » and stop.

3. **Confirm then apply the file** (local mode) — ask with `AskUserQuestion` whether to apply. On yes, apply the **exact plan file** — no re-audit, so the actions are precisely those reviewed:
   ```bash
   pysae-ai-tools glab issue-workflow-update --apply-plan "$PLAN"
   ```
   Output is `{ "applied": true, "from_plan", "count", "results": [{ "project_id", "iid", "action", "result" }] }` where `result` is `set:workflow::…`, `closed`, `reopened+…`, or `error:…`. Report the applied changes and any errors. (A one-shot `--apply` that recomputes-and-applies in a single call still exists, but prefer the `--plan-out` → `--apply-plan` flow so the applied set matches what was shown.)

   In CI mode (`$CI` set), do not prompt: print the plan and exit (let a human apply), unless explicitly invoked with apply intent.

## Safety

- **Dry-run is the default**; `--apply` is the only thing that writes. Always show the plan and get an explicit go before applying. The default scope is **my tickets across the whole group**; `--anyone` widens it to everyone (many tickets across repos), `--project` narrows it.
- Closing strips the board column. The `close` list is the tickets whose merge commit is on a deploy branch (`deploy_branches` per project), the cancelled (`ANNULE`) tickets, and — on a project with `has_deploy: false` — every all-merged ticket (no deployment step, so merged = done).
- `--reopen` (opt-in) writes to closed tickets — always dry-run first and review the `reopen` list. It is deliberately conservative (open MR or merged-not-shipped only, `ANNULE` excluded, last 30 days), but a reopen still reverses a human decision, so confirm before applying.

For full options: `pysae-ai-tools glab issue-workflow-update --help`.

$ARGUMENTS
