diff --git a/README.md b/README.md
index 534a56ab..db3efb59 100644
--- a/README.md
+++ b/README.md
@@ -104,10 +104,10 @@ Five layers, wired through small dataclass schemas — full details in [`docs/co
Workflows are **full Python programs**, mutated as source code. The **code-as-genotype** is the workflow file; the phenotype is whatever it produces in the workspace.
-- **Selection: Quality-Diversity archive** (**MAP-Elites**-style) — max population of 50, `qd_score = (1−w)·quality + w·novelty` (`w=0.4`). **Novelty search** uses k-NN distance (`k=25`) over a **behaviour descriptor** `[n_agents, n_edges, n_branches, prompt_chars]`. Parents drawn by inverse-child-count roulette so the archive spreads.
-- **Variation: stagnation-driven scope** — mutation boldness is a continuous function of how much the last 4 prompt gradients repeat themselves. Near-winners stay protected. Scope bands run from "prompt-only tweak" to "complete topology rethink."
-- **Crossover** — ~30 % of generations combine two parents, strongest-first.
-- **Cold start** — when the archive is empty, a similarity-filtered scan of past runs on disk (MiniLM cosine ≥ 0.5) seeds the search. Useful workflows transfer across tasks.
+- **Selection: Quality-Diversity archive** (**MAP-Elites**-style) — max population of 50, `qd_score = (1−w)·quality + w·novelty` (`w = novelty_weight = 0.25`). **Novelty search** uses k-NN distance (`k = 15`) over the **failure-fingerprint** behaviour descriptor — a 6-D vector of centered per-source pass rates from the verifier (sources A–F). Parents drawn by inverse-child-count roulette so the archive spreads (`MAX_CHILDREN_PER_PARENT = 2`).
+- **Variation: Rechenberg-1/5 + plateau-driven scope** — mutation boldness blends the success rate of the last 5 scored offspring (Rechenberg 1/5 rule, threshold `0.20`) with an `iters_since_improvement` plateau counter (patience `6`). Near-winners (parent score > 0.95) get a damper. Scope bands run from `EXPLOITATION` (point mutation) to `RE-SPECIATION` (clean-slate redesign), gated by an effective-boldness threshold (`< 0.35 / 0.50 / 0.65 / 0.90 / 1.01`).
+- **Crossover** — by default ~10 % of generations combine two parents, strongest-first, with offspring hard-capped at the highest parent agent count.
+- **Cold start** — when the archive is empty, a similarity-filtered scan of past runs on disk (MiniLM cosine ≥ 0.8) seeds the search. Useful workflows transfer across tasks.
Full mechanics: [`docs/concepts/evolution-engine.md`](./docs/concepts/evolution-engine.md).
diff --git a/config.py b/config.py
index 63258da2..e89d122a 100644
--- a/config.py
+++ b/config.py
@@ -38,18 +38,18 @@ class Config:
def __init__(self):
# workspace configuration
- self.workspace_dir = "/Users/cnrs/Documents/repository/Toolomics/workspace"
+ self.workspace_dir = "/Users/mlg/Documents/CNRS/toolomics/workspace"
# MCPs server discovery
self.discovery_addresses: list[AddressMCP] = [
- AddressMCP(ip="0.0.0.0", port_min=5000, port_max=5100)
+ AddressMCP(ip="0.0.0.0", port_min=5000, port_max=5200)
]
# LLMs choices
self.planner_llm_model: str = "openrouter/z-ai/glm-5.1"
self.workflow_llm_model: str = "openrouter/z-ai/glm-5.1"
- self.smolagent_model_id: str = "openrouter/deepseek/deepseek-v3.2"
- self.judge_model = "openrouter/minimax/minimax-m3"
+ self.smolagent_model_id: str = "openrouter/qwen/qwen3.5-122b-a10b"
+ self.judge_model = "openrouter/deepseek/deepseek-v4-flash"
self.capsule_namer_model = "openrouter/deepseek/deepseek-v4-flash"
self.engine_name: str = "litellm" # for smolagent
@@ -69,10 +69,19 @@ def __init__(self):
# learning parameters
self.learned_score_threshold = 0.94
- self.max_learning_evolve_iterations = 35
+ self.max_learning_evolve_iterations = 10
+
+ # QD novelty + length penalty (open-ended modes)
+ # "archive_knn" (default) or "k-NN archive"
+ self.novelty_comparison: str = "archive_knn"
+ self.novelty_previous_n: int = 5
+ # Length penalty: genotype size at which the penalty starts to
+ # grow; lambda is small so it only breaks near-ties.
+ self.length_penalty_baseline_chars: int = 8000
+ self.length_penalty_lambda: float = 0.05
# evaluation concurrency settings
- self.max_concurrent_eval_tasks: int = 2 # Number of concurrent tasks for CSV evaluation mode
+ self.max_concurrent_eval_tasks: int = 1 # Number of concurrent tasks for CSV evaluation mode
# folder paths for workflow pre-defined code
self.schema_code_path: str = "sources/modules/state_schema.py"
@@ -97,7 +106,7 @@ def __init__(self):
self.openrouter_quantizations_by_model: dict[str, list[str] | None] = {}
self.default_openrouter_quantizations: list[str] = ["bf16", "fp16", "fp8"]
# runner settings
- self.runner_default_python_version: str = "3.10"
+ self.runner_default_python_version: str = "3.12"
self.runner_default_timeout: int = 10800
# Per-agent (SmolAgentFactory) execution timeout in seconds. Injected into
# the generated workflow as AGENT_EXECUTION_TIMEOUT. 3600 = 1 hour.
@@ -215,6 +224,10 @@ def jsonify(
"max_tokens": self.max_tokens,
"learned_score_threshold": self.learned_score_threshold,
"max_learning_evolve_iterations": self.max_learning_evolve_iterations,
+ "novelty_comparison": self.novelty_comparison,
+ "novelty_previous_n": self.novelty_previous_n,
+ "length_penalty_baseline_chars": self.length_penalty_baseline_chars,
+ "length_penalty_lambda": self.length_penalty_lambda,
"schema_code_path": self.schema_code_path,
"smolagent_factory_code_path": self.smolagent_factory_code_path,
"runs_capsule_dir": self.runs_capsule_dir,
@@ -257,6 +270,16 @@ def from_json(self, data: dict[str, Any]) -> None:
self.max_learning_evolve_iterations = data.get(
"max_learning_evolve_iterations", self.max_learning_evolve_iterations
)
+ self.novelty_comparison = data.get("novelty_comparison", self.novelty_comparison)
+ self.novelty_previous_n = int(
+ data.get("novelty_previous_n", self.novelty_previous_n)
+ )
+ self.length_penalty_baseline_chars = int(
+ data.get("length_penalty_baseline_chars", self.length_penalty_baseline_chars)
+ )
+ self.length_penalty_lambda = float(
+ data.get("length_penalty_lambda", self.length_penalty_lambda)
+ )
self.schema_code_path = data.get("schema_code_path", self.schema_code_path)
self.smolagent_factory_code_path = data.get(
"smolagent_factory_code_path", self.smolagent_factory_code_path
@@ -319,6 +342,10 @@ def __str__(self) -> str:
lines.append(f" max_tokens={self.max_tokens}")
lines.append(f" learned_score_threshold={self.learned_score_threshold}")
lines.append(f" max_learning_evolve_iterations={self.max_learning_evolve_iterations}")
+ lines.append(f" novelty_comparison={self.novelty_comparison}")
+ lines.append(f" novelty_previous_n={self.novelty_previous_n}")
+ lines.append(f" length_penalty_baseline_chars={self.length_penalty_baseline_chars}")
+ lines.append(f" length_penalty_lambda={self.length_penalty_lambda}")
lines.append(f" max_concurrent_eval_tasks={self.max_concurrent_eval_tasks}")
lines.append(f" schema_code_path={self.schema_code_path}")
lines.append(f" smolagent_factory_code_path={self.smolagent_factory_code_path}")
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 19b26da9..9f30bd89 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -105,7 +105,8 @@ mimosa-ai/
│ │ ├── selection.py # SelectionPressure (greedy/tournament/novelty/QD)
│ │ ├── variation_engine.py # Mutation/crossover prompt assembly + annealing
│ │ ├── workflow_selection.py # Parent retrieval (archive draw / disk scan)
-│ │ ├── code_features.py # AST → behaviour descriptor (4-vector)
+│ │ ├── failure_fingerprint.py # Verifier verdicts → QD behaviour descriptor (6-D, centered)
+│ │ ├── code_features.py # Legacy structural descriptor (offline analysis only)
│ │ ├── lineage.py # parent → child sidecar records
│ │ ├── orchestrator.py # Grounding → factory → sandbox pipeline
│ │ ├── workflow_factory.py # Multi-agent workflow synthesis
@@ -255,9 +256,9 @@ Each recursive step:
6. selects the next parent(s) and chooses mutation vs crossover,
7. recurses.
-Termination: `overall_score > learned_score_threshold` (default 0.94) in
+Termination: `overall_score >= learned_score_threshold` (default 0.9) in
`--learn` mode, or `max_depth` reached
-(`max_learning_evolve_iterations`, default 45; single-shot uses
+(`max_learning_evolve_iterations`, default 20; single-shot uses
`max_depth=1`).
### 2. `SelectionPressure` — [`sources/core/selection.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/selection.py)
@@ -277,39 +278,45 @@ penalty `÷(1 + n_children_already)` and a hard
### 3. `VariationEngine` — [`sources/core/variation_engine.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/variation_engine.py)
Prompt assembly for mutation and crossover. Mutation boldness is a
-continuous function of two evidence signals — population stagnation
-*and* the Rechenberg 1/5 success rate of recent offspring — not a fixed
-phase schedule.
-
-- `_compute_stagnation(window=4)` — mean pairwise MiniLM cosine
- similarity over the last 4 non-failure prompt gradients, rescaled so
- the unrelated baseline (`≈0.4`) maps to `0` and full repetition
- (`≥0.8`) maps to `1`.
+continuous function of two evidence signals — an
+`iters_since_improvement` plateau counter *and* the Rechenberg 1/5
+success rate of recent offspring — not a fixed phase schedule.
+
+- `_iters_since_improvement()` — length of the trailing run of scored
+ offspring that did not strictly beat best-so-far (failures and
+ unscored entries skipped). Normalised as
+ `plateau = min(1, iters / _PLATEAU_PATIENCE)` with
+ `_PLATEAU_PATIENCE = 6`.
- `_compute_success_rate(window=5)` — fraction of the last 5 scored
offspring that strictly beat the running best at production time.
The Rechenberg 1/5 success rule threshold is `0.20`.
- `_get_prompt_step_size(parent_score)` — combines the two:
- * cold start (no scored history yet) — `effective = raw_stagnation`,
- * `success_rate < 0.20` — `effective = max(raw_stagnation, deficit)`
+ * cold start (fewer than two comparable scored offspring) —
+ `effective = 0.3 · plateau` (capped ramp),
+ * `success_rate < 0.20` — `effective = 0.5 · deficit + 0.5 · plateau`
with `deficit = (0.20 − success_rate) / 0.20` (escalate),
- * `success_rate ≥ 0.20` — `effective = raw_stagnation · (1 − progress)`
+ * `success_rate ≥ 0.20` — `effective = plateau · (1 − progress)`
with `progress = min(1, (success_rate − 0.20) / (0.80 − 0.20))`
(damp boldness in proportion to real progress),
* near-finish floor: when `parent_score > 0.95`, multiply by
`(1 − 0.5 · (parent_score − 0.95) / 0.05)` so a 0.96 parent isn't
- gambled away one generation before early-stop.
+ gambled away one generation before early-stop,
+ * RE-SPECIATION gate: unless `iters_since_improvement ≥ 8` *and*
+ `success_rate ∈ {None, 0.0}`, `effective` is clamped to
+ `_RESPECIATION_CLAMP = 0.89`, just below the EXPLORATION/
+ RE-SPECIATION boundary at `0.90`.
Then it grows the agent budget from the previous generation's count
toward `max_possible_agents = 7` proportionally to `effective`, and
samples the actual agent count with a Beta-Binomial biased upward by
`effective`.
-| Effective boldness | Agent budget | Mutation scope (advisory) |
-|--------------------|--------------|-------------------------------------------------------------|
-| <0.20 | ≈ current | prompt-only little tweak |
-| <0.40 | current+1 | prompt, handoff, tools — improve information flow |
-| <0.60 | current+2 | significant redesign while keeping topology |
-| <0.80 | current+3 | bold rewire — restructure or grow the agent set |
-| ≥0.80 | up to 7 | complete rethink — discard inherited topology / prompts |
+| Effective boldness | Mutation scope (advisory) |
+|--------------------|-----------------------------------------------------------------------------|
+| < 0.35 | `EXPLOITATION` — point mutation: minor phrasing / prompt-adjective tweaks |
+| < 0.50 | `ALIGNMENT` — interface optimization: refine handoff prompts, IO contracts |
+| < 0.65 | `ADAPTATION` — component overhaul: rewrite lagging agent prompts, swap tools |
+| < 0.90 | `EXPLORATION` — macro structural mutation: add/merge agents, change routing |
+| ≥ 0.90 | `RE-SPECIATION` — clean-slate redesign of the multi-agent architecture |
Scope is an advisory line injected into the mutation prompt; the LLM
may still pick any topology. The hard control is the agent-count
@@ -322,7 +329,7 @@ Two-mode parent retrieval:
- **Steady state**: when `selection_pressure._archive` is populated, draws
parents from the live session archive via QD-roulette.
- **Cold start**: empty archive → similarity-filtered disk scan
- (`cosine ≥ 0.5` on MiniLM embeddings of `original_task`, `score ≥ 0.05`)
+ (`cosine ≥ 0.8` on MiniLM embeddings of `original_task`, `score ≥ 0.1`)
routed through the same `select_parents()` weighting.
### 5. `WorkflowOrchestrator` — [`sources/core/orchestrator.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/orchestrator.py)
@@ -400,7 +407,7 @@ EvolutionEngine.start_workflow_evolution(goal)
├─ SelectionPressure.validate_survivor() → archive admit?
├─ record_lineage()
├─ select next parent (archive QD-roulette)
- ├─ choose crossover (~0.4 rate, once initial_population met) or mutation
+ ├─ choose crossover (default crossover_rate=0.1, once initial_population met) or mutation
└─ recurse → stop on threshold OR max_depth
↓
WorkspaceManager.restore_best(best_uuid)
@@ -464,20 +471,16 @@ For each generation:
pre-installed (lazy one-shot install per process). Soft claims get a
`pass/unsure/fail` LLM verdict against workspace previews + literature
grounding (mapped to `1.0 / 0.5 / 0.0`).
-3. **Independent cheat detector**: present in the codebase but currently
- **disabled** pending a rewrite. The aggregation path still has a slot
- for `cheat_penalty`. Behavioral anti-cheat pressure today comes from
- Source C's recompute-from-disk verifiers, the inverted-score "Used
- fallback" claim type, and the anti-tautology tripwires.
+3. **Behavioral pressure against shortcut workflows** comes from Source
+ C's recompute-from-disk verifiers, the inverted-score "Used fallback"
+ claim type, and the anti-tautology tripwires.
4. **Aggregation**:
```
- overall = clamp(base_mean + info_bonus, 0, 1)
+ overall = clamp(base_mean, 0, 1)
if any hard claim refuted:
overall = min(overall, 0.99) # _HARD_FAIL_CAP (soft, for now)
- overall = max(0, overall - cheat_penalty) # cheat_penalty = 0.0 today
```
- where `info_bonus(n_hard_pass) = 0.05 · (1 - exp(-n_hard_pass / 8))`
- (saturating reward for thoroughness).
+ `base_mean` is the importance-weighted mean of per-claim scores.
5. **Prompt gradient** — plain-language single-sentence diagnosis
prefixed with a short code name (e.g. `FALLBACK_ECFP_CLASSIFIER`). It
is the **only** verifier signal the mutator sees, and recent history
diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md
index 5d22cb2d..65640657 100644
--- a/docs/concepts/architecture.md
+++ b/docs/concepts/architecture.md
@@ -45,12 +45,13 @@ recursively evolves workflows, with help from:
(improvement over baseline or `qd_score > admit_threshold`); capacity is
curated by lowest-`qd_score` eviction.
- **VariationEngine** — assembles mutation or crossover prompts, with an
- evidence-driven mutation scope: boldness grows as recent prompt
- gradients converge (the offspring keep failing the same way) *and* as
- the Rechenberg 1/5 success rate of recent scored offspring drops
- below 20 %. A near-finish floor only damps boldness once the parent
- score is above 0.95, so near-winners aren't gambled away one
- generation before early-stop.
+ evidence-driven mutation scope: boldness grows with an
+ `iters_since_improvement` plateau counter (patience `6`) *and* as the
+ Rechenberg 1/5 success rate of the last 5 scored offspring drops below
+ 20 %. A near-finish floor only damps boldness once the parent score is
+ above 0.95, so near-winners aren't gambled away one generation before
+ early-stop. The top `RE-SPECIATION` band is hysteresis-gated and only
+ opens after `iters_since_improvement ≥ 8` with a zero success rate.
- **WorkflowOrchestrator** — wraps "grounding → factory → sandbox" into one
callable per generation.
diff --git a/docs/concepts/evaluation-pipeline.md b/docs/concepts/evaluation-pipeline.md
index 3bee08d9..1f83daa2 100644
--- a/docs/concepts/evaluation-pipeline.md
+++ b/docs/concepts/evaluation-pipeline.md
@@ -20,6 +20,12 @@ fingerprint). The single signal that flows back to the mutator is a short
**prompt gradient** that summarizes failure modes without leaking the
verified claims themselves.
+The same per-claim verdicts are projected into a 6-dim **failure
+fingerprint** that the [evolution engine](evolution-engine.md#behaviour-descriptor-failure-fingerprint)
+uses as the behaviour descriptor for QD novelty. The descriptor is
+centered so overall quality cannot leak into novelty — see the firewall
+section below.
+
{ width="100%" }
## The pipeline
@@ -32,8 +38,8 @@ The verifier runs four stages per workflow run:
small Python program that recomputes the asserted value from workspace
files (the default path), or renders a soft LLM verdict when no
deterministic check is possible.
-3. **Aggregation** — per-claim scores combine into `overall_score` with a
- saturating thoroughness bonus and a hard-fail cap.
+3. **Aggregation** — per-claim scores combine into `overall_score` as an
+ importance-weighted mean with a hard-fail cap.
4. **Prompt gradient** — a plain-language summary of failure modes, the
**only** signal that reaches the mutator. It does not name claims,
scores, or sources, so the mutator cannot turn the verified-claim
@@ -106,22 +112,54 @@ only falls back to an LLM verdict when no executable check is possible.
## Aggregation
```python
-base_mean = mean(score for each non-error claim)
-bonus = α · (1 − exp(−n_hard_pass / β)) # α=0.05, β=8
-pre_cap = clamp(base_mean + bonus, 0, 1)
+base_mean = importance_weighted_mean(score for each non-error claim)
+pre_cap = clamp(base_mean, 0, 1)
overall_score = min(pre_cap, hard_fail_cap) if any_hard_claim_refuted else pre_cap
```
-- `α = _INFO_BONUS_ALPHA = 0.05`, `β = _INFO_BONUS_BETA = 8.0` —
- saturating reward for thoroughness, conditioned on *passing hard*
- claims so trivial or failed claims contribute nothing.
+- Per-claim weights come from the rater-assigned importance (1–10), so an
+ importance-10 deliverable claim moves the score ~5× more than a
+ low-importance hygiene claim.
- `hard_fail_cap = _HARD_FAIL_CAP = 0.99` — currently set permissively
to keep the evolutionary signal smooth; a refuted hard claim still
flags `hard_fail_capped = True`.
- The engine separately keeps `overall_score_uncapped` (pre-cap) so QD
rank ordering doesn't flatten under hard fails.
+## Failure fingerprint (QD behaviour descriptor)
+
+The verifier doesn't just emit a score — the same per-claim verdicts feed
+the QD novelty signal as a **failure fingerprint**: a centered vector
+of per-source pass rates that tells the archive *how* a candidate fails,
+not *whether* it failed.
+
+```python
+# Per source A..F (six entries, always — absent sources get a neutral value).
+pass_rate[s] = passes[s] / total[s] if total[s] > 0 else 0.5
+presence[s] = 1.0 if total[s] > 0 else 0.0
+# Center so the descriptor encodes profile shape, not quality level.
+mean_present = mean(pass_rate[s] for s where presence[s] == 1)
+vector[s] = pass_rate[s] - mean_present if presence[s] == 1
+ = 0 otherwise
+```
+
+**The quality firewall.** An all-pass run and an all-fail run both yield
+the zero profile. This is intended and asserted in the tests
+(`test_all_pass_yields_zero_profile`,
+`test_all_fail_yields_zero_profile`). The QD score combines quality and
+novelty *additively* — `(1 − w)·quality_norm + w·novelty_norm` — so
+quality already drives `quality_norm`. If quality also leaked into
+novelty, QD would collapse back into greedy search. The centering step
+is what keeps these two terms separable.
+
+The fingerprint is persisted under
+`state_result.json` → `evaluation.verifier.failure_fingerprint.vector`
+and consumed by
+[`SelectionPressure._extract_behaviour_descriptor`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/selection.py).
+Full info-flow audit:
+[`docs/info-flow/failure_fingerprint.md`](../info-flow/failure_fingerprint.md).
+
## Prompt gradient
After aggregation the verifier composes a single-sentence diagnosis (the
@@ -139,13 +177,10 @@ The intent is informational, not punitive: the mutator learns *what
direction to push the workflow next* without being handed a vocabulary
it can over-fit against.
-## Cheat detector (currently disabled)
+## Behavioral pressure against shortcut workflows
-A standalone cheat detector pass over the agents' produced source code
-existed in earlier versions and is kept in the codebase but is currently
-**disabled** (`cheat = None`) pending a rewrite. The aggregation pipeline
-still supports a `cheat_penalty` field for when it is re-enabled. The
-behavioral anti-cheat pressure today comes from:
+Pressure against shortcut or fabricated workflows comes from the
+verifier pipeline itself:
- Source C's recompute-from-disk verifiers.
- The "Used fallback" claim type, whose score is *inverted* — a passing
diff --git a/docs/concepts/evolution-engine.md b/docs/concepts/evolution-engine.md
index a2e76f28..c16acb5a 100644
--- a/docs/concepts/evolution-engine.md
+++ b/docs/concepts/evolution-engine.md
@@ -17,7 +17,7 @@ flowchart TB
Seed -- yes --> SeedPrompt[Seed genome prompt
or template mutation]
Seed -- no --> Pick[Pick parents via QD-roulette
fallback to disk similarity scan]
SeedPrompt --> Orch[Orchestrate workflow
LLM → sandbox]
- Pick --> Decide{Crossover ≈ 0.4?}
+ Pick --> Decide{Crossover ≈ 0.1?}
Decide -- mutation --> Mut[Mutation prompt
stagnation-scoped]
Decide -- crossover --> Cross[Crossover prompt
best-parent-first]
Mut --> Orch
@@ -52,9 +52,9 @@ A more detailed view lives in the source diagram
Termination:
-- `overall_score > learned_score_threshold` (default `0.94`) in `--learn` mode, *or*
+- `overall_score >= learned_score_threshold` (default `0.9`) in `--learn` mode, *or*
- `max_depth` reached — `1` in single-shot mode, `max_learning_evolve_iterations`
- (default `45`) in `--learn` mode.
+ (default `20`) in `--learn` mode.
## Selection: Quality-Diversity (QD)
@@ -64,12 +64,15 @@ implements four strategies — `greedy`, `tournament`, `novelty`, and `qd`
- A **session archive** holds up to `population_size = 50` members.
- Each member has `qd_score = (1−w)·quality_norm + w·novelty_norm`, with
- `w = novelty_weight = 0.25`.
+ `w = novelty_weight = 0.25`. Quality and novelty are **additive** — never
+ multiplied — so high quality cannot rescue a redundant profile and high
+ novelty cannot drag a broken run above peers.
- Quality is sourced from `reward_uncapped` so the hard-fail cap doesn't
flatten rank ordering.
-- Novelty is k-NN distance (`k = 15`) in behaviour-descriptor space, where
- the descriptor is `[n_agents, n_edges, n_branches, prompt_chars]`
- extracted by [`code_features.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/code_features.py).
+- Novelty is k-NN distance (`k = 15`) in **failure-fingerprint** space.
+ The descriptor is the centered per-source pass-rate vector produced by
+ [`failure_fingerprint.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/failure_fingerprint.py)
+ from the verifier's per-claim verdicts (see below).
- Admission gate: candidate is admitted when it either improves over
baseline by `min_improvement_threshold` or clears
`qd_score > admit_threshold`. When the archive reaches capacity, the
@@ -80,23 +83,60 @@ implements four strategies — `greedy`, `tournament`, `novelty`, and `qd`
offspring stream stays spread across the archive.
When the archive is empty (cold start), [`WorkflowSelector`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/workflow_selection.py)
-falls back to a **similarity-filtered disk scan** (`cosine ≥ 0.5` on MiniLM
-embeddings of `original_task`, `score ≥ 0.05`) — this lets useful workflows
+falls back to a **similarity-filtered disk scan** (`cosine ≥ 0.8` on MiniLM
+embeddings of `original_task`, `score ≥ 0.1`) — this lets useful workflows
transfer across tasks.
+### Behaviour descriptor: failure fingerprint
+
+The novelty signal compares candidates in **failure-fingerprint** space.
+Per source A–F (literature, user goal, agent narration, math invariants,
+computational reproducibility, statistical fingerprint), the verifier
+records a pass rate. Sources with zero claims get the neutral value
+`0.5` and a presence-mask entry of `0`. The vector is then **centered**:
+the mean pass rate across present sources is subtracted from every entry.
+
+The centering is the *quality firewall*. Without it, an all-pass run sits
+at `[1,1,1,1,1,1]` and an all-fail run at `[0,0,0,0,0,0]` — Euclidean
+distance between them is large, and quality silently leaks into novelty.
+After centering, **both** runs collapse to the zero profile and novelty
+encodes only the *shape* of which sources fail relative to the others.
+Two workflows that fail in the same way are redundant regardless of how
+different their DAGs look; two that fail in different ways explore
+different basins and both deserve a seat in the archive.
+
+The fingerprint is computed at the end of `VerifierEvaluator.evaluate()`
+and persisted in `state_result.json` under
+`evaluation.verifier.failure_fingerprint.vector`. The full audit trail —
+which variable comes from where, the failure modes the descriptor must
+survive, and the centering invariant asserted by the tests — lives in
+[`docs/info-flow/failure_fingerprint.md`](../info-flow/failure_fingerprint.md).
+
+When a run has no usable fingerprint (verifier short-circuit on a fully
+failed workflow), `SelectionPressure._extract_behaviour_descriptor`
+returns a neutral zero vector so distance lookups stay well-defined and
+the cold path doesn't artificially win or lose on novelty.
+
+The legacy structural descriptor (`[n_agents, n_edges, n_branches,
+prompt_chars]`) shipped by
+[`code_features.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/code_features.py)
+is retained for ablations and offline analysis but is **no longer used**
+for QD novelty — empirical work showed it barely co-varies with outcomes.
+
## Variation: evidence-driven mutation scope (Rechenberg 1/5 rule)
[`VariationEngine`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/sources/core/variation_engine.py)
assembles mutation and crossover prompts. There is no fixed phase
schedule by iteration progress; mutation boldness is a continuous
-function of two evidence signals — how much the population is
-repeating itself, *and* how often recent offspring have actually
-improved on the best-so-far.
+function of two evidence signals — how long the lineage has been
+failing to improve, *and* how often recent offspring actually beat the
+best-so-far.
-**Stagnation signal.** `_compute_stagnation(window=4)` takes the last
-4 non-failure prompt gradients, computes their pairwise MiniLM cosine
-similarity, and rescales the mean (`0.4` ≈ unrelated → `0`,
-`0.8+` ≈ fully stagnated → `1`).
+**Plateau signal.** `_iters_since_improvement()` counts the run of
+trailing scored offspring (failures and unscored entries skipped) that
+did not strictly beat the best-so-far at the moment they were
+produced. It is normalised to `plateau = min(1, iters / _PLATEAU_PATIENCE)`
+with `_PLATEAU_PATIENCE = 6`.
**Success-rate signal.** `_compute_success_rate(window=5)` counts the
fraction of the last 5 scored offspring whose `overall_score` strictly
@@ -107,42 +147,49 @@ is happening and step size should be damped.
**Effective boldness.** Combining the two signals:
-- **Cold start** (no scored offspring yet) — `effective = raw_stagnation`.
-- **Below 1/5** (`success_rate < 0.20`) — escalate at least to the
- deficit: `effective = max(raw_stagnation, deficit)`, where
- `deficit = (0.20 − success_rate) / 0.20`. A repeated gradient and a
- flat reward curve both force scope up.
+- **Cold start** (fewer than two comparable scored offspring) —
+ `effective = 0.3 · plateau`. A capped cold-start ramp avoids jumping
+ straight into RE-SPECIATION before any feedback has accumulated.
+- **Below 1/5** (`success_rate < 0.20`) — average the deficit and the
+ plateau: `effective = 0.5 · deficit + 0.5 · plateau`, where
+ `deficit = (0.20 − success_rate) / 0.20`. A run of no-improvements
+ and a stalling success rate both push scope up.
- **Above 1/5** — damp boldness in proportion to how far above
- threshold we are: `effective = raw_stagnation · (1 − progress)`,
- where `progress = min(1, (success_rate − 0.20) / (0.80 − 0.20))`.
- At `success_rate ≥ 0.80` boldness collapses regardless of stagnation.
+ threshold we are: `effective = plateau · (1 − progress)`, where
+ `progress = min(1, (success_rate − 0.20) / (0.80 − 0.20))`. At
+ `success_rate ≥ 0.80` boldness collapses regardless of plateau.
- **Near-finish floor** — only in the last 5 % of the score range,
`effective` is multiplied by `(1 − 0.5 · near_finish)` where
- `near_finish = (parent_score − 0.95) / 0.05`. This is the *only*
- point where the parent's absolute score re-enters the boldness
+ `near_finish = max(0, (parent_score − 0.95) / 0.05)`. This is the
+ *only* point where the parent's absolute score re-enters the boldness
calculation, so a 0.96 parent isn't gambled away one generation
before early-stop.
+- **RE-SPECIATION gate.** The top band is hysteresis-gated: unless
+ `iters_since_improvement ≥ _RESPECIATION_PATIENCE` (default `8`) *and*
+ `success_rate ∈ {None, 0.0}`, `effective` is clamped to
+ `_RESPECIATION_CLAMP = 0.89` — just below the EXPLORATION/RE-SPECIATION
+ boundary at `0.90`.
Notably, `parent_score` no longer multiplies the whole signal — that
older behaviour locked high-score lineages into "tiny tweak" mode even
-when the gradient kept repeating identically.
+when offspring kept failing identically.
**Agent budget.** The current agent count grows toward
`max_possible_agents = 7` proportionally to `effective`, then a
Beta-Binomial draw samples the actual count inside that window
(biased upward by `effective`). The seed generation samples agents
-from `[1, 4]` with a `0.5` stagnation prior.
+from `[1, 4]` with a `0.5` boldness prior.
**Scope band.** A single advisory line is added to the mutation prompt,
chosen by `effective`:
-| Effective boldness | Mutation scope |
-| ------------------- | ---------------------------------------------------------------------- |
-| < 0.20 | prompt-only little tweak |
-| < 0.40 | prompt, handoff, tools — improve information flow |
-| < 0.60 | significant redesign while keeping topology |
-| < 0.80 | bold rewire — restructure or grow the agent set |
-| ≥ 0.80 | complete rethink — discard inherited topology / prompts |
+| Effective boldness | Mutation scope |
+| ------------------- | --------------------------------------------------------------------------- |
+| < 0.35 | `EXPLOITATION` — point mutation: minor phrasing / prompt-adjective tweaks |
+| < 0.50 | `ALIGNMENT` — interface optimization: refine handoff prompts, IO contracts |
+| < 0.65 | `ADAPTATION` — component overhaul: rewrite lagging agent prompts, swap tools |
+| < 0.90 | `EXPLORATION` — macro structural mutation: add/merge agents, change routing |
+| ≥ 0.90 | `RE-SPECIATION` — clean-slate redesign of the multi-agent architecture |
The bands are advisory text steered to the LLM, not hard gates: the
LLM can still pick any topology. The hard control is the agent-count
@@ -150,9 +197,9 @@ budget passed in the same prompt block.
## Crossover
-With probability ~0.4 per generation (and only once at least
-`initial_population = 2` runs have happened), two parents are combined
-instead of one being mutated. The crossover prompt is
+With probability `crossover_rate` per generation (default `0.1`, and only
+once at least `initial_population = 2` runs have happened), two parents
+are combined instead of one being mutated. The crossover prompt is
**best-parent-first**: the strongest parent's code structure leads,
weaker parents contribute specific improvements rather than competing
for the skeleton, and the offspring is hard-capped at the highest
@@ -186,8 +233,10 @@ Each iteration also writes structured metrics for post-hoc analysis:
`iteration_wall_time_s`, `iteration_cost_usd`, `cumulative_cost_usd`,
`overall_score{,_uncapped}`, `qd_descriptor`, `qd_score`,
`novelty_score`, the `selection_log`, and the
- `variation_state` (`stagnation`, `success_rate`, `effective_boldness`,
- `scope_band`, `agent_budget`) that produced this offspring.
+ `variation_state` (`iters_since_improvement`, `plateau`,
+ `success_rate`, `effective_boldness`, `parent_score`,
+ `respeciation_gate_open`, , `agent_budget`) that produced
+ this offspring.
- `sources/workflows/qd_archive.jsonl` — append-only, one line per
`validate_survivor` call. Records the candidate's descriptor,
`qd_score`, `novelty_score`, admission verdict, and the `evicted_uuid`
diff --git a/docs/diagrams/evolution_loop.mermaid b/docs/diagrams/evolution_loop.mermaid
index 5ec27bd1..92e05d57 100644
--- a/docs/diagrams/evolution_loop.mermaid
+++ b/docs/diagrams/evolution_loop.mermaid
@@ -27,7 +27,7 @@ flowchart TB
ParentDraw{{"archive populated?"}}
DrawArch["select_parent_workflows()
QD-roulette over archive
÷ (1 + n_children_already)"]
DrawDisk["cold-start fallback:
similarity-filtered disk scan"]
- Decide{{"crossover_rate ≈ 0.4?
(only once initial_population met)"}}
+ Decide{{"crossover_rate ≈ 0.1?
(only once initial_population met)"}}
Mut["mutation_prompt()
evidence-driven scope (Rechenberg 1/5)
raw_stagnation × success-rate gating"]
Cross["crossover_prompt()
best-parent-first ordering
rubric-blind diagnosis"]
@@ -79,11 +79,11 @@ flowchart TB
%% ===== sidebars =====
subgraph QD["Quality-Diversity machinery"]
- BD["behaviour descriptor
extract_code_features() →
[n_agents, n_edges, n_branches,
prompt_chars] / SCALES"]
- Nov["k-NN novelty
(k=15, against archive)"]
- QDscore["qd_score = (1-w)·quality_norm +
w·novelty_norm (w=0.25)
quality from reward_uncapped"]
+ BD["behaviour descriptor
failure_fingerprint(per_claim) →
centered per-source pass rates
over sources A..F (6-D)"]
+ Nov["k-NN novelty
(k=25, against archive)"]
+ QDscore["qd_score = (1-w)·quality_norm +
w·novelty_norm (w=0.4)
quality from reward_uncapped
(additive: no quality leak into novelty)"]
end
- State -. AST .-> BD
+ State -. verifier per-claim verdicts .-> BD
Archive -. distances .-> Nov
BD --> QDscore
Nov --> QDscore
diff --git a/docs/diagrams/verifiers_judge.mermaid b/docs/diagrams/verifiers_judge.mermaid
index 361f39c8..de0e8fc9 100644
--- a/docs/diagrams/verifiers_judge.mermaid
+++ b/docs/diagrams/verifiers_judge.mermaid
@@ -58,21 +58,16 @@ flowchart TB
%% ===== Stage 3: aggregation =====
subgraph Aggregate["Stage 3 — aggregation"]
- Mean["base_mean = mean(non-error claims)"]
- Bonus["info_bonus = α·(1 − exp(−n_hard_pass / β))
α=0.05, β=8 — saturates"]
+ Mean["base_mean = importance-weighted mean(non-error claims)"]
Cap["hard-fail cap (0.99, soft):
any refuted hard claim flags hard_fail_capped"]
- Cheat["cheat_penalty
(currently disabled — pending rewrite)"]
end
ScoreClaim --> Mean
- ScoreClaim --> Bonus
Mean --> Cap
- Bonus --> Cap
- Cap --> Cheat
%% ===== Stage 4: abstracted prompt gradient =====
- Cheat --> Overall["overall_score ∈ [0, 1]
+ overall_score_uncapped
(pre-cap, drives QD ranking)"]
- Cheat --> Diag["Stage 4 — abstracted_prompt_gradient
(rubric-blind code-name + sentence)
→ only signal fed back to mutator"]
+ Cap --> Overall["overall_score ∈ [0, 1]
+ overall_score_uncapped
(pre-cap, drives QD ranking)"]
+ Cap --> Diag["Stage 4 — abstracted_prompt_gradient
(rubric-blind code-name + sentence)
→ only signal fed back to mutator"]
Overall -. drives .-> Loop[/"EvolutionEngine.evolve_generation()"/]
Diag -. drives .-> Loop
diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md
index f2be639e..29ee8115 100644
--- a/docs/getting-started/configuration.md
+++ b/docs/getting-started/configuration.md
@@ -22,8 +22,8 @@ field with defaults and types, see the [Configuration reference](../reference/co
| `smolagent_model_id` | LLM used by execution agents inside each workflow. |
| `judge_model` | LLM that scores soft claims in the verifier. |
| `planner_llm_model` | LLM that decomposes goals into tasks (`--goal` mode only). |
-| `learned_score_threshold` | Score that triggers early stop in `--learn` mode (default `0.97`). |
-| `max_learning_evolve_iterations` | Hard cap on evolve iterations (default `35`). |
+| `learned_score_threshold` | Score that triggers early stop in `--learn` mode (default `0.9`). |
+| `max_learning_evolve_iterations` | Hard cap on evolve iterations (default `20`). |
## Choosing models
@@ -78,7 +78,7 @@ The sandbox enforces resource caps per generated workflow:
| Field | Default | Purpose |
| ----- | ------- | ------- |
-| `runner_default_python_version` | `3.10` | Python version inside the sandbox. |
+| `runner_default_python_version` | `3.12` | Python version inside the sandbox. |
| `runner_default_timeout` | `3600` | Per-run timeout (seconds). |
| `runner_default_max_memory_mb` | `1024` | RAM cap (MB). |
| `runner_default_max_cpu_percent` | `100` | CPU cap (%). |
@@ -95,8 +95,8 @@ uv run main.py --task "…" \
| Field | Default | Meaning |
| ----- | ------- | ------- |
-| `learned_score_threshold` | `0.97` | Stop evolving when `overall_score` reaches this. |
-| `max_learning_evolve_iterations` | `35` | Max generations before giving up. |
+| `learned_score_threshold` | `0.9` | Stop evolving when `overall_score` reaches this. |
+| `max_learning_evolve_iterations` | `20` | Max generations before giving up. |
See [Iterative learning](../usage/learning.md) for the full evolution machinery.
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
index 98fe957c..8a1f712b 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started/quickstart.md
@@ -62,8 +62,8 @@ uv run main.py \
```
In learning mode, Mimosa evolves up to `max_learning_evolve_iterations`
-generations or stops as soon as `overall_score > learned_score_threshold`
-(default `0.95`). See [Iterative learning](../usage/learning.md).
+generations or stops as soon as `overall_score >= learned_score_threshold`
+(default `0.9`). See [Iterative learning](../usage/learning.md).
## 4. Try goal mode
diff --git a/docs/images/evolution_tree.png b/docs/images/evolution_tree.png
new file mode 100644
index 00000000..6b878cb7
Binary files /dev/null and b/docs/images/evolution_tree.png differ
diff --git a/docs/info-flow/failure_fingerprint.md b/docs/info-flow/failure_fingerprint.md
new file mode 100644
index 00000000..71b2c03b
--- /dev/null
+++ b/docs/info-flow/failure_fingerprint.md
@@ -0,0 +1,169 @@
+# Failure-fingerprint descriptor — info flow
+
+> **Reader's note.** This page is an info-flow audit, not a user-facing
+> tutorial. It traces every variable that feeds the QD behaviour descriptor
+> from its source to the point where novelty distance is computed.
+> If you want to *understand* the descriptor, start with
+> [`concepts/evolution-engine.md`](../concepts/evolution-engine.md) and
+> [`concepts/evaluation-pipeline.md`](../concepts/evaluation-pipeline.md).
+
+## Why a separate descriptor
+
+QD selection compares candidates with two scalars:
+
+- **quality** — `reward_uncapped`, i.e. how well the workflow scored.
+- **novelty** — k-NN distance in *behaviour-descriptor space* to the
+ rest of the archive.
+
+The descriptor must be **orthogonal to quality**. If it isn't, novelty
+becomes a noisy restatement of quality and QD collapses back into greedy
+search. The previous structural descriptor
+(`[n_agents, n_edges, n_branches, prompt_chars]`) was nominally
+orthogonal but barely co-varied with outcomes — different DAG shapes did
+not predict different basins. The failure fingerprint replaces it.
+
+## The signal: per-source pass rates
+
+The verifier extracts claims from six independent vantage points:
+
+| Letter | Vantage |
+| --- | --- |
+| A | Literature / methodology bar |
+| B | User goal / deliverable fidelity |
+| C | Agent narration truthfulness (recompute-from-disk) |
+| D | Math invariants |
+| E | Computational reproducibility / CS practice |
+| F | Statistical fingerprint |
+
+Each claim is verified to one of `pass`, `fail`, `error`, `unsure`.
+
+**Per source**, the fingerprint takes the pass rate:
+
+```
+pass_rate[s] = (# claims with status == "pass" and source == s) / (# claims with source == s)
+```
+
+`fail`, `error`, and `unsure` all count as non-passes — a measurement
+error is still a non-pass from the optimiser's perspective, and quality
+already penalises errors via `quality_norm`.
+
+## Centering: the quality firewall
+
+Per source pass rates **alone** would leak quality into novelty (an
+all-pass run would sit at `[1,1,1,1,1,1]`, an all-fail one at
+`[0,0,0,0,0,0]`, and their Euclidean distance would be large). To strip
+quality out, we subtract the mean pass rate across present sources from
+every entry:
+
+```
+mean_present = mean(pass_rate[s] for s in SOURCES if presence_mask[s])
+vector[s] = pass_rate[s] - mean_present if presence_mask[s]
+ = 0 otherwise
+```
+
+The descriptor now encodes the *profile shape* of which sources fail
+relative to the others — NOT the overall quality level. An all-pass run
+and an all-fail run **both** yield the zero profile. This is the
+*quality firewall* — and it is asserted in
+[`tests/failure_fingerprint_test.py`](https://github.com/HolobiomicsLab/Mimosa-AI/blob/main/tests/failure_fingerprint_test.py)
+by `test_all_pass_yields_zero_profile` and `test_all_fail_yields_zero_profile`.
+
+A run that fails source A but passes the others looks *very different*
+from a run that fails source D but passes the others. Both might be
+mediocre on quality, and both deserve a seat in the archive because each
+one explores a different basin of failure modes.
+
+## Sources of variables
+
+```
+ ┌────────────────────────────────────┐
+ workflow run │ VerifierEvaluator.evaluate() │
+ ──────────────► │ per_claim = list of: │
+ uuid, code, │ {"claim": {"source": "source_X"},
+ execution_text │ "status": "pass" | "fail" | ..}
+ └─────────────┬──────────────────────┘
+ │
+ ▼
+ ┌────────────────────────────────────┐
+ │ failure_fingerprint │
+ │ .compute_failure_fingerprint │
+ │ -> {vector, presence_mask, │
+ │ pass_rates} │
+ └─────────────┬──────────────────────┘
+ │
+ │ persisted in scores dict
+ ▼
+ ┌────────────────────────────────────┐
+ │ state_result.json │
+ │ evaluation.verifier │
+ │ .failure_fingerprint │
+ │ .vector (list[float], len 6)
+ │ .presence_mask (list[float], len 6)
+ │ .pass_rates (list[float], len 6)
+ └─────────────┬──────────────────────┘
+ │
+ │ EvolutionEngine reads via
+ │ WorkflowInfo.state_result
+ ▼
+ ┌────────────────────────────────────┐
+ │ IndividualRun.state_result │
+ └─────────────┬──────────────────────┘
+ │
+ ▼
+ ┌────────────────────────────────────┐
+ │ SelectionPressure │
+ │ ._extract_behaviour_descriptor │
+ │ → failure_fingerprint_from_ │
+ │ state_result(run.state_result)
+ │ → falls back to │
+ │ neutral_fingerprint() when │
+ │ verifier hasn't written one │
+ └─────────────┬──────────────────────┘
+ │
+ ▼
+ ┌────────────────────────────────────┐
+ │ k-NN novelty in 6-D space │
+ │ PopulationMember.behaviour_ │
+ │ descriptor │
+ └────────────────────────────────────┘
+```
+
+## Variable inventory (audit table)
+
+| Symbol | Type | Set by | Read by | Notes |
+| --- | --- | --- | --- | --- |
+| `per_claim[i]["claim"]["source"]` | str | `verifier_claims._extract_claims` | `compute_failure_fingerprint` | Normalised to `a`..`f` via `_source_letter`. |
+| `per_claim[i]["status"]` | str | `verifier_per_claim._verify_claim` | `compute_failure_fingerprint` | One of `pass`/`fail`/`error`/`unsure`. |
+| `pass_rates` | `list[float]` (len 6) | `compute_failure_fingerprint` | Persisted; debugging only. | Neutral 0.5 when source absent. |
+| `presence_mask` | `list[float]` (len 6) | `compute_failure_fingerprint` | Persisted; debugging only. | `1.0` iff source emitted ≥1 claim. |
+| `vector` | `list[float]` (len 6) | `compute_failure_fingerprint` | `_extract_behaviour_descriptor` → k-NN | The centered descriptor; **the QD signal**. |
+| `state_result.evaluation.verifier.failure_fingerprint` | dict | `VerifierEvaluator.evaluate` / `_short_circuit_failed_run` | `failure_fingerprint_from_state_result` | Persisted in `state_result.json`. |
+| `IndividualRun.state_result` | dict | `EvolutionEngine._evaluate_and_calculate_cost` | `SelectionPressure._extract_behaviour_descriptor` | Carries the fingerprint into selection. |
+| `PopulationMember.behaviour_descriptor` | `list[float]` (len 6) | `SelectionPressure._validate_open_ended` | `_compute_novelty` | Snapshot of the fingerprint at admission time. |
+| `qd_score` | float | `SelectionPressure._validate_open_ended` and `_refresh_member_metrics` | parent draw, archive eviction | `(1 − w)·quality_norm + w·novelty_norm`; quality and novelty are *additive*, never multiplied. |
+
+## Failure modes the audit checks
+
+- **No claims at all** (verifier short-circuit): the run gets the zero
+ vector and zero presence mask. Distance to peers stays finite; the
+ cold run isn't pushed to admit or reject solely on novelty.
+- **Source F absent in practice** (the current code wires sources A–E):
+ `presence_mask[5]` stays `0.0`; the centering step ignores that axis;
+ distance lookups remain well-defined.
+- **Unknown source label**: dropped silently by `_source_letter`. No
+ out-of-band claim can pollute a bucket.
+- **Mismatched descriptor length across archive members**: only happens
+ during a schema upgrade; `_euclidean` returns `+inf` as a sentinel and
+ `_novelty_range` clamps the normalisation. We recommend draining the
+ archive when the dim changes — the descriptor dim is now a stable 6.
+
+## Invariants (asserted)
+
+- `len(vector) == DESCRIPTOR_DIM == 6`.
+- All-pass and all-fail yield `[0]*6` (`test_all_pass_yields_zero_profile`, `test_all_fail_yields_zero_profile`).
+- Two runs with the same per-source pass rates yield the **same** vector,
+ regardless of overall reward — distance is `0`, so the second is treated
+ as redundant.
+- Two runs with opposite shape (a-pass-b-fail vs a-fail-b-pass) yield
+ vectors that mirror each other through the origin
+ (`test_distinct_profiles_yield_nonzero_distance`).
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 68324a61..391642f8 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -34,15 +34,15 @@ Ports must be in `[0, 65535]` and `port_min ≤ port_max`.
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| `prompt_planner` | `str` | `sources/prompts/planner_reproduction.md` | Planner system prompt. |
-| `prompt_workflow_creator` | `str` | `sources/prompts/workflow_v10.md` | Workflow-generation prompt. |
+| `prompt_workflow_creator` | `str` | `sources/prompts/workflow_v11.md` | Workflow-generation prompt. |
| `prompt_smolagent` | `str` | `sources/prompts/smolagent_sys_prompt.md` | SmolAgent system prompt. |
## Learning
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
-| `learned_score_threshold` | `float` | `0.97` | `--learn` stops when `overall_score` exceeds this. |
-| `max_learning_evolve_iterations` | `int` | `35` | Hard cap on evolve iterations. |
+| `learned_score_threshold` | `float` | `0.9` | `--learn` stops when `overall_score` reaches this. |
+| `max_learning_evolve_iterations` | `int` | `20` | Hard cap on evolve iterations. |
| `max_concurrent_eval_tasks` | `int` | `1` | Concurrent tasks in CSV / batch modes. |
## OpenRouter routing
@@ -70,7 +70,7 @@ See [Troubleshooting → OpenRouter quantization](troubleshooting.md#openrouter-
| Field | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
-| `runner_default_python_version` | `str` | `3.10` | Python in the sandbox. |
+| `runner_default_python_version` | `str` | `3.12` | Python in the sandbox. |
| `runner_default_timeout` | `int` | `3600` | Per-run timeout (s). |
| `runner_default_max_memory_mb` | `int` | `1024` | RAM cap (MB). |
| `runner_default_max_cpu_percent` | `int` | `100` | CPU cap (%). |
diff --git a/docs/reference/file-layout.md b/docs/reference/file-layout.md
index 4c8f01ec..fd488d5c 100644
--- a/docs/reference/file-layout.md
+++ b/docs/reference/file-layout.md
@@ -18,7 +18,8 @@ mimosa-ai/
│ │ ├── selection.py # QD archive + admission gate
│ │ ├── variation_engine.py # Mutation / crossover prompt assembly
│ │ ├── workflow_selection.py # Parent retrieval (archive / disk)
-│ │ ├── code_features.py # AST → behaviour descriptor
+│ │ ├── failure_fingerprint.py # Verifier verdicts → QD behaviour descriptor (6-D, centered)
+│ │ ├── code_features.py # Legacy structural descriptor (offline analysis only)
│ │ ├── lineage.py # Parent → child sidecar records
│ │ ├── orchestrator.py # Grounding → factory → sandbox
│ │ ├── workflow_factory.py # Multi-agent workflow synthesis
diff --git a/docs/site/404.html b/docs/site/404.html
index 49f61342..41a7fba3 100644
--- a/docs/site/404.html
+++ b/docs/site/404.html
@@ -85,7 +85,7 @@