{"record":{"id":"2f8bf946bc86c144","repo":"zeroclaw-labs/zeroclaw","slug":"candidate-sop-has-no-parsed-steps","errorCode":null,"errorMessage":"candidate SOP has no parsed steps","messagePattern":"candidate SOP has no parsed steps","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":251,"sourceCode":"fn validate_candidate(sop_name: &str, manifest_toml: &str, procedure_markdown: &str) -> Result<()> {\n    let tmp = tempfile::tempdir()?;\n    let sop_dir = tmp.path().join(slugify(sop_name));\n    fs::create_dir_all(&sop_dir)?;\n    fs::write(sop_dir.join(\"SOP.toml\"), manifest_toml)?;\n    fs::write(sop_dir.join(\"SOP.md\"), procedure_markdown)?;\n    let sops = load_sops_from_directory(tmp.path(), super::parse_execution_mode(\"supervised\"));\n    if sops.len() != 1 {\n        bail!(\"candidate SOP did not validate as exactly one loadable SOP\");\n    }\n    if sops[0].name != sop_name {\n        bail!(\n            \"candidate manifest name '{}' does not match proposal target '{}'\",\n            sops[0].name,\n            sop_name\n        );\n    }\n    if sops[0].steps.is_empty() {\n        bail!(\"candidate SOP has no parsed steps\");\n    }\n    Ok(())\n}\n\nfn scan_candidate(manifest_toml: &str, procedure_markdown: &str) -> Option<String> {\n    let detector = LeakDetector::new();\n    let content = format!(\"{manifest_toml}\\n{procedure_markdown}\");\n    match detector.scan(&content) {\n        LeakResult::Clean => None,\n        LeakResult::Detected { patterns, .. } => Some(format!(\n            \"credential-like content detected: {}\",\n            patterns.join(\", \")\n        )),\n    }\n}\n\nfn read_or_default_manifest(sop: &Sop) -> Result<String> {\n    if let Some(location) = &sop.location {","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L233-L269","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet md = \"# Deploy check\\n\\nWe verify the deploy and report.\\n\";\n\n// after\nlet md = \"# Deploy check\\n\\n## Steps\\n\\n1. **Verify** - run `deploy --check`\\n2. **Report** - summarize the output\\n\";","handlingStrategy":"validation","validationCode":"// Cheap structural pre-check for parseable steps before proposing:\nfn has_numbered_steps(md: &str) -> bool {\n    let in_steps = md.split(\"## Steps\").nth(1).unwrap_or(\"\");\n    in_steps.lines().any(|l| l.trim_start().starts_with(\"1.\"))\n}","typeGuard":null,"tryCatchPattern":"Err(e) if e.to_string().contains(\"no parsed steps\") => {\n    // send SOP.md back for step formatting; keep the manifest untouched\n}","preventionTips":["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"],"tags":["sop","markdown","validation","procedural-memory"],"backgroundTag":"empty-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}