Skip to main content

apm/cmd/
agents.rs

1use anyhow::Result;
2use apm_core::config::Config;
3use apm_core::wrapper::WrapperKind;
4use std::path::Path;
5use apm_core::start::AgentDiagnostic;
6
7pub fn run_list(root: &Path) -> Result<()> {
8    let config = Config::load(root)?;
9    let entries = apm_core::agents::list_wrappers(root, &config)?;
10
11    let name_w = entries.iter().map(|e| e.name.len()).max().unwrap_or(4).max(4);
12    let kind_w = "built-in".len();
13    let parser_w = "canonical".len();
14
15    println!(
16        "{:<name_w$}  {:<kind_w$}  {:<parser_w$}  STATUS",
17        "NAME", "KIND", "PARSER"
18    );
19    for entry in &entries {
20        let kind_str = match &entry.kind {
21            WrapperKind::Builtin(_) => "built-in",
22            WrapperKind::Custom { .. } => "project",
23        };
24        let status = entry.configured_as.join(", ");
25        println!(
26            "{:<name_w$}  {:<kind_w$}  {:<parser_w$}  {}",
27            entry.name, kind_str, entry.parser, status
28        );
29    }
30    Ok(())
31}
32
33pub fn run_new(root: &Path, name: &str, force: bool) -> Result<()> {
34    apm_core::agents::scaffold_wrapper(root, name, force)?;
35
36    let dir = root.join(".apm").join("agents").join(name);
37    println!("Created:");
38    println!("  {}", dir.join("wrapper.sh").display());
39    println!("  {}", dir.join("manifest.toml").display());
40    println!("  {}", dir.join("apm.worker.md").display());
41    println!("  {}", dir.join("apm.spec-writer.md").display());
42    println!();
43    println!("Next steps:");
44    println!("  1. Edit {}/wrapper.sh to invoke your AI tool", dir.display());
45    println!("  2. Run: apm agents test {name}");
46    Ok(())
47}
48
49pub fn run_test(root: &Path, name: &str) -> Result<()> {
50    let report = apm_core::agents::test_wrapper(root, name)?;
51
52    let label = if report.passed { "PASS" } else { "FAIL" };
53    println!(
54        "{}  exit={}  events={}  non-canonical={}  stderr={}  wall={}ms",
55        label,
56        report.exit_code,
57        report.canonical_events,
58        report.non_canonical_lines,
59        report.stderr_lines,
60        report.wall_millis,
61    );
62
63    if !report.passed {
64        anyhow::bail!("wrapper test failed");
65    }
66    Ok(())
67}
68
69pub fn run_eject(root: &Path, name: &str) -> Result<()> {
70    apm_core::agents::eject_wrapper(root, name)?;
71
72    let script = root.join(".apm").join("agents").join(name).join("wrapper.sh");
73    println!("Ejected to: {}", script.display());
74    println!("Run: apm agents test {name}");
75    Ok(())
76}
77
78pub fn run_resolve(root: &Path, ticket_id: &str, json: bool) -> Result<()> {
79    let diag = apm_core::start::resolve_for_diagnostic(root, ticket_id)?;
80    if json {
81        print_resolve_json(&diag)
82    } else {
83        print_resolve_human(&diag)
84    }
85}
86
87fn print_resolve_human(d: &AgentDiagnostic) -> Result<()> {
88    println!("Agent assignment for {} (state: {}):", d.ticket_id, d.ticket_state);
89    if !d.dispatchable {
90        println!("  note: state {:?} has no worker dispatch; showing resolution for {:?}", d.ticket_state, d.transition_label);
91    }
92    if d.transition_label == "none" {
93        println!("  (no command:start transition defined in this workflow)");
94        return Ok(());
95    }
96
97    const W: usize = 14;
98    let model_val = d.model.as_deref().unwrap_or("—");
99    let container_val = d.container.as_deref().unwrap_or("—");
100    let manifest_status = if d.manifest_present { "[present]" } else { "[absent]" };
101
102    println!("  {:<W$} {}  ({})", "agent", d.agent, d.agent_source);
103    println!("  {:<W$} {}  ({})", "role", d.role, d.role_source);
104    println!("  {:<W$} {}  ({})", "model", model_val, d.model_source);
105    println!("  {:<W$} {}  ({})", "container", container_val, d.container_source);
106    println!("  {:<W$} {}  {}", "manifest", d.manifest_path, manifest_status);
107
108    if d.env.is_empty() {
109        println!("  {:<W$} (none)", "env");
110    } else {
111        println!("  {:<W$}", "env");
112        for (k, v, src) in &d.env {
113            println!("    {k}={v}  ({src})");
114        }
115    }
116
117    if d.keychain.is_empty() {
118        println!("  {:<W$} (none)", "keychain");
119    } else {
120        println!("  {:<W$}", "keychain");
121        let mut entries: Vec<_> = d.keychain.iter().collect();
122        entries.sort_by_key(|(k, _)| k.as_str());
123        for (k, v) in entries {
124            println!("    {k} → {v}");
125        }
126    }
127    Ok(())
128}
129
130fn print_resolve_json(d: &AgentDiagnostic) -> Result<()> {
131    let env_arr: Vec<serde_json::Value> = d.env.iter()
132        .map(|(k, v, src)| serde_json::json!({"key": k, "value": v, "source": src}))
133        .collect();
134    let keychain_obj: serde_json::Map<String, serde_json::Value> = d.keychain.iter()
135        .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
136        .collect();
137    let obj = serde_json::json!({
138        "ticket_id": d.ticket_id,
139        "ticket_state": d.ticket_state,
140        "dispatchable": d.dispatchable,
141        "resolved_from_state": d.resolved_from_state,
142        "transition_label": d.transition_label,
143        "worker_profile_str": d.worker_profile_str,
144        "profile_source": d.profile_source,
145        "agent": d.agent,
146        "agent_source": d.agent_source,
147        "role": d.role,
148        "role_source": d.role_source,
149        "model": d.model,
150        "model_source": d.model_source,
151        "container": d.container,
152        "container_source": d.container_source,
153        "manifest_path": d.manifest_path,
154        "manifest_present": d.manifest_present,
155        "env": env_arr,
156        "keychain": keychain_obj,
157    });
158    println!("{}", serde_json::to_string_pretty(&obj)?);
159    Ok(())
160}