feat: brain-template v2.0 — BSI-v3 complet + tiers documentés

- README reécrit : tiers free/pro/full + modèle clé API + multi-instance
- Sync agents/ (57 agents, kernel-isolation validated)
- Sync scripts/ BSI-v3 (file-lock, preflight, human-gate, brain-status)
- KERNEL.md v0.7.0 — zones + délégation + rendering + isolation
- brain-compose.yml v0.7.0 — rendering mode + kerneluser
- workflows/ — template + brain-engine exemple
- locks/.gitkeep + claims/.gitkeep
- helloWorld : RAG boot tier full only (bsi-rag retiré du template)
This commit is contained in:
2026-03-16 23:26:38 +01:00
parent 0b0e6649c2
commit 878886cd51
110 changed files with 7656 additions and 680 deletions

83
scripts/brain-db-sync.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# brain-db-sync.sh — Sync brain.db depuis les sources brain
#
# Usage :
# brain-db-sync.sh → migrate + log résultat
# brain-db-sync.sh --quiet → log fichier uniquement (pour hooks git)
# brain-db-sync.sh --check → exit 0 si brain.db à jour, exit 2 si stale
#
# Headless : zéro notify-send, zéro dépendance Wayland/display.
# Appelable depuis hook git post-commit, cron, ou manuellement.
#
# Exit codes :
# 0 = sync réussi
# 1 = migrate.py introuvable ou Python absent
# 2 = brain.db stale (--check uniquement)
# 3 = migrate.py a échoué
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MIGRATE="$BRAIN_ROOT/brain-engine/migrate.py"
DB_PATH="$BRAIN_ROOT/brain.db"
LOG_FILE="$BRAIN_ROOT/brain-engine/sync.log"
QUIET=false
CHECK_ONLY=false
for arg in "$@"; do
case "$arg" in
--quiet) QUIET=true ;;
--check) CHECK_ONLY=true ;;
esac
done
log() {
local ts
ts=$(date '+%Y-%m-%dT%H:%M:%S')
echo "[$ts] $*" >> "$LOG_FILE"
$QUIET || echo "[brain-db-sync] $*"
}
# Vérifications préalables
if [[ ! -f "$MIGRATE" ]]; then
log "ERROR: migrate.py introuvable ($MIGRATE)"
exit 1
fi
if ! python3 -c "import sqlite3" 2>/dev/null; then
log "ERROR: python3/sqlite3 absent"
exit 1
fi
# --check : brain.db stale si plus vieux que le dernier commit touchant claims/ ou handoffs/
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)
if [[ "$last_commit_ts" -gt "$db_mtime" ]]; then
log "STALE: brain.db ($db_mtime) < dernier commit claims/handoffs ($last_commit_ts)"
exit 2
fi
log "OK: brain.db à jour"
exit 0
fi
# Sync
log "Démarrage migrate.py..."
if python3 "$MIGRATE" >> "$LOG_FILE" 2>&1; then
claim_count=$(python3 -c "
import sqlite3
conn = sqlite3.connect('$DB_PATH')
n = conn.execute(\"SELECT COUNT(*) FROM claims\").fetchone()[0]
o = conn.execute(\"SELECT COUNT(*) FROM claims WHERE status='open'\").fetchone()[0]
print(f'{o} open / {n} total')
conn.close()
" 2>/dev/null || echo "?")
log "OK — claims: $claim_count"
else
log "ERROR: migrate.py a échoué (voir $LOG_FILE)"
exit 3
fi

127
scripts/brain-index-regen.sh Executable file
View File

@@ -0,0 +1,127 @@
#!/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
#
# 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)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLAIMS_DIR="$BRAIN_ROOT/claims"
INDEX_FILE="$BRAIN_ROOT/BRAIN-INDEX.md"
if [[ ! -f "$INDEX_FILE" ]]; then
echo "❌ BRAIN-INDEX.md introuvable — chemin : $INDEX_FILE"
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

View File

@@ -1,39 +1,54 @@
#!/bin/bash
# brain-notify.sh — Canal Telegram du SUPERVISOR
# Usage: brain-notify.sh "MESSAGE" [urgent|update|info]
# urgent → 🔴 notification sonore — interruption humaine
# update → ✅ notification silencieuse — info non bloquante
# info 💬 notification silencieuse — log passif
# Usage: brain-notify.sh "MESSAGE" [urgent|update|info] [supervisor|monitoring]
#
# Niveaux :
# urgent🔴 notification sonore — interruption humaine
# update → ✅ notification silencieuse — info non bloquante
# info → 💬 notification silencieuse — log passif
#
# Canaux :
# supervisor → groupe SUPERVISOR (défaut pour urgent)
# monitoring → channel Monitoring (défaut pour update/info)
# (si omis) → supervisor pour urgent, monitoring pour update/info
#
# Token lu depuis MYSECRETS — jamais hardcodé.
set -euo pipefail
MYSECRETS="${BRAIN_ROOT:-$HOME/Dev/Docs}/MYSECRETS"
MYSECRETS="${BRAIN_ROOT:-$HOME/Dev/Brain}/MYSECRETS"
if [[ ! -f "$MYSECRETS" ]]; then
echo "[brain-notify] ERREUR : MYSECRETS introuvable à $MYSECRETS" >&2
exit 1
fi
# Lire token + chat_id depuis MYSECRETS (source .env style)
TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$MYSECRETS" | cut -d= -f2-)
CHAT_ID=$(grep '^BRAIN_TELEGRAM_CHAT_ID=' "$MYSECRETS" | cut -d= -f2-)
CHAT_ID_SUPERVISOR=$(grep '^BRAIN_TELEGRAM_CHAT_ID_SUPERVISOR=' "$MYSECRETS" | cut -d= -f2- || true)
CHAT_ID_MONITORING=$(grep '^BRAIN_TELEGRAM_CHAT_ID_MONITORING=' "$MYSECRETS" | cut -d= -f2- || true)
if [[ -z "$TOKEN" || -z "$CHAT_ID" ]]; then
echo "[brain-notify] ERREUR : BRAIN_TELEGRAM_TOKEN ou BRAIN_TELEGRAM_CHAT_ID vide dans MYSECRETS" >&2
# Fallback : ancienne clé unique si les nouvelles ne sont pas encore définies
if [[ -z "$CHAT_ID_SUPERVISOR" && -z "$CHAT_ID_MONITORING" ]]; then
FALLBACK=$(grep '^BRAIN_TELEGRAM_CHAT_ID=' "$MYSECRETS" | cut -d= -f2- || true)
CHAT_ID_SUPERVISOR="$FALLBACK"
CHAT_ID_MONITORING="$FALLBACK"
fi
if [[ -z "$TOKEN" ]]; then
echo "[brain-notify] ERREUR : BRAIN_TELEGRAM_TOKEN vide dans MYSECRETS" >&2
exit 1
fi
MESSAGE="${1:-}"
MESSAGE=$(printf '%b' "${1:-}")
LEVEL="${2:-info}"
CHANNEL="${3:-}"
if [[ -z "$MESSAGE" ]]; then
echo "[brain-notify] ERREUR : message vide" >&2
exit 1
fi
# Préfixe selon le niveau
# Niveau → préfixe + silence
case "$LEVEL" in
urgent) PREFIX="🔴 *BRAIN ESCALADE*" ; SILENT=false ;;
update) PREFIX="✅ *BRAIN UPDATE*" ; SILENT=true ;;
@@ -41,18 +56,34 @@ case "$LEVEL" in
*) PREFIX="💬 *BRAIN*" ; SILENT=true ;;
esac
# Canal par défaut selon le niveau si non spécifié
if [[ -z "$CHANNEL" ]]; then
[[ "$LEVEL" == "urgent" ]] && CHANNEL="supervisor" || CHANNEL="monitoring"
fi
# Sélection du chat_id
case "$CHANNEL" in
supervisor) CHAT_ID="$CHAT_ID_SUPERVISOR" ;;
monitoring) CHAT_ID="$CHAT_ID_MONITORING" ;;
*) CHAT_ID="$CHAT_ID_SUPERVISOR" ;;
esac
if [[ -z "$CHAT_ID" ]]; then
echo "[brain-notify] ERREUR : chat_id manquant pour canal '$CHANNEL' dans MYSECRETS" >&2
exit 1
fi
FULL_MESSAGE="${PREFIX}
${MESSAGE}
_$(date '+%Y-%m-%d %H:%M')_"
# Envoi Telegram
DISABLE_NOTIFICATION=$( [[ "$SILENT" == "true" ]] && echo "true" || echo "false" )
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="$FULL_MESSAGE" \
--data-urlencode "text=$FULL_MESSAGE" \
-d parse_mode="Markdown" \
-d disable_notification="$DISABLE_NOTIFICATION" \
> /dev/null
echo "[brain-notify] [$LEVEL] envoyé"
echo "[brain-notify] [$LEVEL$CHANNEL] envoyé"

173
scripts/brain-setup.sh Executable file
View File

@@ -0,0 +1,173 @@
#!/bin/bash
# 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
#
# Ce script est idempotent — safe à relancer si une étape a échoué.
set -euo pipefail
# ── Config ──────────────────────────────────────────────────────────────────
GITEA="git@git.tetardtek.com:Tetardtek"
BRAIN_NAME="${1:-prod-laptop}"
BRAIN_ROOT="${2:-$HOME/Dev/Brain}"
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"
)
# ── 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 "╔══════════════════════════════════════════════╗"
echo "║ brain-setup.sh — nouvelle machine ║"
echo "║ brain_name : $BRAIN_NAME"
echo "║ brain_root : $BRAIN_ROOT"
echo "╚══════════════════════════════════════════════╝"
echo ""
# ── É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"
# ── Étape 1 — Cloner les satellites ──────────────────────────────────────────
echo ""
echo "[ 1/5 ] Clonage des satellites..."
for entry in "${REPOS[@]}"; do
repo="${entry%%:*}"
dest="${entry#*:}"
dest="${dest/#\~/$HOME}"
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
mkdir -p "$(dirname "$dest")"
git clone "$GITEA/$repo.git" "$dest"
ok "$repo$dest"
fi
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
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 "[ 3/5 ] brain-compose.local.yml..."
LOCAL_COMPOSE="$BRAIN_ROOT/brain-compose.local.yml"
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: "0.5.1"
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 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
# ── Résumé ────────────────────────────────────────────────────────────────────
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ Setup terminé ║"
echo "╚══════════════════════════════════════════════╝"
echo ""
echo " brain_name : $BRAIN_NAME"
echo " brain_root : $BRAIN_ROOT"
echo ""
echo " Prochaine étape :"
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 ""

175
scripts/brain-status.sh Executable file
View File

@@ -0,0 +1,175 @@
#!/bin/bash
# brain-status.sh — Vue live du brain pour toute instance
# Lecture seule. Aucune écriture.
#
# Usage :
# brain-status.sh → résumé complet
# brain-status.sh claims → claims open uniquement
# brain-status.sh locks → fichiers verrouillés
# brain-status.sh signals → signaux pending
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
CLAIMS_DIR="$BRAIN_ROOT/claims"
LOCKS_DIR="$BRAIN_ROOT/locks"
NOW=$(date +%s)
# --- Helpers ---
claim_field() { grep "^${2}:" "$1" | sed 's/^[^:]*: *//' | tr -d '"' | head -1; }
status_icon() {
case "$1" in
open) echo "🟢" ;;
waiting_human) echo "🔶" ;;
paused) echo "⏸ " ;;
closed) echo "✅" ;;
failed) echo "❌" ;;
*) echo "❓" ;;
esac
}
# --- CLAIMS ---
show_claims() {
local filter="${1:-open waiting_human paused}"
local found=0
echo "── Claims ──────────────────────────────────────"
for f in "$CLAIMS_DIR"/*.yml; do
[ -f "$f" ] || continue
local status sess_id scope type opened_at
status=$(claim_field "$f" status)
# Filter
echo "$filter" | grep -qw "$status" || continue
sess_id=$(claim_field "$f" sess_id)
scope=$(claim_field "$f" scope)
type=$(claim_field "$f" type)
opened_at=$(claim_field "$f" opened_at)
printf " %s %-12s %-42s [%s]\n" \
"$(status_icon "$status")" "$type" "$sess_id" "$scope"
found=1
done
[ "$found" -eq 0 ] && echo " (aucun)" || true
}
# --- LOCKS ---
show_locks() {
local found=0
echo "── Locks fichiers ──────────────────────────────"
for f in "$LOCKS_DIR"/*.lock; do
[ -f "$f" ] || continue
local file holder expires_at epoch
file=$(grep '^file:' "$f" | sed 's/^[^:]*: *//')
holder=$(grep '^holder:' "$f" | sed 's/^[^:]*: *//')
expires_at=$(grep '^expires_at:' "$f" | sed 's/^[^:]*: *//')
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
printf " 🔴 %-40s %s (exp: %s)\n" "$file" "$holder" "$expires_at"
else
printf " ⚠️ %-40s expiré\n" "$file"
fi
found=1
done
[ "$found" -eq 0 ] && echo " (aucun)" || true
}
# --- SIGNALS ---
show_signals() {
local brain_index="$BRAIN_ROOT/BRAIN-INDEX.md"
echo "── Signaux pending ─────────────────────────────"
if [ ! -f "$brain_index" ]; then
echo " (BRAIN-INDEX.md introuvable)"
return
fi
local found=0
# Lire les lignes de la table signals avec status=pending
while IFS='|' read -r _ sig_id from_sess to_sess sig_type summary status _; do
sig_id=$(echo "$sig_id" | xargs)
status=$(echo "$status" | xargs)
[ "$status" = "pending" ] || continue
[ -z "$sig_id" ] && continue
[[ "$sig_id" == sig-* ]] || continue
sig_type=$(echo "$sig_type" | xargs)
from_sess=$(echo "$from_sess" | xargs)
summary=$(echo "$summary" | xargs | cut -c1-40)
printf " 📡 %-30s %-16s %s\n" "$sig_id" "$sig_type" "$summary"
found=1
done < "$brain_index"
[ "$found" -eq 0 ] && echo " (aucun)" || true
}
# --- CIRCUIT BREAKERS ---
show_circuit_breakers() {
local fails_dir="$LOCKS_DIR/fails"
local max_fails
max_fails=$(grep -A5 'circuit_breaker:' "$BRAIN_ROOT/brain-compose.yml" \
| grep 'max_consecutive_fails:' | awk '{print $2}' | head -1 2>/dev/null || echo 3)
local found=0
echo "── Circuit breakers ────────────────────────────"
for f in "$fails_dir"/*.count; do
[ -f "$f" ] || continue
local count sess_id
count=$(cat "$f")
sess_id=$(basename "$f" .count)
if [ "$count" -ge "$max_fails" ] 2>/dev/null; then
printf " 🔴 %s : %s/%s fails\n" "$sess_id" "$count" "$max_fails"
else
printf " ⚠️ %s : %s/%s fails\n" "$sess_id" "$count" "$max_fails"
fi
found=1
done
[ "$found" -eq 0 ] && echo " (aucun)" || true
}
# --- HEADER ---
show_header() {
local branch
branch=$(git -C "$BRAIN_ROOT" branch --show-current 2>/dev/null || echo "?")
local open_count=0 lock_count=0
while IFS= read -r f; do [ -f "$f" ] && open_count=$((open_count+1)); done \
< <(find "$CLAIMS_DIR" -name "*.yml" 2>/dev/null)
# recount only open/waiting/paused
open_count=0
for f in "$CLAIMS_DIR"/*.yml; do
[ -f "$f" ] || continue
s=$(claim_field "$f" status)
case "$s" in open|waiting_human|paused) open_count=$((open_count+1)) ;; esac
done
for f in "$LOCKS_DIR"/*.lock; do
[ -f "$f" ] && lock_count=$((lock_count+1))
done
echo "╔══════════════════════════════════════════════╗"
printf "║ 🧠 Brain status %-27s║\n" "$(date +%H:%M)"
printf "║ branch: %-36s║\n" "$branch"
printf "║ open: %s claims locks: %s ║\n" "$open_count" "$lock_count"
echo "╚══════════════════════════════════════════════╝"
}
# --- Router ---
CMD="${1:-all}"
case "$CMD" in
claims) show_claims "open waiting_human paused" ;;
locks) show_locks ;;
signals) show_signals ;;
all|"")
show_header
echo ""
show_claims "open waiting_human paused"
echo ""
show_locks
echo ""
show_signals
echo ""
show_circuit_breakers
;;
*)
echo "Usage : brain-status.sh [all|claims|locks|signals]"
exit 1
;;
esac

74
scripts/brain-tier-count.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# brain-tier-count.sh — Audite les lignes chargées en context_tier: always
# Alerte si > 1500 lignes (seuil warn) ou > 2000 lignes (seuil KPI fail)
#
# Usage : bash scripts/brain-tier-count.sh
# Appelé par : helloWorld au boot (vérification rapide)
#
# Ref : brain-constitution.md ## KPI NORTH STAR
# always-tier total < 1 500 lignes → ok
# always-tier total > 2 000 lignes → context-tier-split requis (KPI fail)
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WARN_THRESHOLD=1500
FAIL_THRESHOLD=2000
total_lines=0
declare -A file_lines
files_found=()
# Extraire et vérifier uniquement le frontmatter YAML (entre les deux premiers ---)
is_always_tier() {
python3 - "$1" <<'PYEOF'
import sys, re
with open(sys.argv[1], 'r', errors='replace') as f:
content = f.read()
# Extraire le frontmatter (entre --- et ---)
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not m:
sys.exit(1)
frontmatter = m.group(1)
if re.search(r'^context_tier:\s*always', frontmatter, re.MULTILINE):
sys.exit(0)
sys.exit(1)
PYEOF
}
# Trouver tous les fichiers always-tier
while IFS= read -r file; do
[[ -f "$file" ]] || continue
if is_always_tier "$file"; then
lines=$(wc -l < "$file")
file_lines["$file"]=$lines
total_lines=$((total_lines + lines))
files_found+=("$file")
fi
done < <(find "$BRAIN_ROOT" -maxdepth 3 \( -name "*.md" -o -name "*.yml" \) | \
grep -v '\.git\|node_modules\|_template\|\.example')
# Affichage
echo "=== Brain Context Tier: always — Audit ==="
echo ""
# Trier par taille décroissante
for file in "${files_found[@]}"; do
rel="${file#$BRAIN_ROOT/}"
printf " %4d lignes %s\n" "${file_lines[$file]}" "$rel"
done | sort -rn
echo ""
echo "────────────────────────────────────"
printf " TOTAL : %d lignes\n" "$total_lines"
if [[ $total_lines -gt $FAIL_THRESHOLD ]]; then
echo " 🔴 KPI FAIL — context-tier-split requis (brain-constitution.md §3)"
echo " Seuil : $FAIL_THRESHOLD / Actuel : $total_lines"
elif [[ $total_lines -gt $WARN_THRESHOLD ]]; then
echo " ⚠️ WARN — approche du seuil KPI ($WARN_THRESHOLD)"
echo " Seuil fail : $FAIL_THRESHOLD / Actuel : $total_lines"
else
echo " ✅ OK — sous le seuil ($WARN_THRESHOLD)"
fi
echo "────────────────────────────────────"

View File

@@ -1,68 +1,214 @@
#!/bin/bash
# brain-watch-local.sh — Daemon SUPERVISOR local (desktop)
# Surveille BRAIN-INDEX.md via inotifywait (instant, sans polling)
# Lance en arrière-plan : nohup brain-watch-local.sh >> ~/brain-watch.log 2>&1 &
# brain-watch-local.sh — Daemon crash handler + supervisor local
# Extension système HORS brain — zéro token, zéro Claude.
#
# Détecte :
# - Nouveau Claim ouvert notify update
# - Claim fermé → notify info
# - Nouveau Signal → notify selon criticité
# - Condition d'escalade → notify urgent
# Responsabilités :
# 1. Crash detection : process Claude mort → auto-close claim BSI
# 2. Stale TTL check : claim expiré → alerte desktop + Telegram
# 3. Réaction aux changements BRAIN-INDEX.md via inotify (ou poll fallback)
# 4. Notify : notify-send (desktop) + brain-notify.sh (Telegram)
#
# PID tracking — convention helloWorld :
# Ouverture claim : echo $PPID > ~/.claude/sessions/<sess-id>.pid
# Fermeture claim : rm -f ~/.claude/sessions/<sess-id>.pid
#
# Install :
# scripts/install-brain-watch.sh local
# systemctl --user enable --now brain-watch-local
set -euo pipefail
BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Docs}"
BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Brain}"
BRAIN_INDEX="$BRAIN_ROOT/BRAIN-INDEX.md"
NOTIFY="$BRAIN_ROOT/scripts/brain-notify.sh"
BRAIN_NOTIFY="$BRAIN_ROOT/scripts/brain-notify.sh"
BSI_QUERY="$BRAIN_ROOT/scripts/bsi-query.sh"
SESSIONS_DIR="${HOME}/.claude/sessions"
STALE_NOTIFIED_FILE="/tmp/brain-watch-local-stale.txt"
POLL_INTERVAL=30
LOG_PREFIX="[brain-watch-local]"
if [[ ! -f "$BRAIN_INDEX" ]]; then
echo "$LOG_PREFIX ERREUR : BRAIN-INDEX.md introuvable à $BRAIN_INDEX" >&2
exit 1
fi
mkdir -p "$SESSIONS_DIR"
touch "$STALE_NOTIFIED_FILE"
if [[ ! -x "$NOTIFY" ]]; then
chmod +x "$NOTIFY"
fi
# ── Helpers ───────────────────────────────────────────────────────────────────
echo "$LOG_PREFIX Démarré — surveillance de $BRAIN_INDEX"
log() { echo "$LOG_PREFIX $*"; }
# Snapshot initial pour détecter les diffs
snapshot_claims() {
grep -c '^\|' "$BRAIN_INDEX" 2>/dev/null || echo 0
notify_desktop() {
local msg="$1"
command -v notify-send &>/dev/null \
&& notify-send "🧠 Brain SUPERVISOR" "$msg" -u normal -t 8000 \
|| true
}
PREV_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1)
PREV_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0)
notify_telegram() {
local msg="$1" level="${2:-info}"
[[ -x "$BRAIN_NOTIFY" ]] && "$BRAIN_NOTIFY" "$msg" "$level" || true
}
inotifywait -m -e close_write,moved_to "$BRAIN_INDEX" 2>/dev/null | while read -r _dir _event _file; do
notify_all() {
notify_desktop "$1"
notify_telegram "$1" "${2:-info}"
}
NEW_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1)
[[ "$NEW_HASH" == "$PREV_HASH" ]] && continue
PREV_HASH="$NEW_HASH"
# ── Crash detection ───────────────────────────────────────────────────────────
NEW_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0)
check_crashed_sessions() {
for pid_file in "$SESSIONS_DIR"/*.pid; do
[[ -f "$pid_file" ]] || continue
# Nouveau claim détecté
if [[ "$NEW_CLAIMS" -gt "$PREV_CLAIMS" ]]; then
SESS=$(grep '^\| sess-' "$BRAIN_INDEX" | tail -1 | awk -F'|' '{print $2}' | xargs)
"$NOTIFY" "Nouvelle session détectée\n*Session :* \`$SESS\`\nVérifier les claims actifs dans BRAIN-INDEX.md" "update"
echo "$LOG_PREFIX Nouveau claim : $SESS"
fi
local sess_id pid claim_line claim_state
sess_id=$(basename "$pid_file" .pid)
pid=$(cat "$pid_file" 2>/dev/null | tr -d '[:space:]' || echo "")
[[ -z "$pid" ]] && continue
# Claim fermé
if [[ "$NEW_CLAIMS" -lt "$PREV_CLAIMS" ]]; then
"$NOTIFY" "Session fermée — claim libéré\nClaims actifs restants : $NEW_CLAIMS" "info"
echo "$LOG_PREFIX Claim fermé — claims restants : $NEW_CLAIMS"
fi
# Process encore vivant → skip
kill -0 "$pid" 2>/dev/null && continue
PREV_CLAIMS="$NEW_CLAIMS"
# Process mort — claim encore open ?
claim_line=$(grep "^| ${sess_id} " "$BRAIN_INDEX" 2>/dev/null | head -1 || true)
[[ -z "$claim_line" ]] && { rm -f "$pid_file"; continue; }
# Détecter signaux BLOCKED_ON (escalade potentielle)
if grep -q 'BLOCKED_ON' "$BRAIN_INDEX" 2>/dev/null; then
BLOCKED=$(grep 'BLOCKED_ON' "$BRAIN_INDEX" | head -1)
"$NOTIFY" "Conflit détecté entre sessions\n$BLOCKED\nIntervention requise." "urgent"
echo "$LOG_PREFIX ESCALADE : BLOCKED_ON détecté"
fi
claim_state=$(echo "$claim_line" | awk -F'|' '{print $8}' | xargs 2>/dev/null || echo "")
done
if [[ "$claim_state" == "open" ]]; then
log "CRASH : $sess_id (PID $pid mort, claim open) → auto-close"
notify_all "💥 Session crashée : $sess_id\nClaim auto-fermé par le crash handler." "urgent"
_auto_close_claim "$sess_id"
fi
rm -f "$pid_file"
done
}
_auto_close_claim() {
local sess_id="$1"
# Remplacer | open | par | closed | sur la ligne du claim
sed -i "s/^| ${sess_id} \(.*\)| open |/| ${sess_id} \1| closed |/" "$BRAIN_INDEX" || {
log "WARNING : sed failed sur $sess_id"
return 1
}
cd "$BRAIN_ROOT"
git add BRAIN-INDEX.md \
&& git commit -m "bsi: auto-close crashed claim ${sess_id}" \
&& git push \
&& log "$sess_id fermé + pushé" \
|| log "WARNING : commit/push échoué après auto-close $sess_id"
}
# ── Stale TTL ─────────────────────────────────────────────────────────────────
check_stale_claims() {
# Source : brain.db via bsi-query.sh — fallback grep BRAIN-INDEX si brain.db absent
local stale_lines
if [[ -x "$BSI_QUERY" ]] && bash "$BSI_QUERY" count-stale &>/dev/null; then
stale_lines=$(bash "$BSI_QUERY" stale 2>/dev/null || true)
else
# Fallback : parse BRAIN-INDEX.md (brain.db absent ou bsi-query.sh indisponible)
stale_lines=$(grep '^| sess-' "$BRAIN_INDEX" 2>/dev/null | grep '| open |' || true)
[[ -z "$stale_lines" ]] && return
# Format fallback : convertir ligne markdown en format bsi-query (sess_id | scope | opened_at | age_h)
stale_lines=$(echo "$stale_lines" | awk -F'|' '{
gsub(/^ +| +$/,"",$2); gsub(/^ +| +$/,"",$4); gsub(/^ +| +$/,"",$6);
print $2 " | " $4 " | " $6 " | fallback"
}')
fi
[[ -z "$stale_lines" ]] && return
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local sess_id
sess_id=$(echo "$line" | cut -d'|' -f1 | xargs)
[[ -z "$sess_id" ]] && continue
grep -qF "$sess_id" "$STALE_NOTIFIED_FILE" 2>/dev/null && continue
local age_h
age_h=$(echo "$line" | cut -d'|' -f4 | xargs)
log "STALE : $sess_id (${age_h})"
notify_all "⚠️ Claim stale : $sess_id\n${age_h}\nRecovery requis." "update"
echo "$sess_id" >> "$STALE_NOTIFIED_FILE"
done <<< "$stale_lines"
}
# ── BSI events (nouveau claim / fermé / signals) ──────────────────────────────
PREV_HASH=""
PREV_CLAIMS=0
bsi_events() {
local new_hash new_claims
new_hash=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1)
[[ "$new_hash" == "$PREV_HASH" ]] && return
PREV_HASH="$new_hash"
# Source : brain.db via bsi-query.sh — fallback grep BRAIN-INDEX si brain.db absent
if [[ -x "$BSI_QUERY" ]] && bash "$BSI_QUERY" count-open &>/dev/null; then
new_claims=$(bash "$BSI_QUERY" count-open 2>/dev/null || echo 0)
else
new_claims=$(grep '^| sess-' "$BRAIN_INDEX" 2>/dev/null | grep -c '| open |' || echo 0)
fi
if [[ "$new_claims" -gt "$PREV_CLAIMS" ]]; then
local sess
if [[ -x "$BSI_QUERY" ]] && bash "$BSI_QUERY" count-open &>/dev/null; then
sess=$(bash "$BSI_QUERY" open 2>/dev/null | head -1 | cut -d'|' -f1 | xargs)
else
sess=$(grep '^| sess-' "$BRAIN_INDEX" | grep '| open |' | tail -1 | awk -F'|' '{print $2}' | xargs)
fi
log "Nouveau claim : $sess"
notify_all "🟢 Nouvelle session : $sess" "update"
fi
if [[ "$new_claims" -lt "$PREV_CLAIMS" ]]; then
log "Claim fermé — restants : $new_claims"
notify_all "✅ Session fermée — claims actifs : $new_claims" "info"
fi
PREV_CLAIMS="$new_claims"
# BLOCKED_ON — uniquement sur lignes sig-
local blocked
blocked=$(grep '^| sig-' "$BRAIN_INDEX" 2>/dev/null | grep 'BLOCKED_ON' | head -1 || true)
if [[ -n "$blocked" ]]; then
log "ESCALADE : BLOCKED_ON"
notify_all "🚨 Conflit inter-sessions\n$blocked\nIntervention requise." "urgent"
fi
# CHECKPOINT / HANDOFF pending
local signal
signal=$(grep '^| sig-' "$BRAIN_INDEX" 2>/dev/null | grep -E 'CHECKPOINT|HANDOFF' | grep 'pending' | head -1 || true)
if [[ -n "$signal" ]]; then
local sig_type sig_to
sig_type=$(echo "$signal" | awk -F'|' '{print $5}' | xargs)
sig_to=$(echo "$signal" | awk -F'|' '{print $4}' | xargs)
log "SIGNAL : $sig_type$sig_to"
notify_all "📋 $sig_type$sig_to\nHandoff disponible." "update"
fi
}
# ── Boucle principale ─────────────────────────────────────────────────────────
log "Démarré — BRAIN_INDEX: $BRAIN_INDEX"
PREV_HASH=$(md5sum "$BRAIN_INDEX" 2>/dev/null | cut -d' ' -f1 || echo "")
PREV_CLAIMS=$(grep '^| sess-' "$BRAIN_INDEX" 2>/dev/null | grep -c '| open |' || echo 0)
if command -v inotifywait &>/dev/null; then
log "Mode inotify — réactif"
while true; do
inotifywait -q -t "$POLL_INTERVAL" -e close_write "$BRAIN_INDEX" 2>/dev/null || true
check_crashed_sessions
check_stale_claims
bsi_events
done
else
log "Mode poll ${POLL_INTERVAL}s (apt install inotify-tools pour le mode réactif)"
while true; do
sleep "$POLL_INTERVAL"
check_crashed_sessions
check_stale_claims
bsi_events
done
fi

View File

@@ -36,6 +36,40 @@ echo "$LOG_PREFIX Démarré — poll toutes les ${POLL_INTERVAL}s"
PREV_HASH=$(md5sum "$BRAIN_INDEX" 2>/dev/null | cut -d' ' -f1 || echo "")
PREV_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" 2>/dev/null | grep -c '^\| sess-' || echo 0)
# Dédup stale — évite de respammer la même notif à chaque poll
STALE_NOTIFIED_FILE="/tmp/brain-watch-stale-notified.txt"
touch "$STALE_NOTIFIED_FILE"
check_stale_claims() {
local now_epoch
now_epoch=$(date +%s)
while IFS= read -r line; do
# Extraire l'ID de session (colonne 2) et la date d'expiration (colonne 6)
local sess_id expire_raw expire_epoch
sess_id=$(echo "$line" | awk -F'|' '{print $2}' | xargs)
expire_raw=$(echo "$line" | awk -F'|' '{print $6}' | xargs)
# Normaliser : "2026-03-14 18:24" ou "2026-03-14 +4h" → epoch
# On ne gère que le format "YYYY-MM-DD HH:MM" (format standard du BSI)
expire_epoch=$(date -d "$expire_raw" +%s 2>/dev/null || echo 0)
[[ "$expire_epoch" -eq 0 ]] && continue
[[ "$now_epoch" -le "$expire_epoch" ]] && continue
# TTL expiré — vérifier si déjà notifié
if grep -qF "$sess_id" "$STALE_NOTIFIED_FILE" 2>/dev/null; then
continue
fi
# Première détection → notifier + mémoriser
"$NOTIFY" "Claim stale détecté\n*Session :* \`$sess_id\`\n*Expiré le :* $expire_raw\nRecovery requis dans la session superviseur." "update"
echo "$LOG_PREFIX STALE : $sess_id (expiré $expire_raw)"
echo "$sess_id" >> "$STALE_NOTIFIED_FILE"
done < <(grep '^| sess-' "$BRAIN_INDEX" 2>/dev/null | grep 'active' || true)
}
while true; do
sleep "$POLL_INTERVAL"
@@ -45,6 +79,9 @@ while true; do
continue
}
# Vérification stale à chaque poll (indépendante du hash)
check_stale_claims
NEW_HASH=$(md5sum "$BRAIN_INDEX" | cut -d' ' -f1)
[[ "$NEW_HASH" == "$PREV_HASH" ]] && continue
PREV_HASH="$NEW_HASH"
@@ -54,7 +91,7 @@ while true; do
NEW_CLAIMS=$(grep -v '^\*Aucun claim' "$BRAIN_INDEX" | grep -c '^\| sess-' 2>/dev/null || echo 0)
if [[ "$NEW_CLAIMS" -gt "$PREV_CLAIMS" ]]; then
SESS=$(grep '^\| sess-' "$BRAIN_INDEX" | tail -1 | awk -F'|' '{print $2}' | xargs)
SESS=$(grep '^| sess-' "$BRAIN_INDEX" | grep 'active' | tail -1 | awk -F'|' '{print $2}' | xargs)
"$NOTIFY" "Nouvelle session détectée\n*Session :* \`$SESS\`" "update"
echo "$LOG_PREFIX Nouveau claim : $SESS"
fi
@@ -66,10 +103,23 @@ while true; do
PREV_CLAIMS="$NEW_CLAIMS"
if grep -q 'BLOCKED_ON' "$BRAIN_INDEX" 2>/dev/null; then
BLOCKED=$(grep 'BLOCKED_ON' "$BRAIN_INDEX" | head -1)
"$NOTIFY" "Conflit inter-sessions (VPS)\n$BLOCKED\nIntervention requise." "urgent"
# BLOCKED_ON : uniquement dans les lignes de signaux réels (commence par "| sig-")
# Évite le faux positif sur la doc du fichier ("- `BLOCKED_ON` — ...")
BLOCKED=$(grep '^| sig-' "$BRAIN_INDEX" 2>/dev/null | grep 'BLOCKED_ON' | head -1 || true)
if [[ -n "$BLOCKED" ]]; then
"$NOTIFY" "Conflit inter-sessions\n$BLOCKED\nIntervention requise." "urgent"
echo "$LOG_PREFIX ESCALADE : BLOCKED_ON"
fi
# CHECKPOINT / HANDOFF signal — notifier le supervisor
SIGNAL=$(grep '^| sig-' "$BRAIN_INDEX" 2>/dev/null | grep -E 'CHECKPOINT|HANDOFF' | grep 'pending' | head -1 || true)
if [[ -n "$SIGNAL" ]]; then
SIG_TYPE=$(echo "$SIGNAL" | awk -F'|' '{print $5}' | xargs)
SIG_FROM=$(echo "$SIGNAL" | awk -F'|' '{print $3}' | xargs)
SIG_TO=$(echo "$SIGNAL" | awk -F'|' '{print $4}' | xargs)
SIG_PAYLOAD=$(echo "$SIGNAL" | awk -F'|' '{print $7}' | xargs)
"$NOTIFY" "📋 *$SIG_TYPE*\n*De :* \`$SIG_FROM\`\n*Pour :* \`$SIG_TO\`\n*Payload :* $SIG_PAYLOAD\nSession cible : lire le fichier au prochain boot." "update"
echo "$LOG_PREFIX SIGNAL : $SIG_TYPE $SIG_FROM$SIG_TO"
fi
done

109
scripts/bsi-query.sh Executable file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# bsi-query.sh — Requêtes BSI via brain.db (SQLite)
# Remplace les grep sur BRAIN-INDEX.md pour les opérations courantes.
#
# Usage :
# bsi-query.sh open → liste les claims open (sess_id | scope | opened_at | age_h)
# bsi-query.sh stale → claims open depuis > 4h
# bsi-query.sh count-open → nombre de claims open (entier, stdout)
# 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
#
# Retour :
# Exit 0 = succès (même si 0 résultats)
# Exit 1 = brain.db absent (fallback : utiliser grep BRAIN-INDEX.md)
# Exit 2 = erreur Python
#
# Sécurité : lecture seule sur brain.db — aucune écriture
# Fallback : si brain.db absent → le script sort 1, l'appelant gère
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="$BRAIN_ROOT/brain.db"
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
exit 1
fi
run_query() {
python3 - "$DB_PATH" "$@" <<'PYEOF'
import sqlite3, sys, os
db_path = sys.argv[1]
cmd = sys.argv[2] if len(sys.argv) > 2 else 'help'
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
if cmd == 'open':
rows = conn.execute("""
SELECT sess_id, scope, opened_at,
ROUND((julianday('now') - julianday(opened_at)) * 24, 1) AS age_h
FROM claims WHERE status = 'open'
ORDER BY opened_at DESC
""").fetchall()
for r in rows:
print(f"{r['sess_id']} | {r['scope']} | {r['opened_at']} | {r['age_h']}h")
elif cmd == 'stale':
rows = conn.execute("""
SELECT sess_id, scope, opened_at,
ROUND((julianday('now') - julianday(opened_at)) * 24, 1) AS age_h
FROM claims
WHERE status = 'open'
AND julianday('now') > julianday(opened_at, '+4 hours')
ORDER BY age_h DESC
""").fetchall()
for r in rows:
print(f"{r['sess_id']} | {r['scope']} | {r['opened_at']} | {r['age_h']}h")
elif cmd == 'count-open':
n = conn.execute("SELECT COUNT(*) FROM claims WHERE status='open'").fetchone()[0]
print(n)
elif cmd == 'count-stale':
n = conn.execute("""
SELECT COUNT(*) FROM claims
WHERE status='open'
AND julianday('now') > julianday(opened_at, '+4 hours')
""").fetchone()[0]
print(n)
elif cmd == 'signals':
rows = conn.execute("""
SELECT sig_id, type, from_sess, to_sess, projet, payload
FROM signals
WHERE state = 'pending'
AND type IN ('CHECKPOINT','HANDOFF','BLOCKED_ON')
ORDER BY created_at DESC
""").fetchall()
for r in rows:
print(f"{r['sig_id']} | {r['type']} | {r['from_sess']} → {r['to_sess']} | {r['projet']}")
elif cmd == 'health':
row = conn.execute("""
SELECT sess_id, date, type, health_score, cold_start_kpi_pass
FROM sessions
ORDER BY date DESC, sess_id DESC
LIMIT 1
""").fetchone()
if row:
kpi = {1:'✅', 0:'❌', None:'—'}.get(row['cold_start_kpi_pass'], '—')
print(f"{row['sess_id']} | {row['type']} | health={row['health_score']} | cold_start={kpi}")
else:
print("aucune session dans brain.db")
else:
print("Usage: bsi-query.sh open|stale|count-open|count-stale|signals|health", file=sys.stderr)
sys.exit(0)
conn.close()
PYEOF
}
run_query "$CMD"

213
scripts/file-lock.sh Executable file
View File

@@ -0,0 +1,213 @@
#!/bin/bash
# file-lock.sh — Mutex fichier BSI-v3-7
# 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.
#
# Usage :
# file-lock.sh acquire <filepath> <sess-id> [ttl_minutes] → acquiert le lock
# file-lock.sh release <filepath> <sess-id> → libère le lock
# file-lock.sh check <filepath> → qui détient le lock ?
# file-lock.sh list → tous les locks actifs
# file-lock.sh cleanup → supprime les locks expirés
#
# Exit codes :
# 0 = succès
# 1 = lock déjà détenu par une autre session (acquire)
# 2 = erreur (sess-id incorrect pour release, fichier introuvable)
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
LOCKS_DIR="$BRAIN_ROOT/locks"
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|^-||'
}
# --- ACQUIRE ---
cmd_acquire() {
local filepath="$1"
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
# 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
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
echo "✅ Lock acquis : $filepath"
echo " Session : $sess_id"
echo " Expire : $expires_at"
}
# --- RELEASE ---
cmd_release() {
local filepath="$1"
local sess_id="$2"
local lockname
lockname=$(filepath_to_lockname "$filepath")
local lockfile="$LOCKS_DIR/${lockname}.lock"
if [ ! -f "$lockfile" ]; 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)"
exit 2
fi
rm -f "$lockfile"
echo "✅ Lock libéré : $filepath"
}
# --- CHECK ---
cmd_check() {
local filepath="$1"
local lockname
lockname=$(filepath_to_lockname "$filepath")
local lockfile="$LOCKS_DIR/${lockname}.lock"
if [ ! -f "$lockfile" ]; 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)
if [ "$now" -lt "$existing_epoch" ]; then
echo "🔴 Locké : $filepath"
echo " Holder : $existing_holder"
echo " Expire : $existing_expires"
else
echo "⚠️ Lock expiré (nettoyable) : $filepath"
echo " Ancien holder : $existing_holder"
fi
}
# --- LIST ---
cmd_list() {
local locks
locks=$(find "$LOCKS_DIR" -name "*.lock" | sort)
if [ -z "$locks" ]; 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"
}
# --- 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
if [ "$count" -eq 0 ]; then
echo "✅ Aucun lock expiré à nettoyer"
else
echo "$count lock(s) nettoyé(s)"
fi
}
# --- Router ---
CMD="${1:-}"
case "$CMD" in
acquire) cmd_acquire "${2:-}" "${3:-}" "${4:-}" ;;
release) cmd_release "${2:-}" "${3:-}" ;;
check) cmd_check "${2:-}" ;;
list) cmd_list ;;
cleanup) cmd_cleanup ;;
*)
echo "Usage : file-lock.sh <acquire|release|check|list|cleanup>"
echo ""
echo " acquire <filepath> <sess-id> [ttl_min] → acquiert le lock (défaut: 60min)"
echo " release <filepath> <sess-id> → libère le lock"
echo " check <filepath> → état du lock"
echo " list → tous les locks actifs"
echo " cleanup → supprime les locks expirés"
exit 1
;;
esac

View File

@@ -1,42 +0,0 @@
#!/bin/bash
# get-telegram-chatid.sh — Récupère le chat_id Telegram et l'écrit dans MYSECRETS
# NE JAMAIS afficher la valeur dans le terminal — écriture directe dans MYSECRETS
#
# Prérequis : avoir envoyé /start au bot sur Telegram
set -euo pipefail
MYSECRETS="${BRAIN_ROOT:-$HOME/Dev/Docs}/MYSECRETS"
TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$MYSECRETS" | cut -d= -f2-)
if [[ -z "$TOKEN" ]]; then
echo "ERREUR : BRAIN_TELEGRAM_TOKEN vide dans MYSECRETS" >&2
exit 1
fi
# Récupérer le chat_id sans l'afficher
RESPONSE=$(curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates")
CHAT_ID=$(echo "$RESPONSE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
results = data.get('result', [])
if not results:
print('NONE')
else:
print(results[-1].get('message', {}).get('chat', {}).get('id', 'NONE'))
" 2>/dev/null)
if [[ "$CHAT_ID" == "NONE" || -z "$CHAT_ID" ]]; then
echo "Aucun message reçu. Envoie /start au bot sur Telegram puis relance ce script." >&2
exit 1
fi
# Écrire dans MYSECRETS sans afficher la valeur
if grep -q '^BRAIN_TELEGRAM_CHAT_ID=' "$MYSECRETS"; then
sed -i "s/^BRAIN_TELEGRAM_CHAT_ID=.*/BRAIN_TELEGRAM_CHAT_ID=${CHAT_ID}/" "$MYSECRETS"
else
echo "BRAIN_TELEGRAM_CHAT_ID=${CHAT_ID}" >> "$MYSECRETS"
fi
echo "✅ BRAIN_TELEGRAM_CHAT_ID enregistré dans MYSECRETS — valeur non affichée"

324
scripts/human-gate-ack.sh Executable file
View File

@@ -0,0 +1,324 @@
#!/bin/bash
# human-gate-ack.sh — BSI-v3-5 Human Gate
# Gère les pauses planifiées (gate:human) et les arrêts d'urgence (pause/resume/abort).
# Point de contrôle humain sur le flux satellite.
#
# Usage :
# human-gate-ack.sh gate <sess_id> [message] → déclare un gate:human (satellite s'arrête)
# human-gate-ack.sh approve <sess_id> [message] → valide le gate → reprise
# human-gate-ack.sh reject <sess_id> [message] → refuse le gate → failed
# human-gate-ack.sh pause <sess_id> [message] → arrêt d'urgence (cascade enfants)
# human-gate-ack.sh resume <sess_id> [message] → reprise après pause
# human-gate-ack.sh abort <sess_id> [message] → abandon définitif
# human-gate-ack.sh status <sess_id> → état du claim + enfants
#
# Statuts claim :
# open → travail en cours
# waiting_human → gate:human déclaré — attend confirmation
# paused → arrêt d'urgence — pre-flight bloque enfants en cascade
# closed → terminé ok
# failed → terminé en erreur
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
CLAIMS_DIR="$BRAIN_ROOT/claims"
# --- Helpers ---
get_claim_file() {
echo "$CLAIMS_DIR/${1}.yml"
}
get_status() {
local claim_file="$1"
grep '^status:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"' | head -1
}
set_status() {
local claim_file="$1"
local new_status="$2"
sed -i "s/^status:.*/status: $new_status/" "$claim_file"
}
append_gate_event() {
local claim_file="$1"
local event="$2"
local message="${3:-}"
local ts
ts=$(date +%Y-%m-%dT%H:%M)
if ! grep -q '^gate_history:' "$claim_file"; then
echo "gate_history:" >> "$claim_file"
fi
if [ -n "$message" ]; then
echo " - { ts: \"$ts\", event: $event, message: \"$message\" }" >> "$claim_file"
else
echo " - { ts: \"$ts\", event: $event }" >> "$claim_file"
fi
}
write_signal() {
local sess_id="$1"
local signal_type="$2"
local message="${3:-}"
local sig_id="sig-$(date +%Y%m%d-%H%M%S)-${signal_type,,}"
local brain_index="$BRAIN_ROOT/BRAIN-INDEX.md"
local ts
ts=$(date +%Y-%m-%dT%H:%M)
# Insérer dans la table signals de BRAIN-INDEX.md
local signal_row="| $sig_id | $sess_id | — | $signal_type | ${message:-$signal_type} | pending |"
if grep -q '## Signals' "$brain_index" 2>/dev/null; then
sed -i "/^## Signals/a $signal_row" "$brain_index"
fi
echo "$sig_id"
}
# Trouve tous les enfants directs d'un claim (parent_satellite = sess_id)
find_children() {
local parent_id="$1"
grep -l "parent_satellite:.*$parent_id" "$CLAIMS_DIR"/*.yml 2>/dev/null \
| xargs -I{} basename {} .yml 2>/dev/null || true
}
# --- GATE (satellite déclare son arrêt planifié) ---
cmd_gate() {
local sess_id="$1"
local message="${2:-gate:human déclenché}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
local current
current=$(get_status "$claim_file")
if [ "$current" != "open" ]; then
echo "❌ Claim non-open (status: $current) — gate:human ignoré"
exit 1
fi
set_status "$claim_file" "waiting_human"
append_gate_event "$claim_file" "HUMAN_GATE" "$message"
write_signal "$sess_id" "HUMAN_GATE" "$message" > /dev/null
echo "🔶 HUMAN GATE — $sess_id"
echo " Message : $message"
echo " Status : waiting_human"
echo " Commande : human-gate-ack.sh approve|reject $sess_id"
}
# --- APPROVE ---
cmd_approve() {
local sess_id="$1"
local message="${2:-approuvé}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
local current
current=$(get_status "$claim_file")
if [ "$current" != "waiting_human" ]; then
echo "❌ Claim non en waiting_human (status: $current)"
exit 1
fi
set_status "$claim_file" "open"
append_gate_event "$claim_file" "APPROVED" "$message"
echo "✅ Gate approuvé — $sess_id"
echo " Satellite peut reprendre."
}
# --- REJECT ---
cmd_reject() {
local sess_id="$1"
local message="${2:-refusé}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
set_status "$claim_file" "failed"
append_gate_event "$claim_file" "REJECTED" "$message"
write_signal "$sess_id" "BLOCKED_ON" "$message" > /dev/null
echo "🚫 Gate refusé — $sess_id → failed"
}
# --- PAUSE (arrêt d'urgence + cascade) ---
cmd_pause() {
local sess_id="$1"
local message="${2:-pause urgence}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
local current
current=$(get_status "$claim_file")
if [ "$current" = "closed" ] || [ "$current" = "failed" ]; then
echo "❌ Claim déjà terminé (status: $current)"
exit 1
fi
set_status "$claim_file" "paused"
append_gate_event "$claim_file" "PAUSED" "$message"
write_signal "$sess_id" "PAUSED" "$message" > /dev/null
echo "⏸ PAUSE — $sess_id"
echo " Message : $message"
echo " Cascade : pré-flight bloquera tous les enfants"
# Cascade — pause récursive des enfants open/waiting
local children
children=$(find_children "$sess_id")
if [ -n "$children" ]; then
echo " Enfants :"
for child_id in $children; do
local child_file
child_file=$(get_claim_file "$child_id")
local child_status
child_status=$(get_status "$child_file")
if [ "$child_status" = "open" ] || [ "$child_status" = "waiting_human" ]; then
set_status "$child_file" "paused"
append_gate_event "$child_file" "PAUSED_CASCADE" "parent $sess_id paused"
echo "$child_id (cascade)"
fi
done
fi
echo ""
echo " Reprise : human-gate-ack.sh resume $sess_id"
echo " Abandon : human-gate-ack.sh abort $sess_id"
}
# --- RESUME ---
cmd_resume() {
local sess_id="$1"
local message="${2:-reprise}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
local current
current=$(get_status "$claim_file")
if [ "$current" != "paused" ]; then
echo "❌ Claim non en pause (status: $current)"
exit 1
fi
set_status "$claim_file" "open"
append_gate_event "$claim_file" "RESUMED" "$message"
echo "▶️ RESUME — $sess_id"
# Cascade resume des enfants paused par cascade
local children
children=$(find_children "$sess_id")
if [ -n "$children" ]; then
for child_id in $children; do
local child_file
child_file=$(get_claim_file "$child_id")
local child_status
child_status=$(get_status "$child_file")
if [ "$child_status" = "paused" ]; then
# Vérifier que la pause vient bien d'une cascade (pas d'une pause manuelle directe)
if grep -q "PAUSED_CASCADE" "$child_file" 2>/dev/null; then
set_status "$child_file" "open"
append_gate_event "$child_file" "RESUMED_CASCADE" "parent $sess_id resumed"
echo " ▶️ $child_id (cascade)"
fi
fi
done
fi
echo " Satellite peut reprendre — pre-flight passera CHECK 1."
}
# --- ABORT ---
cmd_abort() {
local sess_id="$1"
local message="${2:-abandon}"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
set_status "$claim_file" "failed"
append_gate_event "$claim_file" "ABORTED" "$message"
write_signal "$sess_id" "BLOCKED_ON" "aborted: $message" > /dev/null
echo "💀 ABORT — $sess_id → failed"
# Cascade abort des enfants
local children
children=$(find_children "$sess_id")
if [ -n "$children" ]; then
for child_id in $children; do
local child_file
child_file=$(get_claim_file "$child_id")
local child_status
child_status=$(get_status "$child_file")
if [ "$child_status" != "closed" ] && [ "$child_status" != "failed" ]; then
set_status "$child_file" "failed"
append_gate_event "$child_file" "ABORTED_CASCADE" "parent $sess_id aborted"
echo " 💀 $child_id (cascade)"
fi
done
fi
}
# --- STATUS ---
cmd_status() {
local sess_id="$1"
local claim_file
claim_file=$(get_claim_file "$sess_id")
[ -f "$claim_file" ] || { echo "❌ Claim introuvable : $sess_id"; exit 1; }
local current
current=$(get_status "$claim_file")
local scope
scope=$(grep '^scope:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"')
case "$current" in
open) echo "🟢 open — $sess_id [$scope]" ;;
waiting_human) echo "🔶 waiting_human — $sess_id [$scope]" ;;
paused) echo "⏸ paused — $sess_id [$scope]" ;;
closed) echo "✅ closed — $sess_id [$scope]" ;;
failed) echo "❌ failed — $sess_id [$scope]" ;;
*) echo "$current$sess_id [$scope]" ;;
esac
# Enfants
local children
children=$(find_children "$sess_id")
if [ -n "$children" ]; then
echo " Enfants :"
for child_id in $children; do
local child_file
child_file=$(get_claim_file "$child_id")
local child_status
child_status=$(get_status "$child_file")
echo " $child_status$child_id"
done
fi
}
# --- Router ---
CMD="${1:-}"
case "$CMD" in
gate) cmd_gate "${2:-}" "${3:-}" ;;
approve) cmd_approve "${2:-}" "${3:-}" ;;
reject) cmd_reject "${2:-}" "${3:-}" ;;
pause) cmd_pause "${2:-}" "${3:-}" ;;
resume) cmd_resume "${2:-}" "${3:-}" ;;
abort) cmd_abort "${2:-}" "${3:-}" ;;
status) cmd_status "${2:-}" ;;
*)
echo "Usage : human-gate-ack.sh <gate|approve|reject|pause|resume|abort|status> <sess_id> [message]"
exit 1
;;
esac

203
scripts/install-brain-bot.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/bin/bash
# install-brain-bot.sh — Installe brain-bot.py sur le VPS
# =========================================================
#
# Ce script configure le webhook Telegram sur le VPS :
# 1. Copie brain-bot.py dans le dossier brain-watch
# 2. Crée le service systemd brain-bot
# 3. Configure Apache pour proxifier bot.<domaine> → localhost:5001
# 4. Enregistre le webhook Telegram (setWebhook)
#
# Prérequis VPS :
# - Python 3 installé (python3)
# - Apache avec mod_proxy activé
# - Certbot pour le SSL (Let's Encrypt)
# - brain-watch déjà installé (MYSECRETS présent)
#
# Usage :
# bash install-brain-bot.sh
#
# Le script demande les infos manquantes interactivement.
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration — à adapter si besoin
# ---------------------------------------------------------------------------
WATCH_ROOT="/home/tetardtek/brain-watch"
MYSECRETS="$WATCH_ROOT/MYSECRETS"
BOT_PORT=5001
BOT_SCRIPT="$WATCH_ROOT/brain-bot.py"
SERVICE_NAME="brain-bot"
LOG_PREFIX="[install-brain-bot]"
# ---------------------------------------------------------------------------
# Vérifications préalables
# ---------------------------------------------------------------------------
echo "$LOG_PREFIX Vérification des prérequis..."
if [[ ! -f "$MYSECRETS" ]]; then
echo "$LOG_PREFIX ERREUR : MYSECRETS introuvable à $MYSECRETS" >&2
echo " → Lance d'abord install-brain-watch.sh vps" >&2
exit 1
fi
if ! command -v python3 &>/dev/null; then
echo "$LOG_PREFIX ERREUR : python3 non trouvé" >&2
exit 1
fi
TOKEN=$(grep '^BRAIN_TELEGRAM_TOKEN=' "$MYSECRETS" | cut -d= -f2-)
CHAT_ID=$(grep '^BRAIN_TELEGRAM_CHAT_ID_SUPERVISOR=' "$MYSECRETS" | cut -d= -f2-)
if [[ -z "$TOKEN" || -z "$CHAT_ID" ]]; then
echo "$LOG_PREFIX ERREUR : BRAIN_TELEGRAM_TOKEN ou BRAIN_TELEGRAM_CHAT_ID_SUPERVISOR manquant dans MYSECRETS" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Récupérer le domaine pour le webhook
# ---------------------------------------------------------------------------
echo ""
echo "Domaine pour le webhook (ex: bot.tetardtek.com) :"
echo -n "→ "
read -r BOT_DOMAIN
WEBHOOK_URL="https://${BOT_DOMAIN}/webhook"
# ---------------------------------------------------------------------------
# Copie du script
# ---------------------------------------------------------------------------
SCRIPT_SRC="$(dirname "$0")/brain-bot.py"
if [[ ! -f "$SCRIPT_SRC" ]]; then
echo "$LOG_PREFIX ERREUR : brain-bot.py introuvable à $SCRIPT_SRC" >&2
exit 1
fi
cp "$SCRIPT_SRC" "$BOT_SCRIPT"
chmod +x "$BOT_SCRIPT"
echo "$LOG_PREFIX brain-bot.py copié → $BOT_SCRIPT"
# ---------------------------------------------------------------------------
# Service systemd
# ---------------------------------------------------------------------------
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit]
Description=Brain SUPERVISOR Telegram Bot
After=network.target
[Service]
Type=simple
User=tetardtek
WorkingDirectory=${WATCH_ROOT}
Environment=BRAIN_WATCH_ROOT=${WATCH_ROOT}
Environment=BRAIN_BOT_PORT=${BOT_PORT}
ExecStart=/usr/bin/python3 ${BOT_SCRIPT}
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
echo "$LOG_PREFIX Service systemd ${SERVICE_NAME} activé ✓"
# ---------------------------------------------------------------------------
# Apache vhost — proxy vers localhost:5001
# ---------------------------------------------------------------------------
VHOST_FILE="/etc/apache2/sites-available/${BOT_DOMAIN}.conf"
cat > "$VHOST_FILE" << EOF
<VirtualHost *:80>
ServerName ${BOT_DOMAIN}
# Redirect HTTP → HTTPS (Certbot complétera)
RewriteEngine On
RewriteRule ^(.*)$ https://${BOT_DOMAIN}\$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName ${BOT_DOMAIN}
# Proxy vers brain-bot Python
ProxyPreserveHost On
ProxyPass /webhook http://127.0.0.1:${BOT_PORT}/webhook
ProxyPassReverse /webhook http://127.0.0.1:${BOT_PORT}/webhook
ProxyPass /health http://127.0.0.1:${BOT_PORT}/health
ProxyPassReverse /health http://127.0.0.1:${BOT_PORT}/health
# SSL — sera complété par Certbot
# SSLCertificateFile ...
# SSLCertificateKeyFile ...
</VirtualHost>
EOF
a2enmod proxy proxy_http rewrite 2>/dev/null || true
a2ensite "${BOT_DOMAIN}" 2>/dev/null || true
echo "$LOG_PREFIX Vhost Apache créé : $VHOST_FILE"
echo ""
echo "→ Lance Certbot pour le SSL :"
echo " sudo certbot --apache -d ${BOT_DOMAIN}"
echo ""
echo -n "SSL Certbot déjà configuré ? (o/n) : "
read -r SSL_DONE
if [[ "$SSL_DONE" == "o" || "$SSL_DONE" == "O" ]]; then
apache2ctl configtest && systemctl reload apache2
echo "$LOG_PREFIX Apache rechargé ✓"
else
echo "$LOG_PREFIX En attente SSL — relance ce script ou recharge Apache après Certbot"
fi
# ---------------------------------------------------------------------------
# Enregistrement webhook Telegram (setWebhook)
# ---------------------------------------------------------------------------
echo ""
echo "$LOG_PREFIX Enregistrement webhook Telegram → $WEBHOOK_URL"
RESPONSE=$(curl -s -X POST \
"https://api.telegram.org/bot${TOKEN}/setWebhook" \
-d "url=${WEBHOOK_URL}")
OK=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('ok','false'))")
if [[ "$OK" == "True" ]]; then
echo "$LOG_PREFIX ✅ Webhook enregistré → $WEBHOOK_URL"
else
echo "$LOG_PREFIX ⚠️ Réponse Telegram inattendue (vérifier SSL et domaine)"
# Ne pas afficher RESPONSE — peut contenir le token
fi
# ---------------------------------------------------------------------------
# Test de santé
# ---------------------------------------------------------------------------
echo ""
sleep 2
STATUS=$(curl -s "http://127.0.0.1:${BOT_PORT}/health" || echo "DOWN")
if echo "$STATUS" | grep -q '"ok"'; then
echo "$LOG_PREFIX ✅ brain-bot actif sur port ${BOT_PORT}"
else
echo "$LOG_PREFIX ⚠️ brain-bot ne répond pas — vérifier : journalctl -u ${SERVICE_NAME} -n 20"
fi
echo ""
echo "────────────────────────────────────────"
echo " brain-bot installé"
echo " Webhook : $WEBHOOK_URL"
echo " Service : systemctl status ${SERVICE_NAME}"
echo " Logs : journalctl -u ${SERVICE_NAME} -f"
echo "────────────────────────────────────────"

107
scripts/install-brain-engine.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# install-brain-engine.sh — Installe Brain-as-a-Service sur le VPS (BE-3c)
#
# Usage :
# bash scripts/install-brain-engine.sh → installation complète
# bash scripts/install-brain-engine.sh --check → vérifie l'état sans modifier
#
# Prérequis :
# - BRAIN_TOKEN défini dans MYSECRETS
# - Ollama actif + nomic-embed-text pullé
# - brain.db indexé (embed.py déjà lancé)
#
# Après installation :
# sudo systemctl status brain-engine
# curl http://localhost:7700/health
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SERVICE_SRC="$BRAIN_ROOT/toolkit/systemd/brain-engine.service"
SERVICE_DST="/etc/systemd/system/brain-engine.service"
MYSECRETS="$BRAIN_ROOT/MYSECRETS"
# ── Check mode ─────────────────────────────────────────────────────────────────
check_mode() {
echo "=== brain-engine install --check ==="
local ok=true
# BRAIN_TOKEN dans MYSECRETS
if grep -q "^BRAIN_TOKEN=.\+" "$MYSECRETS" 2>/dev/null; then
echo "✅ BRAIN_TOKEN défini dans MYSECRETS"
else
echo "❌ BRAIN_TOKEN absent ou vide dans MYSECRETS"
ok=false
fi
# Service installé
if [[ -f "$SERVICE_DST" ]]; then
echo "✅ Service installé : $SERVICE_DST"
else
echo "⚠️ Service non installé (sudo requis)"
ok=false
fi
# Service actif
if systemctl is-active --quiet brain-engine 2>/dev/null; then
echo "✅ brain-engine actif"
else
echo "⚠️ brain-engine non actif"
ok=false
fi
# /health répond
if curl -sf http://localhost:7700/health &>/dev/null; then
echo "✅ /health répond sur :7700"
else
echo "⚠️ /health injoignable (serveur non démarré ?)"
ok=false
fi
$ok && exit 0 || exit 1
}
# ── Install ────────────────────────────────────────────────────────────────────
install_mode() {
echo "=== brain-engine install ==="
# Vérifications préalables
if ! grep -q "^BRAIN_TOKEN=.\+" "$MYSECRETS" 2>/dev/null; then
echo "❌ BRAIN_TOKEN absent ou vide dans MYSECRETS — arrêt." >&2
echo " Ajouter : BRAIN_TOKEN=<token> dans $MYSECRETS" >&2
exit 1
fi
echo "✅ BRAIN_TOKEN présent"
if ! python3 -c "import fastapi, uvicorn" 2>/dev/null; then
echo "⚙️ Installation des dépendances Python..."
pip3 install fastapi uvicorn httpx --break-system-packages
fi
echo "✅ Dépendances Python OK"
# Copie du service
echo "⚙️ Installation du service systemd..."
sudo cp "$SERVICE_SRC" "$SERVICE_DST"
sudo systemctl daemon-reload
sudo systemctl enable brain-engine
sudo systemctl restart brain-engine
echo "✅ Service installé et démarré"
sleep 2
if curl -sf http://localhost:7700/health | python3 -m json.tool; then
echo "✅ brain-engine opérationnel — port 7700"
else
echo "⚠️ /health injoignable — vérifier : sudo journalctl -u brain-engine -n 50"
exit 1
fi
}
# ── Main ───────────────────────────────────────────────────────────────────────
case "${1:-}" in
--check) check_mode ;;
"") install_mode ;;
*) echo "Usage : install-brain-engine.sh [--check]" >&2; exit 1 ;;
esac

78
scripts/install-brain-hooks.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# install-brain-hooks.sh — Installe les hooks git brain
#
# Usage :
# scripts/install-brain-hooks.sh → installe dans .git/hooks/
# 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
#
# Idempotent — peut être relancé sans risque.
# À relancer sur chaque clone frais (hooks non versionnés dans git).
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOKS_DIR="$BRAIN_ROOT/.git/hooks"
CHECK_ONLY=false
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=true
hook_installed() {
[[ -f "$HOOKS_DIR/post-commit" ]] && grep -q "brain-db-sync" "$HOOKS_DIR/post-commit" 2>/dev/null
}
if $CHECK_ONLY; then
if hook_installed; then
echo "✅ Hooks brain installés"
exit 0
else
echo "⚠️ Hooks brain non installés — lancer: scripts/install-brain-hooks.sh"
exit 1
fi
fi
mkdir -p "$HOOKS_DIR"
# ── post-commit ────────────────────────────────────────────────────────────────
POST_COMMIT="$HOOKS_DIR/post-commit"
# Préserver un hook post-commit existant non-brain (append)
if [[ -f "$POST_COMMIT" ]] && ! grep -q "brain-db-sync" "$POST_COMMIT"; then
echo "" >> "$POST_COMMIT"
echo "# ── brain-db-sync (ajouté par install-brain-hooks.sh) ──" >> "$POST_COMMIT"
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)
if [[ "$_brain_changed" == "yes" ]]; then
BRAIN_ROOT="$(git rev-parse --show-toplevel)"
bash "$BRAIN_ROOT/scripts/brain-db-sync.sh" --quiet || true
fi
HOOK
echo "✅ Hook post-commit existant complété"
else
# Créer from scratch
cat > "$POST_COMMIT" <<'HOOK'
#!/usr/bin/env bash
# brain post-commit hook — installé par scripts/install-brain-hooks.sh
# 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)
if [[ "$_brain_changed" == "yes" ]]; then
BRAIN_ROOT="$(git rev-parse --show-toplevel)"
bash "$BRAIN_ROOT/scripts/brain-db-sync.sh" --quiet || true
fi
HOOK
chmod +x "$POST_COMMIT"
echo "✅ Hook post-commit installé"
fi
echo ""
echo "Hooks brain actifs :"
echo " post-commit → brain-db-sync.sh (déclenché sur claims/ handoffs/ BRAIN-INDEX.md)"
echo ""
echo "Pour vérifier : scripts/install-brain-hooks.sh --check"

View File

@@ -10,37 +10,52 @@
set -euo pipefail
TARGET="${1:-both}"
BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Docs}"
BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Brain}"
VPS_USER="root"
VPS_IP=$(grep '^VPS_IP=' "$BRAIN_ROOT/MYSECRETS" | cut -d= -f2-)
VPS_WATCH_ROOT="/home/tetardtek/brain-watch"
GITEA_BRAIN_URL="git@git.tetardtek.com:Tetardtek/brain.git"
install_local() {
echo "=== Installation SUPERVISOR local ==="
echo "=== Installation SUPERVISOR local (systemd user) ==="
chmod +x "$BRAIN_ROOT/scripts/brain-notify.sh"
chmod +x "$BRAIN_ROOT/scripts/brain-watch-local.sh"
# Lancer en background
LOGFILE="$HOME/brain-watch.log"
nohup "$BRAIN_ROOT/scripts/brain-watch-local.sh" >> "$LOGFILE" 2>&1 &
echo "PID $! — logs : $LOGFILE"
# Créer le service systemd user
SERVICE_DIR="$HOME/.config/systemd/user"
mkdir -p "$SERVICE_DIR"
# Ajouter au .bashrc pour redémarrage automatique (si pas déjà présent)
MARKER="# brain-watch-local"
if ! grep -q "$MARKER" "$HOME/.bashrc" 2>/dev/null; then
cat >> "$HOME/.bashrc" << EOF
cat > "$SERVICE_DIR/brain-watch-local.service" << EOF
[Unit]
Description=Brain SUPERVISOR local — crash handler + BSI watcher
After=default.target
$MARKER
if ! pgrep -f "brain-watch-local.sh" > /dev/null; then
nohup $BRAIN_ROOT/scripts/brain-watch-local.sh >> $HOME/brain-watch.log 2>&1 &
fi
[Service]
Type=simple
ExecStart=$BRAIN_ROOT/scripts/brain-watch-local.sh
Restart=on-failure
RestartSec=10
Environment=BRAIN_ROOT=$BRAIN_ROOT
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
echo "Ajouté au .bashrc — démarrage automatique à l'ouverture du terminal"
fi
echo "✅ SUPERVISOR local installé"
systemctl --user daemon-reload
systemctl --user enable brain-watch-local
systemctl --user start brain-watch-local
systemctl --user status brain-watch-local --no-pager | head -8
# Linger : service actif même sans session ouverte
loginctl enable-linger "$USER" 2>/dev/null || true
echo ""
echo "✅ brain-watch-local installé (systemd user)"
echo " Logs : journalctl --user -u brain-watch-local -f"
echo " Stop : systemctl --user stop brain-watch-local"
}
install_vps() {
@@ -116,4 +131,4 @@ echo "2. Copier le token dans MYSECRETS : BRAIN_TELEGRAM_TOKEN=<token>"
echo "3. Envoyer /start au bot sur Telegram, puis :"
echo " bash brain/scripts/get-telegram-chatid.sh"
echo " → écrit BRAIN_TELEGRAM_CHAT_ID dans MYSECRETS directement — valeur jamais affichée"
echo "4. Tester : BRAIN_ROOT=~/Dev/Docs brain/scripts/brain-notify.sh 'Test SUPERVISOR' urgent"
echo "4. Tester : BRAIN_ROOT=~/Dev/Brain brain/scripts/brain-notify.sh 'Test SUPERVISOR' urgent"

125
scripts/kernel-isolation-check.sh Executable file
View File

@@ -0,0 +1,125 @@
#!/bin/bash
# kernel-isolation-check.sh — Firewall toolkit/private
# Vérifie qu'aucun agent kernel ne contient de dépendances dures vers des fichiers privés.
#
# WARN : référence documentaire (normal — l'agent décrit l'architecture)
# ERROR : dépendance dure (problème — l'agent ne peut pas fonctionner sans le fichier privé)
#
# Usage : bash scripts/kernel-isolation-check.sh [--strict]
# --strict : traite les WARN comme des ERROR
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
AGENTS_DIR="$BRAIN_ROOT/agents"
STRICT=${1:-""}
ERRORS=()
WARNS=()
# --- Patterns ERROR : dépendances dures — jamais dans un agent distributable ---
# Chemin absolu machine, requires:, load:, source: vers privé
ERROR_PATTERNS=(
"toolkit/private/"
"require.*toolkit/private"
"load.*MYSECRETS"
"source.*MYSECRETS"
)
# Patterns de chemin absolu — exclusions pour les placeholders templates
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 ---
# L'agent mentionne le concept mais n'en dépend pas fonctionnellement
WARN_PATTERNS=(
"MYSECRETS"
"brain-compose.local"
"profil/capital"
"profil/objectifs"
"progression/"
)
echo "🔍 Kernel isolation check — agents/ → dépendances privées"
echo ""
# --- Scan ERROR — patterns interdits ---
for pattern in "${ERROR_PATTERNS[@]}"; do
matches=$(grep -rl "$pattern" "$AGENTS_DIR" \
--include="*.md" \
--exclude-dir=reviews \
2>/dev/null || true)
if [ -n "$matches" ]; then
while IFS= read -r file; do
rel="${file#$BRAIN_ROOT/}"
line=$(grep -n "$pattern" "$file" | head -1 | cut -d: -f1)
ERRORS+=(" 🚨 ERROR $rel:$line → dépendance dure \"$pattern\"")
done <<< "$matches"
fi
done
# --- 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 \
| grep -v "$ABSOLUTE_PATH_EXCLUDE" || true)
if [ -n "$matches" ]; then
rel="${file#$BRAIN_ROOT/}"
line=$(echo "$matches" | head -1 | cut -d: -f1)
ERRORS+=(" 🚨 ERROR $rel:$line → chemin machine absolu hardcodé")
fi
done < <(find "$AGENTS_DIR" -name "*.md" \
-not -path "*/reviews/*" \
-not -path "*/_template*" \
| tr '\n' '\0')
# --- Scan WARN ---
for pattern in "${WARN_PATTERNS[@]}"; do
matches=$(grep -rl "$pattern" "$AGENTS_DIR" \
--include="*.md" \
--exclude-dir=reviews \
2>/dev/null || true)
if [ -n "$matches" ]; then
while IFS= read -r file; do
rel="${file#$BRAIN_ROOT/}"
line=$(grep -n "$pattern" "$file" | head -1 | cut -d: -f1)
WARNS+=(" ⚠️ WARN $rel:$line → référence doc \"$pattern\"")
done <<< "$matches"
fi
done
# --- Rapport ---
if [ ${#ERRORS[@]} -gt 0 ]; then
echo "🚨 ERREURS — dépendances dures détectées (kernel NON distribuable) :"
echo ""
for e in "${ERRORS[@]}"; do echo "$e"; done
echo ""
fi
if [ ${#WARNS[@]} -gt 0 ]; then
echo "⚠️ AVERTISSEMENTS — références documentaires (attendu, pas bloquant) :"
echo ""
for w in "${WARNS[@]}"; do echo "$w"; done
echo ""
echo " Ces références décrivent l'architecture brain — elles n'empêchent pas la distribution."
echo " Un utilisateur qui forke aura ses propres fichiers à ces chemins."
echo ""
fi
# --- Résultat ---
if [ ${#ERRORS[@]} -eq 0 ] && [ "$STRICT" != "--strict" ]; then
echo "✅ Kernel isolation OK — aucune dépendance dure privée détectée"
echo " ${#WARNS[@]} références documentaires (normales)"
exit 0
elif [ ${#ERRORS[@]} -eq 0 ] && [ "$STRICT" = "--strict" ] && [ ${#WARNS[@]} -eq 0 ]; then
echo "✅ Kernel isolation OK (strict) — zéro violation"
exit 0
elif [ "$STRICT" = "--strict" ] && [ ${#WARNS[@]} -gt 0 ]; then
echo "🚨 Mode strict — ${#WARNS[@]} WARN traités comme ERROR"
exit 1
else
echo "🚨 ${#ERRORS[@]} erreur(s) bloquante(s) — corriger avant distribution"
exit 1
fi

60
scripts/kernel-lock-gen.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# kernel-lock-gen.sh — Génère kernel.lock
# Checksums SHA-256 de tous les fichiers zone:kernel trackés
# Usage : bash scripts/kernel-lock-gen.sh
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
LOCK_FILE="$BRAIN_ROOT/kernel.lock"
# Extraire la version depuis brain-compose.yml
VERSION=$(grep '^version:' "$BRAIN_ROOT/brain-compose.yml" | head -1 | sed 's/version: "//;s/"//')
GENERATED_AT=$(date +%Y-%m-%dT%H:%M)
# --- Écriture du header ---
cat > "$LOCK_FILE" << EOF
# kernel.lock — généré automatiquement
# Ne pas éditer manuellement.
# Régénérer : bash scripts/kernel-lock-gen.sh
# Vérifier : bash scripts/kernel-isolation-check.sh
kernel_version: "$VERSION"
generated_at: "$GENERATED_AT"
files:
EOF
# --- Fichiers kernel racine ---
KERNEL_ROOT_FILES=(
"KERNEL.md"
"brain-compose.yml"
"brain-constitution.md"
)
for f in "${KERNEL_ROOT_FILES[@]}"; do
if [ -f "$BRAIN_ROOT/$f" ]; then
hash=$(sha256sum "$BRAIN_ROOT/$f" | cut -d' ' -f1)
echo " $f: $hash" >> "$LOCK_FILE"
fi
done
# --- agents/ (hors reviews/) ---
while IFS= read -r -d '' f; do
rel="${f#$BRAIN_ROOT/}"
hash=$(sha256sum "$f" | cut -d' ' -f1)
echo " $rel: $hash" >> "$LOCK_FILE"
done < <(find "$BRAIN_ROOT/agents" -name "*.md" \
-not -path "*/reviews/*" \
-not -path "*/_template*" \
| sort | tr '\n' '\0')
# --- scripts/ ---
while IFS= read -r -d '' f; do
rel="${f#$BRAIN_ROOT/}"
hash=$(sha256sum "$f" | cut -d' ' -f1)
echo " $rel: $hash" >> "$LOCK_FILE"
done < <(find "$BRAIN_ROOT/scripts" -name "*.sh" -o -name "*.py" \
| sort | tr '\n' '\0')
echo "✅ kernel.lock généré — version $VERSION ($(grep -c ': [a-f0-9]\{64\}' "$LOCK_FILE") fichiers)"

219
scripts/kernel-update-check.sh Executable file
View File

@@ -0,0 +1,219 @@
#!/bin/bash
# kernel-update-check.sh — Comparaison kernel local vs upstream
# Détecte les fichiers mis à jour upstream + conflits avec modifications locales
# avant de puller une nouvelle version du kernel.
#
# Usage :
# bash scripts/kernel-update-check.sh
# bash scripts/kernel-update-check.sh --remote <url-ou-path> # upstream custom
# bash scripts/kernel-update-check.sh --apply # applique les updates non-conflictuelles
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
LOCAL_LOCK="$BRAIN_ROOT/kernel.lock"
REMOTE=${1:-""}
REMOTE_ARG=${2:-""}
APPLY=false
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case $1 in
--remote) REMOTE_PATH="$2"; shift 2 ;;
--apply) APPLY=true; shift ;;
*) shift ;;
esac
done
# --- Résolution de l'upstream ---
# Priorité : --remote > brain-compose.yml kernel_upstream > git remote origin/brain-template
UPSTREAM_REMOTE="${REMOTE_PATH:-}"
if [ -z "$UPSTREAM_REMOTE" ]; then
# Lire depuis brain-compose.yml si défini
UPSTREAM_REMOTE=$(grep '^kernel_upstream:' "$BRAIN_ROOT/brain-compose.yml" 2>/dev/null \
| sed "s/kernel_upstream: *['\"]//;s/['\"]$//" || true)
fi
if [ -z "$UPSTREAM_REMOTE" ]; then
echo " Upstream non configuré."
echo " Option 1 : bash scripts/kernel-update-check.sh --remote /path/to/brain-template"
echo " Option 2 : ajouter 'kernel_upstream: <url>' dans brain-compose.yml"
echo ""
echo " Mode local — vérification intégrité uniquement (checksums vs fichiers actuels)"
echo ""
UPSTREAM_REMOTE=""
fi
# --- Lecture du kernel.lock local ---
if [ ! -f "$LOCAL_LOCK" ]; then
echo "🚨 kernel.lock introuvable — lancer d'abord : bash scripts/kernel-lock-gen.sh"
exit 1
fi
LOCAL_VERSION=$(grep '^kernel_version:' "$LOCAL_LOCK" | sed 's/kernel_version: "//;s/"//')
echo "🔍 Kernel update check — version locale : $LOCAL_VERSION"
echo ""
# --- Mode intégrité locale (pas d'upstream) ---
if [ -z "$UPSTREAM_REMOTE" ]; then
MODIFIED=()
MISSING=()
while IFS= read -r line; do
# Extraire chemin et hash depuis " chemin: hash"
if [[ "$line" =~ ^[[:space:]]+([^:]+):[[:space:]]([a-f0-9]{64})$ ]]; then
filepath="${BASH_REMATCH[1]}"
expected_hash="${BASH_REMATCH[2]}"
fullpath="$BRAIN_ROOT/$filepath"
if [ ! -f "$fullpath" ]; then
MISSING+=(" ❓ ABSENT $filepath")
else
actual_hash=$(sha256sum "$fullpath" | cut -d' ' -f1)
if [ "$actual_hash" != "$expected_hash" ]; then
MODIFIED+=(" ✏️ MODIFIÉ $filepath")
fi
fi
fi
done < "$LOCAL_LOCK"
if [ ${#MISSING[@]} -gt 0 ]; then
echo "❓ Fichiers kernel absents :"
for m in "${MISSING[@]}"; do echo "$m"; done
echo ""
fi
if [ ${#MODIFIED[@]} -gt 0 ]; then
echo "✏️ Fichiers kernel modifiés localement depuis le dernier lock :"
for m in "${MODIFIED[@]}"; do echo "$m"; done
echo ""
echo " → Régénérer le lock après validation : bash scripts/kernel-lock-gen.sh"
else
echo "✅ Intégrité kernel OK — aucun fichier modifié depuis kernel.lock v$LOCAL_VERSION"
fi
exit 0
fi
# --- Mode upstream (comparaison avec une source externe) ---
UPSTREAM_LOCK=""
# Déterminer si c'est un path local ou une URL git
if [ -d "$UPSTREAM_REMOTE" ]; then
UPSTREAM_LOCK="$UPSTREAM_REMOTE/kernel.lock"
elif [[ "$UPSTREAM_REMOTE" == git@* ]] || [[ "$UPSTREAM_REMOTE" == https://* ]]; then
# Clone shallow pour récupérer kernel.lock uniquement
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
echo "📡 Récupération upstream : $UPSTREAM_REMOTE"
git clone --depth=1 --quiet "$UPSTREAM_REMOTE" "$TMPDIR/upstream" 2>/dev/null
UPSTREAM_LOCK="$TMPDIR/upstream/kernel.lock"
else
echo "🚨 Format upstream non reconnu : $UPSTREAM_REMOTE"
echo " Attendu : /path/local, git@host:repo, ou https://host/repo"
exit 1
fi
if [ ! -f "$UPSTREAM_LOCK" ]; then
echo "🚨 kernel.lock introuvable dans upstream : $UPSTREAM_REMOTE"
exit 1
fi
UPSTREAM_VERSION=$(grep '^kernel_version:' "$UPSTREAM_LOCK" | sed 's/kernel_version: "//;s/"//')
echo " Version upstream : $UPSTREAM_VERSION"
echo ""
# --- Comparaison fichier par fichier ---
UPDATES=() # Upstream plus récent, pas modifié localement → safe to pull
CONFLICTS=() # Upstream plus récent ET modifié localement → revue requise
ONLY_LOCAL=() # Fichier local non présent upstream → custom local
declare -A UPSTREAM_HASHES
declare -A LOCAL_HASHES
# Charger hashes upstream
while IFS= read -r line; do
if [[ "$line" =~ ^[[:space:]]+([^:]+):[[:space:]]([a-f0-9]{64})$ ]]; then
UPSTREAM_HASHES["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
fi
done < "$UPSTREAM_LOCK"
# Charger hashes locaux
while IFS= read -r line; do
if [[ "$line" =~ ^[[:space:]]+([^:]+):[[:space:]]([a-f0-9]{64})$ ]]; then
LOCAL_HASHES["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
fi
done < "$LOCAL_LOCK"
# Comparer
for filepath in "${!UPSTREAM_HASHES[@]}"; do
upstream_hash="${UPSTREAM_HASHES[$filepath]}"
local_hash="${LOCAL_HASHES[$filepath]:-}"
actual_hash=""
if [ -f "$BRAIN_ROOT/$filepath" ]; then
actual_hash=$(sha256sum "$BRAIN_ROOT/$filepath" | cut -d' ' -f1)
fi
if [ "$upstream_hash" != "$local_hash" ]; then
# Upstream a changé vs notre lock
if [ "$actual_hash" = "$local_hash" ] || [ -z "$actual_hash" ]; then
# Fichier non modifié localement → update safe
UPDATES+=("$filepath")
else
# Modifié localement ET changé upstream → conflit
CONFLICTS+=("$filepath")
fi
fi
done
# Fichiers locaux non présents upstream
for filepath in "${!LOCAL_HASHES[@]}"; do
if [ -z "${UPSTREAM_HASHES[$filepath]:-}" ]; then
ONLY_LOCAL+=("$filepath")
fi
done
# --- Rapport ---
if [ ${#UPDATES[@]} -gt 0 ]; then
echo "⬆️ Mises à jour disponibles ($LOCAL_VERSION$UPSTREAM_VERSION) :"
for f in "${UPDATES[@]}"; do echo "$f"; done
echo ""
fi
if [ ${#CONFLICTS[@]} -gt 0 ]; then
echo "⚠️ Conflits — modifiés localement ET mis à jour upstream :"
for f in "${CONFLICTS[@]}"; do echo " 🔴 $f"; done
echo " → Revue manuelle requise avant pull."
echo ""
fi
if [ ${#ONLY_LOCAL[@]} -gt 0 ]; then
echo "🔵 Fichiers locaux uniquement (non présents upstream) :"
for f in "${ONLY_LOCAL[@]}"; do echo " 🔵 $f"; done
echo ""
fi
if [ ${#UPDATES[@]} -eq 0 ] && [ ${#CONFLICTS[@]} -eq 0 ]; then
echo "✅ Kernel à jour — aucune différence avec upstream v$UPSTREAM_VERSION"
fi
# --- Apply mode ---
if $APPLY && [ ${#UPDATES[@]} -gt 0 ]; then
echo ""
echo "⚙️ --apply : copie des updates non-conflictuelles..."
for filepath in "${UPDATES[@]}"; do
src=""
if [ -d "$UPSTREAM_REMOTE" ]; then
src="$UPSTREAM_REMOTE/$filepath"
else
src="$TMPDIR/upstream/$filepath"
fi
if [ -f "$src" ]; then
cp "$src" "$BRAIN_ROOT/$filepath"
echo "$filepath"
fi
done
echo ""
echo "→ Régénérer le lock : bash scripts/kernel-lock-gen.sh"
fi

260
scripts/preflight-check.sh Executable file
View File

@@ -0,0 +1,260 @@
#!/bin/bash
# preflight-check.sh — BSI-v3-8 Pre-flight check
# Valide les 6 conditions avant qu'un satellite commence à écrire.
# Soft-lock kernel : tout satellite hors scope kernel est bloqué sur zone:kernel.
#
# Usage :
# preflight-check.sh check <sess_id> <filepath> → 6 checks, exit 0 = go
# preflight-check.sh fail <sess_id> → enregistre un échec (circuit breaker)
# preflight-check.sh reset <sess_id> → reset fail counter après succès
# preflight-check.sh status <sess_id> → état circuit breaker
#
# Exit codes (check) :
# 0 = go — toutes les vérifications passent
# 1 = scope violation — filepath hors scope déclaré
# 2 = fichier locké — attendre ou signal BLOCKED_ON
# 3 = circuit breaker — arrêt + signal BLOCKED_ON pilote
# 4 = claim invalide — claim non-open ou introuvable
# 5 = zone violation — filepath zone:kernel, claim hors scope kernel (soft lock)
# 6 = mauvaise branche — theme_branch mismatch
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"
# Chemins zone:kernel — synchronisés avec KERNEL.md + brain-index-regen.sh
KERNEL_SCOPES="agents/ profil/ scripts/ KERNEL.md CLAUDE.md PATHS.md brain-compose.yml brain-constitution.md BRAIN-INDEX.md"
mkdir -p "$FAILS_DIR"
# Détermine si un filepath est zone:kernel
is_kernel_path() {
local filepath="$1"
for kscope in $KERNEL_SCOPES; do
if [[ "$filepath" == ${kscope}* ]] || [[ "$filepath" == "$kscope" ]]; then
return 0
fi
done
return 1
}
# Détermine si un scope déclaré couvre la zone kernel
scope_is_kernel() {
local scope="$1"
for kscope in $KERNEL_SCOPES; do
for scope_entry in $scope; do
if [[ "$kscope" == ${scope_entry}* ]] || [[ "$scope_entry" == ${kscope}* ]]; then
return 0
fi
done
done
return 1
}
# --- CHECK ---
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
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"
exit 4
fi
if [ "$claim_status" = "waiting_human" ]; then
echo "❌ CHECK 1 — Gate:human actif : $sess_id"
echo " → human-gate-ack.sh approve|reject $sess_id"
exit 4
fi
if [ "$claim_status" != "open" ]; then
echo "❌ CHECK 1 — Claim non-open : $claim_status"
exit 4
fi
echo "✅ CHECK 1 — Claim open"
# 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 "")
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
fi
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 '"')
local scope_ok=false
for scope_entry in $claim_scope; do
if [[ "$filepath" == ${scope_entry}* ]] || [[ "$filepath" == "$scope_entry" ]]; then
scope_ok=true
break
fi
done
if [ "$scope_ok" = false ]; then
echo "❌ CHECK 2 — Scope violation : $filepath ∉ [$claim_scope]"
exit 1
fi
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
kerneluser=$(grep '^kerneluser:' "$BRAIN_ROOT/brain-compose.yml" | sed 's/^[^:]*: *//' | tr -d '"' | head -1)
if [ "$kerneluser" = "true" ]; then
echo "⚠️ CHECK 3 — Zone:kernel (kerneluser bypass) : $filepath"
echo " Scope [$claim_scope] hors kernel — modification kernel sur confirmation humaine"
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
fi
if ! is_kernel_path "$filepath" || scope_is_kernel "$claim_scope"; then
echo "✅ CHECK 3 — Zone ok"
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
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 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 [ "${fail_count}" -ge "${max_fails}" ] 2>/dev/null; then
echo "❌ CHECK 5 — Circuit breaker : $fail_count/$max_fails fails consécutifs"
echo " → Signal BLOCKED_ON pilote requis — reset manuel après résolution"
exit 3
fi
echo "✅ CHECK 5 — Circuit breaker ok ($fail_count/$max_fails)"
# CHECK 6 — Theme branch
local theme_branch
theme_branch=$(grep '^theme_branch:' "$claim_file" | sed 's/^[^:]*: *//' | tr -d '"' 2>/dev/null || echo "")
if [ -n "$theme_branch" ]; then
local current_branch
current_branch=$(git -C "$BRAIN_ROOT" branch --show-current 2>/dev/null || echo "")
if [ "$current_branch" != "$theme_branch" ]; then
echo "❌ CHECK 6 — Mauvaise branche : sur '$current_branch', attendu '$theme_branch'"
echo " git checkout $theme_branch"
exit 6
fi
fi
echo "✅ CHECK 6 — Branch ok (${theme_branch:-main})"
echo ""
echo "🟢 PRE-FLIGHT PASS — go"
}
# --- 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"
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 "🔴 Circuit breaker déclenché — signal BLOCKED_ON pilote"
fi
}
# --- 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"
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 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)"
else
echo "✅ Circuit breaker ok : $count/$max_fails ($sess_id)"
fi
}
# --- Router ---
CMD="${1:-}"
case "$CMD" in
check) cmd_check "${2:-}" "${3:-}" ;;
fail) cmd_fail "${2:-}" ;;
reset) cmd_reset "${2:-}" ;;
status) cmd_status "${2:-}" ;;
*)
echo "Usage : preflight-check.sh <check|fail|reset|status>"
echo ""
echo " check <sess_id> <filepath> → 6 checks avant écriture (exit 0=go)"
echo " fail <sess_id> → enregistre un échec (circuit breaker)"
echo " reset <sess_id> → reset fail counter après succès"
echo " status <sess_id> → état circuit breaker"
exit 1
;;
esac

89
scripts/theme-branch-merge.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
# theme-branch-merge.sh — Merge une branche thème sur main après validation
# Vérifie l'état de la chaîne (claims enfants fermés, aucun BLOCKED_ON)
# avant de merger sur main.
#
# Usage :
# bash scripts/theme-branch-merge.sh <theme-name>
# bash scripts/theme-branch-merge.sh brain-engine-be6
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
THEME_NAME="${1:-}"
if [ -z "$THEME_NAME" ]; then
echo "Usage : bash scripts/theme-branch-merge.sh <theme-name>"
exit 1
fi
BRANCH="theme/$THEME_NAME"
CURRENT=$(git -C "$BRAIN_ROOT" branch --show-current)
# --- Vérifier que la branche existe ---
if ! git -C "$BRAIN_ROOT" show-ref --quiet "refs/heads/$BRANCH"; then
echo "🚨 Branche $BRANCH introuvable."
exit 1
fi
# --- Vérifier qu'on est bien sur la branche thème ---
if [ "$CURRENT" != "$BRANCH" ]; then
echo "⚠️ Branche courante : $CURRENT"
echo " Basculer d'abord : git checkout $BRANCH"
exit 1
fi
echo "🔍 Validation pré-merge — $BRANCH → main"
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"
fi
# --- Check 2 : aucun signal BLOCKED_ON pending ---
BLOCKED=$(grep -A3 "BLOCKED_ON" "$BRAIN_ROOT/BRAIN-INDEX.md" 2>/dev/null \
| grep "pending" || true)
if [ -n "$BLOCKED" ]; then
BLOCKERS+=(" 🔴 Signal BLOCKED_ON pending dans BRAIN-INDEX.md")
fi
# --- Check 3 : working tree propre ---
if ! git -C "$BRAIN_ROOT" diff --quiet || ! git -C "$BRAIN_ROOT" diff --cached --quiet; then
BLOCKERS+=(" 🔴 Working tree non propre — commiter avant merge")
fi
# --- Rapport ---
if [ ${#BLOCKERS[@]} -gt 0 ]; then
echo "🚨 Merge bloqué — résoudre avant :"
echo ""
for b in "${BLOCKERS[@]}"; do echo "$b"; done
echo ""
exit 1
fi
echo "✅ Chaîne validée — aucun bloqueur détecté"
echo ""
# --- Merge sur main ---
echo "⚙️ Merge $BRANCH → main"
git -C "$BRAIN_ROOT" checkout main
git -C "$BRAIN_ROOT" merge --no-ff "$BRANCH" -m "theme: merge $BRANCH → main [chaîne verte]"
echo ""
echo "✅ Merge terminé — $BRANCH intégré sur main"
echo ""
# --- Proposer suppression branche ---
echo "Supprimer la branche thème ?"
echo " git branch -d $BRANCH"
echo ""
echo "Régénérer kernel.lock si des fichiers kernel ont changé :"
echo " bash scripts/kernel-lock-gen.sh && bash scripts/kernel-isolation-check.sh"

57
scripts/theme-branch-open.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/bin/bash
# theme-branch-open.sh — Ouvre une branche thème pour un pilote ou satellite
# Crée la branche git theme/<name> depuis main et y bascule.
#
# Usage :
# bash scripts/theme-branch-open.sh <theme-name>
# bash scripts/theme-branch-open.sh brain-engine-be6
# bash scripts/theme-branch-open.sh superoauth-tier3
#
# Convention branche : theme/<name>
# La branche reste locale jusqu'au merge — pas de push automatique.
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
THEME_NAME="${1:-}"
if [ -z "$THEME_NAME" ]; then
echo "Usage : bash scripts/theme-branch-open.sh <theme-name>"
echo "Exemple : bash scripts/theme-branch-open.sh brain-engine-be6"
exit 1
fi
BRANCH="theme/$THEME_NAME"
CURRENT=$(git -C "$BRAIN_ROOT" branch --show-current)
# --- Vérifier qu'on part de main ---
if [ "$CURRENT" != "main" ]; then
echo "⚠️ Branche courante : $CURRENT (attendu : main)"
echo " Basculer sur main d'abord : git checkout main"
exit 1
fi
# --- Vérifier que la branche n'existe pas déjà ---
if git -C "$BRAIN_ROOT" show-ref --quiet "refs/heads/$BRANCH"; then
echo "⚠️ Branche $BRANCH existe déjà."
echo " Pour reprendre : git checkout $BRANCH"
exit 1
fi
# --- Vérifier que main est propre ---
if ! git -C "$BRAIN_ROOT" diff --quiet || ! git -C "$BRAIN_ROOT" diff --cached --quiet; then
echo "🚨 Working tree non propre — commiter ou stasher avant d'ouvrir une branche thème."
exit 1
fi
# --- Créer + basculer ---
git -C "$BRAIN_ROOT" checkout -b "$BRANCH"
echo ""
echo "✅ Branche thème ouverte : $BRANCH"
echo ""
echo "Workflow :"
echo " → Satellites commitent sur cette branche"
echo " → Quand chaîne verte : bash scripts/theme-branch-merge.sh $THEME_NAME"
echo ""
echo "Rappel claim : déclarer theme_branch: $BRANCH dans le claim du pilote"

226
scripts/workflow-launch.sh Executable file
View File

@@ -0,0 +1,226 @@
#!/bin/bash
# workflow-launch.sh — Lance le prochain step d'un workflow thématique
# Lit le workflow YAML, trouve le step à lancer, génère le claim BSI correspondant.
#
# Usage :
# bash scripts/workflow-launch.sh <workflow.yml> # step 1 (ou prochain)
# 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.
# (Futur : kernel-orchestrator lancera automatiquement — BSI-v3-9)
set -euo pipefail
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
WORKFLOW_FILE="${1:-}"
MODE="launch"
TARGET_STEP=""
# --- Parse args ---
shift || true
while [[ $# -gt 0 ]]; do
case $1 in
--step) TARGET_STEP="$2"; shift 2 ;;
--status) MODE="status"; shift ;;
*) shift ;;
esac
done
if [ -z "$WORKFLOW_FILE" ]; then
echo "Usage : bash scripts/workflow-launch.sh <workflow.yml> [--step N] [--status]"
echo "Workflows disponibles :"
ls "$BRAIN_ROOT/workflows/"*.yml 2>/dev/null | grep -v "_template" \
| sed "s|$BRAIN_ROOT/workflows/||" | sed 's/^/ /'
exit 1
fi
# Résolution chemin workflow
if [ ! -f "$WORKFLOW_FILE" ]; then
WORKFLOW_FILE="$BRAIN_ROOT/workflows/$WORKFLOW_FILE"
fi
if [ ! -f "$WORKFLOW_FILE" ]; then
echo "🚨 Workflow introuvable : $WORKFLOW_FILE"
exit 1
fi
# --- Lecture du workflow (parser YAML minimal) ---
THEME_NAME=$(grep '^name:' "$WORKFLOW_FILE" | sed 's/name: *//' | tr -d '"')
THEME_BRANCH=$(grep '^branch:' "$WORKFLOW_FILE" | sed 's/branch: *//' | tr -d '"')
echo "📋 Workflow : $THEME_NAME"
echo " Branche : $THEME_BRANCH"
echo ""
# --- Mode status : afficher l'état de la chaîne ---
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
exit 0
fi
# --- Trouver le prochain step à lancer ---
# Lire les steps depuis le workflow
STEPS=()
STEP_TYPES=()
STEP_SCOPES=()
STEP_ANGLES=()
STEP_GATES=()
current_step=""
current_type=""
current_scope=""
current_angle=""
current_gate=""
while IFS= read -r line; do
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]step:[[:space:]]*([0-9]+) ]]; then
# Sauvegarder le step précédent
if [ -n "$current_step" ]; then
STEPS+=("$current_step")
STEP_TYPES+=("$current_type")
STEP_SCOPES+=("$current_scope")
STEP_ANGLES+=("$current_angle")
STEP_GATES+=("$current_gate")
fi
current_step="${BASH_REMATCH[1]}"
current_type=""
current_scope=""
current_angle=""
current_gate=""
elif [[ "$line" =~ ^[[:space:]]+type:[[:space:]]*(.+) ]]; then
current_type="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^[[:space:]]+scope:[[:space:]]*(.+) ]]; then
current_scope="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^[[:space:]]+story_angle:[[:space:]]*\"(.+)\" ]]; then
current_angle="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^[[:space:]]+gate:[[:space:]]*(.+) ]]; then
current_gate="${BASH_REMATCH[1]}"
fi
done < "$WORKFLOW_FILE"
# Sauvegarder le dernier step
if [ -n "$current_step" ]; then
STEPS+=("$current_step")
STEP_TYPES+=("$current_type")
STEP_SCOPES+=("$current_scope")
STEP_ANGLES+=("$current_angle")
STEP_GATES+=("$current_gate")
fi
TOTAL_STEPS=${#STEPS[@]}
if [ "$TOTAL_STEPS" -eq 0 ]; then
echo "🚨 Aucun step trouvé dans le workflow."
exit 1
fi
# Déterminer le step cible
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
STEP_IDX=$LAST_DONE
fi
if [ "$STEP_IDX" -ge "$TOTAL_STEPS" ]; then
echo "✅ Workflow terminé — tous les steps complétés ($TOTAL_STEPS/$TOTAL_STEPS)"
exit 0
fi
# --- Construire le claim pour ce step ---
STEP_NUM="${STEPS[$STEP_IDX]}"
STEP_TYPE="${STEP_TYPES[$STEP_IDX]}"
STEP_SCOPE="${STEP_SCOPES[$STEP_IDX]}"
STEP_ANGLE="${STEP_ANGLES[$STEP_IDX]}"
STEP_GATE="${STEP_GATES[$STEP_IDX]}"
# Déterminer le next step pour on_done
NEXT_IDX=$((STEP_IDX + 1))
ON_DONE=""
ON_FAIL="signal → BLOCKED_ON pilote"
if [ "$NEXT_IDX" -lt "$TOTAL_STEPS" ]; then
NEXT_TYPE="${STEP_TYPES[$NEXT_IDX]}"
NEXT_SCOPE="${STEP_SCOPES[$NEXT_IDX]}"
NEXT_GATE="${STEP_GATES[$NEXT_IDX]}"
if [ "$NEXT_GATE" = "human" ]; then
ON_DONE="gate:human → \"Step $((NEXT_IDX+1)) prêt ($NEXT_TYPE:$NEXT_SCOPE) — lancer ?\""
else
ON_DONE="trigger → type:$NEXT_TYPE scope:$NEXT_SCOPE"
fi
else
ON_DONE="notify → pilote # dernier step — chaîne terminée"
fi
# Gestion gate sur le step courant
if [ "$STEP_GATE" = "human" ]; then
echo "⏸ GATE HUMAN requis avant ce step."
echo " Confirmer avant de lancer le satellite."
echo ""
fi
# Générer le sess_id
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
echo "✅ Claim généré : claims/${SESS_ID}.yml"
echo ""
echo " Step : $STEP_NUM / $TOTAL_STEPS"
echo " Type : $STEP_TYPE"
echo " Scope : $STEP_SCOPE"
echo " Tâche : $STEP_ANGLE"
if [ -n "$STEP_GATE" ]; then
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}\""