Chapitres

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éClasseTableClé primaire
ChantierChantierEntityps_ac_chantierid_chantier
TravailTravailEntityps_ac_chantier_travailid_travail
TâcheTacheEntitysy_chantier_tacheid_tache
VerrouChantierLockEntitysy_chantier_lock(id_chantier, owner_kind)
Nommage hétérogène (dette de migration) : les tables chantier/travail conservent le préfixe ps_ac_*, la table tâche et le verrou sont en sy_*. La clé primaire de sy_chantier_tache porte 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 colonne ps_ac_chantier.status n'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 chantierps_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âchesy_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 :

ChampRôle
mission_letterLettre de mission structurée (étape 2/5 de la procédure)
preprod_test_planPlan de test preprod — markdown OU YAML v2 (déclenche une QA auto)
ship_commandCommande exacte de clôture, ex. ./ship <tenant>
auto_explodeKill-switch par chantier de l'auto-explode discovery→impl
client_idCodename 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).

ColonneRôle
id_assignmentidentifiant d'assignation
id_chantierFK chantier
agent_codenameagent recruté
roleproduction / lead_production / validation / lead_validation
positionordre d'affichage
date_assigned / date_unassignedcycle de vie
notesjustification 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 :

  1. codename kebab-case — minuscules + chiffres + tirets, 4 à 64 caractères.
  2. codename unique — erreur si déjà utilisé.
  3. priority ∈ {P0, P1, P2, P3}.
  4. first_travail.codename / .title valides et non vides.
  5. ≥1 tâchefirst_tache XOR first_taches obligatoire ; fournir les deux est une erreur.
  6. assignee connu — chaque assignee_codename doit exister dans la table des agents.
  7. ≥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.

ÉtapeActionMécanisme
0aLire la demande email verbatimfaçade inbox du vaisseau-mère
0bExtraire + scanner les pièces jointes avant toute lectureextraction puis scan antivirus ; verdict ≠ cleanblocage
1Audit des agentsrequête sur la table des agents actifs (codename, rôle, groupe)
2Rédiger la mission_letterOBJECTIF / CONTEXTE / PÉRIMÈTRE / LIVRABLES / CONTRAINTES / DOCTRINE, à partir du contenu lu en 0a/0b
3Recrutement explicite≥2 agents distincts si scope tenant
4create_with_skeleton atomiquecf section précédente
5UPDATE post-skeletonmission_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_skeleton applique sa propre validation ≥2 uniquement si le scope commence par tenant.
La façade crée un unique travail -init en phase discovery. 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)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_kind est 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érationMéthodeCLI
Inventaire des verrous vivantslist_active()--locks
Libérer son verrourelease() (idempotent)--release <codename>
Évincer l'owner précédent (même kind)force_claim()--force-claim <codename>
Maintenir vivantheartbeat()

Trois filets de libération :

  1. Hook de fin de session — libération rapide quand une session se termine.
  2. 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).
  3. force_claim manuel.

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ôle validation/lead_validation — la cascade délègue à une exécution QA au lieu de bumper : le travail ne passe done que 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 passent resolved (hors resolved/spam).
  • Conséquence : marquer un chantier done ré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 :

  1. paused → done (mise à jour directe pour éviter la récursion de cascade) ;
  2. tâches todo du paused → cancelled (UPDATE batch direct) ;
  3. decisions_json append symétrique sur le bis ET le paused (audit trail).
ConditionComportement
resolves_travail_id IS NULLno-op silencieux (path legacy)
paused et bis sur des chantiers différentserreur (cross-chantier interdit)
paused déjà doneno-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_task contiennent 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 à fields rend 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.