Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -995,11 +995,11 @@ mod tests {
for (args, error) in [
(
vec![URL, "harness"],
"harness must be followed by claude or codex or agy or muse",
"harness must be followed by claude or codex or agy or grok or muse",
),
(
vec![URL, "harness", "gemini"],
"harness must be followed by claude or codex or agy or muse, not gemini",
"harness must be followed by claude or codex or agy or grok or muse, not gemini",
),
(
vec!["--harness", "claude", URL, "harness", "codex"],
Expand Down
2 changes: 1 addition & 1 deletion src/asks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ mod tests {
default: Some(Harness::Codex),
claude: model_and_effort("opus", "high"),
codex: model_and_effort("gpt-6.1-sol", "max"),
..Default::default()
..harness::Settings::default()
},
..no_settings()
}
Expand Down
14 changes: 10 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ parallel = 3 # how many Tickets a Spec run runs at once; default 3
limit = 3 # how many open issues labelled in-progress stop a Pickup run taking another; default 3

[harness]
default = "claude" # the Harness every Run's sessions run on, claude, codex, agy or muse; default claude
default = "claude" # the Harness every Run's sessions run on, claude, codex, agy, grok or muse; default claude

[harness.claude]
model = "" # the Model Claude Code's sessions run on; default blank, for Claude Code's own
Expand All @@ -401,6 +401,10 @@ effort = "" # how hard that Model reasons; default blank, for Codex's own
model = "" # the Model Antigravity CLI's sessions run on; default blank, for agy's own
effort = "" # how hard that Model reasons; default blank, for agy's own

[harness.grok]
model = "" # the Model Grok Build's sessions run on; default blank, for Grok Build's own
effort = "" # how hard that Model reasons; default blank, for Grok Build's own

[harness.muse]
model = "" # the Model Muse Code's sessions run on; default blank, for Muse Code's own
effort = "" # how hard that Model reasons; default blank, for Muse Code's own
Expand Down Expand Up @@ -538,6 +542,8 @@ mod tests {
"harness.codex.effort",
"harness.agy.model",
"harness.agy.effort",
"harness.grok.model",
"harness.grok.effort",
"harness.muse.model",
"harness.muse.effort"
]
Expand Down Expand Up @@ -589,7 +595,7 @@ mod tests {
model: Some("gpt-6.1-sol".to_string()),
effort: Some("max".to_string()),
},
..Default::default()
..harness::Settings::default()
}
);
assert_eq!(parse("").unwrap().harness, harness::Settings::default());
Expand All @@ -600,7 +606,7 @@ mod tests {
for (text, error) in [
(
"[harness]\ndefault = \"gemini\"\n",
"harness.default must be claude or codex or agy or muse, not \"gemini\"",
"harness.default must be claude or codex or agy or grok or muse, not \"gemini\"",
),
(
"[harness]\ndefault = true\n",
Expand Down Expand Up @@ -881,7 +887,7 @@ mod tests {
let config: toml::Table = completed.parse().unwrap();
let harness = config["harness"].as_table().unwrap();
assert!(harness.contains_key("default"), "{text:?}:\n{completed}");
for name in ["claude", "codex"] {
for name in ["claude", "codex", "agy", "grok"] {
let settings = harness[name].as_table().unwrap();
for key in ["model", "effort"] {
assert!(settings.contains_key(key), "{text:?}:\n{completed}");
Expand Down
6 changes: 5 additions & 1 deletion src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod adapter;
pub(crate) mod agy;
pub(crate) mod claude;
pub(crate) mod codex;
pub(crate) mod grok;
mod instructions;
pub(crate) mod muse;
mod process_tree;
Expand All @@ -26,6 +27,7 @@ pub enum Harness {
Claude,
Codex,
Agy,
Grok,
Muse,
}

Expand Down Expand Up @@ -98,6 +100,8 @@ pub struct Settings {
pub codex: ModelAndEffort,
/// `[harness.agy]`.
pub agy: ModelAndEffort,
/// `[harness.grok]`.
pub grok: ModelAndEffort,
/// `[harness.muse]`.
pub muse: ModelAndEffort,
}
Expand Down Expand Up @@ -286,7 +290,7 @@ mod tests {
default: Some(Harness::Codex),
claude: set("opus", "high"),
codex: set("gpt-6.1-sol", "max"),
..Default::default()
..Settings::default()
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/harness/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::process::Child;

use anyhow::Result;

use super::{Choice, Harness, ModelAndEffort, Settings, agy, claude, codex, muse};
use super::{Choice, Harness, ModelAndEffort, Settings, agy, claude, codex, grok, muse};
use crate::progress::Stream;
use crate::setup::Outside;

Expand Down Expand Up @@ -90,9 +90,10 @@ impl SkillLoading {
}

/// Registration comes from the adapters, including Setup's order.
pub const REGISTERED: [(Harness, &dyn Adapter); 4] = [
pub const REGISTERED: [(Harness, &dyn Adapter); 5] = [
claude::REGISTRATION,
codex::REGISTRATION,
agy::REGISTRATION,
grok::REGISTRATION,
muse::REGISTRATION,
];
35 changes: 20 additions & 15 deletions src/harness/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use anyhow::Result;
use super::adapter::{Adapter, Invocation, SkillLoading};
use super::{Choice, Harness, ModelAndEffort, Settings};
use crate::progress::{self, Stream};
use crate::setup::{Outside, questions::ask_setting};
use crate::setup::{Outside, questions::ask_checked_setting};

pub struct Codex;
pub const REGISTRATION: (Harness, &dyn Adapter) = (Harness::Codex, &Codex);
Expand Down Expand Up @@ -135,13 +135,13 @@ fn ask_codex(
Codex.product_name(),
catalog.listed().join(", ")
));
let model = loop {
let model = ask_setting(outside, "Model", Harness::Codex, current.model.as_deref())?;
match catalog.settle(model.as_deref(), None) {
Ok(settled) => break settled.model,
Err(error) => outside.say(format!("{error:#}")),
}
};
let model = ask_checked_setting(
outside,
"Model",
Harness::Codex,
current.model.as_deref(),
|model| catalog.settle(model, None).map(|settled| settled.model),
)?;
let efforts = catalog.efforts(model.as_deref()).join(", ");
outside.say(match &model {
Some(model) => format!("Efforts {model} supports: {efforts}"),
Expand All @@ -150,13 +150,18 @@ fn ask_codex(
Codex.product_name()
),
});
loop {
let effort = ask_setting(outside, "Effort", Harness::Codex, current.effort.as_deref())?;
match catalog.settle(model.as_deref(), effort.as_deref()) {
Ok(settled) => return Ok(settled),
Err(error) => outside.say(format!("{error:#}")),
}
}
let effort = ask_checked_setting(
outside,
"Effort",
Harness::Codex,
current.effort.as_deref(),
|effort| {
catalog
.settle(model.as_deref(), effort)
.map(|settled| settled.effort)
},
)?;
Ok(ModelAndEffort { model, effort })
}

/// `prompt` as Codex takes it: a first line that loads a Factory skill,
Expand Down
168 changes: 168 additions & 0 deletions src/harness/grok.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Grok Build: unattended sessions and its Claude-shaped Messages stream.

mod catalog;
mod stream;
pub use catalog::Catalog;

use std::path::Path;

use anyhow::Result;

use super::adapter::{Adapter, Invocation, SkillLoading};
use super::{Choice, Harness, ModelAndEffort, Settings};
use crate::progress::Stream;
use crate::setup::{Outside, questions::ask_checked_setting};

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(())
}
fn ask_settings(
&self,
outside: &mut dyn Outside,
current: &ModelAndEffort,
) -> Result<Option<ModelAndEffort>> {
let catalog = match outside.grok_catalog() {
Ok(catalog) => catalog,
Err(error) => {
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 stream(&self, _worktree: &Path, _prompt: &str) -> Box<dyn Stream> {
Box::new(stream::GrokProgress::default())
}
}

#[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 = stream::GrokProgress::default();
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 resume = adapter.session(&choice, progress.session_id(), "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")
]
);
}
}
Loading
Loading