Skip to content

Commit fb1de50

Browse files
feat: auto-discover and recompile all agentic pipelines (#96)
Running \�do-aw compile\ with no path argument now auto-discovers all existing compiled YAML files via the \# @ado-aw source=...\ header, resolves each source markdown path, and recompiles the pipeline. - Make \path\ argument optional in the Compile CLI command - Add \compile_all_pipelines()\ that reuses \detect::detect_pipelines()\ - Gracefully skip missing source files (warn but continue batch) - Reject \--output\ in auto-discovery mode (ambiguous with multiple files) - Add integration tests for auto-discover recompile and missing source Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2e2c4fd commit fb1de50

3 files changed

Lines changed: 230 additions & 6 deletions

File tree

src/compile/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,80 @@ pub async fn compile_pipeline(input_path: &str, output_path: Option<&str>) -> Re
106106
Ok(())
107107
}
108108

109+
/// Auto-discover and recompile all agentic pipelines in the current directory.
110+
///
111+
/// Scans for compiled YAML files containing the `# @ado-aw source=...` header,
112+
/// resolves each source markdown path, and recompiles. Pipelines whose source
113+
/// files are missing are reported but don't abort the batch.
114+
pub async fn compile_all_pipelines() -> Result<()> {
115+
let root = Path::new(".");
116+
info!("Auto-discovering agentic pipelines for recompilation");
117+
118+
let detected = crate::detect::detect_pipelines(root).await?;
119+
120+
if detected.is_empty() {
121+
println!("No agentic pipelines found in the current directory.");
122+
println!("To compile a single file, run: ado-aw compile <path>");
123+
return Ok(());
124+
}
125+
126+
println!("Found {} agentic pipeline(s):", detected.len());
127+
for p in &detected {
128+
println!(
129+
" {} (source: {}, version: {})",
130+
p.yaml_path.display(),
131+
p.source,
132+
p.version
133+
);
134+
}
135+
println!();
136+
137+
let mut success_count = 0;
138+
let mut skip_count = 0;
139+
let mut fail_count = 0;
140+
141+
for pipeline in &detected {
142+
let source_path = root.join(&pipeline.source);
143+
let yaml_output_path = root.join(&pipeline.yaml_path);
144+
145+
if !source_path.exists() {
146+
eprintln!(
147+
" Warning: source '{}' not found for {}, skipping",
148+
pipeline.source,
149+
pipeline.yaml_path.display()
150+
);
151+
skip_count += 1;
152+
continue;
153+
}
154+
155+
let source_str = source_path.to_string_lossy();
156+
let output_str = yaml_output_path.to_string_lossy();
157+
158+
match compile_pipeline(&source_str, Some(&output_str)).await {
159+
Ok(()) => success_count += 1,
160+
Err(e) => {
161+
eprintln!(
162+
" Error compiling '{}': {}",
163+
pipeline.source, e
164+
);
165+
fail_count += 1;
166+
}
167+
}
168+
}
169+
170+
println!();
171+
println!(
172+
"Done: {} compiled, {} skipped, {} failed.",
173+
success_count, skip_count, fail_count
174+
);
175+
176+
if fail_count > 0 {
177+
anyhow::bail!("{} pipeline(s) failed to compile", fail_count);
178+
}
179+
180+
Ok(())
181+
}
182+
109183
/// Check that a compiled pipeline YAML matches its source markdown.
110184
///
111185
/// Compiles the source markdown fresh and compares (whitespace-normalized)

src/main.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ enum Commands {
2929
#[arg(short, long)]
3030
output: Option<PathBuf>,
3131
},
32-
/// Compile markdown to pipeline definition
32+
/// Compile markdown to pipeline definition (or recompile all detected pipelines)
3333
Compile {
34-
/// Path to the input markdown file
35-
path: String,
34+
/// Path to the input markdown file. If omitted, auto-discovers and
35+
/// recompiles all existing agentic pipelines in the current directory.
36+
path: Option<String>,
3637
/// Optional output path for the generated YAML file
3738
#[arg(short, long)]
3839
output: Option<String>,
@@ -142,9 +143,18 @@ async fn main() -> Result<()> {
142143
Commands::Create { output } => {
143144
create::create_agent(output).await?;
144145
}
145-
Commands::Compile { path, output } => {
146-
compile::compile_pipeline(&path, output.as_deref()).await?;
147-
}
146+
Commands::Compile { path, output } => match path {
147+
Some(p) => compile::compile_pipeline(&p, output.as_deref()).await?,
148+
None => {
149+
if output.is_some() {
150+
anyhow::bail!(
151+
"--output cannot be used with auto-discovery mode. \
152+
Specify a path to compile a single file with a custom output."
153+
);
154+
}
155+
compile::compile_all_pipelines().await?
156+
}
157+
},
148158
Commands::Check { source, pipeline } => {
149159
compile::check_pipeline(&source, &pipeline).await?;
150160
}

tests/compiler_tests.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,3 +1520,143 @@ fn test_compiled_output_has_header_comment() {
15201520

15211521
let _ = fs::remove_dir_all(&temp_dir);
15221522
}
1523+
1524+
/// Test that `compile` with no path argument auto-discovers and recompiles all pipelines
1525+
#[test]
1526+
fn test_compile_auto_discover_recompiles_detected_pipelines() {
1527+
let temp_dir = std::env::temp_dir().join(format!(
1528+
"agentic-pipeline-autodiscover-{}",
1529+
std::process::id()
1530+
));
1531+
let _ = fs::remove_dir_all(&temp_dir);
1532+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
1533+
1534+
// Create a source markdown file at agents/my-agent.md
1535+
let agents_dir = temp_dir.join("agents");
1536+
fs::create_dir_all(&agents_dir).expect("Failed to create agents directory");
1537+
1538+
let source_content = r#"---
1539+
name: "Auto Discover Agent"
1540+
description: "An agent for testing auto-discovery"
1541+
---
1542+
1543+
## Auto Discover Agent
1544+
1545+
This agent tests the auto-discovery feature.
1546+
"#;
1547+
fs::write(agents_dir.join("my-agent.md"), source_content)
1548+
.expect("Failed to write source markdown");
1549+
1550+
// Step 1: Compile the source file to create the initial YAML with header
1551+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
1552+
let output = std::process::Command::new(&binary_path)
1553+
.args([
1554+
"compile",
1555+
"agents/my-agent.md",
1556+
])
1557+
.current_dir(&temp_dir)
1558+
.output()
1559+
.expect("Failed to run initial compile");
1560+
1561+
assert!(
1562+
output.status.success(),
1563+
"Initial compile should succeed: {}",
1564+
String::from_utf8_lossy(&output.stderr)
1565+
);
1566+
1567+
// Verify the YAML was created with the header
1568+
let yaml_path = agents_dir.join("my-agent.yml");
1569+
assert!(yaml_path.exists(), "Compiled YAML should exist");
1570+
let initial_yaml = fs::read_to_string(&yaml_path).expect("Should read initial YAML");
1571+
assert!(
1572+
initial_yaml.contains("# @ado-aw"),
1573+
"Initial YAML should contain @ado-aw header"
1574+
);
1575+
1576+
// Step 2: Run compile with no arguments (auto-discover mode)
1577+
let output = std::process::Command::new(&binary_path)
1578+
.args(["compile"])
1579+
.current_dir(&temp_dir)
1580+
.output()
1581+
.expect("Failed to run auto-discover compile");
1582+
1583+
let stdout = String::from_utf8_lossy(&output.stdout);
1584+
let stderr = String::from_utf8_lossy(&output.stderr);
1585+
1586+
assert!(
1587+
output.status.success(),
1588+
"Auto-discover compile should succeed.\nstdout: {}\nstderr: {}",
1589+
stdout, stderr
1590+
);
1591+
1592+
assert!(
1593+
stdout.contains("Found 1 agentic pipeline"),
1594+
"Should report finding 1 pipeline, got stdout: {}",
1595+
stdout
1596+
);
1597+
1598+
assert!(
1599+
stdout.contains("1 compiled"),
1600+
"Should report 1 compiled, got stdout: {}",
1601+
stdout
1602+
);
1603+
1604+
// The YAML should still exist and be valid
1605+
let recompiled_yaml = fs::read_to_string(&yaml_path).expect("Should read recompiled YAML");
1606+
assert!(
1607+
recompiled_yaml.contains("# @ado-aw"),
1608+
"Recompiled YAML should still contain @ado-aw header"
1609+
);
1610+
assert!(
1611+
recompiled_yaml.contains("Auto Discover Agent"),
1612+
"Recompiled YAML should contain agent name"
1613+
);
1614+
1615+
let _ = fs::remove_dir_all(&temp_dir);
1616+
}
1617+
1618+
/// Test that auto-discover mode gracefully skips missing source files
1619+
#[test]
1620+
fn test_compile_auto_discover_skips_missing_source() {
1621+
let temp_dir = std::env::temp_dir().join(format!(
1622+
"agentic-pipeline-autodiscover-missing-{}",
1623+
std::process::id()
1624+
));
1625+
let _ = fs::remove_dir_all(&temp_dir);
1626+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
1627+
1628+
// Create a YAML file with a @ado-aw header pointing to a source that doesn't exist
1629+
let version = env!("CARGO_PKG_VERSION");
1630+
let fake_yaml = format!(
1631+
"# This file is auto-generated by ado-aw. Do not edit manually.\n\
1632+
# @ado-aw source=\"agents/nonexistent.md\" version={}\n\
1633+
name: fake-pipeline\n",
1634+
version
1635+
);
1636+
fs::write(temp_dir.join("orphaned.yml"), fake_yaml).expect("Failed to write fake YAML");
1637+
1638+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
1639+
let output = std::process::Command::new(&binary_path)
1640+
.args(["compile"])
1641+
.current_dir(&temp_dir)
1642+
.output()
1643+
.expect("Failed to run auto-discover compile");
1644+
1645+
let stdout = String::from_utf8_lossy(&output.stdout);
1646+
let stderr = String::from_utf8_lossy(&output.stderr);
1647+
1648+
// Should succeed (skips, doesn't fail)
1649+
assert!(
1650+
output.status.success(),
1651+
"Should succeed even with missing source.\nstdout: {}\nstderr: {}",
1652+
stdout, stderr
1653+
);
1654+
1655+
assert!(
1656+
stderr.contains("not found") || stdout.contains("skipped"),
1657+
"Should warn about missing source.\nstdout: {}\nstderr: {}",
1658+
stdout, stderr
1659+
);
1660+
1661+
let _ = fs::remove_dir_all(&temp_dir);
1662+
}

0 commit comments

Comments
 (0)