Register an installed agent named with --backend (R5.5) (#821)

`rebuild --backend <name>` uses a configured backend of that name as it is, and registers an installed agent whose name matches when it is not added yet; an unknown name adds nothing. Follow-up to #819 (R5.5).
This commit is contained in:
Alex Joaquim Pereira
2026-11-04 18:33:19 +05:30
committed by GitHub
parent f8d6e3585a
commit e59b1ef4d8
3 changed files with 58 additions and 10 deletions
+15 -8
View File
@@ -38,7 +38,7 @@ pub struct RebuildArgs {
/// the computer stopped); the request is ignored.
#[arg(long, requires = "continue_in")]
pub resume: bool,
/// The coding agent to use (a backend name from `ai list`).
/// The coding agent to use (a backend name from `ai list`, or an installed agent such as `Claude Code`).
#[arg(long)]
pub backend: Option<String>,
/// Refinement rounds after the first build.
@@ -71,14 +71,21 @@ pub fn rebuild(cli: &Cli, a: &RebuildArgs) -> u8 {
}
}
/// Add the first agent app found on this computer when no enabled AI can build at all.
fn ensure_agent(hub: &AiHub) -> Result<(), ToolError> {
// An enabled API backend builds with our own agent loop, so this run needs no agent app. Only a
// computer with no AI set up needs one found for it.
if hub.backends().iter().any(|b| b.enabled) {
/// Add the agent app this run asks for, or the first one found when no enabled AI can build.
fn ensure_agent(hub: &AiHub, requested: Option<&str>) -> Result<(), ToolError> {
let configured = hub.backends();
// An enabled backend can build, so a run that names none needs no agent app.
if requested.is_none() && configured.iter().any(|b| b.enabled) {
return Ok(());
}
if let Some(d) = hub.detect().into_iter().find(|d| d.suggestion.kind == BackendKind::AgentCli) {
// A name that is configured is used as it is (including a backend that is off): don't add a
// second one under the same name.
if requested.is_some_and(|name| configured.iter().any(|b| b.name == name)) {
return Ok(());
}
// An explicit name may be an installed agent that isn't added yet: add only that one. With no
// name, add the first agent found.
if let Some(d) = hub.detect().into_iter().find(|d| d.suggestion.kind == BackendKind::AgentCli && requested.is_none_or(|name| d.suggestion.name == name)) {
eprintln!("Using {} ({}).", d.suggestion.name, d.reason);
hub.add_backend(d.suggestion, None)?;
}
@@ -95,7 +102,7 @@ fn run(cli: &Cli, a: &RebuildArgs) -> Result<bool, ToolError> {
if a.generate_assets && a.continue_in.is_some() {
return Err(ToolError::invalid("--generate-assets is for new projects.").with_remediation("Use project-assets-allow for an existing project."));
}
ensure_agent(&hub)?;
ensure_agent(&hub, a.backend.as_deref())?;
let (ws, message) = match &a.continue_in {
Some(dir) => (Workspace::open(&std::path::absolute(dir).map_err(|e| ToolError::io(dir.display(), e))?)?, (!a.resume).then(|| a.request.clone())),
None => {
@@ -102,6 +102,47 @@ fn an_enabled_api_backend_is_enough_without_being_named() {
assert!(!saved.contains("agent_cli"), "no agent app may be saved when an API backend is enabled:\n{saved}");
}
/// A named agent that is installed but not added yet is still registered, so the run can use it.
#[test]
fn a_named_installed_agent_is_added_when_missing() {
let config = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let bin = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
api_config(config.path());
fake_claude(bin.path());
let args = stopped_run(project.path(), Some("Claude Code"));
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
let out = cli(&refs, config.path(), home.path(), bin.path());
let log = out.stderr_text();
assert!(!out.success, "the empty project folder must stop the run:\n{log}");
assert!(log.contains("craft.json"), "the run must stop at the missing project, before any build:\n{log}");
assert!(log.contains("Using Claude Code"), "the named agent must still be registered:\n{log}");
let saved = std::fs::read_to_string(config.path().join("config.toml")).unwrap();
assert!(saved.contains("agent_cli") && saved.contains("name = \"Claude Code\""), "the named agent must be saved:\n{saved}");
}
/// An unknown name must not add a different agent that happens to be installed.
#[test]
fn an_unknown_backend_does_not_add_an_agent() {
let config = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
let bin = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
fake_claude(bin.path());
let args = stopped_run(project.path(), Some("No Such AI"));
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
let out = cli(&refs, config.path(), home.path(), bin.path());
let log = out.stderr_text();
assert!(!out.success, "the empty project folder must stop the run:\n{log}");
assert!(log.contains("craft.json"), "the run must stop at the missing project, before any build:\n{log}");
assert!(!log.contains("Using "), "an unknown name must not pick an installed agent:\n{log}");
let saved = std::fs::read_to_string(config.path().join("config.toml")).unwrap_or_default();
assert!(!saved.contains("agent_cli"), "an unknown name must not save an agent:\n{saved}");
}
/// A command refused before any build starts must not change the person's AI settings either.
#[test]
fn an_invalid_command_does_not_add_an_agent() {
+2 -2
View File
@@ -319,8 +319,8 @@ Owner: open · Status: todo
settings and prints "Using Claude Code (…)": `ensure_agent` (`rebuild_cmd.rs`) runs whenever no
agent app is set up, though this run won't use one. Run it only when the build needs one
— claimed: AlexJoaquimPereira, branch `bugfix/r5.5-no-agent-with-api-backend`
- [ ] `--backend <an unknown or disabled name>` with no enabled AI still finds and saves an agent
before the name is refused: check the name before `ensure_agent` runs
- [ ] `--backend <a name that is turned off>` still builds with it: `builder_backend`'s named
branch doesn't check `enabled`, while Settings treats an AI that is off as unusable
- [x] Bound accumulated data across all lines of one server-sent event, so short lines cannot
grow a streamed API response without limit
— claimed: Afloat16, branch bugfix/r5.5-bounded-sse-events-20261103