use anyhow::{anyhow, bail, Context, Result};
use colored::Colorize;
use crate::ailog;
use crate::charter;
use crate::prompts;
use crate::utils;
pub fn run(
path: &str,
charter_id: &str,
batch_number: u32,
note: Option<&str>,
non_interactive: bool,
) -> Result<()> {
let resolved = utils::resolve_project_root(path)
.ok_or_else(|| anyhow!("StrayMark not installed. Run 'straymark init' first."))?;
let project_root = &resolved.path;
let (charters, _errors) = charter::discover_and_parse(project_root);
let charter = charter::find_by_id(&charters, charter_id)
.ok_or_else(|| {
anyhow!(
"Charter {} not found in .straymark/charters/.\n hint: run `straymark charter list` to see discovered Charters.",
charter_id
)
})?
.clone();
let ailog_ids = match &charter.frontmatter.originating_ailogs {
Some(ids) if !ids.is_empty() => ids.clone(),
_ => bail!(
"Charter {} has no `originating_ailogs` in frontmatter.\n hint: batch-complete writes to the originating AILOG; add it to the Charter or run the command directly on the AILOG file.",
charter.frontmatter.charter_id
),
};
let ailog_id = &ailog_ids[0];
if ailog_ids.len() > 1 {
eprintln!(
"{} Charter has {} originating AILOGs; using the first one ({}).",
"note:".cyan().bold(),
ailog_ids.len(),
ailog_id
);
}
let agent_logs_dir = ailog::agent_logs_dir(project_root);
if !agent_logs_dir.exists() {
bail!(
"AILOG directory not found at {}. Run `straymark repair` to restore framework files.",
agent_logs_dir.display()
);
}
let ailog_path = ailog::find_ailog_file(&agent_logs_dir, ailog_id).ok_or_else(|| {
anyhow!(
"AILOG file for {} not found under {}.\n hint: the file must be named `{}-<slug>.md`.",
ailog_id,
agent_logs_dir.display(),
ailog_id
)
})?;
let ailog_path_rel = ailog_path
.strip_prefix(project_root)
.unwrap_or(&ailog_path)
.to_path_buf();
let pre_content = std::fs::read_to_string(&ailog_path)
.with_context(|| format!("Failed to read AILOG at {}", ailog_path.display()))?;
let entries = ailog::parse_batch_ledger(&pre_content).ok_or_else(|| {
anyhow!(
"AILOG at {} has no `## Batch Ledger` section.\n hint: add the section to the AILOG (see TEMPLATE-AILOG.md) before running batch-complete.\n This Charter does not need the ledger if it is a single-batch effort — `## Actions Performed` is sufficient.",
ailog_path_rel.display()
)
})?;
let target = entries
.iter()
.find(|e| e.n == batch_number)
.ok_or_else(|| {
anyhow!(
"No `### Batch {}` heading in AILOG {}.\n hint: add `### Batch {} — <name>` followed by `(pending)` to the `## Batch Ledger` section.\n Existing batches: {}",
batch_number,
ailog_path_rel.display(),
batch_number,
if entries.is_empty() {
"(none)".to_string()
} else {
entries.iter().map(|e| format!("Batch {}", e.n)).collect::<Vec<_>>().join(", ")
}
)
})?
.clone();
ailog::ensure_pending(&target)?;
println!(
"{} AILOG: {}",
"✔".green().bold(),
ailog_path_rel.display().to_string().dimmed()
);
println!(
"{} {}",
"✔".green().bold(),
target.heading_line.dimmed()
);
let body = if let Some(n) = note {
n.to_string()
} else if non_interactive {
bail!("--non-interactive requires --note");
} else {
prompts::require_interactive()?;
collect_interactively()?
};
if body.trim().is_empty() {
bail!(
"Refusing to write an empty batch body — that would leave the batch indistinguishable from `(pending)`. Pass `--note \"...\"` or fill the prompts."
);
}
if body.trim() == "(pending)" {
bail!(
"Refusing to write a `(pending)` body verbatim — that defeats the gate. Either supply real content or skip the command for this batch."
);
}
let _prev = ailog::write_batch_section(&ailog_path, batch_number, &body)?;
println!();
println!(
"{} `### Batch {}` written.",
"OK".green().bold(),
batch_number
);
println!(
"{} `git add {}` before pushing the batch commit so the AILOG ledger update lands atomically with the work it documents.",
"Reminder:".yellow().bold(),
ailog_path_rel.display()
);
Ok(())
}
fn collect_interactively() -> Result<String> {
println!();
println!("{}", "Collecting batch notes — press Enter on blank line to skip a section.".dimmed());
println!();
let files = prompts::prompt_string(
"Files touched (one-line summary, e.g. `migrations/022.sql, handlers/x.go`)",
None,
true,
)?;
let tests = prompts::prompt_string(
"Tests added or status (e.g. `handler_x_test.go +12 -0, passing`)",
None,
true,
)?;
println!(
"{}",
"Design note / lessons (multiline; finish with `.` on a line by itself, or Ctrl-D):"
.dimmed()
);
let design = prompts::prompt_multiline("note")?;
let mut body = String::new();
let mut wrote = false;
if !files.trim().is_empty() {
body.push_str(&format!("- **Files**: {}", files.trim()));
wrote = true;
}
if !tests.trim().is_empty() {
if wrote {
body.push('\n');
}
body.push_str(&format!("- **Tests**: {}", tests.trim()));
wrote = true;
}
if !design.trim().is_empty() {
if wrote {
body.push_str("\n\n");
}
body.push_str(design.trim());
}
Ok(body)
}