SPB Git forge

spb/ka-guardian

Public
26commits 1branches 0releases
4.5 MBsize
maindefault branch
19 days agolast push
Python 57.1% Shell 13.8% CSS 12% JavaScript 10% HTML 7.2%

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 1 mo ago (Aug 24, 2026) parent 863b070

4 changed files +110 −22

modified deploy/deploy.sh +47 −1
@@ -138,10 +138,56 @@ PLIST
138 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 186 case "${1:-all}" in
142 187 courier) deploy_courier ;;
143 188 runners) deploy_runners ;;
144 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 192 esac
147 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 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 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 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 241 == INTERDITS ABSOLUS ==
242 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 253 - Utilise le venv/node_modules du repo; jamais d'installation globale.
254 254 - INTERDIT d'inventer, stubber ou câbler des données en dur. Chaque donnée vient d'un vrai fetch.
255 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 259 def effort_stack(service: str) -> str:
modified runner/runner.py +27 −19
@@ -334,30 +334,38 @@ class RollbackIn(BaseModel):
334 334 def rollback(r: RollbackIn, x_ka_token: str | None = Header(default=None)) -> dict[str, Any]:
335 335 """Revient au commit d'avant mission (reset --hard si tous les commits sont de l'agent, sinon revert)."""
336 336 check_token(x_ka_token)
337 − log = git(r.dir, "log", "--format=%s", f"{r.base_commit}..HEAD").stdout.strip()
338 − msgs = log.splitlines() if log else []
339 − if not msgs:
337 + log = git(r.dir, "log", "--format=%H %s", f"{r.base_commit}..HEAD").stdout.strip()
338 + entries = [ln.split(" ", 1) for ln in log.splitlines() if ln.strip()]
339 + agent_shas = [h for h, s in entries if s.startswith("[ka")]
340 + if not entries:
340 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 342 else:
345 − rc = git(r.dir, "revert", "--no-edit", f"{r.base_commit}..HEAD")
346 − if rc.returncode != 0:
347 − git(r.dir, "revert", "--abort")
348 − git(r.dir, "reset", "--hard", r.base_commit)
349 − mode = "reset_hard(fallback)"
350 − else:
351 − mode = "revert"
343 + # Revert CIBLÉ, du plus récent au plus ancien, UNIQUEMENT les commits de
344 + # l'agent — jamais reset --hard ni revert de plage : les commits étrangers
345 + # (docs, humains, autres sessions) doivent survivre au rollback (leçon
346 + # fabri-ka 2026-08-24 : un revert de plage avait emporté la page /doc et
347 + # un connecteur), et les commits déjà poussés sur spbgit ne doivent
348 + # jamais exiger de force-push.
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 361 # Pas de git clean : les process de sync de l'app écrivent en continu,
353 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
355 − # être poussé, sinon origin reste en avance sur le nœud.
363 + # Push simple (les reverts s'empilent, fast-forward). Échec attendu sous
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 366 pushed = None
357 − if mode != "aucun_commit":
358 − p = git(r.dir, "push", "--force-with-lease", "origin", "HEAD")
359 − if p.returncode != 0:
360 − p = git(r.dir, "push", "--force", "origin", "HEAD")
367 + if entries:
368 + p = git(r.dir, "push", "origin", "HEAD")
361 369 pushed = p.returncode == 0
362 370 for name in r.pm2:
363 371 sh(f"pm2 restart {shlex.quote(name)} --update-env", timeout=180)
364 372