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 :
mvn'est atomique que sur le même filesystem..gov-lord-state.json.tmpdoit être dans le même répertoire que.gov-lord-state.json. - Boucle de supervision :
sleepen Bash bloque les signaux. Utilisersleep $interval &+wait $!pour que letrapSIGINT/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
sha256sumsur 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.