use super::AgentDriver;
use crate::phase_id::PhaseId;
use std::path::PathBuf;
pub struct PiDriver;
impl AgentDriver for PiDriver {
fn name(&self) -> &'static str {
"Pi"
}
fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
crate::prompt::render_workflow_style(intent, &self.workflow_root())
}
fn workflow_root(&self) -> String {
"$HOME/.pi/agent/gsd-core/workflows".to_string()
}
fn capabilities(&self) -> super::DriverCapabilities {
super::DriverCapabilities {
subagent_dispatch: pi_subagent_dispatch_available(),
}
}
fn build_command(
&self,
_phase: PhaseId,
prompt: &str,
_extra_writable_roots: &[PathBuf],
) -> (&'static str, Vec<String>) {
(
"pi",
vec!["-p".into(), "--no-approve".into(), prompt.to_string()],
)
}
fn health(&self, _state: &crate::state::State) -> Result<(), String> {
let provider = configured_pi_provider().unwrap_or_else(|| "google".to_string());
let output = std::process::Command::new("pi")
.args([
"auth",
"check",
"--json",
"--provider",
&provider,
"--no-refresh",
])
.output()
.map_err(|e| format!("could not run `pi auth check`: {e}"))?;
classify_auth_check(&String::from_utf8_lossy(&output.stdout), output.status.success())
.map_err(|reason| {
format!(
"{reason} for provider `{provider}` — `pi auth check --json --provider {provider}` reports it not ready"
)
})
}
}
fn classify_auth_check(stdout: &str, success: bool) -> Result<(), String> {
let ready = success
&& serde_json::from_str::<serde_json::Value>(stdout)
.ok()
.and_then(|v| v.get("status").and_then(|s| s.as_str()).map(str::to_owned))
.is_some_and(|s| s == "ready");
if ready {
Ok(())
} else {
Err("no provider credential resolves".to_string())
}
}
fn configured_pi_provider() -> Option<String> {
let base = pi_config_dir()?;
let path = base.join("settings.json");
let text = std::fs::read_to_string(&path).ok()?;
let json = serde_json::from_str::<serde_json::Value>(&text).ok()?;
json.get("defaultProvider")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
}
fn pi_config_dir() -> Option<std::path::PathBuf> {
let raw = std::env::var_os("PI_CODING_AGENT_DIR")
.map(std::path::PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.map(|home| std::path::PathBuf::from(home).join(".pi").join("agent"))
})?;
let raw_str = raw.to_string_lossy();
if let Some(rest) = raw_str.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
return Some(std::path::PathBuf::from(home).join(rest));
}
Some(raw)
}
fn pi_subagent_dispatch_available() -> bool {
let Ok(output) = std::process::Command::new("pi")
.args(["list", "--no-approve"])
.output()
else {
return false;
};
output.status.success()
&& String::from_utf8_lossy(&output.stdout)
.to_lowercase()
.contains("@bacnh85/pi-subagent")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mode::Mode;
use crate::state::{AgentKind, State};
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn exec_command_shape() {
let (program, args) = PiDriver.build_command(PhaseId::new(1), "do the thing", &[]);
assert_eq!(program, "pi");
assert_eq!(args, vec!["-p", "--no-approve", "do the thing"]);
}
#[test]
fn classify_auth_check_rejects_not_ready() {
assert!(classify_auth_check(
r#"{"status":"not_ready","provider":"google","reason":"credentials_not_configured"}"#,
false,
)
.is_err());
}
#[test]
fn classify_auth_check_accepts_ready() {
assert!(
classify_auth_check(
r#"{"status":"ready","provider":"google","authType":"api_key"}"#,
true,
)
.is_ok()
);
}
#[test]
fn classify_auth_check_tolerates_formatted_json() {
assert!(classify_auth_check("{\n \"status\": \"ready\"\n}", true).is_ok());
}
#[test]
fn classify_auth_check_rejects_ready_text_with_failed_exit() {
assert!(classify_auth_check(r#"{"status":"ready"}"#, false).is_err());
}
fn test_state() -> State {
State::new(
PhaseId::new(36),
AgentKind::Pi,
Mode::Auto,
std::path::PathBuf::from("/tmp"),
)
}
fn stub_pi_on_path(body: &str, exit_code: i32) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("create stub dir");
let stub = dir.path().join("pi");
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > '{args}'\necho '{body}'\nexit {exit_code}\n",
args = dir.path().join("args.txt").display(),
body = body,
exit_code = exit_code,
);
std::fs::write(&stub, script).expect("write pi stub");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&stub).expect("stat stub").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&stub, perms).expect("chmod +x stub");
}
dir
}
fn stub_pi_with_provider(body: &str, exit_code: i32, provider: &str) -> tempfile::TempDir {
let dir = stub_pi_on_path(body, exit_code);
std::fs::write(
dir.path().join("settings.json"),
format!(r#"{{"defaultProvider":"{provider}"}}"#),
)
.expect("write settings.json");
dir
}
struct PathGuard {
original: Option<std::ffi::OsString>,
}
impl PathGuard {
fn set(path: &std::path::Path) -> Self {
let original = std::env::var_os("PATH");
unsafe { std::env::set_var("PATH", path) };
Self { original }
}
}
impl Drop for PathGuard {
fn drop(&mut self) {
match &self.original {
Some(prev) => unsafe { std::env::set_var("PATH", prev) },
None => unsafe { std::env::remove_var("PATH") },
}
}
}
struct EnvGuard {
name: &'static str,
original: Option<std::ffi::OsString>,
}
impl EnvGuard {
fn set(name: &'static str, value: &std::path::Path) -> Self {
let original = std::env::var_os(name);
unsafe { std::env::set_var(name, value) };
Self { name, original }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.original {
Some(prev) => unsafe { std::env::set_var(self.name, prev) },
None => unsafe { std::env::remove_var(self.name) },
}
}
}
#[test]
fn preflight_invokes_pi_auth_check_and_accepts_ready() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_with_provider(r#"{"status":"ready"}"#, 0, "litellm");
let _path = PathGuard::set(stub_dir.path());
let _cfgdir = EnvGuard::set("PI_CODING_AGENT_DIR", stub_dir.path());
PiDriver
.health(&test_state())
.expect("a `ready` stub should pass preflight");
let argv = std::fs::read_to_string(stub_dir.path().join("args.txt")).unwrap();
assert_eq!(
argv,
"auth\ncheck\n--json\n--provider\nlitellm\n--no-refresh\n"
);
}
#[test]
fn preflight_reports_credentialless_when_auth_check_says_not_ready() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_with_provider(
r#"{"status":"not_ready","reason":"credentials_not_configured"}"#,
0,
"litellm",
);
let _path = PathGuard::set(stub_dir.path());
let _cfgdir = EnvGuard::set("PI_CODING_AGENT_DIR", stub_dir.path());
let err = PiDriver
.health(&test_state())
.expect_err("a `not_ready` stub should fail preflight");
assert!(
err.contains("no provider credential resolves"),
"unexpected error: {err}"
);
}
#[test]
fn preflight_rejects_ready_body_with_failed_exit() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_with_provider(r#"{"status":"ready"}"#, 1, "litellm");
let _path = PathGuard::set(stub_dir.path());
let _cfgdir = EnvGuard::set("PI_CODING_AGENT_DIR", stub_dir.path());
assert!(
PiDriver.health(&test_state()).is_err(),
"a failed exit must not be read as ready even when the body says ready"
);
}
#[test]
fn preflight_falls_back_to_google_when_no_default_provider() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path(r#"{"status":"ready"}"#, 0);
let _path = PathGuard::set(stub_dir.path());
let _cfgdir = EnvGuard::set("PI_CODING_AGENT_DIR", stub_dir.path());
PiDriver
.health(&test_state())
.expect("a default-provider stub must pass preflight via the google fallback");
let argv = std::fs::read_to_string(stub_dir.path().join("args.txt")).unwrap();
assert_eq!(
argv,
"auth\ncheck\n--json\n--provider\ngoogle\n--no-refresh\n"
);
}
#[test]
fn pi_capabilities_detect_subagent_dispatch() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path("npm:@bacnh85/pi-subagent@0.15.1 (user)", 0);
let _path = PathGuard::set(stub_dir.path());
assert!(PiDriver.capabilities().subagent_dispatch);
let argv = std::fs::read_to_string(stub_dir.path().join("args.txt")).unwrap();
assert_eq!(argv, "list\n--no-approve\n");
}
#[test]
fn pi_capabilities_exclude_unvetted_subagent_packages() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path("npm:@mystilleef/pi-subagent@2.0.0 (user)", 0);
let _path = PathGuard::set(stub_dir.path());
assert!(!PiDriver.capabilities().subagent_dispatch);
}
#[test]
fn pi_capabilities_fail_closed_when_no_subagent() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path("No packages installed.", 0);
let _path = PathGuard::set(stub_dir.path());
assert!(!PiDriver.capabilities().subagent_dispatch);
}
#[test]
fn pi_capabilities_fail_closed_when_probe_fails() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path("", 1);
let _path = PathGuard::set(stub_dir.path());
assert!(!PiDriver.capabilities().subagent_dispatch);
}
}