0.28.0 — Émission d'événements vers le journal du service d'agents (Lot 2)
CI / test (push) Successful in 2m1s

- app/agent_journal.py : enveloppe ACC-0.2 signée HMAC-SHA256
  (clé AGENT_EVENEMENTS_CLE, URL AGENT_EVENEMENTS_URL), envoi
  en arrière-plan, timeout 2 s, inactif sans clé, échecs avalés
- main.py : _emettre_changement_colonne accolée aux log_event de
  déplacement (interface, étiquettes forge, webhook, réconciliation,
  PR fusionnée, branche) — kanbante.card.moved pour tout changement
  de colonne, kanbante.card.review_requested à l'entrée en Révision
- config : AGENT_EVENEMENTS_URL/_CLE documentés et lus à l'appel
- 247 tests verts (5 nouveaux : signature revérifiable, inactivité
  sans clé, moved, review_requested, panne du récepteur)
This commit is contained in:
Spark
2026-10-22 18:40:00 +00:00
parent 868496891d
commit 3809462cc7
6 changed files with 410 additions and 2 deletions
+69
View File
@@ -58,6 +58,7 @@ from app.briefing import (
send_ntfy,
)
from app.capture import ntfy_capture_loop, parse_capture_text
from app import agent_journal
from app import models
from app.models import (
KANBAN_COLUMNS,
@@ -2020,6 +2021,38 @@ def _merge_issues_with_board(
return columns
def _emettre_changement_colonne(
full_name: str,
number: int,
from_column: str | None,
to_column: str,
source: str,
title: str = "",
) -> None:
"""Émission Lot 2 : refléter un changement de colonne réel dans
le journal du service d'agents (contrat ACC-0.2, voir
app/agent_journal.py).
Appelée à côté des log_event qui consignent un déplacement —
jamais à leur place. ``kanbante.card.moved`` pour tout
changement de colonne, plus ``kanbante.card.review_requested``
quand la carte ENTRE en colonne Révision. Au mieux : sans clé
configurée, rien ne part ; en cas de panne du service, le
déplacement n'est jamais affecté.
"""
if not to_column or from_column == to_column:
return
actor = "user" if source == "ui" else f"agent:{source}"
ref = f"issue:{full_name}#{number}"
label = (title or "").strip() or ref
url = f"{get_settings().gitea_url}/{full_name}/issues/{number}"
agent_journal.emettre("kanbante.card.moved", ref, label, url, actor=actor)
if to_column == "review" and from_column != "review":
agent_journal.emettre(
"kanbante.card.review_requested", ref, label, url, actor=actor
)
def _adopt_label_columns(
full_name: str,
open_issues: list[dict],
@@ -2063,6 +2096,10 @@ def _adopt_label_columns(
from_column=current, to_column=target, source="gitea",
workspace_id=workspace_id, connection_id=connection_id,
)
_emettre_changement_colonne(
full_name, issue["number"], current, target, "gitea",
title=issue.get("title", ""),
)
placement[issue["number"]] = target
@@ -2392,6 +2429,9 @@ async def board(request: Request, owner: str, repo: str, milestone: str = ""):
full_name, number, "closed", to_column="done", source="gitea",
workspace_id=ws_id, connection_id=conn_id,
)
_emettre_changement_colonne(
full_name, number, None, "done", "gitea",
)
# A kanban:* label changed outside Kanbante wins over local state.
_adopt_label_columns(
@@ -2543,6 +2583,9 @@ def _board_columns_for(
full_name, number, "closed", to_column="done", source="gitea",
workspace_id=workspace_id, connection_id=connection_id,
)
_emettre_changement_colonne(
full_name, number, None, "done", "gitea",
)
_adopt_label_columns(
full_name, open_issues,
get_board_state(
@@ -4138,6 +4181,11 @@ def _apply_external_transition(
from_column="done", to_column=target_column, source="webhook",
workspace_id=workspace_id, connection_id=connection_id,
)
_emettre_changement_colonne(
full_name, number,
current if event_type == "closed" else "done",
target_column, "webhook",
)
return event_type
@@ -4207,6 +4255,10 @@ async def _apply_pr_merged(
to_column="done", source="webhook",
workspace_id=workspace_id, connection_id=connection_id,
)
_emettre_changement_colonne(
full_name, number, None, "done", "webhook",
title=issue.get("title", ""),
)
outcome = "closed"
results.append(f"#{number}:{outcome}")
return results
@@ -4823,6 +4875,19 @@ async def api_move(
from_column or None, to_column,
workspace_id=ws_id, connection_id=conn_id,
)
# Émission Lot 2 (journal du service d'agents) : le titre
# n'est relu que si l'émission est configurée, au mieux.
titre_carte = ""
if agent_journal.actif():
try:
detail = await client.get_issue(owner, repo, issue_number)
titre_carte = detail.get("title", "")
except Exception:
titre_carte = ""
_emettre_changement_colonne(
repo_full_name, issue_number, from_column or None, to_column,
"ui", title=titre_carte,
)
result: dict = {"ok": True, "gitea_sync": "skipped"}
settings = get_settings()
@@ -5156,6 +5221,10 @@ async def api_create_branch(
source="branch",
workspace_id=ws_id, connection_id=conn_id,
)
_emettre_changement_colonne(
full_name, number, current, "in_progress", "branch",
title=issue.get("title", ""),
)
await _sync_kanban_label(
owner, repo, number, "in_progress", client
)