thirdshift 0.11.1

Turn a GitHub issue into a ready-for-review pull request, unattended
//! Grok Build: unattended sessions and its Claude-shaped Messages stream.

mod catalog;
pub use catalog::Catalog;

use std::path::Path;

use anyhow::Result;

use super::adapter::{Adapter, Invocation, SkillLoading, without};
use super::interpretation::{Interpretation, Retained, Stream};
use super::settings::{Terminal, ask_checked_setting};
use super::{Choice, Harness, ModelAndEffort, Settings};

pub struct Grok;
pub const REGISTRATION: (Harness, &dyn Adapter) = (Harness::Grok, &Grok);

impl Adapter for Grok {
    fn name(&self) -> &'static str {
        "grok"
    }
    fn product_name(&self) -> &'static str {
        "Grok Build"
    }
    fn project_skills(&self) -> &'static str {
        ".agents/skills"
    }
    fn skill_loading(&self) -> SkillLoading {
        SkillLoading::Slash
    }
    fn stop_signals(&self) -> &'static [libc::c_int] {
        &[libc::SIGTERM]
    }
    fn environment(&self) -> &'static [(&'static str, &'static str)] {
        &[
            ("GROK_DISABLE_AUTOUPDATER", "1"),
            ("GROK_FOLDER_TRUST", "0"),
        ]
    }
    fn settings<'a>(&self, settings: &'a Settings) -> &'a ModelAndEffort {
        &settings.grok
    }
    fn settings_mut<'a>(&self, settings: &'a mut Settings) -> &'a mut ModelAndEffort {
        &mut settings.grok
    }
    fn session(&self, choice: &Choice, resume: Option<&str>, prompt: &str) -> Invocation {
        let mut args: Vec<String> = [
            "--always-approve",
            "--sandbox",
            "off",
            "--output-format",
            "streaming-messages-json",
        ]
        .map(String::from)
        .to_vec();
        if let Some(model) = &choice.model {
            args.extend(["-m".to_string(), model.clone()]);
        }
        if let Some(effort) = &choice.effort {
            args.extend(["--reasoning-effort".to_string(), effort.clone()]);
        }
        if let Some(id) = resume {
            args.extend(["-r".to_string(), id.to_string()]);
        }
        args.extend(["-p".to_string(), self.skill_loading().prompt(prompt)]);
        Invocation { args, stdin: None }
    }
    fn check(&self, choice: &mut Choice) -> Result<()> {
        crate::progress::step("checking the Model and Effort against grok models");
        let settled = Catalog::read()?.settle(choice.model.as_deref(), choice.effort.as_deref())?;
        choice.model = settled.model;
        choice.effort = settled.effort;
        Ok(())
    }
    /// Only `todo_write`, which acts on nothing outside the session: no shell,
    /// files, web, or MCP through `search_tool` and `use_tool`.
    fn summary(&self, choice: &Choice, prompt: &str, prompt_file: &Path) -> Invocation {
        let mut invocation = self.session(choice, None, prompt);
        invocation.args.truncate(invocation.args.len() - 2);
        invocation.args = without(invocation.args, &["--always-approve"]);
        invocation.args = without(invocation.args, &["--sandbox", "off"]);
        invocation.args.extend(
            [
                "--tools",
                "todo_write",
                "--disallowed-tools",
                "search_tool,use_tool",
                "--disable-web-search",
                "--prompt-file",
            ]
            .map(String::from),
        );
        invocation
            .args
            .push(prompt_file.to_string_lossy().into_owned());
        invocation
    }
    fn ask_settings(
        &self,
        outside: &mut dyn Terminal,
        current: &ModelAndEffort,
    ) -> Result<Option<ModelAndEffort>> {
        let catalog = match Catalog::read() {
            Ok(catalog) => catalog,
            Err(error) => {
                crate::interrupt::check()?;
                outside.say(format!("{error:#}\nThe harness settings stay as they are; rerun `thirdshift setup` once grok models works."));
                return Ok(None);
            }
        };
        outside.say(format!(
            "{}'s Models: {}",
            self.product_name(),
            catalog.listed().join(", ")
        ));
        let model = ask_checked_setting(
            outside,
            "Model",
            Harness::Grok,
            current.model.as_deref(),
            |model| catalog.settle(model, None).map(|settled| settled.model),
        )?;
        outside.say(format!(
            "Efforts {} supports: {}",
            model.as_deref().unwrap_or("Grok Build's default Model"),
            catalog.efforts(model.as_deref()).join(", ")
        ));
        let effort = ask_checked_setting(
            outside,
            "Effort",
            Harness::Grok,
            current.effort.as_deref(),
            |effort| {
                catalog
                    .settle(model.as_deref(), effort)
                    .map(|settled| settled.effort)
            },
        )?;
        Ok(Some(ModelAndEffort { model, effort }))
    }
    fn interpretation(&self, _worktree: &Path, _prompt: &str) -> Interpretation {
        Interpretation::new(self.name(), Box::new(Stream::grok()), Retained::None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::harness::ChosenBy;

    #[test]
    fn a_resume_uses_a_streamed_id_with_the_same_flags_null_stdin_and_fixed_environment() {
        let mut progress = Grok.interpretation(Path::new("/repo"), "");
        progress
            .condense(r#"{"type":"system","subtype":"init","session_id":"abc123","cwd":"/repo"}"#);
        let choice = Choice {
            harness: Harness::Grok,
            model: Some("grok-4.7".to_string()),
            effort: Some("low".to_string()),
            chosen_by: ChosenBy::Command,
        };
        let adapter = choice.harness.adapter();
        let ended = crate::harness::interpretation_tests::finish(progress)
            .outcome
            .unwrap();
        let resume = adapter.session(&choice, ended.session_id.as_deref(), "Continue.");
        assert_eq!(
            resume.args,
            [
                "--always-approve",
                "--sandbox",
                "off",
                "--output-format",
                "streaming-messages-json",
                "-m",
                "grok-4.7",
                "--reasoning-effort",
                "low",
                "-r",
                "abc123",
                "-p",
                "Continue."
            ]
        );
        assert_eq!(resume.stdin, None);
        assert_eq!(
            adapter.environment(),
            [
                ("GROK_DISABLE_AUTOUPDATER", "1"),
                ("GROK_FOLDER_TRUST", "0")
            ]
        );
    }
}