This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26676-chaos-feedback
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d12c03d58db47036fea8de5f6d11050c16879043
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 09:53:14 2026 +0100

    CAMEL-26676: camel-chaos - feedback while adding rules
    
    - A rule that matches nothing in the context now (no endpoint, route or
      step id, at=poll without a scheduled poll consumer, an expectation of a
      route that does not exist) is added with a warning that names some of
      what is there: logged, returned by ChaosManager.checkRule, printed by
      chaos-add and chaos-change, and passed through the MCP and camel ask
      tools.
    - ChaosManager.addRules adds rules separated by ; together, all or none;
      chaos-add takes --rule more than once, and the add tool several rules.
    - A skipped rule counts why (skippedReasons), in the report and the CLI.
    - camel get chaos has a NOTE column (what a lifecycle rule does to its
      route, why a rule was skipped), and says on stderr when there is
      nothing to show.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |  29 ++++-
 components/camel-chaos/src/main/docs/chaos.adoc    |  29 ++++-
 .../apache/camel/component/chaos/ChaosFaults.java  |  43 ++++---
 .../camel/component/chaos/ChaosLifecycle.java      |   2 +-
 .../apache/camel/component/chaos/ChaosManager.java | 125 +++++++++++++++---
 .../camel/component/chaos/ChaosPollStrategy.java   |   2 +-
 .../apache/camel/component/chaos/ChaosReport.java  |  13 ++
 .../apache/camel/component/chaos/ChaosRule.java    |  22 +++-
 .../apache/camel/component/chaos/ChaosTargets.java | 137 ++++++++++++++++++++
 .../component/chaos/ChaosRuleFeedbackTest.java     | 140 +++++++++++++++++++++
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   6 +-
 .../jbang-commands/camel-jbang-cmd-chaos-add.adoc  |   4 +-
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |   2 +-
 .../apache/camel/cli/connector/ChaosAction.java    |  27 +++-
 .../LocalCliConnectorChaosActionTest.java          |  21 ++++
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../core/commands/action/CamelChaosAction.java     |  12 ++
 .../core/commands/action/CamelChaosAddAction.java  |  27 ++--
 .../commands/action/CamelChaosChangeAction.java    |   3 +-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  14 ++-
 .../dsl/jbang/core/commands/process/ListChaos.java |  62 +++++++--
 .../jbang/core/commands/process/ListChaosTest.java |  52 +++++++-
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  12 +-
 23 files changed, 706 insertions(+), 80 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 3c2360541ef8..d1974b66c4cb 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -94,10 +94,15 @@ Rules can also be added, changed and removed from Java 
while the application run
 ChaosManager chaos = ChaosManager.get(camelContext);
 String id = chaos.addRule("endpoint=file:inbox* drop at=poll rate=0.5 
until=2m");
 chaos.changeRule(id, "endpoint=file:inbox* drop at=poll rate=0.2 until=2m");
+List<String> warnings = chaos.checkRule(id);
 chaos.removeRule(id);
+List<String> ids = chaos.addRules("endpoint=seda:dlq error at=dlq; 
endpoint=https:inventory* error status=503");
 String text = chaos.report().toText();
 ----
 
+`addRules` adds rules separated by `;`, as in `camel.chaos.rules`, all of them 
or none when one is not valid, so they
+are in place at the same time. `checkRule` says what a rule matches nothing of 
in the context now, see below.
+
 == Control while it runs
 
 With the Camel CLI, against a running integration (`myapp` is its name or pid):
@@ -105,6 +110,7 @@ With the Camel CLI, against a running integration (`myapp` 
is its name or pid):
 [source,bash]
 ----
 camel cmd chaos-add myapp --rule='endpoint=kafka:orders* delay duration=2s 
rate=0.2 until=5m'
+camel cmd chaos-add myapp --rule='endpoint=seda:dlq error at=dlq' 
--rule='endpoint=https:inventory* error status=503'
 camel cmd chaos-change myapp --id=r1 --rule='endpoint=kafka:orders* delay 
duration=5s rate=0.5 until=5m'
 camel cmd chaos-remove myapp --id=r1
 camel cmd chaos-clear
@@ -114,12 +120,23 @@ camel get chaos myapp --report
 ----
 
 `chaos-add` prints the id of the new rule (`r1`, `r2`, ...), and 
`chaos-change` the version of the rule that replaces
-it. The ids are per integration, so `chaos-add`, `chaos-change` and 
`chaos-remove` need the name or pid of one
+it. `--rule` can be given more than once (or a rule can hold several separated 
by `;`): the rules are added together,
+all or none, which matters when one must be in place before the other fires, 
as each `chaos-add` takes a moment. The ids are per integration, so 
`chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one
 integration; `chaos-clear` without one acts on every running integration that 
has chaos switched on. The rules that
 `chaos-clear` removes stay in the report; with `--reset` the report starts 
again, for a new experiment: no rules,
 route counts or verdict, as for an application that just started (with the 
same seed, and the ids of new rules going on
 from the last one). An id that does not exist is "not found" (exit code 3). 
`camel get chaos` lists the current rules with their id, version, state,
-matches and faults, and `--report` shows the report (also as JSON with 
`--json`).
+matches and faults, and `--report` shows the report (also as JSON with 
`--json`). Their `NOTE` column says what a
+lifecycle rule does to its route (`route poller down, back in 12s`) and why a 
rule did nothing when it was skipped.
+Without a rule to show, `camel get chaos` says so on stderr (no rules, chaos 
off, or no integration with camel-chaos).
+
+A rule that matches nothing in the integration now is added with a warning, 
which `chaos-add` and `chaos-change` print,
+and which is logged: an `endpoint=` or `component=` pattern that matches no 
endpoint of the context, an `at=poll` for
+endpoints that no route consumes with a scheduled poll, a `route=` or `step=` 
pattern that matches no route or step
+id, and an expectation of a route that does not exist. Such a rule fires 0 
times, and is often a typo; the warning
+names some of the endpoints, routes or steps there are. It is not an error, as 
the routes may still change (a `toD`
+creates its endpoints when it sends, and a route can be added later). The 
rules of `camel.chaos.rules` are checked when
+the context has started.
 
 The commands go through the `chaos` action of the CLI connector, which calls 
`ChaosManager`: a rule goes through the
 same parser as `camel.chaos.rules`, and one that is not valid is refused with 
the reason. They work only when chaos is
@@ -137,7 +154,8 @@ as tools, for an AI agent that tries a route against 
failures: `camel_runtime_ch
 They take the same one-line rules: adding, changing, removing and clearing go 
through the same `chaos` action of the CLI
 connector, and the list and the report read the console from the status of the 
application. So chaos must be switched
 on, and a rule that is not valid is refused with the reason; 
`camel_runtime_chaos_clear` takes `reset=true` as
-`chaos-clear --reset` does. Adding and changing a rule are marked destructive, 
so a
+`chaos-clear --reset` does. `camel_runtime_chaos_add` takes several rules 
separated by `;`, and the add and change
+tools return the warnings of a rule that matches nothing, so an agent can fix 
a typo with a change. Adding and changing a rule are marked destructive, so a
 client that honours `destructiveHint` asks before it runs them, and the 
`read-write` access level of the MCP security
 layer does not offer them; the list and the report are read-only. `camel ask` 
has the same tools, and asks before it
 adds or changes a rule. The development MCP tools that `camel run --mcp` 
(`camel.jbang.mcp=true`) serves over HTTP have
@@ -268,7 +286,7 @@ which is read into memory; not a stream or a reader, which 
reading would consume
 the bytes of that text in UTF-16, while the exchange keeps its charset, so 
read as text again it has characters outside
 US-ASCII. `truncate`: an `InputStream` or stream cache body ends after the 
first half of its bytes (the stream is read
 into memory; a stream cache stays a stream cache). On a message the mode 
cannot change, the message is left as it is,
-and the report counts the rule as skipped.
+and the report counts the rule as skipped, with the reason.
 
 |`lifecycle`
 |`action=suspend\|stop\|remove`, `duration=<time>`
@@ -396,7 +414,8 @@ removed while the route was down, and the `reason` when it 
did not go as planned
 
 `ChaosManager.report()` gives the report, as plain text (`toText()`) for a 
person and as JSON (`toJson()`) for a tool,
 and it is logged when the context stops (`camel.chaos.reportOnShutdown`), if 
any rule was added. It states the seed,
-each rule with its id and version, its matches, the faults that fired at each 
hook and the ones that were skipped, its
+each rule with its id and version, its matches, the faults that fired at each 
hook and the ones that were skipped (with
+the reasons, such as `a stream body`, counted in `skippedReasons`), its
 state (`active`, `ended`, `removed` or `replaced`), and per route. The rules 
are in the order of their ids, and the
 versions of an id from the latest. Per route:
 
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 3c2360541ef8..d1974b66c4cb 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -94,10 +94,15 @@ Rules can also be added, changed and removed from Java 
while the application run
 ChaosManager chaos = ChaosManager.get(camelContext);
 String id = chaos.addRule("endpoint=file:inbox* drop at=poll rate=0.5 
until=2m");
 chaos.changeRule(id, "endpoint=file:inbox* drop at=poll rate=0.2 until=2m");
+List<String> warnings = chaos.checkRule(id);
 chaos.removeRule(id);
+List<String> ids = chaos.addRules("endpoint=seda:dlq error at=dlq; 
endpoint=https:inventory* error status=503");
 String text = chaos.report().toText();
 ----
 
+`addRules` adds rules separated by `;`, as in `camel.chaos.rules`, all of them 
or none when one is not valid, so they
+are in place at the same time. `checkRule` says what a rule matches nothing of 
in the context now, see below.
+
 == Control while it runs
 
 With the Camel CLI, against a running integration (`myapp` is its name or pid):
@@ -105,6 +110,7 @@ With the Camel CLI, against a running integration (`myapp` 
is its name or pid):
 [source,bash]
 ----
 camel cmd chaos-add myapp --rule='endpoint=kafka:orders* delay duration=2s 
rate=0.2 until=5m'
+camel cmd chaos-add myapp --rule='endpoint=seda:dlq error at=dlq' 
--rule='endpoint=https:inventory* error status=503'
 camel cmd chaos-change myapp --id=r1 --rule='endpoint=kafka:orders* delay 
duration=5s rate=0.5 until=5m'
 camel cmd chaos-remove myapp --id=r1
 camel cmd chaos-clear
@@ -114,12 +120,23 @@ camel get chaos myapp --report
 ----
 
 `chaos-add` prints the id of the new rule (`r1`, `r2`, ...), and 
`chaos-change` the version of the rule that replaces
-it. The ids are per integration, so `chaos-add`, `chaos-change` and 
`chaos-remove` need the name or pid of one
+it. `--rule` can be given more than once (or a rule can hold several separated 
by `;`): the rules are added together,
+all or none, which matters when one must be in place before the other fires, 
as each `chaos-add` takes a moment. The ids are per integration, so 
`chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one
 integration; `chaos-clear` without one acts on every running integration that 
has chaos switched on. The rules that
 `chaos-clear` removes stay in the report; with `--reset` the report starts 
again, for a new experiment: no rules,
 route counts or verdict, as for an application that just started (with the 
same seed, and the ids of new rules going on
 from the last one). An id that does not exist is "not found" (exit code 3). 
`camel get chaos` lists the current rules with their id, version, state,
-matches and faults, and `--report` shows the report (also as JSON with 
`--json`).
+matches and faults, and `--report` shows the report (also as JSON with 
`--json`). Their `NOTE` column says what a
+lifecycle rule does to its route (`route poller down, back in 12s`) and why a 
rule did nothing when it was skipped.
+Without a rule to show, `camel get chaos` says so on stderr (no rules, chaos 
off, or no integration with camel-chaos).
+
+A rule that matches nothing in the integration now is added with a warning, 
which `chaos-add` and `chaos-change` print,
+and which is logged: an `endpoint=` or `component=` pattern that matches no 
endpoint of the context, an `at=poll` for
+endpoints that no route consumes with a scheduled poll, a `route=` or `step=` 
pattern that matches no route or step
+id, and an expectation of a route that does not exist. Such a rule fires 0 
times, and is often a typo; the warning
+names some of the endpoints, routes or steps there are. It is not an error, as 
the routes may still change (a `toD`
+creates its endpoints when it sends, and a route can be added later). The 
rules of `camel.chaos.rules` are checked when
+the context has started.
 
 The commands go through the `chaos` action of the CLI connector, which calls 
`ChaosManager`: a rule goes through the
 same parser as `camel.chaos.rules`, and one that is not valid is refused with 
the reason. They work only when chaos is
@@ -137,7 +154,8 @@ as tools, for an AI agent that tries a route against 
failures: `camel_runtime_ch
 They take the same one-line rules: adding, changing, removing and clearing go 
through the same `chaos` action of the CLI
 connector, and the list and the report read the console from the status of the 
application. So chaos must be switched
 on, and a rule that is not valid is refused with the reason; 
`camel_runtime_chaos_clear` takes `reset=true` as
-`chaos-clear --reset` does. Adding and changing a rule are marked destructive, 
so a
+`chaos-clear --reset` does. `camel_runtime_chaos_add` takes several rules 
separated by `;`, and the add and change
+tools return the warnings of a rule that matches nothing, so an agent can fix 
a typo with a change. Adding and changing a rule are marked destructive, so a
 client that honours `destructiveHint` asks before it runs them, and the 
`read-write` access level of the MCP security
 layer does not offer them; the list and the report are read-only. `camel ask` 
has the same tools, and asks before it
 adds or changes a rule. The development MCP tools that `camel run --mcp` 
(`camel.jbang.mcp=true`) serves over HTTP have
@@ -268,7 +286,7 @@ which is read into memory; not a stream or a reader, which 
reading would consume
 the bytes of that text in UTF-16, while the exchange keeps its charset, so 
read as text again it has characters outside
 US-ASCII. `truncate`: an `InputStream` or stream cache body ends after the 
first half of its bytes (the stream is read
 into memory; a stream cache stays a stream cache). On a message the mode 
cannot change, the message is left as it is,
-and the report counts the rule as skipped.
+and the report counts the rule as skipped, with the reason.
 
 |`lifecycle`
 |`action=suspend\|stop\|remove`, `duration=<time>`
@@ -396,7 +414,8 @@ removed while the route was down, and the `reason` when it 
did not go as planned
 
 `ChaosManager.report()` gives the report, as plain text (`toText()`) for a 
person and as JSON (`toJson()`) for a tool,
 and it is logged when the context stops (`camel.chaos.reportOnShutdown`), if 
any rule was added. It states the seed,
-each rule with its id and version, its matches, the faults that fired at each 
hook and the ones that were skipped, its
+each rule with its id and version, its matches, the faults that fired at each 
hook and the ones that were skipped (with
+the reasons, such as `a stream body`, counted in `skippedReasons`), its
 state (`active`, `ended`, `removed` or `replaced`), and per route. The rules 
are in the order of their ids, and the
 versions of an id from the latest. Per route:
 
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java
index 27fe753d172a..9a78d96b0a8b 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java
@@ -117,7 +117,7 @@ final class ChaosFaults {
         String scheme = 
ChaosPatterns.scheme(ChaosPatterns.endpointUri(endpoint));
         if (!ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) {
             // the rule matched an endpoint that is not an HTTP one (with a 
pattern such as endpoint=*inventory*)
-            rule.skipped();
+            rule.skipped("not an HTTP endpoint (status= is for the HTTP 
components)");
             return SendHook.PROCEED;
         }
         Message message = exchange.getMessage();
@@ -348,21 +348,22 @@ final class ChaosFaults {
      * skipped when the message has nothing the mode can change.
      */
     static void corrupt(ChaosRule rule, Exchange exchange, Hook hook) {
-        boolean changed;
+        // why the message was left as it is, or null when it was changed
+        String skip;
         try {
-            changed = switch (rule.mode) {
+            skip = switch (rule.mode) {
                 case HEADERS -> corruptHeaders(rule, exchange.getMessage());
                 case CHARSET -> corruptCharset(exchange);
                 case TRUNCATE -> truncate(exchange.getMessage());
             };
         } catch (RuntimeException e) {
             // a body that cannot be read as the mode needs (such as a failing 
type conversion) is left as it is
-            changed = false;
+            skip = "the body could not be read (" + 
e.getClass().getSimpleName() + ")";
         }
-        if (changed) {
+        if (skip == null) {
             rule.fired(hook);
         } else {
-            rule.skipped();
+            rule.skipped(skip);
         }
     }
 
@@ -370,7 +371,7 @@ final class ChaosFaults {
      * Gives every header that is not one of Camel's own ({@code Camel*}) a 
random value of letters and digits, from the
      * random sequence of the rule.
      */
-    private static boolean corruptHeaders(ChaosRule rule, Message message) {
+    private static String corruptHeaders(ChaosRule rule, Message message) {
         List<String> names = new ArrayList<>();
         for (String name : message.getHeaders().keySet()) {
             if (!name.regionMatches(true, 0, "Camel", 0, 5)) {
@@ -384,7 +385,7 @@ final class ChaosFaults {
             }
             message.setHeader(name, new String(value));
         }
-        return !names.isEmpty();
+        return names.isEmpty() ? "no headers to change (Camel* headers are 
left as they are)" : null;
     }
 
     /**
@@ -392,15 +393,18 @@ final class ChaosFaults {
      * stream, which reading would consume) with the bytes of that text in 
UTF-16, while the exchange keeps its charset:
      * read as text again, the body has characters outside US-ASCII.
      */
-    private static boolean corruptCharset(Exchange exchange) {
+    private static String corruptCharset(Exchange exchange) {
         Message message = exchange.getMessage();
         Object body = message.getBody();
-        if (body == null || body instanceof InputStream || body instanceof 
Reader || body instanceof StreamCache) {
-            return false;
+        if (body == null) {
+            return "no body";
+        }
+        if (body instanceof InputStream || body instanceof Reader || body 
instanceof StreamCache) {
+            return "a stream body (charset changes a text body; mode=truncate 
changes a stream)";
         }
         if (body instanceof WrappedFile<?> wrapped && !(wrapped.getFile() 
instanceof File)) {
             // a remote file (such as FTP), whose content may be a stream
-            return false;
+            return "a remote file body (charset changes a text body)";
         }
         String text;
         if (body instanceof CharSequence cs) {
@@ -410,22 +414,25 @@ final class ChaosFaults {
         } else {
             text = 
exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, 
body);
         }
-        if (text == null || text.isEmpty()) {
-            return false;
+        if (text == null) {
+            return "a body that is not text (charset changes a text body)";
+        }
+        if (text.isEmpty()) {
+            return "an empty body";
         }
         message.setBody(text.getBytes(StandardCharsets.UTF_16));
-        return true;
+        return null;
     }
 
     /**
      * Replaces an {@link InputStream} or stream cache body with one that ends 
after the first half of its bytes. The
      * stream is read into memory and closed; a stream cache stays a stream 
cache.
      */
-    private static boolean truncate(Message message) {
+    private static String truncate(Message message) {
         Object body = message.getBody();
         boolean cache = body instanceof StreamCache;
         if (!(body instanceof InputStream) && !cache) {
-            return false;
+            return body == null ? "no body" : "not a stream body (truncate 
changes a stream or a stream cache)";
         }
         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
         try {
@@ -446,6 +453,6 @@ final class ChaosFaults {
         byte[] half = new byte[all.length / 2];
         System.arraycopy(all, 0, half, 0, half.length);
         message.setBody(cache ? new InputStreamCache(half) : new 
ByteArrayInputStream(half));
-        return all.length > 0;
+        return all.length > 0 ? null : "an empty body";
     }
 }
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java
index e64d3e90ade8..c60b0a200735 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java
@@ -301,7 +301,7 @@ final class ChaosLifecycle {
         }
         synchronized (fault) {
             if (skip != null) {
-                rule.skipped();
+                rule.skipped(skip);
                 LOG.info("Chaos rule {} did nothing: {}", rule.id, skip);
                 fault.finish(FaultState.SKIPPED, null, skip);
                 return;
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index 9af42102d310..8882016fb92b 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -131,27 +131,67 @@ public final class ChaosManager {
     public String addRule(String rule) {
         ChaosRule.Builder definition = ChaosRuleParser.parse(rule, 
camelContext.getClassResolver(), maxDuration);
         synchronized (lock) {
+            return add(List.of(definition)).get(0);
+        }
+    }
+
+    /**
+     * Adds rules together: all of them, or none when one is not valid. The 
rules are in the form of
+     * {@code camel.chaos.rules}, separated by {@code ;}, so they are in place 
at the same time, such as a rule for the
+     * dead letter channel and the rule that sends a message there.
+     *
+     * @param  rules                    the rules in their one-line text form, 
separated by {@code ;}
+     * @return                          the ids of the rules, in their order
+     * @throws IllegalArgumentException when a rule is not valid, with the 
reason; no rule is added then
+     * @throws IllegalStateException    when the rules would be more than 
{@link #MAX_RULES}
+     */
+    public List<String> addRules(String rules) {
+        List<String> texts = ChaosRuleParser.splitRules(rules);
+        if (texts.isEmpty()) {
+            throw new IllegalArgumentException("The chaos rule is empty");
+        }
+        List<ChaosRule.Builder> definitions = new ArrayList<>();
+        for (String text : texts) {
+            definitions.add(ChaosRuleParser.parse(text, 
camelContext.getClassResolver(), maxDuration));
+        }
+        synchronized (lock) {
+            return add(definitions);
+        }
+    }
+
+    /** Adds parsed rules, all or none. Called with the lock held. */
+    private List<String> add(List<ChaosRule.Builder> definitions) {
+        for (ChaosRule.Builder definition : definitions) {
             checkRoute(definition);
-            long now = TimeSource.currentTimeMillis();
-            if (rules.size() >= MAX_RULES) {
-                retireEnded(now);
-            }
-            if (rules.size() >= MAX_RULES) {
-                throw new IllegalStateException(
-                        "There are " + MAX_RULES + " chaos rules already; 
remove some before adding more");
-            }
-            String id = "r" + nextId++;
-            ChaosRule added = new ChaosRule(definition, id, seed, now, 
maxDuration);
+        }
+        long now = TimeSource.currentTimeMillis();
+        if (rules.size() + definitions.size() > MAX_RULES) {
+            retireEnded(now);
+        }
+        if (rules.size() + definitions.size() > MAX_RULES) {
+            throw new IllegalStateException(
+                    "There are " + rules.size() + " chaos rules already, and 
at most " + MAX_RULES
+                                            + "; remove some before adding 
more");
+        }
+        List<ChaosRule> added = new ArrayList<>();
+        for (ChaosRule.Builder definition : definitions) {
+            ChaosRule rule = new ChaosRule(definition, "r" + nextId++, seed, 
now, maxDuration);
             // a rule added before the context started starts its clock when 
it has started
-            added.clockStarted = camelContext.isStarted();
-            rules.add(added);
-            update();
-            LOG.info("Chaos rule {} added: {}", id, added.text);
+            rule.clockStarted = camelContext.isStarted();
+            rules.add(rule);
+            added.add(rule);
+        }
+        update();
+        List<String> ids = new ArrayList<>();
+        for (ChaosRule rule : added) {
+            LOG.info("Chaos rule {} added: {}", rule.id, rule.text);
             if (camelContext.isStarted()) {
-                warnIfNoRoute(added);
+                warnIfNoRoute(rule);
+                warnIfNoTarget(rule);
             }
-            return id;
+            ids.add(rule.id);
         }
+        return ids;
     }
 
     /**
@@ -183,11 +223,46 @@ public final class ChaosManager {
             LOG.info("Chaos rule {} changed (version {}): {}", id, 
changed.version, changed.text);
             if (camelContext.isStarted()) {
                 warnIfNoRoute(changed);
+                warnIfNoTarget(changed);
             }
             return changed.version;
         }
     }
 
+    /**
+     * What a rule matches nothing of in the context now: its pattern matches 
no endpoint, route or step id, its
+     * {@code at=poll} finds no consumer that polls on a schedule, or its 
expectation names a route that does not exist.
+     * Such a rule fires 0 times, often because of a typo. The routes may 
still change (a {@code toD} creates its
+     * endpoints when it sends, a route can be added later), so it is a 
warning, not an error. Empty before the context
+     * has started, as its routes are not there yet.
+     *
+     * @param  id                       the id of the rule
+     * @return                          the warnings, empty when the rule 
finds something to fire at
+     * @throws IllegalArgumentException when there is no rule with that id
+     */
+    public List<String> checkRule(String id) {
+        ChaosRule rule;
+        synchronized (lock) {
+            int index = indexOf(id);
+            if (index < 0) {
+                throw new IllegalArgumentException("There is no chaos rule " + 
id);
+            }
+            rule = rules.get(index);
+        }
+        List<String> answer = new ArrayList<>();
+        if (!camelContext.isStarted()) {
+            return answer;
+        }
+        if (rule.expectation != null && 
camelContext.getRoute(rule.expectation.route) == null) {
+            answer.add("expects route " + rule.expectation.route + ", but 
there is no route with that id (yet)");
+        }
+        String target = ChaosTargets.check(camelContext, rule);
+        if (target != null) {
+            answer.add(target);
+        }
+        return answer;
+    }
+
     /**
      * Whether there is a rule with that id that can be changed or removed: 
one that was added and is not removed, nor
      * moved to the report history.
@@ -463,9 +538,27 @@ public final class ChaosManager {
                 warned.add(rule.id);
             }
         }
+        List<ChaosRule> current;
+        synchronized (lock) {
+            current = new ArrayList<>(rules);
+        }
+        for (ChaosRule rule : current) {
+            warnIfNoTarget(rule);
+        }
         return warned;
     }
 
+    // the WARNs for a rule that matches nothing, for the tests
+    final AtomicInteger noTargetWarnings = new AtomicInteger();
+
+    private void warnIfNoTarget(ChaosRule rule) {
+        String warning = ChaosTargets.check(camelContext, rule);
+        if (warning != null) {
+            noTargetWarnings.incrementAndGet();
+            LOG.warn("Chaos rule {}: {}", rule.id, warning);
+        }
+    }
+
     // the WARNs for an expectation of a route that does not exist, for the 
tests
     final AtomicInteger unknownRouteWarnings = new AtomicInteger();
 
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java
index 343c208752d1..44fbbe4891d5 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java
@@ -106,7 +106,7 @@ final class ChaosPollStrategy implements 
PollingConsumerPollStrategy {
             case ERROR -> {
                 if (!beginDelegate(consumer, endpoint)) {
                     // the strategy of the consumer skips this poll anyway
-                    rule.skipped();
+                    rule.skipped("the poll strategy of the consumer skipped 
the poll");
                     return false;
                 }
                 rule.fired(Hook.POLL);
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
index 260065d199b0..1cef8c19cbef 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
@@ -193,6 +193,11 @@ public final class ChaosReport {
                 }
             }
             sb.append(at).append(", skipped ").append(r.skipped);
+            if (!r.skipReasons.isEmpty()) {
+                StringJoiner why = new StringJoiner("; ", " (", ")");
+                r.skipReasons.forEach((reason, count) -> why.add(reason + ": " 
+ count));
+                sb.append(why);
+            }
             if (r.lostAtDlq > 0) {
                 sb.append(", lost at the dead letter channel 
").append(r.lostAtDlq);
             }
@@ -287,6 +292,12 @@ public final class ChaosReport {
             }
             jo.put("fired", fired);
             jo.put("skipped", r.skipped);
+            if (!r.skipReasons.isEmpty()) {
+                // why the rule did nothing, per reason
+                JsonObject why = new JsonObject();
+                r.skipReasons.forEach(why::put);
+                jo.put("skippedReasons", why);
+            }
             if (r.hooks.contains(Hook.DLQ)) {
                 jo.put("lostAtDlq", r.lostAtDlq);
             }
@@ -393,6 +404,7 @@ public final class ChaosReport {
         final long[] fired = new long[Hook.values().length];
         final long firedTotal;
         final long skipped;
+        final Map<String, Long> skipReasons;
         final long lostAtDlq;
         final long endsIn;
         final String expectRoute;
@@ -428,6 +440,7 @@ public final class ChaosReport {
             // a lifecycle rule fired when it took its route down
             firedTotal = total + (lifecycle != null && lifecycle.acted() ? 1 : 
0);
             skipped = rule.skippedCount();
+            skipReasons = rule.skipReasons();
             lostAtDlq = rule.lostAtDlqCount();
             endsIn = Math.max(0, rule.endsAt - now);
             within = rule.within;
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
index 0ddadf31ecec..45115b5c2bc7 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
@@ -19,6 +19,9 @@ package org.apache.camel.component.chaos;
 import java.lang.reflect.Constructor;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.LongAdder;
 
@@ -168,6 +171,8 @@ final class ChaosRule {
     private final AtomicLong matches = new AtomicLong();
     private final LongAdder[] fired = new LongAdder[Hook.values().length];
     private final LongAdder skipped = new LongAdder();
+    // why the rule did nothing when it fired: a reason per kind of message it 
could not change (a few fixed texts)
+    private final ConcurrentMap<String, LongAdder> skipReasons = new 
ConcurrentHashMap<>();
     // the messages lost because the rule failed or dropped the send of the 
dead letter channel, which then handled them
     private final LongAdder lostAtDlq = new LongAdder();
     private final AtomicLong random;
@@ -280,9 +285,22 @@ final class ChaosRule {
         return lostAtDlq.sum();
     }
 
-    /** Counts that the rule fired at a message it could not change, so it did 
nothing. */
-    void skipped() {
+    /**
+     * Counts that the rule fired at a message it could not change, so it did 
nothing.
+     *
+     * @param reason why, such as {@code a stream body (charset changes a text 
body)}: one of a few fixed texts, so the
+     *               report can count them
+     */
+    void skipped(String reason) {
         skipped.increment();
+        skipReasons.computeIfAbsent(reason, k -> new LongAdder()).increment();
+    }
+
+    /** How often the rule did nothing, per reason, in the order of the 
reasons. */
+    Map<String, Long> skipReasons() {
+        Map<String, Long> answer = new TreeMap<>();
+        skipReasons.forEach((reason, count) -> answer.put(reason, 
count.sum()));
+        return answer;
     }
 
     long matches() {
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java
new file mode 100644
index 000000000000..8dead19bb74a
--- /dev/null
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.TreeSet;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.component.chaos.ChaosRule.Hook;
+import org.apache.camel.spi.IdAware;
+import org.apache.camel.support.ScheduledPollConsumer;
+import org.apache.camel.util.URISupport;
+
+/**
+ * Checks what the target of a rule finds in the context now, for the warning 
about a rule that matches nothing (often a
+ * typo). The warning names some of what is there, so a person or a tool can 
fix the pattern.
+ */
+final class ChaosTargets {
+
+    // the most endpoints, routes or steps a warning names
+    private static final int NAMED = 5;
+
+    private ChaosTargets() {
+    }
+
+    /**
+     * Why the rule finds nothing to fire at in the context now, or null when 
it finds something. A lifecycle rule is
+     * not checked: its route was checked when it was added.
+     */
+    static String check(CamelContext camelContext, ChaosRule rule) {
+        if (rule.lifecycle != null) {
+            return null;
+        }
+        return switch (rule.target) {
+            case ENDPOINT, COMPONENT -> checkEndpoints(camelContext, rule);
+            case ROUTE -> checkRoutes(camelContext, rule);
+            case STEP -> checkSteps(camelContext, rule);
+        };
+    }
+
+    private static String checkEndpoints(CamelContext camelContext, ChaosRule 
rule) {
+        Collection<Endpoint> endpoints = camelContext.getEndpoints();
+        boolean any = false;
+        boolean polled = false;
+        TreeSet<String> uris = new TreeSet<>();
+        for (Endpoint endpoint : endpoints) {
+            String uri = ChaosPatterns.endpointUri(endpoint);
+            uris.add(withoutQuery(uri));
+            if (rule.matchesUri(uri)) {
+                any = true;
+            }
+        }
+        if (!any) {
+            return rule.target.key + "=" + rule.pattern + " matches no 
endpoint of the context now, so the rule fires 0"
+                   + " times (a toD creates its endpoints when it sends); the 
endpoints are: " + some(uris);
+        }
+        if (rule.hooks.equals(Set.of(Hook.POLL))) {
+            for (Route route : camelContext.getRoutes()) {
+                if (route.getConsumer() instanceof ScheduledPollConsumer
+                        && 
rule.matchesUri(ChaosPatterns.endpointUri(route.getEndpoint()))) {
+                    polled = true;
+                }
+            }
+            if (!polled) {
+                return "at=poll: no route consumes an endpoint that matches " 
+ rule.target.key + "=" + rule.pattern
+                       + " with a scheduled poll (consumers such as Kafka and 
JMS do not poll on a schedule), so the rule"
+                       + " fires 0 times";
+            }
+        }
+        return null;
+    }
+
+    private static String checkRoutes(CamelContext camelContext, ChaosRule 
rule) {
+        TreeSet<String> ids = new TreeSet<>();
+        for (Route route : camelContext.getRoutes()) {
+            if (rule.matchesId(route.getRouteId())) {
+                return null;
+            }
+            ids.add(route.getRouteId());
+        }
+        return "route=" + rule.pattern + " matches no route of the context 
now, so the rule fires 0 times; the routes are: "
+               + some(ids);
+    }
+
+    private static String checkSteps(CamelContext camelContext, ChaosRule 
rule) {
+        TreeSet<String> ids = new TreeSet<>();
+        for (Route route : camelContext.getRoutes()) {
+            for (Processor processor : route.filter("*")) {
+                String id = processor instanceof IdAware idAware ? 
idAware.getId() : null;
+                if (id != null) {
+                    if (rule.matchesId(id)) {
+                        return null;
+                    }
+                    ids.add(id);
+                }
+            }
+        }
+        return "step=" + rule.pattern + " matches no step of the context now, 
so the rule fires 0 times (a step id is"
+               + " such as to1, or the id set with id(...)); the steps are: " 
+ some(ids);
+    }
+
+    // the endpoints are named as Camel shows them elsewhere (a pattern 
matches them with or without the // after the
+    // scheme), and without their query, which can hold secrets
+    private static String withoutQuery(String uri) {
+        int query = uri.indexOf('?');
+        return URISupport.sanitizeUri(query > 0 ? uri.substring(0, query) : 
uri);
+    }
+
+    private static String some(TreeSet<String> names) {
+        if (names.isEmpty()) {
+            return "none";
+        }
+        StringJoiner sj = new StringJoiner(", ");
+        names.stream().limit(NAMED).forEach(sj::add);
+        int more = names.size() - NAMED;
+        return more > 0 ? sj + " and " + more + " more" : sj.toString();
+    }
+}
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java
new file mode 100644
index 000000000000..e1223f956f1c
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * What a developer (or a tool) learns when adding rules: a warning for a rule 
that matches nothing in the context now
+ * (often a typo), rules added together (all or none), and why a rule did 
nothing when it was skipped.
+ */
+class ChaosRuleFeedbackTest extends ChaosTestSupport {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:orders").routeId("orders")
+                        .setHeader("country", constant("DK")).id("enrich")
+                        .to("mock:stock?retainFirst=1").id("callStock");
+                // a consumer that polls on a schedule, which never polls in 
the test
+                
from("file:target/chaos-feedback?initialDelay=3600000").routeId("poller").to("mock:polled");
+            }
+        };
+    }
+
+    @Test
+    void aRuleThatFindsSomethingHasNoWarning() {
+        for (String rule : List.of("endpoint=mock:stock* error", 
"endpoint=direct:orders delay duration=1ms",
+                "component=mock error", "step=enrich error", "step=call* delay 
duration=1ms",
+                "route=orders corrupt mode=headers", "endpoint=file:target* 
drop at=poll",
+                "endpoint=mock:stock* error expect=orders:1", "route=orders 
stall duration=1s")) {
+            
assertThat(chaos().checkRule(chaos().addRule(rule))).as(rule).isEmpty();
+            chaos().reset();
+        }
+    }
+
+    @Test
+    void anEndpointTypoNamesTheEndpoints() {
+        String id = chaos().addRule("endpoint=mock:stok* error");
+
+        assertThat(chaos().checkRule(id)).singleElement().asString()
+                .startsWith("endpoint=mock:stok* matches no endpoint of the 
context now, so the rule fires 0 times")
+                .contains("the endpoints are: ")
+                .contains("mock://stock")
+                // without the query of the endpoint
+                .doesNotContain("retainFirst");
+        assertThat(chaos().noTargetWarnings.get()).isEqualTo(1);
+    }
+
+    @Test
+    void aStepRouteAndExpectationTypoAreNamed() {
+        assertThat(chaos().checkRule(chaos().addRule("step=callStok 
error"))).singleElement().asString()
+                .startsWith("step=callStok matches no step of the context now")
+                .contains("callStock", "enrich");
+        assertThat(chaos().checkRule(chaos().addRule("route=order corrupt 
mode=headers"))).singleElement().asString()
+                .startsWith("route=order matches no route of the context now")
+                .endsWith("the routes are: orders, poller");
+        assertThat(chaos().checkRule(chaos().addRule("endpoint=mock:stock* 
error expect=shiping:1")))
+                .containsExactly("expects route shiping, but there is no route 
with that id (yet)");
+    }
+
+    @Test
+    void aPollRuleForAnEndpointWithoutAScheduledPollIsNamed() {
+        assertThat(chaos().checkRule(chaos().addRule("endpoint=direct:orders 
drop at=poll"))).singleElement()
+                .asString().startsWith("at=poll: no route consumes an endpoint 
that matches endpoint=direct:orders");
+        // without at=poll the sends to it are reached
+        assertThat(chaos().checkRule(chaos().addRule("endpoint=direct:orders 
drop"))).isEmpty();
+    }
+
+    @Test
+    void aChangedRuleIsCheckedToo() {
+        String id = chaos().addRule("step=enrich error");
+        chaos().changeRule(id, "step=enrihc error");
+
+        
assertThat(chaos().checkRule(id)).singleElement().asString().startsWith("step=enrihc
 matches no step");
+        assertThatThrownBy(() -> chaos().checkRule("r99")).hasMessage("There 
is no chaos rule r99");
+    }
+
+    @Test
+    void rulesAreAddedTogether() {
+        List<String> ids = chaos().addRules(
+                "endpoint=mock:stock* error at=dlq; step=enrich delay 
duration=1ms ;endpoint=mock:stock* error message='a; b'");
+
+        assertThat(ids).containsExactly("r1", "r2", "r3");
+        
assertThat(ruleReport("r3").getString("rule")).isEqualTo("endpoint=mock:stock* 
error message='a; b'");
+    }
+
+    @Test
+    void rulesAreAddedAllOrNone() {
+        assertThatThrownBy(() -> chaos().addRules("step=enrich error; 
step=enrich drop"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageStartingWith("Invalid chaos rule [step=enrich 
drop]");
+        assertThat(chaos().hasRule("r1")).isFalse();
+        assertThatThrownBy(() -> chaos().addRules(" ; ")).hasMessage("The 
chaos rule is empty");
+        // the ids go on from the rules that were added
+        assertThat(chaos().addRule("step=enrich error")).isEqualTo("r1");
+    }
+
+    @Test
+    void theReasonOfASkipIsCounted() {
+        String charset = chaos().addRule("route=orders corrupt mode=charset 
until=2");
+        template.sendBody("direct:orders", new 
ByteArrayInputStream("abc".getBytes(StandardCharsets.UTF_8)));
+        template.sendBody("direct:orders", null);
+        String truncate = chaos().addRule("endpoint=mock:stock* corrupt 
mode=truncate until=1");
+        template.sendBody("direct:orders", "text");
+
+        JsonObject reasons = 
ruleReport(charset).getJsonObject("skippedReasons");
+        assertThat(reasons).containsEntry("a stream body (charset changes a 
text body; mode=truncate changes a stream)",
+                1L).containsEntry("no body", 1L);
+        assertThat(ruleReport(truncate).getJsonObject("skippedReasons"))
+                .containsEntry("not a stream body (truncate changes a stream 
or a stream cache)", 1L);
+        assertThat(chaos().report().toText())
+                .contains("skipped 2 (a stream body (charset changes a text 
body; mode=truncate changes a stream): 1;"
+                          + " no body: 1)");
+    }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 93b29b868044..de06851ac263 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -1079,11 +1079,13 @@ that changes rules is refused. The list and the report 
read the chaos console fr
 
 | `camel_runtime_chaos_add`
 | `false` | `true`
-| Adds a rule (`rule`) and returns its id (`r1`, `r2`, ...).
+| Adds a rule (`rule`), or several separated by `;` (all or none), and returns 
the ids (`r1`, `r2`, ...), with
+  `warnings` for a rule that matches nothing in the application now (often a 
typo).
 
 | `camel_runtime_chaos_change`
 | `false` | `true`
-| Gives the rule with the `id` the new `rule` and one more version; the report 
keeps the version it replaces.
+| Gives the rule with the `id` the new `rule` and one more version; the report 
keeps the version it replaces. Returns
+  `warnings` when the new rule matches nothing in the application now.
 
 | `camel_runtime_chaos_remove`
 | `false` | `false`
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc
index 538caa9f8dd9..fff9e87e943e 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc
@@ -2,7 +2,7 @@
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
 = camel cmd chaos-add
 
-Add a chaos rule to a running integration (camel-chaos, for development only)
+Add chaos rules to a running integration (camel-chaos, for development only)
 
 
 == Usage
@@ -19,7 +19,7 @@ camel cmd chaos-add [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
-| `--rule` | One rule in its one-line text form, as one entry of 
camel.chaos.rules |  | String
+| `--rule` | A rule in its one-line text form, as an entry of 
camel.chaos.rules; can be given more than once |  | List
 | `--timeout` | How long to wait for the integration to answer, in millis or 
as a duration such as 30s | 10000 | long
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index 6ab676025a36..6dbf3a04dd98 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -19,7 +19,7 @@ camel cmd [options]
 |===
 | Subcommand | Description
 | xref:jbang-commands/camel-jbang-cmd-browse.adoc[browse] | Browse pending 
messages on endpoints
-| xref:jbang-commands/camel-jbang-cmd-chaos-add.adoc[chaos-add] | Add a chaos 
rule to a running integration (camel-chaos, for development only)
+| xref:jbang-commands/camel-jbang-cmd-chaos-add.adoc[chaos-add] | Add chaos 
rules to a running integration (camel-chaos, for development only)
 | xref:jbang-commands/camel-jbang-cmd-chaos-change.adoc[chaos-change] | Change 
a chaos rule of a running integration (camel-chaos, for development only)
 | xref:jbang-commands/camel-jbang-cmd-chaos-clear.adoc[chaos-clear] | Remove 
all chaos rules of running integrations (camel-chaos, for development only)
 | xref:jbang-commands/camel-jbang-cmd-chaos-remove.adoc[chaos-remove] | Remove 
a chaos rule of a running integration (camel-chaos, for development only)
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
index 9eb8dba9b8e9..af8e5afe5ede 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java
@@ -19,8 +19,10 @@ package org.apache.camel.cli.connector;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.List;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
 /**
@@ -69,8 +71,14 @@ final class ChaosAction {
                     output.error("A rule is required");
                     return;
                 }
-                answer.put("id", call(type, chaos, "addRule", rule));
+                // one rule or several separated by ; (as camel.chaos.rules), 
added together
+                List<?> ids = (List<?>) call(type, chaos, "addRules", rule);
+                if (ids.size() == 1) {
+                    answer.put("id", ids.get(0));
+                }
+                answer.put("ids", new JsonArray(ids));
                 answer.put("version", 1);
+                warnings(type, chaos, ids, answer);
             } else if ("change".equals(command)) {
                 if (isBlank(id) || isBlank(rule)) {
                     output.error("The id of the rule and the new rule are 
required");
@@ -82,6 +90,7 @@ final class ChaosAction {
                 }
                 answer.put("id", id);
                 answer.put("version", call(type, chaos, "changeRule", id, 
rule));
+                warnings(type, chaos, List.of(id), answer);
             } else if ("remove".equals(command)) {
                 if (isBlank(id)) {
                     output.error("The id of the rule is required");
@@ -117,6 +126,22 @@ final class ChaosAction {
         output.write(answer);
     }
 
+    /**
+     * The warnings of the rules that match nothing in the integration now 
(often a typo), each with the id of its rule,
+     * for the tool to show: the rule is added anyway, as the routes may still 
change.
+     */
+    private static void warnings(Class<?> type, Object chaos, List<?> ids, 
JsonObject answer) throws Exception {
+        JsonArray warnings = new JsonArray();
+        for (Object id : ids) {
+            for (Object warning : (List<?>) call(type, chaos, "checkRule", 
(String) id)) {
+                warnings.add(id + ": " + warning);
+            }
+        }
+        if (!warnings.isEmpty()) {
+            answer.put("warnings", warnings);
+        }
+    }
+
     private static boolean isBlank(String text) {
         return text == null || text.isBlank();
     }
diff --git 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
index 45c480d5814e..46a71e95e65b 100644
--- 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
+++ 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java
@@ -152,6 +152,27 @@ class LocalCliConnectorChaosActionTest {
         assertThat(reportRules()).isEmpty();
     }
 
+    @Test
+    void rulesAreAddedTogetherWithWarningsForTheOnesThatMatchNothing() throws 
Exception {
+        start(null, Map.of("camel.chaos.enabled", "true"));
+
+        CliActionOutcome added = chaos("add", null, "endpoint=mock:target 
error at=dlq; step=tpyo delay duration=1ms");
+        assertThat(added.error()).isNull();
+        assertThat(added.result().<List<Object>> 
getCollection("ids")).containsExactly("r1", "r2");
+        // the id of a single rule only
+        assertThat(added.result().containsKey("id")).isFalse();
+        assertThat(added.result().<List<Object>> 
getCollection("warnings")).singleElement().asString()
+                .startsWith("r2: step=tpyo matches no step of the context 
now");
+
+        CliActionOutcome one = chaos("add", null, "endpoint=mock:target delay 
duration=1ms");
+        assertThat(one.result().getString("id")).isEqualTo("r3");
+        assertThat(one.result().containsKey("warnings")).isFalse();
+
+        CliActionOutcome changed = chaos("change", "r3", "endpoint=mock:taget 
delay duration=1ms");
+        assertThat(changed.result().<List<Object>> 
getCollection("warnings")).singleElement().asString()
+                .startsWith("r3: endpoint=mock:taget matches no endpoint of 
the context now");
+    }
+
     @Test
     void aRuleThatIsNotValidIsRefusedWithTheReason() throws Exception {
         start(null, Map.of("camel.chaos.enabled", "true"));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index b771e638345a..88361384e3ea 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--api-type", "description": 
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
\"[[apigroup\/]version:]kind:[namespace\/]name\", plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": " [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "How [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java
index fd615100286b..5c265e4fed2e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java
@@ -115,4 +115,16 @@ abstract class CamelChaosAction extends ActionBaseCommand {
      * The line printed for the answer of an integration that took the command.
      */
     protected abstract String describe(JsonObject answer);
+
+    /**
+     * The warnings of the answer, one per line after the line of the answer: 
the rules that match nothing in the
+     * integration now (often a typo), which were added anyway.
+     */
+    protected static String warnings(JsonObject answer) {
+        List<Object> warnings = answer.getCollection("warnings");
+        if (warnings == null || warnings.isEmpty()) {
+            return "";
+        }
+        return warnings.stream().map(w -> "\n  Warning: " + 
w).collect(Collectors.joining());
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java
index d08740569a6e..8f4b4a89ed1d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java
@@ -16,30 +16,37 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
 import org.apache.camel.util.json.JsonObject;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "chaos-add",
-                     description = "Add a chaos rule to a running integration 
(camel-chaos, for development only)",
+                     description = "Add chaos rules to a running integration 
(camel-chaos, for development only)",
                      sortOptions = false, showDefaultValues = true,
                      footer = {
                              "%nThe integration must have camel-chaos switched 
on (camel run --chaos, profile dev). The rule is",
                              "checked as camel.chaos.rules is; a rule that is 
not valid is refused with the reason. Its id",
-                             "(r1, r2, ...) is printed, for chaos-change and 
chaos-remove.",
+                             "(r1, r2, ...) is printed, for chaos-change and 
chaos-remove. Several --rule options (or rules",
+                             "separated by ;) are added together, all or none, 
so they are in place at the same time. A rule",
+                             "that matches nothing in the integration now 
(often a typo) is added with a warning.",
                              "%nExamples:",
                              "  camel cmd chaos-add myApp 
--rule='endpoint=kafka:orders* delay duration=2s rate=0.2 until=5m'",
                              "  camel cmd chaos-add myApp 
--rule='endpoint=file:inbox* drop at=poll rate=0.5 until=2m'",
-                             "  camel cmd chaos-add myApp --rule='step=enrich* 
error exception=java.sql.SQLException rate=0.1'" })
+                             "  camel cmd chaos-add myApp --rule='step=enrich* 
error exception=java.sql.SQLException rate=0.1'",
+                             "  camel cmd chaos-add myApp 
--rule='endpoint=seda:dlq error at=dlq' --rule='endpoint=http:stock* error'" })
 public class CamelChaosAddAction extends CamelChaosAction {
 
     @CommandLine.Parameters(description = "Name or pid of the running Camel 
integration (must match one)", arity = "1")
     String name;
 
     @CommandLine.Option(names = { "--rule" }, required = true,
-                        description = "One rule in its one-line text form, as 
one entry of camel.chaos.rules")
-    String rule;
+                        description = "A rule in its one-line text form, as an 
entry of camel.chaos.rules; can be given more"
+                                      + " than once")
+    List<String> rules;
 
     public CamelChaosAddAction(CamelJBangMain main) {
         super(main);
@@ -49,12 +56,18 @@ public class CamelChaosAddAction extends CamelChaosAction {
     public Integer doCall() throws Exception {
         // a rule goes to one integration: its id is only known there
         CamelProcess process = findOne(name);
-        printer().println(describe(chaos(process, "add", root -> 
root.put("rule", rule))));
+        // the rules go together, separated by ; as in camel.chaos.rules (a ; 
inside a rule is quoted)
+        String text = String.join(";", rules);
+        printer().println(describe(chaos(process, "add", root -> 
root.put("rule", text))));
         return 0;
     }
 
     @Override
     protected String describe(JsonObject answer) {
-        return "Added chaos rule " + answer.getString("id");
+        List<Object> ids = answer.getCollection("ids");
+        String added = ids != null && ids.size() > 1
+                ? "Added chaos rules " + 
ids.stream().map(String::valueOf).collect(Collectors.joining(", "))
+                : "Added chaos rule " + answer.getString("id");
+        return added + warnings(answer);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java
index d23f7e917d8c..d76a77e21811 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java
@@ -57,6 +57,7 @@ public class CamelChaosChangeAction extends CamelChaosAction {
 
     @Override
     protected String describe(JsonObject answer) {
-        return "Changed chaos rule " + answer.getString("id") + " (version " + 
answer.get("version") + ")";
+        return "Changed chaos rule " + answer.getString("id") + " (version " + 
answer.get("version") + ")"
+               + warnings(answer);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 3c1d72163917..541cc2a1c106 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -732,10 +732,14 @@ public final class ToolRegistry {
 
         register(tool("chaos_add",
                 "Add a chaos rule to the integration, in the one-line form of 
camel.chaos.rules, e.g. "
-                                   + "'endpoint=kafka:orders* error rate=0.1 
until=5m'. Returns the id of the rule. "
-                                   + "Chaos must be on in the integration; a 
rule that is not valid is refused with "
-                                   + "the reason.")
-                .param("rule", "string", "The rule in its one-line text form, 
as one entry of camel.chaos.rules", true)
+                                   + "'endpoint=kafka:orders* error rate=0.1 
until=5m'; several rules separated by ; "
+                                   + "are added together (all or none). 
Returns the ids of the rules, and warnings for "
+                                   + "a rule that matches nothing in the 
integration now (often a typo: fix it with "
+                                   + "chaos_change). Chaos must be on in the 
integration; a rule that is not valid is "
+                                   + "refused with the reason.")
+                .param("rule", "string",
+                        "The rule in its one-line text form, as an entry of 
camel.chaos.rules; several rules separated by ;",
+                        true)
                 .param("name", "string", CHAOS_NAME, false)
                 .readOnly(false).destructive(true)
                 .executor((ctx, args) -> {
@@ -747,7 +751,7 @@ public final class ToolRegistry {
         register(tool("chaos_change",
                 "Change a chaos rule of the integration: the rule with the id 
gets the new one-line rule and one more "
                                       + "version; the report keeps the version 
it replaces. Returns the id and the new "
-                                      + "version.")
+                                      + "version, and warnings when the new 
rule matches nothing in the integration now.")
                 .param("id", "string", "The id of the rule, such as r1 
(chaos_list shows them)", true)
                 .param("rule", "string", "The new rule in its one-line text 
form", true)
                 .param("name", "string", CHAOS_NAME, false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
index aa189f0f782a..46456d806aee 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.process;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.StringJoiner;
 
 import com.github.freva.asciitable.AsciiTable;
 import com.github.freva.asciitable.Column;
@@ -129,14 +130,18 @@ public class ListChaos extends ProcessWatchCommand {
                 if (r.rule.get("lifecycle") != null) {
                     jo.put("lifecycle", r.rule.get("lifecycle"));
                 }
+                if (r.rule.get("skippedReasons") != null) {
+                    jo.put("skippedReasons", r.rule.get("skippedReasons"));
+                }
                 return jo;
             }).toList()));
             return;
         }
         if (rows.isEmpty()) {
-            printOff(integrations);
+            printNothing(integrations, false);
             return;
         }
+        boolean notes = rows.stream().anyMatch(r -> !note(r.rule).isEmpty());
         printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, 
List.of(
                 new 
Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> 
r.integration.pid),
                 new 
Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30, 
OverflowBehaviour.ELLIPSIS_RIGHT)
@@ -153,6 +158,8 @@ public class ListChaos extends ProcessWatchCommand {
                         .with(r -> String.valueOf(r.rule.get("skipped"))),
                 new Column().header("ENDS 
IN").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
                         .with(r -> endsIn(r.rule)),
+                new 
Column().header("NOTE").visible(notes).dataAlign(HorizontalAlign.LEFT)
+                        .maxWidth(40, OverflowBehaviour.NEWLINE).with(r -> 
note(r.rule)),
                 new 
Column().header("RULE").dataAlign(HorizontalAlign.LEFT).maxWidth(80, 
OverflowBehaviour.NEWLINE)
                         .with(r -> r.rule.getString("rule")))));
     }
@@ -185,7 +192,7 @@ public class ListChaos extends ProcessWatchCommand {
             printReport(integration, report);
         }
         if (!any) {
-            printOff(integrations);
+            printNothing(integrations, true);
         }
     }
 
@@ -209,6 +216,7 @@ public class ListChaos extends ProcessWatchCommand {
         JsonArray rules = report.getCollection("rules");
         if (rules != null && !rules.isEmpty()) {
             List<JsonObject> list = 
rules.stream().map(JsonObject.class::cast).toList();
+            boolean notes = list.stream().anyMatch(r -> !note(r).isEmpty());
             printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, list, 
List.of(
                     new 
Column().header("ID").dataAlign(HorizontalAlign.LEFT).with(r -> 
r.getString("id")),
                     new 
Column().header("VER").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
@@ -221,6 +229,8 @@ public class ListChaos extends ProcessWatchCommand {
                     new 
Column().header("SKIPPED").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
                             .with(r -> String.valueOf(r.get("skipped"))),
                     new 
Column().header("EXPECT").dataAlign(HorizontalAlign.LEFT).with(ListChaos::expectation),
+                    new 
Column().header("NOTE").visible(notes).dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(40, 
OverflowBehaviour.NEWLINE).with(ListChaos::note),
                     new 
Column().header("RULE").dataAlign(HorizontalAlign.LEFT).maxWidth(80, 
OverflowBehaviour.NEWLINE)
                             .with(r -> r.getString("rule")))));
         }
@@ -244,15 +254,53 @@ public class ListChaos extends ProcessWatchCommand {
     }
 
     /**
-     * Without a rule to show, says why for each integration where chaos is 
off, on stderr, so the output stays empty.
+     * Without a rule to show, says why on stderr, so the output stays empty: 
for each integration where chaos is off
+     * the reason, for each one where it is on that it has no rules, and when 
no integration has camel-chaos at all,
+     * that.
      */
-    private void printOff(List<Row> integrations) {
+    private void printNothing(List<Row> integrations, boolean report) {
+        boolean any = false;
         for (Row integration : integrations) {
-            if (integration.chaos != null && !integration.isOn()) {
-                printer().printDiagnostic(integration.name + " (pid " + 
integration.pid + "): "
-                                          + 
integration.chaos.getStringOrDefault("reason", "chaos is off"));
+            if (integration.chaos == null) {
+                continue;
+            }
+            any = true;
+            String prefix = integration.name + " (pid " + integration.pid + 
"): ";
+            if (!integration.isOn()) {
+                printer().printDiagnostic(prefix + 
integration.chaos.getStringOrDefault("reason", "chaos is off"));
+            } else if (!report) {
+                printer().printDiagnostic(prefix + "no chaos rules (add one 
with camel cmd chaos-add)");
             }
         }
+        if (!any && !integrations.isEmpty()) {
+            printer().printDiagnostic("No running integration has camel-chaos: 
run it with camel run --chaos");
+        }
+    }
+
+    /**
+     * What a person should know about a rule, besides its counts: what a 
lifecycle rule did to its route, and why the
+     * rule did nothing when it was skipped.
+     */
+    static String note(JsonObject rule) {
+        StringJoiner sj = new StringJoiner("; ");
+        if (rule.get("lifecycle") instanceof JsonObject l) {
+            String state = l.getString("state");
+            StringBuilder sb = new StringBuilder("route 
").append(l.getString("route")).append(' ').append(state);
+            if (l.get("backIn") instanceof Number n) {
+                sb.append(", back in 
").append(TimeUtils.printDuration(n.longValue()));
+            } else if ("back".equals(state) && l.get("downAt") instanceof 
Number down
+                    && l.get("backAt") instanceof Number back) {
+                sb.append(" after 
").append(TimeUtils.printDuration(back.longValue() - down.longValue()));
+            }
+            if (l.getString("reason") != null) {
+                sb.append(": ").append(l.getString("reason"));
+            }
+            sj.add(sb);
+        }
+        if (rule.get("skippedReasons") instanceof Map<?, ?> reasons && 
rule.get("lifecycle") == null) {
+            reasons.forEach((reason, count) -> sj.add("skipped " + count + ": 
" + reason));
+        }
+        return sj.toString();
     }
 
     private static long fired(JsonObject rule) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
index 51949221bef9..04c7c5abe6ef 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
@@ -159,6 +159,54 @@ class ListChaosTest extends ProcessCommandTestSupport {
         assertThat(out).contains("orders:100 pending (2 completed, 30s left)");
     }
 
+    @Test
+    void theNoteSaysWhatALifecycleRuleDidAndWhyARuleWasSkipped() throws 
Exception {
+        JsonObject chaos = chaosOn();
+        JsonObject stall = rule(1, "active", "route=poller stall 
duration=20s");
+        stall.put("id", "r2");
+        JsonObject lifecycle = new JsonObject();
+        lifecycle.put("action", "suspend");
+        lifecycle.put("route", "poller");
+        lifecycle.put("state", "down");
+        lifecycle.put("backIn", 12000L);
+        stall.put("lifecycle", lifecycle);
+        JsonObject corrupt = rule(1, "active", "route=shipping corrupt 
mode=charset");
+        corrupt.put("id", "r3");
+        corrupt.put("skippedReasons", new JsonObject(Map.of("a stream body", 
79L)));
+        JsonArray rules = chaos.getCollection("rules");
+        rules.add(stall);
+        rules.add(corrupt);
+        writeStatus(chaos);
+
+        assertThat(run()).isZero();
+
+        String out = printer.getStdout();
+        assertThat(out).contains("NOTE");
+        assertThat(lineOf(out, "r2")).contains("route poller down, back in 
12s");
+        assertThat(lineOf(out, "r3")).contains("skipped 79: a stream body");
+    }
+
+    @Test
+    void withoutANoteThereIsNoNoteColumn() throws Exception {
+        writeStatus(chaosOn());
+
+        assertThat(run()).isZero();
+
+        assertThat(printer.getStdout()).doesNotContain("NOTE");
+    }
+
+    @Test
+    void noRulesSaysSo() throws Exception {
+        JsonObject chaos = chaosOn();
+        chaos.put("rules", new JsonArray());
+        writeStatus(chaos);
+
+        assertThat(run()).isZero();
+
+        assertThat(printer.getStdout()).isEmpty();
+        assertThat(printer.getStderr()).contains("orders (pid " + TEST_PID + 
"): no chaos rules");
+    }
+
     @Test
     void chaosOff() throws Exception {
         writeStatus(new JsonObject(Map.of("enabled", false, "reason", "Chaos 
is off: it stays off with profile prod")));
@@ -186,7 +234,9 @@ class ListChaosTest extends ProcessCommandTestSupport {
 
         assertThat(run()).isZero();
 
-        assertThat(printer.getOutput()).isEmpty();
+        // nothing on stdout, and why on stderr
+        assertThat(printer.getStdout()).isEmpty();
+        assertThat(printer.getStderr()).contains("No running integration has 
camel-chaos: run it with camel run --chaos");
     }
 
     private int run(String... args) throws Exception {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 339dcf2e26eb..e5b3c737a0a2 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -1094,11 +1094,14 @@ public class RuntimeTools {
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = """
                   Add a chaos rule to the Camel application, in the one-line 
form of camel.chaos.rules, e.g. \
-                  'endpoint=kafka:orders* error rate=0.1 until=5m'. Returns 
the id of the rule. Chaos must be on in \
-                  the application; a rule that is not valid is refused with 
the reason.""")
+                  'endpoint=kafka:orders* error rate=0.1 until=5m'; several 
rules separated by ; are added together \
+                  (all or none). Returns the ids of the rules, and warnings 
for a rule that matches nothing in the \
+                  application now (often a typo: fix it with 
camel_runtime_chaos_change). Chaos must be on in the \
+                  application; a rule that is not valid is refused with the 
reason.""")
     public JsonObject camel_runtime_chaos_add(
             @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid,
-            @ToolArg(description = "The rule in its one-line text form, as one 
entry of camel.chaos.rules") String rule) {
+            @ToolArg(description = "The rule in its one-line text form, as an 
entry of camel.chaos.rules; several"
+                                   + " rules separated by ;") String rule) {
         requireArg("rule", rule);
         return delegateToRegistry("chaos_add", nameOrPid, Map.of("rule", 
rule));
     }
@@ -1107,7 +1110,8 @@ public class RuntimeTools {
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = """
                   Change a chaos rule of the Camel application: the rule with 
the id gets the new one-line rule and \
-                  one more version; the report keeps the version it replaces. 
Returns the id and the new version.""")
+                  one more version; the report keeps the version it replaces. 
Returns the id and the new version, \
+                  and warnings when the new rule matches nothing in the 
application now.""")
     public JsonObject camel_runtime_chaos_change(
             @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid,
             @ToolArg(description = "The id of the rule, such as r1 
(camel_runtime_chaos_list shows them)") String id,

Reply via email to