1#![allow(
8 dead_code,
9 reason = "AC.2 internalizes Claude-only storage seams before their later deletion or full consumer cutover."
10)]
11
12use crate::config::AtmConfig;
13use crate::error::AtmError;
14use crate::schema::{AgentMember, HOME_DIR_METADATA_KEY, InboxMessage};
15use crate::types::{AgentName, IsoTimestamp, PaneId, TeamName};
16pub use atm_storage::contract::{RosterHarness, RosterMemberKind};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22use super::mail::DoctorFinding;
23use super::{ReplaySource, sealed};
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct RosterStoreHealthSnapshot {
27 pub team: TeamName,
28 pub member_count: u64,
29 #[serde(default)]
30 pub stale: bool,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub refreshed_at: Option<IsoTimestamp>,
33}
34
35pub type RosterEntry = atm_storage::contract::RosterMember;
36
37pub fn roster_member_record_from_claude_code_member(
38 team_name: TeamName,
39 member: AgentMember,
40) -> RosterEntry {
41 let harness = member
42 .extra
43 .get("harness")
44 .and_then(Value::as_str)
45 .and_then(roster_harness_from_metadata)
46 .unwrap_or(RosterHarness::CodexCli);
47 let recipient_pane_id = member.tmux_pane_id;
48 let mut metadata_json = member.extra;
49 if !member.agent_id.is_empty() {
50 metadata_json.insert(
51 "agentId".to_string(),
52 Value::String(member.agent_id.to_string()),
53 );
54 }
55 if let Some(joined_at) = member.joined_at {
56 metadata_json.insert(
57 "joinedAt".to_string(),
58 Value::Number(serde_json::Number::from(joined_at)),
59 );
60 }
61 if !member.home_dir.is_empty() {
62 metadata_json.insert(
63 HOME_DIR_METADATA_KEY.to_string(),
64 Value::String(member.home_dir.as_path().display().to_string()),
65 );
66 }
67
68 RosterEntry {
69 team_name,
70 agent_name: member.name,
71 member_kind: RosterMemberKind::Permanent,
72 harness,
73 agent_type: member.agent_type,
74 model: member.model,
75 recipient_pane_id,
76 metadata_json,
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub(crate) struct ProjectedRosterEntry {
82 pub member_name: AgentName,
83 pub harness: RosterHarness,
84 pub inbox_path: Option<PathBuf>,
85 pub tmux_pane_id: Option<PaneId>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub(crate) struct ProjectionRoster {
90 pub team_name: TeamName,
91 pub members: Arc<[ProjectedRosterEntry]>,
92}
93
94impl ProjectionRoster {
95 pub fn from_roster_snapshot(team_name: TeamName, records: &[RosterEntry]) -> Self {
96 let members = records
97 .iter()
98 .map(|record| ProjectedRosterEntry {
99 member_name: record.agent_name.clone(),
100 harness: record.harness,
101 inbox_path: None,
102 tmux_pane_id: record.recipient_pane_id.clone(),
103 })
104 .collect::<Vec<_>>()
105 .into();
106 Self { team_name, members }
107 }
108
109 pub fn contains_member(&self, member: &AgentName) -> bool {
110 self.members
111 .iter()
112 .any(|entry| entry.member_name == *member)
113 }
114}
115
116fn roster_harness_from_metadata(value: &str) -> Option<RosterHarness> {
117 match value {
118 "claude-code" => Some(RosterHarness::ClaudeCode),
119 "codex-cli" => Some(RosterHarness::CodexCli),
120 "gemini-cli" => Some(RosterHarness::GeminiCli),
121 "opencode" => Some(RosterHarness::Opencode),
122 _ => None,
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
127pub struct RosterStoreDoctorReport {
128 pub findings: Vec<DoctorFinding>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133pub struct ConfigLoadRequest {
134 pub current_dir: PathBuf,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ConfigLoadResponse {
140 pub config: Option<AtmConfig>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
144pub struct ConfigDoctorReport {
145 pub findings: Vec<DoctorFinding>,
146}
147
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "kebab-case")]
150pub enum ProjectionAppendMode {
151 RecoveredLogicalMessageSet,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156pub struct NonClaudeOutboundDeliveryRequest {
157 pub team: TeamName,
158 pub agent: AgentName,
159 pub recipient_pane_id: Option<PaneId>,
160 pub messages: Vec<InboxMessage>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168pub struct NonClaudeOutboundDeliveryResponse {
169 pub delivered_messages: usize,
170}
171
172#[deprecated(note = "use canonical atm-storage types instead")]
174pub trait RosterStore: sealed::Sealed {
175 fn replace_roster(
179 &self,
180 team: &TeamName,
181 members: &[RosterEntry],
182 source: Option<&ReplaySource>,
183 ) -> Result<(), AtmError>;
184 fn load_roster(&self, team: &TeamName) -> Result<Vec<RosterEntry>, AtmError>;
188 fn query_membership(
192 &self,
193 team: &TeamName,
194 member: &AgentName,
195 ) -> Result<Option<RosterEntry>, AtmError>;
196 fn list_teams(&self) -> Result<Vec<TeamName>, AtmError>;
201 fn health_snapshot(&self, team: &TeamName) -> Result<RosterStoreHealthSnapshot, AtmError>;
205}
206
207pub trait RosterStoreDoctor: sealed::Sealed + Send + Sync {
209 fn inspect_roster_store(&self) -> Result<RosterStoreDoctorReport, AtmError>;
214}
215
216pub trait ConfigIngress: sealed::Sealed {
218 fn load_config(&self, request: ConfigLoadRequest) -> Result<ConfigLoadResponse, AtmError>;
223}
224
225pub trait ConfigDoctor: sealed::Sealed + Send + Sync {
227 fn inspect_config(&self) -> Result<ConfigDoctorReport, AtmError>;
232}
233
234pub trait NonClaudeOutbound: sealed::Sealed {
236 fn deliver_payloads(
241 &self,
242 request: NonClaudeOutboundDeliveryRequest,
243 ) -> Result<NonClaudeOutboundDeliveryResponse, AtmError>;
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 struct WitnessRosterStoreDoctor;
251 struct WitnessConfigDoctor;
252
253 impl sealed::Sealed for WitnessRosterStoreDoctor {}
254 impl sealed::Sealed for WitnessConfigDoctor {}
255
256 impl RosterStoreDoctor for WitnessRosterStoreDoctor {
257 fn inspect_roster_store(&self) -> Result<RosterStoreDoctorReport, AtmError> {
258 Ok(RosterStoreDoctorReport::default())
259 }
260 }
261
262 impl ConfigDoctor for WitnessConfigDoctor {
263 fn inspect_config(&self) -> Result<ConfigDoctorReport, AtmError> {
264 Ok(ConfigDoctorReport::default())
265 }
266 }
267
268 #[test]
269 fn roster_store_doctor_trait_is_object_safe_and_compiles() {
270 fn assert_object_safe(_doctor: &dyn RosterStoreDoctor) {}
271
272 let witness = WitnessRosterStoreDoctor;
273 assert_object_safe(&witness);
274 }
275
276 #[test]
277 fn config_doctor_trait_is_object_safe_and_compiles() {
278 fn assert_object_safe(_doctor: &dyn ConfigDoctor) {}
279
280 let witness = WitnessConfigDoctor;
281 assert_object_safe(&witness);
282 }
283}