Chapters
On this page
DOC-05 / Technical reference · Chapter 04
Chantiers, travaux & tasks
The data model that organises all work: one chantier, N travaux, N tasks — created atomically, locked, cascaded.
The Synedre OS harness organises all development work into a hierarchy persisted in the database — 1 chantier = N travaux = N tasks — so that the orchestrator (Atlas) and the agents can create, track, parallelise and close work with no state outside the database. This page documents the actual data model, the Python entity API that drives it (create_with_skeleton), the seven-step creation procedure, the multi-session locking mechanism, and the status cascades (including the travail-bis resolution of a paused travail).
The data model
Hierarchy
ps_ac_chantier (1 chantier)
└─ ps_ac_chantier_travail (N travaux, FK id_chantier)
└─ sy_chantier_tache (N tasks, FK id_travail)
Three Python entities wrap these tables (each extends the base Entity class):
| Entity | Class | Table | Primary key |
|---|---|---|---|
| Chantier | ChantierEntity | ps_ac_chantier | id_chantier |
| Travail | TravailEntity | ps_ac_chantier_travail | id_travail |
| Task | TacheEntity | sy_chantier_tache | id_tache |
| Lock | ChantierLockEntity | sy_chantier_lock | (id_chantier, owner_kind) |
Heterogeneous naming (migration debt): the chantier/travail tables keep theps_ac_*prefix, while the task table and the lock aresy_*. The primary key ofsy_chantier_tachestill carries a name inherited from CodeMyShop. Do not "fix" this without a dedicated chantier.
Every query targets the vaisseau_mere_ac schema of the mothership database.
Statuses
The canonical chantier/travail statuses are defined in code:
CANONICAL_STATUSES = {"discovery","planning","dev","test","bascule","done","paused","cancelled"}
CANONICAL_PRIORITIES = {"P0","P1","P2","P3"}
Theps_ac_chantier.statuscolumn has no CHECK constraint in the database — the enum is only guaranteed by application-level validation. Statuses actually present so far:planning,dev,test,done,cancelled.
Tasks (sy_chantier_tache.status) use a distinct vocabulary: todo, doing, done, paused, cancelled (default todo).
Scopes
The scope discriminates the synchronisation perimeter (monolith vs OSS) and gates certain validations.
Chantier scope — ps_ac_chantier.scope, with a real CHECK constraint in the database:
synedre | codemyshop-oss | codemyshop-enterprise | tenant | business | juridique | negociation | conseil
The docstring of create_with_skeleton only lists the first five values. The database allows three more (added for Atlas intents). When they conflict, the database wins.
Task scope — sy_chantier_tache.scope, validated by TacheEntity.VALID_SCOPES AND by an identical database CHECK:
synedre-internal | codemyshop-oss | codemyshop-enterprise | tenant-single | tenant-multi | infra | doctrine
An invalid task scope raises an error right inside TacheEntity.create().
Notable fields
ps_ac_chantier:
| Field | Role |
|---|---|
mission_letter | Structured mission letter (step 2/5 of the procedure) |
preprod_test_plan | Preprod test plan — markdown OR YAML v2 (triggers auto QA) |
ship_command | Exact closing command, e.g. ./ship <tenant> |
auto_explode | Per-chantier kill-switch for the discovery→impl auto-explode |
client_id | Client codename or NULL (internal chantier) |
ps_ac_chantier_travail notably carries agent_codename, agent_prompt, zone_perimeter, exit_criteria, the JSON columns context_json/decisions_json/discoveries_json, depends_on_travail_id (dependency graph with cycle detection) and resolves_travail_id (the heart of the travail-bis pattern).
sy_chantier_tache carries assignee_codename, estimated_tokens, recommended_model, position, last_test_result.
Persisted recruitment
The recruitment of agents onto a chantier (step 3) is persisted in the sy_chantier_agent table, distinct from sy_chantier_tache.assignee_codename (which only carries the assignment of an isolated task).
| Column | Role |
|---|---|
id_assignment | assignment identifier |
id_chantier | chantier FK |
agent_codename | recruited agent |
role | production / lead_production / validation / lead_validation |
position | display order |
date_assigned / date_unassigned | lifecycle |
notes | recruitment justification |
This is the table the cascade queries to decide whether a QA team is present: a count of the validation/lead_validation roles > 0 switches the task→travail cascade to a QA run instead of a direct bump.
create_with_skeleton — atomic creation
ChantierEntity.create_with_skeleton() is the single canonical path to create a chantier: it atomically inserts the chantier + its first travail + at least one task, preventing orphan chantiers.
Signature
ChantierEntity().create_with_skeleton(
codename, title, *,
first_travail: dict,
first_tache: dict | None = None, # DEPRECATED (singular, backward compat)
first_taches: list[dict] | None = None, # PREFERRED (doctrine v3)
client_id: str | None = None,
priority: str = "P2",
scope: str | None = None,
current_focus: str | None = None,
notes: str | None = None,
) -> dict
Each task in first_taches: {title, assignee_codename} + optional {priority, estimated_tokens, estimated_h, description, position, recommended_model, scope, skills, tools}.
Blocking validations
All accumulated before the first INSERT:
- kebab-case codename — lowercase + digits + hyphens, 4 to 64 characters.
- unique codename — error if already used.
- priority ∈ {P0, P1, P2, P3}.
- first_travail.codename / .title valid and non-empty.
- ≥1 task —
first_tacheXORfirst_tachesrequired; providing both is an error. - known assignee — each
assignee_codenamemust exist in the agents table. - ≥2 distinct assignees if the scope starts with
tenant(doctrine v3) — the set of assignees must contain at least two distinct codenames.
Non-blocking warning: a missing estimated_tokens emits a warning but does not prevent creation — an auto-estimator will fill it in.
Atomicity / rollback
The insertion runs as a single write wrapped in BEGIN; … COMMIT; with stop-on-error: if one INSERT fails, the whole transaction is rolled back — no chantier without a travail, no travail without a task. Travaux and tasks reference their parent via INSERT ... SELECT id_chantier FROM ps_ac_chantier WHERE codename=…, so the INSERT order within the same transaction guarantees FK resolution.
Token estimation & recommended model
If recommended_model is not provided, it is auto-computed:
P0 OR recurring failure (≥2 test=fail iterations) → opus
estimated_tokens >= 8000 (or None) → opus
estimated_tokens >= 1500 → sonnet
otherwise → haiku
Outside the skeleton, TacheEntity.create() auto-estimates estimated_tokens when absent, then recomputes recommended_model on the row.
Skills / tools
After insertion, the skeleton attaches the optional per-task skills/tools. An unknown skill/tool does not fail: it emits a warning and a pending proposal.
CLI
python3 synedre/ac_entities/chantier.py --create-with-skeleton [--json-file PATH] # otherwise JSON payload via stdin
A hook warns if a raw SQL INSERT on these tables is detected outside this façade. The module exposes other CLI verbs (--list, --show, --create-travail, --update, --close, --resolve…) — see --help for the full list.
The seven-step procedure
Hard rule: no chantier is created without walking through these steps in order. For email-triggered chantiers, a façade runs steps 0a→4 under constraint (refusal if a scan fails or fewer than two agents). It stops at the atomic skeleton creation and performs neither step 2 (writing the mission_letter) nor step 5 (the post-skeleton UPDATE) — those two steps stay manual.
| Step | Action | Mechanism |
|---|---|---|
| 0a | Read the email request verbatim | mothership inbox façade |
| 0b | Extract + scan attachments before any reading | extraction then antivirus scan; verdict ≠ clean ⇒ block |
| 1 | Agent audit | query on the active agents table (codename, role, group) |
| 2 | Write the mission_letter | OBJECTIVE / CONTEXT / PERIMETER / DELIVERABLES / CONSTRAINTS / DOCTRINE, from the content read in 0a/0b |
| 3 | Explicit recruitment | ≥2 distinct agents if tenant scope |
| 4 | atomic create_with_skeleton | see previous section |
| 5 | Post-skeleton UPDATE | mission_letter + scope + preprod_test_plan + ship_command via a name-qualified UPDATE on the codename |
The email façade
Actual flow of an email-triggered chantier creation:
The "STEP 1…5" labels printed by the script are its own internal numbering, not to be confused with the 0a→5 steps of the procedure. The script's "STEP 5 — SKELETON PAYLOAD" corresponds to step 4 of the procedure (build + atomic INSERT). The façade never goes beyond.
--id <email_id>
STEP 1 fetch the email (via the internal API, never raw SQL)
STEP 2 extract + scan attachments
→ if an attachment is not "clean" → BLOCK, return 2
STEP 3 request summary (title / client / priority / scope)
STEP 4 list active agents + prompt for recruitment
→ if fewer than 2 distinct agents → BLOCK, return 3
STEP 5 first_taches payload (1 task/agent) + first_travail "-init" (discovery phase)
→ create_with_skeleton(**payload) = step 4 of the procedure
Exit codes: 2 = non-clean attachment, 3 = fewer than two agents, 4 = validation error. --dry-run prints the payload without INSERT.
Two distinct "≥2 agents" checks: the façade ALWAYS requires ≥2 agents — an unconditional check, independent of scope; only the block message cites the scope.create_with_skeletonapplies its own ≥2 validation only if the scope starts withtenant.
The façade creates a single-inittravail in thediscoveryphase. The transition to implementation travaux stays manual or goes through the auto-explode.
preprod_test_plan / ship_command fields (step 5)
Mandatory for any non-trivial chantier. Without them, the cascade to the chantier logs a non-blocking warning, but the "READY TO SHIP" banner cannot show. If preprod_test_plan starts with version: (YAML v2), the cascade triggers an automated QA in the background.
Multi-session locking
ChantierLockEntity lets you open N different chantiers in parallel (N terminals / workers) while refusing two sessions on the same chantier. It is not an ergonomic blocker but an anti-collision safety net.
Model
- Table
sy_chantier_lock, composite primary key(id_chantier, owner_kind)whereowner_kind∈ {user, worker} (database CHECK). user= human session;worker= sub-agent spawned by the task worker. Both can coexist on the same chantier; each kind only contests its own slot.owner_kindis detected via a worker-context environment variable, then by falling back to a walk of the parent processes.session_id= session identifier > fingerprint of the transcript path >pid-user@host.
TTL & acquisition
The TTL is 30 minutes. Acquisition does an INSERT ... ON CONFLICT DO UPDATE that only succeeds if the session matches OR if the lock has expired (last activity > 30 minutes). Otherwise it returns {acquired: False, owner: {...}}.
The lock is acquired automatically when fetching a chantier's context. On refusal, a ChantierLockedError is raised and intercepted by the matching skill. A kill-switch is available to disable it entirely. Legacy mode: if the table does not exist, acquisition always succeeds.
Verbs & lifecycle
| Operation | Method | CLI |
|---|---|---|
| Inventory of live locks | list_active() | --locks |
| Release your lock | release() (idempotent) | --release <codename> |
| Kick the previous owner (same kind) | force_claim() | --force-claim <codename> |
| Keep alive | heartbeat() | — |
Three release safety nets:
- Session-stop hook — fast release when a session ends.
- TTL cron — every 5 minutes, deletes locks whose last activity exceeds 30 minutes. Idempotent, always exit 0, silent skip if the database is unavailable. This is the real safeguard (crash, dropped session).
- Manual
force_claim.
Status cascades
The entities' update() methods are overridden to propagate terminal statuses from one level to the next. Cascade limited to 1 step per level (each step re-triggers its own cascade).
sy_chantier_tache.update(status='done'/'cancelled')
└─ _cascade_to_travail
if ALL tasks of the travail done/cancelled AND ≥1 done:
├─ QA team present on the chantier?
│ COUNT(*) sy_chantier_agent WHERE role IN ('validation','lead_validation')
│ YES → qa_run(id_travail):
│ verdict pass? → travail 'done'
│ otherwise → fix tasks created, travail stays 'dev' (NO bump)
│ NO → direct bump of the travail to 'done'
ps_ac_chantier_travail.update(status='done'/'cancelled')
└─ _cascade_resolves (on 'done' only) → travail-bis
└─ _cascade_to_chantier
if ALL travaux done/cancelled AND ≥1 done
→ chantier 'test' (= preprod, Founder review)
└─ bug-detector (on 'done', non-blocking)
The task→travail bump is not always direct. If the chantier carries a QA team — at least one agent with rolevalidation/lead_validation— the cascade delegates to a QA run instead of bumping: the travail only goesdoneif the verdict passes, otherwise fix tasks are created and the travail stays active. A QA exception is fail-safe (logged, no bump).
The final test → done transition stays manual (Founder action after ./ship), never cascaded.
Native SQL cascade (inbox)
Independently of the Python application cascades above, a native database trigger propagates the done transition on the database side, invisible from Python:
- A function + a trigger
AFTER UPDATE OF status ON ps_ac_chantier. - When the status moves to
done(and was not), all incoming emails attached to the chantier becomeresolved(excludingresolved/spam). - Consequence: marking a chantier
doneautomatically resolves the attached incoming emails, with no Python line at all. Worth keeping in mind for anyone debugging an email that wentresolved"on its own".
Discovery-only safeguard / auto-explode
The cascade to the chantier does NOT promote to test if all done travaux are in the discovery phase: otherwise a fresh chantier (created by the email façade with a single -init discovery travail) would move to preprod review as soon as the analysis ends, before implementation travaux even exist.
In that case, the discovery→impl auto-explode can fire, guarded by three successive kill-switches: an environment variable, a database flag auto_explode=FALSE, and a global enablement (YAML). The chantier then stays in planning for review.
Resolve-paused — the travail-bis pattern
When a travail is set to paused (blocked), a travail-bis is created carrying resolves_travail_id = <id of the paused>. When that bis goes done, _cascade_resolves() automatically cleans up the parent paused travail, before the cascade to the chantier (because it changes the chantier's terminal count).
Effects when the bis goes done:
- paused →
done(direct update to avoid cascade recursion); - the paused travail's
todotasks →cancelled(direct batch UPDATE); decisions_jsonsymmetric append on both the bis AND the paused (audit trail).
| Condition | Behaviour |
|---|---|
resolves_travail_id IS NULL | silent no-op (legacy path) |
| paused and bis on different chantiers | error (cross-chantier forbidden) |
paused already done | idempotent no-op |
| A↔B cycle detected | error |
| paused not found (orphan FK) | warn + skip |
| paused.status ∉ {paused, dev, planning} | warn + skip |
Associated database constraints: a partial index on resolves_travail_id WHERE NOT NULL, a no-self-resolve CHECK (resolves_travail_id <> id_travail), and an ON DELETE SET NULL FK. The /chantier --resolve --dry-run skill exposes this pattern.
Pitfalls & invariants
- SQL reads break on newlines:
mission_letter,notes,preprod_test_plan,current_focus,next_action,current_taskcontain line breaks. The affected listings go through a dedicated CSV reader. Reuse this pattern for any new read of these columns. - The entity's fields must follow the DDL: adding a column in the database without adding it to
fieldssilently makes the update inoperative on that column. - No status CHECK in the database on chantier/travail — the enum is only held by the code. A raw SQL INSERT can introduce an out-of-enum status.
- Chantier scope: the database wins over the docstring — always check the CHECK constraint rather than the Python docstring.
- Anti-leak: the database credentials are read from the root environment file. Never in clear text in the code or the database.