sync: scission owner/template + brain-template-export + BRAIN_MODE guard + /visualize scope filter + port orphelins fix
This commit is contained in:
44
KERNEL.md
44
KERNEL.md
@@ -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) |
|
||||
|
||||
@@ -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
379
agents/CATALOG.yml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
212
agents/secrets-manager.md
Normal 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 |
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
72
scripts/brain-db-backup.sh
Executable 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
|
||||
@@ -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
143
scripts/brain-dev.sh
Executable 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
|
||||
@@ -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
1087
scripts/brain-launch.sh
Executable file
File diff suppressed because it is too large
Load Diff
193
scripts/brain-pair-client.py
Normal file
193
scripts/brain-pair-client.py
Normal 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()
|
||||
197
scripts/brain-pair-server.py
Normal file
197
scripts/brain-pair-server.py
Normal 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
105
scripts/brain-pair.sh
Executable 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
310
scripts/brain-secrets-sync.sh
Executable 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
|
||||
@@ -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
89
scripts/brain-start-laptop.sh
Executable 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
332
scripts/brain-state-bot.sh
Executable 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
188
scripts/brain-sync-replica.sh
Executable 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
|
||||
92
scripts/brain-template-export.sh
Executable file
92
scripts/brain-template-export.sh
Executable 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
45
scripts/brain-template-push.sh
Executable 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."
|
||||
@@ -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
50
scripts/bsi-db.py
Normal 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
105
scripts/bsi-peer-poll.sh
Executable 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"
|
||||
@@ -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
61
scripts/dev-start.sh
Executable 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
335
scripts/diagram-init.sh
Executable 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
119
scripts/diagram-patch.sh
Executable 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
106
scripts/feature-gate-status.sh
Executable 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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
177
scripts/migrate-claims-to-db.py
Normal file
177
scripts/migrate-claims-to-db.py
Normal 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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
60
scripts/sync-secrets-from-vps.sh
Executable file
60
scripts/sync-secrets-from-vps.sh
Executable 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
124
scripts/sync-template.sh
Executable 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é"
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
107
wiki/cold-start.md
Normal 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
107
wiki/concepts.md
Normal 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
202
wiki/multi-instance.md
Normal 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
77
wiki/patterns.md
Normal 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
139
wiki/session-lifecycle.md
Normal 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
179
wiki/vocabulary.md
Normal 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.
|
||||
@@ -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
|
||||
# ---
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user