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>
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 | |