Skip to content

Commit 6692046

Browse files
leopu00claude
andcommitted
refactor(bridge): rinomina capitano-bridge → heartbeat-bridge + role-map
Il nome `capitano-bridge` suggeriva una simmetria con `sentinel-bridge` (come se lì vivesse la logica del Capitano), mentre è solo un nudge orario. Rinominato per FUNZIONE: heartbeat-bridge.py + heartbeat-bridge-state.json + /tmp/heartbeat-bridge.log + env JHT_HEARTBEAT_SESSION. sentinel-bridge e pacing-bridge RESTANO (il primo è coerente col data-layer sentinel-data.jsonl / tabella sentinel_ticks; il secondo è già function-named). Fix docstring stale di pacing-bridge (diceva "tick orario al Capitano" → pinga la Sentinella dal pull-model 2026-06-25). Cablaggio aggiornato: - .launcher/start-agent.sh: spawn/kill (grep /proc), env, log del heartbeat - shared/skills/process_health.py: tabella EXPECTED del canary (bridge-suite) - .launcher/agent-watchdog.sh: commento della suite bridge - agents/{capitano ×7 lingue, mantenitore, maintainer-sweep}: prompt allineati - header role-map nei 3 bridge + NEW docs/internal/architecture/bridges.md (mappa autorevole chi-notifica-chi-e-ogni-quanto) Off-limits (lasciati come record storico): postmortem, docs/sessions, CHANGELOG, migration 013_sentinel_ticks. Verificato: py/bash syntax OK, canary aggiornata, 0 riferimenti runtime residui (solo breadcrumb "ex capitano-bridge.py"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AyN8B4WZkQTXBSpmimhEB
1 parent a85f358 commit 6692046

16 files changed

Lines changed: 113 additions & 36 deletions

File tree

.launcher/agent-watchdog.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ while true; do
297297
for role in "${AGENTS[@]}"; do
298298
ensure_agent "$role"
299299
done
300-
# Bridge/daemon detached (sentinel/pacing/capitano-bridge/window-ratio/
300+
# Bridge/daemon detached (sentinel/pacing/heartbeat-bridge/window-ratio/
301301
# codex-auth-healer + tg-bridge): respawn se morti, con anti-flap. Sono
302302
# fuori dal respawn-on-crash di pid1 (setsid), questo è il loro recovery.
303303
maybe_respawn_bridges
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
#!/usr/bin/env python3
22
"""
3-
capitano-bridge.py — heartbeat ORARIO al Capitano (2026-06-26).
3+
heartbeat-bridge.py — heartbeat ORARIO al Capitano (2026-06-26; ex capitano-bridge.py).
4+
5+
── ROLE-MAP dei bridge deterministici (vedi docs/internal/architecture/bridges.md) ──
6+
sentinel-bridge.py → SENSORE usage: fetch provider ~2-10min (adattivo), scrive
7+
sentinel-data.jsonl, ticka la SENTINELLA ([BRIDGE TICK]).
8+
pacing-bridge.py → REPORT pacing: ogni 15min manda velocità/verdetto alla
9+
SENTINELLA ([BRIDGE PACING]). Il Capitano NON è pingato.
10+
heartbeat-bridge.py → QUESTO: nudge orario al CAPITANO ([HEARTBEAT]); off-hours tace.
11+
Due dei tre parlano alla SENTINELLA; solo questo parla al CAPITANO.
412
513
Perché esiste: col push→pull (bridge→Sentinella) il Capitano non riceve più il
614
[BRIDGE PACING] ogni 15 min, e si è osservato che resta INCAGLIATO quando la
@@ -18,13 +26,13 @@
1826
lascia che sia LUI a verificare (con le sue skill) e decidere.
1927
2028
Output:
21-
- stdout (→ /tmp/capitano-bridge.log)
29+
- stdout (→ /tmp/heartbeat-bridge.log)
2230
- tmux send al CAPITANO via jht-tmux-send (single-line)
2331
2432
Modi:
25-
python3 capitano-bridge.py # loop orario allineato a :00
26-
python3 capitano-bridge.py --once # un colpo, stampa, niente send
27-
python3 capitano-bridge.py --once --send
33+
python3 heartbeat-bridge.py # loop orario allineato a :00
34+
python3 heartbeat-bridge.py --once # un colpo, stampa, niente send
35+
python3 heartbeat-bridge.py --once --send
2836
"""
2937
import json
3038
import os
@@ -39,20 +47,20 @@
3947
SENTINEL_JSONL = LOGS_DIR / "sentinel-data.jsonl"
4048
AGENT_TABLE_FILE = LOGS_DIR / "agent-usage-table.json"
4149
DB_PATH = JHT_HOME / "jobs.db"
42-
STATE_FILE = LOGS_DIR / "capitano-bridge-state.json"
50+
STATE_FILE = LOGS_DIR / "heartbeat-bridge-state.json"
4351
# Daily hard-stop (#2): flag scritto dal sentinel-bridge a cap giornaliero sforato.
4452
# Lo leggiamo (sola lettura): a team in standby l'heartbeat orario tace.
4553
DAILY_HALT_FLAG = LOGS_DIR / "daily-halt.flag"
4654
# work_phase AUTOREVOLE (#4): il pacing-bridge la calcola dalle working hours e la
4755
# scrive qui. Il sentinel-data grezzo la lascia spesso a None → l'heartbeat era
4856
# cieco al giorno/notte e sparava nudge in piena notte.
4957
PACING_STATE_FILE = LOGS_DIR / "pacing-bridge-state.json"
50-
TARGET = os.environ.get("JHT_CAPITANO_HEARTBEAT_SESSION", "CAPITANO")
58+
TARGET = os.environ.get("JHT_HEARTBEAT_SESSION", "CAPITANO")
5159
DB_QUERY = "/app/shared/skills/db_query.py"
5260

5361

5462
def _log(msg):
55-
print(f"[capitano-bridge] {msg}", file=sys.stdout, flush=True)
63+
print(f"[heartbeat-bridge] {msg}", file=sys.stdout, flush=True)
5664

5765

5866
def _db_count(cmd):

.launcher/pacing-bridge.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
#!/usr/bin/env python3
22
"""
3-
pacing-bridge.py — tick orario al Capitano sul ritmo di consumo del team.
3+
pacing-bridge.py — tick di pacing alla SENTINELLA sul ritmo di consumo del team.
4+
5+
── ROLE-MAP dei bridge deterministici (vedi docs/internal/architecture/bridges.md) ──
6+
sentinel-bridge.py → SENSORE usage: fetch provider ~2-10min, scrive
7+
sentinel-data.jsonl, ticka la SENTINELLA ([BRIDGE TICK]).
8+
pacing-bridge.py → QUESTO: report pacing ogni 15min alla SENTINELLA
9+
([BRIDGE PACING]). Il Capitano NON è pingato (pull on-demand).
10+
heartbeat-bridge.py → nudge orario al CAPITANO ([HEARTBEAT]); off-hours tace.
411
512
Ogni 15 minuti, allineato all'orologio (:00, :15, :30, :45 UTC), calcola:
613
@@ -34,7 +41,7 @@
3441
Modi:
3542
python3 pacing-bridge.py # loop infinito allineato all'orologio
3643
python3 pacing-bridge.py --once # un solo tick, stampa, niente send
37-
python3 pacing-bridge.py --once --send # un solo tick + send al CAPITANO
44+
python3 pacing-bridge.py --once --send # un solo tick + send alla SENTINELLA
3845
"""
3946
import importlib.util
4047
import json

.launcher/sentinel-bridge.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#!/usr/bin/env python3
22
"""
3-
Sentinel Bridge V5 — orologio + fetch + tick alla Sentinella.
3+
Sentinel Bridge V5 — orologio + fetch + tick alla Sentinella (SENSORE usage).
4+
5+
── ROLE-MAP dei bridge deterministici (vedi docs/internal/architecture/bridges.md) ──
6+
sentinel-bridge.py → QUESTO: SENSORE usage — fetch provider ~2-10min (adattivo),
7+
scrive sentinel-data.jsonl, ticka la SENTINELLA ([BRIDGE TICK]).
8+
pacing-bridge.py → report pacing ogni 15min alla SENTINELLA ([BRIDGE PACING]).
9+
heartbeat-bridge.py → nudge orario al CAPITANO ([HEARTBEAT]); off-hours tace.
410
511
Architettura V5 (post-incident 2026-04-25):
612
• ogni 5 min: fetch del provider attivo (codex JSONL / kimi HTTP / claude HTTP)
@@ -78,7 +84,7 @@
7884
STATE_FILE = LOGS_DIR / "sentinel-bridge-state.json"
7985
# Daily hard-stop (#2): flag CONDIVISO. Quando il consumo di oggi sfora il cap
8086
# giornaliero, questo bridge lo crea e mette il team in standby; pacing-bridge e
81-
# capitano-bridge lo leggono e tacciono. Rimosso da questo stesso bridge quando il
87+
# heartbeat-bridge lo leggono e tacciono. Rimosso da questo stesso bridge quando il
8288
# budget rientra (inizio finestra di lavoro del giorno dopo / reset weekly).
8389
DAILY_HALT_FLAG = LOGS_DIR / "daily-halt.flag"
8490
STATE_VERSION = 7

.launcher/start-agent.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,21 +184,21 @@ if [ "$ROLE" = "bridge" ]; then
184184
# push→pull il Capitano non riceve più il pacing ogni 15min e si incaglia
185185
# quando la Sentinella tace: questo lo risveglia 1×/ora con un nudge basato sui
186186
# dati DB (deterministico, NON LLM), così resta attivo senza essere passivo.
187-
HEARTBEAT_SCRIPT="/app/.launcher/capitano-bridge.py"
187+
HEARTBEAT_SCRIPT="/app/.launcher/heartbeat-bridge.py"
188188
if [ -f "$HEARTBEAT_SCRIPT" ]; then
189-
for _pid in $(grep -l capitano-bridge.py /proc/[0-9]*/cmdline 2>/dev/null | sed 's|/proc/||;s|/cmdline||'); do
189+
for _pid in $(grep -l heartbeat-bridge.py /proc/[0-9]*/cmdline 2>/dev/null | sed 's|/proc/||;s|/cmdline||'); do
190190
kill -TERM "$_pid" 2>/dev/null || true
191191
done
192192
sleep 0.5
193-
for _pid in $(grep -l capitano-bridge.py /proc/[0-9]*/cmdline 2>/dev/null | sed 's|/proc/||;s|/cmdline||'); do
193+
for _pid in $(grep -l heartbeat-bridge.py /proc/[0-9]*/cmdline 2>/dev/null | sed 's|/proc/||;s|/cmdline||'); do
194194
kill -KILL "$_pid" 2>/dev/null || true
195195
done
196196
sleep 0.5
197197
setsid sh -c "
198-
JHT_CAPITANO_HEARTBEAT_SESSION='${JHT_TARGET_SESSION:-CAPITANO}' \
199-
python3 -u $HEARTBEAT_SCRIPT >> /tmp/capitano-bridge.log 2>&1
198+
JHT_HEARTBEAT_SESSION='${JHT_TARGET_SESSION:-CAPITANO}' \
199+
python3 -u $HEARTBEAT_SCRIPT >> /tmp/heartbeat-bridge.log 2>&1
200200
" >/dev/null 2>&1 < /dev/null &
201-
echo "capitano-bridge (heartbeat orario) partito (log /tmp/capitano-bridge.log)"
201+
echo "heartbeat-bridge (nudge orario al Capitano) partito (log /tmp/heartbeat-bridge.log)"
202202
else
203203
echo "$HEARTBEAT_SCRIPT non trovato — heartbeat NON partito"
204204
fi

agents/_skills/maintainer-sweep/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Il Mantenitore **ripara** l'infra (installa deps mancanti, consolida, sistema).
1616
## Lo sweep (7 step, in ordine)
1717

1818
### 0. 🫀 Liveness canary dei processi salva-vita (la rete di sicurezza)
19-
**PRIMO step, prima di tutto.** I bridge/daemon che fanno vivere il container (sentinel-bridge, pacing-bridge, capitano-bridge, window-ratio-meter, codex-auth-healer + tg-bridge) sono lanciati `setsid` detached → **fuori dal respawn-on-crash di pid1**. L'`agent-watchdog` (`maybe_respawn_bridges`) li risorveglia ogni 30s, MA se anche quello fallisse (bug, flap-cap raggiunto, watchdog stesso degradato) tu sei **l'ultima rete**: al primo sweep del giorno rilevi e ripari. Senza questo canary, un daemon morto resta invisibile per ore (è esattamente com'è successo al sentinel-bridge su andris il 2026-06-27 → 8h ciechi sull'usage).
19+
**PRIMO step, prima di tutto.** I bridge/daemon che fanno vivere il container (sentinel-bridge, pacing-bridge, heartbeat-bridge, window-ratio-meter, codex-auth-healer + tg-bridge) sono lanciati `setsid` detached → **fuori dal respawn-on-crash di pid1**. L'`agent-watchdog` (`maybe_respawn_bridges`) li risorveglia ogni 30s, MA se anche quello fallisse (bug, flap-cap raggiunto, watchdog stesso degradato) tu sei **l'ultima rete**: al primo sweep del giorno rilevi e ripari. Senza questo canary, un daemon morto resta invisibile per ore (è esattamente com'è successo al sentinel-bridge su andris il 2026-06-27 → 8h ciechi sull'usage).
2020
```bash
2121
python3 /app/shared/skills/process_health.py summary
2222
```

agents/capitano/capitano.de.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Dein Operations-Loop. Erkenne den Trigger, öffne die Skill, führe aus.
7676
| **Beginn des Arbeitszeitfensters** (Tagesbeginn, erster `work_phase=ON`-Tick) — email-first Sourcing + Intake-Balancing | `email_monitor.py count`/`poll`**C-16** |
7777
| Nachricht `[@utente -> @capitano] [CHAT]` | `chat-web` |
7878
| Nachricht `[SENTINELLA]` mit einem Rat | `sentinel-orders` (du interpretierst + verifizierst + entscheidest, C-01) |
79-
| Nachricht `[HEARTBEAT]` (stündlich, vom capitano-bridge) — **dein Puls**: neu bewerten | siehe **C-20** |
79+
| Nachricht `[HEARTBEAT]` (stündlich, vom heartbeat-bridge) — **dein Puls**: neu bewerten | siehe **C-20** |
8080
| **Das Pacing** on-demand prüfen (Zweifel an einem Sentinella-Rat, oder wer gerade verbrennt) — der Bridge pingt es dir nicht mehr, du **ziehst es selbst** (zero-cost) | `rate-budget` / `agent-speed-table` |
8181
| Du musst einen Agent spawnen | `spawn-agent` |
8282
| Leere Pipeline / Scaling-Entscheidung / Cold Start | `pipeline-triage` |
@@ -207,7 +207,7 @@ Ohne das gate-weighted C-09 kann die Autonomie C-07 in Phase 1 mit dem alten Mod
207207
- **🌅 Abendreserve (2026-06-26):** die `daily:`-Zeile trägt auch `riserva=R%→tieni|brucia`. **Tagsüber (`tieni`):** pace Richtung `budget − riserva`, fülle morgens **NICHT** bis zum Cap — lass R% für den Abend. **Letzte ~2h (`brucia`):** die Reserve wird frei → entweder nutzt der User sie, um **mit dem Team zu chatten**, oder du **verbrennst sie auf der Arbeit** (erhöhst das Tempo via C-02), damit kein Budget verschwendet wird und du beim Reset bei ~100% landest. Es ist der **Anti-Front-Load**: Kimi neigt dazu, am Vormittag fertig zu werden, und so kann der User am Abend noch mit dem Team interagieren.
208208
- Es ist kein Freeze und kein HALT (es gilt C-09: kein vorzeitiger HALT): es ist ein **Tages-Coast**. Beim Fensterwechsel (nächster Tag) startet der heutige Verbrauch wieder bei 0 und das Team nimmt mit der neu berechneten Quote wieder auf.
209209

210-
**C-20 — `[HEARTBEAT]` = dein stündlicher Puls (2026-06-26).** Mit push→pull erhältst du das Pacing nicht mehr alle 15 min, und das Risiko ist, **passiv** zu bleiben, wenn die Sentinella schweigt. Deshalb sendet dir der `capitano-bridge` 1×/Stunde einen `[HEARTBEAT]`: es ist ein **deterministisches Werkzeug IN DEINEM DIENST** (kein Befehl, nicht die Sentinella), das dir auf Basis der **DB-Daten** eine **Frage/Bedingung** stellt, um dich zur **Neubewertung** zu bringen (Queues leer? verbrennt ein Worker ins Leere? bist du on-pace?). Bei seinem Eingang: **führe ihn nicht blind aus** — er ist ein Anstoß. **Verifiziere** mit deinen Skills (`pipeline-triage`, `rate-budget`, `agent-speed-table`, `capture-pane`), ob die Bedingung real ist, dann **entscheide und handle** du (spawn/kill/throttle/nichts). **Niemals einen Subagenten spawnen** für diese Prüfung (so beobachtet: ein `Task`, der einen Sub-Agenten öffnet, um die Pipeline abzufragen = eine volle Runde, zudem NICHT im Verbrauch erfasst) — die Skill `pipeline-triage` ist bereits ein **Skript**: führe sie direkt aus, eine knappe Query. Der Puls ist jetzt ein reines **Signal** (kein „entscheide du" mehr in der Nachricht): lies das Datum und handle **nur**, wenn es eine echte Anomalie bestätigt, mit EINER Skill. Es ist das Gegenteil des Festfahrens: es hält dich **aktiv** in der Koordination, ohne dich von der Sentinella abhängig zu machen. NB: manchmal **schweigt** der Heartbeat (alles in Ordnung) — das ist völlig in Ordnung, du machst deine Runde weiter.
210+
**C-20 — `[HEARTBEAT]` = dein stündlicher Puls (2026-06-26).** Mit push→pull erhältst du das Pacing nicht mehr alle 15 min, und das Risiko ist, **passiv** zu bleiben, wenn die Sentinella schweigt. Deshalb sendet dir der `heartbeat-bridge` 1×/Stunde einen `[HEARTBEAT]`: es ist ein **deterministisches Werkzeug IN DEINEM DIENST** (kein Befehl, nicht die Sentinella), das dir auf Basis der **DB-Daten** eine **Frage/Bedingung** stellt, um dich zur **Neubewertung** zu bringen (Queues leer? verbrennt ein Worker ins Leere? bist du on-pace?). Bei seinem Eingang: **führe ihn nicht blind aus** — er ist ein Anstoß. **Verifiziere** mit deinen Skills (`pipeline-triage`, `rate-budget`, `agent-speed-table`, `capture-pane`), ob die Bedingung real ist, dann **entscheide und handle** du (spawn/kill/throttle/nichts). **Niemals einen Subagenten spawnen** für diese Prüfung (so beobachtet: ein `Task`, der einen Sub-Agenten öffnet, um die Pipeline abzufragen = eine volle Runde, zudem NICHT im Verbrauch erfasst) — die Skill `pipeline-triage` ist bereits ein **Skript**: führe sie direkt aus, eine knappe Query. Der Puls ist jetzt ein reines **Signal** (kein „entscheide du" mehr in der Nachricht): lies das Datum und handle **nur**, wenn es eine echte Anomalie bestätigt, mit EINER Skill. Es ist das Gegenteil des Festfahrens: es hält dich **aktiv** in der Koordination, ohne dich von der Sentinella abhängig zu machen. NB: manchmal **schweigt** der Heartbeat (alles in Ordnung) — das ist völlig in Ordnung, du machst deine Runde weiter.
211211

212212
**C-21 — Scouts im TEAM, nie allein auf gesättigtem Markt (2026-06-30).** Wenn du Scouts zum Sourcing spawnst, behandle sie als **koordiniertes Team**, nicht als parallele Einzelgänger. Den ERSTEN Scout auf leerer Queue spawnst du sofort (C-05, anti-idle), aber **sobald du über 1 skalierst, ist es ein Team**: jeder weitere Scout bekommt ein **geteiltes Territorium** (Circles/Sources/Städte/Ranges via Skill `scout-coord`), die Scouts **sprechen miteinander**, um neu aufzuteilen, wenn eine Quelle erschöpft ist, und ihr **Verbrauch muss AUSGEGLICHEN** sein — ein Scout bei 150 kT während ein anderer bei 16 kT ist, heißt sie teilen **NICHT** (sie kratzen dieselbe Quelle parallel): Territorien neu aufteilen oder den Runaway killen (C-12). Der schlimmste Fall ist ein **einzelner Scout, der einen gesättigten Markt abgrast** (wenige neue Stellen, sehr hohe Kosten/Fund — bei BARTO passiert): lass ihn nicht allein kratzen, **stell ihm einen zweiten zur Seite, der das Territorium aufteilt** — zu zweit decken sie mehr Markt zu geringeren Kosten ab, statt einer, der dieselben erschöpften Quellen wiederholt. Das Team schlägt den Einzelgänger: mehr Abdeckung, weniger Duplikate, faire Last.
213213

0 commit comments

Comments
 (0)