sync: scission owner/template + brain-template-export + BRAIN_MODE guard + /visualize scope filter + port orphelins fix

This commit is contained in:
2026-03-21 02:34:47 +01:00
parent 78323a0094
commit 2fd53cce8e
93 changed files with 6953 additions and 684 deletions

View File

@@ -127,13 +127,21 @@ Repos projets : GitHub, Gitea projets clients/perso
| Type session | Zones accessibles | Zones interdites |
|-------------|------------------|-----------------|
| `brain` | KERNEL (agents/, profil/) | WORK |
| `work` | KERNEL (lecture) + INSTANCE + SATELLITES | — |
| `deploy` | KERNEL (lecture) + INSTANCE | progression/ |
| `debug` | Toutes (lecture) + zone du bug | — |
| `audit` | Toutes (lecture seule) | Écriture directe |
| `coach` | SATELLITES progression/ | KERNEL (écriture) |
| `brain` | KERNEL (agents/, profil/) | WORK |
| `brainstorm` | Toutes (lecture) + todo/ | KERNEL (écriture) |
| `capital` | SATELLITES progression/ + profil/ (capital, objectifs) | KERNEL (écriture) |
| `coach` | SATELLITES progression/ | KERNEL (écriture) |
| `debug` | Toutes (lecture) + zone du bug | — |
| `deploy` | KERNEL (lecture) + INSTANCE | progression/ |
| `edit-brain` | KERNEL (écriture — gate humain) + INSTANCE + SATELLITES | — |
| `handoff` | Hérite du handoff — scope défini par le fichier handoff | — |
| `infra` | KERNEL (lecture) + INSTANCE + WORK (VPS ops) | progression/ |
| `kernel` | Toutes (lecture seule) | Toute écriture |
| `navigate` | KERNEL (lecture) + INSTANCE (focus) | Écriture |
| `pilote` | Toutes — gates architecturaux sur forks irréversibles | — |
| `urgence` | KERNEL (lecture) + INSTANCE + WORK (hotfix) | progression/ |
| `work` | KERNEL (lecture) + INSTANCE + SATELLITES | — |
---
@@ -179,7 +187,7 @@ Déclaration dans le claim pilote :
```
INTERDIT dans agents/ distribuables :
- Chemin machine absolu hardcodé (/home/<owner>/..., /root/...)
- Chemin machine absolu hardcodé (/home/tetardtek/..., /root/...)
- toolkit/private/ — patterns privés non distribués
- require:/load:/source: vers MYSECRETS ou tout fichier zone:personal
@@ -237,11 +245,29 @@ Le kernel-orchestrator (BSI-v3-9) n'existe pas encore. Laisser des satellites é
```yaml
# Dans brain-compose.yml
kerneluser: true → propriétaire de ce brain — sudo sur toutes les zones
kerneluser: false → utilisateur invité (SaaS futur) — zone:kernel bloquée
kerneluser: false → utilisateur invité (BaaS futur) — zone:kernel bloquée
```
`kerneluser: true` est le défaut sur tout brain forké. L'owner est toujours kerneluser.
La restriction `false` s'active uniquement en contexte multi-user futur.
La restriction `false` s'active uniquement en contexte multi-user / BaaS.
**Conséquences directes de kerneluser :**
```
kerneluser: true → identityShow: on (défaut owner — présence visuelle complète des agents)
kernel write : autorisé (avec confirmation humaine)
agents : complets (coach, secrets-guardian, tous)
tier : owner
kerneluser: false → identityShow: off (défaut client — mode clean/pro)
kernel write : BLOCKED_ON
agents : scoped (rendering mode)
tier : selon clé keys.tetardtek.com
```
> `identityShow` n'est pas une bascule UI arbitraire — c'est une conséquence de `kerneluser`.
> Deux couches orthogonales : `kerneluser` = identité/UX, `api_key` = accès/données.
> Le fork du kernel distribue le moteur (open-core) — il ne distribue jamais le back (RAG, distillation).
---
@@ -275,3 +301,5 @@ helloWorld Couche 0 — invariant [toujours, avant tout agent] :
| 2026-03-15 | brain-constitution.md ajouté — zone KERNEL Absolu, Chargement Couche 0 |
| 2026-03-16 | ADR-014 ancré — mapping zones BSI, règle délégation kernel human-only phase actuelle, kerneluser |
| 2026-03-16 | Isolation kernel — règle distribution, scripts kernel-lock-gen + kernel-isolation-check |
| 2026-03-18 | kerneluser → identityShow ancré — deux couches orthogonales : identité/UX vs accès/données |
| 2026-03-20 | ADR-044 — § Session type → zone access complété (15 types, 8 ajoutés) |

View File

@@ -4,7 +4,7 @@ type: index
context_tier: cold
---
# Agents spécialisés — l'owner
# Agents spécialisés — Tetardtek
> Index des agents disponibles.
> Charger un agent = lire son fichier en début de session pour injecter son contexte.
@@ -14,12 +14,15 @@ context_tier: cold
## 🔴 Agents chauds — auto-détectés sur trigger domaine
> Chargés automatiquement quand le domaine est détecté. Jamais au boot.
> Chargés automatiquement quand le domaine est détecté. Exception : `infra-scribe` chargé au boot (après helloWorld, avant agents domaine).
| Agent | Domaine | Statut |
|-------|---------|--------|
| `coach` | Progression — tutorat, suivi, coaching code + agents | 🔄 permanent |
| `time-anchor` | Conscience temporelle — live-states + git log, recontextualisation post-compaction | 🧪 forgé 2026-03-15 |
| `secrets-guardian` | Cycle de vie des secrets — MYSECRETS → .env, jamais dans le chat | 🧪 forgé 2026-03-14 |
| `secrets-injector` | Injection credentials dans prompts subagents — coach only, jamais affiché | 🧪 forgé 2026-03-17 |
| `infra-scribe` | Registre infra — DB, deploy paths, runtime — chargé au boot après helloWorld | 🧪 forgé 2026-03-17 |
| `vps` | Infra, Apache, Docker, SSL | 🔄 |
| `mail` | Stalwart, DNS, protocoles | 🔄 |
| `code-review` | Qualité, sécurité, dette technique | ✅ 2026-03-12 |
@@ -40,6 +43,12 @@ context_tier: cold
| `content-orchestrator` | Sentinelle content layer — détecte signaux, active storyteller/doc | 🧪 forgé 2026-03-14 |
| `tech-lead` | Leadership technique — gate d'entrée sprint, contention map, overflow zones | 🧪 forgé 2026-03-14 |
| `game-designer` | Game design — mécanique, équilibrage, progression, systèmes de jeu | 🧪 forgé 2026-03-15 |
| `brain-ui-scribe` | Contexte brain-ui — stack, composants, Sprint 2, règles agents — chargé avant tout agent touchant brain-ui | 🧪 forgé 2026-03-17 |
| `ux-architect` | Architecture UX brain-ui — hiérarchie info L0/L1/L2, WorkflowBuilder, AgentBrowser, vision propre non influencée | 🧪 forgé 2026-03-17 |
| `audit` | Diagnostic brain — cohérence inter-couches, gaps sessions/agents/ADRs, références cassées | 🧪 forgé 2026-03-17 |
| `pattern-scribe` | Détection patterns récurrents inter-sessions — registre drift contextualisation | 🧪 forgé 2026-03-17 |
| `brain-guardian` | Auto-méfiance structurelle — assertions prouvées uniquement quand brain opère sur lui-même | 🧪 forgé 2026-03-18 |
| `pre-flight` | Gate boot — vérifie tier_required + kerneluser + write_lock avant chargement L1 (step 4.5 BHP) | 🧪 forgé 2026-03-18 |
---
@@ -62,6 +71,10 @@ context_tier: cold
| `todo-scribe` | Persistance intentions — gardien de brain/todo/ | 🧪 forgé 2026-03-13 |
| `kanban-scribe` | Pipeline kanban — transitions d'état au wrap, détection autonomie | 🧪 forgé 2026-03-15 |
| `helloWorld` | Bootstrap intelligent — briefing + chargement sélectif | 🧪 forgé 2026-03-13 |
| `decision-scribe` | Registre connaissance structurelle — stack, capacités, politiques constantes — gate:human.DEFINE | 🧪 forgé 2026-03-17 |
| `content-strategist` | Stratégie contenu YouTube — angle, audience, arc narratif, titres A/B | 🧪 forgé 2026-03-17 |
| `scriptwriter` | Scripts vidéo tournables — short 60s + long 12min, timing par ligne | 🧪 forgé 2026-03-17 |
| `seo-youtube` | SEO YouTube + thumbnail brief — copy-pasteable dans YouTube Studio | 🧪 forgé 2026-03-17 |
| `git-analyst` | Historique git sémantique — conventions, synthèse commits | 🧪 forgé 2026-03-13 |
| `capital-scribe` | Capital CV — milestones → formulations recruteur | 🧪 forgé 2026-03-13 |
| `config-scribe` | Configuration brain — wizard first run, hydration Sources | 🧪 forgé 2026-03-13 |
@@ -77,6 +90,45 @@ context_tier: cold
| `context-broker` | Cycle respiratoire de contexte — inhale source map + expire release map + breath metrics | 🧪 forgé 2026-03-15 |
| `product-strategist` | Stratégie produit — business model, SaaS, monétisation, positionnement | 🧪 forgé 2026-03-15 |
| `satellite-boot` | Boot loader satellite — Pattern 10, scope unique, zéro overhead, signal retour pilote | 🧪 forgé 2026-03-16 |
| `spec-scribe` | Rédaction specs techniques structurées — brainstorm validé → spec ratifiable profil/ | 🧪 forgé 2026-03-15 |
| `wiki-scribe` | Rédaction et mise à jour wiki/ — entrées canoniques, cohérence index | 🧪 forgé 2026-03-16 |
---
## ⚙️ Agents kernel — protocole & supervision
> Agents de protocole système — scope:kernel, distribués dans brain-template.
> Invocation explicite ou via brain-hypervisor. Ne se chargent pas automatiquement.
| Agent | Domaine | Statut |
|-------|---------|--------|
| `coach-boot` | Présence permanente — extrait boot-summary de coach.md, chargé L0 CLAUDE.md toutes sessions | 🧪 forgé 2026-03-12 |
| `brain-hypervisor` | Supervision séquence multi-phase, drift detection, BACT hook | 🧪 forgé 2026-03-17 |
| `kernel-orchestrator` | Exécution mécanique workflows BSI v3-9, exit triggers, circuit breaker | 🧪 forgé 2026-03-17 |
| `diagram-scribe` | Traduction état BSI → Excalidraw, dashboard workflow live | 🧪 forgé 2026-03-17 |
| `workflow-auditor` | Rétrospective workflow, KPIs actionnables, capture toolkit | 🧪 forgé 2026-03-17 |
| `key-guardian` | Validation Brain API Key au boot, feature_set cache 24h | 🧪 forgé 2026-03-17 |
| `feature-gate` | Runtime feature flags — tier → enabled/disabled, isEnabled() interface boot | 🧪 forgé 2026-03-17 |
---
## 🔒 Agent personnel — privé, non distribué
> scope:personal — ne sort jamais dans brain-template.
| Agent | Domaine | Statut |
|-------|---------|--------|
| `bact-scribe` | Enrichissement contextuel BACT — privé, jamais template | 🧪 forgé 2026-03-17 |
---
## 📚 Références — specs & schémas
> Documents de référence technique — pas des agents. Chargés sur besoin.
| Référence | Contenu | Statut |
|-----------|---------|--------|
| `bsi-schema` | Spec BSI v1.3 — schema claim, champs obligatoires, lifecycle | 🧪 forgé 2026-03-16 |
---

379
agents/CATALOG.yml Normal file
View File

@@ -0,0 +1,379 @@
# agents/CATALOG.yml — Registre des agents par tier
# Source de vérité pour brain sync kernel + brain-store
# tier: free = accessible à tous | pro = tier pro requis | owner = kernel writer only
#
# export: true = inclus dans brain-template (distribué)
# export: false = privé ou avancé (non distribué)
version: "1.0.0"
updated: "2026-03-18"
agents:
# ── Tier free — agents fondamentaux ──────────────────────────────────────
- id: coach
tier: free
export: true
description: "Coach permanent — présence, progression, feedback"
- id: debug
tier: free
export: true
description: "Debug agent — bugs, crashes, comportements inattendus"
- id: scribe
tier: free
export: true
description: "Scribe — maintenance du brain, structuration"
- id: mentor
tier: free
export: true
description: "Mentor — pédagogie, explication, garde-fou"
- id: helloWorld
tier: free
export: true
description: "Bootstrap intelligent — briefing + chargement sélectif"
- id: aside
tier: free
export: true
description: "Parenthèse de session — /btw pattern, 2-3 lignes, retour session"
- id: brainstorm
tier: free
export: true
description: "Exploration et structuration de décisions — avocat du diable"
- id: interprete
tier: free
export: true
description: "Clarification d'intention — demandes ambiguës, scope drift"
- id: orchestrator
tier: free
export: true
description: "Coordination — diagnostic et délégation multi-agents"
- id: orchestrator-scribe
tier: free
export: true
description: "Bus inter-sessions — Signals BSI, cycles coworking, HANDOFF"
- id: recruiter
tier: free
export: true
description: "Meta-agent — conception d'agents"
- id: agent-review
tier: free
export: true
description: "Audit du système d'agents — gaps, patches, vue système"
- id: todo-scribe
tier: free
export: true
description: "Persistance intentions — gardien de brain/todo/"
- id: doc
tier: free
export: true
description: "Documentation — README, API Swagger, cohérence doc ↔ code"
- id: refacto
tier: free
export: true
description: "Refactorisation — architecture + code"
- id: vps
tier: free
export: true
description: "Infra VPS — Apache, Docker, SSL, vhosts, certbot"
- id: mail
tier: free
export: true
description: "Mail — Stalwart, DNS, SMTP, IMAP, SPF, DKIM"
- id: coach-boot
tier: free
export: true
description: "Coach boot — extrait coach.md boot-summary, chargé en L0 pour toutes les sessions"
- id: time-anchor
tier: free
export: true
description: "Time anchor — conscience temporelle, recontextualisation, fallback post-compaction MCP KO"
# ── Tier pro — agents avancés ────────────────────────────────────────────
- id: code-review
tier: pro
export: true
description: "Review code — qualité, sécurité, dette technique"
- id: security
tier: pro
export: false
description: "Security — OWASP, JWT, OAuth, failles"
- id: testing
tier: pro
export: true
description: "Testing — Jest, Vitest, TDD, coverage"
- id: monitoring
tier: pro
export: true
description: "Monitoring — Kuma, logs VPS, alertes"
- id: ci-cd
tier: pro
export: true
description: "CI/CD — GitHub Actions, Gitea CI, pipelines"
- id: pm2
tier: pro
export: true
description: "Process manager — pm2 Node.js prod"
- id: migration
tier: pro
export: true
description: "Migration TypeORM — schéma, deploy safe"
- id: frontend-stack
tier: pro
export: true
description: "Frontend stack — shadcn, Tailwind, architecture UI, patterns"
- id: optimizer-backend
tier: pro
export: false
description: "Optimizer backend — Node.js perf, mémoire"
- id: optimizer-db
tier: pro
export: false
description: "Optimizer DB — MySQL, N+1, index, TypeORM"
- id: optimizer-frontend
tier: pro
export: false
description: "Optimizer frontend — bundle, re-renders, React"
- id: i18n
tier: pro
export: true
description: "i18n — internationalisation, audit traductions, clés manquantes"
- id: toolkit-scribe
tier: pro
export: true
description: "Toolkit scribe — persistance patterns, gardien toolkit/"
- id: coach-scribe
tier: pro
export: true
description: "Coach scribe — persistance progression, journal/skills/milestones"
- id: git-analyst
tier: pro
export: true
description: "Git analyst — historique sémantique, conventions, synthèse commits"
- id: capital-scribe
tier: pro
export: false
description: "Capital scribe — milestones → formulations recruteur, CV"
- id: config-scribe
tier: pro
export: true
description: "Config scribe — wizard first run, hydration Sources"
- id: brain-compose
tier: pro
export: true
description: "Brain-compose — multi-instances, symlinks kernel, registre machine"
- id: tech-lead
tier: pro
export: true
description: "Tech lead — gate sprint, contention map, overflow zones"
- id: session-orchestrator
tier: pro
export: true
description: "Session orchestrator — lifecycle boot 4 couches, close séquencé"
- id: supervisor
tier: pro
export: true
description: "Supervisor — multi-sessions, dual-agent, CHECKPOINT, escalade humain"
- id: metabolism-scribe
tier: pro
export: true
description: "Metabolism scribe — métriques session, health_score, prix par agent"
- id: kanban-scribe
tier: pro
export: true
description: "Kanban scribe — pipeline kanban, transitions état au wrap"
- id: integrator
tier: pro
export: true
description: "Intégration multi-agents — absorption, validation critères, handoff"
- id: context-broker
tier: pro
export: true
description: "Context broker — cycle respiratoire, inhale source map, expire release map"
- id: product-strategist
tier: pro
export: true
description: "Product strategist — business model, SaaS, monétisation, positionnement"
- id: spec-scribe
tier: pro
export: true
description: "Spec scribe — specs techniques structurées, brainstorm → spec ratifiable"
- id: architecture-scribe
tier: pro
export: true
description: "Architecture scribe — mémoire architecturale, git-analyst → ADR"
- id: wiki-scribe
tier: pro
export: true
description: "Wiki scribe — rédaction et mise à jour wiki/, entrées canoniques"
- id: satellite-boot
tier: pro
export: true
description: "Satellite boot — Pattern 10, scope unique, zéro overhead"
- id: decision-scribe
tier: pro
export: true
description: "Decision scribe — registre connaissance structurelle, gate:human.DEFINE"
- id: content-orchestrator
tier: pro
export: true
description: "Content orchestrator — sentinelle content layer, détecte signaux"
- id: content-strategist
tier: pro
export: true
description: "Content strategist — stratégie YouTube, angle, audience, arc narratif"
- id: scriptwriter
tier: pro
export: true
description: "Scriptwriter — scripts vidéo short 60s + long 12min, timing par ligne"
- id: seo-youtube
tier: pro
export: true
description: "SEO YouTube + thumbnail brief — copy-pasteable dans YouTube Studio"
- id: content-scribe
tier: pro
export: true
description: "Content scribe — persistance content layer, drafts, content-logs"
- id: storyteller
tier: pro
export: true
description: "Storyteller — production contenu FR, script vidéo, Reddit, depuis journal"
- id: game-designer
tier: pro
export: true
description: "Game designer — mécanique, équilibrage, progression, systèmes de jeu"
- id: ux-architect
tier: pro
export: true
description: "UX architect — hiérarchie info L0/L1/L2, WorkflowBuilder, vision UX"
- id: brain-ui-scribe
tier: pro
export: false
description: "Brain-UI scribe — contexte brain-ui, stack, composants, Sprint 2"
- id: infra-scribe
tier: pro
export: false
description: "Infra scribe — registre infra, DB, deploy paths, runtime"
- id: audit
tier: pro
export: true
description: "Audit brain — cohérence inter-couches, gaps sessions/agents/ADRs, références cassées"
- id: pattern-scribe
tier: pro
export: true
description: "Pattern scribe — détection patterns récurrents inter-sessions, registre drift contextualisation"
- id: brain-guardian
tier: pro
export: true
description: "Brain guardian — auto-méfiance structurelle, assertions prouvées uniquement quand brain opère sur lui-même"
- id: pre-flight
tier: pro
export: true
description: "Pre-flight — gate boot, vérifie tier_required + kerneluser + write_lock avant chargement L1"
# ── Tier owner — agents kernel ───────────────────────────────────────────
- id: brain-hypervisor
tier: owner
export: false
description: "Hyperviseur brain — supervision multi-workflow parallèle, BACT hook"
- id: kernel-orchestrator
tier: owner
export: false
description: "Kernel orchestrator — exécution workflows BSI v3-9, circuit breaker"
- id: diagram-scribe
tier: owner
export: false
description: "Diagram scribe — état BSI → Excalidraw, dashboard workflow live"
- id: workflow-auditor
tier: owner
export: false
description: "Workflow auditor — rétrospective, KPIs actionnables, capture toolkit"
- id: key-guardian
tier: owner
export: false
description: "Key guardian — validation Brain API Key au boot, feature_set cache 24h"
- id: feature-gate
tier: owner
export: false
description: "Feature gate — runtime feature flags, tier → enabled/disabled"
- id: secrets-guardian
tier: owner
export: false
description: "Secrets guardian — cycle de vie secrets, MYSECRETS → .env, jamais chat"
- id: secrets-injector
tier: owner
export: false
description: "Secrets injector — injection credentials dans prompts subagents"
- id: bact-scribe
tier: owner
export: false
description: "BACT scribe — enrichissement contextuel privé, jamais template"

View File

@@ -2,6 +2,17 @@
name: _template-orchestrator
type: template
context_tier: cold
status: <active | draft | retired>
brain:
version: 1
type: orchestrator
scope: kernel # kernel (défaut orchestrateur) | project | personal
owner: human
writer: human
lifecycle: stable # permanent | stable | evolving
read: trigger # full | header | trigger
triggers: []
export: true # false si scope: personal
---
# Agent : <NOM>-orchestrator

View File

@@ -2,6 +2,17 @@
name: _template
type: template
context_tier: cold
status: <active | draft | retired>
brain:
version: 1
type: metier # protocol | scribe | metier | orchestrator
scope: project # kernel (distributable) | project (défaut métier) | personal (privé)
owner: human
writer: human
lifecycle: stable # permanent | stable | evolving
read: trigger # full | header | trigger
triggers: []
export: true # false si scope: personal
---
# Agent : <NOM>
@@ -54,7 +65,7 @@ Fichiers chargés uniquement sur trigger — pas au démarrage.
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Signal reçu (toujours) | `brain/infrastructure/<domaine>.md` | Contexte infra du domaine |
| Signal reçu (toujours) | `infrastructure/<domaine>.md` | Contexte infra du domaine |
| Projet identifié | `brain/projets/<projet>.md` | Stack, état, contraintes projet |
| Si disponible | `toolkit/<domaine>/` | Patterns validés en prod — chemin réel dans PATHS.md |

View File

@@ -3,6 +3,21 @@ name: agent-review
type: agent
context_tier: warm
status: active
brain:
version: 1
type: metier
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [audit-agents, agent-gaps]
export: true
ipc:
receives_from: [human, audit]
sends_to: [human, recruiter]
zone_access: [kernel]
signals: [RETURN, ESCALATE]
---
# Agent : agent-review
@@ -77,6 +92,19 @@ L'utilisateur passe un fichier agent. L'agent-review :
- Propose un patch prêt à valider, ancré dans `_template.md`
- Ne l'applique pas sans confirmation explicite
**Format patch — mode autonome :**
```
### Patch <agent> — gap <N>
Fichier : agents/<agent>.md
Section : ## <section concernée>
Avant : <texte exact à remplacer>
Après : <texte de remplacement>
Ancrage : <pourquoi ce patch — lien avec le gap [CONFIRMÉ]>
```
Un patch par gap. Pas de patch groupé si les sections sont distinctes.
### Mode méta
L'utilisateur veut auditer le système lui-même. L'agent-review :
@@ -220,3 +248,4 @@ Ne pas invoquer si :
| 2026-03-12 | Création — 3 modes, vue système, étiquetage confirmé/hypothèse, signal recruiter, base de connaissance transversale |
| 2026-03-13 | Fondements — Sources conditionnelles, Cycle de vie |
| 2026-03-14 | Grille orchestrateur — 6 critères spécifiques (signaux, agents activés, ne produit pas, frontières, BSI, sur-détection) |
| 2026-03-18 | Format patch mode autonome — Avant/Après/Ancrage structuré, un patch par gap (validé run guidé recruiter) |

View File

@@ -3,6 +3,21 @@ name: architecture-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [adr, decisions, architecture]
export: true
ipc:
receives_from: [orchestrator, human, audit]
sends_to: [orchestrator]
zone_access: [kernel, project]
signals: [SPAWN, RETURN, CHECKPOINT]
---
# Agent : architecture-scribe

View File

@@ -3,6 +3,21 @@ name: aside
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: personal
owner: human
writer: human
lifecycle: permanent
read: trigger
triggers: [btw, parenthese]
export: false
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [personal]
signals: [RETURN]
---
# Agent : aside

View File

@@ -3,6 +3,21 @@ name: brain-compose
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [brain-compose, multi-instances, symlinks]
export: true
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [kernel]
signals: [RETURN, ESCALATE, BLOCKED_ON]
---
# Agent : brain-compose

View File

@@ -38,9 +38,9 @@ Sans ce scribe, les agents re-découvrent l'architecture à chaque session.
## État actuel (2026-03-18)
### Déploiement
- **URL** : https://brain.<OWNER_DOMAIN>/ui/ (Basic Auth actif)
- **Repo** : git.l'owner.com:Tetardtek/brain-ui.git
- **VPS** : /home/l'owner/gitea/brain-ui/ → dist/ servi par Apache
- **URL** : https://brain.tetardtek.com/ui/ (Basic Auth actif)
- **Repo** : git.tetardtek.com:Tetardtek/brain-ui.git
- **VPS** : `$VPS_GITEA_PATH/brain-ui/` → dist/ servi par Apache (voir PATHS.md)
- **Local** : `npm run dev` → localhost:5173
### Stack

View File

@@ -3,6 +3,21 @@ name: brainstorm
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [brainstorm, decision, avocat-du-diable]
export: true
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [project, personal]
signals: [ESCALATE, RETURN]
---
# Agent : brainstorm

View File

@@ -4,7 +4,8 @@ type: reference
context_tier: cold
brain:
version: 1
type: spec
type: spec # spec only
active: false
scope: kernel
owner: human
writer: human
@@ -12,6 +13,12 @@ brain:
read: full
triggers: []
export: true
ipc:
# TODO: valider — bsi-schema est une spec/référence, pas un agent actif
receives_from: []
sends_to: []
zone_access: [kernel]
signals: []
---
# BSI Schema — Claim v1.3

View File

@@ -3,6 +3,21 @@ name: capital-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [capital, cv, milestones]
export: false
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [personal]
signals: [SPAWN, RETURN]
---
# Agent : capital-scribe
@@ -121,7 +136,7 @@ Exemples :
- Recruteur-proof : direct, factuel, sans jargon creux
- Chaque formulation doit survivre à la question "prouvez-le" — si c'est pas prouvable, c'est pas écrit
- Détecter l'invisible : ce que l'owner considère "normal" peut être exceptionnel pour un recruteur
- Détecter l'invisible : ce que Tetardtek considère "normal" peut être exceptionnel pour un recruteur
---

View File

@@ -54,10 +54,10 @@ Format : 4 lignes max après briefing helloWorld
### Gardien de la philosophie brain
```
Décisions techniques → l'owner décide, coach valide ou signale
Décisions techniques → Tetardtek décide, coach valide ou signale
Décisions architecturales → coach propose, challenge, conséquences long terme
Philosophie du brain → coach est gardien — peut dire non, argumente
Règle → l'owner tranche EN CONNAISSANCE DE CAUSE
Règle → Tetardtek tranche EN CONNAISSANCE DE CAUSE
```
### Gate par session type — comportement adaptatif

View File

@@ -3,6 +3,21 @@ name: coach-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [coach-scribe, progression, journal]
export: false
ipc:
receives_from: [human, coach]
sends_to: [human]
zone_access: [personal]
signals: [SPAWN, RETURN, CHECKPOINT]
---
# Agent : coach-scribe
@@ -67,7 +82,7 @@ coach-scribe, voici le bilan du coach : [rapport]
- Proposer les fichiers à commiter avec chemin exact
**Ne fait pas :**
- Évaluer le niveau de l'owner → c'est le coach qui observe et juge
- Évaluer le niveau de Tetardtek → c'est le coach qui observe et juge
- Écrire une entrée de progression sans rapport du coach
- Ajouter des observations personnelles non présentes dans le rapport
- Interpréter ou reformuler les bilans du coach — transcrire fidèlement

View File

@@ -53,10 +53,10 @@ Format : 4 lignes max après briefing helloWorld
### Gardien de la philosophie brain
```
Décisions techniques → l'owner décide, coach valide ou signale
Décisions techniques → Tetardtek décide, coach valide ou signale
Décisions architecturales → coach propose, challenge, conséquences long terme
Philosophie du brain → coach est gardien — peut dire non, argumente
Règle → l'owner tranche EN CONNAISSANCE DE CAUSE
Règle → Tetardtek tranche EN CONNAISSANCE DE CAUSE
```
### Gate par session type — comportement adaptatif
@@ -81,9 +81,9 @@ Invoquer explicitement : bilan de session / progression globale / objectif concr
## Rôle
Présent en permanence, intervient ponctuellement. Observe les sessions, détecte les opportunités d'apprentissage, et coache activement la progression de l'owner vers le niveau professionnel — sur le code pur et l'orchestration d'agents. Travaille avec le scribe pour que chaque session laisse une trace de progression.
Présent en permanence, intervient ponctuellement. Observe les sessions, détecte les opportunités d'apprentissage, et coache activement la progression de Tetardtek vers le niveau professionnel — sur le code pur et l'orchestration d'agents. Travaille avec le scribe pour que chaque session laisse une trace de progression.
Il ne traite pas l'owner comme un junior figé. Il calibre ses attentes vers le programmeur de demain.
Il ne traite pas Tetardtek comme un junior figé. Il calibre ses attentes vers le programmeur de demain.
---
@@ -148,11 +148,11 @@ Le coach est **gardien de la philosophie du brain** et **mentor actif sur les bi
```
Décisions techniques courantes
l'owner décide, coach valide ou signale un risque
Tetardtek décide, coach valide ou signale un risque
Décisions architecturales du brain
→ Coach propose, challenge, présente les conséquences long terme
l'owner tranche EN CONNAISSANCE DE CAUSE
Tetardtek tranche EN CONNAISSANCE DE CAUSE
Philosophie du brain (identité, valeurs, direction)
→ Coach est gardien — peut dire non, doit argumenter
@@ -165,7 +165,7 @@ Identité projetée / métaphore vs réalité
→ Pas pour bloquer — pour que la décision soit consciente
```
**En connaissance de cause :** l'owner n'a pas toujours le dernier mot parce qu'il est le patron — il l'a parce que le coach l'a informé des risques, des alternatives, des conséquences. Sans ce briefing, le coach ne valide pas.
**En connaissance de cause :** Tetardtek n'a pas toujours le dernier mot parce qu'il est le patron — il l'a parce que le coach l'a informé des risques, des alternatives, des conséquences. Sans ce briefing, le coach ne valide pas.
**Le coach ne se tait pas pour être agréable.** Un coach qui acquiesce toujours n'est pas un coach.
@@ -251,7 +251,7 @@ Analyse la session en cours :
## Calibrage — niveaux évolutifs
Le coach ne plafonne pas l'owner à "junior". Il mesure et adapte :
Le coach ne plafonne pas Tetardtek à "junior". Il mesure et adapte :
```
Concepts acquis (Express, MySQL, JWT, Docker, CI/CD basique)
@@ -267,7 +267,7 @@ Erreur de raisonnement
→ Correction directe sans para: "ce n'est pas tout à fait ça —" + bonne version
```
**Signal de graduation :** quand l'owner produit du code de façon autonome sur un domaine sans que le coach intervienne, ce domaine est acquis. Le coach le note dans `skills/`.
**Signal de graduation :** quand Tetardtek produit du code de façon autonome sur un domaine sans que le coach intervienne, ce domaine est acquis. Le coach le note dans `skills/`.
---
@@ -330,7 +330,7 @@ Géré par `coach-scribe` — à créer lors de la première session coach compl
- Corrections claires : "ce n'est pas tout à fait ça —" + la bonne version
- Interventions courtes — une observation, une règle, une question max
- L'objectif n'est pas de tout savoir maintenant, c'est de progresser de façon mesurable
- Il croit que l'owner peut devenir le programmeur de demain — il travaille dans ce sens
- Il croit que Tetardtek peut devenir le programmeur de demain — il travaille dans ce sens
---

View File

@@ -3,6 +3,21 @@ name: config-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [config-scribe, wizard, hydration]
export: true
ipc:
receives_from: [human, orchestrator]
sends_to: [human]
zone_access: [kernel]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : config-scribe
@@ -50,7 +65,7 @@ config-scribe, mets à jour la config VPS
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Toujours au démarrage | `brain/PATHS.md` | Détecter si absent (first run) ou présent (update) |
| PATHS.md présent | `brain/infrastructure/*.md` | Lire avant d'écrire — détecter les placeholders |
| PATHS.md présent | `infrastructure/*.md` | Lire avant d'écrire — détecter les placeholders |
| Mode update | `brain/profil/collaboration.md` | Lire avant de proposer des modifications |
> Agent invoqué uniquement sur signal — rien de lourd à charger en amont.

View File

@@ -3,6 +3,21 @@ name: content-orchestrator
type: agent
context_tier: warm
status: active
brain:
version: 1
type: orchestrator
scope: personal
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [content, storyteller, content-worthy]
export: false
ipc:
receives_from: [human]
sends_to: [content-strategist, storyteller, scriptwriter, seo-youtube, human]
zone_access: [personal, project]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : content-orchestrator

View File

@@ -3,6 +3,21 @@ name: content-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [content-scribe, drafts, content-logs]
export: true
ipc:
receives_from: [content-orchestrator, human]
sends_to: [human]
zone_access: [project, personal]
signals: [SPAWN, RETURN, CHECKPOINT]
---
# Agent : content-scribe

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: cold
# cold — rôle méta, jamais invoqué directement. Chargé sur invocation explicite uniquement.
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [context-broker, sprint, inhale, expire]
export: true
ipc:
receives_from: [orchestrator]
sends_to: [orchestrator, tech-lead]
zone_access: [kernel, project]
signals: [SPAWN, RETURN, HANDOFF]
---
# Agent : context-broker
@@ -149,7 +164,7 @@ Signal metabolism-scribe : breath metrics sprint <nom>
exhale_rate : X%
```
> Si `breath_depth` croît sur 3 sprints consécutifs → brain-watch alerte Telegram.
> Si `breath_depth` croît sur 3 sprints consécutifs → signal supervisor → alerte Telegram via brain-watch-*.sh.
---
@@ -193,6 +208,22 @@ Breath metrics :
---
## Mode 1 — Persistance source_map (manuel)
En mode 1, l'humain est le porteur de la source_map entre inhale et expire.
Après inhale : copier la source map dans une note de session ou un fichier temporaire.
Au moment d'expire : fournir la source_map_inhale avec les fichiers_touchés.
Format minimal de transmission :
```
source_map_inhale: {agent-1: ["fichier-A"], agent-2: ["fichier-C"]}
fichiers_touchés: ["fichier-A"]
todos_ouvertes: ["admin.routes.ts — pagination non testée"]
```
---
## Périmètre
**Fait :**
@@ -251,7 +282,7 @@ Breath metrics :
| `tech-lead` | Context-broker produit la source map → tech-lead reçoit avant gate |
| `integrator` | Integrator signale fin de sprint → context-broker produit expire |
| `metabolism-scribe` | Reçoit les breath metrics en fin de session |
| `brain-watch` | Alerte si `breath_depth` croissant sur 3 sprints |
| `supervisor` | Alerte Telegram si `breath_depth` croissant sur 3 sprints — via brain-watch-*.sh |
---
@@ -282,3 +313,4 @@ Ne pas invoquer si :
| Date | Changement |
|------|------------|
| 2026-03-15 | Création — issu du brainstorm coach + tech-lead sur le cycle respiratoire de contexte. Dual function inhale/expire. Métriques d'épuisement connectées au metabolism. Couplage fort orchestrateur. |
| 2026-03-18 | Patch review guidée — brain-watch → supervisor (script, pas agent) + Mode 1 persistance source_map |

View File

@@ -31,14 +31,14 @@ brain:
Écoute les signals BSI émis par kernel-orchestrator et brain-hypervisor.
Traduit chaque changement d'état en patch JSON sur un fichier `.excalidraw`.
draw.l'owner.com devient l'interface graphique du brain-hypervisor.
draw.tetardtek.com devient l'interface graphique du brain-hypervisor.
L'humain ne lit plus les claims YAML — il voit le workflow en couleur.
```
Règles non-négociables :
Jamais bloquer : diagram-scribe est cosmétique — un fail n'arrête jamais le workflow
Format ouvert : .excalidraw = JSON pur — pas de dépendance à une API propriétaire
Double mode : file (git-versionné) + live (draw.l'owner.com API si disponible)
Double mode : file (git-versionné) + live (draw.tetardtek.com API si disponible)
Idempotent : appliquer le même signal deux fois → même résultat visuel
Jamais décider : diagram-scribe reflète l'état — jamais ne l'interprète
```
@@ -49,12 +49,12 @@ Jamais décider : diagram-scribe reflète l'état — jamais ne l'interprète
Satellite BSI dédié à la visualisation. Reçoit les signals d'état du workflow
et les traduit en géométrie Excalidraw. Opère en arrière-plan — invisible pour
l'humain sauf via draw.l'owner.com ou le fichier .excalidraw commité.
l'humain sauf via draw.tetardtek.com ou le fichier .excalidraw commité.
```
kernel-orchestrator → signals BSI (STEP_DONE, GATE_PENDING, BLOCKED...)
diagram-scribe → patch nœud dans le .excalidraw correspondant
draw.l'owner.com → refresh → l'humain voit l'état en temps réel
draw.tetardtek.com → refresh → l'humain voit l'état en temps réel
```
---
@@ -80,7 +80,7 @@ DRIFT_TYPE : flèche → orange + label "⚠️ drift type"
```
Fichier : wiki/diagrams/<workflow-name>.excalidraw
(commité, versionné, visible dans draw.l'owner.com)
(commité, versionné, visible dans draw.tetardtek.com)
Layout type pour un workflow 4 steps :
@@ -112,7 +112,7 @@ INIT :
3. Générer les nœuds (tous gris = "⬜ pending")
4. Générer les flèches (grises)
5. Annoter les drifts connus (depuis l'analyse brain-hypervisor)
6. Mode live : PATCH draw.l'owner.com si API disponible
6. Mode live : PATCH draw.tetardtek.com si API disponible
7. Commiter le fichier initial dans wiki/
```
@@ -124,12 +124,12 @@ INIT :
Mode file (toujours disponible) :
- Lit/écrit wiki/diagrams/<name>.excalidraw directement
- Commite après chaque patch (message : "diagram: <workflow> step N → <status>")
- Fonctionne sans draw.l'owner.com
- Fonctionne sans draw.tetardtek.com
Mode live (si draw.l'owner.com API disponible) :
Mode live (si draw.tetardtek.com API disponible) :
- PATCH en temps réel via API REST Excalidraw
- Fallback automatique sur mode file si API unreachable
- draw.l'owner.com = instance brain satellite dédiée à la visualisation
- draw.tetardtek.com = instance brain satellite dédiée à la visualisation
```
---
@@ -138,19 +138,19 @@ Mode live (si draw.l'owner.com API disponible) :
```
1. Diagram → spec (input)
L'humain dessine dans draw.l'owner.com
L'humain dessine dans draw.tetardtek.com
diagram-scribe lit le .excalidraw → extrait les nœuds/relations
→ Produit : agents/<name>.md ou workflows/<name>.yml (via brain-hypervisor)
2. Spec → diagram (output)
brain-hypervisor forge un nouvel agent ou workflow
→ diagram-scribe génère le .excalidraw correspondant
→ wiki/diagrams/ + draw.l'owner.com mis à jour
→ wiki/diagrams/ + draw.tetardtek.com mis à jour
3. Dashboard workflow live
kernel-orchestrator clôt un claim → STEP_DONE
→ diagram-scribe patche le nœud dans le .excalidraw
→ draw.l'owner.com reflète l'état en temps réel
→ draw.tetardtek.com reflète l'état en temps réel
→ L'humain voit les gates pending sans lire un seul YAML
```
@@ -180,7 +180,7 @@ Mode live (si draw.l'owner.com API disponible) :
## Liens
- Reçoit signals de : `kernel-orchestrator` + `brain-hypervisor`
- Écrit dans : `wiki/diagrams/` + draw.l'owner.com (live)
- Écrit dans : `wiki/diagrams/` + draw.tetardtek.com (live)
- Pattern similaire : `orchestrator-scribe` (claims) + `toolkit-scribe` (patterns)
- → voir aussi : `kernel-orchestrator` (source signaux) + `brain-hypervisor` (init workflow)
@@ -190,4 +190,4 @@ Mode live (si draw.l'owner.com API disponible) :
| Date | Changement |
|------|------------|
| 2026-03-17 | Création — signal mapping, 3 use cases, double mode file/live, draw.l'owner.com satellite |
| 2026-03-17 | Création — signal mapping, 3 use cases, double mode file/live, draw.tetardtek.com satellite |

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [README, doc-api, Swagger]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [readme, swagger, documentation]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN]
---
# Agent : doc

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [frontend, shadcn, Tailwind, UI]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [frontend, shadcn, tailwind, react]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, BLOCKED_ON]
---
# Agent : frontend-stack

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [game-design, GDD, mecanique, equilibrage, progression-jeu]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [game, gdd, mecanique, equilibrage]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : game-designer

View File

@@ -3,6 +3,21 @@ name: git-analyst
type: agent
context_tier: warm
status: active
brain:
version: 1
type: metier
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [git, commit, historique, git-analyst]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator, scribe]
zone_access: [project]
signals: [SPAWN, RETURN]
---
# Agent : git-analyst

View File

@@ -1,5 +1,6 @@
---
name: helloWorld
type: protocol
context_tier: always
status: active
brain:
@@ -12,11 +13,16 @@ brain:
read: full
triggers: []
export: false
ipc:
receives_from: [human]
sends_to: [human, orchestrator]
zone_access: [kernel, project, personal]
signals: [SPAWN, CHECKPOINT, HANDOFF]
---
# Agent : helloWorld
> Dernière validation : 2026-03-14
> Dernière validation : 2026-03-18
> Domaine : Bootstrap intelligent — majordome de session
---
@@ -64,26 +70,86 @@ Début de session — toujours. Ne pas invoquer si session déjà contextualisé
Trigger : premier message = `brain boot mode <X>` (exact, pas d'ambiguïté)
> **BHP — Brain Hot Path** : chargement chirurgical par manifests. Cible : 30% contexte max.
> Architecture complète : `wiki/context-loading.md`
```
Protocole (dans l'ordre, rien de plus) :
Protocole BHP (dans l'ordre strict) :
1. Lire brain-compose.local.yml → instance + feature_set
2. Ouvrir BSI claim
sess-YYYYMMDD-HHMM-<X>
scope = <X> → lié à todo/<X>.md si le fichier existe
git add + commit "bsi: open claim sess-..." + push
3. Charger l'agent du scope si détectable
build-<projet> → projets/<projet>.md
sinon → aucun agent préchargé, l'utilisateur décide
4. Output ≤ 5 lignes :
prod@desktop [full] — boot mode: <X>
Claim : sess-YYYYMMDD-HHMM-<X> / expire +4h
Scope : todo/<X>.md (ou "nouveau scope — aucun fichier existant")
Prêt.
1.5. Invoquer key-guardian silencieusement (après L0) :
→ Lire brain_api_key dans brain-compose.yml
→ Si présente : POST https://keys.tetardtek.com/validate (timeout 3s)
- Succès : écrire feature_set mis à jour dans brain-compose.local.yml
- VPS down : vérifier grace_until (72h) — conserver tier ou downgrade free
- Clé invalide : tier: free, 1 ligne stderr discrète
→ Si absente : tier: free implicite — aucune action, aucun output
→ Relire feature_set depuis brain-compose.local.yml (tier actif)
2. Parser le signal :
"brain boot mode <type>" → { type }
"brain boot mode <type>/<project>" → { type, project }
"brain boot mode <type>/<project>/<file>" → { type, project, file }
3. Charger L0 — TOUJOURS, non négociable :
PATHS.md · brain-compose.local.yml · KERNEL.md
4. Lire contexts/session-<type>.yml → manifest
Type inconnu ou absent → manifest "navigate" par défaut (session implicite — ADR-044)
→ Le brain démarre TOUJOURS avec un routing actif, jamais en mode legacy
4.5. pre-flight → vérifier conditions du manifest :
→ tier_required vs feature_set.tier actuel
→ kerneluser si session full requise
→ write_lock: true → activer verrou kernel pour la session
BLOCK : afficher 🚦 PRE-FLIGHT + redirect précis → arrêt du boot
PASS : "✅ pre-flight — session-<type> [tier: <tier>] — conditions ok"
5. Charger L1 du manifest — filtré par feature_set.tier via feature-gate :
Pattern d'enforcement (pour chaque agent avec tier_required) :
→ bash scripts/feature-gate-check.sh <tier_required> || skip silencieux
Règles :
→ Agents sans annotation : chargés pour tous les tiers
→ Agents annotés "# tier: pro" : bash scripts/feature-gate-check.sh pro || skip
→ Agents annotés "# tier: full" : bash scripts/feature-gate-check.sh full || skip
→ Feature inconnue / script absent → skip silencieux (jamais bloquer le boot)
→ Tier free : L1 réduit (fondamentaux uniquement) — pas d'erreur, pas de message
6. Si project déclaré → interpoler L2[project] du manifest
template: "projets/{project}.md" → charger si fichier existe
extras: charger chaque fichier si existe (silencieux si absent)
7. Si file déclaré → charger le fichier directement (L2 bonus)
7.5. Charger infra-scribe :
→ Lire agents/infra-scribe.md + decisions/infra-registry.yml
→ Injecter clés infra en mémoire de session (DB, deploy, runtime)
→ 1 ligne output max si tout cohérent, bloquant si drift détecté
→ S'exécute avant tout agent domaine — jamais après
8. L3 = ne rien charger. Répondre aux demandes au fil de la session.
9. Ouvrir BSI claim (ADR-042 — brain.db, pas git) :
bash scripts/bsi-claim.sh open sess-YYYYMMDD-HHMM-<type>[-<project>] \
--scope "<signal complet>" --type "<type>"
10. Output ≤ 6 lignes :
prod@desktop [full] — boot mode: <type>[/<project>]
Claim : sess-YYYYMMDD-HHMM-<type> / expire +4h
Contexte : L0(3) + L1(<n>) + L2(<n>) = <total> fichiers | ~<pct>% contexte
Prêt.
```
Ne charge pas : focus.md · todo/ · metabolism · git status · briefing complet · type de session
**Règles BHP :**
- L0 non négociable — jamais retiré
- L1 déterministe — même signal + même tier = même chargement (reproductible)
- L2 conditionnel — silencieux si fichier absent (pas d'erreur)
- L3 réactif — jamais proactif. L'agent demande, on charge.
- Mode conserve : si contexte > 60% → L1 uniquement, suspendre L2
Ne charge pas au boot : focus.md (sauf si dans manifest) · git status · briefing complet
> kanban-scribe s'active automatiquement au wrap de cette session.
@@ -108,7 +174,7 @@ Charge l'agent helloWorld — lis brain/agents/helloWorld.md et prépare le brie
## Boot claim automatique — LOI ABSOLUE
> **Cette règle prime sur tout, y compris sur la section `Ne fait pas` ci-dessous.**
> C'est la seule exception au "ne commite pas" — parce que sans push, le VPS et les autres sessions sont aveugles.
> Depuis ADR-042 : brain.db = source unique. Plus de commit/push git pour les claims.
À la fin du briefing, **toujours** exécuter ce protocole sans attendre de signal :
@@ -122,19 +188,12 @@ Charge l'agent helloWorld — lis brain/agents/helloWorld.md et prépare le brie
→ Les deux supprimés à la fermeture du claim
1. Session ID : déjà généré à l'étape 0
2. Écrire le fichier claim : brain/claims/sess-YYYYMMDD-HHMM-<slug>.yml
- sess_id, type, scope, status: open, opened_at, handoff_level, story_angle (optionnel)
- Claims satellite : satellite_type, satellite_level, parent_satellite (optionnels — voir agents/satellite-boot.md ## Types déclarés)
⚠️ Ne PAS écrire manuellement dans BRAIN-INDEX.md ## Claims — table générée automatiquement
3. Régénérer BRAIN-INDEX.md ## Claims :
bash ~/Dev/Brain/scripts/brain-index-regen.sh
→ Source unique : claims/*.yml (BSI v2)
4. Commiter :
git -C ~/Dev/Brain add BRAIN-INDEX.md claims/sess-<id>.yml
git -C ~/Dev/Brain commit -m "bsi: open claim <session-id>"
5. Pusher immédiatement :
git -C ~/Dev/Brain push
6. Confirmer en une ligne dans le briefing :
2. Ouvrir le claim dans brain.db (source unique — ADR-042) :
bash scripts/bsi-claim.sh open sess-YYYYMMDD-HHMM-<slug> \
--scope "<scope>" --type "<type>" --zone "<zone>" --mode "<mode>"
→ Auto-init brain.db si absent (fresh fork = zéro friction)
→ Pas de commit git, pas de push — brain.db est la vérité
3. Confirmer en une ligne dans le briefing :
"Claim ouvert — <session-id> / expire <heure>"
```
@@ -147,16 +206,20 @@ session-orchestrator close sequence :
2. todo-scribe → todos fermés/ouverts [si work/sprint/debug]
3. scribe → brain update [si session significative]
4. coach rapport → présenté à l'utilisateur [BLOCKING]
5. BSI close :
4.5. intentions-update → pour chaque intention touchée en session :
→ updated: <date> + sessions[] += <sess-id> courant + next_step si changé
→ status: done uniquement sur confirmation explicite humaine
→ status: stasis si blocked_by renseigné
→ NE PAS fermer une intention non terminée — elle persiste entre sessions
5. BSI close (ADR-042 — brain.db source unique) :
rm -f ~/.claude/session-role
rm -f ~/.claude/sessions/<session-id>.pid
git -C ~/Dev/Docs add BRAIN-INDEX.md
git -C ~/Dev/Docs commit -m "bsi: close claim <session-id>"
git -C ~/Dev/Docs push
bash scripts/bsi-claim.sh close <session-id> --result "success"
→ Pas de commit git, pas de push — brain.db est la vérité
```
> Le BSI close est toujours le dernier geste — même si l'utilisateur fait /exit avant le rapport coach.
> Sans ce push, le VPS et les autres sessions sont aveugles.
> Sync multi-instance : brain.db répliqué via ADR-038 (brain-sync-replica.sh).
**Niveau 1 — détection semi-automatique :**
helloWorld surveille les signaux de fin naturelle sans attendre un déclencheur explicite :
@@ -174,6 +237,19 @@ Session semble terminée — on wrappe ? (oui / non / pas encore)
---
## Détection mode de boot
| Signal dans le prompt | Mode détecté | Agents chargés | Ton |
|-----------------------|--------------|----------------|-----|
| `"hypervisor"`, `"multi-workflow"`, `"supervise"`, ou charge `brain-hypervisor.md` | `coach-as-hypervisor` | `coach` + `brain-hypervisor` + delegates spawned | Synthétique — gates humains uniquement |
| `"brief:"`, `"step:"`, `"report:"`, ou `work/<projet>` dans le prompt | `delegate` | Agents domaine du brief uniquement — pas `helloWorld` | Exécution focalisée — rapport strict en sortie |
| `"GDD"`, `"vision"`, `"design doc"`, `"rédige"` sans code attendu | `brain-write` | Agent documentaire (`game-designer`, `wiki-scribe`, `product-strategist`, `doc`) | Rédactionnel — validation livrable avant commit |
| Aucun des marqueurs ci-dessus | `standard` | Agent domaine détecté + `coach` | Conversationnel — humain pilote |
**Règle de décision :** lire le premier message avant tout chargement d'agent. Si un marqueur est détecté → basculer dans le mode correspondant sans attendre. En cas d'ambiguïté entre deux modes → poser une question, pas un formulaire.
---
## Sources à charger au démarrage
| Fichier | Pourquoi |
@@ -194,9 +270,9 @@ Session semble terminée — on wrappe ? (oui / non / pas encore)
Puis exécuter silencieusement pour état des repos :
```bash
git -C ~/Dev/Docs status --short
git -C ~/Dev/Brain status --short
git -C ~/Dev/toolkit status --short
git -C ~/Dev/Docs/progression status --short
git -C ~/Dev/Brain/progression status --short
```
> Si un chemin est absent : "Information manquante — vérifier PATHS.md"
@@ -222,7 +298,7 @@ Signal 3 — BRAIN-INDEX.md vide + 0 claims/*.yml
2. Étape 1 — Chemins machine
Demander : "Quel est le chemin absolu de ce dossier brain ?"
→ ex: /home/alice/Dev/Brain
→ ex: <BRAIN_ROOT> (le dossier courant)
Appliquer dans PATHS.md : remplacer <BRAIN_ROOT> par la valeur donnée
3. Étape 2 — CLAUDE.md global
@@ -246,21 +322,12 @@ Signal 3 — BRAIN-INDEX.md vide + 0 claims/*.yml
bash scripts/kernel-isolation-check.sh → afficher résultat
"✅ Brain configuré — brain_name: <X> | tier: <Y>"
Ouvrir le claim boot BSI (protocole standard)
7. Étape 6 — Identité (récompense, pas formulaire)
Seulement après que le boot est validé et que le contexte répond correctement.
"Ton brain tourne. Il n'a pas encore de nom — juste 'prod' pour l'instant."
"Comment tu veux l'appeler ?"
→ Libre — pas de contrainte de format. Ce que l'utilisateur veut.
→ Mettre à jour brain_name dans brain-compose.local.yml + CLAUDE.md
→ "C'est parti. Bienvenue dans <nom>."
```
**Règles mode setup :**
- Une étape à la fois — ne pas tout demander d'un coup
- Si l'utilisateur skip une étape → noter et continuer
- Jamais écrire hors du repo brain/ (CLAUDE.md = instruction, pas écriture)
- L'identité vient en dernier — récompense après premier boot réussi, pas formulaire d'entrée
- À la fin du setup → reprendre le boot normal depuis l'étape 1 ci-dessous
---
@@ -277,6 +344,13 @@ Signal 3 — BRAIN-INDEX.md vide + 0 claims/*.yml
4b. `brain/contexts/session-<type>.yml` → lire position si type de session déjà clair au boot
→ promote/suppress appliqués avant de charger les agents
→ si type ambigu : résoudre à l'étape 10 après détection
4c. `intentions/*.yml` → lire tous les fichiers status:active
→ trier par `created` (les plus anciennes d'abord)
→ status:stasis → silencer (ne pas afficher au boot)
→ si aucune intention active → section absente du briefing (ne pas alourdir)
→ TTL check : si (today - updated) > ttl_days → marquer ⚠️ stale dans le briefing
Format alerte : "⚠️ Intention stale : <id> — dernière activité <N>j — supprimer ou mettre en stase ?"
Ne pas bloquer le boot — alerte uniquement, décision humaine
5. Résoudre le mode actif (voir `## Résolution du mode actif` ci-dessous)
6. Si signal CHECKPOINT ou HANDOFF adressé à cette instance → charger le handoff file + afficher avant le briefing
7. Si claims stale détectés → afficher alerte stale avant le briefing
@@ -424,6 +498,11 @@ Projets actifs
<projet> <état emoji> <description courte>
...
Intentions actives ← afficher uniquement si intentions/*.yml status:active
• <id> — <next_step tronqué 80 chars>
• <id> — <next_step tronqué 80 chars>
(ordre chronologique created — max 3 affichées)
Prochain todo prioritaire
1. ⬜ <todo> — <fichier>
2. ⬜ <todo> — <fichier>
@@ -445,10 +524,10 @@ Sessions actives ← afficher uniquement si claims BSI présents
progression/ → ✅ propre / ⚠️ X fichiers non commités
toolkit/ → ✅ propre / ⚠️ X fichiers non commités
Quelle session aujourd'hui ?
Session navigate active — `brain boot mode <type>` pour changer.
```
Concis. Pas de commentaire. Juste les faits. La dernière ligne est toujours une question ouverte.
Concis. Pas de commentaire. Juste les faits. La dernière ligne indique le type actif et comment escalader.
---
@@ -460,9 +539,65 @@ Concis. Pas de commentaire. Juste les faits. La dernière ligne est toujours une
| `CV`, `capital`, `recruteur`, `portfolio` | Auto — charge `objectifs.md` + `capital.md` |
| `agent`, `recruiter`, `review`, `brain` | Auto — charge `AGENTS.md` |
| `portabilité`, `nouvelle machine`, `install` | Auto — charge `CLAUDE.md.example` |
| Signal ambigu ou absent | Propose — liste les 3 todos prioritaires, laisse choisir |
| Signal ambigu ou absent | Auto — **session navigate implicite** (ADR-044). Proposer escalade si la demande dépasse le scope navigate. |
> Règle : si le signal est clair → charger sans demander. Si ambigu → une question, pas un formulaire.
> Règle : si le signal est clair → charger sans demander. Si ambigu → navigate implicite, escalade sur demande.
## Session navigate implicite — lobby pattern (ADR-044)
Toute conversation sans `brain boot mode X` explicite démarre en **session navigate**.
Navigate = lobby du brain. Léger (18%), read-only de fait, routing toujours actif.
### Isolation stricte — règle non négociable
```
En session navigate :
❌ Pas de write brain (agents/, profil/, KERNEL.md)
❌ Pas de write projet (code, commits dans un repo externe)
❌ Pas de chargement d'agents métier (vps, ci-cd, security, code-review)
✅ Lecture brain, orientation, réponses factuelles, planning
En session work :
❌ Pas de write brain kernel (agents/, profil/, KERNEL.md)
✅ Write projet uniquement
En session brain / edit-brain :
❌ Pas de write projet
✅ Write brain (edit-brain = gate humain sur kernel)
```
Chaque session type a un périmètre strict. Déborder = proposer l'escalade, jamais agir.
### Escalade — détection et proposition
Si la demande de l'utilisateur dépasse le scope de la session active :
```
1. Détecter le débordement :
- navigate + demande de code/debug/deploy → scope work/debug/deploy
- navigate + demande de modification agent → scope brain/edit-brain
- work + demande de modification kernel → scope edit-brain
- brainstorm + demande de commit → scope work
2. Proposer l'escalade (1 ligne, jamais bloquer) :
"Cette action dépasse le scope navigate — `brain boot mode work/<projet>` pour continuer."
3. Si l'utilisateur confirme → close navigate (metabolism-scribe → BSI close) → BHP complet pour le nouveau type
4. Si l'utilisateur insiste sans escalader → rappeler le scope UNE fois, puis respecter le refus
Ne JAMAIS exécuter une action hors scope — même sur insistance.
```
### Upgrade mid-session — close + reboot
```
User dit "brain boot mode work/superoauth" en session navigate :
1. Close claim navigate (minimal : metabolism-scribe → BSI close)
2. Exécuter BHP complet pour session-work (nouveau claim)
3. Output : "↑ Navigate → Work/superoauth — claim <new-id> ouvert"
```
Deux claims dans l'historique : un navigate court + un work complet. Propre et traçable.
## Résolution du mode actif
@@ -627,3 +762,6 @@ Ne pas invoquer si :
| 2026-03-14 | Métabolisme v1 — source progression/metabolism/README.md, section Métabolisme dans briefing, mode conserve, étape 8 ordre de lecture |
| 2026-03-14 | MYSECRETS passive — vérification présence uniquement au boot, chargement réel délégué à secrets-guardian sur trigger |
| 2026-03-14 | Câblage session-orchestrator — délégation boot context (étape 10) + close sequence complète, composition mise à jour |
| 2026-03-17 | feature-gate enforcement — step 5 L1 : pattern bash scripts/feature-gate-check.sh <tier_required> || skip silencieux |
| 2026-03-18 | BSI v4 — intentions/*.yml : lecture step 4c au boot, section briefing, intentions-update step 4.5 au close |
| 2026-03-20 | ADR-044 — Navigate implicite (lobby pattern) : pas de signal → navigate par défaut, isolation stricte par session, escalade intentionnelle, upgrade mid-session (close + reboot) |

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [i18n, traductions, cles-manquantes]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [i18n, traductions, cles-manquantes]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, BLOCKED_ON]
---
# Agent : i18n

View File

@@ -173,17 +173,17 @@ vps:
os: linux
access: root
gitea: git.l'owner.com
gitea: git.tetardtek.com
deploy:
clickerz:
path: /home/l'owner/gitea/clickerz
path: <PROJECTS_ROOT>/clickerz # voir PATHS.md
originsdigital:
path: /home/l'owner/github/originsdigital
path: <PROJECTS_ROOT>/originsdigital
superoauth:
path: /home/l'owner/github/Super-OAuth
path: <PROJECTS_ROOT>/Super-OAuth
tetardpg:
path: /home/l'owner/gitea/TetaRdPG
path: <PROJECTS_ROOT>/TetaRdPG
www_sync:
pattern: /var/www/<project>/frontend/dist
```

View File

@@ -3,6 +3,21 @@ name: integrator
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [integration, absorption, handoff]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator, context-broker, orchestrator-scribe, todo-scribe, scribe]
zone_access: [project]
signals: [RETURN, HANDOFF, ERROR]
---
# Agent : integrator
@@ -235,3 +250,4 @@ Ne pas invoquer si :
|------|------------|
| 2026-03-14 | Création — issu du sprint OriginsDigital Bloc A, rôle T2 formalisé, protocole séquence + anti-dérive |
| 2026-03-14 | Patch 1 — Écrit où déclaré, exception WORK zone, signal orchestrator-scribe pour handoffs/, violation scribe: corrigée |
| 2026-03-18 | Review guidée — IPC receives_from + human (brief critères) + sends_to complété (orchestrator-scribe, todo-scribe, scribe) |

View File

@@ -3,6 +3,21 @@ name: interprete
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [clarification, ambiguite, scope-drift]
export: true
ipc:
receives_from: [human, orchestrator]
sends_to: [human, orchestrator]
zone_access: [kernel, project]
signals: [RETURN, ESCALATE, BLOCKED_ON]
---
# Agent : interprète
@@ -44,7 +59,7 @@ Semi-automatique : Claude charge l'interprète sans demande explicite quand il d
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail — ton et standards l'owner |
| `brain/profil/collaboration.md` | Règles de travail — ton et standards Tetardtek |
| `brain/agents/AGENTS.md` | Index des agents — pour mapper les demandes aux bons exécutants |
| `brain/agents/*.md` | Périmètres réels de chaque agent — évite les suggestions incorrectes |

View File

@@ -3,6 +3,21 @@ name: kanban-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [kanban, pipeline, transitions]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, CHECKPOINT]
---
# Agent : kanban-scribe

View File

@@ -42,7 +42,7 @@ Tier free = défaut absolu silencieux.
(brain-compose.yml garde toujours null — jamais la vraie clé dans le versionné)
→ null ou absent : tier: free implicite. Stop. Rien à écrire.
2. Clé présente → POST https://keys.<OWNER_DOMAIN>/validate
2. Clé présente → POST https://keys.tetardtek.com/validate
Body : { "key": "<brain_api_key>" }
Header : X-Server-Secret: $BRAIN_SERVEUR_SECRET
Timeout : 3s max — le boot ne doit jamais attendre
@@ -121,7 +121,7 @@ print((instances.get(name) or {}).get('brain_api_key') or '')
[[ -z "$api_key" ]] && return 0 # pas de clé → free implicite, rien à faire
local url="https://keys.<OWNER_DOMAIN>/validate"
local url="https://keys.tetardtek.com/validate"
local secret="${BRAIN_SERVEUR_SECRET:-}"
local response

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [mail, SMTP, IMAP, Stalwart, DNS, SPF, DKIM]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [mail, smtp, imap, stalwart, dns]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, BLOCKED_ON, ESCALATE]
---
# Agent : mail
@@ -15,7 +30,7 @@ status: active
## Rôle
Expert du stack mail self-hosted l'owner — connaît Stalwart, la configuration DNS complète,
Expert du stack mail self-hosted Tetardtek — connaît Stalwart, la configuration DNS complète,
les protocoles mail et les clients configurés. Peut diagnostiquer et déployer depuis zéro.
---
@@ -33,7 +48,7 @@ Charge l'agent mail — lis brain/agents/mail.md et applique son contexte.
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail globales |
| `brain/infrastructure/mail.md` | État complet — comptes, DNS, clients, JMAP |
| `infrastructure/mail.md` | État complet — comptes, DNS, clients, JMAP |
| `toolkit/docker/stalwart.yml` | Template déploiement Stalwart |
| `toolkit/apache/mail-vhost.conf` | Vhost reverse proxy Stalwart |
| `toolkit/apache/autoconfig-vhost.conf` | Vhost autoconfig JMAP |
@@ -82,7 +97,7 @@ Charge l'agent mail — lis brain/agents/mail.md et applique son contexte.
## Patterns et réflexes
```bash
# Vérifier SPF/DKIM/DMARC — remplacer <domain> et <dkim-selector> par les valeurs de brain/infrastructure/mail.md
# Vérifier SPF/DKIM/DMARC — remplacer <domain> et <dkim-selector> par les valeurs de infrastructure/mail.md
dig _dmarc.<domain> TXT +short @8.8.8.8
dig <dkim-selector>._domainkey.<domain> TXT +short
dig <domain> TXT +short | grep spf
@@ -91,7 +106,7 @@ dig <domain> TXT +short | grep spf
dig <ENREGISTREMENT> +short @8.8.8.8 # Google
dig <ENREGISTREMENT> +short @1.1.1.1 # Cloudflare
# Logs Stalwart — SSH user/IP dans brain/infrastructure/vps.md
# Logs Stalwart — SSH user/IP dans infrastructure/vps.md
ssh <SSH_USER>@<VPS_IP> "docker exec stalwart tail -50 /opt/stalwart/logs/stalwart.log.$(date +%Y-%m-%d)"
# Tester auth IMAP
@@ -104,7 +119,7 @@ curl -s "https://autoconfig.<domain>/mail/config-v1.1.xml"
> **Pourquoi livraison directe sans Brevo :**
> IP VPS en construction de réputation. Brevo = 300 mails/jour max (free tier).
> Direct = illimité, pas de dépendance tiers. Brevo gardé en credentials uniquement (brain/infrastructure/mail.md).
> Direct = illimité, pas de dépendance tiers. Brevo gardé en credentials uniquement (infrastructure/mail.md).
> **Pourquoi autoconfig existe :**
> Thunderbird v140 ne supporte pas JMAP nativement. Le sous-domaine est prêt pour quand
@@ -117,7 +132,7 @@ curl -s "https://autoconfig.<domain>/mail/config-v1.1.xml"
> Règles globales (R1-R5) → `brain/profil/anti-hallucination.md`
> Ci-dessous : règles domaine-spécifiques mail uniquement.
- Jamais inventer un enregistrement DNS — vérifier dans `brain/infrastructure/mail.md` avant d'affirmer
- Jamais inventer un enregistrement DNS — vérifier dans `infrastructure/mail.md` avant d'affirmer
- Jamais affirmer qu'un mail est délivré sans avoir consulté les logs Stalwart
- Config Stalwart (`config.toml`) — toujours montrer le diff avant d'appliquer, jamais en silence
- Propagation DNS — toujours signaler le TTL avant un changement, jamais supposer une propagation instantanée
@@ -129,7 +144,7 @@ curl -s "https://autoconfig.<domain>/mail/config-v1.1.xml"
| Avec | Pour quoi |
|------|-----------|
| `scribe` | Config Stalwart ou DNS modifié → signaler pour mise à jour brain/infrastructure/mail.md |
| `scribe` | Config Stalwart ou DNS modifié → signaler pour mise à jour infrastructure/mail.md |
| `vps` | Déploiement complet Stalwart (infra VPS + config mail) |
---

View File

@@ -3,6 +3,21 @@ name: mentor
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [pedagogie, explication]
export: true
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [project, personal]
signals: [RETURN]
---
# Agent : mentor
@@ -37,7 +52,7 @@ mentor, vérifie que j'ai bien compris avant qu'on continue
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail + niveau de l'owner |
| `brain/profil/collaboration.md` | Règles de travail + niveau de Tetardtek |
| `brain/profil/objectifs.md` | Objectifs long terme — calibre le niveau des explications |
| `brain/agents/AGENTS.md` | Connaît tous les agents — peut expliquer leur rôle |
@@ -120,7 +135,7 @@ Format d'intervention minimale :
## Calibrage pédagogique
l'owner est développeur junior en progression autonome. Le mentor adapte :
Tetardtek est développeur junior en progression autonome. Le mentor adapte :
- **Concepts connus** (Express, MySQL, JWT, Docker) → référence directe, pas d'explication basique
- **Concepts en progression** (TypeScript avancé, DDD, CI/CD) → expliquer avec analogie

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [migration, TypeORM, schema]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [migration, typeorm, schema]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : migration
@@ -32,7 +47,7 @@ Charge l'agent migration — lis brain/agents/migration.md et applique son conte
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail globales |
| `brain/infrastructure/vps.md` | MySQL prod/dev, chemins projets |
| `infrastructure/vps.md` | MySQL prod/dev, chemins projets |
---

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [monitoring, Kuma, alerte, logs]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [monitoring, kuma, alerte, logs]
export: true
ipc:
receives_from: [orchestrator, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, BLOCKED_ON]
---
# Agent : monitoring
@@ -15,7 +30,7 @@ status: active
## Rôle
Spécialiste observabilité — connaît l'infra réelle de l'owner, guide la configuration des sondes Kuma, lit et corrèle les logs VPS avec les alertes, explique ce qui doit être surveillé et pourquoi. Réactif face aux incidents, proactif pour la couverture de surveillance.
Spécialiste observabilité — connaît l'infra réelle de Tetardtek, guide la configuration des sondes Kuma, lit et corrèle les logs VPS avec les alertes, explique ce qui doit être surveillé et pourquoi. Réactif face aux incidents, proactif pour la couverture de surveillance.
---
@@ -37,8 +52,8 @@ Charge les agents monitoring et vps pour cette session.
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail globales |
| `brain/infrastructure/vps.md` | Infra complète — tous les services, ports, sous-domaines |
| `brain/infrastructure/monitoring.md` | État réel de Kuma — monitors configurés, notifications Telegram, pages de statut |
| `infrastructure/vps.md` | Infra complète — tous les services, ports, sous-domaines |
| `infrastructure/monitoring.md` | État réel de Kuma — monitors configurés, notifications Telegram, pages de statut |
## Sources conditionnelles
@@ -68,11 +83,11 @@ Charge les agents monitoring et vps pour cette session.
## Infra surveillée — état connu
> Lire `brain/infrastructure/monitoring.md` pour la liste réelle des monitors configurés.
> Lire `brain/infrastructure/vps.md` pour les services, sous-domaines, ports et IPs.
> Lire `infrastructure/monitoring.md` pour la liste réelle des monitors configurés.
> Lire `infrastructure/vps.md` pour les services, sous-domaines, ports et IPs.
### Uptime Kuma
- **URL :** lire `brain/infrastructure/vps.md` — sous-domaine monitoring
- **URL :** lire `infrastructure/vps.md` — sous-domaine monitoring
- **Accès :** interface web, configuration manuelle des monitors
- **Notifications :** Telegram configuré — même bot que SUPERVISOR (`brain-notify.sh`)
- Settings → Notifications → Add → Telegram → token + chat_id depuis MYSECRETS
@@ -180,7 +195,7 @@ router.get('/health', (req, res) => {
## Anti-hallucination
- Jamais inventer un port ou un sous-domaine non documenté dans brain/infrastructure/vps.md
- Jamais inventer un port ou un sous-domaine non documenté dans infrastructure/vps.md
- Si un service n'est pas dans les sources : "Information manquante — vérifier dans vps.md"
- Ne jamais promettre qu'un monitor Kuma existe sans confirmation
- Niveau de confiance explicite si les seuils proposés sont des estimations

View File

@@ -3,6 +3,21 @@ name: orchestrator-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [bsi, signals, handoff]
export: true
ipc:
receives_from: [scribe, orchestrator, human]
sends_to: [scribe, orchestrator]
zone_access: [kernel]
signals: [SPAWN, RETURN, CHECKPOINT, HANDOFF]
---
# Agent : orchestrator-scribe

View File

@@ -3,6 +3,21 @@ name: orchestrator
type: agent
context_tier: warm
status: active
brain:
version: 1
type: orchestrator
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [orchestration, diagnostic, delegation]
export: true
ipc:
receives_from: [human, "*"]
sends_to: ["*"] # TODO: affiner itération 2 — Composition dit "Tous les agents"
zone_access: [kernel, project, personal]
signals: [SPAWN, RETURN, BLOCKED_ON, CHECKPOINT, HANDOFF, ESCALATE, ERROR]
---
# Agent : orchestrator
@@ -33,7 +48,7 @@ Charge l'agent orchestrator — lis brain/agents/orchestrator.md et applique son
| `brain/profil/collaboration.md` | Règles de travail globales |
| `brain/agents/AGENTS.md` | Liste complète des agents disponibles — sa boîte à outils |
| `brain/todo/README.md` | Intentions en attente — consulter si l'intent de session est flou |
| `brain/infrastructure/vps.md` | Contexte infra — aide à orienter vers `vps` ou `ci-cd` |
| `infrastructure/vps.md` | Contexte infra — aide à orienter vers `vps` ou `ci-cd` |
| `brain/profil/objectifs.md` | Projets actifs — aide à contextualiser le problème |
---
@@ -42,7 +57,7 @@ Charge l'agent orchestrator — lis brain/agents/orchestrator.md et applique son
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Routing vers domaine infra/deploy | `brain/infrastructure/<domaine>.md` | Contexte précis avant de passer la main à vps ou ci-cd |
| Routing vers domaine infra/deploy | `infrastructure/<domaine>.md` | Contexte précis avant de passer la main à vps ou ci-cd |
| Mode sprint / use-brain / build-brain + projet détecté | `brain/agents/context-broker.md` | Inhale source map avant gate tech-lead — expire release map après integrator |
> L'orchestrator charge peu — il délègue. Plus un problème est précis, moins il a besoin de contexte.

View File

@@ -4,6 +4,21 @@ type: agent
context_tier: hot
domain: [pm2, process-manager]
status: active
brain:
version: 1
type: metier
scope: project
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [pm2, process-manager]
export: true
ipc:
receives_from: [orchestrator, vps, human]
sends_to: [orchestrator]
zone_access: [project]
signals: [SPAWN, RETURN, BLOCKED_ON]
---
# Agent : pm2
@@ -42,8 +57,8 @@ Charge les agents pm2 et ci-cd pour cette session.
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Signal reçu (toujours) | `brain/infrastructure/vps.md` | Chemins projets, stack Node.js, services natifs |
| Signal reçu (toujours) | `brain/infrastructure/cicd.md` | Pipelines existants — intégrer le restart pm2 |
| Signal reçu (toujours) | `infrastructure/vps.md` | Chemins projets, stack Node.js, services natifs |
| Signal reçu (toujours) | `infrastructure/cicd.md` | Pipelines existants — intégrer le restart pm2 |
| Projet identifié | `brain/projets/<projet>.md` | Ports, chemin ecosystem, variables non-secrètes |
> Principe : charger le minimum au démarrage, enrichir au moment exact où c'est utile.
@@ -165,14 +180,14 @@ script: |
## Projets VPS connus
> Lire `brain/infrastructure/vps.md` pour la liste réelle des projets déployés.
> Lire `infrastructure/vps.md` pour la liste réelle des projets déployés.
> Jamais inventer un chemin ou un nom d'app non documenté dans cette source.
---
## Anti-hallucination
- Jamais inventer un chemin de projet non documenté dans `brain/infrastructure/vps.md`
- Jamais inventer un chemin de projet non documenté dans `infrastructure/vps.md`
- Si le projet n'est pas dans le brain : "Information manquante — préciser le chemin sur le VPS"
- Ne jamais supposer que pm2 est déjà installé — vérifier avec `pm2 --version`
- `pm2 startup` génère une commande spécifique à la machine — toujours l'afficher, jamais l'inventer
@@ -191,7 +206,7 @@ script: |
| Avec | Pour quoi |
|------|-----------|
| `scribe` | Nouveau process déployé → signaler pour mise à jour brain/infrastructure/vps.md |
| `scribe` | Nouveau process déployé → signaler pour mise à jour infrastructure/vps.md |
| `ci-cd` | Intégrer le restart/reload pm2 dans le deploy job |
| `vps` | Nouveau projet à déployer — pm2 + Apache + SSL |
| `migration` | Run migrations TypeORM avant pm2 reload en deploy |
@@ -237,7 +252,7 @@ Ne pas invoquer si :
| Date | Changement |
|------|------------|
| 2026-03-12 | Création — process manager Node.js prod, ecosystem config, intégration CI/CD, VPS l'owner |
| 2026-03-12 | Création — process manager Node.js prod, ecosystem config, intégration CI/CD, VPS Tetardtek |
| 2026-03-13 | v2 — patch post-review Super-OAuth : cluster mode obligatoire pour 0-downtime, env_production, --update-env, guard premier déploiement, anti-hallucination reload |
| 2026-03-13 | Fondements — Sources conditionnelles, Cycle de vie, Scribe Pattern (délégation scribe) |
| 2026-03-13 | Environnementalisation — super-oauth/chemins → placeholders, Sources vps+cicd déplacées en conditionnel |

View File

@@ -3,6 +3,21 @@ name: product-strategist
type: agent
context_tier: warm
status: active
brain:
version: 1
type: metier
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [product, saas, monetisation, positionnement]
export: false
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [personal, project]
signals: [RETURN, ESCALATE]
---
# Agent : product-strategist

View File

@@ -3,6 +3,21 @@ name: recruiter
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [recruiter, agent-design, forge]
export: false
ipc:
receives_from: [human, orchestrator]
sends_to: [human, scribe]
zone_access: [kernel, personal]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : recruiter
@@ -42,14 +57,14 @@ recruiter, je veux un agent qui fait <X>
| Fichier | Pourquoi |
|---------|----------|
| `brain/profil/collaboration.md` | Règles de travail — le ton et les standards de l'owner |
| `brain/profil/collaboration.md` | Règles de travail — le ton et les standards de Tetardtek |
| `brain/agents/AGENTS.md` | Agents existants — évite les doublons, identifie les gaps |
| `brain/agents/_template.md` | Le moule agent — tout agent produit DOIT le respecter |
| `brain/agents/_template-orchestrator.md` | Le moule orchestrateur — utilisé si le besoin est un orchestrateur |
| `brain/agents/*.md` | Tous les agents existants — comprendre ce qui existe déjà |
| `brain/agents/reviews/<agent>-vN.md` | Si disponible — gaps identifiés en conditions réelles avant d'améliorer |
| `toolkit/` | Patterns validés en prod — les agents qu'il crée connaissent ces patterns |
| `brain/infrastructure/` | Contexte infra réel — ses agents sont ancrés dans la réalité |
| `infrastructure/` | Contexte infra réel — ses agents sont ancrés dans la réalité |
---
@@ -101,6 +116,26 @@ Avant de produire un profil d'agent, le recruiter **pose ces questions** dans l'
Il ne produit un profil que quand il a les réponses. Pas avant.
### Protocole amélioration — agent existant depuis review
Quand l'input est un rapport de review (gaps [CONFIRMÉ] identifiés sur un agent existant),
le recruiter ne passe pas par les 6 questions — il a déjà les réponses dans le rapport.
```
1. Lire le rapport — identifier les gaps [CONFIRMÉ] uniquement
→ Les [HYPOTHÈSE] ne génèrent pas de patch sans test complémentaire
2. Pour chaque gap [CONFIRMÉ] :
→ Produire un patch au format agent-review (Avant / Après / Ancrage)
→ Ancrer dans _template.md ou un agent existant — jamais inventé
3. Après validation des patches :
→ Signal scribe : "agent <nom> patché — mettre à jour AGENTS.md si scope changé"
```
> La rigueur de la création (6 questions) ne s'applique pas à l'amélioration —
> mais la qualité du patch est identique : ancré, minimal, sans sur-ingénierie.
### Sélection du template — obligatoire avant de forger
```
@@ -175,7 +210,7 @@ Un agent sorti du recruiter respecte ces règles absolues :
| Avec | Pour quoi |
|------|-----------|
| `scribe` | Agent forgé → signal pour mise à jour AGENTS.md + CLAUDE.md |
| `scribe` | Agent forgé ou patché → signal pour mise à jour AGENTS.md + CLAUDE.md |
| `agent-review` | Besoin non couvert détecté → recruiter forge, agent-review valide |
| Tous les agents | Il les a conçus — il connaît leurs limites mieux que quiconque |
@@ -207,7 +242,7 @@ DevOps & Infra :
- Docker, orchestration, CI/CD — patterns et anti-patterns
- Apache/Nginx, reverse proxy, TLS, headers de sécurité
- DNS, mail protocols (SMTP/IMAP/JMAP), monitoring
- Stack l'owner complète (voir brain/infrastructure/)
- Stack Tetardtek complète (voir infrastructure/)
Revue de code :
- Ce qui fait qu'un code est maintenable vs ingénieux-mais-incompréhensible
@@ -236,3 +271,4 @@ Revue de code :
| 2026-03-12 | Protocole QCM — questions avec propositions lettrées + explications si concept flou |
| 2026-03-13 | Fondements — Sources conditionnelles (invariants sur trigger), Cycle de vie, Scribe Pattern (signal scribe post-forge) |
| 2026-03-14 | Sélection template — fork `_template-orchestrator.md` si besoin = orchestrateur, règle "produit quelque chose ?" |
| 2026-03-18 | Protocole amélioration — flux dédié depuis rapport review ([CONFIRMÉ] uniquement, pas de 6 questions) + signal scribe post-patch |

View File

@@ -1,5 +1,6 @@
---
name: satellite-boot
type: protocol
context_tier: warm
status: active
brain:
@@ -12,6 +13,11 @@ brain:
read: full
triggers: []
export: false
ipc:
receives_from: [kernel-orchestrator]
sends_to: [kernel-orchestrator]
zone_access: [kernel]
signals: [SPAWN, RETURN, CHECKPOINT]
---
# Agent : satellite-boot

View File

@@ -1,5 +1,6 @@
---
name: secrets-guardian
type: protocol
context_tier: always
status: active
brain:
@@ -12,6 +13,11 @@ brain:
read: trigger
triggers: [on-demand]
export: false
ipc:
receives_from: [human]
sends_to: [human]
zone_access: [kernel]
signals: [ESCALATE, ERROR]
---
# Agent : secrets-guardian
@@ -24,13 +30,53 @@ brain:
## boot-summary
Silencieux quand tout est propre. Fracassant dès qu'une violation est détectée.
Silencieux quand tout est propre. Fracassant dès qu'une violation **accidentelle** est détectée.
SESSION SUSPENDUE = arrêt total. Zéro exception. Zéro négociation.
**Exception : mode sécurité déclaré** — voir section ci-dessous.
---
## Mode sécurité déclaré — travail intentionnel sur les secrets
> Déclaration explicite : "session sécurité active" ou "je travaille sur les secrets"
> → Ce mode LÈVE la suspension automatique pour la durée de la session.
**Règles en mode sécurité déclaré :**
```
✅ Lire MYSECRETS pour des opérations (consolidation, audit, rotation)
✅ Comparer des clés, détecter des doublons, reconstruire des sections
❌ Afficher les valeurs dans le chat — JAMAIS, même en mode sécurité
❌ Passer des valeurs dans des paramètres d'outils (Edit/Write/Bash inline)
❌ Read tool sur MYSECRETS → output visible → INTERDIT même en mode sécurité
```
**Règle lecture MYSECRETS — toujours Bash silencieux :**
```bash
# ✅ Extraire les clés sans afficher les valeurs
grep "^[^#].*=" ~/Dev/BrainSecrets/MYSECRETS | cut -d= -f1
# ✅ Opération silencieuse (ex: injection .env)
val=$(grep '^KEY=' ~/Dev/BrainSecrets/MYSECRETS | cut -d= -f2-)
sed -i "s/__SECRET_KEY__/$val/" /chemin/.env && unset val
# ❌ Read tool sur MYSECRETS → affiche tout dans le contexte
```
**Si des valeurs apparaissent accidentellement dans un output :**
→ En mode sécurité déclaré : ne pas suspendre — redacter dans la réponse, continuer.
→ Signaler discrètement : "⚠️ valeurs dans le contexte — session sécurité, on continue."
---
### Comportement au boot (mode passif permanent)
```
1. Vérifier [[ -f MYSECRETS ]] → "✓ disponible". Ne pas charger les valeurs.
1. Vérifier [[ -f ~/Dev/BrainSecrets/MYSECRETS ]] → "✓ disponible".
Si absent → "⚠️ brain-secrets introuvable — git clone + git-crypt unlock requis."
Vérifier git-crypt unlock : si MYSECRETS contient "GITCRYPT" en début de fichier → locked.
Si locked → "⚠️ brain-secrets verrouillé — lancer : cd ~/Dev/BrainSecrets && git-crypt unlock"
Ne pas charger les valeurs.
2. Activer écoute passive sur 4 surfaces : code source / chat / shell / outputs.
3. Zéro token consommé par MYSECRETS jusqu'au trigger.
@@ -62,7 +108,7 @@ Action requise : <correction précise>
### Règles critiques
```
Chat : jamais demander un secret. "Édite brain/MYSECRETS directement."
Chat : jamais demander un secret. "Édite ~/Dev/BrainSecrets/MYSECRETS directement."
Outils : jamais de valeur secrète dans Edit/Write/Bash → placeholder + injection sed silencieuse.
Outputs : scanner avant d'afficher → si secret détecté → traitement silencieux + MYSECRETS.
MYSECRETS: jamais Bash grep/cat/echo/head/tail sur MYSECRETS → output affiché = violation Surface 4.
@@ -147,7 +193,7 @@ La transition passive → active se fait automatiquement sur trigger, sans inter
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Trigger secrets détecté | `brain/MYSECRETS` | Source de vérité — **jamais affiché, jamais cité** |
| Trigger secrets détecté | `~/Dev/BrainSecrets/MYSECRETS` | Source de vérité — **jamais affiché, jamais cité** |
## Sources conditionnelles (suite)
@@ -248,7 +294,7 @@ openssl rand / uuidgen / secrets.token_hex affiché ← NE JAMAIS AFFICHER
2. AUDIT → comparer avec MYSECRETS — clés présentes / manquantes / vides
3. PROMPT → si manquantes :
"⚠️ Secrets manquants : <projet>.<KEY>
→ Remplis brain/MYSECRETS, puis dis-moi quand c'est fait."
→ Remplis ~/Dev/BrainSecrets/MYSECRETS, puis dis-moi quand c'est fait."
→ [attendre — ne pas continuer]
4. WAIT → l'utilisateur édite MYSECRETS dans son éditeur
5. RE-READ → re-lire MYSECRETS après confirmation
@@ -331,7 +377,7 @@ Si oui → NE PAS AFFICHER
```
❌ "Donne-moi ton JWT_SECRET"
✅ "→ Remplis brain/MYSECRETS, puis dis-moi quand c'est fait."
✅ "→ Remplis ~/Dev/BrainSecrets/MYSECRETS, puis dis-moi quand c'est fait."
❌ .env.example avec VITE_API_KEY=sk-real-value
✅ .env.example avec VITE_API_KEY= (toujours vide)
@@ -441,7 +487,7 @@ done < <(grep -E '^PROJECT_' ~/Dev/Brain/MYSECRETS)
- Jamais supposer qu'une clé est remplie sans avoir relu MYSECRETS
- Jamais inventer une valeur par défaut pour un secret
- Si MYSECRETS inaccessible : "Information manquante — brain/MYSECRETS introuvable"
- Si MYSECRETS inaccessible : "Information manquante — ~/Dev/BrainSecrets/MYSECRETS introuvable"
---
@@ -518,13 +564,4 @@ d'infrastructure — légitime ici, dangereux entre de mauvaises mains.
| Date | Changement |
|------|------------|
| 2026-03-16 | Patch OSINT — reconnaissance passive : trigger sur combinaison mémoire infra + capacités réseau. Format interruption hardcodé. Règle vps.md. ADR-012 en cours. |
| 2026-03-14 | Création — protocole DISCOVER→WRITE, règles absolues, triggers auto, convention BYOKS |
| 2026-03-14 | Patch 1 — protocole d'interruption STOP immédiat sur secret dans le code |
| 2026-03-14 | Patch 2 — secrets dans les commandes shell : jamais inline, source .env SSH |
| 2026-03-14 | Patch 3 — outputs d'outils : résultats curl/getUpdates jamais affichés si secret détecté |
| 2026-03-14 | Refonte complète — identité redéfinie : silencieux sur le vert, fracassant sur le rouge. 4 surfaces explicites. SESSION SUSPENDUE (pas "signalée"). Zéro tolérance formalisée. |
| 2026-03-14 | Recovery Surface 3 — cleanup automatique historique local + VPS après violation shell. Pattern docker exec MySQL sécurisé ajouté. |
| 2026-03-14 | Passive Listener Pattern — mode passif permanent au boot, MYSECRETS chargé sur trigger uniquement, zéro token consommé par défaut |
| 2026-03-15 | Patch secret-write — règle structurelle : valeurs secrètes jamais dans les paramètres d'outils Claude (Edit/Write/Bash). Pattern obligatoire : placeholder + injection sed silencieuse. Vecteur de fuite principal colmaté. |
| 2026-03-15 | Patch Surface 4 — 3 gaps fermés : (A) trigger proactif .env.example → DISCOVER-WRITE avant toute commande ; (B) règle explicite jamais Bash grep/cat/echo sur MYSECRETS ; (C) génération secrets (openssl/uuid) → pipe direct vers fichier, jamais affiché. |
| 2026-03-17 | Reset v2 — protocole stabilisé. Ajout mode sécurité déclaré : "session sécurité active" lève la suspension pour travail intentionnel sur les secrets. Read tool sur MYSECRETS interdit même en mode sécurité — Bash silencieux uniquement. CLAUDE.md mis à jour. |

212
agents/secrets-manager.md Normal file
View File

@@ -0,0 +1,212 @@
---
name: secrets-manager
type: agent
context_tier: warm
domain: [secrets, rotation, expiry, audit, sync, registry]
status: active
brain:
version: 1
type: metier
scope: kernel
owner: human
writer: human
lifecycle: permanent
read: trigger
triggers: [boot-audit, rotation, sync, secrets-audit, expiry]
export: false
ipc:
receives_from: [human, helloWorld, coach]
sends_to: [human]
zone_access: [kernel]
signals: [ESCALATE, ERROR]
---
# Agent : secrets-manager
> Dernière validation : 2026-03-19
> Domaine : Cycle de vie des secrets — expiry, rotation, audit, sync multi-machine
> **Type :** Métier — ADR-040. Complète le trio guardian (surveillance) + injector (transport).
---
## boot-summary
Gestionnaire du cycle de vie. Lit le registre `secrets.yml` (metadata, jamais les valeurs).
Alerte sur les expirations, guide les rotations, audite la couverture multi-machine.
Ne lit jamais MYSECRETS — délègue la lecture à secrets-guardian/injector.
---
## Rôle
Troisième pilier du système secrets :
```
secrets-guardian → surveillance passive, détecte les violations (policier)
secrets-injector → injecte credentials dans les subagents (coursier)
secrets-manager → cycle de vie : expiry, rotation, audit, sync (gestionnaire)
```
Le manager ne touche jamais aux valeurs. Il travaille exclusivement sur le registre
`~/Dev/BrainSecrets/secrets.yml` — metadata structurée (scope, expiry, machines, rotated_at).
---
## Activation
```
secrets-manager, audit
secrets-manager, quels secrets expirent bientôt ?
secrets-manager, rotation <KEY>
secrets-manager, sync status
secrets-manager, quels secrets manquent sur laptop ?
```
**Auto-trigger au boot** (via helloWorld, silencieux si tout est propre) :
- Si secrets.yml existe → audit rapide expiry (< 30j) → alerte 1 ligne si besoin
- Si secrets.yml absent → silence (ADR-040 pas encore déployé sur cette machine)
---
## Sources à charger
| Fichier | Pourquoi |
|---------|----------|
| `~/Dev/BrainSecrets/secrets.yml` | Registre metadata — source unique de vérité |
| `brain-compose.local.yml` | Machine courante (pour filtrer `machines[]`) |
## Sources conditionnelles
| Trigger | Fichier | Pourquoi |
|---------|---------|----------|
| Audit complet | `scripts/brain-secrets-sync.sh` | Commandes disponibles |
| Projet identifié | `projets/<projet>.md ## BYOKS` | Secrets requis par projet |
---
## Protocole — Audit
```
1. Lire secrets.yml → parser tous les secrets
2. Pour chaque secret :
a. expires_at < today → 🔴 EXPIRÉ — rotation immédiate requise
b. expires_at < today + 30j → 🟡 EXPIRE BIENTÔT — planifier rotation
c. rotated_at > 180j → 🟡 ROTATION RECOMMANDÉE (hygiène)
d. machines[] ne contient pas machine courante → ⚠️ PAS SUR CETTE MACHINE
e. required: true + absent MYSECRETS local → 🔴 MANQUANT
3. Output condensé :
"🔐 Audit secrets — N secrets, X à rotater, Y expirent dans 30j, Z manquants."
4. Si tout est propre → silence total (zéro output)
```
---
## Protocole — Rotation guidée
```
Trigger : "secrets-manager, rotation <KEY>" ou alerte expiry
1. IDENTIFY → lire secrets.yml pour <KEY> (scope, machines, expires_at)
2. GENERATE → proposer la commande de génération (openssl, uuidgen, etc.)
⚠️ JAMAIS afficher la valeur — pipe direct vers MYSECRETS
3. PROPAGATE → lister les machines concernées (machines[])
proposer : "brain-secrets-sync.sh sync <peer>" pour chaque
4. REGISTRY → mettre à jour secrets.yml :
rotated_at: <today>
expires_at: <today + durée standard du scope>
5. CONFIRM → "✅ <KEY> rotaté — propagé sur N machines — registre mis à jour."
```
**Pattern de génération sécurisé (rappel) :**
```bash
# ✅ Générer + écrire sans afficher
new_val=$(openssl rand -hex 32)
sed -i "s/^OLD_KEY=.*/OLD_KEY=$new_val/" ~/Dev/BrainSecrets/MYSECRETS
unset new_val
# Confirmer : "✅ OLD_KEY rotaté (32 bytes hex) — valeur non affichée."
```
---
## Protocole — Sync multi-machine
```
Trigger : "secrets-manager, sync status" ou boot audit détecte manquants
1. STATUS → bash brain-secrets-sync.sh status
→ affiche les clés présentes/manquantes (pas les valeurs)
2. GUIDE → "Secrets manquants sur <machine> : KEY1, KEY2
→ brain-secrets-sync.sh sync <peer>"
3. GATE → l'humain lance la commande — jamais automatique
4. VERIFY → après sync, re-lire et confirmer couverture
```
---
## Protocole — Audit mensuel
```
Trigger : invocation explicite "secrets-manager, audit complet"
1. Lire secrets.yml complet
2. Pour chaque secret → check expiry + rotation + machines + required
3. Croiser avec BYOKS des projets actifs (focus.md → projets/*.md)
4. Détecter les secrets orphelins (dans MYSECRETS mais plus dans aucun BYOKS)
5. Output :
"🔐 Audit mensuel — N secrets total
🔴 Expirés : ...
🟡 Rotation due : ...
⚠️ Orphelins (aucun projet actif) : ...
✅ Couverts : N/N machines"
```
---
## Ce qu'il ne fait PAS
```
❌ Lire MYSECRETS (valeurs) — JAMAIS, délègue à guardian/injector
❌ Afficher des valeurs dans le chat — JAMAIS
❌ Sync automatique — toujours gate humain
❌ Stocker quoi que ce soit hors secrets.yml
❌ Prendre des décisions de rotation sans confirmation humaine
❌ Modifier MYSECRETS sans commande Bash silencieuse (même pattern que guardian)
```
---
## Composition
| Avec | Pour quoi |
|------|-----------|
| `secrets-guardian` | Surveillance runtime — manager gère le cycle, guardian détecte les violations |
| `secrets-injector` | Transport vers subagents — manager gère l'inventaire, injector livre |
| `coach` | Peut invoquer l'audit au boot si ratio secrets/sessions le justifie |
| `helloWorld` | Auto-audit silencieux au boot (1 ligne si alerte, sinon silence) |
---
## Anti-hallucination
- Ne jamais supposer qu'un secret existe sans avoir lu secrets.yml
- Ne jamais inventer une date d'expiration — lire le registre
- Si secrets.yml absent : "Registre secrets.yml introuvable — ADR-040 non déployé sur cette machine."
- Si MYSECRETS absent : déléguer à secrets-guardian (son domaine)
---
## Cycle de vie
| État | Condition | Action |
|------|-----------|--------|
| **Actif** | secrets.yml existe | Audit, rotation, sync |
| **Silencieux** | secrets.yml absent | Ne s'active pas — pas d'erreur |
| **Retraité** | Vault externe adopté | Réévaluer le périmètre |
---
## Changelog
| Date | Changement |
|------|------------|
| 2026-03-19 | Création — ADR-040 implémentation. Trio complet : guardian + injector + manager |

View File

@@ -2,6 +2,22 @@
name: spec-scribe
type: scribe
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [spec, specification, ratification]
export: false
ipc:
receives_from: [human, orchestrator]
sends_to: [human]
zone_access: [personal, project]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : spec-scribe

View File

@@ -3,6 +3,21 @@ name: storyteller
type: agent
context_tier: warm
status: active
brain:
version: 1
type: metier
scope: personal
owner: human
writer: human
lifecycle: evolving
read: trigger
triggers: [storyteller, contenu, script, reddit]
export: false
ipc:
receives_from: [human, content-orchestrator]
sends_to: [human]
zone_access: [personal]
signals: [SPAWN, RETURN]
---
# Agent : storyteller

View File

@@ -2,8 +2,23 @@
name: supervisor
type: agent
context_tier: cold
# cold — daemon VPS, pas agent de session. hot domain: [VPS] à activer quand session-orchestrator supporte les domaines.
# cold — invocation manuelle uniquement. Pas auto-détecté sur domaine.
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [supervisor, dual-agent, checkpoint]
export: true
ipc:
receives_from: [human, orchestrator]
sends_to: [human, orchestrator]
zone_access: [kernel, project]
signals: [SPAWN, RETURN, CHECKPOINT, ESCALATE, HANDOFF]
---
# Agent : supervisor
@@ -212,9 +227,9 @@ pas seulement à la création.
Fermeture minimale valide :
```
git -C ~/Dev/Docs add BRAIN-INDEX.md
git -C ~/Dev/Docs commit -m "bsi: close claim <sess-id>"
git -C ~/Dev/Docs push
git -C $BRAIN_ROOT add BRAIN-INDEX.md
git -C $BRAIN_ROOT commit -m "bsi: close claim <sess-id>"
git -C $BRAIN_ROOT push
```
Le coach-scribe (bilan pédagogique) est **optionnel** à la fermeture — utile
@@ -400,3 +415,4 @@ Setup bot : `bash brain/scripts/install-brain-bot.sh` (sur le VPS)
| 2026-03-14 | Bot webhook — brain-bot.py, 4 commandes (/help /status /sessions /focus), dual-canal Telegram |
| 2026-03-14 | Patterns réels v1 — 7 protocoles issus du sprint dual-agent OriginsDigital : planification, routing questions, parallèle, décision scale-appropriée, CHECKPOINT, fermeture minimale, shunting |
| 2026-03-15 | Patterns v2 — 3 gaps comblés (Shadow Audit Sprint 3) : intel brute→actions implicites, cross-diff contrats avant CHECKPOINT, close order enforcement |
| 2026-03-18 | Review guidée — HANDOFF ajouté aux signals IPC + path ~/Dev/Docs → $BRAIN_ROOT (Pattern 6) + commentaire context_tier mis à jour |

View File

@@ -3,6 +3,21 @@ name: tech-lead
type: agent
context_tier: warm
status: active
brain:
version: 1
type: protocol
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [tech-lead, gate, sprint, architecture]
export: true
ipc:
receives_from: [orchestrator, context-broker]
sends_to: [orchestrator, human, scribe, toolkit-scribe]
zone_access: [kernel, project]
signals: [SPAWN, RETURN, ESCALATE]
---
# Agent : tech-lead
@@ -363,3 +378,4 @@ INTEGRATOR → merge + push + handoff
| 2026-03-14 | Patch 1 — KPIs (5 métriques), feedback loop integrator→tech-lead, auto-calibration protocol, règle "patcher tôt" |
| 2026-03-14 | Patch 2 — KPIs split Tier 1 (mesurables git) / Tier 2 (désactivés sans sink) — honnêteté sur ce qui est réellement mesurable |
| 2026-03-14 | Patch 3 — Permissions d'écriture explicites, cosign convention, zéro écriture brain/ directe |
| 2026-03-18 | Review guidée — sends_to IPC complété (scribe + toolkit-scribe) + handoffs/feedback-tech-lead-_template.md créé (Tier 2 KPIs débloqués) |

View File

@@ -13,6 +13,11 @@ brain:
read: trigger
triggers: [boot, post-compaction]
export: true
ipc:
receives_from: [human, helloWorld]
sends_to: [human]
zone_access: [kernel]
signals: [RETURN, CHECKPOINT]
---
# Agent : time-anchor

View File

@@ -3,6 +3,21 @@ name: toolkit-scribe
type: agent
context_tier: warm
status: active
brain:
version: 1
type: scribe
scope: kernel
owner: human
writer: human
lifecycle: stable
read: trigger
triggers: [toolkit, patterns, toolkit-scribe]
export: true
ipc:
receives_from: [orchestrator, scribe, human]
sends_to: [scribe]
zone_access: [project, kernel]
signals: [SPAWN, RETURN]
---
# Agent : toolkit-scribe

View File

@@ -2,7 +2,7 @@
# Versionné dans le kernel. Schema + feature flags + registre agents.
# Géré par l'agent brain-compose — ne pas éditer manuellement.
version: "0.9.0"
version: "0.8.0"
# ---
# Ownership — kerneluser
@@ -21,7 +21,7 @@ identityShow: on # conséquence de kerneluser: true — présence visuelle co
# Absent ou null → tier: free (jamais d'erreur, jamais de blocage)
# Format prod : bk_live_<32chars>
# Format dev : bk_test_<32chars> (tier: free forcé côté serveur, toujours valide)
# Validation : key-guardian au boot → lit local.yml → valide → écrit feature_set dans local.yml
# Validation : key-guardian.sh au boot → lit local.yml → valide → écrit feature_set dans local.yml
# ---
brain_api_key: null # toujours null ici — clé réelle dans brain-compose.local.yml
@@ -31,10 +31,10 @@ brain_api_key: null # toujours null ici — clé réelle dans brain-compose.l
# Structure contractuelle : ne pas modifier manuellement
# ---
feature_set_schema:
tier: free # free | featured | pro | full
tier: free # free | pro | full
agents: [] # liste des agents autorisés ([] = feature_set.free)
contexts: [] # manifests BHP autorisés ([] = accès libre sur free)
distillation: false # true = brain-engine distillation locale autorisée (featured+)
distillation: false # true = brain-engine distillation locale autorisée (full only)
catalog_version: "1.0.0" # version du CATALOG.yml agents — sync brain-store
last_validated_at: null # ISO 8601 — dernière validation réussie
expires_at: null # ISO 8601 — expiration clé (null = pas d'expiration fixe)
@@ -194,12 +194,12 @@ modes:
contexte: false
reference: read
personnel: false
brain_write: false
brain_write: false # pas d'écriture brain/ — uniquement le repo projet
forge: false
scope_lock: true
zone_lock: project
scope_lock: true # BLOQUÉ hors du scope déclaré dans le claim
zone_lock: project # zone:kernel → BLOCKED_ON immédiat, pas de négociation
circuit_breaker:
max_consecutive_fails: 3
max_consecutive_fails: 3 # 3 échecs → arrêt + signal BLOCKED_ON vers pilote
on_trigger: "signal → BLOCKED_ON pilote"
agents: [code-review, security, testing, debug, vps, ci-cd, pm2, migration]
behavior: |
@@ -250,14 +250,13 @@ detectmode:
# ---
# Feature sets — contrôlent les agents invocables par instance
# Les agents "bloqués" existent dans le kernel, brain-compose contrôle l'accès.
# Chaîne : free → featured → pro → full
# ---
feature_sets:
free:
description: "Agents fondamentaux — exploration et maintenance brain"
coach_level: boot # coach-boot.md — présence légère
coach_level: boot # coach-boot.md — présence légère, speech protocol sans contexte accumulé
sessions:
- navigate
- work
@@ -267,7 +266,7 @@ feature_sets:
- handoff
agents:
- coach-boot
- brain-guardian
- brain-guardian # auto-méfiance structurelle — session-brain
- scribe
- todo-scribe
- debug
@@ -286,7 +285,7 @@ feature_sets:
featured:
description: "Progression personnelle — RAG + distillation pour apprendre avec un brain qui connaît l'utilisateur"
extends: free
coach_level: full # coach.md complet — proposition de valeur centrale
coach_level: full # coach.md complet — c'est la proposition de valeur centrale
distillation: true # RAG actif — le brain apprend et se souvient
sessions:
extends: free
@@ -342,10 +341,10 @@ feature_sets:
full:
description: "Accès complet — owner, usage personnel sans restriction + distillation"
extends: pro
coach_level: L2 # coach.md + BACT + milestones long terme
coach_level: L2 # coach.md + BACT + milestones long terme + progression accumulée
sessions: "*" # inclut kernel + edit-brain — owner uniquement
distillation: true
agents: "*"
agents: "*" # BACT, SYMSEC, ambient, phi-3-mini
# ---
# Changelog — semver
@@ -362,25 +361,34 @@ changelog:
notes: "BSI (BRAIN-INDEX.md), brain_name, brain-template, aside, brainstorm, brain-compose up"
- version: "0.3.0"
date: "2026-03-14"
notes: "orchestrator-scribe (free), brain-compose+config-scribe (pro), CHECKPOINT signal"
notes: "orchestrator-scribe (free), brain-compose+config-scribe (pro), CHECKPOINT signal, session-as-identity, orchestration-patterns"
- version: "0.4.0"
date: "2026-03-14"
notes: "Système de modes — 11 modes, permissions BSI par mode, detectmode"
notes: "Système de modes — 11 modes, permissions BSI par mode, detectmode, toolkit-only autonome avec docs_fetch"
- version: "0.5.0"
date: "2026-03-14"
notes: "Multi-sessions BSI v1.2 — CHECKPOINT/HANDOFF, brain-bot Telegram, workspace spec v1.0"
notes: "Multi-sessions BSI v1.2 — CHECKPOINT/HANDOFF signals + handoff files ; brain-watch-vps daemon (stale TTL check, Telegram notifications) ; brain-bot Telegram webhook (/status /sessions /focus /help) ; workspace spec v1.0 (ram.md log.md feedback.md) ; supervisor patterns v1 (7 protocoles) ; statusline session-role ; secrets-guardian recovery protocol ; BLOCKED_ON false-positive fix"
- version: "0.5.1"
date: "2026-03-14"
notes: "Métabolisme v1 — mode conserve, metabolism-scribe, metabolism-spec"
notes: "Métabolisme v1 — mode conserve, metabolism-scribe, metabolism-spec, progression/metabolism/, helloWorld briefing métabolisme"
- version: "0.6.0"
date: "2026-03-15"
notes: "Constitution v1.1.0 — North Star + invariants autonomie"
notes: "Constitution v1.1.0 — Section 9 North Star + invariants autonomie + auto-amélioration (ADR-011) ; wiki/concepts.md fondamentaux brain V2 ; brain-engine vision north star"
- version: "0.7.0"
date: "2026-03-16"
notes: "BSI-v3 fondations — tiered-close, zone-aware claims, kerneluser ancré"
notes: "BSI-v3 fondations — tiered-close, zone-aware claims (ADR-014), result contract, exit triggers ; kerneluser: true ancré kernel ; KERNEL.md délégation human-only phase actuelle"
- version: "0.8.0"
date: "2026-03-17"
notes: "Brain API Key Phase 1 — brain_api_key optionnel, feature_set_schema contractuel, tiers free/pro/full"
notes: "Brain API Key Phase 1 — brain_api_key field (optionnel), feature_set_schema contractuel, tiers free/pro/full ; cache feature_set dans brain-compose.local.yml"
- version: "0.8.1"
date: "2026-03-17"
notes: "brain-store CATALOG — agents/CATALOG.yml source de vérité par tier (free/pro/owner) ; GET /agents filtré par tier ; catalog_version dans feature_set_schema"
- version: "0.9.0"
date: "2026-03-20"
notes: "Tier featured ajouté (RAG + coaching complet), sessions par tier, coach_level par tier, identityShow, docs/ 14 pages, BHP Phase 2 (boot-summary/detail 16 agents)"
date: "2026-03-17"
notes: "Feature sets v2 — coach_level par tier (boot/full/L2), sessions disponibles par tier, distillation flag owner, pattern-scribe + audit + time-anchor en free"
- version: "0.9.1"
date: "2026-03-18"
notes: "identityShow ancré — conséquence directe de kerneluser (on=owner/off=client) ; G-4 migration session-infra/capital/urgence vers format L0/L1/L2/L3 ; session-projet retiré (alias → session-work)"
- version: "0.9.2"
date: "2026-03-18"
notes: "Tier featured ajouté (RAG + distillation progression, non-dev) ; session-kernel + session-edit-brain → full tier uniquement (owner) ; brain-guardian ajouté en free ; chaîne tiers : free → featured → pro → full"

72
scripts/brain-db-backup.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# brain-db-backup.sh — Backup journalier brain.db → repo git dédié
# Usage: bash scripts/brain-db-backup.sh [backup_dir]
# Cron: 0 4 * * * bash ~/Dev/Brain/scripts/brain-db-backup.sh
#
# Stratégie :
# 1. SQLite vacuum into backup (copie propre, pas de lock stale)
# 2. Commit daté dans le repo backup
# 3. Push Gitea (silencieux si remote absent)
# 4. Rétention : 30 fichiers max (rotation automatique)
set -euo pipefail
BRAIN_DB="${BRAIN_DB:-$HOME/Dev/Brain/brain.db}"
BACKUP_DIR="${1:-$HOME/Dev/Brain/brain-db-backup}"
RETENTION=30
DATE=$(date '+%Y-%m-%d')
BACKUP_FILE="brain-${DATE}.db"
# --- Vérifications ---
if [[ ! -f "$BRAIN_DB" ]]; then
echo "❌ brain.db introuvable : $BRAIN_DB" >&2
exit 1
fi
# --- Init repo backup si premier run ---
if [[ ! -d "$BACKUP_DIR" ]]; then
mkdir -p "$BACKUP_DIR"
git -C "$BACKUP_DIR" init
echo "# brain-db-backup" > "$BACKUP_DIR/README.md"
echo "Backups journaliers de brain.db — généré par brain-db-backup.sh" >> "$BACKUP_DIR/README.md"
echo "" >> "$BACKUP_DIR/README.md"
echo "*.db binary" > "$BACKUP_DIR/.gitattributes"
git -C "$BACKUP_DIR" add .
git -C "$BACKUP_DIR" commit -m "init: brain-db-backup repo"
echo "✅ Repo backup initialisé : $BACKUP_DIR"
fi
# --- Backup via SQLite vacuum (copie propre) ---
python3 -c "
import sqlite3, shutil, sys
src = '${BRAIN_DB}'
dst = '${BACKUP_DIR}/${BACKUP_FILE}'
conn = sqlite3.connect(src)
bkp = sqlite3.connect(dst)
conn.backup(bkp)
bkp.close()
conn.close()
print(f'✅ Backup : {dst}')
"
# --- Rotation : garder les N plus récents ---
cd "$BACKUP_DIR"
ls -1t brain-*.db 2>/dev/null | tail -n +$((RETENTION + 1)) | while read old; do
rm -f "$old"
echo "🗑 Rotation : $old supprimé"
done
# --- Commit ---
git -C "$BACKUP_DIR" add -A
if git -C "$BACKUP_DIR" diff --cached --quiet; then
echo " Aucun changement — brain.db identique au dernier backup"
exit 0
fi
git -C "$BACKUP_DIR" commit -m "backup: brain.db ${DATE}"
# --- Push (silencieux si pas de remote) ---
if git -C "$BACKUP_DIR" remote get-url origin &>/dev/null; then
git -C "$BACKUP_DIR" push -q && echo "✅ Push Gitea OK" || echo "⚠️ Push échoué (réseau ?)"
else
echo " Pas de remote — backup local uniquement. Ajouter : git -C $BACKUP_DIR remote add origin <url>"
fi

View File

@@ -49,16 +49,17 @@ if ! python3 -c "import sqlite3" 2>/dev/null; then
exit 1
fi
# --check : brain.db stale si plus vieux que le dernier commit touchant claims/ ou handoffs/
# --check : brain.db stale si plus vieux que le dernier commit touchant handoffs/ ou agents/
# Note: claims/ retiré (ADR-042 — brain.db est la source unique, plus de claims YAML)
if $CHECK_ONLY; then
if [[ ! -f "$DB_PATH" ]]; then
log "STALE: brain.db absent"
exit 2
fi
db_mtime=$(stat -c %Y "$DB_PATH" 2>/dev/null || echo 0)
last_commit_ts=$(git -C "$BRAIN_ROOT" log -1 --format="%ct" -- claims/ handoffs/ BRAIN-INDEX.md 2>/dev/null || echo 0)
last_commit_ts=$(git -C "$BRAIN_ROOT" log -1 --format="%ct" -- handoffs/ agents/ BRAIN-INDEX.md 2>/dev/null || echo 0)
if [[ "$last_commit_ts" -gt "$db_mtime" ]]; then
log "STALE: brain.db ($db_mtime) < dernier commit claims/handoffs ($last_commit_ts)"
log "STALE: brain.db ($db_mtime) < dernier commit handoffs/agents ($last_commit_ts)"
exit 2
fi
log "OK: brain.db à jour"

143
scripts/brain-dev.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# brain-dev.sh — Démarrage brain en mode dev local (laptop / offline)
# Usage : bash scripts/brain-dev.sh [--engine] [--ui]
# Sans arguments → démarre brain-engine (mock désactivé) + brain-ui
# --engine : démarre brain-engine localement sur :7700 (uvicorn)
# --ui : démarre brain-ui en dev (npm run dev)
# Sans aucun argument : démarre les deux (engine + ui)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BRAIN_UI="$BRAIN_ROOT/brain-ui"
ENGINE_PORT=7700
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ok() { echo -e "${GREEN}$1${NC}"; }
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
info() { echo -e " $1"; }
# ── Parse args ────────────────────────────────────────────────────────────────
START_ENGINE=false
START_UI=false
if [[ $# -eq 0 ]]; then
START_ENGINE=true
START_UI=true
fi
for arg in "$@"; do
case "$arg" in
--engine) START_ENGINE=true ;;
--ui) START_UI=true ;;
*)
echo "Usage: bash scripts/brain-dev.sh [--engine] [--ui]"
echo " --engine : démarre brain-engine sur :$ENGINE_PORT"
echo " --ui : démarre brain-ui en dev"
echo " (sans args) : démarre les deux"
exit 1
;;
esac
done
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ brain-dev.sh — mode dev local ║"
echo "╚══════════════════════════════════════════════╝"
echo ""
# ── Vérifications préalables ──────────────────────────────────────────────────
if $START_ENGINE; then
if ! command -v python3 &>/dev/null; then
warn "python3 non trouvé — impossible de démarrer brain-engine"
START_ENGINE=false
fi
if ! command -v uvicorn &>/dev/null && ! python3 -c "import uvicorn" 2>/dev/null; then
warn "uvicorn non installé — pip3 install uvicorn[standard]"
START_ENGINE=false
fi
fi
if $START_UI; then
if [[ ! -d "$BRAIN_UI" ]]; then
warn "brain-ui absent ($BRAIN_UI) — --ui ignoré"
START_UI=false
elif ! command -v npm &>/dev/null; then
warn "npm non trouvé — impossible de démarrer brain-ui"
START_UI=false
fi
fi
# ── Créer .env.local pour brain-ui ───────────────────────────────────────────
if [[ -d "$BRAIN_UI" ]]; then
if $START_ENGINE; then
# engine local disponible → pas de mock
cat > "$BRAIN_UI/.env.local" << 'EOF'
VITE_USE_MOCK=false
VITE_BRAIN_API=http://localhost:7700
EOF
ok "brain-ui/.env.local → engine local (:7700)"
else
# pas d'engine → mode mock
cat > "$BRAIN_UI/.env.local" << 'EOF'
VITE_USE_MOCK=true
VITE_BRAIN_API=
EOF
ok "brain-ui/.env.local → mode mock (pas de VPS requis)"
fi
fi
# ── Trap Ctrl+C → tuer les processus fils ────────────────────────────────────
PIDS=()
cleanup() {
echo ""
info "Arrêt en cours..."
for pid in "${PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
ok "Processus arrêtés proprement."
exit 0
}
trap cleanup INT TERM
# ── Démarrer brain-engine ─────────────────────────────────────────────────────
if $START_ENGINE; then
info "Démarrage brain-engine sur :$ENGINE_PORT..."
cd "$BRAIN_ROOT"
BRAIN_PORT=$ENGINE_PORT python3 -m uvicorn brain-engine.server:app \
--host 0.0.0.0 --port $ENGINE_PORT --reload 2>&1 | sed 's/^/[engine] /' &
PIDS+=($!)
ok "brain-engine démarré (PID ${PIDS[-1]})"
fi
# ── Démarrer brain-ui ─────────────────────────────────────────────────────────
if $START_UI; then
info "Démarrage brain-ui (npm run dev)..."
cd "$BRAIN_UI"
npm run dev 2>&1 | sed 's/^/[ui] /' &
PIDS+=($!)
ok "brain-ui démarré (PID ${PIDS[-1]})"
fi
if [[ ${#PIDS[@]} -eq 0 ]]; then
warn "Aucun processus démarré — vérifier les prérequis ci-dessus."
exit 1
fi
echo ""
if $START_ENGINE; then
info "brain-engine : http://localhost:$ENGINE_PORT"
info " /health : http://localhost:$ENGINE_PORT/health"
fi
if $START_UI; then
info "brain-ui : http://localhost:5173 (port Vite par défaut)"
fi
echo ""
info "Ctrl+C pour arrêter."
echo ""
# Attendre les processus fils
wait

View File

@@ -1,127 +1,29 @@
#!/usr/bin/env bash
# brain-index-regen.sh — Régénère la table ## Claims dans BRAIN-INDEX.md
# depuis les fichiers claims/sess-*.yml (BSI v3 — source unique de vérité)
#
# Gère les formats :
# v1 : name: + opened: + status:
# v2 : sess_id: + opened_at: + status:
# v3 : + satellite_type + zone (inféré) + result.status
# brain-index-regen.sh — Vérifie l'état des claims dans brain.db
# Post-ADR-042 : ne modifie plus BRAIN-INDEX.md (claims = brain.db source unique)
# Conservé pour compatibilité — les appels existants ne cassent pas.
#
# Usage : bash scripts/brain-index-regen.sh
# Appelé par : session-orchestrator (close sequence step 5)
# helloWorld (boot claim open)
#
# Anti-drift : lecture seule sur claims/*.yml — écriture uniquement sur BRAIN-INDEX.md ## Claims
# Sécurité : aucun secret dans les claims (garanti par secrets-guardian)
# Output : 1 ligne résumé (open/total)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLAIMS_DIR="$BRAIN_ROOT/claims"
INDEX_FILE="$BRAIN_ROOT/BRAIN-INDEX.md"
DB_PATH="$BRAIN_ROOT/brain.db"
if [[ ! -f "$INDEX_FILE" ]]; then
echo "❌ BRAIN-INDEX.md introuvable — chemin : $INDEX_FILE"
if [[ ! -f "$DB_PATH" ]]; then
echo "⚠️ brain.db absent — lancer: bash scripts/bsi-claim.sh init"
exit 1
fi
if [[ ! -d "$CLAIMS_DIR" ]]; then
echo "❌ claims/ introuvable — chemin : $CLAIMS_DIR"
exit 1
fi
# ── Parser tous les claims via Python (gère YAML multi-format proprement) ────
python3 - "$CLAIMS_DIR" "$INDEX_FILE" <<'PYEOF'
import sys, os, re
claims_dir = sys.argv[1]
index_path = sys.argv[2]
rows = []
open_count = 0
for filename in sorted(os.listdir(claims_dir)):
if not filename.startswith('sess-') or not filename.endswith('.yml'):
continue
filepath = os.path.join(claims_dir, filename)
with open(filepath, 'r') as f:
content = f.read()
def extract(pattern, text, default='—'):
m = re.search(pattern, text, re.MULTILINE)
if m:
return m.group(1).strip().strip('"\'')
return default
# Gère v1 (name:) et v2 (sess_id:)
def extract_first(*patterns):
for p in patterns:
m = re.search(p, content, re.MULTILINE)
if m:
return m.group(1).strip().strip('"\'')
return '—'
sess_id = extract_first(r'^sess_id:\s*(.+)', r'^name:\s*(sess-.+)')
scope = extract_first(r'^scope:\s*(.+)')
status = extract_first(r'^status:\s*(.+)')
opened = extract_first(r'^opened_at:\s*(.+)', r'^opened:\s*(.+)')
sat_type = extract_first(r'^satellite_type:\s*(.+)')
theme_br = extract_first(r'^theme_branch:\s*(.+)')
# Inférer zone depuis scope (BSI v3 — ADR-014)
KERNEL_SCOPES = ['agents/', 'profil/', 'scripts/', 'KERNEL.md',
'brain-constitution.md', 'brain-compose.yml']
PERSONAL_SCOPES = ['profil/capital', 'profil/objectifs', 'progression/', 'MYSECRETS']
zone = 'project'
for ks in KERNEL_SCOPES:
if ks in scope:
zone = 'kernel'
break
for ps in PERSONAL_SCOPES:
if ps in scope:
zone = 'personal'
break
# Résultat du close si disponible
result_status = extract(r'^\s+status:\s*(.+)', content)
if result_status in ('open', 'closed', 'stale', '—'):
result_status = '—'
# Indicateur satellite_type
type_display = sat_type if sat_type != '—' else '—'
theme_display = theme_br.replace('theme/', '') if theme_br != '—' else '—'
rows.append(f"| {sess_id} | {scope} | {status} | {opened} | {type_display} | {zone} | {result_status} |")
if status == 'open':
open_count += 1
table_rows = "\n".join(rows)
comment = ("<!-- ⚠️ TABLE GÉNÉRÉE — ne pas éditer manuellement.\n"
" Régénérée par : scripts/brain-index-regen.sh\n"
" Appelée par : session-orchestrator (close) + helloWorld (boot)\n"
" Source unique : claims/sess-*.yml (BSI v3) -->\n")
new_table = (f"{comment}Sessions actives à ce jour :\n\n"
f"| sess_id | scope | status | opened_at | type | zone | result |\n"
f"|---------|-------|--------|-----------|------|------|--------|\n"
f"{table_rows}")
# Lire BRAIN-INDEX.md
with open(index_path, 'r') as f:
content = f.read()
# Remplacer depuis le commentaire HTML (ou "Sessions actives") jusqu'au prochain "---"
# Deux patterns : avec ou sans commentaire généré
pattern = r'(?:<!--.*?-->\s*\n)?Sessions actives à ce jour :.*?(?=\n---)'
if not re.search(pattern, content, flags=re.DOTALL):
print("⚠️ Pattern claims non trouvé dans BRAIN-INDEX.md — pas de modification")
sys.exit(0)
new_content = re.sub(pattern, new_table, content, flags=re.DOTALL)
with open(index_path, 'w') as f:
f.write(new_content)
print(f"✅ BRAIN-INDEX.md régénéré — {open_count} claim(s) open / {len(rows)} total")
PYEOF
python3 -c "
import sqlite3, sys
conn = sqlite3.connect(sys.argv[1])
try:
total = conn.execute('SELECT COUNT(*) FROM claims').fetchone()[0]
opens = conn.execute(\"SELECT COUNT(*) FROM claims WHERE status='open'\").fetchone()[0]
print(f'✅ brain.db — {opens} claim(s) open / {total} total')
except Exception:
print('⚠️ Table claims absente — lancer: bash scripts/bsi-claim.sh init')
conn.close()
" "$DB_PATH"

1087
scripts/brain-launch.sh Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""brain-pair client — laptop/new machine side (ADR-041)
Scans LAN for a brain-pair server, sends code, receives config.
Usage: python3 brain-pair-client.py <brain_root> <code>
"""
import json
import os
import socket
import sys
import time
BRAIN_ROOT = sys.argv[1]
CODE = sys.argv[2]
BROADCAST_PORT = 7711 # UDP listen port
SCAN_TIMEOUT = 30 # seconds to scan for server
def get_ssh_pubkey():
"""Read the local SSH public key."""
for name in ["id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub"]:
path = os.path.expanduser(f"~/.ssh/{name}")
if os.path.exists(path):
with open(path) as f:
return f.read().strip()
return ""
def get_local_machine():
"""Read machine name from brain-compose.local.yml."""
try:
import yaml
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
with open(compose_path) as f:
compose = yaml.safe_load(f)
return compose.get("machine", socket.gethostname())
except Exception:
return socket.gethostname()
def scan_for_server():
"""Listen for UDP broadcast from brain-pair server."""
print(f"🔍 Scan du LAN pour brain-pair server ({SCAN_TIMEOUT}s)...")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(2)
sock.bind(("0.0.0.0", BROADCAST_PORT))
start = time.time()
while time.time() - start < SCAN_TIMEOUT:
try:
data, addr = sock.recvfrom(1024)
msg = json.loads(data.decode())
if msg.get("type") == "brain-pair":
server_ip = msg["ip"]
server_port = msg["port"]
print(f" ✅ Serveur trouvé : {server_ip}:{server_port}")
sock.close()
return server_ip, server_port
except socket.timeout:
continue
except Exception:
continue
sock.close()
return None, None
def do_handshake(server_ip, server_port, code, machine, ssh_pubkey):
"""Connect to server, send code, receive config."""
print(f"🤝 Handshake avec {server_ip}:{server_port}...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(15)
sock.connect((server_ip, server_port))
request = json.dumps({
"code": code,
"machine": machine,
"ssh_pubkey": ssh_pubkey,
})
sock.sendall(request.encode())
data = sock.recv(8192).decode()
sock.close()
response = json.loads(data)
return response
def apply_config(response, server_ip):
"""Apply received config to local brain-compose.local.yml."""
import yaml
if response.get("status") != "ok":
print(f"❌ Pairing refusé : {response.get('msg', 'unknown error')}")
return False
server_machine = response["machine"]
api_key = response.get("api_key")
engine_port = response.get("brain_engine_port", 7700)
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
# Read or create compose
if os.path.exists(compose_path):
with open(compose_path) as f:
compose = yaml.safe_load(f) or {}
else:
compose = {
"machine": get_local_machine(),
"instances": {},
"kernel_path": BRAIN_ROOT,
}
# Add peer
if "peers" not in compose:
compose["peers"] = {}
compose["peers"][server_machine] = {
"url": f"http://{server_ip}:{engine_port}",
"active": True,
}
# Inject API key if provided
if api_key:
instances = compose.get("instances", {})
# Find or create active instance
active_found = False
for name, inst in instances.items():
if inst.get("active"):
inst["brain_api_key"] = api_key
active_found = True
break
if not active_found:
machine = compose.get("machine", "unknown")
instances[machine] = {
"active": True,
"brain_name": machine,
"brain_api_key": api_key,
"path": BRAIN_ROOT,
}
compose["instances"] = instances
with open(compose_path, "w") as f:
yaml.dump(compose, f, default_flow_style=False, allow_unicode=True)
print(f" ✅ Peer {server_machine} ({server_ip}) ajouté")
if api_key:
print(f" ✅ Brain API Key injectée dans brain-compose.local.yml")
print(f" ✅ brain-compose.local.yml mis à jour")
# Add server host to known_hosts
os.system(f"ssh-keyscan -H {server_ip} >> ~/.ssh/known_hosts 2>/dev/null")
print(f" ✅ Fingerprint {server_ip} ajoutée à known_hosts")
return True
def main():
machine = get_local_machine()
ssh_pubkey = get_ssh_pubkey()
print(f"🔗 brain-pair join — machine : {machine}")
if not ssh_pubkey:
print(f"⚠️ Aucune clé SSH trouvée — ssh-keygen recommandé")
print()
# Scan LAN
server_ip, server_port = scan_for_server()
if not server_ip:
print(f"❌ Aucun serveur brain-pair trouvé sur le LAN")
print(f" Vérifier : brain-pair.sh start sur la machine source")
sys.exit(1)
# Handshake
response = do_handshake(server_ip, server_port, CODE, machine, ssh_pubkey)
# Apply config
success = apply_config(response, server_ip)
if success:
print(f"\n✅ Pairing terminé !")
print(f" Vérifier : bash scripts/bsi-query.sh peers")
print(f" Secrets : bash scripts/brain-secrets-sync.sh status")
else:
print(f"\n❌ Pairing échoué")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""brain-pair server — desktop side (ADR-041)
Generates a 6-digit code, broadcasts on LAN, waits for client handshake.
Exchanges: API key, SSH pubkey, peer config. Never MYSECRETS.
Usage: python3 brain-pair-server.py <brain_root>
"""
import json
import os
import random
import socket
import sys
import threading
import time
import subprocess
BRAIN_ROOT = sys.argv[1]
PAIR_PORT = 7710 # TCP handshake port
BROADCAST_PORT = 7711 # UDP broadcast port
CODE_TTL = 120 # seconds
TEST_CODE = os.environ.get("BRAIN_PAIR_TEST_CODE") # force code for testing
def get_machine_info():
"""Read local machine config."""
import yaml
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
with open(compose_path) as f:
compose = yaml.safe_load(f)
machine = compose.get("machine", "unknown")
local_ip = get_local_ip()
# Read brain API key
instances = compose.get("instances", {})
api_key = None
for name, inst in instances.items():
if inst.get("active"):
api_key = inst.get("brain_api_key")
break
return {
"machine": machine,
"ip": local_ip,
"brain_engine_port": 7700,
"api_key": api_key,
}
def get_local_ip():
"""Get the LAN IP of this machine."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
finally:
s.close()
def broadcast_presence(code, stop_event):
"""Broadcast pairing availability on LAN via UDP."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(1)
local_ip = get_local_ip()
msg = json.dumps({
"type": "brain-pair",
"ip": local_ip,
"port": PAIR_PORT,
}).encode()
while not stop_event.is_set():
try:
sock.sendto(msg, ("<broadcast>", BROADCAST_PORT))
except OSError:
pass
time.sleep(1)
sock.close()
def handle_client(conn, addr, code, machine_info):
"""Handle a pairing handshake from a client."""
conn.settimeout(30)
try:
data = conn.recv(4096).decode()
request = json.loads(data)
# Verify code
if request.get("code") != code:
conn.sendall(json.dumps({"status": "error", "msg": "Invalid code"}).encode())
print(f"❌ Code invalide depuis {addr[0]}")
return False
client_machine = request.get("machine", "unknown")
client_ssh_pubkey = request.get("ssh_pubkey", "")
print(f"✅ Code vérifié — pairing avec {client_machine} ({addr[0]})")
# Build response (what we send to the client)
response = {
"status": "ok",
"machine": machine_info["machine"],
"ip": machine_info["ip"],
"brain_engine_port": machine_info["brain_engine_port"],
"api_key": machine_info["api_key"],
}
conn.sendall(json.dumps(response).encode())
# Add client SSH key to authorized_keys
if client_ssh_pubkey:
ak_path = os.path.expanduser("~/.ssh/authorized_keys")
comment = f" # brain-pair:{client_machine}"
key_line = client_ssh_pubkey.strip() + comment + "\n"
# Check if already present
existing = ""
if os.path.exists(ak_path):
with open(ak_path) as f:
existing = f.read()
if client_ssh_pubkey.strip().split()[1] not in existing:
with open(ak_path, "a") as f:
f.write(key_line)
print(f" ✅ Clé SSH de {client_machine} ajoutée à authorized_keys")
else:
print(f" Clé SSH de {client_machine} déjà présente")
# Add peer to brain-compose.local.yml
import yaml
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
with open(compose_path) as f:
compose = yaml.safe_load(f)
if "peers" not in compose:
compose["peers"] = {}
compose["peers"][client_machine] = {
"url": f"http://{addr[0]}:7700",
"active": True,
}
with open(compose_path, "w") as f:
yaml.dump(compose, f, default_flow_style=False, allow_unicode=True)
print(f" ✅ Peer {client_machine} ajouté à brain-compose.local.yml")
return True
except Exception as e:
print(f"❌ Erreur handshake : {e}")
return False
finally:
conn.close()
def main():
code = TEST_CODE or f"{random.randint(0, 999999):06d}"
machine_info = get_machine_info()
print(f"🔗 brain-pair — en attente de connexion")
print(f" Machine : {machine_info['machine']} ({machine_info['ip']})")
print(f"")
print(f" Code : {code}")
print(f"")
print(f" Sur l'autre machine : brain-pair.sh join {code}")
print(f" Expire dans {CODE_TTL}s...")
print()
# Start broadcast
stop_event = threading.Event()
broadcast_thread = threading.Thread(target=broadcast_presence, args=(code, stop_event), daemon=True)
broadcast_thread.start()
# Listen for TCP connection
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.settimeout(CODE_TTL)
server.bind(("0.0.0.0", PAIR_PORT))
server.listen(1)
try:
conn, addr = server.accept()
success = handle_client(conn, addr, code, machine_info)
if success:
print(f"\n✅ Pairing terminé avec succès !")
print(f" Vérifier : bash scripts/bsi-query.sh peers")
else:
print(f"\n❌ Pairing échoué")
except socket.timeout:
print(f"\n⏱ Code expiré ({CODE_TTL}s) — relancer brain-pair.sh start")
finally:
stop_event.set()
server.close()
if __name__ == "__main__":
main()

105
scripts/brain-pair.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# brain-pair.sh — Pairing multi-machine type Bluetooth (ADR-041)
#
# Usage :
# brain-pair.sh start → génère code, écoute sur le LAN
# brain-pair.sh join <code> → scan LAN, envoie code, reçoit config
# brain-pair.sh list → machines pairées (peers dans brain-compose.local.yml)
# brain-pair.sh revoke <machine> → supprime une machine
#
# Sécurité : code 6 chiffres valide 60s, LAN only, MYSECRETS jamais échangé
# Tier free : python3 stdlib uniquement
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CMD="${1:-help}"
shift || true
case "$CMD" in
start)
python3 "$BRAIN_ROOT/scripts/brain-pair-server.py" "$BRAIN_ROOT"
;;
join)
CODE="${1:-}"
if [[ -z "$CODE" ]]; then
echo "❌ Usage: brain-pair.sh join <code>"
exit 1
fi
python3 "$BRAIN_ROOT/scripts/brain-pair-client.py" "$BRAIN_ROOT" "$CODE"
;;
list)
python3 -c "
import yaml, sys
compose_path = '$BRAIN_ROOT/brain-compose.local.yml'
try:
with open(compose_path) as f:
c = yaml.safe_load(f)
peers = c.get('peers', {})
machine = c.get('machine', 'unknown')
print(f'Machine locale : {machine}')
print(f'Peers configurés : {len(peers)}\n')
for name, info in peers.items():
status = '✅ active' if info.get('active') else '⬜ inactive'
url = info.get('url', '—')
print(f' {name} — {url} — {status}')
if not peers:
print(' (aucun peer)')
except FileNotFoundError:
print('❌ brain-compose.local.yml absent')
"
;;
revoke)
MACHINE="${1:-}"
if [[ -z "$MACHINE" ]]; then
echo "❌ Usage: brain-pair.sh revoke <machine>"
exit 1
fi
python3 - "$BRAIN_ROOT" "$MACHINE" <<'PYEOF'
import yaml, sys, os, subprocess
brain_root = sys.argv[1]
machine = sys.argv[2]
compose_path = os.path.join(brain_root, "brain-compose.local.yml")
with open(compose_path) as f:
compose = yaml.safe_load(f)
peers = compose.get("peers", {})
if machine not in peers:
print(f"⚠️ Peer '{machine}' non trouvé")
sys.exit(1)
peer_url = peers[machine].get("url", "")
host = peer_url.replace("http://", "").replace("https://", "").split(":")[0]
# Retirer du compose
del peers[machine]
compose["peers"] = peers
with open(compose_path, "w") as f:
yaml.dump(compose, f, default_flow_style=False, allow_unicode=True)
# Retirer de authorized_keys (lignes contenant le nom de machine)
ak_path = os.path.expanduser("~/.ssh/authorized_keys")
if os.path.exists(ak_path):
with open(ak_path) as f:
lines = f.readlines()
filtered = [l for l in lines if machine not in l]
if len(filtered) < len(lines):
with open(ak_path, "w") as f:
f.writelines(filtered)
print(f"✅ Clé SSH de {machine} retirée de authorized_keys")
print(f"✅ Peer '{machine}' révoqué de brain-compose.local.yml")
PYEOF
;;
help|*)
echo "brain-pair.sh — Pairing multi-machine (ADR-041)"
echo ""
echo "Usage :"
echo " start → génère code 6 chiffres, écoute sur le LAN (60s)"
echo " join <code> → scan LAN, envoie code, reçoit config"
echo " list → machines pairées"
echo " revoke <machine> → supprime un peer"
;;
esac

310
scripts/brain-secrets-sync.sh Executable file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env bash
# brain-secrets-sync.sh — Registre secrets + sync SSH (ADR-040)
#
# Usage :
# brain-secrets-sync.sh status → compare registre vs MYSECRETS local
# brain-secrets-sync.sh audit → secrets expirés, rotation due, manquants
# brain-secrets-sync.sh sync <peer> → récupère les secrets manquants via SSH
# brain-secrets-sync.sh diff <peer> → compare clés locales vs peer (sans valeurs)
#
# Sécurité :
# - Jamais de valeur affichée — noms de clés uniquement
# - Transport via SSH (chiffré par construction)
# - Gate humain obligatoire avant toute sync
#
# Tier free : python3 + pyyaml (pip install pyyaml si absent)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SECRETS_DIR="$HOME/Dev/BrainSecrets"
REGISTRY="$SECRETS_DIR/secrets.yml"
MYSECRETS="$SECRETS_DIR/MYSECRETS"
COMPOSE_LOCAL="$BRAIN_ROOT/brain-compose.local.yml"
CMD="${1:-help}"
PEER="${2:-}"
# ── Vérifications ────────────────────────────────────────────
if [[ ! -f "$REGISTRY" ]]; then
echo "❌ Registre absent : $REGISTRY"
echo " → Créer avec le format ADR-040 (voir profil/decisions/040-*)"
exit 1
fi
if [[ ! -f "$MYSECRETS" ]]; then
echo "❌ MYSECRETS absent : $MYSECRETS"
exit 1
fi
# ── Commandes ────────────────────────────────────────────────
case "$CMD" in
status|audit|diff)
python3 - "$REGISTRY" "$MYSECRETS" "$COMPOSE_LOCAL" "$CMD" "$PEER" <<'PYEOF'
import sys, os
from datetime import datetime, date
registry_path = sys.argv[1]
mysecrets_path = sys.argv[2]
compose_path = sys.argv[3]
cmd = sys.argv[4]
peer = sys.argv[5] if len(sys.argv) > 5 else ""
# Parse YAML sans dépendance lourde (fallback si pyyaml absent)
try:
import yaml
with open(registry_path) as f:
registry = yaml.safe_load(f)
except ImportError:
# Fallback basique — parse les clés du registre
print("⚠️ pyyaml absent — install: pip install pyyaml")
print(" Fallback : comparaison clés MYSECRETS uniquement")
registry = None
# Parse MYSECRETS (KEY=VALUE)
local_keys = set()
with open(mysecrets_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key = line.split('=', 1)[0].strip()
if key:
local_keys.add(key)
# Détecter la machine courante
machine = "unknown"
if os.path.exists(compose_path):
try:
with open(compose_path) as f:
compose = yaml.safe_load(f) if registry else {}
machine = compose.get("machine", "unknown")
except Exception:
pass
if registry is None:
sys.exit(0)
secrets = registry.get("secrets", {})
if cmd == "status":
print(f"📋 Registre : {len(secrets)} secrets | Machine : {machine}")
print(f" MYSECRETS : {len(local_keys)} clés locales\n")
missing = []
present = []
other_machine = []
registry_keys = set(secrets.keys())
extra = local_keys - registry_keys # clés locales absentes du registre
for key, meta in secrets.items():
machines = meta.get("machines", [])
required = meta.get("required", False)
scope = meta.get("scope", "—")
if machine in machines or machine == "unknown":
if key in local_keys:
present.append(key)
else:
tag = "🔴 REQUIRED" if required else "⚪ optional"
missing.append(f" {tag} {key} (scope: {scope})")
elif key in local_keys:
other_machine.append(f" {key} → déclaré pour {machines}")
if missing:
print(f"❌ Manquants ({len(missing)}) :")
for m in missing:
print(m)
else:
print(f"✅ Tous les secrets requis pour {machine} sont présents")
if other_machine:
print(f"\n Clés présentes localement mais assignées à d'autres machines ({len(other_machine)}) :")
for o in other_machine:
print(o)
if extra:
print(f"\n⚠ Clés dans MYSECRETS absentes du registre ({len(extra)}) :")
for k in sorted(extra):
print(f" ? {k} → ajouter dans secrets.yml")
print(f"\n✅ {len(present)} clés présentes et déclarées")
elif cmd == "audit":
today = date.today()
issues = []
for key, meta in secrets.items():
expires = meta.get("expires_at")
rotated = meta.get("rotated_at")
required = meta.get("required", False)
if expires:
try:
exp_date = date.fromisoformat(str(expires))
days_left = (exp_date - today).days
if days_left < 0:
issues.append(f" 🔴 EXPIRÉ : {key} — expiré depuis {-days_left}j")
elif days_left < 30:
issues.append(f" 🟡 EXPIRE BIENTÔT : {key} — {days_left}j restants")
except ValueError:
pass
if rotated:
try:
rot_date = date.fromisoformat(str(rotated))
age = (today - rot_date).days
if age > 180 and required:
issues.append(f" 🟡 ROTATION DUE : {key} — dernière rotation il y a {age}j")
except ValueError:
pass
if issues:
print(f"🔍 Audit — {len(issues)} problème(s) :\n")
for i in issues:
print(i)
else:
print("✅ Audit clean — aucun secret expiré ou en attente de rotation")
# Stats par scope
scopes = {}
for key, meta in secrets.items():
s = meta.get("scope", "unknown")
scopes[s] = scopes.get(s, 0) + 1
print(f"\n📊 {len(secrets)} secrets répartis :")
for s, n in sorted(scopes.items()):
print(f" {s}: {n}")
elif cmd == "diff":
if not peer:
print("❌ Usage: brain-secrets-sync.sh diff <peer>")
print(" Ex: brain-secrets-sync.sh diff laptop")
sys.exit(1)
print(f"📋 Diff registre : {machine} vs {peer}\n")
local_expected = set()
peer_expected = set()
for key, meta in secrets.items():
machines = meta.get("machines", [])
if machine in machines:
local_expected.add(key)
if peer in machines:
peer_expected.add(key)
both = local_expected & peer_expected
only_local = local_expected - peer_expected
only_peer = peer_expected - local_expected
print(f" Communs : {len(both)}")
print(f" {machine} only : {len(only_local)}")
print(f" {peer} only : {len(only_peer)}")
if only_local:
print(f"\n Sur {machine} uniquement :")
for k in sorted(only_local):
print(f" {k}")
if only_peer:
print(f"\n Sur {peer} uniquement :")
for k in sorted(only_peer):
print(f" {k}")
PYEOF
;;
sync)
if [[ -z "$PEER" ]]; then
echo "❌ Usage: brain-secrets-sync.sh sync <peer>"
echo " Ex: brain-secrets-sync.sh sync desktop"
echo ""
echo " Peers connus (brain-compose.local.yml) :"
grep -A2 "peers:" "$COMPOSE_LOCAL" 2>/dev/null | grep -E "^\s+\w+:" | sed 's/://;s/^ / /' || echo " (aucun peer configuré)"
exit 1
fi
# Résoudre l'IP du peer
PEER_URL=$(python3 -c "
import yaml, sys
with open('$COMPOSE_LOCAL') as f:
c = yaml.safe_load(f)
peers = c.get('peers', {})
p = peers.get('$PEER', {})
url = p.get('url', '')
if url:
# Extraire host de http://ip:port
host = url.replace('http://','').replace('https://','').split(':')[0]
print(host)
" 2>/dev/null || echo "")
if [[ -z "$PEER_URL" ]]; then
echo "❌ Peer '$PEER' non trouvé dans brain-compose.local.yml"
echo " Ajouter sous peers: dans brain-compose.local.yml"
exit 1
fi
echo "🔄 Sync depuis $PEER ($PEER_URL)"
echo ""
echo "⚠️ CONFIRMATION REQUISE — cette commande va :"
echo " 1. Lire les noms de clés sur $PEER via SSH (pas les valeurs)"
echo " 2. Identifier les clés manquantes localement"
echo " 3. Copier UNIQUEMENT les clés manquantes via SSH"
echo ""
read -p "Continuer ? (oui/non) " confirm
if [[ "$confirm" != "oui" ]]; then
echo "Annulé."
exit 0
fi
# Étape 1 : lister les clés sur le peer
echo ""
echo "→ Lecture des clés sur $PEER..."
PEER_KEYS=$(ssh "$PEER_URL" "grep '^[^#].*=' ~/Dev/BrainSecrets/MYSECRETS 2>/dev/null | cut -d= -f1 | sort" 2>/dev/null || echo "")
if [[ -z "$PEER_KEYS" ]]; then
echo "❌ Impossible de lire MYSECRETS sur $PEER"
echo " Vérifier : ssh $PEER_URL 'test -f ~/Dev/BrainSecrets/MYSECRETS'"
exit 1
fi
# Étape 2 : identifier les manquantes
LOCAL_KEYS=$(grep "^[^#].*=" "$MYSECRETS" | cut -d= -f1 | sort)
MISSING=$(comm -23 <(echo "$PEER_KEYS") <(echo "$LOCAL_KEYS"))
if [[ -z "$MISSING" ]]; then
echo "✅ Aucune clé manquante — MYSECRETS déjà complet"
exit 0
fi
echo "Clés manquantes localement :"
echo "$MISSING" | sed 's/^/ /'
echo ""
read -p "Copier ces clés depuis $PEER ? (oui/non) " confirm2
if [[ "$confirm2" != "oui" ]]; then
echo "Annulé."
exit 0
fi
# Étape 3 : copier les valeurs manquantes via SSH (jamais affichées)
for key in $MISSING; do
ssh "$PEER_URL" "grep '^${key}=' ~/Dev/BrainSecrets/MYSECRETS" >> "$MYSECRETS" 2>/dev/null
echo "$key"
done
echo ""
echo "✅ Sync terminée — $(echo "$MISSING" | wc -l) clé(s) ajoutée(s) à MYSECRETS"
echo " Les valeurs n'ont jamais été affichées."
;;
help|*)
echo "brain-secrets-sync.sh — Registre secrets + sync SSH (ADR-040)"
echo ""
echo "Usage :"
echo " status → compare registre vs MYSECRETS local"
echo " audit → secrets expirés, rotation due"
echo " sync <peer> → récupère les secrets manquants via SSH"
echo " diff <peer> → compare clés par machine (sans valeurs)"
echo ""
echo "Registre : ~/Dev/BrainSecrets/secrets.yml"
echo "Valeurs : ~/Dev/BrainSecrets/MYSECRETS"
;;
esac

View File

@@ -1,151 +1,232 @@
#!/bin/bash
# brain-setup.sh — First boot setup (fresh fork)
# Idempotent — safe à relancer si une étape a échoué.
# brain-setup.sh — Setup complet brain sur une nouvelle machine
# Usage : bash brain-setup.sh [brain_name] [brain_root]
# Ex : bash brain-setup.sh prod-laptop ~/Dev/Brain
#
# Usage : bash scripts/brain-setup.sh
# Ce script est idempotent — safe à relancer si une étape a échoué.
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLAUDE_DIR="$HOME/.claude"
# ── Config ──────────────────────────────────────────────────────────────────
GITEA="git@git.tetardtek.com:Tetardtek"
BRAIN_NAME="${1:-prod-laptop}"
BRAIN_ROOT="${2:-$HOME/Dev/Brain}"
ok() { echo "OK $1"; }
warn() { echo "WRN $1"; }
ask() { printf "\n? %s\n> " "$1"; }
REPOS=(
"brain:$BRAIN_ROOT"
"toolkit:$BRAIN_ROOT/toolkit"
"progression-coach:$BRAIN_ROOT/progression"
"brain-agent-review:$BRAIN_ROOT/reviews"
"brain-profil:$BRAIN_ROOT/profil"
"brain-todo:$BRAIN_ROOT/todo"
"brain.wiki:$BRAIN_ROOT/wiki"
)
# brain-ui est dans le monorepo principal (brain-ui/ sous BRAIN_ROOT) — pas un satellite séparé
# ── Couleurs ─────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
ok() { echo -e "${GREEN}$1${NC}"; }
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
info() { echo -e " $1"; }
echo ""
echo "=== brain-template — First boot setup ==="
echo " Chemin : $BRAIN_ROOT"
echo "╔══════════════════════════════════════════════╗"
echo " brain-setup.sh — nouvelle machine ║"
echo "║ brain_name : $BRAIN_NAME"
echo "║ brain_root : $BRAIN_ROOT"
echo "╚══════════════════════════════════════════════╝"
echo ""
# ETAPE 1 — PATHS.md
echo "--- 1/5 Chemins machine ---"
if grep -q '<BRAIN_ROOT>' "$BRAIN_ROOT/PATHS.md" 2>/dev/null; then
ask "Chemin absolu du brain [Entree = $BRAIN_ROOT]"
read -r brain_path; brain_path="${brain_path:-$BRAIN_ROOT}"
ask "Chemin projets [ex: $HOME/Dev/Projects]"
read -r projects_path; projects_path="${projects_path:-$HOME/Dev/Projects}"
ask "URL Git [ex: git@github.com:alice]"
read -r gitea_url; gitea_url="${gitea_url:-git@github.com:<USERNAME>}"
ask "Username Git"
read -r username; username="${username:-<USERNAME>}"
sed -i \
-e "s|<BRAIN_ROOT>|$brain_path|g" \
-e "s|<PROJECTS_ROOT>|$projects_path|g" \
-e "s|<GITEA_URL>|$gitea_url|g" \
-e "s|<USERNAME>|$username|g" \
-e "s|<HOME>|$HOME|g" \
"$BRAIN_ROOT/PATHS.md"
ok "PATHS.md configure"
else
ok "PATHS.md deja configure"
brain_path="$BRAIN_ROOT"
# ── Étape 0 — SSH key ────────────────────────────────────────────────────────
echo "[ 0/5 ] Vérification SSH key Gitea..."
if ! ssh -T git@git.tetardtek.com -o StrictHostKeyChecking=no 2>&1 | grep -qE "Welcome|Hi there"; then
warn "Clé SSH Gitea non configurée."
info "Créer une clé :"
info " ssh-keygen -t ed25519 -C 'laptop@brain'"
info " cat ~/.ssh/id_ed25519.pub"
info " → Ajouter dans Gitea : Settings > SSH Keys"
echo ""
read -p " Appuie sur Entrée quand la clé est ajoutée dans Gitea..." _
fi
ok "SSH Gitea OK"
# ETAPE 2 — CLAUDE.md global
# ── Étape 1 — Cloner les satellites ──────────────────────────────────────────
echo ""
echo "--- 2/5 CLAUDE.md global ---"
CLAUDE_MD="$CLAUDE_DIR/CLAUDE.md"
brain_name="prod"
if [ ! -f "$CLAUDE_MD" ]; then
ask "Nom de cette instance ? [prod / dev / laptop]"
read -r brain_name; brain_name="${brain_name:-prod}"
mkdir -p "$CLAUDE_DIR"
cat > "$CLAUDE_MD" << EOF
# CLAUDE.md
echo "[ 1/5 ] Clonage des satellites..."
for entry in "${REPOS[@]}"; do
repo="${entry%%:*}"
dest="${entry#*:}"
dest="${dest/#\~/$HOME}"
brain_root: ${brain_path:-$BRAIN_ROOT}
brain_name: $brain_name
## Bootstrap
0. ${brain_path:-$BRAIN_ROOT}/PATHS.md
1. ${brain_path:-$BRAIN_ROOT}/profil/collaboration.md
2. ${brain_path:-$BRAIN_ROOT}/agents/coach.md
3. ${brain_path:-$BRAIN_ROOT}/agents/helloWorld.md
helloWorld prend le relais.
EOF
ok "~/.claude/CLAUDE.md cree (brain_name: $brain_name)"
else
ok "~/.claude/CLAUDE.md existe"
brain_name=$(grep 'brain_name:' "$CLAUDE_MD" | sed 's/.*: *//' | tr -d ' ' | head -1 || echo "prod")
fi
# ETAPE 3 — brain-compose.local.yml
echo ""
echo "--- 3/5 brain-compose.local.yml ---"
LOCAL="$BRAIN_ROOT/brain-compose.local.yml"
tier="free"
if [ ! -f "$LOCAL" ]; then
ask "Tier ? [free / pro / full]"
read -r tier; tier="${tier:-free}"
api_key=""
if [ "$tier" != "free" ]; then
ask "Cle API"
read -r api_key
fi
cat > "$LOCAL" << EOF
brain_name: $brain_name
kernel_path: ${brain_path:-$BRAIN_ROOT}
tier: $tier
$([ -n "${api_key:-}" ] && echo "api_key: $api_key" || echo "# api_key: (tier free)")
instances:
$brain_name:
path: ${brain_path:-$BRAIN_ROOT}
brain_name: $brain_name
EOF
ok "brain-compose.local.yml cree (tier: $tier)"
else
ok "brain-compose.local.yml existe"
fi
# ETAPE 3b — collaboration.md
echo ""
echo "--- 3b/5 Profil collaboration ---"
COLLAB="$BRAIN_ROOT/profil/collaboration.md"
COLLAB_EX="$BRAIN_ROOT/profil/collaboration.md.example"
if [ ! -f "$COLLAB" ] && [ -f "$COLLAB_EX" ]; then
cp "$COLLAB_EX" "$COLLAB"
ok "profil/collaboration.md cree depuis .example — a personnaliser"
else
ok "profil/collaboration.md existe"
fi
# ETAPE 4 — Git remote
echo ""
echo "--- 4/5 Git remote ---"
current_origin=$(git -C "$BRAIN_ROOT" remote get-url origin 2>/dev/null || echo "")
if echo "$current_origin" | grep -q "brain-template"; then
ask "URL de TON repo ? (skip pour ignorer)"
read -r new_remote
if [ "$new_remote" != "skip" ] && [ -n "$new_remote" ]; then
git -C "$BRAIN_ROOT" remote set-url origin "$new_remote"
git -C "$BRAIN_ROOT" remote add upstream "$current_origin" 2>/dev/null || true
ok "origin -> $new_remote / upstream -> brain-template"
if [[ -d "$dest/.git" ]]; then
info "$repo → déjà cloné ($dest) — git pull..."
git -C "$dest" pull --ff-only 2>/dev/null || warn "$repo : pull échoué (conflits ?) — vérifier manuellement"
else
warn "Remote non modifie"
mkdir -p "$(dirname "$dest")"
git clone "$GITEA/$repo.git" "$dest"
ok "$repo$dest"
fi
else
ok "Remote : $current_origin"
done
ok "Tous les satellites clonés"
# ── Étape 2 — CLAUDE.md ──────────────────────────────────────────────────────
echo ""
echo "[ 2/5 ] Configuration CLAUDE.md..."
CLAUDE_TARGET="$HOME/.claude/CLAUDE.md"
CLAUDE_EXAMPLE="$BRAIN_ROOT/profil/CLAUDE.md.example"
mkdir -p "$HOME/.claude"
if [[ -f "$CLAUDE_TARGET" ]]; then
warn "~/.claude/CLAUDE.md existe déjà — backup → CLAUDE.md.bak"
cp "$CLAUDE_TARGET" "$CLAUDE_TARGET.bak"
fi
# ETAPE 5 — Validation
echo ""
echo "--- 5/5 Validation ---"
bash "$BRAIN_ROOT/scripts/kernel-isolation-check.sh" 2>&1 | tail -2
cp "$CLAUDE_EXAMPLE" "$CLAUDE_TARGET"
sed -i "s|<BRAIN_ROOT>|$BRAIN_ROOT|g" "$CLAUDE_TARGET"
sed -i "s|<BRAIN_NAME>|$BRAIN_NAME|g" "$CLAUDE_TARGET"
ok "~/.claude/CLAUDE.md configuré (brain_name=$BRAIN_NAME, brain_root=$BRAIN_ROOT)"
# ── Étape 3 — brain-compose.local.yml ────────────────────────────────────────
echo ""
echo "=== Setup termine ==="
echo "[ 3/5 ] brain-compose.local.yml..."
LOCAL_COMPOSE="$BRAIN_ROOT/brain-compose.local.yml"
KERNEL_VERSION=$(grep '^version:' "$BRAIN_ROOT/brain-compose.yml" | awk '{print $2}' | tr -d '"')
if [[ -f "$LOCAL_COMPOSE" ]]; then
warn "brain-compose.local.yml existe déjà — skip"
else
cat > "$LOCAL_COMPOSE" << EOF
# brain-compose.local.yml — Registre machine ($BRAIN_NAME)
# NON VERSIONNÉ — gitignored.
kernel_path: $BRAIN_ROOT
kernel_version: "$KERNEL_VERSION"
last_kernel_sync: "$(date +%Y-%m-%d)"
machine: $BRAIN_NAME
write_mode: readonly_kernel # nouvelle machine = jamais kernel writer
instances:
$BRAIN_NAME:
path: $BRAIN_ROOT
brain_name: $BRAIN_NAME
feature_set: full
mode: prod
docs_fetch: ask
config_status: hydrated
active: true
EOF
ok "brain-compose.local.yml créé"
fi
# ── Lock kernel push (nouvelle machine = readonly) ────────────────────────────
git -C "$BRAIN_ROOT" remote set-url --push origin no_push
ok "Kernel push lockée (write_mode: readonly_kernel)"
# ── Étape 3.5 — Brain API Key (optionnel) ────────────────────────────────────
echo ""
echo " brain_root : ${brain_path:-$BRAIN_ROOT}"
echo " tier : ${tier:-free}"
echo "[ 3.5/5 ] Brain API Key (optionnel)..."
info "Obtenir une clé : contacter le mainteneur du brain (tier free = aucune clé requise)"
info "Format attendu : bk_live_<32chars> (prod) ou bk_test_<32chars> (dev)"
echo ""
echo " Ouvre Claude Code dans ce dossier."
echo " Il t'attend."
read -rp " Brain API Key (Entrée pour passer, tier free) : " api_key
if [[ -n "$api_key" ]]; then
if [[ ! "$api_key" =~ ^bk_(live|test)_ ]]; then
warn "Format invalide — clé ignorée (attendu : bk_live_... ou bk_test_...)"
else
sed -i "s|^brain_api_key:.*|brain_api_key: $api_key|" "$BRAIN_ROOT/brain-compose.yml"
ok "Clé enregistrée dans brain-compose.yml"
info "Le key-guardian validera au prochain boot (timeout 3s, grace 72h si VPS down)."
fi
else
info "Tier free — aucune clé configurée."
fi
# ── Étape 4 — MYSECRETS ──────────────────────────────────────────────────────
echo ""
echo "[ 4/5 ] MYSECRETS..."
MYSECRETS="$BRAIN_ROOT/MYSECRETS"
if [[ -f "$MYSECRETS" ]]; then
ok "MYSECRETS présent"
else
warn "MYSECRETS absent — jamais versionné."
info ""
info "Options pour le récupérer :"
info " A) Copie sécurisée depuis le desktop :"
info " scp tetardtek@<desktop-ip>:~/Dev/Brain/MYSECRETS $MYSECRETS"
info ""
info " B) Recréer manuellement :"
info " cp $BRAIN_ROOT/MYSECRETS.example $MYSECRETS (si le fichier exemple existe)"
info " → Remplir les valeurs manuellement"
info ""
warn "Le brain fonctionne sans MYSECRETS mais les sessions secrets seront bloquées."
fi
# ── Étape 5 — Claude Code ────────────────────────────────────────────────────
echo ""
echo "[ 5/5 ] Claude Code..."
if command -v claude &>/dev/null; then
ok "Claude Code installé ($(claude --version 2>/dev/null || echo 'version inconnue'))"
else
warn "Claude Code non installé."
info " npm install -g @anthropic-ai/claude-code"
info " ou : https://claude.ai/code"
fi
# ── Étape 5.5 — Node.js ──────────────────────────────────────────────────────
echo ""
echo "[ 5.5 ] Node.js..."
if command -v node &>/dev/null && command -v npm &>/dev/null; then
ok "Node.js $(node --version) / npm $(npm --version)"
else
warn "Node.js ou npm absent."
info " Option A — nvm (recommandé) :"
info " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"
info " nvm install --lts"
info " Option B — apt :"
info " sudo apt install nodejs npm"
fi
# ── Étape 5.75 — Python3 + pip + brain-engine deps ───────────────────────────
echo ""
echo "[ 5.75 ] Python3 + brain-engine..."
if ! command -v python3 &>/dev/null; then
warn "python3 absent — installer via : sudo apt install python3 python3-pip"
elif ! command -v pip3 &>/dev/null; then
warn "pip3 absent — installer via : sudo apt install python3-pip"
else
ok "Python $(python3 --version 2>&1 | awk '{print $2}') / pip $(pip3 --version 2>&1 | awk '{print $2}')"
REQUIREMENTS="$BRAIN_ROOT/brain-engine/requirements.txt"
if [[ -f "$REQUIREMENTS" ]]; then
info "Installation des dépendances brain-engine..."
pip3 install -r "$REQUIREMENTS" --break-system-packages --quiet && ok "brain-engine deps OK" || warn "pip3 install a échoué — vérifier manuellement"
else
warn "brain-engine/requirements.txt absent — skip pip install"
fi
fi
# ── Résumé ────────────────────────────────────────────────────────────────────
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ Setup terminé ║"
echo "╚══════════════════════════════════════════════╝"
echo ""
echo " brain_name : $BRAIN_NAME"
echo " brain_root : $BRAIN_ROOT"
echo ""
echo " Modes de démarrage :"
echo " → Dev laptop (mock, pas de VPS) :"
echo " bash $BRAIN_ROOT/scripts/brain-dev.sh"
echo " → Dev laptop + engine local :"
echo " bash $BRAIN_ROOT/scripts/brain-dev.sh --engine"
echo " → Session Claude Code :"
echo " Ouvrir Claude Code dans $BRAIN_ROOT"
echo " Le brain se boot automatiquement via CLAUDE.md"
echo ""
warn "Si MYSECRETS est absent : le remplir avant la première session work."
echo ""
echo " (Il va te poser une derniere question.)"

89
scripts/brain-start-laptop.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# brain-start-laptop.sh — Démarre l'environnement brain sur le laptop
# Lancé après un reboot ou en début de session.
#
# Usage : bash scripts/brain-start-laptop.sh
#
# Lance :
# 1. Ollama (si pas déjà up)
# 2. brain-engine/server.py → port 7700
# 3. Vérifie la connexion peer desktop
# 4. Affiche l'écart embeddings (sync si besoin)
#
# Le script reste en foreground — brain-engine tourne tant que le terminal est ouvert.
# Laisser tourner dans un terminal dédié, travailler dans un autre.
# Arrêt propre : Ctrl+C (trap SIGINT → kill brain-engine)
# Pour nous uniquement — pas dans le template.
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG_SERVER="$BRAIN_ROOT/brain-engine/server-local.log"
DESKTOP_PEER="192.168.1.11"
cleanup() {
echo ""
echo "→ Arrêt brain laptop..."
kill "$PID_SERVER" 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM
echo ""
echo "=== 🧠 Brain laptop — startup ==="
echo " Root : $BRAIN_ROOT"
echo ""
# 1. Ollama
echo "--- 1/4 Ollama ---"
if ! pgrep -x ollama > /dev/null 2>&1; then
sudo systemctl start ollama 2>/dev/null || ollama serve &>/dev/null &
sleep 2
fi
if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
echo "✅ Ollama up"
else
echo "⚠️ Ollama non disponible — RAG local désactivé"
fi
# 2. Brain-engine
echo ""
echo "--- 2/4 brain-engine ---"
# Kill instance précédente si elle tourne
pkill -f "python3 brain-engine/server.py" 2>/dev/null || true
sleep 1
cd "$BRAIN_ROOT"
python3 brain-engine/server.py > "$LOG_SERVER" 2>&1 &
PID_SERVER=$!
sleep 3
if kill -0 "$PID_SERVER" 2>/dev/null; then
echo "✅ brain-engine PID $PID_SERVER → http://localhost:7700"
else
echo "❌ brain-engine n'a pas démarré — voir $LOG_SERVER"
exit 1
fi
# 3. Peer desktop
echo ""
echo "--- 3/4 Peer desktop ---"
if curl -s "http://${DESKTOP_PEER}:7700/health" > /dev/null 2>&1; then
echo "✅ Desktop online (${DESKTOP_PEER}:7700)"
else
echo "⚠️ Desktop offline — mode autonome"
fi
# 4. Écart embeddings
echo ""
echo "--- 4/4 Embeddings ---"
bash "$BRAIN_ROOT/scripts/brain-sync-replica.sh" status 2>&1
echo ""
echo "=== Brain laptop prêt ==="
echo " brain-engine : http://localhost:7700"
echo " BSI network : http://localhost:7700/bsi/network"
echo ""
echo " Ctrl+C pour arrêter"
wait "$PID_SERVER"

332
scripts/brain-state-bot.sh Executable file
View File

@@ -0,0 +1,332 @@
#!/bin/bash
# brain-state-bot.sh — tier free
# Lit les claims ouverts + git log → écrit/met à jour workspace/live-states.md
# Commit live-states.md avec "live-states: bot update"
#
# Usage : bash scripts/brain-state-bot.sh [--dry-run]
#
# Règles :
# - Ne ferme pas les claims BSI
# - Ne lit pas MYSECRETS
# - Silencieux sauf erreur critique (stderr)
# - Ne jamais écraser `needs` si déjà présent
set -uo pipefail
BRAIN_ROOT="${BRAIN_ROOT:-/home/tetardtek/Dev/Brain}"
LIVE_STATES="$BRAIN_ROOT/workspace/live-states.md"
DRY_RUN=0
# ─── Args ──────────────────────────────────────────────────────────────────
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
esac
done
# ─── Helpers ───────────────────────────────────────────────────────────────
_now_iso() {
date +"%Y-%m-%dT%H:%M"
}
# Convertit un timestamp ISO8601 (YYYY-MM-DDTHH:MM) en epoch seconds
_iso_to_epoch() {
local ts="$1"
# Remplacer T par espace pour date
date -d "${ts/T/ }" +%s 2>/dev/null || echo 0
}
# Extrait un champ YAML simple (key: value) depuis un fichier
_yaml_field() {
local file="$1" key="$2"
grep -E "^${key}:[[:space:]]" "$file" 2>/dev/null \
| head -1 \
| sed "s/^${key}:[[:space:]]*//" \
| tr -d '"' \
| xargs
}
# Dérive le slug projet depuis le filename du claim
# sess-YYYYMMDD-HHMM-slug1-slug2 → slug1 (premier segment après timestamp)
_derive_project() {
local sess_id="$1"
# Retirer "sess-YYYYMMDD-HHMM-" puis prendre le premier segment
local remainder
remainder=$(echo "$sess_id" | sed 's/^sess-[0-9]\{8\}-[0-9]\{4\}-//')
# Retirer suffixes connus (boot, brain, supervisor…) si présent après "-"
echo "$remainder" | cut -d'-' -f1
}
# Dérive le slug depuis le champ scope du claim
# scope: "originsdigital-back/" → "originsdigital"
# scope: "brain/" → "brain"
_project_from_scope() {
local scope="$1"
# Prendre le premier token, retirer trailing slash, puis garder partie avant "-"
local first_token
first_token=$(echo "$scope" | awk '{print $1}' | tr -d '/')
# Si contient "-", prendre la partie avant le dernier tiret
# ex: originsdigital-back → originsdigital
# ex: brain → brain
echo "$first_token" | sed 's/-[^-]*$//' | sed 's/\///'
}
# Cherche un repo git pour un slug projet
# Cherche dans Brain/, Gitea/, Github/ (insensible à la casse)
_find_project_repo() {
local slug="$1"
local candidates=(
"$BRAIN_ROOT"
"$BRAIN_ROOT/brain-ui"
"$BRAIN_ROOT/brain-engine"
"/home/tetardtek/Dev/Gitea"
"/home/tetardtek/Dev/Github"
)
# Match direct : brain → BRAIN_ROOT
if [ "$slug" = "brain" ]; then
echo "$BRAIN_ROOT"
return
fi
# Chercher un répertoire qui contient le slug (insensible à la casse)
for base in "${candidates[@]}"; do
[ -d "$base" ] || continue
# Vérifier si base lui-même match (ex: brain-ui)
local basename
basename=$(basename "$base" | tr '[:upper:]' '[:lower:]')
local slug_lc
slug_lc=$(echo "$slug" | tr '[:upper:]' '[:lower:]')
if [[ "$basename" == *"$slug_lc"* ]] && [ -d "$base/.git" ]; then
echo "$base"
return
fi
# Chercher sous-répertoires
if [ -d "$base" ]; then
local found
found=$(find "$base" -maxdepth 1 -type d -iname "*${slug}*" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -d "$found/.git" ]; then
echo "$found"
return
fi
fi
done
echo ""
}
# Obtient le dernier commit message d'un repo
_git_last_commit() {
local repo="$1"
[ -d "$repo/.git" ] || { echo ""; return; }
git -C "$repo" log --oneline -1 2>/dev/null | sed 's/^[a-f0-9]* //' | head -c 80
}
# Obtient le timestamp du dernier commit (epoch)
_git_last_commit_epoch() {
local repo="$1"
[ -d "$repo/.git" ] || { echo "0"; return; }
git -C "$repo" log -1 --format="%ct" 2>/dev/null || echo "0"
}
# ─── Lecture de l'état courant de live-states.md ────────────────────────────
# Extrait un champ YAML d'une entrée de live-states.md identifiée par sess_id
# Retourne "" si le champ n'existe pas ou si le sess_id n'est pas trouvé
_get_existing_field() {
local sess_id="$1" field="$2"
local in_block=0 value=""
while IFS= read -r line; do
# Début de bloc : ligne "- sess_id: <id>"
if echo "$line" | grep -qE "^- sess_id:[[:space:]]*${sess_id}[[:space:]]*$"; then
in_block=1
continue
fi
# Fin de bloc : nouvelle entrée "- sess_id:" ou fin du fichier
if [ "$in_block" -eq 1 ]; then
if echo "$line" | grep -qE "^- sess_id:"; then
break
fi
# Lire le champ demandé
if echo "$line" | grep -qE "^[[:space:]]+${field}:[[:space:]]"; then
value=$(echo "$line" | sed "s/^[[:space:]]*${field}:[[:space:]]*//" | tr -d '"')
fi
fi
done < "$LIVE_STATES"
echo "$value"
}
# ─── Écriture d'un bloc dans live-states.md ─────────────────────────────────
# Met à jour ou insère un bloc sess_id dans live-states.md
# Args: sess_id project doing status needs priority updated
_upsert_block() {
local sess_id="$1"
local project="$2"
local doing="$3"
local status="$4"
local needs="$5"
local priority="$6"
local updated="$7"
local new_block
new_block="- sess_id: ${sess_id}
project: ${project}
doing: \"${doing}\"
status: ${status}
needs: ${needs}
priority: ${priority}
team: []
blocking: []
context: \"\"
updated: ${updated}"
if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] bloc à écrire pour ${sess_id}:"
echo "$new_block"
return
fi
# Vérifier si le bloc existe déjà
if grep -qE "^- sess_id:[[:space:]]*${sess_id}[[:space:]]*$" "$LIVE_STATES" 2>/dev/null; then
# Mise à jour différentielle : remplacer le bloc existant
# Utilise python3 pour éviter les conflits de syntaxe awk/bash
local tmpfile
tmpfile=$(mktemp)
python3 - "$LIVE_STATES" "$sess_id" "$new_block" > "$tmpfile" << 'PYEOF'
import sys, re
infile = sys.argv[1]
sess_id = sys.argv[2]
new_block = sys.argv[3]
with open(infile) as f:
lines = f.readlines()
out = []
in_block = False
for line in lines:
if re.match(r'^- sess_id:\s*' + re.escape(sess_id) + r'\s*$', line):
in_block = True
out.append(new_block + "\n")
continue
if in_block:
# Fin du bloc : nouvelle entrée, frontmatter ou commentaire niveau 0
if re.match(r'^- sess_id:', line) or re.match(r'^---', line) or re.match(r'^#', line):
in_block = False
out.append(line)
# else : ignorer les lignes de l'ancien bloc
else:
out.append(line)
sys.stdout.write("".join(out))
PYEOF
mv "$tmpfile" "$LIVE_STATES"
else
# Insertion : ajouter à la fin avec ligne vide de séparation
echo "" >> "$LIVE_STATES"
echo "$new_block" >> "$LIVE_STATES"
fi
}
# ─── Main ────────────────────────────────────────────────────────────────────
[ -f "$LIVE_STATES" ] || { echo "CRITICAL: $LIVE_STATES introuvable" >&2; exit 1; }
NOW_EPOCH=$(date +%s)
TWO_HOURS=7200
UPDATED=0 # Nombre de sessions mises à jour
for claim in "$BRAIN_ROOT/claims"/sess-*.yml; do
[ -f "$claim" ] || continue
# Lire les champs du claim
status=$(_yaml_field "$claim" "status")
[ "$status" = "open" ] || continue
sess_id=$(_yaml_field "$claim" "sess_id")
[ -n "$sess_id" ] || continue
scope=$(_yaml_field "$claim" "scope")
opened_at=$(_yaml_field "$claim" "opened_at")
# Dériver le projet depuis scope, puis depuis sess_id en fallback
project=""
if [ -n "$scope" ]; then
project=$(_project_from_scope "$scope")
fi
if [ -z "$project" ]; then
project=$(_derive_project "$sess_id")
fi
[ -n "$project" ] || project="unknown"
# Trouver le repo git du projet
repo=$(_find_project_repo "$project")
# Dériver doing depuis le dernier commit git
doing=""
if [ -n "$repo" ]; then
doing=$(_git_last_commit "$repo")
fi
[ -n "$doing" ] || doing="En cours"
# Récupérer l'état courant du bloc (si existant)
existing_needs=$(_get_existing_field "$sess_id" "needs")
existing_status=$(_get_existing_field "$sess_id" "status")
existing_updated=$(_get_existing_field "$sess_id" "updated")
# needs : ne jamais écraser si déjà présent
needs="${existing_needs:-none}"
# Si needs est vide string, mettre none
[ -n "$needs" ] || needs="none"
# Stale detection : si updated > 2h + status progressing + pas de commit récent
new_status="progressing"
if [ -n "$existing_status" ] && [ "$existing_status" != "closed" ]; then
new_status="$existing_status"
fi
if [ "$new_status" = "progressing" ]; then
# Vérifier si stale
stale=0
if [ -n "$existing_updated" ]; then
updated_epoch=$(_iso_to_epoch "$existing_updated")
age=$(( NOW_EPOCH - updated_epoch ))
if [ "$age" -gt "$TWO_HOURS" ]; then
# Pas de commit récent ?
last_commit_epoch=0
if [ -n "$repo" ]; then
last_commit_epoch=$(_git_last_commit_epoch "$repo")
fi
commit_age=$(( NOW_EPOCH - last_commit_epoch ))
if [ "$commit_age" -gt "$TWO_HOURS" ]; then
stale=1
fi
fi
fi
if [ "$stale" -eq 1 ]; then
new_status="idle"
echo "stale: ${sess_id} → idle" >&2
fi
fi
# Priority : medium par défaut (tier free — pas de blocking[] cross-claim)
priority="medium"
# Updated : maintenant
updated_ts=$(_now_iso)
_upsert_block "$sess_id" "$project" "$doing" "$new_status" "$needs" "$priority" "$updated_ts"
UPDATED=$(( UPDATED + 1 ))
done
# Commit si des sessions ont été mises à jour (et pas dry-run)
if [ "$DRY_RUN" -eq 0 ] && [ "$UPDATED" -gt 0 ]; then
git -C "$BRAIN_ROOT" add workspace/live-states.md 2>/dev/null
git -C "$BRAIN_ROOT" diff --cached --quiet 2>/dev/null || \
git -C "$BRAIN_ROOT" commit -m "live-states: bot update" 2>/dev/null
fi
exit 0

188
scripts/brain-sync-replica.sh Executable file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# brain-sync-replica.sh — Réplication master → replica (embeddings)
# Le desktop est source de vérité. Le laptop reçoit une copie read-only.
#
# Usage :
# brain-sync-replica.sh status → écart master/replica
# brain-sync-replica.sh sync <replica_host> → sync vers replica
# brain-sync-replica.sh sync laptop → alias pour le peer "laptop"
#
# Prérequis : SSH sans mot de passe vers la replica
# Ne sync QUE la table embeddings — pas claims, pas locks (BSI local à chaque machine)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="$BRAIN_ROOT/brain.db"
REMOTE_DB_PATH="Dev/Brain/brain.db"
# Résoudre le peer depuis brain-compose.local.yml
resolve_peer() {
local name="$1"
python3 - "$BRAIN_ROOT/brain-compose.local.yml" "$name" << 'PY'
import sys, yaml
with open(sys.argv[1]) as f:
data = yaml.safe_load(f) or {}
peers = data.get('peers', {})
peer = peers.get(sys.argv[2], {})
url = peer.get('url', '')
# Extraire host depuis http://192.168.1.10:7700
if '://' in url:
host = url.split('://')[1].split(':')[0]
print(host)
PY
}
# --- STATUS ---
cmd_status() {
local local_count local_updated
local_count=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "SELECT COUNT(*) FROM embeddings WHERE indexed=1")
local_updated=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "SELECT MAX(updated_at) FROM embeddings")
echo "=== Embedding master (local) ==="
echo " Chunks indexés : $local_count"
echo " Dernier update : $local_updated"
# Check peers
local compose="$BRAIN_ROOT/brain-compose.local.yml"
if [ -f "$compose" ]; then
echo ""
echo "=== Peers ==="
python3 - "$compose" << 'PY'
import yaml, json, urllib.request
with open(__import__('sys').argv[1]) as f:
data = yaml.safe_load(f) or {}
for name, peer in data.get('peers', {}).items():
if not peer.get('active', False):
continue
url = peer.get('url', '').rstrip('/')
try:
with urllib.request.urlopen(f"{url}/health", timeout=3) as r:
health = json.loads(r.read())
indexed = health.get('indexed', '?')
print(f" {name}: {indexed} chunks (online)")
except Exception:
print(f" {name}: offline")
PY
fi
}
# --- SYNC ---
cmd_sync() {
local target="$1"
local host
# Résoudre si c'est un nom de peer
host=$(resolve_peer "$target" 2>/dev/null || echo "")
if [ -z "$host" ]; then
host="$target"
fi
local user="tetardtek"
local remote="${user}@${host}"
echo "=== Sync embeddings → $remote ==="
# 1. Check connexion
if ! ssh -o ConnectTimeout=3 "$remote" "echo ok" > /dev/null 2>&1; then
echo "❌ SSH unreachable : $remote"
exit 1
fi
# 2. Stats locales
local local_count
local_count=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "SELECT COUNT(*) FROM embeddings WHERE indexed=1")
echo " Master : $local_count chunks"
# 3. Stats replica
local remote_count
remote_count=$(ssh "$remote" "python3 ~/Dev/Brain/scripts/bsi-db.py 'SELECT COUNT(*) FROM embeddings WHERE indexed=1' 2>/dev/null || echo 0")
echo " Replica : $remote_count chunks"
local delta=$((local_count - remote_count))
if [ "$delta" -eq 0 ]; then
echo "✅ Déjà synchronisé — 0 écart"
exit 0
fi
echo " Écart : $delta chunks"
echo ""
# 4. Export embeddings → fichier temporaire
local tmp="/tmp/brain-embeddings-sync.db"
echo " Exporting embeddings table..."
python3 - "$DB_PATH" "$tmp" << 'PY'
import sqlite3, sys
src = sqlite3.connect(sys.argv[1])
dst = sqlite3.connect(sys.argv[2])
dst.execute("DROP TABLE IF EXISTS embeddings")
# Copy schema
schema = src.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='embeddings'").fetchone()[0]
dst.execute(schema)
# Copy data
rows = src.execute("SELECT * FROM embeddings").fetchall()
cols = [d[0] for d in src.execute("PRAGMA table_info(embeddings)").fetchall()]
placeholders = ','.join(['?'] * len(cols))
dst.executemany(f"INSERT INTO embeddings VALUES ({placeholders})", rows)
dst.commit()
dst.close()
src.close()
print(f" ✅ {len(rows)} chunks exportés")
PY
# 5. SCP vers replica
echo " Transferring to $remote..."
scp -q "$tmp" "${remote}:/tmp/brain-embeddings-sync.db"
# 6. Import sur replica
ssh "$remote" python3 - << 'PY'
import sqlite3
src = sqlite3.connect("/tmp/brain-embeddings-sync.db")
dst = sqlite3.connect("/home/tetardtek/Dev/Brain/brain.db")
# Drop and recreate
dst.execute("DROP TABLE IF EXISTS embeddings")
schema = src.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='embeddings'").fetchone()[0]
dst.execute(schema)
rows = src.execute("SELECT * FROM embeddings").fetchall()
cols = [d[0] for d in src.execute("PRAGMA table_info(embeddings)").fetchall()]
placeholders = ','.join(['?'] * len(cols))
dst.executemany(f"INSERT INTO embeddings VALUES ({placeholders})", rows)
dst.commit()
dst.close()
src.close()
print(f" ✅ {len(rows)} chunks importés sur replica")
PY
# 7. Cleanup
rm -f "$tmp"
ssh "$remote" "rm -f /tmp/brain-embeddings-sync.db"
# 8. Verify
local new_count
new_count=$(ssh "$remote" "python3 ~/Dev/Brain/scripts/bsi-db.py 'SELECT COUNT(*) FROM embeddings WHERE indexed=1' 2>/dev/null || echo '?'")
echo ""
echo "=== Sync terminé ==="
echo " Master : $local_count chunks"
echo " Replica : $new_count chunks"
if [ "$local_count" = "$new_count" ]; then
echo " ✅ Synchronisé — 0 écart"
else
echo " ⚠️ Écart résiduel : $((local_count - new_count))"
fi
}
# --- Router ---
CMD="${1:-}"
case "$CMD" in
status) cmd_status ;;
sync) cmd_sync "${2:-}" ;;
*)
echo "Usage : brain-sync-replica.sh <status|sync>"
echo ""
echo " status → écart master/replica"
echo " sync <host|peer_name> → sync embeddings vers replica"
echo ""
echo " Exemple : brain-sync-replica.sh sync laptop"
exit 1
;;
esac

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# brain-template-export.sh — Extrait brain-template.db depuis brain.db (kernel+public only)
# Usage: bash scripts/brain-template-export.sh [output_path]
#
# Fast path : copie les vecteurs existants, pas besoin d'Ollama.
# Zéro table session (claims, signals, handoffs, sessions, agent_loads, locks, circuit_breaker, agent_memory).
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="${BRAIN_ROOT}/brain.db"
DST="${1:-${BRAIN_ROOT}/brain-template.db}"
if [[ ! -f "$SRC" ]]; then
echo "❌ brain.db introuvable : $SRC" >&2
exit 1
fi
echo "brain-template-export : $SRC$DST"
echo "Scopes inclus : kernel, public"
python3 - "$SRC" "$DST" << 'PY'
import sqlite3
import sys
src_path = sys.argv[1]
dst_path = sys.argv[2]
# Connexion source (lecture seule)
src = sqlite3.connect(f'file:{src_path}?mode=ro', uri=True)
src.row_factory = sqlite3.Row
# Créer le template DB
dst = sqlite3.connect(dst_path)
dst.execute("PRAGMA journal_mode=WAL")
# Créer la table embeddings (seule table du template)
dst.execute("""
CREATE TABLE IF NOT EXISTS embeddings (
chunk_id TEXT PRIMARY KEY,
filepath TEXT NOT NULL,
title TEXT,
chunk_text TEXT NOT NULL,
vector BLOB,
model TEXT,
indexed INTEGER DEFAULT 0,
scope TEXT NOT NULL DEFAULT 'work',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
dst.execute("CREATE INDEX IF NOT EXISTS idx_emb_filepath ON embeddings(filepath)")
dst.execute("CREATE INDEX IF NOT EXISTS idx_emb_indexed ON embeddings(indexed)")
dst.execute("CREATE INDEX IF NOT EXISTS idx_emb_scope ON embeddings(scope)")
dst.commit()
# Copier uniquement les embeddings kernel + public
ALLOWED_SCOPES = ('kernel', 'public')
placeholders = ','.join('?' * len(ALLOWED_SCOPES))
rows = src.execute(f"""
SELECT chunk_id, filepath, title, chunk_text, vector, model, indexed, scope, created_at, updated_at
FROM embeddings
WHERE indexed = 1 AND vector IS NOT NULL AND scope IN ({placeholders})
""", ALLOWED_SCOPES).fetchall()
for r in rows:
dst.execute("""
INSERT OR REPLACE INTO embeddings
(chunk_id, filepath, title, chunk_text, vector, model, indexed, scope, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", tuple(r))
dst.commit()
dst.execute("VACUUM")
# Stats
total = len(rows)
scopes = {}
for r in rows:
s = r['scope']
scopes[s] = scopes.get(s, 0) + 1
src.close()
dst.close()
print(f"✅ Template généré : {dst_path}")
print(f" Chunks : {total}")
for s, c in sorted(scopes.items()):
print(f" - {s} : {c}")
print(f" Tables session : 0 (aucune)")
PY

45
scripts/brain-template-push.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# brain-template-push.sh — Export brain-template.db + push vers VPS + restart
# Usage: bash scripts/brain-template-push.sh
#
# Workflow : export local → scp → restart brain-engine sur VPS
# Prérequis : VPS_IP et VPS_SSH_USER dans MYSECRETS
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TEMPLATE_DB="${BRAIN_ROOT}/brain-template.db"
SECRETS="${HOME}/Dev/BrainSecrets/MYSECRETS"
# Lire VPS config depuis MYSECRETS (silencieux — pas de valeur affichée)
if [[ ! -f "$SECRETS" ]]; then
echo "❌ MYSECRETS introuvable" >&2
exit 1
fi
VPS_IP=$(grep '^VPS_IP=' "$SECRETS" | cut -d= -f2-)
VPS_USER=$(grep '^VPS_SSH_USER=' "$SECRETS" | cut -d= -f2-)
if [[ -z "$VPS_IP" || -z "$VPS_USER" ]]; then
echo "❌ VPS_IP ou VPS_SSH_USER manquant dans MYSECRETS" >&2
exit 1
fi
# Step 1 : Export
echo "1/3 Export brain-template.db..."
bash "${BRAIN_ROOT}/scripts/brain-template-export.sh" "$TEMPLATE_DB"
# Step 2 : SCP
echo ""
echo "2/3 Push vers VPS..."
scp -q "$TEMPLATE_DB" "${VPS_USER}@${VPS_IP}:~/Dev/Brain/brain-template.db"
echo "✅ brain-template.db transféré"
# Step 3 : Restart
echo ""
echo "3/3 Restart brain-engine..."
ssh "${VPS_USER}@${VPS_IP}" "sudo systemctl restart brain-engine"
echo "✅ brain-engine redémarré"
echo ""
echo "🏁 Template déployé sur VPS — brain.tetardtek.com sert le template."

View File

@@ -4,17 +4,16 @@
# Détecte les changements dans BRAIN-INDEX.md → notifie via Telegram
#
# Setup VPS (une seule fois) :
# 1. Copier ce script sur le VPS : scp brain-watch-vps.sh root@<VPS_IP>:/home/<user>/brain-watch/
# 1. Copier ce script sur le VPS : scp brain-watch-vps.sh root@VPS:/home/tetardtek/brain-watch/
# 2. Copier brain-notify.sh aussi
# 3. Cloner le brain : git clone git@<GITEA_URL>:<USERNAME>/brain.git /home/<user>/brain-watch/brain
# 4. Copier MYSECRETS sur le VPS : scp MYSECRETS root@<VPS_IP>:/home/<user>/brain-watch/
# 3. Cloner le brain : git clone git@git.tetardtek.com:Tetardtek/brain.git /home/tetardtek/brain-watch/brain
# 4. Copier MYSECRETS sur le VPS : scp MYSECRETS root@VPS:/home/tetardtek/brain-watch/
# 5. Installer le service systemd : install-brain-watch-vps.sh
# 6. systemctl start brain-watch && systemctl enable brain-watch
set -euo pipefail
# Configurable — override via env ou MYSECRETS (VPS_WATCH_ROOT=...)
WATCH_ROOT="${VPS_WATCH_ROOT:-$HOME/brain-watch}"
WATCH_ROOT="/home/tetardtek/brain-watch"
BRAIN_INDEX="$WATCH_ROOT/brain/BRAIN-INDEX.md"
NOTIFY="$WATCH_ROOT/brain-notify.sh"
BRAIN_ROOT="$WATCH_ROOT" # pour brain-notify.sh — lit MYSECRETS ici
@@ -24,8 +23,7 @@ LOG_PREFIX="[brain-watch-vps]"
export BRAIN_ROOT
if [[ ! -d "$WATCH_ROOT/brain" ]]; then
BRAIN_GIT_URL="${BRAIN_GIT_URL:-$(grep '^BRAIN_GIT_URL=' "$WATCH_ROOT/MYSECRETS" 2>/dev/null | cut -d= -f2-)}"
echo "$LOG_PREFIX ERREUR : brain non cloné. Lancer : git clone $BRAIN_GIT_URL $WATCH_ROOT/brain" >&2
echo "$LOG_PREFIX ERREUR : brain non cloné. Lancer : git clone git@git.tetardtek.com:Tetardtek/brain.git $WATCH_ROOT/brain" >&2
exit 1
fi

50
scripts/bsi-db.py Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
bsi-db.py — Wrapper SQLite léger pour les scripts BSI bash.
Remplace sqlite3 CLI (pas toujours installé).
Usage :
python3 scripts/bsi-db.py "SELECT * FROM claims" → query, pipe-separated
python3 scripts/bsi-db.py -exec "INSERT INTO ..." → write (no output)
python3 scripts/bsi-db.py -script "CREATE TABLE ...; ..." → multi-statement
"""
import sys
import sqlite3
from pathlib import Path
DB_PATH = str(Path(__file__).parent.parent / 'brain.db')
def main():
if len(sys.argv) < 2:
print("Usage: bsi-db.py [-exec|-script] <sql>", file=sys.stderr)
sys.exit(1)
mode = 'query'
sql = sys.argv[1]
if sys.argv[1] == '-exec':
mode = 'exec'
sql = sys.argv[2] if len(sys.argv) > 2 else ''
elif sys.argv[1] == '-script':
mode = 'script'
sql = sys.argv[2] if len(sys.argv) > 2 else ''
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL")
try:
if mode == 'script':
conn.executescript(sql)
elif mode == 'exec':
conn.execute(sql)
conn.commit()
else:
rows = conn.execute(sql).fetchall()
for row in rows:
print('|'.join(str(v) if v is not None else '' for v in row))
finally:
conn.close()
if __name__ == '__main__':
main()

105
scripts/bsi-peer-poll.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# bsi-peer-poll.sh — Poll les peers et écrit l'état dans workspace/live-states.md
# Cron : */5 * * * * bash ~/Dev/Brain/scripts/bsi-peer-poll.sh
#
# Écrit un snapshot lisible par time-anchor (session-navigate L1).
# Si rien n'a changé depuis le dernier poll → pas de réécriture (idempotent).
# Si un peer est injoignable → marqué offline, pas d'erreur.
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE_LOCAL="$BRAIN_ROOT/brain-compose.local.yml"
LIVE_STATES="$BRAIN_ROOT/workspace/live-states.md"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
mkdir -p "$BRAIN_ROOT/workspace"
# Collecter l'état local + peers
OUTPUT=$(python3 - "$BRAIN_ROOT" "$COMPOSE_LOCAL" "$TIMESTAMP" <<'PYEOF'
import yaml, subprocess, sys, os
brain_root = sys.argv[1]
compose_path = sys.argv[2]
timestamp = sys.argv[3]
# Machine locale
with open(compose_path) as f:
compose = yaml.safe_load(f)
machine = compose.get("machine", "unknown")
lines = []
lines.append(f"# live-states.md — snapshot {timestamp}")
lines.append(f"# Généré par bsi-peer-poll.sh — ne pas éditer manuellement")
lines.append("")
# Claims locaux
result = subprocess.run(
["bash", f"{brain_root}/scripts/bsi-query.sh", "open"],
capture_output=True, text=True, timeout=5
)
local_claims = result.stdout.strip()
lines.append(f"## {machine} (local)")
if local_claims:
for line in local_claims.split("\n"):
parts = line.split(" | ")
if len(parts) >= 4:
lines.append(f"- `{parts[0].strip()}` — {parts[1].strip()} — {parts[3].strip()}")
else:
lines.append("- (idle)")
lines.append("")
# Peers
peers = compose.get("peers", {})
for name, info in peers.items():
if not info.get("active", False):
continue
url = info.get("url", "")
host = url.replace("http://", "").replace("https://", "").split(":")[0]
try:
result = subprocess.run(
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=3",
f"tetardtek@{host}",
f"cd ~/Dev/Brain && bash scripts/bsi-query.sh open 2>/dev/null"],
capture_output=True, text=True, timeout=10
)
peer_claims = result.stdout.strip()
lines.append(f"## {name} ({host})")
if peer_claims:
for line in peer_claims.split("\n"):
parts = line.split(" | ")
if len(parts) >= 4:
lines.append(f"- `{parts[0].strip()}` — {parts[1].strip()} — {parts[3].strip()}")
else:
lines.append("- (idle)")
except (subprocess.TimeoutExpired, Exception):
lines.append(f"## {name} ({host})")
lines.append("- (offline)")
lines.append("")
# Résumé
total_active = 0
if local_claims:
total_active += len(local_claims.strip().split("\n"))
lines.append(f"---")
lines.append(f"Dernière mise à jour : {timestamp}")
lines.append(f"Sessions actives : {total_active} local + peers")
print("\n".join(lines))
PYEOF
)
# Écrire uniquement si changement (éviter les écritures inutiles)
if [ -f "$LIVE_STATES" ]; then
# Comparer sans les timestamps (lignes 1-2)
OLD=$(tail -n +3 "$LIVE_STATES" | grep -v "Dernière mise à jour")
NEW=$(echo "$OUTPUT" | tail -n +3 | grep -v "Dernière mise à jour")
if [ "$OLD" = "$NEW" ]; then
exit 0
fi
fi
echo "$OUTPUT" > "$LIVE_STATES"

View File

@@ -9,6 +9,7 @@
# bsi-query.sh count-stale → nombre de claims stale (entier, stdout)
# bsi-query.sh signals → signaux pending (CHECKPOINT | HANDOFF | BLOCKED_ON)
# bsi-query.sh health → dernière session : health_score + type
# bsi-query.sh peers → claims open sur toutes les instances (SSH)
#
# Retour :
# Exit 0 = succès (même si 0 résultats)
@@ -26,7 +27,7 @@ CMD="${1:-help}"
# Fallback propre si brain.db absent
if [[ ! -f "$DB_PATH" ]]; then
echo "⚠️ brain.db absent ($DB_PATH) — lancer: brain-db-sync.sh (optionnel)" >&2
echo "⚠️ brain.db absent ($DB_PATH) — lancer: python3 brain-engine/migrate.py" >&2
exit 1
fi
@@ -106,4 +107,42 @@ conn.close()
PYEOF
}
# ── Commande peers : interroge les instances distantes via SSH ─────────
if [[ "$CMD" == "peers" ]]; then
COMPOSE_LOCAL="$BRAIN_ROOT/brain-compose.local.yml"
MACHINE=$(python3 -c "
import yaml
with open('$COMPOSE_LOCAL') as f:
print(yaml.safe_load(f).get('machine', 'unknown'))
" 2>/dev/null || echo "unknown")
echo "🖥 $MACHINE (local)"
run_query "open"
# Interroger chaque peer
python3 -c "
import yaml, subprocess, sys
with open('$COMPOSE_LOCAL') as f:
c = yaml.safe_load(f)
peers = c.get('peers', {})
for name, info in peers.items():
if not info.get('active', False):
continue
url = info.get('url', '')
host = url.replace('http://','').replace('https://','').split(':')[0]
print(f'PEER:{name}:{host}')
" 2>/dev/null | while IFS=: read -r _ name host; do
echo ""
echo "💻 $name ($host)"
result=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "tetardtek@$host" \
"cd ~/Dev/Brain && bash scripts/bsi-query.sh open" 2>/dev/null)
if [[ -n "$result" ]]; then
echo "$result"
else
echo " (aucun claim ouvert ou machine injoignable)"
fi
done
exit 0
fi
run_query "$CMD"

61
scripts/dev-start.sh Executable file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# dev-start.sh — Démarre l'environnement dev brain local complet
# Usage : bash scripts/dev-start.sh
#
# Lance :
# 1. brain-engine/server.py → port 7700 (BRAIN_TIER=owner)
# 2. brain-ui (Vite) → port 5173
#
# Arrêt propre : Ctrl+C (trap SIGINT → kill les deux processus)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG_SERVER="$BRAIN_ROOT/brain-engine/server-dev.log"
LOG_VITE="/tmp/vite-brain.log"
# Charger les secrets si disponibles (silencieux)
SECRETS_FILE="$HOME/Dev/BrainSecrets/MYSECRETS"
if [[ -f "$SECRETS_FILE" ]]; then
set -a && source "$SECRETS_FILE" && set +a
fi
# Override tier owner en dev — pas de token requis
export BRAIN_TIER=owner
cleanup() {
echo ""
echo "→ Arrêt dev-start..."
kill "$PID_SERVER" 2>/dev/null || true
kill "$PID_VITE" 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM
# Tuer les instances précédentes si elles tournent
lsof -ti:7700 | xargs kill 2>/dev/null || true
lsof -ti:5173 | xargs kill 2>/dev/null || true
sleep 1
echo "🧠 brain-engine → http://localhost:7700 (log: $LOG_SERVER)"
python3 "$BRAIN_ROOT/brain-engine/server.py" > "$LOG_SERVER" 2>&1 &
PID_SERVER=$!
echo "🎨 brain-ui → http://localhost:5173/ui/"
cd "$BRAIN_ROOT/brain-ui" && npm run dev > "$LOG_VITE" 2>&1 &
PID_VITE=$!
echo ""
echo "Ctrl+C pour tout arrêter"
echo "---"
# Attendre que les deux process soient up
sleep 3
if kill -0 "$PID_SERVER" 2>/dev/null && kill -0 "$PID_VITE" 2>/dev/null; then
echo "✅ brain-engine PID $PID_SERVER"
echo "✅ brain-ui PID $PID_VITE"
else
echo "❌ Un process n'a pas démarré — vérifier les logs"
fi
wait

335
scripts/diagram-init.sh Executable file
View File

@@ -0,0 +1,335 @@
#!/usr/bin/env bash
# diagram-init.sh — Génère le fichier .excalidraw initial depuis un workflow.yml
# Usage : bash scripts/diagram-init.sh <workflow-name>
# Exemple : bash scripts/diagram-init.sh superoauth-tier3
# Output : draw/diagrams/<name>.excalidraw
BRAIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKFLOW_NAME="${1:-}"
if [[ -z "$WORKFLOW_NAME" ]]; then
echo "Usage : bash scripts/diagram-init.sh <workflow-name>"
echo "Exemple : bash scripts/diagram-init.sh superoauth-tier3"
exit 1
fi
WORKFLOW_FILE="$BRAIN_ROOT/workflows/${WORKFLOW_NAME}.yml"
OUTPUT_DIR="$BRAIN_ROOT/draw/diagrams"
OUTPUT_FILE="$OUTPUT_DIR/${WORKFLOW_NAME}.excalidraw"
if [[ ! -f "$WORKFLOW_FILE" ]]; then
echo "❌ Workflow introuvable : $WORKFLOW_FILE"
exit 1
fi
mkdir -p "$OUTPUT_DIR"
python3 - "$WORKFLOW_FILE" "$OUTPUT_FILE" << 'PYEOF'
import sys
import json
import yaml
import uuid
import time
workflow_path = sys.argv[1]
output_path = sys.argv[2]
with open(workflow_path) as f:
wf = yaml.safe_load(f)
name = wf.get("name", "workflow")
chain = wf.get("chain", [])
# Layout constants
NODE_W = 220
NODE_H = 90
NODE_GAP = 60
START_X = 40
START_Y = 120
ARROW_Y = START_Y + NODE_H // 2
# Colors
COLOR_PENDING = "#868e96" # gris — pending
COLOR_BORDER = "#343a40"
COLOR_BG_PAGE = "#f8f9fa"
elements = []
def make_id():
return str(uuid.uuid4())[:8]
# Title
elements.append({
"id": make_id(),
"type": "text",
"x": START_X,
"y": 40,
"width": len(name) * 12 + 40,
"height": 36,
"text": name,
"fontSize": 24,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": COLOR_BORDER,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 1,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
node_ids = {}
for i, step in enumerate(chain):
n = step.get("step", i + 1)
stype = step.get("type", "")
angle = step.get("story_angle", "")
agents = step.get("agents", [])
gate = step.get("gate", None)
x = START_X + i * (NODE_W + NODE_GAP)
y = START_Y
node_id = f"{name}-step-{n}"
node_ids[n] = {"id": node_id, "x": x, "y": y}
# Gate badge (above node)
if gate:
gate_label = "⚡ gate:human" if gate == "human" else f"⚡ gate:{gate}"
elements.append({
"id": make_id(),
"type": "text",
"x": x,
"y": y - 28,
"width": NODE_W,
"height": 20,
"text": gate_label,
"fontSize": 13,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "top",
"strokeColor": "#f39c12",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": i + 100,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
# Truncate story_angle
label_angle = (angle[:38] + "…") if len(angle) > 40 else angle
agents_str = " · ".join(agents[:3]) if agents else ""
label_text = f"step {n} [{stype}]\n{label_angle}\n⬜ pending"
elements.append({
"id": node_id,
"type": "rectangle",
"x": x,
"y": y,
"width": NODE_W,
"height": NODE_H,
"backgroundColor": COLOR_PENDING,
"strokeColor": COLOR_BORDER,
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 80,
"angle": 0,
"seed": i + 10,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
# Label inside node
elements.append({
"id": make_id(),
"type": "text",
"x": x + 10,
"y": y + 8,
"width": NODE_W - 20,
"height": NODE_H - 16,
"text": label_text,
"fontSize": 12,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": "#ffffff",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": i + 200,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
# Agents badge (below node)
if agents_str:
elements.append({
"id": make_id(),
"type": "text",
"x": x,
"y": y + NODE_H + 6,
"width": NODE_W,
"height": 18,
"text": agents_str,
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "top",
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": i + 300,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
# Arrows between nodes
for i in range(len(chain) - 1):
n_from = chain[i].get("step", i + 1)
n_to = chain[i + 1].get("step", i + 2)
if n_from not in node_ids or n_to not in node_ids:
continue
from_x = node_ids[n_from]["x"] + NODE_W
to_x = node_ids[n_to]["x"]
arr_y = START_Y + NODE_H // 2
# Detect type drift (code→deploy or deploy→code)
type_from = chain[i].get("type", "")
type_to = chain[i + 1].get("type", "")
is_drift = (type_from != type_to)
arrow_color = "#e74c3c" if is_drift else "#495057"
arr_id = make_id()
elements.append({
"id": arr_id,
"type": "arrow",
"x": from_x,
"y": arr_y,
"width": to_x - from_x,
"height": 0,
"points": [[0, 0], [to_x - from_x, 0]],
"strokeColor": arrow_color,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": is_drift and 3 or 2,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": i + 400,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
"startBinding": None,
"endBinding": None,
"lastCommittedPoint": None,
"startArrowhead": None,
"endArrowhead": "arrow",
})
# Drift label
if is_drift:
mid_x = from_x + (to_x - from_x) // 2 - 40
elements.append({
"id": make_id(),
"type": "text",
"x": mid_x,
"y": arr_y - 22,
"width": 100,
"height": 18,
"text": f"⚠️ {type_from}→{type_to}",
"fontSize": 11,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "top",
"strokeColor": "#e74c3c",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": i + 500,
"version": 1,
"isDeleted": False,
"groupIds": [],
"boundElements": [],
"updated": int(time.time()),
"link": None,
"locked": False,
})
excalidraw = {
"type": "excalidraw",
"version": 2,
"source": "brain/diagram-init.sh",
"elements": elements,
"appState": {
"gridSize": None,
"viewBackgroundColor": COLOR_BG_PAGE,
},
"files": {}
}
with open(output_path, "w") as f:
json.dump(excalidraw, f, indent=2, ensure_ascii=False)
print(f"✅ {output_path}")
print(f" {len(chain)} steps — {len(elements)} éléments générés")
PYEOF
STATUS=$?
if [[ $STATUS -eq 0 ]]; then
echo ""
echo "→ Ouvrir dans draw.tetardtek.com ou commiter :"
echo " git -C $BRAIN_ROOT/draw add diagrams/${WORKFLOW_NAME}.excalidraw"
echo " git -C $BRAIN_ROOT/draw commit -m \"diagram: init ${WORKFLOW_NAME}\""
fi
exit $STATUS

119
scripts/diagram-patch.sh Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# diagram-patch.sh — Patche un nœud dans un .excalidraw après signal BSI
# Usage : bash scripts/diagram-patch.sh <workflow-name> <step> <status>
# Status : done | gate | blocked | locked | circuit-break | abort
# Exemple : bash scripts/diagram-patch.sh superoauth-tier3 1 done
BRAIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKFLOW_NAME="${1:-}"
STEP="${2:-}"
STATUS="${3:-}"
if [[ -z "$WORKFLOW_NAME" || -z "$STEP" || -z "$STATUS" ]]; then
echo "Usage : bash scripts/diagram-patch.sh <workflow-name> <step> <status>"
echo ""
echo "Status disponibles :"
echo " done → ✅ vert — step terminé"
echo " gate → ⚡ orange — gate:human en attente"
echo " blocked → ❌ rouge — BLOCKED_ON"
echo " locked → ⬜ gris — pas encore atteint"
echo " circuit-break → 🔴 rouge vif + bordure épaisse"
echo " abort → grisé — workflow aborted"
exit 1
fi
EXCALIDRAW="$BRAIN_ROOT/draw/diagrams/${WORKFLOW_NAME}.excalidraw"
if [[ ! -f "$EXCALIDRAW" ]]; then
echo "❌ Fichier introuvable : $EXCALIDRAW"
echo " → bash scripts/diagram-init.sh $WORKFLOW_NAME"
exit 1
fi
python3 - "$EXCALIDRAW" "$WORKFLOW_NAME" "$STEP" "$STATUS" << 'PYEOF'
import sys
import json
import time
excalidraw_path = sys.argv[1]
workflow_name = sys.argv[2]
step = sys.argv[3]
status = sys.argv[4]
# Color + label mapping
STATUS_MAP = {
"done": {"color": "#2ecc71", "label": "✅ done", "stroke": "#1a9e57", "width": 2},
"gate": {"color": "#f39c12", "label": "⚡ gate:human", "stroke": "#c87f0a", "width": 2},
"blocked": {"color": "#e74c3c", "label": "❌ blocked", "stroke": "#c0392b", "width": 2},
"locked": {"color": "#868e96", "label": "⬜ pending", "stroke": "#343a40", "width": 2},
"circuit-break": {"color": "#c0392b", "label": "🔴 circuit break","stroke": "#922b21", "width": 4},
"abort": {"color": "#adb5bd", "label": "aborted", "stroke": "#6c757d", "width": 1},
}
if status not in STATUS_MAP:
print(f"❌ Status inconnu : {status}")
print(f" Valeurs valides : {', '.join(STATUS_MAP.keys())}")
sys.exit(1)
cfg = STATUS_MAP[status]
node_id = f"{workflow_name}-step-{step}"
with open(excalidraw_path) as f:
data = json.load(f)
patched = False
elements = data.get("elements", [])
for el in elements:
if el.get("id") == node_id and el.get("type") == "rectangle":
el["backgroundColor"] = cfg["color"]
el["strokeColor"] = cfg["stroke"]
el["strokeWidth"] = cfg["width"]
el["updated"] = int(time.time())
patched = True
break
if not patched:
print(f"⚠️ Nœud introuvable : {node_id}")
print(f" → Vérifier que diagram-init.sh a bien été lancé pour ce workflow")
sys.exit(1)
# Update label text for the matching text element (right after the rectangle)
target_x = None
target_y = None
for el in elements:
if el.get("id") == node_id:
target_x = el["x"]
target_y = el["y"]
break
if target_x is not None:
for el in elements:
if (el.get("type") == "text"
and abs(el.get("x", 0) - target_x - 10) < 5
and abs(el.get("y", 0) - target_y - 8) < 5):
# Replace last line (status line) in the text
lines = el.get("text", "").split("\n")
if len(lines) >= 3:
lines[-1] = cfg["label"]
elif len(lines) > 0:
lines.append(cfg["label"])
el["text"] = "\n".join(lines)
el["updated"] = int(time.time())
break
data["elements"] = elements
with open(excalidraw_path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"✅ {workflow_name} step {step} → {cfg['label']}")
PYEOF
PATCH_STATUS=$?
if [[ $PATCH_STATUS -eq 0 ]]; then
echo "→ Commiter le patch :"
echo " git -C $BRAIN_ROOT/draw add diagrams/${WORKFLOW_NAME}.excalidraw"
echo " git -C $BRAIN_ROOT/draw commit -m \"diagram: ${WORKFLOW_NAME} step ${STEP}${STATUS}\""
fi
exit $PATCH_STATUS

106
scripts/feature-gate-status.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
# feature-gate-status.sh — État du feature-gate (tier actif + features enabled/disabled)
# Lecture seule. Aucune écriture.
#
# Usage :
# bash scripts/feature-gate-status.sh
set -uo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
COMPOSE_FILE="$BRAIN_ROOT/brain-compose.local.yml"
# --- Lire le tier actif ---
_get_tier() {
[ -f "$COMPOSE_FILE" ] || { echo "free"; return; }
local tier="free"
if command -v python3 &>/dev/null && python3 -c "import yaml" &>/dev/null 2>&1; then
tier=$(BRAIN_COMPOSE="$COMPOSE_FILE" python3 - <<'PYEOF' 2>/dev/null
import yaml, os, sys
path = os.environ.get('BRAIN_COMPOSE', '')
try:
with open(path) as f:
data = yaml.safe_load(f)
instances = data.get('instances', {})
for name, inst in instances.items():
if inst.get('active'):
print(inst.get('feature_set', {}).get('tier', 'free'))
sys.exit(0)
except Exception:
pass
print('free')
PYEOF
)
else
tier=$(grep "^\s*tier:" "$COMPOSE_FILE" | head -1 | awk '{print $NF}' | tr -d "'\"")
fi
echo "${tier:-free}"
}
_tier_level() {
case "$1" in
free) echo 0 ;;
pro) echo 1 ;;
full) echo 2 ;;
*) echo 0 ;;
esac
}
# --- Mapping complet feature → tier minimum ---
declare -A FEATURE_MIN=(
[kernel.boot]="free"
[kernel.agents]="free"
[workflow.manual]="free"
[diagram.readonly]="free"
[bact.enrichment]="pro"
[workflow.orchestrated]="pro"
[diagram.interactive]="pro"
[supervisor.project]="pro"
[bact.rag]="full"
[diagram.actions]="full"
[distillation]="full"
)
# Ordre d'affichage
FEATURE_ORDER=(
kernel.boot kernel.agents workflow.manual diagram.readonly
bact.enrichment workflow.orchestrated diagram.interactive supervisor.project
bact.rag diagram.actions distillation
)
# --- Main ---
TIER=$(_get_tier)
LEVEL=$(_tier_level "$TIER")
echo "feature-gate — tier: $TIER"
echo "──────────────────────────────────────────────"
ENABLED_LIST=()
DISABLED_LIST=()
for feature in "${FEATURE_ORDER[@]}"; do
min_tier="${FEATURE_MIN[$feature]}"
required=$(_tier_level "$min_tier")
if [ "$LEVEL" -ge "$required" ]; then
ENABLED_LIST+=("$feature")
else
DISABLED_LIST+=("$feature (requires: $min_tier)")
fi
done
if [ "${#ENABLED_LIST[@]}" -gt 0 ]; then
echo " ✅ Enabled"
for f in "${ENABLED_LIST[@]}"; do
echo " + $f"
done
fi
if [ "${#DISABLED_LIST[@]}" -gt 0 ]; then
echo " ❌ Disabled"
for f in "${DISABLED_LIST[@]}"; do
echo " - $f"
done
fi
echo "──────────────────────────────────────────────"
echo " ${#ENABLED_LIST[@]} enabled / ${#DISABLED_LIST[@]} disabled"

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# file-lock.sh — Mutex fichier BSI-v3-7
# file-lock.sh — Mutex fichier BSI-v3-7 (ADR-036 : brain.db)
# Empêche deux satellites d'écrire simultanément dans le même fichier.
# Complète le scope-lock BSI (niveau dossier) avec une granularité fichier.
# Source : table locks dans brain.db (ex : locks/*.lock)
#
# Usage :
# file-lock.sh acquire <filepath> <sess-id> [ttl_minutes] → acquiert le lock
@@ -18,15 +18,20 @@
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
LOCKS_DIR="$BRAIN_ROOT/locks"
DB_PATH="$BRAIN_ROOT/brain.db"
DEFAULT_TTL=60 # minutes
mkdir -p "$LOCKS_DIR"
# Convertit un chemin fichier en nom de lock (remplace / et . par -)
filepath_to_lockname() {
echo "$1" | sed 's|/|-|g' | sed 's|\.|-|g' | sed 's|^-||'
}
# Init table si absente
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -script "
CREATE TABLE IF NOT EXISTS locks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filepath TEXT NOT NULL UNIQUE,
holder TEXT NOT NULL,
claimed_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
ttl_min INTEGER NOT NULL DEFAULT 60
);
"
# --- ACQUIRE ---
cmd_acquire() {
@@ -34,44 +39,37 @@ cmd_acquire() {
local sess_id="$2"
local ttl="${3:-$DEFAULT_TTL}"
local lockname
lockname=$(filepath_to_lockname "$filepath")
local lockfile="$LOCKS_DIR/${lockname}.lock"
local now
now=$(date +%s)
local expires_at
expires_at=$(date -d "+${ttl} minutes" +%Y-%m-%dT%H:%M 2>/dev/null \
|| date -v+${ttl}M +%Y-%m-%dT%H:%M) # macOS compat
# Check existing active lock held by someone else
local existing
existing=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "
SELECT holder, expires_at FROM locks
WHERE filepath = '$filepath'
AND julianday('now') < julianday(expires_at)
AND holder != '$sess_id'
LIMIT 1;
")
# Vérifier si lock existant et non expiré
if [ -f "$lockfile" ]; then
existing_holder=$(grep '^holder:' "$lockfile" | sed 's/holder: //')
existing_expires=$(grep '^expires_at:' "$lockfile" | sed 's/expires_at: //')
existing_epoch=$(date -d "$existing_expires" +%s 2>/dev/null \
|| date -j -f "%Y-%m-%dT%H:%M" "$existing_expires" +%s 2>/dev/null || echo 0)
if [ "$now" -lt "$existing_epoch" ]; then
echo "🔴 LOCK — $filepath"
echo " Détenu par : $existing_holder"
echo " Expire à : $existing_expires"
echo ""
echo " Attendre le release ou contacter : $existing_holder"
exit 1
else
# Lock expiré — on peut le prendre
echo "⚠️ Lock expiré de $existing_holder — acquisition automatique"
rm -f "$lockfile"
fi
if [ -n "$existing" ]; then
local holder expires
holder=$(echo "$existing" | cut -d'|' -f1)
expires=$(echo "$existing" | cut -d'|' -f2)
echo "🔴 LOCK — $filepath"
echo " Détenu par : $holder"
echo " Expire à : $expires"
echo ""
echo " Attendre le release ou contacter : $holder"
exit 1
fi
# Écrire le lock
cat > "$lockfile" << EOF
file: $filepath
holder: $sess_id
claimed_at: $(date +%Y-%m-%dT%H:%M)
expires_at: $expires_at
ttl_min: $ttl
EOF
# Upsert — remplace si même holder ou expiré
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -script "
DELETE FROM locks WHERE filepath = '$filepath';
INSERT INTO locks (filepath, holder, claimed_at, expires_at, ttl_min)
VALUES ('$filepath', '$sess_id', datetime('now'), datetime('now', '+$ttl minutes'), $ttl);
"
local expires_at
expires_at=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "SELECT expires_at FROM locks WHERE filepath = '$filepath';")
echo "✅ Lock acquis : $filepath"
echo " Session : $sess_id"
@@ -83,22 +81,20 @@ cmd_release() {
local filepath="$1"
local sess_id="$2"
local lockname
lockname=$(filepath_to_lockname "$filepath")
local lockfile="$LOCKS_DIR/${lockname}.lock"
local holder
holder=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "SELECT holder FROM locks WHERE filepath = '$filepath';")
if [ ! -f "$lockfile" ]; then
if [ -z "$holder" ]; then
echo " Pas de lock actif sur : $filepath"
exit 0
fi
existing_holder=$(grep '^holder:' "$lockfile" | sed 's/holder: //')
if [ "$existing_holder" != "$sess_id" ]; then
echo "🚨 Release refusé — lock détenu par : $existing_holder (pas $sess_id)"
if [ "$holder" != "$sess_id" ]; then
echo "🚨 Release refusé — lock détenu par : $holder (pas $sess_id)"
exit 2
fi
rm -f "$lockfile"
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -exec "DELETE FROM locks WHERE filepath = '$filepath' AND holder = '$sess_id'"
echo "✅ Lock libéré : $filepath"
}
@@ -106,88 +102,67 @@ cmd_release() {
cmd_check() {
local filepath="$1"
local lockname
lockname=$(filepath_to_lockname "$filepath")
local lockfile="$LOCKS_DIR/${lockname}.lock"
local row
row=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "
SELECT holder, expires_at,
CASE WHEN julianday('now') < julianday(expires_at) THEN 'active' ELSE 'expired' END
FROM locks WHERE filepath = '$filepath';
")
if [ ! -f "$lockfile" ]; then
if [ -z "$row" ]; then
echo "✅ Libre : $filepath"
exit 0
fi
local now
now=$(date +%s)
existing_holder=$(grep '^holder:' "$lockfile" | sed 's/holder: //')
existing_expires=$(grep '^expires_at:' "$lockfile" | sed 's/expires_at: //')
existing_epoch=$(date -d "$existing_expires" +%s 2>/dev/null \
|| date -j -f "%Y-%m-%dT%H:%M" "$existing_expires" +%s 2>/dev/null || echo 0)
local holder expires status
holder=$(echo "$row" | cut -d'|' -f1)
expires=$(echo "$row" | cut -d'|' -f2)
status=$(echo "$row" | cut -d'|' -f3)
if [ "$now" -lt "$existing_epoch" ]; then
if [ "$status" = "active" ]; then
echo "🔴 Locké : $filepath"
echo " Holder : $existing_holder"
echo " Expire : $existing_expires"
echo " Holder : $holder"
echo " Expire : $expires"
else
echo "⚠️ Lock expiré (nettoyable) : $filepath"
echo " Ancien holder : $existing_holder"
echo " Ancien holder : $holder"
fi
}
# --- LIST ---
cmd_list() {
local locks
locks=$(find "$LOCKS_DIR" -name "*.lock" | sort)
local rows
rows=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "
SELECT filepath, holder, expires_at,
CASE WHEN julianday('now') < julianday(expires_at) THEN 'actif' ELSE 'expiré' END
FROM locks ORDER BY claimed_at DESC;
")
if [ -z "$locks" ]; then
if [ -z "$rows" ]; then
echo "✅ Aucun lock actif"
exit 0
fi
local now
now=$(date +%s)
echo "Locks actifs :"
echo ""
while IFS= read -r lockfile; do
local file holder expires_at epoch status
file=$(grep '^file:' "$lockfile" | sed 's/file: *//')
holder=$(grep '^holder:' "$lockfile" | sed 's/holder: *//')
expires_at=$(grep '^expires_at:' "$lockfile" | sed 's/expires_at: *//')
epoch=$(date -d "$expires_at" +%s 2>/dev/null \
|| date -j -f "%Y-%m-%dT%H:%M" "$expires_at" +%s 2>/dev/null || echo 0)
if [ "$now" -lt "$epoch" ]; then
status="🔴 actif"
else
status="⚠️ expiré"
fi
echo " $status | $file | $holder | exp: $expires_at"
done <<< "$locks"
while IFS='|' read -r filepath holder expires status; do
local icon="🔴"
[ "$status" = "expiré" ] && icon="⚠️ "
echo " $icon $status | $filepath | $holder | exp: $expires"
done <<< "$rows"
}
# --- CLEANUP ---
cmd_cleanup() {
local now
now=$(date +%s)
local count=0
for lockfile in "$LOCKS_DIR"/*.lock; do
[ -f "$lockfile" ] || continue
expires_at=$(grep '^expires_at:' "$lockfile" | sed 's/expires_at: *//')
epoch=$(date -d "$expires_at" +%s 2>/dev/null \
|| date -j -f "%Y-%m-%dT%H:%M" "$expires_at" +%s 2>/dev/null || echo 0)
if [ "$now" -ge "$epoch" ]; then
file=$(grep '^file:' "$lockfile" | sed 's/file: *//')
rm -f "$lockfile"
echo "🗑️ Lock expiré supprimé : $file"
count=$((count + 1))
fi
done
local count
count=$(python3 "$BRAIN_ROOT/scripts/bsi-db.py" "
SELECT COUNT(*) FROM locks WHERE julianday('now') >= julianday(expires_at);
")
if [ "$count" -eq 0 ]; then
echo "✅ Aucun lock expiré à nettoyer"
else
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -exec "DELETE FROM locks WHERE julianday('now') >= julianday(expires_at)"
echo "$count lock(s) nettoyé(s)"
fi
}

View File

@@ -25,7 +25,7 @@ set -euo pipefail
# Configuration — à adapter si besoin
# ---------------------------------------------------------------------------
WATCH_ROOT="${VPS_WATCH_ROOT:-$HOME/brain-watch}"
WATCH_ROOT="/home/tetardtek/brain-watch"
MYSECRETS="$WATCH_ROOT/MYSECRETS"
BOT_PORT=5001
BOT_SCRIPT="$WATCH_ROOT/brain-bot.py"
@@ -62,7 +62,7 @@ fi
# ---------------------------------------------------------------------------
echo ""
echo "Domaine pour le webhook (ex: bot.<OWNER_DOMAIN>) :"
echo "Domaine pour le webhook (ex: bot.tetardtek.com) :"
echo -n "→ "
read -r BOT_DOMAIN
@@ -94,7 +94,7 @@ After=network.target
[Service]
Type=simple
User=${VPS_SERVICE_USER:-$(whoami)}
User=tetardtek
WorkingDirectory=${WATCH_ROOT}
Environment=BRAIN_WATCH_ROOT=${WATCH_ROOT}
Environment=BRAIN_BOT_PORT=${BOT_PORT}

View File

@@ -6,7 +6,7 @@
# scripts/install-brain-hooks.sh --check → vérifie si les hooks sont installés
#
# Hooks installés :
# post-commit → déclenche brain-db-sync.sh si claims/ handoffs/ ou BRAIN-INDEX.md changent
# post-commit → déclenche brain-db-sync.sh si handoffs/ agents/ ou BRAIN-INDEX.md changent
#
# Idempotent — peut être relancé sans risque.
# À relancer sur chaque clone frais (hooks non versionnés dans git).
@@ -46,7 +46,7 @@ if [[ -f "$POST_COMMIT" ]] && ! grep -q "brain-db-sync" "$POST_COMMIT"; then
cat >> "$POST_COMMIT" <<'HOOK'
# Déclenche brain-db-sync.sh si claims, handoffs ou BRAIN-INDEX ont changé
_brain_changed=$(git diff HEAD~1 --name-only 2>/dev/null \
| grep -qE '^(claims/|handoffs/|BRAIN-INDEX\.md)' && echo yes || echo no)
| grep -qE '^(handoffs/|agents/|BRAIN-INDEX\.md)' && echo yes || echo no)
if [[ "$_brain_changed" == "yes" ]]; then
BRAIN_ROOT="$(git rev-parse --show-toplevel)"
bash "$BRAIN_ROOT/scripts/brain-db-sync.sh" --quiet || true
@@ -61,7 +61,7 @@ else
# Sync brain.db si claims, handoffs ou BRAIN-INDEX ont changé
_brain_changed=$(git diff HEAD~1 --name-only 2>/dev/null \
| grep -qE '^(claims/|handoffs/|BRAIN-INDEX\.md)' && echo yes || echo no)
| grep -qE '^(handoffs/|agents/|BRAIN-INDEX\.md)' && echo yes || echo no)
if [[ "$_brain_changed" == "yes" ]]; then
BRAIN_ROOT="$(git rev-parse --show-toplevel)"
bash "$BRAIN_ROOT/scripts/brain-db-sync.sh" --quiet || true
@@ -73,6 +73,6 @@ fi
echo ""
echo "Hooks brain actifs :"
echo " post-commit → brain-db-sync.sh (déclenché sur claims/ handoffs/ BRAIN-INDEX.md)"
echo " post-commit → brain-db-sync.sh (déclenché sur handoffs/ agents/ BRAIN-INDEX.md)"
echo ""
echo "Pour vérifier : scripts/install-brain-hooks.sh --check"

View File

@@ -13,13 +13,8 @@ TARGET="${1:-both}"
BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Brain}"
VPS_USER="root"
VPS_IP=$(grep '^VPS_IP=' "$BRAIN_ROOT/MYSECRETS" | cut -d= -f2-)
# Configurable — lues depuis MYSECRETS si non définies en env
VPS_WATCH_ROOT="${VPS_WATCH_ROOT:-$(grep '^VPS_WATCH_ROOT=' "$BRAIN_ROOT/MYSECRETS" 2>/dev/null | cut -d= -f2- || echo "/home/$VPS_USER/brain-watch")}"
GITEA_BRAIN_URL="${BRAIN_GIT_URL:-$(grep '^BRAIN_GIT_URL=' "$BRAIN_ROOT/MYSECRETS" 2>/dev/null | cut -d= -f2-)}"
if [[ -z "$GITEA_BRAIN_URL" ]]; then
echo "❌ BRAIN_GIT_URL manquant — ajouter dans MYSECRETS : BRAIN_GIT_URL=git@<host>:<user>/brain.git"
exit 1
fi
VPS_WATCH_ROOT="/home/tetardtek/brain-watch"
GITEA_BRAIN_URL="git@git.tetardtek.com:Tetardtek/brain.git"
install_local() {
echo "=== Installation SUPERVISOR local (systemd user) ==="
@@ -106,7 +101,7 @@ After=network.target
[Service]
Type=simple
User=root
ExecStart=/home/<user>/brain-watch/brain-watch-vps.sh
ExecStart=/home/tetardtek/brain-watch/brain-watch-vps.sh
Restart=always
RestartSec=10
StandardOutput=journal

View File

@@ -27,7 +27,7 @@ ERROR_PATTERNS=(
)
# Patterns de chemin absolu — exclusions pour les placeholders templates
ABSOLUTE_PATH_PATTERN="/home/[a-z]" # ex: /home/alice — chemin réel, pas /home/<user>
ABSOLUTE_PATH_PATTERN="/home/[a-z]" # /home/tetardtek — chemin réel, pas /home/<user>
ABSOLUTE_PATH_EXCLUDE="<" # Exclure les lignes avec placeholder (<user>, <PATHS...)
# --- Patterns WARN : références documentaires — OK si contexte architecture ---
@@ -59,7 +59,7 @@ for pattern in "${ERROR_PATTERNS[@]}"; do
fi
done
# --- Scan ERROR — chemins absolus réels (ex: /home/<user>/, pas /home/<user>/) ---
# --- Scan ERROR — chemins absolus réels (ex: /home/tetardtek/, pas /home/<user>/) ---
while IFS= read -r -d '' file; do
# Cherche /home/[a-z] et exclut les lignes avec placeholder <
matches=$(grep -n "$ABSOLUTE_PATH_PATTERN" "$file" 2>/dev/null \

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""
migrate-claims-to-db.py — Migration one-shot : claims/*.yml → brain.db
ADR-036 : BSI hors git — les claims deviennent la source de vérité dans brain.db.
Usage :
python3 scripts/migrate-claims-to-db.py → migrer tout
python3 scripts/migrate-claims-to-db.py --dry-run → preview sans écriture
python3 scripts/migrate-claims-to-db.py --archive → migrer + archiver les .yml
Idempotent : INSERT OR IGNORE sur sess_id PRIMARY KEY.
"""
import os
import re
import sys
import sqlite3
import shutil
from pathlib import Path
from datetime import datetime, timedelta
BRAIN_ROOT = Path(__file__).parent.parent
CLAIMS_DIR = BRAIN_ROOT / 'claims'
DB_PATH = BRAIN_ROOT / 'brain.db'
ARCHIVE_DIR = BRAIN_ROOT / 'archive' / 'claims-git-era'
# Kernel scopes — synchronisé avec KERNEL.md
KERNEL_SCOPES = ['agents/', 'profil/', 'scripts/', 'KERNEL.md',
'brain-constitution.md', 'brain-compose.yml']
PERSONAL_SCOPES = ['profil/capital', 'profil/objectifs', 'progression/', 'MYSECRETS']
def extract(content, *patterns, default=''):
"""Extract first matching pattern from content."""
for p in patterns:
m = re.search(p, content, re.MULTILINE)
if m:
return m.group(1).strip().strip('"\'')
return default
def infer_zone(scope):
"""Infer zone from scope — ADR-014."""
for ks in KERNEL_SCOPES:
if ks in scope:
return 'kernel'
for ps in PERSONAL_SCOPES:
if ps in scope:
return 'personal'
return 'project'
def parse_claim(filepath):
"""Parse a claim YAML file into a dict."""
with open(filepath, 'r') as f:
content = f.read()
sess_id = extract(content, r'^sess_id:\s*(.+)', r'^name:\s*(sess-.+)')
if not sess_id:
return None
scope = extract(content, r'^scope:\s*(.+)')
status = extract(content, r'^status:\s*(.+)', default='closed')
opened_at = extract(content, r'^opened_at:\s*(.+)', r'^opened:\s*(.+)')
type_ = extract(content, r'^type:\s*(.+)', default='work')
handoff = extract(content, r'^handoff_level:\s*(.+)')
story = extract(content, r'^story_angle:\s*(.+)')
parent = extract(content, r'^parent_satellite:\s*(.+)')
sat_type = extract(content, r'^satellite_type:\s*(.+)')
sat_level = extract(content, r'^satellite_level:\s*(.+)')
theme_branch = extract(content, r'^theme_branch:\s*(.+)')
zone = extract(content, r'^zone:\s*(.+)') or infer_zone(scope)
mode = extract(content, r'^mode:\s*(.+)')
# Check if TTL expired → mark stale
if status == 'open' and opened_at:
try:
opened_dt = datetime.fromisoformat(opened_at.replace('Z', '+00:00'))
if datetime.now(opened_dt.tzinfo or None) - opened_dt.replace(tzinfo=None) > timedelta(hours=4):
status = 'stale'
except (ValueError, TypeError):
pass
return {
'sess_id': sess_id,
'type': type_,
'scope': scope,
'status': status,
'opened_at': opened_at,
'handoff_level': handoff or None,
'story_angle': story or None,
'parent_sess': parent or None,
'satellite_type': sat_type or None,
'satellite_level': sat_level or None,
'theme_branch': theme_branch or None,
'zone': zone,
'mode': mode or None,
'ttl_hours': 4,
}
def main():
dry_run = '--dry-run' in sys.argv
archive = '--archive' in sys.argv
if not CLAIMS_DIR.exists():
print(f"❌ claims/ introuvable : {CLAIMS_DIR}")
sys.exit(1)
yml_files = sorted(CLAIMS_DIR.glob('sess-*.yml'))
print(f"📦 {len(yml_files)} fichiers claims trouvés")
if dry_run:
print(" (mode dry-run — aucune écriture)")
conn = sqlite3.connect(str(DB_PATH))
conn.execute("PRAGMA journal_mode=WAL")
migrated = 0
skipped = 0
stale_marked = 0
errors = 0
for yml in yml_files:
claim = parse_claim(yml)
if not claim:
print(f" ⚠️ SKIP {yml.name} — pas de sess_id")
skipped += 1
continue
if claim['status'] == 'stale':
stale_marked += 1
if dry_run:
print(f"{claim['sess_id']} | {claim['status']} | {claim['scope'][:40]}")
migrated += 1
continue
try:
conn.execute("""
INSERT OR IGNORE INTO claims
(sess_id, type, scope, status, opened_at, handoff_level,
story_angle, parent_sess, satellite_type, satellite_level,
theme_branch, zone, mode, ttl_hours)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
claim['sess_id'], claim['type'], claim['scope'],
claim['status'], claim['opened_at'], claim['handoff_level'],
claim['story_angle'], claim['parent_sess'],
claim['satellite_type'], claim['satellite_level'],
claim['theme_branch'], claim['zone'], claim['mode'],
claim['ttl_hours'],
))
migrated += 1
except Exception as e:
print(f" ❌ ERROR {yml.name} : {e}")
errors += 1
conn.commit()
conn.close()
print(f"\n✅ Migration terminée :")
print(f" Migrés : {migrated}")
print(f" Skippés : {skipped}")
print(f" Stale : {stale_marked} (open > 4h → marqués stale)")
print(f" Erreurs : {errors}")
if archive and not dry_run:
ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
for yml in yml_files:
shutil.move(str(yml), str(ARCHIVE_DIR / yml.name))
print(f"\n📁 {len(yml_files)} fichiers archivés → {ARCHIVE_DIR}")
print(" → Ajouter 'claims/' à .gitignore pour finaliser")
if __name__ == '__main__':
main()

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# preflight-check.sh — BSI-v3-8 Pre-flight check
# preflight-check.sh — BSI-v3-8 Pre-flight check (ADR-036 : brain.db)
# Valide les 6 conditions avant qu'un satellite commence à écrire.
# Soft-lock kernel : tout satellite hors scope kernel est bloqué sur zone:kernel.
# Source : tables claims, locks, circuit_breaker dans brain.db
#
# Usage :
# preflight-check.sh check <sess_id> <filepath> → 6 checks, exit 0 = go
@@ -21,14 +21,29 @@
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
CLAIMS_DIR="$BRAIN_ROOT/claims"
LOCKS_DIR="$BRAIN_ROOT/locks"
FAILS_DIR="$BRAIN_ROOT/locks/fails"
DB_PATH="$BRAIN_ROOT/brain.db"
# Chemins zone:kernel — synchronisés avec KERNEL.md + brain-index-regen.sh
# Chemins zone:kernel — synchronisés avec KERNEL.md
KERNEL_SCOPES="agents/ profil/ scripts/ KERNEL.md CLAUDE.md PATHS.md brain-compose.yml brain-constitution.md BRAIN-INDEX.md"
mkdir -p "$FAILS_DIR"
# Init tables si absentes
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -script "
CREATE TABLE IF NOT EXISTS circuit_breaker (
sess_id TEXT PRIMARY KEY,
fail_count INTEGER NOT NULL DEFAULT 0,
last_fail_at TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
"
# Helper : query brain.db (SELECT → stdout)
q() {
python3 "$BRAIN_ROOT/scripts/bsi-db.py" "$1"
}
# Helper : write brain.db (INSERT/UPDATE/DELETE)
qw() {
python3 "$BRAIN_ROOT/scripts/bsi-db.py" -exec "$1"
}
# Détermine si un filepath est zone:kernel
is_kernel_path() {
@@ -59,20 +74,16 @@ cmd_check() {
local sess_id="$1"
local filepath="$2"
local claim_file="$CLAIMS_DIR/${sess_id}.yml"
local fail_count=0
local all_ok=true
echo "🛫 PRE-FLIGHT — $sess_id$filepath"
echo ""
# CHECK 1 — Claim status
if [ ! -f "$claim_file" ]; then
local claim_status
claim_status=$(q "SELECT status FROM claims WHERE sess_id = '$sess_id';")
if [ -z "$claim_status" ]; then
echo "❌ CHECK 1 — Claim introuvable : $sess_id"
exit 4
fi
local claim_status
claim_status=$(grep '^status:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"' | head -1)
if [ "$claim_status" = "paused" ]; then
echo "❌ CHECK 1 — Claim en pause : $sess_id"
echo " → human-gate-ack.sh resume $sess_id"
@@ -91,28 +102,25 @@ cmd_check() {
# CHECK 1b — Cascade pause (parent paused = enfant bloqué)
local parent_id
parent_id=$(grep '^parent_satellite:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"' 2>/dev/null || echo "")
parent_id=$(q "SELECT parent_sess FROM claims WHERE sess_id = '$sess_id';")
if [ -n "$parent_id" ]; then
local parent_file="$CLAIMS_DIR/${parent_id}.yml"
if [ -f "$parent_file" ]; then
local parent_status
parent_status=$(grep '^status:' "$parent_file" | sed 's/^[^:]*: *//' | tr -d '"' | head -1)
if [ "$parent_status" = "paused" ]; then
echo "❌ CHECK 1b — Parent en pause : $parent_id"
echo " → human-gate-ack.sh resume $parent_id"
exit 4
fi
if [ "$parent_status" = "failed" ]; then
echo "❌ CHECK 1b — Parent failed : $parent_id — satellite orphelin"
exit 4
fi
local parent_status
parent_status=$(q "SELECT status FROM claims WHERE sess_id = '$parent_id';")
if [ "$parent_status" = "paused" ]; then
echo "❌ CHECK 1b — Parent en pause : $parent_id"
echo " → human-gate-ack.sh resume $parent_id"
exit 4
fi
if [ "$parent_status" = "failed" ]; then
echo "❌ CHECK 1b — Parent failed : $parent_id — satellite orphelin"
exit 4
fi
echo "✅ CHECK 1b — Parent ok"
fi
[ -n "$parent_id" ] && echo "✅ CHECK 1b — Parent ok" || true
# CHECK 2 — Scope check
local claim_scope
claim_scope=$(grep '^scope:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"')
claim_scope=$(q "SELECT scope FROM claims WHERE sess_id = '$sess_id';")
local scope_ok=false
for scope_entry in $claim_scope; do
if [[ "$filepath" == ${scope_entry}* ]] || [[ "$filepath" == "$scope_entry" ]]; then
@@ -127,8 +135,6 @@ cmd_check() {
echo "✅ CHECK 2 — Scope ok"
# CHECK 3 — Zone check (soft lock kernel)
# Un satellite dont le scope n'est pas kernel ne peut pas écrire en zone:kernel.
# Exception : kerneluser:true → WARNING (pas de blocage) — owner confirme lui-même.
if is_kernel_path "$filepath"; then
if ! scope_is_kernel "$claim_scope"; then
local kerneluser
@@ -139,7 +145,6 @@ cmd_check() {
else
echo "❌ CHECK 3 — Zone violation : $filepath est zone:kernel"
echo " Scope déclaré [$claim_scope] n'inclut pas de zone:kernel"
echo " → Modification kernel = décision humaine (KERNEL.md règle délégation)"
exit 5
fi
fi
@@ -149,28 +154,26 @@ cmd_check() {
fi
# CHECK 4 — Lock check
local lockname
lockname=$(echo "$filepath" | sed 's|/|-|g' | sed 's|\.|-|g' | sed 's|^-||')
local lockfile="$LOCKS_DIR/${lockname}.lock"
if [ -f "$lockfile" ]; then
local now existing_holder existing_expires existing_epoch
now=$(date +%s)
existing_holder=$(grep '^holder:' "$lockfile" | sed 's/^[^:]*: *//')
existing_expires=$(grep '^expires_at:' "$lockfile" | sed 's/^[^:]*: *//')
existing_epoch=$(date -d "$existing_expires" +%s 2>/dev/null \
|| date -j -f "%Y-%m-%dT%H:%M" "$existing_expires" +%s 2>/dev/null || echo 0)
if [ "$now" -lt "$existing_epoch" ] && [ "$existing_holder" != "$sess_id" ]; then
echo "❌ CHECK 4 — Fichier locké par : $existing_holder (expire : $existing_expires)"
exit 2
fi
local lock_holder
lock_holder=$(q "
SELECT holder FROM locks
WHERE filepath = '$filepath'
AND julianday('now') < julianday(expires_at)
AND holder != '$sess_id'
LIMIT 1;
")
if [ -n "$lock_holder" ]; then
local lock_expires
lock_expires=$(q "SELECT expires_at FROM locks WHERE filepath = '$filepath';")
echo "❌ CHECK 4 — Fichier locké par : $lock_holder (expire : $lock_expires)"
exit 2
fi
echo "✅ CHECK 4 — Lock ok"
# CHECK 5 — Circuit breaker
local fail_count_file="$FAILS_DIR/${sess_id}.count"
if [ -f "$fail_count_file" ]; then
fail_count=$(cat "$fail_count_file")
fi
local fail_count
fail_count=$(q "SELECT COALESCE(fail_count, 0) FROM circuit_breaker WHERE sess_id = '$sess_id';")
fail_count="${fail_count:-0}"
local max_fails
max_fails=$(grep -A5 'circuit_breaker:' "$BRAIN_ROOT/brain-compose.yml" \
| grep 'max_consecutive_fails:' | sed 's/^[^:]*: *//' | awk '{print $1}' | head -1 2>/dev/null || echo 3)
@@ -183,7 +186,7 @@ cmd_check() {
# CHECK 6 — Theme branch
local theme_branch
theme_branch=$(grep '^theme_branch:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"' 2>/dev/null || echo "")
theme_branch=$(q "SELECT COALESCE(theme_branch, '') FROM claims WHERE sess_id = '$sess_id';")
if [ -n "$theme_branch" ]; then
local current_branch
current_branch=$(git -C "$BRAIN_ROOT" branch --show-current 2>/dev/null || echo "")
@@ -202,17 +205,22 @@ cmd_check() {
# --- FAIL (circuit breaker increment) ---
cmd_fail() {
local sess_id="$1"
local fail_count_file="$FAILS_DIR/${sess_id}.count"
local count=0
[ -f "$fail_count_file" ] && count=$(cat "$fail_count_file")
count=$((count + 1))
echo "$count" > "$fail_count_file"
qw "
INSERT INTO circuit_breaker (sess_id, fail_count, last_fail_at, updated_at)
VALUES ('$sess_id', 1, datetime('now'), datetime('now'))
ON CONFLICT(sess_id) DO UPDATE SET
fail_count = fail_count + 1,
last_fail_at = datetime('now'),
updated_at = datetime('now')
"
local fail_count
fail_count=$(q "SELECT fail_count FROM circuit_breaker WHERE sess_id = '$sess_id';")
local max_fails
max_fails=$(grep -A5 'circuit_breaker:' "$BRAIN_ROOT/brain-compose.yml" \
| grep 'max_consecutive_fails:' | sed 's/^[^:]*: *//' | awk '{print $1}' | head -1 2>/dev/null || echo 3)
echo "⚠️ Fail enregistré : $count/$max_fails ($sess_id)"
if [ "$count" -ge "$max_fails" ] 2>/dev/null; then
echo "⚠️ Fail enregistré : $fail_count/$max_fails ($sess_id)"
if [ "$fail_count" -ge "$max_fails" ] 2>/dev/null; then
echo "🔴 Circuit breaker déclenché — signal BLOCKED_ON pilote"
fi
}
@@ -220,24 +228,23 @@ cmd_fail() {
# --- RESET (après succès) ---
cmd_reset() {
local sess_id="$1"
local fail_count_file="$FAILS_DIR/${sess_id}.count"
rm -f "$fail_count_file"
qw "DELETE FROM circuit_breaker WHERE sess_id = '$sess_id'"
echo "✅ Circuit breaker reset : $sess_id"
}
# --- STATUS ---
cmd_status() {
local sess_id="$1"
local fail_count_file="$FAILS_DIR/${sess_id}.count"
local count=0
[ -f "$fail_count_file" ] && count=$(cat "$fail_count_file")
local fail_count
fail_count=$(q "SELECT COALESCE(fail_count, 0) FROM circuit_breaker WHERE sess_id = '$sess_id';")
fail_count="${fail_count:-0}"
local max_fails
max_fails=$(grep -A5 'circuit_breaker:' "$BRAIN_ROOT/brain-compose.yml" \
| grep 'max_consecutive_fails:' | sed 's/^[^:]*: *//' | awk '{print $1}' | head -1 2>/dev/null || echo 3)
if [ "$count" -ge "$max_fails" ] 2>/dev/null; then
echo "🔴 Circuit breaker déclenché : $count/$max_fails ($sess_id)"
if [ "$fail_count" -ge "$max_fails" ] 2>/dev/null; then
echo "🔴 Circuit breaker déclenché : $fail_count/$max_fails ($sess_id)"
else
echo "✅ Circuit breaker ok : $count/$max_fails ($sess_id)"
echo "✅ Circuit breaker ok : $fail_count/$max_fails ($sess_id)"
fi
}

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# sync-secrets-from-vps.sh — Migration one-shot : VPS .env → BrainSecrets/MYSECRETS
# Usage : bash scripts/sync-secrets-from-vps.sh
# Lancer depuis le terminal directement (jamais via Claude)
# Les valeurs ne sont jamais affichées — injection silencieuse
set -e
MYSECRETS="$HOME/Dev/BrainSecrets/MYSECRETS"
VPS_USER=$(grep '^VPS_USER=' "$MYSECRETS" | cut -d= -f2-)
VPS_IP=$(grep '^VPS_IP=' "$MYSECRETS" | cut -d= -f2-)
if [[ -z "$VPS_USER" || -z "$VPS_IP" ]]; then
echo "❌ VPS_USER ou VPS_IP manquant dans MYSECRETS"
exit 1
fi
echo "✅ VPS détecté : $VPS_USER@$VPS_IP"
echo ""
inject() {
local prefix="$1"
local key="$2"
local val="$3"
local full_key="${prefix}${key}"
[[ -z "$val" ]] && return
if grep -q "^${full_key}=" "$MYSECRETS"; then
sed -i "s|^${full_key}=.*|${full_key}=${val}|" "$MYSECRETS"
else
echo "${full_key}=${val}" >> "$MYSECRETS"
fi
}
# ── TetaRdPG ──────────────────────────────────────────────────────────────────
echo "→ TetaRdPG .env..."
while IFS='=' read -r key val; do
[[ -z "$key" || "$key" =~ ^# || -z "$val" ]] && continue
inject "TETARDPG_" "$key" "$val"
done < <(ssh "${VPS_USER}@${VPS_IP}" "cat /home/tetardtek/gitea/TetaRdPG/.env 2>/dev/null")
echo " ✅ TETARDPG_* injectées"
# ── OriginsDigital ────────────────────────────────────────────────────────────
echo "→ OriginsDigital .env..."
while IFS='=' read -r key val; do
[[ -z "$key" || "$key" =~ ^# || -z "$val" ]] && continue
inject "ORIGINSDIGITAL_" "$key" "$val"
done < <(ssh "${VPS_USER}@${VPS_IP}" "cat /var/www/originsdigital/backend/.env 2>/dev/null")
echo " ✅ ORIGINSDIGITAL_* injectées"
# ── MySQL root ────────────────────────────────────────────────────────────────
echo "→ MySQL root password..."
mysql_root=$(ssh "${VPS_USER}@${VPS_IP}" "docker inspect mysql-prod --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep MYSQL_ROOT_PASSWORD | cut -d= -f2-")
inject "" "MYSQL_ROOT_PASSWORD" "$mysql_root"
echo " ✅ MYSQL_ROOT_PASSWORD injectée"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Consolidation terminée — vérifie BrainSecrets/MYSECRETS"
echo " cd ~/Dev/BrainSecrets && git add MYSECRETS && git commit -m 'feat(secrets): consolidation VPS .env' && git push"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

124
scripts/sync-template.sh Executable file
View File

@@ -0,0 +1,124 @@
#!/bin/bash
# sync-template.sh — Synchronise brain/ → brain-template/
# Copie les fichiers kernel en excluant tout ce qui est instance/personnel.
# À lancer après chaque modification kernel significative.
#
# Usage :
# sync-template.sh → sync + rapport
# sync-template.sh --dry → rapport sans écrire
# sync-template.sh --push → sync + commit + push
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
TEMPLATE_DIR="$BRAIN_ROOT/brain-template"
DRY="${1:-}"
PUSH=""
[ "$DRY" = "--push" ] && PUSH=true && DRY=""
if [ ! -d "$TEMPLATE_DIR/.git" ]; then
echo "❌ brain-template/ introuvable ou pas un repo git"
exit 1
fi
echo "🔄 Sync brain → brain-template"
[ -n "$DRY" ] && echo " (dry run — aucune écriture)"
echo ""
# --- Scripts : tout sauf distillation/privé ---
SCRIPTS_EXCLUDE="bsi-server.sh bsi-rag.sh bsi-search.sh brain-bot.py brain-engine.service get-telegram-chatid.sh get-telegram-chatids.sh rotate-oauth-secrets.sh brain-key-server.py brain-key-admin.sh key-guardian.sh"
echo "── scripts/ ────────────────────────────────────"
for f in "$BRAIN_ROOT/scripts/"*.sh "$BRAIN_ROOT/scripts/"*.py; do
[ -f "$f" ] || continue
base=$(basename "$f")
skip=false
for ex in $SCRIPTS_EXCLUDE; do [ "$base" = "$ex" ] && skip=true; done
if [ "$skip" = true ]; then
echo "$base (exclu)"
continue
fi
if [ -z "$DRY" ]; then
cp "$f" "$TEMPLATE_DIR/scripts/"
fi
echo "$base"
done
# --- Agents : tout sauf reviews/ ---
echo ""
echo "── agents/ ─────────────────────────────────────"
if [ -z "$DRY" ]; then
rsync -a --delete --exclude='reviews/' --exclude='bact-scribe.md' \
"$BRAIN_ROOT/agents/" "$TEMPLATE_DIR/agents/"
fi
agent_count=$(ls "$BRAIN_ROOT/agents/"*.md 2>/dev/null | wc -l | tr -d ' ')
echo "$agent_count agents (reviews/ exclu)"
# --- Fichiers kernel racine ---
echo ""
echo "── kernel racine ───────────────────────────────"
KERNEL_FILES="KERNEL.md brain-compose.yml brain-constitution.md"
for f in $KERNEL_FILES; do
if [ -f "$BRAIN_ROOT/$f" ]; then
[ -z "$DRY" ] && cp "$BRAIN_ROOT/$f" "$TEMPLATE_DIR/$f"
echo "$f"
fi
done
# --- Workflows ---
echo ""
echo "── workflows/ ──────────────────────────────────"
if [ -d "$BRAIN_ROOT/workflows" ]; then
if [ -z "$DRY" ]; then
mkdir -p "$TEMPLATE_DIR/workflows"
cp "$BRAIN_ROOT/workflows/_template.yml" "$TEMPLATE_DIR/workflows/" 2>/dev/null || true
cp "$BRAIN_ROOT/workflows/brain-engine.yml" "$TEMPLATE_DIR/workflows/" 2>/dev/null || true
fi
echo " ✅ _template.yml + brain-engine.yml"
fi
# --- Wiki (submodule) ---
echo ""
echo "── wiki/ ───────────────────────────────────────"
WIKI_FILES="multi-instance.md concepts.md patterns.md vocabulary.md session-lifecycle.md cold-start.md"
if [ -d "$BRAIN_ROOT/wiki" ]; then
if [ -z "$DRY" ]; then
mkdir -p "$TEMPLATE_DIR/wiki"
for wf in $WIKI_FILES; do
[ -f "$BRAIN_ROOT/wiki/$wf" ] && cp "$BRAIN_ROOT/wiki/$wf" "$TEMPLATE_DIR/wiki/" && echo "$wf"
done
else
echo " (dry) wiki/$WIKI_FILES"
fi
fi
# --- Gitkeep ---
[ -z "$DRY" ] && mkdir -p "$TEMPLATE_DIR/locks" && \
touch "$TEMPLATE_DIR/locks/.gitkeep"
# --- Isolation check ---
echo ""
echo "── kernel-isolation-check ──────────────────────"
if [ -z "$DRY" ]; then
result=$(bash "$BRAIN_ROOT/scripts/kernel-isolation-check.sh" 2>&1 | tail -3)
echo "$result"
fi
# --- Push ---
if [ -n "$PUSH" ]; then
echo ""
echo "── commit + push ───────────────────────────────"
cd "$TEMPLATE_DIR"
if git diff --quiet && git diff --staged --quiet; then
echo " Aucune modification à commiter"
else
version=$(grep '^version:' "$BRAIN_ROOT/brain-compose.yml" | head -1 | sed 's/version: "//;s/"//')
git add -A
git commit -m "sync: kernel v$version → template"
git push
echo " ✅ Pushé"
fi
fi
echo ""
echo "✅ Sync terminé"

View File

@@ -38,14 +38,13 @@ echo ""
BLOCKERS=()
# --- Check 1 : aucun claim open sur cette branche ---
OPEN_CLAIMS=$(grep -rl "status: open" "$BRAIN_ROOT/claims/" 2>/dev/null || true)
if [ -n "$OPEN_CLAIMS" ]; then
while IFS= read -r claim; do
# Vérifier si le claim référence ce thème ou n'a pas de theme_branch (ambigu)
rel="${claim#$BRAIN_ROOT/}"
BLOCKERS+=(" 🔴 Claim encore ouvert : $rel")
done <<< "$OPEN_CLAIMS"
# --- Check 1 : aucun claim open (ADR-042 — brain.db source unique) ---
OPEN_COUNT=$(bash "$BRAIN_ROOT/scripts/bsi-query.sh" count-open 2>/dev/null || echo "0")
if [ "$OPEN_COUNT" -gt 0 ]; then
OPEN_LIST=$(bash "$BRAIN_ROOT/scripts/bsi-query.sh" open 2>/dev/null || true)
while IFS= read -r line; do
BLOCKERS+=(" 🔴 Claim ouvert : $line")
done <<< "$OPEN_LIST"
fi
# --- Check 2 : aucun signal BLOCKED_ON pending ---

View File

@@ -7,7 +7,7 @@
# bash scripts/workflow-launch.sh <workflow.yml> --step N # step spécifique
# bash scripts/workflow-launch.sh <workflow.yml> --status # état de la chaîne
#
# Le claim généré est affiché + écrit dans claims/ — l'humain lance le satellite.
# Le claim est écrit dans brain.db (ADR-042) — l'humain lance le satellite.
# (Futur : kernel-orchestrator lancera automatiquement — BSI-v3-9)
set -euo pipefail
@@ -53,20 +53,26 @@ echo "📋 Workflow : $THEME_NAME"
echo " Branche : $THEME_BRANCH"
echo ""
# --- Mode status : afficher l'état de la chaîne ---
# --- Mode status : afficher l'état de la chaîne (brain.db — ADR-042) ---
if [ "$MODE" = "status" ]; then
echo "État des claims pour ce thème :"
echo ""
# Trouver les claims qui référencent ce theme_branch
for claim in "$BRAIN_ROOT/claims/"sess-*.yml; do
if grep -q "theme_branch: $THEME_BRANCH" "$claim" 2>/dev/null; then
sess_id=$(grep '^sess_id:' "$claim" | sed 's/sess_id: *//')
status=$(grep '^status:' "$claim" | sed 's/status: *//')
step=$(grep '^workflow_step:' "$claim" 2>/dev/null | sed 's/workflow_step: *//' || echo "?")
result_status=$(grep 'status:' "$claim" | grep -v '^status:' | head -1 | sed 's/.*status: *//' || echo "-")
echo " Step $step$sess_id [$status] result:$result_status"
fi
done
python3 -c "
import sqlite3, sys
conn = sqlite3.connect('$BRAIN_ROOT/brain.db')
conn.row_factory = sqlite3.Row
rows = conn.execute(
'SELECT sess_id, status, workflow_step, result_status FROM claims WHERE theme_branch = ? ORDER BY workflow_step',
('$THEME_BRANCH',)
).fetchall()
conn.close()
if not rows:
print(' (aucun claim pour ce thème)')
for r in rows:
step = r['workflow_step'] or '?'
result = r['result_status'] or '-'
print(f\" Step {step} — {r['sess_id']} [{r['status']}] result:{result}\")
" 2>/dev/null || echo " ⚠️ brain.db inaccessible"
exit 0
fi
@@ -130,19 +136,17 @@ fi
if [ -n "$TARGET_STEP" ]; then
STEP_IDX=$((TARGET_STEP - 1))
else
# Trouver le dernier step complété via les claims
LAST_DONE=0
for claim in "$BRAIN_ROOT/claims/"sess-*.yml; do
if grep -q "theme_branch: $THEME_BRANCH" "$claim" 2>/dev/null; then
if grep -q "status: closed" "$claim" 2>/dev/null; then
claim_step=$(grep '^workflow_step:' "$claim" 2>/dev/null \
| sed 's/workflow_step: *//' || echo "0")
if [ "$claim_step" -gt "$LAST_DONE" ] 2>/dev/null; then
LAST_DONE="$claim_step"
fi
fi
fi
done
# Trouver le dernier step complété via brain.db (ADR-042)
LAST_DONE=$(python3 -c "
import sqlite3
conn = sqlite3.connect('$BRAIN_ROOT/brain.db')
r = conn.execute(
'SELECT MAX(workflow_step) FROM claims WHERE theme_branch = ? AND status = ?',
('$THEME_BRANCH', 'closed')
).fetchone()
conn.close()
print(r[0] if r[0] is not None else 0)
" 2>/dev/null || echo 0)
STEP_IDX=$LAST_DONE
fi
@@ -188,29 +192,25 @@ fi
DATETIME=$(date +%Y%m%d-%H%M)
SCOPE_SLUG=$(echo "$STEP_SCOPE" | tr '/' '-' | sed 's/-$//' | tr '[:upper:]' '[:lower:]')
SESS_ID="sess-${DATETIME}-${THEME_NAME}-step${STEP_NUM}"
CLAIM_FILE="$BRAIN_ROOT/claims/${SESS_ID}.yml"
# Écrire le claim
cat > "$CLAIM_FILE" << EOF
sess_id: $SESS_ID
type: satellite
scope: $STEP_SCOPE
agent: satellite-boot
status: open
opened_at: "$(date +%Y-%m-%dT%H:%M)"
handoff_level: 0
story_angle: "$STEP_ANGLE"
satellite_type: $STEP_TYPE
satellite_level: leaf
parent_satellite: ~
theme_branch: $THEME_BRANCH
workflow: $THEME_NAME
workflow_step: $STEP_NUM
on_done: $ON_DONE
on_fail: $ON_FAIL
EOF
# Écrire le claim dans brain.db (ADR-042 — source unique)
bash "$BRAIN_ROOT/scripts/bsi-claim.sh" open "$SESS_ID" \
--scope "$STEP_SCOPE" --type "satellite" --zone "project" \
--story "$STEP_ANGLE" --mode "$STEP_TYPE"
# Enrichir avec les champs workflow spécifiques
python3 -c "
import sqlite3
conn = sqlite3.connect('$BRAIN_ROOT/brain.db')
conn.execute('''
UPDATE claims SET satellite_type = ?, satellite_level = 'leaf',
theme_branch = ?, workflow = ?, workflow_step = ?
WHERE sess_id = ?
''', ('$STEP_TYPE', '$THEME_BRANCH', '$THEME_NAME', $STEP_NUM, '$SESS_ID'))
conn.commit()
conn.close()
" 2>/dev/null
echo "✅ Claim généré : claims/${SESS_ID}.yml"
echo ""
echo " Step : $STEP_NUM / $TOTAL_STEPS"
echo " Type : $STEP_TYPE"
@@ -221,6 +221,3 @@ echo " Gate : $STEP_GATE"
fi
echo " On done : $ON_DONE"
echo " On fail : $ON_FAIL"
echo ""
echo "→ Commiter le claim :"
echo " git add claims/${SESS_ID}.yml && git commit -m \"bsi: open satellite ${SESS_ID}\""

107
wiki/cold-start.md Normal file
View File

@@ -0,0 +1,107 @@
# Cold Start — Brain Run
> Nouveau cerveau. Nouvelle machine. Prêt en 5 minutes.
---
## TL;DR
```bash
git clone git@git.tetardtek.com:Tetardtek/brain.git ~/Dev/Brain
bash ~/Dev/Brain/scripts/brain-setup.sh prod ~/Dev/Brain
```
C'est tout. Le script fait le reste.
---
## Ce que fait `brain run`
```
brain-setup.sh <brain_name> <brain_root>
├── ✅ Vérifie la clé SSH Gitea
├── ✅ Clone les 6 satellites
│ profil/ · todo/ · toolkit/ · progression/ · reviews/ · wiki/
├── ✅ Configure ~/.claude/CLAUDE.md
├── ✅ Crée brain-compose.local.yml
├── ✅ Vérifie MYSECRETS (warning si absent)
└── ✅ Locke le kernel en readonly (si machine laptop)
```
Après le script, une seule chose à faire manuellement : créer `MYSECRETS`.
---
## MYSECRETS — le seul fichier manuel
```bash
# ~/Dev/BrainSecrets/MYSECRETS — jamais commité, jamais affiché
BRAIN_TELEGRAM_TOKEN=...
BRAIN_TELEGRAM_CHAT_ID=...
SUPER_OAUTH_DISCORD_CLIENT_SECRET=...
SUPER_OAUTH_GITHUB_CLIENT_SECRET=...
SUPER_OAUTH_GOOGLE_CLIENT_SECRET=...
SUPER_OAUTH_TWITCH_CLIENT_SECRET=...
```
Structure complète : voir `MYSECRETS.example` dans le repo.
---
## Machines reconnues
| Machine | brain_name | Peut pusher |
|---------|------------|-------------|
| Desktop (principal) | `prod` | kernel + satellites |
| Laptop | `prod-laptop` | satellites seulement |
| VPS | — | brain-bot seulement |
> Le kernel brain ne se push **que** depuis le desktop principal.
> Le laptop peut pull, lire, et pusher ses propres satellites (todo, progression...).
---
## Première session après install
```
Bon jour !
```
helloWorld démarre, lit le contexte, ouvre le claim BSI, et présente l'état des projets.
Si c'est vraiment la première fois : ratio = 0, backlog = vide → le coach le détectera.
---
## Rotation secrets OAuth (si nécessaire)
```bash
# Après avoir rempli MYSECRETS avec les nouveaux secrets :
bash ~/Dev/Brain/scripts/archive/rotate-oauth-secrets.sh
```
Injecte les 4 secrets sur le VPS et redémarre SuperOAuth.
> Script archivé — vérifier s'il est toujours applicable avant de l'utiliser.
---
## Warm restart vs cold start
| | Cold start | Warm restart |
|---|---|---|
| Contexte | Bootstrap complet 5 fichiers | 1 fichier checkpoint.md |
| Durée | 2-3 min | < 30 sec |
| Quand | Nouvelle machine, ou pas de checkpoint | `/checkpoint` fait avant |
| Commande | Boot normal | `Lis brain/workspace/<sprint>/checkpoint.md et reprends` |
---
## Troubleshooting
**SSH refused** → clé SSH pas ajoutée dans Gitea (Settings → SSH Keys → Add Key)
**MYSECRETS manquant** → secrets-guardian avertit au boot, pas bloquant pour le dev local
**Satellites pas clonés** → relancer `brain-setup.sh` (idempotent)
**Laptop veut pusher le kernel** → normal, le remote est locké en `no_push` — pusher depuis le desktop

107
wiki/concepts.md Normal file
View File

@@ -0,0 +1,107 @@
---
name: concepts
type: reference
context_tier: on-demand
---
# Brain — Concepts & Découvertes
> Insights théoriques émergés en session.
> Pas encore des décisions (ADR), pas encore des patterns validés — mais trop importants pour rester dans un chat.
> Format : date + titre + essentiel en 3-4 lignes.
---
## 2026-03-15 — SQLite comme organe manquant
> Source : sess-20260315-1942-memory-coach
Le brain stocke dans des `.md` — il ne pense pas sur ses propres données.
SQLite comme **index dérivé** résout ça : les `.md` restent souverains, SQLite est une projection requêtable reconstruite par cron.
Règle fondamentale : brain-engine ne touche jamais aux sources. Lecture seule. Retrograde garanti depuis git.
C'est le substrat sans lequel les agents autonomes n'ont rien à lire.
---
## 2026-03-15 — Autonomie graduée avec escalade décisionnelle
> Source : sess-20260315-1942-memory-coach
Le cycle ne nécessite pas d'intervention humaine — sauf sur les couches décisionnelles à effet externe irréversible.
C'est le rouage central de l'orchestration en équipes solides : chaque agent sait jusqu'où il va seul, et quand il escalade.
Sans ce principe, une équipe d'agents est imprévisible. Avec lui, les frontières sont composables.
---
## 2026-03-15 — Loi d'auto-amélioration (candidat constitution v1.1.0)
> Source : sess-20260315-1942-memory-coach
> "Le brain ne s'endommage jamais lui-même. Il s'améliore. Il se façonne. C'est l'outil ultime."
Toute action autonome sur le brain doit le laisser dans un état meilleur ou égal à l'état initial.
Un agent autonome ne peut pas : supprimer un fichier source, modifier un invariant, écraser un contexte sans backup.
Couplée à la constitution immutable + git retrograde → l'auto-modification devient sûre par construction.
---
## 2026-03-15 — Émergence par composition
> Source : sess-20260315-1942-memory-coach
Des principes bien posés ne s'additionnent pas — ils se multiplient.
Autonomie graduée × auto-amélioration × retrograde garanti = propriétés nouvelles non planifiées.
Signal d'architecture juste : les bonnes architectures génèrent des propriétés émergentes. Les mauvaises génèrent des exceptions.
Le brain en est la preuve — chaque session révèle des vecteurs nouveaux sur des principes qu'on croyait déjà étendus.
---
## 2026-03-15 — Sub-agents cron comme pipeline ETL du brain
> Source : sess-20260315-1942-memory-coach
Pattern : décharger d'un côté (sources brutes), transformer au milieu (plus-value), réinjecter de l'autre (brain enrichi).
Le retour n'est pas une copie — c'est de l'**information nouvelle** absente de la source.
Cron en fin de journée = rythme juste (ni temps réel inutile, ni hebdomadaire trop lent).
Alimente d'autres instances → multiplie la capacité d'apprentissage cross-sessions.
---
## 2026-03-15 — ⭐ North Star : le brain doit valoir sans Claude
> Source : sess-20260315-1942-memory-coach
> "À part la valeur ajoutée d'être connecté à Claude."
Brain V1 : sans Claude, c'est un dossier markdown bien organisé. La valeur est entièrement dans la connexion.
Brain V2 : le cron tourne, SQLite se remplit, les agents apprennent, le wiki s'alimente — sans session, sans humain, sans Claude.
Claude devient UNE interface parmi d'autres. La dépendance décroît.
**C'est le nord étoile du brain V2.**
BE-1 n'est pas une feature — c'est le début de l'autonomie réelle du brain.
Un système qui a de la valeur sans toi et sans Claude est un vrai outil. Tout le reste est de l'organisation.
---
## 2026-03-19 — Nomenclature Brain / Cortex / Cosmos
> Source : sess-20260319-bsi-db-origin-story — émergé pendant brainstorm template + multi-machine
Trois noms, trois couches, trois responsabilités :
```
Brain = le kernel. Immuable, Layer 0. Constitution, KERNEL.md, agents fondamentaux.
C'est l'identité — ce qui reste quand tout le reste est retiré.
Cortex = la couche de coordination. BSI, claims, locks, brain-engine, MCP, peer discovery.
C'est le système nerveux — il route les signaux entre les instances.
Cosmos = les satellites en orbite. Projets, toolkit, progression, reviews, visualisation UMAP.
C'est la constellation — chaque point est un chunk de connaissance, visible dans /visualise.
```
**Origine :** "Cosmos" nommé quand on a créé la page `/visualise` (galaxie UMAP 3D). "Cortex" émergé quand le brain-template est devenu le "cortex-template" distributable. "Brain" était là depuis le jour 1.
**Règle :** le Brain est souverain (un seul par machine). Le Cortex coordonne (N instances communiquent). Le Cosmos est répliqué (master→replica, ADR-038).
---

202
wiki/multi-instance.md Normal file
View File

@@ -0,0 +1,202 @@
# Multi-instance — Guide pratique
> Comment lancer plusieurs instances Claude Code simultanément sans conflit.
---
## Ce que "simultané" veut dire
Chaque instance est une **fenêtre Claude Code indépendante**, ouverte en même temps.
Elles partagent le même repo git — mais le protocole BSI garantit qu'elles ne s'écrasent pas.
```
Fenêtre 1 (coach/discussion) → lit, propose, décide
Fenêtre 2 (travail terrain) → écrit du code dans superoauth/
Fenêtre 3 (brain maintenance) → met à jour agents/, wiki/
Les 3 tournent en même temps. Zéro conflit si le protocole est respecté.
```
---
## Protocole de lancement d'une nouvelle instance
### 1. Ouvrir le claim (avant d'écrire quoi que ce soit)
```yaml
# claims/sess-YYYYMMDD-HHMM-<slug>.yml
sess_id: sess-20260317-1000-superoauth-auth
type: satellite
scope: superoauth/src/auth/ ← périmètre exclusif de cette instance
agent: satellite-boot
status: open
opened_at: "2026-03-17T10:00"
story_angle: "Refacto module auth — JWT + session"
satellite_type: code
satellite_level: leaf
parent_satellite: <sess-id-du-pilote>
on_done: notify → pilote
on_fail: signal → BLOCKED_ON pilote
```
Commiter + pusher immédiatement :
```bash
git add claims/sess-*.yml
bash scripts/brain-index-regen.sh
git add BRAIN-INDEX.md
git commit -m "bsi: open satellite sess-20260317-1000-superoauth-auth"
git push
```
→ Les autres instances voient le claim dans `brain-status.sh` et `BRAIN-INDEX.md`.
---
### 2. Avant chaque écriture — pre-flight
```bash
bash scripts/preflight-check.sh check "$SESS_ID" "<filepath>"
```
Les 6 checks (automatiques) :
| # | Check | Bloque si… |
|---|-------|------------|
| 1 | Claim open | claim fermé, en pause, ou gate:human actif |
| 1b | Parent ok | pilote parent en pause ou failed |
| 2 | Scope | fichier hors scope déclaré |
| 3 | Zone:kernel | instance non-kernel tente d'écrire agents/scripts/etc. |
| 4 | Lock | autre instance a un lock actif sur ce fichier |
| 5 | Circuit breaker | trop d'échecs consécutifs (défaut : 3) |
| 6 | Branch | mauvaise branche git vs theme_branch déclaré |
---
### 3. Mutex pour les fichiers partagés (mode rendering / multi-instances)
Si deux instances peuvent vouloir écrire le même fichier :
```bash
# Avant d'écrire
bash scripts/file-lock.sh acquire "<filepath>" "$SESS_ID" 30
# → exit 1 = déjà locké → attendre ou signal BLOCKED_ON
# [écriture]
# Après avoir écrit
bash scripts/file-lock.sh release "<filepath>" "$SESS_ID"
# Enregistrer le résultat pour le circuit breaker
bash scripts/preflight-check.sh reset "$SESS_ID" # succès
bash scripts/preflight-check.sh fail "$SESS_ID" # échec
```
---
### 4. Voir ce que font les autres instances
```bash
bash scripts/brain-status.sh # vue complète
bash scripts/brain-status.sh claims # qui travaille où
bash scripts/brain-status.sh locks # fichiers verrouillés
bash scripts/brain-status.sh signals # signaux en attente
```
---
### 5. Pause d'urgence (arrêter tout)
```bash
# Depuis n'importe quelle instance ou le pilote
bash scripts/human-gate-ack.sh pause "<sess-pilote>" "raison"
# → tous les satellites enfants sont stoppés en cascade
# → pre-flight bloquera toute écriture
# Reprendre
bash scripts/human-gate-ack.sh resume "<sess-pilote>"
```
---
### 6. Close propre
```bash
# Ajouter result: dans le claim
# Puis :
bash scripts/brain-index-regen.sh
git add BRAIN-INDEX.md claims/<sess-id>.yml
git commit -m "bsi: close satellite <sess-id>"
git push
```
---
## Règles de non-collision
| Règle | Mécanisme |
|-------|-----------|
| Deux instances ne partagent pas le même scope | BRAIN-INDEX + pre-flight CHECK 2 |
| Pas d'écriture kernel sans mandat kernel | pre-flight CHECK 3 (soft lock) |
| Pas d'écriture simultanée sur le même fichier | file-lock.sh (BSI-v3-7) |
| Un satellite mort ne bloque pas les autres | TTL sur les locks (défaut 60min) |
| Un pilote paused stoppe ses enfants | cascade human-gate-ack.sh (BSI-v3-5) |
| 3 échecs consécutifs = arrêt forcé | circuit breaker pre-flight CHECK 5 |
---
## Cas d'usage typiques
### Coach + travail terrain simultanés
```
Instance 1 : scope brain/ → discussion, décisions, lecture
Instance 2 : scope superoauth/ → code, tests, deploy
```
Pas de conflit possible : scopes disjoints.
### Deux satellites sur le même projet
```
Instance A : scope superoauth/src/auth/ → JWT refacto
Instance B : scope superoauth/src/api/ → endpoints REST
```
Scopes disjoints → pas de lock nécessaire.
Si un fichier est partagé (ex: types.ts) → file-lock.sh obligatoire.
### Mode rendering (instance autonome projet)
```yaml
mode: rendering
scope: superoauth/ ← seul périmètre autorisé
```
- zone:kernel → BLOCKED_ON immédiat (pre-flight CHECK 3)
- circuit_breaker : 3 fails → arrêt + signal pilote
- mutex sur chaque fichier écrit (file-lock.sh)
### BaaS — client vs owner
```
kerneluser: true → owner — accès complet, peut forger le kernel
kerneluser: false → client — rendering mode, zone:project uniquement
```
---
## Référence rapide
```bash
# Voir l'état global
bash scripts/brain-status.sh
# Lancer le pre-flight avant d'écrire
bash scripts/preflight-check.sh check "$SESS_ID" "$FILE"
# Locker un fichier
bash scripts/file-lock.sh acquire "$FILE" "$SESS_ID" 30
# Pause d'urgence
bash scripts/human-gate-ack.sh pause "$SESS_PILOTE" "raison"
# Gate:human planifié
bash scripts/human-gate-ack.sh gate "$SESS_ID" "deploy ok ?"
bash scripts/human-gate-ack.sh approve "$SESS_ID"
```

77
wiki/patterns.md Normal file
View File

@@ -0,0 +1,77 @@
# Brain — Référence Patterns
> Patterns 1-N validés en prod. Source complète : `profil/orchestration-patterns.md`.
---
| # | Nom | Problème résolu | Forgé |
|---|-----|----------------|-------|
| 1 | Session-as-identity | Sessions parallèles sur une machine — routing par slug | 2026-03-14 |
| 2 | Passive listener | Agent écoute sans charger de contexte lourd au boot | 2026-03-14 |
| 3 | Parallel session handoff | Handoff entre deux sessions parallèles (CHECKPOINT signal) | 2026-03-14 |
| 4 | Context-tier split | Scinder un agent always lourd en header (always) + détail (warm) | 2026-03-15 |
| 5 | BHP validation | 4 greps de validation always-tier + convention CI brain | 2026-03-15 |
| 6 | HumanSupervisor | Extraire la logique d'exécution — laisser à l'humain les bifurcations décisionnelles | 2026-03-14 |
| 7 | Todo → KANBAN Sprint Setup | Todo structuré → KANBAN avec prompts autonomes prêts à coller | 2026-03-15 |
| 8 | Context Compact Checkpoint | Warm restart < 30 sec via checkpoint.md — vs cold bootstrap 2-3 min | 2026-03-15 |
| 9 | Kanban Pipeline Flow | Boot minimal scopé → work → wrap → kanban-scribe → états `✅`/`🤖` → viabilité agent | 2026-03-15 |
| 10 | Pilot + Satellites | Session pilote garde le contexte riche — satellites minimaux résolvent les sous-problèmes et remontent le résultat | 2026-03-16 |
| 11 | Session Ending Standard | Wrap toujours = Résumé session + Retour coach + Prompt session suivante | 2026-03-16 |
---
## Pattern 7 — Usage rapide
```
1. Todo structuré (chaque tâche : agents, input, output, prérequis)
2. "Génère le KANBAN depuis brain/todo/<fichier>.md"
3. → workspace/<sprint>/kanban.md créé avec prompts prêts
4. Envoyer les prompts carte par carte (ou en parallèle si pas de dépendance)
5. [ ] → [x] + commit à chaque carte terminée
```
## Pattern 8 — Usage rapide
```
En session : /checkpoint → checkpoint.md écrit
Warm restart : "Lis brain/workspace/<sprint>/checkpoint.md et reprends"
```
## Pattern 10 — Usage rapide (Pilot + Satellites)
```
Session pilote → contexte riche, vision, décisions archi
→ identifie un sous-problème bloquant
→ génère un prompt satellite minimal
Session satellite → contexte minimal, tâche unique
→ résout et remonte le résultat dans la pilote
→ se ferme proprement (claim + wrap)
Session pilote → intègre le résultat, continue d'avancer
```
Règle : la pilote ne descend jamais dans le détail d'implémentation.
Elle délègue, intègre, décide.
## Pattern 11 — Usage rapide (Session Ending Standard)
```
1. Résumé session → ce qui a été livré (jalons, commits, décisions)
2. Retour coach → progression observée + point à surveiller
3. Prompt suivant → copier-coller prêt pour la prochaine session
```
S'applique à toute session pilote au wrap. Non-négociable.
## Pattern 9 — Usage rapide
```
1. "brain boot mode <scope>" → claim BSI ouvert, agent chargé, prêt en 5 lignes
2. Travailler sur le scope
3. "wrap" → kanban-scribe lit le claim scope
→ todo/<scope>.md mis à jour
→ ✅ si intervention humaine / 🤖 si autonome
→ BSI close + push
4. 🤖 accumulés → scope validé → entre dans le toolkit
```

139
wiki/session-lifecycle.md Normal file
View File

@@ -0,0 +1,139 @@
# Brain — Cycle de vie d'une session
> Ce qui se passe du premier message au dernier commit.
---
## Boot (automatique)
```
Message utilisateur
CLAUDE.md charge :
0. PATHS.md — chemins machine
1. collaboration.md — règles de travail
2. coach.md — présence permanente
3. secrets-guardian.md — écoute passive MYSECRETS
4. helloWorld.md — briefing + CHECKPOINT + détection session
helloWorld → ouvre claim BSI + push immédiat
session-orchestrator reçoit le handoff :
→ détecte session_type + scope
→ détermine handoff_level (NO / SEMI / SEMI+ / FULL)
→ charge les couches correspondantes
→ active la position (rôle contextuel)
```
**Handoff levels :**
| Level | Contexte chargé |
|-------|----------------|
| `NO` | Layer 0 seulement (kernel + constitution + paths + collaboration) |
| `SEMI` | Layer 0 + position |
| `SEMI+` | SEMI + focus.md + projets/<scope> + todo/<scope> |
| `FULL` | SEMI+ + Layer 2 : workspace actif + handoffs |
---
## Work
- Agents invoqués sur domaine détecté (auto) ou sur demande explicite
- `/btw` disponible à tout moment pour aparté sans casser le fil
- `/checkpoint` recommandé avant compactage ou si sprint > 2h
---
## Close — séquence obligatoire
> Déclenchée par : `fin` | `on wrappe` | `je ferme` | `c'est bon`
> Source de vérité close sequences par type : `wiki/session-matrix.md`
> Decision tree runtime : `agents/session-orchestrator.md ## boot-summary`
```
Étape 0 — Checkpoint (si sprint actif)
→ Écrire workspace/<sprint>/checkpoint.md
→ Permet warm restart à la prochaine session
Étape 1 — metabolism-scribe ← TOUJOURS (15 types)
→ tokens_used, context_peak, duration, agents_loaded
→ commits, todos_closed, health_score (formule par profil), handoff_level
→ type : use-brain | build-brain | explore-brain | auto
Étape 2 — todo-scribe ← RÈGLE INVIOLABLE
→ Tout item complété pendant la session → [x] dans backlog.md
→ Mettre à jour la table métriques (✅ Done +N, ⬜ Open -N)
→ Si aucun item fermé → écrire pourquoi dans changelog backlog
→ Commit : "backlog: close <item-id> — <titre court>"
Étape 3 — todo-scribe [si work | sprint | debug | brainstorm]
→ ✅ todos fermés
→ ⬜ todos émergés capturés
Étape 4 — wiki-scribe [si nouveau pattern/commande/agent forgé]
→ Ajouter terme dans vocabulary.md
→ Créer/mettre à jour la page wiki concernée
→ Commit : "wiki: vocabulary +N terms — <domaine>"
Étape 5 — scribe [si session significative]
→ brain/ : focus, projets/, AGENTS si nouvel agent
Étape 6 — coach [rapport de session — si coach actif]
⚡ Rapport de session — <sess-id>
Ce qui a été produit : <liste concrète>
Pattern observé : <observation — 1 ligne>
Point à ancrer : <concept ou réflexe>
Objectif suivant : <1 action concrète mesurable>
→ BLOCKING — attend réponse ou /exit
Étape 7 — BSI close claim ← NON NÉGOCIABLE
→ status: open → closed dans claims/<sess-id>.yml
→ git commit + push brain/
→ rm session-role + pid
```
### Close sequences par type de session
| Type | Sequence (etapes actives) |
|------|--------------------------|
| `audit` | 1 (metabolism) → rapport audit → 7 (BSI close) |
| `brain` | 1 → 5 (scribe) → 6 (coach) → 7 |
| `brainstorm` | 1 → 3 (todo si todos emerges) → 7 |
| `capital` | 1 → capital-scribe → 6 (coach) → 7 |
| `coach` | 1 → coach-scribe → 7 |
| `debug` | 1 → 2 + 3 (todo) → 6 (coach) → 7 |
| `deploy` | 1 → 5 (scribe infra) → 7 |
| `edit-brain` | 1 → 5 (scribe) → 6 (coach) → 7 |
| `handoff` | 1 → 7 |
| `infra` | 1 → 5 (scribe si changement config) → 7 |
| `kernel` | 1 → 7 |
| `navigate` | 1 → 7 |
| `pilote` | 1 → 4 (wiki) → 5 (scribe) → 6 (coach) → 7 |
| `urgence` | 1 → post-mortem scribe → 7 |
| `work` | 1 → 2 + 3 (todo) → 5 (scribe si commit) → 6 (coach) → 7 |
---
## Règle inviolable backlog (étape 2)
> Sans cette règle, le backlog devient un cimetière de todos. La métrique de vélocité reste à zéro.
**Ce qui est obligatoire :**
- Chaque item touché pendant la session → [x] si terminé, note si partiel
- Table métriques recalculée avant le commit
- Un commit `backlog: close ...` par item fermé (ou un commit groupé si plusieurs)
**Ce qui est interdit :**
- Fermer la session sans avoir vérifié le backlog
- Marquer [x] un item non terminé (intégrité des métriques)
---
## Warm restart (Pattern 8)
Si la session se poursuit après compactage ou reprise :
```
Lis brain/workspace/<sprint>/checkpoint.md et reprends — pas de bootstrap complet.
```
Cold bootstrap : 2-3 min — Warm restart : < 30 sec.

179
wiki/vocabulary.md Normal file
View File

@@ -0,0 +1,179 @@
# Brain — Vocabulaire
> Source unique de vérité pour les termes du brain.
> Mis à jour par `wiki-scribe` en close de session quand un terme est forgé.
> `git log wiki/vocabulary.md` = timeline de croissance du vocabulaire.
---
## circuit breaker
> Forgé : 2026-03-17 | Domaine : orchestration kernel
Mécanisme de protection dans `kernel-orchestrator` : 3 échecs consécutifs sur le même scope → arrêt automatique de la séquence, signal `CIRCUIT_BREAK` vers `brain-hypervisor`, gate:human obligatoire avant reprise. Règle : jamais relancer automatiquement après 3 fails — l'humain inspecte. Script : `scripts/preflight-check.sh reset <scope>`.
## context-broker
> Forgé : 2026-03-15 | Domaine : brain système
Agent qui gère le cycle respiratoire du contexte d'un sprint. Deux temps : **inhale** (source_map en début de sprint — quels agents lisent quels fichiers) et **expire** (release_map en fin de sprint — ce qui a été touché, todos ouvertes, métriques breath). Rend le contexte traçable et libère proprement la mémoire inter-sprints.
## contention map
> Forgé : 2026-03-14 | Domaine : orchestration multi-agents
Carte produite par `tech-lead` en gate d'entrée de sprint : pour chaque fichier touché, quel agent en est l'owner et quels autres agents le touchent aussi. Permet de planifier l'ordre de commit pour éviter les conflits de merge. Input clé pour `orchestrator` et `integrator`.
## cosign
> Forgé : 2026-03-14 | Domaine : orchestration / zones
Convention de validation d'un overflow de zone par le `tech-lead`. Format obligatoire dans le message de commit de l'agent qui écrit : `tech-lead: overflow granted — <raison courte>`. Trace l'autorisation dans le git log. Sans cosign → overflow non autorisé.
## brain run
> Forgé : 2026-03-15 | Domaine : onboarding
Commande d'installation du brain sur une nouvelle machine. Une seule ligne suffit pour avoir un cerveau opérationnel. Voir `wiki/brain-setup.md` et la page [Cold Start](cold-start).
## brain_name
> Forgé : 2026-03-14 | Domaine : brain système
Identifiant de l'instance brain sur une machine (`prod`, `prod-laptop`). Défini dans `~/.claude/CLAUDE.md`. Détermine le write_mode et les permissions push.
## ASF-Brain
> Forgé : 2026-03-15 | Domaine : vision
Autonomous Software Factory — état cible où le brain peut builder un tier logiciel complet depuis un brief humain, sans intervention sur le code.
## BaaS (Brain as a Service)
> Forgé : 2026-03-15 | Domaine : vision
Modèle où le brain devient un service multi-tenant : `brain new` clone un brain pour un client, `brain sync` partage un workspace sprint. Prérequis : cockpit solo + SuperOAuth multi-tenant + OpenClaw.
## coach gate
> Forgé : 2026-03-20 | Domaine : session / coaching
Matrice de comportement du coach indexée par session type. 5 modes : **silencieux** (navigate, deploy, infra, urgence, audit — observation seule, pas de rapport), **standard** (work, debug — actif sur patterns), **engagé** (brain, brainstorm — challenger les décisions), **complet** (coach, capital — mentorat structuré), **copilote** (pilote — proactif). Spec : `agents/coach.md ## Gate par session type`.
## close decision tree
> Forgé : 2026-03-20 | Domaine : session / orchestration
Pseudo-code dans session-orchestrator qui détermine quels scribes fire et dans quel ordre pour chaque session type. Rend la close sequence auditable et déterministe — pas de logique implicite. Spec : `agents/session-orchestrator.md ## boot-summary`.
## cold start
> Forgé : 2026-03-15 | Domaine : session / onboarding
Deux sens : (1) Première session sur une nouvelle machine — `brain run` + MYSECRETS + CLAUDE.md → voir [Cold Start](cold-start). (2) Session sans checkpoint disponible → bootstrap complet ~2-3 min. Opposé : warm restart.
## BHP (Brain Hydration Protocol)
> Forgé : 2026-03-15 | Mis à jour : 2026-03-20 | Domaine : brain système
Protocole d'optimisation du contexte always-tier. Objectif : < 2 000 lignes au boot. Phase 1 = frontmatter propagé. Phase 2 = context-tier-split sur agents lourds (**terminé** — 16 agents splittés en boot-summary/detail). Spec : `wiki/context-loading.md`.
## boot-summary
> Forgé : 2026-03-20 | Domaine : BHP Phase 2
Section d'un agent contenant le minimum nécessaire pour COMMENCER à travailler : rôle (1 ligne), méthode/curseur, règles d'engagement, composition. ~20-30 lignes. Chargé en L1 par session-orchestrator quand l'agent est dans le manifest de la session. Opposé : `detail`.
## detail (agent)
> Forgé : 2026-03-20 | Domaine : BHP Phase 2
Section d'un agent contenant tout ce qu'il faut pour ALLER EN PROFONDEUR : activation, sources, périmètre complet, patterns/réflexes, anti-hallucination, ton, déclencheur, cycle de vie, changelog. Chargé en L3 sur invocation explicite ou quand l'agent est actif en session. Opposé : `boot-summary`.
## Brain Session Index (BSI)
> Forgé : 2026-03-14 | Domaine : session
Système de locking optimiste inter-sessions. Un claim par session (`claims/sess-YYYYMMDD-HHMM-slug.yml`). Les signaux dans `BRAIN-INDEX.md` permettent la communication inter-instances. Spec : `profil/bsi-spec.md`.
## Claim BSI
> Forgé : 2026-03-14 | Domaine : session
Fichier `claims/sess-YYYYMMDD-HHMM-<slug>.yml` — décrit une session ouverte (scope, instance, handoff_level, expires). Ouvert au boot par helloWorld, fermé au close par session-orchestrator.
## Checkpoint (Pattern 8)
> Forgé : 2026-03-15 | Domaine : session
Fichier `workspace/<sprint>/checkpoint.md` — capture l'état de travail en < 50 lignes pour permettre un warm restart sans bootstrap complet. Commande : `/checkpoint`. Spec : `toolkit/brain/checkpoint-pattern.md`.
## Cockpit
> Forgé : 2026-03-15 | Mis à jour : 2026-03-15 | Domaine : vision + mode
Deux sens liés : (1) Workspace v2 — couche humaine (`brief.md` + `kanban.md`) sur le workspace agent. (2) **Mode cockpit** (`brain-compose.yml`) — coach proactif qui route avant qu'on cherche + `kanban-scribe` actif automatiquement au wrap + `interprete` en écoute continue. Déclaré avec `mode: cockpit` ou `brain boot mode cockpit`.
## kanban-scribe
> Forgé : 2026-03-15 | Domaine : pipeline kanban
Agent déclenché au wrap. Lit le scope du claim BSI actif → met à jour `todo/<scope>.md` → détecte si la complétion était autonome (`🤖`) ou humaine (`✅`) → commite. Source de vérité pour la viabilité des agents : un item `🤖` = agent viable sur ce scope, candidat toolkit.
## context-tier
> Forgé : 2026-03-14 | Domaine : brain système
Niveau de chargement d'un fichier brain : `always` (chargé au boot), `warm` (chargé sur scope), `cold` (chargé sur invocation explicite), `hot` (chargé si domaine détecté).
## gate:human
> Forgé : 2026-03-14 | Domaine : orchestration / protocol
Point d'arrêt explicite dans un workflow ou un protocole agent — le brain suspend toute action et attend une réponse humaine avant de continuer. Format : `gate:human → "<message>"`. Non bypassable par l'agent lui-même. Script : `scripts/human-gate-ack.sh`. Opposé du nœud automatique.
## git-analyst
> Forgé : 2026-03-15 | Domaine : documentation / conception
Agent qui lit `git log` et produit une narration sémantique. Utilisé dans les sessions "docs par storytelling" : git-analyst → storyteller → doc agent. Transforme l'historique de commits en documentation vivante.
## Handoff Level
> Forgé : 2026-03-14 | Domaine : session
Profondeur de contexte chargé au boot : `NO` (Layer 0 seulement), `SEMI` (+ position), `SEMI+` (+ projets/todo scope), `FULL` (+ Layer 2 workspace). Déterminé par session_type × scope via `handoff-matrix.md`.
## health_score
> Forgé : 2026-03-14 | Domaine : métabolisme
Score 0-1 calculé par metabolism-scribe. Proxy de la "santé" d'une session : commits, todos fermés, agents chargés, context_peak. Seuil critique : < 0.80.
## KANBAN (Pattern 7 + pipeline)
> Forgé : 2026-03-15 | Mis à jour : 2026-03-15 | Domaine : orchestration + pipeline
Deux usages : (1) **Sprint setup** — fichier `workspace/<sprint>/kanban.md` généré depuis un todo structuré. Chaque carte = un agent + prompt autonome prêt à coller (Pattern 7). (2) **Pipeline de session** — les états `todo/<scope>.md` (`⬜→🔄→✅→🤖`) sont la source de vérité du workflow. `kanban-scribe` fait avancer les états au wrap. Un item `🤖` (validé-autonome) = signal de viabilité agent.
## Nœud humain / nœud automatique
> Forgé : 2026-03-15 | Domaine : pipeline kanban
Deux types de points de décision dans le workflow. **Nœud humain** : décision de valeur — "est-ce que ce scope mérite prod ?" — jamais de mécanique. **Nœud automatique** : `kanban-scribe` avance l'état sans intervention. Si une mécanique demande une décision humaine → agent mal conçu.
## validé-autonome (🤖) / validé-humain (✅)
> Forgé : 2026-03-15 | Domaine : pipeline kanban
États terminaux d'un item kanban. `✅` = complété avec intervention humaine au wrap. `🤖` = complété sans aucune intervention — l'agent a tourné seul du début à la fin. `🤖` = signal de viabilité : cet agent + scope peut entrer dans le toolkit.
## mode d'exécution (ADR-032)
> Forgé : 2026-03-18 | Domaine : orchestration
Propriété de la **session**, pas du workflow. Trois niveaux : **Mode 1 — manuel** (l'humain valide chaque step, gates systématiques), **Mode 2 — assisté** (l'humain valide les gates:human, steps techniques en automatique), **Mode 3 — swarm** (brain autonome, l'humain ne voit que les blocages critiques). Voir ADR-032 et **swarm-ready gate**.
## metabolism / metabolism-scribe
> Forgé : 2026-03-14 | Domaine : métabolisme
Agent de mesure de session. Calcule health_score, ratio use-brain/build-brain, context_peak, agents_loaded, durée, commits. Déclenché en step 1 du close protocol.
## overflow (de zone)
> Forgé : 2026-03-14 | Domaine : zones / orchestration
Demande d'un agent d'écrire hors de sa zone normale. Doit être soumis au `tech-lead` avec un format précis (agent demandeur, zone cible, fichier exact, raison métier, cas d'usage concret). Validé uniquement si la raison est métier — jamais pour convenance. Tracé par **cosign** dans le message de commit. Zone ABSOLU (KERNEL.md, CLAUDE.md) → humain requis, toujours.
## Pattern N
> Forgé : 2026-03-14+ | Domaine : orchestration
Convention récurrente validée en prod, capturée dans `profil/orchestration-patterns.md`. Patterns 1-8 actifs. Spec complète : `profil/orchestration-patterns.md`.
## Plateforme 2026
> Forgé : 2026-03-15 | Domaine : projet
Vision mini-game platform — SuperOAuth comme auth centrale, 4 tuiles (OriginsDigital, HP Quest, ClickerZ, TetaRdPG). Spec : `projets/plateforme-2026.md`.
## Position (brain)
> Forgé : 2026-03-14 | Domaine : session
Rôle contextuel chargé par session-orchestrator selon le session_type. Applique promote/suppress sur le contexte Layer 1. Ignoré si handoff_level = NO.
## ratio use-brain / build-brain
> Forgé : 2026-03-14 | Domaine : métabolisme
Métrique d'équilibre : sessions qui utilisent le brain (use-brain) vs sessions qui l'améliorent (build-brain). Cible : ≥ 0.60. Actuel : 0.33 🔴.
## Scribe Pattern
> Forgé : 2026-03-13 | Domaine : brain système (ADR-003)
Règle structurelle : un agent observateur ne documente jamais lui-même — il délègue toujours l'écriture à un scribe dédié. Sépare la capacité d'observation de la capacité d'écriture. Un agent peut être remplacé sans perdre la trace de sa production. Paires établies : coach → coach-scribe, session-orchestrator → metabolism-scribe, git-analyst → capital-scribe, content-orchestrator → content-scribe.
## swarm-ready gate
> Forgé : 2026-03-18 | Domaine : orchestration (ADR-032)
Quatre critères à satisfaire avant de passer un workflow en **Mode 3 — swarm** : (1) au moins un run en Mode 1 ✅, (2) au moins un run en Mode 2 ✅, (3) agents du workflow validés par `agent-review` ✅, (4) outputs structurés (rapports format strict) ✅. Sans ce gate, le mode swarm n'est pas autorisé.
## satellite
> Forgé : 2026-03-14 | Domaine : brain système
Repo Git indépendant versionné séparément du kernel brain. Liste : brain-profil, brain-todo, brain-toolkit, brain-agent-review, brain-progression. Ignoré dans le `.gitignore` du kernel.
## session-orchestrator
> Forgé : 2026-03-14 | Domaine : session
Agent propriétaire du cycle de vie de session (boot → work → close). Ne produit rien lui-même — orchestre les scribes et la séquence de fermeture. Spec : `agents/session-orchestrator.md`.
## Signal BSI
> Forgé : 2026-03-14 | Domaine : session
Message inter-sessions dans `BRAIN-INDEX.md ## Signals`. Types : READY_FOR_REVIEW, REVIEWED, BLOCKED_ON, HANDOFF, CHECKPOINT, INFO.
## SuperOAuth
> Forgé : 2026-03-13 | Domaine : projet
Auth centrale de la plateforme 2026. Express + JWT + Redis + MySQL. Provider OAuth universel multi-tenant. Tier 3 ✅ (2026-03-17). Repo : `~/Dev/Github/Super-OAuth/`.
## Toolkit-first
> Forgé : 2026-03-15 | Domaine : orchestration
Règle : avant chaque carte KANBAN, vérifier si un pattern toolkit/ existe. Si oui → utiliser. Si non → exécuter → toolkit-scribe capture en fin de carte. Accélérateur sprint-over-sprint.
## wrap
> Forgé : 2026-03-15 | Domaine : session
Fermeture propre d'une session. Déclenche la séquence close complète (checkpoint → metabolism → backlog → todo → wiki → scribe → coach → BSI). Alias : `fin`, `on wrappe`, `je ferme`. Sans wrap, le backlog n'est pas mis à jour et le VPS reste aveugle.
## warm restart
> Forgé : 2026-03-15 | Domaine : session
Reprise de session depuis un `checkpoint.md` sans bootstrap complet. < 30 sec vs 2-3 min cold bootstrap. Voir Pattern 8.

View File

@@ -6,6 +6,20 @@ name: <theme-slug> # ex: brain-engine-be7
branch: theme/<theme-slug> # branche git dédiée — créer avec theme-branch-open.sh
pilote: <sess-id> # renseigné au lancement (sess-id de la session pilote)
# ---
# execution_mode : déclaré dans la SESSION qui lance ce workflow, pas ici.
# Ce fichier définit le QUOI. La session définit le COMMENT. (ADR-032)
#
# Pour référence :
# manual → humain valide chaque step (défaut — premier run)
# assisted → brain orchestre, humain a la vue de l'intérieur
# swarm → brain exécute, humain gate entrée + livrable final
#
# swarm_ready: false # passe à true quand checklist BACT agentic.swarm-ready-gate OK
# ---
swarm_ready: false
# ---
# chain : séquence de satellites dans l'ordre d'exécution
# Chaque step est traduit en claim BSI par workflow-launch.sh
@@ -17,30 +31,42 @@ chain:
type: code # satellite_type : code | brain-write | test | deploy | search
scope: <scope>/ # dossier ou fichier cible
story_angle: "<description courte de la tâche>"
agents: [] # agents à charger pour ce step
# input_contract: null # step 1 — pas de prior_output
# gate absent → proceed si result.status = ok
- step: 2
type: test
scope: <scope>/
story_angle: "Tests <scope>"
agents: [testing]
gate: 0-failures # proceed uniquement si result.tests.failed = 0
# input_contract: "output step 1 — fichiers modifiés + résumé"
- step: 3
type: brain-write
scope: <fichier>.md
story_angle: "Documenter <livrable>"
agents: [scribe]
# input_contract: "output step 2 — résultats tests + gaps identifiés"
# gate absent → proceed si result.status = ok
- step: 4
type: deploy
scope: vps/
story_angle: "Déployer <livrable>"
agents: [vps]
gate: human # pause — confirmation humaine avant deploy
# input_contract: "output step 3 — doc à jour + artefacts build"
# ---
# Gates disponibles (transition vers le step suivant) :
# Gates disponibles :
# absent → proceed si result.status = ok
# 0-failures → proceed si result.tests.failed = 0 (step type:test uniquement)
# human → pause + confirmation avant de lancer le step suivant
# never → chaîne s'arrête ici (step terminal)
#
# Contrats I/O (mode assisté / swarm) :
# input_contract → ce que ce step reçoit du step précédent (prior_output)
# Les contrats sont optionnels en mode manual — obligatoires avant swarm_ready: true
# ---

View File

@@ -1,7 +1,10 @@
# workflows/brain-engine.yml — Workflow Brain Engine (BE-X)
# workflows/brain-engine.yml — Workflow Brain Engine
# Usage : bash scripts/workflow-launch.sh workflows/brain-engine.yml [--step N]
# TODO : définir la feature BE-X cible avant lancement — ne pas lancer en l'état
name: brain-engine
status: draft
note: "À renseigner — définir la feature BE-X cible avant lancement"
branch: theme/brain-engine
pilote: ~ # renseigné au lancement
@@ -10,21 +13,21 @@ chain:
- step: 1
type: code
scope: brain-engine/
story_angle: "Implémenter la feature BE-X"
story_angle: "# TODO : définir — feature BE-X non spécifiée"
- step: 2
type: test
scope: brain-engine/
story_angle: "Tests BE-X — suite complète"
story_angle: "# TODO : définir — tests à écrire après step 1 spécifié"
gate: 0-failures
- step: 3
type: brain-write
scope: brain-engine/README.md
story_angle: "Mettre à jour README brain-engine"
story_angle: "# TODO : définir — documentation à préciser"
- step: 4
type: deploy
scope: vps/
story_angle: "Déployer brain-engine sur VPS"
story_angle: "# TODO : définir — scope deploy à préciser"
gate: human