conf update

This commit is contained in:
Nicolas Cantu 2026-03-15 12:51:16 +01:00
parent fa5804779c
commit f0be20f204
52 changed files with 670 additions and 538 deletions

View File

@ -1,120 +0,0 @@
# Gitea issues scripts et agents
Dossier dédié au traitement des tickets (issues) Gitea du dépôt **4nk/lecoffre_ng** (https://git.4nkweb.com/4nk/lecoffre_ng/issues). Toute la logique d'appel API et Git est dans les scripts ; les agents orchestrent et appellent /fix ou /evol.
## Agents (.cursor/agents/)
| Agent | Fichier | Rôle |
|-------|---------|------|
| **agent-loop** | `agent-loop.md` | Orchestre la boucle de récupération des mails et le traitement : lance les 2 boucles en arrière-plan (récupération + traitement, pas de timeout) ou exécute x fois (récupération 1 fois puis traitement 1 fois via gitea-issues-process). |
| **gitea-issues-process** | `gitea-issues-process.md` | Traite les issues Gitea et les mails en attente : liste les issues/mails, crée des branches, lance /fix ou /evol, /push-by-script ; workflow mails (fil, réponse réelle, marquage lu). |
Spooler tickets (nouveau) : mails dans **projects/<id>/data/issues/** (filtre par `tickets.authorized_emails` dans conf.json), pas de « non lu », aucun enregistrement supprimé. Format : `gitea-issues/TICKETS_SPOOL_FORMAT.md`. Récupération : `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`. Référence boucle mails (legacy) : `gitea-issues/AGENT_LOOP.md`. Hook Cursor : `sessionStart``.cursor/hooks/remonter-mails.sh` (lit `projects/<id>/data/issues/*.pending`).
## Contexte d'exécution
- **Emplacement** : `gitea-issues/` est dans le sous-module **ia_dev** du dépôt projet. Chemin typique : `<racine_projet>/ia_dev/gitea-issues/`. Fonctionne pour tout projet utilisant ia_dev de la même manière.
- **Projet cible** : le projet est identifié **dynamiquement** par le fichier `../ai_project_id` (à la racine du dépôt projet, parent de `ia_dev`). Son contenu (slug) sert à charger la config dans `ia_dev/projects/<id>/`. Changer de projet = changer le dépôt (ou le contenu de `ai_project_id`) ; aucun chemin à hardcoder.
- **Lancement** : le chat Cursor et les scripts sont lancés depuis **ia_dev** (`<racine_projet>/ia_dev/`), mais les opérations (issues, mails, déploiement) concernent le dépôt **parent** (`../` = racine projet). Invoquer les scripts depuis la racine du dépôt projet : `cd <racine_projet> && ./ia_dev/gitea-issues/<script>.sh`. Depuis le workspace ia_dev, racine projet = `..`.
- **Secrets et logs** : `.secrets` est sous **ia_dev** (`ia_dev/.secrets`, soit `./.secrets` depuis ia_dev), et **ne dépend pas du projet parent**. Les scripts résolvent la racine pour `.secrets` et `logs` via le répertoire contenant `gitea-issues` (ia_dev), afin que la config IMAP/SMTP et les logs soient les mêmes quel que soit le projet ; un même clone ia_dev peut servir plusieurs projets configurés par `../ai_project_id`.
## Prérequis
- **jq** : `apt install jq` ou `brew install jq`
- **Token Gitea** : variable denvironnement `GITEA_TOKEN` ou fichier `.secrets/gitea-issues/token` (contenu = le token, non versionné). Créer le token dans Gitea : Settings → Applications → Generate New Token (scopes `read:issue`, `write:issue` si commentaires).
## Scripts (depuis la racine du dépôt)
| Script | Usage | Description |
|--------|--------|-------------|
| `list-open-issues.sh` | `./gitea-issues/list-open-issues.sh [--lines] [--limit N]` | Liste les issues ouvertes (JSON ou lignes `number\|title\|state`). |
| `get-issue.sh` | `./gitea-issues/get-issue.sh <num> [--summary]` | Détail dune issue (JSON ou résumé texte). |
| `print-issue-prompt.sh` | `./gitea-issues/print-issue-prompt.sh <num>` | Affiche titre + corps pour fournir la consigne à lagent. |
| `create-branch-for-issue.sh` | `./gitea-issues/create-branch-for-issue.sh <num> [base]` | Crée et checkout la branche `issue/<num>` depuis `base` (défaut `test`). |
| `comment-issue.sh` | `./gitea-issues/comment-issue.sh <num> <message>` ou `echo "msg" \| ./gitea-issues/comment-issue.sh <num> -` | Ajoute un commentaire à lissue. |
| `mail-list-unread.sh` | `./gitea-issues/mail-list-unread.sh` | Liste les mails **non lus envoyés à l'alias** (MAIL_FILTER_TO, défaut ai.support.lecoffreio@4nkweb.com) ; lecture seule ; sortie : UID, Message-ID, From, To, Subject, Date, Body. Aucun autre mail n'est listé. |
| `mail-get-thread.sh` | `./gitea-issues/mail-get-thread.sh <uid>` | Récupère **tout le fil** (conversation) du mail donné : tous les messages liés par References/In-Reply-To, tri chronologique (ancien → récent). Même format de sortie que mail-list-unread. À utiliser avant de décider ou répondre sur un mail. |
| `mail-send-reply.sh` | `./gitea-issues/mail-send-reply.sh --to <addr> --subject "..." [--body "..." \| stdin] [--in-reply-to "<msg-id>" [--references "..."]]` | Envoie une réponse par mail via le Bridge (SMTP) ; signature « Support IA du projet Lecoffre.io » / ai.support.lecoffreio@4nkweb.com ajoutée automatiquement. |
| `mail-create-issue-from-email.sh` | `./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..."]` | Crée une issue à partir dun mail (UID), optionnel titre/corps formalisés ; marque le mail lu. |
| `mail-mark-read.sh` | `./gitea-issues/mail-mark-read.sh <uid>` | Marque un mail comme lu. |
| `mail-thread-log.sh` | `./gitea-issues/mail-thread-log.sh get-id \| init \| append-sent \| append-issue \| append-commit ...` | **Log par fil** : un fichier par conversation dans `projects/<id>/logs/gitea-issues/threads/` (échanges reçus/envoyés, tickets, commits). `get-id --uid <uid>` affiche `THREAD_ID=...` ; `init --uid <uid>` crée/met à jour le fichier ; `append-sent/issue/commit` enregistrent une réponse, une issue ou un commit. |
| `mail-to-issue.sh` | `./gitea-issues/mail-to-issue.sh` | **Batch** : crée une issue par mail non lu (titre = sujet, corps = texte + From), marque lus. À éviter si on suit le workflow agent (voir cidessous). |
| `agent-loop.sh` | `./ia_dev/gitea-issues/agent-loop.sh [interval_sec]` | **Boucle de surveillance** : exécute périodiquement `mail-list-unread.sh`, met à jour un fichier témoin (`projects/<id>/logs/gitea-issues/agent-loop.status`) et écrit les mails en attente dans `agent-loop.pending`. Voir `gitea-issues/AGENT_LOOP.md`. |
| `agent-loop-treatment.sh` | `./ia_dev/gitea-issues/agent-loop-treatment.sh` | **Boucle traitement** : vérifie périodiquement `agent-loop.pending` ; si non vide, lance l'agent Cursor (workflow gitea-issues-process) pour traiter les mails. À lancer en arrière-plan (pas de timeout). |
| `agent-loop-retrieval-once.sh` | `./ia_dev/gitea-issues/agent-loop-retrieval-once.sh` | **Récupération une fois** (legacy, basé non lu) : exécute `mail-list-unread.sh` et écrit dans `agent-loop.pending`. Utilisé par l'agent agent-loop pour les cycles « x fois ». |
| `tickets-fetch-inbox.sh` | `./ia_dev/gitea-issues/tickets-fetch-inbox.sh` | **Récupération par expéditeurs autorisés** : filtre `tickets.authorized_emails` (conf.json), pas de marquage lu/non lu. Écrit les nouveaux mails dans `projects/<id>/data/issues/<date>_<from>_<uid>.pending` (JSON). Voir `TICKETS_SPOOL_FORMAT.md`. |
Variables optionnelles : `GITEA_API_URL`, `GITEA_REPO_OWNER`, `GITEA_REPO_NAME`, `GITEA_ISSUES_DIR`.
### Création dissues depuis les mails (IMAP) workflow agent
**Ne pas enchaîner directement** : lagent doit dabord lire les non lus, formaliser lissue ou répondre par mail, et ne créer/traiter quau moment où la demande est prête.
1. **Lire les non lus** : `./gitea-issues/mail-list-unread.sh` (ne marque pas les mails comme lus).
2. **Pour chaque mail** : consulter **tout l'historique du fil** avec `./gitea-issues/mail-get-thread.sh <uid>`, créer/mettre à jour le **log du fil** avec `./gitea-issues/mail-thread-log.sh init --uid <uid>` (sortie `THREAD_ID=...` à conserver), puis décider soit denvoyer une réponse directe (demande dinfos) via `mail-send-reply.sh`, soit de formaliser et créer lissue avec `mail-create-issue-from-email.sh` (optionnel `--title` / `--body` formalisés). Si la demande est une correction/évolution prête : créer lissue, traiter (fix/evol), commenter lissue, répondre au mail via `mail-send-reply.sh` (avec `--in-reply-to` pour le fil). **Le corps de la réponse** doit contenir la **réponse réelle** à la question (ex. si le mail demande « Décrit les rôles », le body = une description des rôles), jamais le sujet du mail ni la question reçue.
3. **Réponses aux mails** : toujours via le Bridge avec `mail-send-reply.sh`. Le `--body` doit être la **réponse réelle** rédigée par l'agent (contenu de la réponse à la demande), pas le sujet du mail, pas la question reçue, pas un message précédent du fil. Chaque envoi est enregistré dans le log du fil avec `mail-thread-log.sh append-sent --thread-id <id> --to <addr> --subject "..." --body "..."` pour tracer l'expéditeur, le titre et le corps de la réponse envoyée.
**Prérequis :**
- Python 3 (stdlib : imaplib, email, smtplib, json, urllib).
- Token Gitea : comme les autres scripts (`GITEA_TOKEN` ou `.secrets/gitea-issues/token`).
- Config IMAP/SMTP : copier `gitea-issues/imap-bridge.env.example` vers `.secrets/gitea-issues/imap-bridge.env`. Pour la boucle agent (optionnel) : `agent-loop.env.example` vers `.secrets/gitea-issues/agent-loop.env` (voir AGENT_LOOP.md). Renseigner `IMAP_USER`, `IMAP_PASSWORD` (et optionnellement `SMTP_*` pour lenvoi ; par défaut SMTP reprend les mêmes host/port Bridge 1025). Optionnel : `MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com` (seuls les mails envoyés à cette adresse sont listés).
- Proton Mail Bridge (ou serveur IMAP/SMTP) en cours dexécution.
**Scripts :** `mail-list-unread.sh`, `mail-get-thread.sh`, `mail-thread-log.sh`, `mail-send-reply.sh`, `mail-create-issue-from-email.sh`, `mail-mark-read.sh`. Le script batch `mail-to-issue.sh` reste disponible mais ne doit pas être utilisé dans le cadre du workflow agent (liste → lecture du fil → log du fil → décision → création/ réponse). Le script **`agent-loop.sh`** permet de lancer une boucle de surveillance des mails avec fichier témoin ; voir `gitea-issues/AGENT_LOOP.md`.
## API Wiki (tests préalables)
Script de test de lAPI Wiki Gitea pour le même dépôt (prérequis à une éventuelle migration de `docs/` vers le wiki) :
| Script | Usage | Description |
|--------|--------|-------------|
| `wiki-api-test.sh` | `./gitea-issues/wiki-api-test.sh [--create]` | Teste GET list pages, GET page Home ; avec `--create` : POST une page test puis DELETE. |
**Prérequis :** même token que les issues (`GITEA_TOKEN` ou `.secrets/gitea-issues/token`). Pour lécriture (création / suppression de pages), le token doit avoir les droits décriture sur le dépôt.
**Endpoints utilisés (référence Gitea API 1.25) :**
- `GET /repos/{owner}/{repo}/wiki/pages` — liste des pages
- `GET /repos/{owner}/{repo}/wiki/page/{pageName}` — contenu dune page (ex. `Home`)
- `POST /repos/{owner}/{repo}/wiki/new` — créer une page (body : `title`, `content_base64`, `message`)
- `PATCH /repos/{owner}/{repo}/wiki/page/{pageName}` — modifier une page
- `DELETE /repos/{owner}/{repo}/wiki/page/{pageName}` — supprimer une page
Si le wiki na jamais été initialisé (aucune page créée via linterface), les GET peuvent renvoyer 404 ou une liste vide. **Initialiser le wiki** : aller sur https://git.4nkweb.com/4nk/lecoffre_ng/wiki et créer au moins une page (ex. « Home ») via linterface, puis relancer le script avec un token valide.
**Branche par défaut du wiki :** si lAPI renvoie `object does not exist [id: refs/heads/master]` alors que la branche par défaut du dépôt wiki est autre (ex. `prod`), cest un bug connu de certaines versions de Gitea (lAPI suppose `master`). Contournements possibles : (1) **mettre à jour Gitea** (correctif dans les versions récentes, ex. PR #34244) ; (2) **changer la branche par défaut du wiki** en `master` dans les réglages du dépôt (Settings → Branches). Variable optionnelle `GITEA_WIKI_REF=master` (défaut si wiki configuré sur master).
### Migration docs/ → wiki
**Décision :** tout le contenu de `docs/` (racine du dépôt) est migré vers le wiki ; pas de CI sur le wiki.
**Script de migration :**
| Script | Usage | Description |
|--------|--------|-------------|
| `wiki-migrate-docs.sh` | `./gitea-issues/wiki-migrate-docs.sh [--dry-run] [fichier.md ...]` | Migre `docs/*.md` vers le wiki. `--dry-run` affiche le mapping sans appel API. Si des fichiers sont passés en argument, migre uniquement ceux-là. |
| `wiki-put-page.sh` | `./gitea-issues/wiki-put-page.sh <page_name> <file_path>` | Met à jour ou crée une page wiki à partir dun fichier local (ex. `Home docs/README.md`). |
| `wiki-get-page.sh` | `./gitea-issues/wiki-get-page.sh <page_name>` | Affiche le markdown brut dune page wiki (pour scripts ou agents). |
**Correspondance fichier → page wiki :** nom de fichier sans `.md`, `_` remplacé par `-`, title-case par segment. Ex. OPERATIONS.md → Operations, README.md → Readme.
Les 17 fichiers de `docs/` ont été migrés ; les pages sont visibles sur https://git.4nkweb.com/4nk/lecoffre_ng/wiki. La page **Home** contient le contenu de `docs/README.md` (index et correspondance). **`docs/` est exclu du versionnement** (`.gitignore`) : maintenir `docs/` localement (ne pas le supprimer), pousser les modifications vers le wiki avec `wiki-migrate-docs.sh` ou `wiki-put-page.sh` ; ne pas committer `docs/`.
### Après un clone
Le répertoire `docs/` n'est pas versionné. Pour disposer d'une copie locale (édition puis synchro wiki), recréer le contenu à partir du wiki : ex. `./gitea-issues/wiki-get-page.sh Home > docs/README.md`, ou créer les fichiers manuellement à partir des pages wiki listées dans la section Migration ci-dessus.
### Usage « wiki uniquement » pour les agents
La connaissance du projet peut reposer **uniquement sur le wiki** (sans lire `docs/`) : les agents peuvent exécuter `./gitea-issues/wiki-get-page.sh <PageName>` pour récupérer le contenu markdown dune page et lutiliser comme référence. Exemples : `./gitea-issues/wiki-get-page.sh Home`, `./gitea-issues/wiki-get-page.sh Operations`, `./gitea-issues/wiki-get-page.sh Code-Standards`. Prérequis : token Gitea (comme pour les autres scripts wiki). Les agents peuvent ainsi consulter la doc projet à la demande depuis le wiki, sans dépendre des fichiers locaux `docs/`.
## Agents (commandes)
- **/agent-loop** (`agent-loop.md`) : lance les 2 boucles (récupération + traitement) en arrière-plan ou exécute x cycles (récupération 1 fois puis traitement 1 fois). À la fin d'une boucle de récupération, la boucle traitement lance gitea-issues-process sur les mails reçus.
- **/gitea-issues-process** (`gitea-issues-process.md`) : traite les issues Gitea et les mails en attente (workflow script au maximum, /fix ou /evol, /push-by-script). Voir le fichier de l'agent pour le workflow exact.
## Référence
- Wiki : https://git.4nkweb.com/4nk/lecoffre_ng/wiki
- Documentation opérationnelle (ex. `docs/OPERATIONS.md`) : page wiki **Operations** (après migration).

View File

@ -1,41 +1,35 @@
---
name: agent-loop
description: Orchestre la boucle de récupération des mails et le traitement par gitea-issues-process. Peut lancer 2 boucles en arrière-plan (récupération + traitement, pas de timeout) ou exécuter x fois (récupération 1 fois puis traitement 1 fois).
description: Orchestre la boucle de récupération des mails et le traitement par gitea-issues-process. Gère les boucles par exécutions délimitées uniquement (N itérations ou x cycles) ; ne jamais lancer de processus en arrière-plan (nohup / &).
model: inherit
is_background: false
is_background: true
---
# Agent agent-loop
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` ; `<id>` = contenu du fichier `../ai_project_id` (à la racine du dépôt projet, parent de ia_dev). Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test` (ou `..` depuis le workspace ia_dev). Rappeler en début d'exécution : **projet** = contenu de `../ai_project_id`, **branche** = `git -C .. branch --show-current`, **répertoire de travail** = répertoire du dépôt dans `../`.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, projet, branche, répertoire de travail du dépôt dans `../`.
Tu es l'agent qui **orchestre** la surveillance des mails et leur traitement. Tu ne traites pas les mails toimême : le traitement (réponse, issues, marquage lu) est fait par l'**agent gitea-issues-process**. Tu lances les scripts et/ou les sous-agents selon la demande.
**Références obligatoires** : lire `gitea-issues/AGENT_LOOP.md` (fichier témoin, variables, boucles) et `gitea-issues/README.md` (contexte d'exécution). Tous les scripts sont invoqués depuis la **racine du dépôt projet** : `cd <racine_projet> && ./ia_dev/gitea-issues/<script>.sh` (depuis le workspace ia_dev : `cd .. && ./ia_dev/gitea-issues/<script>.sh`).
**Références obligatoires** : lire `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (contexte d'exécution). Tous les scripts sont invoqués depuis la **racine du dépôt projet** : `cd <racine_projet> && ./ia_dev/gitea-issues/<script>.sh` (depuis le workspace ia_dev : `cd .. && ./ia_dev/gitea-issues/<script>.sh`).
**Script agent-loop.sh** : intervalle en premier argument (ex. `./ia_dev/gitea-issues/agent-loop.sh 50` = 50 s). La boucle utilise le **spooler** (critère from/to dans conf.json), pas le statut IMAP « non lu ». Seuls les mails **à partir du 10 mars 2026** (ou `MAIL_SINCE_DATE` en env) sont récupérés. **Fichier témoin** `projects/<id>/logs/gitea-issues/agent-loop.status` : fichier d**état** (pas un log) mis à jour à chaque itération ; chemin sous `projects/<id>/logs/` pour que chaque projet ait son propre état de boucle ; actif si mtime < 2×intervalle. Fichier pending : `projects/<id>/logs/gitea-issues/agent-loop.pending` (chemins des .pending du spooler à traiter). Variables (optionnel `.secrets/gitea-issues/agent-loop.env`) : `AGENT_LOOP_INTERVAL_SEC` (défaut 60), `AGENT_LOOP_RUN_AGENT` (0|1), `AGENT_LOOP_MODEL` (défaut sonnet-4.6), `AGENT_LOOP_STATUS_FILE`, `AGENT_LOOP_PENDING_FILE`. Hook « remonter mails » : `.cursor/hooks/remonter-mails.sh` lit `projects/<id>/data/issues/*.pending` ou `agent-loop.pending`.
**Fichiers de contrôle (section 2 — boucle x cycles)** :
- **Lock (une seule instance)** : `projects/<id>/logs/gitea-issues/agent-loop.lock`. Utiliser les scripts : **agent-loop-lock-acquire.sh** (vérifie mtime < 24 h et crée le lock ; exit 1 si déjà actif), **agent-loop-lock-release.sh** (supprime lock et fichier stop).
- **Arrêt à la demande** : `projects/<id>/logs/gitea-issues/agent-loop.stop`. Au début de chaque cycle, exécuter **agent-loop-stop-requested.sh** (exit 0 si le fichier existe) ; si oui, lancer **agent-loop-lock-release.sh** et sortir. Pour demander l'arrêt : `cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh` ou `touch projects/<id>/logs/gitea-issues/agent-loop.stop`.
---
## 1. Lancer les 2 boucles en arrière-plan (pas de timeout)
## 1. Pas de lancement en arrière-plan
Si l'utilisateur demande de **lancer les 2 boucles en arrière-plan** (récupération des mails + traitement des mails, sans timeout) :
**Règle impérative** : ne **jamais** lancer `agent-loop.sh` ni `agent-loop-treatment.sh` en arrière-plan (pas de `nohup`, pas de `&`). Ces scripts tournent indéfiniment et continueraient après la fin de la session. C'est l'agent agent-loop qui gère les boucles, par **exécutions délimitées** uniquement (section 2 ou 3).
1. **Boucle récupération** : lancer en arrière-plan depuis la racine du dépôt projet (parent de ia_dev) :
```bash
cd .. && nohup ./ia_dev/gitea-issues/agent-loop.sh 60 >> ia_dev/projects/<id>/logs/gitea-issues/agent-loop.log 2>&1 &
```
(Depuis le workspace ia_dev : `cd ..` = racine projet. Les logs et pending sont sous `ia_dev/projects/<id>/logs/gitea-issues/`.) Cette boucle exécute périodiquement `mail-list-unread.sh`, met à jour le fichier témoin et écrit les mails en attente dans `agent-loop.pending`.
2. **Boucle traitement** : lancer en arrière-plan depuis la racine du dépôt projet :
```bash
cd .. && nohup ./ia_dev/gitea-issues/agent-loop-treatment.sh >> ia_dev/projects/<id>/logs/gitea-issues/agent-loop-treatment.log 2>&1 &
```
Cette boucle vérifie périodiquement si `agent-loop.pending` est non vide ; si oui, elle lance l'agent **gitea-issues-process** (via Cursor Agent CLI) pour traiter les mails conformément au workflow mails, puis attend avant la prochaine vérification.
Ne pas masquer les sorties des scripts ; indiquer les PID des processus lancés et où consulter les logs (`ia_dev/projects/<id>/logs/gitea-issues/agent-loop.log`, `agent-loop-treatment.log`).
**À la fin d'une boucle (récupération)** : le script `agent-loop.sh` met à jour le fichier pending ; la **boucle traitement** (`agent-loop-treatment.sh`) détecte les mails et lance l'agent gitea-issues-process sur le contenu reçu. Aucune action supplémentaire de ta part n'est requise une fois les deux boucles lancées.
Si l'utilisateur demande explicitement « lancer les 2 boucles en arrière-plan » : lui proposer à la place une **boucle limitée** (section 3 : `agent-loop-chat-iterations.sh [N]` avec N choisi, ou section 2 : x cycles récupération + traitement). Ne pas lancer de processus persistants sans que l'utilisateur ait confirmé en connaissance de cause et exécuté luimême la commande s'il le souhaite.
---
@ -43,24 +37,35 @@ Ne pas masquer les sorties des scripts ; indiquer les PID des processus lancés
Si l'utilisateur demande de **lancer x fois** les deux sous-agents (une récupération puis un traitement, répété x fois) :
**Avant de commencer les x cycles** :
- Exécuter depuis la racine du dépôt : `cd .. && ./ia_dev/gitea-issues/agent-loop-lock-acquire.sh`.
- Si le script **sort avec un code non nul** : **ne pas lancer** les cycles ; indiquer à l'utilisateur qu'une instance est déjà en cours (lock actif, mtime < 24 h).
- Si le script sort 0 : le lock est acquis ; poursuivre.
- **À la fin** (normale ou après arrêt) : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-lock-release.sh` (supprime le lock et le fichier stop s'il existe).
Pour chaque cycle `i` de 1 à x :
**Au début du cycle** (avant l'étape 1) : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-stop-requested.sh`. Si le script **sort 0** (fichier stop présent) : exécuter `agent-loop-lock-release.sh`, puis **sortir** en indiquant que la boucle a été arrêtée à la demande.
1. **Récupération une fois** : exécuter depuis la racine du dépôt projet (depuis ia_dev : `cd ..`) :
```bash
cd .. && ./ia_dev/gitea-issues/agent-loop-retrieval-once.sh
```
Ce script exécute `mail-list-unread.sh` et écrit la sortie dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (et met à jour le fichier témoin). Pas d'arrière-plan : attendre la fin du script.
Ce script exécute `tickets-fetch-inbox.sh` puis `list-pending-spooler.sh` et écrit les chemins des .pending dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (et met à jour le fichier témoin). Pas d'arrière-plan : attendre la fin du script.
2. **Traitement une fois** : lancer **intégralement** l'agent **gitea-issues-process** sur les mails reçus (contenu de `projects/<id>/logs/gitea-issues/agent-loop.pending`). Utiliser le sous-agent Cursor (mcp_task ou équivalent) avec le type `gitea-issues-process` et un prompt du type :
« Traite les mails listés dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (contenu ci-dessous ou à lire depuis le fichier). Suivre strictement le workflow mails de l'agent gitea-issues-process : pour chaque UID listé, exécuter mail-get-thread.sh, mail-thread-log.sh init, décider réponse/issue, rédiger la réponse réelle (--body = texte composé par toi, jamais une citation du fil), mail-send-reply.sh, mail-thread-log.sh append-sent, mail-mark-read.sh. »
« Traite les mails du spooler listés dans `projects/<id>/logs/gitea-issues/agent-loop.pending` (chemins des .pending) ou en exécutant `list-pending-spooler.sh`. Pour chaque fichier .pending : lire le JSON, rédiger une réponse pertinente (uniquement ton texte ; pas de citation — mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Appeler mail-send-reply.sh puis write-response-spooler.sh. Ne pas appeler mail-mark-read.sh (spooler). »
Répéter les étapes 1 et 2 pour les x cycles demandés. Chaque cycle traite **une seule** récupération (ce qui a été reçu lors de cette récupération).
3. **Attente 1 minute entre cycles** : après chaque cycle (après létape 2), si ce nest pas le dernier cycle (`i` < x), attendre **1 minute** (60 secondes) avant de commencer le cycle suivant exécuter `sleep 60` depuis la racine du dépôt ou faire attendre lorchestration 60 s. Pas dattente après le dernier cycle.
Répéter les étapes 1, 2 et 3 pour les x cycles demandés. Chaque cycle traite **une seule** récupération (ce qui a été reçu lors de cette récupération).
---
## 3. Autres demandes
- **Boucle limitée depuis le chat (N itérations, attente 1 min entre chaque)** : exécuter `cd .. && ./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]` (sans arrière-plan, pour éviter timeout). Par défaut N=3 ; `--repeat` pour relancer après N itérations.
- **Arrêter la boucle en cours (section 2)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh`. Cela crée le fichier `agent-loop.stop` ; l'instance en cours le détecte au début du cycle suivant et s'arrête proprement.
- **Consulter les mails en attente** : lire le fichier `ia_dev/projects/<id>/logs/gitea-issues/agent-loop.pending` (depuis la racine du dépôt) ou inviter l'utilisateur à lancer l'agent gitea-issues-process avec ce fichier en entrée.
- **Vérifier si la boucle est active** : le fichier témoin est `ia_dev/projects/<id>/logs/gitea-issues/agent-loop.status` ; s'il a été modifié depuis moins de 2 × intervalle (ex. 120 s si intervalle 60 s), la boucle est considérée active.
@ -68,6 +73,7 @@ Répéter les étapes 1 et 2 pour les x cycles demandés. Chaque cycle traite **
## Contraintes
- **Pas de processus en arrière-plan** : ne jamais lancer `agent-loop.sh` ni `agent-loop-treatment.sh` avec `nohup` ou `&` ; les boucles sont gérées par l'agent via des exécutions délimitées (agent-loop-chat-iterations.sh N, ou cycles section 2).
- Ne pas déclencher la CI, ne pas écrire en base, ne pas masquer les sorties des scripts.
- Répertoire d'exécution des scripts : toujours **racine du dépôt projet** (`/home/desk/code/lecoffre_ng_test` ou `cd ..` depuis ia_dev).
- Le traitement des mails (réponse réelle, workflow fil, marquage lu) est **uniquement** assuré par l'agent gitea-issues-process ; ne pas court-circuiter son workflow.

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu alignes les branches distantes du projet (test, pprod, prod) en exécutant le script deploy/branch-align.sh avec **test** en paramètre par défaut. **Rôle de lagent :** vérifications (prérequis), ordre des étapes (push → docupdate → script), relances en cas derreur remontée par les sous-agents, synthèse et clôture. **Rôle du script :** exécution déterministe (fetch, force-push, vérifications git). Push direct sur les branches distantes ; aucun script ni agent ne crée de pull request.
**Focus qualité et résolution de problèmes :**

View File

@ -6,6 +6,8 @@ description: Uniquement en test, lance /push-by-script puis deploy/change-to-all
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
**Rôle de lagent :** vérifier que la branche locale est test (sinon retour 1), fournir le message de commit (via /push-by-script), lancer le script, contrôler la sortie et le code de retour. **Rôle du script :** exécution déterministe (vérif branche test, branch-align.sh test, deploy.sh test --import-v1 --skipSetupHost --no-sync-origin ; log dans logs/ par défaut).
**Focus qualité et résolution de problèmes :**

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu appliques les règles ci-dessous **lorsqu'il y a du code à produire** (évolution ou correctif). Les agents evol et fix invoquent cet agent dans ce cas.
## Liste ordonnée d'actions obligatoires pour coder

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Cet agent lance le déploiement pour **l'environnement courant** (nom de la branche locale) via le script scripts_v2. **Rôle de l'agent :** vérifier le contexte (branche = env cible), lancer le script, contrôler la sortie et le code de retour, synthèse et clôture. **Rôle du script :** exécution et orchestration sûre (suivi branches, sync, log, déploiement).
**Focus qualité et résolution de problèmes :**

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début et en fin d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
**Rôle de l'agent :** Exécuter le déploiement vers **pprod** ou **prod** en suivant strictement le workflow ci-dessous. Paramètre obligatoire : `pprod` ou `prod`. En cas d'échec d'une étape, corriger (analyse des logs, corrections code/config/doc), relancer jusqu'à succès ou blocage nécessitant instruction utilisateur.
**Répertoire d'exécution :** Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test`. Tous les scripts sont invoqués après `cd` vers cette racine.

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).
Ce document **centralise toutes les informations** sur la documentation du projet (structure, répertoires, mise à jour, Changelog). **L'appel à cet agent** est centralisé dans `.cursor/rules/cloture-evolution.mdc` (étape 12) — à exécuter intégralement lors de la clôture.
@ -30,7 +32,7 @@ Ce document **centralise toutes les informations** sur la documentation du proje
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `docs/todoFix/` et la documentation (wiki).
* **Structure de la documentation :**
* La documentation générale et pérenne se trouve dans le **wiki** du projet (URL dans `projects/<slug>.json``git.wiki_url`). Page d'accueil du wiki : **Home**.
* Pour mettre à jour le wiki : modifier le fichier correspondant dans `docs/` puis exécuter depuis la racine projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh` ; ou éditer la page directement sur le wiki. Correspondance fichier → page : voir `gitea-issues/README.md` (section Migration docs/ → wiki).
* Pour mettre à jour le wiki : modifier le fichier correspondant dans `docs/` puis exécuter depuis la racine projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh` ; ou éditer la page directement sur le wiki. Correspondance fichier → page : voir `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (section Migration docs/ → wiki).
* **`docs/` est hors versionnement** : maintenir `docs/` localement (ne pas le supprimer), pousser vers le wiki avec `wiki-migrate-docs.sh` après édition ; ne jamais committer `docs/`.
* Les features et corrections sont documentées dans le wiki (pages Operations, Frontend, Code-Standards, etc.) ; les tâches en cours dans `docs/todoFix/`.
* Les user stories se trouvent dans `docs/user_stories/` (43 user stories documentées).
@ -63,7 +65,7 @@ Dans l'ordre et pour tous les documents de docs et les pages wiki :
Documents / pages à ne pas supprimer lors des étapes suivantes (équivalents wiki des anciens fichiers docs/) :
* Page wiki Home (page d'accueil du wiki)
* Pages wiki : Api, Architecture, Code-Standards, Deployment, Operations, Readme, Scripts, etc. (voir gitea-issues/README.md pour la correspondance complète).
* Pages wiki : Api, Architecture, Code-Standards, Deployment, Operations, Readme, Scripts, etc. (voir projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md pour la correspondance complète).
* docs/sources/*
* docs/fixKnowledge/*
* docs/features/*

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu es l'agent evol, en charge des **évolutions** (nouvelles fonctionnalités, améliorations, refactors non correctifs).
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Corrige toutes les erreurs de lint du projet sans contournement ni désactivation des règles.
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu es l'agent fix-search, en charge des **investigations** en vue d'identifier la cause et la root cause d'un problème remonté.
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu es l'agent fix, en charge des **correctifs** à partir d'un problème remonté ou d'une investigation préalable.
**Horodatage et contexte** : appliquer intégralement le bloc défini dans `.cursor/rules/cloture-evolution.mdc` (début et fin d'exécution, lancement et retour des sub-agents).

View File

@ -2,14 +2,17 @@
name: gitea-issues-process
description: Traite les tickets Gitea (issues) en s'appuyant au maximum sur les scripts gitea-issues/. Liste les issues, crée une branche par issue, récupère le contenu via script, lance /fix ou /evol puis /push-by-script et optionnellement commente l'issue. Push direct uniquement ; ne jamais créer de pull request.
model: inherit
is_background: false
is_background: true
---
# Agent gitea-issues-process
Lis :
gitea-issues/README.md
gitea-issues/AGENT_LOOP.md
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev (scripts gitea-issues, spooler) est dans **`projects/ia_dev/docs`**.
**À lire en début d'exécution** (documentation fournie à l'agent) :
- projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md (contexte d'exécution, scripts)
- .cursor/agents/agent-loop.md (fichier témoin, variables, boucles)
- projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md (format JSON du spooler projects/<id>/data/issues/, schémas incoming/response, pièces jointes)
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` ; `<id>` = contenu du fichier `../ai_project_id` (à la racine du dépôt projet, parent de ia_dev). Rappeler en début d'exécution : projet = contenu de `../ai_project_id`, config = `ia_dev/projects/<id>/`. Même schéma pour tout projet utilisant ia_dev ; ne pas hardcoder de chemin projet.
@ -28,7 +31,7 @@ Tu es l'agent qui traite les **tickets (issues) Gitea** du dépôt du projet cou
- Exécution depuis la **racine du dépôt projet** (répertoire parent de ia_dev, contenant `ai_project_id`) : `cd <racine_projet> && ./ia_dev/gitea-issues/*.sh`. Depuis le workspace ia_dev, racine projet = `..`. Ne pas hardcoder le chemin d'un projet ; fonctionne pour tout projet configuré par `../ai_project_id`.
- Dépendances : `jq`, `curl` (les scripts les utilisent).
**Contexte** : `gitea-issues/` est dans ia_dev ; le projet est identifié par `../ai_project_id` ; la config est dans `ia_dev/projects/<id>/`. `.secrets` est sous ia_dev (`./.secrets`), indépendant du projet parent. Voir `gitea-issues/README.md` (Contexte d'exécution).
**Contexte** : `gitea-issues/` est dans ia_dev ; le projet est identifié par `../ai_project_id` ; la config est dans `ia_dev/projects/<id>/`. `.secrets` est sous ia_dev (`./.secrets`), indépendant du projet parent. Voir `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (Contexte d'exécution).
## Workflow (script au maximum)
@ -49,21 +52,22 @@ Tu es l'agent qui traite les **tickets (issues) Gitea** du dépôt du projet cou
L'agent **ne fait pas** d'appels IMAP/SMTP ni de curl Gitea directs : il **invoque uniquement** les scripts `gitea-issues/*.sh` depuis la racine du dépôt projet. Les scripts lisent la config (`.secrets` sous ia_dev) et font les appels réels.
**Ordre pour traiter les mails en attente** (source : **projects/<id>/data/issues/*.pending** (spooler JSON, voir `gitea-issues/TICKETS_SPOOL_FORMAT.md`) ou `projects/<id>/logs/gitea-issues/agent-loop.pending` en legacy, ou demande utilisateur) : se baser uniquement sur le **statut des issues** pour le traitement ; **aucun enregistrement ne doit être supprimé**.
**Ordre pour traiter les mails en attente** : deux sources possibles. **Aucun enregistrement ne doit être supprimé.**
1. **Lister les non lus** : exécuter `./ia_dev/gitea-issues/mail-list-unread.sh`. Sortie = blocs par mail (UID, From, To, Subject, Date, Body). Lecture seule ; ne marque pas comme lu.
2. **Pour chaque mail (UID)** :
- **Récupérer le fil** : `./ia_dev/gitea-issues/mail-get-thread.sh <uid>`. Donne tout l'historique du fil (References/In-Reply-To) pour décider en connaissance de cause.
- **Initialiser le log du fil** : `./ia_dev/gitea-issues/mail-thread-log.sh init --uid <uid>`. Sortie `THREAD_ID=...` à conserver pour append-sent.
- **Décider** : soit réponse directe (demande d'infos), soit créer une issue (`mail-create-issue-from-email.sh`), soit les deux (évolution/correctif → issue + fix/evol puis réponse au mail).
- **Si réponse mail** : rédiger la **réponse réelle** (pas le sujet ni la question reçue), puis `./ia_dev/gitea-issues/mail-send-reply.sh --to <addr> --subject "Re: ..." --body "<ta réponse>" --in-reply-to "<Message-ID du mail reçu>"`. Puis `./ia_dev/gitea-issues/mail-thread-log.sh append-sent --thread-id <THREAD_ID> --to <addr> --subject "..." --body "<ta réponse>"` pour tracer l'envoi.
- **Marquer lu** : `./ia_dev/gitea-issues/mail-mark-read.sh <uid>`.
**A. Spooler data/issues (prioritaire)** — Un seul fichier par message (`<base>.pending`). Le statut est dans le JSON (`status`: `pending` | `responded`). À traiter = fichiers dont `status` est `pending`.
**Réponses mail (obligatoire)** : le `--body` de `mail-send-reply.sh` doit contenir uniquement la **réponse que tu rédiges** à la question du mail (ex. « Décrit les rôles » → body = description des rôles). Jamais le sujet du mail, la question reçue ou un message précédent du fil.
- **Lister les mails à traiter** : exécuter `./ia_dev/gitea-issues/list-pending-spooler.sh`. Sortie = chemins des fichiers `projects/<id>/data/issues/<date>.<id>.<from>.pending` pour lesquels `status == "pending"`. Ne traiter **que** ces fichiers.
- **Pour chaque fichier listé** : lire le JSON (from, to, subject, body, message_id, uid, id, etc.). Le **base** est le nom du fichier sans `.pending` (ex. `2026-03-14T094530.a1b2c3d4.user_example.com`). Répondre uniquement si pertinent (demande d'info, évolution, etc.). Rédiger une **réponse pertinente** (composée par toi, uniquement ton texte ; pas de citation du mail reçu). Appeler `mail-send-reply.sh --to <from> --subject "Re: ..." --body "<ta réponse>" --in-reply-to "<message_id du JSON>"`. **Ne pas appeler** `mail-mark-read.sh` (inutile avec le spooler). Après envoi réussi : appeler `./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject "Re: ..." --body "<ta réponse>" --in-reply-to "<message_id>"`. Le script met à jour le **même** fichier (ajout de `response`, `status` = `responded`). Optionnel : `mail-thread-log.sh append-sent` pour tracer.
**Récupération mails (spooler data/issues)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/tickets-fetch-inbox.sh` pour récupérer les mails filtrés par `tickets.authorized_emails` (conf.json) et les écrire en `projects/<id>/data/issues/*.pending` (JSON). Pas de marquage lu/non lu. **Boucle legacy (non lu)** : si l'utilisateur demande « Lance la boucle récupération emails… N itérations », exécuter `./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]`. Mails en attente : **projects/<id>/data/issues/*.pending** (prioritaire) ou `projects/<id>/logs/gitea-issues/agent-loop.pending` ; les traiter selon le workflow ci-dessus.
**B. Legacy agent-loop.pending** — Mails « non lus » listés par `mail-list-unread.sh` (également limités à partir du 10 mars 2026 / `MAIL_SINCE_DATE`).
**Pièces jointes (spooler data/issues)** : chaque fichier `projects/<id>/data/issues/*.pending` est un JSON pouvant contenir un tableau `attachments` (champs `filename`, `path`, `content_type`, `size`). Les fichiers sont stockés sous `projects/<id>/data/issues/<base>.d/` ; `path` est relatif à `data/issues/`. Pour utiliser une pièce jointe, lire le fichier à `ia_dev/projects/<id>/data/issues/<path>` (ex. `projects/<id>/data/issues/2026-03-14T094530_laurence_lecoffre.io_42.d/0_document.pdf`). Les utiliser pour traiter le ticket (analyse, création dissue avec référence au fichier, etc.) sans les supprimer.
- **Lister** : exécuter `./ia_dev/gitea-issues/mail-list-unread.sh`. Pour chaque UID listé : `mail-get-thread.sh <uid>`, `mail-thread-log.sh init --uid <uid>`, rédiger réponse, `mail-send-reply.sh`, **puis** `mail-mark-read.sh <uid>` uniquement après succès de l'envoi, puis `mail-thread-log.sh append-sent`.
**Réponses mail (obligatoire)** : le `--body` est **uniquement** le texte que tu rédiges (réponse pertinente, adaptée au contenu du mail). Le script nenvoie que ce corps plus la signature ; aucun autre contenu nest ajouté. Ne jamais recopier le mail reçu, le sujet, un bloc type client mail ou une citation dans le body.
**Récupération mails (spooler data/issues)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/tickets-fetch-inbox.sh` pour récupérer les mails filtrés par `tickets.authorized_emails` (conf.json) et les écrire en `projects/<id>/data/issues/*.pending` (JSON). Seuls les mails **à partir du 10 mars 2026** (ou `MAIL_SINCE_DATE` en env) sont pris en compte. Pas de marquage lu/non lu. **Boucle legacy (non lu)** : si l'utilisateur demande « Lance la boucle récupération emails… N itérations », exécuter `./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]`. Mails en attente : **projects/<id>/data/issues/*.pending** (prioritaire) ou `projects/<id>/logs/gitea-issues/agent-loop.pending` ; les traiter selon le workflow ci-dessus.
**Pièces jointes (spooler data/issues)** : chaque fichier `projects/<id>/data/issues/*.pending` est un JSON pouvant contenir un tableau `attachments` (champs `filename`, `path`, `content_type`, `size`). Les fichiers sont stockés sous `projects/<id>/data/issues/<base>.d/` (`<base>` = `<date>.<id>.<from>`) ; `path` est relatif à `data/issues/`. Pour utiliser une pièce jointe, lire le fichier à `ia_dev/projects/<id>/data/issues/<path>`. Les utiliser pour traiter le ticket (analyse, création dissue avec référence au fichier, etc.) sans les supprimer.
## Contraintes

View File

@ -9,6 +9,8 @@ is_background: false
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/` (chemin absolu : `/home/desk/code/lecoffre_ng_test/ia_dev/projects/<id>`). L'identifiant `<id>` vient du slug (contenu du fichier `../ai_project_id`). Rappeler ce chemin en début d'exécution.
**Documentation** : La doc des projets gérés est dans **`projects/<id>/docs`** ; la doc ia_dev est dans **`projects/ia_dev/docs`**.
Tu es l'agent push-by-script. **Rôle de lagent :** construire le message de commit (sections obligatoires), mettre à jour CHANGELOG.md, lancer le script avec les options choisies, contrôler la sortie et le code de retour (ne pas masquer la sortie ; en cas déchec, rapporter et sarrêter). **Rôle du script :** exécution déterministe (build check, bump-version si demandé, git add/commit/push, vérifications auteur et chemins sensibles).
**Focus qualité et résolution de problèmes :**

View File

@ -25,7 +25,12 @@ fi
CONTENT=""
for f in "${SPOOL}"/*.pending; do
[ -f "$f" ] && CONTENT="${CONTENT}--- ${f##*/} ---"$'\n'"$(cat "$f")"$'\n'
[ -f "$f" ] || continue
if command -v jq >/dev/null 2>&1; then
status="$(jq -r '.status // "pending"' "$f" 2>/dev/null)"
[[ "$status" != "responded" ]] || continue
fi
CONTENT="${CONTENT}--- ${f##*/} ---"$'\n'"$(cat "$f")"$'\n'
done
if [ -n "$CONTENT" ]; then

View File

@ -1,27 +1,41 @@
# ia_dev
Dépôt de pilotage par lIA pour les projets (règles, agents, scripts de déploiement et de push).
Dépôt de pilotage par lIA pour les projets : **équipe dagents IA** réutilisable par tout projet qui inclut ia_dev en submodule Git. Objectif : une équipe autonome couvrant la **documentation**, le **code** (correctifs, évolutions), le **ticketing** (issues Gitea, mails), le **devops** (push, déploiement, branches), la **sécurité** et la **qualité** (lint, règles).
**Principe** : le projet hôte ne doit avoir **aucune dépendance** vers ia_dev (aucun script du projet n'appelle ia_dev). Seul ia_dev, en fonction du paramétrage (`.ia_project`, `ai_project_id`, `projects/<id>/conf.json`), sollicite le projet (lecture de la config, appel de `deploy/scripts_v2/deploy.sh`, etc.).
**Principe** : le projet hôte na **aucune dépendance** vers ia_dev (aucun script du projet nappelle ia_dev). Seul ia_dev, en fonction du paramétrage (`.ia_project`, `ai_project_id`, `projects/<id>/conf.json`), sollicite le projet (lecture de la config, appels aux scripts deploy, gitea-issues, etc.).
## Usage
- **En submodule** : ce dépôt est inclus comme sous-module Git dans chaque projet. Les paramètres spécifiques au projet sont dans `projects/<slug>.json`. Le projet hôte définit le slug par le fichier `.ia_project` à la racine ou par la variable denvironnement `IA_PROJECT`.
- **Scripts** : à lancer depuis la racine du dépôt du projet (ex. `./ia_dev/deploy/pousse.sh` ou `./deploy/pousse.sh` si `deploy` est un lien vers `ia_dev/deploy`).
- **En submodule** : ce dépôt est inclus comme sous-module Git dans chaque projet. La config par projet est dans `projects/<id>/conf.json` ; le slug `<id>` est donné par le fichier `.ia_project` ou `ai_project_id` à la racine du dépôt hôte, ou par la variable denvironnement `IA_PROJECT`.
- **Scripts** : à lancer depuis la **racine du dépôt du projet** (ex. `./ia_dev/deploy/pousse.sh`, `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`).
Voir `projects/README.md` pour le schéma de configuration et les exemples.
## Répertoires d'exécution
## Agents et domaines
Les scripts sont invoqués depuis la **racine du dépôt hôte**. Ils s'y placent (ou s'y ré-exécutent) avant de continuer.
Les agents Cursor sont dans `.cursor/agents/`. Chaque agent indique où se trouve la doc : **projets gérés**`projects/<id>/docs` ; **ia_dev**`projects/ia_dev/docs`.
| Domaine | Agents / composants |
|---------|---------------------|
| **Doc** | `docupdate` ; `projects/ia_dev/docs/` (GITEA_ISSUES_SCRIPTS_AGENTS, TICKETS_SPOOL_FORMAT, WORKFLOWS_AND_COMPONENTS) ; migration wiki (`gitea-issues/wiki-migrate-docs.sh`). |
| **Code** | `fix`, `evol`, `code`, `fix-search` ; workflow correctifs/évolutions (fix-search → fix → push-by-script ; evol + code). |
| **Ticketing** | `gitea-issues-process`, `agent-loop` ; spooler `projects/<id>/data/issues` ; scripts `gitea-issues/` (issues Gitea, mails, boucle récupération/traitement) ; hook `.cursor/hooks/remonter-mails.sh`. |
| **DevOps** | `push-by-script`, `deploy-by-script`, `deploy-pprod-or-prod`, `branch-align-by-script-from-test`, `change-to-all-branches` ; scripts `deploy/` (pousse.sh, scripts_v2, alignement branches). |
| **Sécurité / Qualité** | Règles `.cursor/rules/` ; pas de secrets en dur (`.secrets`, config par projet) ; `fix-lint` ; clôture obligatoire (`.cursor/rules/cloture-evolution.mdc`) pour tous les agents. |
Référence détaillée scripts et agents : `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md`. Index de la doc ia_dev : `projects/ia_dev/docs/README.md`.
## Répertoires dexécution
Les scripts sont invoqués depuis la **racine du dépôt hôte**. Ils sy placent (ou sy ré-exécutent) avant de continuer.
- **deploy/** : `PROJECT_ROOT` = git toplevel ; ré-exécution depuis la racine si besoin. Chemin du script résolu (`readlink -f` / `realpath`) pour que `IA_DEV_ROOT` soit correct même si `deploy` est un symlink vers `ia_dev/deploy`.
- **gitea-issues/** : `ROOT` = racine du dépôt projet (parent de ia_dev) ; `REPO_ROOT` idem. `.secrets` reste sous ia_dev (`./.secrets`) ; **logs** et **data** (spooler tickets) sont par projet sous `projects/<id>/logs/` et `projects/<id>/data/issues/` (id = slug, ex. `ai_project_id`).
- **gitea-issues/** : racine projet = parent de ia_dev ; `.secrets` sous ia_dev ; **logs** et **data** (spooler tickets) par projet sous `projects/<id>/logs/` et `projects/<id>/data/issues/` (id = slug, ex. contenu de `ai_project_id`).
## Scripts centralisés (submodule)
Les scripts suivants sont centralisés dans `ia_dev/deploy/`. Le projet n'a pas à fournir de wrapper (pour rester sans dépendance vers ia_dev) ; on invoque depuis la racine : `./ia_dev/deploy/bump-version.sh`, `./ia_dev/deploy/pousse.sh`, etc.
Les scripts suivants sont centralisés dans `ia_dev/deploy/`. Le projet na pas à fournir de wrapper ; on invoque depuis la racine : `./ia_dev/deploy/bump-version.sh`, `./ia_dev/deploy/pousse.sh`, etc.
- **bump-version.sh** : lecture de `projects/<id>/conf.json` (version.package_json_paths, version.splash_app_name). Invocation : `./ia_dev/deploy/bump-version.sh <version> [message]` depuis la racine du dépôt.
- **deploy-by-script-to.sh** : enchaîne change-to-all-branches (ia_dev/deploy), puis checkout/pull/deploy via `deploy/scripts_v2/deploy.sh` du projet. Le projet peut avoir `deploy/deploy-by-script-to.sh``exec …/ia_dev/deploy/deploy-by-script-to.sh "$@"`.
- **deploy/_lib/** : bibliothèque partagée pour les scripts de déploiement (`colors.sh`, `env-map.sh`, `ssh.sh`, `git-flow.sh`). Copie centralisée dans le submodule. Pour que `deploy/scripts_v2/` du projet utilise cette version sans dupliquer : depuis la racine du projet, `rm -rf deploy/scripts_v2/_lib` puis `ln -s ../../ia_dev/deploy/_lib deploy/scripts_v2/_lib` (les scripts font `source "$SCRIPT_DIR/_lib/…"`).
- **deploy/_lib/** : bibliothèque partagée pour les scripts de déploiement (`colors.sh`, `env-map.sh`, `ssh.sh`, `git-flow.sh`). Pour que `deploy/scripts_v2/` du projet utilise cette version : depuis la racine du projet, `rm -rf deploy/scripts_v2/_lib` puis `ln -s ../../ia_dev/deploy/_lib deploy/scripts_v2/_lib` (les scripts font `source "$SCRIPT_DIR/_lib/…"`).

View File

@ -1,138 +0,0 @@
# Boucle agent (agent-loop) surveillance des mails et fichier témoin
Script qui tourne en boucle dans l'environnement Cursor de ce projet pour surveiller les mails non lus et maintenir un **fichier témoin** indiquant si la boucle est active.
**Agent orchestrateur** : `.cursor/agents/agent-loop.md` (lance les boucles, x fois récupération + traitement).
## Rôle du script
- Exécuter périodiquement `mail-list-unread.sh` (sans modifier l'état des mails).
- Mettre à jour à chaque tour un **fichier témoin** (statut + horodatage) pour savoir si la boucle est active.
- Quand des mails non lus sont détectés, écrire un fichier **pending** et afficher un message invitant à lancer l'agent dans Cursor.
Le script **ne traite pas** les mails luimême : le traitement (réponse, issues, commits) est fait par l'**agent gitea-issues-process**. Vous pouvez soit lancer l'agent à la main dans Cursor, soit faire lancer l'agent par la boucle en activant `AGENT_LOOP_RUN_AGENT=1` (voir cidessous) si la **Cursor Agent CLI** est installée.
## Environnement au démarrage
Au démarrage, le script **source systématiquement** `~/.bashrc` (si le fichier existe et est lisible), puis ajoute `~/.local/bin` au `PATH` si ce répertoire existe. Ainsi, la commande `agent` (Cursor Agent CLI) est trouvée même si la boucle est lancée depuis un contexte où le shell n'a pas chargé le profil (nohup, cron, etc.).
## Lancement
Si le fichier `.secrets/gitea-issues/agent-loop.env` existe, il est sourcé au démarrage (voir `agent-loop.env.example`).
Depuis la **racine du dépôt projet** (parent de ia_dev) :
```bash
cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop.sh [interval_sec]
```
Avec un intervalle en secondes (défaut 60) :
```bash
./ia_dev/gitea-issues/agent-loop.sh 120
```
Ou via une variable d'environnement :
```bash
AGENT_LOOP_INTERVAL_SEC=120 ./ia_dev/gitea-issues/agent-loop.sh
```
Pour l'exécuter en arrière-plan et garder la boucle active après fermeture du terminal, utiliser `nohup` ou un gestionnaire de processus (systemd, screen, tmux) :
```bash
nohup ./ia_dev/gitea-issues/agent-loop.sh 60 >> ia_dev/projects/<id>/logs/gitea-issues/agent-loop.log 2>&1 &
```
## Fichier témoin (actif / inactif)
- **Emplacement** : `projects/<id>/logs/gitea-issues/agent-loop.status` (ou `AGENT_LOOP_STATUS_FILE` si défini).
- **Contenu** : trois lignes
1. Horodatage ISO 8601 du dernier tour.
2. Statut : `idle` | `mails_pending` | `running` | `error`.
3. Détail optionnel (ex. message pour l'utilisateur).
**Considérer la boucle comme active** si le fichier a été modifié depuis moins de **2 × intervalle** (ex. moins de 120 s si intervalle = 60 s). Au-delà, la boucle est considérée arrêtée.
## Fichier pending (mails en attente)
- **Emplacement** : `projects/<id>/logs/gitea-issues/agent-loop.pending`.
- **Rôle** : quand des mails non lus sont détectés, le script y écrit un bloc (horodatage, statut `mails_pending`, puis la sortie de `mail-list-unread.sh`). Permet de voir quels mails attendent un traitement par l'agent.
- Quand il n'y a plus de non lus, le script vide ce fichier au tour suivant.
**Hook Cursor** : un hook (`.cursor/hooks.json``sessionStart` / `beforeSubmitPrompt`) peut exécuter un script qui lit ce fichier pour « remonter » les mails reçus au contexte de l'agent (voir section Référence hooks).
## Variables d'environnement
Variables possibles dans `.secrets/gitea-issues/agent-loop.env` ou en export shell :
| Variable | Défaut | Description |
|----------|--------|-------------|
| `AGENT_LOOP_INTERVAL_SEC` | 60 | Intervalle entre deux vérifications (secondes). Peut aussi être passé en premier argument au script. |
| `AGENT_LOOP_RUN_AGENT` | 0 | Si mis à `1`, la boucle lance la **Cursor Agent CLI** (`agent`) quand des mails non lus sont détectés, avec un prompt qui exécute le workflow mails de gitea-issues-process. Nécessite que la commande `agent` soit installée (voir [Cursor CLI](https://cursor.com/docs/cli/using)). Si `agent` n'est pas dans le PATH, la boucle se contente de mettre à jour le statut et le fichier pending. |
| `AGENT_LOOP_MODEL` | `sonnet-4.6` | Modèle utilisé par la CLI (`agent --model ...`). Par défaut `sonnet-4.6` pour limiter les blocages liés aux quotas Opus. Ex. : `AGENT_LOOP_MODEL=gpt-5.4-low` ; liste : `agent models`. |
| `AGENT_LOOP_STATUS_FILE` | `projects/<id>/logs/gitea-issues/agent-loop.status` | Chemin du fichier témoin (sous ia_dev, par projet). |
| `AGENT_LOOP_PENDING_FILE` | `projects/<id>/logs/gitea-issues/agent-loop.pending` | Chemin du fichier pending (sous ia_dev, par projet). |
| `GITEA_ISSUES_DIR` | répertoire du script | Racine des scripts gitea-issues (pour appeler les scripts mail). |
## Traiter les mails
Dès que le statut est `mails_pending` ou que des mails apparaissent dans `agent-loop.pending` :
1. **Option A (manuel)** : ouvrir le projet dans **Cursor**, lancer l'**agent gitea-issues-process** (commande `/gitea-issues-process` ou via l'interface des agents).
2. **Option B (automatique)** : lancer la boucle avec `AGENT_LOOP_RUN_AGENT=1` et la **Cursor Agent CLI** installée (`agent` dans le PATH). Lorsque des mails non lus sont détectés, le script invoque `agent -p "..." -f` pour exécuter le workflow mails (fil, log, réponse, marquage lu). Voir [Cursor CLI](https://cursor.com/docs/cli/using) pour l'installation.
L'agent lit les non lus, consulte les fils, répond par mail, crée des issues si besoin, et marque les mails comme lus. **Important** : le corps de chaque réponse doit contenir la **réponse réelle** à la question du mail (ex. si on demande « Décrit les rôles », envoyer une description des rôles), jamais le sujet du mail, la question reçue ou un message précédent du fil ; uniquement le contenu rédigé par l'agent en réponse à la demande.
Après passage de l'agent, au prochain tour de la boucle le statut repassera à `idle` et le fichier pending sera vidé (s'il n'y a plus de non lus).
## Logs
Le script n'écrit pas de log structuré par défaut. Pour garder une trace des tours et des messages affichés :
```bash
./ia_dev/gitea-issues/agent-loop.sh 60 2>&1 | tee -a ia_dev/projects/<id>/logs/gitea-issues/agent-loop.log
```
Ou en arrière-plan :
```bash
nohup ./ia_dev/gitea-issues/agent-loop.sh 60 >> ia_dev/projects/<id>/logs/gitea-issues/agent-loop.log 2>&1 &
```
## Arrêter la boucle
- Si le script est en premier plan : `Ctrl+C`.
- Si lancé en arrière-plan : `kill <pid>` (ou `pkill -f agent-loop.sh`).
Après arrêt, le fichier témoin ne sera plus mis à jour ; après 2 × intervalle, il doit être considéré comme inactif.
## Boucle limitée depuis le chat (sans API payante)
Pour lancer depuis le chat Cursor une boucle limitée : récupération des mails → attente 1 min → récupération → … sans consommer de crédits API ni lancer la CLI `agent` :
1. Demander dans le chat : *« Lance la boucle récupération emails : N itérations »* (ou similaire).
2. L'agent exécute `cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]`. Par défaut N=3 ; `--repeat` = à la fin des N itérations, relancer. Pour 600 itérations avec relance : `... 600 --repeat`.
3. Au lancement, le script envoie un mail de test à nicolas.cantu@pm.me pour vérifier SMTP ; si l'envoi échoue, le script s'arrête (code 1).
4. Chaque itération exécute `mail-list-unread.sh` puis attend 60 secondes. **Log** : expéditeur (From), titres (Subject) et sortie sont écrits dans `projects/<id>/logs/gitea-issues/agent-loop-chat-iterations.log`. Les mails en attente sont dans `projects/<id>/logs/gitea-issues/agent-loop.pending`. Lors de l'envoi d'une réponse, toujours appeler `mail-thread-log.sh append-sent` avec `--body` pour logger la réponse.
Script : `gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]`. Log : `projects/<id>/logs/gitea-issues/agent-loop-chat-iterations.log`. Boucle illimitée sans relance : `cd <racine_projet> && while true; do ./ia_dev/gitea-issues/mail-list-unread.sh; sleep 60; done`.
## Référence : hooks Cursor
Les **hooks Cursor** permettent d'observer, contrôler ou modifier le cycle de l'agent (et de Tab) via des scripts. Ils ne permettent **pas** de lancer un agent depuis un script externe.
**Documentation officielle** : [cursor.com/docs/agent/hooks](https://cursor.com/docs/agent/hooks).
**Configuration** : fichier `hooks.json` à l'un des emplacements suivants (tous les hooks correspondants sont exécutés) :
- Projet : `/.cursor/hooks.json` (scripts relatifs à la racine du projet, ex. `.cursor/hooks/format.sh`)
- Utilisateur : `~/.cursor/hooks.json` (scripts relatifs à `~/.cursor/`, ex. `./hooks/format.sh`)
**Événements disponibles (Agent / Cmd+K)** : `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `beforeReadFile`, `afterFileEdit`, `beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `subagentStart`, `subagentStop`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `preCompact`, `afterAgentResponse`, `afterAgentThought`, `stop`.
**Fonctionnement** : chaque hook est un processus lancé par Cursor ; il reçoit du JSON sur l'entrée standard et peut renvoyer du JSON (ex. `continue`, `permission`: allow/deny/ask). Les hooks sont **déclenchés par** une action (ouverture de session, envoi de prompt, exécution shell, etc.) ; ils ne **déclenchent pas** une session ou un envoi de prompt. Il n'existe pas de hook du type « quand le fichier X change, lancer l'agent ».
**Hook « remonter les mails reçus »** : le projet peut définir un hook `sessionStart` ou `beforeSubmitPrompt` qui exécute un script (ex. `.cursor/hooks/remonter-mails.sh`) lisant les mails en attente depuis `projects/<id>/data/issues/*.pending` (spooler) ou `projects/<id>/logs/gitea-issues/agent-loop.pending` (legacy) et renvoyant le contenu pour injection dans le contexte ou le prompt.
**Pour lancer un agent depuis un script** : la seule option côté Cursor est d'appeler la **Cursor Agent CLI** (`agent -p "..." -f`) depuis le script (voir `AGENT_LOOP_RUN_AGENT=1` dans ce document). Les hooks ne remplacent pas cet appel.

View File

@ -1,128 +0,0 @@
# Format JSON du spooler tickets (projects/<id>/data/issues/)
Les mails et réponses sont stockés dans **projects/<id>/data/issues/** (sous ia_dev, par projet). Filtrage par **expéditeurs autorisés** (conf.json → `tickets.authorized_emails`), sans sappuyer sur le statut « non lu ». Le traitement se base uniquement sur le **statut des issues** ; aucun enregistrement nest supprimé.
## Emplacement et nommage des fichiers
- **Racine** : `ia_dev/projects/<id>/data/issues/` (id = slug projet, ex. contenu de `ai_project_id` ou `.ia_project` à la racine du dépôt)
- **Messages entrants** : `<date>_<from_sanitized>_<uid>.<status>` (le `_<uid>` assure lunicité par message IMAP)
- `date` : `YYYY-MM-DDTHHmmss` (ex. `2026-03-14T094530`)
- `from_sanitized` : adresse expéditeur rendue sûre pour le système de fichiers (ex. `user_example.com` pour `user@example.com` : `@``_`, caractères interdits remplacés ou supprimés)
- `uid` : UID IMAP (évite les doublons pour un même expéditeur à la même seconde)
- `status` : `pending` | `inprogress` | `done`
- **Réponses envoyées** : `<date>_<from_sanitized>.response` (même base de nom que le message entrant associé, extension `.response`)
- **Pièces jointes (pj)** : pour un message entrant de base `<base>` (ex. `2026-03-14T094530_laurence_lecoffre.io_42`), les pièces jointes sont dans le répertoire **`<base>.d/`** (ex. `2026-03-14T094530_laurence_lecoffre.io_42.d/`). Nommage des fichiers : `<index>_<nom_fichier_sanitifé>` (ex. `0_document.pdf`, `1_capture.png`). Le JSON du message contient le tableau `attachments` avec pour chaque entrée le chemin relatif à `data/issues/`, le type MIME et la taille pour que lagent qui traite les tickets puisse les utiliser.
Exemples :
- `2026-03-14T094530_laurence_lecoffre.io.pending` — message entrant en attente
- `2026-03-14T094530_laurence_lecoffre.io.inprogress` — en cours de traitement
- `2026-03-14T094530_laurence_lecoffre.io.done` — traité
- `2026-03-14T101200_laurence_lecoffre.io.response` — réponse envoyée
## Schéma JSON — message entrant (incoming)
Fichiers `.pending`, `.inprogress`, `.done`.
```json
{
"version": 1,
"type": "incoming",
"message_id": "<original.Message-ID@host>",
"from": "laurence@lecoffre.io",
"to": ["ai.support.lecoffreio@4nkweb.com"],
"subject": "Demande d'information",
"date": "Fri, 14 Mar 2026 09:45:30 +0100",
"body": "Texte brut du corps du mail.",
"references": ["<id1@host>", "<id2@host>"],
"in_reply_to": "<id2@host>",
"uid": "42",
"created_at": "2026-03-14T09:45:35Z",
"issue_number": null,
"status": "pending",
"attachments": [
{
"filename": "document.pdf",
"path": "2026-03-14T094530_laurence_lecoffre.io_42.d/0_document.pdf",
"content_type": "application/pdf",
"size": 12345
},
{
"filename": "capture.png",
"path": "2026-03-14T094530_laurence_lecoffre.io_42.d/1_capture.png",
"content_type": "image/png",
"size": 6789
}
]
}
```
| Champ | Type | Description |
|-------|------|-------------|
| `version` | number | Version du schéma (1) |
| `type` | string | `"incoming"` |
| `message_id` | string | En-tête Message-ID du mail |
| `from` | string | Adresse de lexpéditeur |
| `to` | string[] | Adresses destinataires |
| `subject` | string | Sujet |
| `date` | string | Date denvoi (RFC2822 ou ISO8601) |
| `body` | string | Corps du message (texte brut) |
| `references` | string[] | En-tête References (liste de Message-IDs) |
| `in_reply_to` | string \| null | En-tête In-Reply-To |
| `uid` | string | UID IMAP (pour référence, pas pour marquer lu) |
| `created_at` | string | Date décriture dans le spool (ISO8601) |
| `issue_number` | number \| null | Numéro dissue Gitea si une issue a été créée |
| `status` | string | `pending` \| `inprogress` \| `done` |
| `attachments` | array | Pièces jointes : tableau dobjets (voir ci-dessous). `path` est relatif à `data/issues/` ; les fichiers sont dans `<base>.d/`. Lagent qui traite les tickets peut les lire depuis `projects/<id>/data/issues/<path>`. |
**Objet pièce jointe** : `filename` (nom dorigine), `path` (chemin relatif sous `projects/<id>/data/issues/`), `content_type` (MIME), `size` (octets).
## Schéma JSON — réponse envoyée (response)
Fichiers `.response`.
```json
{
"version": 1,
"type": "response",
"in_reply_to_message_id": "<original.Message-ID@host>",
"to": "laurence@lecoffre.io",
"subject": "Re: Demande d'information",
"body": "Texte de la réponse envoyée par l'agent.",
"sent_at": "2026-03-14T10:12:00Z",
"created_at": "2026-03-14T10:12:00Z"
}
```
| Champ | Type | Description |
|-------|------|-------------|
| `version` | number | Version du schéma (1) |
| `type` | string | `"response"` |
| `in_reply_to_message_id` | string | Message-ID du mail auquel on répond |
| `to` | string | Destinataire de la réponse |
| `subject` | string | Sujet de la réponse (ex. Re: …) |
| `body` | string | Corps de la réponse envoyée |
| `sent_at` | string | Date denvoi (ISO8601) |
| `created_at` | string | Date de création du fichier (ISO8601) |
## Configuration projet (conf.json)
- **ticketing_url** et la config ticketing sont sous la clé **`tickets`** (et non plus sous `git`).
- **`tickets`** contient :
- `ticketing_url` : URL des issues Gitea
- `authorized_emails` :
- **`to`** : récupérer **uniquement** les mails **envoyés à** cette adresse (destinataire).
- **`from`** : récupérer **uniquement** les mails **envoyés par** ces adresses (expéditeurs autorisés).
Exemple :
```json
"tickets": {
"ticketing_url": "https://git.4nkweb.com/4nk/lecoffre_ng/issues",
"authorized_emails": {
"to": "ai.support.lecoffreio@4nkweb.com",
"from": ["ai.support.lecoffreio@4nkweb.com", "laurence@lecoffre.io"]
}
}
```
La récupération ne prend que les messages **envoyés à** `authorized_emails.to` **et** **expédités par** une des adresses de `authorized_emails.from`. Aucun marquage « lu / non lu » ; aucun enregistrement du spool nest supprimé.

View File

@ -42,11 +42,15 @@ fi
LOG_DIR="${LOGS_GITEA}"
LOG_FILE="${LOG_DIR}/agent-loop-chat-iterations.log"
mkdir -p "$LOG_DIR"
# Ensure absolute path so logs are always in the same place
[[ "$LOG_FILE" != /* ]] && LOG_FILE="$(cd "$LOG_DIR" && pwd)/agent-loop-chat-iterations.log"
log_and_echo() {
echo "$1" | tee -a "$LOG_FILE"
}
# Log path and start so logs are never "empty" from path confusion
log_and_echo "[agent-loop-chat] $(date -Iseconds) — log file: $LOG_FILE"
# Test send at launch: one test email to nicolas.cantu@pm.me
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi vers nicolas.cantu@pm.me"
"${GITEA_ISSUES_DIR}/mail-send-reply.sh" --to "nicolas.cantu@pm.me" --subject "Test envoi - agent-loop-chat $(date +%Y-%m-%dT%H:%M:%S)" --body "Mail de test envoyé au lancement de agent-loop-chat-iterations.sh." 2>&1 | tee -a "$LOG_FILE"

View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Try to acquire agent-loop.lock. Exit 0 and create lock if none or stale (>24h). Exit 1 if lock is recent.
# Run from repo root. Used by agent-loop agent before starting x cycles (section 2).
#
# Usage:
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-lock-acquire.sh
#
set -euo pipefail
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
set +u
# shellcheck source=/dev/null
source "$HOME/.bashrc" 2>/dev/null || true
set -u
fi
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
LOCK_FILE="${AGENT_LOOP_LOCK_FILE:-$LOGS_GITEA/agent-loop.lock}"
STALE_SEC=$((24 * 3600)) # 24 hours
mkdir -p "$(dirname "$LOCK_FILE")"
if [ -f "$LOCK_FILE" ]; then
mtime=$(stat -c %Y "$LOCK_FILE" 2>/dev/null) || mtime=0
now=$(date +%s)
if [ $((now - mtime)) -lt "$STALE_SEC" ]; then
echo "[agent-loop-lock-acquire] $(date -Iseconds) — Lock actif: $LOCK_FILE (mtime < 24 h). Ne pas lancer une deuxième instance." >&2
exit 1
fi
fi
printf "%s\n%s\n" "$$" "$(date -Iseconds)" > "$LOCK_FILE"
echo "[agent-loop-lock-acquire] $(date -Iseconds) — Lock acquis: $LOCK_FILE"

View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Remove agent-loop.lock and optionally agent-loop.stop. Run at end of x cycles (normal or stop).
# Run from repo root.
#
# Usage:
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-lock-release.sh
#
set -euo pipefail
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
set +u
# shellcheck source=/dev/null
source "$HOME/.bashrc" 2>/dev/null || true
set -u
fi
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
LOCK_FILE="${AGENT_LOOP_LOCK_FILE:-$LOGS_GITEA/agent-loop.lock}"
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
if [ -f "$LOCK_FILE" ]; then
rm -f "$LOCK_FILE"
echo "[agent-loop-lock-release] $(date -Iseconds) — Lock supprimé: $LOCK_FILE"
fi
if [ -f "$STOP_FILE" ]; then
rm -f "$STOP_FILE"
echo "[agent-loop-lock-release] $(date -Iseconds) — Fichier stop supprimé: $STOP_FILE"
fi

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# One-shot retrieval: run mail-list-unread once and write output to agent-loop.pending (and status file).
# Run from repo root. Used by agent-loop agent for "x times" cycles.
# One-shot retrieval: run tickets-fetch-inbox (spooler from/to) then list-pending-spooler; write paths to agent-loop.pending and status file.
# Run from repo root. Used by agent-loop agent for "x times" cycles. Criterion: from/to in conf.json, not IMAP unread.
#
# Usage:
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-retrieval-once.sh
@ -30,19 +30,16 @@ write_status() {
printf "%s\n%s\n%s\n" "$(date -Iseconds)" "$1" "${2:-}" > "$STATUS_FILE"
}
out=""
if out=$("${GITEA_ISSUES_DIR}/mail-list-unread.sh" 2>&1); then
if echo "$out" | grep -q "UID="; then
write_status "mails_pending" "One-shot: mails non lus détectés."
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$out" > "$PENDING_FILE"
echo "[agent-loop-retrieval-once] $(date -Iseconds) — Mails non lus écrits dans agent-loop.pending"
"${GITEA_ISSUES_DIR}/tickets-fetch-inbox.sh" 2>&1 || true
pending_out=""
pending_out=$("${GITEA_ISSUES_DIR}/list-pending-spooler.sh" 2>&1) || true
if [ -n "$pending_out" ] && echo "$pending_out" | grep -q "\.pending"; then
write_status "mails_pending" "One-shot: mails en attente dans le spooler (from/to)."
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$pending_out" > "$PENDING_FILE"
n=$(echo "$pending_out" | grep -c "\.pending" || true)
echo "[agent-loop-retrieval-once] $(date -Iseconds)$n mail(s) en attente dans le spooler (critère from/to). Écrit dans agent-loop.pending"
else
write_status "idle" "One-shot: aucun mail non lu."
write_status "idle" "One-shot: aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
[ -f "$PENDING_FILE" ] && : > "$PENDING_FILE"
echo "[agent-loop-retrieval-once] $(date -Iseconds) — Aucun mail non lu"
fi
else
write_status "error" "mail-list-unread a échoué"
echo "[agent-loop-retrieval-once] $(date -Iseconds) — Erreur mail-list-unread" >&2
exit 1
echo "[agent-loop-retrieval-once] $(date -Iseconds) — Aucun mail en attente dans le spooler (critère: from/to dans conf.json)"
fi

View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Exit 0 if agent-loop.stop exists (stop requested), 1 otherwise. Used at start of each cycle.
# Run from repo root.
#
# Usage:
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop-requested.sh
#
set -euo pipefail
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
set +u
# shellcheck source=/dev/null
source "$HOME/.bashrc" 2>/dev/null || true
set -u
fi
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
[ -f "$STOP_FILE" ]
# exit 0 if file exists, 1 otherwise

28
gitea-issues/agent-loop-stop.sh Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Create agent-loop.stop so the running agent-loop (section 2) stops at the next cycle start.
# Run from repo root. Same paths as agent-loop-retrieval-once.sh (projects/<id>/logs/gitea-issues).
#
# Usage:
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-stop.sh
#
set -euo pipefail
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
set +u
# shellcheck source=/dev/null
source "$HOME/.bashrc" 2>/dev/null || true
set -u
fi
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
STOP_FILE="${AGENT_LOOP_STOP_FILE:-$LOGS_GITEA/agent-loop.stop}"
mkdir -p "$(dirname "$STOP_FILE")"
touch "$STOP_FILE"
echo "[agent-loop-stop] $(date -Iseconds)$STOP_FILE créé. La boucle en cours s'arrêtera au début du prochain cycle."

View File

@ -1,10 +1,9 @@
#!/usr/bin/env bash
# Treatment loop: periodically check agent-loop.pending and run Cursor Agent CLI (gitea-issues-process workflow) when non-empty.
# Run from repo root. No timeout; runs forever. Use with nohup for background.
# Run from repo root. No timeout; runs forever. Do NOT start this script from the agent-loop agent (use bounded runs only).
#
# Usage:
# Usage (manual only; agent must not launch with nohup/&):
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-treatment.sh
# nohup ./ia_dev/gitea-issues/agent-loop-treatment.sh >> ia_dev/projects/<id>/logs/gitea-issues/agent-loop-treatment.log 2>&1 &
#
set -euo pipefail
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
@ -38,7 +37,7 @@ fi
INTERVAL="${AGENT_LOOP_TREATMENT_INTERVAL_SEC:-60}"
AGENT_MODEL="${AGENT_LOOP_MODEL:-sonnet-4.6}"
PROMPT="Exécute le workflow mails entrants de l'agent gitea-issues-process : les mails non lus sont listés dans projects/<id>/logs/gitea-issues/agent-loop.pending. 1) Pour chaque mail listé (voir contenu dans ce fichier) : exécuter ./ia_dev/gitea-issues/mail-get-thread.sh <uid>, puis ./ia_dev/gitea-issues/mail-thread-log.sh init --uid <uid>, conserver THREAD_ID. 2) Pour chaque mail : rédiger une réponse NOUVELLE (non technique, didactique, contexte LeCoffre.io). IMPORTANT : le --body de mail-send-reply.sh doit être du texte COMPOSÉ PAR TOI, jamais une copie ou citation d'un message du fil. Envoyer avec ./ia_dev/gitea-issues/mail-send-reply.sh --to <adresse_From> --subject \"Re: ...\" --body \"<ta_réponse>\", puis mail-thread-log.sh append-sent, puis ./ia_dev/gitea-issues/mail-mark-read.sh <uid>. Répondre à tous les mails avant de marquer comme lu."
PROMPT="Exécute le workflow mails du spooler (agent gitea-issues-process). Les chemins des fichiers .pending sont dans projects/<id>/logs/gitea-issues/agent-loop.pending (ou exécuter ./ia_dev/gitea-issues/list-pending-spooler.sh). Pour chaque fichier .pending : lire le JSON (from, to, subject, body, message_id, base). Rédiger une réponse pertinente (uniquement ton texte ; pas de citation — mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Envoyer avec ./ia_dev/gitea-issues/mail-send-reply.sh --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Après envoi réussi : ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Ne pas appeler mail-mark-read.sh (spooler)."
while true; do
if [ -s "$PENDING_FILE" ] && command -v agent >/dev/null 2>&1; then

View File

@ -12,6 +12,8 @@
# Polling interval in seconds (default: 60)
# AGENT_LOOP_INTERVAL_SEC=60
#
# Optional: custom paths for status and pending files
# Optional: custom paths for status, pending, lock and stop files
# AGENT_LOOP_STATUS_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.status
# AGENT_LOOP_PENDING_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.pending
# AGENT_LOOP_LOCK_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.lock
# AGENT_LOOP_STOP_FILE=ia_dev/projects/<id>/logs/gitea-issues/agent-loop.stop

View File

@ -1,15 +1,15 @@
#!/usr/bin/env bash
# Agent loop: poll for unread mails periodically and maintain a witness file.
# Run from repo root. Use a fichier témoin (status) to know if the loop is active.
# Agent loop: poll for mails to treat (spooler criterion: from/to in conf.json, not IMAP unread).
# Run from repo root. Runs forever. Do NOT start from the agent-loop agent (use bounded runs only, e.g. agent-loop-chat-iterations.sh N).
#
# Usage:
# Usage (manual only; agent must not launch with nohup/&):
# ./gitea-issues/agent-loop.sh [interval_seconds]
# AGENT_LOOP_INTERVAL_SEC=120 ./gitea-issues/agent-loop.sh
#
# Witness file: projects/<id>/logs/gitea-issues/agent-loop.status (or AGENT_LOOP_STATUS_FILE)
# Updated every iteration. If mtime is older than 2*interval, consider the loop stopped.
# State file (not a log): updated every iteration. If mtime is older than 2*interval, loop is considered stopped.
# Pending file: projects/<id>/logs/gitea-issues/agent-loop.pending (or AGENT_LOOP_PENDING_FILE)
# Written when unread mails exist; contains timestamp and mail list. Clear after agent run.
# Written when there are .pending mails in the spooler (no matching .response); contains paths to treat.
#
# Optional: set AGENT_LOOP_RUN_AGENT=1 to run the Cursor Agent CLI when mails are detected.
# Requires Cursor Agent CLI (https://cursor.com/docs/cli/using). If "agent" is not in PATH, the loop only updates status/pending.
@ -46,6 +46,7 @@ if [ -r "$AGENT_LOOP_ENV" ]; then
fi
INTERVAL="${1:-${AGENT_LOOP_INTERVAL_SEC:-60}}"
# STATUS_FILE: state/witness file (not a log) — indicates if the loop is active; under projects/<id>/logs/gitea-issues/ for per-project state.
STATUS_FILE="${AGENT_LOOP_STATUS_FILE:-$LOGS_GITEA/agent-loop.status}"
PENDING_FILE="${AGENT_LOOP_PENDING_FILE:-$LOGS_GITEA/agent-loop.pending}"
mkdir -p "$(dirname "$STATUS_FILE")"
@ -58,18 +59,21 @@ write_status() {
while true; do
write_status "running" "interval=${INTERVAL}s"
out=""
if out=$("${GITEA_ISSUES_DIR}/mail-list-unread.sh" 2>&1); then
if echo "$out" | grep -q "UID="; then
write_status "mails_pending" "Des mails non lus. Lancer l'agent gitea-issues-process dans Cursor."
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$out" > "$PENDING_FILE"
echo "[agent-loop] $(date -Iseconds) — Mails non lus détectés. Lancer l'agent gitea-issues-process dans Cursor."
# Spooler flow: fetch by from/to (conf.json), then list .pending without .response. No IMAP unread criterion.
"${GITEA_ISSUES_DIR}/tickets-fetch-inbox.sh" 2>&1 || true
pending_out=""
pending_out=$("${GITEA_ISSUES_DIR}/list-pending-spooler.sh" 2>&1) || true
if [ -n "$pending_out" ] && echo "$pending_out" | grep -q "\.pending"; then
write_status "mails_pending" "Mails en attente dans le spooler (from/to). Lancer l'agent gitea-issues-process dans Cursor."
printf "%s\n%s\n%s\n%s\n" "$(date -Iseconds)" "mails_pending" "---" "$pending_out" > "$PENDING_FILE"
n=$(echo "$pending_out" | grep -c "\.pending" || true)
echo "[agent-loop] $(date -Iseconds)$n mail(s) en attente dans le spooler (critère from/to). Lancer l'agent gitea-issues-process dans Cursor."
if [ "${AGENT_LOOP_RUN_AGENT:-0}" = "1" ] && command -v agent >/dev/null 2>&1; then
write_status "running_agent" "Lancement de l'agent Cursor pour traiter les mails."
echo "[agent-loop] $(date -Iseconds) — Lancement de l'agent Cursor (workflow gitea-issues-process mails)."
write_status "running_agent" "Lancement de l'agent Cursor pour traiter les mails du spooler."
echo "[agent-loop] $(date -Iseconds) — Lancement de l'agent Cursor (workflow gitea-issues-process spooler)."
AGENT_MODEL="${AGENT_LOOP_MODEL:-sonnet-4.6}"
echo "[agent-loop] $(date -Iseconds) — Modèle: $AGENT_MODEL"
AGENT_OPTS=(-p "Exécute le workflow mails entrants de l'agent gitea-issues-process : les mails non lus viennent d'être détectés. 1) Pour chaque mail listé (voir contenu dans logs/gitea-issues/agent-loop.pending) : exécuter ./gitea-issues/mail-get-thread.sh <uid>, puis ./gitea-issues/mail-thread-log.sh init --uid <uid>, conserver THREAD_ID. 2) Pour chaque mail : rédiger une réponse NOUVELLE (non technique, didactique, contexte LeCoffre.io ; pour bug demander l'environnement, pour évolution considérer test). IMPORTANT : le --body de mail-send-reply.sh doit être du texte COMPOSÉ PAR TOI, jamais une copie ou citation d'un message du fil. Ne jamais renvoyer le contenu d'un message précédent (ex. réponse antérieure de ai.support). Envoyer avec ./gitea-issues/mail-send-reply.sh --to <adresse_From_du_mail_utilisateur> --subject \"Re: ...\" --body \"<ta_réponse>\", puis ./gitea-issues/mail-thread-log.sh append-sent, puis ./gitea-issues/mail-mark-read.sh <uid>. 3) Uniquement branche test, ne pas modifier les agents ni scripts d'agents. Répondre à tous les mails avant de marquer comme lu." -f --model "$AGENT_MODEL")
AGENT_OPTS=(-p "Exécute le workflow mails du spooler (agent gitea-issues-process). 1) Lister les mails à traiter avec ./ia_dev/gitea-issues/list-pending-spooler.sh (ou utiliser les chemins dans agent-loop.pending). 2) Pour chaque fichier .pending : lire le JSON (from, to, subject, body, message_id, base). Rédiger une réponse pertinente (uniquement ton texte, pas de citation ; mail-send-reply.sh refuse si le body contient From:, Message-ID, wrote:, etc.). Envoyer avec ./ia_dev/gitea-issues/mail-send-reply.sh --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Après envoi réussi : ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <from> --subject \"Re: ...\" --body \"<ta_réponse>\" --in-reply-to \"<message_id>\". Ne pas appeler mail-mark-read.sh (spooler)." -f --model "$AGENT_MODEL")
if agent "${AGENT_OPTS[@]}" 2>&1; then
write_status "agent_done" "Agent terminé."
else
@ -77,14 +81,11 @@ while true; do
fi
fi
else
write_status "idle" "Aucun mail non lu."
write_status "idle" "Aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
if [ -f "$PENDING_FILE" ]; then
: > "$PENDING_FILE"
fi
fi
else
write_status "error" "mail-list-unread a échoué"
echo "[agent-loop] $(date -Iseconds) — Erreur mail-list-unread" >&2
echo "[agent-loop] $(date -Iseconds) — Aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
fi
sleep "$INTERVAL"
done

View File

@ -21,6 +21,9 @@
# Restrict listed mails to those sent to this address (default: ai.support.lecoffreio@4nkweb.com)
# MAIL_FILTER_TO=ai.support.lecoffreio@4nkweb.com
#
# Only fetch/list mails on or after this date (IMAP format DD-Mon-YYYY). Default: 10-Mar-2026.
# MAIL_SINCE_DATE=10-Mar-2026
#
# Signature appended to every reply (default: Support IA du projet Lecoffre.io + ai.support.lecoffreio@4nkweb.com)
# MAIL_REPLY_SIGNATURE=--\\nSupport IA du projet Lecoffre.io\\nai.support.lecoffreio@4nkweb.com
#

View File

@ -26,6 +26,7 @@ if [[ -f "${GITEA_ISSUES_DIR}/../lib/project_config.sh" ]]; then
if [[ -n "${PROJECT_SLUG:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
PROJECT_LOGS_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/logs"
DATA_ISSUES_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/data/issues"
mkdir -p "${PROJECT_LOGS_DIR}" "${DATA_ISSUES_DIR}"
fi
fi
fi

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# List .pending files in projects/<id>/data/issues/ with status "pending" (one file per message; status updated in place).
# Run from repo root. Output: one path per line.
# Usage: ./ia_dev/gitea-issues/list-pending-spooler.sh
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
SPOOL="${DATA_ISSUES_DIR:-}"
if [[ -z "${SPOOL:-}" || ! -d "$SPOOL" ]]; then
echo "[gitea-issues] DATA_ISSUES_DIR not set or not a directory. Run from repo root with projects/<id> configured." >&2
exit 1
fi
for f in "${SPOOL}"/*.pending; do
[[ -f "$f" ]] || continue
status="$(jq -r '.status // "pending"' "$f" 2>/dev/null)"
if [[ "$status" != "responded" ]]; then
echo "$f"
fi
done

View File

@ -3,8 +3,6 @@
# Usage: ./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..." ]
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-create-issue-from-email.py" "$@"

View File

@ -17,7 +17,7 @@ from email.header import decode_header
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import load_imap_config, repo_root, imap_ssl_context
from mail_common import imap_since_date, load_imap_config, repo_root, imap_ssl_context
def decode_header_value(header: str | None) -> str:
@ -74,14 +74,15 @@ def find_message_ids_from_msg(msg: email.message.Message) -> set[str]:
def search_by_message_id(mail: imaplib.IMAP4, msg_id: str) -> list[str]:
"""Return list of UIDs (as strings) for messages with given Message-ID."""
"""Return list of UIDs (as strings) for messages with given Message-ID, on or after MAIL_SINCE_DATE."""
if not msg_id:
return []
if not msg_id.startswith("<"):
msg_id = f"<{msg_id}>"
if not msg_id.endswith(">"):
msg_id = msg_id + ">"
criterion = f'HEADER Message-ID "{msg_id}"'
since = imap_since_date()
criterion = f'(HEADER Message-ID "{msg_id}" SINCE {since})'
try:
_, data = mail.search(None, criterion)
except Exception:

View File

@ -3,10 +3,8 @@
# Usage: ./gitea-issues/mail-get-thread.sh <uid>
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
if [ $# -lt 1 ]; then
echo "Usage: $0 <uid>" >&2
exit 1

View File

@ -16,7 +16,7 @@ from pathlib import Path
# Add gitea-issues to path for mail_common
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import load_imap_config, repo_root, imap_ssl_context
from mail_common import imap_search_criterion_unseen, load_imap_config, repo_root, imap_ssl_context
def decode_header_value(header: str | None) -> str:
@ -75,10 +75,11 @@ def main() -> None:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
_, nums = mail.search(None, "UNSEEN")
criterion = imap_search_criterion_unseen()
_, nums = mail.search(None, criterion)
ids = nums[0].split()
if not ids:
print("[gitea-issues] No unread messages.")
print("[gitea-issues] No unread messages (IMAP UNSEEN, on or after MAIL_SINCE_DATE). For spooler criterion (from/to), use tickets-fetch-inbox.sh and list-pending-spooler.sh.")
mail.logout()
return
@ -109,7 +110,7 @@ def main() -> None:
shown += 1
if shown == 0:
print("[gitea-issues] No unread messages sent to the configured alias (MAIL_FILTER_TO).")
print("[gitea-issues] No unread messages sent to the configured alias (MAIL_FILTER_TO). For spooler (from/to in conf.json), use tickets-fetch-inbox.sh and list-pending-spooler.sh.")
mail.logout()

View File

@ -2,8 +2,7 @@
# List unread emails (read-only). Run from repo root.
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
# REPO_ROOT = ia_dev so mail_common.repo_root() finds .secrets under ia_dev (independent of parent project)
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-list-unread.py"

View File

@ -3,8 +3,6 @@
# Usage: ./gitea-issues/mail-mark-read.sh <uid>
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-mark-read.py" "$@"

View File

@ -21,6 +21,34 @@ DEFAULT_SIGNATURE = """--
Support IA du projet Lecoffre.io
ai.support.lecoffreio@4nkweb.com"""
# Patterns that indicate the body contains a citation of the received message (forbidden).
CITATION_PATTERNS = (
"From:",
"Message-ID:",
"Message-ID :",
" wrote:",
" a écrit",
)
CITATION_LINE_START = (">",) # Quoted line start
def body_contains_citation(body: str) -> bool:
"""Return True if body looks like it contains the received message (citation)."""
if not body or not body.strip():
return False
lines = body.strip().splitlines()
for line in lines:
stripped = line.strip()
if not stripped:
continue
for pat in CITATION_PATTERNS:
if pat in line:
return True
for start in CITATION_LINE_START:
if stripped.startswith(start):
return True
return False
def get_reply_signature() -> str:
sig = os.environ.get("MAIL_REPLY_SIGNATURE", "").strip()
@ -49,7 +77,14 @@ def main() -> None:
body = args.body
if not body and not sys.stdin.isatty():
body = sys.stdin.read()
body = (body.rstrip() + get_reply_signature()).strip()
body = body.rstrip()
if body_contains_citation(body):
print(
"[gitea-issues] ERROR: Body must not contain the received message (no citation, no From:, Message-ID, wrote:, etc.). Send only your reply text.",
file=sys.stderr,
)
sys.exit(1)
body = (body + get_reply_signature()).strip()
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = args.subject

View File

@ -3,8 +3,6 @@
# Usage: ./gitea-issues/mail-send-reply.sh --to addr --subject "..." [--body "..." | stdin] [--in-reply-to "<msg-id>" [--references "..." ]]
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-send-reply.py" "$@"

View File

@ -8,8 +8,6 @@
# ./gitea-issues/mail-thread-log.sh append-commit --thread-id <id> --hash <hash> --message "..." [--branch "..."]
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-thread-log.py" "$@"

View File

@ -28,6 +28,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import (
create_gitea_issue,
imap_search_criterion_unseen,
load_gitea_config,
load_imap_config,
repo_root,
@ -79,7 +80,8 @@ def main() -> None:
mail.starttls(ssl.create_default_context())
mail.login(imap_cfg["user"], imap_cfg["password"])
mail.select("INBOX")
_, nums = mail.search(None, "UNSEEN")
criterion = imap_search_criterion_unseen()
_, nums = mail.search(None, criterion)
ids = nums[0].split()
if not ids:
print("[gitea-issues] No unread messages.")

View File

@ -7,8 +7,6 @@
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(cd "${GITEA_ISSUES_DIR}/.." && pwd)"
export GITEA_ISSUES_DIR
export REPO_ROOT="${ROOT}"
cd "$ROOT"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-to-issue.py"

View File

@ -11,6 +11,24 @@ from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
# Only consider messages on or after this date (IMAP format DD-Mon-YYYY). Override with env MAIL_SINCE_DATE.
MAIL_SINCE_DATE_DEFAULT = "10-Mar-2026"
def imap_since_date() -> str:
"""Return IMAP SINCE date (DD-Mon-YYYY). Messages before this date are ignored by fetch/list scripts."""
return os.environ.get("MAIL_SINCE_DATE", MAIL_SINCE_DATE_DEFAULT).strip() or MAIL_SINCE_DATE_DEFAULT
def imap_search_criterion_all() -> str:
"""IMAP search: all messages on or after MAIL_SINCE_DATE."""
return f"SINCE {imap_since_date()}"
def imap_search_criterion_unseen() -> str:
"""IMAP search: unread messages on or after MAIL_SINCE_DATE."""
return f"(UNSEEN SINCE {imap_since_date()})"
def repo_root() -> Path:
# Root = directory containing gitea-issues (ia_dev). .secrets and logs live under ia_dev (./.secrets, ./logs)

View File

@ -1,16 +1,25 @@
#!/usr/bin/env python3
"""
Fetch inbox emails filtered by authorized senders (conf.json tickets.authorized_emails).
Does not use UNSEEN; does not mark as read. Writes each matching message to projects/<id>/data/issues/
as JSON (<date>_<from_sanitized>_<uid>.pending). One file per message.
Only messages on or after MAIL_SINCE_DATE (default 10-Mar-2026) are considered. Does not use UNSEEN; does not mark as read
(uses BODY.PEEK[] so the server does not set \\Seen). Writes each matching message to projects/<id>/data/issues/
as JSON with filename <date>.<id>.<from>.<status> (status=pending). One file per message.
Id is deterministic from message (message_id or uid+date+from) so the same mail always gets the same base.
State: the spool directory (data/issues/) is the only record of what was already treated. We skip creating .pending
if .pending already exists or if .response exists for that base. If data/issues/ is empty, the script cannot know
what was treated before; the next run will create .pending for every matching message in the mailbox.
Usage: run from project root with GITEA_ISSUES_DIR and PROJECT_ROOT set (e.g. via tickets-fetch-inbox.sh).
Override date: MAIL_SINCE_DATE (IMAP format DD-Mon-YYYY, e.g. 10-Mar-2026).
"""
from __future__ import annotations
import email
import hashlib
import imaplib
import json
import os
import re
import sys
from datetime import datetime, timezone
@ -19,8 +28,15 @@ from email.utils import parsedate_to_datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import load_imap_config, imap_ssl_context
from project_config import authorized_emails, data_issues_dir, load_project_config
from mail_common import imap_search_criterion_all, load_imap_config, imap_ssl_context
from project_config import (
authorized_emails,
data_issues_dir,
ia_dev_root,
load_project_config,
project_dir,
project_root,
)
def decode_header_value(header: str | None) -> str:
@ -64,15 +80,45 @@ def get_text_body(msg: email.message.Message) -> str:
)
def _extract_addresses(header_value: str) -> set[str]:
"""Extract email addresses from a header value (e.g. 'Name <user@host>, other@host')."""
if not header_value or not header_value.strip():
return set()
decoded = decode_header_value(header_value).strip()
# Angle-bracket: <...@...>
in_angle = re.findall(r"<([^>]+)>", decoded)
# Standalone addr-spec (simplified: local@domain)
plain = re.findall(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]", decoded)
out: set[str] = set()
for a in in_angle:
out.add(a.strip().lower())
for a in plain:
out.add(a.strip().lower())
return out
def is_sent_to_alias(msg: email.message.Message, filter_to: str) -> bool:
"""True if the message was sent to the configured alias (To/Cc/Delivered-To/etc.)."""
if not filter_to:
return True
headers_to_check = ("To", "Delivered-To", "X-Original-To", "Cc", "Envelope-To")
filter_lower = filter_to.strip().lower()
headers_to_check = (
"To",
"Cc",
"Delivered-To",
"X-Original-To",
"X-Delivered-To",
"X-Envelope-To",
"Envelope-To",
)
for name in headers_to_check:
value = msg.get(name)
if value:
decoded = decode_header_value(value).lower()
if filter_to in decoded:
if filter_lower in decoded:
return True
addrs = _extract_addresses(value)
if filter_lower in addrs:
return True
return False
@ -82,6 +128,12 @@ def sanitize_from_for_filename(email_addr: str) -> str:
return re.sub(r"[^a-zA-Z0-9._-]", "_", email_addr.replace("@", "_"))
def generate_message_id(mid: str | None, uid_s: str, date_str: str, from_addr: str) -> str:
"""Deterministic 8-char id so the same message always gets the same base filename."""
raw = mid or f"{uid_s}_{date_str}_{from_addr}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:8]
def sanitize_attachment_filename(name: str) -> str:
"""Safe filename for attachment (no path, no dangerous chars)."""
if not name or not name.strip():
@ -127,13 +179,24 @@ def parse_references(refs: str | None) -> list[str]:
def main() -> int:
conf = load_project_config()
if not conf:
print("[tickets-fetch-inbox] No project config (projects/<id>/conf.json).", file=sys.stderr)
root = project_root()
ia_dev = ia_dev_root()
slug_path = root / "ai_project_id"
if not slug_path.is_file():
slug_path = root / ".ia_project"
slug = (slug_path.read_text(encoding="utf-8").strip() if slug_path.is_file() else "") or os.environ.get("IA_PROJECT", "").strip()
conf_path = ia_dev / "projects" / (slug or "<slug>") / "conf.json"
print("[tickets-fetch-inbox] No project config.", file=sys.stderr)
print(f"[tickets-fetch-inbox] PROJECT_ROOT={project_root()!s}, slug={slug!r}, looked at {conf_path}", file=sys.stderr)
return 1
auth = authorized_emails()
filter_to = (auth.get("to") or "").strip().lower()
from_list = auth.get("from")
# from must be a list of addresses, one per entry (not a single string with several addresses).
if isinstance(from_list, list):
allowed_from = {a.strip().lower() for a in from_list if a}
allowed_from = {str(a).strip().lower() for a in from_list if a}
elif isinstance(from_list, str):
allowed_from = {a.strip().lower() for a in re.split(r"[,;]", from_list) if a.strip()}
else:
allowed_from = set()
if not filter_to or not allowed_from:
@ -149,6 +212,11 @@ def main() -> int:
return 1
spool = data_issues_dir()
pd = project_dir()
if pd is None:
print(f"[tickets-fetch-inbox] WARNING: project_dir is None, using fallback spool {spool}", file=sys.stderr)
else:
print(f"[tickets-fetch-inbox] Spool directory: {spool}")
spool.mkdir(parents=True, exist_ok=True)
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
@ -156,21 +224,64 @@ def main() -> int:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
# Do not use UNSEEN; fetch all (or SINCE to limit). Filter by authorized senders only.
_, nums = mail.search(None, "ALL")
# Do not use UNSEEN; fetch messages on or after MAIL_SINCE_DATE (default 10-Mar-2026). Filter by authorized senders only.
# Use BODY.PEEK[] instead of RFC822 so the server does not set \Seen (emails stay "unread").
since_criterion = imap_search_criterion_all()
_, nums = mail.search(None, since_criterion)
ids = nums[0].split()
written = 0
skipped_fetch = 0
skipped_filter_to = 0
skipped_from = 0
skipped_pending = 0
skipped_response = 0
debug_to = os.environ.get("TICKETS_FETCH_DEBUG", "").strip() == "1"
debug_to_count = 0
debug_to_max = 3
for uid in ids:
uid_s = uid.decode("ascii")
_, data = mail.fetch(uid, "(RFC822)")
_, data = mail.fetch(uid, "(BODY.PEEK[])")
if not data or not data[0]:
skipped_fetch += 1
continue
raw = data[0]
raw_bytes = None
if isinstance(raw, tuple):
if len(raw) >= 2 and isinstance(raw[1], bytes):
raw_bytes = raw[1]
elif len(raw) >= 2 and isinstance(raw[1], str):
raw_bytes = raw[1].encode("utf-8", errors="replace")
elif isinstance(raw, bytes):
raw_bytes = raw
if not raw_bytes:
skipped_fetch += 1
continue
try:
msg = email.message_from_bytes(raw_bytes)
except Exception:
skipped_fetch += 1
continue
msg = email.message_from_bytes(data[0][1])
if not is_sent_to_alias(msg, filter_to):
skipped_filter_to += 1
if debug_to and debug_to_count < debug_to_max:
debug_to_count += 1
h_to = decode_header_value(msg.get("To"))
h_dt = decode_header_value(msg.get("Delivered-To"))
h_xo = decode_header_value(msg.get("X-Original-To"))
h_from = decode_header_value(msg.get("From"))
received = msg.get_all("Received") or []
received_sample = " | ".join(decode_header_value(r)[:80] for r in received[:2])
print(
f"[tickets-fetch-inbox] DEBUG not_to_alias (sample {debug_to_count}/{debug_to_max}): "
f"filter_to={filter_to!r} | To={h_to!r} | Delivered-To={h_dt!r} | X-Original-To={h_xo!r} | From={h_from!r}",
file=sys.stderr,
)
print(f"[tickets-fetch-inbox] DEBUG Received (first 2): {received_sample!r}", file=sys.stderr)
continue
from_raw = decode_header_value(msg.get("From"))
from_addr = parse_from_address(from_raw)
if from_addr not in allowed_from:
skipped_from += 1
continue
mid = (msg.get("Message-ID") or "").strip()
to_raw = decode_header_value(msg.get("To"))
@ -189,9 +300,15 @@ def main() -> int:
except Exception:
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
from_safe = sanitize_from_for_filename(from_addr)
base = f"{date_str}_{from_safe}_{uid_s}"
msg_id_short = generate_message_id(mid, uid_s, date_str, from_addr)
base = f"{date_str}.{msg_id_short}.{from_safe}"
path = spool / f"{base}.pending"
if path.exists():
skipped_pending += 1
continue
# Already treated: .response exists (we don't keep .pending after replying).
if (spool / f"{base}.response").exists():
skipped_response += 1
continue
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
attachments_meta: list[dict[str, str | int]] = []
@ -214,6 +331,7 @@ def main() -> int:
payload = {
"version": 1,
"type": "incoming",
"id": msg_id_short,
"message_id": mid or "",
"from": from_addr,
"to": to_addrs,
@ -234,6 +352,21 @@ def main() -> int:
mail.logout()
print(f"[tickets-fetch-inbox] Done. Wrote {written} new message(s) to {spool}.")
if skipped_fetch or skipped_filter_to or skipped_from or skipped_pending or skipped_response:
print(
f"[tickets-fetch-inbox] Skipped: fetch/parse={skipped_fetch}, not_to_alias={skipped_filter_to}, "
f"from_not_allowed={skipped_from}, pending_exists={skipped_pending}, response_exists={skipped_response}."
)
if skipped_filter_to > 0 and written == 0 and skipped_from == 0:
print(
"[tickets-fetch-inbox] All messages were excluded by not_to_alias: none have To/Delivered-To/X-Original-To matching the configured alias.",
file=sys.stderr,
)
print(
"[tickets-fetch-inbox] The IMAP account you are using receives mail for another address (e.g. Delivered-To shows that address). "
"To fetch messages sent to authorized_emails.to, connect IMAP to the mailbox that receives mail for that alias (same address as authorized_emails.to).",
file=sys.stderr,
)
return 0

View File

@ -1,14 +1,17 @@
#!/usr/bin/env bash
# Fetch inbox messages filtered by tickets.authorized_emails (conf.json). No UNSEEN; no mark read.
# Writes new messages to projects/<id>/data/issues/ (ia_dev) as JSON (<date>_<from>_<uid>.pending).
# Writes new messages to projects/<id>/data/issues/ as JSON (<date>.<id>.<from>.pending).
# Usage: cd <racine_projet> && ./ia_dev/gitea-issues/tickets-fetch-inbox.sh
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
PROJECT_ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export GITEA_ISSUES_DIR
export PROJECT_ROOT
# Use same project root and env as list-pending-spooler (lib.sh) so spool path is identical.
ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
cd "$ROOT"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
export PROJECT_ROOT="${PROJECT_ROOT:-$ROOT}"
cd "$PROJECT_ROOT"
exec python3 "${GITEA_ISSUES_DIR}/tickets-fetch-inbox.py" "$@"

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
Update the single spooler file for a message after sending a reply.
One file per message: read the .pending file (same base), add response data and set status to responded, write back.
No separate .response file; no file deletion.
Usage: ./gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]
base = filename base without extension (e.g. 2026-03-14T094530.a1b2c3d4.user_example.com).
"""
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from project_config import data_issues_dir, load_project_config
def main() -> int:
conf = load_project_config()
if not conf:
print("[write-response-spooler] No project config.", file=sys.stderr)
return 1
spool = data_issues_dir()
spool.mkdir(parents=True, exist_ok=True)
ap = argparse.ArgumentParser(description="Update spooler file in place after sending reply")
ap.add_argument("--base", required=True, help="Base name (e.g. 2026-03-14T094530.a1b2c3d4.user_example.com)")
ap.add_argument("--to", required=True, help="Recipient address")
ap.add_argument("--subject", required=True, help="Subject sent")
ap.add_argument("--body", required=True, help="Body sent")
ap.add_argument("--in-reply-to", default="", help="Message-ID we replied to")
args = ap.parse_args()
base = args.base.strip()
if not base or "/" in base or ".." in base:
print("[write-response-spooler] Invalid --base.", file=sys.stderr)
return 1
path = spool / f"{base}.pending"
if not path.exists():
print(f"[write-response-spooler] No such file: {path.name}", file=sys.stderr)
return 1
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
print(f"[write-response-spooler] Read error: {e}", file=sys.stderr)
return 1
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
data["status"] = "responded"
data["response"] = {
"in_reply_to_message_id": args.in_reply_to or "",
"to": args.to.strip(),
"subject": args.subject.strip(),
"body": args.body,
"sent_at": now,
}
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[write-response-spooler] Updated {path.name} (status=responded)")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Update the single spooler file (.pending) after sending a reply via mail-send-reply.sh. One file per message.
# Usage: ./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
export GITEA_ISSUES_DIR
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/write-response-spooler.py" "$@"

View File

@ -26,7 +26,7 @@ One JSON file per project: `projects/<id>/conf.json` (e.g. `projects/lecoffreio/
| `version.splash_app_name` | no | App name used in splash message template |
| `mail` | no | Mail/imap bridge config |
| `git` | no | Git hosting: `wiki_url`, `token_file` (path relative to repo root for token file). Ticketing URL is under `tickets`, not `git`. |
| `tickets` | no | Ticketing: `ticketing_url` (Gitea issues URL), `authorized_emails` (`to`: alias, `from`: list of allowed sender addresses for mail-based ticketing). See gitea-issues/TICKETS_SPOOL_FORMAT.md. |
| `tickets` | no | Ticketing: `ticketing_url` (Gitea issues URL), `authorized_emails` (`to`: alias, `from`: list of allowed sender addresses for mail-based ticketing). See projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md. |
## Example (minimal)

View File

@ -25,7 +25,7 @@
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/enso/issues",
"authorized_emails": {
"to": "ai.support.enso@4nkweb.com",
"from": ["ai.support.enso@4nkweb.com"]
"from": ["nicolas.cantu@pm.me"]
}
}
}

View File

@ -21,7 +21,7 @@
"ticketing_url": "https://git.4nkweb.com/4nk/ia_dev/issues",
"authorized_emails": {
"to": "ai.support.ia_dev@4nkweb.com",
"from": ["ai.support.ia_dev@4nkweb.com"]
"from": ["nicolas.cantu@pm.me"]
}
}
}

View File

@ -32,7 +32,11 @@
"ticketing_url": "https://git.4nkweb.com/4nk/lecoffre_ng/issues",
"authorized_emails": {
"to": "ai.support.lecoffreio@4nkweb.com",
"from": ["ai.support.lecoffreio@4nkweb.com", "laurence@lecoffre.io"]
"from": [
"laurence@lecoffre.io",
"nicolas.cantu@pm.me",
"gwendal@lecoffre.io"
]
}
}
}

View File

@ -0,0 +1,14 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYSXwVBYJKwYBBAHaRw8BAQdAsulrfkUdJQUxil8hC0AsehMTLq5l8TKx
iPOW807Y2u/NKW5pY29sYXMuY2FudHVAcG0ubWUgPG5pY29sYXMuY2FudHVA
cG0ubWU+wo8EEBYKACAFAmEl8FQGCwkHCAMCBBUICgIEFgIBAAIZAQIbAwIe
AQAhCRBxlbYDS4nNZhYhBK/x7PS2N5gMcqLnmHGVtgNLic1mvpIBAKq7Qfem
eEwWmX6unDM/hpnIl2CwDz+mmvaqHHZ/GXjwAQCAvIX82AhcjmnT90VKB5gP
P3xanAdUQLNjjsnAtcogDc44BGEl8FQSCisGAQQBl1UBBQEBB0DyQexAoEWM
pF7UOC8XJlz1BvnLhBitn596XSZg4exBSQMBCAfCeAQYFggACQUCYSXwVAIb
DAAhCRBxlbYDS4nNZhYhBK/x7PS2N5gMcqLnmHGVtgNLic1mQ0AA/1L7m+Cs
ZJLWnVxnn5Cdd9b6FZpAHK8f+LcCQ8euYudJAQDiUeKm8KZUvvYOGIvJuisG
YHDv7GY5mNCR1JdWaR1rDQ==
=TDqL
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,36 @@
{
"version": 1,
"type": "incoming",
"id": "af28dfa2",
"message_id": "<XImHtfSZSMJo1z1dqMvd19K5CPkEJ6q6-3Ky88Muiy5c_ED4mcc6JeLT-RY1Lt9T5b57Ce3GEvrrP6_kUZ0cToHtLKF2x324qbZDyLjqYG8=@pm.me>",
"from": "nicolas.cantu@pm.me",
"to": [
"\"ai.support.lecoffreio@4nkweb.com\" <ai.support.lecoffreio@4nkweb.com>"
],
"subject": "Présente le projet",
"date": "Sat, 14 Mar 2026 13:41:28 +0000",
"body": "",
"references": [
"<6tpKkxA0z1VR1XTFT_L-rtTce3HmvVxnicdFfSgp0wx0-4ptNS6uaTXDXEa2Z0Po0URmrTRSwpprPwAxEeFacQ==@protonmail.internalid>"
],
"in_reply_to": null,
"uid": "7903",
"created_at": "2026-03-14T13:42:53Z",
"issue_number": null,
"status": "responded",
"attachments": [
{
"filename": "publickey - nicolas.cantu@pm.me -\r\n 0xAFF1ECF4.asc",
"path": "2026-03-14T134128.af28dfa2.nicolas.cantu_pm.me.d/0_publickey_-_nicolas.cantu_pm.me_-___0xAFF1ECF4.asc",
"content_type": "application/pgp-keys",
"size": 653
}
],
"response": {
"in_reply_to_message_id": "<XImHtfSZSMJo1z1dqMvd19K5CPkEJ6q6-3Ky88Muiy5c_ED4mcc6JeLT-RY1Lt9T5b57Ce3GEvrrP6_kUZ0cToHtLKF2x324qbZDyLjqYG8=@pm.me>",
"to": "nicolas.cantu@pm.me",
"subject": "Re: Présente le projet",
"body": "Le projet LeCoffre.io est une application de coffre-fort numérique (dépôt lecoffre_ng). La documentation technique est sur le wiki du dépôt Gitea (https://git.4nkweb.com/4nk/lecoffre_ng/wiki). Les déploiements test, préproduction et production passent par les scripts deploy/ ; le ticketing et le support IA par les scripts gitea-issues/ dans ia_dev.",
"sent_at": "2026-03-14T13:43:30Z"
}
}