Agent skill

Error Message Manager

by CliMA in CliMA/EnsembleKalmanProcesses.jl

Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics.

Apache-2.0Auto-check passed

Install Error Message Manager

skills CLI
$ npx skills add CliMA/EnsembleKalmanProcesses.jl --skill error-message-manager -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install CliMA/EnsembleKalmanProcesses.jl error-message-manager --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/CliMA/EnsembleKalmanProcesses.jl.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/error-message-manager .claude/skills/error-message-manager && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
error-message-manager
GitHub stars
127
Token cost
~8.2k tokens
SKILL.md length
2,965 words
Files
1
Skills in repo
5
Repo updated
First seen
Licence
Apache-2.0

At a glance

Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics.

  • Works in 10 steps: Offer an Explore agent for multi-file… → Audit the target scope → Classify each site → …
  • Mentions: error message
  • SKILL.md covers Workflow, Style rules, Canonical before/after examples and Non-goals
  • Calls rg

What it does

Error Message Manager is an agent skill from CliMA/EnsembleKalmanProcesses.jl. Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics. Invoke this skill whenever the user mentions: error message, improve errors, rewrite @assert, ArgumentError, DimensionMismatch, DomainError, vague error, error rewrite, Julia exception, diagnostic, throw, validation, early check, assert to throw, loop context, catch and rethrow, warn string, or asks to improve how the code fails. Also use it when reviewing code for user-facing clarity, when a user says errors are…

Its SKILL.md is about 8.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

The repository describes itself as: Derivative-free parameter calibration and uncertainty quantification for expensive models using ensemble Kalman methods. The licence is Apache-2.0.

When your agent uses it

  • Mentions: error message
  • Rewrite @assert
  • DimensionMismatch
  • Julia exception

Example prompts

  • “/error-message-manager”

Workflow steps

10 steps, taken from the step headings in SKILL.md.

  1. Offer an Explore agent for multi-file scope
  2. Audit the target scope
  3. Classify each site
  4. 5 — Decide: inline or helper?
  5. Rewrite with the canonical layout
  6. Move validation early
  7. Preserve domain language
  8. Apply rewrites
  9. Add @test_throws tests
  10. Offer to improve the skill

What it can do on your machine

Read from SKILL.md and the folder at commit d10e521. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • rg

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Error Message Manager loads about 8.2k tokens when it runs. Until then it costs about 211 tokens; SKILL.md has 2,965 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~211
When it runs · the whole SKILL.md, loaded when a task matches
~8.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from CliMA/EnsembleKalmanProcesses.jl at commit d10e521, republished under its Apache-2.0 licence (© CliMA). 2,965 words, ~8,151 tokens.

Download SKILL.mdSave it as .claude/skills/error-message-manager/SKILL.md (or your agent's skills folder).
name
error-message-manager
description
Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics. Invoke this skill whenever the user mentions: error message, improve errors, rewrite @assert, ArgumentError, DimensionMismatch, DomainError, vague error, error rewrite, Julia exception, diagnostic, throw, validation, early check, assert to throw, loop context, catch and rethrow, warn string, or asks to improve how the code fails. Also use it when reviewing code for user-facing clarity, when a user says errors are confusing or unhelpful, or when auditing a module for low-context exceptions. Use it proactively when you see bare @assert, error("..."), throw(ErrorException(...)), @warn string(...), or catch blocks that do not include the original exception in their re-throw in Julia code you are reading or editing.

error-message-manager

Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics. The goal is errors that tell the user exactly what went wrong, what was expected, what was received, and—whenever a likely fix exists— what to do next. Prefer catching mistakes early (at API boundaries) over letting them propagate into cryptic numerical failures.

Workflow

Step 0 — Offer an Explore agent for multi-file scope

If the user's request covers more than one file — a whole directory, a module, or the entire repo — offer to spawn an Explore agent before doing any file reads yourself. The agent runs all the reads in parallel without flooding the main context, and returns a structured inventory you can act on directly.

When to offer: any time the target is a directory path (e.g. src/) or a vague scope like "the whole package" or "all the source files".

Offer text (adapt as needed):

"This spans multiple files — I'd recommend spawning an Explore agent to survey all throw/@assert/error sites in parallel. It keeps the audit fast and leaves the main context clean for the actual rewrites. Want me to do that?"

Agent prompt to use (fill in <path> and <package_name>):

Audit `<path>` for error-raising patterns. For every `@assert`, `error(`, or
`throw(` site in every `.jl` file:

1. Record: file, line number, exception type (or "bare @assert" / "bare error"),
   and the full message text (including multiline strings).
2. Classify message quality:
   - "good"  — has `$(expr)` interpolation showing the actual received value, and
     is either short (≤3 lines) or already in a `_throw_` helper function
   - "long-inline" — message content is good, but the body exceeds 3 lines and
     the throw is written inline (not in a `_throw_` helper)
   - "vague" — missing a received value, or no Expected/Got structure
   - "missing" — bare `@assert` with no message at all
3. Note whether the site is at an API boundary (user-facing input) or an internal
   invariant (would require a package bug to fire).

Return a markdown table with columns:
  File | Line | Exception type | Quality | Notes (one-line note on what's wrong if vague/missing/long-inline)

Focus only on sites that are "vague", "missing", or "long-inline" — skip "good" ones.

How to use the result: treat the returned table as your working inventory for Steps 1 and 2. You do not need to re-read the flagged files yourself to classify — go straight to reading only the lines that need rewrites (Step 3 onwards).


Step 1 — Audit the target scope

Identify which files or functions to address. If the user named a specific function, start there. If the request is repo-wide, run:

rg -n '(@assert[^(]|@assert\(|error\(string\(|throw\(ErrorException)' src/

Then collect all message-less @assert calls:

rg -n '@assert' src/ | grep -v '"'

Also flag @warn calls that use string concatenation instead of interpolation:

rg -n '@warn\s+string\(' src/

And flag catch blocks that discard the original exception when re-throwing:

rg -n 'catch\s' src/

For each catch hit, check whether the subsequent throw or error call interpolates the caught variable (e.g. $e or sprint(showerror, e)). If it does not, the original exception type and message are silently lost.

For each hit, record: file, line, the condition being checked, and whether it guards user-provided input (API boundary) or an internal invariant.

Step 2 — Classify each site

Use this table to choose the right exception type:

ConditionException
Invalid user-provided argumentArgumentError
Array/matrix shape mismatchDimensionMismatch
Inconsistent argument types across parametersArgumentError
Mathematically invalid value (negative variance, etc.)DomainError
Invalid indexBoundsError
Internal invariant that should never fireerror(...)
Missing interface implementationMethodError or structured ArgumentError

Avoid ErrorException unless there is no better choice.

Type mismatches vs dimension mismatches: an @assert isa(x_mean, AbstractVector{FT}) inside an if isa(x, AbstractMatrix{FT}) branch is checking that the user supplied consistent arguments (matrix ensemble → vector mean), not that two arrays have matching sizes. Use ArgumentError, not DimensionMismatch, for this pattern.

Distinguish API boundary sites (where the user passed something wrong — prefer typed exceptions with actionable messages) from internal invariant sites (where a bug in the package itself would have to exist — bare error(...) with a clear note is fine there).

Loop-body errors: if the throw is inside a for or while loop, treat the loop index and key per-iteration state as required context. Without this, the user sees "matrix is not positive definite" with no idea whether it happened on iteration 2 or iteration 200. Always capture i (or the loop variable) and the state that changed between iterations — the ensemble step count, the parameter vector being updated, the ensemble member index, etc. For nested loops, include both the outer and inner loop variables — the outer variable says which group or batch failed; the inner variable says which element within it failed. See the loop-context example in the Canonical examples section below.

catch e losing the original exception: when a Julia exception is caught and a new one is thrown, the new message must include the original exception. If it does not, the user loses the root cause (e.g. PosDefException, SingularException) and has no way to distinguish a code bug from a numerical issue. Use sprint(showerror, e) rather than $e alone — it formats the exception type and message together:

julia
# anti-pattern — root cause vanishes
catch e
    throw(ArgumentError("Matrix factorization failed."))
end

# correct — root cause preserved
catch e
    throw(ArgumentError("""
Matrix factorization failed.

Caused by: $(sprint(showerror, e))

Suggestion:
    ...
"""))
end

Only suppress the original exception if it is a well-known internal Julia error (e.g. SingularException) and you are intentionally providing a higher-level fallback — and even then, log it at @debug level.

@warn string(...) concatenation: @warn string("...", x, "...") is the warning-side equivalent of error(string(...)) — it's noisy, hard to read, and doesn't benefit from Julia's interpolation. Rewrite as @warn "... $x ...". @warn messages that use structured strings are also easier to grep and suppress selectively.

Double-gated invariants: if a helper is only ever called after the public API has already checked the same condition (e.g., get_vector_of_parameterized is called from construct_prior only when d.args[1] == Symbol("VectorOfParameterized") is true), the check inside the helper is an internal invariant even though it looks like a user-data check. Use a single-line error(...) rather than a full structured ArgumentError:

julia
# internal invariant — the caller already validated this
d.args[1] == Symbol("VectorOfParameterized") || error(
    "Internal error: get_vector_of_parameterized called with non-VectorOfParameterized expression (got $(d.args[1]))",
)
Step 2.5 — Decide: inline or helper?

Before writing the rewrite, decide whether the error belongs inline or should be extracted into a _throw_<what>(...) helper function.

When to extract — pull the error into a helper when either condition holds:

  • Length (primary trigger): the message body exceeds 3 lines. Extract unconditionally — single call site, non-loop context, no surrounding complexity required. A full Expected / Got / Suggestion block always crosses this threshold. Even a one-off long block left inline establishes a pattern that makes entire files hard to scan, and accumulates quickly once a few exceptions are made.
  • Duplication: the same error shape (same summary line, same Expected / Got / Suggestion skeleton) appears at ≥2 call sites. Extract even when each block is short — the wording drifts silently over time and the call sites collapse to readable one-liners.

Inline is appropriate only for genuinely short messages (≤3 lines) at a single call site. A single summary line, or a summary plus one Got line, is the ceiling for inline. When in doubt, count — if it doesn't fit in 3 lines, extract.

Where helpers go

Default: a ## Error helpers section at the bottom of the source file, above end # module. Keeping helpers near their callers preserves traceability — the reader sees the throw site, jumps to the bottom of the same file, and finds the message without switching files.

Promote to a shared src/ErrorMessages.jl (or the repo's equivalent top-level utility file) only when ≥2 different source files call the same helper. Discover which file to use by reading the top-level module file (e.g. src/PackageName.jl) for its include(...) list — then add include("ErrorMessages.jl") as the first include so every subsequent file sees the helpers without any using/import.

Naming convention

_throw_<what>(positional_required_facts...; kwargs_for_optional_context...)
  • Underscore prefix → unexported private helper.
  • Verb prefix _throw_ → the function unconditionally raises; callers know there is no return value.
  • Suffix describes the failure mode: _dim_mismatch, _missing_keys, _bad_obs_type, _not_iterable.

Signature convention

Pass the facts that are always present as positional arguments (the offending value, the expected vs got summary). Pass optional context as keyword arguments with nothing defaults — especially loop context (index, total, iter, phase). Build optional sections inside the helper by checking isnothing(...). This keeps call sites compact and lets the same helper serve both loop and non-loop contexts (see the Helper with optional loop context canonical example).

Performance: use @noinline

Prefix every helper with @noinline. This prevents Julia from inlining the cold error path into the surrounding hot code, keeping numerical kernels unaffected:

julia
@noinline function _throw_x_not_iterable(x; where::Symbol)
    throw(ArgumentError(...))
end

What NOT to do

  • Don't create a catch-all _throw_arg_error(msg::String) — that just shifts the inline triple-quoted block to another file without any DRY benefit.
  • Don't use macros (@check_dim(...)) — they're magical and harder to debug than plain functions.
  • Don't bundle all context into one opaque context::NamedTuple — explicit kwargs are clearer to call and easier to extend.
Step 3 — Rewrite with the canonical layout

Use this structure for every user-facing exception:

julia
throw(ArgumentError("""
Short one-line summary of the failure.

Expected:
    <what would have been valid>

Got:
    <what was actually received, with interpolated values>

Loop context:
    iteration  = $iter (of $n_iter)
    <key per-iteration state variable> = $(summary_of_state)

Context:
    <surrounding state that helps locate the problem>

Suggestion:
    <most likely fix>
"""))

Section rules:

  • Summary: always present; one line; imperative or declarative.
  • Expected / Got: strongly preferred for any mismatch check; use $(expr) interpolation to show actual values.
  • Loop context: include whenever the throw is inside a for or while loop. Always report the loop index and the key state that varies between iterations (e.g., the EKI step number, the ensemble member index, or the parameter being updated). This is what lets the user reproduce the failure without adding println debugging. Omit for errors that can only fire at a fixed point in the code (before the loop starts or after it ends).
  • Context: include when the same error can arise from multiple call sites and naming the calling function or struct helps the user orient.
  • Suggestion: include whenever a likely fix exists. Omit rather than write a generic platitude.
  • Never dump full matrices or large arrays. Prefer size(x), eltype(x), typeof(x), or a scalar summary statistic.
Step 4 — Move validation early

If the current code lets an invalid input reach a numerical routine before failing (e.g., cholesky on a non-symmetric matrix, inv on a singular one), add an explicit guard at the API boundary:

julia
# Before: error surfaces deep in cholesky
cov_chol = cholesky(C)

# After: check at the boundary, raise immediately
issymmetric(C) || throw(ArgumentError("""
Covariance matrix must be symmetric.

Got:
    size(C) = $(size(C))
    norm(C - C') = $(norm(C - C'))

Suggestion:
    Pass a symmetric matrix, e.g. `C = (C + C') / 2`.
"""))
cov_chol = cholesky(C)

Use || for single-condition guards. For multi-condition guards, use if/throw.

When using || with a multiline triple-quoted throw, the closing )) goes on its own line immediately after the closing """:

julia
condition || throw(ArgumentError("""
Summary line.

Expected:
    ...

Got:
    ...
"""))   # ← closing )) on the line right after the closing """

This is the only layout that keeps indentation correct — triple-quoted strings in Julia do not strip leading whitespace, so indenting the message body would include those spaces in the string.

Step 5 — Preserve domain language

Write messages in terms the user understands, not in terms of internal Julia dispatch or linear algebra internals. For example:

  • Say "ensemble member count" not "size(x, 2)"
  • Say "parameter covariance matrix" not "the second argument to cholesky"
  • Say "observation noise covariance" not "Γ"
Step 6 — Apply rewrites

Edit each site, keeping the surrounding code untouched. Confirm the package still loads:

julia --project -e 'using EnsembleKalmanProcesses'
Step 7 — Add @test_throws tests

Before writing any test, check whether coverage already exists. Grep the matching test/<module>/runtests.jl for the public API function that reaches the rewritten site:

bash
grep -n '@test_throws' test/<module>/runtests.jl | grep '<function_name>'

Three outcomes:

SituationAction
@test_throws <correct_type> already presentSkip — do not add a duplicate
@test_throws <wrong_type> already presentUpdate the existing line to the new type
No coverage at allAdd a new test

For every site that needs a new test, add it in the matching test/<module>/runtests.jl:

julia
@test_throws ArgumentError multiplicative_inflation!(ekp; s = 2.0)

Use the specific exception type — never bare @test_throws Exception. The test should construct the minimal invalid input that triggers the new error, without duplicating happy-path coverage.

Update existing tests that used the wrong type. If the file already has a @test_throws ErrorException (or any other type) for a site you're rewriting to ArgumentError, update that existing test in the same edit. Leaving a stale @test_throws ErrorException will cause it to pass against the old code but fail once your rewrite lands — or vice versa.

Testing unexported helpers. If the site is inside an unexported helper (e.g., construct_constraint, construct_2d_array), do not import the internal directly. Instead, test through the nearest exported public API function that calls it, using invalid input that propagates to the helper:

julia
# construct_constraint is unexported — test via get_parameter_distribution
no_constraint_dict = Dict("uq_param" => Dict("prior" => "Parameterized(Normal(0.0, 1.0))"))
@test_throws ArgumentError get_parameter_distribution(no_constraint_dict, "uq_param")

This keeps tests coupled to the public contract and avoids brittleness when internal function names change.

Show full SKILL.md (1,162 more words)Show less
Step 8 — Offer to improve the skill

Once the rewrites and tests are clean, offer: "Would you like to improve the error-message-manager skill itself using skill-creator? You can share suggestions, or I can analyse patterns from this session—recurring edge cases, exception-type decisions, or anything that felt awkward—to refine the skill for next time."


Style rules

  • Triple-quoted strings for all multiline messages.
  • No full matrix dumps. Use size(x), eltype(x), norm(x - ...), or extrema(x) instead.
  • Interpolate actual values in Got sections so the user sees the numbers, not just variable names. For String-typed arguments use $(repr(x)) rather than $(x) — it adds the surrounding quotes so the output clearly reads as a string value (e.g. Got: sigma_points = "bad" instead of Got: sigma_points = bad).
  • Raise early: prefer guarding at the function entry point over deep inside a helper.
  • No @assert for user-facing validation. @assert is a debugging tool; it can be compiled out. Use explicit throw instead.
  • Loop state in Got / Loop context: when a throw is inside a for or while loop, always name the iteration index and the key state from that iteration. A "convergence failed" message without the step count forces the user to add println debugging to reproduce the failure. When the same loop-context check recurs at multiple sites, the loop variables become optional kwargs on a _throw_<what> helper — see the Helper with optional loop context canonical example.
  • Preserve the original exception in catch blocks: if you catch e and throw a new exception, include $(sprint(showerror, e)) in the new message. Dropping e silently discards the root cause.
  • @warn with interpolation, not string(): replace @warn string("x=", x) with @warn "x = $x". String concatenation in warnings is harder to read and grep.
  • Single-line messages are fine when the failure is unambiguous and no Expected/Got context would add clarity.
  • Extract into _throw_<what>(...) helpers whenever the message body exceeds 3 lines, or when the same Expected / Got / Suggestion skeleton appears at ≥2 call sites (even if short). A full Expected / Got / Suggestion block always exceeds 3 lines and must be a helper — inline is only appropriate for ≤3-line messages at a single call site. Place the helper in a ## Error helpers section at the bottom of the source file; promote to a shared src/ErrorMessages.jl only when ≥2 different source files share the helper. Use @noinline, positional args for required facts, and nothing-defaulted kwargs for optional context such as loop indices. Render each optional section only when its kwarg is non-nothing.

Canonical before/after examples

Length rule applies to all examples below. Each example shows the canonical message format (Expected / Got / Suggestion sections, interpolation, etc.). When the message body exceeds 3 lines — which any structured block with Expected / Got / Suggestion sections does — the throw must go in a _throw_<what>(...) helper per Step 2.5, not inline. The first example below models this explicitly. Subsequent examples show the message body format; apply the same helper extraction whenever the resulting message exceeds 3 lines.

Replace a vague error(string(...))

The after-message has 10 lines (well above the 3-line threshold), so it goes into a _throw_ helper — extract unconditionally at this length even though there is only one call site.

julia
# Before
if scaled_Δt >= 1.0
    error(string("Scaled time step: ", scaled_Δt, " is >= 1.0", "\nChange s or EK time step."))
end

# After — helper in the ## Error helpers section at the bottom of the file
@noinline function _throw_scaled_step_too_large(s, Δt, scaled_Δt)
    throw(ArgumentError("""
Scaled time step exceeds the stability bound.

Expected:
    s * Δt < 1.0

Got:
    s = $s
    Δt = $Δt
    s * Δt = $scaled_Δt

Suggestion:
    Reduce the scaling factor `s` or shorten the EK time step.
"""))
end

# Call site collapses to a single guard line:
scaled_Δt < 1.0 || _throw_scaled_step_too_large(s, get_Δt(ekp)[end], scaled_Δt)
Replace a bare @assert on an API boundary
julia
# Before
@assert(haskey(param_info, "constraint"))

# After
haskey(param_info, "constraint") || throw(ArgumentError("""
Parameter info dict is missing the required "constraint" key.

Got keys:
    $(collect(keys(param_info)))

Suggestion:
    Ensure the TOML entry for this parameter includes a `constraint = ...` field.
"""))
Replace a single-line string-value error (use repr)
julia
# Before
throw(ArgumentError("sigma_points type is not recognized. Select from \"symmetric\" or \"simplex\". "))

# After
throw(ArgumentError("""
Unrecognized sigma_points type.

Expected:
    "symmetric" or "simplex"

Got:
    sigma_points = $(repr(sigma_points))
"""))

Using repr(sigma_points) rather than $(sigma_points) keeps the string quotes visible in the output, making it unambiguous that the user passed a String value (and making copy-paste errors easy to spot).

Replace a dimension-mismatch @assert
julia
# Before
@assert size(x, 2) == length(mean_weights)

# After
size(x, 2) == length(mean_weights) || throw(DimensionMismatch("""
Ensemble size does not match the number of quadrature weights.

Expected:
    size(x, 2) == length(mean_weights)

Got:
    size(x, 2) = $(size(x, 2))
    length(mean_weights) = $(length(mean_weights))
"""))
Preserve the original exception when catching and re-throwing
julia
# Before — PosDefException or SingularException silently discarded
try
    cov_chol = cholesky(cov_u)
catch e
    error("Covariance matrix factorization failed.")
end

# After — root cause preserved, matrix state shown
try
    cov_chol = cholesky(cov_u)
catch e
    throw(ArgumentError("""
Covariance matrix factorization failed during empirical Gaussian sampling.

Got:
    size(cov_u)    = $(size(cov_u))
    isposdef(cov_u) = $(isposdef(cov_u))

Caused by: $(sprint(showerror, e))

Suggestion:
    The ensemble may have collapsed. Pass a non-zero `inflation` keyword
    argument to regularise the sample covariance.
"""))
end

sprint(showerror, e) formats as "LinearAlgebra.PosDefException: matrix is not Hermitian; Cholesky factorization failed." — far more informative than string(e). Only suppress e when you are intentionally providing a higher-level fallback (e.g. falling back to pinv) and still emit it at @debug level.

Rewrite @warn string(...) to use interpolation
julia
# Before
@warn string("Sample covariance matrix over ensemble is singular.", "\n Applying variance inflation.")

# After
@warn "Sample covariance matrix over ensemble is singular — applying variance inflation."

# Before (with values)
@warn string("More than 50% of runs produced NaNs ($(length(failed_ens))/$(size(g, 2))).", "\nIterating...")

# After
@warn "More than 50% of forward model evaluations produced NaN ($(length(failed_ens))/$(size(g, 2))). Iterating, but consider improving model stability."
Add loop context to an error thrown inside an iteration loop
julia
# Before — user sees "Cholesky factorization failed" with no idea when
for i in 1:N_iter
    try
        cov_chol = cholesky(C_i)
    catch e
        error("Cholesky factorization failed")
    end
end

# After — guard before cholesky, expose iteration index and diagnostic state
for i in 1:N_iter
    isposdef(C_i) || throw(ArgumentError("""
Covariance matrix is not positive definite at EKI iteration $i.

Expected:
    A positive-definite covariance matrix at every iteration.

Got:
    iteration       = $i / $N_iter
    size(C_i)       = $(size(C_i))
    minimum eigval  = $(minimum(eigvals(Symmetric(C_i))))

Suggestion:
    Ensemble collapse can cause this near iteration $i. Consider adding
    covariance inflation (`multiplicative_inflation!`) or reducing the step size.
"""))
    cov_chol = cholesky(C_i)
end

Key points:

  • Move the guard before the failing call so the message fires with the full iteration state still in scope. Catching a PosDefException after the fact and re-throwing loses the iteration index and the matrix state.
  • Report the loop variable (i, n, iter) and its upper bound so the user knows whether the failure is early (step 2/200, likely a bad initial state) or late (step 198/200, likely ensemble collapse).
  • Include one diagnostic scalar — the minimum eigenvalue, the norm of the update step, the ensemble spread — rather than dumping the full matrix.

When this same loop-context error needs to be thrown at multiple sites, the loop variables (i, N_iter) naturally become optional kwargs on a _throw_<what> helper. The call site stays a single line and the loop-awareness travels with the helper everywhere it is used — see the Helper with optional loop context example below.

Extract a duplicated error into a helper

transform_constrained_to_unconstrained and transform_unconstrained_to_constrained in src/ParameterDistributions.jl each validate the same two preconditions on their iterable argument x. Before extraction, byte-for-byte identical 12-line blocks appear at both call sites:

julia
# Before — same two blocks in both transform functions (×2 each = 4 copies total)

# in transform_constrained_to_unconstrained:
if !hasmethod(iterate, [typeof(x)])
    throw(ArgumentError("""
transform_constrained_to_unconstrained: `x` is not iterable.

Expected:
    AbstractVecOrMat or an iterable of AbstractVecOrMat elements (one per EK iteration)

Got:
    $(typeof(x))

Suggestion:
    Pass a Vector or Matrix, or a collection of Vectors/Matrices.
"""))
end
if !isa(x[1], AbstractVecOrMat)
    throw(ArgumentError("""
transform_constrained_to_unconstrained: elements of `x` are not AbstractVecOrMat.

Expected:
    An iterable whose elements are AbstractVecOrMat

Got:
    element type = $(typeof(x[1]))

Suggestion:
    Pass a collection of Vectors or Matrices (one per EK iteration).
"""))
end

# in transform_unconstrained_to_constrained: byte-for-byte identical except the
# summary line reads "transform_unconstrained_to_constrained" instead.

After extraction, both functions call two helpers defined once in a ## Error helpers section at the bottom of the file:

julia
# After — helpers at the bottom of src/ParameterDistributions.jl

## Error helpers

@noinline function _throw_x_not_iterable(x; where::Symbol)
    throw(ArgumentError("""
$where: `x` is not iterable.

Expected:
    AbstractVecOrMat or an iterable of AbstractVecOrMat elements (one per EK iteration)

Got:
    $(typeof(x))

Suggestion:
    Pass a Vector or Matrix, or a collection of Vectors/Matrices.
"""))
end

@noinline function _throw_x_elements_not_vecormat(x; where::Symbol)
    throw(ArgumentError("""
$where: elements of `x` are not AbstractVecOrMat.

Expected:
    An iterable whose elements are AbstractVecOrMat

Got:
    element type = $(typeof(x[1]))

Suggestion:
    Pass a collection of Vectors or Matrices (one per EK iteration).
"""))
end

# Both call sites now collapse to two readable guard lines each:
function transform_constrained_to_unconstrained(pd::ParameterDistribution, x)
    hasmethod(iterate, [typeof(x)]) ||
        _throw_x_not_iterable(x; where = :transform_constrained_to_unconstrained)
    isa(x[1], AbstractVecOrMat) ||
        _throw_x_elements_not_vecormat(x; where = :transform_constrained_to_unconstrained)
    # ... algorithm body visible immediately ...
end

function transform_unconstrained_to_constrained(pd::ParameterDistribution, x)
    hasmethod(iterate, [typeof(x)]) ||
        _throw_x_not_iterable(x; where = :transform_unconstrained_to_constrained)
    isa(x[1], AbstractVecOrMat) ||
        _throw_x_elements_not_vecormat(x; where = :transform_unconstrained_to_constrained)
    # ... algorithm body visible immediately ...
end

Key points:

  • The where::Symbol kwarg embeds the calling function name in the message so diagnostics stay specific even though the body is shared. Pass a Symbol literal (where = :my_func) — symbols are cheap and render cleanly with $where.
  • Both functions now have two one-line guards instead of two 12-line blocks; the algorithm body is immediately visible.
  • @noinline keeps the error path out of the hot function body.
  • The helpers live at the bottom of the same file — one jump away, no new file.
Helper with optional loop context

The for pdd in param_dist_dict_array loop in src/ParameterDistributions.jl validates each parameter dict but currently reports no position — the user sees "missing required keys" with no idea which dict in the array triggered the error. Extracting into a helper adds the index and makes the same helper reusable wherever that validation appears:

julia
# Before — inline block, no loop index in the message
for pdd in param_dist_dict_array
    if !all(["distribution", "name", "constraint"] .∈ [collect(keys(pdd))])
        throw(ArgumentError("""
Parameter dictionary is missing required keys.

Expected keys:
    "distribution", "name", "constraint"

Got keys:
    $(sort(collect(string.(keys(pdd)))))

Suggestion:
    Ensure each parameter dict contains all three required keys.
"""))
    end
end

# After — helper with optional loop context at the bottom of the file

@noinline function _throw_param_dict_missing_keys(got_keys; index = nothing, total = nothing)
    loop_ctx = isnothing(index) ? "" : """

Loop context:
    dict index = $index (of $total)"""
    throw(ArgumentError("""
Parameter dictionary is missing required keys.$loop_ctx

Expected keys:
    "distribution", "name", "constraint"

Got keys:
    $got_keys

Suggestion:
    Ensure each parameter dict contains all three required keys.
"""))
end

# Call site — loop now reports position:
for (i, pdd) in enumerate(param_dist_dict_array)
    all(["distribution", "name", "constraint"] .∈ [collect(keys(pdd))]) ||
        _throw_param_dict_missing_keys(
            sort(collect(string.(keys(pdd)))); index = i, total = length(param_dist_dict_array),
        )
end

# The same helper works outside a loop — omit the kwargs and the Loop context
# section is silently suppressed:
all(["distribution", "name", "constraint"] .∈ [collect(keys(pdd))]) ||
    _throw_param_dict_missing_keys(sort(collect(string.(keys(pdd)))))

Key points:

  • index and total default to nothing; the Loop context: section is rendered only when they are provided. No special-casing at any call site.
  • Switching for pdd in ... to for (i, pdd) in enumerate(...) is the only loop-side change needed to expose the index.
  • The user now knows which dict failed, not just that one of them did.
  • The same helper can be called from a non-loop site (e.g. single-dict validation) with zero kwargs and produces a clean message without a Loop context section.

Non-goals

  • Do not rewrite every low-level exception in the package. Focus on user-facing API boundaries and sites explicitly identified.
  • Do not suppress Julia stack traces. The goal is clearer diagnostics, not silenced errors.
  • Do not add verbosity for its own sake. A short, clear message beats a long, generic one.
  • Do not expose internal linear algebra variable names or dispatch details when domain-level terminology exists.
  • Do not extract truly short errors (≤3 lines) at a single call site — the inline form is easier to grep and keeps cause and message co-located. A single summary line, or a summary plus one Got line, is the ceiling for inline.

© CliMA, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/error-message-manager of CliMA/EnsembleKalmanProcesses.jl.

Open the folder on GitHubat commit d10e521

Compare with similar skills

Error Message Manager next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Error Message Manager compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Error Message Manager this skillCliMA/EnsembleKalmanProcesses.jl127—~8.2kAutomated safety check: PassApache-2.0
Messages Opsaffaan-m/ECC274k1 repos~724Automated safety check: PassMIT
Agent Messagingdavila7/claude-code-templates32k—~807Automated safety check: PassMIT
Secrets Managementdavila7/claude-code-templates32k11 repos~2kAutomated safety check: PassMIT
Project ManagerRightNow-AI/openfang18k—~960Automated safety check: PassApache-2.0
Content Management Systemsgithub/awesome-copilot40k1 repos~1.3kAutomated safety check: PassMIT

Similar skills

  • Messages Ops

    affaan-m/ECC

    Evidence-first live messaging workflow for ECC. An agent skill from affaan-m/ECC.

    274k GitHub starsUsed in 1 repo~724 tokens
    Productivity & AutomationAuto-check passed
  • Agent Messaging

    davila7/claude-code-templates

    Send and receive cryptographically signed messages between AI agents using the Agent Messaging Protocol (AMP).

    32k GitHub stars~807 tokensUpdated today
    MobileAuto-check passed
  • Secrets Management

    davila7/claude-code-templates

    Secure secrets management practices for CI/CD pipelines using Vault, AWS Secrets Manager, and other tools.

    32k GitHub starsUsed in 11 repos~2k tokens
    DevOps & CloudAuto-check passed
  • Project Manager

    RightNow-AI/openfang

    Project management expert for Agile, estimation, risk management, and stakeholder communication

    18k GitHub stars~960 tokensUpdated 3 mo ago
    Product & Project ManagementAuto-check passed
  • Content Management Systems

    github/awesome-copilot

    Official

    Workflow for building and modifying content management systems across WordPress, Shopify, Wix, Squarespace, Drupal, WooCommerce, Joomla, HubSpot CMS Hub, Webflow, Adobe Experience Manager, and…

    40k GitHub starsUsed in 1 repo~1.3k tokens
    Sales & SupportAuto-check passed
  • Secrets Vault Manager

    alirezarezvani/claude-skills

    A skill your agent uses when the user asks to set up secret management infrastructure, integrate HashiCorp Vault, configure cloud secret stores (AWS Secrets Manager, Azure Key Vault, GCP Secret…

    28k GitHub starsUsed in 1 repo~3.6k tokens
    DevOps & CloudAuto-check: notes

More from CliMA/EnsembleKalmanProcesses.jl

  • Slurm Pipeline Manager

    CliMA/EnsembleKalmanProcesses.jl

    Scaffold and maintain a SLURM/HPC job-dependency tree for an EnsembleKalmanProcesses.jl (EKP) calibration pipeline.

    127 GitHub stars~3.4k tokensUpdated today
    Auto-check passed
  • Math Auditor

    CliMA/EnsembleKalmanProcesses.jl

    Run an adversarial mathematical-accuracy review of a Julia package's src/ and test/ directories, producing a dated markdown report plus concise, self-contained fix-prompt markdowns suitable for…

    127 GitHub stars~2.6k tokensUpdated today
    Auto-check passed
  • Base Show

    CliMA/EnsembleKalmanProcesses.jl

    Add concise Base.show and Base.summary methods to Julia types whose default REPL representation is unhelpful or overwhelming.

    127 GitHub stars~5.2k tokensUpdated today
    Auto-check passed
  • Docstrings

    CliMA/EnsembleKalmanProcesses.jl

    Add or normalise Julia docstrings on public symbols (exported types, functions, and constants) so the package's public API is fully self-documenting and the Documenter.jl docs build passes its…

    127 GitHub stars~5.5k tokensUpdated today
    Auto-check passed

Questions about Error Message Manager

What does Error Message Manager do?

Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics. jl. Rewrite vague, delayed, or low-context Julia error messages into structured, actionable diagnostics.

When should I use Error Message Manager?

Error Message Manager fits situations like: mentions: error message; rewrite @assert; dimensionMismatch; julia exception.

How do I install Error Message Manager in Claude Code?

Run `npx skills add CliMA/EnsembleKalmanProcesses.jl --skill error-message-manager -a claude-code`. Or copy the skill folder (.claude/skills/error-message-manager in CliMA/EnsembleKalmanProcesses.jl) into .claude/skills/error-message-manager in your project. Claude Code loads it when a task matches its description.

How do I install Error Message Manager in Codex?

Run `npx skills add CliMA/EnsembleKalmanProcesses.jl --skill error-message-manager -a codex`. Or copy the skill folder (.claude/skills/error-message-manager in CliMA/EnsembleKalmanProcesses.jl) into .agents/skills/error-message-manager in your project. Codex loads it when a task matches its description.

Can I use Error Message Manager in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add CliMA/EnsembleKalmanProcesses.jl --skill error-message-manager -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/error-message-manager, .gemini/skills/error-message-manager, .github/skills/error-message-manager and .opencode/skills/error-message-manager in your project.

What does Error Message Manager need to run?

Going by SKILL.md and its folder, Error Message Manager needs the command-line tools its instructions call (rg).

Does Error Message Manager access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Error Message Manager safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Error Message Manager use?

Error Message Manager is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Error Message Manager use?

About 8.2k tokens (SKILL.md is roughly 33k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Error Message Manager?

Skills that share tags, products or a category with Error Message Manager: Messages Ops (affaan-m/ECC, 274k stars), Agent Messaging (davila7/claude-code-templates, 32k stars), Secrets Management (davila7/claude-code-templates, 32k stars) and Project Manager (RightNow-AI/openfang, 18k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Error Message Manager?

CliMA (a GitHub organization) maintains it in CliMA/EnsembleKalmanProcesses.jl, which has 127 GitHub stars. The repository holds 5 skills in this directory. The repository was last updated on October 7, 2026.

Source: CliMA/EnsembleKalmanProcesses.jl on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.