zeroclaw-labs/zeroclaw · error · anyhow::Error

{field} cannot be empty

Error message

{field} cannot be empty

What it means

create_proposal validates three required text fields with require_nonempty, which trims and rejects blank values in a fixed order: sop_name first, then procedure_markdown, then description. The bail message names the offending field. This is the earliest validation in the proposal pipeline - it fires before leak scanning and candidate round-tripping.

Source

Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:469

fn hash_sop_dir(dir: &Path) -> Result<String> {
    let mut hasher = Sha256::new();
    for name in ["SOP.toml", "SOP.md"] {
        let path = dir.join(name);
        hasher.update(name.as_bytes());
        hasher.update([0]);
        if path.exists() {
            hasher.update(fs::read(path)?);
        }
        hasher.update([0]);
    }
    Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
}

fn require_nonempty<'a>(field: &str, value: &'a str) -> Result<&'a str> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        bail!("{field} cannot be empty");
    }
    Ok(trimmed)
}

fn safe_component(value: &str) -> String {
    let out = value
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
                ch
            } else {
                '-'
            }
        })
        .collect::<String>();
    out.trim_matches('-').to_string()
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Populate all three fields with non-blank text before calling create_proposal; the reported field name tells you which one is blank
  2. If the error comes from capture_successful_run, fix the source SOP's blank description in its manifest/SOP.toml and reload the engine
  3. Trim inputs at the call site so accidental whitespace-only submissions are caught early

Example fix

// before
let draft = ProposalDraft {
    sop_name: "deploy-check".into(),
    description: "   ".into(),
    manifest_toml: None,
    procedure_markdown: md,
    source_run_id: None,
    requested_by: None,
};

// after
let draft = ProposalDraft {
    sop_name: "deploy-check".into(),
    description: "Verify the deploy finished".into(),
    manifest_toml: None,
    procedure_markdown: md,
    source_run_id: None,
    requested_by: None,
};
Defensive patterns

Strategy: validation

Validate before calling

fn draft_is_complete(d: &ProposalDraft) -> bool {
    [&d.sop_name, &d.procedure_markdown, &d.description]
        .iter()
        .all(|f| !f.trim().is_empty())
}

Try / catch

Err(e) if e.to_string().contains("cannot be empty") => {
    // the message names the field (sop_name | procedure_markdown | description):
    // collect the missing field, prompt for it, resubmit the draft
}

Prevention

When it happens

Trigger: Calling sop::procedural_memory::create_proposal with a draft whose sop_name, procedure_markdown, or description is empty or whitespace-only ("", " ", "\n"). The first blank field in the order sop_name -> procedure_markdown -> description is the one reported.

Common situations: UI/model flows that build ProposalDraft from optional form fields without defaults, or capture_successful_run operating on a SOP whose stored description is empty (the SOP loader allowed it but proposals do not).

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/f434a11e58d2282a. Report an issue: GitHub.