#!/usr/bin/env bash
#
# deck-run <TaskID> | deck-run --no-task — spawn primitive (UI-agnostico) di loom-deck.
#
# Apre una tab Ptyxis nella window ATTIVA (quella col focus = il deck) e vi
# avvia una sessione Claude Code già bound alla task via LOOM_TASK, dritta sul
# prompt iniziale scelto con --prompt-kind (default: `recap`, cioè la skill
# `/loom-works:recap-status` sulla task).
# Riusabile identico da TUI (Ink) e da web.
#
# Quattro assi ORTOGONALI, da non confondere fra loro:
#   binding task   <TaskID> ..... --no-task
#   continuità     nuova ........ --resume [--fork]
#   prompt         --prompt-kind none|recap|preflight|run|checkpoint | --prompt <testo>
#   modello        --model fable|opus|sonnet|haiku
# Fuori dai quattro, e volutamente non un asse: --new-window, che cambia SOLO il
# verbo dell'exec finale (tab nella finestra attiva → finestra nuova).
# Il terzo asse (T56) prima non esisteva: il prompt era una CONSEGUENZA degli
# altri due (bound ⇒ recap, --no-task ⇒ niente, --resume ⇒ niente), quindi
# "task-bound SENZA prompt" era inesprimibile e l'unico modo di non avere un
# prompt era perdere la task. Il kind è un SIMBOLO e il suo testo sta nel
# catalogo condiviso `prompt-catalog` (sibling di questo script).
# T117 — accanto al kind c'è ora `--prompt <testo>`, che passa il prompt
# LETTERALE. I due sono mutuamente esclusivi: sono due modi di dire la stessa
# cosa, e accettarli insieme costringerebbe a stabilire quale vince. Serve a chi
# mostra il prompt all'utente prima dello spawn e glielo lascia modificare —
# dopo una modifica nessun kind descrive più quel testo.
# Il quarto asse (T108) è indipendente dagli altri tre: vale sul ramo bound come
# su --no-task, su una sessione nuova come su una ripresa.
#
# Con --no-task la sessione è NUDA: niente LOOM_TASK, niente --session-id.
# Serve al lavoro spot che non appartiene a nessuna task.
# T134 — un prompt iniziale ce l'ha se glielo si passa LETTERALE (`--prompt`):
# il lavoro sulla doc — drenare un file inbox, srotolare l'hard-wrap di un path
# — non appartiene a nessuna task e non deve ereditarne una. I `--prompt-kind`
# del catalogo restano invece rifiutati con --no-task: nominano tutti un TaskID.
#
# Con --resume <sid> riprende una conversazione esistente; aggiungendo --fork la
# riprende RAMANDOLA (`--fork-session`): id nuovo, transcript copiato, l'origine
# resta intatta e scrivibile.
#
# Su stdout, prima di aprire la tab, stampa una riga
#   LOOM_DECK_INTAB <comando>
# col comando che girerà DENTRO la tab (la sessione `claude` con le sue env).
# Serve a chi spawna deck-run e vuole mostrare l'invocazione vera senza
# ricomporla per conto proprio; le diagnostiche restano tutte su stderr.
#
# Modalità spawn (LOOM_DECK_SPAWN_MODE):
#   inline   → ptyxis --tab -- <cmd>            (default)  comando inline, LOOM_TASK diretta, zero dconf
#   profile  → ptyxis --tab-with-profile=<UUID>            riusa il profilo, riscrive il custom-command via dconf
#
# La scelta fra le due è la Decisione aperta §9 della proposta: la modalità
# "inline" è il default perché non muta stato dconf condiviso. Vedi la nota
# di decisione nella task folder di T18.
#
# Env:
#   LOOM_DECK_SPAWN_MODE   inline|profile          (default: inline)
#   LOOM_DECK_PROFILE_UUID UUID profilo Ptyxis     (default: cc-host)
#   LOOM_DECK_WORKDIR      dir di lavoro della tab (default: $PWD)
#   LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
#                          T161: inerte sugli spawn che arrivano dal deck TUI —
#                          quelli passano SEMPRE `--prompt`, su cui non ha mai
#                          vinto. Resta per le invocazioni a mano e per i test.
#   LOOM_DECK_INTAB_CMD    override comando in-tab (default: la sessione CC; usato per i test)
#   LOOM_DECK_STATE_PROFILE  PTYXIS_PROFILE annunciata a compass (default: bindings/claude
#                            del progetto, letto da dconf; settata a vuoto = nessun annuncio)
#   LOOM_DECK_PERMISSION_MODE  override del permissionMode del file config (default: campo file, poi 'manual')
#   LOOM_DECK_MODEL        modello quando --model non è passato (batte il catalogo, default: fable)
#
set -euo pipefail

# Il catalogo dei prompt è un file DATI sibling di questo script, non un `case`
# qui dentro: lo legge anche il deck TUI, che con la stessa risoluzione sibling
# arriva allo stesso file della stessa installazione.
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPT_CATALOG="${LOOM_DECK_PROMPT_CATALOG:-${_SCRIPT_DIR}/prompt-catalog}"

TASK=""
SESSION_ID=""
RESUME_ID=""
NO_TASK=0
FORK=0
# T162 — finestra NUOVA invece di una tab nella finestra attiva.
#
# `ptyxis --tab` va sempre nella finestra col focus, e va bene per il deck, che
# è già una tab della finestra del progetto. Un chiamante che vive FUORI da
# quella finestra — compass, che riapre una conversazione pinnata di un
# progetto chiuso — non ha una finestra attiva giusta: senza un verbo diverso
# la tab finisce nella finestra di un altro progetto, o in una a caso.
#
# Non è un quinto asse: non cambia cosa gira nella tab, solo dove la tab nasce.
# Vale identico nei due rami di spawn (inline e profile).
NEW_WINDOW=0
TITLE_NOTE=""
# Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
# "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
# (nessun prompt, come sempre) e il secondo è un errore d'uso.
PROMPT_KIND=""
# T117 — prompt letterale. Vuoto NON basta a dire "non passato": `--prompt ''`
# è una richiesta legittima (nessun prompt, deciso da chi ha svuotato il campo),
# quindi la presenza del flag si tiene in un booleano a parte.
PROMPT_TEXT=""
PROMPT_GIVEN=0
# Vuoto = flag non passato: la cascata (env, poi default) si risolve più sotto,
# insieme alla validazione, così l'ingresso da argomento e quello da env passano
# per lo stesso enum.
MODEL=""
# Positional <TaskID> + flag opzionale --session-id <uuid> (T27): il deck genera
# l'UUID e lo pinna così il binding sidecar sessionId↔taskId è deterministico.
# --no-task (T42): modalità NUDA, senza TaskID — nessuna LOOM_TASK, nessun prompt
# iniziale, nessun sessionId pinnato. Flag esplicito e non "positional opzionale"
# perché un `deck-run` a mani vuote resta un errore d'uso, non una sessione spot.
# --resume <sid> (T49): riapre una sessione esistente con `claude --resume`.
# Componibile con entrambe le forme: `deck-run <TaskID> --resume <sid>` (scoped:
# la ripresa eredita LOOM_TASK + titolo · task) e `deck-run --no-task --resume
# <sid>` (spot: resume nudo). Su resume niente prompt iniziale (si continua la
# conversazione, non se ne inietta una nuova) e niente --session-id (l'id ce
# l'ha già la sessione ripresa).
# --fork (T28): MODIFICATORE di --resume, speculare al `--fork-session` del CLI
# (che è definito allo stesso modo: "when resuming, create a new session ID").
# Non è una terza forma di spawn ma una variante della ripresa → richiede
# --resume, e senza di esso è un errore d'uso, non un fork "dell'ultima".
# Sotto --fork il --session-id torna AMMESSO (anzi: è il punto) — la mutua
# esclusione con --resume esiste perché una ripresa nuda riscrive il transcript
# dell'id ripreso, mentre il fork ne apre uno NUOVO, che possiamo quindi pinnare.
# --prompt-kind <none|recap|recap-task|recap-epic|preflight|run|checkpoint>: sceglie il prompt iniziale fra
# quelli del catalogo qui sotto. Enum e non stringa libera: il prompt viaggia
# dentro apici singoli in `bash -lc`, quindi il testo va tenuto in un posto solo
# e verificato una volta, invece di spostare il rischio di quoting su ogni
# chiamante. Assente → `recap`, così ogni invocazione preesistente è invariata.
# --title-note <testo> (T64): appende la nota della conversazione al titolo della
# tab. È l'UNICO ingresso di testo libero nel titolo — gli altri componenti
# (emoji/name dal file committato, TaskID) sono controllati — quindi il testo non
# si quota, si RIDUCE a un alfabeto sicuro (vedi _sane_note): tutto ciò che non
# ci rientra sparisce, apici inclusi. L'alfabeto ammette quattro emoji (T156)
# perché il deck ci fa passare il prefisso del titolo di fallback (`🚀 slug`);
# ogni altra emoji cade ancora.
# --model <fable|opus|sonnet|haiku> (T108): modello della sessione. Enum e non
# stringa libera perché il valore finisce dentro `bash -lc` e un refuso
# produrrebbe una tab con un comando che il CLI rifiuta. Le voci sono ALIAS e
# restano tali: espanderle a id versionati (`claude-opus-5`) cablerebbe qui una
# generazione, che al primo bump diventa un modello inesistente.
while [[ $# -gt 0 ]]; do
  case "$1" in
    --model)
      MODEL="${2:-}"; shift 2 ;;
    --model=*)
      MODEL="${1#*=}"; shift ;;
    --title-note)
      TITLE_NOTE="${2:-}"; shift 2 ;;
    --title-note=*)
      TITLE_NOTE="${1#*=}"; shift ;;
    --prompt-kind)
      PROMPT_KIND="${2:-}"; shift 2 ;;
    --prompt-kind=*)
      PROMPT_KIND="${1#*=}"; shift ;;
    --prompt)
      PROMPT_TEXT="${2:-}"; PROMPT_GIVEN=1; shift 2 ;;
    --prompt=*)
      PROMPT_TEXT="${1#*=}"; PROMPT_GIVEN=1; shift ;;
    --session-id)
      SESSION_ID="${2:-}"; shift 2 ;;
    --session-id=*)
      SESSION_ID="${1#*=}"; shift ;;
    --resume)
      RESUME_ID="${2:-}"; shift 2 ;;
    --resume=*)
      RESUME_ID="${1#*=}"; shift ;;
    --fork)
      FORK=1; shift ;;
    --no-task)
      NO_TASK=1; shift ;;
    --new-window)
      NEW_WINDOW=1; shift ;;
    *)
      if [[ -z "$TASK" ]]; then TASK="$1"; shift
      else echo "argomento inatteso: '$1'" >&2; exit 2; fi ;;
  esac
done

USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--session-id <uuid>]
                                                   (es. deck-run T18)
     | deck-run --no-task                          (sessione nuda, senza task)
     | deck-run <TaskID> --resume <uuid>           (riprende sessione scoped)
     | deck-run --no-task --resume <uuid>          (riprende sessione spot)
     | deck-run <TaskID> --resume <uuid> --fork [--session-id <nuovo-uuid>]
                                                   (forka: ramo con id NUOVO)

  --prompt-kind  prompt iniziale della sessione bound (default: recap)
       none        nessun prompt — sessione aperta sulla task, a mani nude
       recap       /loom-works:recap-status <TaskID>       (dispatcher)
       recap-task  /loom-works:recap-status-task <TaskID>  (task non cappello)
       recap-epic  /loom-works:recap-status-epic <TaskID>  (Size: Epic)
       preflight   /loom-works:preflight-task <TaskID>
       run         /loom-works:run-task <TaskID>
       checkpoint  /loom-works:checkpoint-task <TaskID>

  --prompt       prompt LETTERALE, per chi lo ha già composto (o fatto
                 modificare). Esclusivo con --prompt-kind; vuoto = nessun
                 prompt. Ammesso anche con --no-task: un testo già scritto non
                 ha una task da nominare, a differenza dei kind del catalogo

  --model        modello della sessione: fable|opus|sonnet|haiku
                 (default: il modello del kind in catalogo, o 'fable' fuori catalogo)
                 valore ignoto → fallback sul default, con avviso su stderr

  --title-note   nota della conversazione, appesa al titolo tab
                 (ridotta a lettere/cifre/spazi/-/_, cap 60 char)

  --new-window   apre una FINESTRA nuova invece di una tab nella finestra
                 attiva. Per chi chiama da fuori dalla finestra del progetto"

if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
  echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
  echo "$USAGE" >&2
  exit 2
fi
if [[ $NO_TASK -eq 0 && -z "$TASK" ]]; then
  echo "$USAGE" >&2
  exit 2
fi
if [[ $FORK -eq 1 && -z "$RESUME_ID" ]]; then
  echo "--fork richiede --resume <uuid>: si forka una conversazione esistente, non il nulla" >&2
  echo "$USAGE" >&2
  exit 2
fi
if [[ -n "$RESUME_ID" && -n "$SESSION_ID" && $FORK -eq 0 ]]; then
  echo "--resume e --session-id sono mutuamente esclusivi (la sessione ripresa ha già il suo id)" >&2
  echo "usa --fork se vuoi un ramo con id nuovo" >&2
  echo "$USAGE" >&2
  exit 2
fi
# Un kind fuori catalogo è un errore d'uso del CHIAMANTE (simbolo interno, non
# configurazione utente) → exit, non fallback silenzioso come permissionMode:
# lì il valore arriva da un file editabile a mano, qui da un argomento nostro.
if [[ -n "$PROMPT_KIND" ]]; then
  case "$PROMPT_KIND" in
    none|recap|recap-task|recap-epic|preflight|run|checkpoint) ;;
    *) echo "--prompt-kind ignoto: '${PROMPT_KIND}' (usa none|recap|recap-task|recap-epic|preflight|run|checkpoint)" >&2
       echo "$USAGE" >&2
       exit 2 ;;
  esac
fi
# Ogni prompt del catalogo nomina il TaskID: senza task non c'è nulla da
# chiedere, e un `--no-task --prompt-kind run` è una richiesta contraddittoria,
# non una da degradare in silenzio.
if [[ $NO_TASK -eq 1 && -n "$PROMPT_KIND" ]]; then
  echo "--prompt-kind richiede una task: una sessione --no-task non ha prompt iniziale" >&2
  echo "$USAGE" >&2
  exit 2
fi
# T134/D9 — `--prompt` con `--no-task` è AMMESSO, e il divieto che c'era qui è
# caduto. Il razionale del divieto valeva per il catalogo, non per il testo
# letterale: i sette kind nominano tutti il TaskID, quindi senza task non c'è
# nulla da chiedere loro; un prompt già composto dal chiamante non ha invece
# nessuna task da nominare — e ce ne sono di legittimi che non ne hanno una.
# Il caso che ha fatto cadere il divieto: il deck offre il drain di un file
# inbox e lo srotolamento dei `.md` di un path, due lavori che stanno sulla doc
# e non su una task; legarli a un cappello esporterebbe `LOOM_TASK`, farebbe
# iniettare in contesto il task file di un lavoro chiuso e metterebbe `· T<n>`
# nel titolo della tab (D10 preflight).
#
# Il divieto gemello su `--prompt-kind` RESTA. Toglierlo non è simmetrico:
# ogni template del catalogo interpola `{TASK}`, e senza task produrrebbe un
# prompt come `/loom-works:run-task ` con l'id vuoto — cioè una sessione che
# parte su un comando monco, che è il guasto che il divieto esiste per evitare.
# Due modi di dire la stessa cosa: accettarli insieme obbligherebbe a stabilire
# quale vince, cioè a scrivere una precedenza che nessun chiamante ha chiesto.
if [[ $PROMPT_GIVEN -eq 1 && -n "$PROMPT_KIND" ]]; then
  echo "--prompt e --prompt-kind sono mutuamente esclusivi (il testo o il simbolo, non entrambi)" >&2
  echo "$USAGE" >&2
  exit 2
fi

MODE="${LOOM_DECK_SPAWN_MODE:-inline}"
PROFILE_UUID="${LOOM_DECK_PROFILE_UUID:-5a36ae48df1c4d4882f43060e3e59656}"
WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"

# ── Titolo tab = label standard loom (matchabile da compass) ──────────────────
# compass matcha le finestre cercando nel titolo un prefisso `{emoji} {name}`,
# dove i due token sono DERIVATI dal contratto committato .claude/loom-works.json
# → es. "🧵 loom-works". Un titolo "cc <task>" non contiene la label → finestra
# ORFANA (progetto appare spento anche mentre ci lavori dentro). Deriviamo la
# label dal file e la usiamo come titolo, con
# suffisso "· <task>" per restare distinguibili fra più tab claude (il suffisso
# non rompe il match: è .includes, non equals — stesso pattern del "· deck").
# Fallback su "cc <task>" se manca il file o jq (progetto non loom-registered).

# Riduzione della nota a un alfabeto sicuro (T64). Whitelist, non blacklist: il
# titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
# nota è testo digitato a mano — enumerare i caratteri pericolosi significa
# sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
# italiane, spazio, trattino, underscore e sei emoji (sotto); tutto il resto
# (apici, backslash, `$`, backtick, `;`, quadre, ogni altra emoji…) cade.
#
# T156 — le emoji entrano per il PREFISSO del titolo che il deck compone
# (`🚀 slug`). Senza di loro il prefisso spariva qui e restava nella lista del
# deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la stessa
# conversazione portava due titoli diversi, senza errore. Le quadre che
# delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
# stacca già dallo slug.
# T161/T160 — le sette stanno tutte nei TEMPLATE di titolo del catalogo delle
# azioni (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può
# riscrivere: quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox
# che si drena, 📏 il path che si srotola, 🧭 il bersaglio doc che si
# riorganizza.
# Questa whitelist ha un GEMELLO TypeScript (`saneNote` in `src/sane-note.ts`),
# che riduce il template al salvataggio perché quello che sta nel file di
# progetto sia quello che la tab mostrerà. I due lati non si leggono a vicenda e
# vanno cambiati insieme: il custode è il gate di `test/deck-run.test.ts`, che
# passa la stessa batteria di input a questa funzione e al gemello e pretende
# l'uguaglianza.
# La whitelist le nomina una per una e NON apre un intervallo di code point. Due
# ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
# cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
# quel verdetto si verifica un glifo alla volta (🗺️ è astrale e finisce comunque
# in `·`); e l'ordine di un intervallo dentro un bracket expression segue la
# collazione della locale, non i code point.
#
# Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
# da sola, quindi il limite serve solo a non spingere fuori vista la parte del
# titolo che compass matcha, non a far stare la nota nella tab.
_sane_note() {  # <raw> → nota ridotta, spazi collassati, cap 60 char
  # LC_ALL locale alla funzione: serve a bash per tagliare a 60 CARATTERI e non
  # a 60 byte (a byte, un cap che cade a metà di una `à` lascia UTF-8 rotto nel
  # titolo). L'assegnazione rifà setlocale, l'uscita dalla funzione lo ripristina.
  local LC_ALL=C.UTF-8
  local s
  # La locale va rimessa SUL `sed`, non ereditata da qui: `local LC_ALL` non
  # porta l'attributo export quando `LC_ALL` non era già esportata nell'ambiente
  # (misurato: il figlio la vede vuota), quindi `sed` ricade su `LANG` — sotto
  # una locale `C` lavora a byte e un carattere multibyte fuori whitelist lascia
  # nel titolo i byte che coincidono con quelli ammessi. Le emoji astrali, che
  # condividono i primi due byte `f0 9f`, ne lasciano sempre due: `🔥 fuoco`
  # diventa `\xf0\x9f fuoco`, cioè UTF-8 rotto dentro il titolo della tab.
  # `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
  # come byte di continuazione di una sequenza UTF-8.
  s="$(printf '%s' "$1" \
       | LC_ALL=C.UTF-8 sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏🧭-]//g' \
       | tr -s ' ')"
  s="${s# }"; s="${s% }"
  printf '%s' "${s:0:60}"
}

_find_project_root() {  # <start-dir> → dir con .claude/loom-works.json, o vuoto
  local d="${1%/}"
  while [[ -n "$d" && "$d" != "/" ]]; do
    [[ -f "$d/.claude/loom-works.json" ]] && { printf '%s\n' "$d"; return 0; }
    d="$(dirname "$d")"
  done
  [[ -f "/.claude/loom-works.json" ]] && { printf '/\n'; return 0; }
  return 1
}

# Project root risolta UNA volta: la usano sia la label sia permissionMode (T45).
_proot=""
if command -v jq >/dev/null 2>&1; then
  _proot="$(_find_project_root "$WORKDIR" || true)"
fi
_cfg=""
[[ -n "$_proot" ]] && _cfg="${_proot}/.claude/loom-works.json"

# La label vale per OGNI surface claude, task o no (T42): il match compass è
# window-level e non sa nulla di task → legare la titolazione alla presenza di
# una task sarebbe un errore di layer. Cambia solo il suffisso: `· <task>` quando
# la sessione è bound, nessun suffisso quando è nuda.
if [[ $NO_TASK -eq 1 ]]; then TITLE="cc"; else TITLE="cc ${TASK}"; fi
if [[ -n "$_cfg" ]]; then
  _label="$(jq -r 'if (.emoji and .name)
                   then "\(.emoji) \(.name)" else empty end' \
            "$_cfg" 2>/dev/null || true)"
  if [[ -n "${_label:-}" ]]; then
    if [[ $NO_TASK -eq 1 ]]; then TITLE="${_label}"; else TITLE="${_label} · ${TASK}"; fi
  fi
fi
# Suffisso nota (T64): distingue fra loro le tab di più conversazioni sulla
# STESSA task, che altrimenti condividono un titolo identico (`label · T81`) —
# la nota è già la maniglia con cui l'utente le distingue in lista. In coda per
# la solita ragione degli altri suffissi: il match compass è `.includes(label)`,
# la label deve restare intatta in TESTA.
# T150 — nudo, senza i caporali `«…»` che lo decoravano: la nota di fallback
# generata dal deck quando il campo è vuoto passa dallo stesso alfabeto ridotto
# e non ha bisogno di una cornice per distinguersi da un titolo scritto a mano.
# Nota ridotta a vuoto (era tutta emoji/punteggiatura) → nessun suffisso.
if [[ -n "$TITLE_NOTE" ]]; then
  _note="$(_sane_note "$TITLE_NOTE")"
  [[ -n "$_note" ]] && TITLE="${TITLE} ${_note}"
fi
# Suffisso fork (T28): un ramo eredita task e label dell'origine, quindi senza
# marcatore le due tab risulterebbero omonime nella stessa window. Suffisso e
# non prefisso perché il match compass è `.includes(label)` e la label deve
# restare intatta in testa — stesso schema di `· <task>` e `· deck`.
[[ $FORK -eq 1 ]] && TITLE="${TITLE} · fork"

# ── permissionMode (T45) ─────────────────────────────────────────────────────
# Precedenza: env LOOM_DECK_PERMISSION_MODE > campo del file config > 'manual'.
# Il flag è passato SEMPRE, anche per 'manual': lo spawn resta deterministico e
# leggibile nel process tree, invece di dipendere dal default del CLI (che può
# cambiare fra versioni). Un valore fuori enum NON viene passato al CLI (che
# rifiuterebbe, lasciando una tab con un comando rotto): fallback su 'manual'
# con segnalazione su stderr.
PERM_MODE="${LOOM_DECK_PERMISSION_MODE:-}"
if [[ -z "$PERM_MODE" && -n "$_cfg" ]]; then
  PERM_MODE="$(jq -r '.permissionMode // empty' "$_cfg" 2>/dev/null || true)"
fi
case "${PERM_MODE:-}" in
  acceptEdits|auto|bypassPermissions|manual|dontAsk|plan) ;;
  '') PERM_MODE="manual" ;;
  *)  echo "permissionMode ignoto: '${PERM_MODE}' → fallback 'manual'" >&2
      PERM_MODE="manual" ;;
esac
MODE_FLAG="--permission-mode ${PERM_MODE} "

# ── modello (T108 + T152) ────────────────────────────────────────────────────
# Precedenza: --model > env LOOM_DECK_MODEL > modello del kind nel catalogo >
# 'fable'.
#
# T161 — questa cascata vale per chi invoca deck-run A MANO. Il deck TUI passa
# SEMPRE `--model` già risolto (default del deck ← override di progetto nel
# blocco `spawn` di `.claude/loom-works.json`), quindi dal deck si ferma al primo
# gradino e i tre sotto non intervengono mai. Il blocco di progetto qui non si
# legge di proposito: `jq` è dipendenza opzionale di questo script, e dargli un
# JSON da consultare la renderebbe obbligatoria su un percorso che oggi degrada
# senza. L'ultimo gradino è il gemello bash di `MODEL_DEFAULT` in spawn.ts e
# va tenuto allineato a mano: i due lati non si leggono a vicenda (il deck deve
# poter MOSTRARE la selezione iniziale prima che deck-run esista come processo),
# quindi un cambio qui senza l'altro fa partire dalla lista un modello diverso
# da quello che il detail aveva scritto a schermo.
# Fino a T152 il default era un valore unico per tutta la famiglia,
# perché il frontmatter `model:` delle skill riscriveva comunque il modello di
# sessione — un default configurabile qui non sarebbe servito a nulla. Da
# quando le skill non dichiarano più `model:`, il default DEVE viaggiare col
# kind: `run` costa quanto `preflight` solo se «vuole sonnet» è scritto da
# qualche parte che deck-run consulta, e quella parte è il catalogo che il
# deck TUI legge già per il testo del prompt (T117) — stessa fonte, stesso
# `kind`, niente seconda copia.
# Il flag è passato SEMPRE, anche sul default, per la stessa ragione di
# permissionMode: lo spawn resta leggibile nel process tree invece di dipendere
# dal default del CLI, che cambia fra versioni.
# Valore fuori enum → fallback con avviso, non exit. Regime opposto a
# --prompt-kind, e il discriminante è dove finisce il valore: un kind ignoto si
# ferma DENTRO lo script (nessun template da scegliere, l'errore è del deck),
# un modello ignoto arriverebbe al CLI e produrrebbe una tab con un comando che
# fallisce all'avvio — cioè il guasto che permissionMode evita degradando.
_model_for_kind() {  # <kind> → modello dichiarato in catalogo (3ª colonna), o niente
  local want="$1" k t m
  [[ -n "$want" && -f "$PROMPT_CATALOG" ]] || return 1
  while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
    [[ -z "$k" || "$k" == \#* ]] && continue
    [[ "$k" == "$want" && -n "$m" ]] && { printf '%s' "$m"; return 0; }
  done < "$PROMPT_CATALOG"
  return 1
}
# Kind di catalogo effettivo: SOLO quando la sessione è bound e senza prompt
# letterale — le due strade (--no-task, --prompt) non hanno una riga di
# catalogo da consultare, né per il prompt (sotto) né per il modello (qui).
# Riusato tal quale nella composizione del prompt: stesso guard, stesso valore.
CATALOG_KIND=""
if [[ $PROMPT_GIVEN -eq 0 && $NO_TASK -eq 0 ]]; then
  CATALOG_KIND="${PROMPT_KIND:-recap}"
fi
MODEL="${MODEL:-${LOOM_DECK_MODEL:-}}"
if [[ -z "$MODEL" && -n "$CATALOG_KIND" && "$CATALOG_KIND" != "none" ]]; then
  MODEL="$(_model_for_kind "$CATALOG_KIND" || true)"
fi
case "${MODEL:-}" in
  fable|opus|sonnet|haiku) ;;
  '') MODEL="fable" ;;
  *)  echo "modello ignoto: '${MODEL}' → fallback 'fable' (usa fable|opus|sonnet|haiku)" >&2
      MODEL="fable" ;;
esac
MODEL_FLAG="--model ${MODEL} "

# ── Profilo di stato per compass ─────────────────────────────────────────────
# Lo stato di una sessione (running/ask/done) viaggia verso compass via D-Bus
# keyed su $PTYXIS_PROFILE (hook Claude → `compass <stato>`), e compass lo mappa
# a un progetto SOLO se quell'UUID compare in `bindings/<surface>/profile` del
# registry. Ma nel modo `inline` la tab nasce da `ptyxis --tab` nudo, che eredita
# il profilo DEFAULT di Ptyxis: l'annuncio arriva keyed su un UUID che nessun
# progetto dichiara → stato scartato, pallino fermo su idle per sempre. Il match
# della FINESTRA non se ne accorge (passa dal titolo, non dal profilo), quindi il
# progetto risulta presente ma senza stato — un guasto che non si manifesta come
# errore, solo come pallino che non cambia mai.
# Rimedio: forzare in-tab la PTYXIS_PROFILE del binding claude del progetto, così
# l'annuncio è keyed sull'UUID che compass cerca davvero. Il profilo bindato NON
# è usabile come profilo vero della tab (`--tab-with-profile`): ha
# use-custom-command=true, che fa scartare il comando inline — vedi il ramo
# `profile` sotto, che per questo deve riscrivere dconf.
#
# `${VAR+set}`, non `${VAR:-}`: env settata anche a VUOTO = "salta il lookup"
# (deterministico per i test, che altrimenti leggerebbero il dconf della macchina
# su cui gira la suite). Non settata = risolvi dal registry.
# Assente/non risolvibile → nessun prefisso: si degrada al comportamento di prima
# (stato orfano) invece di rompere lo spawn.
if [[ -n "${LOOM_DECK_STATE_PROFILE+set}" ]]; then
  STATE_PROFILE="${LOOM_DECK_STATE_PROFILE}"
else
  STATE_PROFILE=""
  if [[ -n "$_cfg" ]] && command -v dconf >/dev/null 2>&1; then
    _pid="$(jq -r '.id // empty' "$_cfg" 2>/dev/null || true)"
    if [[ -n "$_pid" ]]; then
      STATE_PROFILE="$(dconf read "/org/lamemind/loom/projects/${_pid}/bindings/claude/profile" 2>/dev/null | tr -d "'" || true)"
    fi
  fi
fi
# Whitelist come per la nota del titolo: il valore entra in `bash -lc "…"`, e un
# registry è pur sempre un file editabile a mano. Un UUID Ptyxis è esadecimale;
# accetto l'alfabeto id-safe e scarto il resto invece di quotare.
if [[ -n "$STATE_PROFILE" && ! "$STATE_PROFILE" =~ ^[A-Za-z0-9_-]+$ ]]; then
  echo "profilo di stato ignorato (caratteri non ammessi): '${STATE_PROFILE}'" >&2
  STATE_PROFILE=""
fi
PROFILE_ENV=""
[[ -n "$STATE_PROFILE" ]] && PROFILE_ENV="PTYXIS_PROFILE=${STATE_PROFILE} "

# --session-id per la tab: pinna il sessionId della sessione CC (vuoto → CC ne
# genera uno). Lo spazio finale tiene la concatenazione pulita quando è assente.
SID_FLAG=""
[[ -n "$SESSION_ID" ]] && SID_FLAG="--session-id ${SESSION_ID} "

# --resume per la tab (T49): riprende la sessione esistente. Stesso pattern
# flag-opzionale di SID_FLAG — la variante resta dentro l'unico IN_TAB_CMD.
RES_FLAG=""
[[ -n "$RESUME_ID" ]] && RES_FLAG="--resume ${RESUME_ID} "

# --fork-session per la tab (T28): CC apre un id NUOVO invece di riscrivere
# quello ripreso → mai due writer sullo stesso JSONL. Terzo gemello del pattern
# flag-opzionale, per la stessa ragione degli altri due: la variante non deve
# duplicare IN_TAB_CMD.
FORK_FLAG=""
[[ $FORK -eq 1 ]] && FORK_FLAG="--fork-session "

# ── Catalogo dei prompt iniziali (T56 · T117) ────────────────────────────────
# Il prompt iniziale è la scelta di CHI APRE la sessione, non una proprietà della
# task: lo stesso task file si apre per leggerne lo stato, per un preflight, per
# eseguirlo o a mani nude. Il kind è quell'intento.
#   none       → nessun prompt (≠ stringa vuota: PROMPT_ARG sparisce del tutto,
#                come sul ramo --resume, altrimenti CC riceve un posizionale vuoto)
#   recap      → skill di recap sulla task. È un DISPATCHER: `recap-status`
#                risolve la task, classifica (progetto / task / epica) e passa a
#                una sotto-skill. Fino a v0.45 era invece un prompt diretto
#                ("recap stato task <id>"), scelto perché non esisteva una skill
#                di recap tarata sulla singola task e quella di progetto avrebbe
#                risposto largo — vincolo caduto col dispatcher.
#   recap-task → la sotto-skill diretta, senza il giro del dispatcher
#   recap-epic → idem per un cappello (`Size: Epic`)
#                I due specializzati li sceglie chi SA già come è fatta la task:
#                il detail del deck, che il task file l'ha aperto. Chi non lo sa
#                — gli acceleratori della lista, che leggono solo `tasks.md`, e
#                quindi non vedono il `Size` — resta su `recap` e lascia
#                classificare al dispatcher. Il criterio non è duplicato: sta in
#                `taskIsEpic` (src/tasks.ts) e legge lo stesso campo che leggerebbe
#                il dispatcher; quello che cambia è solo QUANDO viene letto.
#   preflight  → skill di preflight sulla task
#   run        → skill di esecuzione sulla task
#   checkpoint → skill di checkpoint sulla task
#
# T117 — i testi non stanno più qui ma nel file dati `prompt-catalog`, sibling di
# questo script. Il deck TUI li mostra nel campo prompt del detail prima dello
# spawn, e per farlo deve leggerli: come `case` bash sarebbe una seconda scrittura
# delle stesse regole. Il file resta l'unica copia, letta da entrambi i lati.
# Formato a righe e non JSON perché `jq` è dipendenza OPZIONALE di questo script
# (con fallback) e leggere il catalogo lo renderebbe dura.
#
# Precedenza con l'env: LOOM_DECK_ENTER_PROMPT VINCE sul kind (retro-compat: chi
# lo usa oggi non si accorge del cambio), tranne su `none` — chiedere
# esplicitamente nessun prompt non è una richiesta che un default d'ambiente
# possa scavalcare. Non vince invece su `--prompt`, che è una richiesta esplicita
# e già risolta: un override d'ambiente che scavalca un testo composto a mano
# butterebbe via proprio la modifica che il flag esiste per portare.
# Ogni template nomina il TaskID via `{TASK}`: la lista del deck è monofamiglia
# (`T`, unico prefisso del contratto loom), quindi nessun prompt discrimina sulla
# forma dell'id.
_prompt_template() {  # <kind> → template col placeholder {TASK}, o niente
  local want="$1" k t m
  [[ -f "$PROMPT_CATALOG" ]] || return 1
  # 3 variabili, non 2 (T152): da quando il catalogo porta una 3ª colonna
  # modello, un `read -r k t` a due si mangerebbe il tab separatore e il
  # modello dentro `t`, che è esattamente il template passato al CLI.
  while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
    [[ -z "$k" || "$k" == \#* ]] && continue
    [[ "$k" == "$want" ]] && { printf '%s' "$t"; return 0; }
  done < "$PROMPT_CATALOG"
  return 1
}

if [[ $PROMPT_GIVEN -eq 1 ]]; then
  PROMPT="$PROMPT_TEXT"
elif [[ $NO_TASK -eq 1 ]]; then
  # Sessione nuda senza `--prompt`: nessun prompt iniziale, punto.
  #
  # Il default `recap` del catalogo NON si applica qui, e la guardia deve stare
  # a monte invece che a valle. Finché il ramo `--no-task` scartava il prompt
  # al momento di comporre il comando in-tab, un `PROMPT` risolto e mai usato
  # era innocuo; da quando il PROMPT_ARG entra anche in quel ramo (T134/D9,
  # per il drain e lo srotolamento) il testo del catalogo ci arriva davvero —
  # e siccome ogni template interpola `{TASK}`, con la task vuota diventa
  # `/loom-works:recap-status ` con l'id monco: la sessione vuota del deck (`c`)
  # nasceva su un recap che nessuno ha chiesto.
  #
  # Stessa ragione per `LOOM_DECK_ENTER_PROMPT`, che interpola `{TASK}` a sua
  # volta e qui resta quindi inerte: senza task non ha nulla da nominare.
  PROMPT=""
else
  _kind="$CATALOG_KIND"
  if [[ "$_kind" == "none" ]]; then
    PROMPT=""
  else
    # Catalogo assente o voce mancante: il kind è già passato dall'enum sopra,
    # quindi qui manca il FILE — un'installazione mutila, non un errore d'uso.
    # Si degrada a nessun prompt dicendolo, invece di aprire una sessione con un
    # prompt inventato al volo (che sarebbe la copia del catalogo).
    if ! PROMPT="$(_prompt_template "$_kind")"; then
      echo "catalogo prompt non leggibile ('${PROMPT_CATALOG}'): sessione senza prompt iniziale" >&2
      PROMPT=""
    fi
    PROMPT="${PROMPT//\{TASK\}/${TASK}}"
  fi
  if [[ "$_kind" != "none" && -n "${LOOM_DECK_ENTER_PROMPT:-}" ]]; then
    PROMPT="${LOOM_DECK_ENTER_PROMPT//\{TASK\}/${TASK}}"
  fi
fi

# Comando eseguito dentro la tab: bind LOOM_TASK per la sessione, avvia CC sul PROMPT.
# `--name '<TITLE>'` = canale AUTORITATIVO del terminal-title (Ptyxis -T è solo il
# titolo iniziale, prima che CC parta): CC lo setta e nomina anche la sessione nel
# picker. Override-abile via LOOM_DECK_INTAB_CMD per i test empirici (probe non-CC).
# Ramo --no-task (T42): saltano i TRE elementi che legano la sessione alla task —
# iniezione LOOM_TASK, prompt iniziale, --session-id pinnato. Resta claude nudo,
# titolato e con il permission mode del progetto.
# Su resume (T49) il prompt iniziale salta anche nel ramo task: riprendere una
# conversazione significa continuarla, non iniettarle un nuovo messaggio. Il
# fork (T28) ricade nello stesso ramo — è una ripresa, con id nuovo.
# Il kind `none` (T56) atterra sulla stessa assenza per una ragione diversa: lì
# la sessione è nuova e bound, ma l'utente ha chiesto di entrarci a mani nude.
#
# QUOTING DEL PROMPT — il testo entra in `bash -lc "$IN_TAB_CMD"`, cioè in una
# riga che una shell PARSA. Finché i template erano scritti qui dentro bastava il
# vincolo "nessun apice singolo nel catalogo"; da T117 il testo può arrivare da
# `--prompt`, cioè digitato a mano dall'utente nel campo del detail — e lì un
# apice singolo chiuderebbe la stringa, consegnando alla shell tutto ciò che
# segue come comando. Si passa quindi per la stessa forma canonica di
# `shellQuote` in spawn.ts: apice chiuso, apice letterale escapato, apice
# riaperto (`'\''`). Dentro apici singoli nessun altro carattere resta speciale,
# quindi non c'è nient'altro da enumerare — newline compresi.
_shell_quote() {  # <testo> → il testo come argomento singolo per una shell POSIX
  printf "'%s'" "${1//\'/\'\\\'\'}"
}
PROMPT_ARG=""
[[ -n "$PROMPT" && -z "$RESUME_ID" ]] && PROMPT_ARG="$(_shell_quote "$PROMPT")"
# Il SID_FLAG entra anche nel ramo --no-task, che altrimenti lo scarta insieme
# agli altri elementi task-bound: nel fork di una sessione SPOT il sessionId
# pinnato non serve a legare una task (non c'è) ma a rendere noto in anticipo
# l'id del ramo, senza il quale il lineage non sarebbe registrabile.
# PROFILE_ENV in testa a entrambi i rami: lo stato è una proprietà della SESSIONE
# (esiste anche senza task), esattamente come la label del titolo — legarlo al
# ramo task-bound ripeterebbe l'errore di layer già evitato per la titolazione.
# MODEL_FLAG in entrambi i rami e prima dei flag di continuità: il modello è la
# scelta di chi apre la sessione, non una proprietà della task né della ripresa
# (T108) — stesso layer di MODE_FLAG, che infatti gli sta accanto.
if [[ $NO_TASK -eq 1 ]]; then
  # T134/D9 — il PROMPT_ARG entra anche qui: dal 134 una sessione nuda può
  # nascere su un prompt letterale (drain di un file inbox, srotolamento di un
  # path). Resta vuoto su ogni altro percorso `--no-task`, perché senza
  # `--prompt` il ramo del catalogo non gira — `PROMPT_KIND` con `--no-task`
  # è ancora rifiutato a monte.
  _default_intab="${PROFILE_ENV}claude --name '${TITLE}' ${MODE_FLAG}${MODEL_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
else
  _default_intab="${PROFILE_ENV}LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${MODEL_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
fi
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"

# ANNUNCIO del comando in-tab, su stdout, prima dell'exec.
#
# È l'unico modo che ha un chiamante di sapere cosa gira davvero dentro la tab:
# il comando si compone QUI — catalogo dei prompt, quoting, permission mode,
# profilo di stato, titolo — e ricomporlo dall'altro lato sarebbe una seconda
# scrittura delle stesse regole, divergente al primo flag aggiunto. Il deck lo
# legge da questa riga e la mostra nella propria riga di stato.
#
# Deve stare DOPO l'ultima assegnazione di IN_TAB_CMD e PRIMA dell'exec (che
# sostituisce il processo): annunciare un valore ancora modificabile
# annuncerebbe qualcosa di diverso da quello eseguito.
#
# stdout e non stderr: qui non c'è nessuna anomalia da segnalare, è l'esito
# normale del lavoro dello script. Le diagnostiche restano su stderr, e i
# chiamanti che leggono l'ULTIMA riga di stdout (lo shim `ptyxis` dei test)
# vedono comunque quello che vedevano prima.
printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"

# Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova. Una
# variabile e non due rami duplicati per ogni modalità di spawn — `--new-window`
# è l'unica differenza fra i due casi.
if [[ $NEW_WINDOW -eq 1 ]]; then WIN_FLAG="--new-window"; else WIN_FLAG="--tab"; fi

case "$MODE" in
  inline)
    # --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.
    # LOOM_TASK viaggia dentro il comando → nessuna scrittura dconf.
    exec ptyxis "$WIN_FLAG" -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
    ;;
  profile)
    # --tab-with-profile: riusa il profilo (custom-command FISSO) → per iniettare
    # LOOM_TASK+task va riscritto il custom-command al volo via dconf (stato condiviso).
    # Con --new-window i due flag convivono: la finestra nasce e il profilo
    # decide cosa gira nella sua prima tab.
    dpath="/org/gnome/Ptyxis/Profiles/${PROFILE_UUID}/"
    dconf write "${dpath}use-custom-command" true
    dconf write "${dpath}custom-command" "'bash -lc \"${IN_TAB_CMD//\"/\\\"}\"'"
    if [[ $NEW_WINDOW -eq 1 ]]; then
      exec ptyxis --new-window --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
    else
      exec ptyxis --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
    fi
    ;;
  *)
    echo "LOOM_DECK_SPAWN_MODE ignoto: '$MODE' (usa 'inline' o 'profile')" >&2
    exit 2
    ;;
esac
