SPB Git

spb/ka-guardian

Public

Rollback ciblé (revert des seuls commits [ka*], plus jamais de reset/revert de plage) + pousseur launchd com.ka.pousseur (push binaires Apple, contourne LNP) + consignes: push raté en mission = attendu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 24, 2026) parent 863b070 Browse files

4 changed files +110 −22

modified deploy/deploy.sh +47 −1
@@ -138,10 +138,56 @@ PLIST
138 done 138 done
139 } 139 }
140 140
141+deploy_pousseur() {
142+ # Pousseur launchd (binaires Apple) : pousse les commits en attente des repos
143+ # Ka du nœud — les missions Claude ne peuvent pas pousser elles-mêmes
144+ # (macOS 26 Local Network Privacy bloque les descendants du runner python).
145+ typeset -A REPOS
146+ REPOS[M3U96a]='~/apps/vrai-prix
147+~/apps/sorti-ka
148+~/apps/job-ka'
149+ REPOS[M3U96b]='~/apps/groupe-ka
150+~/apps/lou-ka
151+/opt/api-ka
152+~/apps/crea-ka
153+~/apps/resto-ka'
154+ REPOS[M4M64a]='~/apps/immo-ka
155+~/fabri-ka'
156+ REPOS[M4M64b]='~/auto-ka
157+~/apps/food-ka'
158+ for n in $RUNNER_NODES; do
159+ echo "=== pousseur → $n"
160+ scp -q "$ROOT/deploy/pousseur.sh" $n:ka-pousseur.sh
161+ ssh $n "chmod +x ~/ka-pousseur.sh
162+ cat > ~/.ka-pousseur-repos <<'REPOS'
163+${REPOS[$n]}
164+REPOS
165+ cat > ~/Library/LaunchAgents/com.ka.pousseur.plist <<'PLIST'
166+<?xml version=\"1.0\" encoding=\"UTF-8\"?>
167+<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
168+<plist version=\"1.0\"><dict>
169+ <key>Label</key><string>com.ka.pousseur</string>
170+ <key>ProgramArguments</key><array>
171+ <string>/bin/zsh</string>
172+ <string>/Users/simon-pierreboucher/ka-pousseur.sh</string>
173+ </array>
174+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
175+ <key>StandardOutPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/pousseur.log</string>
176+ <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/pousseur.log</string>
177+</dict></plist>
178+PLIST
179+ mkdir -p ~/ka-guardian-spool
180+ launchctl unload ~/Library/LaunchAgents/com.ka.pousseur.plist 2>/dev/null || true
181+ launchctl load ~/Library/LaunchAgents/com.ka.pousseur.plist" \
182+ && echo " ✓ $n pousseur ok" || echo " ✗ $n pousseur KO"
183+ done
184+}
185+
141 case "${1:-all}" in 186 case "${1:-all}" in
142 courier) deploy_courier ;; 187 courier) deploy_courier ;;
143 runners) deploy_runners ;; 188 runners) deploy_runners ;;
144 orchestrators) deploy_orchestrators ;; 189 orchestrators) deploy_orchestrators ;;
145− all) deploy_courier; deploy_runners; deploy_orchestrators ;; 190+ pousseur) deploy_pousseur ;;
191+ all) deploy_courier; deploy_runners; deploy_orchestrators; deploy_pousseur ;;
146 esac 192 esac
147 echo "terminé." 193 echo "terminé."
added deploy/pousseur.sh +34 −0
@@ -0,0 +1,34 @@
1+#!/bin/zsh
2+# ============================================
3+# KA pousseur — pousse vers spbgit les commits en attente des repos Ka du nœud.
4+# Chaîne 100 % binaires Apple (launchd → zsh → /usr/bin/git → /usr/bin/ssh) :
5+# contourne macOS 26 Local Network Privacy, qui bloque les `git push` lancés
6+# par les missions Claude (descendants du runner python homebrew).
7+# Repos surveillés : un chemin par ligne dans ~/.ka-pousseur-repos.
8+# Push SIMPLE uniquement (jamais de force) : un non-fast-forward est loggé,
9+# jamais résolu d'autorité.
10+# ============================================
11+export PATH=/usr/bin:/bin:/usr/sbin:/sbin
12+SPOOL=~/ka-guardian-spool
13+LOG=$SPOOL/pousseur.log
14+REPOS_FILE=~/.ka-pousseur-repos
15+mkdir -p $SPOOL
16+
17+while true; do
18+ if [[ -f $REPOS_FILE ]]; then
19+ while IFS= read -r dir; do
20+ [[ -z $dir || $dir == \#* ]] && continue
21+ d=${dir/#\~/$HOME}
22+ [[ -d $d/.git ]] || continue
23+ ahead=$(/usr/bin/git -C "$d" rev-list --count origin/main..main 2>/dev/null)
24+ if [[ -n $ahead && $ahead -gt 0 ]]; then
25+ if /usr/bin/git -C "$d" push origin main >>$LOG 2>&1; then
26+ echo "$(date '+%F %T') poussé $ahead commit(s): $d" >>$LOG
27+ else
28+ echo "$(date '+%F %T') ÉCHEC push ($ahead en attente): $d" >>$LOG
29+ fi
30+ fi
31+ done <$REPOS_FILE
32+ fi
33+ sleep 300
34+done
modified orchestrator/main.py +2 −2
@@ -236,7 +236,7 @@ Le venv Python et/ou node_modules du repo sont déjà installés — utilise-les
236 6. RE-TESTE réellement: le connecteur doit rapporter un volume plausible (ordre de grandeur de la médiane {health.get('median_found')}). INTERDIT d'inventer, stubber ou câbler des données en dur. Un test qui retourne 0 ou 3 items quand la médiane est {health.get('median_found')} N'EST PAS une réparation. 236 6. RE-TESTE réellement: le connecteur doit rapporter un volume plausible (ordre de grandeur de la médiane {health.get('median_found')}). INTERDIT d'inventer, stubber ou câbler des données en dur. Un test qui retourne 0 ou 3 items quand la médiane est {health.get('median_found')} N'EST PAS une réparation.
237 7. REDÉMARRE uniquement le process concerné: `pm2 restart {sync_proc}`. Puis vérifie que le site répond: `curl -s -o /dev/null -w '%{{http_code}}' http://localhost:{svc['web_port']}/` (attendu: 200/3xx). 237 7. REDÉMARRE uniquement le process concerné: `pm2 restart {sync_proc}`. Puis vérifie que le site répond: `curl -s -o /dev/null -w '%{{http_code}}' http://localhost:{svc['web_port']}/` (attendu: 200/3xx).
238 8. COMMITTE: `git add -A && git commit -m "[{AGENT}] fix connecteur {source}: <résumé court>"`. Si tu as touché plusieurs fichiers pour des raisons distinctes, fais des commits séparés et clairs. 238 8. COMMITTE: `git add -A && git commit -m "[{AGENT}] fix connecteur {source}: <résumé court>"`. Si tu as touché plusieurs fichiers pour des raisons distinctes, fais des commits séparés et clairs.
239−9. POUSSE sur spbgit (OBLIGATOIRE — le push fait partie du travail): `git push origin main`. Vérifie qu'il réussit; en cas d'échec réseau, re-tente une fois après 30 s. S'il échoue encore, dis-le explicitement dans ton verdict final (champ actions) — mais ne modifie JAMAIS ~/.ssh ni la config git pour « réparer » le push. 239+9. POUSSE sur spbgit (le push fait partie du travail): `git push origin main`. NOTE: sur certains nœuds le push échoue en « no route to host » depuis une mission (macOS 26 Local Network Privacy) — c'est ATTENDU: le pousseur launchd (com.ka.pousseur) poussera tes commits dans les 5 minutes. Si le push échoue, signale-le simplement dans ton rapport final et passe à la suite; n'insiste pas et ne modifie JAMAIS ~/.ssh ni la config git pour « réparer » le push.
240 240
241 == INTERDITS ABSOLUS == 241 == INTERDITS ABSOLUS ==
242 Toucher aux autres connecteurs ou aux autres apps du nœud; modifier la config pm2/ngrok/launchd ou le schéma de la base; installer des paquets globaux; supprimer des données; `git push --force` ou push d'une autre branche que main; toucher à ~/.ssh, aux clés API ou aux fichiers hors du repo. Ne désactive JAMAIS un connecteur pour « réparer » sa santé. Si la source est définitivement morte (site fermé, domaine à vendre, 404 permanent confirmé), ne force rien: documente et conclus en verdict site_source_mort. 242 Toucher aux autres connecteurs ou aux autres apps du nœud; modifier la config pm2/ngrok/launchd ou le schéma de la base; installer des paquets globaux; supprimer des données; `git push --force` ou push d'une autre branche que main; toucher à ~/.ssh, aux clés API ou aux fichiers hors du repo. Ne désactive JAMAIS un connecteur pour « réparer » sa santé. Si la source est définitivement morte (site fermé, domaine à vendre, 404 permanent confirmé), ne force rien: documente et conclus en verdict site_source_mort.
@@ -253,7 +253,7 @@ COMMON_RULES = """== RÈGLES COMMUNES (non négociables) ==
253 - Utilise le venv/node_modules du repo; jamais d'installation globale. 253 - Utilise le venv/node_modules du repo; jamais d'installation globale.
254 - INTERDIT d'inventer, stubber ou câbler des données en dur. Chaque donnée vient d'un vrai fetch. 254 - INTERDIT d'inventer, stubber ou câbler des données en dur. Chaque donnée vient d'un vrai fetch.
255 - INTERDITS: toucher aux autres apps du nœud, config pm2/ngrok/launchd, schéma de la base (sauf migration prévue par le repo), `git push --force` ou push d'une autre branche que main, ~/.ssh, clés API, suppression de données. 255 - INTERDITS: toucher aux autres apps du nœud, config pm2/ngrok/launchd, schéma de la base (sauf migration prévue par le repo), `git push --force` ou push d'une autre branche que main, ~/.ssh, clés API, suppression de données.
256−- À la fin: `pm2 restart <process de sync>`, vérifie que le site répond (curl localhost), committe en messages clairs préfixés [%AGENT%], puis POUSSE sur spbgit (OBLIGATOIRE): `git push origin main` — re-tente une fois après 30 s si échec réseau, et signale tout push raté dans ton rapport final.""" 256+- À la fin: `pm2 restart <process de sync>`, vérifie que le site répond (curl localhost), committe en messages clairs préfixés [%AGENT%], puis POUSSE sur spbgit: `git push origin main`. Un échec « no route to host » est ATTENDU depuis une mission (macOS 26 LNP) — le pousseur launchd s'en chargera dans les 5 minutes: signale-le et passe à la suite, sans toucher à ~/.ssh ni à la config git."""
257 257
258 258
259 def effort_stack(service: str) -> str: 259 def effort_stack(service: str) -> str:
modified runner/runner.py +27 −19
@@ -334,30 +334,38 @@ class RollbackIn(BaseModel):
334 def rollback(r: RollbackIn, x_ka_token: str | None = Header(default=None)) -> dict[str, Any]: 334 def rollback(r: RollbackIn, x_ka_token: str | None = Header(default=None)) -> dict[str, Any]:
335 """Revient au commit d'avant mission (reset --hard si tous les commits sont de l'agent, sinon revert).""" 335 """Revient au commit d'avant mission (reset --hard si tous les commits sont de l'agent, sinon revert)."""
336 check_token(x_ka_token) 336 check_token(x_ka_token)
337− log = git(r.dir, "log", "--format=%s", f"{r.base_commit}..HEAD").stdout.strip() 337+ log = git(r.dir, "log", "--format=%H %s", f"{r.base_commit}..HEAD").stdout.strip()
338− msgs = log.splitlines() if log else [] 338+ entries = [ln.split(" ", 1) for ln in log.splitlines() if ln.strip()]
339− if not msgs: 339+ agent_shas = [h for h, s in entries if s.startswith("[ka")]
340+ if not entries:
340 mode = "aucun_commit" 341 mode = "aucun_commit"
341− elif all(s.startswith("[ka") for s in msgs):
342− git(r.dir, "reset", "--hard", r.base_commit)
343− mode = "reset_hard"
344 else: 342 else:
345− rc = git(r.dir, "revert", "--no-edit", f"{r.base_commit}..HEAD") 343+ # Revert CIBLÉ, du plus récent au plus ancien, UNIQUEMENT les commits de
346− if rc.returncode != 0: 344+ # l'agent — jamais reset --hard ni revert de plage : les commits étrangers
347− git(r.dir, "revert", "--abort") 345+ # (docs, humains, autres sessions) doivent survivre au rollback (leçon
348− git(r.dir, "reset", "--hard", r.base_commit) 346+ # fabri-ka 2026-08-24 : un revert de plage avait emporté la page /doc et
349− mode = "reset_hard(fallback)" 347+ # un connecteur), et les commits déjà poussés sur spbgit ne doivent
350− else: 348+ # jamais exiger de force-push.
351− mode = "revert" 349+ reverted: list[str] = []
350+ skipped: list[str] = []
351+ for sha in agent_shas: # git log liste déjà du plus récent au plus ancien
352+ rc = git(r.dir, "revert", "--no-edit", sha)
353+ if rc.returncode != 0:
354+ git(r.dir, "revert", "--abort")
355+ skipped.append(sha[:9])
356+ else:
357+ reverted.append(sha[:9])
358+ mode = f"revert_cible({len(reverted)}/{len(agent_shas)})"
359+ if skipped:
360+ mode += f" sautés:{','.join(skipped)}"
352 # Pas de git clean : les process de sync de l'app écrivent en continu, 361 # Pas de git clean : les process de sync de l'app écrivent en continu,
353 # on ne supprime jamais de fichiers non trackés apparus pendant la mission. 362 # on ne supprime jamais de fichiers non trackés apparus pendant la mission.
354− # Les agents poussent leurs commits sur spbgit → le rollback doit aussi 363+ # Push simple (les reverts s'empilent, fast-forward). Échec attendu sous
355− # être poussé, sinon origin reste en avance sur le nœud. 364+ # macOS 26 Local Network Privacy (runner = python) : le pousseur launchd
365+ # (com.ka.pousseur, chaîne binaires Apple) poussera dans les 5 minutes.
356 pushed = None 366 pushed = None
357− if mode != "aucun_commit": 367+ if entries:
358− p = git(r.dir, "push", "--force-with-lease", "origin", "HEAD") 368+ p = git(r.dir, "push", "origin", "HEAD")
359− if p.returncode != 0:
360− p = git(r.dir, "push", "--force", "origin", "HEAD")
361 pushed = p.returncode == 0 369 pushed = p.returncode == 0
362 for name in r.pm2: 370 for name in r.pm2:
363 sh(f"pm2 restart {shlex.quote(name)} --update-env", timeout=180) 371 sh(f"pm2 restart {shlex.quote(name)} --update-env", timeout=180)
364 372
Menu

Theme