use super::{AgentAdapter, 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 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 output = std::process::Command::new("pi")
.args([
"auth",
"check",
"--json",
"--provider",
"google",
"--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(),
)
}
}
pub struct PiAgent;
impl AgentAdapter for PiAgent {
fn name(&self) -> &'static str {
PiDriver.name()
}
fn exec_command(
&self,
phase: PhaseId,
prompt: &str,
extra_writable_roots: &[PathBuf],
) -> (&'static str, Vec<String>) {
PiDriver.build_command(phase, prompt, extra_writable_roots)
}
fn completion_signal_detected(&self, _output: &str) -> bool {
false
}
fn preflight(&self, state: &crate::state::State) -> Result<(), String> {
PiDriver.health(state)
}
fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
PiDriver.render_prompt(intent)
}
}
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 — run `pi auth check` for details".to_string())
}
}
#[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) = PiAgent.exec_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
}
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") },
}
}
}
#[test]
fn preflight_invokes_pi_auth_check_and_accepts_ready() {
let _guard = ENV_MUTEX.lock().unwrap();
let stub_dir = stub_pi_on_path(r#"{"status":"ready"}"#, 0);
let _path = PathGuard::set(stub_dir.path());
PiAgent
.preflight(&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\ngoogle\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_on_path(
r#"{"status":"not_ready","reason":"credentials_not_configured"}"#,
0,
);
let _path = PathGuard::set(stub_dir.path());
let err = PiAgent
.preflight(&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_on_path(r#"{"status":"ready"}"#, 1);
let _path = PathGuard::set(stub_dir.path());
assert!(
PiAgent.preflight(&test_state()).is_err(),
"a failed exit must not be read as ready even when the body says ready"
);
}
}