Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package amake for openSUSE:Factory checked in at 2026-06-23 17:41:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/amake (Old) and /work/SRC/openSUSE:Factory/.amake.new.1956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "amake" Tue Jun 23 17:41:18 2026 rev:3 rq:1361280 version:0.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/amake/amake.changes 2026-05-12 19:30:59.465836579 +0200 +++ /work/SRC/openSUSE:Factory/.amake.new.1956/amake.changes 2026-06-23 17:44:17.877477383 +0200 @@ -1,0 +2,10 @@ +Tue Jun 23 06:56:09 UTC 2026 - Alessio Biancalana <[email protected]> + +- Update to version 0.5.0: + * chore: update project version to 0.5.0 (#22) + * build(deps): bump actions/checkout from 6 to 7 (#21) + * build(deps): bump assert_cmd from 2.2.1 to 2.2.2 (#19) + * feat: implement model selection (#20) + * feat: add `pi` adapter + +------------------------------------------------------------------- Old: ---- amake-0.4.0.tar.zst New: ---- amake-0.5.0.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ amake.spec ++++++ --- /var/tmp/diff_new_pack.nOydQ9/_old 2026-06-23 17:44:20.101554893 +0200 +++ /var/tmp/diff_new_pack.nOydQ9/_new 2026-06-23 17:44:20.105555032 +0200 @@ -16,7 +16,7 @@ # Name: amake -Version: 0.4.0 +Version: 0.5.0 Release: 0 Summary: A task runner for AI CLI tools # TODO: Run `cargo lock2rpmprovides --spdx` after vendoring to get the ++++++ _service ++++++ --- /var/tmp/diff_new_pack.nOydQ9/_old 2026-06-23 17:44:20.157556843 +0200 +++ /var/tmp/diff_new_pack.nOydQ9/_new 2026-06-23 17:44:20.161556984 +0200 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/dottorblaster/amake.git</param> <param name="scm">git</param> - <param name="revision">v0.4.0</param> + <param name="revision">v0.5.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="versionrewrite-replacement">\1</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.nOydQ9/_old 2026-06-23 17:44:20.185557820 +0200 +++ /var/tmp/diff_new_pack.nOydQ9/_new 2026-06-23 17:44:20.189557959 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/dottorblaster/amake.git</param> - <param name="changesrevision">3e06dac50819379550a00d6f6967413298df5ad4</param></service></servicedata> + <param name="changesrevision">f3aa5caf1cfa242c6d0f911e12388afb1cca6ece</param></service></servicedata> (No newline at EOF) ++++++ amake-0.4.0.tar.zst -> amake-0.5.0.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/.github/workflows/ci.yml new/amake-0.5.0/.github/workflows/ci.yml --- old/amake-0.4.0/.github/workflows/ci.yml 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/.github/workflows/ci.yml 2026-06-22 17:36:13.000000000 +0200 @@ -13,7 +13,7 @@ build-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/Cargo.lock new/amake-0.5.0/Cargo.lock --- old/amake-0.4.0/Cargo.lock 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/Cargo.lock 2026-06-22 17:36:13.000000000 +0200 @@ -92,9 +92,9 @@ [[package]] name = "assert_cmd" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/Cargo.toml new/amake-0.5.0/Cargo.toml --- old/amake-0.4.0/Cargo.toml 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/Cargo.toml 2026-06-22 17:36:13.000000000 +0200 @@ -1,6 +1,6 @@ [package] name = "amake" -version = "0.3.0" +version = "0.5.0" edition = "2024" license = "Apache-2.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/README.md new/amake-0.5.0/README.md --- old/amake-0.4.0/README.md 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/README.md 2026-06-22 17:36:13.000000000 +0200 @@ -59,11 +59,14 @@ tool = "aider" # override the default tool prompt = "Refactor error handling to use thiserror." files = ["src/main.rs", "src/error.rs"] # context files passed to the tool +model = "gpt-4" # model to use (passed as --model to the tool) auto_approve = true # tool-specific "don't ask" flag -extra_args = ["--model", "gpt-4"] # passed verbatim to the tool CLI +extra_args = ["--verbose"] # passed verbatim to the tool CLI (last-occurrence wins for any duplicate flag) timeout = 300 # seconds; kill the child if it runs longer (optional) ``` +`model` is just a first-class alias for the tool's own `--model` flag — useful for tools like `claude-code` where the flag spelling is the same, and easier to read than `extra_args = ["--model", "gpt-4"]`. It can be set on a task or under `[defaults]`. If both a task and `extra_args` set `--model`, the value in `extra_args` comes later in argv and wins (every tool in the table uses last-occurrence-wins for duplicate flags). + ### Timeout and retry AI CLIs hang and flake. `timeout` (in seconds) caps how long a single attempt may run; `[tasks.<name>.retry]` re-runs failed attempts with backoff: @@ -146,13 +149,15 @@ aider claude-code copilot +pi ``` -| Adapter | Binary | Auto-approve | Notes | -|---|---|---|---| -| `claude-code` | `claude` | `--dangerously-skip-permissions` | Prompt via `--print` | -| `aider` | `aider` | `--yes` | Prompt via `--message` | -| `copilot` | `gh` | (none) | Runs `gh copilot suggest -t shell` | +| Adapter | Binary | Auto-approve | Model | Notes | +|---|---|---|---|---| +| `claude-code` | `claude` | `--dangerously-skip-permissions` | `--model <name>` | Prompt via `--print` | +| `aider` | `aider` | `--yes` | `--model <name>` | Prompt via `--message` | +| `copilot` | `gh` | (none) | `--model <name>` | Runs `gh copilot suggest -t shell` | +| `pi` | `pi` | (none) | `--model <name>` | Prompt via `-p`; context files via inline `@path` | If the tool name doesn't match a built-in, amake treats it as a bare binary and passes `extra_args` + prompt as a positional arg. Good enough for most things: @@ -263,7 +268,7 @@ prompt = "Refactor error handling to use thiserror." files = ["src/main.rs", "src/error.rs"] auto_approve = true -extra_args = ["--model", "gpt-4"] +model = "gpt-4" ``` ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/aider.rs new/amake-0.5.0/src/adapter/aider.rs --- old/amake-0.4.0/src/adapter/aider.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/adapter/aider.rs 2026-06-22 17:36:13.000000000 +0200 @@ -43,6 +43,10 @@ cmd.arg("--file").arg(file); } + if let Some(model) = &task.model { + cmd.arg("--model").arg(model); + } + cmd.args(&task.extra_args); apply_workdir(&mut cmd, sandboxed, workdir); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/claude_code.rs new/amake-0.5.0/src/adapter/claude_code.rs --- old/amake-0.4.0/src/adapter/claude_code.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/adapter/claude_code.rs 2026-06-22 17:36:13.000000000 +0200 @@ -43,6 +43,10 @@ cmd.arg("--file").arg(file); } + if let Some(model) = &task.model { + cmd.arg("--model").arg(model); + } + cmd.args(&task.extra_args); cmd.arg(&task.prompt); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/copilot.rs new/amake-0.5.0/src/adapter/copilot.rs --- old/amake-0.4.0/src/adapter/copilot.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/adapter/copilot.rs 2026-06-22 17:36:13.000000000 +0200 @@ -40,6 +40,9 @@ } cmd.args(["copilot", "suggest", "-t", "shell"]); + if let Some(model) = &task.model { + cmd.arg("--model").arg(model); + } cmd.arg(&task.prompt); cmd.args(&task.extra_args); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/generic.rs new/amake-0.5.0/src/adapter/generic.rs --- old/amake-0.4.0/src/adapter/generic.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/adapter/generic.rs 2026-06-22 17:36:13.000000000 +0200 @@ -50,6 +50,10 @@ ); } + if let Some(model) = &task.model { + cmd.arg("--model").arg(model); + } + cmd.args(&task.extra_args); cmd.arg(&task.prompt); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/mod.rs new/amake-0.5.0/src/adapter/mod.rs --- old/amake-0.4.0/src/adapter/mod.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/adapter/mod.rs 2026-06-22 17:36:13.000000000 +0200 @@ -2,6 +2,7 @@ mod claude_code; mod copilot; mod generic; +mod pi; mod sandbox; use crate::config::Task; @@ -39,6 +40,7 @@ ); adapters.insert("aider".into(), Box::new(aider::AiderAdapter)); adapters.insert("copilot".into(), Box::new(copilot::CopilotAdapter)); + adapters.insert("pi".into(), Box::new(pi::PiAdapter)); Self { adapters } } @@ -93,6 +95,7 @@ retry: None, idle_warn: None, idle_kill: None, + model: None, } } @@ -176,6 +179,68 @@ assert_eq!(args, &["copilot", "suggest", "-t", "shell", "Suggest"]); } + // -- pi adapter tests -- + + #[test] + fn pi_basic() { + let adapter = pi::PiAdapter; + let task = make_task("Hello"); + let cmd = adapter.build_command(&task, None, false, None); + assert_eq!(cmd.get_program(), "pi"); + let args = get_args(&cmd); + assert_eq!(args, &["-p", "Hello"]); + } + + #[test] + fn pi_auto_approve() { + let adapter = pi::PiAdapter; + let task = make_task("Hello"); + // auto_approve=true emits a warning but passes no extra flags + let cmd = adapter.build_command(&task, None, true, None); + let args = get_args(&cmd); + assert_eq!(args, &["-p", "Hello"]); + } + + #[test] + fn pi_with_files() { + let adapter = pi::PiAdapter; + let mut task = make_task("Hello"); + task.files = vec![PathBuf::from("a.rs"), PathBuf::from("b.rs")]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_eq!(args, &["-p", "@a.rs", "@b.rs", "Hello"]); + } + + #[test] + fn pi_with_files_and_extra_args() { + let adapter = pi::PiAdapter; + let mut task = make_task("Hi"); + task.files = vec![PathBuf::from("src/main.rs")]; + task.extra_args = vec!["--model".into(), "foo".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_eq!(args, &["-p", "@src/main.rs", "--model", "foo", "Hi"]); + } + + #[test] + fn pi_sandbox_falls_back() { + let adapter = pi::PiAdapter; + let task = make_task("Fix"); + let sandbox = SandboxConfig::default(); + let cmd = adapter.build_command(&task, None, true, Some(&sandbox)); + // pi has no clampdown agent, so it falls back to running directly + assert_eq!(cmd.get_program(), "pi"); + } + + #[test] + fn pi_workdir_set_when_not_sandboxed() { + let adapter = pi::PiAdapter; + let task = make_task("Hello"); + let workdir = PathBuf::from("/my/project"); + let cmd = adapter.build_command(&task, Some(&workdir), false, None); + assert_eq!(cmd.get_current_dir(), Some(Path::new("/my/project"))); + } + #[test] fn generic_passthrough() { let adapter = GenericPassthrough::new("mytool"); @@ -244,6 +309,7 @@ assert!(names.contains(&"claude-code")); assert!(names.contains(&"aider")); assert!(names.contains(&"copilot")); + assert!(names.contains(&"pi")); } #[test] @@ -261,4 +327,230 @@ let cmd = adapter.build_command(&task, Some(&workdir), false, None); assert_eq!(cmd.get_current_dir(), Some(Path::new("/my/project"))); } + + // -- model flag -- + + fn assert_contains_pair(args: &[&std::ffi::OsStr], key: &str, value: &str) { + let mut iter = args.iter(); + let mut found = false; + while let Some(a) = iter.next() { + if a.to_string_lossy() == key + && iter + .next() + .map(|v| v.to_string_lossy() == value) + .unwrap_or(false) + { + found = true; + break; + } + } + assert!( + found, + "expected {key} {value:?} in args, got {:?}", + args.iter() + .map(|a| a.to_string_lossy().into_owned()) + .collect::<Vec<_>>() + ); + } + + fn assert_does_not_contain(args: &[&std::ffi::OsStr], key: &str) { + for a in args { + assert_ne!( + a.to_string_lossy(), + key, + "unexpected {key} in args: {:?}", + args + ); + } + } + + #[test] + fn claude_code_model_set() { + let adapter = claude_code::ClaudeCodeAdapter; + let mut task = make_task("Hello"); + task.model = Some("sonnet".into()); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_contains_pair(&args, "--model", "sonnet"); + } + + #[test] + fn claude_code_model_unset() { + let adapter = claude_code::ClaudeCodeAdapter; + let task = make_task("Hello"); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_does_not_contain(&args, "--model"); + } + + #[test] + fn claude_code_model_before_extra_args() { + let adapter = claude_code::ClaudeCodeAdapter; + let mut task = make_task("Hello"); + task.model = Some("sonnet".into()); + task.extra_args = vec!["--model".into(), "opus".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + // The config's --model comes first; the extra_args --model comes later and wins. + let pos_sonnet = args + .iter() + .position(|a| a.to_string_lossy() == "--model") + .expect("first --model"); + assert_eq!(args[pos_sonnet + 1], "sonnet"); + assert_eq!(args[pos_sonnet + 2], "--model"); + assert_eq!(args[pos_sonnet + 3], "opus"); + } + + #[test] + fn aider_model_set() { + let adapter = aider::AiderAdapter; + let mut task = make_task("Fix it"); + task.model = Some("gpt-4".into()); + let cmd = adapter.build_command(&task, None, true, None); + let args = get_args(&cmd); + assert_contains_pair(&args, "--model", "gpt-4"); + } + + #[test] + fn aider_model_unset() { + let adapter = aider::AiderAdapter; + let task = make_task("Fix it"); + let cmd = adapter.build_command(&task, None, true, None); + let args = get_args(&cmd); + assert_does_not_contain(&args, "--model"); + } + + #[test] + fn aider_model_before_extra_args() { + let adapter = aider::AiderAdapter; + let mut task = make_task("Fix it"); + task.model = Some("gpt-4".into()); + task.extra_args = vec!["--model".into(), "opus".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + let pos = args + .iter() + .position(|a| a.to_string_lossy() == "--model") + .unwrap(); + assert_eq!(args[pos + 1], "gpt-4"); + assert_eq!(args[pos + 2], "--model"); + assert_eq!(args[pos + 3], "opus"); + } + + #[test] + fn copilot_model_set() { + let adapter = copilot::CopilotAdapter; + let mut task = make_task("Suggest"); + task.model = Some("gpt-4o".into()); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_contains_pair(&args, "--model", "gpt-4o"); + } + + #[test] + fn copilot_model_unset() { + let adapter = copilot::CopilotAdapter; + let task = make_task("Suggest"); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_does_not_contain(&args, "--model"); + } + + #[test] + fn copilot_model_before_extra_args() { + let adapter = copilot::CopilotAdapter; + let mut task = make_task("Suggest"); + task.model = Some("gpt-4o".into()); + task.extra_args = vec!["--model".into(), "haiku".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + // For copilot, the prompt sits between the config --model and the + // extra_args --model, so the extra-args value lands later in argv and + // wins. Find the LAST --model occurrence and confirm it carries "haiku". + let last_pos = args + .iter() + .rposition(|a| a.to_string_lossy() == "--model") + .unwrap(); + assert_eq!(args[last_pos + 1], "haiku"); + // The config value still appears earlier in argv. + let first_pos = args + .iter() + .position(|a| a.to_string_lossy() == "--model") + .unwrap(); + assert_eq!(args[first_pos + 1], "gpt-4o"); + } + + #[test] + fn pi_model_set() { + let adapter = pi::PiAdapter; + let mut task = make_task("Hello"); + task.model = Some("claude".into()); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_contains_pair(&args, "--model", "claude"); + } + + #[test] + fn pi_model_unset() { + let adapter = pi::PiAdapter; + let task = make_task("Hello"); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_does_not_contain(&args, "--model"); + } + + #[test] + fn pi_model_before_extra_args() { + let adapter = pi::PiAdapter; + let mut task = make_task("Hi"); + task.files = vec![PathBuf::from("src/main.rs")]; + task.model = Some("claude".into()); + task.extra_args = vec!["--model".into(), "gpt-4".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + // --model from config comes after the @file tokens but before extra_args + let pos = args + .iter() + .position(|a| a.to_string_lossy() == "--model") + .unwrap(); + assert_eq!(args[pos + 1], "claude"); + assert_eq!(args[pos + 2], "--model"); + assert_eq!(args[pos + 3], "gpt-4"); + } + + #[test] + fn generic_passthrough_model_set() { + let adapter = GenericPassthrough::new("mytool"); + let mut task = make_task("Do stuff"); + task.model = Some("sonnet".into()); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_contains_pair(&args, "--model", "sonnet"); + } + + #[test] + fn generic_passthrough_model_unset() { + let adapter = GenericPassthrough::new("mytool"); + let task = make_task("Do stuff"); + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + assert_does_not_contain(&args, "--model"); + } + + #[test] + fn generic_passthrough_model_before_extra_args() { + let adapter = GenericPassthrough::new("mytool"); + let mut task = make_task("Do stuff"); + task.model = Some("sonnet".into()); + task.extra_args = vec!["--model".into(), "opus".into()]; + let cmd = adapter.build_command(&task, None, false, None); + let args = get_args(&cmd); + let pos = args + .iter() + .position(|a| a.to_string_lossy() == "--model") + .unwrap(); + assert_eq!(args[pos + 1], "sonnet"); + assert_eq!(args[pos + 2], "--model"); + assert_eq!(args[pos + 3], "opus"); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/adapter/pi.rs new/amake-0.5.0/src/adapter/pi.rs --- old/amake-0.4.0/src/adapter/pi.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/amake-0.5.0/src/adapter/pi.rs 2026-06-22 17:36:13.000000000 +0200 @@ -0,0 +1,60 @@ +use super::Adapter; +use super::sandbox::{apply_workdir, start_sandboxed_command}; +use crate::config::Task; +use crate::sandbox::SandboxConfig; +use std::path::Path; +use std::process::Command; + +pub struct PiAdapter; + +impl Adapter for PiAdapter { + fn name(&self) -> &str { + "pi" + } + + fn clampdown_agent(&self) -> Option<&str> { + None + } + + fn build_command( + &self, + task: &Task, + workdir: Option<&Path>, + auto_approve: bool, + sandbox: Option<&SandboxConfig>, + ) -> Command { + let mut cmd = Command::new("pi"); + + let sandboxed = start_sandboxed_command( + &mut cmd, + self.clampdown_agent(), + self.name(), + sandbox, + workdir, + ); + + if auto_approve { + eprintln!("warning: auto_approve is set for pi but no known flag exists — ignoring"); + } + + cmd.arg("-p"); + + // pi uses inline @<path> tokens in the prompt for context files. + // Each is passed as a separate argv entry so paths containing + // spaces are preserved by the kernel's argv boundary. + for file in &task.files { + cmd.arg(format!("@{}", file.display())); + } + + if let Some(model) = &task.model { + cmd.arg("--model").arg(model); + } + + cmd.args(&task.extra_args); + cmd.arg(&task.prompt); + + apply_workdir(&mut cmd, sandboxed, workdir); + + cmd + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/config.rs new/amake-0.5.0/src/config.rs --- old/amake-0.4.0/src/config.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/config.rs 2026-06-22 17:36:13.000000000 +0200 @@ -15,6 +15,7 @@ pub retry: Option<RetryConfig>, pub idle_warn: Option<u64>, pub idle_kill: Option<u64>, + pub model: Option<String>, } #[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Default)] @@ -75,6 +76,7 @@ retry: Option<RetryConfig>, idle_warn: Option<u64>, idle_kill: Option<u64>, + model: Option<String>, } #[derive(Debug, Clone)] @@ -92,6 +94,7 @@ pub retry: Option<RetryConfig>, pub idle_warn: Option<u64>, pub idle_kill: Option<u64>, + pub model: Option<String>, } impl From<RawTask> for Task { @@ -116,6 +119,7 @@ retry: raw.retry, idle_warn: raw.idle_warn, idle_kill: raw.idle_kill, + model: raw.model, } } } @@ -196,6 +200,13 @@ .ok_or_else(|| Error::NoTool(task_name.into())) } + pub fn effective_model(&self, task: &Task) -> Option<String> { + task.model + .as_ref() + .or(self.defaults.model.as_ref()) + .cloned() + } + pub fn effective_workdir(&self, task: &Task) -> Option<PathBuf> { task.workdir .as_ref() @@ -672,4 +683,58 @@ assert_eq!(effective.attempts, 5); assert_eq!(effective.backoff, BackoffStrategy::Exponential); } + + #[test] + fn parses_model_field() { + let toml = r#" +[tasks.t] +prompt = "x" +model = "sonnet" +"#; + let cfg = Config::from_str(toml, Path::new("Amakefile")).unwrap(); + assert_eq!(cfg.tasks["t"].model.as_deref(), Some("sonnet")); + } + + #[test] + fn effective_model_inherits_default() { + let toml = r#" +[defaults] +model = "opus" + +[tasks.t] +prompt = "x" +"#; + let cfg = Config::from_str(toml, Path::new("Amakefile")).unwrap(); + assert_eq!( + cfg.effective_model(&cfg.tasks["t"]).as_deref(), + Some("opus") + ); + } + + #[test] + fn effective_model_task_overrides_default() { + let toml = r#" +[defaults] +model = "opus" + +[tasks.t] +prompt = "x" +model = "sonnet" +"#; + let cfg = Config::from_str(toml, Path::new("Amakefile")).unwrap(); + assert_eq!( + cfg.effective_model(&cfg.tasks["t"]).as_deref(), + Some("sonnet") + ); + } + + #[test] + fn effective_model_none_when_unset() { + let toml = r#" +[tasks.t] +prompt = "x" +"#; + let cfg = Config::from_str(toml, Path::new("Amakefile")).unwrap(); + assert!(cfg.effective_model(&cfg.tasks["t"]).is_none()); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/src/runner.rs new/amake-0.5.0/src/runner.rs --- old/amake-0.4.0/src/runner.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/src/runner.rs 2026-06-22 17:36:13.000000000 +0200 @@ -163,6 +163,7 @@ let mut rendered_task = task.clone(); rendered_task.prompt = rendered_prompt; + rendered_task.model = config.effective_model(task); let resolved = registry.resolve_or_generic(&tool); let adapter = resolved.adapter(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amake-0.4.0/tests/cli.rs new/amake-0.5.0/tests/cli.rs --- old/amake-0.4.0/tests/cli.rs 2026-05-08 15:34:05.000000000 +0200 +++ new/amake-0.5.0/tests/cli.rs 2026-06-22 17:36:13.000000000 +0200 @@ -40,7 +40,8 @@ amake().arg("adapters").assert().success().stdout( predicate::str::contains("claude-code") .and(predicate::str::contains("aider")) - .and(predicate::str::contains("copilot")), + .and(predicate::str::contains("copilot")) + .and(predicate::str::contains("pi")), ); } @@ -1190,3 +1191,170 @@ "two attempts of ~1s idle-kill plus 1s backoff should fit well under 10s" ); } + +// ── Model flag ── + +#[test] +fn dry_run_emits_model_flag() { + let dir = TempDir::new().unwrap(); + setup_amakefile( + &dir, + r#" +[tasks.greet] +tool = "claude-code" +prompt = "Hello" +model = "sonnet" +"#, + ); + + amake() + .args([ + "run", + "--dry-run", + "-f", + dir.path().join("Amakefile").to_str().unwrap(), + "greet", + ]) + .assert() + .success() + .stdout( + predicate::str::contains("[greet]") + .and(predicate::str::contains("claude")) + .and(predicate::str::contains("--model")) + .and(predicate::str::contains("sonnet")), + ); +} + +#[test] +fn dry_run_inherits_default_model() { + let dir = TempDir::new().unwrap(); + setup_amakefile( + &dir, + r#" +[defaults] +tool = "claude-code" +model = "opus" + +[tasks.greet] +prompt = "Hello" +"#, + ); + + amake() + .args([ + "run", + "--dry-run", + "-f", + dir.path().join("Amakefile").to_str().unwrap(), + "greet", + ]) + .assert() + .success() + .stdout(predicate::str::contains("--model").and(predicate::str::contains("opus"))); +} + +#[test] +fn dry_run_task_model_overrides_default() { + let dir = TempDir::new().unwrap(); + setup_amakefile( + &dir, + r#" +[defaults] +tool = "claude-code" +model = "opus" + +[tasks.greet] +prompt = "Hello" +model = "sonnet" +"#, + ); + + amake() + .args([ + "run", + "--dry-run", + "-f", + dir.path().join("Amakefile").to_str().unwrap(), + "greet", + ]) + .assert() + .success() + .stdout(predicate::str::contains("sonnet").and(predicate::str::contains("opus").not())); +} + +#[test] +fn extra_args_model_overrides_config_model() { + let dir = TempDir::new().unwrap(); + setup_amakefile( + &dir, + r#" +[tasks.greet] +tool = "claude-code" +prompt = "Hello" +model = "sonnet" +extra_args = ["--model", "opus"] +"#, + ); + + let output = amake() + .args([ + "run", + "--dry-run", + "-f", + dir.path().join("Amakefile").to_str().unwrap(), + "greet", + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + let stdout = String::from_utf8(output).unwrap(); + // The extra_args --model should appear later in the rendered command + // and therefore "win" against the config's --model. + let sonnet_pos = stdout + .find("sonnet") + .expect("expected 'sonnet' to appear in dry-run output"); + let opus_pos = stdout + .rfind("opus") + .expect("expected 'opus' to appear in dry-run output"); + let last_model_pos = stdout + .rfind("--model") + .expect("expected '--model' to appear"); + // The last --model must be the extra_args one carrying "opus". + let after_last = &stdout[last_model_pos..]; + assert!( + after_last.contains("opus"), + "expected last --model to carry 'opus', got tail: {after_last:?}" + ); + // 'sonnet' must appear before 'opus' in the rendered command. + assert!( + sonnet_pos < opus_pos, + "expected 'sonnet' to appear before 'opus', got:\n{stdout}" + ); +} + +#[test] +fn dry_run_no_model_when_unset() { + let dir = TempDir::new().unwrap(); + setup_amakefile( + &dir, + r#" +[tasks.greet] +tool = "claude-code" +prompt = "Hello" +"#, + ); + + amake() + .args([ + "run", + "--dry-run", + "-f", + dir.path().join("Amakefile").to_str().unwrap(), + "greet", + ]) + .assert() + .success() + .stdout(predicate::str::contains("--model").not()); +} ++++++ amake.obsinfo ++++++ --- /var/tmp/diff_new_pack.nOydQ9/_old 2026-06-23 17:44:20.313562281 +0200 +++ /var/tmp/diff_new_pack.nOydQ9/_new 2026-06-23 17:44:20.317562420 +0200 @@ -1,5 +1,5 @@ name: amake -version: 0.4.0 -mtime: 1778247245 -commit: 3e06dac50819379550a00d6f6967413298df5ad4 +version: 0.5.0 +mtime: 1782142573 +commit: f3aa5caf1cfa242c6d0f911e12388afb1cca6ece ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/amake/vendor.tar.zst /work/SRC/openSUSE:Factory/.amake.new.1956/vendor.tar.zst differ: char 7, line 1
