fix: tier featured dans feature-gate + TIER_RANK + scripts manquants

This commit is contained in:
2026-03-20 20:38:26 +01:00
parent 8244a07881
commit 2b69c3769a
3 changed files with 337 additions and 6 deletions

231
scripts/bsi-claim.sh Executable file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env bash
# bsi-claim.sh — Open/close claims dans brain.db (source unique — ADR-042)
#
# Usage :
# bsi-claim.sh open <sess_id> [--scope X] [--type X] [--zone X] [--mode X] [--story "X"]
# bsi-claim.sh close <sess_id> [--result X]
# bsi-claim.sh close-stale → ferme tous les claims open > TTL (4h par défaut)
# bsi-claim.sh exists <sess_id> → exit 0 si open, exit 1 sinon
# bsi-claim.sh init → crée brain.db + table claims si absent
#
# Garantie tier free : python3 + sqlite3 stdlib — zéro dépendance externe.
# Auto-init : si brain.db ou table claims absente → créée automatiquement.
#
# Exit codes :
# 0 = succès
# 1 = argument manquant / erreur usage
# 2 = erreur Python / DB
set -euo pipefail
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="$BRAIN_ROOT/brain.db"
CMD="${1:-help}"
shift || true
python3 - "$DB_PATH" "$CMD" "$@" <<'PYEOF'
import sqlite3
import sys
from datetime import datetime, timezone
db_path = sys.argv[1]
cmd = sys.argv[2] if len(sys.argv) > 2 else "help"
args = sys.argv[3:]
CLAIMS_SCHEMA = """
CREATE TABLE IF NOT EXISTS claims (
sess_id TEXT PRIMARY KEY,
type TEXT,
scope TEXT,
status TEXT DEFAULT 'open',
opened_at TEXT,
closed_at TEXT,
handoff_level TEXT,
story_angle TEXT,
health_score REAL,
context_at_close REAL,
cold_start_kpi_pass INTEGER,
ttl_hours REAL DEFAULT 4.0,
expires_at TEXT,
instance TEXT,
parent_sess TEXT,
satellite_type TEXT,
satellite_level TEXT,
theme_branch TEXT,
zone TEXT,
mode TEXT,
workflow TEXT,
workflow_step INTEGER,
result_status TEXT,
result_json TEXT
)
"""
def get_db():
"""Connect and ensure table exists (auto-init for fresh forks)."""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
conn.execute(CLAIMS_SCHEMA)
conn.commit()
return conn
def parse_opts(args):
"""Parse --key value pairs from args."""
opts = {}
i = 0
while i < len(args):
if args[i].startswith("--") and i + 1 < len(args):
opts[args[i][2:]] = args[i + 1]
i += 2
else:
i += 1
return opts
def cmd_open():
if not args:
print("❌ Usage: bsi-claim.sh open <sess_id> [--scope X] [--type X] ...", file=sys.stderr)
sys.exit(1)
sess_id = args[0]
opts = parse_opts(args[1:])
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M")
conn = get_db()
# Vérifier si déjà open
existing = conn.execute(
"SELECT status FROM claims WHERE sess_id = ?", (sess_id,)
).fetchone()
if existing and existing["status"] == "open":
print(f"⚠️ Claim déjà ouvert : {sess_id}")
conn.close()
sys.exit(0)
new_scope = opts.get("scope", "brain/")
# Scope overlap detection — BSI mutex
open_claims = conn.execute(
"SELECT sess_id, scope, zone FROM claims WHERE status = 'open'"
).fetchall()
for oc in open_claims:
oc_scope = oc["scope"] or ""
# Overlap = un scope est préfixe de l'autre, ou identique
if (new_scope.startswith(oc_scope) or oc_scope.startswith(new_scope)
or new_scope == oc_scope):
oc_zone = oc["zone"] or "project"
# Zone kernel = hard block
if oc_zone == "kernel" or opts.get("zone") == "kernel":
print(f"🔴 SCOPE CONFLICT — zone kernel verrouillée")
print(f" Existant : {oc['sess_id']} → scope: {oc_scope} (zone: {oc_zone})")
print(f" Demandé : {sess_id} → scope: {new_scope}")
print(f" → Fermer le claim existant d'abord : bsi-claim.sh close {oc['sess_id']}")
conn.close()
sys.exit(1)
# Zone project = soft warning (parallélisme autorisé avec avertissement)
print(f"⚠️ SCOPE OVERLAP détecté")
print(f" Existant : {oc['sess_id']} → scope: {oc_scope}")
print(f" Demandé : {sess_id} → scope: {new_scope}")
print(f" → Parallélisme autorisé — attention aux conflits d'écriture")
conn.execute("""
INSERT OR REPLACE INTO claims
(sess_id, type, scope, status, opened_at, zone, mode, story_angle,
handoff_level, instance, ttl_hours, expires_at)
VALUES (?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 4.0, datetime(?, '+4 hours'))
""", (
sess_id,
opts.get("type", "navigate"),
new_scope,
now,
opts.get("zone", "project"),
opts.get("mode"),
opts.get("story"),
opts.get("handoff", "0"),
opts.get("instance"),
now,
))
conn.commit()
conn.close()
print(f"✅ Claim ouvert : {sess_id}")
def cmd_close():
if not args:
print("❌ Usage: bsi-claim.sh close <sess_id> [--result X]", file=sys.stderr)
sys.exit(1)
sess_id = args[0]
opts = parse_opts(args[1:])
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M")
conn = get_db()
cur = conn.execute(
"UPDATE claims SET status = 'closed', closed_at = ?, result_status = ? WHERE sess_id = ? AND status = 'open'",
(now, opts.get("result", "success"), sess_id)
)
conn.commit()
if cur.rowcount == 0:
print(f"⚠️ Claim non trouvé ou déjà fermé : {sess_id}")
else:
print(f"✅ Claim fermé : {sess_id}")
conn.close()
def cmd_close_stale():
conn = get_db()
cur = conn.execute("""
UPDATE claims
SET status = 'closed',
closed_at = datetime('now'),
result_status = 'stale-auto-closed'
WHERE status = 'open'
AND julianday('now') > julianday(opened_at, '+' || COALESCE(ttl_hours, 4) || ' hours')
""")
conn.commit()
n = cur.rowcount
if n > 0:
print(f"✅ {n} claim(s) stale fermé(s)")
else:
print(" Aucun claim stale")
conn.close()
def cmd_exists():
if not args:
print("❌ Usage: bsi-claim.sh exists <sess_id>", file=sys.stderr)
sys.exit(1)
conn = get_db()
row = conn.execute(
"SELECT status FROM claims WHERE sess_id = ? AND status = 'open'", (args[0],)
).fetchone()
conn.close()
sys.exit(0 if row else 1)
def cmd_init():
conn = get_db()
n = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0]
conn.close()
print(f"✅ brain.db prêt — table claims ({n} entrées)")
def cmd_help():
print("Usage: bsi-claim.sh <open|close|close-stale|exists|init>")
print(" open <sess_id> [--scope X] [--type X] [--zone X] [--mode X] [--story 'X']")
print(" close <sess_id> [--result X]")
print(" close-stale — ferme les claims open > TTL")
print(" exists <sess_id> — exit 0 si open, exit 1 sinon")
print(" init — crée brain.db + table si absent")
commands = {
"open": cmd_open,
"close": cmd_close,
"close-stale": cmd_close_stale,
"exists": cmd_exists,
"init": cmd_init,
"help": cmd_help,
}
fn = commands.get(cmd, cmd_help)
fn()
PYEOF