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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new a12f29b0fa99 CAMEL-26788: camel-jbang - camel cmd chaos-explore saves 
its report, and a replayable scenario per failed experiment, in test/chaos 
(#29514)
a12f29b0fa99 is described below

commit a12f29b0fa99068fb4f14ebbf927e921c2d4c875
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 00:55:28 2026 +0100

    CAMEL-26788: camel-jbang - camel cmd chaos-explore saves its report, and a 
replayable scenario per failed experiment, in test/chaos (#29514)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |   3 +-
 components/camel-chaos/src/main/docs/chaos.adoc    |   3 +-
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |   5 +-
 .../camel-jbang-cmd-chaos-experiment.adoc          |   2 +-
 .../camel-jbang-cmd-chaos-explore.adoc             |   1 +
 .../apache/camel/cli/connector/ChaosAction.java    |   2 +
 .../LocalCliConnectorChaosActionTest.java          |   8 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../apache/camel/dsl/jbang/core/commands/Ask.java  |   6 +
 .../dsl/jbang/core/commands/ChaosExplore.java      | 185 ++++++++++++++++++++-
 .../action/CamelChaosExperimentAction.java         |  11 +-
 .../core/commands/ai/ChaosExplorePlaybook.java     |  28 +++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  47 +++---
 .../dsl/jbang/core/common/ChaosExperimentRun.java  |  73 ++++++++
 .../dsl/jbang/core/commands/ChaosExploreTest.java  |  81 ++++++++-
 .../core/commands/action/CamelChaosActionTest.java |   7 +
 .../jbang/core/common/ChaosExperimentRunTest.java  |  70 +++++++-
 .../src/test/resources/golden/cli-help.txt         |  14 +-
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |   7 +-
 19 files changed, 499 insertions(+), 56 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 0c8b96325a9c..3a4f00f07c39 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
@@ -439,7 +439,8 @@ The Camel CLI runs an experiment in one call: `camel cmd 
chaos-experiment` begin
 `--body`, `--repeat` and `--interval`, as `camel cmd send`), waits until its 
expectations are decided (at most
 `--wait`), and ends it. `camel cmd chaos-explore-start` and 
`chaos-explore-finish` start and finish the exploration.
 `camel cmd chaos-explore` hands a whole exploration to an AI: it runs the 
experiments by itself for the budget, and
-reports what held and what broke (the MCP prompt `camel_chaos_explore` is the 
same playbook).
+reports what held and what broke (the MCP prompt `camel_chaos_explore` is the 
same playbook). It saves the report in
+`test/chaos/reports`, and each experiment that failed as a scenario in 
`test/chaos`, to replay it.
 
 == Scenarios
 
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 0c8b96325a9c..3a4f00f07c39 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -439,7 +439,8 @@ The Camel CLI runs an experiment in one call: `camel cmd 
chaos-experiment` begin
 `--body`, `--repeat` and `--interval`, as `camel cmd send`), waits until its 
expectations are decided (at most
 `--wait`), and ends it. `camel cmd chaos-explore-start` and 
`chaos-explore-finish` start and finish the exploration.
 `camel cmd chaos-explore` hands a whole exploration to an AI: it runs the 
experiments by itself for the budget, and
-reports what held and what broke (the MCP prompt `camel_chaos_explore` is the 
same playbook).
+reports what held and what broke (the MCP prompt `camel_chaos_explore` is the 
same playbook). It saves the report in
+`test/chaos/reports`, and each experiment that failed as a scenario in 
`test/chaos`, to replay it.
 
 == Scenarios
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 4720ad63b47e..e744bbfe3775 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -954,7 +954,10 @@ camel cmd chaos-explore --budget=10m
 camel cmd chaos-explore myApp --budget=1h --focus=refunds --api-type=anthropic
 ----
 
-Run the integration with `--stub=remote`, so the experiments do not reach real 
systems. A route fed over HTTP or from
+It saves what it found in the project: the report and the journal in 
`test/chaos/reports/explore-<time>.md` (and
+`.json`), and each experiment that failed as a scenario, 
`test/chaos/explore-<time>-<n>.chaos`, which
+`camel run * --chaos=<file>` or `camel cmd chaos-load` replays (`--no-save` 
leaves them out). Run the integration with
+`--stub=remote`, so the experiments do not reach real systems. A route fed 
over HTTP or from
 files cannot be fed by an experiment yet. MCP clients have the same playbook 
as the prompt `camel_chaos_explore`. See
 xref:next@components:others:chaos.adoc#_an_exploration[An exploration] in the 
camel-chaos documentation.
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
index f058c4f411d6..a8793619685d 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
@@ -29,7 +29,7 @@ camel cmd chaos-experiment [options]
 | `--repeat` | How many messages to send in all (at most 10000) |  | int
 | `--rule` | A rule in its one-line text form, as an entry of 
camel.chaos.rules; can be given more than once, and one of them needs an 
expectation (expect=) |  | List
 | `--timeout` | How long to wait for the integration to answer, in millis or 
as a duration such as 30s | 10000 | long
-| `--wait` | The longest wait for the verdict after the messages were sent (at 
most 10m) | 2m | String
+| `--wait` | The longest wait for the verdict after the messages were sent, 
and at least the within= of the expectations (at most 10m) | 2m | String
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc
index 8ddbf5b6a012..eb684864ce1a 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc
@@ -25,6 +25,7 @@ camel cmd chaos-explore [options]
 | `--focus` | What to explore, such as a route id (default: all routes) |  | 
String
 | `--max-iterations` | Maximum number of tool-calling rounds | 100 | int
 | `--model` | Model to use. Default: the provider's default; with Ollama the 
installed model best at tool calling |  | String
+| `--save` | Save the report in test/chaos/reports (as markdown, and the 
journal as JSON), and each experiment that failed as a scenario in test/chaos, 
to replay it | true | boolean
 | `--show-tools` | Show the tool calls and results as they happen |  | boolean
 | `--timeout` | Timeout in seconds for one answer of the LLM (an experiment 
can take minutes) | 300 | int
 | `--tools` | The tools the model gets: core (a small set, for a local model), 
full, or auto | auto | ToolSet
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 e313d421dca3..1d3cbfceac55 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
@@ -161,6 +161,8 @@ final class ChaosAction {
                 Map<String, Object> begun = (Map<String, Object>) call(type, 
chaos, "beginExperiment",
                         root.getString("hypothesis"), rule);
                 answer.putAll(begun);
+                // a rule that matches nothing makes the experiment say 
nothing: as with add, the reason goes along
+                warnings(type, chaos, (List<?>) begun.get("rules"), answer);
             } else if ("experiment-end".equals(command)) {
                 // what came of it, and its rules are removed
                 @SuppressWarnings("unchecked")
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 eae9ee24a76d..1f41b807f294 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
@@ -278,6 +278,14 @@ class LocalCliConnectorChaosActionTest {
         assertThat(ended.result().getString("verdict")).isEqualTo("FAIL");
         assertThat(ended.result().getString("note")).isEqualTo("no error 
handler");
         assertThat(chaos("experiment-end", null, null).error()).isEqualTo("No 
experiment is open");
+
+        // a rule that matches nothing (a typo) comes with its warning, as 
with add
+        action.put("rule", "endpoint=file:/nowhere error 
expect=start:failed<=0 within=1m");
+        CliActionOutcome typo = CliActionOutcome.dispatch(connector, 
action).get(10, TimeUnit.SECONDS);
+        assertThat(typo.error()).isNull();
+        assertThat(typo.result().<List<Object>> 
getCollection("warnings")).singleElement().asString()
+                .startsWith("r2: endpoint=file:/nowhere matches no");
+        assertThat(chaos("experiment-end", null, null).error()).isNull();
         assertThat(chaos("experiment-begin", null, 
null).error()).isEqualTo("The rules of the experiment are required");
     }
 
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 b7bba9ca7fc9..f05a290e35fb 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/Ask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
index dbdccd7ca25b..fef688a641cd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
@@ -131,6 +131,10 @@ public class Ask extends CamelCommand {
 
     // the tools the model does not get, such as starting a chaos exploration 
that camel cmd chaos-explore started
     Set<String> hiddenTools = Set.of();
+    // the last answer of the model, such as the report of camel cmd 
chaos-explore, which it saves
+    String lastAnswer;
+    // the model that answered, and its API, such as qwen3.6 (ollama)
+    String usedModel;
 
     private long targetPid;
     private AskTools askTools;
@@ -170,6 +174,7 @@ public class Ask extends CamelCommand {
             return 1;
         }
         warnSmallModel(client.model, client.ollamaParameterBillions());
+        usedModel = client.model + " (" + client.apiType + ")";
 
         RuntimeHelper.ProcessInfo process = findProcess(nameOrPid);
         if (process != null) {
@@ -296,6 +301,7 @@ public class Ask extends CamelCommand {
                     continue;
                 }
                 
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
List.of()));
+                lastAnswer = response.text();
                 printStats(totalUsage, startTime);
                 return 0;
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
index 4a59b7eef5fc..fcfa6e1cad4e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java
@@ -16,6 +16,11 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -31,6 +36,7 @@ import 
org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 import picocli.CommandLine.Parameters;
@@ -110,6 +116,11 @@ public class ChaosExplore extends CamelCommand {
             description = "Show the tool calls and results as they happen")
     boolean showTools;
 
+    @Option(names = { "--save" }, negatable = true, defaultValue = "true", 
fallbackValue = "true",
+            description = "Save the report in test/chaos/reports (as markdown, 
and the journal as JSON), and each"
+                          + " experiment that failed as a scenario in 
test/chaos, to replay it")
+    boolean save = true;
+
     @Option(names = { "--verbose" },
             description = "Print debug information of the LLM calls")
     boolean verbose;
@@ -133,34 +144,64 @@ public class ChaosExplore extends CamelCommand {
             printer().printErr(started.error());
             return started.exitCode();
         }
-        printer().println("Let there be chaos: " + process.name() + " (pid " + 
process.pid() + "), "
+        String integration = displayName(process);
+        printer().println("Let there be chaos: " + integration + " (pid " + 
process.pid() + "), "
                           + 
TimeUtils.printDuration(started.reply().getLongOrDefault("budget", 0))
                           + (focus != null && !focus.isBlank() ? ", focus: " + 
focus : ""));
         int answer;
+        Ask ask = ask(process, integration);
         try {
-            answer = ask(process).doCall();
+            answer = ask.doCall();
         } catch (Exception e) {
             // the LLM fails with an exit code; anything else must not cost 
the report of what was explored so far
-            printer().printErr("camel ask failed: " + e.getMessage());
+            // an exception without a message is known by its type
+            printer().printErr("camel ask failed: " + (e.getMessage() != null 
? e.getMessage() : e.getClass().getSimpleName()));
             answer = CliExitCode.FAILURE;
         } finally {
             // the AI may have finished it already; else its rules go now
             ActionClient.execute(process, "chaos", root -> root.put("command", 
"explore-finish"), 10000);
         }
-        JsonObject exploration = 
ChaosExperimentRun.exploration(RuntimeHelper.readStatus(process.pid()));
+        JsonObject status = RuntimeHelper.readStatus(process.pid());
+        JsonObject exploration = ChaosExperimentRun.exploration(status);
         List<String> lines = journal(exploration);
         printer().println();
         lines.forEach(printer()::println);
+        if (save && exploration != null) {
+            try {
+                Saved saved = save(Path.of("test", "chaos"), 
LocalDateTime.now(),
+                        new Run(integration, budget, focus, ask.usedModel, 
seed(status)), exploration, ask.lastAnswer,
+                        lines);
+                printer().println();
+                printer().println("Report: " + saved.report());
+                saved.scenarios()
+                        .forEach(s -> printer().println("Scenario: " + s + " 
(replay: camel run * --chaos=" + s + ")"));
+            } catch (IOException e) {
+                // the journal is printed already: say why it is not saved, 
rather than a stack trace
+                printer().printErr("Could not save the report in test/chaos: " 
+ e.getMessage());
+            }
+        }
         if (failed(exploration)) {
             return CliExitCode.FAILURE;
         }
         return answer;
     }
 
+    /** The name of the integration as camel get chaos has it: its 
CamelContext, else the name of the process. */
+    static String displayName(CamelProcess process) {
+        JsonObject status = process.status();
+        if (status != null && status.get("context") instanceof JsonObject 
context) {
+            String name = context.getString("name");
+            if (name != null && !name.isBlank() && !"CamelJBang".equals(name)) 
{
+                return name;
+            }
+        }
+        return process.name();
+    }
+
     /** camel ask, with the playbook as its question, for the integration. */
-    Ask ask(CamelProcess process) {
+    Ask ask(CamelProcess process, String integration) {
         Ask ask = new Ask(getMain());
-        ask.question = 
List.of(ChaosExplorePlaybook.text(ChaosExplorePlaybook.ASK, process.name(), 
budget, focus, true));
+        ask.question = 
List.of(ChaosExplorePlaybook.text(ChaosExplorePlaybook.ASK, integration, 
budget, focus, true));
         ask.nameOrPid = Long.toString(process.pid());
         ask.url = url;
         ask.apiType = apiType;
@@ -173,11 +214,135 @@ public class ChaosExplore extends CamelCommand {
         ask.showTools = showTools;
         ask.showStats = true;
         ask.verbose = verbose;
-        // the exploration runs with the budget of the user: the AI does not 
start another one
-        ask.hiddenTools = Set.of("chaos_explore_start");
+        // the exploration runs with the budget of the user: the AI does not 
start another one, not even with a camel
+        // command, and the chaos tools are all it needs
+        ask.hiddenTools = Set.of("chaos_explore_start", "cli_exec");
         return ask;
     }
 
+    /** What the exploration ran with, for the report. */
+    record Run(String integration, String budget, String focus, String model, 
Long seed) {
+    }
+
+    /** The files written: the report (markdown, with the journal as JSON next 
to it), and a scenario per failure. */
+    record Saved(Path report, Path journal, List<Path> scenarios) {
+    }
+
+    private static Long seed(JsonObject status) {
+        if (status != null && status.get("chaos") instanceof JsonObject chaos
+                && chaos.get("report") instanceof JsonObject report && 
report.get("seed") instanceof Number n) {
+            return n.longValue();
+        }
+        return null;
+    }
+
+    /**
+     * Saves what the exploration found (CAMEL-26788): the report of the AI 
with the journal of the experiments as
+     * markdown in reports/, the journal as JSON next to it, and each 
experiment that failed as a scenario, so camel run
+     * --chaos or camel cmd chaos-load replays its verdict.
+     *
+     * @param dir the chaos folder of the project, test/chaos, which camel run 
and export leave alone
+     */
+    static Saved save(
+            Path dir, LocalDateTime when, Run run, JsonObject exploration, 
String report, List<String> journal)
+            throws IOException {
+        String stamp = 
when.format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
+        String date = when.format(DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm"));
+        Path reports = Files.createDirectories(dir.resolve("reports"));
+        List<Path> scenarios = new ArrayList<>();
+        List<Object> experiments = exploration.getCollection("experiments");
+        if (experiments != null) {
+            for (Object o : experiments) {
+                if (!(o instanceof JsonObject e) || 
!"FAIL".equals(e.getString("verdict"))) {
+                    continue;
+                }
+                Path scenario = dir.resolve("explore-" + stamp + "-" + 
e.getIntegerOrDefault("number", 0) + ".chaos");
+                Files.writeString(scenario, scenario(e, run, date, scenario));
+                scenarios.add(scenario);
+            }
+        }
+
+        StringBuilder md = new StringBuilder();
+        md.append("# Chaos exploration: 
").append(run.integration()).append("\n\n");
+        md.append(date).append(" · budget ").append(run.budget());
+        if (run.focus() != null && !run.focus().isBlank()) {
+            md.append(" · focus ").append(run.focus());
+        }
+        if (run.model() != null) {
+            md.append(" · ").append(run.model());
+        }
+        if (run.seed() != null) {
+            md.append(" · seed ").append(run.seed());
+        }
+        md.append("\n\n## Report\n\n").append(report != null && 
!report.isBlank()
+                ? report.strip() : "(the AI wrote no report)").append("\n\n## 
Experiments\n\n```text\n");
+        journal.forEach(line -> md.append(line).append('\n'));
+        md.append("```\n");
+        if (!scenarios.isEmpty()) {
+            md.append("\n## Scenarios\n\nEach experiment that failed, to 
replay its verdict:\n\n");
+            for (Path s : scenarios) {
+                md.append("- `").append(s).append("`: `camel run * 
--chaos=").append(s).append("`\n");
+            }
+        }
+        Path markdown = reports.resolve("explore-" + stamp + ".md");
+        Files.writeString(markdown, md.toString());
+
+        JsonObject json = new JsonObject();
+        json.put("integration", run.integration());
+        json.put("budget", run.budget());
+        if (run.focus() != null) {
+            json.put("focus", run.focus());
+        }
+        if (run.model() != null) {
+            json.put("model", run.model());
+        }
+        if (run.seed() != null) {
+            json.put("seed", run.seed());
+        }
+        json.put("report", report);
+        json.put("exploration", exploration);
+        json.put("scenarios", scenarios.stream().map(Path::toString).toList());
+        Path journalFile = reports.resolve("explore-" + stamp + ".json");
+        Files.writeString(journalFile, Jsoner.prettyPrint(json.toJson()));
+        return new Saved(markdown, journalFile, scenarios);
+    }
+
+    /** A failed experiment as a scenario: what it tested and found as 
comments, the seed, and its rules. */
+    static String scenario(JsonObject experiment, Run run, String date, Path 
file) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("# From the chaos exploration of 
").append(run.integration()).append(" on ").append(date)
+                .append(", experiment 
").append(experiment.getIntegerOrDefault("number", 0)).append(": ")
+                .append(experiment.getString("verdict")).append('\n');
+        String hypothesis = experiment.getString("hypothesis");
+        if (hypothesis != null) {
+            sb.append("# hypothesis: ").append(hypothesis).append('\n');
+        }
+        List<Object> expectations = experiment.getCollection("expectations");
+        if (expectations != null) {
+            for (Object o : expectations) {
+                if (!(o instanceof JsonObject x)) {
+                    continue;
+                }
+                sb.append("# expect ").append(x.getString("expect")).append(": 
").append(x.getString("state"))
+                        .append(", 
").append(x.getString("value")).append('\n');
+            }
+        }
+        String note = experiment.getString("note");
+        if (note != null) {
+            sb.append("# note: ").append(note).append('\n');
+        }
+        sb.append("# replay: camel run * --chaos=").append(file)
+                .append(" (and send it messages as the experiment did, when 
its routes do not feed themselves)\n");
+        if (run.seed() != null) {
+            sb.append("seed=").append(run.seed()).append('\n');
+        }
+        List<Object> rules = experiment.getCollection("ruleTexts");
+        if (rules != null) {
+            rules.forEach(r -> sb.append(r).append('\n'));
+        }
+        return sb.toString();
+    }
+
     /** The journal of the exploration: how it ended, and a line per 
experiment with its expectations. */
     static List<String> journal(JsonObject exploration) {
         List<String> lines = new ArrayList<>();
@@ -197,7 +362,9 @@ public class ChaosExplore extends CamelCommand {
                   + (count == 1 ? " experiment" : " experiments") + ", " + 
failed + " failed");
         if (experiments != null) {
             for (Object o : experiments) {
-                lines.addAll(ListChaos.experimentLines((JsonObject) o, "  "));
+                if (o instanceof JsonObject e) {
+                    lines.addAll(ListChaos.experimentLines(e, "  "));
+                }
             }
         }
         return lines;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
index 3d6a42842a7f..91eda0f15abe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
@@ -97,7 +97,8 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
     String interval;
 
     @CommandLine.Option(names = { "--wait" }, defaultValue = "2m",
-                        description = "The longest wait for the verdict after 
the messages were sent (at most 10m)")
+                        description = "The longest wait for the verdict after 
the messages were sent, and at least the"
+                                      + " within= of the expectations (at most 
10m)")
     String wait;
 
     @CommandLine.Option(names = { "--note" },
@@ -146,6 +147,10 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
                         feed, absolute(body), headers != null ? 
String.join("\n", headers) : null,
                         repeat, interval)
                 : null;
+        String why = traffic != null ? 
ChaosExperimentRun.checkFeed(RuntimeHelper.readStatus(process.pid()), feed) : 
null;
+        if (why != null) {
+            throw CliException.usage(why.replace("The feed", "--feed"));
+        }
         JsonObject result = ChaosExperimentRun.run(new 
ChaosExperimentRun.Integration() {
             @Override
             public JsonObject chaos(String command, Consumer<JsonObject> 
options) {
@@ -188,6 +193,10 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
         if (!lines.isEmpty()) {
             lines.set(0, "Experiment " + lines.get(0));
         }
+        List<Object> warnings = experiment.getCollection("warnings");
+        if (warnings != null) {
+            warnings.forEach(w -> lines.add("  Warning: " + w));
+        }
         JsonObject traffic = (JsonObject) experiment.get("traffic");
         if (traffic != null) {
             lines.add("  traffic: " + traffic(traffic));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
index 33a80209ac75..181f53d49339 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
@@ -80,20 +80,29 @@ public final class ChaosExplorePlaybook {
                 route still completing its messages in time.
                    - hypothesis: what should hold, in a few words, such as "a 
failed refund is retried, not lost"
                    - rule: the suggested rule with a short until= (such as 
until=30s), and one expectation that \
-                decides the hypothesis, with within= a little longer than 
until=: expect=<route>:failed<=0 \
-                (nothing fails), expect=<route>-><next route>:lost<=0 (nothing 
lost between two routes), \
+                decides the hypothesis, with within= a little longer than 
until=. Expect 0 where nothing may go \
+                wrong, not a guess such as failed<=5, which hides the messages 
that were lost: expect=<route>:failed<=0 \
+                (nothing fails), expect=<route>-><next route>:lost<=0 (nothing 
lost when a route hands messages to \
+                the next route, as the suggestions of kind lost name them), \
                 expect=<route>:20 (20 completed), expect=<route>:maxTime<=5s 
(none slower than 5s)
-                   - feed: when the feed of the suggestion is kind send, its 
endpoint, with body, repeat (such as \
-                20) and interval (such as 100..300); leave feed out when the 
route feeds itself (kind self). \
+                   - feed: a route that does not feed itself gets no messages 
unless you give feed. When the feed \
+                of the suggestion is kind send, always give its endpoint, with 
body (a message as the route \
+                expects it, such as JSON), repeat (such as 20) and interval 
(such as 100..300); leave feed out \
+                only when the route feeds itself (kind self). \
                 Routes fed over http or from files cannot be fed by an 
experiment yet: try them only when they \
                 feed themselves
+                   - a rule at=dlq fails the dead letter channel, so it fires 
only when a message goes there: use \
+                the combined suggestions, which add the fault that sends 
messages there too. A message the \
+                dead letter channel did not deliver is handled there, so it 
does not fail in its route: the \
+                experiment counts it as lost at the dead letter channel, and 
fails then
                    - wait: longer than the within= of the expectation
                    - note: what you make of the result, in one sentence
                 4. Read the result: the verdict (FAIL, PASS or INCONCLUSIVE), 
the expectations, what the routes \
-                did, and the traffic. On FAIL, call %s for the failed 
exchanges and their exceptions, and say why it \
+                did, the traffic, and the warnings: a rule that matched 
nothing made the experiment say nothing, so \
+                fix the rule (its pattern, such as endpoint=file://parked) and 
run it again. On FAIL, call %s for the failed exchanges and their exceptions, 
and say why it \
                 broke. On INCONCLUSIVE, run it again with a longer wait. On 
PASS, move on.
-                5. Go on while timeLeft of the result is more than the next 
experiment needs (its until= and wait, \
-                plus a minute). Stop with about a minute left.
+                5. Go on while timeLeft of the result (millis) is more than 
the next experiment needs: its wait \
+                plus a minute. When it is less, do not begin another 
experiment: finish.
                 6. Finish with %s, and write the report:
                    - Summary: how many experiments, how many held and how many 
broke
                    - Findings: per FAIL the route, what broke, the evidence 
(the expectation and its actual value, \
@@ -102,7 +111,8 @@ public final class ChaosExplorePlaybook {
                 exist; say when it looks like a bug in Camel rather than in 
the route
                    - What held: one line per PASS
                    - Not tried: the kinds of fault or routes left out, and why 
(no feed, out of time)
-                """.formatted(where, time, about, start, tools.report(), 
tools.suggest(), tools.experiment(),
-                tools.errors(), tools.finish());
+                """
+                .formatted(where, time, about, start, tools.report(), 
tools.suggest(), tools.experiment(),
+                        tools.errors(), tools.finish());
     }
 }
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 0c9f44d0454f..045e389c4808 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
@@ -963,7 +963,8 @@ public final class ToolRegistry {
                                           + "INCONCLUSIVE: the wait was up 
before one was decided, so wait longer than "
                                           + "its within=), the expectations 
with their actual values, the faults per "
                                           + "rule, what the routes did 
meanwhile, the traffic sent, and the time left "
-                                          + "of the exploration (timeLeft, 
millis). chaos_suggest has rules to try and "
+                                          + "of the exploration (timeLeft, 
millis), and warnings for a rule that matched "
+                                          + "nothing (fix it and run again). 
chaos_suggest has rules to try and "
                                           + "the feed of each route.")
                 .param("rule", "string",
                         "The rules in their one-line text form, separated by 
;, one of them with expect=, such as"
@@ -982,7 +983,7 @@ public final class ToolRegistry {
                         "The pause between two messages in millis, or a range 
such as 100..500 (at most 60000)", false)
                 .param("wait", "string",
                         "The longest wait for the verdict after the messages 
were sent, such as 90s (default 2m, at most"
-                                         + " 10m)",
+                                         + " 10m); it is at least the within= 
of the expectations",
                         false)
                 .param("note", "string", "What to note with the experiment in 
the journal", false)
                 .param("name", "string", CHAOS_NAME, false)
@@ -1005,26 +1006,32 @@ public final class ToolRegistry {
                                     feed, args.get("body"), 
args.get("headers"), repeat,
                                     args.get("interval"))
                             : null;
-                    return ChaosExperimentRun.run(new 
ChaosExperimentRun.Integration() {
-                        @Override
-                        public JsonObject chaos(String command, 
Consumer<JsonObject> options) {
-                            String answer = ctx.executeAction("chaos", root -> 
{
-                                root.put("command", command);
-                                options.accept(root);
-                            });
-                            return Jsoner.deserialize(answer, new 
JsonObject());
-                        }
+                    // the run checks the feed first: an experiment without 
messages says nothing, so the AI gets the
+                    // routes to feed instead, as a tool error (and the status 
is read once for it)
+                    try {
+                        return ChaosExperimentRun.run(new 
ChaosExperimentRun.Integration() {
+                            @Override
+                            public JsonObject chaos(String command, 
Consumer<JsonObject> options) {
+                                String answer = ctx.executeAction("chaos", 
root -> {
+                                    root.put("command", command);
+                                    options.accept(root);
+                                });
+                                return Jsoner.deserialize(answer, new 
JsonObject());
+                            }
 
-                        @Override
-                        public JsonObject send(ChaosExperimentRun.Feed f) {
-                            return ctx.sendMessage(f.endpoint(), f.body(), 
f.headers(), f.repeat(), f.interval());
-                        }
+                            @Override
+                            public JsonObject send(ChaosExperimentRun.Feed f) {
+                                return ctx.sendMessage(f.endpoint(), f.body(), 
f.headers(), f.repeat(), f.interval());
+                            }
 
-                        @Override
-                        public JsonObject status() {
-                            return ctx.readFullStatus();
-                        }
-                    }, args.get("hypothesis"), rule, traffic, wait, 
args.get("note")).toJson();
+                            @Override
+                            public JsonObject status() {
+                                return ctx.readFullStatus();
+                            }
+                        }, args.get("hypothesis"), rule, traffic, wait, 
args.get("note")).toJson();
+                    } catch (IllegalArgumentException e) {
+                        throw new ToolExecutionException(e.getMessage(), e);
+                    }
                 }));
 
         register(tool("chaos_explore_finish",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
index 742c29a7c6dc..4be77ccc7705 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
@@ -17,7 +17,10 @@
 package org.apache.camel.dsl.jbang.core.common;
 
 import java.util.List;
+import java.util.StringJoiner;
 import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonObject;
@@ -132,6 +135,18 @@ public final class ChaosExperimentRun {
     static JsonObject run(
             Integration integration, String hypothesis, String rules, Feed 
feed, long wait, String note, Clock clock)
             throws InterruptedException {
+        if (feed != null) {
+            // a feed that reaches no route makes an experiment without 
messages, whose verdict says nothing
+            String why = checkFeed(integration.status(), feed.endpoint());
+            if (why != null) {
+                throw new IllegalArgumentException(why);
+            }
+        }
+        // an expectation such as failed<=0 is decided when its within= is up: 
a shorter wait leaves it undecided
+        long within = longestWithin(rules);
+        if (within + POLL * 2 > wait) {
+            wait = Math.min(within + POLL * 2, MAX_WAIT);
+        }
         JsonObject begun = integration.chaos("experiment-begin", root -> {
             if (hypothesis != null && !hypothesis.isBlank()) {
                 root.put("hypothesis", hypothesis);
@@ -139,6 +154,8 @@ public final class ChaosExperimentRun {
             root.put("rule", rules);
         });
         int number = begun.getIntegerOrDefault("number", 0);
+        // a rule that matches nothing in the integration now (often a typo)
+        List<Object> warnings = begun.getCollection("warnings");
         JsonObject traffic = null;
         long waited = 0;
         try {
@@ -170,6 +187,9 @@ public final class ChaosExperimentRun {
             answer.put("traffic", traffic);
         }
         answer.put("waited", waited);
+        if (warnings != null && !warnings.isEmpty()) {
+            answer.put("warnings", warnings);
+        }
         JsonObject exploration = exploration(integration.status());
         if (exploration != null && exploration.containsKey("timeLeft")) {
             answer.put("timeLeft", exploration.getLong("timeLeft"));
@@ -177,6 +197,22 @@ public final class ChaosExperimentRun {
         return answer;
     }
 
+    /** The longest within= of the rules, in millis, or 0 (an expectation 
without it has the time of its rule). */
+    static long longestWithin(String rules) {
+        long answer = 0;
+        Matcher m = WITHIN.matcher(rules);
+        while (m.find()) {
+            try {
+                answer = Math.max(answer, 
TimeUtils.toMilliSeconds(m.group(1)));
+            } catch (RuntimeException e) {
+                // not a time: camel-chaos refuses the rule with the reason
+            }
+        }
+        return answer;
+    }
+
+    private static final Pattern WITHIN = 
Pattern.compile("(?:^|[\\s;])within=([^\\s;]+)");
+
     /** Sends the traffic: the summary of the send, or why it could not be 
sent. */
     private static JsonObject traffic(Integration integration, Feed feed) {
         JsonObject answer;
@@ -219,6 +255,43 @@ public final class ChaosExperimentRun {
         }
     }
 
+    /**
+     * Why the feed reaches no route of the integration, or null when it does 
(or the status does not tell): the feed is
+     * the id of a route, a pattern, or the endpoint a route starts from, as 
camel cmd send finds its target.
+     */
+    public static String checkFeed(JsonObject status, String feed) {
+        List<Object> routes = status != null ? status.getCollection("routes") 
: null;
+        if (routes == null || routes.isEmpty() || feed.endsWith("*")) {
+            return null;
+        }
+        String endpoint = normalize(feed);
+        StringJoiner inputs = new StringJoiner(", ");
+        for (Object o : routes) {
+            if (!(o instanceof JsonObject route)) {
+                continue;
+            }
+            String id = route.getString("routeId");
+            String from = route.getString("from");
+            if (feed.equals(id)) {
+                return null;
+            }
+            if (from != null) {
+                String input = normalize(from);
+                if (input.equals(endpoint) || input.startsWith(endpoint + 
"?")) {
+                    return null;
+                }
+            }
+            inputs.add(id + " (" + from + ")");
+        }
+        return "The feed " + feed + " is not where a route starts, so no 
message would reach the integration: give the"
+               + " id of a route, or the endpoint it starts from: " + inputs;
+    }
+
+    // kafka://refunds and kafka:refunds are the same endpoint
+    private static String normalize(String uri) {
+        return uri.replace("://", ":");
+    }
+
     /** The experiment with the number in the report of the status, or null. */
     static JsonObject experiment(JsonObject status, int number) {
         JsonObject exploration = exploration(status);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
index fa94b0c72251..0741551c995a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java
@@ -16,13 +16,18 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import picocli.CommandLine;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -39,7 +44,7 @@ class ChaosExploreTest {
         CommandLine.populateCommand(command, "shop", "--budget=30m", 
"--focus=refunds", "--model=qwen3.6",
                 "--api-type=ollama", "--tools=core", "--show-tools");
 
-        Ask ask = command.ask(new CamelProcess(4711, "shop", null));
+        Ask ask = command.ask(new CamelProcess(4711, "shop.camel.yaml", null), 
"shop");
 
         assertThat(ask.question).singleElement().asString()
                 .contains("explore how the integration shop copes", "The 
exploration is running (30m)", "chaos_experiment");
@@ -52,8 +57,19 @@ class ChaosExploreTest {
         assertThat(ask.timeout).isEqualTo(300);
         assertThat(ask.maxIterations).isEqualTo(100);
         assertThat(ask.yes).isFalse();
-        // the exploration runs with the budget of the user: the AI does not 
get the tool to start another one
-        assertThat(ask.hiddenTools).containsExactly("chaos_explore_start");
+        // the exploration runs with the budget of the user: the AI does not 
get the tools to start another one
+        
assertThat(ask.hiddenTools).containsExactlyInAnyOrder("chaos_explore_start", 
"cli_exec");
+    }
+
+    @Test
+    void theIntegrationIsNamedAsCamelGetChaosNamesIt() {
+        // camel run * names the process after its files, and the CamelContext 
by --name
+        JsonObject status = new JsonObject(Map.of("context", new 
JsonObject(Map.of("name", "shop"))));
+        assertThat(ChaosExplore.displayName(new CamelProcess(1, 
"chaos.camel.yaml parked.camel.yaml", status)))
+                .isEqualTo("shop");
+        JsonObject unnamed = new JsonObject(Map.of("context", new 
JsonObject(Map.of("name", "CamelJBang"))));
+        assertThat(ChaosExplore.displayName(new CamelProcess(1, 
"shop.camel.yaml", unnamed))).isEqualTo("shop.camel.yaml");
+        assertThat(ChaosExplore.displayName(new CamelProcess(1, 
"shop.camel.yaml", null))).isEqualTo("shop.camel.yaml");
     }
 
     @Test
@@ -80,4 +96,63 @@ class ChaosExploreTest {
         assertThat(ChaosExplore.journal(null)).containsExactly(
                 "No journal of the exploration in the status of the 
integration");
     }
+
+    @Test
+    void whatTheExplorationFoundIsSaved(@TempDir Path dir) throws Exception {
+        JsonObject failed = new JsonObject(
+                Map.of("number", 1, "verdict", "FAIL", "open", false, 
"endedBy", "ended",
+                        "hypothesis", "nothing is lost after the hand-off", 
"rules", new JsonArray(List.of("r1", "r2")),
+                        "ruleTexts", new JsonArray(
+                                List.of("route=shipping error rate=0.2 
until=30s"
+                                        + " expect=orders->shipping:lost<=0 
within=45s",
+                                        "endpoint=seda:dlq error at=dlq 
until=30s")),
+                        "expectations", new JsonArray(
+                                List.of(new JsonObject(
+                                        Map.of("expect", 
"orders->shipping:lost<=0", "state", "unmet", "value", "11 lost")))),
+                        "note", "the seda hand-off has no error handling"));
+        JsonObject held = new JsonObject(
+                Map.of("number", 2, "verdict", "PASS", "open", false,
+                        "rules", new JsonArray(List.of("r3")), "ruleTexts", 
new JsonArray(List.of("route=refunds delay"))));
+        JsonObject exploration = new JsonObject(
+                Map.of("running", false, "finishedBy", "finished",
+                        "experiments", new JsonArray(List.of(failed, held))));
+        ChaosExplore.Run run = new ChaosExplore.Run("shop", "10m", "orders", 
"qwen3.6 (ollama)", -42L);
+
+        ChaosExplore.Saved saved = ChaosExplore.save(dir, 
LocalDateTime.of(2026, 11, 9, 14, 2, 30), run, exploration,
+                "## Summary\n1 broke", List.of("Chaos exploration finished: 2 
experiments, 1 failed"));
+
+        
assertThat(saved.report()).isEqualTo(dir.resolve("reports/explore-20261109-140230.md"));
+        assertThat(Files.readString(saved.report())).contains("# Chaos 
exploration: shop",
+                "2026-11-09 14:02 · budget 10m · focus orders · qwen3.6 
(ollama) · seed -42",
+                "## Report\n\n## Summary\n1 broke", "Chaos exploration 
finished: 2 experiments, 1 failed",
+                "## Scenarios", "camel run * --chaos=" + 
dir.resolve("explore-20261109-140230-1.chaos"));
+        JsonObject journal = (JsonObject) 
Jsoner.deserialize(Files.readString(saved.journal()));
+        assertThat(journal.getString("integration")).isEqualTo("shop");
+        assertThat(journal.getLong("seed")).isEqualTo(-42);
+        assertThat(journal.get("exploration")).isInstanceOf(Map.class);
+        // only the experiment that failed is a scenario: comments, the seed, 
and its rules together
+        
assertThat(saved.scenarios()).containsExactly(dir.resolve("explore-20261109-140230-1.chaos"));
+        assertThat(Files.readString(saved.scenarios().get(0))).isEqualTo(
+                "# From the chaos exploration of shop on 2026-11-09 14:02, 
experiment 1: FAIL\n"
+                                                                         + "# 
hypothesis: nothing is lost after the hand-off\n"
+                                                                         + "# 
expect orders->shipping:lost<=0: unmet, 11 lost\n"
+                                                                         + "# 
note: the seda hand-off has no error handling\n"
+                                                                         + "# 
replay: camel run * --chaos="
+                                                                         + 
dir.resolve("explore-20261109-140230-1.chaos")
+                                                                         + " 
(and send it messages as the experiment did,"
+                                                                         + " 
when its routes do not feed themselves)\n"
+                                                                         + 
"seed=-42\n"
+                                                                         + 
"route=shipping error rate=0.2 until=30s"
+                                                                         + " 
expect=orders->shipping:lost<=0 within=45s\n"
+                                                                         + 
"endpoint=seda:dlq error at=dlq until=30s\n");
+    }
+
+    @Test
+    void saveIsOnByDefault() {
+        ChaosExplore command = new ChaosExplore(new CamelJBangMain());
+        CommandLine.populateCommand(command);
+        assertThat(command.save).isTrue();
+        CommandLine.populateCommand(command, "--no-save");
+        assertThat(command.save).isFalse();
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index a4ec332abbf1..5ec3f1c84b8f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -573,6 +573,13 @@ class CamelChaosActionTest extends 
ActionCommandTestSupport {
                 "  waited 2m0s for the verdict; 8m15s left of the exploration",
                 "  An expectation was not decided when it ended: wait longer 
than its within= (--wait)");
 
+        JsonObject typo = new JsonObject(
+                Map.of("number", 4, "verdict", "PASS", "elapsed", 1_000L,
+                        "warnings",
+                        new JsonArray(List.of("r5: endpoint=file:/parked 
matches no endpoint of the context now"))));
+        assertThat(CamelChaosExperimentAction.lines(typo))
+                .contains("  Warning: r5: endpoint=file:/parked matches no 
endpoint of the context now");
+
         // another reason: waiting longer would not help (CAMEL-26790)
         JsonObject idle = new JsonObject(
                 Map.of("number", 4, "verdict", "INCONCLUSIVE", "rules", new 
JsonArray(List.of("r4")),
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
index b799e19b21d7..e0b558c1cbb6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
@@ -44,6 +44,7 @@ class ChaosExperimentRunTest {
         RuntimeException sendFails;
         RuntimeException endFails;
         RuntimeException beginFails;
+        List<String> beginWarnings;
         JsonObject ended = experiment("FAIL", false);
 
         @Override
@@ -56,7 +57,11 @@ class ChaosExperimentRunTest {
                 if (beginFails != null) {
                     throw beginFails;
                 }
-                return experiment("PENDING", true);
+                JsonObject begun = experiment("PENDING", true);
+                if (beginWarnings != null) {
+                    begun.put("warnings", new JsonArray(beginWarnings));
+                }
+                return begun;
             }
             if (endFails != null) {
                 throw endFails;
@@ -121,6 +126,8 @@ class ChaosExperimentRunTest {
 
     @Test
     void theTrafficThenTheWaitUntilTheVerdict() throws Exception {
+        // read first by the check of the feed
+        integration.shows("PENDING", true);
         integration.shows("PENDING", true);
         integration.shows("PENDING", true);
         integration.shows("FAIL", true);
@@ -147,7 +154,9 @@ class ChaosExperimentRunTest {
         integration.shows("PENDING", true);
         integration.ended = experiment("INCONCLUSIVE", false);
 
-        JsonObject answer = ChaosExperimentRun.run(integration, null, RULE, 
null, 5_000, null, clock);
+        // an expectation without within= has the time of its rule, which the 
wait does not know
+        JsonObject answer = ChaosExperimentRun.run(integration, null, 
"route=refunds error expect=refunds:failed<=0", null,
+                5_000, null, clock);
 
         assertThat(answer.getString("verdict")).isEqualTo("INCONCLUSIVE");
         assertThat(answer.getLong("waited")).isEqualTo(5_000);
@@ -194,6 +203,63 @@ class ChaosExperimentRunTest {
         assertThat(integration.sent).isNull();
     }
 
+    @Test
+    void aFeedThatReachesNoRouteIsRefusedBeforeTheExperiment() {
+        JsonObject status = new JsonObject(
+                Map.of("routes", new JsonArray(
+                        List.of(
+                                new JsonObject(Map.of("routeId", "orders", 
"from", "timer://orders?period=1000")),
+                                new JsonObject(
+                                        Map.of("routeId", "refunds", "from", 
"kafka://refunds?brokers=localhost:9092"))))));
+        integration.statuses.add(status);
+
+        // the route id, the endpoint it starts from (with or without //), and 
a pattern reach a route
+        assertThat(ChaosExperimentRun.checkFeed(status, "refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, 
"kafka:refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, 
"kafka://refunds")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, "ref*")).isNull();
+        // an endpoint no route starts from: no message would arrive, so the 
verdict would say nothing
+        assertThatThrownBy(() -> ChaosExperimentRun.run(integration, null, 
RULE,
+                new ChaosExperimentRun.Feed("direct:refunds", "R-1", null, 5, 
null), 60_000, null, clock))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("The feed direct:refunds is not where a route 
starts, so no message would reach the"
+                            + " integration: give the id of a route, or the 
endpoint it starts from:"
+                            + " orders (timer://orders?period=1000), refunds 
(kafka://refunds?brokers=localhost:9092)");
+        assertThat(integration.actions).isEmpty();
+    }
+
+    @Test
+    void aRuleThatMatchesNothingComesWithItsWarning() throws Exception {
+        integration.shows("PENDING", true);
+        integration.beginWarnings = List.of("r1: endpoint=file:/parked matches 
no endpoint of the context now");
+        integration.ended = experiment("PASS", false);
+
+        JsonObject answer = ChaosExperimentRun.run(integration, null, RULE, 
null, 1_000, null, clock);
+
+        // the PASS says nothing: the AI reads why
+        assertThat(answer.<List<Object>> getCollection("warnings"))
+                .containsExactly("r1: endpoint=file:/parked matches no 
endpoint of the context now");
+    }
+
+    @Test
+    void theWaitIsAtLeastTheWithinOfTheExpectations() throws Exception {
+        integration.shows("PENDING", true);
+        integration.ended = experiment("PASS", false);
+
+        // RULE has within=30s: a wait of 5s would end it before failed<=0 is 
decided
+        JsonObject answer = ChaosExperimentRun.run(integration, null, RULE, 
null, 5_000, null, clock);
+
+        assertThat(answer.getLong("waited")).isEqualTo(32_000);
+        assertThat(ChaosExperimentRun.longestWithin("route=a error expect=a:1 
within=1m; route=b drop expect=b:1 within=90s"))
+                .isEqualTo(90_000);
+        // the ; between the rules is not part of the time, also when it 
follows the time without a space
+        assertThat(ChaosExperimentRun.longestWithin("route=a error expect=a:1 
within=3m; route=b drop expect=b:1 within=90s"))
+                .isEqualTo(180_000);
+        assertThat(ChaosExperimentRun.longestWithin("route=a error expect=a:1 
within=3m;route=b drop within=1m"))
+                .isEqualTo(180_000);
+        assertThat(ChaosExperimentRun.longestWithin("route=a error")).isZero();
+    }
+
     @Test
     void theWait() {
         
assertThat(ChaosExperimentRun.waitMillis(null)).isEqualTo(ChaosExperimentRun.DEFAULT_WAIT);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index 2b32127c9287..4d03a9ef8a3f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -786,7 +786,8 @@ rules, send its traffic, wait for the verdict, and end it
                               range such as 100..500 to draw each pause at
                               random (at most 60s)
       --wait=<wait>         The longest wait for the verdict after the messages
-                              were sent (at most 10m)
+                              were sent, and at least the within= of the
+                              expectations (at most 10m)
                               Default: 2m
       --note=<note>         What to note with the experiment when it ends
       --end                 End the open experiment (with --note) instead of
@@ -829,9 +830,10 @@ failed<=0 within=70s' \
   camel cmd chaos-experiment --end --note='stopped by hand'
 
 == camel cmd chaos-explore --help
-Usage: camel cmd chaos-explore [-h] [--show-tools] [--verbose] [--yes]
-                               [--api-key=<apiKey>] [--api-type=<apiType>]
-                               [--budget=<budget>] [--focus=<focus>]
+Usage: camel cmd chaos-explore [-h] [--[no-]save] [--show-tools] [--verbose]
+                               [--yes] [--api-key=<apiKey>]
+                               [--api-type=<apiType>] [--budget=<budget>]
+                               [--focus=<focus>]
                                [--max-iterations=<maxIterations>]
                                [--model=<model>] [--timeout=<timeout>]
                                [--tools=<tools>] [--url=<url>] [<name>]
@@ -869,6 +871,10 @@ with faults, for a time budget, and reports what held and 
what broke
                                calls that camel ask asks for first, without
                                asking
       --show-tools           Show the tool calls and results as they happen
+      --[no-]save            Save the report in test/chaos/reports (as
+                               markdown, and the journal as JSON), and each
+                               experiment that failed as a scenario in
+                               test/chaos, to replay it
       --verbose              Print debug information of the LLM calls
 
 This command starts a chaos exploration with the budget (camel cmd
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 95e1d96f308f..813e34815deb 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
@@ -1295,8 +1295,9 @@ public class RuntimeTools {
                   the experiment, which removes its rules. Returns the verdict 
(FAIL: an expectation is unmet; PASS: \
                   all are met; INCONCLUSIVE: the wait was up before one was 
decided, so wait longer than its \
                   within=), the expectations with their actual values, the 
faults per rule, what the routes did \
-                  meanwhile, the traffic sent, and the time left of the 
exploration (timeLeft, millis). \
-                  camel_runtime_chaos_suggest has rules to try and the feed of 
each route.""")
+                  meanwhile, the traffic sent, the time left of the 
exploration (timeLeft, millis), and warnings \
+                  for a rule that matched nothing (fix it and run again). 
camel_runtime_chaos_suggest has rules to \
+                  try and the feed of each route.""")
     public JsonObject camel_runtime_chaos_experiment(
             @ToolArg(description = CHAOS_NAME_OR_PID_DESC, required = false) 
String nameOrPid,
             @ToolArg(description = "The rules in their one-line text form, 
separated by ;, one of them with expect=,"
@@ -1313,7 +1314,7 @@ public class RuntimeTools {
             @ToolArg(description = "The pause between two messages in millis, 
or a range such as 100..500",
                      required = false) String interval,
             @ToolArg(description = "The longest wait for the verdict after the 
messages were sent, such as 90s"
-                                   + " (default 2m, at most 10m)",
+                                   + " (default 2m, at most 10m); it is at 
least the within= of the expectations",
                      required = false) String wait,
             @ToolArg(description = "What to note with the experiment in the 
journal", required = false) String note) {
         requireArg("rule", rule);

Reply via email to