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:
83
scripts/brain-db-sync.sh
Executable file
83
scripts/brain-db-sync.sh
Executable 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
127
scripts/brain-index-regen.sh
Executable 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
|
||||
@@ -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
173
scripts/brain-setup.sh
Executable 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
175
scripts/brain-status.sh
Executable 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
74
scripts/brain-tier-count.sh
Executable 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 "────────────────────────────────────"
|
||||
@@ -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
|
||||
|
||||
@@ -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
109
scripts/bsi-query.sh
Executable 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
213
scripts/file-lock.sh
Executable 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
|
||||
@@ -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
324
scripts/human-gate-ack.sh
Executable 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
203
scripts/install-brain-bot.sh
Executable 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
107
scripts/install-brain-engine.sh
Executable 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
78
scripts/install-brain-hooks.sh
Executable 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"
|
||||
@@ -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
125
scripts/kernel-isolation-check.sh
Executable 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
60
scripts/kernel-lock-gen.sh
Executable 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
219
scripts/kernel-update-check.sh
Executable 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
260
scripts/preflight-check.sh
Executable 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
89
scripts/theme-branch-merge.sh
Executable 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
57
scripts/theme-branch-open.sh
Executable 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
226
scripts/workflow-launch.sh
Executable 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}\""
|
||||
Reference in New Issue
Block a user