Files
claudetools/imported-conversations/general-work/claude-projects/99918cbf-1f1c-4e49-ace0-f7a73ae40c80/tool-results/toolu_01XWexWYoRx7ynXpxHeUxwWD.txt
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
Enhanced code review and frontend validation with intelligent triggers:

Code Review Agent Enhancement:
- Added Sequential Thinking MCP integration for complex issues
- Triggers on 2+ rejections or 3+ critical issues
- New escalation format with root cause analysis
- Comprehensive solution strategies with trade-off evaluation
- Educational feedback to break rejection cycles
- Files: .claude/agents/code-review.md (+308 lines)
- Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md

Frontend Design Skill Enhancement:
- Automatic invocation for ANY UI change
- Comprehensive validation checklist (200+ checkpoints)
- 8 validation categories (visual, interactive, responsive, a11y, etc.)
- 3 validation levels (quick, standard, comprehensive)
- Integration with code review workflow
- Files: .claude/skills/frontend-design/SKILL.md (+120 lines)
- Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines)

Settings Optimization:
- Repaired .claude/settings.local.json (fixed m365 pattern)
- Reduced permissions from 49 to 33 (33% reduction)
- Removed duplicates, sorted alphabetically
- Created SETTINGS_PERMISSIONS.md documentation

Checkpoint Command Enhancement:
- Dual checkpoint system (git + database)
- Saves session context to API for cross-machine recall
- Includes git metadata in database context
- Files: .claude/commands/checkpoint.md (+139 lines)

Decision Rationale:
- Sequential Thinking MCP breaks rejection cycles by identifying root causes
- Automatic frontend validation catches UI issues before code review
- Dual checkpoints enable complete project memory across machines
- Settings optimization improves maintainability

Total: 1,200+ lines of documentation and enhancements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 16:23:52 -07:00

485 lines
20 KiB
Plaintext

1→//! IPC (Inter-Process Communication) for tray application
2→//!
3→//! Provides a named pipe server (Windows) or Unix socket (Unix) for the
4→//! tray application to communicate with the agent service.
5→
6→use serde::{Deserialize, Serialize};
7→use std::sync::Arc;
8→use tokio::sync::{broadcast, RwLock};
9→use tracing::{debug, error, info, warn};
10→
11→/// Pipe name for Windows named pipe
12→#[cfg(windows)]
13→pub const PIPE_NAME: &str = r"\\.\pipe\gururmm-agent";
14→
15→/// Socket path for Unix domain socket
16→#[cfg(unix)]
17→pub const SOCKET_PATH: &str = "/var/run/gururmm/agent.sock";
18→
19→/// Tray policy - controls what the tray app can show and do
20→#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21→pub struct TrayPolicy {
22→ /// Whether to show the tray icon at all
23→ pub enabled: bool,
24→
25→ /// Show connection status in tooltip/menu
26→ pub show_status: bool,
27→
28→ /// Show agent info (version, hostname, etc.) in menu
29→ pub show_info: bool,
30→
31→ /// Allow user to trigger manual check-in
32→ pub allow_force_checkin: bool,
33→
34→ /// Allow user to view agent logs
35→ pub allow_view_logs: bool,
36→
37→ /// Allow user to open web dashboard
38→ pub allow_open_dashboard: bool,
39→
40→ /// Allow user to stop the agent (dangerous!)
41→ pub allow_stop_agent: bool,
42→
43→ /// Dashboard URL (if allow_open_dashboard is true)
44→ pub dashboard_url: Option<String>,
45→
46→ /// Custom tooltip text
47→ pub tooltip_text: Option<String>,
48→
49→ /// Custom icon (base64 encoded PNG)
50→ pub custom_icon: Option<String>,
51→}
52→
53→impl TrayPolicy {
54→ /// Default permissive policy (for development/testing)
55→ pub fn default_permissive() -> Self {
56→ Self {
57→ enabled: true,
58→ show_status: true,
59→ show_info: true,
60→ allow_force_checkin: true,
61→ allow_view_logs: true,
62→ allow_open_dashboard: true,
63→ allow_stop_agent: false,
64→ dashboard_url: None,
65→ tooltip_text: None,
66→ custom_icon: None,
67→ }
68→ }
69→
70→ /// Restrictive policy (minimal visibility)
71→ pub fn default_restrictive() -> Self {
72→ Self {
73→ enabled: true,
74→ show_status: true,
75→ show_info: false,
76→ allow_force_checkin: false,
77→ allow_view_logs: false,
78→ allow_open_dashboard: false,
79→ allow_stop_agent: false,
80→ dashboard_url: None,
81→ tooltip_text: None,
82→ custom_icon: None,
83→ }
84→ }
85→}
86→
87→/// Agent status for tray display
88→#[derive(Debug, Clone, Serialize, Deserialize)]
89→pub struct AgentStatus {
90→ /// Whether connected to server
91→ pub connected: bool,
92→
93→ /// Last successful check-in time (ISO 8601)
94→ pub last_checkin: Option<String>,
95→
96→ /// Server URL we're connected to
97→ pub server_url: String,
98→
99→ /// Agent version
100→ pub agent_version: String,
101→
102→ /// Device ID
103→ pub device_id: String,
104→
105→ /// Hostname
106→ pub hostname: String,
107→
108→ /// Error message (if any)
109→ pub error: Option<String>,
110→}
111→
112→/// Request from tray to agent
113→#[derive(Debug, Clone, Serialize, Deserialize)]
114→#[serde(tag = "type", content = "payload")]
115→#[serde(rename_all = "snake_case")]
116→pub enum IpcRequest {
117→ /// Get current agent status
118→ GetStatus,
119→
120→ /// Get current tray policy
121→ GetPolicy,
122→
123→ /// Force immediate check-in
124→ ForceCheckin,
125→
126→ /// Stop the agent service
127→ StopAgent,
128→
129→ /// Subscribe to status updates
130→ Subscribe,
131→
132→ /// Unsubscribe from updates
133→ Unsubscribe,
134→
135→ /// Ping (health check)
136→ Ping,
137→}
138→
139→/// Response from agent to tray
140→#[derive(Debug, Clone, Serialize, Deserialize)]
141→#[serde(tag = "type", content = "payload")]
142→#[serde(rename_all = "snake_case")]
143→pub enum IpcResponse {
144→ /// Current agent status
145→ Status(AgentStatus),
146→
147→ /// Current tray policy
148→ Policy(TrayPolicy),
149→
150→ /// Success acknowledgment
151→ Ok,
152→
153→ /// Pong response to ping
154→ Pong,
155→
156→ /// Error
157→ Error { message: String },
158→
159→ /// Action denied by policy
160→ Denied { message: String },
161→
162→ /// Status update (pushed to subscribers)
163→ StatusUpdate(AgentStatus),
164→
165→ /// Policy update (pushed to subscribers)
166→ PolicyUpdate(TrayPolicy),
167→}
168→
169→/// Shared state for IPC server
170→pub struct IpcState {
171→ /// Current agent status
172→ pub status: RwLock<AgentStatus>,
173→
174→ /// Current tray policy
175→ pub policy: RwLock<TrayPolicy>,
176→
177→ /// Broadcast channel for status updates
178→ pub status_tx: broadcast::Sender<AgentStatus>,
179→
180→ /// Broadcast channel for policy updates
181→ pub policy_tx: broadcast::Sender<TrayPolicy>,
182→
183→ /// Channel to request force check-in
184→ pub force_checkin_tx: tokio::sync::mpsc::Sender<()>,
185→
186→ /// Channel to request agent stop
187→ pub stop_agent_tx: tokio::sync::mpsc::Sender<()>,
188→}
189→
190→impl IpcState {
191→ pub fn new(
192→ initial_status: AgentStatus,
193→ initial_policy: TrayPolicy,
194→ force_checkin_tx: tokio::sync::mpsc::Sender<()>,
195→ stop_agent_tx: tokio::sync::mpsc::Sender<()>,
196→ ) -> Self {
197→ let (status_tx, _) = broadcast::channel(16);
198→ let (policy_tx, _) = broadcast::channel(16);
199→
200→ Self {
201→ status: RwLock::new(initial_status),
202→ policy: RwLock::new(initial_policy),
203→ status_tx,
204→ policy_tx,
205→ force_checkin_tx,
206→ stop_agent_tx,
207→ }
208→ }
209→
210→ /// Update status and broadcast to subscribers
211→ pub async fn update_status(&self, status: AgentStatus) {
212→ *self.status.write().await = status.clone();
213→ let _ = self.status_tx.send(status);
214→ }
215→
216→ /// Update policy and broadcast to subscribers
217→ pub async fn update_policy(&self, policy: TrayPolicy) {
218→ *self.policy.write().await = policy.clone();
219→ let _ = self.policy_tx.send(policy);
220→ }
221→
222→ /// Handle an IPC request
223→ pub async fn handle_request(&self, request: IpcRequest) -> IpcResponse {
224→ match request {
225→ IpcRequest::GetStatus => {
226→ let status = self.status.read().await.clone();
227→ IpcResponse::Status(status)
228→ }
229→
230→ IpcRequest::GetPolicy => {
231→ let policy = self.policy.read().await.clone();
232→ IpcResponse::Policy(policy)
233→ }
234→
235→ IpcRequest::ForceCheckin => {
236→ let policy = self.policy.read().await;
237→ if !policy.allow_force_checkin {
238→ return IpcResponse::Denied {
239→ message: "Force check-in not allowed by policy".to_string(),
240→ };
241→ }
242→ drop(policy);
243→
244→ match self.force_checkin_tx.send(()).await {
245→ Ok(_) => IpcResponse::Ok,
246→ Err(_) => IpcResponse::Error {
247→ message: "Failed to trigger check-in".to_string(),
248→ },
249→ }
250→ }
251→
252→ IpcRequest::StopAgent => {
253→ let policy = self.policy.read().await;
254→ if !policy.allow_stop_agent {
255→ return IpcResponse::Denied {
256→ message: "Stopping agent not allowed by policy".to_string(),
257→ };
258→ }
259→ drop(policy);
260→
261→ match self.stop_agent_tx.send(()).await {
262→ Ok(_) => IpcResponse::Ok,
263→ Err(_) => IpcResponse::Error {
264→ message: "Failed to request agent stop".to_string(),
265→ },
266→ }
267→ }
268→
269→ IpcRequest::Subscribe => {
270→ // Subscription is handled at the connection level
271→ IpcResponse::Ok
272→ }
273→
274→ IpcRequest::Unsubscribe => {
275→ IpcResponse::Ok
276→ }
277→
278→ IpcRequest::Ping => IpcResponse::Pong,
279→ }
280→ }
281→}
282→
283→// ============================================================================
284→// Windows Named Pipe Server
285→// ============================================================================
286→
287→#[cfg(windows)]
288→pub mod server {
289→ use super::*;
290→ use std::ffi::OsStr;
291→ use std::os::windows::ffi::OsStrExt;
292→ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
293→ use tokio::net::windows::named_pipe::{ServerOptions, NamedPipeServer};
294→
295→ /// Run the IPC server (Windows named pipe)
296→ pub async fn run_ipc_server(state: Arc<IpcState>) -> anyhow::Result<()> {
297→ info!("Starting IPC server on {}", super::PIPE_NAME);
298→
299→ loop {
300→ // Create a new pipe instance
301→ let server = match ServerOptions::new()
302→ .first_pipe_instance(false)
303→ .create(super::PIPE_NAME)
304→ {
305→ Ok(s) => s,
306→ Err(e) => {
307→ // First instance needs different handling
308→ if e.kind() == std::io::ErrorKind::NotFound {
309→ ServerOptions::new()
310→ .first_pipe_instance(true)
311→ .create(super::PIPE_NAME)?
312→ } else {
313→ error!("Failed to create pipe: {}", e);
314→ tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
315→ continue;
316→ }
317→ }
318→ };
319→
320→ // Wait for a client to connect
321→ if let Err(e) = server.connect().await {
322→ error!("Failed to accept pipe connection: {}", e);
323→ continue;
324→ }
325→
326→ info!("IPC client connected");
327→
328→ // Spawn a task to handle this client
329→ let state = Arc::clone(&state);
330→ tokio::spawn(async move {
331→ if let Err(e) = handle_client(server, state).await {
332→ debug!("IPC client disconnected: {}", e);
333→ }
334→ });
335→ }
336→ }
337→
338→ async fn handle_client(
339→ pipe: NamedPipeServer,
340→ state: Arc<IpcState>,
341→ ) -> anyhow::Result<()> {
342→ let (reader, mut writer) = tokio::io::split(pipe);
343→ let mut reader = BufReader::new(reader);
344→ let mut line = String::new();
345→
346→ // Subscribe to updates
347→ let mut status_rx = state.status_tx.subscribe();
348→ let mut policy_rx = state.policy_tx.subscribe();
349→ let mut subscribed = false;
350→
351→ loop {
352→ tokio::select! {
353→ // Read request from client
354→ result = reader.read_line(&mut line) => {
355→ match result {
356→ Ok(0) => break, // EOF
357→ Ok(_) => {
358→ let trimmed = line.trim();
359→ if trimmed.is_empty() {
360→ line.clear();
361→ continue;
362→ }
363→
364→ match serde_json::from_str::<IpcRequest>(trimmed) {
365→ Ok(request) => {
366→ if matches!(request, IpcRequest::Subscribe) {
367→ subscribed = true;
368→ } else if matches!(request, IpcRequest::Unsubscribe) {
369→ subscribed = false;
370→ }
371→
372→ let response = state.handle_request(request).await;
373→ let response_json = serde_json::to_string(&response)?;
374→ writer.write_all(response_json.as_bytes()).await?;
375→ writer.write_all(b"\n").await?;
376→ writer.flush().await?;
377→ }
378→ Err(e) => {
379→ warn!("Invalid IPC request: {}", e);
380→ let response = IpcResponse::Error {
381→ message: format!("Invalid request: {}", e),
382→ };
383→ let response_json = serde_json::to_string(&response)?;
384→ writer.write_all(response_json.as_bytes()).await?;
385→ writer.write_all(b"\n").await?;
386→ writer.flush().await?;
387→ }
388→ }
389→ line.clear();
390→ }
391→ Err(e) => {
392→ return Err(e.into());
393→ }
394→ }
395→ }
396→
397→ // Push status updates to subscribed clients
398→ result = status_rx.recv(), if subscribed => {
399→ if let Ok(status) = result {
400→ let response = IpcResponse::StatusUpdate(status);
401→ let response_json = serde_json::to_string(&response)?;
402→ writer.write_all(response_json.as_bytes()).await?;
403→ writer.write_all(b"\n").await?;
404→ writer.flush().await?;
405→ }
406→ }
407→
408→ // Push policy updates to subscribed clients
409→ result = policy_rx.recv(), if subscribed => {
410→ if let Ok(policy) = result {
411→ let response = IpcResponse::PolicyUpdate(policy);
412→ let response_json = serde_json::to_string(&response)?;
413→ writer.write_all(response_json.as_bytes()).await?;
414→ writer.write_all(b"\n").await?;
415→ writer.flush().await?;
416→ }
417→ }
418→ }
419→ }
420→
421→ Ok(())
422→ }
423→}
424→
425→// ============================================================================
426→// Unix Domain Socket Server (placeholder for future)
427→// ============================================================================
428→
429→#[cfg(unix)]
430→pub mod server {
431→ use super::*;
432→
433→ /// Run the IPC server (Unix domain socket)
434→ pub async fn run_ipc_server(_state: Arc<IpcState>) -> anyhow::Result<()> {
435→ // TODO: Implement Unix socket server for macOS/Linux
436→ info!("Unix IPC server not yet implemented");
437→
438→ // For now, just sleep forever
439→ loop {
440→ tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
441→ }
442→ }
443→}
444→
445→#[cfg(test)]
446→mod tests {
447→ use super::*;
448→
449→ #[test]
450→ fn test_tray_policy_serialization() {
451→ let policy = TrayPolicy::default_permissive();
452→ let json = serde_json::to_string(&policy).unwrap();
453→ let parsed: TrayPolicy = serde_json::from_str(&json).unwrap();
454→ assert_eq!(parsed.enabled, policy.enabled);
455→ assert_eq!(parsed.allow_stop_agent, policy.allow_stop_agent);
456→ }
457→
458→ #[test]
459→ fn test_ipc_request_serialization() {
460→ let request = IpcRequest::GetStatus;
461→ let json = serde_json::to_string(&request).unwrap();
462→ assert!(json.contains("get_status"));
463→
464→ let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
465→ assert!(matches!(parsed, IpcRequest::GetStatus));
466→ }
467→
468→ #[test]
469→ fn test_ipc_response_serialization() {
470→ let response = IpcResponse::Denied {
471→ message: "Not allowed".to_string(),
472→ };
473→ let json = serde_json::to_string(&response).unwrap();
474→ assert!(json.contains("denied"));
475→
476→ let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
477→ assert!(matches!(parsed, IpcResponse::Denied { .. }));
478→ }
479→}
480→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>