feat: phase 2 — am lab (benchmark N agents) + plugins d'événements (contrat JSON)

- #62: am lab --agents a,b --task <f> — séquentiel ou --parallel, capture
  durée/exit/sortie/coût estimé, rapport comparatif tableau + --json stable,
  tâches versionnables dans <state>/lab/ (--list), rejouables à l'identique
- #75: plugins/ sous state_dir, déclenchés sur on_install/on_start/on_stop/
  on_update (App::emit), contrat JSON stdin/stdout, timeout configurable,
  échec non bloquant, garde anti-récursion AM_PLUGINS_RUNNING, am plugins
  + am plugins --test <name> (CI), exemples notify + ci-webhook
- intégration REPL (parse, complétion, bannière, is_am_command), help, tips,
  man pages (am-lab.1, am-plugins.1), README, ROADMAP (26/27), v0.5.4
- 334 tests verts (309 + 25)

closes #62
closes #75
This commit is contained in:
2026-08-18 14:17:30 -04:00
parent aa0d8e43eb
commit 4c7866a25a
29 changed files with 2123 additions and 8 deletions
+31
View File
@@ -0,0 +1,31 @@
# Plugin scripts — agent-manager (issue #75)
#
# Les plugins sont des points d'extension par script, exécutés sur les
# événements du cycle de vie des agents (complément des hooks de l'axe 6) :
# on_install · on_start · on_stop · on_update
#
# Contrat :
# - entrée : l'événement JSON sur stdin
# {"event":"on_start","kind":"start","ts":"…","agent":"claude-code",
# "pid":1234,"cwd":"…","project":"…","args":[…],"env_keys":[…],…}
# - sortie : un objet JSON sur stdout
# {"ok": true, "message": "…", "data": {…}}
# ou {"ok": false, "error": "…"} — signalé en warning, jamais fatal
# - une sortie invalide (pas de JSON) est signalée en warning sans casser am
# - un plugin qui dépasse son timeout est tué (défaut 10 s)
# - les plugins n'ont pas accès aux valeurs des secrets (env_keys seulement)
# - AM_PLUGINS_RUNNING=1 est exporté pendant un plugin : un am imbriqué
# saute les plugins (anti-récursion)
#
# Installation :
# cp notify.py notify.yaml ~/.local/state/agent-manager/plugins/ (Linux)
# cp notify.py notify.yaml %LOCALAPPDATA%\agent-manager\plugins\ (Windows)
#
# Vérification :
# am plugins # liste les plugins découverts
# am plugins --test notify # exécute le contrat sur un événement de test
#
# Configuration (config.yaml) :
# plugins:
# timeout_secs: 10 # temps max d'un plugin (défaut 10 s)
# enabled: [notify] # vide = tous les plugins du répertoire
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Plugin exemple (issue #75) : poste les événements install/update vers un
webhook CI. Si la variable d'environnement CI_WEBHOOK_URL n'est pas définie,
le plugin répond ok sans rien faire (échec non bloquant, jamais fatal).
Exemple : CI_WEBHOOK_URL=https://hooks.example.com/am-ci am update --all
"""
import json
import os
import sys
import urllib.request
def main() -> int:
ev = json.load(sys.stdin)
url = os.environ.get("CI_WEBHOOK_URL")
if not url:
print(json.dumps({"ok": True, "message": "CI_WEBHOOK_URL non définie — rien à faire"}))
return 0
req = urllib.request.Request(
url,
data=json.dumps(ev).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
status = resp.status
except Exception as exc: # échec non bloquant : warning seulement
print(json.dumps({"ok": False, "error": f"webhook: {exc}"}))
return 0
print(json.dumps({"ok": True, "message": f"webhook {status}"}))
return 0
if __name__ == "__main__":
sys.exit(main())
+4
View File
@@ -0,0 +1,4 @@
# Manifest du plugin ci-webhook (issue #75).
command: "python ci-webhook.py" # Linux : "python3 ci-webhook.py"
events: [on_install, on_update]
timeout_secs: 10
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Plugin exemple (issue #75) : notifie le démarrage / l'arrêt d'un agent.
Contrat : l'événement JSON arrive sur stdin, la réponse JSON part sur stdout.
Installation : copier notify.py + notify.yaml dans <state_dir>/plugins/ puis
relancer am. Test : am plugins --test notify
"""
import json
import sys
def main() -> int:
ev = json.load(sys.stdin)
agent = ev.get("agent") or "inconnu"
event = ev.get("event", "?")
pid = ev.get("pid")
detail = f" (pid {pid})" if pid else ""
print(json.dumps({"ok": True, "message": f"{event} de {agent}{detail}"}))
return 0
if __name__ == "__main__":
sys.exit(main())
+7
View File
@@ -0,0 +1,7 @@
# Manifest du plugin notify (issue #75).
# command: interpréteur + script (indispensable sur Windows pour .py).
command: "python notify.py" # Linux : "python3 notify.py"
# Événements écoutés (défaut : les 4 événements du cycle de vie).
events: [on_start, on_stop]
# Temps maximal d'exécution avant kill (défaut : settings.plugins.timeout_secs, 10).
timeout_secs: 5