Implementeaza planul claude-master-plan-discord-bridge-20260830 (15 taskuri, 3 lane-uri paralele) — un bot subtire discord.py peste CLI-ul `claude`, cu proces persistent per fir alimentat pe stdin cu --input-format stream-json. Nucleu: runner (proces persistent + reaper 20min + respawn --resume), stream (parser tolerant), session_store (scriere atomica, lock per fir, detectare PID reuse, recovery), limits (max 4 procese, timeout tur, rate per user, plafon cost pe zi), render (un loop de editare per canal, interval adaptiv). Adaptor: allowlist guild/canal/user fail-closed cu respingerea webhook-urilor, comenzi !new/!cd/!model/!status/!stop/!cleanup, cost si model in subsolul fiecarui raspuns. Mesajul sosit in timpul unui tur devine steering, nu tur nou. Securitate: hook PreToolUse fail-closed care cere confirmare in Discord pentru operatiuni ireversibile, wrapper `infra` cu lista explicita de hosturi. Deny rules raman strat cosmetic, nu bariera (verificat: /usr/bin/ssh trece pe langa). Ops: alerte email pe conventia repo-ului, !cleanup pentru orfani, unit systemd user cu KillMode=control-group si limite de memorie, install.sh idempotent. Verificat: 275 teste fara retea/Discord/API (10.8s), identic cu si fara discord.py instalat; e2e pe CLI real confirma steering-ul mid-tur (mesaj la 6s intr-un tool call de 25s schimba raspunsul final). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
5.6 KiB
INTERFACES — contract intre lane-uri (proprietate: orchestrator, NU modifica)
Cele trei lane-uri lucreaza in acelasi director. Acest fisier fixeaza cine ce fisier scrie si ce semnaturi trec granita, ca merge-ul sa fie mecanic.
Proprietate pe fisiere (STRICTA — nu scrie in fisierele altui lane)
| Lane | Fisiere pe care le creeaza/editeaza |
|---|---|
| A (nucleu + adaptor) | session_store.py, stream.py, runner.py, render.py, limits.py, bot.py, config.py, tests/**, requirements.txt, requirements-dev.txt |
| B (securitate) | security/confirm_hook.py, security/infra, security/approvals.py, security/bot-settings.json.example, security/README.md, tests/test_confirm_hook.py, tests/test_infra.py |
| C (ops) | alerts.py, cleanup.py, ops/claude-discord.service, ops/install.sh, ops/logrotate.conf, tests/test_alerts.py, tests/test_cleanup.py, README.md, si liniile de index din ../README.md + /workspace/romfastsql/CLAUDE.md |
Fisiere partajate ca citire: acest INTERFACES.md. Nimeni nu-l editeaza.
Layout runtime (in afara repo)
~/.claude-discord/
env # 0600: DISCORD_TOKEN, allowlist, ALERT_RECIPIENT, COST_CAP_USD_DAY
bot-settings.json # settings pasat cu --settings (hook PreToolUse) — Lane B
state.json # stare sesiuni — Lane A
venv/
logs/bot.log
Modelul de date state.json (Lane A e autoritatea)
{
"version": 1,
"threads": {
"<discord_thread_or_channel_id>": {
"sid": "uuid sesiune claude sau null",
"cwd": "/workspace/<proiect>",
"model": "sonnet",
"pid": 12345,
"pid_start_time": 987654.21,
"inflight": {"turn_id": "...", "started_at": 1756512000.0, "user_id": "...", "message_id": "..."},
"cost_usd_total": 0.0,
"last_active": 1756512000.0
}
},
"cost": {"day": "2026-08-30", "usd": 0.0}
}
pid_start_time = /proc/<pid>/stat field 22, pentru detectarea PID reuse.
Granita A <-> B (aprobari)
Lane B expune security/approvals.py. Lane A il importa si nu-i cunoaste interiorul.
# security/approvals.py — implementat de Lane B, consumat de Lane A
async def wait_for_decision(request_id: str, timeout: float) -> str: ...
# returneaza "allow" | "deny"; la timeout returneaza "deny" (fail-closed)
def submit_decision(request_id: str, decision: str) -> bool: ...
# apelat de bot.py cand utilizatorul apasa butonul; True daca cererea exista
async def pending_requests() -> list[dict]: ...
# [{"request_id", "thread_id", "tool_name", "command", "created_at"}]
def set_on_request(callback) -> None: ...
# Lane A inregistreaza aici un async callback(request: dict) apelat cand
# hook-ul cere o confirmare; bot.py posteaza atunci butoanele in fir.
Canalul hook -> bot e un director de cereri pe disc (~/.claude-discord/approvals/), fiindca
hook-ul PreToolUse ruleaza intr-un proces separat, nu in botul Python. Lane B alege formatul;
Lane A vede doar functiile de mai sus.
Fail-closed e obligatoriu: orice eroare, timeout sau fisier corupt => deny.
Granita A <-> C (alerte)
Lane C expune alerts.py. Lane A il apeleaza in caile de esec.
# alerts.py — implementat de Lane C, consumat de Lane A
def alert(level: str, subject: str, body: str, dedup_key: str | None = None) -> None: ...
# level: "INFO" | "WARN" | "CRITICAL"
# trimite email prin `mail -s "[LEVEL] subject" "$ALERT_RECIPIENT"` (conventia repo,
# vezi proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh)
# NU arunca niciodata exceptii — o alerta esuata nu are voie sa doboare botul
# dedup_key: aceeasi cheie nu retrimite in fereastra de 1h
Conditiile pe care Lane A le semnaleaza (T12): proces mort neasteptat, crash loop, plafon de cost atins, state.json corupt, orfani detectati la sweep.
Granita A <-> C (cleanup)
# cleanup.py — implementat de Lane C, consumat de bot.py pentru comanda !cleanup
def find_orphans(state: dict) -> list[dict]: ...
# procese `claude` din cgroup-ul serviciului care nu apar in state.json,
# plus copii lasati in urma (servere pornite in tururi anterioare)
# -> [{"pid", "cmdline", "age_s", "rss_mb"}]
def kill_orphans(orphans: list[dict], dry_run: bool = True) -> list[dict]: ...
Granita comuna: config
Lane A creeaza config.py, care citeste ~/.claude-discord/env. B si C il importa
pentru cai si setari; nu-si citesc singure env-ul.
# config.py — implementat de Lane A
STATE_DIR: pathlib.Path # ~/.claude-discord
APPROVALS_DIR: pathlib.Path # ~/.claude-discord/approvals
STATE_FILE: pathlib.Path
LOG_DIR: pathlib.Path
def get(key: str, default=None) -> str | None: ... # citeste din env-ul incarcat
Daca config.py nu exista inca la momentul in care B sau C au nevoie de el (lane-uri paralele),
scrie codul care il importa oricum — se rezolva la merge — si NU crea o varianta proprie.
Reguli de test
pytestfara marker: zero retea, zero Discord, zero API. Suita sub 3s.- Testele care ating CLI-ul real:
@pytest.mark.e2e, excluse implicit prinpytest.ini(Lane A scriepytest.inicuaddopts = -m "not e2e"). - Fiecare lane isi scrie doar propriile fisiere de test, dupa tabelul de proprietate.
Decizii deja luate (nu le redeschide)
--permission-mode bypassPermissionse intentionat; deny rules sunt strat cosmetic, nu bariera.- Accesul larg la /workspace si la infrastructura e FUNCTIONALITATE ceruta, nu bug.
- Fara user separat
cdbot, fara audit append-only, fara dashboard web, fara Agent SDK. - Fara reluare automata a turului pierdut (risc de dubla executie).
- Model default
sonnet;!model opusper fir.