fix(compose): render resolved scope block — agent sees concrete paths, not generic text
Dogfood gap #2 from prepare workflow: capability text fragments say "your task's scope" generically, but agent never sees the resolved scope params from task.toml. Migration agent had no way to know whitelist = kei-chat-store/** without me pasting it. Fix: render_scope_block() injects a resolved-params section between capability fragments and task body. Shows: - files-whitelist / files-denylist glob lists - cargo-check-crates / cargo-test-crates - test-count-min (if Option<u32>::Some) - report-fields-required If no scope params set, block is empty (no section rendered). Now `kei-agent-runtime prepare` emits fully self-contained prompt — no external context needed. Substrate unblocked for parallel migrations. Tests: 41/41 (was 41 — no regression; scope-block added tests deferred to follow-up since compose_smoke already covers existing path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
68e37ecf68
commit
e075ae8df1
1 changed files with 72 additions and 1 deletions
|
|
@ -18,23 +18,94 @@ const SEPARATOR: &str = "\n\n---\n\n";
|
|||
|
||||
/// Compose prompt text. `kit_root` is the repo root that holds `_roles/`
|
||||
/// and `_capabilities/` directories.
|
||||
///
|
||||
/// Order: capability fragments → resolved scope block → task body.
|
||||
/// The scope block makes whitelist/denylist/verification params visible
|
||||
/// to the agent — capability text references "your task's scope" generically;
|
||||
/// without this block the agent has no way to know concrete paths.
|
||||
pub fn compose_prompt(task: &TaskSpec, kit_root: &Path) -> Result<String> {
|
||||
if task.task.role.is_empty() {
|
||||
return Err(anyhow!("task.role is empty"));
|
||||
}
|
||||
let resolved = resolve_role(kit_root, &task.task.role)?;
|
||||
let mut fragments: Vec<String> = Vec::with_capacity(resolved.required.len() + 1);
|
||||
let mut fragments: Vec<String> = Vec::with_capacity(resolved.required.len() + 2);
|
||||
for cap_name in &resolved.required {
|
||||
let frag = load_capability_text(kit_root, cap_name)
|
||||
.with_context(|| format!("capability {cap_name}"))?;
|
||||
fragments.push(frag);
|
||||
}
|
||||
let scope_block = render_scope_block(task);
|
||||
if !scope_block.is_empty() {
|
||||
fragments.push(scope_block);
|
||||
}
|
||||
if !task.body.text.trim().is_empty() {
|
||||
fragments.push(task.body.text.clone());
|
||||
}
|
||||
Ok(fragments.join(SEPARATOR))
|
||||
}
|
||||
|
||||
fn render_scope_block(task: &TaskSpec) -> String {
|
||||
let mut lines = vec!["## Your task's scope (resolved from task.toml)".to_string()];
|
||||
if !task.scope.files_whitelist.is_empty() {
|
||||
lines.push(String::new());
|
||||
lines.push("**files-whitelist** (you MAY Edit/Write these):".to_string());
|
||||
for p in &task.scope.files_whitelist {
|
||||
lines.push(format!("- `{p}`"));
|
||||
}
|
||||
}
|
||||
if !task.scope.files_denylist.is_empty() {
|
||||
lines.push(String::new());
|
||||
lines.push("**files-denylist** (you MUST NOT Edit/Write these):".to_string());
|
||||
for p in &task.scope.files_denylist {
|
||||
lines.push(format!("- `{p}`"));
|
||||
}
|
||||
}
|
||||
if !task.verification.cargo_check_crates.is_empty() {
|
||||
lines.push(String::new());
|
||||
lines.push(format!(
|
||||
"**cargo check MUST pass** for: {}",
|
||||
task.verification
|
||||
.cargo_check_crates
|
||||
.iter()
|
||||
.map(|c| format!("`{c}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
if !task.verification.cargo_test_crates.is_empty() {
|
||||
lines.push(format!(
|
||||
"**cargo test MUST pass** for: {}",
|
||||
task.verification
|
||||
.cargo_test_crates
|
||||
.iter()
|
||||
.map(|c| format!("`{c}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
if let Some(n) = task.verification.test_count_min {
|
||||
if n > 0 {
|
||||
lines.push(format!("**minimum test count:** {n}"));
|
||||
}
|
||||
}
|
||||
if !task.output.report_fields_required.is_empty() {
|
||||
lines.push(String::new());
|
||||
lines.push(format!(
|
||||
"**report MUST include fields:** {}",
|
||||
task.output
|
||||
.report_fields_required
|
||||
.iter()
|
||||
.map(|f| format!("`{f}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
if lines.len() == 1 {
|
||||
return String::new();
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn load_capability_text(kit_root: &Path, cap_name: &str) -> Result<String> {
|
||||
let (category, slug) = split_cap_name(cap_name)?;
|
||||
let path = kit_root
|
||||
|
|
|
|||
Loading…
Reference in a new issue