ggen_cli_lib/cmds/agent.rs
1//! Agent noun — the AGI-facing CLI surface over `ggen_core::agent::PackAgent`.
2//!
3//! `ggen agent <verb>` is the third transport over the one authoritative pack
4//! lifecycle facade, alongside the Rust library API (`ggen_core::agent`) and the
5//! `ggen.packs.*` MCP/A2A tools (`ggen-a2a-mcp`). Every verb emits structured
6//! JSON an autonomous agent can parse and chain, covering the full
7//! project-bring-up lifecycle an AGI drives to complete a project:
8//!
9//! ```text
10//! capabilities → search / list / show → resolve → compatibility
11//! → install → status → verify (and remove)
12//! ```
13//!
14//! Because every verb routes through the same `PackAgent` the MCP/A2A surface
15//! uses — not a parallel implementation — the three transports cannot drift, and
16//! the durable-state contract (lockfile entry with a non-empty digest, a signed
17//! provenance receipt) and the fail-closed error behaviour are identical
18//! everywhere.
19
20use clap_noun_verb::{NounVerbError, Result};
21use clap_noun_verb_macros::verb;
22
23use ggen_core::agent::{InstallRequest, PackAgent};
24
25// ── helpers ─────────────────────────────────────────────────────────────────
26
27/// Construct an agent rooted at the current working directory (the project root
28/// for a CLI invocation, matching where `install` writes the lockfile).
29fn agent() -> Result<PackAgent> {
30 PackAgent::new()
31 .map_err(|e| NounVerbError::execution_error(format!("agent init failed: {}", e)))
32}
33
34/// Construct an agent at an explicit `--root`, or the current directory if none
35/// is given. Used by the read-only `status` / `verify` verbs.
36fn agent_at(root: Option<String>) -> Result<PackAgent> {
37 match root {
38 Some(r) => Ok(PackAgent::at_root(r)),
39 None => agent(),
40 }
41}
42
43/// Lift a facade error into a CLI error, preserving the message.
44fn lift<T>(r: ggen_core::agent::AgentResult<T>) -> Result<T> {
45 r.map_err(|e| NounVerbError::execution_error(e.to_string()))
46}
47
48/// Serialize a facade outcome to JSON so every verb emits a uniform,
49/// agent-parseable result.
50fn json<T: serde::Serialize>(value: T) -> Result<serde_json::Value> {
51 serde_json::to_value(value)
52 .map_err(|e| NounVerbError::execution_error(format!("serialization failed: {}", e)))
53}
54
55// ── discovery (read-only) ───────────────────────────────────────────────────
56
57/// Describe the agent's operations and capability surfaces — the discovery entry
58/// point an agent calls first to learn the contract.
59#[verb]
60pub fn capabilities() -> Result<serde_json::Value> {
61 json(agent()?.capabilities())
62}
63
64/// Relevance-rank packs in the local registry by a text query.
65#[verb]
66pub fn search(#[arg(index = 1)] query: String, limit: Option<usize>) -> Result<serde_json::Value> {
67 json(lift(agent()?.search(&query, limit))?)
68}
69
70/// List all packs in the local registry, optionally filtered by category.
71#[verb]
72pub fn list(category: Option<String>) -> Result<serde_json::Value> {
73 json(lift(agent()?.list(category.as_deref()))?)
74}
75
76/// Full detail for one pack: metadata, packages, templates, dependencies, and
77/// the validation (quality-gate) result.
78#[verb]
79pub fn show(#[arg(index = 1)] pack_id: String) -> Result<serde_json::Value> {
80 json(lift(agent()?.show(&pack_id))?)
81}
82
83/// Resolve a capability surface (e.g. `mcp`, `web`) to concrete pack IDs,
84/// optionally narrowed by `--projection` and `--runtime`.
85#[verb]
86pub fn resolve(
87 #[arg(index = 1)] surface: String, projection: Option<String>, runtime: Option<String>,
88) -> Result<serde_json::Value> {
89 json(lift(agent()?.resolve_capability(
90 &surface,
91 projection.as_deref(),
92 runtime.as_deref(),
93 ))?)
94}
95
96/// Check whether a comma-separated set of packs composes without conflicts
97/// (overlapping packages or unloadable packs). The pre-flight before a
98/// multi-pack install.
99#[verb]
100pub fn compatibility(#[arg(index = 1)] packs: String) -> Result<serde_json::Value> {
101 let a = agent()?;
102 let ids: Vec<String> = packs
103 .split(',')
104 .map(|s| s.trim().to_string())
105 .filter(|s| !s.is_empty())
106 .collect();
107 let res = crate::runtime::block_on(a.check_compatibility(&ids))
108 .map_err(|e| NounVerbError::execution_error(e.to_string()))?;
109 json(lift(res)?)
110}
111
112// ── installed state / provenance (read-only) ────────────────────────────────
113
114/// Report installed packs from the project lockfile (`--root` to inspect another
115/// project; default is the current directory).
116#[verb]
117pub fn status(root: Option<String>) -> Result<serde_json::Value> {
118 json(lift(agent_at(root)?.status())?)
119}
120
121/// Verify a provenance receipt against its signing key. Fail-closed: a missing
122/// key, malformed receipt, or bad signature yields `is_valid: false`.
123#[verb]
124pub fn verify(
125 #[arg(index = 1)] receipt_path: String, root: Option<String>,
126) -> Result<serde_json::Value> {
127 json(agent_at(root)?.verify(&receipt_path))
128}
129
130// ── mutating lifecycle ──────────────────────────────────────────────────────
131
132/// Install a pack: write the lockfile with a non-empty digest and emit a signed
133/// provenance receipt. `--dry_run` previews without writing durable state.
134#[verb]
135pub fn install(
136 #[arg(index = 1)] pack_id: String, force: Option<bool>, dry_run: Option<bool>,
137) -> Result<serde_json::Value> {
138 let a = agent()?;
139 let req = InstallRequest {
140 pack_id,
141 force: force.unwrap_or(false),
142 dry_run: dry_run.unwrap_or(false),
143 emit_receipt: true,
144 };
145 let res = crate::runtime::block_on(a.install(req))
146 .map_err(|e| NounVerbError::execution_error(e.to_string()))?;
147 json(lift(res)?)
148}
149
150/// Remove a pack from the project lockfile. Fail-closed: a missing lockfile or
151/// an absent pack errors and leaves the lockfile intact.
152#[verb]
153pub fn remove(#[arg(index = 1)] pack_id: String) -> Result<serde_json::Value> {
154 json(lift(agent()?.remove(&pack_id))?)
155}