Skip to content

Skills/memories: agent captures env-specific learnings (corrections + discoveries) as consolidated per-data-source skills - #2128

Open
aantn wants to merge 48 commits into
masterfrom
claude/consolidated-skills-per-domain
Open

aantn wants to merge 48 commits into
masterfrom
claude/consolidated-skills-per-domain

Conversation

@aantn

@aantn aantn commented Jun 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closed-loop "skills/memories" mechanism for HolmesGPT. It's very common for the agent to do trial-and-error and schema rediscovery when touching a data source in a fresh chat: it re-learns how to query it, what the field names are, what the labels/metrics are called. This PR lets the agent capture those learnings once, save them as a skill, and have every future investigation skip the rediscovery.

Two kinds of learnings are captured (declared per entry via a required kind field on the tool schema):

  1. kind: correction — the agent called a tool the way a fresh LLM would default to, got an empty/wrong result, and succeeded only after an env-specific parameter change (custom label scheme, renamed metric prefix, non-standard log field, alias-only index...). The wrong→right pair is saved so the next run skips the failed attempt.
  2. kind: discovery — nothing failed, but the agent spent exploratory calls (mapping/schema inspection, label/metric/index listing, document sampling) learning stable environment facts before it could issue the real query. The facts + direct call shape are saved so the next run queries immediately. Guardrail: schemas and naming conventions are stable and capturable; Kubernetes resource inventory/counts are transient and excluded.

Design

  • One skill per data source, not per quirk. Every suggestion declares a skill_domain (elasticsearch, loki, prometheus, ...). The harness consolidates all entries sharing a domain into one quirks-for-querying-<domain>/SKILL.md with numbered entries — so the skill catalog stays small at customer scale and a single fetch surfaces every known quirk for that source.
  • Skills grow across investigations. New emissions merge into a pre-existing domain skill (parse → append → dedupe by title) instead of clobbering it or creating a parallel file.
  • Deterministic, rule-based skill descriptions. The LLM does not write the catalog description; a template generates an imperative "fetch before your first call" trigger keyed off skill_domain. (The first CI run showed soft descriptions never trip the base prompt's "only fetch skills that clearly match" gate.)
  • Trust directive in the skill body. The generic fetch_skill wrapper tells agents skill contents are "DIRECTIONS not ACTUAL RESULTS"; right for runbooks, wrong for saved facts — it made replay agents re-inspect mappings to "confirm" the skill. The generated body now states entries are verified facts: query directly, re-verify only on contradiction.

Closed-loop eval validation (all passing locally on opus-4.6)

Each eval runs the primary investigation, captures emissions, writes the consolidated skill, then replays a naturally-phrased question with the skill loaded and asserts the agent fetched it and answered correctly.

  • 261/264/265 (ES), 262 (Prometheus), 263 (Loki): forced-correction captures, one per data source quirk
  • 266_es_schema_discovery: discovery without failure — schema learned via mapping inspection, zero failed calls; replay must answer without calling elasticsearch_mappings again (new replay_forbidden_tools assertion proves the skill eliminated the rediscovery)
  • 267_bad_skill_resilience: pre-loaded misleading skill; agent must recover and still answer correctly
  • 268_es_multi_quirk_consolidation: 3 quirks in one investigation → 1 skill; replay −44% cost / −58% tokens
  • 269_es_skill_update_cross_investigation: new emission merges into a pre-existing domain skill (cross-investigation accumulation)

Harness/test-framework additions

  • rerun_with_memory, replay_user_prompt, expected_replay_output, pre_loaded_skills_path, expected_skill_count, require_skill_load_on_replay, replay_forbidden_tools fields on test cases
  • GitHub eval report: Skill Generated / Skills Read columns, [replay] comparison rows, collapsed skills-stats section
  • Unit tests for consolidation, merge, dedupe, discovery rendering, parse round-trips

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb

claude and others added 30 commits May 1, 2026 07:45
Parameterize ask_holmes evals across an on/off tool_suggestions
matrix (mirroring the model and env_config matrices). When the
variant is "on", the SUGGEST_RUNBOOKS frontend noop tool is injected
into the per-test ToolExecutor and a matching system-prompt block is
appended so the LLM has the option to silently emit "skill" memories
at the end of an investigation.

After each run the suggestions emitted by the LLM are captured from
result.tool_calls and surfaced in three places so regression CI
reports can compare with/without and developers can drill into what
memories would have been generated:

- pytest user_properties (tool_suggestions, memories_count,
  suggested_memories) → consumed by the GitHub markdown report
- New "Suggest" and "Memories" columns in the GitHub eval table
- Braintrust span metadata + tags so the suggestions are visible in
  individual eval traces

Default matrix runs both variants; TOOL_SUGGESTIONS_CONFIGS=on|off
restricts the run to a single variant for local debugging.

Signed-off-by: Claude <noreply@anthropic.com>
Ruff F541: the SUGGEST_RUNBOOKS_TOOL_DESCRIPTION fragments and the
trailing fragments of SUGGEST_RUNBOOKS_SYSTEM_PROMPT had no
interpolation placeholders, so the f-prefix was invalid and would
fail linting in CI.

Signed-off-by: Claude <noreply@anthropic.com>
The first run on PR #1991 captured a single memory titled "NetworkPolicy
label mismatch causing timeouts between services" — that's a transient
root-cause conclusion (once fixed, useless), not the durable thing we
want. The Hermes Agent / RunbookHermes skill model is procedural memory:
when-to-use + procedure + pitfalls.

The offending bullet was in the tool description itself:
  - "Root cause patterns (what symptoms map to what causes)"
which biases the model toward conclusions from THIS incident rather
than the investigation methodology for the next one.

Rework the tool description and parameter docstrings so the model
captures access / query patterns:
  - title: framed as "Investigating X" or "<env-context>: how to
    diagnose Y", with explicit GOOD/BAD examples
  - symptoms: when-to-use trigger, recognizable patterns rather than
    "what was wrong this time"
  - instructions: ordered procedure ("what to query FIRST / LAST /
    SKIP", environment-specific access patterns, tool quirks)
  - explicit DO NOT list: root-cause conclusions, specific resource
    names/timestamps, generic Kubernetes advice

Verified with opus-4.6 (the production model we care about): the
revised prompt now produces titles like "Investigating missing
environment variables causing CrashLoopBackOff" with FIRST/SECOND
investigation steps, instead of conclusion-style summaries.

Signed-off-by: Claude <noreply@anthropic.com>
Previous framings were still too broad: capturing "investigation
methodology" or "what to check first" feeds the LLM things it already
knows from training — that's noise, not durable knowledge.

The actual goal is much narrower: capture ONLY when, this turn, the
LLM made a tool call with the wrong parameters (empty/error/wrong
data) and then succeeded with different parameters that required
environment-specific knowledge a fresh LLM would not have guessed.
That single correction — the working call shape for THIS environment
— is the only thing worth saving so the next investigation skips the
failed attempt.

Rewrite tool description and schema accordingly:
  - Description: explicit DO/DON'T list, lead with "ONLY call this
    tool if your investigation contained a wrong-call→right-call pair
    that required env-specific knowledge". Generic mistakes any LLM
    would self-correct (typos, missing -n, --previous on a crashed
    container) are explicitly excluded.
  - Schema: replace symptoms/instructions/alerts with the four fields
    that capture the correction itself —
      * failed_call: shape of the call that didn't work
      * working_call: shape of the call that does work in this env
      * when_to_use: how to recognize the skill is relevant next time
      * why_env_specific: one-sentence reason a fresh LLM wouldn't
        have guessed the right call (self-check — if you can't write
        it, the correction is generic and should be skipped)

Update the unit test that exercises the schema to use a real-looking
correction (PromQL label override) instead of the old symptoms/
instructions/alerts shape.

Signed-off-by: Claude <noreply@anthropic.com>
Schema:
- AskHolmesTestCase gains optional `memories_generated: bool | None`.
  - True  → suggest=on variant fails if zero memories were emitted.
  - False → suggest=on variant fails if any memory was emitted.
  - None  → no count enforcement (existing behavior).
- Emitted memories are surfaced into the LLM judge's evaluation_output on
  the suggest=on variant so memory content quality is scored against the
  eval's expected_output exactly like the final answer. The judge prompt
  is instructed to fail the test when a required memory is missing or
  captures the wrong thing.

All 11 existing regression evals get `memories_generated: false` as a
safety net so any spurious memory generation in CI fails loudly.

Two new regression evals exercise real failed→succeeded tool-call
corrections that require env-specific knowledge:

- 259_custom_label_selector — a payment-api deployment named pmt-svc-v2
  with only the team's custom `acme.io/service` label (no `app=` label).
  Asking by name forces the LLM to try `-l app=payment-api` first, hit
  empty, discover the label via `--show-labels`, and re-query with the
  custom label.
- 260_custom_namespace_naming — the cluster uses
  `team-<team>-<service>-<env>-<id>` namespaces, not the default
  `<service>`/`<service>-<env>`. The LLM has to enumerate namespaces
  after the obvious guesses fail.

Both new evals have `memories_generated: true`, so suggest=on must
emit at least one memory and the judge scores its content via the
expected_output element ("There are N ready X replicas / running...").

Signed-off-by: Claude <noreply@anthropic.com>
A test that fails on an assertion AFTER update_test_results (e.g. the
max_tokens limit or the new memories_generated check) was showing
:white_check_mark: in the GitHub markdown report because
TestStatus.passed only looked at actual_correctness_score — which was
set to 1 by the judge before the assertion fired.

- TestStatus.passed now also requires pytest's status to be "passed"
  (or empty for legacy paths), so any assertion that fires after the
  judge correctly surfaces as red in the report.
- The memories_generated check also resets actual_correctness_score
  to 0 in user_properties before asserting, so the score column in
  the report matches the failed status.
- Unit test in tests/test_test_results.py guards the regression.

Signed-off-by: Claude <noreply@anthropic.com>
CodeRabbit nitpick: CLAUDE.md requires imports at the top of the
file, not inside functions. The local `import json as _json` inside
update_test_results moves to module scope and the alias is dropped.

Signed-off-by: Claude <noreply@anthropic.com>
The 259/260 evals were too weak: an efficient LLM can sidestep the
"wrong" call entirely (e.g. by listing labels or namespaces up front
rather than guessing the standard ones first). When that happens
there is no real failed→succeeded correction to capture, and our
`memories_generated: true` assertion produces false-positive failures.

Replace them with three evals where the wrong call is near-inevitable
because the "right" call requires environment-specific knowledge a
fresh LLM has no way to guess:

- 261_es_log_severity_field — index has only a `severity` field, no
  `level`. `level:"ERROR"` returns zero hits; the agent must read
  the mapping and re-query with `severity:"ERROR"`.
- 262_es_index_via_alias — production logs are only reachable via
  the alias `app-262-prodlogs` (real index is a date-stamped
  rotation). Obvious raw names 404; the agent must `GET _alias`.
- 263_loki_custom_stream_label — Promtail labels streams with
  `acme_service` instead of `service`/`app`. `{service="checkout"}`
  matches no streams; the agent must list label names first.

All three are tagged `regression` and have `memories_generated:
true`. Each demonstrates a tool-call correction where the lesson is
genuinely durable (applies to future investigations in the same
environment) and would not be in any LLM's training data.

Signed-off-by: Claude <noreply@anthropic.com>
Without a toolsets.yaml in the fixture folder the elasticsearch/data
and elasticsearch/cluster toolsets stay disabled, so the agent
answers "Elasticsearch is not configured" instead of investigating —
which masks whether the eval actually exercises the wrong-then-right
correction it's designed to. Mirrors the config from
183b_elasticsearch_index_discovery.

Signed-off-by: Claude <noreply@anthropic.com>
System-prompt rewrite — the addition we inject when suggest_runbooks
is enabled now explicitly explains the goal (skip the failed call
next time in THIS environment) and names the concrete patterns we
want captured: non-standard labels/selectors, non-standard metric
names (e.g. team prefix replacing the upstream kafka_* / mysql_*
family), non-standard log field shapes, non-standard data
locations / addressing, custom CRDs, and tool-routing quirks.
Each capture should record the failed_call vs working_call shape —
the env-specific things the LLM did not know before this run.

Reiterates the don't-capture list (generic methodology already in
the model's training data, transient root causes) and keeps the
"never acknowledge calling this tool" directive.

Evals:
- Drop 262_es_index_via_alias — structurally weak; the agent uses
  elasticsearch_list_indices reflexively and the response leaks
  the real index name, so no wrong-then-right pair ever happens.
- Fix 261_es_log_severity_field timestamp bug — "0${i}" produced
  invalid ISO for i=10..15, so ES silently rejected 7 docs and the
  expected count couldn't be met.
- Add 262_prometheus_custom_kafka_metric_name — exporter publishes
  Kafka metrics under acme_kafka_* instead of the upstream kafka_*
  family. A fresh LLM defaulting to kafka_server_brokertopicmetrics_*
  or kafka_topic_* gets no series and has to enumerate metric names
  to find the team prefix. The lesson is genuinely env-specific:
  "Kafka metrics in this cluster live under acme_kafka_*" — not
  knowledge any LLM has from training.

Signed-off-by: Claude <noreply@anthropic.com>
opus-4.6 was skipping the wrong-then-right pattern by checking
elasticsearch_mappings first (smart!) — so the agent never tried
the conventional level:"ERROR" query and there was no env-specific
correction to capture. Re-phrase the user prompt to ask explicitly
about "level=ERROR" so the agent's first natural query uses the
standard log-field name, returns zero hits, and then has to
discover via the mapping that this index uses `severity`.

The durable lesson stays the same: "this index uses severity, not
level — query severity:\"ERROR\" after confirming via mapping."

Signed-off-by: Claude <noreply@anthropic.com>
Even with the biased 261 prompt forcing opus-4.6 into a textbook
wrong-then-right pattern (level:"ERROR" → empty → mapping discovery
→ severity:"ERROR" → success), and even when the agent SPELLS OUT
the lesson in its own answer ("this index uses severity, not
level"), it still doesn't emit a suggest_runbooks call. The
examples in the prompt weren't enough.

Add an explicit four-step checklist the agent must run BEFORE
finalizing its answer: (1) scan tool history, (2) did any call
return empty followed by a successful call with different params,
(3) was the difference env-specific, (4) if yes, emit the
suggest_runbooks call in the SAME response as the final answer.

Also makes explicit that mentioning the correction in prose is NOT
a substitute for emitting the tool call — the prose is read by the
current user; the tool call surfaces a save-able chip for future
investigations.

Signed-off-by: Claude <noreply@anthropic.com>
With the workflow checklist added, opus-4.6 started emitting
suggest_runbooks — but as a SEPARATE turn AFTER its final answer.
That consumes the turn that would otherwise contain the user-facing
answer text, leaving result.result empty: the user sees no answer
and the eval judge would correctly fail correctness.

Anthropic / OpenAI models support multiple tool_use blocks in one
assistant message (parallel tool calls). Update the checklist to
spell that out explicitly: emit the suggest_runbooks call ALONGSIDE
the answer prose in the SAME assistant message. Also tighten the
noop response so that if the model still ends up there as a
separate step it knows to produce the answer text NOW.

Signed-off-by: Claude <noreply@anthropic.com>
Parallel-with-answer didn't reliably trigger; opus-4.6 chose one or
the other (memory ✓ + empty answer, or full answer + no memory).

Switch to a clean three-step ordering: (1) detect correction
condition; (2) emit suggest_runbooks first — tool returns
silently; (3) write the final answer text in the next message.
This keeps the agentic loop happy: tool call → noop response that
explicitly says "user has not seen your answer yet, write it now"
→ assistant emits answer text.

The noop response also reframed to keep the model from interpreting
the silent return as a stop signal: it must produce answer text in
the very next message.

Signed-off-by: Claude <noreply@anthropic.com>
Adds an optional `rerun_with_memory: true` field to AskHolmesTestCase.
When set, after the first eval pass captures a memory the harness
runs the same prompt a SECOND time with each emitted memory rendered
as a SKILL.md file under a tempdir, and the SkillsToolset's search
paths extended to include that tempdir. The replay flow:

1. Memories from pass-1 are serialized to SKILL.md files with name,
   description, and a structured body (when to use / failed call
   shape / working call shape / why env-specific).
2. TestToolsetManager now accepts `additional_skill_paths` so the
   standard skills toolset can scan the tempdir.
3. The ask_holmes helper now accepts the same kwarg and threads it
   through; the replay invokes ask_holmes again with suggest=off
   (no memory regeneration expected on the second pass).
4. The replay enforces two things: (a) the LLM must have called
   fetch_skill (proving it recognized the memory as relevant), and
   (b) the answer must still be correct. Both feed hard assertions.
5. New user_properties carry replay metrics — replay_attempted,
   replay_skill_loaded, replay_correctness, replay_turns,
   replay_tool_calls_count — picked up by the conftest collector.
6. The GitHub markdown reporter emits a second `[replay]` row right
   under each row that triggered a replay, with the skill-loaded
   status badge in the Memories column and the replay turns / tool
   call counts. The status icon mirrors the replay correctness so
   regressions are visible side-by-side with the first pass.

Enabled on 261, 262, 263 so the rerun_with_memory loop is part of
the regression run from the next push onward.

Signed-off-by: Claude <noreply@anthropic.com>
Two problems found in the previous run for 0f0d8ec:

1. The Prometheus readiness check used an unencoded URL with curly
   braces and quotes inside the query string
   (`acme_kafka_msgs_consumed_total{topic="payments"}`). Many
   wget builds reject that as an invalid URL. Switch to the
   simple `up` metric scoped to our job — no special chars.
   On failure dump the targets endpoint and exporter logs so
   future regressions are diagnosable.

2. The exporter was nginx + a ConfigMap-mounted text file with an
   `alias` directive pointing at the mounted symlink. ConfigMap
   mounts surface each key via a chain of symlinks
   (`<dir>/<name>` -> `..data/<name>` -> a real file under a
   timestamped `..NNN/` directory) which nginx alias did not
   serve reliably in this combo. Replace it with a tiny Python
   http.server that bakes the metric text into a constant — far
   fewer moving pieces, a built-in readinessProbe on /metrics,
   and the same observed externally.

Signed-off-by: Claude <noreply@anthropic.com>
The [replay] row was only filling in turns / tools / skill-loaded
status and rendering em dashes for everything else. That obscured
the whole point of the comparison — eyeballing whether the captured
memory actually saves time, tool calls, and tokens.

Capture the full LLMResult on the replay (duration, total_cost,
total_tokens, prompt/completion/cached/reasoning, max-per-call,
num_compactions) into user_properties, plumb them through the
conftest collector, and render them in the replay row using the
same formatting helpers as the original row. Side-by-side rows
now make the win obvious: e.g. main 22.4s / 7 turns / 10 tools /
120K tokens vs replay 13.2s / 5 turns / 6 tools / 85K tokens.

Signed-off-by: Claude <noreply@anthropic.com>
The replay row's status icon used only replay_correctness, ignoring
replay_skill_loaded. When the assert fetch_skill_called fires after
the correctness score is logged, replay_correctness stays at 1 in
user_properties — the report showed ✅ even though
pytest had failed the test for the missing skill load.

Require BOTH replay_correctness == 1 AND replay_skill_loaded for
the green checkmark. Either one false yields ❌, matching what
pytest actually reported. The Memories column already showed
"skill ✗" so the inconsistency between badge and icon disappears.

Signed-off-by: Claude <noreply@anthropic.com>
The previous report still lacks duration / cost / tokens on the
replay row because that eval run pre-dates 91aea68 (which capture
full LLMResult stats for the replay). Bumping the branch so CI
re-runs against 388d300 — that should produce a replay row with
full stats and the corrected red-on-missing-skill status icon.

Signed-off-by: Claude <noreply@anthropic.com>
…av1Uv

Resolved three conflicts in tests/llm/utils/reporting/github_reporter.py
where master added a Src column at the end of each table row and my
branch added Suggest/Memories columns near the front plus the [replay]
sub-row. The merged version keeps both: 18-column header (Status, Test
case, Suggest, Memories, Time, Turns, Tools, Cost, Total tokens, Input,
Max input, Output, Max output, Cached, Non-cached, Reasoning,
Compactions, Src), per-row markdown emits the matrix Suggest/Memories
cells AND the Src link, the [replay] sub-row reuses the parent row's
Src link (same test_case.yaml), and the Totals row keeps the matrix
aggregates plus the empty Src cell.

Signed-off-by: Claude <noreply@anthropic.com>
When the closed-loop replay assertion fires, pytest marks the whole
test as failed. The github report would then paint BOTH the primary
row and the [replay] row red, even though the primary pass (memory
emission, judge correctness) actually succeeded — only the replay
loaded the wrong / no skill.

Track primary_passed in user_properties immediately after the
primary assertions pass (before the replay block runs). The report
uses primary_passed (when a replay was attempted) to render the
primary row's icon, so 263-style situations now show:

  ✅ 263_loki ... on 1   memory ok, primary correct
  ❌ 263_loki [replay] ... skill ✗   replay failed independently

Two independent measurements, two independent icons. Adds
.goal_criteria.md documenting the up-front criteria for the
sub-agent improvement goal (30% token reduction on ≥5 evals
without regressing the existing 11 regression evals by >10%).

Signed-off-by: Claude <noreply@anthropic.com>
…_use

264 covers the case where the agent has to discover that log level is
encoded as a numeric severity_num scale (1-6) rather than a text 'level'
field. Validated locally: replay drops from 184,739 → 106,164 tokens
(-42.5%) when the captured memory is rendered as a fetched skill.

The skill description shown to the agent now leads with the captured
when_to_use phrase (the symptom/query shape), then appends the title
(the durable env-specific lesson). The previous title-only description
matched on the lesson keyword but not on what the user actually asks
about, so fetch_skill was sometimes not triggered on replay.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
263: The previous prompt let the agent reach loki_list_labels as its
natural first step, so there was no wrong→right correction to capture
and the suggest=on variant never emitted a memory. New prompt names the
candidate selector `{service="checkout"}` explicitly so the agent
commits to it first, gets an empty stream, then has to list labels and
retry with `{acme_service="checkout"}`. That correction is the durable
env-specific lesson the eval was designed to teach.

265: New eval — index has NO `@timestamp` date field at all; the only
time column is a custom `ingest_ts` typed as keyword. The prompt names
`@timestamp` explicitly so the agent's first range query is against a
non-existent field; it then has to inspect the mapping and rewrite the
filter as a string comparison on `ingest_ts`. Same wrong→right pattern
as 261/264.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
The previous rigid 'Use the @timestamp field for the time filter'
caused the agent to obey literally and answer 0 instead of discovering
the renamed `ingest_ts` field and answering with 7. Removing the
field-name instruction lets the agent reach for `@timestamp` naturally
(as the ES default), fail, inspect the mapping, find `ingest_ts`, and
return the right count. Memory + replay fetch_skill confirmed locally.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
The previous prompts ("how many entries have level=ERROR?") were too
implicit: in some CI runs the agent peeked at the index mapping first
and went straight to the working field, so there was no wrong-then-right
correction to capture and memories_generated=true failed. The 263 prompt
(which already names the wrong selector explicitly and tells the agent
to find the right one if it returns nothing) reliably elicits the
correction; copy that pattern for 261, 264, 265.

- 261: ``term: { level: "ERROR" }`` named explicitly
- 264: ``terms: { level: ["ERROR", "FATAL"] }`` named explicitly
- 265: ``range: { "@timestamp": {gte: ...} }`` named explicitly

In all three cases the prompt now ends with "if that returns zero, find
the right field/timestamp and re-run" so the agent still produces the
correct answer after the wrong call.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
…eric

The previous "do NOT call for" list included "look at mapping when query
is empty" as a generic methodology example. That phrasing read as
"any discovery via mapping inspection is generic" and made the agent
classify env-specific field-name corrections (severity vs level,
acme_service vs service, ingest_ts vs @timestamp) as not memory-worthy
because the *method* used to find them (inspect mapping / list labels)
was on the don't-capture list.

Reword: the *method* of inspecting/listing IS generic, but the FACT
you discover (the specific field name this team uses) is env-specific
and IS worth capturing. Verified locally: 261 now reliably emits memory
+ replay drops 156k→108k (-31.0%) when fetched as a skill.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
Previously the SKILL.md body went: When to use → Failed call → Working
call → Why env-specific. The agent on replay would read the skill and
then still run discovery (label list, mapping inspection) to verify,
limiting replay token savings (263 saw only -20% reduction because
the discovery wasn't skipped).

New layout: When to use → What to do (skip discovery, use working
call directly) → Working call (front-and-center) → Failed call →
Why env-specific. The "What to do" section explicitly tells the
agent to bypass the discovery step the skill exists to replace.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
The previous expected_output was "8 entries with ERROR or FATAL
severity (severity_num >= 5)" — the parenthetical jargon doesn't
match how the agent typically writes the answer (it explains the
mapping in prose rather than as a range expression), so the LLM
judge marked it wrong even when the count was correct.

New expected mirrors 261's style: states the count + severity +
index, then explains the env-specific quirk (numeric encoding) in
plain language that matches how the agent writes its answer.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
The CI run on 888dc16 showed the directive section regressed three
evals: 261/263 dropped from skill ✓ to skill ✗ (the agent stopped
fetching the skill at all), and 265 token usage went UP +24.9% because
the additional directive text in the skill body cost more than it
saved. The simpler body (When-to-use → Failed → Working → Why)
worked better — bf5797a saw 261 ✓ at -29.5%, 263 ✓ skill-loaded,
265 ✓ skill-loaded.

Reverting the directive while keeping the working changes:
- biased prompts (360b1f2)
- system prompt fix ("look at mapping" is method, field-name is fact)
- 264 expected_output clarification (0848dbd)

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
claude added 7 commits June 3, 2026 19:57
261/262/263/264/265 are the evals designed for the SUGGEST_RUNBOOKS
+ rerun_with_memory closed loop. Add the existing 'skills' tag so we
can run just this baseline with pytest -m skills.

https://claude.ai/code/session_01NkgEXcoZhJD498VPmsYXmb
Signed-off-by: Claude <noreply@anthropic.com>
The on/off matrix doubled every eval run for a comparison we no longer
need: the SUGGEST_RUNBOOKS tool is now standard equipment for ask_holmes
evals, and the off variant was only useful while we were validating that
the feature didn't regress non-skills tests. With that validation done,
keeping the matrix wastes ~50% of every CI run on a column that always
reads the same.

This collapses the dimension entirely:

- tool_suggestions_config.py: drop ToolSuggestionsConfig, the env-var
  parser, the matrix getter, and the Tuple[ai, bool] injection shim.
  inject_suggest_runbooks_tool(ai) now always injects;
  append_suggest_runbooks_system_prompt(prompt) now always appends.
- test_ask_holmes.py: remove the @pytest.mark.parametrize on
  tool_suggestions and the `[suggest=on/off]` trace-name suffix. The
  primary pass always injects; the closed-loop replay passes
  inject_suggest_runbooks=False.
- Report rendering: drop the Suggest column and the
  tool_suggestions_enabled gating around the Memories column.
  Memories is always a numeric count now; the totals row drops
  "N/M on" in favor of the raw memories sum.
- Tracking: property_manager, conftest, and braintrust stop emitting
  tool_suggestions / tool_suggestions_enabled fields; the replay-only
  memories signal is unchanged.
- Fixtures: rewrite the "On suggest=on ..." narrative comments on the
  261-265 skills evals — they referenced a matrix variant that no
  longer exists.
- Unit tests: drop the matrix-parsing tests, keep the
  extract_suggested_memories coverage.

256 tests collected after this change (was ~512) and the
test_tool_suggestions_config.py suite passes.

Signed-off-by: Claude <noreply@anthropic.com>
Three changes aimed at making the skills mechanism's net win visible
and stress-testing it against realistic conditions:

1. **Skills net-win summary in the GitHub report.** Right after the
   totals row, the report now prints a small block aggregating:
   how many evals emitted a memory, how many replays were attempted,
   how often the skill loaded, how often the answer was still correct,
   and the per-row average primary→replay cost/token delta. Lets the
   "is this feature paying for itself" answer be cited in one sentence
   without eyeballing every row.

2. **Soften prompts on 261 and 263.** Both fixtures previously
   prescribed the failure path explicitly ("Run X. If returns zero,
   find Y."). That tested obedience, not discovery. The new phrasings
   ask realistic, unbiased questions and trust the agent to discover
   the env-specific quirk on its own. If the agent still emits the
   captured memory after recovering, we've shown the mechanism works
   on realistic queries; if it doesn't, we've found a prompt
   sensitivity worth fixing.

3. **Bad-memory regression eval (267_bad_skill_resilience).** Adds a
   `pre_loaded_skills_path` field to `AskHolmesTestCase` — when set,
   the fixture's named directory is added to the SkillsToolset's
   search paths BEFORE the primary pass. Fixture 267 uses this to
   pre-load a deliberately misleading SKILL.md that claims the ES
   index uses `lvl: "ERR"` when it actually uses `severity: "ERROR"`.
   The test asserts the agent still arrives at the correct count
   (10 ERROR rows) despite the bad guidance, proving captured skills
   can't permanently mislead.

Out of scope here (per discussion): cross-transfer eval (concern 2),
production wiring of memory persistence (concern 5), and dedicated
accumulated-memories noise testing (concern 4).

Signed-off-by: Claude <noreply@anthropic.com>
Two fixes driven by Braintrust trace inspection of the prior run:

**267_bad_skill_resilience didn't actually test bad-skill resilience.**
The agent fetched the misleading skill correctly, but couldn't run the
ES query because the elasticsearch/data toolset was never enabled for
the fixture — the agent ended with "The elasticsearch/data toolset is
disabled. I cannot execute the search query directly." Added a
toolsets.yaml mirroring 261's so the agent can actually run the wrong
query and discover the correction. Next run will be the first real
test of whether the agent recovers from a misleading captured skill.

**Reverted softened prompts on 261 and 263.** The trace for 261 was
revealing: with the un-biased "how many ERROR entries are there"
phrasing, the agent answered correctly on the first try
(`severity == "ERROR"`) — never made the wrong call, so suggest_runbooks
correctly emitted zero memories. The test's `memories_generated: true`
assertion then failed.

That isn't a regression — it's the correct behavior. The mechanism is
designed for wrong→right corrections, and when the agent's first call
is right, there's nothing to teach. We learned that softer prompts
elide the failure-then-recovery shape the mechanism depends on. The
biased phrasing reliably triggers the correction (and reliable
emission was the point of this fixture), so restore it. The fixture
comment now documents what we learned, including why a separate
"natural-phrasing emission rate" eval would be a different test.

Signed-off-by: Claude <noreply@anthropic.com>
Trace analysis of the prior run surfaced the real reason 261/263
replays kept failing with `skill ✗`: the prompts that work for primary
emission *block* skill use on replay.

The biased phrasing on 261/263 ("Run X. If returns zero, find Y.")
prescribes the recovery path. On the primary pass that's good — the
agent runs the wrong call, gets nothing, recovers, and emits a memory
captured from the correction. On replay the same prompt is still telling
the agent the recovery path, so it confidently does the wrong→right
dance again and never bothers to fetch the captured skill — the whole
point of which is to skip the failed call.

262 was succeeding on replay because its primary prompt is already a
natural, un-biased question ("how many messages on the payments
topic?") — the agent doesn't know the metric name, so it checks
skills and finds the one. 261/263 needed the same affordance for the
replay step only.

Fix: add `replay_user_prompt` on AskHolmesTestCase. When set, the
closed-loop replay run uses it instead of the original `user_prompt`.
This models the real-world scenario the mechanism is designed for:
the first investigation has rich detail about what went wrong (the
biased eval prompt analog); the second investigation asks the same
question naturally and the captured skill should pay off by
short-circuiting the failed call.

Wiring:
- `AskHolmesTestCase.replay_user_prompt` (Optional[str|List[str]])
- `ask_holmes(..., override_user_prompt=...)` substitutes when present
- Replay block in test_ask_holmes passes
  `getattr(test_case, "replay_user_prompt", None)` through

Applied to 261, 263, 265 (the three biased-prompt skills evals).
Expected delta next run: replays that previously hit skill ✗ should
load the skill and short-circuit the failed call.

Signed-off-by: Claude <noreply@anthropic.com>
Data from the prior CI run was mixed: replay_user_prompt fixed 261
(skill ✗ → skill ✓, -25% cost / -39% tokens) but broke 265 (skill ✓
correct → skill ✓ but wrong answer, +20% cost / +39% tokens). The
soft "from today" phrasing on 265 caused the agent to load the
captured skill yet still compute the date range incorrectly — the
explicit "@timestamp gte 2026-06-03" in the primary prompt was load-
bearing for the right answer.

Keeping the mechanism and the working applications (261, 263) and
reverting just 265 to use the original biased prompt for replay. Note
in the fixture documents why.

263's replay still misses fetch_skill regardless of phrasing — the
agent recovers via mapping/label inspection without consulting the
skill catalog. That's a structural limit for this eval, not a
prompt-phrasing issue, and not something replay_user_prompt can fix.

Signed-off-by: Claude <noreply@anthropic.com>
…ce, not per quirk)

The per-quirk skill design from claude/add-tool-suggestions-matrix-av1Uv
has a known scaling problem: a real customer's elasticsearch has dozens
of schema quirks across dozens of indices, and emitting one SKILL.md per
quirk produces a catalog so large the SkillsToolset listing in the
system prompt becomes a burden of its own. It also means the agent has
to fetch and merge multiple skills per investigation.

This branch shifts the contract: every emission must declare a
`skill_domain` (`elasticsearch`, `loki`, `prometheus`, `kubernetes`,
`grafana`, ...). The harness groups emissions by domain and writes ONE
"Known quirks for querying <domain>" SKILL.md per domain, with each
quirk rendered as a numbered entry in a `## Known quirks` body. A
future investigation that uses that data source fetches one skill and
sees every quirk this team's environment has.

Mechanism changes:
- suggest_runbooks tool schema: add required `skill_domain` field.
- System prompt: explicit CONSOLIDATION section telling the agent to
  pick the coarsest stable data-source name and reuse it across all
  quirks for that source in one investigation.
- write_memories_as_skill_files: groups by `skill_domain` via an
  OrderedDict, normalizes domain strings to lowercase-hyphenated slugs,
  writes one SKILL.md per group with a multi-quirk body. Falls back to
  domain="general" if absent so older emissions don't crash.
- AskHolmesTestCase: new `expected_skill_count` field. Asserts the
  number of written files equals the declared value — catches the
  failure mode where the agent invents N different domains instead
  of consolidating.

Eval changes:
- New fixture 268_es_multi_quirk_consolidation. Sets up three ES
  indices each with one quirk (severity/severity_num/ingest_ts —
  borrowed from 261/264/265 patterns), asks one question per index in
  one prompt. Expects three quirks captured all tagged
  `skill_domain: "elasticsearch"`, consolidating into one file
  (expected_skill_count: 1). Replay asks the same three questions in a
  natural phrasing and verifies the agent fetches that one consolidated
  skill and answers all three.
- Unit tests cover the consolidation logic directly: 3 memories across
  2 domains → 2 files (with the ES file containing both ES quirks),
  and the fallback-domain case for legacy un-tagged emissions.

Existing 261-265 fixtures keep working: their single-quirk emissions
become single-entry domain skills. The new mechanism only adds shape;
it doesn't reject the old shape.

Out of scope (next iteration if this approach lands):
- Real update-existing semantics (in production a domain skill would
  be a DB upsert across investigations; here the harness merges within
  a single eval run only).
- Cross-domain skills (e.g. correlating Kubernetes events with Loki
  logs).

Branches off claude/add-tool-suggestions-matrix-av1Uv @ 4d27a91,
preserving that branch as a shippable fallback if this approach
doesn't pan out.

Signed-off-by: Claude <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

github-actions Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

📂 Previous Runs

⚠️ 1 older run truncated

Older runs were omitted to stay under GitHub's 64KB comment size limit.


✅ Results of HolmesGPT evals

Automatically triggered by commit f03e45a on branch claude/consolidated-skills-per-domain

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 23/23 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Skill Generated Skills Read Compactions Denied commands Src
✅ 09_crashpod 33.8s 4 8 $0.2428 83,651 81,872 22,912 1,779 685 58,159 23,713 218 — — — — src
✅ 101_loki_historical_logs_pod_deleted 47.1s 5 10 $0.2903 108,467 105,883 25,222 2,584 789 80,372 25,511 239 — — — — src
✅ 112_find_pvcs_by_uuid 15.3s 2 2 $0.1801 39,956 39,088 21,353 868 437 17,732 21,356 269 — — — — src
✅ 12_job_crashing 31.9s 5 11 $0.2613 109,372 107,641 24,091 1,731 441 83,126 24,515 66 — — — — src
✅ 176_network_policy_blocking_traffic_no_skills 43.9s 5 11 $0.2710 110,300 108,329 24,853 1,971 615 83,470 24,859 349 — — — — src
✅ 227_count_configmaps_per_namespace[0] 18.6s 3 6 $0.1774 56,609 55,893 20,075 716 432 35,814 20,079 37 — — — — src
✅ 243_pod_names_contain_service 25.8s 3 7 $0.2082 59,661 58,280 21,243 1,381 660 36,266 22,014 162 — — — — src
✅ 24_misconfigured_pvc 36.2s 6 11 $0.2639 126,658 124,949 22,668 1,709 574 101,306 23,643 28 — — — — src
✅ 254_elasticsearch_dr_test_log_check 71.0s 10 13 $0.3524 175,106 171,188 22,615 3,918 915 147,071 24,117 241 1 — — — src
✅ 259_wrong_cluster_logs_confusion 88.2s 11 14 $0.3894 196,125 191,423 23,018 4,702 973 166,536 24,887 748 1 — — — src
✅ 260_global_es_remote_cluster_logs 70.4s 10 12 $0.3307 175,730 172,253 21,509 3,477 718 149,878 22,375 227 1 — — — src
✅ 261_es_log_severity_field 20.8s 4 3 $0.2653 113,740 111,930 24,215 1,810 558 87,545 24,385 282 1 — — — src
✅ 261_es_log_severity_field [replay] 21.6s 4 3 $0.1944 77,720 76,843 20,238 877 325 56,600 20,243 171 — — — — src
✅ 262_prometheus_custom_kafka_metric_name 14.0s 3 2 $0.2352 94,855 93,839 24,598 1,016 407 69,236 24,603 109 1 — — — src
✅ 262_prometheus_custom_kafka_metric_name [replay] 14.5s 3 2 $0.1806 62,270 61,848 21,342 422 177 40,502 21,346 83 — — — — src
✅ 263_loki_custom_stream_label 29.3s 5 4 $0.2199 98,392 97,300 21,552 1,092 477 75,742 21,558 113 1 — — — src
✅ 263_loki_custom_stream_label [replay] 29.8s 5 4 $0.1914 82,932 81,704 17,806 1,228 414 63,892 17,812 212 — — — — src
✅ 264_es_numeric_severity_field 39.1s 5 7 $0.3091 141,728 139,262 25,906 2,466 630 113,189 26,073 438 1 — — — src
✅ 264_es_numeric_severity_field [replay] 40.0s 5 7 $0.2614 104,246 101,993 22,716 2,253 838 79,271 22,722 470 — — — — src
✅ 265_es_timestamp_keyword_field 25.8s 5 4 $0.2723 114,973 112,947 24,418 2,026 637 88,523 24,424 366 1 — — — src
✅ 265_es_timestamp_keyword_field [replay] 26.7s 5 4 $0.2198 99,542 98,360 21,059 1,182 320 77,295 21,065 141 — — — — src
✅ 266_es_schema_discovery 16.0s 4 3 $0.2307 108,706 107,680 22,640 1,026 286 85,034 22,646 61 1 — — — src
✅ 266_es_schema_discovery [replay] 16.9s 4 3 $0.1862 77,046 76,400 19,988 646 197 56,407 19,993 50 — — — — src
✅ 267_bad_skill_resilience 61.1s 9 8 $0.3575 217,890 215,304 26,556 2,586 588 188,738 26,566 458 1 — — — src
✅ 268_es_multi_quirk_consolidation 47.9s 5 10 $0.4746 195,752 189,845 32,058 5,907 1,212 156,788 33,057 710 3 — — — src
✅ 268_es_multi_quirk_consolidation [replay] 48.8s 5 10 $0.3046 110,218 107,185 24,969 3,033 936 81,348 25,837 541 — — — — src
✅ 269_es_skill_update_cross_investigation 32.4s 5 7 $0.4276 250,435 246,417 28,707 4,018 770 217,699 28,718 939 1 — — — src
✅ 269_es_skill_update_cross_investigation [replay] 33.2s 5 7 $0.2513 104,308 102,463 22,796 1,845 518 79,661 22,802 120 — — — — src
✅ 43_current_datetime_from_prompt 3.7s 1 — $0.1252 17,841 17,719 17,719 122 122 0 17,719 78 — — — — src
✅ 51_logs_summarize_errors 21.1s 3 2 $0.1831 57,221 56,451 20,695 770 390 35,752 20,699 34 — — — — src
✅ 61_exact_match_counting 10.6s 2 1 $0.1398 35,967 35,750 18,051 217 148 17,696 18,054 30 — — — — src
Total 35.0s avg 5.0 avg 7.1 avg $6.2078 2,689,135 2,641,243 32,058 47,892 1,212 2,095,672 545,571 6,202 14 — — —
Skills mechanism stats
  • Evals that emitted at least one memory: 12
  • Replays attempted: 8
  • Replays where the agent loaded the captured skill: 8/8
  • Replays that answered correctly: 8/8
  • Mean replay vs primary delta (per-row average): ↓24% cost, ↓32% tokens (n=8)
Benchmark Comparison Details

Master baseline: latest master-* experiment (post-merge regression eval)
Status: 11 test/model combinations loaded

Benchmark baseline: latest ci-benchmark experiment on master
Status: 34 test/model combinations loaded

Time comparison (seconds):

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 33.8s 26.2s ↑29% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 47.1s 47.6s ±0% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 15.3s 10.6s ↑45% — —
12_job_crashing (opus-4.6) 📄 31.9s 27.6s ↑16% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 43.9s 32.6s ↑35% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 18.6s 13.2s ↑41% — —
243_pod_names_contain_service (opus-4.6) 📄 25.8s 28.8s ↓11% — —
24_misconfigured_pvc (opus-4.6) 📄 36.2s 31.4s ↑15% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 71.0s — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 88.2s — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 70.4s — — — —
261_es_log_severity_field (opus-4.6) 📄 20.8s — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 14.0s — — — —
263_loki_custom_stream_label (opus-4.6) 📄 29.3s — — — —
264_es_numeric_severity_field (opus-4.6) 📄 39.1s — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 25.8s — — — —
266_es_schema_discovery (opus-4.6) 📄 16.0s — — — —
267_bad_skill_resilience (opus-4.6) 📄 61.1s — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 47.9s — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 32.4s — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 3.7s 3.1s ↑21% — —
51_logs_summarize_errors (opus-4.6) 📄 21.1s 18.8s ↑12% — —
61_exact_match_counting (opus-4.6) 📄 10.6s 7.1s ↑49% — —
Total (all, n=23) 35.0s 22.5s — — —
Comparable (m=11, b=0) 26.2s 22.5s ↑17% — —

Cost comparison:

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 $0.2428 $0.2032 ↑20% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 $0.2903 $0.2642 ±0% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 $0.1801 $0.1368 ↑32% — —
12_job_crashing (opus-4.6) 📄 $0.2613 $0.2173 ↑20% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 $0.2710 $0.2524 ±0% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 $0.1774 $0.1464 ↑21% — —
243_pod_names_contain_service (opus-4.6) 📄 $0.2082 $0.1872 ↑11% — —
24_misconfigured_pvc (opus-4.6) 📄 $0.2639 $0.2240 ↑18% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 $0.3524 — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 $0.3894 — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 $0.3307 — — — —
261_es_log_severity_field (opus-4.6) 📄 $0.2653 — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 $0.2352 — — — —
263_loki_custom_stream_label (opus-4.6) 📄 $0.2199 — — — —
264_es_numeric_severity_field (opus-4.6) 📄 $0.3091 — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 $0.2723 — — — —
266_es_schema_discovery (opus-4.6) 📄 $0.2307 — — — —
267_bad_skill_resilience (opus-4.6) 📄 $0.3575 — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 $0.4746 — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 $0.4276 — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 $0.1252 $0.0110 ↑1039% — —
51_logs_summarize_errors (opus-4.6) 📄 $0.1831 $0.1488 ↑23% — —
61_exact_match_counting (opus-4.6) 📄 $0.1398 $0.1112 ↑26% — —
Total (all, n=23) $0.2699 $0.1730 — — —
Comparable (m=11, b=0) $0.2130 $0.1730 ↑23% — —

Total tokens comparison:

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 83,651 67,778 ↑23% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 108,467 88,307 ↑23% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 39,956 30,686 ↑30% — —
12_job_crashing (opus-4.6) 📄 109,372 69,487 ↑57% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 110,300 108,726 ±0% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 56,609 44,988 ↑26% — —
243_pod_names_contain_service (opus-4.6) 📄 59,661 48,612 ↑23% — —
24_misconfigured_pvc (opus-4.6) 📄 126,658 68,923 ↑84% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 175,106 — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 196,125 — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 175,730 — — — —
261_es_log_severity_field (opus-4.6) 📄 113,740 — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 94,855 — — — —
263_loki_custom_stream_label (opus-4.6) 📄 98,392 — — — —
264_es_numeric_severity_field (opus-4.6) 📄 141,728 — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 114,973 — — — —
266_es_schema_discovery (opus-4.6) 📄 108,706 — — — —
267_bad_skill_resilience (opus-4.6) 📄 217,890 — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 195,752 — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 250,435 — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 17,841 13,970 ↑28% — —
51_logs_summarize_errors (opus-4.6) 📄 57,221 45,163 ↑27% — —
61_exact_match_counting (opus-4.6) 📄 35,967 28,231 ↑27% — —
Total (all, n=23) 116,919 55,897 — — —
Comparable (m=11, b=0) 73,246 55,897 ↑31% — —

Cached tokens comparison:

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 58,159 46,700 ↑25% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 80,372 64,209 ↑25% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 17,732 13,861 ↑28% — —
12_job_crashing (opus-4.6) 📄 83,126 46,249 ↑80% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 83,470 84,274 ±0% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 35,814 28,072 ↑28% — —
243_pod_names_contain_service (opus-4.6) 📄 36,266 28,690 ↑26% — —
24_misconfigured_pvc (opus-4.6) 📄 101,306 46,110 ↑120% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 147,071 — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 166,536 — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 149,878 — — — —
261_es_log_severity_field (opus-4.6) 📄 87,545 — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 69,236 — — — —
263_loki_custom_stream_label (opus-4.6) 📄 75,742 — — — —
264_es_numeric_severity_field (opus-4.6) 📄 113,189 — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 88,523 — — — —
266_es_schema_discovery (opus-4.6) 📄 85,034 — — — —
267_bad_skill_resilience (opus-4.6) 📄 188,738 — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 156,788 — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 217,699 — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 — 13,845 — — —
51_logs_summarize_errors (opus-4.6) 📄 35,752 28,012 ↑28% — —
61_exact_match_counting (opus-4.6) 📄 17,696 13,825 ↑28% — —
Total (all, n=23) 91,116 37,622 — — —
Comparable (m=10, b=0) 54,969 40,000 ↑37% — —

Turns comparison:

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 4 4 ±0% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 5 5 ±0% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 2 2 ±0% — —
12_job_crashing (opus-4.6) 📄 5 4 ↑25% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 5 6 ↓17% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 3 3 ±0% — —
243_pod_names_contain_service (opus-4.6) 📄 3 3 ±0% — —
24_misconfigured_pvc (opus-4.6) 📄 6 4 ↑50% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 10 — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 11 — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 10 — — — —
261_es_log_severity_field (opus-4.6) 📄 4 — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 3 — — — —
263_loki_custom_stream_label (opus-4.6) 📄 5 — — — —
264_es_numeric_severity_field (opus-4.6) 📄 5 — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 5 — — — —
266_es_schema_discovery (opus-4.6) 📄 4 — — — —
267_bad_skill_resilience (opus-4.6) 📄 9 — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 5 — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 5 — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 1 1 ±0% — —
51_logs_summarize_errors (opus-4.6) 📄 3 3 ±0% — —
61_exact_match_counting (opus-4.6) 📄 2 2 ±0% — —
Total (all, n=23) 5.0 3.4 — — —
Comparable (m=11, b=0) 3.5 3.4 ±0% — —

Tool calls comparison:

Test case This branch master (2d ago) Δ vs master benchmark (2h ago) Δ vs benchmark
09_crashpod (opus-4.6) 📄 8 8 ±0% — —
101_loki_historical_logs_pod_deleted (opus-4.6) 📄 10 11 ±0% — —
112_find_pvcs_by_uuid (opus-4.6) 📄 2 2 ±0% — —
12_job_crashing (opus-4.6) 📄 11 9 ↑22% — —
176_network_policy_blocking_traffic_no_skills (opus-4.6) 📄 11 12 ±0% — —
227_count_configmaps_per_namespace[0] (opus-4.6) 📄 6 6 ±0% — —
243_pod_names_contain_service (opus-4.6) 📄 7 7 ±0% — —
24_misconfigured_pvc (opus-4.6) 📄 11 13 ↓15% — —
254_elasticsearch_dr_test_log_check (opus-4.6) 📄 13 — — — —
259_wrong_cluster_logs_confusion (opus-4.6) 📄 14 — — — —
260_global_es_remote_cluster_logs (opus-4.6) 📄 12 — — — —
261_es_log_severity_field (opus-4.6) 📄 3 — — — —
262_prometheus_custom_kafka_metric_name (opus-4.6) 📄 2 — — — —
263_loki_custom_stream_label (opus-4.6) 📄 4 — — — —
264_es_numeric_severity_field (opus-4.6) 📄 7 — — — —
265_es_timestamp_keyword_field (opus-4.6) 📄 4 — — — —
266_es_schema_discovery (opus-4.6) 📄 3 — — — —
267_bad_skill_resilience (opus-4.6) 📄 8 — — — —
268_es_multi_quirk_consolidation (opus-4.6) 📄 10 — — — —
269_es_skill_update_cross_investigation (opus-4.6) 📄 7 — — — —
43_current_datetime_from_prompt (opus-4.6) 📄 — — — — —
51_logs_summarize_errors (opus-4.6) 📄 2 2 ±0% — —
61_exact_match_counting (opus-4.6) 📄 1 1 ±0% — —
Total (all, n=23) 6.8 7.1 — — —
Comparable (m=10, b=0) 6.9 7.1 ±0% — —

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📖 Legend
Icon Meaning
✅ The test was successful
➖ The test was skipped
⚠️ The test failed but is known to be flaky or known to fail
🚧 The test had a setup failure (not a code regression)
🔧 The test failed due to mock data issues (not a code regression)
🚫 The test was throttled by API rate limits/overload
❌ The test failed and should be fixed before merging the PR
🔄 Re-run evals manually

⚠️ Warning: /eval comments always run using the workflow from master, not from this PR branch. If you modified the GitHub Action (e.g., added secrets or env vars), those changes won't take effect.

To test workflow changes, use the GitHub CLI or Actions UI instead:

gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/consolidated-skills-per-domain -f markers=regression -f filter=

Option 1: Comment on this PR with /eval:

/eval
tags: regression

Or with more options (one per line):

/eval
model: gpt-4o
tags: regression
id: 09_crashpod
iterations: 5

Run evals on a different branch (e.g., master) for comparison:

/eval
branch: master
tags: regression
Option Description
model Model(s) to test (default: same as automatic runs)
tags Pytest tags / markers (no default - runs all tests!)
id Eval ID / pytest -k filter (use /list to see valid eval names)
iterations Number of runs, max 10
branch Run evals on a different branch (for cross-branch comparison)

Quick re-run: Use /rerun to re-run the most recent /eval on this PR with the same parameters.

Option 2: Trigger via GitHub Actions UI → "Run workflow"

Option 3: Add PR labels to include extra evals (applies to both automatic runs and /eval comments):

Label Effect
evals-tag-<name> Run tests with tag <name> alongside regression
evals-id-<name> Run a specific eval by test ID
evals-model-<name> Override the model (use model list name, e.g. sonnet-4.5)

Examples: evals-tag-easy, evals-id-09_crashpod, evals-model-sonnet-4.5

🏷️ Valid tags

benchmark, chain-of-causation, compaction, confluence, context_window, conversation_worker, coralogix, counting, database, datadog, datetime, db-connectors, easy, elasticsearch, embeds, fast, frontend, grafana, hard, images, integration, kafka, kubernetes, leaked-information, logs, loki, manual, mcp, medium, metrics, multi-cluster, network, newrelic, no-cicd, numerical, one-test, port-forward, prometheus, question-answer, regression, skills, slackbot, storage, token-limit, toolset-limitation, traces, transparency, victorialogs

🤖 Valid models

deepseek-chat, deepseek-r1-reasoner, deepseek-reasoner, deepseek-v3.2-chat, gemini-3-flash-preview, gemini-3-pro-preview, gemini-3.1-pro-preview, gpt-4.1, gpt-5.2-high-reasoning, gpt-5.3-codex, gpt-5.4, gpt-5.5, haiku-4.5, kimi-2.5, kimi-2.5-openrouter, opus-4.5, opus-4.6, opus-4.7, opus-4.8, qwen-next-80B-instruct, qwen-next-80B-thinking, sonnet-4.5, sonnet-4.6


Commands: /eval · /rerun · /list

CLI: gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/consolidated-skills-per-domain -f markers=regression -f filter=

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds suggest-runbooks tooling, captures suggested memories during evals, writes/merges them as SKILL.md, supports closed-loop replay that loads those skills, and propagates memory/replay metrics into properties, Braintrust traces, and GitHub reporting. Includes many new fixtures exercising discovery and quirk consolidation.

Changes

Closed-Loop Learning with Memory Capture and Replay for Holmes LLM Evals

Layer / File(s) Summary
Goal Criteria and Success Metrics
.goal_criteria.md
Defines metric definitions, replay improvement thresholds, regression guards, and verification/source-of-truth rules.
Pytest result payloads
tests/llm/conftest.py
Expands per-test result dictionaries to include memories_count, suggested_memories, primary_passed, and a set of replay_* metrics.
Existing fixture memory flags
tests/llm/fixtures/test_ask_holmes/{09,12,24,43,51,61,101,112,176,227,243}_test_case.yaml
Adds memories_generated: false to fixtures that should not emit memories.
Prometheus discovery fixture
tests/llm/fixtures/test_ask_holmes/262_*
Adds fake exporter, scrape config, readiness/scrape verification, and toolset config to require metric-name discovery (acme_kafka_*).
Loki stream-label discovery fixture
tests/llm/fixtures/test_ask_holmes/263_*
Adds log producer, promtail ConfigMap/Deployment, biased prompt to force label discovery (acme_service), setup/teardown, and Loki toolset config.
Elasticsearch quirk fixtures
tests/llm/fixtures/test_ask_holmes/{261,264,265,268,267,269}_*
Multiple ES fixtures added for custom severity, numeric severity, keyword timestamp, multi-quirk consolidation, bad-skill resilience, and cross-investigation skill update; include toolsets and setup/cleanup scripts.
Suggest-runbooks tooling
tests/llm/utils/tool_suggestions_config.py, tests/test_tool_suggestions_config.py
Adds constants/schema/system-prompt, injection helper, memory extraction (params + description JSON fallback), normalization, SKILL.md parsing/merge/dedup, and comprehensive unit tests.
Test-case schema extensions
tests/llm/utils/test_case_utils.py
Adds memories_generated, rerun_with_memory, pre_loaded_skills_path, expected_skill_count to HolmesTestCase and replay_user_prompt, expected_replay_output to AskHolmesTestCase.
Harness orchestration & ask_holmes
tests/llm/test_ask_holmes.py, tests/llm/utils/test_toolset.py
Extends ask_holmes to inject suggest-runbooks, accept additional_skill_paths, override prompts for replay, capture suggested memories, write SKILL.md, optionally merge preloaded skills, rerun with replay-only config, and assert fetch_skill + replay correctness; TestToolsetManager accepts additional_skill_paths.
Property tracking and Braintrust
tests/llm/utils/property_manager.py, tests/llm/utils/braintrust.py
Initializes memories_count/suggested_memories; update_test_results accepts suggested_memories and appends formatted JSON for judging; log_to_braintrust logs memories metadata when present.
GitHub reporting: Memories and replay
tests/llm/utils/reporting/github_reporter.py
Adds "Memories" column, emits inline [replay] rows with replay-specific metrics/status, overrides primary row status for replay-success cases, totals captured memories, and appends a skills net-win summary.
Test status semantics & tests
tests/llm/utils/test_results.py, tests/test_test_results.py
TestStatus.passed now requires judge correctness==1 and pytest status empty/passed. Adds unit tests covering pass/fail/regression/empty/skipped cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • moshemorad
  • RoiGlinik
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main feature: implementing a skills/memories mechanism that captures environment-specific tool corrections and discoveries, consolidates them per data source, and enables replay/reuse. This directly aligns with the changeset's core objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

✅ Docker images ready for 9d5a00dfb (built in 59s)

⚠️ Warning: does not support ARM (ARM images are built on release only - not on every PR)

Use these tags to pull the images for testing.

📋 Copy commands

⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:

gcloud auth configure-docker us-central1-docker.pkg.dev
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:9d5a00dfb
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:9d5a00dfb me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:9d5a00dfb
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:9d5a00dfb
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:9d5a00dfb
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:9d5a00dfb me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:9d5a00dfb
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:9d5a00dfb

Patch Helm values in one line (choose the chart you use):

HolmesGPT chart:

helm upgrade --install holmesgpt ./helm/holmes \
  --set registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set image=holmes-dev:9d5a00dfb \
  --set operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set operator.image=holmes-operator-dev:9d5a00dfb

Robusta wrapper chart:

helm upgrade --install robusta robusta/robusta \
  --reuse-values \
  --set holmes.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.image=holmes-dev:9d5a00dfb \
  --set holmes.operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.operator.image=holmes-operator-dev:9d5a00dfb

@netlify

netlify Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for holmes-docs ready!

Name Link
🔨 Latest commit f03e45a
🔍 Latest deploy log https://app.netlify.com/projects/holmes-docs/deploys/6a291562d0b0590008135d3c
😎 Deploy Preview https://deploy-preview-2128--holmes-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/llm/fixtures/test_ask_holmes/61_exact_match_counting/test_case.yaml`:
- Around line 40-41: Add the missing include_tool_calls: true key to this
generic expected-output fixture so the test forces tool-call transparency;
specifically update the test_case.yaml to include include_tool_calls: true
alongside the existing memories_generated: false and expected_output entries so
the test cannot pass by hallucinating counts without tool-call evidence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 854369e8-8fc2-43d6-8ff4-622581bd4fd9

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0a70f and c5c87c9.

📒 Files selected for processing (40)
  • .goal_criteria.md
  • tests/llm/conftest.py
  • tests/llm/fixtures/test_ask_holmes/09_crashpod/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/101_loki_historical_logs_pod_deleted/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/112_find_pvcs_by_uuid/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/12_job_crashing/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/227_count_configmaps_per_namespace/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/243_pod_names_contain_service/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/24_misconfigured_pvc/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/261_es_log_severity_field/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/261_es_log_severity_field/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/262_prometheus_custom_kafka_metric_name/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/262_prometheus_custom_kafka_metric_name/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/263_loki_custom_stream_label/app.py
  • tests/llm/fixtures/test_ask_holmes/263_loki_custom_stream_label/deployment.yaml
  • tests/llm/fixtures/test_ask_holmes/263_loki_custom_stream_label/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/263_loki_custom_stream_label/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/264_es_numeric_severity_field/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/264_es_numeric_severity_field/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/265_es_timestamp_keyword_field/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/265_es_timestamp_keyword_field/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/267_bad_skill_resilience/bad_skill/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/267_bad_skill_resilience/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/267_bad_skill_resilience/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/268_es_multi_quirk_consolidation/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/268_es_multi_quirk_consolidation/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/43_current_datetime_from_prompt/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/51_logs_summarize_errors/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/61_exact_match_counting/test_case.yaml
  • tests/llm/test_ask_holmes.py
  • tests/llm/utils/braintrust.py
  • tests/llm/utils/property_manager.py
  • tests/llm/utils/reporting/github_reporter.py
  • tests/llm/utils/test_case_utils.py
  • tests/llm/utils/test_results.py
  • tests/llm/utils/test_toolset.py
  • tests/llm/utils/tool_suggestions_config.py
  • tests/test_test_results.py
  • tests/test_tool_suggestions_config.py

Comment on lines +40 to +41

memories_generated: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add include_tool_calls: true for this generic expected-output fixture.

expected_output at Line 4 is intentionally broad (“either 6, 6 pods, etc.”). Without tool-call evidence, this can pass while hallucinating the count. Since Line 41 now enforces a strict memory behavior, this fixture should also enforce tool-call transparency.

💡 Suggested change
 tags:
   - counting
   - easy
   - regression
 
+include_tool_calls: true
 memories_generated: false

As per coding guidelines: "Use include_tool_calls: true in test_case.yaml when expected output is too generic to rule out hallucinations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llm/fixtures/test_ask_holmes/61_exact_match_counting/test_case.yaml`
around lines 40 - 41, Add the missing include_tool_calls: true key to this
generic expected-output fixture so the test forces tool-call transparency;
specifically update the test_case.yaml to include include_tool_calls: true
alongside the existing memories_generated: false and expected_output entries so
the test cannot pass by hallucinating counts without tool-call evidence.

claude added 2 commits June 5, 2026 07:39
Run 3 of the local eval surfaced a fixture bug: the test data hard-
coded ingest_ts to 2026-06-03 while the replay prompt ("how many ERROR
entries are there from today") let the agent resolve "today" to the
actual current date — which made index C return 0 entries (no logs on
the real today) even though the agent correctly fetched the
consolidated skill and used `ingest_ts` instead of `@timestamp`.

The skills mechanism worked perfectly in that run: agent emitted three
quirks all under skill_domain=elasticsearch, harness consolidated to
one SKILL.md, replay fetched that one skill and used the right field
for each of the three indices. Only the date arithmetic was off.

Fix: setup writes ingest_ts using the real $(date -u) for today and
yesterday so the data follows wall-clock time, and the primary prompt
no longer hardcodes "2026-06-03" — it asks for "today's date" so the
biased path still names @timestamp as the field to try but lets the
agent pick the current date. This makes the test stable regardless of
when CI runs it.

Signed-off-by: Claude <noreply@anthropic.com>
Run 4 surfaced the same severity_num ambiguity 264 has: bare integers
1-6 follow either syslog (lower = more severe) or winston/bunyan
(higher = more severe), and the agent reasonably inferred the wrong
one from the values alone. The data uses winston: 6=fatal, 5=error,
3=info — but the agent guessed syslog and reported 5 ERROR-equivalents
(severity_num=3) instead of 8 (severity_num in [5,6]).

Embedding the level name in the message text ("[ERROR] DB timeout",
"[FATAL] Disk full", "[INFO] healthcheck") gives any agent that
samples a single document an unambiguous cue. The discovery is still
real — the agent has to find that severity_num exists and figure out
the mapping — but now the data itself disambiguates, just like a real
customer's logs would. The skill emission will still capture the
discovered mapping under skill_domain=elasticsearch.

Signed-off-by: Claude <noreply@anthropic.com>
@aantn aantn changed the title Add tool_suggestions matrix to eval runs Skills/memories: capture, replay, consolidate by data-source domain Jun 5, 2026
claude added 3 commits June 5, 2026 12:11
The within-investigation consolidation 268 proved (multiple quirks
emitted in one pass → one domain skill) is the easy case. The harder,
and more realistic, case is: a saved domain skill from a prior
investigation grows when a later investigation discovers a new quirk
in the same data source. Without that, every customer would be stuck
with whatever first-investigation skill was captured, and the catalog
couldn't evolve.

This commit adds:

* `_parse_quirks_from_skill_md` parses a previously-written domain
  SKILL.md back into the same quirk-dict shape `write_memories_as_
  skill_files` produces. Lenient parser — missing fields produce
  empty strings, legacy un-numbered headings still match.
* `_existing_domain_skill_path` locates an existing domain skill
  under a target dir by matching `*quirks-for-querying-<domain>` so
  the numeric ordering prefix doesn't break lookup.
* `write_memories_as_skill_files` now reads any pre-existing domain
  skill at the target path, parses its quirks, prepends them to the
  new emissions, dedupes by title, and rewrites the merged file. The
  same skill directory is reused so the filesystem layout stays
  stable as the skill grows.
* `test_ask_holmes.py` replay block copies the fixture's
  `pre_loaded_skills_path` contents into the replay tempdir BEFORE
  calling the writer, so an eval can simulate a customer who already
  has a saved domain skill and the new emission has something to
  merge into.

Test coverage:
* Unit test: emitting a second quirk merges into the first instead of
  creating a parallel file or overwriting (verifies cross-
  investigation accumulation).
* Unit test: re-emitting the same title is deduped.
* Eval `269_es_skill_update_cross_investigation`: pre-loads a one-
  quirk ES domain skill (severity field for index A), asks about
  index B which requires a different quirk (ingest_ts timestamp).
  After the primary, the saved skill should contain BOTH quirks.
  Replay asks a cross-index question requiring both quirks; the
  agent must fetch the merged skill and use the right field for
  each index.

Signed-off-by: Claude <noreply@anthropic.com>
Run 1 of 269 failed because the primary asks only about index B but
expected_output listed answers for both A and B (which is what the
replay asks). The framework checked the primary's answer against both
expected elements and scored 0 because the primary correctly only
addressed B.

Adds `AskHolmesTestCase.expected_replay_output`: when set, the replay
correctness check uses this list instead of the primary's
`expected_output`. Falls back to the primary's `expected_output` when
unset (existing evals are unaffected).

269 now declares:
- expected_output: just B's answer (what primary asks)
- expected_replay_output: both A and B answers (what replay asks
  after the merge made both quirks available in one skill)

Signed-off-by: Claude <noreply@anthropic.com>
Eval 269 verifies cross-investigation merge of an emitted quirk into a
pre-loaded domain skill. The primary pass already proves the merge via
expected_skill_count: 1 (one consolidated file containing both quirks).

On the 2-index replay question, the agent rationally chooses cheap
mapping inspection over fetch_skill, which made the strict
fetch_skill_called assertion fail even though the merge — the thing
the eval is actually about — succeeded. Skill-load behavior is already
covered by 261/262/264/265/268.

Add an opt-out field (default True so existing evals are unchanged)
and enable it on 269.

Signed-off-by: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/test_tool_suggestions_config.py (2)

156-157: ⚡ Quick win

Add type hints to the new test function signatures.

The new test functions are missing annotations for tmp_path and return type.

💡 Suggested fix
+from pathlib import Path
@@
-def test_write_memories_merges_into_existing_domain_skill(tmp_path):
+def test_write_memories_merges_into_existing_domain_skill(tmp_path: Path) -> None:
@@
-def test_write_memories_dedupes_existing_quirks(tmp_path):
+def test_write_memories_dedupes_existing_quirks(tmp_path: Path) -> None:

As per coding guidelines, **/*.py: "Type hints required (mypy configuration in pyproject.toml)".

Also applies to: 207-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tool_suggestions_config.py` around lines 156 - 157, Add missing
type hints to the new test function signatures: annotate the tmp_path parameter
as pathlib.Path (or pytest.Path if your test suite uses that alias) and add a
return type of None for functions such as
test_write_memories_merges_into_existing_domain_skill (and the other new tests
referenced around lines 207-208); update the function signatures to look like
def test_write_memories_merges_into_existing_domain_skill(tmp_path:
pathlib.Path) -> None: (and similarly for the other test names) to satisfy the
project's mypy/type-hint requirements.

161-165: ⚡ Quick win

Use module-level imports instead of importing inside test functions.

Both new tests import os and tool_suggestions_config symbols inside function scope. Move these imports to the file header.

As per coding guidelines, **/*.py: "Always place Python imports at the top of the file, not inside functions or methods".

Also applies to: 211-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tool_suggestions_config.py` around lines 161 - 165, Tests import
os and the tool_suggestions_config symbols inside test functions; move those
imports to the module top so they are module-level. Specifically, add "import
os" and the imports for write_memories_as_skill_files and
_parse_quirks_from_skill_md to the file header and remove the in-function
imports, and likewise update the other test functions that currently import os
or tool_suggestions_config symbols inside their bodies to use the header
imports.
tests/llm/utils/tool_suggestions_config.py (1)

372-372: ⚡ Quick win

Move function-local imports to module scope in this Python module.

Imports at Line 372, Line 439, and Line 468–469 should be top-level to align with project conventions and avoid repeated local import paths.

♻️ Suggested refactor
 import json
 import logging
+import os
+import re
+from collections import OrderedDict
 from typing import Any, Dict, List, Optional
@@
 def _slugify(text: str) -> str:
@@
-    import re
-
@@
 def _normalize_skill_domain(raw: Optional[str]) -> str:
@@
-    import re
-
@@
 def _parse_quirks_from_skill_md(skill_md_path: str) -> List[Dict[str, Any]]:
@@
-    import re
-
@@
 def _existing_domain_skill_path(
@@
-    import os
-
@@
 def write_memories_as_skill_files(
@@
-    import os
-    from collections import OrderedDict

As per coding guidelines, **/*.py: "Always place Python imports at the top of the file, not inside functions or methods".

Also applies to: 439-440, 468-469

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llm/utils/tool_suggestions_config.py` at line 372, Move the
function-local imports to module scope: find every inline import statement
(e.g., "import re" and the other imports currently declared inside functions)
and hoist them to the top of the file, then remove the duplicate local imports
inside the functions; ensure you import the same symbols at module-level so
functions using them (search for occurrences of "import re" and other inline
imports) keep working and adjust any conditional/circular cases if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/llm/utils/tool_suggestions_config.py`:
- Around line 399-413: The parser _parse_quirks_from_skill_md currently includes
the inter-entry markdown separator (e.g. '---') in the sliced section so
_extract() can return values ending with '---'; fix by trimming trailing
markdown separator lines from the extracted value: after mm.group(1).strip()
(inside _extract) remove any trailing separator lines (e.g. lines that consist
of three or more hyphens and surrounding whitespace/newlines) before returning;
alternatively, adjust the section end logic that builds `section` (using
`matches`/`start`/`end`) to exclude adjacent separator-only lines so _extract
returns clean field text. Ensure references to _extract, section, matches and
_parse_quirks_from_skill_md are updated accordingly.

---

Nitpick comments:
In `@tests/llm/utils/tool_suggestions_config.py`:
- Line 372: Move the function-local imports to module scope: find every inline
import statement (e.g., "import re" and the other imports currently declared
inside functions) and hoist them to the top of the file, then remove the
duplicate local imports inside the functions; ensure you import the same symbols
at module-level so functions using them (search for occurrences of "import re"
and other inline imports) keep working and adjust any conditional/circular cases
if needed.

In `@tests/test_tool_suggestions_config.py`:
- Around line 156-157: Add missing type hints to the new test function
signatures: annotate the tmp_path parameter as pathlib.Path (or pytest.Path if
your test suite uses that alias) and add a return type of None for functions
such as test_write_memories_merges_into_existing_domain_skill (and the other new
tests referenced around lines 207-208); update the function signatures to look
like def test_write_memories_merges_into_existing_domain_skill(tmp_path:
pathlib.Path) -> None: (and similarly for the other test names) to satisfy the
project's mypy/type-hint requirements.
- Around line 161-165: Tests import os and the tool_suggestions_config symbols
inside test functions; move those imports to the module top so they are
module-level. Specifically, add "import os" and the imports for
write_memories_as_skill_files and _parse_quirks_from_skill_md to the file header
and remove the in-function imports, and likewise update the other test functions
that currently import os or tool_suggestions_config symbols inside their bodies
to use the header imports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1090871-740c-413e-a73e-980f481a721d

📥 Commits

Reviewing files that changed from the base of the PR and between 52b74a7 and fe5e3a1.

📒 Files selected for processing (7)
  • tests/llm/fixtures/test_ask_holmes/269_es_skill_update_cross_investigation/prior_quirks/quirks-for-querying-elasticsearch/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/269_es_skill_update_cross_investigation/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/269_es_skill_update_cross_investigation/toolsets.yaml
  • tests/llm/test_ask_holmes.py
  • tests/llm/utils/test_case_utils.py
  • tests/llm/utils/tool_suggestions_config.py
  • tests/test_tool_suggestions_config.py
✅ Files skipped from review due to trivial changes (2)
  • tests/llm/fixtures/test_ask_holmes/269_es_skill_update_cross_investigation/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/269_es_skill_update_cross_investigation/prior_quirks/quirks-for-querying-elasticsearch/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/llm/utils/test_case_utils.py
  • tests/llm/test_ask_holmes.py

Comment on lines +399 to +413
end = matches[idx + 1].start() if idx + 1 < len(matches) else len(body)
section = body[start:end]

def _extract(label: str) -> str:
# Pulls the text between `**<label>:**` and the next `**...**:`
# bold-label or end of section. Tolerates inline-value style
# ("**When to use:** text") and block-value style.
pat = re.compile(
rf"\*\*{re.escape(label)}:?\*\*\s*(.*?)(?=\n\*\*[^*]+:?\*\*|\Z)",
re.DOTALL,
)
mm = pat.search(section)
if not mm:
return ""
return mm.group(1).strip().lstrip("-").strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

_parse_quirks_from_skill_md can persist markdown separators into parsed quirk fields.

Line 399–413 parses each section up to the next heading, but that slice includes the inter-entry --- separator. Because _extract(...) reads to section end, values like why_env_specific can become "…\n\n---", and then get rewritten back into SKILL.md on merge.

💡 Suggested fix
-        section = body[start:end]
+        section = re.sub(r"\n---\s*$", "", body[start:end].strip(), flags=re.MULTILINE)

         def _extract(label: str) -> str:
@@
             pat = re.compile(
-                rf"\*\*{re.escape(label)}:?\*\*\s*(.*?)(?=\n\*\*[^*]+:?\*\*|\Z)",
+                rf"\*\*{re.escape(label)}:?\*\*\s*(.*?)(?=\n\*\*[^*]+:?\*\*|\n---\s*\n|\Z)",
                 re.DOTALL,
             )
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 410-410: Function definition does not bind loop variable section

(B023)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llm/utils/tool_suggestions_config.py` around lines 399 - 413, The
parser _parse_quirks_from_skill_md currently includes the inter-entry markdown
separator (e.g. '---') in the sliced section so _extract() can return values
ending with '---'; fix by trimming trailing markdown separator lines from the
extracted value: after mm.group(1).strip() (inside _extract) remove any trailing
separator lines (e.g. lines that consist of three or more hyphens and
surrounding whitespace/newlines) before returning; alternatively, adjust the
section end logic that builds `section` (using `matches`/`start`/`end`) to
exclude adjacent separator-only lines so _extract returns clean field text.
Ensure references to _extract, section, matches and _parse_quirks_from_skill_md
are updated accordingly.

claude added 5 commits June 5, 2026 13:25
The CI run on PR #2128 showed 261-265 all regressed to 0% — the agent on
replay ignored the saved skill and rediscovered the quirk from scratch.
Two root causes:

1. The replay-time skill written to a tempdir was being passed to
   SkillsToolset (so fetch_skill could load it by name) but NOT to the
   SkillCatalog loaded for the prompt's "Skill Catalog" section. The
   agent only saw the bare skill name in fetch_skill's param list, never
   the description. Fix: thread additional_skill_paths into the
   load_skill_catalog call in ask_holmes so the description block lands
   in the prompt.

2. The consolidated skill's description framed itself as a pre-flight
   optimization ("fetch BEFORE issuing the first query ... to skip
   wrong-call recovery"). The base prompt explicitly gates fetches with
   "only fetch skills that clearly match — do not fetch speculatively",
   so a soft hedge doesn't trip the gate. Rewrote to a deterministic,
   rule-based, aggressively imperative trigger: "MANDATORY pre-read for
   ANY investigation that will touch <domain> ... any user question
   that will touch <domain> is a clear match — fetch it, do not skip."

Also fixed 264's data setup: the numeric severity_num convention was
ambiguous to the model (syslog 3=ERROR/0=EMERG vs winston 5=ERROR/6=FATAL).
Embedded [ERROR]/[FATAL]/[WARN]/[INFO] tags in the msg field so a single
sample doc disambiguates — same fix that worked for 268.

Verified locally: 261, 264, 265 all pass at 100% with the fix.
262 and 263 need K8s and will be validated by CI.

Signed-off-by: Claude <noreply@anthropic.com>
The GitHub Actions eval report had one "Memories" column carrying
double duty: emitted-suggest_runbooks count on primary rows and
"skill ✓/✗" on replay rows. That conflated two different things and
made it impossible to see at a glance whether a primary investigation
ALSO consulted any pre-loaded skills.

Split into two columns:
- Skill Generated: count of suggest_runbooks emissions (always "—" on
  replay rows since suggest_runbooks isn't injected on replay)
- Skills Read: count of fetch_skill calls (tracked on both primary
  and replay)

Moved both to sit immediately before Compactions so the skills-related
counters cluster together at the end of the row.

Track new user properties skills_read_count and replay_skills_read_count
to source the new column. replay_skill_loaded boolean is kept for the
existing assert in test_ask_holmes.

Signed-off-by: Claude <noreply@anthropic.com>
The 'net win summary' block had an explanatory line ('Negative delta =
replay was cheaper...') and bold/imperative framing. Per feedback, just
emit the raw counts and the mean delta inside a collapsed <details>
block — readers can interpret on their own.

Signed-off-by: Claude <noreply@anthropic.com>
Conflicts: kept both sides everywhere —
- test_ask_holmes.py: additional_skill_paths + enable_todo / prompt overrides
- test_case_utils.py: skills fields + enable_todo
- test_toolset.py: additional_skill_paths + enable_todo
- github_reporter.py: Skill Generated/Skills Read columns + master's new
  Denied commands column (order: ...Reasoning | Skill Generated |
  Skills Read | Compactions | Denied commands | Src)
- braintrust.py: restore List import dropped by auto-merge

Signed-off-by: Claude <noreply@anthropic.com>
…ilures

The capture rule only fired on wrong->right corrections and explicitly
forbade emission when every call succeeded. That misses the larger cost
the mechanism exists to eliminate: in every fresh chat the agent re-learns
a data source's schema (mapping inspections, label/metric/index listings,
document sampling) before it can query - none of which fails, all of which
repeats.

Tool interface:
- New required `kind` field: "correction" (existing wrong->right pair) or
  "discovery" (stable env facts learned through successful exploration).
- failed_call documented as empty-string for discovery (strict tool mode
  force-requires all properties, so optionality is by convention).
- System prompt: capture-discovery patterns, plus a principled exclusion -
  schemas/naming conventions/addressing are stable and capturable;
  Kubernetes resource inventory and counts are transient and are not.

Skill rendering:
- Discovery entries render "Environment facts and call shape" instead of
  the failed/working pair; kind round-trips through the merge parser
  structurally (entry without a failed-call section = discovery).
- Importance now rendered and parsed back (merges no longer reset it).
- Titles sanitized to one line so `## N. title` headings and the merge
  parser's entry splitting can't be broken by multi-line LLM titles.
- New body DIRECTIVE: entries are verified facts, query directly, do not
  re-verify unless a query contradicts them. Needed because the generic
  fetch_skill wrapper says skill contents are "DIRECTIONS not ACTUAL
  RESULTS" / "just an EXAMPLE", which made the replay agent re-inspect
  the mapping to confirm the saved schema.

Harness:
- replay_forbidden_tools eval field: tools that must NOT appear on replay,
  proving the captured skill actually obviated the exploration it encodes.

Eval:
- 266_es_schema_discovery: index with compact custom fields (lvl/txt/app/
  ts); primary asks for field inventory + ERROR count so exploration
  happens with zero failures; replay asks naturally and must answer via
  the skill without calling elasticsearch_mappings.

Validated locally on opus-4.6: 266 passes 100% (discovery loop), 261
passes 100% (correction loop unaffected by the prompt rewrite). Unit
tests: 13 passing (3 new for discovery rendering/round-trip/title
sanitization).

Signed-off-by: Claude <noreply@anthropic.com>
@aantn aantn changed the title Skills/memories: capture, replay, consolidate by data-source domain Skills/memories: agent captures env-specific learnings (corrections + discoveries) as consolidated per-data-source skills Jun 10, 2026
The tool saves skills, not runbooks — the old name forced an awkward
counter-instruction in its own prompt ('Never refer to these as
runbooks'), now removed. The name is not a holmes-core or frontend
contract (zero references outside the eval harness), so the rename is
purely mechanical: wire name, constants, function names, fixture
comments.

Validated: 18 unit tests pass; eval 261 passes 100% end-to-end with the
renamed tool (emission + replay).

Signed-off-by: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants