Chapitres
Sur cette page
DOC-05 / Référence technique · Chapitre 04
Chantiers, travaux & tâches
Le modèle de données qui organise tout travail : un chantier, N travaux, N tâches — créés atomiquement, verrouillés, cascadés.
Le harness Synedre OS organise tout travail de développement en une hiérarchie persistée en base — 1 chantier = N travaux = N tâches — afin que l'orchestrateur (Atlas) et les agents puissent créer, suivre, paralléliser et clôturer du travail sans état hors-base. Cette page documente le modèle de données réel, l'API d'entité Python qui le manipule (create_with_skeleton), la procédure de création en sept étapes, le mécanisme de verrou multi-session et les cascades de statut (dont la résolution travail-bis d'un travail en pause).
Le modèle de données
Hiérarchie
ps_ac_chantier (1 chantier)
└─ ps_ac_chantier_travail (N travaux, FK id_chantier)
└─ sy_chantier_tache (N tâches, FK id_travail)
Trois entités Python encapsulent ces tables (chacune étend la classe de base Entity) :
| Entité | Classe | Table | Clé primaire |
|---|---|---|---|
| Chantier | ChantierEntity | ps_ac_chantier | id_chantier |
| Travail | TravailEntity | ps_ac_chantier_travail | id_travail |
| Tâche | TacheEntity | sy_chantier_tache | id_tache |
| Verrou | ChantierLockEntity | sy_chantier_lock | (id_chantier, owner_kind) |
Nommage hétérogène (dette de migration) : les tables chantier/travail conservent le préfixeps_ac_*, la table tâche et le verrou sont ensy_*. La clé primaire desy_chantier_tacheporte encore un nom hérité de CodeMyShop. Ne pas « corriger » sans chantier dédié.
Toutes les requêtes ciblent le schéma vaisseau_mere_ac de la base du vaisseau-mère.
Statuts
Les statuts canoniques chantier/travail sont définis dans le code :
CANONICAL_STATUSES = {"discovery","planning","dev","test","bascule","done","paused","cancelled"}
CANONICAL_PRIORITIES = {"P0","P1","P2","P3"}
La colonneps_ac_chantier.statusn'a pas de contrainte CHECK en base — l'énumération n'est garantie que par la validation applicative. Statuts effectivement présents à ce jour :planning,dev,test,done,cancelled.
Les tâches (sy_chantier_tache.status) utilisent un vocabulaire distinct : todo, doing, done, paused, cancelled (défaut todo).
Scopes
Le scope discrimine le périmètre de synchronisation (monolithe vs OSS) et conditionne des validations.
Scope chantier — ps_ac_chantier.scope, avec une contrainte CHECK réelle en base :
synedre | codemyshop-oss | codemyshop-enterprise | tenant | business | juridique | negociation | conseil
La docstring de create_with_skeleton ne liste que les cinq premières valeurs. La base en autorise trois de plus (ajoutées pour les intents Atlas). En cas de conflit, la base fait foi.
Scope tâche — sy_chantier_tache.scope, validé par TacheEntity.VALID_SCOPES ET par un CHECK base identique :
synedre-internal | codemyshop-oss | codemyshop-enterprise | tenant-single | tenant-multi | infra | doctrine
Un scope tâche invalide lève une erreur dès TacheEntity.create().
Champs notables
ps_ac_chantier :
| Champ | Rôle |
|---|---|
mission_letter | Lettre de mission structurée (étape 2/5 de la procédure) |
preprod_test_plan | Plan de test preprod — markdown OU YAML v2 (déclenche une QA auto) |
ship_command | Commande exacte de clôture, ex. ./ship <tenant> |
auto_explode | Kill-switch par chantier de l'auto-explode discovery→impl |
client_id | Codename client ou NULL (chantier interne) |
ps_ac_chantier_travail porte notamment agent_codename, agent_prompt, zone_perimeter, exit_criteria, les colonnes JSON context_json/decisions_json/discoveries_json, depends_on_travail_id (graphe de dépendances avec détection de cycle) et resolves_travail_id (cœur du pattern travail-bis).
sy_chantier_tache porte assignee_codename, estimated_tokens, recommended_model, position, last_test_result.
Recrutement persisté
Le recrutement d'agents sur un chantier (étape 3) est persisté dans la table sy_chantier_agent, distincte de sy_chantier_tache.assignee_codename (qui ne porte que l'assignation d'une tâche isolée).
| Colonne | Rôle |
|---|---|
id_assignment | identifiant d'assignation |
id_chantier | FK chantier |
agent_codename | agent recruté |
role | production / lead_production / validation / lead_validation |
position | ordre d'affichage |
date_assigned / date_unassigned | cycle de vie |
notes | justification du recrutement |
C'est cette table que la cascade interroge pour décider si une équipe QA est présente : un décompte des rôles validation/lead_validation > 0 bascule la cascade tâche→travail vers une exécution QA au lieu d'un bump direct.
create_with_skeleton — création atomique
ChantierEntity.create_with_skeleton() est l'unique voie canonique pour créer un chantier : elle insère atomiquement le chantier + son premier travail + au moins une tâche, empêchant les chantiers orphelins.
Signature
ChantierEntity().create_with_skeleton(
codename, title, *,
first_travail: dict,
first_tache: dict | None = None, # DEPRECATED (singulier, backward compat)
first_taches: list[dict] | None = None, # PRÉFÉRÉ (doctrine v3)
client_id: str | None = None,
priority: str = "P2",
scope: str | None = None,
current_focus: str | None = None,
notes: str | None = None,
) -> dict
Chaque tâche de first_taches : {title, assignee_codename} + optionnels {priority, estimated_tokens, estimated_h, description, position, recommended_model, scope, skills, tools}.
Validations bloquantes
Toutes accumulées avant le premier INSERT :
- codename kebab-case — minuscules + chiffres + tirets, 4 à 64 caractères.
- codename unique — erreur si déjà utilisé.
- priority ∈ {P0, P1, P2, P3}.
- first_travail.codename / .title valides et non vides.
- ≥1 tâche —
first_tacheXORfirst_tachesobligatoire ; fournir les deux est une erreur. - assignee connu — chaque
assignee_codenamedoit exister dans la table des agents. - ≥2 assignees distincts si le scope commence par
tenant(doctrine v3) — le set des assignees doit contenir au moins deux codenames distincts.
Warning non-bloquant : un estimated_tokens manquant émet un avertissement mais n'empêche pas la création — un auto-estimator le comblera.
Atomicité / rollback
L'insertion se fait en un seul appel d'écriture enveloppé BEGIN; … COMMIT; avec arrêt sur erreur : si un INSERT échoue, toute la transaction est annulée — pas de chantier sans travail, pas de travail sans tâche. Les travaux et tâches référencent leur parent par INSERT ... SELECT id_chantier FROM ps_ac_chantier WHERE codename=…, donc l'ordre des INSERT dans la même transaction garantit la résolution des clés étrangères.
Estimation de tokens & modèle recommandé
Si recommended_model n'est pas fourni, il est auto-calculé :
P0 OU échec récurrent (≥2 itérations test=fail) → opus
estimated_tokens >= 8000 (ou None) → opus
estimated_tokens >= 1500 → sonnet
sinon → haiku
Hors du skeleton, TacheEntity.create() auto-estime estimated_tokens s'il est absent, puis recalcule recommended_model sur la ligne.
Skills / tools
Après insertion, le skeleton attache les skills/tools optionnels par tâche. Un skill/tool inconnu n'échoue pas : il génère un avertissement et une proposition en attente.
CLI
python3 synedre/ac_entities/chantier.py --create-with-skeleton [--json-file PATH] # sinon payload JSON via stdin
Un hook avertit si un INSERT SQL brut sur ces tables est détecté hors de cette façade. Le module expose d'autres verbes CLI (--list, --show, --create-travail, --update, --close, --resolve…) — voir --help pour la liste complète.
La procédure en sept étapes
Règle dure : aucun chantier ne se crée sans parcourir ces étapes dans l'ordre. Pour les chantiers déclenchés par email, une façade exécute les étapes 0a→4 sous contrainte (refus si scan KO ou moins de deux agents). Elle s'arrête à la création atomique du skeleton et n'effectue ni l'étape 2 (rédaction de la mission_letter) ni l'étape 5 (UPDATE post-skeleton) — ces deux étapes restent manuelles.
| Étape | Action | Mécanisme |
|---|---|---|
| 0a | Lire la demande email verbatim | façade inbox du vaisseau-mère |
| 0b | Extraire + scanner les pièces jointes avant toute lecture | extraction puis scan antivirus ; verdict ≠ clean ⇒ blocage |
| 1 | Audit des agents | requête sur la table des agents actifs (codename, rôle, groupe) |
| 2 | Rédiger la mission_letter | OBJECTIF / CONTEXTE / PÉRIMÈTRE / LIVRABLES / CONTRAINTES / DOCTRINE, à partir du contenu lu en 0a/0b |
| 3 | Recrutement explicite | ≥2 agents distincts si scope tenant |
| 4 | create_with_skeleton atomique | cf section précédente |
| 5 | UPDATE post-skeleton | mission_letter + scope + preprod_test_plan + ship_command via UPDATE nominatif sur le codename |
La façade email
Flux réel d'une création de chantier déclenchée par email :
Les libellés « ÉTAPE 1…5 » imprimés par le script sont sa propre numérotation interne, à ne pas confondre avec les étapes 0a→5 de la procédure. Le « ÉTAPE 5 — PAYLOAD SKELETON » du script correspond à l'étape 4 de la procédure (construction + INSERT atomique). La façade ne va jamais au-delà.
--id <email_id>
ÉTAPE 1 récupère l'email (via l'API interne, jamais en SQL brut)
ÉTAPE 2 extrait + scanne les pièces jointes
→ si un attachement n'est pas "clean" → BLOCAGE, return 2
ÉTAPE 3 résumé de la demande (titre / client / priorité / scope)
ÉTAPE 4 liste les agents actifs + invite au recrutement
→ si moins de 2 agents distincts → BLOCAGE, return 3
ÉTAPE 5 payload first_taches (1 tâche/agent) + first_travail "-init" (phase discovery)
→ create_with_skeleton(**payload) = étape 4 de la procédure
Codes de sortie : 2 = attachement non-clean, 3 = moins de deux agents, 4 = erreur de validation. --dry-run affiche le payload sans INSERT.
Deux contrôles « ≥2 agents », distincts : la façade exige TOUJOURS ≥2 agents — contrôle inconditionnel, indépendant du scope ; seul le message de blocage cite le scope.create_with_skeletonapplique sa propre validation ≥2 uniquement si le scope commence partenant.
La façade crée un unique travail-initen phasediscovery. La transition vers les travaux d'implémentation reste manuelle ou passe par l'auto-explode.
Champs preprod_test_plan / ship_command (étape 5)
Obligatoires pour tout chantier non-trivial. Sans eux, la cascade vers le chantier journalise un avertissement non-bloquant, mais le bandeau « PRÊT À SHIP » ne peut pas s'afficher. Si preprod_test_plan commence par version: (YAML v2), la cascade déclenche une QA automatisée en arrière-plan.
Le verrou multi-session
ChantierLockEntity permet d'ouvrir N chantiers différents en parallèle (N terminaux / workers) tout en refusant deux sessions sur le même chantier. Ce n'est pas un bloqueur ergonomique mais un filet anti-collision.
Modèle
- Table
sy_chantier_lock, clé primaire composite(id_chantier, owner_kind)oùowner_kind∈ {user, worker} (CHECK base). user= session humaine ;worker= sous-agent spawné par le worker de tâches. Les deux peuvent coexister sur le même chantier ; chaque kind ne conteste que son propre slot.owner_kindest détecté via une variable d'environnement de contexte worker, puis par fallback sur le parcours des processus parents.session_id= identifiant de session > empreinte du chemin de transcript >pid-user@host.
TTL & acquisition
Le TTL est de 30 minutes. L'acquisition fait un INSERT ... ON CONFLICT DO UPDATE qui ne réussit que si la session correspond OU si le verrou est expiré (dernière activité > 30 minutes). Sinon elle retourne {acquired: False, owner: {...}}.
Le verrou est acquis automatiquement à la récupération du contexte d'un chantier. En cas de refus, une erreur ChantierLockedError est levée et interceptée par le skill correspondant. Kill-switch disponible pour le désactiver totalement. Mode legacy : si la table n'existe pas, l'acquisition réussit toujours.
Verbes & cycle de vie
| Opération | Méthode | CLI |
|---|---|---|
| Inventaire des verrous vivants | list_active() | --locks |
| Libérer son verrou | release() (idempotent) | --release <codename> |
| Évincer l'owner précédent (même kind) | force_claim() | --force-claim <codename> |
| Maintenir vivant | heartbeat() | — |
Trois filets de libération :
- Hook de fin de session — libération rapide quand une session se termine.
- Cron TTL — toutes les 5 minutes, supprime les verrous dont la dernière activité dépasse 30 minutes. Idempotent, exit 0 toujours, skip silencieux si la base est indisponible. C'est le vrai garde-fou (crash, session coupée).
force_claimmanuel.
Les cascades de statut
Les update() des entités sont surchargés pour propager les statuts terminaux d'un niveau au suivant. Cascade limitée à 1 cran par niveau (chaque cran re-déclenche sa propre cascade).
sy_chantier_tache.update(status='done'/'cancelled')
└─ _cascade_to_travail
si TOUTES les tâches du travail done/cancelled ET ≥1 done :
├─ équipe QA présente sur le chantier ?
│ COUNT(*) sy_chantier_agent WHERE role IN ('validation','lead_validation')
│ OUI → qa_run(id_travail) :
│ verdict pass ? → travail 'done'
│ sinon → fix tasks créées, travail reste 'dev' (PAS de bump)
│ NON → bump direct du travail en 'done'
ps_ac_chantier_travail.update(status='done'/'cancelled')
└─ _cascade_resolves (sur 'done' uniquement) → travail-bis
└─ _cascade_to_chantier
si TOUS les travaux done/cancelled ET ≥1 done
→ chantier 'test' (= preprod, review du Fondateur)
└─ bug-detector (sur 'done', non-bloquant)
Le bump tâche→travail n'est pas systématiquement direct. Si le chantier porte une équipe QA — au moins un agent avec rôlevalidation/lead_validation— la cascade délègue à une exécution QA au lieu de bumper : le travail ne passedoneque si le verdict est positif, sinon des fix tasks sont créées et le travail reste actif. Une exception de la QA est fail-safe (log, aucun bump).
Le passage final test → done reste manuel (acte du Fondateur après ./ship), jamais cascadé.
Cascade SQL native (inbox)
Indépendamment des cascades applicatives Python ci-dessus, un trigger base natif propage le passage done côté base, invisible depuis Python :
- Une fonction + un trigger
AFTER UPDATE OF status ON ps_ac_chantier. - Quand le statut passe à
done(et ne l'était pas), tous les emails entrants rattachés au chantier passentresolved(horsresolved/spam). - Conséquence : marquer un chantier
donerésout automatiquement les emails entrants rattachés, sans aucune ligne Python. À garder en tête pour qui débugge un email passéresolved« tout seul ».
Garde-fou discovery-only / auto-explode
La cascade vers le chantier ne promeut PAS en test si tous les travaux done sont en phase discovery : sinon un chantier neuf (créé par la façade email avec un seul travail -init discovery) passerait en preprod review dès la fin de l'analyse, avant que les travaux d'implémentation existent.
Dans ce cas, l'auto-explode discovery→impl peut se déclencher, gardé par trois kill-switches successifs : variable d'environnement, flag base auto_explode=FALSE, et activation globale (YAML). Le chantier reste alors en planning pour review.
Resolve-paused — le pattern travail-bis
Quand un travail est mis en paused (bloqué), on crée un travail-bis qui porte resolves_travail_id = <id du paused>. Quand ce bis passe done, _cascade_resolves() nettoie automatiquement le paused parent, avant la cascade vers le chantier (car il change le décompte terminal du chantier).
Effets quand le bis passe done :
- paused →
done(mise à jour directe pour éviter la récursion de cascade) ; - tâches
tododu paused →cancelled(UPDATE batch direct) ; decisions_jsonappend symétrique sur le bis ET le paused (audit trail).
| Condition | Comportement |
|---|---|
resolves_travail_id IS NULL | no-op silencieux (path legacy) |
| paused et bis sur des chantiers différents | erreur (cross-chantier interdit) |
paused déjà done | no-op idempotent |
| cycle A↔B détecté | erreur |
| paused introuvable (FK orpheline) | warn + skip |
| paused.status ∉ {paused, dev, planning} | warn + skip |
Contraintes base associées : index partiel sur resolves_travail_id WHERE NOT NULL, CHECK anti-auto-résolution (resolves_travail_id <> id_travail), FK ON DELETE SET NULL. Le skill /chantier --resolve --dry-run expose ce pattern.
Pièges & invariants
- La lecture SQL casse sur les retours ligne :
mission_letter,notes,preprod_test_plan,current_focus,next_action,current_taskcontiennent des sauts de ligne. Les listings concernés passent par un lecteur CSV dédié. Réutiliser ce pattern pour toute nouvelle lecture de ces colonnes. - Les champs de l'entité doivent suivre la DDL : ajouter une colonne en base sans l'ajouter à
fieldsrend la mise à jour silencieusement inopérante sur cette colonne. - Pas de CHECK status en base sur chantier/travail — l'énumération n'est tenue que par le code. Un INSERT SQL brut peut introduire un statut hors-enum.
- Scope chantier : la base prime sur la docstring — toujours vérifier la contrainte CHECK plutôt que la docstring Python.
- Anti-leak : les identifiants de base de données sont lus depuis le fichier d'environnement racine. Jamais en clair dans le code ni dans la base.