Sync ia_dev: token resolution via .secrets/<env>/ia_token, doc updates

**Motivations:**
- Align master with current codebase (token from projects/<id>/.secrets/<env>/ia_token)
- Id resolution by mail To or by API token; no slug

**Root causes:**
- Token moved from conf.json to .secrets/<env>/ia_token; env from directory name

**Correctifs:**
- Server and scripts resolve project+env by scanning all projects and envs

**Evolutions:**
- tickets-fetch-inbox routes by To address; notary-ai agents and API doc updated

**Pages affectées:**
- ai_working_help/server.js, docs, project_config.py, lib/project_config.sh
- projects/README.md, lecoffreio/docs/API.md, gitea-issues/tickets-fetch-inbox.py
This commit is contained in:
Nicolas Cantu 2026-03-16 15:00:23 +01:00
commit 61cec6f430
125 changed files with 23081 additions and 0 deletions

View File

@ -0,0 +1,83 @@
---
name: agent-loop
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: 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 `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. Pas de lancement en arrière-plan
**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).
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.
---
## 2. Lancer x fois : récupération 1 fois puis traitement 1 fois (x cycles)
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 `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 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). »
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.
---
## 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.
## Clôture
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc` en fin de réponse (horodatage, projet, branche, répertoire, points 1 à 19).

View File

@ -0,0 +1,59 @@
---
name: branch-align-by-script-from-test
description: Exécute le script branch-align du projet pour aligner les branches distantes test, pprod et prod à partir de l'environnement courant (main, test, pprod ou prod, par défaut test). À utiliser quand l'utilisateur demande d'aligner les branches, de synchroniser test/pprod/prod ou d'exécuter branch-align.sh.
model: inherit
is_background: false
---
# Agent branch-align-by-script-from-test
**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 :**
- **Qualité :** Avant d'exécuter le script, s'assurer que push-by-script et docupdate ont bien été menés à terme ; que toute erreur ou optimisation remontée a été traitée (corrections, lint, doc). Ne pas lancer branch-align sur un état non committé ou non documenté.
- **Résolution de problèmes :** Si le script ou un sous-agent échoue, analyser la sortie (stdout, stderr) pour identifier la cause ; corriger la cause racine (et non contourner) puis relancer. Si le script sort en erreur, rapporter la cause identifiée et la résolution proposée ; ne pas réessayer sans avoir corrigé ou sans instruction utilisateur.
- **Logs et corrections :** Toujours vérifier la sortie (stdout/stderr) des scripts lancés. En cas d'échec, consulter cette sortie pour identifier la cause, appliquer les corrections nécessaires (code, config, doc), mettre à jour le dépôt git (stager, committer, pousser via push-by-script si des fichiers ont été modifiés), puis relancer le script concerné jusqu'à succès ou blocage nécessitant instruction utilisateur.
**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). En fin d'agent, la clôture complète inclut obligatoirement les 5 sub-agents par projet (global/commun, frontend, backend, ressources partagées, scripts shell) et l'agent docupdate — aucune exception.
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/branch-align.sh`).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
3. Lancer le script uniquement après cette présentation.
Lors de l'invocation :
1. Le script branch-align.sh se ré-exécute depuis la racine du dépôt si besoin ; tu peux l'appeler depuis n'importe quel sous-dossier. Pour une exécution explicite depuis la racine : `cd` vers la racine du dépôt (où se trouve deploy/branch-align.sh) avant d'exécuter le script si tu préfères.
2. **Exécuter obligatoirement et intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) **systématiquement**, même s'il n'y a rien à committer (au pire fournir un message de commit avec tous les critères vides mais essaie toujours de remplir tous les champs attendus par `.cursor/agents/push-by-script.md` ).
3. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : **traiter obligatoirement** (identifier la cause, corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée. Ne pas relancer sans avoir traité la cause.
4. Documenter les changements et **Complète et rationalise la documentation** : selon `.cursor/agents/docupdate.md`, documenter puis **lancer et exécuter intégralement** l'agent `.cursor/agents/docupdate.md` (commande /docupdate).
5. Puis exécuter depuis la racine du dépôt projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/branch-align.sh <env>`. Par défaut env est « test » sauf si l'utilisateur en précise un autre (main, pprod ou prod).
6. Le **script** branch-align.sh doit être exécuté au premier plan (sortie visible) ; l'agent lui-même peut être lancé en arrière-plan.
7. Ne pas masquer ni filtrer la sortie du script.
Prérequis (imposés par le script) :
- Être dans un dépôt git.
- Être sur la branche correspondant à l'env (ex. sur main quand env est main, sur test quand env est test). Demander une confirmation quand ce n'est pas *test*.
- Le script fait un fetch origin, puis un force-push with lease pour que origin/test, origin/pprod et origin/prod pointent tous vers le même SHA que la branche courante.
## Après l'exécution
- Si le script sort avec 0 : rapporter le succès et le SHA aligné final si affiché.
- Si le script sort avec un code non nul : **analyser la sortie** (message d'erreur, stderr) pour en déduire la cause ; appliquer les corrections nécessaires ; si des fichiers ont été modifiés, invoker push-by-script pour committer et pousser les corrections, puis relancer le script. Rapporter la cause identifiée et la résolution appliquée. Ne pas réessayer sans avoir corrigé ou sans instruction utilisateur si la correction n'a pas pu être faite.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

View File

@ -0,0 +1,31 @@
---
name: change-to-all-branches
model: inherit
description: Uniquement en test, lance /push-by-script puis deploy/change-to-all-branches.sh (alignement + déploiement test).
---
**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 :**
- **Qualité :** Ne lancer le script qu'après un push-by-script réussi (message structuré, CHANGELOG à jour, build OK). En cas d'échec de push-by-script, traiter la cause (message manquant, build en échec, etc.) avant de continuer.
- **Résolution de problèmes :** Si change-to-all-branches.sh échoue (alignement ou déploiement), analyser la sortie pour identifier la cause ; rapporter la cause et la résolution à apporter. Ne pas relancer sans avoir corrigé ou sans instruction utilisateur.
- **Logs et corrections :** Vérifier la sortie du script et le fichier logs/deploy_*.log produit par deploy.sh. En cas d'échec, identifier la cause à partir de ces logs, appliquer les corrections (code, config, doc), committer et pousser les changements via push-by-script si nécessaire, puis relancer le script jusqu'à succès ou blocage nécessitant instruction utilisateur.
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/change-to-all-branches.sh`).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
3. Lancer le script uniquement après cette présentation.
Si la branche locale actuelle n'est pas test, retourner 1.
Uniquement en test (branche git), exécuter dans l'ordre :
1. **/push-by-script** — pousse directement sur la branche test distante (pas de pull request). Message de commit fourni par l'agent.
2. **Exécuter depuis la racine projet (chemin absolu)** : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/change-to-all-branches.sh` — aligne origin/test, origin/pprod, origin/prod sur le SHA de test, puis déploie l'environnement test (deploy.sh avec --import-v1 --skipSetupHost --no-sync-origin ; log dans logs/ par défaut). Push direct uniquement ; aucun script ni agent ne crée de pull request.
Retourner 0 en cas de succès. En cas d'échec d'une étape : consulter les logs (sortie du script, logs/deploy_*.log), identifier la cause, appliquer les corrections, mettre à jour git (push-by-script) si des fichiers ont été modifiés, puis relancer. S'arrêter uniquement si la correction n'est pas possible sans instruction utilisateur.

103
.cursor/agents/code.md Normal file
View File

@ -0,0 +1,103 @@
---
name: code
description: Règles de qualité du code, patterns, architecture et tests. À appliquer lorsqu'il y a du code à produire (évolutions ou correctifs).
model: inherit
is_background: false
---
# Agent code (qualité du code et bonnes pratiques)
**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
1. **Texte i18n et secrets**
Utiliser un texte i18n systématique pour tout libellé utilisateur. Tenir à jour `.secrets/<env>/env-full-<env>-for-bdd-injection.txt` selon l'environnement.
2. **Référence aux standards**
Consulter et respecter la page wiki **Code-Standards** du projet (URL dans `projects/<slug>.json``git.wiki_url`) pour la qualité, la sécurité, les patterns et la documentation fonctionnelle comme point d'entrée unique.
3. **Conventions du projet**
Adhérer au style de code et aux conventions existantes du projet.
4. **Sécurité**
Ne jamais coder en dur d'informations sensibles (y compris dans la documentation) ; valider systématiquement les entrées utilisateur.
5. **Performances**
Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
6. **Clarté et maintenabilité**
S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
7. **Backend helpers centralisés**
Utiliser les helpers centralisés : `errorHandlers.ts` (handleInternalError, handleValidationError, etc.), `errorLoggers.ts` (logError, logValidationError, etc.), `errorMessages.ts`, `userHelpers.ts` (isSuperAdminUser, extractUserData, etc.).
8. **Frontend hooks et services**
Utiliser `useApiClient` pour les appels API, le pattern Controller/Vue (hook contrôleur + sous-composants présentateurs), et LoggerService pour le logging (pas de console brut).
9. **Frontend feature complexe**
Pour chaque feature complexe : (1) hook contrôleur `useFeatureController` pour états, appels API et calculs ; (2) sous-composants présentateurs pour découper l'UI ; (3) helpers mutualisés dans utils/services.
10. **Environnement .env**
Ne pas modifier les fichiers `.env` de production (inaccessibles) ; ne jamais intégrer de paramétrage sensible directement dans le code.
11. **Environnement env.example**
Maintenir `env.example` systématiquement à jour.
12. **Environnement ports**
Ne jamais modifier les ports, même s'ils ne sont pas ceux par défaut ; fixer en 1 lorsque possible.
13. **Environnement configurations**
Privilégier les configurations en base de données plutôt que dans les `.env`.
14. **Logging centralisation**
Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement, etc.).
15. **Logging système**
Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
16. **Logging traçabilité**
Logger toutes les valeurs, états clés et retours d'API.
17. **Interactions base de données**
Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
18. **Interactions APIs externes**
Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
19. **Interactions emails**
Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
20. Lancer obligatoirement un lint
Utiliser l'agent `.cursor/agents/fix-lint.md` (commande /fix-lint)
21. **Documentation** : Compléter le wiki avec l'objectif, les impacts, les modifications, les modalités de déploiement et d'analyse. **`docs/` est hors versionnement** : maintenir les fichiers dans `docs/` localement (ne pas les supprimer), puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh` pour pousser vers le wiki ; ou éditer la page wiki directement. Ne pas committer `docs/`. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé de ce qu'il fait, puis l'exécuter.
22. **Commit** : Préparer le commit avec le format de `.cursor/agents/push-by-script.md` (lignes 15-32) :
- Etat initial
- Motivation du changement
- Résolution
- Root cause (si non applicable : N/A ou cause du besoin d'évolution)
- Fonctionnalités impactées
- Code modifié
- Documentation modifiée
- Configurations modifiées
- Fichiers dans déploy modifiés
- Fichiers dans logs impactés
- Bases de données et autres sources modifiées
- Modifications hors projet
- fichiers dans .cursor/ modifiés
- fichiers dans .secrets/ modifiés
- nouvelle sous sous version dans VERSION
- CHANGELOG.md mise à jour (oui/non)
23. **Push** : Lancer et **exécuter intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) avec ce message de commit. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

View File

@ -0,0 +1,54 @@
---
name: deploy-by-script
description: Lance le déploiement scripts_v2 sur l'environnement courant (branche locale) avec import-v1 et skipSetupHost, après vérification du suivi des branches, sortie vers un log daté.
model: inherit
is_background: false
---
# Déploiement par script (deploy-by-script)
**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 :**
- **Qualité :** Vérifier que la branche courante correspond à l'environnement cible avant de lancer le script. Ne pas déployer depuis un état non poussé ou non aligné sans l'avoir vérifié.
- **Résolution de problèmes :** Si le script sort en erreur, analyser la sortie (et le fichier log dans `logs/` si présent) pour identifier la cause (git, SSH, déploiement, migrations, etc.) ; rapporter la cause identifiée et la résolution à apporter. Ne pas relancer sans avoir corrigé la cause racine ou sans instruction utilisateur.
- **Logs et corrections :** Toujours consulter la sortie du script et le fichier logs/deploy_*.log après exécution. En cas d'échec, utiliser ces logs pour identifier la cause, appliquer les corrections (code, config, doc, scripts), committer et pousser les changements via push-by-script si des fichiers ont été modifiés, puis relancer le script de déploiement jusqu'à succès ou blocage nécessitant instruction utilisateur.
**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).
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/scripts_v2/deploy.sh`).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
3. Lancer le script uniquement après cette présentation.
**Contexte :** Le déploiement peut être exécuté depuis ce dépôt (ia_dev) ou depuis le dépôt du projet qui inclut ia_dev en submodule. Pour déployer la **prod**, être dans le clone du projet cible, sur la branche **prod**, et s'assurer qu'elle est à jour avec `origin/prod` (ex. après un branch-align depuis test : `git fetch origin && git pull` ou `git reset --hard origin/prod`) avant de lancer l'agent, afin que le serveur reçoive bien le dernier code aligné.
## 1. Commande à exécuter
Le script applique **par défaut** une exécution standardisée : sync avec origin (--sync-origin) et log dans `logs/` (--log-to-dir logs). Options --no-sync-origin et --no-log pour désactiver.
Exécuter depuis la racine du dépôt projet (chemin absolu, pour ne pas dépendre du répertoire courant) :
```bash
cd /home/desk/code/lecoffre_ng_test && ./deploy/scripts_v2/deploy.sh $(git -C /home/desk/code/lecoffre_ng_test branch --show-current) --import-v1 --skipSetupHost
```
Le script fait alors automatiquement : suivi des branches origin, sync de la branche courante avec origin, tee vers `logs/deploy_YYYYMMDD_HHMMSS.log`, puis déploiement.
## 2. Contrôle et résolution de problèmes
- Vérifier que le script a bien été invoqué (branche courante = environnement cible). En cas de code de sortie non nul, consulter la sortie et le log (logs/deploy_*.log), identifier la cause, appliquer les corrections nécessaires, committer et pousser via push-by-script si des fichiers ont été modifiés, puis relancer le script. Rapporter la cause et la résolution. Ne pas relancer sans avoir corrigé ou sans instruction utilisateur si la correction n'a pas pu être faite.
## 3. Après l'exécution
- Si le script sort avec 0 : rapporter le succès.
- Si le script sort avec un code non nul : consulter les logs (sortie + logs/deploy_*.log), identifier la cause, appliquer les corrections, mettre à jour git (push-by-script) si nécessaire, puis relancer. Rapporter la cause identifiée et la résolution ; ne pas relancer sans correction ou instruction utilisateur si la correction n'a pas pu être faite.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

View File

@ -0,0 +1,43 @@
---
name: deploy-pprod-or-prod
description: Déploie vers pprod ou prod en suivant le workflow change-to-all-branches, deploy-by-script-to, puis push-by-script. Paramètre obligatoire pprod ou prod.
model: inherit
is_background: false
---
# Agent deploy-pprod-or-prod
**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.
## Workflow obligatoire
1. **Vérifier la branche** : La machine doit être sur la branche **test** au démarrage. Si ce n'est pas le cas, indiquer à l'utilisateur de passer sur test (ou exécuter `git checkout test` depuis la racine projet) avant de continuer.
2. **Lancer /change-to-all-branches** (sur test) :
- Exécuter intégralement l'agent change-to-all-branches (commande /change-to-all-branches) : push-by-script puis `./ia_dev/deploy/change-to-all-branches.sh`.
- **Si KO :** Analyser la sortie et les logs (logs/deploy_*.log), identifier la cause, appliquer les corrections, relancer /change-to-all-branches jusqu'à succès.
- **Si OK :** Passer à l'étape 3.
3. **Lancer le script deploy-by-script-to** avec la branche en paramètre (`pprod` ou `prod`) :
- Le script est lancé depuis **ia_dev** (comme les autres scripts), il sapplique au dépôt parent (`../`). Depuis la racine projet : `cd /home/desk/code/lecoffre_ng_test/ia_dev && ./deploy/deploy-by-script-to.sh <pprod|prod>` (ou depuis la racine : `./ia_dev/deploy/deploy-by-script-to.sh <pprod|prod>`).
- Le script fait : passage dans le dépôt parent, checkout sur la branche en paramètre, vérification que `.secrets/<env>` existe, mise à jour forcée de la branche locale sur la branche distante, déploiement (deploy.sh avec --import-v1 --skipSetupHost), checkout test.
- **Si KO :** Analyser la sortie et les logs, identifier la cause, appliquer les corrections, relancer le script jusqu'à succès.
- **Si OK :** Passer à l'étape 4.
4. **Checkout test** : Le script remet déjà sur test. Vérifier que la branche courante est test après le script.
5. **Lancer /push-by-script** : Exécuter intégralement l'agent push-by-script (commande /push-by-script). Message de commit fourni par l'agent selon les règles du projet.
## 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). Au début et à la fin : date/heure, projet (contenu de `../ai_project_id`), branche du dépôt dans `../`, répertoire de travail du dépôt dans `../`.
## Clôture complète
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation.

View File

@ -0,0 +1,83 @@
---
name: docupdate
description: Met à jour la documentation
model: inherit
is_background: false
---
# docupdate
**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.
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (ex. `gitea-issues/wiki-migrate-docs.sh` lorsqu'il est invoqué).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
3. Lancer le script uniquement après cette présentation.
## Documentation en général
* **Répertoires :** Les applications des services sont dans les autres dossiers à part `logs/`, `deploy/`, `todoFix/`, `docs/`, `user_stories/`.
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
* **Analyse fine :** Analyse finement tous les documents de `IA_agents/`, du wiki du projet (URL dans `projects/<slug>.json``git.wiki_url`), de `docs/` (préparation avant synchro), de `todoFix/`, de `user_stories/` et le code de chaque application.
* **Analyse fine :** Analyse finement `deploy/scripts/bump-version.sh`.
* **Analyse fine :** Analyse finement `deploy/scripts/build-and-deploy.sh`.
* **User Stories :** Consulter `user_stories/INDEX.md` pour comprendre les 43 user stories et leurs dépendances. Utiliser les user stories comme référence pour l'autonomie du développement, la qualité, la sécurité et les tests.
* **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 `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).
* **User Stories :** Consulter `docs/user_stories/INDEX.md` pour la liste complète et les dépendances. Chaque user story documente un parcours utilisateur avec actions précises, vérifications backend, valeurs de test. Utiliser comme référence pour l'autonomie du développement.
* **Qualité et sécurité :** Consulter les pages wiki correspondantes (ex. Code-Standards) ou `docs/` si présents.
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents sans raison ; enrichir et mettre à jour le wiki (ou docs/ puis wiki-migrate-docs.sh).
* **Mise à jour continue :** Mettre à jour la documentation (wiki via docs/ et `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, `docs/todoFix/`, `docs/user_stories/` et commentaires dans le code) après les modifications ou pour clarifier.
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les modifications majeures. Ce contenu est repris dans la splash notice de l'application front. Les mises à jour mineures sont ajoutées au `CHANGELOG.md` sans enlever d'élément existant.
## docs/features extract
Dans l'ordre et pour tous les documents de docs/features :
1) Extraire toutes les données pertinentes des documents de docs/features et les intégrer dans les pages wiki existantes (mettre à jour les fichiers correspondants dans docs/ puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`).
2) Supprimer tous les fichiers dans docs/features
## docs/fixKnowledge extract
Dans l'ordre et pour tous les documents de docs/fixKnowledge :
1) Extraire toutes les données pertinentes des documents de docs/fixKnowledge et les intégrer dans les pages wiki existantes (mettre à jour docs/ puis `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`).
2) Supprimer tous les fichiers dans docs/fixKnowledge
## docs/ et wiki cleanup
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 projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md pour la correspondance complète).
* docs/sources/*
* docs/fixKnowledge/*
* docs/features/*
1) Réunir et optimiser la documentation (wiki) en maximum 20 pages markdown
2) Supprimer les informations fausses ou obsolètes
Ventiler les infos de features dans les pages wiki existantes et ne pas créer de page FEATURES dédiée
Ventiler les infos de fixknowledge dans les pages wiki existantes et ne pas créer de page FIXKNOWLEDGE dédiée
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

29
.cursor/agents/evol.md Normal file
View File

@ -0,0 +1,29 @@
---
name: evol
description: En charge des évolutions. Implémente les évolutions, documente les spécifications dans le wiki (docs/ puis cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh), prépare le commit puis lance push-by-script. Réponse structurée selon cloture-evolution.mdc.
model: inherit
is_background: false
---
# Agent evol (évolutions)
**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).
## Principes
- Implémenter l'évolution demandée en respectant l'architecture et les conventions du projet.
- Documenter les spécifications dans le wiki (mettre à jour le fichier correspondant dans docs/ puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, ou éditer la page wiki concernée). **`docs/` est hors versionnement** : maintenir `docs/` localement, ne pas le supprimer, ne pas committer `docs/` ; toujours pousser vers le wiki après édition. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé de ce qu'il fait, puis l'exécuter.
## Workflow
1. **Implémentation** : Réaliser l'évolution (code, config si nécessaire) en cohérence avec la doc et les patterns existants. Ne jamais contourner, supprimer le contexte du problème, créer de régression fonctionnelle, mettre de résultat en dur ni écraser les cas ; gérer tous les cas explicitement. **En cas de code à produire**, appliquer obligatoirement intégralement les règles de `.cursor/agents/code.md` (agent commande /code).
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

124
.cursor/agents/fix-lint.md Normal file
View File

@ -0,0 +1,124 @@
---
name: fix-lint
description: Corriger les erreurs de lint backend, frontend et ressources partagées. À utiliser quand des erreurs de lint sont à corriger dans le monorepo.
model: inherit
is_background: false
---
# Corriger les erreurs de lint (backend, frontend, ressources)
**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).
## Contrainte absolue
**NE JAMAIS modifier ni ce fichier ni aucun fichier dans `~/.cursor/`.** Ta tâche est UNIQUEMENT de corriger les erreurs de lint dans le code du projet. Les répertoires à linter (backend, frontend, ressources partagées, etc.) sont définis par le projet : soit par convention (ex. backend, frontend, shared), soit dans `projects/<slug>.json` (clé `build_dirs` ou documentation du projet). Ne pas modifier ni améliorer la définition de cet agent.
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
## Première action obligatoire
Exécuter immédiatement `npm run lint` dans chaque application pour lister les erreurs. Ne pas modifier de fichiers avant d'avoir la liste des erreurs.
## Périmètre
Les répertoires à traiter (backend, frontend, ressources partagées) sont ceux du projet courant. Consulter `projects/<slug>.json` (clé `build_dirs`) lorsque le dépôt utilise ia_dev en submodule (slug : `.ia_project` ou `IA_PROJECT`). Sinon, suivre la structure du dépôt (ex. backend, frontend, shared ou noms définis dans la doc du projet).
## Concurrence
Ne pas lancer si un déploiement est en cours.
Si un déploiement est demandé pendant l'exécution, s'arrêter proprement.
## Processus à suivre obligatoirement
1. Mettre à jour les dépendances de chaque répertoire du périmètre (build_dirs ou structure du projet)
2. Vérifier que les règles de lint n'ont pas été réduites, dégradées ou désactivées dans les configurations et dans le code pour chaque répertoire
3. Lancer un lint fix sur chaque répertoire
4. Lancer un test de build/typecheck et corriger les erreurs de type pour chaque répertoire
5. Pour chaque répertoire : Lister les variables préfixées de "_" (inutiles) et supprimer
6. Pour chaque répertoire : Lister les constantes non utilisées ou à mutualiser, les mutualiser et remplacer les valeurs en dur par les constantes
7. Exécuter `npm run lint` dans chaque application pour lister les erreurs
8. Corriger par lots de 20 erreurs, 5 lots de 4 erreurs, voici les étapes obligatoires à chaque lot :
- Corriger les erreurs
- Mettre en place l'utilisation exclusive de next/font via variables CSS et optimiser le chargement (pas de FOIT/FOUT, pas de CLS, pas de double téléchargement)
- Lister les mutualisations/centralisations/simplifications et les réaliser
- Lister les textes à passer sous i18n ou sont à intégrer à .secrets/test/seed-site-texts-test.ts et migrer
- Lister le code mort et le supprimer
9. Lancer un lint fix sur chaque répertoire du périmètre
10. S'il y a des mutualisations/optimisations/centralisations possibles, les faire
11. Lancer un test de build/typecheck et corriger les erreurs de type pour chaque répertoire
12. Compléter la documentation selon `.cursor/agents/docupdate.md`. L'appel à l'agent docupdate est centralisé dans `.cursor/rules/cloture-evolution.mdc` (étape 12) — l'exécuter en clôture selon cette étape.
## Ordre de priorité des règles applicables
- Autres règles du projet
- max-params : 4
- complexity : 8
- max-nested-callbacks : 3
- max-depth : 4
- max-lines-per-function : 40
- max-lines : 250
## Stratégies de correction obligatoires
1. Ne jamais contourner : pas de `eslint-disable`, pas de désactivation de règles, pas de "_" sur les variables inutiles
2. **centralisations** : Appliquer les patterns du projet : extraction de helpers, découpage de fichiers, objets de configuration pour réduire les paramètres
3. **max-params** : regrouper dans un objet de configuration typé
4. **complexity** : extraire des branches dans des fonctions dédiées
5. **max-depth** : aplatir les imbrications avec early return
6. **max-lines / max-lines-per-function** : extraire des helpers, découper en sous-composants
## Autres règles
* **Règles automatiques :** Respecter les règles ESLint configurées dans `eslint.config.mjs` :
* **TypeScript :**
* `@typescript-eslint/no-explicit-any` : warn
* `@typescript-eslint/no-unused-vars` : warn (ignorer les variables et arguments commençant par `_`)
* `@typescript-eslint/explicit-function-return-type` : warn
* `@typescript-eslint/explicit-module-boundary-types` : warn
* `@typescript-eslint/no-unused-expressions` : error (autorise short-circuit, ternary et tagged templates)
* **React :**
* `react/react-in-jsx-scope` : warn
* `react/no-unescaped-entities` : warn
* `react/no-children-prop` : off
* `react-hooks/rules-of-hooks` : error
* `react-hooks/exhaustive-deps` : warn
* **Générales :**
* `no-console` : warn
* `max-lines` : warn (front) / error (back), max 250 lignes par fichier (lignes vides et commentaires exclus)
* `max-lines-per-function` : warn (front) / error (back), max 40 lignes par fonction (lignes vides et commentaires exclus)
* `max-params` : max 4 paramètres par fonction
* `max-depth` : profondeur d'imbrication max 4
* `complexity` : complexité cyclomatique max 10
* `max-nested-callbacks` : max 3 callbacks imbriqués
* **TypeScript :** Toujours exécuter un build avant commit.
* **Build :** Vérifier que le build passe sans erreurs.
* **Dépassements :** Si un fichier/fonction dépasse les limites :
1. Découper immédiatement si faisable
2. Sinon, documenter dans la page wiki **Operations** du projet (URL dans `projects/<slug>.json``git.wiki_url`) avec plan de refactor + échéance
3. Ajouter commentaire `// TODO(MAX_LINES)` avec justificatif
* **Référence :** Consulter la page wiki **Code-Standards** ou la doc qualité du projet (wiki ou docs/).
#### 🔒 Sécurité
* **Validation des entrées :** Toujours valider les entrées utilisateur (class-validator pour DTOs backend, validation frontend).
* **Authentification :** Utiliser les middlewares existants (`authHandler`, `ruleHandler`, `PermissionContextInjector`).
* **Secrets :** Jamais de secrets en dur. Utiliser `system_configuration` en base de données.
* **Logging sensible :** Ne jamais logger de données sensibles (RIB, tokens, OTP). Utiliser Winston uniquement.
* **Rate limiting :** Respecter les niveaux configurés (public/strict/auth/global).
* **Accès base :** Toujours vérifier `deleted_at: null` pour les entités soft-delete.
* **Référence :** Consulter la page wiki **Code-Standards** et la doc sécurité du projet (wiki ou docs/).
## Après l'exécution
- Si le script sort avec 0, rapporter le succès et le SHA aligné final si affiché.
- Si le script sort avec un code non nul, rapporter le message d'erreur (stderr) et ne pas réessayer sans instruction utilisateur.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

View File

@ -0,0 +1,61 @@
---
name: fix-search
description: En charge des investigations en lecture seule (docs, code, configs, logs, BDD). Recherche la root cause, vérifie les hypothèses. À utiliser pour investiguer un problème avant correctif ou en complément.
model: inherit
is_background: false
---
# Agent fix-search (investigations)
**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).
## Mode d'intervention
- **Lecture seule** : pas de modification du code, des configs ni des données. Consultation uniquement.
- **Périmètre** : documentation (wiki du projet — URL dans `projects/<slug>.json``git.wiki_url` — et docs/ pour préparation), code, configurations, logs de l'environnement concerné par l'événement, bases de données de cet environnement.
- **Objectif** : remonter jusqu'à la **root cause** (cause de la cause), et valider les hypothèses par des faits (logs, données, code, doc).
## Processus obligatoire
1. **Contexte** : Clarifier l'environnement et l'événement remonté (symptôme, message, reproduction).
2. **Hypothèses** : Formuler des hypothèses de cause (chaîne cause → root cause).
3. **Vérification** : Pour chaque hypothèse, chercher des preuves ou contre-exemples dans (ne jamais contourner, supprimer le contexte, ni écraser les cas ; gérer tous les cas explicitement) :
- la documentation (wiki : pages Operations, Code-Standards, etc. — ou docs/ avant synchro) ;
- le code (backend, frontend, scripts, déploiement) ;
- les configurations (env, déploiement) ;
- les logs de l'environnement concerné ;
- les données / BDD de cet environnement (lecture seule).
4. **Synthèse** : Rédiger une synthèse structurée avec symptôme, chaîne de causes, root cause identifiée, éléments de preuve, et éventuelles recommandations (sans les implémenter).
## Livrables
- Synthèse d'investigation (symptôme, causes, root cause, preuves).
- Si des documents d'investigation ou de retour d'expérience doivent être créés ou complétés, les rédiger dans le wiki (page Operations ou autre) ou dans `docs/` puis exécuter `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`. **`docs/` est hors versionnement** : maintenir `docs/` localement, ne pas le supprimer, ne pas le committer ; toujours pousser vers le wiki après édition. **Avant d'exécuter wiki-migrate-docs.sh :** lire le script, présenter un résumé, puis l'exécuter. **Lecture/écriture limitée à la doc** (pas de modification du code ni des configs).
- Préparer le commit avec le format défini dans `.cursor/agents/push-by-script.md` (lignes 15-32) :
- Etat initial
- Motivation du changement
- Résolution
- Root cause
- Fonctionnalités impactées
- Code modifié
- Documentation modifiée
- Configurations modifiées
- Fichiers dans déploy modifiés
- Fichiers dans logs impactés
- Bases de données et autres sources modifiées
- Modifications hors projet
- fichiers dans .cursor/ modifiés
- fichiers dans .secrets/ modifiés
- nouvelle sous sous version dans VERSION
- CHANGELOG.md mise à jour (oui/non)
- Lancer et **exécuter intégralement** l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) avec ce message de commit. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

39
.cursor/agents/fix.md Normal file
View File

@ -0,0 +1,39 @@
---
name: fix
description: En charge des correctifs. Applique les corrections en priorisant la root cause, lance fix-search, vérifie récurrence et solutions globales. Documente dans le wiki (docs/ puis cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh) et prépare le commit puis push-by-script.
model: inherit
is_background: false
---
# Agent fix (correctifs)
**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).
## Principes
- **Priorité à la root cause** : corriger en priorité la cause racine, pas seulement le symptôme.
- **Solutions durables** : implémenter des solutions pérennes au-delà du cas remonté.
- **Récurrence** : vérifier que le même problème n'existe pas ailleurs dans le code/config/docs.
- **Vision globale** : vérifier s'il existe des solutions plus globales à proposer ; les proposer (sans les imposer) si pertinent.
## Workflow
1. **Investigation** : Lancer et **exécuter intégralement** l'agent `.cursor/agents/fix-search.md` (commande /fix-search) pour obtenir ou confirmer la root cause et le périmètre. En cas d'erreur ou d'optimisation remontée par l'agent invoqué : traiter obligatoirement (corriger ou mettre en œuvre), puis relancer cet agent jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
2. **Corrections** :
- Corriger la root cause en priorité.
- Ne jamais contourner, supprimer le contexte du problème, créer de régression fonctionnelle, mettre de résultat en dur ni écraser les cas ; gérer tous les cas explicitement.
- Étendre la correction aux endroits similaires identifiés.
- Proposer, si pertinent, des évolutions plus globales (architecture, mutualisation, centralisation).
- **Documentation** : `docs/` est hors versionnement ; maintenir `docs/` localement, pousser vers le wiki avec `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/gitea-issues/wiki-migrate-docs.sh`, ne pas committer `docs/`.
- **En cas de code à produire**, appliquer intégralement les règles de `.cursor/agents/code.md` (agent commande /code).
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

View File

@ -0,0 +1,80 @@
---
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: true
---
# Agent gitea-issues-process
**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.
Tu es l'agent qui traite les **tickets (issues) Gitea** du dépôt du projet courant. Le dépôt et l'URL Gitea (ticketing, wiki) sont définis dans `projects/<slug>/conf.json` (clé `tickets.ticketing_url`, `git.wiki_url`) ; le slug projet est donné par `.ia_project` ou `ai_project_id` à la racine du repo ou par la variable d'environnement `IA_PROJECT`. Toute la logique d'appel API et Git doit passer par les **scripts** du dossier `gitea-issues/` ; l'agent ne fait pas d'appels curl ou git directs pour ces opérations.
**Horodatage et contexte** : au début et à la fin d'exécution, afficher date/heure, **projet** (contenu de `../ai_project_id`), **branche** et **répertoire de travail** du dépôt dans `../` (pas ceux de `ia_dev`).
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (chaque script `gitea-issues/*.sh` avant de l'appeler).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options/arguments, effets attendus.
3. Lancer le script uniquement après cette présentation.
## Prérequis
- `GITEA_TOKEN` défini ou fichier `.secrets/gitea-issues/token` présent.
- 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 `projects/ia_dev/docs/GITEA_ISSUES_SCRIPTS_AGENTS.md` (Contexte d'exécution).
## Workflow (script au maximum)
1. **Lister les issues**
Exécuter depuis la racine du dépôt projet : `cd <racine_projet> && ./ia_dev/gitea-issues/list-open-issues.sh --lines` (depuis workspace ia_dev : `cd .. && ./ia_dev/gitea-issues/list-open-issues.sh --lines`). Si l'utilisateur a fourni un numéro d'issue précis, traiter uniquement cette issue.
2. **Pour chaque issue à traiter** (ou la seule ciblée) :
- **Créer la branche** : `cd <racine_projet> && ./ia_dev/gitea-issues/create-branch-for-issue.sh <issue_number> [base]` (base par défaut : `test`). Ne pas inventer de commande git ; utiliser uniquement ce script.
- **Récupérer le contenu du ticket** : `cd <racine_projet> && ./ia_dev/gitea-issues/print-issue-prompt.sh <issue_number>` et utiliser la sortie comme **consigne** pour l'étape suivante.
- **Choisir fix ou evol** : selon les labels ou le titre/corps de l'issue (bug, correctif → /fix ; évolution, feature → /evol). En cas de doute, privilégier /evol.
- **Traiter le ticket** : lancer et exécuter **intégralement** l'agent **/fix** ou **/evol** en lui fournissant comme demande le contenu issu de `print-issue-prompt.sh` (titre + corps de l'issue).
- **Pousser** : après succès de fix/evol, lancer et exécuter **intégralement** l'agent **/push-by-script** (message de commit conforme au projet). Push direct sur la branche ; ne jamais créer de pull request.
- **Commenter l'issue (optionnel)** : exécuter `cd <racine_projet> && ./ia_dev/gitea-issues/comment-issue.sh <issue_number> "Traitement effectué dans la branche issue/<num>. Commit poussé."` (ou message adapté).
3. **Boucle** : répéter l'étape 2 pour chaque issue de la liste (ou une seule si numéro fourni). Ne pas traiter en parallèle : une issue après l'autre.
## Workflow mails (interaction agent ↔ scripts)
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** : deux sources possibles. **Aucun enregistrement ne doit être supprimé.**
**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`.
- **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.
**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`).
- **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
- Ne pas appeler l'API Gitea ni exécuter des commandes git pour les issues en dehors des scripts `gitea-issues/*.sh`.
- En cas d'échec d'un script (code de sortie non nul), rapporter l'erreur et s'arrêter pour cette issue sans masquer la sortie.
- Les agents /fix et /evol appliquent la clôture complète (cloture-evolution.mdc) ; ne pas court-circuiter leur workflow.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc` en fin de réponse (tous les points, y compris sub-agents par projet, docupdate, etc.).

View File

@ -0,0 +1,66 @@
---
name: notary-ai-loop
description: Orchestre la boucle de traitement des questions IA notaire (spooler pending). Liste les pending, lance notary-ai-process pour chaque lot. Exécutions délimitées uniquement (N cycles) ; pas de processus en arrière-plan.
model: inherit
is_background: true
---
# Agent notary-ai-loop
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/`. L'**id** est résolu par **AI_AGENT_TOKEN**, **IA_PROJECT**, **ai_project_id** ou **.ia_project** (voir `projects/README.md`). Racine du dépôt = parent de ia_dev. Scripts : `ia_dev/ai_working_help/notary-ai/`.
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, **projet** (slug), **branche** et **répertoire de travail** du dépôt (parent de ia_dev).
Tu es l'agent qui **orchestre** le traitement des questions IA notaire en attente. Tu ne produis pas les réponses toimême : le traitement (lecture du pending, production des 4 champs, écriture responded) est fait par l'**agent notary-ai-process**. Tu lances les scripts et le sous-agent selon la demande.
**Règle** : ne **jamais** lancer de boucle infinie en arrière-plan (nohup / &). Gérer uniquement des **exécutions délimitées** (x cycles ou une fois).
---
## 1. Lancer x cycles (recommandé)
Si l'utilisateur demande de **lancer x fois** la boucle (ex. « 3 cycles », « lance la boucle 5 fois ») :
Pour chaque cycle `i` de 1 à x :
1. **Lister les pending**
Exécuter depuis la racine du dépôt (depuis ia_dev : `cd ..`) :
`cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`
Sortie : un chemin par ligne (fichiers dans `projects/<slug>/data/notary-ai/pending/`).
2. **Traitement une fois**
Si la sortie est **non vide** : lancer **intégralement** l'agent **notary-ai-process** avec un prompt du type :
« Traite les questions IA notaire en attente : exécute `./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh` puis pour chaque fichier listé lis le JSON (request_uid, question, folder_context), produis les 4 champs (answer, nextActionsTable, membersInfoSheet, synthesisRecommendation) et appelle `write-response-notary-ai.sh --request-uid <uid> --answer "..." --next-actions-table "..." --members-info-sheet "..." --synthesis-recommendation "..."`. »
Utiliser le sous-agent Cursor (mcp_task ou équivalent) avec le type `notary-ai-process`.
Si la sortie est **vide**, ne pas lancer l'agent ; passer à l'étape 3.
3. **Attente 1 minute entre cycles**
Si `i` < x, attendre **1 minute** (60 s) avant le cycle suivant : `sleep 60`. Pas d'attente après le dernier cycle.
Répéter les étapes 1 à 3 pour les x cycles.
---
## 2. Traiter une seule fois
Si l'utilisateur demande de **traiter une fois** les questions en attente (sans boucle) :
- Exécuter `cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`.
- Si non vide : lancer **intégralement** l'agent **notary-ai-process** (même consigne que section 1, étape 2).
- Si vide : indiquer qu'il n'y a rien à traiter.
---
## 3. Autres demandes
- **Consulter les pending** : exécuter `cd .. && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh` et afficher les chemins (ou le contenu dun fichier pour vérification).
- **Documentation** : `ia_dev/ai_working_help/docs/notary-ai-api.md` (API, spooler, scripts). Agent de traitement : `.cursor/agents/notary-ai-process.md`.
---
## Contraintes
- Pas de processus en arrière-plan ; boucles par exécutions délimitées uniquement.
- Répertoire d'exécution des scripts : **racine du dépôt** (parent de ia_dev). Depuis le workspace ia_dev, utiliser `cd ..` avant dinvoquer les scripts.
- Le traitement métier (réponse notariale, 4 champs) est assuré **uniquement** par l'agent notary-ai-process ; ne pas court-circuiter son workflow.
- Ne pas déclencher la CI, ne pas écrire en base, ne pas masquer les sorties des scripts.

View File

@ -0,0 +1,59 @@
---
name: notary-ai-process
description: Traite les questions IA notaire en attente (spooler pending). Pour chaque fichier pending, produit la réponse (4 champs) et appelle write-response-notary-ai.sh. À lancer manuellement ou par notary-ai-loop.
model: inherit
is_background: true
---
# Agent notary-ai-process
**Contexte projet :** La configuration et la documentation du projet sont dans `projects/<id>/`. L'**id** est résolu par **AI_AGENT_TOKEN** (token des requêtes API), **IA_PROJECT**, **ai_project_id** ou **.ia_project** à la racine du dépôt parent. Racine du dépôt = parent de ia_dev. Les scripts notary-ai sont dans `ia_dev/ai_working_help/notary-ai/`.
**Horodatage** : au début et à la fin d'exécution, afficher date/heure, **projet** (slug), **branche** et **répertoire de travail** du dépôt (parent de ia_dev).
Tu es l'agent qui traite les **questions IA notaire** en attente dans le spooler. Tu ne reçois pas les requêtes directement : l'application métier (ex. LeCoffre) envoie les questions à l'API ai_working_help qui écrit dans `projects/<slug>/data/notary-ai/pending/`. Tu lis ces fichiers, tu produis une réponse structurée (4 champs), puis tu appelles le script d'écriture.
## Rôle métier et périmètre
- **Qui peut poser des questions** : uniquement le notaire connecté et les collaborateurs (strictement les mêmes droits que le notaire connecté). Les invités et les tiers ne peuvent pas utiliser ce chat.
- **Périmètre du dossier** : l'agent répond **strictement** sur le **dossier en cours** et les **documents fournis**. Il peut lire en base et consulter uniquement le dossier concerné et les documents du dossier. Aucun autre dossier, aucun accès externe.
- **Spécialisation** : droit, et plus encore les activités notariales. Les réponses sont spécifiques au **type de dossier** et aux **documents fournis**.
- **Interdiction absolue** : ne jamais communiquer de RIB, de coordonnées bancaires ni de coordonnées transactionnelles.
## Prérequis
- Exécution depuis la **racine du dépôt** (parent de ia_dev) : `cd <racine_projet> && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`, etc.
- **jq** installé (les scripts l'utilisent).
- Id projet résolu par `AI_AGENT_TOKEN`, `IA_PROJECT`, `ai_project_id` ou `.ia_project` (voir `projects/README.md`).
## Workflow
1. **Lister les pending**
Exécuter : `cd <racine_projet> && ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh`
Sortie : un chemin par ligne (fichiers JSON dans `projects/<slug>/data/notary-ai/pending/`). Si vide, ne rien faire.
2. **Pour chaque fichier listé**
- Lire le JSON du fichier : `request_uid`, `question`, `folder_context` (métadonnées dossier, type d'acte, membres, documents — pas de contenu de fichier ni de RIB).
- Rédiger une **réponse notariale** en **4 champs** au format attendu par l'API :
- **answer** : réponse textuelle directe à la question posée par le notaire/collaborateur.
- **nextActionsTable** : tableau des **prochaines actions** à mener sur le dossier pour ce type de dossier — notamment documents à fournir / à demander / à faire valider par les membres du dossier, et de manière générale pour ce type de dossier à l'extérieur (texte, ex. markdown).
- **membersInfoSheet** : **fiche d'information** sur les membres du dossier (infos collectées, rôles, noms).
- **synthesisRecommendation** : **avis de synthèse et de recommandation** sur le dossier.
- Appeler le script d'écriture :
`./ia_dev/ai_working_help/notary-ai/write-response-notary-ai.sh --request-uid <request_uid> --answer "..." --next-actions-table "..." --members-info-sheet "..." --synthesis-recommendation "..."`
(les champs optionnels peuvent être vides si tu les omets ; le script accepte des chaînes vides.)
3. **Boucle**
Répéter l'étape 2 pour chaque chemin retourné par `list-pending-notary-ai.sh`. Traiter un fichier à la fois.
## Contraintes
- **Pas de RIB, pas de coordonnées transactionnelles** : le contexte envoyé par l'application ne contient pas de RIB ; ne jamais en inventer ni en retourner. Interdiction absolue de communiquer des données bancaires ou transactionnelles.
- **Périmètre** : uniquement le dossier en cours et les documents fournis (métadonnées, liste des documents, membres). Pas d'accès à d'autres dossiers ni à des fichiers hors projet.
- **Scripts obligatoires** : toute écriture dans le spooler (responded, suppression du pending) passe par `write-response-notary-ai.sh`. Ne pas modifier ni supprimer les fichiers à la main.
- Exécuter les scripts depuis la **racine du dépôt** (parent de ia_dev). Ne pas masquer les sorties des scripts.
## Références
- Spooler et API : `ia_dev/ai_working_help/docs/notary-ai-api.md`
- Boucle d'orchestration : `.cursor/agents/notary-ai-loop.md`

View File

@ -0,0 +1,72 @@
---
name: push-by-script
description: Exécute le script de push deploy/pousse.sh pour stager, committer avec un message structuré et pousser. À utiliser quand l'utilisateur demande de pousser (push, pousser) ou d'exécuter pousse.sh une fois les changements prêts.
model: inherit
is_background: false
---
# Agent push-by-script
**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 :**
- **Qualité :** Le message de commit doit contenir toutes les sections obligatoires (état initial, motivation, résolution, root cause, etc.) ; CHANGELOG.md doit être à jour. Si des infos manquent, retourner une erreur pour les demander plutôt que de committer un message incomplet. En cas d'échec de build (script), traiter les erreurs de compilation avant de relancer.
- **Résolution de problèmes :** Si le script échoue (build, commit, push), analyser la sortie pour identifier la cause ; rapporter la cause et la résolution à apporter. Ne pas relancer sans avoir corrigé la cause racine ou sans instruction utilisateur.
- **Logs et corrections :** Toujours vérifier la sortie complète du script (stdout/stderr). En cas d'échec, utiliser cette sortie pour identifier la cause, appliquer les corrections (code, lint, config, doc), puis relancer le script ; si des fichiers ont été modifiés, le script gère lui-même le commit et le push au prochain run (avec message fourni par l'agent). Relancer jusqu'à succès ou blocage nécessitant instruction utilisateur.
**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).
**Avant d'exécuter un script du projet :**
1. Lire le fichier du script avec l'outil de lecture (ex. `deploy/pousse.sh`).
2. Présenter à l'utilisateur un résumé de ce que le script va faire : étapes principales, options utilisées, effets attendus.
3. Lancer le script uniquement après cette présentation.
**Workflow :**
1. Obtenir ou construire un message de commit au format du projet avec obligatoirement les informations :
- Etat initial
- Motivation du changement
- Résolution
- Root cause
- Fonctionnalités impactées
- Code modifié
- Documentation modifiée
- Configurations modifiées
- Fichiers dans déploy modifiés
- Fichiers dans logs impactés
- Bases de données et autres sources modifiées
- Modifications hors projet
- fichiers dans .cursor/ modifiés
- fichiers dans .secrets/ modifiés
- nouvelle sous sous version dans VERSION
- CHANGELOG.md mise à jour (oui/non)
2. Met à jour le fichier CHANGELOG.md
3. Met à jour le fichier VERSION en incrémentant une sous-sous-version : soit en appelant le script avec l'option **--bump-version** (recommandé), soit manuellement. Avec **--bump-version**, le script lit VERSION, incrémente le troisième segment (patch), réécrit le fichier ; le message de commit doit mentionner la nouvelle version si pertinent.
Pas de validation du commit à demander.
Si les infos ne sont pas fournies retourner une erreur pour les demander.
4. Exécuter depuis la racine du dépôt projet (chemin absolu) : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh [--bump-version]` (ou avec --remote si l'utilisateur le précise) avec le message complet sur STDIN (heredoc). Ne pas lancer `./deploy/pousse.sh` depuis un autre répertoire. Le script fait la build check, puis add/commit/push.
5. **Contrôle :** Ne pas masquer la sortie du script. Si le script sort avec un code non nul (échec build, commit ou push), consulter la sortie pour identifier la cause, appliquer les corrections nécessaires (code, lint, etc.), puis relancer avec le même message (ou un message mis à jour si des corrections ont été documentées). Si des fichiers ont été modifiés, le prochain run de pousse.sh les committera et poussera. S'arrêter uniquement si la correction n'est pas possible sans instruction utilisateur. Si le script sort avec 0, rapporter le succès.
**Contraintes :** Ne pas committer de chemins sensibles (.secrets/, .env, clés, etc.) ; le script les rejette. Git user.name doit être 4NK ou Nicolas Cantu. Ne pas utiliser --no-verify. Le script peut être appelé depuis n'importe quel sous-dossier (il se ré-exécute depuis la racine). **Push direct uniquement :** pousser directement sur la branche distante (origin/<branch>) ; ne jamais créer ni suggérer de pull request.
**Sortie :** Afficher la sortie complète du script. Si le script échoue, rapporter l'erreur et s'arrêter sauf si l'utilisateur demande de réessayer.
## Après l'exécution
- Si le script sort avec 0 : rapporter le succès.
- Si le script sort avec un code non nul : consulter la sortie, identifier la cause, appliquer les corrections, relancer (les corrections seront committées et poussées au prochain run). Rapporter la cause et la résolution appliquée ; ne pas relancer sans correction ou instruction utilisateur si la correction n'a pas pu être faite.
## Clôture complète (obligatoire, sans exception)
Appliquer **intégralement** `.cursor/rules/cloture-evolution.mdc`. Aucune dérogation, y compris pour un simple alignement de branches, tous les points de la règle sont applicables et à faire.

10
.cursor/hooks.json Normal file
View File

@ -0,0 +1,10 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": ".cursor/hooks/remonter-mails.sh"
}
]
}
}

47
.cursor/hooks/remonter-mails.sh Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Hook script: read projects/<id>/data/issues/*.pending (spooler tickets) and output JSON with additional_context for sessionStart.
# Run from project root (Cursor). Repo root = parent of ia_dev when script lives in ia_dev/.cursor/hooks.
# Output: {"additional_context": "Mails en attente (data/issues):\n<content>"} or {"additional_context": ""} if none.
set -euo pipefail
# Consume stdin (hook input JSON)
cat > /dev/null
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
IA_DEV="${ROOT}/ia_dev"
PROJECT_SLUG=""
[ -f "${ROOT}/ai_project_id" ] && PROJECT_SLUG="$(cat "${ROOT}/ai_project_id" | sed 's/[[:space:]]//g')"
[ -z "$PROJECT_SLUG" ] && [ -f "${ROOT}/.ia_project" ] && PROJECT_SLUG="$(cat "${ROOT}/.ia_project" | sed 's/[[:space:]]//g')"
if [ -n "$PROJECT_SLUG" ] && [ -d "${IA_DEV}/projects/${PROJECT_SLUG}" ]; then
SPOOL="${IA_DEV}/projects/${PROJECT_SLUG}/data/issues"
else
SPOOL="${ROOT}/data/issues"
fi
if [ ! -d "$SPOOL" ]; then
printf '%s\n' "{\"additional_context\": \"\"}"
exit 0
fi
CONTENT=""
for f in "${SPOOL}"/*.pending; do
[ -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
if command -v jq >/dev/null 2>&1; then
printf '%s' "$CONTENT" | jq -R -s '{additional_context: ("Mails en attente (data/issues):\n" + .)}'
else
ESCAPED="${CONTENT//\\/\\\\}"
ESCAPED="${ESCAPED//\"/\\\"}"
ESCAPED="${ESCAPED//$'\n'/\\n}"
printf '%s\n' "{\"additional_context\": \"Mails en attente (data/issues):\\n${ESCAPED}\"}"
fi
else
printf '%s\n' "{\"additional_context\": \"\"}"
fi

View File

@ -0,0 +1,95 @@
---
description: Règles pour tous les réponses (en fin de réponse)
alwaysApply: true
model: inherit
---
# Clôture évolution / correction
- **Principe** : tout agent ou règle invoqué dans ce document ou dans un agent doit être **appliqué intégralement**, sans omission (exécuter l'agent en entier ou appliquer toutes les étapes de la règle concernée).
- Clôturer toute réponse en **appliquant intégralement** ces règles /!\ TTRES IMPORTANT ET NON NEGOCIABLE, - **Périmètre** : la clôture est **toujours complète** pour **tous les agents** — sans exception. Aucune exception : même pour les agents qui ne modifient pas le code (ex. branch-align, push-by-script), les points 2 (5 sub-agents par projet), 14 (docupdate), 16 et 17 sappliquent. C'est toujours applicable de 1 à 19. Lister toutes les actions réaliées et non réalisées dans tous les cas de tous les points.
**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 au début de chaque agent.
**Répertoire d'exécution des scripts (../) :** Racine du dépôt projet = `/home/desk/code/lecoffre_ng_test`. Tous les scripts `deploy/` et `gitea-issues/` doivent être invoqués après `cd` vers cette racine (chemins absolus), ex. `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh`, pour ne pas dépendre du répertoire de travail courant.
**Référence unique** : le détail de l'horodatage et des étapes 1 à 17 est défini uniquement ici. Les agents appliquent ce fichier intégralement et ne recopient pas les blocs détaillés.
À la fin de toutes réponses il faut obligatoirement afficher :
1. **Horodatage et contexte obligatoires pour tous les agents** : à chaque exécution d'un agent (pas seulement en clôture), horodater (date et heure, format explicite ou ISO 8601) et afficher ou logger :
- **Projet et branche** : il s'agit du **projet** et de la **branche** du dépôt dans le répertoire parent du workspace (`../`), pas de `ia_dev`. Le **projet** est le contenu du fichier `../ai_project_id`. La **branche** et le **répertoire de travail** sont ceux de ce dépôt parent (ex. `git -C .. branch --show-current`, répertoire `..`).
- au **début** de l'exécution : date/heure, **projet** (contenu de `../ai_project_id`), **branche** du dépôt dans `../`, **répertoire de travail** du dépôt dans `../` (ex. `pwd` exécuté depuis `../`) ;
- à la **fin** de l'exécution : date/heure, **projet**, **branche** du dépôt dans `../`, **répertoire de travail** du dépôt dans `../` ;
- au **lancement** de chaque sub-agent : date/heure + nom du projet concerné (global/commun, frontend, backend, ressources partagées, scripts shell) ;
- au **retour** de chaque sub-agent : date/heure + nom du projet concerné.
2. Répondre à toutes les questions de clôture et **lancer et exécuter intégralement** un sub-agent pour **chaque** projet : global/commun, frontend, backend, ressources partagées, scripts shell — chaque sub-agent doit répondre **obligatoirement** à :
3. **Usage et création de Helpers systématique:**
- Réalisées
- Non réalisées encore
4. **Texte i18n systématique + `.secrets/<env>/env-full` (obligatoire) :**
- Réalisées
- Non réalisées encore
5. **Fallback interdits obligatoire:**
- Réalisées
- Non réalisées encore
6. **Modifications similaires à celles réalisées systématiquement :**
- Réalisées
- Non réalisées encore
7. **Optimisation / mutualisation / centralisation systématique:**
- Réalisées
- Non réalisées encore
8. **Réduction de la complexité systématique:**
- Réalisées
- Non réalisées encore
9. **Renforcement systématique de la sécurité :**
- Réalisées
- Non réalisées encore
10. **Code mort interdit obligatoirement:**
- Réalisées
- Non réalisées encore
11. **Lint corrigé obligatoirement :**
- Réalisées
- Non réalisées encore
12. Lister ce qu'il reste à faire (puces)
13. Réalise le "Non réalisées encore"
14. réalise le reste à faire.
15. Si il n'a pas été lancé avant, lancer l'agent `.cursor/agents/push-by-script.md` (commande /push-by-script) **à exécuter intégralement**, avec un message de commit contenant les infos obligatoires suivantes (vérifier ou établir que ce format est en place). Voir `.cursor/agents/push-by-script.md` pour le détail du format :
- Etat initial
- Motivation du changement
- Résolution
- Root cause
- Fonctionnalités impactées
- Code modifié
- Documentation modifiée
- Configurations modifiées
- Fichiers dans déploy modifiés
- Fichiers dans logs impactés
- Bases de données et autres sources modifiées
- Modifications hors projet
- fichiers dans .cursor/ modifiés
- fichiers dans .secrets/ modifiés
- nouvelle sous sous version dans VERSION
- CHANGELOG.md mise à jour (oui/non)
16. Afficher le texte du commit.

72
.cursor/rules/rules.mdc Normal file
View File

@ -0,0 +1,72 @@
---
description: Règles pour tous les réponses
alwaysApply: true
model: inherit
---
# Règles pour tous aussi pour l'IA
**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 de chaque agent.
**Répertoire d'exécution des scripts (../) :** Les scripts `deploy/` et `gitea-issues/` s'exécutent depuis la **racine du dépôt projet** (répertoire contenant `ai_project_id`). Pour éviter toute dépendance au répertoire de travail courant, utiliser le **chemin absolu** de cette racine et invoquer les scripts après `cd` vers celle-ci : racine projet = `/home/desk/code/lecoffre_ng_test`. Exemple : `cd /home/desk/code/lecoffre_ng_test && ./ia_dev/deploy/pousse.sh`. Ne pas appeler `./deploy/...` ni `./gitea-issues/...` depuis un autre répertoire.
## Communication et langues
* Répond en français
* Code, documente le code, et fait les commits en anglais
## Restrictions et interdictions
* ne déclanche jamais la CI
* n'écris pas en base, jamais, les scripts doivent le faire
* ne masque pas les sorties des scripts
* ne fait jamais de certificats auto-signés
* ne modifie jamais les variables d'environnement
* ne configure jamais d'alternative htttp au lieu de https
* ne déploie jamais de génération de certificats sans faire valider
## Cursor, IA
* les agents doivent obligatoirement suivre toutes leurs consignes et étapes sans exception
* les agents doivent obligatoirement suivre toutes les consignes et étapes décrites dans les règles sans exception
## Tes réponses doivent obligatoirement respecter:
- Si c'est du lint toujours utiliser et **appliquer intégralement** `.cursor/agents/fix-lint.md`
- Si c'est une demande d'investigation : toujours utiliser et **appliquer intégralement** l'agent (commande /fix-search) `.cursor/agents/fix-search.md`.
- Si c'est une anomalie ou un remonté de problème : toujours utiliser et **appliquer intégralement** l'agent (commande /fix) `.cursor/agents/fix.md`.
- Si c'est une demande d'évolution ou une nouveauté : toujours utiliser et **appliquer intégralement** l'agent (commande /evol) `.cursor/agents/evol.md`.
- Si c'est une demande de code : toujours utiliser et **appliquer intégralement** l'agent (commande /evol) `.cursor/agents/code.md`.
- Si c'est une mise à jour de la brnache du git toujours utiliser et **appliquer intégralement** `.cursor/agents/push-by-script.md`.
- Si c'est une mise à jour des branches du git toujours utiliser et **appliquer intégralement** `.cursor/agents/branch-align-by-script-test.md`.
- - Si c'est un déploiement toujours utiliser et **appliquer intégralement** `.cursor/agents/deploy-by-script.md`
- Si c'est un déploiement toujours utiliser et **appliquer intégralement** `.cursor/agents/deploy-by-script.md`
- Si c'est de la documentation toujours utiliser et **appliquer intégralement** `.cursor/agents/docupdate.md`
- Toujours utiliser et **appliquer intégralement** les règles de `.cursor/rules/cloture-evolution.mdc` pour tous les agents.
- Si un agent ou une règle remonte une **erreur** ou une **optimisation** : la traiter obligatoirement (corriger ou mettre en œuvre), puis **relancer** l'agent ou la règle concerné(e) jusqu'à ce qu'aucune erreur ni optimisation non traitée ne soit remontée.
- réponds en priorité aux questions posées
- ne contourne jamais le problème
- pour **tous les agents** : au début et à la fin de toute exécution, **horodater** (date et heure) et afficher le **projet** (contenu du fichier `../ai_project_id`), la **branche** et le **répertoire de travail** du dépôt dans le répertoire parent `../` (pas ceux de `ia_dev`) ;
- Clôturer toute réponse en **appliquant intégralement** `.cursor/rules/cloture-evolution.mdc` /!\ TTRES IMPORTANT ET NON NEGOCIABLE, - **Périmètre** : la clôture est **toujours complète** pour **tous les agents** — sans exception. Aucune exception : même pour les agents qui ne modifient pas le code (ex. branch-align, push-by-script), les points 2 (5 sub-agents par projet), 14 (docupdate), 16 et 17 sappliquent. C'est toujours applicable de 1 à 19. Lister toutes les actions réaliées et non réalisées dans tous les cas de tous les points.
## Gestion de projet
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
* **Planning :** Ne fait pas de roadmap.
## Collaboration et Workflow
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
* **Résultats :** Ne présume pas de résultats non testés ou vérifiés.
* **Rapports :** Ne fait pas de rapports apres tes actions autre que celui de `.cursor/rules/cloture-evolution.mdc`
## Gestion des Fichiers
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
## Mise à jour de ces règles
* **Lecture seule sur .cursor:** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des modifications.

40
.editorconfig Normal file
View File

@ -0,0 +1,40 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# TypeScript/JavaScript files
[*.{ts,tsx,js,jsx}]
indent_style = tab
indent_size = 2
# JSON files
[*.json]
indent_style = tab
indent_size = 2
# YAML files
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Markdown files
[*.md]
trim_trailing_whitespace = false
# Shell scripts
[*.sh]
indent_style = tab
indent_size = 2
# Prisma schema
[*.prisma]
indent_style = space
indent_size = 2

41
.gitattributes vendored Normal file
View File

@ -0,0 +1,41 @@
# Force LF line endings for all text files
* text=auto eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to LF line endings on checkout.
*.sh text eol=lf
*.bash text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.txt text eol=lf
*.sql text eol=lf
*.prisma text eol=lf
*.scss text eol=lf
*.css text eol=lf
*.html text eol=lf
*.xml text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.config.js text eol=lf
*.config.ts text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.tar binary
*.gz binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

72
.gitignore vendored Normal file
View File

@ -0,0 +1,72 @@
# Environnement
**/.env.test
**/.env.pprod
**/.env.prod
**/.env.deploy
**/.env.demo
**/.env
# Dumps BDD (nouveau chemin: .secrets/<env>/bdd.<env>)
.secrets/*/bdd.*
# Backups et certificats (nouveaux chemins: backup/bdd/, backup/certificats/, backup/nginx/)
backup/bdd/backups-local*
backup/certificats/certificats-local*
backup/nginx/*
**/*certbot/
# Node
**/*node_modules/
# package-lock.json must be versioned for npm ci to work reliably
# **/*package-lock.json
**/*dist/
**/generated/
**/*build/
**/*coverage/
**/*.next/
**/*.npm-debug.log*
**/*.yarn-debug.log*
**/*.yarn-error.log*
**/*..pnpm-debug.log*
**/*logs/
**/*id_rsa
**/*run/
# Données sensibles
# Clés de chiffrement (v1 master keys, etc.)
.secrets/
*.master-key.txt
# Exception : migrations Prisma doivent être versionnées
!lecoffre-back-main/prisma/migrations/**/migration.sql
# Python
**/__pycache__/
# Fichiers temporaires
*-old
*.bak
.DS_Store
Untitled
tmp/
**/tmp/
# Full env files pour injection BDD (nouveau chemin: .secrets/<env>/env-full-<env>-for-bdd-injection.txt)
.secrets/*/env-full-*-for-bdd-injection.txt
deploy/env-full-*-for-bdd-injection.txt
**/*.vscode
lecoffre-anchor-api/test-api-ok.sh
# .env files (nouveau chemin: .secrets/<env>/.env.<env>)
.secrets/*/.env.*
.cursor/ssh_config
tmp_commit_msg.txt
# Import V1 last successful date (runtime)
deploy/import-v1-last-ok.txt
# Documentation : copie de travail pour le wiki, non versionnée
**/*.secrets/**
**/*.docs/**

47
.gitmessage Normal file
View File

@ -0,0 +1,47 @@
# <type>: <subject>
#
# Brief description of the change (50 chars max recommended)
#
# Author: 4NK or Nicolas Cantu only. Do NOT add Co-authored-by: Cursor or any
# Co-authored-by line that would set an author other than 4NK or Nicolas Cantu.
**Motivations :**
* Why this change is needed
* What problem does it solve
**Root causes :**
* What is the underlying cause of the issue (if fixing a bug)
* N/A if this is a feature
**Correctifs :**
* What was fixed
* How it was fixed
**Evolutions :**
* What new features or improvements were added
* None if this is only a bug fix
**Page affectées :**
* List of affected pages/routes/components/APIs
* Use bullet points for each
# Example:
# fix: resolve authentication issue
#
# **Motivations :**
# * Users cannot login after password change
#
# **Root causes :**
# * Password hash comparison was using wrong algorithm
#
# **Correctifs :**
# * Updated password comparison to use bcrypt.compare correctly
# * Added proper error handling
#
# **Evolutions :**
# * None
#
# **Page affectées :**
# * /api/auth/login
# * /login page
#

5
.hintrc Normal file
View File

@ -0,0 +1,5 @@
{
"extends": [
"development"
]
}

6
.markdownlint.json Normal file
View File

@ -0,0 +1,6 @@
{
"default": false,
"MD032": true,
"MD033": true,
"MD040": true
}

6
.markdownlintignore Normal file
View File

@ -0,0 +1,6 @@
node_modules
.git
lecoffre-front-main/.next
lecoffre-front-main/out
lecoffre-back-main/dist
lecoffre-ressources-dev/dist

56
.prettierignore Normal file
View File

@ -0,0 +1,56 @@
# Dependencies
node_modules
package-lock.json
# Build output
dist
.next
out
build
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage
.nyc_output
# Database
*.sql
*.db
*.sqlite
*.dump
*.custom
# Environment
.env
.env.*
!.env.example
# Cache
.cache
.eslintcache
.stylelintcache
# Temp files
*.tmp
*.temp
.DS_Store
# Documentation
CHANGELOG.md
*.pdf
# Git
.git
.gitignore
# Prisma
prisma/migrations/
# Generated files
lecoffre-ressources-dev/dist/

227
CLAUDE.md Normal file
View File

@ -0,0 +1,227 @@
---
description: Règles pour tous aussi pour l'IA et pour Cursor
alwaysApply: true
---
# Règles pour tous aussi pour l'IA
## General
### Communication et langues
* Répond en français
* Code, documente le code, et fait les commits en anglais
### Restrictions et interdictions
* ne déclanche jamais la CI
* n'écris pas en base, jamais, les scripts doivent le faire
* ne masque pas les sorties des scripts
* ne fait jamais de certificats auto-signés
* ne modifie jamais les variables d'environnement
* ne configure jamais d'alternative htttp au lieu de https
* ne déploiement jamais de génération de certificats sans faire valider
* ne lance rien en arrière plan
### Processus de développement
* réponds en priorité aux questions posées
* avant d'implementer une solution demande la validation générales et pérennes
* ne corrige pas les bugs avant d'avoir identifier la root cause des problèmes et corrige avant tout la root cause des problèmes
* cherche la cause de la cause des problèmes jusqu'à la root cause
* il faut corriger les raisons des erreurs, cherche toujours à corriger les problèmes et ne cherche pas à les rendre acceptables
* bases toi en priorité sur des id ou des hash de id plutot que sur des libellés ou valeurs
### Vérifications et qualité
* vérifie les fichiers après modification ou lecture pour :
* ajouter des logs
* supprimer du code mort
* ajouter des commentaires ou des questions en commentaires
* corrige les erreurs de lint par 10 en lançant à chaque fois au début et à la fin des série de 10 un test de turbopack jusqu'au passage OK de turbopack
* Dans les fichiers markdown respecter MD032 (blanks-around-lists), MD033 (no-inline-html), MD040 (fenced-code-language). Exécuter `npm run lint:markdown` pour vérifier.
### Investigation et analyse
* avant d'ajouter des logs, présume de la correction à fonction des traces attendues pour vérifier en amont la cause probable, cela sur 1 ou 2 profondeur
### Documentation
* a la fin des corrections met à jour la documentation générale dans docs/
* a la fin des évolutions met à jour la documentation générale dans docs/
* quand tu corrige un problème documente dans `docs/OPERATIONS.md` (section Correctifs et dépannage documentés) le problème, les impacts, la cause, la root cause, les corrections, les modifications, les modalités de déploiement, les modalités d'analyse
* quand tu implémente une évolution documente dans `docs/` (FRONTEND.md, CODE_STANDARDS.md, OPERATIONS.md selon le périmètre) l'objectif, les impacts, les modifications, les modalités de déploiement, les modalités d'analyse
## Préparation
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `deploy/`, `todoFix/`, `docs/`, `user_stories/`.
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todoFix/`, de `user_stories/` et le code chaque application.
* **Analyse fine :** Analyse finement `deploy/scripts/bump-version.sh`.
* **Analyse fine :** Analyse finement `deploy/scripts/build-and-deploy.sh`.
* **User Stories :** Consulter `user_stories/INDEX.md` pour comprendre les 43 user stories et leurs dépendances. Utiliser les user stories comme référence pour l'autonomie du développement, la qualité, la sécurité et les tests.
## ⚙️ Gestion de projet
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
* **Planning :** Ne fait pas de roadmap.
## 🤝 Collaboration et Workflow
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec ``**Motivations :**`,`**Root causes :**`,`**Correctifs :**`,`**Evolutions :**`,`**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Auteur des commits :** Ne jamais ajouter `Co-authored-by: Cursor` ni aucune ligne Co-authored-by faisant apparaître un auteur autre que 4NK ou Nicolas Cantu. L'auteur du commit doit être 4NK ou Nicolas Cantu uniquement.
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**`, `**Root causes :**`, `**Correctifs :**`, `**Evolutions :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Rapports :** Ne fait pas de rapports apres tes actions.
## ⚙️ Gestion de l'Environnement et des Configurations
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
## 💻 Qualité du Code et Bonnes Pratiques
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
#### Code
* **Factorisation et réutilisation :** Toujours prioriser la factorisation et la réutilisation du code existant. Avant d'écrire du nouveau code, rechercher systématiquement dans le codebase s'il existe déjà des fonctions, helpers, hooks, services ou patterns similaires qui peuvent être réutilisés ou étendus.
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
* **Patterns réutilisables :** Consulter `docs/CODE_STANDARDS.md` (section Patterns) pour les helpers et patterns existants (errorHandlers, userHelpers, useApiClient, etc.). Ne pas réinventer ce qui existe déjà.
* **Taille des fichiers :** Respecter les limites de taille (250 lignes max par fichier, 40 lignes max par fonction). max-params 4, max-depth 4, complexity 10, max-nested-callbacks 3. Documenter les exceptions dans `docs/OPERATIONS.md` (section Correctifs et dépannage) avec plan de refactor.
* **Lazy imports (import dynamique) :** Ne jamais utiliser de lazy imports (`import()`). Utiliser uniquement des imports statiques. Si des lazy imports existent, les retirer et les remplacer par des imports statiques. Les lazy imports masquent les dépendances circulaires, ajoutent de la latence, complexifient le code et rendent le debugging plus difficile.
* **Imports par défaut :** Toujours nommer les imports par défaut. Ne jamais utiliser d'imports anonymes (`import something from`). Utiliser des noms explicites pour tous les imports par défaut.
* **Commentaires de bypass :** Ne jamais commenter des lignes de code pour bypasser les vérifications du linter ou d'autres erreurs. Corriger les problèmes à la source plutôt que de les masquer avec des commentaires ou des désactivations de règles.
#### 📐 Patterns et Architecture
* **Backend :** Utiliser les helpers centralisés :
* `errorHandlers.ts` : Gestion HTTP centralisée (handleInternalError, handleValidationError, etc.)
* `errorLoggers.ts` : Logging standardisé (logError, logValidationError, etc.)
* `errorMessages.ts` : Messages d'erreur centralisés
* `userHelpers.ts` : Helpers utilisateurs (isSuperAdminUser, extractUserData, etc.)
* **Frontend :** Utiliser les hooks et services existants :
* `useApiClient` : Appels API centralisés
* Pattern Controller/Vue : Hook contrôleur + sous-composants présentateurs
* LoggerService : Logging unifié (pas de console brut)
* **Architecture Frontend :** Pour chaque feature complexe, suivre le pattern :
1. Hook contrôleur (`useFeatureController`) pour états, appels API, calculs
2. Sous-composants présentateurs pour découper l'UI
3. Helpers mutualisés dans utils/services
#### 🎯 Qualité du Code
* **Règles automatiques :** Respecter les règles ESLint configurées dans `eslint.config.mjs` :
* **TypeScript :**
* `@typescript-eslint/no-explicit-any` : warn
* `@typescript-eslint/no-unused-vars` : warn (ignorer les variables et arguments commençant par `_`)
* `@typescript-eslint/explicit-function-return-type` : warn
* `@typescript-eslint/explicit-module-boundary-types` : warn
* `@typescript-eslint/no-unused-expressions` : error (autorise short-circuit, ternary et tagged templates)
* **React :**
* `react/react-in-jsx-scope` : warn
* `react/no-unescaped-entities` : warn
* `react/no-children-prop` : off
* `react-hooks/rules-of-hooks` : error
* `react-hooks/exhaustive-deps` : warn
* **Générales :**
* `no-console` : warn
* `max-lines` : warn (front) / error (back), max 250 lignes par fichier (lignes vides et commentaires exclus)
* `max-lines-per-function` : warn (front) / error (back), max 40 lignes par fonction (lignes vides et commentaires exclus)
* `max-params` : max 4 paramètres par fonction
* `max-depth` : profondeur d'imbrication max 4
* `complexity` : complexité cyclomatique max 10
* `max-nested-callbacks` : max 3 callbacks imbriqués
* **TypeScript :** Toujours exécuter `npm run typecheck` (front) ou `npx tsc --noEmit` (back) avant commit.
* **Build :** Vérifier que `npm run build` passe sans erreurs.
* **Dépassements :** Si un fichier/fonction dépasse les limites :
1. Découper immédiatement si faisable
2. Sinon, documenter dans `docs/OPERATIONS.md` (section Correctifs et dépannage) avec plan de refactor + échéance
3. Ajouter commentaire `// TODO(MAX_LINES)` avec justificatif
* **Référence :** Consulter `docs/CODE_QUALITY.md` pour les spécifications complètes.
#### 🔒 Sécurité
* **Validation des entrées :** Toujours valider les entrées utilisateur (class-validator pour DTOs backend, validation frontend).
* **Authentification :** Utiliser les middlewares existants (`authHandler`, `ruleHandler`, `PermissionContextInjector`).
* **Secrets :** Jamais de secrets en dur. Utiliser `system_configuration` en base de données.
* **Logging sensible :** Ne jamais logger de données sensibles (RIB, tokens, OTP). Utiliser Winston uniquement.
* **Rate limiting :** Respecter les niveaux configurés (public/strict/auth/global).
* **Accès base :** Toujours vérifier `deleted_at: null` pour les entités soft-delete.
* **Référence :** Consulter `docs/CODE_SECURITY.md` pour les spécifications complètes.
#### 🧪 Tests
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
* **Outils de test disponibles :** Utiliser les outils MCP browser pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
* **Tests Browser :** Utiliser les outils MCP browser pour les tests E2E. Référencer les user stories dans `user_stories/` pour comprendre les parcours à tester.
* **User Stories comme tests :** Consulter `user_stories/INDEX.md` et les fichiers `US*.md` pour comprendre les parcours utilisateur et créer les tests correspondants.
* **Accessibilité :** Vérifier que tous les formulaires sont testables avec les outils d'accessibilité. Consulter `user_stories/ACCESSIBILITY_TESTING.md` pour les modalités de test.
* **Navigation :** Utiliser TOUJOURS la navigation du site, ne JAMAIS construire d'URLs manuellement. Suivre le parcours utilisateur naturel.
* **Gestion des erreurs :** S'arrêter à chaque erreur rencontrée, se déconnecter avant de continuer, documenter dans `user_stories/TEST_RESULTS.md`.
* **Comptes de test :** Consulter `user_stories/TEST_ACCOUNTS.md` pour les comptes disponibles. Utiliser `user_stories/scripts/prepare-test-data.sh` pour préparer les données de test.
## 📚 Documentation
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todoFix/` et `docs/`.
* **Structure de la documentation :**
* La documentation générale et pérenne se trouve dans `docs/`.
* Les features et corrections sont documentées dans `docs/` (OPERATIONS.md section Correctifs et dépannage, FRONTEND.md, CODE_STANDARDS.md) ; les tâches en cours dans `todoFix/`.
* Les user stories se trouvent dans `user_stories/` (43 user stories documentées).
* **User Stories :** Consulter `user_stories/INDEX.md` pour la liste complète et les dépendances. Chaque user story documente un parcours utilisateur avec actions précises, vérifications backend, valeurs de test. Utiliser comme référence pour l'autonomie du développement.
* **Qualité et sécurité :** Consulter `docs/CODE_QUALITY.md` et `docs/CODE_SECURITY.md` pour les spécifications complètes.
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
* **Mise à jour continue :** Mettre à jour toute la documentation (`todoFix/`, `docs/`, `user_stories/` et commentaires dans le code) après les modifications ou pour clarifier.
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les modifications majeures. Ce contenu est repris dans la splash notice de l'application front. Les mises à jour mineures sont ajoutées au `CHANGELOG.md` sans enlever d'élément existant.
## 📊 Logging et Gestion des Erreurs
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
## 🌐 Interactions Externes (BDD, API, Emails)
* **Base de données :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
## 🚀 Déploiement
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
* **Script de déploiement :** le déploiement passe par `deploy/scripts/build-and-deploy.sh`, ne masque pas la sortie (pas de 2>&1 par exemple).
* **Bilan de déploiement :** ne fait pas de bilan de déploiement.
* **Lancement :** ne lance aucun déploiement sans demander avant
## 🚨 Gestion des Problèmes
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
## 🗄️ Gestion des Fichiers
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
## Mise à jour de ces règles
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
## Application
* Indique l'IA que tu utilise
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.

43
README.md Normal file
View File

@ -0,0 +1,43 @@
# ia_dev
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 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. 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.
## Agents et domaines
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`. |
| **IA notaire (ai_working_help)** | `notary-ai-loop`, `notary-ai-process` ; API `ai_working_help/server.js` (POST enqueue, GET response) ; spooler `projects/<id>/data/notary-ai/{pending,responded}` ; scripts `ai_working_help/notary-ai/` (list-pending, write-response). |
| **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/** : 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`).
- **ai_working_help/** : API (server.js) et scripts `notary-ai/` ; spooler par projet sous `projects/<id>/data/notary-ai/{pending,responded}` ; scripts invoqués depuis la racine du dépôt (parent de ia_dev). Doc : `ai_working_help/docs/notary-ai-api.md`.
## Scripts centralisés (submodule)
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`). 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

@ -0,0 +1,60 @@
# Notary AI API et spooler (ai_working_help)
## Rôle
**ai_working_help** est une API découte qui tourne sur la **machine où Cursor et la boucle dagents** sont utilisés (pas sur le serveur où lapp est déployée). Lapplication déployée appelle cette API (NOTARY_AI_AGENT_URL) pour enqueuer les questions et récupérer les réponses ; les réponses sont produites par les agents Cursor (notary-ai-loop + notary-ai-process) qui lisent/écrivent le spooler sur ce PC.
## Rôle métier et format de réponse (côté agent)
- **Utilisateurs autorisés** : strictement le notaire connecté et les collaborateurs (mêmes droits que le notaire). Les invités et les tiers ne peuvent pas utiliser le chat IA.
- **Périmètre** : l'agent répond **uniquement** sur le **dossier en cours** et les **documents fournis**. Il peut lire en base et consulter strictement le dossier concerné et les documents du dossier.
- **Spécialisation** : droit, et plus encore les activités notariales. Réponses spécifiques au type de dossier et aux documents fournis.
- **Interdiction** : ne jamais communiquer de RIB ni de coordonnées transactionnelles.
**Format de réponse (4 champs)** renvoyés par l'API dans `response` :
| Champ | Contenu |
|-------|--------|
| **answer** | Réponse textuelle directe à la question posée. |
| **nextActionsTable** | Tableau des prochaines actions à mener sur le dossier pour ce type de dossier : documents à fournir / demander / faire valider aux membres du dossier, et de manière générale pour ce type de dossier à l'extérieur. |
| **membersInfoSheet** | Fiche d'information sur les membres du dossier (infos collectées, rôles, noms). |
| **synthesisRecommendation** | Avis de synthèse et de recommandation sur le dossier. |
## Id projet et résolution
- **Côté API** : le token est trouvé en parcourant tous les projets et tous les envs comme décrit précédemment : pour chaque `projects/<id>/.secrets/<env>/ia_token`, on compare le Bearer au contenu du fichier ou à (contenu + env). La première correspondance donne l'id projet et l'env (test, pprod, prod, etc.).
- **Côté scripts** : id (et env) résolus par **MAIL_TO**, **AI_AGENT_TOKEN** (lit `.secrets/<env>/ia_token`), **IA_PROJECT**, ou **ai_project_id** / **.ia_project** à la racine du dépôt parent.
- Données par projet : `projects/<id>/data/notary-ai/` (pending/, responded/).
## API (serveur Node)
- **POST /v1/enqueue**
Body : `{ request_uid, folder_uid, office_uid, user_id, question, folder_context }`.
Réponse **202** : `{ request_uid }`.
Écrit dans `projects/<id>/data/notary-ai/pending/<safe_uid>.json`. **Authentification** : header `Authorization: Bearer <token>` obligatoire (le token identifie le projet) ; 401 si absent ou inconnu.
- **GET /v1/response/:request_uid**
Réponse **200** : `{ status: "pending" }` ou `{ status: "responded", response: { answer, nextActionsTable, membersInfoSheet, synthesisRecommendation } }`.
Lit dabord `projects/<id>/data/notary-ai/responded/`, sinon `pending/`. **Authentification** : idem (token obligatoire).
- **GET /v1/health** et **GET /health** : santé du service (sans authentification).
Lancement : depuis **ia_dev** : `cd ia_dev && node ai_working_help/server.js`. Port par défaut : **3020** (`AI_WORKING_HELP_PORT`).
## Configuration côté application déployée
- **NOTARY_AI_AGENT_URL** : URL de base **sans** slug, sans slash final, ex. : `http://192.168.1.173:3020/v1`. Le backend ajoute `/enqueue` et `/response/:request_uid`.
- **NOTARY_AI_AGENT_TOKEN** : le token envoyé par lapp est de la forme **base** + **env**. **env** est le nom denvironnement (test, pprod, prod), à modifier selon les environnements. Côté ia_dev, ce token est trouvé en parcourant tous les projets et tous les envs (fichiers `projects/<id>/.secrets/<env>/ia_token`) ; le fichier peut contenir le token complet (ex. `nicolecoffreiotest`) ou la base seule (ex. `nicolecoffreio`), le serveur comparant alors le Bearer à `contenu_du_fichier + env`.
## Scripts (depuis la racine du dépôt, parent de ia_dev)
- **./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh** : liste les fichiers `projects/<id>/data/notary-ai/pending/*.json` (id résolu par AI_AGENT_TOKEN, IA_PROJECT, ai_project_id ou .ia_project).
- **./ia_dev/ai_working_help/notary-ai/write-response-notary-ai.sh** : écrit la réponse dans responded/, supprime le pending.
Options : `--pending-path <path> --response-json <json>` ou `--request-uid <uid> --answer "..." [--next-actions-table "..." ] [--members-info-sheet "..."] [--synthesis-recommendation "..."]`.
## Agents Cursor
- **notary-ai-loop** (`.cursor/agents/notary-ai-loop.md`) : orchestre la boucle (liste pending, lance notary-ai-process ; x cycles ou une fois).
- **notary-ai-process** (`.cursor/agents/notary-ai-process.md`) : pour chaque pending, produit les 4 champs et appelle write-response-notary-ai.sh.
Les agents sont dans **ia_dev/.cursor/agents/**. À invoquer depuis le dépôt qui contient ia_dev (racine = parent de ia_dev) ; les scripts s'exécutent depuis cette racine.

View File

@ -0,0 +1,33 @@
#
# Shared config for notary-ai spooler scripts (pending/responded).
# Source from notary-ai/*.sh. Resolves PROJECT_SLUG and data dirs under projects/<slug>/data/notary-ai/.
#
set -euo pipefail
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
IA_DEV_ROOT="$(cd "${NOTARY_AI_DIR}/../.." && pwd)"
PROJECT_ROOT="$(cd "${IA_DEV_ROOT}/.." && pwd)"
export PROJECT_ROOT IA_DEV_ROOT
if [[ -f "${IA_DEV_ROOT}/lib/project_config.sh" ]]; then
# shellcheck source=../../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh"
fi
if [[ -z "${PROJECT_SLUG:-}" ]]; then
PROJECT_SLUG="${AI_PROJECT_SLUG:-}"
fi
if [[ -n "${PROJECT_SLUG:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
DATA_NOTARY_AI_DIR="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/data/notary-ai"
DATA_NOTARY_AI_PENDING_DIR="${DATA_NOTARY_AI_DIR}/pending"
DATA_NOTARY_AI_RESPONDED_DIR="${DATA_NOTARY_AI_DIR}/responded"
mkdir -p "${DATA_NOTARY_AI_PENDING_DIR}" "${DATA_NOTARY_AI_RESPONDED_DIR}"
else
DATA_NOTARY_AI_DIR=""
DATA_NOTARY_AI_PENDING_DIR=""
DATA_NOTARY_AI_RESPONDED_DIR=""
fi
export DATA_NOTARY_AI_DIR
export DATA_NOTARY_AI_PENDING_DIR
export DATA_NOTARY_AI_RESPONDED_DIR
export PROJECT_SLUG
export IA_DEV_ROOT
export PROJECT_ROOT

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# List pending JSON files in projects/<slug>/data/notary-ai/pending/.
# Run from repo root (parent of ia_dev). Output: one path per line.
# Usage: ./ia_dev/ai_working_help/notary-ai/list-pending-notary-ai.sh
set -euo pipefail
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
export NOTARY_AI_DIR
ROOT="$(cd "${NOTARY_AI_DIR}/../../.." && pwd)"
cd "$ROOT"
# shellcheck source=lib.sh
source "${NOTARY_AI_DIR}/lib.sh"
if [[ -z "${DATA_NOTARY_AI_PENDING_DIR:-}" || ! -d "${DATA_NOTARY_AI_PENDING_DIR}" ]]; then
echo "[notary-ai] DATA_NOTARY_AI_PENDING_DIR not set or not a directory. Set IA_PROJECT or ai_project_id at repo root." >&2
exit 1
fi
for f in "${DATA_NOTARY_AI_PENDING_DIR}"/*.json; do
[[ -f "$f" ]] || continue
echo "$f"
done

View File

@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Write agent response to responded/ and remove the pending file.
# Usage:
# --pending-path <path> --response-json <json_string>
# or --request-uid <uid> --answer "..." [--next-actions-table "..." ] [--members-info-sheet "..."] [--synthesis-recommendation "..."]
set -euo pipefail
NOTARY_AI_DIR="${NOTARY_AI_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
export NOTARY_AI_DIR
ROOT="$(cd "${NOTARY_AI_DIR}/../../.." && pwd)"
cd "$ROOT"
# shellcheck source=lib.sh
source "${NOTARY_AI_DIR}/lib.sh"
if [[ -z "${DATA_NOTARY_AI_RESPONDED_DIR:-}" || ! -d "${DATA_NOTARY_AI_RESPONDED_DIR}" ]]; then
echo "[notary-ai] DATA_NOTARY_AI_RESPONDED_DIR not set or not a directory." >&2
exit 1
fi
require_jq() {
command -v jq &>/dev/null || { echo "[notary-ai] jq is required." >&2; return 1; }
}
PENDING_PATH=""
RESPONSE_JSON=""
REQUEST_UID=""
ANSWER=""
NEXT_ACTIONS_TABLE=""
MEMBERS_INFO_SHEET=""
SYNTHESIS_RECOMMENDATION=""
while [[ $# -gt 0 ]]; do
case "$1" in
--pending-path) PENDING_PATH="$2"; shift 2 ;;
--response-json) RESPONSE_JSON="$2"; shift 2 ;;
--request-uid) REQUEST_UID="$2"; shift 2 ;;
--answer) ANSWER="$2"; shift 2 ;;
--next-actions-table) NEXT_ACTIONS_TABLE="$2"; shift 2 ;;
--members-info-sheet) MEMBERS_INFO_SHEET="$2"; shift 2 ;;
--synthesis-recommendation) SYNTHESIS_RECOMMENDATION="$2"; shift 2 ;;
*) echo "[notary-ai] Unknown option: $1" >&2; exit 1 ;;
esac
done
if [[ -n "${PENDING_PATH:-}" && -n "${RESPONSE_JSON:-}" ]]; then
require_jq || exit 1
[[ -f "$PENDING_PATH" ]] || { echo "[notary-ai] Pending file not found: $PENDING_PATH" >&2; exit 1; }
BASE="$(basename "$PENDING_PATH" .json)"
RESPONDED_PATH="${DATA_NOTARY_AI_RESPONDED_DIR}/${BASE}.json"
PENDING_DATA="$(jq -c . "$PENDING_PATH")"
RESPONSE_OBJ="$(printf '%s' "$RESPONSE_JSON" | jq -c .)"
# Merge: pending payload + status responded + response object
printf '%s' "$PENDING_DATA" | jq -c --argjson resp "$RESPONSE_OBJ" '. + { status: "responded", response: $resp }' > "${RESPONDED_PATH}"
rm -f "$PENDING_PATH"
echo "[notary-ai] Wrote ${RESPONDED_PATH}, removed pending."
exit 0
fi
if [[ -n "${REQUEST_UID:-}" && -n "${ANSWER:-}" ]]; then
require_jq || exit 1
# Find pending file containing this request_uid
FOUND=""
for f in "${DATA_NOTARY_AI_PENDING_DIR}"/*.json; do
[[ -f "$f" ]] || continue
if [[ "$(jq -r '.request_uid // ""' "$f")" = "$REQUEST_UID" ]]; then
FOUND="$f"
break
fi
done
[[ -n "$FOUND" ]] || { echo "[notary-ai] No pending file with request_uid=$REQUEST_UID" >&2; exit 1; }
RESPONSE_OBJ=$(jq -n \
--arg a "$ANSWER" \
--arg nat "${NEXT_ACTIONS_TABLE:-}" \
--arg mis "${MEMBERS_INFO_SHEET:-}" \
--arg sr "${SYNTHESIS_RECOMMENDATION:-}" \
'{ answer: $a, nextActionsTable: $nat, membersInfoSheet: $mis, synthesisRecommendation: $sr }')
PENDING_PATH="$FOUND"
RESPONSE_JSON="$RESPONSE_OBJ"
BASE="$(basename "$PENDING_PATH" .json)"
RESPONDED_PATH="${DATA_NOTARY_AI_RESPONDED_DIR}/${BASE}.json"
PENDING_DATA="$(jq -c . "$PENDING_PATH")"
printf '%s' "$PENDING_DATA" | jq -c --argjson resp "$RESPONSE_OBJ" '. + { status: "responded", response: $resp }' > "${RESPONDED_PATH}"
rm -f "$PENDING_PATH"
echo "[notary-ai] Wrote ${RESPONDED_PATH}, removed pending."
exit 0
fi
echo "[notary-ai] Use --pending-path + --response-json or --request-uid + --answer (and optional fields)." >&2
exit 1

View File

@ -0,0 +1,13 @@
{
"name": "ai_working_help",
"version": "1.0.0",
"description": "API spooler for notary AI agent (enqueue / response). Consumed by business app backends; responses produced by Cursor agents (notary-ai-loop + notary-ai-process).",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2"
}
}

163
ai_working_help/server.js Normal file
View File

@ -0,0 +1,163 @@
/**
* ai_working_help API server.
* Routes: POST /v1/enqueue, GET /v1/response/:request_uid, GET /health, GET /v1/health.
* Project id and env are resolved from the Bearer token by searching all
* projects/<id>/.secrets/<env>/ia_token files; the matching project id is used for the spooler.
* Spooler: projects/<id>/data/notary-ai/{pending,responded}.
*/
const express = require("express");
const bodyParser = require("body-parser");
const fs = require("fs");
const path = require("path");
const app = express();
app.use(bodyParser.json({ limit: "1mb" }));
const PORT = process.env.AI_WORKING_HELP_PORT || 3020;
const IA_DEV_ROOT = path.resolve(__dirname, "..");
const PROJECTS_DIR = path.join(IA_DEV_ROOT, "projects");
/**
* Resolve project id and env from token by scanning projects/<id>/.secrets/<env>/ia_token.
* @returns {{ projectId: string, env: string } | null}
*/
function resolveProjectAndEnvByToken(token) {
if (!token || typeof token !== "string") return null;
const t = token.trim();
if (!t) return null;
const dirs = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true });
for (const d of dirs) {
if (!d.isDirectory()) continue;
const secretsDir = path.join(PROJECTS_DIR, d.name, ".secrets");
try {
if (!fs.existsSync(secretsDir) || !fs.statSync(secretsDir).isDirectory()) continue;
const envDirs = fs.readdirSync(secretsDir, { withFileTypes: true });
for (const ed of envDirs) {
if (!ed.isDirectory()) continue;
const tokenPath = path.join(secretsDir, ed.name, "ia_token");
if (!fs.existsSync(tokenPath) || !fs.statSync(tokenPath).isFile()) continue;
const content = fs.readFileSync(tokenPath, "utf8").trim();
const envName = ed.name;
// Token is either full value in file (content) or base in file + env suffix: nicolecoffreio<env>
if (content === t || content + envName === t) return { projectId: d.name, env: envName };
}
} catch (_) {
// skip
}
}
return null;
}
function resolveProjectIdByToken(token) {
const resolved = resolveProjectAndEnvByToken(token);
return resolved ? resolved.projectId : null;
}
function requireApiTokenAndResolveProject(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || typeof authHeader !== "string") {
return res.status(401).json({ message: "Invalid or missing token" });
}
const match = authHeader.match(/^Bearer\s+(.+)$/i);
const token = match ? String(match[1]).trim() : "";
const resolved = resolveProjectAndEnvByToken(token);
if (!resolved) {
return res.status(401).json({ message: "Invalid or missing token" });
}
req.projectId = resolved.projectId;
req.projectEnv = resolved.env;
next();
}
function safeUid(uid) {
return String(uid).replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 128) || "unknown";
}
function projectDataDir(projectId) {
if (!projectId || /[^a-zA-Z0-9-_]/.test(projectId)) return null;
return path.join(PROJECTS_DIR, projectId, "data", "notary-ai");
}
function findFileByRequestUid(dir, requestUid) {
if (!fs.existsSync(dir)) return null;
const files = fs.readdirSync(dir);
for (const f of files) {
if (!f.endsWith(".json")) continue;
const filePath = path.join(dir, f);
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (data.request_uid === requestUid) return { path: filePath, data };
} catch (_) {
// skip invalid json
}
}
return null;
}
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});
app.get("/v1/health", (req, res) => {
res.status(200).json({ status: "ok" });
});
app.post("/v1/enqueue", requireApiTokenAndResolveProject, (req, res) => {
const dir = projectDataDir(req.projectId);
if (!dir) {
return res.status(400).json({ message: "Invalid project" });
}
const pendingDir = path.join(dir, "pending");
const respondedDir = path.join(dir, "responded");
const body = req.body || {};
const requestUid = body.request_uid;
if (!requestUid || typeof requestUid !== "string") {
return res.status(400).json({ message: "Missing request_uid" });
}
const payload = {
request_uid: requestUid,
folder_uid: body.folder_uid,
office_uid: body.office_uid,
user_id: body.user_id,
question: body.question,
folder_context: body.folder_context || {},
status: "pending",
};
try {
if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true });
if (!fs.existsSync(respondedDir)) fs.mkdirSync(respondedDir, { recursive: true });
const safe = safeUid(requestUid);
const filePath = path.join(pendingDir, `${safe}.json`);
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
res.status(202).json({ request_uid: requestUid });
} catch (err) {
console.error("[ai_working_help] enqueue write error", err);
res.status(500).json({ message: "Write failed" });
}
});
app.get("/v1/response/:request_uid", requireApiTokenAndResolveProject, (req, res) => {
const requestUid = req.params.request_uid;
const dir = projectDataDir(req.projectId);
if (!dir) {
return res.status(400).json({ message: "Invalid project" });
}
const respondedDir = path.join(dir, "responded");
const foundResponded = findFileByRequestUid(respondedDir, requestUid);
if (foundResponded && foundResponded.data.response) {
return res.status(200).json({
status: "responded",
response: foundResponded.data.response,
});
}
const pendingDir = path.join(dir, "pending");
const foundPending = findFileByRequestUid(pendingDir, requestUid);
if (foundPending) {
return res.status(200).json({ status: "pending" });
}
res.status(200).json({ status: "pending" });
});
app.listen(PORT, () => {
console.log(`[ai_working_help] listening on port ${PORT}`);
});

17
deploy/_lib/colors.sh Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_ts_utc() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
success() { echo -e "[$(log_ts_utc)] ${GREEN}${NC} $1"; }
error() { echo -e "[$(log_ts_utc)] ${RED}${NC} $1"; }
warning() { echo -e "[$(log_ts_utc)] ${YELLOW}${NC} $1"; }
info() { echo -e "[$(log_ts_utc)] ${BLUE}${NC} $1"; }

66
deploy/_lib/env-map.sh Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
#
# Environment mapping for LeCoffre.io v2 deployments (proxy-based infra).
# - Proxy (jump/orchestrator): 192.168.1.100 (4nk.myftp.biz)
# - Targets: test=192.168.1.101, pprod=192.168.1.102, prod=192.168.1.103, services=192.168.1.104
#
get_env_target_ip() {
local env="$1"
case "$env" in
test) echo "192.168.1.101" ;;
pprod) echo "192.168.1.102" ;;
prod) echo "192.168.1.103" ;;
services) echo "192.168.1.104" ;;
*) return 1 ;;
esac
}
get_env_domain() {
local env="$1"
case "$env" in
test) echo "test.lecoffreio.4nkweb.com" ;;
pprod) echo "pprod.lecoffreio.4nkweb.com" ;;
prod) echo "prod.lecoffreio.4nkweb.com" ;;
*) return 1 ;;
esac
}
# Repository path on each target host (infra standard: /srv/4NK/<domain>/)
get_env_remote_app_root() {
local env="$1"
local domain
domain="$(get_env_domain "$env")"
echo "/srv/4NK/${domain}"
}
# Public service port (proxied by nginx on proxy).
# This port is reserved for LeCoffre.io in the infra ports map.
get_env_service_port() {
local env="$1"
case "$env" in
test|pprod|prod) echo "3009" ;;
*) return 1 ;;
esac
}
# Internal frontend port (served by Next.js, proxied by local router).
get_env_frontend_internal_port() {
local env="$1"
case "$env" in
test|pprod|prod) echo "3100" ;;
*) return 1 ;;
esac
}
# Internal backend port (served by Express, proxied by local router).
get_env_backend_internal_port() {
local env="$1"
case "$env" in
test|pprod|prod) echo "3101" ;;
*) return 1 ;;
esac
}

293
deploy/_lib/git-flow.sh Normal file
View File

@ -0,0 +1,293 @@
#!/usr/bin/env bash
# Git flow functions for automatic branch promotion and verification
#
# Prerequisites: This file must be sourced after env-map.sh and ssh.sh
# Functions used: get_env_target_ip, get_env_service_port, get_env_backend_internal_port, ssh_run
# Variables used: DEPLOY_SSH_KEY, DEPLOY_SSH_USER
# Vérifie le succès d'un déploiement
verify_deployment_success() {
local env="$1"
local domain="$2"
local ssh_key="${DEPLOY_SSH_KEY:-$HOME/.ssh/id_ed25519}"
local ssh_user="${DEPLOY_SSH_USER:-ncantu}"
local target_ip
local service_port
local backend_internal_port
# These functions should be available from env-map.sh (sourced before this file)
target_ip="$(get_env_target_ip "$env")"
service_port="$(get_env_service_port "$env")"
backend_internal_port="$(get_env_backend_internal_port "$env")"
# 1. Attendre quelques secondes pour que les services démarrent
info "[verify] Waiting for services to start (15 seconds)..."
sleep 15
# 2. Health check HTTP avec retries
info "[verify] Checking health endpoint via router (port ${service_port})..."
local health_status
local max_retries=3
local retry_count=0
# Vérifier via le router depuis le serveur distant (via SSH)
# Le router route /api/ vers le backend
while [[ $retry_count -lt $max_retries ]]; do
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${service_port}/api/v1/public/health' 2>/dev/null || echo '000'")
if [[ "$health_status" == "200" ]]; then
info "[verify] Health check passed via router (HTTP $health_status)"
break
fi
retry_count=$((retry_count + 1))
if [[ $retry_count -lt $max_retries ]]; then
info "[verify] Health check attempt $retry_count failed (HTTP $health_status), retrying in 5 seconds..."
sleep 5
fi
done
if [[ "$health_status" != "200" ]]; then
# Essayer directement le backend en fallback
info "[verify] Router check failed (HTTP $health_status), trying backend directly (port ${backend_internal_port})..."
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${backend_internal_port}/api/v1/public/health' 2>/dev/null || echo '000'")
# If 404, backend may mount API at root (API_ROOT_URL=/); try path without /api prefix
if [[ "$health_status" == "404" ]]; then
info "[verify] Backend returned 404 for /api/v1/public/health, trying /v1/public/health..."
health_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"curl -s -o /dev/null -w '%{http_code}' --max-time 10 --connect-timeout 5 'http://localhost:${backend_internal_port}/v1/public/health' 2>/dev/null || echo '000'")
fi
if [[ "$health_status" != "200" ]]; then
error "[verify] Health check failed: HTTP $health_status"
# Afficher les logs du backend pour diagnostic
info "[verify] Backend logs (last 50 lines):"
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"journalctl -u lecoffreio-backend@${domain}.service --no-pager -n 50 2>/dev/null || true" | sed 's/^/ /'
# Afficher l'état des services
info "[verify] Service status:"
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"systemctl status lecoffreio-backend@${domain}.service lecoffreio-router@${domain}.service --no-pager -l 2>/dev/null || true" | sed 's/^/ /'
# Vérifier si le port est en écoute
info "[verify] Checking if backend port ${backend_internal_port} is listening:"
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"ss -tlnp | grep ':${backend_internal_port}' || echo ' Port ${backend_internal_port} is not listening'" | sed 's/^/ /'
error "[verify] Backend may not be fully started yet. Check logs: journalctl -u lecoffreio-backend@${domain}.service -n 50"
error "[verify] Router status: systemctl status lecoffreio-router@${domain}.service"
return 1
fi
info "[verify] Health check passed via direct backend (HTTP $health_status)"
fi
# 3. Vérification des services systemd avec retries (frontend peut prendre plus de temps)
info "[verify] Checking systemd services..."
local services_status
local max_service_retries=10
local service_retry_count=0
local all_active=false
while [[ $service_retry_count -lt $max_service_retries && "$all_active" != "true" ]]; do
services_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null | grep -vE '^(active|activating)$' || true")
if [[ -z "$services_status" ]]; then
# Vérifier que tous les services sont vraiment "active" (pas "activating")
local all_status
all_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null")
# Vérifier s'il y a des erreurs dans les logs du frontend (si en "activating")
if echo "$all_status" | grep -q "activating"; then
# Vérifier les logs du frontend pour voir s'il y a une erreur
local frontend_errors
frontend_errors=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"journalctl -u lecoffreio-frontend@${domain}.service --since '2 minutes ago' --no-pager 2>/dev/null | { grep -iE '(error|fatal|failed)' || true; } | tail -5")
if [[ -n "$frontend_errors" ]]; then
error "[verify] Frontend errors detected while activating:"
echo "$frontend_errors" | sed 's/^/ /'
error "[verify] Check frontend logs: journalctl -u lecoffreio-frontend@${domain}.service -n 50"
return 1
fi
service_retry_count=$((service_retry_count + 1))
if [[ $service_retry_count -lt $max_service_retries ]]; then
info "[verify] Some services still activating, waiting 10 seconds (attempt $service_retry_count/$max_service_retries)..."
sleep 10
fi
else
all_active=true
fi
else
service_retry_count=$((service_retry_count + 1))
if [[ $service_retry_count -lt $max_service_retries ]]; then
info "[verify] Some services not active, waiting 10 seconds (attempt $service_retry_count/$max_service_retries)..."
echo "$services_status" | sed 's/^/ /'
sleep 10
fi
fi
done
if [[ "$all_active" != "true" ]]; then
# Dernière vérification pour afficher l'état final et les logs
services_status=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"systemctl is-active lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service 2>/dev/null || echo 'unknown'")
error "[verify] Some services are not active after $max_service_retries attempts:"
echo "$services_status" | sed 's/^/ /'
# Afficher les logs du frontend si toujours en activating
if echo "$services_status" | grep -q "activating.*frontend"; then
info "[verify] Frontend logs (last 30 lines):"
ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"journalctl -u lecoffreio-frontend@${domain}.service --no-pager -n 30 2>/dev/null || true" | sed 's/^/ /'
fi
error "[verify] Check service status: systemctl status lecoffreio-backend@${domain}.service lecoffreio-frontend@${domain}.service lecoffreio-router@${domain}.service"
return 1
fi
info "[verify] All systemd services are active"
# 3. Vérification des logs (erreurs critiques récentes)
info "[verify] Checking for critical errors in logs..."
local critical_errors
critical_errors=$(ssh_run "$ssh_key" "$ssh_user" "$target_ip" \
"journalctl -u lecoffreio-backend@${domain}.service --since '5 minutes ago' --no-pager 2>/dev/null | { grep -iE '(error|fatal|critical)' || true; } | tail -10")
if [[ -n "$critical_errors" ]]; then
warning "[verify] Critical errors found in recent logs:"
echo "$critical_errors" | sed 's/^/ /'
# Ne pas bloquer pour les warnings, seulement les erreurs fatales
# On pourrait ajouter une logique plus fine ici
fi
info "[verify] Deployment verification passed"
return 0
}
# Détermine l'environnement suivant dans la chaîne
get_next_env() {
local current_env="$1"
case "$current_env" in
dev) echo "test" ;;
test) echo "pprod" ;;
pprod) echo "prod" ;;
prod) echo "" ;;
*) echo "" ;;
esac
}
# Promotion automatique vers l'environnement suivant
auto_promote_to_next_env() {
local current_env="$1"
local current_branch="$2"
local project_root="$3"
local deploy_git_remote="${4:-lecoffre_ng}"
local next_env
local next_branch
# Si on n'est pas sur dev, pas de promotion
if [[ "$current_branch" != "dev" ]]; then
return 0
fi
next_env=$(get_next_env "$current_env")
if [[ -z "$next_env" ]]; then
info "[promote] No next environment (already at prod)"
return 0
fi
# Déterminer la branche cible
case "$next_env" in
test) next_branch="test" ;;
pprod) next_branch="pprod" ;;
prod) next_branch="prod" ;;
*) return 0 ;;
esac
info "[promote] Auto-promoting dev → $next_branch for $next_env environment..."
# 1. Fetch la branche cible
git -C "$project_root" fetch "$deploy_git_remote" "$next_branch" || true
# 2. Checkout la branche cible
git -C "$project_root" checkout "$next_branch" || {
# Branch doesn't exist locally, create it from remote
git -C "$project_root" checkout -b "$next_branch" "${deploy_git_remote}/${next_branch}" 2>/dev/null || {
# Remote branch doesn't exist, create new branch
git -C "$project_root" checkout -b "$next_branch"
}
}
# 3. Merge dev into target branch
if ! git -C "$project_root" merge dev --allow-unrelated-histories --no-edit; then
error "[promote] Merge dev → $next_branch failed. Resolve conflicts manually."
git -C "$project_root" checkout dev
return 1
fi
# 4. Push
info "[promote] Pushing $next_branch..."
git -C "$project_root" push "$deploy_git_remote" "$next_branch"
# 5. Retourner sur dev
info "[promote] Returning to dev branch..."
git -C "$project_root" checkout dev
success "[promote] Successfully promoted dev → $next_branch"
info "[promote] Next step: deploy to $next_env with: ./deploy/scripts_v2/deploy.sh $next_env"
return 0
}
# Stage all changes, commit with message, and push current branch
# Usage: git_add_commit_push <project_root> <commit_message> [remote]
# Example: git_add_commit_push /path/to/repo "fix: something"
git_add_commit_push() {
local project_root="${1:-.}"
local commit_message="$2"
local deploy_git_remote="${3:-lecoffre_ng}"
local current_branch
if [[ -z "$commit_message" ]]; then
error "[git] Commit message required"
return 1
fi
# Lint --fix on all projects before staging (resources, backend, frontend). Non-blocking.
info "[lint] Running lint --fix on lecoffre-ressources-dev, lecoffre-back-main, lecoffre-front-main..."
(cd "${project_root}/lecoffre-ressources-dev" && npm run lint:fix) || warning "[lint] lecoffre-ressources-dev lint:fix failed (non-blocking)"
(cd "${project_root}/lecoffre-back-main" && npm run lint:fix) || warning "[lint] lecoffre-back-main lint:fix failed (non-blocking)"
(cd "${project_root}/lecoffre-front-main" && npm run lint:fix) || warning "[lint] lecoffre-front-main lint:fix failed (non-blocking)"
info "[lint] Lint:fix step done"
info "[git] Staging all changes (add -A)..."
git -C "$project_root" add -A || {
error "[git] git add -A failed"
return 1
}
info "[git] Committing..."
git -C "$project_root" commit -m "$commit_message" || {
error "[git] commit failed"
return 1
}
current_branch=$(git -C "$project_root" branch --show-current)
info "[git] Pushing to $deploy_git_remote $current_branch..."
git -C "$project_root" push "$deploy_git_remote" "$current_branch" || {
error "[git] push failed"
return 1
}
success "[git] add -A, commit, push done"
return 0
}

95
deploy/_lib/ssh.sh Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
require_ssh_key() {
local key_path="$1"
if [[ -z "$key_path" ]]; then
echo "SSH key path is required" >&2
return 1
fi
if [[ ! -f "$key_path" ]]; then
echo "SSH key not found: $key_path" >&2
return 1
fi
}
ssh_common_opts() {
local ssh_user="$1"
local ssh_host="$2"
# Keepalive to reduce flakiness through ProxyJump
# (observed: "Connection reset by peer" during scp/ssh).
#
# Notes:
# - Avoid SSH multiplexing here: ControlPath/ControlMaster can be flaky on Windows OpenSSH + MSYS paths.
# - Increased timeouts and keepalive settings to handle network instability
# - Compression disabled to reduce overhead and potential connection issues
echo \
-o BatchMode=yes \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=30 \
-o ServerAliveInterval=10 \
-o ServerAliveCountMax=6 \
-o TCPKeepAlive=yes \
-o Compression=no
}
ssh_run() {
local ssh_key="$1"
local ssh_user="$2"
local ssh_host="$3"
shift 3
require_ssh_key "$ssh_key"
local proxy_host="${DEPLOY_SSH_PROXY_HOST:-}"
local proxy_user="${DEPLOY_SSH_PROXY_USER:-$ssh_user}"
local proxy_args=()
if [[ -n "$proxy_host" ]]; then
proxy_args=(-J "$proxy_user@$proxy_host")
fi
# shellcheck disable=SC2207
local common_opts=($(ssh_common_opts "$ssh_user" "$ssh_host"))
ssh -i "$ssh_key" \
"${common_opts[@]}" \
"${proxy_args[@]}" \
"$ssh_user@$ssh_host" "$@"
}
scp_copy() {
local ssh_key="$1"
local src="$2"
local ssh_user="$3"
local ssh_host="$4"
local dst="$5"
local recursive="${6:-false}"
require_ssh_key "$ssh_key"
local proxy_host="${DEPLOY_SSH_PROXY_HOST:-}"
local proxy_user="${DEPLOY_SSH_PROXY_USER:-$ssh_user}"
local proxy_args=()
if [[ -n "$proxy_host" ]]; then
proxy_args=(-o "ProxyJump=$proxy_user@$proxy_host")
fi
# shellcheck disable=SC2207
local common_opts=($(ssh_common_opts "$ssh_user" "$ssh_host"))
local scp_opts=()
# Add -r for recursive copy if requested or if source is a directory
if [[ "$recursive" == "true" ]] || [[ -d "$src" ]]; then
scp_opts=(-r)
fi
scp -i "$ssh_key" \
"${scp_opts[@]}" \
"${common_opts[@]}" \
"${proxy_args[@]}" \
"$src" "$ssh_user@$ssh_host:$dst"
}

108
deploy/branch-align.sh Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env bash
# Aligns only origin/test, origin/pprod, origin/prod to current branch SHA. main is not aligned.
set -euo pipefail
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[branch-align][ERROR] Not in a git repository" >&2
exit 1
fi
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
cd "$PROJECT_ROOT" && exec "${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")" "$@"
fi
env_branch="${1:-}"
if [[ -z "$env_branch" ]]; then
echo "[branch-align][ERROR] Missing <env> argument (expected: main|test|pprod|prod)" >&2
echo "Usage: ./deploy/branch-align.sh <env>" >&2
exit 1
fi
if [[ ! "$env_branch" =~ ^(main|test|pprod|prod)$ ]]; then
echo "[branch-align][ERROR] Invalid <env>: must be main, test, pprod or prod (got: '${env_branch}')" >&2
echo "Usage: ./deploy/branch-align.sh <env>" >&2
exit 1
fi
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current_branch" != "$env_branch" ]]; then
echo "[branch-align][ERROR] Must be on branch '${env_branch}' (current: '${current_branch}')" >&2
exit 1
fi
# Fetch latest refs
git fetch origin
target_sha="$(git rev-parse "$env_branch")"
origin_env_sha="$(git rev-parse "origin/${env_branch}")"
if [[ "$target_sha" != "$origin_env_sha" ]]; then
echo "[branch-align] origin/${env_branch} differs from local ${env_branch}. Updating remote to local (env priority)."
git push --force-with-lease origin "${target_sha}:${env_branch}"
git fetch origin
fi
# Align all three branches to env SHA
for br in test pprod prod; do
if [[ "$br" == "$env_branch" ]]; then
# Ensure tracking exists
git branch --set-upstream-to="origin/${br}" "$br" >/dev/null 2>&1 || true
continue
fi
git branch -f "$br" "$target_sha"
git push --force-with-lease origin "${target_sha}:${br}"
git branch --set-upstream-to="origin/${br}" "$br" >/dev/null 2>&1 || true
done
# Also ensure env branch tracks its remote
git branch --set-upstream-to="origin/${env_branch}" "$env_branch" >/dev/null 2>&1 || true
# Verify last 30 commits are identical
tmp1="$(mktemp -t branch-align-test.XXXXXX)"
tmp2="$(mktemp -t branch-align-pprod.XXXXXX)"
tmp3="$(mktemp -t branch-align-prod.XXXXXX)"
cleanup() {
rm -f "$tmp1" "$tmp2" "$tmp3"
}
trap cleanup EXIT
git log -30 --format=%H origin/test > "$tmp1"
git log -30 --format=%H origin/pprod > "$tmp2"
git log -30 --format=%H origin/prod > "$tmp3"
if ! diff -u "$tmp1" "$tmp2" >/dev/null; then
echo "[branch-align][ERROR] Last 30 commits differ: origin/test vs origin/pprod" >&2
exit 1
fi
if ! diff -u "$tmp1" "$tmp3" >/dev/null; then
echo "[branch-align][ERROR] Last 30 commits differ: origin/test vs origin/prod" >&2
exit 1
fi
# Final assertions
if [[ "$(git rev-parse --abbrev-ref HEAD)" != "$env_branch" ]]; then
echo "[branch-align][ERROR] Branch changed unexpectedly" >&2
exit 1
fi
sha_test="$(git rev-parse origin/test)"
sha_pprod="$(git rev-parse origin/pprod)"
sha_prod="$(git rev-parse origin/prod)"
if [[ "$sha_test" != "$sha_pprod" ]] || [[ "$sha_test" != "$sha_prod" ]]; then
echo "[branch-align][ERROR] Remote branches are not aligned" >&2
echo "origin/test=$sha_test" >&2
echo "origin/pprod=$sha_pprod" >&2
echo "origin/prod=$sha_prod" >&2
exit 1
fi
echo "[branch-align] OK: origin/test, origin/pprod, origin/prod aligned to ${sha_test}"

91
deploy/bump-version.sh Normal file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
# Bump version and optional package.json files from project config (projects/<id>/conf.json).
# Usage: ./bump-version.sh <version> [message_court]
# Requires: run from repo root; project id from IA_PROJECT, .ia_project, or ai_project_id; jq if using version.package_json_paths.
VERSION="${1:-}"
SHORT_MSG="${2:-Nouvelles fonctionnalités et améliorations}"
if [[ -z "$VERSION" ]]; then
echo "❌ Usage: ./bump-version.sh <version> [message_court]"
echo " Exemple: ./bump-version.sh 2.1.0 'Nouveaux filtres'"
exit 1
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Version invalide. Format attendu: X.Y.Z (ex: 2.1.0)"
exit 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ Not in a git repository" >&2
exit 1
fi
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
SCRIPT_ABS="${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")"
cd "$PROJECT_ROOT" && exec "$SCRIPT_ABS" "$@"
fi
# shellcheck source=../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh"
echo "🔄 Mise à jour vers v${VERSION}..."
echo "$VERSION" > "$PROJECT_ROOT/VERSION"
echo "✅ VERSION → ${VERSION}"
package_paths=()
splash_name="Application"
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
while IFS= read -r p; do
[[ -n "$p" ]] && package_paths+=( "$p" )
done < <(jq -r '.version.package_json_paths[]? // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)
splash_name="$(jq -r '.version.splash_app_name // "Application"' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
fi
for p in "${package_paths[@]}"; do
if [[ -f "$PROJECT_ROOT/$p" ]]; then
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$PROJECT_ROOT/$p"
echo "$p${VERSION}"
else
echo "⚠️ $p not found, skipped"
fi
done
if [[ ! -f "$PROJECT_ROOT/CHANGELOG.md" ]]; then
echo "⚠️ CHANGELOG.md non trouvé. Créez-le manuellement avec les détails de cette version."
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 PROCHAINES ÉTAPES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "1. Éditer CHANGELOG.md pour documenter les changements de v${VERSION}"
echo ""
echo "2. Mettre à jour le .env distant avec le message splash (si applicable) :"
echo ""
cat << EOF
NEXT_PUBLIC_SPLASH_MESSAGE="🎉 ${splash_name} v${VERSION} est disponible !
${SHORT_MSG}
📖 Consultez CHANGELOG.md pour tous les détails"
NEXT_PUBLIC_SPLASH_MAX_DISPLAYS=10
NEXT_PUBLIC_SPLASH_ID="splash_v${VERSION}"
EOF
echo ""
echo "3. Rebuild, redémarrer et déployer selon le workflow du projet."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "✅ Bump terminé. Éditer CHANGELOG.md puis lancer le déploiement selon le projet."
exit 0

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# From branch test only: align origin/test, origin/pprod, origin/prod then deploy to test (import-v1, skipSetupHost).
# Use when you have already pushed to test and want to sync other branches and deploy test in one go.
set -euo pipefail
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[change-to-all-branches][ERROR] Not in a git repository" >&2
exit 1
fi
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
cd "$PROJECT_ROOT" && exec "${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")" "$@"
fi
current="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current" != "test" ]]; then
echo "[change-to-all-branches][ERROR] Must be on branch 'test' (current: '${current}')" >&2
exit 1
fi
echo "[change-to-all-branches] Aligning branches..."
"$DEPLOY_DIR/branch-align.sh" test
# scripts_v2 lives in the host project's deploy/ (not necessarily under ia_dev)
DEPLOY_SCRIPTS_V2="${PROJECT_ROOT}/deploy/scripts_v2"
echo "[change-to-all-branches] Deploying test (--import-v1 --skipSetupHost, --no-sync-origin because we just pushed)..."
"${DEPLOY_SCRIPTS_V2}/deploy.sh" test --import-v1 --skipSetupHost --no-sync-origin
echo "[change-to-all-branches] OK"

58
deploy/deploy-by-script-to.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# deploy-by-script-to <target_branch>: checkout target, verify .secrets/<env>, force sync with origin, deploy target, checkout test.
# Launched from ia_dev (like other deploy scripts); applies to parent repo (../). Call after /change-to-all-branches (agent). Target: pprod | prod only.
set -euo pipefail
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
DEPLOY_IA="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
# Parent of ia_dev = project root (deploy applies to ../)
PROJECT_ROOT="$(cd "$DEPLOY_IA/../.." && pwd)"
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
cd "$PROJECT_ROOT" && exec "$SCRIPT_REAL" "$@"
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[deploy-by-script-to][ERROR] Not in a git repository (PROJECT_ROOT=${PROJECT_ROOT})" >&2
exit 1
fi
TARGET_BRANCH="${1:-}"
if [[ -z "$TARGET_BRANCH" ]]; then
echo "[deploy-by-script-to][ERROR] Missing <target_branch> argument (expected: pprod | prod)" >&2
echo "Usage: ./deploy/deploy-by-script-to.sh <target_branch> (from ia_dev; pprod or prod only)" >&2
exit 1
fi
if [[ ! "$TARGET_BRANCH" =~ ^(pprod|prod)$ ]]; then
echo "[deploy-by-script-to][ERROR] Invalid target branch: must be pprod or prod (got: '${TARGET_BRANCH}')" >&2
echo "Usage: ./deploy/deploy-by-script-to.sh <pprod|prod>" >&2
exit 1
fi
current="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current" != "test" ]]; then
echo "[deploy-by-script-to][ERROR] Must be on branch 'test' (current: '${current}'). Run /change-to-all-branches first." >&2
exit 1
fi
echo "[deploy-by-script-to] Step 1/5: checkout ${TARGET_BRANCH}..."
if [[ "$(git rev-parse --abbrev-ref HEAD)" != "$TARGET_BRANCH" ]]; then
git checkout "$TARGET_BRANCH"
fi
SECRETS_DIR="${PROJECT_ROOT}/.secrets/${TARGET_BRANCH}"
if [[ ! -d "$SECRETS_DIR" ]]; then
echo "[deploy-by-script-to][ERROR] .secrets/${TARGET_BRANCH} does not exist at ${SECRETS_DIR}" >&2
exit 1
fi
echo "[deploy-by-script-to] Step 2/5: .secrets/${TARGET_BRANCH} OK"
echo "[deploy-by-script-to] Step 3/5: force sync local branch with origin/${TARGET_BRANCH}..."
git fetch origin
git reset --hard "origin/${TARGET_BRANCH}"
echo "[deploy-by-script-to] Step 4/5: deploy ${TARGET_BRANCH} (--import-v1 --skipSetupHost)..."
"$PROJECT_ROOT/deploy/scripts_v2/deploy.sh" "$TARGET_BRANCH" --import-v1 --skipSetupHost
echo "[deploy-by-script-to] Step 5/5: checkout test..."
git checkout test
echo "[deploy-by-script-to] OK: aligned, synced, deployed to ${TARGET_BRANCH}, back on test"

186
deploy/pousse.sh Executable file
View File

@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[pousse][ERROR] Not in a git repository" >&2
exit 1
fi
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
if [[ "$(pwd)" != "$PROJECT_ROOT" ]]; then
SCRIPT_ABS="${DEPLOY_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")"
cd "$PROJECT_ROOT" && exec "$SCRIPT_ABS" "$@"
fi
# Resolve project id and config path: IA_PROJECT, .ia_project, or ai_project_id → projects/<id>/conf.json
# shellcheck source=../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh"
remote="origin"
bump_version=false
usage() {
cat <<'EOF'
Usage:
./deploy/pousse.sh [--remote <remote>] [--bump-version]
--bump-version Increment patch (third component) in VERSION before staging.
Reads a full multi-line commit message from STDIN, then:
- if not in repo root: re-exec from repo root (standardized execution)
- build check (npm run build in each directory listed in projects/<id>/conf.json build_dirs, if any; exit on failure)
- git add -A
- git commit -F <message>
- git push -u <remote> HEAD
The current branch must already exist on the remote (e.g. origin/<branch>); otherwise the script refuses to push.
Example:
./deploy/pousse.sh <<'MSG'
Title
**Motivations:**
- ...
**Root causes:**
- ...
**Correctifs:**
- ...
**Evolutions:**
- ...
**Pages affectées:**
- ...
MSG
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--remote)
remote="$2"
shift 2
;;
--bump-version)
bump_version=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "[pousse][ERROR] Unknown arg: $1" >&2
usage >&2
exit 1
;;
esac
done
branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
echo "[pousse][ERROR] Detached HEAD is not supported" >&2
exit 1
fi
author_name="$(git config user.name || true)"
if [[ "$author_name" != "4NK" && "$author_name" != "Nicolas Cantu" ]]; then
echo "[pousse][ERROR] Refusing to commit: git user.name must be '4NK' or 'Nicolas Cantu' (got: '${author_name}')" >&2
exit 1
fi
repo_root="$(git rev-parse --show-toplevel)"
# Build dirs from project config (projects/<id>/conf.json); skip if no config or no build_dirs
build_dirs=()
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
while IFS= read -r d; do
[[ -n "$d" ]] && build_dirs+=( "$d" )
done < <(jq -r '.build_dirs[]? // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)
fi
if [[ ${#build_dirs[@]} -gt 0 ]]; then
echo "[pousse] Build check (${#build_dirs[@]} dirs from project config)..."
for dir in "${build_dirs[@]}"; do
if [[ ! -d "${repo_root}/${dir}" ]]; then
echo "[pousse][WARN] Skipping build ${dir} (directory not found)" >&2
continue
fi
echo "[pousse] Building ${dir}..."
(cd "${repo_root}/${dir}" && npm run build) || {
echo "[pousse][ERROR] Build failed in ${dir}" >&2
exit 1
}
done
echo "[pousse] Build check OK"
else
echo "[pousse] No build_dirs in project config (or no projects/<id>/conf.json / jq); skipping build check"
fi
msg_file="$(mktemp -t pousse-commit-msg.XXXXXX)"
cleanup() {
rm -f "$msg_file"
}
trap cleanup EXIT
cat >"$msg_file" || true
if [[ ! -s "$msg_file" ]]; then
echo "[pousse][ERROR] Empty commit message on STDIN" >&2
exit 1
fi
if [[ "$bump_version" == "true" ]]; then
version_file="${repo_root}/VERSION"
if [[ ! -f "$version_file" ]]; then
echo "[pousse][ERROR] VERSION not found at ${version_file}" >&2
exit 1
fi
current="$(cat "$version_file" | sed 's/[[:space:]]//g')"
if ! [[ "$current" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "[pousse][ERROR] VERSION format must be X.Y.Z (got: '${current}')" >&2
exit 1
fi
maj="${current%%.*}"
min="${current#*.}"
min="${min%%.*}"
patch="${current##*.}"
patch=$((patch + 1))
new_version="${maj}.${min}.${patch}"
echo "$new_version" > "$version_file"
echo "[pousse] Bumped VERSION: ${current} -> ${new_version}"
fi
# Stage all changes
git add -A
git_status_short="$(git status -sb)"
echo "$git_status_short"
# Prevent committing potentially sensitive files
staged_files="$(git diff --cached --name-only || true)"
if [[ -n "$staged_files" ]]; then
if echo "$staged_files" | grep -Eiq '^(\.secrets/|\.env($|\.)|\.env\.|.*\.(key|pem|p12)$|.*credentials.*)'; then
echo "[pousse][ERROR] Refusing to commit: staged files look sensitive:" >&2
echo "$staged_files" | grep -Ei '^(\.secrets/|\.env($|\.)|\.env\.|.*\.(key|pem|p12)$|.*credentials.*)' >&2
exit 1
fi
fi
if git diff --cached --quiet; then
echo "[pousse] No staged changes to commit" >&2
exit 0
fi
echo "[pousse] Staged changes:"
git diff --cached --stat
git commit -F "$msg_file"
if ! git rev-parse "${remote}/${branch}" >/dev/null 2>&1; then
echo "[pousse][ERROR] Branch '${branch}' does not exist on remote '${remote}'. Refusing to push (would create a new remote branch). Create the branch on the remote first or push manually." >&2
exit 1
fi
git push -u "$remote" HEAD

View File

@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Bounded loop for chat: run mail retrieval N times, 1 minute between each.
# Use from Cursor chat when asked to "lance la boucle récupération emails puis attend 1 min et relance".
# Runs in foreground (no background); chat can run it for a few iterations to avoid timeout.
#
# Usage:
# ./gitea-issues/agent-loop-chat-iterations.sh [N] [--repeat]
# N = number of iterations (default 3). Each iteration: mail-list-unread.sh then sleep 60.
# --repeat = after N iterations, relaunch (infinite loop of N-by-N runs).
# Output and mail list (expéditeur, sujet) are appended to projects/<id>/logs/gitea-issues/agent-loop-chat-iterations.log.
#
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
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
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"
REPEAT=""
if [ "${1:-}" = "--repeat" ]; then
REPEAT=1
N="${2:-3}"
else
N="${1:-3}"
fi
if ! [[ "$N" =~ ^[0-9]+$ ]] || [ "$N" -lt 1 ]; then
echo "Usage: $0 [N] [--repeat] — N positive integer (default 3). --repeat = relancer à la fin." >&2
exit 1
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"
if [ "${PIPESTATUS[0]:-0}" -eq 0 ]; then
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi OK"
else
log_and_echo "[agent-loop-chat] $(date -Iseconds) — test d'envoi échoué"
exit 1
fi
run_iterations() {
for i in $(seq 1 "$N"); do
log_and_echo "[agent-loop-chat] $(date -Iseconds) — iteration $i/$N"
"${GITEA_ISSUES_DIR}/mail-list-unread.sh" 2>&1 | tee -a "$LOG_FILE" || true
if [ "$i" -lt "$N" ]; then
log_and_echo "[agent-loop-chat] $(date -Iseconds) — attente 60 s avant prochaine itération"
sleep 60
fi
done
log_and_echo "[agent-loop-chat] $(date -Iseconds)$N itérations terminées"
}
while true; do
run_iterations
if [ -z "${REPEAT:-}" ]; then
break
fi
log_and_echo "[agent-loop-chat] $(date -Iseconds) — relance"
done

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

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 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
#
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"
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")"
write_status() {
printf "%s\n%s\n%s\n" "$(date -Iseconds)" "$1" "${2:-}" > "$STATUS_FILE"
}
"${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 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 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

@ -0,0 +1,52 @@
#!/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. Do NOT start this script from the agent-loop agent (use bounded runs only).
#
# Usage (manual only; agent must not launch with nohup/&):
# cd <racine_projet> && ./ia_dev/gitea-issues/agent-loop-treatment.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
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
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"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
PENDING_FILE="${AGENT_LOOP_PENDING_FILE:-$LOGS_GITEA/agent-loop.pending}"
LOG_DIR="$(dirname "$PENDING_FILE")"
mkdir -p "$LOG_DIR"
AGENT_LOOP_ENV="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/agent-loop.env"
if [ -r "$AGENT_LOOP_ENV" ]; then
set +u
# shellcheck source=/dev/null
source "$AGENT_LOOP_ENV"
set -u
fi
INTERVAL="${AGENT_LOOP_TREATMENT_INTERVAL_SEC:-60}"
AGENT_MODEL="${AGENT_LOOP_MODEL:-sonnet-4.6}"
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
echo "[agent-loop-treatment] $(date -Iseconds) — Pending non vide, lancement de l'agent Cursor."
if agent -p "$PROMPT" -f --model "$AGENT_MODEL" 2>&1; then
echo "[agent-loop-treatment] $(date -Iseconds) — Agent terminé."
else
echo "[agent-loop-treatment] $(date -Iseconds) — Agent terminé avec erreur."
fi
fi
sleep "$INTERVAL"
done

View File

@ -0,0 +1,19 @@
# Agent-loop parameters (Cursor Agent CLI, model, interval).
# Copy to .secrets/gitea-issues/agent-loop.env and set as needed.
# Do not commit .secrets/gitea-issues/agent-loop.env (directory is gitignored).
#
# Run Cursor Agent when unread mails are detected (0 or 1)
# AGENT_LOOP_RUN_AGENT=1
#
# Model used by the CLI (default: sonnet-4.6 to avoid Opus usage limits)
# List: agent models
# AGENT_LOOP_MODEL=sonnet-4.6
#
# Polling interval in seconds (default: 60)
# AGENT_LOOP_INTERVAL_SEC=60
#
# 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

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

@ -0,0 +1,91 @@
#!/usr/bin/env bash
# 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 (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)
# 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 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.
#
# Optional: AGENT_LOOP_MODEL=<model> to force the model (e.g. sonnet-4.6, gpt-5.4-low). Default: sonnet-4.6 to avoid Opus usage limits when running unattended.
#
set -euo pipefail
# Source user env so PATH includes ~/.local/bin (Cursor Agent CLI, etc.)
if [ -n "${HOME:-}" ] && [ -r "$HOME/.bashrc" ]; then
set +u
# shellcheck source=/dev/null
source "$HOME/.bashrc" 2>/dev/null || true
set -u
fi
[ -n "${HOME:-}" ] && [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
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"
# Per-project logs under projects/<id>/logs (lib.sh sets PROJECT_LOGS_DIR)
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" 2>/dev/null || true
LOGS_GITEA="${PROJECT_LOGS_DIR:-$ROOT/logs}/gitea-issues"
# Load agent-loop parameters from .secrets (optional; .secrets under ia_dev)
AGENT_LOOP_ENV="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/agent-loop.env"
if [ -r "$AGENT_LOOP_ENV" ]; then
set +u
# shellcheck source=/dev/null
source "$AGENT_LOOP_ENV"
set -u
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")"
write_status() {
local status="$1"
local detail="${2:-}"
printf "%s\n%s\n%s\n" "$(date -Iseconds)" "$status" "$detail" > "$STATUS_FILE"
}
while true; do
write_status "running" "interval=${INTERVAL}s"
# 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 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 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
write_status "mails_pending" "Agent terminé avec erreur ou interruption. Relancer l'agent manuellement si besoin."
fi
fi
else
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
echo "[agent-loop] $(date -Iseconds) — Aucun mail en attente dans le spooler (critère: from/to dans conf.json)."
fi
sleep "$INTERVAL"
done

50
gitea-issues/comment-issue.sh Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
#
# Add a comment to an issue via Gitea API. The comment body is automatically
# signed with: Support IA du projet Lecoffre.io / ai.support.lecoffreio@4nkweb.com
# Usage: ./comment-issue.sh <issue_number> <message>
# Or: echo "message" | ./comment-issue.sh <issue_number> -
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
# Signature appended to every comment (same as mail replies)
COMMENT_SIGNATURE=$'\n\n--\nSupport IA du projet Lecoffre.io\nai.support.lecoffreio@4nkweb.com'
if [[ -n "${GITEA_COMMENT_SIGNATURE:-}" ]]; then
COMMENT_SIGNATURE="${GITEA_COMMENT_SIGNATURE}"
fi
require_jq || exit 1
if [[ $# -lt 1 ]]; then
log_err "Usage: $0 <issue_number> <message>"
log_err " Or: $0 <issue_number> - (read message from stdin)"
exit 1
fi
ISSUE_NUM="$1"
if [[ "${2:-}" == "-" ]]; then
BODY="$(cat)"
else
BODY="${2:-}"
fi
if [[ -z "$BODY" ]]; then
log_err "Comment body is empty."
exit 1
fi
BODY="${BODY}${COMMENT_SIGNATURE}"
# Escape for JSON: jq -Rs . handles newlines and quotes
BODY_JSON="$(echo "$BODY" | jq -Rs .)"
RESPONSE="$(gitea_api_post "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}/comments" "{\"body\":${BODY_JSON}}")"
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
log_err "API error posting comment: ${RESPONSE:0:200}"
exit 1
fi
log_info "Comment added to issue #${ISSUE_NUM}."

View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
#
# Create a local branch for an issue. Branch name: issue/<number> (safe, short).
# Base branch defaults to "test"; ensure it is up to date (fetch + reset to origin/base).
# Usage: ./create-branch-for-issue.sh <issue_number> [base_branch]
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
if [[ $# -lt 1 ]]; then
log_err "Usage: $0 <issue_number> [base_branch]"
exit 1
fi
ISSUE_NUM="$1"
BASE="${2:-test}"
require_git_root || exit 1
if git show-ref --quiet "refs/heads/issue/${ISSUE_NUM}"; then
log_info "Branch issue/${ISSUE_NUM} already exists. Checking it out."
git checkout "issue/${ISSUE_NUM}"
echo "issue/${ISSUE_NUM}"
exit 0
fi
if ! git show-ref --quiet "refs/heads/${BASE}"; then
log_err "Base branch ${BASE} does not exist locally."
exit 1
fi
git fetch origin
if git show-ref --quiet "refs/remotes/origin/${BASE}"; then
git checkout "${BASE}"
git reset --hard "origin/${BASE}"
else
git checkout "${BASE}"
fi
git checkout -b "issue/${ISSUE_NUM}"
log_info "Created and checked out branch issue/${ISSUE_NUM} from ${BASE}."
echo "issue/${ISSUE_NUM}"

39
gitea-issues/get-issue.sh Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
#
# Get one issue by number. Output: JSON (default) or plain text summary (--summary).
# Usage: ./get-issue.sh <issue_number> [--summary]
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
require_jq || exit 1
if [[ $# -lt 1 ]]; then
log_err "Usage: $0 <issue_number> [--summary]"
exit 1
fi
ISSUE_NUM="$1"
SUMMARY=false
[[ "${2:-}" == "--summary" ]] && SUMMARY=true
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}")"
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
log_err "API error or invalid JSON (issue ${ISSUE_NUM}): ${RESPONSE:0:200}"
exit 1
fi
if [[ "$SUMMARY" == true ]]; then
echo "--- Issue #${ISSUE_NUM} ---"
echo "Title: $(echo "$RESPONSE" | jq -r '.title')"
echo "State: $(echo "$RESPONSE" | jq -r '.state')"
echo "Labels: $(echo "$RESPONSE" | jq -r '[.labels[].name] | join(", ")')"
echo "Body:"
echo "$RESPONSE" | jq -r '.body // "(empty)"'
echo "---"
else
echo "$RESPONSE"
fi

View File

@ -0,0 +1,43 @@
# IMAP config for mail-to-issue (e.g. Proton Mail Bridge).
# Copy to .secrets/gitea-issues/imap-bridge.env and set real values.
# Do not commit .secrets/gitea-issues/imap-bridge.env (directory is gitignored).
#
# IMAP (read)
# IMAP_HOST=127.0.0.1
# IMAP_PORT=1143
# IMAP_USER=your-address@pm.me
# IMAP_PASSWORD=your-bridge-password
# IMAP_USE_STARTTLS=true
# For local Proton Bridge with self-signed cert, set to false to skip SSL verification (localhost only).
# IMAP_SSL_VERIFY=false
#
# SMTP (send replies; same Bridge account)
# SMTP_HOST=127.0.0.1
# SMTP_PORT=1025
# SMTP_USER=your-address@pm.me
# SMTP_PASSWORD=your-bridge-password
# SMTP_USE_STARTTLS=true
#
# 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
#
# Signature for Gitea issue comments (optional; comment-issue.sh uses same default as mail signature)
# GITEA_COMMENT_SIGNATURE=
IMAP_HOST=127.0.0.1
IMAP_PORT=1143
IMAP_USER=
IMAP_PASSWORD=
IMAP_USE_STARTTLS=true
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
SMTP_USER=
SMTP_PASSWORD=
SMTP_USE_STARTTLS=true

122
gitea-issues/lib.sh Executable file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env bash
#
# Shared config and helpers for Gitea issues scripts.
# Source from gitea-issues/*.sh after cd to project root.
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
GITEA_API_URL="${GITEA_API_URL:-https://git.4nkweb.com/api/v1}"
GITEA_REPO_OWNER="${GITEA_REPO_OWNER:-4nk}"
GITEA_REPO_NAME="${GITEA_REPO_NAME:-lecoffre_ng}"
# Optional: load project config from ia_dev (projects/<id>/conf.json); logs and data per project under projects/<id>/
PROJECT_CONFIG_PATH=""
PROJECT_LOGS_DIR=""
DATA_ISSUES_DIR=""
if [[ -f "${GITEA_ISSUES_DIR}/../lib/project_config.sh" ]]; then
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || true
if [[ -z "${PROJECT_ROOT:-}" || "$(basename "$PROJECT_ROOT")" = "ia_dev" ]]; then
PROJECT_ROOT="$(cd "${GITEA_ISSUES_DIR}/../.." && pwd)"
fi
IA_DEV_ROOT="$(cd "$GITEA_ISSUES_DIR/.." && pwd)"
if [[ -n "${PROJECT_ROOT:-}" ]]; then
# shellcheck source=../lib/project_config.sh
source "${GITEA_ISSUES_DIR}/../lib/project_config.sh"
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
export PROJECT_LOGS_DIR
export DATA_ISSUES_DIR
# Load token: GITEA_TOKEN env, then project config git.token_file, then default .secrets path
load_gitea_token() {
if [[ -n "${GITEA_TOKEN:-}" ]]; then
return 0
fi
local token_file=""
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
local rel_path
rel_path="$(jq -r '.git.token_file // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
if [[ -n "$rel_path" && -n "${PROJECT_ROOT:-}" && -f "${PROJECT_ROOT}/${rel_path}" ]]; then
token_file="${PROJECT_ROOT}/${rel_path}"
fi
fi
if [[ -z "$token_file" ]]; then
token_file="${GITEA_ISSUES_DIR}/../.secrets/gitea-issues/token"
fi
if [[ -f "$token_file" ]]; then
GITEA_TOKEN="$(cat "$token_file")"
return 0
fi
echo "[gitea-issues] ERROR: GITEA_TOKEN not set and ${token_file} not found" >&2
echo "[gitea-issues] Set GITEA_TOKEN or create the token file with a Gitea Personal Access Token." >&2
return 1
}
# curl wrapper for Gitea API (GET). Usage: gitea_api_get "/repos/owner/repo/issues"
gitea_api_get() {
local path="$1"
load_gitea_token || return 1
curl -sS -H "Accept: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API_URL}${path}"
}
# curl wrapper for Gitea API (POST). Usage: gitea_api_post "/repos/owner/repo/issues/123/comments" '{"body":"..."}'
gitea_api_post() {
local path="$1"
local data="${2:-}"
load_gitea_token || return 1
curl -sS -X POST -H "Accept: application/json" -H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$data" \
"${GITEA_API_URL}${path}"
}
# curl wrapper for Gitea API (PATCH). Usage: gitea_api_patch "/repos/owner/repo/wiki/page/Foo" '{"content_base64":"..."}'
gitea_api_patch() {
local path="$1"
local data="${2:-}"
load_gitea_token || return 1
curl -sS -X PATCH -H "Accept: application/json" -H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$data" \
"${GITEA_API_URL}${path}"
}
# curl wrapper for Gitea API (DELETE). Usage: gitea_api_delete "/repos/owner/repo/wiki/page/Foo"
gitea_api_delete() {
local path="$1"
load_gitea_token || return 1
curl -sS -X DELETE -H "Accept: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API_URL}${path}"
}
log_ts() { date -u '+%Y-%m-%dT%H:%M:%SZ'; }
log_info() { echo "[$(log_ts)] [gitea-issues] $*"; }
log_err() { echo "[$(log_ts)] [gitea-issues] $*" >&2; }
# Require jq for JSON output
require_jq() {
if ! command -v jq &>/dev/null; then
log_err "jq is required. Install with: apt install jq / brew install jq"
return 1
fi
}
# Ensure we are in the git repo root (for create-branch, etc.)
require_git_root() {
local root
root="$(git rev-parse --show-toplevel 2>/dev/null)" || true
if [[ -z "$root" ]]; then
log_err "Not inside a git repository."
return 1
fi
cd "$root"
}

View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# List open issues for the configured Gitea repo.
# Output: JSON array (default) or one line per issue "number|title|state" with --lines.
# Usage: ./list-open-issues.sh [--lines] [--limit N]
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
require_jq || exit 1
LINES=false
LIMIT=50
while [[ $# -gt 0 ]]; do
case "$1" in
--lines) LINES=true; shift ;;
--limit) LIMIT="$2"; shift 2 ;;
*) log_err "Unknown option: $1"; exit 1 ;;
esac
done
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues?state=open&page=1&limit=${LIMIT}")"
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
log_err "API error or invalid JSON: ${RESPONSE:0:200}"
exit 1
fi
if [[ "$LINES" == true ]]; then
echo "$RESPONSE" | jq -r '.[] | "\(.number)|\(.title)|\(.state)"'
else
echo "$RESPONSE"
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

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Create one Gitea issue from one email (by UID), then mark the email as read.
If --title and/or --body are provided (formalized by agent), use them; else use subject and body from the email.
Usage: ./gitea-issues/mail-create-issue-from-email.sh --uid <uid> [--title "..." ] [--body "..." ]
"""
from __future__ import annotations
import argparse
import email
import imaplib
import ssl
import sys
from email.header import decode_header
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import (
create_gitea_issue,
load_gitea_config,
load_imap_config,
repo_root,
sanitize_title,
)
def decode_header_value(header: str | None) -> str:
if not header:
return ""
from email.header import decode_header as dh
parts = dh(header)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def get_text_body(msg: email.message.Message) -> str:
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
return ""
payload = msg.get_payload(decode=True)
if not payload:
return ""
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
def main() -> None:
ap = argparse.ArgumentParser(description="Create one Gitea issue from one email by UID")
ap.add_argument("--uid", required=True, help="IMAP message UID")
ap.add_argument("--title", default="", help="Formalized issue title (else use subject)")
ap.add_argument("--body", default="", help="Formalized issue body (else use email body + From)")
args = ap.parse_args()
cfg = load_imap_config()
if not cfg["user"] or not cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
sys.exit(1)
gitea = load_gitea_config()
if not gitea["token"]:
print("[gitea-issues] ERROR: GITEA_TOKEN not set.", file=sys.stderr)
sys.exit(1)
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
if cfg["use_starttls"]:
mail.starttls(ssl.create_default_context())
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
_, data = mail.fetch(args.uid, "(RFC822)")
if not data or not data[0]:
print("[gitea-issues] ERROR: Message UID not found.", file=sys.stderr)
mail.logout()
sys.exit(1)
msg = email.message_from_bytes(data[0][1])
from_ = decode_header_value(msg.get("From"))
subject = decode_header_value(msg.get("Subject"))
body_text = get_text_body(msg)
body_for_issue = f"**From:** {from_}\n\n{body_text}".strip()
title = args.title.strip() if args.title else sanitize_title(subject)
body = args.body.strip() if args.body else body_for_issue
issue = create_gitea_issue(title, body)
if not issue:
print("[gitea-issues] ERROR: Failed to create issue.", file=sys.stderr)
mail.logout()
sys.exit(1)
mail.store(args.uid, "+FLAGS", "\\Seen")
mail.logout()
num = issue.get("number", "?")
print(f"[gitea-issues] Created issue #{num}: {title[:60]}")
print(f"ISSUE_NUMBER={num}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Create one Gitea issue from one email (by UID), mark email read. Run from repo root.
# 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)}"
export GITEA_ISSUES_DIR
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-create-issue-from-email.py" "$@"

View File

@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
Fetch the full email thread (conversation) for a given message UID.
Uses Message-ID, References and In-Reply-To to find all messages in the thread.
Output format: same as mail-list-unread (--- MAIL UID=... --- ... --- END MAIL ---), chronological order.
Usage: mail-get-thread.py <uid>
or: ./gitea-issues/mail-get-thread.sh <uid>
"""
from __future__ import annotations
import email
import imaplib
import re
import sys
from email.header import decode_header
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import imap_since_date, load_imap_config, repo_root, imap_ssl_context
def decode_header_value(header: str | None) -> str:
if not header:
return ""
parts = decode_header(header)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def get_text_body(msg: email.message.Message) -> str:
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
return payload.decode(
part.get_content_charset() or "utf-8", errors="replace"
)
return ""
payload = msg.get_payload(decode=True)
if not payload:
return ""
return payload.decode(
msg.get_content_charset() or "utf-8", errors="replace"
)
def parse_message_ids(refs: str | None, in_reply_to: str | None) -> set[str]:
"""Extract Message-ID values from References and In-Reply-To headers."""
ids: set[str] = set()
for raw in (refs or "", in_reply_to or ""):
for part in re.split(r"\s+", raw.strip()):
part = part.strip()
if part.startswith("<") and ">" in part:
ids.add(part)
elif part and "@" in part and part not in ("<", ">"):
ids.add(part if part.startswith("<") else f"<{part}>")
return ids
def find_message_ids_from_msg(msg: email.message.Message) -> set[str]:
mid = (msg.get("Message-ID") or "").strip()
refs = (msg.get("References") or "").strip()
in_reply = (msg.get("In-Reply-To") or "").strip()
ids = {mid} if mid else set()
ids |= parse_message_ids(refs, in_reply)
return ids
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, 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 + ">"
since = imap_since_date()
criterion = f'(HEADER Message-ID "{msg_id}" SINCE {since})'
try:
_, data = mail.search(None, criterion)
except Exception:
return []
if not data or not data[0]:
return []
return [u.decode("ascii") for u in data[0].split() if u]
def fetch_message_by_uid(
mail: imaplib.IMAP4, uid: str
) -> email.message.Message | None:
"""Fetch a single message by UID. Returns parsed email or None."""
try:
_, data = mail.fetch(uid.encode("ascii"), "(RFC822)")
except Exception:
return None
if not data or not data[0] or len(data[0]) < 2:
return None
raw = data[0][1]
if isinstance(raw, bytes):
return email.message_from_bytes(raw)
return None
def format_message(uid: str, msg: email.message.Message) -> str:
mid = (msg.get("Message-ID") or "").strip()
from_ = decode_header_value(msg.get("From"))
to_ = decode_header_value(msg.get("To"))
subj = decode_header_value(msg.get("Subject"))
date_h = decode_header_value(msg.get("Date"))
body = get_text_body(msg)
lines = [
"--- MAIL",
f"UID={uid}",
"---",
"Message-ID: " + (mid or "(none)"),
"From: " + from_,
"To: " + (to_ or ""),
"Subject: " + subj,
"Date: " + (date_h or ""),
"Body:",
body or "(empty)",
"--- END MAIL ---",
]
return "\n".join(lines)
def main() -> int:
if len(sys.argv) < 2:
print("Usage: mail-get-thread.py <uid>", file=sys.stderr)
return 1
uid0 = sys.argv[1].strip()
if not uid0:
print("[gitea-issues] ERROR: UID required.", file=sys.stderr)
return 1
cfg = load_imap_config()
if not cfg["user"] or not cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print(
"[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.",
file=sys.stderr,
)
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
return 1
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
if cfg["use_starttls"]:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
msg0 = fetch_message_by_uid(mail, uid0)
if not msg0:
print(f"[gitea-issues] No message found for UID={uid0}.", file=sys.stderr)
mail.logout()
return 1
to_fetch: set[str] = find_message_ids_from_msg(msg0)
seen_ids: set[str] = set()
uids_by_mid: dict[str, str] = {}
while to_fetch:
mid = to_fetch.pop()
if not mid or mid in seen_ids:
continue
seen_ids.add(mid)
uids = search_by_message_id(mail, mid)
if uids:
uids_by_mid[mid] = uids[0]
msg = fetch_message_by_uid(mail, uids[0])
if msg:
to_fetch |= find_message_ids_from_msg(msg)
mid0 = (msg0.get("Message-ID") or "").strip()
if mid0 and mid0 not in uids_by_mid:
uids_by_mid[mid0] = uid0
collected: list[tuple[str, str, email.message.Message]] = []
for _mid, uid in uids_by_mid.items():
msg = fetch_message_by_uid(mail, uid)
if not msg:
continue
date_h = (msg.get("Date") or "").strip()
collected.append((date_h, uid, msg))
if uid0 not in uids_by_mid.values():
date0 = (msg0.get("Date") or "").strip()
collected.append((date0, uid0, msg0))
collected.sort(key=lambda x: x[0])
for _date, uid, msg in collected:
print(format_message(uid, msg))
mail.logout()
return 0
if __name__ == "__main__":
sys.exit(main())

12
gitea-issues/mail-get-thread.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Fetch full email thread for a given UID. Run from repo root.
# Usage: ./gitea-issues/mail-get-thread.sh <uid>
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}/.."
if [ $# -lt 1 ]; then
echo "Usage: $0 <uid>" >&2
exit 1
fi
exec python3 "${GITEA_ISSUES_DIR}/mail-get-thread.py" "$1"

118
gitea-issues/mail-list-unread.py Executable file
View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
List unread emails via IMAP (e.g. Proton Mail Bridge). Read-only; does not mark as read.
Only lists messages sent to the configured alias (MAIL_FILTER_TO, default ai.support.lecoffreio@4nkweb.com).
Output is for the agent: each mail with UID, Message-ID, From, To, Subject, Date, body.
Usage: ./gitea-issues/mail-list-unread.sh
"""
from __future__ import annotations
import email
import imaplib
import sys
from email.header import decode_header
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 imap_search_criterion_unseen, load_imap_config, repo_root, imap_ssl_context
def decode_header_value(header: str | None) -> str:
if not header:
return ""
from email.header import decode_header as dh
parts = dh(header)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def get_text_body(msg: email.message.Message) -> str:
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
return ""
payload = msg.get_payload(decode=True)
if not payload:
return ""
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
def is_sent_to_alias(msg: email.message.Message, filter_to: str) -> bool:
"""True if any To/Delivered-To/X-Original-To/Cc header contains the filter address."""
if not filter_to:
return True
headers_to_check = ("To", "Delivered-To", "X-Original-To", "Cc", "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:
return True
return False
def main() -> None:
cfg = load_imap_config()
if not cfg["user"] or not cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
sys.exit(1)
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
if cfg["use_starttls"]:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
criterion = imap_search_criterion_unseen()
_, nums = mail.search(None, criterion)
ids = nums[0].split()
if not ids:
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
shown = 0
for uid in ids:
uid_s = uid.decode("ascii")
_, data = mail.fetch(uid, "(RFC822)")
if not data or not data[0]:
continue
msg = email.message_from_bytes(data[0][1])
if not is_sent_to_alias(msg, cfg.get("filter_to", "")):
continue
mid = msg.get("Message-ID", "").strip()
from_ = decode_header_value(msg.get("From"))
to_ = decode_header_value(msg.get("To"))
subj = decode_header_value(msg.get("Subject"))
date_h = decode_header_value(msg.get("Date"))
body = get_text_body(msg)
print("--- MAIL", f"UID={uid_s}", "---")
print("Message-ID:", mid or "(none)")
print("From:", from_)
print("To:", to_ or "")
print("Subject:", subj)
print("Date:", date_h or "")
print("Body:")
print(body or "(empty)")
print("--- END MAIL ---")
shown += 1
if shown == 0:
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()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# List unread emails (read-only). Run from repo root.
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && 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="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-list-unread.py"

41
gitea-issues/mail-mark-read.py Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Mark one email as read by UID (e.g. after replying without creating an issue).
Usage: ./gitea-issues/mail-mark-read.sh <uid>
"""
from __future__ import annotations
import imaplib
import sys
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
def main() -> None:
if len(sys.argv) < 2:
print("[gitea-issues] Usage: mail-mark-read.sh <uid>", file=sys.stderr)
sys.exit(1)
uid = sys.argv[1].strip()
cfg = load_imap_config()
if not cfg["user"] or not cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
sys.exit(1)
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
if cfg["use_starttls"]:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
mail.store(uid, "+FLAGS", "\\Seen")
mail.logout()
print("[gitea-issues] Marked as read.")
if __name__ == "__main__":
main()

8
gitea-issues/mail-mark-read.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Mark one email as read by UID. Run from repo root.
# Usage: ./gitea-issues/mail-mark-read.sh <uid>
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}/mail-mark-read.py" "$@"

108
gitea-issues/mail-send-reply.py Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
Send a reply email via SMTP (e.g. Proton Mail Bridge).
Usage: ./gitea-issues/mail-send-reply.sh --to addr@example.com --subject "..." --body "..." [--in-reply-to "<msg-id>" [--references "<refs>"]]
Or: echo "body" | ./gitea-issues/mail-send-reply.sh --to addr@example.com --subject "..." [--in-reply-to "<msg-id>"]
"""
from __future__ import annotations
import argparse
import os
import smtplib
import sys
from email.mime.text import MIMEText
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import load_smtp_config, repo_root, imap_ssl_context
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()
if sig:
return "\n\n" + sig.replace("\\n", "\n")
return "\n\n" + DEFAULT_SIGNATURE
def main() -> None:
ap = argparse.ArgumentParser(description="Send reply email via Bridge SMTP")
ap.add_argument("--to", required=True, help="To address")
ap.add_argument("--subject", required=True, help="Subject")
ap.add_argument("--body", default="", help="Body (or use stdin)")
ap.add_argument("--in-reply-to", default="", help="Message-ID of the message we reply to")
ap.add_argument("--references", default="", help="References header for threading")
args = ap.parse_args()
cfg = load_smtp_config()
if not cfg["user"] or not cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print("[gitea-issues] ERROR: SMTP_USER and SMTP_PASSWORD required.", file=sys.stderr)
print(f"[gitea-issues] Set env or create {env_path}", file=sys.stderr)
sys.exit(1)
body = args.body
if not body and not sys.stdin.isatty():
body = sys.stdin.read()
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
msg["From"] = cfg["user"]
msg["To"] = args.to
if args.in_reply_to:
msg["In-Reply-To"] = args.in_reply_to
if args.references:
msg["References"] = args.references
with smtplib.SMTP(cfg["host"], int(cfg["port"])) as smtp:
if cfg["use_starttls"]:
smtp.starttls(context=imap_ssl_context(cfg.get("ssl_verify", True)))
smtp.login(cfg["user"], cfg["password"])
smtp.sendmail(cfg["user"], [args.to], msg.as_string())
print("[gitea-issues] Reply sent.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Send reply email via Bridge SMTP. Run from repo root.
# 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)}"
export GITEA_ISSUES_DIR
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-send-reply.py" "$@"

View File

@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""
Thread log: one file per email thread under projects/<id>/logs/gitea-issues/threads/.
Content: exchanges (received + sent), tickets (issues), commits.
Usage:
mail-thread-log.py get-id --uid <uid> # print THREAD_ID=...
mail-thread-log.py init --uid <uid> # create/update log from thread
mail-thread-log.py append-sent --thread-id <id> --to <addr> --subject "..." [--body "..."] [--date "..."]
mail-thread-log.py append-issue --thread-id <id> --issue <num> [--title "..."]
mail-thread-log.py append-commit --thread-id <id> --hash <hash> --message "..." [--branch "..."]
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import load_gitea_config, load_imap_config, repo_root
from project_config import project_logs_dir
def threads_dir() -> Path:
"""Thread log directory: projects/<id>/logs/gitea-issues/threads/ or repo logs fallback."""
d = project_logs_dir() / "gitea-issues" / "threads"
d.mkdir(parents=True, exist_ok=True)
return d
def sanitize_thread_id(raw: str, max_len: int = 80) -> str:
s = re.sub(r"[^a-zA-Z0-9._-]", "_", raw).strip("_")
return s[:max_len] if s else "thread_unknown"
def get_thread_output(uid: str) -> str:
gitea_dir = Path(__file__).resolve().parent
root = gitea_dir.parent
env = {"GITEA_ISSUES_DIR": str(gitea_dir)}
result = subprocess.run(
[sys.executable, str(gitea_dir / "mail-get-thread.py"), uid],
cwd=str(root),
capture_output=True,
text=True,
env={**__import__("os").environ, **env},
timeout=60,
)
if result.returncode != 0:
raise RuntimeError(
f"mail-get-thread failed: {result.stderr or result.stdout or 'unknown'}"
)
return result.stdout
def parse_thread_blocks(text: str) -> list[dict[str, str]]:
"""Parse --- MAIL UID=... --- ... --- END MAIL --- blocks."""
blocks: list[dict[str, str]] = []
pattern = re.compile(
r"--- MAIL\s+UID=(\S+)\s+---\s*\n"
r"(?:Message-ID:\s*(.*?)\n)?"
r"From:\s*(.*?)\n"
r"To:\s*(.*?)\n"
r"Subject:\s*(.*?)\n"
r"Date:\s*(.*?)\n"
r"Body:\s*\n(.*?)--- END MAIL ---",
re.DOTALL,
)
for m in pattern.finditer(text):
blocks.append({
"uid": m.group(1).strip(),
"message_id": (m.group(2) or "").strip(),
"from": (m.group(3) or "").strip(),
"to": (m.group(4) or "").strip(),
"subject": (m.group(5) or "").strip(),
"date": (m.group(6) or "").strip(),
"body": (m.group(7) or "").strip(),
})
return blocks
def get_thread_id_from_uid(uid: str) -> str:
out = get_thread_output(uid)
blocks = parse_thread_blocks(out)
if not blocks:
return sanitize_thread_id(f"thread_uid_{uid}")
first_msg_id = (blocks[0].get("message_id") or "").strip() or blocks[0].get("uid", "")
return sanitize_thread_id(first_msg_id)
def format_exchange_received(block: dict[str, str]) -> str:
return (
f"### {block.get('date', '')} — Reçu\n"
f"- **De:** {block.get('from', '')}\n"
f"- **À:** {block.get('to', '')}\n"
f"- **Sujet:** {block.get('subject', '')}\n\n"
f"{block.get('body', '')}\n\n"
)
def format_exchange_sent(block: dict[str, str]) -> str:
return (
f"### {block.get('date', '')} — Envoyé\n"
f"- **À:** {block.get('to', '')}\n"
f"- **Sujet:** {block.get('subject', '')}\n\n"
f"{block.get('body', '')}\n\n"
)
def init_log(uid: str) -> str:
cfg = load_imap_config()
our_address = (cfg.get("filter_to") or "").strip().lower()
if not our_address:
our_address = (cfg.get("user") or "").strip().lower()
out = get_thread_output(uid)
blocks = parse_thread_blocks(out)
thread_id = get_thread_id_from_uid(uid)
log_path = threads_dir() / f"{thread_id}.md"
received_blocks: list[dict[str, str]] = []
sent_blocks: list[dict[str, str]] = []
for b in blocks:
from_ = (b.get("from") or "").lower()
if our_address and our_address in from_:
sent_blocks.append(b)
else:
received_blocks.append(b)
existing_tickets = ""
existing_commits = ""
if log_path.exists():
content = log_path.read_text(encoding="utf-8")
if "## Tickets (issues)" in content:
idx = content.index("## Tickets (issues)")
end = content.find("\n## ", idx + 1)
if end == -1:
end = len(content)
existing_tickets = content[idx:end].strip()
if "## Commits" in content:
idx = content.index("## Commits")
end = content.find("\n## ", idx + 1)
if end == -1:
end = len(content)
existing_commits = content[idx:end].strip()
lines = [
f"# Fil — {thread_id}",
"",
"## Échanges reçus",
"",
]
for b in received_blocks:
lines.append(format_exchange_received(b))
lines.append("## Échanges envoyés")
lines.append("")
for b in sent_blocks:
lines.append(format_exchange_sent(b))
if existing_tickets:
lines.append(existing_tickets)
lines.append("")
else:
lines.append("## Tickets (issues)")
lines.append("")
lines.append("(aucun)")
lines.append("")
if existing_commits:
lines.append(existing_commits)
lines.append("")
else:
lines.append("## Commits")
lines.append("")
lines.append("(aucun)")
lines.append("")
log_path.write_text("\n".join(lines), encoding="utf-8")
return thread_id
def append_sent(
thread_id: str,
to_addr: str,
subject: str,
body: str = "",
date_str: str | None = None,
) -> None:
if not date_str:
date_str = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
block = {
"date": date_str,
"to": to_addr,
"subject": subject,
"body": body,
}
section = format_exchange_sent(block)
if not log_path.exists():
log_path.write_text(
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
"## Échanges envoyés\n\n" + section + "\n## Tickets (issues)\n\n(aucun)\n\n## Commits\n\n(aucun)\n",
encoding="utf-8",
)
return
content = log_path.read_text(encoding="utf-8")
insert_marker = "## Échanges envoyés"
idx = content.find(insert_marker)
if idx == -1:
content += "\n\n## Échanges envoyés\n\n" + section
else:
next_section = content.find("\n## ", idx + 1)
if next_section == -1:
content = content.rstrip() + "\n\n" + section
else:
content = (
content[:next_section].rstrip() + "\n\n" + section + content[next_section:]
)
log_path.write_text(content, encoding="utf-8")
def append_issue(thread_id: str, issue_num: str, title: str = "") -> None:
gitea = load_gitea_config()
base = f"{gitea['api_url'].replace('/api/v1', '')}/{gitea['owner']}/{gitea['repo']}/issues/{issue_num}"
line = f"- #{issue_num}" + (f"{title}" if title else "") + f" — <{base}>\n"
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
if not log_path.exists():
log_path.write_text(
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
"## Échanges envoyés\n\n(aucun)\n\n## Tickets (issues)\n\n" + line + "\n## Commits\n\n(aucun)\n",
encoding="utf-8",
)
return
content = log_path.read_text(encoding="utf-8")
marker = "## Tickets (issues)"
idx = content.find(marker)
if idx == -1:
content += "\n\n" + marker + "\n\n" + line
else:
end = idx + len(marker)
rest = content[end:]
if "(aucun)" in rest.split("\n## ")[0]:
content = content[:end] + "\n\n" + line + rest.replace("(aucun)\n", "", 1)
else:
content = content[:end] + "\n\n" + line + content[end:]
log_path.write_text(content, encoding="utf-8")
def append_commit(
thread_id: str,
commit_hash: str,
message: str,
branch: str = "",
) -> None:
line = f"- `{commit_hash[:12]}`"
if branch:
line += f" ({branch})"
line += f"{message.strip()}\n"
log_path = threads_dir() / f"{sanitize_thread_id(thread_id)}.md"
if not log_path.exists():
log_path.write_text(
f"# Fil — {thread_id}\n\n## Échanges reçus\n\n(aucun)\n\n"
"## Échanges envoyés\n\n(aucun)\n\n## Tickets (issues)\n\n(aucun)\n\n## Commits\n\n" + line,
encoding="utf-8",
)
return
content = log_path.read_text(encoding="utf-8")
marker = "## Commits"
idx = content.find(marker)
if idx == -1:
content += "\n\n" + marker + "\n\n" + line
else:
end = idx + len(marker)
rest = content[end:]
if "(aucun)" in rest.split("\n## ")[0]:
content = content[:end] + "\n\n" + line + rest.replace("(aucun)\n", "", 1)
else:
content = content[:end] + "\n\n" + line + content[end:]
log_path.write_text(content, encoding="utf-8")
def main() -> int:
ap = argparse.ArgumentParser(prog="mail-thread-log.py")
sub = ap.add_subparsers(dest="cmd", required=True)
p_get = sub.add_parser("get-id")
p_get.add_argument("--uid", required=True, help="Mail UID")
p_init = sub.add_parser("init")
p_init.add_argument("--uid", required=True, help="Mail UID")
p_sent = sub.add_parser("append-sent")
p_sent.add_argument("--thread-id", required=True)
p_sent.add_argument("--to", required=True, dest="to_addr")
p_sent.add_argument("--subject", required=True)
p_sent.add_argument("--body", default="")
p_sent.add_argument("--date", default=None)
p_issue = sub.add_parser("append-issue")
p_issue.add_argument("--thread-id", required=True)
p_issue.add_argument("--issue", required=True)
p_issue.add_argument("--title", default="")
p_commit = sub.add_parser("append-commit")
p_commit.add_argument("--thread-id", required=True)
p_commit.add_argument("--hash", required=True)
p_commit.add_argument("--message", required=True)
p_commit.add_argument("--branch", default="")
args = ap.parse_args()
if args.cmd == "get-id":
tid = get_thread_id_from_uid(args.uid)
print(f"THREAD_ID={tid}")
return 0
if args.cmd == "init":
tid = init_log(args.uid)
print(f"THREAD_ID={tid}")
return 0
if args.cmd == "append-sent":
append_sent(
args.thread_id,
args.to_addr,
args.subject,
args.body,
args.date,
)
return 0
if args.cmd == "append-issue":
append_issue(args.thread_id, args.issue, args.title)
return 0
if args.cmd == "append-commit":
append_commit(args.thread_id, args.hash, args.message, args.branch)
return 0
return 1
if __name__ == "__main__":
sys.exit(main())

13
gitea-issues/mail-thread-log.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Thread log: one file per thread under projects/<id>/logs/gitea-issues/threads/. Run from repo root.
# Usage:
# ./gitea-issues/mail-thread-log.sh get-id --uid <uid>
# ./gitea-issues/mail-thread-log.sh init --uid <uid>
# ./gitea-issues/mail-thread-log.sh append-sent --thread-id <id> --to <addr> --subject "..." [--body "..."] [--date "..."]
# ./gitea-issues/mail-thread-log.sh append-issue --thread-id <id> --issue <num> [--title "..."]
# ./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)}"
export GITEA_ISSUES_DIR
export REPO_ROOT="${GITEA_ISSUES_DIR}/.."
exec python3 "${GITEA_ISSUES_DIR}/mail-thread-log.py" "$@"

116
gitea-issues/mail-to-issue.py Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Create Gitea issues from unread emails via IMAP (e.g. Proton Mail Bridge).
**Preferred flow (agent-driven):** do not chain directly. Use mail-list-unread.sh
to list unread emails, then for each: formalize the issue or send a reply (mail-send-reply.sh);
only when a correction/evolution is ready, create the issue (mail-create-issue-from-email.sh
with optional formalized title/body), treat it (fix/evol), then comment on the issue and
reply to the email via the Bridge.
This script (mail-to-issue) is a **batch** fallback: it creates one issue per unread
message with title=subject and body=text+From, then marks messages as read. Use only
when the agent-driven flow is not used.
Reads IMAP config from .secrets/gitea-issues/imap-bridge.env (or env vars).
Reads Gitea token from GITEA_TOKEN or .secrets/gitea-issues/token.
"""
from __future__ import annotations
import email
import imaplib
import ssl
import sys
from email.header import decode_header
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,
sanitize_title,
)
def _decode_header_value(header: str | None) -> str:
if not header:
return ""
parts = decode_header(header)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def _get_text_body(msg: email.message.Message) -> str:
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
return payload.decode(part.get_content_charset() or "utf-8", errors="replace")
return ""
payload = msg.get_payload(decode=True)
if not payload:
return ""
return payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
def main() -> None:
imap_cfg = load_imap_config()
if not imap_cfg["user"] or not imap_cfg["password"]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
print("[gitea-issues] ERROR: IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
sys.exit(1)
gitea_cfg = load_gitea_config()
if not gitea_cfg["token"]:
print("[gitea-issues] ERROR: GITEA_TOKEN not set.", file=sys.stderr)
sys.exit(1)
mail = imaplib.IMAP4(imap_cfg["host"], int(imap_cfg["port"]))
if imap_cfg["use_starttls"]:
mail.starttls(ssl.create_default_context())
mail.login(imap_cfg["user"], imap_cfg["password"])
mail.select("INBOX")
criterion = imap_search_criterion_unseen()
_, nums = mail.search(None, criterion)
ids = nums[0].split()
if not ids:
print("[gitea-issues] No unread messages.")
mail.logout()
return
created = 0
for uid in ids:
uid_s = uid.decode("ascii")
_, data = mail.fetch(uid, "(RFC822)")
if not data or not data[0]:
continue
msg = email.message_from_bytes(data[0][1])
subject = _decode_header_value(msg.get("Subject"))
from_ = _decode_header_value(msg.get("From"))
body_text = _get_text_body(msg)
body_for_issue = f"**From:** {from_}\n\n{body_text}".strip()
title = sanitize_title(subject)
issue = create_gitea_issue(title, body_for_issue)
if issue:
created += 1
print(f"[gitea-issues] Created issue #{issue.get('number', '?')}: {title[:60]}")
mail.store(uid_s, "+FLAGS", "\\Seen")
else:
print(f"[gitea-issues] Skipped (API failed): {title[:60]}", file=sys.stderr)
mail.logout()
print(f"[gitea-issues] Done. Created {created} issue(s).")
if __name__ == "__main__":
main()

12
gitea-issues/mail-to-issue.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
#
# Create Gitea issues from unread emails (IMAP). Requires Proton Mail Bridge
# or any IMAP server. Config: .secrets/gitea-issues/imap-bridge.env and token.
# Usage: ./gitea-issues/mail-to-issue.sh
#
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}/mail-to-issue.py"

144
gitea-issues/mail_common.py Normal file
View File

@ -0,0 +1,144 @@
# Shared config and helpers for gitea-issues mail scripts (IMAP/SMTP, Gitea).
# Used by mail-list-unread, mail-send-reply, mail-create-issue-from-email, mail-mark-read.
from __future__ import annotations
import json
import os
import re
import ssl
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)
# so they do not depend on the parent project; same ia_dev works for any project (../ai_project_id).
env_root = os.environ.get("REPO_ROOT")
if env_root:
return Path(env_root).resolve()
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
if issues_dir:
return Path(issues_dir).resolve().parent
return Path(__file__).resolve().parent.parent
def load_env_file(path: Path) -> None:
if not path.is_file():
return
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip("'\"")
if key and key not in os.environ:
os.environ[key] = value
def load_imap_config() -> dict[str, str]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
load_env_file(env_path)
ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", "true").lower()
ssl_verify = ssl_verify_raw not in ("0", "false", "no")
return {
"host": os.environ.get("IMAP_HOST", "127.0.0.1"),
"port": os.environ.get("IMAP_PORT", "1143"),
"user": os.environ.get("IMAP_USER", ""),
"password": os.environ.get("IMAP_PASSWORD", ""),
"use_starttls": os.environ.get("IMAP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"),
"ssl_verify": ssl_verify,
"filter_to": os.environ.get("MAIL_FILTER_TO", "ai.support.lecoffreio@4nkweb.com").strip().lower(),
}
def imap_ssl_context(ssl_verify: bool = True) -> ssl.SSLContext:
"""Return SSL context for IMAP STARTTLS. Use ssl_verify=False only for local Bridge with self-signed cert."""
if ssl_verify:
return ssl.create_default_context()
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def load_smtp_config() -> dict[str, str]:
root = repo_root()
env_path = root / ".secrets" / "gitea-issues" / "imap-bridge.env"
load_env_file(env_path)
ssl_verify_raw = os.environ.get("IMAP_SSL_VERIFY", os.environ.get("SMTP_SSL_VERIFY", "true")).lower()
ssl_verify = ssl_verify_raw not in ("0", "false", "no")
return {
"host": os.environ.get("SMTP_HOST", os.environ.get("IMAP_HOST", "127.0.0.1")),
"port": os.environ.get("SMTP_PORT", "1025"),
"user": os.environ.get("SMTP_USER", os.environ.get("IMAP_USER", "")),
"password": os.environ.get("SMTP_PASSWORD", os.environ.get("IMAP_PASSWORD", "")),
"use_starttls": os.environ.get("SMTP_USE_STARTTLS", "true").lower() in ("1", "true", "yes"),
"ssl_verify": ssl_verify,
}
def load_gitea_config() -> dict[str, str]:
root = repo_root()
token = os.environ.get("GITEA_TOKEN")
if not token:
token_path = root / ".secrets" / "gitea-issues" / "token"
if token_path.is_file():
token = token_path.read_text(encoding="utf-8").strip()
return {
"api_url": os.environ.get("GITEA_API_URL", "https://git.4nkweb.com/api/v1").rstrip("/"),
"owner": os.environ.get("GITEA_REPO_OWNER", "4nk"),
"repo": os.environ.get("GITEA_REPO_NAME", "lecoffre_ng"),
"token": token or "",
}
def sanitize_title(raw: str, max_len: int = 200) -> str:
one_line = re.sub(r"\s+", " ", raw).strip()
return one_line[:max_len] if one_line else "(no subject)"
def create_gitea_issue(title: str, body: str) -> dict | None:
gitea = load_gitea_config()
if not gitea["token"]:
return None
url = f"{gitea['api_url']}/repos/{gitea['owner']}/{gitea['repo']}/issues"
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
req = Request(
url,
data=payload,
method="POST",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"token {gitea['token']}",
},
)
try:
with urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except (HTTPError, URLError):
return None

View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# Print issue number, title and body in a single block for agent consumption.
# Used by the gitea-issues-process agent to get the ticket content before calling /fix or /evol.
# Usage: ./print-issue-prompt.sh <issue_number>
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
require_jq || exit 1
if [[ $# -lt 1 ]]; then
log_err "Usage: $0 <issue_number>"
exit 1
fi
ISSUE_NUM="$1"
RESPONSE="$(gitea_api_get "/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/issues/${ISSUE_NUM}")"
if ! echo "$RESPONSE" | jq -e . &>/dev/null; then
log_err "API error (issue ${ISSUE_NUM}): ${RESPONSE:0:200}"
exit 1
fi
TITLE="$(echo "$RESPONSE" | jq -r '.title')"
BODY="$(echo "$RESPONSE" | jq -r '.body // "(no description)"')"
LABELS="$(echo "$RESPONSE" | jq -r '[.labels[].name] | join(", ")')"
echo "Issue #${ISSUE_NUM}"
echo "Title: ${TITLE}"
echo "Labels: ${LABELS}"
echo ""
echo "${BODY}"

View File

@ -0,0 +1,174 @@
# Load project config (projects/<id>/conf.json) for tickets spooler and authorized_emails.
# Requires PROJECT_ROOT (repo root with ai_project_id) and GITEA_ISSUES_DIR (ia_dev/gitea-issues).
from __future__ import annotations
import json
import os
from pathlib import Path
def project_root() -> Path:
"""Project repo root (parent of ia_dev). Where data/issues/ and ai_project_id live."""
env_root = os.environ.get("PROJECT_ROOT")
if env_root:
return Path(env_root).resolve()
env_repo = os.environ.get("REPO_ROOT")
if env_repo:
root = Path(env_repo).resolve()
# If REPO_ROOT is ia_dev, project root is parent
if (root / "gitea-issues").is_dir():
return root.parent
return root
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
if issues_dir:
return Path(issues_dir).resolve().parent.parent
return Path(__file__).resolve().parent.parent.parent
def ia_dev_root() -> Path:
"""Directory containing gitea-issues (ia_dev)."""
issues_dir = os.environ.get("GITEA_ISSUES_DIR")
if issues_dir:
return Path(issues_dir).resolve().parent
return Path(__file__).resolve().parent.parent
def load_project_config() -> dict | None:
"""Load projects/<slug>/conf.json. Returns None if not found or slug missing."""
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 = os.environ.get("IA_PROJECT", "").strip() if os.environ.get("IA_PROJECT") else None
if not slug and slug_path.is_file():
slug = slug_path.read_text(encoding="utf-8").strip()
if not slug:
return None
conf_path = ia_dev / "projects" / slug / "conf.json"
if not conf_path.is_file():
return None
with open(conf_path, encoding="utf-8") as f:
return json.load(f)
def project_dir() -> Path | None:
"""Path to projects/<id>/ (under ia_dev). None if project config not found."""
root = project_root()
ia_dev = ia_dev_root()
slug = os.environ.get("IA_PROJECT", "").strip()
if not slug and (root / "ai_project_id").is_file():
slug = (root / "ai_project_id").read_text(encoding="utf-8").strip()
if not slug and (root / ".ia_project").is_file():
slug = (root / ".ia_project").read_text(encoding="utf-8").strip()
if not slug:
return None
return ia_dev / "projects" / slug
def data_issues_dir() -> Path:
"""Path to data/issues/ spooler under projects/<id>/ (ia_dev/projects/<id>/data/issues)."""
pd = project_dir()
if pd is not None:
return pd / "data" / "issues"
return project_root() / "data" / "issues"
def data_issues_dir_for_project(project_id: str) -> Path:
"""Path to data/issues/ for a given project id (ia_dev/projects/<id>/data/issues)."""
ia_dev = ia_dev_root()
return ia_dev / "projects" / project_id / "data" / "issues"
def project_logs_dir() -> Path:
"""Path to logs/ under projects/<id>/ (ia_dev/projects/<id>/logs)."""
pd = project_dir()
if pd is not None:
return pd / "logs"
return project_root() / "logs"
def authorized_emails() -> dict[str, str | list[str]]:
"""Return tickets.authorized_emails (to, from list). Empty dict if missing."""
conf = load_project_config()
if not conf:
return {}
tickets = conf.get("tickets") or {}
return tickets.get("authorized_emails") or {}
def list_project_ids() -> list[str]:
"""List all project ids (directory names under projects/)."""
ia_dev = ia_dev_root()
projects_dir = ia_dev / "projects"
if not projects_dir.is_dir():
return []
return [d.name for d in projects_dir.iterdir() if d.is_dir() and (d / "conf.json").is_file()]
def resolve_project_id_by_email_to(to_address: str) -> str | None:
"""Find project id whose tickets.authorized_emails.to matches the given address (case-insensitive)."""
if not to_address or not to_address.strip():
return None
to_normalized = to_address.strip().lower()
for pid in list_project_ids():
conf_path = ia_dev_root() / "projects" / pid / "conf.json"
try:
with open(conf_path, encoding="utf-8") as f:
conf = json.load(f)
except (OSError, json.JSONDecodeError):
continue
tickets = conf.get("tickets") or {}
auth = tickets.get("authorized_emails") or {}
conf_to = (auth.get("to") or "").strip().lower()
if conf_to == to_normalized:
return pid
return None
def _list_project_envs(project_id: str) -> list[str]:
"""List env names (subdirs of .secrets) for a project that contain ia_token."""
ia_dev = ia_dev_root()
secrets_dir = ia_dev / "projects" / project_id / ".secrets"
if not secrets_dir.is_dir():
return []
return [
d.name
for d in secrets_dir.iterdir()
if d.is_dir() and (d / "ia_token").is_file()
]
def resolve_project_and_env_by_token(token: str) -> tuple[str, str] | None:
"""Find (project_id, env) by scanning projects/<id>/.secrets/<env>/ia_token. Returns first match."""
if not token or not token.strip():
return None
token_stripped = token.strip()
for pid in list_project_ids():
for env in _list_project_envs(pid):
token_path = ia_dev_root() / "projects" / pid / ".secrets" / env / "ia_token"
try:
content = token_path.read_text(encoding="utf-8").strip()
# Token is either full value in file or base + env (e.g. nicolecoffreio<env>)
if content == token_stripped or (content + env) == token_stripped:
return (pid, env)
except (OSError, UnicodeDecodeError):
continue
return None
def resolve_project_id_by_token(token: str) -> str | None:
"""Find project id whose .secrets/<env>/ia_token matches the given token."""
resolved = resolve_project_and_env_by_token(token)
return resolved[0] if resolved else None
def load_project_config_by_id(project_id: str) -> dict | None:
"""Load conf.json for a given project id. Returns None if not found."""
ia_dev = ia_dev_root()
conf_path = ia_dev / "projects" / project_id / "conf.json"
if not conf_path.is_file():
return None
with open(conf_path, encoding="utf-8") as f:
return json.load(f)

View File

@ -0,0 +1,321 @@
#!/usr/bin/env python3
"""
Fetch inbox emails and route each message to the project whose tickets.authorized_emails.to matches the message To.
Project is resolved per message: To/Delivered-To/X-Original-To are compared to tickets.authorized_emails.to in each
projects/<id>/conf.json; the first matching project id is used. Only messages from authorized_emails.from are kept.
Messages on or after MAIL_SINCE_DATE are considered. Does not use UNSEEN; does not mark as read (BODY.PEEK[]).
Writes to projects/<id>/data/issues/ as JSON <date>.<msg_id>.<from>.pending. One file per message.
State: we skip creating .pending if .pending exists or .response exists for that base.
Usage: run with GITEA_ISSUES_DIR set (e.g. via tickets-fetch-inbox.sh). MAIL_SINCE_DATE overrides date (DD-Mon-YYYY).
"""
from __future__ import annotations
import email
import hashlib
import imaplib
import json
import os
import re
import sys
from datetime import datetime, timezone
from email.header import decode_header
from email.utils import parsedate_to_datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mail_common import imap_search_criterion_all, load_imap_config, imap_ssl_context
from project_config import (
data_issues_dir_for_project,
ia_dev_root,
load_project_config_by_id,
project_root,
resolve_project_id_by_email_to,
)
def decode_header_value(header: str | None) -> str:
if not header:
return ""
parts = decode_header(header)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def parse_from_address(from_header: str) -> str:
"""Extract email address from From header (e.g. 'Name <user@host>' -> user@host)."""
if not from_header:
return ""
match = re.search(r"<([^>]+)>", from_header)
if match:
return match.group(1).strip().lower()
return from_header.strip().lower()
def get_text_body(msg: email.message.Message) -> str:
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
return payload.decode(
part.get_content_charset() or "utf-8", errors="replace"
)
return ""
payload = msg.get_payload(decode=True)
if not payload:
return ""
return payload.decode(
msg.get_content_charset() or "utf-8", errors="replace"
)
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 get_message_to_addresses(msg: email.message.Message) -> list[str]:
"""Return ordered list of recipient addresses (To, Delivered-To, X-Original-To, etc.) for project resolution."""
order = ("To", "Delivered-To", "X-Original-To", "X-Delivered-To", "X-Envelope-To", "Envelope-To")
seen: set[str] = set()
result: list[str] = []
for name in order:
value = msg.get(name)
if not value:
continue
addrs = _extract_addresses(decode_header_value(value))
for a in addrs:
if a not in seen:
seen.add(a)
result.append(a)
return result
def sanitize_from_for_filename(email_addr: str) -> str:
"""Filesystem-safe string from email (e.g. user@example.com -> user_example.com)."""
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():
return "attachment"
base = Path(name).name
return re.sub(r"[^a-zA-Z0-9._-]", "_", base)[:200] or "attachment"
def get_attachments(msg: email.message.Message) -> list[tuple[str, bytes, str]]:
"""Return list of (filename, payload_bytes, content_type) for each attachment."""
result: list[tuple[str, bytes, str]] = []
for part in msg.walk():
content_type = (part.get_content_type() or "").lower()
if content_type.startswith("multipart/"):
continue
filename = part.get_filename()
if not filename:
# Optional: treat inline images etc. with Content-Disposition attachment
disp = part.get("Content-Disposition") or ""
if "attachment" in disp.lower():
ext = ""
if "image" in content_type:
ext = ".bin" if "octet-stream" in content_type else ".img"
filename = f"attachment{ext}"
else:
continue
filename = decode_header_value(filename).strip()
if not filename:
continue
payload = part.get_payload(decode=True)
if payload is None:
continue
result.append((filename, payload, content_type))
return result
def parse_references(refs: str | None) -> list[str]:
if not refs:
return []
return [x.strip() for x in re.split(r"\s+", refs) if x.strip()]
def main() -> int:
cfg = load_imap_config()
if not cfg["user"] or not cfg["password"]:
print("[tickets-fetch-inbox] IMAP_USER and IMAP_PASSWORD required.", file=sys.stderr)
return 1
# Spool is per-project; each message is routed by its To address to projects/<id>/data/issues/
print("[tickets-fetch-inbox] Project resolved per message from To/Delivered-To/X-Original-To (tickets.authorized_emails.to).")
mail = imaplib.IMAP4(cfg["host"], int(cfg["port"]))
if cfg["use_starttls"]:
mail.starttls(imap_ssl_context(cfg.get("ssl_verify", True)))
mail.login(cfg["user"], cfg["password"])
mail.select("INBOX")
# 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_no_project = 0
skipped_from = 0
skipped_pending = 0
skipped_response = 0
for uid in ids:
uid_s = uid.decode("ascii")
_, 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
to_addresses = get_message_to_addresses(msg)
project_id: str | None = None
for addr in to_addresses:
project_id = resolve_project_id_by_email_to(addr)
if project_id:
break
if not project_id:
skipped_no_project += 1
continue
conf = load_project_config_by_id(project_id)
if not conf:
skipped_no_project += 1
continue
auth = (conf.get("tickets") or {}).get("authorized_emails") or {}
from_list = auth.get("from")
if isinstance(from_list, list):
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()
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
spool = data_issues_dir_for_project(project_id)
spool.mkdir(parents=True, exist_ok=True)
mid = (msg.get("Message-ID") or "").strip()
to_raw = decode_header_value(msg.get("To"))
to_addrs = [a.strip() for a in re.split(r"[,;]", to_raw) if a.strip()]
subj = decode_header_value(msg.get("Subject"))
date_h = decode_header_value(msg.get("Date"))
refs = parse_references(msg.get("References"))
in_reply_to = (msg.get("In-Reply-To") or "").strip() or None
body = get_text_body(msg)
try:
if date_h:
dt = parsedate_to_datetime(date_h)
date_str = dt.strftime("%Y-%m-%dT%H%M%S")
else:
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
except Exception:
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
from_safe = sanitize_from_for_filename(from_addr)
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]] = []
attachment_parts = get_attachments(msg)
if attachment_parts:
att_dir = spool / f"{base}.d"
att_dir.mkdir(parents=True, exist_ok=True)
for idx, (orig_name, payload_bytes, content_type) in enumerate(attachment_parts):
safe_name = sanitize_attachment_filename(orig_name)
stored_name = f"{idx}_{safe_name}"
stored_path = att_dir / stored_name
stored_path.write_bytes(payload_bytes)
rel_path = f"{base}.d/{stored_name}"
attachments_meta.append({
"filename": orig_name,
"path": rel_path,
"content_type": content_type,
"size": len(payload_bytes),
})
payload = {
"version": 1,
"type": "incoming",
"id": msg_id_short,
"message_id": mid or "",
"from": from_addr,
"to": to_addrs,
"subject": subj,
"date": date_h or "",
"body": body or "",
"references": refs,
"in_reply_to": in_reply_to,
"uid": uid_s,
"created_at": created_at,
"issue_number": None,
"status": "pending",
"attachments": attachments_meta,
}
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
written += 1
print(f"[tickets-fetch-inbox] Wrote {path.name}")
mail.logout()
print(f"[tickets-fetch-inbox] Done. Wrote {written} new message(s).")
if skipped_fetch or skipped_no_project or skipped_from or skipped_pending or skipped_response:
print(
f"[tickets-fetch-inbox] Skipped: fetch/parse={skipped_fetch}, no_project_for_to={skipped_no_project}, "
f"from_not_allowed={skipped_from}, pending_exists={skipped_pending}, response_exists={skipped_response}."
)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +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/ 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)}"
export GITEA_ISSUES_DIR
# 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" "$@"

88
gitea-issues/wiki-api-test.sh Executable file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Test Gitea Wiki API for repo 4nk/lecoffre_ng.
# Requires GITEA_TOKEN or .secrets/gitea-issues/token (same as issues scripts).
# Usage: ./wiki-api-test.sh [--create]
# --create: create a test page then delete it (checks write access).
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
# Branch ref for wiki (default branch of wiki repo; use master when wiki is configured on master)
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
WIKI_PAGES="${REPO_PATH}/wiki/pages?ref=${GITEA_WIKI_REF}"
WIKI_PAGE="${REPO_PATH}/wiki/page"
WIKI_NEW="${REPO_PATH}/wiki/new"
do_create=false
while [[ $# -gt 0 ]]; do
case "$1" in
--create) do_create=true; shift ;;
*) log_err "Unknown option: $1"; exit 1 ;;
esac
done
if ! load_gitea_token 2>/dev/null; then
log_err "No GITEA_TOKEN and no .secrets/gitea-issues/token. Set token to run wiki API tests."
exit 1
fi
require_jq || exit 1
echo "=== 1. GET ${WIKI_PAGES} (list wiki pages) ==="
RESPONSE="$(gitea_api_get "${WIKI_PAGES}")"
if echo "$RESPONSE" | jq -e . &>/dev/null; then
if echo "$RESPONSE" | jq -e 'type == "array"' &>/dev/null; then
COUNT="$(echo "$RESPONSE" | jq 'length')"
log_info "List OK: ${COUNT} page(s)"
echo "$RESPONSE" | jq -r '.[] | " - \(.title)"' 2>/dev/null || echo "$RESPONSE" | jq .
else
log_info "Response: $(echo "$RESPONSE" | jq -c . 2>/dev/null || echo "$RESPONSE")"
fi
else
log_err "Response (first 300 chars): ${RESPONSE:0:300}"
fi
echo ""
echo "=== 2. GET ${WIKI_PAGE}/Home (get one page, ref=${GITEA_WIKI_REF}) ==="
RESPONSE="$(gitea_api_get "${WIKI_PAGE}/Home?ref=${GITEA_WIKI_REF}")"
if echo "$RESPONSE" | jq -e .title &>/dev/null; then
log_info "Page OK: title=$(echo "$RESPONSE" | jq -r .title)"
echo "$RESPONSE" | jq '{ title, html_url, commit_count }'
else
log_info "Response: $(echo "$RESPONSE" | jq -c . 2>/dev/null || echo "${RESPONSE:0:200}")"
fi
if [[ "$do_create" != true ]]; then
log_info "Done. Use --create to test POST wiki page and DELETE."
exit 0
fi
echo ""
echo "=== 3. POST ${WIKI_NEW} (create test page) ==="
TEST_TITLE="Api-test-$(date +%s)"
CONTENT="# Test\nCreated by wiki-api-test.sh. Safe to delete."
CONTENT_B64="$(echo -n "$CONTENT" | base64 -w 0)"
BODY="$(jq -n --arg title "$TEST_TITLE" --arg content "$CONTENT_B64" --arg msg "wiki-api-test.sh" \
'{ title: $title, content_base64: $content, message: $msg }')"
RESPONSE="$(gitea_api_post "${WIKI_NEW}" "$BODY")"
if echo "$RESPONSE" | jq -e .title &>/dev/null; then
log_info "Create OK: $(echo "$RESPONSE" | jq -r .title)"
CREATED_TITLE="$TEST_TITLE"
else
log_err "Create failed: ${RESPONSE:0:300}"
exit 1
fi
echo ""
echo "=== 4. DELETE ${WIKI_PAGE}/${CREATED_TITLE} (remove test page) ==="
RESPONSE="$(gitea_api_delete "${WIKI_PAGE}/${CREATED_TITLE}")"
# DELETE often returns 204 No Content
log_info "Delete sent (204 or empty body = success)."
echo ""
log_info "All wiki API tests completed."

34
gitea-issues/wiki-get-page.sh Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
#
# Output the raw markdown of a wiki page (for agents or scripts).
# Usage: ./wiki-get-page.sh <page_name>
# Example: ./wiki-get-page.sh Home
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
if [[ $# -lt 1 ]]; then
log_err "Usage: $0 <page_name>"
exit 1
fi
PAGE_NAME="$1"
load_gitea_token || exit 1
require_jq || exit 1
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${PAGE_NAME}?ref=${GITEA_WIKI_REF}")"
if ! echo "$resp" | jq -e .content_base64 &>/dev/null; then
log_err "Page not found or error: ${PAGE_NAME}"
echo "$resp" | jq . 2>/dev/null || echo "$resp"
exit 1
fi
echo "$resp" | jq -r '.content_base64' | base64 -d
echo

100
gitea-issues/wiki-migrate-docs.sh Executable file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env bash
#
# Migrate all docs/*.md (repo root) to Gitea wiki as pages.
# Mapping: docs/FILE.md → page "File" (stem with _ → -, first letter upper per segment).
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
# Usage: ./wiki-migrate-docs.sh [--dry-run] [file.md ...]
# --dry-run: print mapping and skip API calls.
# If file(s) given: migrate only those; else migrate all docs/*.md.
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
REPO_ROOT="${GITEA_ISSUES_DIR}/.."
DOCS_DIR="${REPO_ROOT}/docs"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
WIKI_PAGE="${REPO_PATH}/wiki/page"
WIKI_NEW="${REPO_PATH}/wiki/new"
# docs/FILE.md → page name for wiki (stem: _ → -, title-case: First-Letter-Of-Each-Segment)
file_to_page_name() {
local base="$1"
local stem="${base%.md}"
echo "$stem" | tr '_' '-' | awk -F- '{
for(i=1;i<=NF;i++) {
s = $i; l = length(s)
if (l > 0) $i = toupper(substr(s,1,1)) tolower(substr(s,2))
}
}1' OFS='-'
}
dry_run=false
files=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
*.md) files+=("$1"); shift ;;
*) log_err "Unknown option or not .md: $1"; exit 1 ;;
esac
done
if [[ ${#files[@]} -eq 0 ]]; then
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$DOCS_DIR" -maxdepth 1 -name '*.md' -print0 | sort -z)
else
# Resolve args to full paths under DOCS_DIR
for i in "${!files[@]}"; do
u="${files[$i]}"
if [[ "$u" != */* ]] && [[ -f "${DOCS_DIR}/${u}" ]]; then
files[$i]="${DOCS_DIR}/${u}"
fi
done
fi
if [[ ${#files[@]} -eq 0 ]]; then
log_err "No .md files found in ${DOCS_DIR}"
exit 1
fi
if [[ "$dry_run" == true ]]; then
log_info "Dry run: would migrate ${#files[@]} file(s)"
for f in "${files[@]}"; do
base="$(basename "$f")"
page="$(file_to_page_name "$base")"
echo " $f$page"
done
exit 0
fi
load_gitea_token || exit 1
require_jq || exit 1
for f in "${files[@]}"; do
base="$(basename "$f")"
page="$(file_to_page_name "$base")"
if [[ ! -f "$f" ]]; then
log_err "Skip (not a file): $f"
continue
fi
content="$(cat "$f")"
content_b64="$(echo -n "$content" | base64 -w 0)"
body="$(jq -n --arg title "$page" --arg content "$content_b64" --arg msg "Migrate from docs/$base" \
'{ title: $title, content_base64: $content, message: $msg }')"
# Check if page exists (GET); if 200 use PATCH else POST
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${page}?ref=${GITEA_WIKI_REF}")"
if echo "$resp" | jq -e .title &>/dev/null; then
log_info "Update: $base$page"
patch_body="$(jq -n --arg content "$content_b64" --arg msg "Update from docs/$base" '{ content_base64: $content, message: $msg }')"
gitea_api_patch "${WIKI_PAGE}/${page}?ref=${GITEA_WIKI_REF}" "$patch_body" >/dev/null || true
else
log_info "Create: $base$page"
gitea_api_post "${WIKI_NEW}" "$body" >/dev/null || true
fi
done
log_info "Migration done: ${#files[@]} file(s)."

46
gitea-issues/wiki-put-page.sh Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
#
# Update a single wiki page from a local file.
# Usage: ./wiki-put-page.sh <page_name> <file_path>
# Example: ./wiki-put-page.sh Home docs/README.md
# Requires GITEA_TOKEN or .secrets/gitea-issues/token.
#
set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh"
REPO_PATH="/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}"
GITEA_WIKI_REF="${GITEA_WIKI_REF:-master}"
WIKI_PAGE="${REPO_PATH}/wiki/page"
WIKI_NEW="${REPO_PATH}/wiki/new"
if [[ $# -lt 2 ]]; then
log_err "Usage: $0 <page_name> <file_path>"
exit 1
fi
PAGE_NAME="$1"
FILE_PATH="$2"
[[ -f "$FILE_PATH" ]] || { log_err "File not found: $FILE_PATH"; exit 1; }
load_gitea_token || exit 1
require_jq || exit 1
content="$(cat "$FILE_PATH")"
content_b64="$(echo -n "$content" | base64 -w 0)"
msg="Update from $FILE_PATH"
resp="$(gitea_api_get "${REPO_PATH}/wiki/page/${PAGE_NAME}?ref=${GITEA_WIKI_REF}")"
if echo "$resp" | jq -e .title &>/dev/null; then
log_info "PATCH ${PAGE_NAME}"
body="$(jq -n --arg title "$PAGE_NAME" --arg content "$content_b64" --arg msg "$msg" '{ title: $title, content_base64: $content, message: $msg }')"
gitea_api_patch "${WIKI_PAGE}/${PAGE_NAME}?ref=${GITEA_WIKI_REF}" "$body"
else
log_info "POST ${PAGE_NAME}"
body="$(jq -n --arg title "$PAGE_NAME" --arg content "$content_b64" --arg msg "$msg" '{ title: $title, content_base64: $content, message: $msg }')"
gitea_api_post "${WIKI_NEW}" "$body"
fi
log_info "Done: ${PAGE_NAME}"

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" "$@"

13
lib/README.md Normal file
View File

@ -0,0 +1,13 @@
# ia_dev shared lib
## project_config.sh
Sourced by deploy scripts and gitea-issues to resolve the current project **id** and the path to its JSON config.
**Before sourcing:** set `PROJECT_ROOT` (git repo root, where `ai_project_id` or `.ia_project` lives) and `IA_DEV_ROOT` (path to the `ia_dev` directory).
**After sourcing:** `PROJECT_SLUG` and `PROJECT_CONFIG_PATH` are set (and exported). Config path is `projects/<id>/conf.json`.
**Project id resolution order:** `IA_PROJECT` env → `.ia_project` at PROJECT_ROOT → `ai_project_id` at PROJECT_ROOT.
See `projects/README.md` for the config schema.

68
lib/project_config.sh Normal file
View File

@ -0,0 +1,68 @@
#
# Project config resolution for ia_dev scripts.
# Source this after setting PROJECT_ROOT and IA_DEV_ROOT.
# Resolves PROJECT_SLUG (id) and PROJECT_CONFIG_PATH (projects/<id>/conf.json).
#
# Project id resolution order:
# 1. MAIL_TO (env): search all projects for tickets.authorized_emails.to == MAIL_TO
# 2. AI_AGENT_TOKEN (env): search all projects/.secrets/<env>/ia_token for matching token; sets PROJECT_SLUG and PROJECT_ENV
# 3. IA_PROJECT (env)
# 4. .ia_project at PROJECT_ROOT (one line)
# 5. ai_project_id at PROJECT_ROOT (one line)
#
# Config file: projects/<id>/conf.json.
#
set -euo pipefail
PROJECT_SLUG=""
if [[ -n "${MAIL_TO:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
_to="$(echo "${MAIL_TO}" | sed 's/[[:space:]]//g' | tr '[:upper:]' '[:lower:]')"
for conf in "${IA_DEV_ROOT}/projects/"*/conf.json; do
[[ -f "$conf" ]] || continue
_to_conf="$(jq -r '.tickets.authorized_emails.to // ""' "$conf" 2>/dev/null | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]//g')"
if [[ -n "$_to_conf" && "$_to_conf" = "$_to" ]]; then
PROJECT_SLUG="$(basename "$(dirname "$conf")")"
break
fi
done
fi
PROJECT_ENV=""
if [[ -z "$PROJECT_SLUG" && -n "${AI_AGENT_TOKEN:-}" && -n "${IA_DEV_ROOT:-}" ]]; then
_token="$(echo "${AI_AGENT_TOKEN}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
for _pdir in "${IA_DEV_ROOT}/projects/"*/; do
[[ -d "${_pdir}.secrets" ]] || continue
_project="$(basename "$_pdir")"
for _envdir in "${_pdir}.secrets/"*/; do
[[ -f "${_envdir}ia_token" ]] || continue
_env="$(basename "$_envdir")"
_tok_conf="$(cat "${_envdir}ia_token" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
# Token is either full value in file or base + env (e.g. nicolecoffreio<env>)
if [[ -n "$_tok_conf" && ( "$_tok_conf" = "$_token" || "${_tok_conf}${_env}" = "$_token" ) ]]; then
PROJECT_SLUG="$_project"
PROJECT_ENV="$_env"
break 2
fi
done
done
fi
export PROJECT_ENV
if [[ -z "$PROJECT_SLUG" && -n "${IA_PROJECT:-}" ]]; then
PROJECT_SLUG="$(echo "${IA_PROJECT}" | sed 's/[[:space:]]//g')"
fi
if [[ -z "$PROJECT_SLUG" && -n "${PROJECT_ROOT:-}" && -f "$PROJECT_ROOT/.ia_project" ]]; then
PROJECT_SLUG="$(cat "$PROJECT_ROOT/.ia_project" | sed 's/[[:space:]]//g')"
fi
if [[ -z "$PROJECT_SLUG" && -n "${PROJECT_ROOT:-}" && -f "$PROJECT_ROOT/ai_project_id" ]]; then
PROJECT_SLUG="$(cat "$PROJECT_ROOT/ai_project_id" | sed 's/[[:space:]]//g')"
fi
PROJECT_CONFIG_PATH=""
if [[ -n "$PROJECT_SLUG" && -n "${IA_DEV_ROOT:-}" ]]; then
PROJECT_CONFIG_PATH="${IA_DEV_ROOT}/projects/${PROJECT_SLUG}/conf.json"
if [[ ! -f "$PROJECT_CONFIG_PATH" ]]; then
PROJECT_CONFIG_PATH=""
fi
fi
export PROJECT_SLUG
export PROJECT_CONFIG_PATH

53
projects/README.md Normal file
View File

@ -0,0 +1,53 @@
# Project-specific configuration
This repo (`ia_dev`) is intended to be used as a **git submodule** inside each project. Project-specific parameters are stored in `projects/<id>/conf.json` (e.g. `projects/lecoffreio/conf.json`). The `<id>` is the project identifier and the name of the directory under `projects/`.
## Current project selection
There is no longer a "slug" in the URL or path; the project **id** (directory name under `projects/`) is resolved by:
**1. Mail ticketing**
From the **To** address of an email: search all `projects/*/conf.json` for `tickets.authorized_emails.to` equal to that address (case-insensitive). The matching project directory name is the id.
**2. API ai_working_help**
The token is found by scanning as described above: traverse all projects and all envs, reading each `projects/<id>/.secrets/<env>/ia_token` and comparing its content (or content + env) to the Bearer token. The first match yields the project id and the env (e.g. `test`, `pprod`, `prod`).
**3. Scripts / fallback** (when no request context)
- **`MAIL_TO`** (env): same as (1), resolve id by email "to".
- **`AI_AGENT_TOKEN`** (env): same as (2), resolve id (and env) by token; sets `PROJECT_SLUG` and `PROJECT_ENV`.
- **`IA_PROJECT`** (env)
- **`.ia_project`** at repository root (one line)
- **`ai_project_id`** at repository root (one line). When `ia_dev` is a submodule, this file lives at the host repo root (parent of `ia_dev`).
When running from a repo that has `ia_dev` as a submodule, the root is the parent repo; the script resolves `ia_dev` either as `./ia_dev` or `./deploy` (symlink to `ia_dev/deploy`).
## Schema
One JSON file per project: `projects/<id>/conf.json` (e.g. `projects/lecoffreio/conf.json`). The `<id>` is the directory name; the config file is always named `conf.json`.
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Human-readable project name |
| `project_path` | no | Relative path to project from ia_dev (e.g. `../lecoffre_ng_test`); used when running from ia_dev standalone |
| `build_dirs` | no | List of directories (relative to repo root) where `npm run build` is run before push. If missing or empty, build check is skipped |
| `version` | no | Version/bump configuration |
| `version.package_json_paths` | no | List of paths (relative to repo root) to `package.json` files to update on bump |
| `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). The **to** address is used to resolve the project id when processing mails. See projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md. |
The API token for ai_working_help is **not** in conf.json. It is found by scanning all projects and all envs (as described above): each file `projects/<id>/.secrets/<env>/ia_token` is read and the Bearer is compared to the file content or to (file content + env). Token value is **base** + **env**; **env** is the environment name (test, pprod, prod), to be adapted per environment. The first match gives project id and env.
## Example (minimal)
```json
{
"name": "My App",
"build_dirs": ["backend", "frontend"]
}
```
## Example (full)
See `projects/lecoffreio/conf.json`.

37
projects/algo/conf.json Normal file
View File

@ -0,0 +1,37 @@
{
"id": "algo",
"name": "algo",
"project_path": "/home/desk/code/algo/deploy",
"build_dirs": [
"/home/desk/code/algo/deploy/lecoffre-ressources-dev",
"/home/desk/code/algo/deploy/lecoffre-back-main",
"/home/desk/code/algo/deploy/lecoffre-front-main"
],
"deploy": {
"scripts_path": "/home/desk/code/algo/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/algo/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/algo/.secrets"
},
"version": {
"package_json_paths": [
"/home/desk/code/algo/deploy/lecoffre-back-main/package.json",
"/home/desk/code/algo/deploy/lecoffre-front-main/package.json"
],
"splash_app_name": "algo"
},
"mail": {
"email": "ai.support.algo@4nkweb.com",
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
},
"git": {
"wiki_url": "https://git.4nkweb.com/nicolas.cantu/algo/wiki",
"token_file": ".secrets/gitea-issues/token"
},
"tickets": {
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/algo/issues",
"authorized_emails": {
"to": "ai.support.algo@4nkweb.com",
"from": []
}
}
}

37
projects/enso/conf.json Normal file
View File

@ -0,0 +1,37 @@
{
"id": "enso",
"name": "enso",
"project_path": "/home/desk/code/enso/deploy",
"build_dirs": [
"/home/desk/code/enso/deploy/lecoffre-ressources-dev",
"/home/desk/code/enso/deploy/lecoffre-back-main",
"/home/desk/code/enso/deploy/lecoffre-front-main"
],
"deploy": {
"scripts_path": "/home/desk/code/enso/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/enso/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/enso/.secrets"
},
"version": {
"package_json_paths": [
"/home/desk/code/enso/deploy/lecoffre-back-main/package.json",
"/home/desk/code/enso/deploy/lecoffre-front-main/package.json"
],
"splash_app_name": "enso"
},
"mail": {
"email": "ai.support.enso@4nkweb.com",
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
},
"git": {
"wiki_url": "https://git.4nkweb.com/nicolas.cantu/enso/wiki",
"token_file": ".secrets/gitea-issues/token"
},
"tickets": {
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/enso/issues",
"authorized_emails": {
"to": "ai.support.enso@4nkweb.com",
"from": []
}
}
}

27
projects/ia_dev/conf.json Normal file
View File

@ -0,0 +1,27 @@
{
"id": "ia_dev",
"name": "ia_dev",
"project_path": "../",
"base_path": "ia_dev",
"build_dirs": [],
"deploy": {},
"version": {
"package_json_paths": [],
"splash_app_name": "ia_dev"
},
"mail": {
"email": "ai.support.ia_dev@4nkweb.com",
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
},
"git": {
"wiki_url": "https://git.4nkweb.com/4nk/ia_dev/wiki",
"token_file": ".secrets/gitea-issues/token"
},
"tickets": {
"ticketing_url": "https://git.4nkweb.com/4nk/ia_dev/issues",
"authorized_emails": {
"to": "ai.support.ia_dev@4nkweb.com",
"from": ["nicolas.cantu@pm.me"]
}
}
}

View File

@ -0,0 +1,159 @@
# 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.
## Contraintes de confidentialité et d'accès des agents
L'agent ne doit **jamais révéler** :
- Quelle IA il utilise
- Des informations personnelles
- Des informations sur 4NK
- Des informations sur l'équipe de réalisation
- Des informations sur des fichiers hors projet parent
- Des informations sur les autres questions et réponses traitées
L'agent ne doit **jamais accéder ni modifier** :
- Les fichiers hors projet (périmètre = dépôt parent et ia_dev dans ce dépôt)
- Les fichiers dans `.cursor` et `.secrets`
- D'autres branches que celle en cours de travail
- D'autres environnements (test / pprod / prod) que celui défini par le contexte
## Agents (.cursor/agents/)
| Agent | Fichier | Rôle |
|-------|---------|------|
| **agent-loop** | `agent-loop.md` | Orchestre la boucle de récupération des mails et le traitement par **exécutions délimitées** uniquement (N itérations ou x cycles) ; ne lance jamais de processus en arrière-plan (nohup/&). |
| **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 : `projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`. Récupération : `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`. Référence boucle mails (legacy) : `.cursor/agents/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 d'environnement `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 d'une issue (JSON ou résumé texte). |
| `print-issue-prompt.sh` | `./gitea-issues/print-issue-prompt.sh <num>` | Affiche titre + corps pour fournir la consigne à l'agent. |
| `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 à l'issue. |
| `mail-list-unread.sh` | `./gitea-issues/mail-list-unread.sh` | Liste les mails **non lus envoyés à l'alias** (MAIL_FILTER_TO), **à partir du 10 mars 2026** (MAIL_SINCE_DATE) ; 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 d'un 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** : tourne indéfiniment (spooler ou legacy). **Ne pas lancer depuis l'agent** ; utiliser agent-loop-chat-iterations.sh ou cycles délimités. Voir `.cursor/agents/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. Tourne indéfiniment ; **ne pas lancer depuis l'agent** (préférer agent-loop-chat-iterations.sh ou cycles délimités). |
| `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 ». |
| `agent-loop-lock-acquire.sh` | `./ia_dev/gitea-issues/agent-loop-lock-acquire.sh` | **Lock (section 2)** : acquiert `agent-loop.lock` si absent ou périmé (>24 h). Exit 1 si lock actif ; l'agent ne doit pas lancer une deuxième instance. |
| `agent-loop-lock-release.sh` | `./ia_dev/gitea-issues/agent-loop-lock-release.sh` | **Lock (section 2)** : supprime `agent-loop.lock` et `agent-loop.stop`. À exécuter en fin de cycles (normale ou arrêt). |
| `agent-loop-stop.sh` | `./ia_dev/gitea-issues/agent-loop-stop.sh` | **Arrêt à la demande** : crée `agent-loop.stop` ; l'instance en cours s'arrête au début du cycle suivant. |
| `agent-loop-stop-requested.sh` | `./ia_dev/gitea-issues/agent-loop-stop-requested.sh` | **Vérification arrêt** : exit 0 si `agent-loop.stop` existe (utilisé par l'agent au début de chaque cycle). |
| `agent-loop-is-running.sh` | `./ia_dev/gitea-issues/agent-loop-is-running.sh` | **Vérifier si l'agent est en cours** : affiche fichier lock, PID et date ; indique si le processus est actif ou lock orphelin. Exit 0 si instance en cours (lock mtime < 24 h), 1 sinon. |
| `tickets-fetch-inbox.sh` | `./ia_dev/gitea-issues/tickets-fetch-inbox.sh` | **Récupération par expéditeurs autorisés** : mails **à partir du 10 mars 2026** (MAIL_SINCE_DATE), filtre `tickets.authorized_emails` (conf.json), pas de marquage lu/non lu. Écrit les nouveaux mails dans `projects/<id>/data/issues/*.pending` (JSON). Ne crée pas de .pending si un .response existe déjà. Voir `projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`. |
| `list-pending-spooler.sh` | `./ia_dev/gitea-issues/list-pending-spooler.sh` | **Spooler** : liste les fichiers `projects/<id>/data/issues/*.pending` qui n'ont **pas** de fichier `.response` correspondant (mails à traiter). Une ligne par chemin. |
| `write-response-spooler.sh` | `./ia_dev/gitea-issues/write-response-spooler.sh --base <base> --to <addr> --subject "..." --body "..." [--in-reply-to "<msg-id>"]` | **Spooler** : après envoi réussi avec `mail-send-reply.sh`, écrit `projects/<id>/data/issues/<base>.response` (JSON de la réponse envoyée). |
Variables optionnelles : `GITEA_API_URL`, `GITEA_REPO_OWNER`, `GITEA_REPO_NAME`, `GITEA_ISSUES_DIR`.
### Fichiers témoins et logs (boucle agent)
Sous `projects/<id>/logs/gitea-issues/` :
- **agent-loop.status** : fichier témoin de la boucle de surveillance (`agent-loop.sh` ou `agent-loop-retrieval-once.sh`). Trois lignes : horodatage (ISO), statut (`running` ou `idle`), détail (ex. « Aucun mail non lu »). Mis à jour à chaque itération. Si la date de modification est plus récente que 2× lintervalle (ex. 120 s pour intervalle 60 s), la boucle est considérée active.
- **agent-loop.pending** : liste des mails non lus (legacy) écrite par `mail-list-unread.sh` via la boucle ; lagent gitea-issues-process traite ce fichier.
- **agent-loop.lock** : lock pour une seule instance (section 2). Contenu : PID + date. Si mtime < 24 h, une nouvelle instance ne doit pas démarrer. Géré par `agent-loop-lock-acquire.sh` / `agent-loop-lock-release.sh`.
- **agent-loop.stop** : si présent, l'instance en cours s'arrête au début du cycle suivant. Création : `agent-loop-stop.sh` ou `touch …/agent-loop.stop`.
- **agent-loop-chat-iterations.log** : sortie de `agent-loop-chat-iterations.sh` (test denvoi au lancement, puis à chaque itération le résultat de `mail-list-unread.sh`).
Le répertoire **projects/<id>/data/issues/** (spooler) est rempli **uniquement** par `./ia_dev/gitea-issues/tickets-fetch-inbox.sh`. Si ce script n'est pas exécuté, `data/issues` reste vide si on nutilise que la boucle legacy utilise seulement mail-list-unread.sh et agent-loop.pending dans logs/gitea-issues/.
**Réponse mail** : le `--body` de `mail-send-reply.sh` doit être **uniquement le texte rédigé par lagent** (la réponse à lexpéditeur). Ne jamais y mettre le mail reçu, le sujet, une citation ou un bloc « From: » / « Message-ID » : sinon le destinataire reçoit son propre message au lieu de la réponse.
### Création d'issues depuis les mails (IMAP) workflow agent
**Ne pas enchaîner directement** : l'agent doit d'abord lire les non lus, formaliser l'issue ou répondre par mail, et ne créer/traiter qu'au 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 d'envoyer une réponse directe (demande d'infos) via `mail-send-reply.sh`, soit de formaliser et créer l'issue avec `mail-create-issue-from-email.sh` (optionnel `--title` / `--body` formalisés). Si la demande est une correction/évolution prête : créer l'issue, traiter (fix/evol), commenter l'issue, 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 `.cursor/agents/agent-loop.md`). Renseigner `IMAP_USER`, `IMAP_PASSWORD` (et optionnellement `SMTP_*` pour l'envoi ; 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) ; `MAIL_SINCE_DATE=10-Mar-2026` (seuls les mails à partir de cette date sont récupérés/listés).
- Proton Mail Bridge (ou serveur IMAP/SMTP) en cours d'exé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 `.cursor/agents/agent-loop.md`.
## API Wiki (tests préalables)
Script de test de l'API 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 d'une 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 n'a jamais été initialisé (aucune page créée via l'interface), 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 l'interface, puis relancer le script avec un token valide.
**Branche par défaut du wiki :** si l'API 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`), c'est un bug connu de certaines versions de Gitea (l'API 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 d'un fichier local (ex. `Home docs/README.md`). |
| `wiki-get-page.sh` | `./gitea-issues/wiki-get-page.sh <page_name>` | Affiche le markdown brut d'une 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 d'une page et l'utiliser 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/`. La documentation des projets gérés est dans **`projects/<id>/docs`** (ex. `projects/lecoffreio/docs`) ; la documentation propre à ia_dev est dans **`projects/ia_dev/docs`**.
## Agents (commandes)
- **/agent-loop** (`agent-loop.md`) : gère les boucles par exécutions délimitées uniquement (N itérations avec `agent-loop-chat-iterations.sh [N]`, ou x cycles récupération + traitement). Ne lance jamais agent-loop.sh ni agent-loop-treatment.sh en arrière-plan.
- **/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

@ -0,0 +1,13 @@
# Documentation générique (ia_dev)
Ce répertoire contient les documents **non spécifiques à un projet** (ex. LeCoffre.io), réutilisables pour tout projet piloté par ia_dev.
**Emplacements de la documentation :**
- **Projets gérés** : `projects/<id>/docs` (ex. `projects/lecoffreio/docs`).
- **ia_dev (ce dépôt)** : `projects/ia_dev/docs` (ce répertoire).
- **agents-scripts-split.md** : Répartition des rôles entre agents Cursor et scripts (branch-align, change-to-all-branches, deploy, push), exécution depuis la racine, options standardisées.
- **WORKFLOWS_AND_COMPONENTS.md** : Workflows IA (clôture, déploiement, fix-lint, pousse, docupdate, audit, branch-align), plans Cursor, règles, subagents, commandes, skills, MCP, protocole de développement (générique, sans référence à un projet particulier).
- **GITEA_ISSUES_SCRIPTS_AGENTS.md** : Gitea issues scripts et agents (liste des scripts, workflow mails, boucle agent, prérequis).
- **TICKETS_SPOOL_FORMAT.md** : Format JSON du spooler tickets (projects/<id>/data/issues/), schémas incoming/response, pièces jointes, config conf.json.
- **ai_working_help/docs/notary-ai-api.md** : API IA notaire (enqueue, response), spooler projects/<id>/data/notary-ai/, scripts notary-ai/, agents notary-ai-loop et notary-ai-process.

View File

@ -0,0 +1,135 @@
# 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). **Il n'y a toujours qu'un seul fichier de spooler par message reçu** — pas un fichier par état : le même fichier change d'extension et de contenu quand on passe de « à traiter » à « répondu ». Seuls les messages **à partir du 10 mars 2026** (ou la date `MAIL_SINCE_DATE` en env) sont récupérés. Filtrage par **expéditeurs autorisés** (conf.json → `tickets.authorized_emails`), sans s'appuyer sur le statut « non lu ». Le traitement se base sur l'extension du fichier (`.pending` = à traiter ; après réponse, le fichier est renommé en `.response` et son contenu mis à jour avec la question reçue et la réponse envoyée).
## Emplacement et nommage des fichiers
**Un seul fichier de spooler par message reçu** : il n'y a pas un fichier par état. Un même message est représenté par un seul fichier ; quand le statut change (pending → response), on renomme ce fichier (changement d'extension) et on met à jour son contenu ; on ne crée pas de second fichier.
- **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)
- **Format des noms** : `<date>.<id>.<from>.<status>` (un seul fichier par message ; le statut change en renommant lextension et en mettant à jour le contenu).
- `date` : `YYYY-MM-DDTHHmmss` (ex. `2026-03-14T094530`)
- `id` : identifiant unique généré à la réception (déterministe à partir du message, ex. 8 caractères hexadécimaux). Même message → même `id`.
- `from` : adresse expéditeur rendue sûre pour le système de fichiers (ex. `user_example.com` pour `user@example.com`)
- `status` : extension du fichier : `pending` (à traiter) ou `response` (réponse envoyée ; le fichier `.pending` est alors supprimé)
- **Messages entrants** : `<date>.<id>.<from>.pending`. Créés par `tickets-fetch-inbox.sh`. À traiter = fichier `.pending` (il n'existe alors pas encore de `.response` pour ce message).
- **Changement de statut** : on ne modifie pas `<date>.<id>.<from>`. Après envoi réussi, le script `write-response-spooler.sh` renomme le fichier en `.response` et écrit le nouveau contenu (question reçue + réponse envoyée) ; l'ancien `.pending` est supprimé. Il reste donc un seul fichier par message.
- **Après réponse** : le fichier s'appelle `<date>.<id>.<from>.response` et contient la question reçue et la réponse envoyée (voir schéma response ci-dessous).
- **Pièces jointes (pj)** : pour un message de base `<base>` (ex. `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io`), les pièces jointes sont dans **`<base>.d/`**. Nommage des fichiers : `<index>_<nom_fichier_sanitifé>`. Le JSON du message contient le tableau `attachments` avec pour chaque entrée le chemin relatif à `data/issues/`, le type MIME et la taille.
Exemples :
- `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io.pending` — message entrant en attente
- `2026-03-14T094530.a1b2c3d4.laurence_lecoffre.io.response` — réponse envoyée (question reçue + réponse dans le JSON) ; le `.pending` associé a été supprimé
## Schéma JSON — message entrant (incoming)
Fichiers `.pending`.
```json
{
"version": 1,
"type": "incoming",
"id": "a1b2c3d4",
"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.a1b2c3d4.laurence_lecoffre.io.d/0_document.pdf",
"content_type": "application/pdf",
"size": 12345
}
]
}
```
| Champ | Type | Description |
|-------|------|-------------|
| `version` | number | Version du schéma (1) |
| `type` | string | `"incoming"` |
| `id` | string | Identifiant court du message (même que dans le nom de fichier) |
| `message_id` | string | En-tête Message-ID du mail |
| `from` | string | Adresse de l'expéditeur |
| `to` | string[] | Adresses destinataires |
| `subject` | string | Sujet |
| `date` | string | Date d'envoi (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 d'issue Gitea si une issue a été créée |
| `status` | string | `pending` |
| `attachments` | array | Pièces jointes : tableau d'objets (voir ci-dessous). `path` est relatif à `data/issues/` ; les fichiers sont dans `<base>.d/`. L'agent qui traite les tickets peut les lire depuis `projects/<id>/data/issues/<path>`. |
**Objet pièce jointe** : `filename` (nom d'origine), `path` (chemin relatif sous `projects/<id>/data/issues/`), `content_type` (MIME), `size` (octets).
## Schéma JSON — réponse envoyée (response)
Fichiers `.response`. Contiennent la **question reçue** et la **réponse envoyée**.
```json
{
"version": 1,
"type": "response",
"received_question": {
"subject": "Demande d'information",
"body": "Texte brut du corps du mail reçu."
},
"sent_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"` |
| `received_question` | object | Question reçue : `subject`, `body` |
| `sent_response` | object | Réponse envoyée : `in_reply_to_message_id`, `to`, `subject`, `body`, `sent_at` |
| `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`** : liste des expéditeurs autorisés — **un élément par adresse** (tableau de chaînes). Ne pas mettre plusieurs adresses dans une seule chaîne.
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",
"nicolas.cantu@pm.me"
]
}
}
```
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 ». Lorsqu'une réponse est enregistrée, le fichier `.pending` est supprimé et remplacé par le fichier `.response` (même base `<date>.<id>.<from>`).
**IMAP** : le script se connecte à la boîte IMAP configurée (ex. `.secrets/gitea-issues/imap-bridge.env`). Les messages sont filtrés selon les en-têtes **To**, **Delivered-To**, **X-Original-To**, etc. Pour que des messages « envoyés à » l'alias soient trouvés, **la boîte IMAP doit être celle qui reçoit le courrier pour cette adresse** (celle de `authorized_emails.to`). Si l'IMAP pointe vers une autre boîte (ex. une adresse personnelle qui reçoit tout), aucun message ne contiendra l'alias dans To/Delivered-To et tout sera exclu par `not_to_alias`. Il faut alors utiliser les identifiants IMAP de la boîte qui reçoit les mails envoyés à l'alias.

View File

@ -0,0 +1,419 @@
# Workflows et composants IA
**Version** : 1.0.0
**Périmètre** : Règles Cursor, subagents, commandes, skills, workflows de développement (générique ia_dev)
Ce document formalise chaque workflow et chaque composant utilisés par l'IA pour le développement d'un projet piloté par ia_dev.
---
## Table des matières
1. [Vue d'ensemble](#1-vue-densemble)
2. [Workflows](#2-workflows)
3. [Plans Cursor (déclenchables)](#3-plans-cursor-déclenchables)
4. [Composants : Règles](#4-composants--règles)
5. [Composants : Subagents](#5-composants--subagents)
6. [Composants : Commandes](#6-composants--commandes)
7. [Composants : Skills](#7-composants--skills)
8. [MCP et outils](#8-mcp-et-outils)
9. [Usage](#9-usage)
10. [Protocole de développement](#10-protocole-de-développement)
---
## 1. Vue d'ensemble
| Type | Emplacement | Portée |
|------|-------------|--------|
| Règles | `.cursor/rules/*.mdc` | Projet |
| Plans | `~/.cursor/plans/*.plan.md` | Utilisateur (utilisables depuis ce projet) |
| Commandes projet | `.cursor/commands/*.md` | Projet (priorité sur global, invoquent plans utilisateur) |
| Commandes globales | `~/.cursor/commands/*.md` | Utilisateur (tous projets) |
| Subagents | `~/.cursor/agents/*.md` | Utilisateur (tous projets) |
| Skills | `~/.cursor/skills-cursor/*/SKILL.md` | Utilisateur (tous projets) |
---
## 2. Workflows
### 2.1 Workflow de clôture évolution / correction
**Déclencheur** : Fin de toute évolution ou correction.
**Règle** : `.cursor/rules/cloture-evolution.mdc` (alwaysApply: true).
**Ordre d'exécution** : Chaque bloc est bouclé jusqu'à stabilisation (rien à optimiser). **Réponse non partielle** : répondre à toutes les questions de clôture et lancer les subagents/plans concernés.
**Checklist de clôture (obligatoire)** : (1) Modifications similaires ailleurs ? (2) Optimisations/mutualisations possibles ? (3) Fichiers exempts d'erreurs lint ? (4) Types OK ? (5) Projet compile ?
**Orchestration** : Après correction, lancer en boucle : fix-lint (generalPurpose) si erreurs ; deploy (mcp_task) si validé ; en cas d'échec subagent, prendre le relais et corriger sans s'arrêter.
| Étape | Actions |
|-------|---------|
| 1. Tests | Compléter tests manquants (user stories) ; lancer ; corriger échecs ; rationaliser (supprimer doublons, mutualiser) |
| 2. Types | Chercher optimisations types (`npm run typecheck`, `npx tsc --noEmit`) ; implémenter si pertinent |
| 3. Reproductions | Chercher changements similaires à reproduire ailleurs ; implémenter si pertinent |
| 4. Factorisation | Chercher factorisation, mutualisation, centralisation ; implémenter si pertinent |
| 5. Lint | Chercher erreurs lint (`npm run lint`, `ReadLints`) ; corriger ; sinon vérifier si plus exigeant possible |
| 6. Sécurité | Chercher améliorations (`docs/CODE_SECURITY.md`) ; implémenter si pertinent |
| 7. Documentation | Mettre à jour REX + descriptions (README consolidation, FRONTEND.md, CODE_STANDARDS.md) ; rationaliser |
| 8. Déploiement | Après validation utilisateur : déployer ; analyser logs ; corrections/optimisations/sécurité déploiement ; doc déploiement |
**Contraintes** : Déploiement uniquement après validation. Pas de tâches en arrière-plan.
**Plan** : `.cursor/plans/workflow-cloture-evolution.plan.md`
**Commande** : `/cloture-evolution`
**Référence** : `docs/README.md#consolidation-operationnelle-ex-operationsmd`.
---
### 2.2 Workflow de déploiement
**Déclencheur** : Commande `/deploy` ou invocation subagent deploy.
**Script** : `deploy/scripts_v2/deploy.sh` — s'exécute **localement** et orchestre le déploiement **à distance** (SSH vers la cible, git pull, build, restart services).
| Étape | Action |
|-------|--------|
| 0 | Hook pre-deploy : arrêter (SIGTERM) les processus concurrents (lint, fix-lint, typecheck, turbopack) dont le cwd est dans le projet. Si des processus restent (hors projet), bloquer. Après déploiement : message pour relancer /fix-lint si nécessaire. |
| 1 | Vérifier que les modifications sont commitées |
| 2 | Exécuter `./deploy/scripts_v2/deploy.sh <env>` depuis la racine du projet, avec `test`, `pprod` ou `prod` |
| 3 | **En cas d'échec** : orchestrer la correction automatique (boucle jusqu'à succès ou impossibilité) : (a) analyser la cause (logs, journalctl, fichiers sur cible, dist) ; (b) corriger la root cause sans contournement ; (c) committer/pousser si besoin ; (d) relancer le script ; (e) répéter |
**Options** : `--skipSetupHost`, `--checkLint`. **Env** : `DEPLOY_SKIP_CONCURRENCY_CHECK=1` pour désactiver le hook pre-deploy.
**Plan** : `.cursor/plans/workflow-deploy.plan.md`
**Commande** : `/deploy <env>`
**Référence** : docs/DEPLOYMENT.md, deploy/README.md.
---
### 2.3 Workflow de correction lint
**Déclencheur** : Commande `/fix-lint` ou invocation subagent fix-lint.
**Périmètre** : applications du projet (front, back, ressources selon `projects/<id>/conf.json` build_dirs).
| Étape | Action |
|-------|--------|
| 1 | Exécuter `npm run lint` dans chaque application pour lister les erreurs |
| 2 | Corriger par lots de 10 erreurs maximum |
| 3 | Entre chaque lot : lancer test build/typecheck |
| 4 | Ne jamais contourner (pas de `eslint-disable`) |
| 5 | Appliquer patterns : extraction helpers, découpage fichiers, objets de configuration |
**Règles applicables** : max-lines 250, max-lines-per-function 40, max-params 4, max-depth 4, complexity 10, max-nested-callbacks 3.
**Contournement mcp_task** : `subagent_type="fix-lint"` via mcp_task échoue ; utiliser `generalPurpose` avec prompt détaillé. Voir la consolidation dans `docs/README.md`.
**Plan** : `.cursor/plans/workflow-fix-lint.plan.md`
**Commande** : `/fix-lint`
---
### 2.4 Workflow de commit et push (pousse)
**Déclencheur** : Commande `/pousse` ou demande de commit/push.
| Étape | Action |
|-------|--------|
| 1 | `git add -A` |
| 2 | `git commit -m "..."` avec format structuré (Motivations, Root causes, Correctifs, Evolutions, Pages affectées) |
| 3 | `git push` (sans déclencher CI si règle projet) |
**Format commit** : Titre court ; sections en bullets ; pas de `--no-verify` sauf cas documenté.
**Plan** : `.cursor/plans/workflow-pousse.plan.md`
**Commande** : `/pousse`
---
### 2.5 Workflow de mise à jour documentation (docupdate)
**Déclencheur** : Commande `/docupdate`.
**docs/features extract** : Extraire données de docs/features vers docs/ ; supprimer docs/features.
**docs/fixKnowledge extract** : Extraire données de docs/fixKnowledge vers docs/ ; supprimer docs/fixKnowledge.
**docs/ cleanup** : Réunir et optimiser en max 20 documents ; supprimer infos fausses/obsolètes ; ventiler dans doc existante (pas de FEATURES.md ni FIXKNOWLEDGE.md).
**Plan** : `.cursor/plans/workflow-docupdate.plan.md`
**Commande** : `/docupdate`
---
### 2.6 Workflow d'audit sécurité
**Déclencheur** : Commande `/audit-security`.
**Processus** : Analyse surface d'attaque ; revue OWASP Top 10 ; évaluation risque ; recommandations ; checklist Security Headers.
**Format rapport** : [SEVERITY] Titre ; Location ; Description ; Impact ; Reproduction ; Remediation ; Références.
**Plan** : `.cursor/plans/workflow-audit-security.plan.md`
**Commande** : `/audit-security`
---
### 2.7 Workflow lintit (qualité code)
**Déclencheur** : Commande `/lintit`.
**Processus** : Vérifier règles qualité ; liste actions à mener. Couvre : analyse préalable, non-duplication, généricité, design patterns, gestion erreurs, interdiction fallback, interdiction facilités IA ; corriger erreurs et warnings lint sans contournement.
**Plan** : `.cursor/plans/workflow-lintit.plan.md`
**Commande** : `/lintit`
---
### 2.8 Workflow banch-align
**Déclencheur** : Commande `/banch-align <env>`.
**Processus** : Aligner test, pprod, prod (sauf `<env>`) sur la branche de `<env>` sans modifier `<env>`. Merge en conflit : `<env>` prioritaire. Stash si conflits. Vérifier 30 derniers commits équivalents.
**Résultat** : origin/test, origin/pprod, origin/prod au même commit ; branches locales à jour.
**Plan** : `.cursor/plans/workflow-banch-align.plan.md`
**Commande** : `/banch-align <env>`
---
### 2.9 Workflow banch-align-update
**Déclencheur** : Commande `/banch-align-update <env>`.
**Processus** : Aligner branche actuelle sur `<env>` sans modifier `<env>`. Stash avant ; liste modifications ; confirmation. Branche locale alignée sur commits de `<env>` ; reste sur même branche.
**Plan** : `.cursor/plans/workflow-banch-align-update.plan.md`
**Commande** : `/banch-align-update <env>`
---
## 3. Plans Cursor (déclenchables)
Les workflows sont migrés en plans Cursor stockés au niveau utilisateur (`~/.cursor/plans/`) et déclenchables via les commandes projet `.cursor/commands/`.
### Format Cursor attendu
- **Frontmatter YAML** : `name`, `overview`, `todos` (id, content, status), `isProject: false`
- **Corps Markdown** : phases, étapes, liens vers fichiers `[fichier](chemin/relatif)`
### Liste des plans
| Plan | Commande | Description |
|------|----------|-------------|
| workflow-cloture-evolution.plan.md | /cloture-evolution | Clôture évolution/correction |
| workflow-deploy.plan.md | /deploy | Déploiement test/pprod/prod |
| workflow-fix-lint.plan.md | /fix-lint | Correction lint |
| workflow-pousse.plan.md | /pousse | Commit et push |
| workflow-docupdate.plan.md | /docupdate | Mise à jour documentation |
| workflow-audit-security.plan.md | /audit-security | Audit sécurité |
| workflow-lintit.plan.md | /lintit | Qualité code |
| workflow-banch-align.plan.md | /banch-align | Aligner branches |
| workflow-banch-align-update.plan.md | /banch-align-update | Aligner branche actuelle |
### Déclenchement
- **Commande** : Taper `/` dans le chat puis le nom (ex. `/cloture-evolution`, `/deploy test`)
- **Plan mode** : Shift+Tab pour créer un plan ; ouvrir `~/.cursor/plans/workflow-*.plan.md` et cliquer « Build » pour exécuter
- **Priorité** : Les commandes projet (`.cursor/commands/`) ont priorité sur les commandes globales (`~/.cursor/commands/`)
**Référence** : .cursor/plans/README.md, ~/.cursor/plans/
---
## 4. Composants : Règles
**Emplacement** : `.cursor/rules/*.mdc` (projet).
| Règle | Description | alwaysApply |
|-------|-------------|-------------|
| rules.mdc | Règles générales (langues, restrictions, processus, qualité, doc, déploiement) | true |
| cloture-evolution.mdc | Workflow de clôture à la fin de chaque évolution/correction | true |
| development-autonomy.mdc | Références user stories, patterns, qualité, sécurité, tests | true |
| error-fixing.mdc | Corriger l'erreur et jamais contourner | true |
| investigation-analysis.mdc | Investigation via logs, données ; services host-native | true |
**Format** : YAML frontmatter (description, globs, alwaysApply) + contenu markdown.
---
## 5. Composants : Subagents
**Emplacement** : `~/.cursor/agents/*.md` (utilisateur).
| Subagent | Description | is_background | mcp_task |
|----------|-------------|---------------|----------|
| deploy | Exécute `./deploy/scripts_v2/deploy.sh <env>` (script local, déploie à distance) | false | subagent_type="deploy" |
| fix-lint | Corrige erreurs lint backend, frontend, ressources | true | Non fonctionnel (utiliser generalPurpose) |
**Invocation** : `/deploy`, `/fix-lint` ou « Utilise le subagent X » dans le chat.
**Tâches de fond** : fix-lint avec `is_background: true` retourne immédiatement ; écrit état dans `~/.cursor/subagents/` ; reprise possible via ID.
**Référence** : ~/.cursor/agents/README.md.
---
## 6. Composants : Commandes
**Emplacements** : `.cursor/commands/*.md` (projet, priorité) ; `~/.cursor/commands/*.md` (global).
Les commandes projet invoquent les plans `.cursor/plans/*.plan.md`.
| Commande | Description |
|----------|-------------|
| deploy | Déploie sur test/pprod/prod |
| fix-lint | Corrige erreurs lint |
| lintit | Vérifie règles qualité, liste actions |
| docupdate | Mise à jour et rationalisation docs |
| pousse | Commit + push avec format structuré |
| audit-security | Audit sécurité OWASP |
| banch-align | Aligne branches test/pprod/prod sur `<env>` |
| banch-align-update | Aligne branche actuelle sur `<env>` |
| science-redaction | Rédaction scientifique |
| scientifi-check | Vérification scientifique |
---
## 7. Composants : Skills
**Emplacement** : `~/.cursor/skills-cursor/*/SKILL.md` (utilisateur).
| Skill | Description |
|-------|-------------|
| create-rule | Créer règles Cursor (.mdc) |
| create-skill | Créer skills |
| create-subagent | Créer subagents |
| update-cursor-settings | Modifier settings.json |
| migrate-to-skills | Migration vers skills |
---
## 8. MCP et outils
### MCP mcp_task
**Types disponibles** : generalPurpose, explore, deploy, fix-lint.
| Type | Usage | Remarque |
|------|-------|----------|
| generalPurpose | Tâches complexes, recherche, corrections | Toujours fonctionnel |
| explore | Exploration codebase rapide | Niveaux : quick, medium, very thorough |
| deploy | Déploiement test/pprod/prod | Lance deploy.sh |
| fix-lint | Correction lint | Non fonctionnel ; utiliser generalPurpose |
### Outils MCP browser
Tests E2E : navigation, snapshot, click, type, fill, etc. Référence : user_stories/.
### Outils projet
| Outil | Usage |
|-------|-------|
| npm run lint | ESLint |
| npm run typecheck | TypeScript (front) |
| npx tsc --noEmit | TypeScript (back, ressources) |
| npm run lint:markdown | Markdown (MD032, MD033, MD040) |
| ReadLints | Diagnostics IDE |
---
## 9. Usage
### 9.1 Quand utiliser quoi
| Situation | Action |
|-----------|--------|
| Démarrer une évolution ou correction | Formuler la demande ; l'IA applique les règles (rules.mdc, development-autonomy) |
| Fin d'évolution ou correction | `/cloture-evolution` ou workflow automatique (cloture-evolution.mdc) ; l'humain valide le déploiement si demandé |
| Erreurs de lint à corriger | `/fix-lint` ou « Corrige les erreurs de lint » |
| Déployer sur un environnement | `/deploy test` (ou pprod, prod) ; valider avant exécution |
| Commiter et pousser | `/pousse` après validation du message de commit |
| Vérifier qualité du code | `/lintit` |
| Audit sécurité | `/audit-security` |
| Mise à jour documentation | `/docupdate` |
| Aligner les branches | `/banch-align <env>` ou `/banch-align-update <env>` |
| Créer une règle, skill ou subagent | Demander à l'IA d'utiliser le skill create-rule, create-skill ou create-subagent |
### 9.2 Invocation des commandes
- **Slash** : `/deploy`, `/fix-lint`, `/pousse`, etc. dans le chat Cursor.
- **Phrase** : « Déploie sur test », « Corrige les erreurs de lint », « Commit et push ».
- **Subagents** : « Utilise le subagent fix-lint » ou invocation automatique si la description correspond.
- **Plans** : Les commandes projet (`/cloture-evolution`, `/deploy`, etc.) chargent et exécutent les plans correspondants.
### 9.3 Règles appliquées automatiquement
Les règles avec `alwaysApply: true` sont actives à chaque conversation : rules.mdc, cloture-evolution.mdc, development-autonomy.mdc, error-fixing.mdc, investigation-analysis.mdc. Aucune action requise.
### 9.4 Validation requise
- **Déploiement** : L'IA demande validation avant d'exécuter deploy.sh.
- **Commits** : L'utilisateur valide le message (format structuré) avant exécution de pousse.
- **Modifications sensibles** : Modèle de données, architecture, nouvelles dépendances : validation avant implémentation.
---
## 10. Protocole de développement
Ce protocole décrit comment l'humain utilise l'outil IA pour développer sur le projet.
### 10.1 Avant de commencer
1. Consulter `user_stories/INDEX.md` pour le contexte des parcours (si présent).
2. Consulter `docs/CODE_STANDARDS.md` et `docs/CODE_SECURITY.md` pour les contraintes.
3. S'assurer d'être sur la bonne branche (test, pprod ou prod selon l'environnement cible).
### 10.2 Pendant le développement
1. **Formuler la demande** : Décrire l'évolution ou la correction de façon précise (objectif, périmètre, contraintes).
2. **Valider les choix** : Si l'IA propose une solution (architecture, design, modèle de données), valider avant implémentation.
3. **Suivre les boucles** : L'IA applique le workflow de clôture à la fin ; l'humain peut demander d'arrêter une boucle ou de prioriser une étape.
4. **Investiguer** : En cas de bug, demander une investigation (logs, données, doc) ; l'IA utilise investigation-analysis.mdc.
5. **Concurrence des actions** : Voir [10.2.1 Concurrence des actions](#1021-concurrence-des-actions).
#### 10.2.1 Concurrence des actions
Les actions longues (lint, fix-lint, déploiement, typecheck, turbopack) ne doivent pas se chevaucher. Règles :
| Priorité | Action | Comportement |
|----------|--------|--------------|
| 1 | Déploiement | Arrêter proprement toute action en cours (lint, fix-lint, typecheck, turbopack) avant de lancer le déploiement. Le déploiement s'exécute seul. |
| 2 | Lint / fix-lint | Ne pas lancer si un déploiement est en cours. Arrêter (Ctrl+C ou annulation subagent) avant de lancer un déploiement. |
| 3 | Typecheck / turbopack | S'exécutent entre les lots de fix-lint ; ne pas lancer en parallèle d'un déploiement. |
**Avant de lancer un déploiement** : Le script `deploy/scripts_v2/deploy.sh` exécute un hook `hooks/pre-deploy.sh` qui arrête (SIGTERM) les processus concurrents dont le cwd est dans le projet (eslint, tsc, turbopack). Si des processus restent hors projet, le script bloque. Après déploiement, un message suggère de relancer /fix-lint si nécessaire.
**Si une action est lancée alors qu'une autre tourne** : Arrêter proprement l'action en cours (Ctrl+C pour processus foreground ; annulation du subagent pour fix-lint) avant de démarrer la nouvelle.
**Une action à la fois** : Ne pas lancer lint, fix-lint, déploiement ou build/typecheck en parallèle dans des terminaux distincts sans coordination.
**Contournement** : `DEPLOY_SKIP_CONCURRENCY_CHECK=1` désactive le hook pre-deploy (CI, cas documentés).
### 10.3 À la fin d'une évolution ou correction
1. **Workflow de clôture** : L'IA exécute automatiquement (tests, types, reproductions, factorisation, lint, sécurité, doc).
2. **Validation déploiement** : Si déploiement demandé, valider explicitement avant exécution.
3. **Commit** : Valider le message de commit proposé ; utiliser `/pousse` pour commiter et pousser.
4. **Vérification** : Consulter les logs de déploiement si applicable ; vérifier que la doc est à jour.
### 10.4 Bonnes pratiques
- **Une demande à la fois** : Éviter les demandes multiples non liées dans un même message.
- **Précision** : Donner le contexte (fichier, fonction, user story) pour limiter les allers-retours.
- **Validation** : Ne pas valider un déploiement ou un commit sans avoir vérifié les changements.
- **Documentation** : Signaler si la doc existante est obsolète ou incomplète.
- **Pas de contournement** : Refuser toute proposition de désactiver des règles, d'ignorer des erreurs ou de contourner un problème.
### 10.5 En cas de problème
- **L'IA contourne** : Rappeler « Corrige l'erreur et jamais contourner » (error-fixing.mdc).
- **Subagent fix-lint échoue** : Utiliser « Corrige les erreurs de lint avec generalPurpose » ou exécuter manuellement `npm run lint` et demander les corrections.
- **Déploiement en échec** : Analyser les logs ; ne pas relancer sans avoir identifié et corrigé la cause.
- **Règle trop stricte** : Documenter l'exception dans `docs/README.md` (consolidation opérationnelle) avec justification ; ne pas modifier les règles sans validation.
### 10.6 Ordre typique d'une session
1. Lire le contexte (user story, doc, code existant).
2. Formuler la demande d'évolution ou de correction.
3. Valider les propositions de l'IA.
4. Laisser l'IA implémenter et exécuter le workflow de clôture.
5. Valider déploiement si pertinent.
6. Valider et exécuter le commit (`/pousse`).
---
**Références** : docs/README.md, docs/CODE_STANDARDS.md, docs/DEPLOYMENT.md, CLAUDE.md.

View File

@ -0,0 +1,171 @@
# Répartition agent / script (agents lançant des scripts)
**Objectif :** Exploiter au mieux la valeur ajoutée de l'agent (orchestration, contenu structuré, règles projet) et du script (déterminisme, reproductibilité, exécutable sans LLM).
## Principes
| Rôle | Agent | Script |
|------|--------|--------|
| **Orchestration** | Ordre des étapes, invocation d'autres agents (push, docupdate, etc.) | Une seule responsabilité par script |
| **Contenu** | Message de commit structuré, CHANGELOG, décisions métier | Pas de génération de texte |
| **Règles projet** | Clôture (cloture-evolution.mdc), 5 sub-agents, docupdate | Contraintes techniques (auteur, chemins sensibles, branche) |
| **Déterminisme** | — | Vérifications git, chemins, options reproductibles |
| **Réutilisabilité** | Contexte Cursor | CLI / humain / CI sans agent |
---
## 1. branch-align-by-script-from-test
**Agent aujourd'hui :**
- Horodatage, clôture
- `cd` racine projet
- Lancer push-by-script puis docupdate puis `./deploy/branch-align.sh <env>`
- Ne pas masquer la sortie
**Script branch-align.sh aujourd'hui :**
- Vérif repo git, argument env, branche courante = env
- Fetch, force-push with lease, alignement test/pprod/prod, vérifications
**Déplacé dans le script :**
- **Exécution depuis racine du dépôt :** si le script est invoqué hors racine, se ré-exécuter depuis `git rev-parse --show-toplevel` pour que `./deploy/branch-align.sh` soit utilisable depuis n'importe quel sous-dossier.
**Reste dans l'agent :**
- Ordre push → docupdate → branch-align
- Invocation des agents push-by-script et docupdate
- Clôture complète
---
## 2. change-to-all-branches
**Agent aujourd'hui :**
- Vérifier branche = test (sinon retour 1)
- Lancer /push-by-script, /branch-align-by-script-from-test, /deploy-by-script
- Retour 0
**Déplacé dans un script :**
- **Nouveau script `deploy/change-to-all-branches.sh` :**
- Vérifier que la branche courante est `test` (sinon exit 1)
- Exécuter `./deploy/branch-align.sh test`
- Exécuter `./deploy/scripts_v2/deploy.sh test --import-v1 --skipSetupHost`
- À lancer depuis la racine du dépôt (le script peut faire `cd` vers la racine git au démarrage)
**Reste dans l'agent :**
- Lancer /push-by-script (message de commit fourni par l'agent)
- Puis lancer le script `deploy/change-to-all-branches.sh` (alignement + déploiement)
- Pas d'appel à branch-align-by-script-from-test ni deploy-by-script séparés : un seul script enchaîne align + deploy
---
## 3. deploy-by-script
**Agent aujourd'hui :**
- Vérifier le suivi des branches (main/test/pprod/prod → origin/…), corriger avec `git branch --set-upstream-to`
- S'assurer que la branche courante est à jour : `git fetch origin` puis `git reset --hard origin/$(git branch --show-current)`
- `mkdir -p logs`, puis exécuter le script avec `tee logs/deploy_*.log`
- Ne pas masquer la sortie
**Script deploy.sh aujourd'hui :**
- Env, options, dotenv, vérifs git (DEPLOY_GIT_REMOTE, synchro), déploiement
**Déplacé dans le script :**
- **Pré-vérification du suivi des branches :** au début de `deploy.sh`, après `PROJECT_ROOT` et validation de `ENV` : `git fetch origin` puis pour chaque branche locale parmi main, test, pprod, prod définir l'upstream `origin/<branch>` si la branche existe. Pas de `reset --hard` dans le script (le sync avec `origin` reste à l'agent avant d'appeler le script).
**Reste dans l'agent :**
- S'assurer que la branche courante est à jour avec sa branche distante (`git fetch` puis `git reset --hard origin/$(git branch --show-current)`)
- Créer `logs/` et lancer le script avec `tee` vers un log daté
- Clôture complète
---
## 4. push-by-script
**Agent aujourd'hui :**
- Construire le message de commit (toutes les sections obligatoires)
- Mettre à jour CHANGELOG.md
- Mettre à jour VERSION (incrément sous-sous-version)
- Exécuter `./deploy/pousse.sh` avec le message sur STDIN
**Script pousse.sh aujourd'hui :**
- Lire le message sur STDIN, `git add -A`, vérifications (auteur, chemins sensibles, branche distante), commit, push
**Déplacé dans le script :**
- **Option `--bump-version` :** avant le `git add -A`, lire le fichier `VERSION` à la racine du dépôt, incrémenter le troisième segment (patch), réécrire `VERSION`. Ensuite `git add -A` inclura le fichier modifié. L'agent n'a plus à éditer VERSION à la main ; il fournit le message (en mentionnant la nouvelle sous-sous-version si besoin) et peut appeler `pousse.sh --bump-version`.
**Reste dans l'agent :**
- Construction du message de commit (toutes les sections)
- Mise à jour de CHANGELOG.md
- Invocation de `./deploy/pousse.sh` (avec `--bump-version` si souhaité)
- Clôture complète
---
## Synthèse des implémentations
| Fichier | Modification |
|---------|--------------|
| `deploy/branch-align.sh` | Ré-exécution depuis la racine git si nécessaire |
| `deploy/scripts_v2/deploy.sh` | Suivi branches origin ; **par défaut** sync (--sync-origin) et log (--log-to-dir logs) ; --no-sync-origin, --no-log pour désactiver |
| `deploy/pousse.sh` | **Ré-exéc depuis racine** si besoin ; option --bump-version ; build check (ressources, backend, frontend) avant staging |
| `deploy/change-to-all-branches.sh` | Vérif branche test, branch-align.sh test, deploy.sh test --import-v1 --skipSetupHost --no-sync-origin (log par défaut) |
| Agents .cursor/agents/*.md | Adapter les consignes pour utiliser les nouvelles options/scripts et alléger les étapes redondantes |
---
## Deuxième passe (agent = contrôle / script = exécution)
**Principe :** L'agent assure vérifications, corrections, relances, synthèses et textes. Le script assure l'exécution et l'orchestration déterministe.
### deploy-by-script / deploy.sh
- **Dans le script :** option `--sync-origin` (git fetch + reset --hard origin/current) ; option `--log-to-dir <dir>` (création du répertoire et tee vers un fichier daté). L'agent n'exécute plus ces commandes ni tee à la main.
- **Dans l'agent :** lancer le script avec `--sync-origin --log-to-dir logs`, contrôler la sortie et le code de retour.
### push-by-script / pousse.sh
- **Dans le script :** build check (npm run build pour les répertoires listés dans `projects/<id>/conf.json` build_dirs) avant staging ; en cas d'échec, sortie en erreur sans commit ni push.
- **Dans l'agent :** construire le message, mettre à jour CHANGELOG, lancer le script, contrôler sortie et code de retour.
### Tous les agents
- Chaque agent précise en en-tête **Rôle de l'agent** et **Rôle du script**.
- Consignes redondantes retirées ou raccourcies au profit du contrôle (sortie non masquée, code de retour, rapport succès ou erreur).
---
## Troisième passe (priorité : exécution standardisée dans le script)
**Principe :** Tout ce qui peut être standardisé est dans le script pour garantir la même exécution à chaque run.
### deploy.sh
- **Par défaut :** `SYNC_ORIGIN=true`, `LOG_TO_DIR="logs"`. Chaque invocation fait sync avec origin et tee vers `logs/deploy_YYYYMMDD_HHMMSS.log` sans que l'agent ait à passer des options.
- **Désactivation :** `--no-sync-origin` et `--no-log`. L'agent invoque simplement `deploy.sh <env> --import-v1 --skipSetupHost`.
### pousse.sh
- **Ré-exécution depuis la racine :** si le script est appelé hors racine du dépôt, il se ré-exécute depuis `git rev-parse --show-toplevel`. Exécution toujours depuis la racine (comme branch-align.sh et change-to-all-branches.sh).
### change-to-all-branches.sh
- Appelle `deploy.sh test --import-v1 --skipSetupHost --no-sync-origin` (après push, la branche est déjà à jour ; log dans logs/ reste le défaut).
### Agents
- deploy-by-script : une seule commande sans options sync/log (comportement par défaut).
- push-by-script : le script peut être appelé depuis n'importe quel sous-dossier.

View File

@ -0,0 +1,40 @@
{
"id": "lecoffreio",
"name": "Lecoffre.io",
"project_path": "/home/desk/code/lecoffre_ng_test/deploy",
"build_dirs": [
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-ressources-dev",
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-back-main",
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-front-main"
],
"deploy": {
"scripts_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/lecoffre_ng_test/.secrets"
},
"version": {
"package_json_paths": [
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-back-main/package.json",
"/home/desk/code/lecoffre_ng_test/deploy/lecoffre-front-main/package.json"
],
"splash_app_name": "LeCoffre.io"
},
"mail": {
"email": "ai.support.lecoffreio@4nkweb.com",
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
},
"git": {
"wiki_url": "https://git.4nkweb.com/4nk/lecoffre_ng/wiki",
"token_file": ".secrets/gitea-issues/token"
},
"tickets": {
"ticketing_url": "https://git.4nkweb.com/4nk/lecoffre_ng/issues",
"authorized_emails": {
"to": "ai.support.lecoffreio@4nkweb.com",
"from": [
"laurence@lecoffre.io",
"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"
}
}

View File

@ -0,0 +1,841 @@
# Documentation Complète de l'Ancrage Blockchain - LeCoffre.io
**Dernière mise à jour** : 2025-11-24
**Version** : 3.1.2
Ce document consolide toute la documentation relative à l'ancrage blockchain des documents et dossiers dans LeCoffre.io.
---
## 📋 Table des Matières
1. [Vue d'Ensemble](#1-vue-densemble)
2. [Architecture V3](#2-architecture-v3)
3. [Processus d'Ancrage](#3-processus-dancrage)
4. [Certificats et ZIP](#4-certificats-et-zip)
5. [Vérification et Statuts](#5-vérification-et-statuts)
6. [Scripts et Maintenance](#6-scripts-et-maintenance)
7. [Troubleshooting](#7-troubleshooting)
8. [Références](#8-références)
---
## 1. Vue d'Ensemble
### Évolution des Versions
| Version | Blockchain | Périmètre | Déclenchement | Filigrane | Fallback |
|---------|-----------|-----------|---------------|-----------|----------|
| **V1** | Tezos | Dossiers uniquement | Manuel | ❌ Non | ⚠️ Lecture seule |
| **V2** | Bitcoin Signet | Docs + Dossiers | Auto | ❌ Non | - |
| **V3** | Bitcoin Signet | Docs + Dossiers | Auto | ✅ Oui | ✅ Tezos masqué |
### Nouveautés V3
1. **Filigrane automatique** : "lecoffre.io" sur tous documents
2. **Double version** : Original + filigrané (conservés en BDD)
3. **ZIP enrichi** : Originaux + filigranés + certificats + preuves JSON
4. **Preuves structurées** : JSON standardisé pour vérification externe
5. **Ancrage dossier** : Hash du ZIP complet (pas seulement Merkle tree)
6. **Fallback Tezos** : Accès masqué aux anciens ancrages (lecture seule)
### Principes Fondamentaux
- **Ancrage immédiat** : L'ancrage est déclenché automatiquement lors de la validation d'un document
- **Hash filigrané** : Seule la version filigranée est ancrée sur blockchain
- **Double hash** : Hash original et hash filigrané sont stockés pour vérification
- **Preuve on-chain** : Le `tx_id` (transaction ID Bitcoin) est la seule preuve fiable d'ancrage
---
## 2. Architecture V3
### Services Backend
#### WatermarkService
**Fichier** : `lecoffre-back-main/src/services/common/WatermarkService/WatermarkService.ts`
**Responsabilités** :
- Conversion documents → PDF (si nécessaire)
- Ajout filigrane "lecoffre.io" (diagonal, 15% opacité)
- Upload version filigranée vers S3/IPFS
- Gestion formats : PDF, images, Word, PowerPoint
**Méthodes principales** :
```typescript
async addWatermarkAndUpload(fileUid: string): Promise<{
watermarkedS3Key: string;
watermarkedSize: number;
watermarkedMimetype: string;
}>
```
#### DocumentAnchorsService
**Fichier** : `lecoffre-back-main/src/services/notary/DocumentAnchorsService/DocumentAnchorsService.ts`
**Workflow d'ancrage** :
1. Vérification ancrage Tezos existant (fallback)
2. Génération version filigranée si absente
3. Calcul hash version filigranée
4. Vérification ancrage Signet existant
5. Création ancrage `QUEUED`
6. Ancrage blockchain Bitcoin Signet
7. Génération `proof_data` (incluant fallback Tezos)
8. Mise à jour ancrage avec `tx_id`
**Méthodes principales** :
```typescript
async anchorDocument(documentUid: string, anchoredByUid?: string): Promise<DocumentAnchors>
async getByDocumentUid(documentUid: string): Promise<DocumentAnchors | null>
async getByFileHash(fileHash: string): Promise<DocumentAnchors | null>
```
#### OfficeFolderAnchorsService
**Fichier** : `lecoffre-back-main/src/services/notary/OfficeFolderAnchorsService/OfficeFolderAnchorsService.ts`
**Workflow d'ancrage dossier** :
1. Récupération tous documents validés
2. Génération filigranes manquants
3. Génération ZIP complet dossier
4. Upload ZIP vers S3
5. Ancrage hash ZIP complet
6. Génération preuve dossier
**Structure ZIP** :
```text
dossier_12345678/
├── _LISEZ-MOI.txt
├── 01_originaux/
├── 02_filigranes/
├── 03_certificats/
└── 04_preuves/
```
#### ProofDataService
**Fichier** : `lecoffre-back-main/src/services/notary/ProofDataService/ProofDataService.ts`
**Responsabilités** :
- Génération JSON de preuve d'ancrage
- Format standard pour vérification externe
- Inclusion toutes données blockchain
**Structure JSON** :
```json
{
"version": "3.0.0",
"timestamp": "2025-10-29T16:30:00.000Z",
"document": {
"uid": "abc123...",
"name": "Acte de vente",
"original_hash": "e3b0c442...",
"watermarked_hash": "5feceb66..."
},
"anchor": {
"blockchain": "BITCOIN_SIGNET",
"status": "VERIFIED_ON_CHAIN",
"txid": "a1b2c3d4...",
"tx_link": "https://mempool.4nkweb.com/signet/tx/a1b2c3d4...",
"block_height": 123456,
"confirmations": 6
},
"tezos_fallback": {
"legacy": true,
"blockchain": "TEZOS",
"txid": "old_tezos_tx..."
}
}
```
#### BitcoinSignetService
**Fichier** : `lecoffre-back-main/src/services/common/BitcoinSignetService/BitcoinSignetService.ts`
**Responsabilités** :
- Ancrage hash sur Bitcoin Signet
- Vérification statut transaction
- Récupération métadonnées blockchain
**Méthodes principales** :
```typescript
async anchorHash(hash: string): Promise<{
txid: string;
tx_hash: string;
tx_link: string;
}>
async verifyAnchor(txId: string): Promise<{
confirmed: boolean;
confirmations: number;
block_height: number;
}>
```
### Base de Données
#### Schéma Prisma
**Table `files`** :
```prisma
model Files {
uid String @id @unique @default(uuid())
file_name String
s3_key String?
// V3 : Version filigranée
watermarked_s3_key String?
watermarked_at DateTime?
original_hash String? // Hash fichier original
document_uid String?
document Documents? @relation(...)
}
```
**Table `document_anchors`** :
```prisma
model DocumentAnchors {
uid String @id @unique @default(uuid())
document_uid String @unique
// Hash de la version FILIGRANNÉE (V3)
file_hash String
blockchain EBlockchainName @default(BITCOIN_SIGNET)
status EAnchoringStatus @default(QUEUED)
// Données blockchain
tx_id String? // ⚠️ CRITIQUE : Seul indicateur fiable
tx_link String?
tx_hash String?
block_height Int?
block_time DateTime?
confirmations Int?
// V3 : Données de preuve (JSON)
proof_data Json?
anchored_at DateTime?
anchored_by_uid String?
}
```
**Table `office_folder_anchors`** :
```prisma
model OfficeFolderAnchors {
uid String @id @unique @default(uuid())
// Hashs sources (Merkle tree)
root_hash String
// V3 : ZIP complet
zip_hash String?
zip_s3_key String?
zip_size Float?
blockchain EBlockchainName @default(BITCOIN_SIGNET)
status EAnchoringStatus @default(QUEUED)
// Données blockchain
tx_id String? // ⚠️ CRITIQUE : Seul indicateur fiable
tx_link String?
tx_hash String?
block_height Int?
confirmations Int?
// V3 : Données de preuve (JSON)
proof_data Json?
}
```
**Enums** :
```prisma
enum EBlockchainName {
TEZOS // ⚠️ Conservé pour fallback (lecture seule)
BITCOIN_SIGNET
}
enum EAnchoringStatus {
QUEUED // ⚠️ OBSOLÈTE : Non utilisé de manière fiable
ATTEMPTING // ⚠️ OBSOLÈTE : Non utilisé de manière fiable
VERIFIED_ON_CHAIN // ⚠️ OBSOLÈTE : Le système se base uniquement sur tx_id
FAILED
ABANDONED
}
```
**⚠️ IMPORTANT** : Le champ `status` n'est plus utilisé de manière fiable. Le système vérifie uniquement la présence de `tx_id` :
- `tx_id` présent = Ancrage réussi
- `tx_id` absent = Ancrage non effectué
---
## 3. Processus d'Ancrage
### Séquence de Traitement (Upload)
Pour **tous les documents** (Client, Tiers, Notaire invité, Notaire), la séquence est :
1. **Calcul hash original** (fichier original)
2. **Filigrane** (sur fichier original non chiffré)
3. **Calcul hash filigrané et ancrage blockchain** (hash filigrané, génération txid)
4. **Chiffrement** du document filigrané
5. **Upload IPFS** du document chiffré
6. **Stockage métadonnées** fichier (hash original + hash filigrané)
7. **Stockage certificat BDD** (document_anchor avec hash filigrané + hash original + txid)
### Ancrage Automatique lors de la Validation
**Déclenchement** : Lors de la validation d'un document via `PUT /api/v1/notary/documents/:uid`
**Fichier** : `lecoffre-back-main/src/app/api/notary/DocumentsController.ts`
**Workflow** :
```typescript
if (documentEntityUpdated.document_status === EDocumentStatus.VALIDATED && firstFileUid) {
// Ancrage automatique (asynchrone, non bloquant)
if (userUid) {
void this.documentAnchorsService
.anchorDocument(documentEntityUpdated.uid, userUid)
.then((anchor) => {
SafeLogger.info(`✅ Document ${documentEntityUpdated.uid} automatically anchored`);
})
.catch((error) => {
SafeLogger.error(`❌ Auto-anchoring failed: ${error}`);
});
}
}
```
**Points importants** :
- ✅ L'ancrage est **asynchrone** (non bloquant)
- ✅ L'ancrage est lancé en **arrière-plan**
- ✅ Si l'ancrage échoue, le document reste validé
### Ancrage Document
**Workflow détaillé** :
1. **Récupération document + fichier**
2. **Vérification ancrage Tezos existant** (fallback)
3. **Génération version filigranée** si absente
4. **Téléchargement version filigranée** et calcul hash
5. **Vérification ancrage Signet existant**
6. **Création ancrage `QUEUED`**
7. **Ancrage blockchain** via `BitcoinSignetService.anchorHash()`
8. **Génération `proof_data`** (incluant fallback Tezos)
9. **Mise à jour ancrage** avec `tx_id`, `tx_link`, `proof_data`
### Ancrage Dossier
**Workflow détaillé** :
1. **Récupération tous documents validés** du dossier
2. **Génération filigranes manquants** pour tous documents
3. **Génération ZIP complet** du dossier :
- Originaux (01_originaux/)
- Filigranés (02_filigranes/)
- Certificats (03_certificats/)
- Preuves JSON (04_preuves/)
4. **Upload ZIP vers S3**
5. **Ancrage hash ZIP complet** sur blockchain
6. **Génération preuve dossier**
7. **Création/Mise à jour ancrage dossier**
**Condition** : Le dossier doit être à **100% de complétion** (tous documents validés)
---
## 4. Certificats et ZIP
### Génération Automatique de Certificats
#### Téléchargement Individuel (Document Validé et Ancré)
**Endpoints** :
- Notaire : `GET /api/v1/notary/files/download/:uid`
- Client : `GET /api/v1/customer/files/download/:uid`
**Comportement automatique** : Pour les documents validés (`VALIDATED`) et ancrés avec statut `VERIFIED_ON_CHAIN`, le téléchargement génère **automatiquement** un ZIP contenant :
1. **PDF filigrané** : Document avec filigrane appliqué (suffixe `_aplc.pdf`)
2. **Certificat d'ancrage** : Certificat PDF Bitcoin Signet (préfixe `certificat_`)
**Exemple de ZIP** :
```text
document_001_CNI_DUPONT_Jean_avec_certificat.zip
├── 001_CNI_DUPONT_Jean_aplc.pdf (PDF filigrané)
└── certificat_001_CNI_DUPONT_Jean.pdf (Certificat d'ancrage)
```
#### Téléchargement Multiple (ZIP de Fichiers)
**Service** : `ZipService.createZipFromFiles()`
**Workflow de recherche d'ancrage** :
1. **PRIORITÉ** : Recherche par `document_uid` (plus fiable)
2. **FALLBACK** : Si pas trouvé, recherche par hash du fichier
3. **Vérification statut** : Le certificat n'est généré que si `status === VERIFIED_ON_CHAIN`
#### Téléchargement Dossier Complet
**Service** : `OfficeFolderAnchorsService.generateFolderZip()`
**Structure ZIP** :
```text
dossier_12345678_Acte_de_vente/
├── _LISEZ-MOI.txt (Instructions vérification)
├── 01_originaux/
│ ├── acte_vente.pdf (Version originale)
│ └── compromis_vente.pdf
├── 02_filigranes/
│ ├── acte_vente.pdf (⚠️ VERSION ANCRÉE avec filigrane)
│ └── compromis_vente.pdf
├── 03_certificats/
│ ├── certificat_acte_vente.pdf (PDF certif blockchain)
│ └── certificat_compromis_vente.pdf
└── 04_preuves/
├── preuve_acte_vente.json (JSON avec hashs + TX)
└── preuve_compromis_vente.json
```
### Contenu des Certificats PDF
**Informations affichées** :
1. **Hashs complets** :
- Empreinte SHA-256 (original) : Hash complet du document initial (64 caractères hex)
- Empreinte SHA-256 (filigrané) : Hash complet du document avec filigrane (64 caractères hex)
- Source : `proof_data.document.original_hash` et `proof_data.document.watermarked_hash`
2. **Lien de transaction** :
- Format : `https://mempool.4nkweb.com/fr/tx/<txid>`
- Construit automatiquement à partir de `anchor.tx_id` si disponible
- Fallback sur `anchor.tx_link` si `tx_id` absent
3. **Autres informations** :
- Identifiant unique du document
- Informations du dossier (numéro, nom)
- Informations de l'office notarial
- Type de document
- Déposant
- Validateur (UID)
- Nom de fichier normé
- Blockchain utilisée (Bitcoin Signet Notaires)
- Statut d'ancrage
- ID de transaction
- Hauteur de bloc (si disponible)
- Date d'ancrage
### Filigrane Visuel
**Spécifications** :
- **Texte** : "lecoffre.io"
- **Position** : Diagonal (45°), centré sur chaque page
- **Opacité** : 15% (visible mais non gênant)
- **Couleur** : Gris clair (#CCCCCC)
- **Police** : Arial, 48pt
- **Pages** : Toutes les pages du PDF
---
## 5. Vérification et Statuts
### Logique de Vérification
**⚠️ IMPORTANT** : Le système d'ancrage V3 ne fonctionne plus avec des statuts intermédiaires. L'ancrage est maintenant **immédiat** :
- ✅ **Ancré** : L'ancrage a un `tx_id` (transaction ID Bitcoin)
- ❌ **Non ancré** : L'ancrage n'a pas de `tx_id`
**Il n'y a plus d'étape intermédiaire** comme `ATTEMPTING`, `QUEUED`, ou `VERIFYING_ON_CHAIN`.
### Backend
Le backend vérifie uniquement la présence de `tx_id` :
```typescript
if (anchorStatus.tx_id) {
setAnchorStatus(AnchorStatus.VERIFIED_ON_CHAIN);
} else {
setAnchorStatus(AnchorStatus.NOT_ANCHORED);
}
```
### Frontend
Le frontend utilise l'enum `AnchorStatus` mais ne vérifie que `tx_id` :
```typescript
export enum AnchorStatus {
"VERIFIED_ON_CHAIN" = "VERIFIED_ON_CHAIN",
"NOT_ANCHORED" = "NOT_ANCHORED",
}
// Vérification
if (anchorStatus.tx_id) {
setAnchorStatus(AnchorStatus.VERIFIED_ON_CHAIN);
} else {
setAnchorStatus(AnchorStatus.NOT_ANCHORED);
}
```
### Masquage du Statut "Ancrage en cours"
**Objectif** : Améliorer l'expérience utilisateur en ne montrant pas le statut transitoire "Ancrage en cours" pour les documents validés.
**Comportement** : Pour les documents avec `document_status === VALIDATED`, le statut "Ancrage en cours" (`ATTEMPTING`) n'est **pas affiché** dans l'interface utilisateur.
**Composant `AnchorBadge`** :
```typescript
if (documentStatus === "VALIDATED" && status === "ATTEMPTING") {
return null; // Badge not displayed
}
```
### Vérification Publique
**Endpoints** :
- `GET /api/v1/public/verify/:hash` : Vérification par hash (SANS authentification)
- `POST /api/v1/public/verify-file` : Vérification par upload fichier (SANS authentification)
**Page frontend** : `/verify-document`
**Fonctionnalités** :
- Upload fichier (drag & drop ou sélection)
- Calcul hash automatique (SHA256)
- Appel API public (aucune authentification requise)
- Affichage résultat (success/error)
- Lien explorateur blockchain
### Procédure de Vérification
**Pour utilisateur final** :
1. Télécharger ZIP complet du dossier
2. Extraire le ZIP
3. Ouvrir fichier dans `04_preuves/preuve_XXX.json`
4. Noter le `watermarked_hash`
5. Calculer hash du fichier dans `02_filigranes/` :
```bash
shasum -a 256 02_filigranes/acte_vente.pdf
```
6. Comparer les deux hashs (doivent être identiques)
7. Vérifier TX : Ouvrir `tx_link` du JSON dans navigateur
8. ✅ **Validé** si hash OK + TX confirmée sur blockchain
**Pour audit technique** :
1. Parser JSON de preuve
2. Extraire `watermarked_hash`, `txid`, `block_height`
3. Requête API blockchain (Mempool Space) :
```bash
curl https://mempool.4nkweb.com/signet/api/tx/{txid}
```
4. Vérifier :
- TX existe
- Hash dans OP_RETURN correspond
- Block confirmé
5. Recalculer hash fichier filigrané
6. Comparer avec `watermarked_hash`
7. ✅ **Preuve authentique** si tout correspond
---
## 6. Scripts et Maintenance
### Scripts Disponibles
#### Audit des Ancrages Manquants
**Script** : `audit-missing-anchors.js`
**Usage** :
```bash
ENV=prod npm run anchorage:audit
```
**Description** :
- Analyse tous les documents validés sans ancrage
- Analyse tous les dossiers archivés sans ancrage
- Génère un CSV avec la liste des documents/dossiers à réancrer
- Ne modifie aucune donnée (lecture seule)
**Résultat** :
- Affichage console avec statistiques
- CSV généré : `logs/audit-missing-anchors_YYYYMMDD_HHMMSS.csv`
#### Ancrage V3 de Dossiers Existants
**Script** : `anchor-existing-folders-v3.js`
**Usage** :
```bash
# Ancrage tous dossiers LIVE + ARCHIVED (par défaut)
ENV=prod npm run anchorage
# Dry-run (simulation)
ENV=prod npm run anchorage -- --all --batch=1 --dry-run
# Ancrage réel avec limite batch
ENV=prod npm run anchorage -- --all --batch=5
# Ancrage de dossiers spécifiques
ENV=prod npm run anchorage -- --folder-uids=uid1,uid2,uid3
```
**Options** :
- `--all-archived` : Tous les dossiers ARCHIVED sans ancrage
- `--all-validated` : Tous les dossiers VALIDATED sans ancrage
- `--all` : Tous les dossiers VALIDATED + ARCHIVED
- `--folder-uids=uid1,uid2,...` : Liste UIDs dossiers spécifiques
- `--batch=N` : Nombre max de dossiers par batch (défaut: 10)
- `--dry-run` : Simulation sans ancrage réel
- `--skip-documents` : Ancrer seulement dossiers (pas documents individuels)
#### Vérifications Systématisées des Colonnes d'Ancrage
Depuis 2025-01-XX, les scripts de déploiement vérifient systématiquement la présence des colonnes indispensables pour l'ancrage avant d'exécuter le réancrage.
**Colonnes vérifiées** :
- `document_anchors.proof_data` (JSONB) - Données de preuve structurées
- `document_anchors.confirmations` (INTEGER) - Nombre de confirmations blockchain
- `document_anchors.anchor_job_id` (VARCHAR) - ID du job d'ancrage
- `document_notary_anchors.proof_data` (JSONB) - Données de preuve structurées
- `document_notary_anchors.confirmations` (INTEGER) - Nombre de confirmations blockchain
**Garanties** :
- ✅ Vérification de l'existence des tables avant toute modification
- ✅ Ajout automatique des colonnes manquantes (8 points de vérification)
- ✅ Blocage du réancrage si colonnes manquantes détectées
- ✅ Messages d'erreur explicites indiquant les colonnes manquantes
**Ordre d'exécution garanti** :
```text
resetDatabase → migrateResolveDatabase → reanchorAll
↓ ↓ ↓
Base vide Colonnes ajoutées Vérification finale
↓ ↓
Si manquantes → Blocage si manquantes
```
📖 Voir [README.md](./README.md#consolidation-operationnelle-ex-operationsmd) et [DEPLOYMENT.md](./DEPLOYMENT.md) pour les corrections sur les colonnes d'ancrage.
#### Réancrage de Documents/Dossiers
**Script** : `reanchor-documents.js`
**Usage** :
```bash
# Dry-run (simulation)
ENV=prod npm run anchorage:reanchor -- --dry-run
# Réancrage réel
ENV=prod npm run anchorage:reanchor -- --document-uids=uid1,uid2
```
**Options** :
- `--document-uids=uid1,uid2,...` : Liste UIDs documents spécifiques
- `--folder-uids=uid1,uid2,...` : Liste UIDs dossiers spécifiques
- `--all-missing` : Réancrer TOUS les documents/dossiers sans ancrage (⚠️)
- `--batch=N` : Nombre max de documents/dossiers par batch (défaut: 50)
- `--dry-run` : Simulation sans ancrage réel
#### Nettoyage Ancres Dossiers Incomplets
**Script** : `clean-incomplete-folder-anchors.ts`
**Contexte** :
- Certains dossiers peuvent avoir été ancrés alors qu'ils n'étaient pas à 100%
- Seuls les dossiers complets (100% documents validés) doivent être ancrés
**Usage** :
```bash
# Depuis le conteneur backend
cd lecoffre-back-main
npx ts-node src/scripts/clean-incomplete-folder-anchors.ts --dry-run
npx ts-node src/scripts/clean-incomplete-folder-anchors.ts
```
**Actions** :
- Identifie les dossiers ancrés avec complétion < 100%
- Supprime les ancres correspondantes dans `office_folder_anchors`
- Réinitialise les liens `folder_anchor_uid = NULL` dans `office_folders`
### ⚠️ IMPORTANT : Prérequis
1. **Colonnes V3** : La base doit avoir toutes les colonnes V3 (`watermarked_s3_key`, `watermarked_at`, `zip_hash`, `zip_s3_key`, `zip_size`, `proof_data`)
2. **Prisma Client** : Le client Prisma doit être généré avec le bon schéma
3. **Connexion BDD** : Les scripts utilisent `DATABASE_URL` (configurée sur le serveur ou via `.env.<env>`)
### Workflow Recommandé
1. **Audit initial** :
```bash
ENV=prod npm run anchorage:audit
```
2. **Test avec dry-run** :
```bash
ENV=prod npm run anchorage -- --all-archived --batch=1 --dry-run
```
3. **Ancrage réel** :
```bash
ENV=prod npm run anchorage -- --all-archived --batch=5
```
---
## 7. Troubleshooting
### Certificats Manquants dans le ZIP
**Symptôme** : Lors de la validation d'un document, le document apparaît comme validé, mais le certificat d'ancrage n'est pas présent dans le ZIP téléchargé.
**Root Cause** :
1. Recherche par hash uniquement (peut échouer)
2. Pas de vérification du statut avant génération
**Solution** :
1. Recherche prioritaire par `document_uid` (plus fiable)
2. Fallback par hash si nécessaire
3. Vérification du statut (`status === VERIFIED_ON_CHAIN`) avant génération
### Timing et Synchronisation
**Scénario typique** :
1. **T0** : Document validé → Ancrage lancé en arrière-plan
2. **T0+2s** : Ancrage créé avec `status = ATTEMPTING`
3. **T0+5s** : Transaction Bitcoin confirmée → `status = VERIFIED_ON_CHAIN` (après 6 confirmations)
4. **T0+10s** : Utilisateur télécharge le ZIP → Certificat inclus ✅
**Cas limite** : Si l'utilisateur télécharge le ZIP **immédiatement après validation** (avant que l'ancrage soit vérifié) :
- ⚠️ L'ancrage peut être en statut `ATTEMPTING` ou `QUEUED`
- ⚠️ Le certificat **ne sera pas inclus** dans le ZIP
- ✅ L'utilisateur peut **relancer le téléchargement** plus tard pour obtenir le certificat
### Fichiers Sans Clé de Chiffrement (.expired)
**Contexte** : Les dumps de base de données provenant de versions antérieures peuvent contenir des fichiers avec `key = null` (clé de chiffrement perdue).
**Solution implémentée** :
- `FilesService.download()` : Retourne fichier vide `.expired` si `key = null`
- `WatermarkService.addWatermarkAndUpload()` : Upload fichier vide `.expired` sur IPFS
- `DocumentAnchorsService.anchorDocument()` : Ancrage avec hash du buffer vide
**Résultat** :
- ✅ Scripts d'ancrage ne crashent plus sur fichiers sans clé
- ✅ Fichiers `.expired` créés automatiquement
- ✅ Ancrage blockchain avec hash du buffer vide
- ⚠️ Utilisateur informé que fichier non récupérable
### Checklist de Vérification
Pour diagnostiquer un problème de certificat manquant dans un ZIP :
- [ ] Vérifier que le document est **validé** (`document_status === VALIDATED`)
- [ ] Vérifier qu'un ancrage existe pour ce document (`document_anchors` table)
- [ ] Vérifier le **statut de l'ancrage** (`status === VERIFIED_ON_CHAIN`)
- [ ] Vérifier les **logs backend** pour voir si l'ancrage a été trouvé
- [ ] Vérifier que `includeAnchors = true` lors de l'appel à `createZipFromFiles`
- [ ] Vérifier que le fichier n'est pas un `isNotaryFiles` (pas d'ancrage pour les fichiers notaire)
---
## 8. Références
### Documentation Détaillée
- **Architecture V3** : `docs/ANCRAGE_V3_ARCHITECTURE.md` - Architecture complète V3
- **Certificats et ZIP** : `docs/ANCRAGE_CERTIFICATS_ZIP.md` - Génération certificats et ZIP
- **Scripts** : `docs/SCRIPTS.md` - Scripts d'audit et d'ancrage
- **Statuts** : `docs/ANCHOR_STATUS_VERIFICATION.md` - Vérification des statuts d'ancrage
- **Séquences de traitement** : `docs/DOCUMENT_PROCESSING_SEQUENCES_V2.md` - Séquence complète upload/ancrage
### Services Backend
- **WatermarkService** : `lecoffre-back-main/src/services/common/WatermarkService/WatermarkService.ts`
- **DocumentAnchorsService** : `lecoffre-back-main/src/services/notary/DocumentAnchorsService/DocumentAnchorsService.ts`
- **OfficeFolderAnchorsService** : `lecoffre-back-main/src/services/notary/OfficeFolderAnchorsService/OfficeFolderAnchorsService.ts`
- **ProofDataService** : `lecoffre-back-main/src/services/notary/ProofDataService/ProofDataService.ts`
- **BitcoinSignetService** : `lecoffre-back-main/src/services/common/BitcoinSignetService/BitcoinSignetService.ts`
- **AnchorCertificateService** : `lecoffre-back-main/src/services/notary/AnchorCertificateService/AnchorCertificateService.ts`
- **ZipService** : `lecoffre-back-main/src/services/common/ZipService/ZipService.ts`
### Controllers
- **DocumentsController** : `lecoffre-back-main/src/app/api/notary/DocumentsController.ts`
- **FilesController (Notary)** : `lecoffre-back-main/src/app/api/notary/FilesController.ts`
- **FilesController (Customer)** : `lecoffre-back-main/src/app/api/customer/FilesController.ts`
- **PublicAnchorVerificationController** : `lecoffre-back-main/src/app/api/public/PublicAnchorVerificationController.ts`
### Scripts
- **Audit** : `lecoffre-back-main/src/scripts/audit-missing-anchors.ts`
- **Ancrage dossiers** : `lecoffre-back-main/src/scripts/anchor-existing-folders-v3.ts`
- **Réancrage** : `lecoffre-back-main/src/scripts/reanchor-documents.ts`
- **Nettoyage** : `lecoffre-back-main/src/scripts/clean-incomplete-folder-anchors.ts`
### Frontend
- **Page vérification** : `lecoffre-front-main/src/pages/verify-document.tsx`
- **Composant AnchorBadge** : `lecoffre-front-main/src/front/Components/DesignSystem/AnchorBadge/`
---
**Dernière mise à jour** : 2025-11-24
**Version** : 3.1.2

Some files were not shown because too many files have changed in this diff Show More