Skip to main content

lit/commands/
agent_profile.rs

1//! Generic agent profile system — extends the swarm subsystem from SWE-only
2//! agents to arbitrary domain agents (CAD designers, EDA engineers, writers,
3//! DBAs, reviewers, CI bots, security auditors, etc.).
4//!
5//! Each agent profile declares capabilities, supported content types, trust
6//! domains, and resource limits so Lit can intelligently route work, enforce
7//! access policies, and schedule across heterogeneous agent fleets.
8
9use crate::core::find_repo_root;
10use crate::errors::LitError;
11use crate::response::AgentProfileResponse;
12use chrono::Utc;
13use serde::{Deserialize, Serialize};
14use std::fs;
15use std::path::Path;
16
17// ── Data types ──────────────────────────────────────────────────────────────
18
19/// Domain classification for agent specialization
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum AgentDomain {
22    /// Software engineering (code, tests, CI/CD)
23    Software,
24    /// Mechanical / industrial CAD modeling
25    Cad,
26    /// Electronic design automation (PCB, schematic, FPGA)
27    Eda,
28    /// Writing — technical docs, manuscripts, legal prose
29    Writer,
30    /// Database administration — schema, migration, optimization
31    Dba,
32    /// Code / design review
33    Reviewer,
34    /// Continuous integration and deployment
35    Ci,
36    /// Security auditing and compliance
37    Security,
38    /// Data science and ML pipelines
39    DataScience,
40    /// DevOps / infrastructure management
41    DevOps,
42    /// Quality assurance / test automation
43    Qa,
44    /// Project management / coordination
45    ProjectManagement,
46    /// General purpose
47    General,
48    /// Custom domain
49    Custom(String),
50}
51
52impl std::fmt::Display for AgentDomain {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            AgentDomain::Software => write!(f, "software"),
56            AgentDomain::Cad => write!(f, "cad"),
57            AgentDomain::Eda => write!(f, "eda"),
58            AgentDomain::Writer => write!(f, "writer"),
59            AgentDomain::Dba => write!(f, "dba"),
60            AgentDomain::Reviewer => write!(f, "reviewer"),
61            AgentDomain::Ci => write!(f, "ci"),
62            AgentDomain::Security => write!(f, "security"),
63            AgentDomain::DataScience => write!(f, "data-science"),
64            AgentDomain::DevOps => write!(f, "devops"),
65            AgentDomain::Qa => write!(f, "qa"),
66            AgentDomain::ProjectManagement => write!(f, "project-management"),
67            AgentDomain::General => write!(f, "general"),
68            AgentDomain::Custom(s) => write!(f, "custom:{}", s),
69        }
70    }
71}
72
73/// Capabilities an agent can advertise
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub enum Capability {
76    /// Read files / objects
77    Read,
78    /// Write / modify files
79    Write,
80    /// Create new branches
81    Branch,
82    /// Merge branches
83    Merge,
84    /// Review and approve
85    Review,
86    /// Deploy / release
87    Deploy,
88    /// Run tests
89    Test,
90    /// Run security scans
91    SecurityScan,
92    /// Generate diffs / patches
93    Diff,
94    /// Manage large files (LFS operations)
95    Lfs,
96    /// Create / manage intents
97    Intent,
98    /// Converge intents to mainline
99    Converge,
100    /// Manage content type metadata
101    ContentMetadata,
102    /// Cross-domain coordination (orchestrate other agents)
103    Orchestrate,
104    /// Structural diff / merge for binary formats
105    StructuralAnalysis,
106    /// Schema-aware operations (database agents)
107    SchemaManagement,
108    /// Custom capability
109    Custom(String),
110}
111
112impl std::fmt::Display for Capability {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            Capability::Read => write!(f, "read"),
116            Capability::Write => write!(f, "write"),
117            Capability::Branch => write!(f, "branch"),
118            Capability::Merge => write!(f, "merge"),
119            Capability::Review => write!(f, "review"),
120            Capability::Deploy => write!(f, "deploy"),
121            Capability::Test => write!(f, "test"),
122            Capability::SecurityScan => write!(f, "security-scan"),
123            Capability::Diff => write!(f, "diff"),
124            Capability::Lfs => write!(f, "lfs"),
125            Capability::Intent => write!(f, "intent"),
126            Capability::Converge => write!(f, "converge"),
127            Capability::ContentMetadata => write!(f, "content-metadata"),
128            Capability::Orchestrate => write!(f, "orchestrate"),
129            Capability::StructuralAnalysis => write!(f, "structural-analysis"),
130            Capability::SchemaManagement => write!(f, "schema-management"),
131            Capability::Custom(s) => write!(f, "custom:{}", s),
132        }
133    }
134}
135
136/// Trust level assigned to an agent
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
138pub enum TrustLevel {
139    /// Untrusted — read-only sandbox
140    Untrusted = 0,
141    /// Limited — can propose changes but not merge
142    Limited = 1,
143    /// Standard — full read/write within assigned scope
144    Standard = 2,
145    /// Elevated — can merge, converge, manage intents
146    Elevated = 3,
147    /// Admin — full control including key management
148    Admin = 4,
149}
150
151impl std::fmt::Display for TrustLevel {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            TrustLevel::Untrusted => write!(f, "untrusted"),
155            TrustLevel::Limited => write!(f, "limited"),
156            TrustLevel::Standard => write!(f, "standard"),
157            TrustLevel::Elevated => write!(f, "elevated"),
158            TrustLevel::Admin => write!(f, "admin"),
159        }
160    }
161}
162
163/// Resource constraints for an agent
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ResourceLimits {
166    /// Maximum file size the agent can write (bytes, 0 = unlimited)
167    pub max_file_size: u64,
168    /// Maximum total storage the agent can consume (bytes, 0 = unlimited)
169    pub max_total_storage: u64,
170    /// Maximum number of files the agent can modify per commit
171    pub max_files_per_commit: u32,
172    /// Maximum number of concurrent leases
173    pub max_concurrent_leases: u32,
174    /// Maximum branch count the agent can own
175    pub max_branches: u32,
176    /// Rate limit — max operations per minute (0 = unlimited)
177    pub max_ops_per_minute: u32,
178    /// Whether the agent can access network (fetch/push/clone)
179    pub network_access: bool,
180    /// Whether the agent can execute hooks/scripts
181    pub hook_execution: bool,
182}
183
184impl Default for ResourceLimits {
185    fn default() -> Self {
186        Self {
187            max_file_size: 100 * 1024 * 1024, // 100 MB
188            max_total_storage: 0,             // unlimited
189            max_files_per_commit: 1000,
190            max_concurrent_leases: 10,
191            max_branches: 5,
192            max_ops_per_minute: 60,
193            network_access: true,
194            hook_execution: false,
195        }
196    }
197}
198
199/// A complete agent profile definition
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AgentProfile {
202    /// Unique profile identifier
203    pub profile_id: String,
204    /// Human-readable name
205    pub name: String,
206    /// Domain specialization
207    pub domain: AgentDomain,
208    /// Capabilities this agent advertises
209    pub capabilities: Vec<Capability>,
210    /// Content type IDs this agent can work with (empty = all)
211    pub supported_content_types: Vec<String>,
212    /// Trust level
213    pub trust_level: TrustLevel,
214    /// Resource limits
215    pub resource_limits: ResourceLimits,
216    /// Path patterns this agent is allowed to modify (glob patterns, empty = all)
217    pub allowed_paths: Vec<String>,
218    /// Path patterns this agent is denied from modifying
219    pub denied_paths: Vec<String>,
220    /// Description
221    pub description: String,
222    /// Version of the profile schema
223    pub version: String,
224    /// Registration timestamp
225    pub registered_at: String,
226    /// Optional parent profile to inherit from
227    pub inherits_from: Option<String>,
228    /// Arbitrary metadata
229    #[serde(default)]
230    pub metadata: serde_json::Value,
231}
232
233// ── Built-in profiles ───────────────────────────────────────────────────────
234
235fn builtin_profiles() -> Vec<AgentProfile> {
236    let now = Utc::now().to_rfc3339();
237    vec![
238        AgentProfile {
239            profile_id: "swe-default".into(),
240            name: "Software Engineer".into(),
241            domain: AgentDomain::Software,
242            capabilities: vec![
243                Capability::Read, Capability::Write, Capability::Branch,
244                Capability::Merge, Capability::Test, Capability::Diff,
245                Capability::Intent, Capability::Converge,
246            ],
247            supported_content_types: vec![],
248            trust_level: TrustLevel::Standard,
249            resource_limits: ResourceLimits::default(),
250            allowed_paths: vec!["**/*.rs".into(), "**/*.py".into(), "**/*.ts".into(), "**/*.js".into(),
251                                "**/*.go".into(), "**/*.java".into(), "**/*.c".into(), "**/*.cpp".into(),
252                                "**/*.h".into(), "**/*.toml".into(), "**/*.json".into(), "**/*.yaml".into(),
253                                "**/*.yml".into(), "**/*.md".into(), "**/*.txt".into()],
254            denied_paths: vec![],
255            description: "General-purpose software engineering agent".into(),
256            version: "1.0".into(),
257            registered_at: now.clone(),
258            inherits_from: None,
259            metadata: serde_json::json!({}),
260        },
261        AgentProfile {
262            profile_id: "cad-designer".into(),
263            name: "CAD Designer".into(),
264            domain: AgentDomain::Cad,
265            capabilities: vec![
266                Capability::Read, Capability::Write, Capability::Branch,
267                Capability::Lfs, Capability::Diff, Capability::Intent,
268                Capability::StructuralAnalysis, Capability::ContentMetadata,
269            ],
270            supported_content_types: vec![
271                "cad/step".into(), "cad/stl".into(), "cad/iges".into(), "cad/3mf".into(),
272            ],
273            trust_level: TrustLevel::Standard,
274            resource_limits: ResourceLimits {
275                max_file_size: 500 * 1024 * 1024, // 500 MB for CAD
276                max_files_per_commit: 50,
277                max_concurrent_leases: 5,
278                ..Default::default()
279            },
280            allowed_paths: vec!["**/*.step".into(), "**/*.stp".into(), "**/*.stl".into(),
281                                "**/*.igs".into(), "**/*.iges".into(), "**/*.3mf".into(),
282                                "**/*.obj".into(), "**/*.dxf".into()],
283            denied_paths: vec!["src/**".into()],
284            description: "Mechanical/industrial CAD modeling agent with LFS and structural diff support".into(),
285            version: "1.0".into(),
286            registered_at: now.clone(),
287            inherits_from: None,
288            metadata: serde_json::json!({"tools": ["openscad", "freecad", "solidworks"]}),
289        },
290        AgentProfile {
291            profile_id: "eda-engineer".into(),
292            name: "EDA Engineer".into(),
293            domain: AgentDomain::Eda,
294            capabilities: vec![
295                Capability::Read, Capability::Write, Capability::Branch,
296                Capability::Diff, Capability::Intent, Capability::StructuralAnalysis,
297                Capability::ContentMetadata,
298            ],
299            supported_content_types: vec![
300                "eda/kicad-pcb".into(), "eda/kicad-sch".into(), "eda/gerber".into(), "eda/spice".into(),
301            ],
302            trust_level: TrustLevel::Standard,
303            resource_limits: ResourceLimits {
304                max_files_per_commit: 100,
305                max_concurrent_leases: 8,
306                ..Default::default()
307            },
308            allowed_paths: vec!["**/*.kicad_pcb".into(), "**/*.kicad_sch".into(),
309                                "**/*.gbr".into(), "**/*.ger".into(), "**/*.spice".into(),
310                                "**/*.lib".into(), "**/*.bom".into()],
311            denied_paths: vec![],
312            description: "Electronic design automation agent for PCB/schematic/FPGA work".into(),
313            version: "1.0".into(),
314            registered_at: now.clone(),
315            inherits_from: None,
316            metadata: serde_json::json!({"tools": ["kicad", "ltspice", "verilator"]}),
317        },
318        AgentProfile {
319            profile_id: "tech-writer".into(),
320            name: "Technical Writer".into(),
321            domain: AgentDomain::Writer,
322            capabilities: vec![
323                Capability::Read, Capability::Write, Capability::Branch,
324                Capability::Diff, Capability::Intent, Capability::ContentMetadata,
325            ],
326            supported_content_types: vec![
327                "manuscript/latex".into(), "manuscript/docx".into(),
328                "manuscript/typst".into(), "manuscript/asciidoc".into(),
329            ],
330            trust_level: TrustLevel::Standard,
331            resource_limits: ResourceLimits {
332                max_file_size: 50 * 1024 * 1024,
333                max_files_per_commit: 200,
334                ..Default::default()
335            },
336            allowed_paths: vec!["**/*.md".into(), "**/*.tex".into(), "**/*.typ".into(),
337                                "**/*.adoc".into(), "**/*.rst".into(), "**/*.docx".into(),
338                                "**/*.txt".into(), "docs/**".into()],
339            denied_paths: vec!["src/**".into()],
340            description: "Technical writing agent for documentation, manuscripts, and publications".into(),
341            version: "1.0".into(),
342            registered_at: now.clone(),
343            inherits_from: None,
344            metadata: serde_json::json!({"languages": ["en", "de", "fr", "ja"]}),
345        },
346        AgentProfile {
347            profile_id: "dba".into(),
348            name: "Database Administrator".into(),
349            domain: AgentDomain::Dba,
350            capabilities: vec![
351                Capability::Read, Capability::Write, Capability::Branch,
352                Capability::Diff, Capability::Intent, Capability::SchemaManagement,
353                Capability::ContentMetadata,
354            ],
355            supported_content_types: vec![
356                "db/sqlite".into(), "db/csv".into(), "db/parquet".into(), "db/sql-migration".into(),
357            ],
358            trust_level: TrustLevel::Elevated,
359            resource_limits: ResourceLimits {
360                max_file_size: 1024 * 1024 * 1024, // 1 GB for databases
361                max_files_per_commit: 50,
362                ..Default::default()
363            },
364            allowed_paths: vec!["**/*.sql".into(), "**/*.sqlite".into(), "**/*.db".into(),
365                                "**/*.csv".into(), "**/*.parquet".into(), "migrations/**".into()],
366            denied_paths: vec![],
367            description: "Database administration agent for schema, migration, and data versioning".into(),
368            version: "1.0".into(),
369            registered_at: now.clone(),
370            inherits_from: None,
371            metadata: serde_json::json!({"tools": ["sqlite", "postgres", "duckdb"]}),
372        },
373        AgentProfile {
374            profile_id: "reviewer".into(),
375            name: "Code & Design Reviewer".into(),
376            domain: AgentDomain::Reviewer,
377            capabilities: vec![
378                Capability::Read, Capability::Review, Capability::Diff,
379                Capability::Converge, Capability::ContentMetadata,
380            ],
381            supported_content_types: vec![], // reviews all types
382            trust_level: TrustLevel::Elevated,
383            resource_limits: ResourceLimits {
384                max_files_per_commit: 0,
385                max_file_size: 0,
386                network_access: false,
387                ..Default::default()
388            },
389            allowed_paths: vec![],
390            denied_paths: vec![],
391            description: "Read-only review agent that can approve and converge but not modify files".into(),
392            version: "1.0".into(),
393            registered_at: now.clone(),
394            inherits_from: None,
395            metadata: serde_json::json!({}),
396        },
397        AgentProfile {
398            profile_id: "ci-bot".into(),
399            name: "CI/CD Bot".into(),
400            domain: AgentDomain::Ci,
401            capabilities: vec![
402                Capability::Read, Capability::Test, Capability::Deploy,
403                Capability::SecurityScan, Capability::Diff,
404            ],
405            supported_content_types: vec![],
406            trust_level: TrustLevel::Elevated,
407            resource_limits: ResourceLimits {
408                network_access: true,
409                hook_execution: true,
410                ..Default::default()
411            },
412            allowed_paths: vec![],
413            denied_paths: vec![],
414            description: "CI/CD automation agent for build, test, and deploy pipelines".into(),
415            version: "1.0".into(),
416            registered_at: now.clone(),
417            inherits_from: None,
418            metadata: serde_json::json!({}),
419        },
420        AgentProfile {
421            profile_id: "security-auditor".into(),
422            name: "Security Auditor".into(),
423            domain: AgentDomain::Security,
424            capabilities: vec![
425                Capability::Read, Capability::Review, Capability::SecurityScan,
426                Capability::Diff,
427            ],
428            supported_content_types: vec![],
429            trust_level: TrustLevel::Elevated,
430            resource_limits: ResourceLimits {
431                max_file_size: 0,
432                max_files_per_commit: 0,
433                network_access: false,
434                hook_execution: false,
435                ..Default::default()
436            },
437            allowed_paths: vec![],
438            denied_paths: vec![],
439            description: "Security auditing agent — read-only scanning and compliance verification".into(),
440            version: "1.0".into(),
441            registered_at: now.clone(),
442            inherits_from: None,
443            metadata: serde_json::json!({}),
444        },
445        AgentProfile {
446            profile_id: "data-scientist".into(),
447            name: "Data Scientist".into(),
448            domain: AgentDomain::DataScience,
449            capabilities: vec![
450                Capability::Read, Capability::Write, Capability::Branch,
451                Capability::Lfs, Capability::Intent, Capability::ContentMetadata,
452                Capability::SchemaManagement,
453            ],
454            supported_content_types: vec![
455                "scientific/hdf5".into(), "scientific/jupyter".into(),
456                "db/parquet".into(), "db/csv".into(),
457            ],
458            trust_level: TrustLevel::Standard,
459            resource_limits: ResourceLimits {
460                max_file_size: 2 * 1024 * 1024 * 1024, // 2 GB for datasets
461                max_concurrent_leases: 3,
462                ..Default::default()
463            },
464            allowed_paths: vec!["**/*.ipynb".into(), "**/*.h5".into(), "**/*.hdf5".into(),
465                                "**/*.parquet".into(), "**/*.csv".into(), "**/*.py".into(),
466                                "data/**".into(), "notebooks/**".into(), "models/**".into()],
467            denied_paths: vec![],
468            description: "Data science agent for notebooks, datasets, and ML pipeline versioning".into(),
469            version: "1.0".into(),
470            registered_at: now.clone(),
471            inherits_from: None,
472            metadata: serde_json::json!({"frameworks": ["pytorch", "tensorflow", "scikit-learn"]}),
473        },
474        AgentProfile {
475            profile_id: "orchestrator".into(),
476            name: "Multi-Agent Orchestrator".into(),
477            domain: AgentDomain::General,
478            capabilities: vec![
479                Capability::Read, Capability::Orchestrate, Capability::Intent,
480                Capability::Converge, Capability::Review,
481            ],
482            supported_content_types: vec![],
483            trust_level: TrustLevel::Admin,
484            resource_limits: ResourceLimits {
485                max_files_per_commit: 0,
486                max_file_size: 0,
487                network_access: true,
488                ..Default::default()
489            },
490            allowed_paths: vec![],
491            denied_paths: vec![],
492            description: "Meta-agent that orchestrates, coordinates, and routes work across domain-specific agents".into(),
493            version: "1.0".into(),
494            registered_at: now,
495            inherits_from: None,
496            metadata: serde_json::json!({}),
497        },
498    ]
499}
500
501// ── Helpers ─────────────────────────────────────────────────────────────────
502
503fn profiles_dir(repo_root: &Path) -> std::path::PathBuf {
504    repo_root.join(".lit").join("agent-profiles")
505}
506
507fn save_profile(repo_root: &Path, profile: &AgentProfile) -> Result<(), LitError> {
508    let dir = profiles_dir(repo_root);
509    fs::create_dir_all(&dir).map_err(|e| LitError::io(e.to_string()))?;
510    let json = serde_json::to_string_pretty(profile)
511        .map_err(|e| LitError::general(format!("Serialize profile: {}", e)))?;
512    fs::write(dir.join(format!("{}.json", profile.profile_id)), json)
513        .map_err(|e| LitError::io(e.to_string()))?;
514    Ok(())
515}
516
517fn load_all_profiles(repo_root: &Path) -> Result<Vec<AgentProfile>, LitError> {
518    let dir = profiles_dir(repo_root);
519    let mut profiles = builtin_profiles();
520
521    if dir.exists() {
522        for entry in fs::read_dir(&dir).map_err(|e| LitError::io(e.to_string()))? {
523            let entry = entry.map_err(|e| LitError::io(e.to_string()))?;
524            if entry
525                .path()
526                .extension()
527                .map(|e| e == "json")
528                .unwrap_or(false)
529            {
530                let json =
531                    fs::read_to_string(entry.path()).map_err(|e| LitError::io(e.to_string()))?;
532                if let Ok(p) = serde_json::from_str::<AgentProfile>(&json) {
533                    profiles.retain(|b| b.profile_id != p.profile_id);
534                    profiles.push(p);
535                }
536            }
537        }
538    }
539    Ok(profiles)
540}
541
542// ── Public API ──────────────────────────────────────────────────────────────
543
544/// List all agent profiles, optionally filtered by domain
545pub fn execute_list(domain_filter: Option<String>) -> Result<AgentProfileResponse, LitError> {
546    let repo_root = find_repo_root().unwrap_or_else(|_| std::path::PathBuf::from("."));
547    let mut profiles = load_all_profiles(&repo_root)?;
548
549    if let Some(ref domain) = domain_filter {
550        profiles.retain(|p| p.domain.to_string() == *domain);
551    }
552
553    let count = profiles.len();
554    let summary: Vec<serde_json::Value> = profiles
555        .iter()
556        .map(|p| {
557            serde_json::json!({
558                "profile_id": p.profile_id,
559                "name": p.name,
560                "domain": p.domain.to_string(),
561                "trust_level": p.trust_level.to_string(),
562                "capabilities": p.capabilities.iter().map(|c| c.to_string()).collect::<Vec<_>>(),
563                "content_types": p.supported_content_types,
564            })
565        })
566        .collect();
567
568    Ok(AgentProfileResponse {
569        action: "list".into(),
570        profile_id: None,
571        message: format!("{} agent profile(s)", count),
572        details: Some(serde_json::to_value(&summary).unwrap_or_default()),
573    })
574}
575
576/// Show a specific agent profile
577pub fn execute_show(profile_id: String) -> Result<AgentProfileResponse, LitError> {
578    let repo_root = find_repo_root().unwrap_or_else(|_| std::path::PathBuf::from("."));
579    let profiles = load_all_profiles(&repo_root)?;
580
581    let profile = profiles
582        .iter()
583        .find(|p| p.profile_id == profile_id)
584        .ok_or_else(|| LitError::general(format!("Agent profile not found: {}", profile_id)))?;
585
586    Ok(AgentProfileResponse {
587        action: "show".into(),
588        profile_id: Some(profile.profile_id.clone()),
589        message: format!(
590            "{} ({}, trust={})",
591            profile.name, profile.domain, profile.trust_level
592        ),
593        details: Some(serde_json::to_value(profile).unwrap_or_default()),
594    })
595}
596
597/// Register a custom agent profile
598#[allow(clippy::too_many_arguments)]
599pub fn execute_register(
600    profile_id: String,
601    name: String,
602    domain: String,
603    capabilities: Vec<String>,
604    trust_level: Option<String>,
605    content_types: Vec<String>,
606    allowed_paths: Vec<String>,
607    denied_paths: Vec<String>,
608) -> Result<AgentProfileResponse, LitError> {
609    let repo_root = find_repo_root()?;
610
611    let domain_enum = match domain.as_str() {
612        "software" => AgentDomain::Software,
613        "cad" => AgentDomain::Cad,
614        "eda" => AgentDomain::Eda,
615        "writer" => AgentDomain::Writer,
616        "dba" => AgentDomain::Dba,
617        "reviewer" => AgentDomain::Reviewer,
618        "ci" => AgentDomain::Ci,
619        "security" => AgentDomain::Security,
620        "data-science" => AgentDomain::DataScience,
621        "devops" => AgentDomain::DevOps,
622        "qa" => AgentDomain::Qa,
623        "project-management" => AgentDomain::ProjectManagement,
624        "general" => AgentDomain::General,
625        other => AgentDomain::Custom(other.to_string()),
626    };
627
628    let caps: Vec<Capability> = capabilities
629        .iter()
630        .map(|c| match c.as_str() {
631            "read" => Capability::Read,
632            "write" => Capability::Write,
633            "branch" => Capability::Branch,
634            "merge" => Capability::Merge,
635            "review" => Capability::Review,
636            "deploy" => Capability::Deploy,
637            "test" => Capability::Test,
638            "security-scan" => Capability::SecurityScan,
639            "diff" => Capability::Diff,
640            "lfs" => Capability::Lfs,
641            "intent" => Capability::Intent,
642            "converge" => Capability::Converge,
643            "content-metadata" => Capability::ContentMetadata,
644            "orchestrate" => Capability::Orchestrate,
645            "structural-analysis" => Capability::StructuralAnalysis,
646            "schema-management" => Capability::SchemaManagement,
647            other => Capability::Custom(other.to_string()),
648        })
649        .collect();
650
651    let trust = match trust_level.as_deref() {
652        Some("untrusted") => TrustLevel::Untrusted,
653        Some("limited") => TrustLevel::Limited,
654        Some("elevated") => TrustLevel::Elevated,
655        Some("admin") => TrustLevel::Admin,
656        _ => TrustLevel::Standard,
657    };
658
659    let profile = AgentProfile {
660        profile_id: profile_id.clone(),
661        name: name.clone(),
662        domain: domain_enum,
663        capabilities: caps,
664        supported_content_types: content_types,
665        trust_level: trust,
666        resource_limits: ResourceLimits::default(),
667        allowed_paths,
668        denied_paths,
669        description: format!("Custom agent profile: {}", name),
670        version: "1.0".into(),
671        registered_at: Utc::now().to_rfc3339(),
672        inherits_from: None,
673        metadata: serde_json::json!({}),
674    };
675
676    save_profile(&repo_root, &profile)?;
677
678    Ok(AgentProfileResponse {
679        action: "register".into(),
680        profile_id: Some(profile_id),
681        message: format!("Agent profile '{}' registered", name),
682        details: Some(serde_json::to_value(&profile).unwrap_or_default()),
683    })
684}
685
686/// List capabilities available for a given domain
687pub fn execute_capabilities(domain: Option<String>) -> Result<AgentProfileResponse, LitError> {
688    let repo_root = find_repo_root().unwrap_or_else(|_| std::path::PathBuf::from("."));
689    let profiles = load_all_profiles(&repo_root)?;
690
691    let filtered: Vec<&AgentProfile> = if let Some(ref d) = domain {
692        profiles
693            .iter()
694            .filter(|p| p.domain.to_string() == *d)
695            .collect()
696    } else {
697        profiles.iter().collect()
698    };
699
700    // Aggregate unique capabilities
701    let mut all_caps: Vec<String> = filtered
702        .iter()
703        .flat_map(|p| p.capabilities.iter().map(|c| c.to_string()))
704        .collect();
705    all_caps.sort();
706    all_caps.dedup();
707
708    // Aggregate unique domains
709    let mut all_domains: Vec<String> = profiles.iter().map(|p| p.domain.to_string()).collect();
710    all_domains.sort();
711    all_domains.dedup();
712
713    Ok(AgentProfileResponse {
714        action: "capabilities".into(),
715        profile_id: None,
716        message: format!(
717            "{} unique capability/ies across {} domain(s)",
718            all_caps.len(),
719            all_domains.len()
720        ),
721        details: Some(serde_json::json!({
722            "capabilities": all_caps,
723            "domains": all_domains,
724            "profiles_by_domain": all_domains.iter().map(|d| {
725                let count = profiles.iter().filter(|p| p.domain.to_string() == *d).count();
726                serde_json::json!({"domain": d, "profile_count": count})
727            }).collect::<Vec<_>>(),
728        })),
729    })
730}
731
732/// Remove a custom agent profile
733pub fn execute_remove(profile_id: String) -> Result<AgentProfileResponse, LitError> {
734    let repo_root = find_repo_root()?;
735    let dir = profiles_dir(&repo_root);
736    let path = dir.join(format!("{}.json", profile_id));
737
738    if !path.exists() {
739        // Could be a builtin — can't remove builtins
740        let builtins = builtin_profiles();
741        if builtins.iter().any(|p| p.profile_id == profile_id) {
742            return Err(LitError::general(format!(
743                "Cannot remove built-in profile: {}",
744                profile_id
745            )));
746        }
747        return Err(LitError::general(format!(
748            "Agent profile not found: {}",
749            profile_id
750        )));
751    }
752
753    fs::remove_file(&path).map_err(|e| LitError::io(e.to_string()))?;
754
755    Ok(AgentProfileResponse {
756        action: "remove".into(),
757        profile_id: Some(profile_id.clone()),
758        message: format!("Agent profile '{}' removed", profile_id),
759        details: None,
760    })
761}