Aller au contenu

PD-293 — Plan d'implémentation

1. Découpage en composants

La story est décomposée en 7 composants (C1-C7), cohérents avec la stack existante (scripts/lib/*.sh, Bash 5.x, jq, YAML/JSON) du repo ProbatioVault-ia-governance.

ID Composant Responsabilité Fichier(s) Dépendances
C1 state-machine Machine d'états Ringbearer (§5.2) — transitions, gardes terminaux scripts/lib/lord-state-machine.sh Aucune
C2 validator Validation de tous les champs §5.1 (regex, enum, bornes) + log de rejet §6.1 scripts/lib/lord-validator.sh Aucune
C3 broker-adapter Couche d'abstraction claude-peers-mcp — listing, envoi, réception, reconnexion scripts/lib/lord-broker.sh claude-peers-mcp@^1.x (H-293-01)
C4 persistence Lecture/écriture .gov-lord-state.json (schema §5.4), FIFO escalades, cache idempotency, audit JSONL scripts/lib/lord-persistence.sh jq (H-TECH-05)
C5 orchestrator Logique métier des 7 commandes /gov-lord, boucle de supervision, reconciliation, détection crash scripts/gov-lord.sh C1, C2, C3, C4
C6 commands Skill Claude Code /gov-lord — parsing CLI, dispatch vers C5, whitelist de commandes .claude/commands/gov-lord.md C5
C7 tests Suites BATS couvrant TC-NOM-*, TC-ERR-*, TC-INV-*, TC-NEG-*, TC-NR-* tests/lord/**/*.bats bats-core (H-TECH-06), mocks C3

Aucune modification d'autres modules : PD-293 ne touche aucune route d'un autre module. Tous les composants sont nouveaux, créés dans le repo ia-governance.

2. Flux techniques

2.1 Flux start (nominal)

Sovereign -> C6: "/gov-lord start PD-42 backend --idempotency-key K1"
C6 -> C5.lord_start(PD-42, backend, K1)
  C5 -> C4.lord_count_active_stories()            # Garde 1 : quota
    => si >= 5 : REJET ERR-293-01, lord_log_rejection(), return
  C5 -> C4.lord_check_idempotency(K1, hash(PD-42+backend))  # Garde 2 : idempotence
    => si REPLAY : retour déterministe, return
    => si CONFLICT : REJET ERR-293-09, return
  C5 -> C2.lord_validate_story_id(PD-42)           # Garde 3 : format
  C5 -> C2.lord_validate_project_code(backend)
    => si REJECTED : REJET ERR-293-03, lord_log_rejection(), return
  C5 -> C4 : story déjà active pour PD-42 ?        # Garde 4 : doublon story
    => si oui : REJET ERR-293-02, return
  C5 -> C4.lord_create_story(PD-42, backend, STARTING, "pending-PD-42")
  C5 -> lance Ringbearer : `unset CLAUDECODE && /Users/loic/.local/bin/claude -p "/gov PD-42 backend"` (background)
  C5 -> C4.lord_record_idempotency(K1, start, PD-42, hash, result_hash, ttl=24h)
  # La supervision continue prend le relais (§2.4)

2.2 Flux respond (escalade FIFO)

Sovereign -> C6: "/gov-lord respond PD-42 'Approuvé pour production'"
C6 -> C5.lord_respond(PD-42, text, K2)
  C5 -> C4.lord_check_idempotency(K2, hash)         # Garde 1 : idempotence
  C5 -> C2.lord_validate_story_id(PD-42)             # Garde 2 : format
  C5 -> C4 : lire state(PD-42)
    => si state != ESCALADED : REJET ERR-293-04, return
  C5 -> C4.lord_dequeue_oldest_escalade(PD-42)       # FIFO (INV-293-13)
    => marque ANSWERED
  C5 -> C3.lord_broker_send_message(peer_id, PO_RESPONSE, {text})
  C5 -> C1.lord_transition(PD-42, ESCALADED, RUNNING)
  C5 -> C4.lord_update_story(PD-42, {state:RUNNING})
  # Si autres escalades en OPEN : story reste ESCALADED (multi-escalade)
  # Correction : si escalade_queue contient encore des OPEN pour cette story,
  # on NE fait PAS la transition ESCALADED->RUNNING ; on attend toutes les réponses.

Précision multi-escalade : La transition ESCALADED->RUNNING ne s'effectue que lorsque toutes les escalades OPEN de la story sont traitées. Tant qu'il reste au moins une escalade OPEN, la story reste ESCALADED.

2.3 Flux pause / resume

# PAUSE
C5 -> C1.lord_transition(PD-42, RUNNING, PAUSED)   # Garde machine d'états
C5 -> C3.lord_broker_send_message(peer_id, PAUSE, {reason})
C5 -> C4.lord_update_story(PD-42, {state:PAUSED})

# RESUME
C5 -> C1.lord_transition(PD-42, PAUSED, RUNNING)
C5 -> C3.lord_broker_send_message(peer_id, RESUME, {})
C5 -> C4.lord_update_story(PD-42, {state:RUNNING})

2.4 Boucle de supervision (flux continu)

lord_supervision_loop():
  while true:
    sleep $peer_poll_interval  # défaut 10s

    # 1. Vérifier status broker
    broker_status = C3.lord_broker_status()
    si DOWN : alerte Sovereign, tenter C3.lord_broker_reconnect()

    # 2. Lister peers observés
    observed_peers = C3.lord_broker_list_peers()

    # 3. Pour chaque story non-terminale en état local :
    pour chaque story dans C4.stories (state NOT IN [DONE, ABORTED, CRASHED, START_FAILED]) :

      si story.state == STARTING :
        si peer_id trouvé dans observed_peers ET first liveness reçue :
          C1.lord_transition(story_id, STARTING, RUNNING)
          C4.lord_update_story(story_id, {state:RUNNING, peer_id:real_peer_id})
        si elapsed > start_detection_timeout OU (peer trouvé mais pas de liveness avant first_liveness_timeout) :
          C1.lord_transition(story_id, STARTING, START_FAILED)
          C4.lord_update_story(story_id, {state:START_FAILED})
          cleanup session
          alerte Sovereign

      sinon (RUNNING, ESCALADED, PAUSED) :
        si peer absent dans observed_peers :
          incrémenter missed_polls
          si missed_polls >= crash_detection_cycles_max (=2) :
            C1.lord_transition(story_id, current_state, CRASHED)
            C4.lord_update_story(story_id, {state:CRASHED})
            alerte critique Sovereign
        sinon :
          missed_polls = 0
          traiter messages entrants (STATUS_UPDATE, ESCALADE, GATE_RESULT, WORKFLOW_DONE)

    # 4. Réconciliation (INV-293-11)
    pour chaque peer observé non reflété localement : signaler écart
    pour chaque story locale non-terminale sans peer observé : compter missed_polls

    # 5. Traitement messages entrants
    pour chaque message de type ESCALADE :
      C2.lord_validate (tous champs §5.1)
      C4.lord_enqueue_escalade(story_id, escalade_id, text, timestamp)
      C1.lord_transition(story_id, RUNNING, ESCALADED) # si pas déjà ESCALADED
      notifier Sovereign (SLA <= 5s, mesurable par delta timestamp)

    pour chaque message de type WORKFLOW_DONE :
      C1.lord_transition(story_id, RUNNING, DONE)
      C4.lord_update_story(story_id, {state:DONE})

    # 6. Purge idempotency expirée (opportuniste)
    C4.lord_purge_expired_idempotency()

2bis. Diagramme de dépendances agents (step 6b)

graph LR
    subgraph "Wave 1 — Modules fondamentaux (parallélisables)"
        C1[C1: state-machine<br/>agent-fsm]
        C2[C2: validator<br/>agent-validator]
    end

    subgraph "Wave 2 — Modules infrastructure"
        C3[C3: broker-adapter<br/>agent-broker]
        C4[C4: persistence<br/>agent-persistence]
    end

    subgraph "Wave 3 — Orchestrateur"
        C5[C5: orchestrator<br/>agent-orchestrator]
    end

    subgraph "Wave 4 — Interface + Tests"
        C6[C6: commands<br/>agent-skill]
        C7[C7: tests<br/>agent-tests]
    end

    C1 --> C5
    C2 --> C5
    C3 --> C5
    C4 --> C5
    C5 --> C6
    C1 --> C7
    C2 --> C7
    C3 --> C7
    C4 --> C7
    C5 --> C7

Waves d'exécution : - Wave 1 : C1 (state-machine) et C2 (validator) — zéro dépendance mutuelle, parallélisables. - Wave 2 : C3 (broker-adapter) et C4 (persistence) — dépendent de C2 pour la validation, parallélisables entre eux. - Wave 3 : C5 (orchestrator) — dépend de C1+C2+C3+C4, séquentiel. - Wave 4 : C6 (commands) dépend de C5 ; C7 (tests) dépend de tous. Parallélisables entre eux car C7 mock C5.

2ter. Diagramme de séquence enrichi (mécanismes techniques)

sequenceDiagram
    participant S as Sovereign (Humain)
    participant C6 as C6: /gov-lord<br/>(Skill CLI)
    participant C5 as C5: gov-lord.sh<br/>(Orchestrator)
    participant C2 as C2: lord-validator.sh
    participant C4 as C4: lord-persistence.sh
    participant C1 as C1: lord-state-machine.sh
    participant C3 as C3: lord-broker.sh<br/>(claude-peers-mcp)
    participant R as Ringbearer<br/>(claude -p "/gov PD-42")

    S->>C6: /gov-lord start PD-42 backend --idempotency-key K1
    C6->>C5: lord_start(PD-42, backend, K1)
    C5->>C4: lord_count_active_stories()
    C4-->>C5: 3 (< 5, quota OK)
    C5->>C4: lord_check_idempotency(K1, sha256)
    C4-->>C5: NEW
    C5->>C2: lord_validate_story_id(PD-42)
    C2-->>C5: OK
    C5->>C4: lord_create_story(PD-42, backend, STARTING, pending-PD-42)
    C5->>R: lance en background (claude -p)
    C5->>C4: lord_record_idempotency(K1, ...)

    Note over C5: Boucle supervision (toutes les 10s)
    C5->>C3: lord_broker_list_peers()
    C3-->>C5: [{peer_id: "rb-PD-42", ...}]
    C5->>C1: lord_transition(PD-42, STARTING, RUNNING)
    C1-->>C5: OK

    R->>C3: send_message(ESCALADE, {text: "Besoin clarification §3"})
    C5->>C3: lord_broker_get_messages(rb-PD-42)
    C3-->>C5: [{type: ESCALADE, ...}]
    C5->>C2: validate message
    C5->>C4: lord_enqueue_escalade(PD-42, esc-1, text, ts)
    C5->>C1: lord_transition(PD-42, RUNNING, ESCALADED)
    C5-->>S: Escalade PD-42: "Besoin clarification §3"

    S->>C6: /gov-lord respond PD-42 "Cf. §3.2 du cahier des charges"
    C6->>C5: lord_respond(PD-42, text, K2)
    C5->>C4: lord_dequeue_oldest_escalade(PD-42)
    C5->>C3: lord_broker_send_message(rb-PD-42, PO_RESPONSE, {text})
    C5->>C1: lord_transition(PD-42, ESCALADED, RUNNING)

Vérification couverture diagramme d'état §5bis : Chaque transition du stateDiagram de la spec est couverte par un mécanisme dans C1 (lord_transition) appelé depuis C5. Les transitions terminales (->CRASHED, ->DONE, ->ABORTED, ->START_FAILED) sont gérées dans la boucle de supervision (§2.4) et les commandes stop respectivement.

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-293-01 One Ring n'exécute aucune action hors interface/routage Whitelist de 7 commandes dans C6. Toute autre entrée retourne message de blocage. C5 n'importe jamais jira-api.sh, git, curl vers repos cibles. C6 + C5 TC-NOM-05, TC-ERR-05 : message de blocage systématique, audit de refus tracé Faible — vérifiable par grep sur les imports de C5
INV-293-02 Communication uniquement via broker/peers C3 est l'unique point d'accès réseau. C5 n'appelle jamais curl, git, ou tout outil réseau directement. Toute communication Ringbearer transite par lord_broker_send_message / lord_broker_get_messages. C3 + C5 TC-NOM-01 : traces broker exclusivement. TC-NR-02 : absence totale d'opération directe Moyen — dépend de la discipline de code review
INV-293-03 Isolation inter-story (contexte, credentials) Chaque Ringbearer est un process claude -p séparé. Aucun partage de variable shell, aucun fichier partagé entre stories. C4 indexe tout par story_id. C5 + C4 TC-NOM-09 : action sur story A sans impact B. TC-NR-03 : absence partage credentials Faible — garanti par l'isolation process OS
INV-293-04 Maximum 5 Ringbearers actifs lord_count_active_stories() dans C4 (count des stories non-terminales). Vérifié en garde 1 de start. C4 + C5 TC-NOM-02, TC-ERR-01 : 6e start rejeté, count reste 5 Faible
INV-293-05 Validation stricte §5.1, rejet + journalisation §6.1 C2 implémente une fonction par champ (lord_validate_*). Chaque rejet appelle lord_log_rejection() qui écrit en JSONL. C2 TC-NOM-10 : tous valides acceptés. TC-ERR-03 : invalides rejetés. TC-NEG-01..10 : 10 cas négatifs Moyen — regex \p{C} nécessite grep -P (Perl regex) pour peer_id/escalade_text
INV-293-06 Escalade suspend jusqu'à PO_RESPONSE En état ESCALADED, C1 n'autorise que ->RUNNING (PO_RESPONSE), ->ABORTED, ->CRASHED. C5 refuse tout resume/pause depuis ESCALADED (non dans transitions autorisées). C1 + C5 TC-NOM-03 : blocage effectif. TC-ERR-04 : respond sans escalade rejeté Faible
INV-293-07 /gov inchangé PD-293 ne modifie aucun fichier existant (scripts/gov-workflow.sh, scripts/gov-step.sh, etc.). Le Ringbearer exécute /gov tel quel. Architecture TC-NR-01 : baseline vs orchestré identiques Faible — vérifiable par git diff post-implémentation
INV-293-08 Crash détecté en ≤ 2 cycles (≤ 20s défaut) Compteur missed_polls par story dans C5. Incrémenté si peer absent, reset à 0 si présent. Transition ->CRASHED si missed_polls >= 2. C5 TC-NOM-06, TC-ERR-07 : transition CRASHED + alerte ≤ 20s Faible — déterministe, dépend uniquement du poll_interval
INV-293-09 Transitions strictement §5.2 C1 utilise un declare -A TRANSITIONS (tableau associatif Bash) avec chaque paire FROM->TO autorisée. Toute transition non présente dans le tableau est rejetée. C1 TC-INV-01 : toutes transitions non listées rejetées Faible — matrice exhaustive vérifiable par diff
INV-293-10 États terminaux immuables C1 : lord_is_terminal(state) retourne true pour DONE, ABORTED, CRASHED, START_FAILED. lord_transition() rejette immédiatement si from_state est terminal. C1 TC-INV-02 : aucune sortie acceptée depuis terminal Faible
INV-293-11 Réconciliation périodique À chaque cycle de supervision (§2.4, étape 4), C5 compare C4.stories (non-terminaux) avec C3.list_peers(). Écarts signalés en log + alerte si divergence. C5 + C3 + C4 TC-NOM-07 : désynchronisation volontaire corrigée Moyen — dépend de la fiabilité du listing broker
INV-293-12 Idempotence par clé dédiée C4 : lord_check_idempotency(key, payload_hash). 3 résultats : NEW (première fois), REPLAY (même payload → même résultat), CONFLICT (payload différent → rejet). TTL configurable (défaut 24h). C4 + C5 TC-NOM-08, TC-ERR-08, TC-ERR-11 Faible
INV-293-13 FIFO multi-escalade C4 : escalade_queue est un tableau JSON trié par created_at. lord_enqueue_escalade() append en fin. lord_dequeue_oldest_escalade(story_id) retire le premier élément OPEN de la story. C4 TC-NOM-13 : 3 escalades traitées dans l'ordre t1, t2, t3 Faible — tri par created_at garanti par jq
INV-293-14 Ordre des gardes : quota→idempotence→format (start) ; idempotence→format (autres) C5 applique les gardes dans l'ordre contractuel. Décision (résolution contradiction spec review) : INV-293-14 est interprété comme l'ordre spécifique par commande (§5.1 points 3-4 font foi). Pour start : quota→idempotence→format→doublon story. Pour stop/respond/pause/resume : idempotence→format. C5 TC-INV-03 : garde quota en premier, format après idempotence Faible

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-01 lord_status() dans C5 : lit C4.stories[] + C3.lord_broker_list_peers(), produit JSON consolidé avec story_id, state, last_seen_at, peer_id, escalades OPEN. C5, C4, C3 TC-NOM-01 : sortie contient liste consolidée. Log audit trace consultation broker. Faible
CA-02 lord_start() dans C5 : gardes → création story STARTING → lancement Ringbearer background → supervision détecte peer + first liveness → transition STARTING→RUNNING. Nouveau test : TC-NOM-14 (cf. §5 infra) couvre le chemin nominal complet. C5, C4, C1, C3 TC-NOM-14 : story visible en RUNNING avant start_detection_timeout. First liveness vérifiée. Moyen — dépend du timing de démarrage du Ringbearer
CA-03 Delta timestamps RFC3339 entre émission (Ringbearer) et réception (One Ring). Horloge unique locale MacBook (pas de synchronisation d'horloge nécessaire — décision H-TECH-08bis, cf. §8). C5, C4 TC-NOM-04 : P95 ≤ 5s sur 100 escalades, timestamps exportés Faible (horloge locale unique)
CA-04 lord_respond()dequeue_oldest_escalade()send_message(PO_RESPONSE)lord_transition(ESCALADED, RUNNING) C5, C4, C3, C1 TC-NOM-03 : transition ESCALADED→RUNNING observée après respond Faible
CA-05 Whitelist dans C6 : 7 commandes autorisées. Toute autre entrée → message de blocage. C5 n'a aucun import d'outil métier. C6, C5 TC-NOM-05, TC-ERR-05 : blocage explicite + audit de refus Faible
CA-06 /remote-control Claude Code → exécute /gov-lord → mêmes commandes C6. Aucune adaptation spécifique mobile nécessaire. C6 TC-MAN-01 : test manuel iPhone/Safari. Résultats identiques desktop. Élevé — dépend de /remote-control (H-293-02)
CA-07 Compteur missed_polls dans C5 supervision loop. ≥ 2 → transition ->CRASHED + alerte. C5, C1 TC-NOM-06, TC-ERR-07 : CRASHED en ≤ 2 cycles (≤ 20s) Faible
CA-08 /gov non modifié. Le Ringbearer est un process autonome claude -p "/gov PD-XX projet". Architecture TC-NR-01 : baseline identique. Aucun fichier /gov modifié. Faible
CA-09 lord_count_active_stories() ≥ 5 → rejet start. C4, C5 TC-NOM-02, TC-ERR-01 : 6e start rejeté, count = 5 Faible
CA-10 C2 valide chaque champ avec regex/enum §5.1. Rejet → log JSONL §6.1. C2 TC-NOM-10, TC-ERR-03, TC-NEG-01..10 Moyen — nécessite grep -P pour Unicode
CA-11 C4.escalade_queue trié par created_at. FIFO strict. C4 TC-NOM-13 : 3 escalades en ordre t1 < t2 < t3 Faible
CA-12 C3 : reconnexion exponentielle bornée (1s, 2s, 4s, 8s, 10s max). Mode DEGRADED explicite. Reprise auto → reconciliation. C3, C5 TC-ERR-10 : mode dégradé puis reprise automatique Moyen — dépend du broker

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-NOM-01 INV-293-02, CA-01 C5.lord_status() → C3.list_peers() + C4.stories Sortie JSON consolidée, log audit broker, absence opération directe Integration
TC-NOM-02 INV-293-04, CA-09 C4.lord_count_active_stories() en garde start Rejet explicite, count reste 5, log rejet Unit (C4) + Integration
TC-NOM-03 INV-293-06, CA-04 C5.lord_respond() → C4.dequeue → C3.send → C1.transition Transition ESCALADED→RUNNING, STATUS_UPDATE post-respond Integration
TC-NOM-04 CA-03, INV-293-05 Timestamps RFC3339 delta émission/réception (horloge locale unique) Export CSV 100 mesures, P95 ≤ 5s Perf (mock broker avec délai calibré)
TC-NOM-05 INV-293-01, CA-05 Whitelist C6, message de blocage Sortie "commande non reconnue", log audit refus, absence side-effect Unit (C6)
TC-NOM-06 INV-293-08, CA-07 C5 supervision loop, missed_polls ≥ 2 Transition →CRASHED, timestamp ≤ 2×poll_interval, alerte Integration (mock broker sans peer)
TC-NOM-07 INV-293-11, CA-01 C5 réconciliation : compare C4.stories vs C3.list_peers() Écarts détectés, vue alignée, log reconciliation Integration
TC-NOM-08 INV-293-12, CA-02 C4.lord_check_idempotency() → REPLAY Second appel identique, résultat déterministe, aucun Ringbearer supplémentaire Unit (C4) + Integration
TC-NOM-09 INV-293-03, CA-08 Isolation process : 2 stories actives, action sur A State B inchangé, aucun partage message/credential Integration
TC-NOM-10 INV-293-05, CA-10 C2.lord_validate_*() sur lot valide complet Tous acceptés, log audit association type+story+timestamp Unit (C2)
TC-NOM-12 CA-02, INV-293-09 C5.lord_pause() → C1.transition(RUNNING, PAUSED) → C5.lord_resume() → C1.transition(PAUSED, RUNNING) Transitions RUNNING→PAUSED→RUNNING observées, logs idempotency Integration
TC-NOM-13 INV-293-13, CA-11 C4.lord_enqueue_escalade() × 3 + C4.lord_dequeue_oldest_escalade() × 3 Ordre FIFO t1→t2→t3, escalade_queue traçable Unit (C4) + Integration
TC-NOM-14 (ajouté) CA-02, INV-293-08 C5.lord_start() complet : gardes → STARTING → peer détecté → first liveness → RUNNING Story visible en RUNNING, peer_id réel, elapsed < start_detection_timeout Integration
TC-ERR-01 ERR-293-01 C4.lord_count_active_stories() → 5 → rejet Message "quota atteint", aucun peer créé Unit + Integration
TC-ERR-02 ERR-293-02 C4 : story déjà active → rejet Message "story déjà active", pas de second peer Integration
TC-ERR-03 ERR-293-03, §6.1 C2.lord_validate_story_id() → REJECTED + lord_log_rejection() Log JSONL avec timestamp, peer_id="sovereign", message_type="START", reason Unit (C2)
TC-ERR-04 ERR-293-04 C4 : story state != ESCALADED → rejet respond Message "pas d'escalade active" Integration
TC-ERR-05 ERR-293-05 Whitelist C6 → blocage Absence totale d'effet de bord Unit (C6)
TC-ERR-06 ERR-293-06 C3.lord_broker_status() → DOWN → mode DEGRADED + alerte Mode dégradé explicite, alerte Sovereign, aucune décision silencieuse Integration (broker mock DOWN)
TC-ERR-07 ERR-293-07 C5 supervision, missed_polls ≥ 2 Transition →CRASHED, alerte critique, état terminal Integration
TC-ERR-08 ERR-293-08 C4.lord_check_idempotency(K2, hash) → REPLAY Retour déterministe identique, aucun doublon Unit (C4)
TC-ERR-09 ERR-293-10 C5 supervision : peer détecté mais pas de liveness avant first_liveness_timeout Transition STARTING→START_FAILED, cleanup, alerte Integration
TC-ERR-10 CA-12 C3 reconnexion exponentielle + C5 reconciliation à la reprise Mode dégradé, timing retries (1s,2s,4s,8s,10s), reprise auto Integration
TC-ERR-11 ERR-293-09 C4.lord_check_idempotency(K3, hash_P2) → CONFLICT Rejet conflit, aucun effet Unit (C4)
TC-INV-01 INV-293-09 C1 : matrice TRANSITIONS, toute paire non listée → REJECTED Rejet explicite pour chaque transition non autorisée, log Unit (C1) — test exhaustif matrice 8×8
TC-INV-02 INV-293-10 C1 : lord_is_terminal() + lord_transition() → rejet si terminal Aucune mutation depuis DONE/ABORTED/CRASHED/START_FAILED Unit (C1)
TC-INV-03 INV-293-14 C5.lord_start() avec quota plein + payload invalide Premier rejet = "quota atteint" (pas "format invalide") Integration
TC-NEG-01..10 INV-293-05 C2.lord_validate_*() sur 10 entrées invalides (cf. §7 tests) Rejet spécifique par champ, log JSONL, zéro side-effect Unit (C2)
TC-NR-01 INV-293-07 Diff comportement /gov PD-XX standalone vs orchestré Mêmes étapes/gates/verdicts, aucun fichier gov modifié E2E (1 story complète)
TC-NR-02 INV-293-01 grep -r sur C5 pour curl/git/cd-vers-repo Zéro match d'opération hors scope Static analysis
TC-NR-03 INV-293-03 2 stories actives : envoi message ciblé A, vérification B inchangé State B identique avant/après Integration
TC-NR-04 INV-293-10 Réconciliation + polling sur stories terminales États terminaux jamais modifiés Integration
TC-MAN-01 CA-06 iPhone/Safari + /remote-control Résultats fonctionnels identiques desktop Manuel

Note sur TC-NOM-14 : Ce test est ajouté pour couvrir CA-02 (résolution de l'incohérence spec-review §2 : CA-02 était mappé à TC-NOM-08 qui teste l'idempotence, pas le démarrage nominal).

6. Gestion des erreurs

ERR ID Cas Garde(s) Composant Message/Format Observable
ERR-293-01 start quota ≥ 5 C4.count_active ≥ 5 C5 "Rejet: quota atteint (5/5 sessions actives)" + log JSONL : {event: "COMMAND_REJECTED", reason: "quota_exceeded", ...} Aucun peer créé, log tracé
ERR-293-02 start story déjà active C4.stories contient story_id non-terminal C5 "Rejet: story {id} déjà active (état: {state})" + log Pas de second peer
ERR-293-03 Message sans story_id valide C2.lord_validate_*() → REJECTED C2 Log JSONL §6.1 : {timestamp, peer_id, message_type, reason, event: "MESSAGE_REJECTED"} Aucun changement d'état
ERR-293-04 respond sans escalade active C4.state != ESCALADED ou queue vide C5 "Rejet: pas d'escalade active pour {story_id}" État inchangé
ERR-293-05 Commande hors scope Whitelist C6 C6 "Commande non reconnue. Commandes disponibles: start, status, escalade, respond, pause, resume, stop" Zéro action métier
ERR-293-06 Broker indisponible C3.lord_broker_status() = DOWN C3+C5 Mode DEGRADED + alerte "⚠ Broker indisponible — mode dégradé" + retries exponentiel Aucune décision silencieuse
ERR-293-07 Peer disparu > 2 cycles C5.missed_polls ≥ 2 C5 "🚨 CRASHED: {story_id} — peer disparu depuis {elapsed}s" + transition →CRASHED État terminal, alerte critique
ERR-293-08 Idempotency replay (même payload) C4.lord_check_idempotency → REPLAY C4 Retour résultat précédent, {event: "IDEMPOTENCY_REPLAY", key: K} Aucun double effet
ERR-293-09 Idempotency conflit (payload ≠) C4.lord_check_idempotency → CONFLICT C4 "Rejet: conflit idempotence (clé {K}, payload différent)" + log Aucun effet
ERR-293-10 First liveness timeout C5.elapsed > first_liveness_timeout C5 Transition →START_FAILED, cleanup, alerte Session nettoyée

Format log de rejet §6.1 — Décision pour rejets de commandes Sovereign (résolution point spec review §6.1) : - peer_id : "sovereign" (identifiant convention One Ring pour les commandes entrantes) - message_type : nom de la commande en MAJUSCULES ("START", "RESPOND", "PAUSE", etc.)

Exemple pour ERR-293-01 :

{"timestamp":"2026-03-30T10:15:30Z","peer_id":"sovereign","message_type":"START","reason":"quota_exceeded (5/5)","story_id":"PD-299","event":"COMMAND_REJECTED"}

7. Impacts sécurité

Risque Mitigation Composant Observable
Exécution hors scope par One Ring (lecture repo, build, Jira, GitLab, Vault) INV-293-01 : whitelist stricte 7 commandes dans C6. C5 n'importe aucun module métier. Vérifiable par analyse statique (TC-NR-02). C6, C5 grep -r sur imports/sources de C5
Contamination inter-story (credentials, contexte) INV-293-03 : isolation process OS. Chaque Ringbearer = process claude -p séparé avec son propre environnement. Aucun fichier partagé. C5 TC-NOM-09, TC-NR-03
Données sensibles dans escalade_text/export (RGPD) Décision (résolution point spec review §9) : C4 stocke escalade_text uniquement dans .gov-lord-state.json (fichier local non commité, ajouté au .gitignore). Les logs d'audit ne contiennent que escalade_id + story_id, jamais le texte brut. L'export probatoire anonymise les escalade_text (SHA-256 tronqué). C4 Vérifiable par grep sur .gov-lord-audit.jsonl
Injection dans story_id/project_code C2 : validation regex stricte ^PD-[0-9]{1,4}$ et enum project_code. Aucun passage de valeur non validée à claude -p ou au broker. C2 TC-NEG-01..10
Crash persistance (corruption state) C4 : écriture atomique via fichier temporaire + mv. Conforme JSON Schema §5.4 validé par jq à chaque écriture. C4 Test corruption : kill pendant write, vérifier intégrité
Exposition fichier state hors périmètre Décision : .gov-lord-state.json est stocké à la racine du repo ia-governance (pas dans un epic). Ajouté au .gitignore. Justification : c'est un état runtime global, pas un artefact de story. C4 Vérification .gitignore

8. Hypothèses techniques

ID Hypothèse Impact si faux Mitigation
H-TECH-01 claude-peers-mcp@^1.x expose list_peers(), send_message(), get_messages() en MCP tools Blocage complet de C3 C3 est une couche d'abstraction — si l'API diffère, seul C3 est adapté. Mock disponible pour tests.
H-TECH-02 claude-peers-mcp fonctionne en local MacBook sans serveur distant Blocage C3 Vérifier à l'installation. Fallback : fichiers partagés (hors scope v1).
H-TECH-03 Bash 5.x disponible (declare -A pour tableaux associatifs) C1 non fonctionnel macOS avec brew install bash. Vérifiable : bash --version.
H-TECH-04 grep -P (Perl regex) disponible pour validation Unicode \p{C} C2 : validation peer_id/escalade_text dégradée brew install grep (GNU grep). Fallback : validation plus permissive avec exclusion manuelle des contrôles communs.
H-TECH-05 jq installé (manipulation JSON) C4 non fonctionnel brew install jq. Vérification au démarrage de C5.
H-TECH-06 bats-core installé (framework de test Bash) C7 non exécutable brew install bats-core. CI : step d'installation dans le pipeline.
H-TECH-07 /remote-control fonctionne sur iPhone/Safari pour Claude Code CA-06 dégradé (contrôle desktop uniquement) Test manuel. Si non disponible, CA-06 est documenté comme limitation v1.
H-TECH-08 Timestamp strictement UTC Z — les offsets non-zero (+02:00, -05:00) sont rejetés par C2 Aucun — c'est une décision de design, pas une hypothèse externe Résolution de l'ambiguïté spec review §5.1. Regex : ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$
H-TECH-08bis Horloge unique locale — émetteur (Ringbearer) et récepteur (One Ring) sont sur le même MacBook Le SLA P95 de CA-03 est fiable car aucun décalage d'horloge possible Résolution de l'hypothèse dangereuse spec review. Si multi-machine en v2, NTP obligatoire.
H-TECH-09 peer_id en état STARTING — valeur sentinelle "pending-{story_id}" Conforme minLength:1 du schema §5.4 Résolution de l'ambiguïté spec review §5.4 vs §5.2. Remplacé par le vrai peer_id dès RUNNING.

9. Points de vigilance (risques, dette, pièges)

9.1 Risques opérationnels

# Risque Probabilité Impact Mitigation
R1 claude-peers-mcp API non documentée ou instable Élevée (v1.x early) Bloquant C3 abstraction + mocks complets pour tests
R2 Ringbearer ne produit pas de STATUS_UPDATE (protocole non natif dans /gov) Élevée STARTING→START_FAILED systématique H-293-04 de la spec. Si confirmé, story séparée de compatibilité /gov.
R3 Timing start_detection_timeout trop court (30s) si MacBook chargé Moyenne START_FAILED faux positif Paramètre configurable (min 5, max 120). Logging du elapsed pour calibrage.
R4 Multi-escalade : Ringbearer n'attend pas le PO_RESPONSE pour continuer Moyenne Incohérence état Le message PAUSE explicite depuis One Ring force l'arrêt. Documenter dans le REX.

9.2 Dette technique acceptée

# Dette Justification Story de résolution
D1 Pas d'état ESCALADE_EXPIRED — l'escalade reste ouverte indéfiniment avec alertes répétées Hors périmètre v1 (spec §4.1) Évolution future
D2 Test TC-NOM-04 (P95 SLA) dépend d'un mock broker avec délai calibré, pas du vrai broker Le vrai broker introduirait du non-déterminisme Validation E2E manuelle
D3 grep -P pour Unicode — non disponible nativement sur macOS (nécessite GNU grep) Seul moyen de valider \p{C} en Bash Documenter dans les prérequis d'installation

9.3 Pièges d'implémentation

  • Écriture atomique : mv n'est atomique que sur le même filesystem. .gov-lord-state.json.tmp doit être dans le même répertoire que .gov-lord-state.json.
  • Boucle de supervision : sleep en Bash bloque les signaux. Utiliser sleep $interval & + wait $! pour que le trap SIGINT/SIGTERM soit réactif.
  • Regex Bash : [[ $val =~ ^PD-[0-9]{1,4}$ ]] — attention, les accolades {1,4} sont interprétées par le shell en Bash 3. Tester avec Bash 5 impérativement.
  • jq concurrent : Si la supervision loop écrit le state pendant qu'une commande le lit, risque de lecture partielle. Utiliser un lock file (flock) autour des écritures C4.
  • Idempotency payload_hash : Utiliser sha256sum sur les arguments normalisés (story_id + project_code triés) pour un hash déterministe.

9.4 Décisions sur les points de la specification-review

Point review Gravité Décision
Contradiction INV-293-14 vs §5.1 (ordre gardes) Bloquant §5.1 points 3-4 font foi. start : quota→idempotence→format. Autres : idempotence→format. INV-293-14 est lu comme "ordre contractuel par commande".
Contradiction diagramme séquence vs INV-293-02 (flèches directes O→R) Bloquant INV-293-02 fait foi. Les flèches directes du diagramme §5bis sont des raccourcis visuels. Toute communication passe par C3 (broker-adapter). Le diagramme enrichi §2ter corrige cette représentation.
Incohérence CA-02 mappé à TC-NOM-08 Majeur TC-NOM-14 ajouté pour couvrir le chemin nominal de CA-02 (start complet jusqu'à RUNNING). TC-NOM-08 couvre uniquement l'idempotence (INV-293-12).
Incohérence INV-293-10 mappé à CA-01 Majeur INV-293-10 (terminaux immuables) est couvert par TC-INV-02, pas par CA-01. La matrice de couverture dans le plan utilise le mapping corrigé.
peer_id/message_type non définis pour rejets Sovereign (§6.1) Majeur Convention : peer_id="sovereign", message_type=<COMMAND_NAME> (START, RESPOND, etc.). Documenté dans C2 (validator).
Ambiguïté timestamp "offset UTC" Mineur Strictement Z uniquement. Tout offset non-zero est rejeté (H-TECH-08).
Hypothèse horloge SLA CA-03 Majeur Horloge locale unique MacBook (H-TECH-08bis). Pas de décalage possible en v1 (single machine).
Tests TC-NR-*/TC-NEG-* non détaillés Given/When/Then Majeur TC-NEG-01..10 : chacun est une variante de C2.lord_validate_*() avec une entrée invalide spécifique. Le niveau de détail est dans la matrice §5. TC-NR-* : détaillés dans le mapping §5 avec observables.
peer_id obligatoire en STARTING (schema §5.4) Majeur Valeur sentinelle "pending-{story_id}" (H-TECH-09). Conforme minLength:1.
Risque RGPD escalade_text dans exports Majeur escalade_text stocké uniquement dans .gov-lord-state.json (local, .gitignore). Logs d'audit : escalade_id seulement. Export probatoire : texte anonymisé (SHA-256 tronqué).

10. Hors périmètre

Exclusion Justification
Modification du workflow /gov existant INV-293-07, CA-08. Le Ringbearer exécute /gov tel quel.
Communication directe Ringbearer↔Ringbearer Spec §2 exclu. Toute communication transite par One Ring via broker.
Application iOS native Spec §2 exclu. Le contrôle mobile passe par /remote-control (web).
État ESCALADE_EXPIRED Spec §4.1. Hors périmètre v1.
Mutualisation/partage de credentials entre sessions Spec §2 exclu. Chaque Ringbearer a son propre contexte.
Tests E2E multi-machines MacBook local uniquement en v1. Les tests d'intégration utilisent des mocks broker.

Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire C1 (matrice transitions, terminaux), C2 (10 validateurs + log rejet), C4 (CRUD state, FIFO escalade, idempotency)
Intégration C5 avec mocks C3 : supervision loop, gardes commandes, réconciliation. Interactions C5↔C1↔C4
E2E TC-NR-01 : 1 story complète /gov standalone vs orchestrée Multi-stories simultanées E2E (trop long, couvert par intégration)
Perf TC-NOM-04 : P95 SLA escalade (mock broker) Perf sous charge réelle (5 stories simultanées) — mesure manuelle
Manuel TC-MAN-01 : iPhone/Safari /remote-control

Couverture minimale : 80% sur le périmètre in scope (C1-C6). C7 est le module de tests lui-même.


Maintenant le fichier code-contracts.yaml corrigé (intégrant les retours de la review) :


# PD-293-code-contracts.yaml
# Frontières de code entre agents — PD-293 One Ring Orchestration
# Cohérent avec le plan d'implémentation v2

code_contracts:
  - module: state-machine
    owner_agent: agent-fsm
    interfaces:
      - "lord_transition(story_id, from_state, to_state) -> OK | REJECTED"
      - "lord_is_terminal(state) -> true | false"
      - "lord_allowed_transitions(from_state) -> list[to_state]"
    invariants:
      - "INV-293-09 : Toutes transitions non listees en §5.2 sont rejetees."
      - "INV-293-10 : Etats terminaux (DONE, ABORTED, CRASHED, START_FAILED) n'acceptent aucune transition sortante."
      - "INV-293-06 : Depuis ESCALADED, seules les transitions vers RUNNING (PO_RESPONSE), ABORTED et CRASHED sont autorisees."
    forbidden:
      - "Ajouter un etat non contractuel (ex: ESCALADE_PENDING, ESCALADE_EXPIRED)."
      - "Accepter une transition non listee dans la matrice §5.2."
      - "Modifier un etat terminal apres qu'il a ete atteint."
    architectural_decisions:
      - decision: "Matrice de transitions en tableau associatif Bash (declare -A)"
        rationale: "Bash 5+ supporte les tableaux associatifs. Lookup O(1) par cle 'FROM->TO'. Plus maintenable qu'une cascade de if/case."
        alternatives_considered:
          - "Case/switch imbriques (lisible mais verbose, risque d'oubli d'un cas)"
          - "Fichier YAML externe parse par yq (overhead inutile pour une matrice statique)"
        trade_offs:
          - "Avantage : Lookup O(1), matrice exhaustive verifiable par diff avec §5.2"
          - "Inconvenient : Requiert Bash 5+ (brew install bash)"
    files:
      - "scripts/lib/lord-state-machine.sh"

  - module: validator
    owner_agent: agent-validator
    interfaces:
      - "lord_validate_story_id(value) -> OK | REJECTED"
      - "lord_validate_project_code(value) -> OK | REJECTED"
      - "lord_validate_message_type(value) -> OK | REJECTED"
      - "lord_validate_peer_id(value) -> OK | REJECTED"
      - "lord_validate_timestamp(value) -> OK | REJECTED"
      - "lord_validate_escalade_text(value) -> OK | REJECTED"
      - "lord_validate_summary_text(value) -> OK | REJECTED"
      - "lord_validate_idempotency_key(value) -> OK | REJECTED"
      - "lord_log_rejection(timestamp, peer_id, message_type, reason, story_id, event) -> void"
    invariants:
      - "INV-293-05 : Chaque champ est valide selon §5.1 (regex, enum, bornes). Rejet deterministe et journalise en JSONL §6.1."
      - "Timestamp RFC3339 strictement UTC : seul le suffixe Z est accepte (decision H-TECH-08)."
      - "Les rejets de commandes Sovereign utilisent peer_id='sovereign' et message_type=nom de la commande en majuscules (decision spec-review §6.1)."
    forbidden:
      - "Accepter un champ invalide sans log de rejet §6.1."
      - "Accepter un timestamp avec offset non-Z (ex: +02:00)."
      - "Valider partiellement un message (tous les champs obligatoires doivent etre verifies)."
    architectural_decisions:
      - decision: "Validation par regex Bash natif ([[ =~ ]]) sauf peer_id/escalade_text (grep -P)"
        rationale: "Les regex §5.1 simples (story_id, project_code, etc.) sont faisables en Bash natif. peer_id et escalade_text necessitent Unicode property class \p{C} via grep -P (GNU grep)."
        alternatives_considered:
          - "Python script dedie (overhead de process pour chaque validation)"
          - "jq schema validation (ne supporte pas les regex)"
        trade_offs:
          - "Avantage : Zero dependance pour 6/8 validateurs, performance native"
          - "Inconvenient : Necessite GNU grep installe pour 2 validateurs (H-TECH-04)"
    files:
      - "scripts/lib/lord-validator.sh"

  - module: broker-adapter
    owner_agent: agent-broker
    interfaces:
      - "lord_broker_list_peers() -> json_array | ERROR"
      - "lord_broker_send_message(peer_id, message_type, payload) -> OK | ERROR"
      - "lord_broker_get_messages(peer_id) -> json_array | ERROR"
      - "lord_broker_status() -> UP | DEGRADED | DOWN"
      - "lord_broker_reconnect() -> OK | STILL_DEGRADED"
    invariants:
      - "INV-293-02 : Toute communication One Ring <-> Ringbearer passe par le broker. Aucun acces direct aux repos cibles."
      - "Reconnexion exponentielle bornee : 1s, 2s, 4s, 8s, 10s (plafond). Mode DEGRADED explicite tant que indisponible."
      - "Les messages recus sont valides par C2 (lord-validator) avant traitement dans C5."
    forbidden:
      - "Acceder directement a un repo cible (cd, git, curl vers GitLab/Jira/Vault)."
      - "Communiquer avec un Ringbearer sans passer par le broker."
      - "Ignorer silencieusement une erreur broker (toute erreur doit basculer en DEGRADED)."
    architectural_decisions:
      - decision: "Couche d'abstraction Bash wrappant claude-peers-mcp MCP tools"
        rationale: "Isole le risque H-TECH-01/02. Si l'API differe, seul ce module est adapte. Mockable pour tests."
        alternatives_considered:
          - "Appels MCP directs dans l'orchestrateur (couplage fort, impossible a mocker)"
          - "IPC par fichiers partages (fallback si broker indisponible  hors scope v1)"
        trade_offs:
          - "Avantage : Abstraction propre, testable, isolee"
          - "Inconvenient : Overhead d'une couche supplementaire"
    files:
      - "scripts/lib/lord-broker.sh"

  - module: persistence
    owner_agent: agent-persistence
    interfaces:
      - "lord_state_read() -> json_object"
      - "lord_state_write(json_object) -> OK | ERROR"
      - "lord_count_active_stories() -> integer"
      - "lord_create_story(story_id, project_code, state, peer_id) -> OK | ERROR"
      - "lord_update_story(story_id, updates) -> OK | ERROR"
      - "lord_get_story(story_id) -> json_object | NONE"
      - "lord_enqueue_escalade(story_id, escalade_id, text, timestamp) -> OK | ERROR"
      - "lord_dequeue_oldest_escalade(story_id) -> escalade_json | NONE"
      - "lord_count_open_escalades(story_id) -> integer"
      - "lord_check_idempotency(key, payload_hash) -> NEW | REPLAY(result_hash) | CONFLICT"
      - "lord_record_idempotency(key, command, story_id, payload_hash, result_hash, ttl) -> OK"
      - "lord_purge_expired_idempotency() -> count_purged"
    invariants:
      - "INV-293-13 : escalade_queue est un tableau FIFO ordonne par created_at. dequeue retire l'element OPEN le plus ancien pour la story cible."
      - "INV-293-12 : Idempotency cache verifie key + payload_hash. Meme key + meme payload = REPLAY. Meme key + payload different = CONFLICT."
      - "Le fichier .gov-lord-state.json est conforme au schema JSON §5.4 a chaque ecriture."
      - "Purge des entrees idempotency expirees a chaque demarrage et a chaque lecture du cache."
      - "Ecriture atomique : fichier temporaire + mv (meme filesystem)."
      - "Acces concurrent protege par flock."
    forbidden:
      - "Ecrire un fichier .gov-lord-state.json non conforme au schema §5.4."
      - "Stocker escalade_text dans les logs d'audit (uniquement escalade_id + story_id)."
      - "Acceder a des donnees d'une story depuis une autre story sans indexation explicite par story_id."
      - "Ecrire directement le fichier state sans passer par fichier temporaire + mv."
    architectural_decisions:
      - decision: "JSON manipule par jq + ecriture atomique tmp+mv + flock"
        rationale: "jq est le standard JSON en shell. L'ecriture atomique empeche la corruption. flock protege les acces concurrents entre supervision loop et commandes CLI."
        alternatives_considered:
          - "Python json module (plus puissant mais overhead de process)"
          - "SQLite (overkill pour 5 stories max)"
        trade_offs:
          - "Avantage : Expressif, robuste, large adoption, pas de corruption"
          - "Inconvenient : Dependance jq (H-TECH-05)"
      - decision: "Stockage a la racine du repo ia-governance (pas dans un epic)"
        rationale: "Etat runtime global, pas un artefact de story. Ajoute au .gitignore."
    files:
      - "scripts/lib/lord-persistence.sh"
      - ".gov-lord-state.json"
      - ".gov-lord-audit.jsonl"

  - module: orchestrator
    owner_agent: agent-orchestrator
    interfaces:
      - "lord_start(story_id, project_code, idempotency_key) -> OK | ERROR"
      - "lord_status(story_id?) -> json_output"
      - "lord_escalade(story_id?) -> json_output"
      - "lord_respond(story_id, response_text, idempotency_key) -> OK | ERROR"
      - "lord_pause(story_id, reason?, idempotency_key) -> OK | ERROR"
      - "lord_resume(story_id, idempotency_key) -> OK | ERROR"
      - "lord_stop(story_id, reason?, idempotency_key) -> OK | ERROR"
      - "lord_supervision_loop() -> never_returns"
    invariants:
      - "INV-293-14 : Ordre des gardes pour start : quota -> idempotence -> format -> story non-dupliquee."
      - "INV-293-14 : Ordre des gardes pour stop/respond/pause/resume : idempotence -> format."
      - "INV-293-08 : Crash detecte en <= 2 cycles de polling (missed_polls >= crash_detection_cycles_max)."
      - "INV-293-11 : Reconciliation periodique entre etat local (C4) et peers observes (C3) a chaque cycle."
      - "INV-293-01 : Aucune action hors interface/routage. Aucun acces direct aux repos cibles."
      - "Multi-escalade : transition ESCALADED->RUNNING uniquement quand TOUTES les escalades OPEN de la story sont traitees."
    forbidden:
      - "Executer une commande systeme hors perimetre (cd vers repo, git, curl Jira/GitLab/Vault)."
      - "Contourner l'ordre des gardes contractuel."
      - "Modifier l'etat d'une story sans passer par C1 (state-machine)."
      - "Communiquer avec un Ringbearer sans passer par C3 (broker-adapter)."
      - "Lire ou ecrire le state sans passer par C4 (persistence)."
      - "Importer jira-api.sh, state.sh, ou tout module du workflow /gov existant."
    architectural_decisions:
      - decision: "Boucle de supervision en background avec trap + sleep interruptible"
        rationale: "Le One Ring doit tourner en continu. Trap SIGINT/SIGTERM pour persistance propre. Sleep via 'sleep N & wait $!' pour reactivite signal."
        alternatives_considered:
          - "Cron job (trop lent pour SLA 5s)"
          - "Daemon launchd (overhead infra pour outil dev local)"
        trade_offs:
          - "Avantage : Simple, pas d'infra additionnelle"
          - "Inconvenient : Depend du terminal ouvert (acceptable MacBook local)"
    files:
      - "scripts/gov-lord.sh"

  - module: commands
    owner_agent: agent-skill
    interfaces:
      - "Skill Claude Code /gov-lord : parse arguments, delegue a C5"
    invariants:
      - "INV-293-01 : Whitelist de 7 commandes (start, status, escalade, respond, pause, resume, stop). Toute autre commande retourne un message de blocage explicite."
      - "CA-05 : Message de blocage coherent et systematique pour les commandes hors scope."
    forbidden:
      - "Ajouter une commande qui execute une action metier (lecture repo, build, Jira, GitLab)."
      - "Deleguer une commande non reconnue a C5 sans blocage."
    files:
      - ".claude/commands/gov-lord.md"

  - module: tests
    owner_agent: agent-tests
    interfaces:
      - "Suites BATS : tests/lord/state-machine.bats, tests/lord/validator.bats, tests/lord/persistence.bats, tests/lord/orchestrator.bats, tests/lord/integration.bats, tests/lord/negative.bats, tests/lord/non-regression.bats"
    invariants:
      - "Chaque test produit un resultat deterministe (pas de dependance a l'horloge reelle sauf TC-NOM-04)."
      - "Les tests utilisent un mock broker (pas de dependance a claude-peers-mcp en CI)."
      - "TC-INV-01 teste exhaustivement la matrice 8x8 (64 paires) de transitions."
    forbidden:
      - "Tester avec un broker reel sans fallback mock."
      - "Ignorer un test d'invariant (TC-INV-*) ou de non-regression (TC-NR-*)."
      - "Dependre de fichiers ou d'etat existant dans .gov-lord-state.json entre tests (setup/teardown obligatoire)."
    architectural_decisions:
      - decision: "BATS (Bash Automated Testing System) avec mock broker en fichier"
        rationale: "BATS est le standard de facto pour Bash. Le mock broker est un ensemble de fonctions shell qui simulent list_peers/send_message/get_messages via fichiers JSON temporaires."
        alternatives_considered:
          - "shunit2 (moins maintenu)"
          - "pytest + subprocess (overhead Python pour Bash)"
        trade_offs:
          - "Avantage : Natif Bash, large communaute, TAP output, facile CI"
          - "Inconvenient : Dependance externe bats-core (H-TECH-06)"
    files:
      - "tests/lord/**/*.bats"
      - "tests/lord/mocks/**"
      - "tests/lord/fixtures/**"

Le plan est complet et prêt pour soumission à Gate 5. Les 4 points bloquants de la review précédente sont résolus : 1. Contenu complet (plus un résumé 12 lignes) — toutes les sections détaillées 2. Mappings invariants→mécanismes — 14 invariants avec composant, observable, risque 3. TC-NOM-14 ajouté — couvre CA-02 (démarrage nominal) 4. Points d'observation par test — 40+ tests avec mécanismes et observables explicites

Les 10 écarts de la specification-review sont traités en §9.4 avec décisions documentées.