zeroclaw-labs/zeroclaw · error · anyhow::Error
candidate SOP has no parsed steps
Error message
candidate SOP has no parsed steps
What it means
validate_candidate requires the round-trip-loaded candidate SOP to contain at least one parsed step. The loader parsed the SOP.md (name matched, manifest was fine) but produced an empty steps list, meaning the procedure markdown contains no steps in a format the loader recognizes. Proposals without executable steps are rejected because an applied SOP would be a no-op.
Source
Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:251
fn validate_candidate(sop_name: &str, manifest_toml: &str, procedure_markdown: &str) -> Result<()> {
let tmp = tempfile::tempdir()?;
let sop_dir = tmp.path().join(slugify(sop_name));
fs::create_dir_all(&sop_dir)?;
fs::write(sop_dir.join("SOP.toml"), manifest_toml)?;
fs::write(sop_dir.join("SOP.md"), procedure_markdown)?;
let sops = load_sops_from_directory(tmp.path(), super::parse_execution_mode("supervised"));
if sops.len() != 1 {
bail!("candidate SOP did not validate as exactly one loadable SOP");
}
if sops[0].name != sop_name {
bail!(
"candidate manifest name '{}' does not match proposal target '{}'",
sops[0].name,
sop_name
);
}
if sops[0].steps.is_empty() {
bail!("candidate SOP has no parsed steps");
}
Ok(())
}
fn scan_candidate(manifest_toml: &str, procedure_markdown: &str) -> Option<String> {
let detector = LeakDetector::new();
let content = format!("{manifest_toml}\n{procedure_markdown}");
match detector.scan(&content) {
LeakResult::Clean => None,
LeakResult::Detected { patterns, .. } => Some(format!(
"credential-like content detected: {}",
patterns.join(", ")
)),
}
}
fn read_or_default_manifest(sop: &Sop) -> Result<String> {
if let Some(location) = &sop.location {View on GitHub (pinned to 88bb9c8533)
Solutions
- Rewrite SOP.md with a '## Steps' section containing numbered list items in the loader's format: 1. **Title** - body (see default_procedure_markdown for the exact shape)
- Verify the fix by round-trip loading the candidate in a temp dir and asserting steps is non-empty before re-submitting the proposal
- For captured runs, check the source SOP's SOP.md on disk - fix it there, then re-run capture_successful_run
Example fix
// before let md = "# Deploy check\n\nWe verify the deploy and report.\n"; // after let md = "# Deploy check\n\n## Steps\n\n1. **Verify** - run `deploy --check`\n2. **Report** - summarize the output\n";
Defensive patterns
Strategy: validation
Validate before calling
// Cheap structural pre-check for parseable steps before proposing:
fn has_numbered_steps(md: &str) -> bool {
let in_steps = md.split("## Steps").nth(1).unwrap_or("");
in_steps.lines().any(|l| l.trim_start().starts_with("1."))
} Try / catch
Err(e) if e.to_string().contains("no parsed steps") => {
// send SOP.md back for step formatting; keep the manifest untouched
} Prevention
- Author SOP.md with default_procedure_markdown's format: '## Steps' + numbered '**Title** - body' items
- After any SOP.md edit, round-trip load and assert steps.len() > 0
- Never strip the numbered list when reformatting documentation
When it happens
Trigger: create_proposal or apply_proposal with a procedure_markdown that has no numbered steps under the expected '## Steps' section - only prose, headings, or bullet points the parser ignores. Via capture_successful_run: the source SOP's SOP.md lost its numbered steps (e.g. someone replaced it with narrative docs), so append_captured_notes inherits a step-less document.
Common situations: Writing SOP.md as narrative documentation instead of the step format produced by default_procedure_markdown ('## Steps' followed by '1. **Title** - body' items), or a markdown reformatting tool renumbering/reformatting steps out of the recognizable shape.
Related errors
- candidate SOP did not validate as exactly one loadable SOP
- candidate manifest name '{}' does not match proposal target
- SOP name does not contain a safe path component
- {field} cannot be empty
- 'since' must be before 'until'
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/2f8bf946bc86c144.
Report an issue: GitHub.