From fc0cbbcb6bad12644103f8f9087da50ca6d83292 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Tue, 20 Oct 2026 05:49:14 +0200 Subject: [PATCH] refactor(forge): classify account notifications beside the vocabulary The triage mapped the forges' notification reasons onto the claw's verdicts, but it co-evolves with the vocabulary, not with the claw: a reason that enters it (Gitea's collapsed shape) needs a verdict in the same change, and its tests pin the mapping against the vocabulary's own types. It now lives in the integration crate as the default classification; a consumer may reclassify on top. The claw imports it like the rest of the forge seam. --- crates/rhema-claw/src/forge.rs | 4 +-- crates/rhema-claw/src/handler.rs | 2 +- crates/rhema-claw/src/lib.rs | 1 - .../rhema-forge-integration/src/lib.rs | 5 ++++ .../rhema-forge-integration}/src/triage.rs | 27 ++++++++++--------- 5 files changed, 22 insertions(+), 17 deletions(-) rename {crates/rhema-claw => integrations/rhema-forge-integration}/src/triage.rs (89%) diff --git a/crates/rhema-claw/src/forge.rs b/crates/rhema-claw/src/forge.rs index 0edcf87..d1c7cc1 100644 --- a/crates/rhema-claw/src/forge.rs +++ b/crates/rhema-claw/src/forge.rs @@ -6,7 +6,7 @@ //! list_account_watches`] — and loops the kind's repos through a //! [`Watcher`] (poll, ingest, advance the per-repo cursor), then the //! kind's account watches through the identity store's credentials: -//! poll the account's notifications, [`crate::triage::classify`] each +//! poll the account's notifications, [`triage::classify`] each //! one, and either ingest it as a role-tagged thread event, surface it //! to the account's human, or drop it. Neither pass touches the model //! (spec §1). Reading the tables every pass is what makes a watch added @@ -23,6 +23,7 @@ use std::sync::{Arc, Mutex, PoisonError}; use rhema_autonomous_agent::{Source, SourceError, async_trait}; use rhema_forge_integration::forge_key_fn; +use rhema_forge_integration::triage::{self, Triage}; use rhema_forge_integration::{ Forge, GitHubForge, GitLabForge, GiteaForge, Notification, Watcher, register_forge, }; @@ -30,7 +31,6 @@ use rhema_identity::{IdentityStore, SecretKind}; use rhema_rsd::{Clock, Dispatcher, Event, KeyFn, SystemClock}; use crate::config::{ForgeKind, resolve_token}; -use crate::triage::{self, Triage}; use crate::watch_store::{AccountWatchRecord, ClawStore, WatchRecord}; /// The cache key of a forge client: everything that changes the client diff --git a/crates/rhema-claw/src/handler.rs b/crates/rhema-claw/src/handler.rs index 9bec25a..7af17a7 100644 --- a/crates/rhema-claw/src/handler.rs +++ b/crates/rhema-claw/src/handler.rs @@ -25,12 +25,12 @@ use std::sync::{Arc, Mutex, PoisonError}; use crate::config::ForgeKind; use crate::forge::{record_client, resolve_watch_token}; -use crate::triage::Role; use crate::watch_store::ClawStore; use rhema_agent_tools::AllowAll; use rhema_autonomous_agent::{Handler, SessionContext, async_trait}; use rhema_coding_agent::{CodingAgent, CodingAgentConfig}; use rhema_core::{Content, Event as ModelEvent, EventSink, Message, Session}; +use rhema_forge_integration::triage::Role; use rhema_forge_integration::{ EventKind, Forge, ForgeError, ForgeEvent, Notification, RepoRef, Snapshot, ThreadState, decode, parse_key, diff --git a/crates/rhema-claw/src/lib.rs b/crates/rhema-claw/src/lib.rs index 81b5160..0bc68bb 100644 --- a/crates/rhema-claw/src/lib.rs +++ b/crates/rhema-claw/src/lib.rs @@ -94,7 +94,6 @@ pub mod settings; pub mod task_source; pub mod task_store; pub mod task_tools; -pub mod triage; pub mod turn_store; pub mod watch_store; pub mod watch_tools; diff --git a/integrations/rhema-forge-integration/src/lib.rs b/integrations/rhema-forge-integration/src/lib.rs index 6f0ed51..31a1fa2 100644 --- a/integrations/rhema-forge-integration/src/lib.rs +++ b/integrations/rhema-forge-integration/src/lib.rs @@ -41,6 +41,9 @@ //! - [`watcher`] — the dispatcher seam: polls a forge into an RSD //! `Dispatcher` (keys, dedup, cursors) and decodes delivered inbox //! items back into forge events +//! - [`triage`] — what an account notification earns: the default +//! classification of the notification vocabulary into agent +//! dispatches, surfaces, and drops #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -51,6 +54,7 @@ pub mod forge; pub mod gitea; pub mod github; pub mod gitlab; +pub mod triage; pub mod watcher; // Re-exported so implementors of the async seam can apply #[async_trait] @@ -65,6 +69,7 @@ pub use forge::{Forge, ForgePage}; pub use gitea::GiteaForge; pub use github::GitHubForge; pub use gitlab::GitLabForge; +pub use triage::{Role, Triage}; pub use watcher::{ WatchError, Watcher, decode, forge_key_fn, parse_key, register_forge, resource_key, }; diff --git a/crates/rhema-claw/src/triage.rs b/integrations/rhema-forge-integration/src/triage.rs similarity index 89% rename from crates/rhema-claw/src/triage.rs rename to integrations/rhema-forge-integration/src/triage.rs index f907625..318bfb4 100644 --- a/crates/rhema-claw/src/triage.rs +++ b/integrations/rhema-forge-integration/src/triage.rs @@ -1,23 +1,24 @@ -//! The triage: what a notification earns, decided deterministically -//! before any model call (spec §1 — the model is never the detector). +//! The notification triage: what a notification earns, decided +//! deterministically before any model call. //! //! A [`Notification`] carries the forge's own reason for bothering the -//! account; [`classify`] reduces it to one of three verdicts. An +//! account; [`classify`] reduces it to one of three verdicts. A //! [`Triage::Agent`] dispatch becomes an RSD event whose payload names //! the [`Role`] — the persona the handler's prompt opens with, so one //! thread can move from review-requested to implement-feedback across //! dispatches with the session carrying the continuity. A -//! [`Triage::Surface`] never dispatches an agent: a mention is not a -//! assignment, and the account's human hears about it through the -//! device-notification queue instead. [`Triage::Ignore`] drops the noise -//! (CI churn) on the floor. +//! [`Triage::Surface`] never dispatches an agent: a mention is not an +//! assignment, and the account's human hears about it some other way. +//! [`Triage::Ignore`] drops the noise (CI churn) on the floor. //! -//! The mapping is a pure function and this module's tests pin it: the -//! forges' vocabularies are external data, and the policy must not drift -//! with them. +//! The mapping lives beside the vocabulary on purpose: a reason that +//! enters it (Gitea's collapsed shape, a new adapter's spelling) needs +//! a verdict in the same change, and this module's tests pin the +//! mapping so the co-edit cannot drift. A consumer may reclassify on +//! top — [`classify`] is the default, not a mandate. -use rhema_forge_integration::gitea::COLLAPSED_REASON; -use rhema_forge_integration::{Notification, NotificationReason}; +use crate::gitea::COLLAPSED_REASON; +use crate::{Notification, NotificationReason}; /// The agent persona a dispatched notification runs as. The key stays /// the thread — one session, one voice per thread, ever — and the role @@ -83,7 +84,7 @@ pub fn classify(notification: &Notification) -> Triage { #[cfg(test)] mod tests { use super::*; - use rhema_forge_integration::{RepoRef, Timestamp}; + use crate::{RepoRef, Timestamp}; fn notification(reason: NotificationReason) -> Notification { Notification {