8 Commits

Author SHA1 Message Date
codex
8bc8940d48 feat(hermes): one open proposal per rule, and link every issue to its run
All checks were successful
Tests / Declarative: Post Actions passed: 1413
One SonarQube rule is usually one root cause spread across many files. S2208
appears in three Ariadne modules and the cognitive-complexity rule in dozens,
and a sweep with no memory of what it already proposed would open a
near-identical pull request for every instance. Thirty of those get read as
none, which costs more than proposing nothing.

The sweep now skips any rule that already has an open proposal for that
project. The rules under review are read back from the open pull requests'
own titles rather than from a stored index: the pull requests are the thing
that actually exists, an index could disagree with them, and disagreeing is
the one failure mode that matters here. Once the open one is dealt with, the
next instance of that rule becomes eligible again.

This is not the root-cause collapse - it does not make one pull request fix
every instance of a rule, it just stops proposing the same rule repeatedly.
The collapse needs multi-file patch sets, which the frozen patch contract
cannot express yet.

Fails open like every other duplicate check here: an unreadable list yields no
known rules, so a lookup failure costs one extra proposal rather than
silently dropping a whole rule.

Issues now link their run id into the Hermes console, matching what pull
requests already do. Both artifacts claim a model made the call; both should
let a reader open the page where that call is visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:39:05 -03:00
codex
eebc9b16c4 feat(hermes): let an escalated diagnosis carry the fix it could not apply
All checks were successful
Tests / Declarative: Post Actions passed: 1404
Ariadne opens a pull request when it can: the repository is mapped, the file
is in the write allowlist, and the change is one anchored snippet the
validator can check. When any of that fails the incident escalates with a
diagnosis and nothing else - even though the model that wrote the diagnosis
frequently knows exactly what the fix is. That knowledge was discarded at the
moment it was most useful, because the cases where no patch is possible are
exactly the cases a maintainer has to do by hand.

A diagnosis may now carry up to three code suggestions: the file, what is
wrong there, and the code to change it to. Rendered into the issue under a
heading that says the change was not applied, because a code block in an issue
reads like something that already happened unless it is told otherwise.

Deliberately not a patch, and the difference is the safety story. A patch must
survive the validator because Ariadne acts on it. A suggestion is read by a
person who is already going to edit that file, so being wrong costs them a
moment's thought rather than a bad commit - which is why suggestions may
describe changes too large or too diffuse for the patcher to have attempted,
and why nothing here is anchored, applied or pushed.

Bounded at three. A diagnosis that suggests a dozen changes has stopped
diagnosing and started rewriting, and an issue that long buries the reason a
person was called in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:32:03 -03:00
codex
75c7ac10c7 feat(hermes): show the failure in the issue instead of citing where it is
All checks were successful
Tests / Declarative: Post Actions passed: 1374
Issue #2 on bstein/ariadne cites its evidence as "console_failures marker
'=== FAILURES ===' at line 1949". That is precise and unusable: the
maintainer has to open Jenkins, find build 408, scroll a 2000-line console and
reconstruct what the diagnosis had already read. The whole point of filing an
issue is that someone can act on it without doing that.

The traceback was not merely unrendered - it was never collected. The junit
query asked for errorDetails and not errorStackTrace, so the bundle carried
the assertion that failed but not the line it failed on, and no amount of
rendering would have found it. Both halves are fixed: the trace is fetched and
bounded, and the issue now opens an Evidence section with it.

Console regions are the fallback for a build that published no test results,
clipped to their last lines because the tail of a failure region holds the
failure while the head only approaches it. A fence inside an excerpt is
escaped so raw log text cannot break out of the code block and spill into the
rendered page.

Bounded deliberately. The body has a hard character budget, and an issue that
spends it on console noise buries the one sentence saying why a person is
needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:24:15 -03:00
codex
9f24f8d999 feat(hermes): let a diagnosis propose a remediation that does not exist yet
All checks were successful
Tests / Declarative: Post Actions passed: 1275
Every action in the allowlist got there because someone hit the failure by
hand, recognised the pattern, and wired a job for it. That loop only closes if
a person happens to read enough issues to notice the same failure recurring,
so a failure nobody reviews twice never earns an action. Until now a diagnosis
that fit nothing could only say a human was needed; it could not say what the
human should build.

An optional suggested_remediation field closes the other half of the loop.
When nothing in the allowlist fits, Hermes may name the remediation it
believes would work and the evidence that should be required before running it
is safe. It lands in the incident issue under a heading that states plainly
the remediation does not exist and was not performed, and in the audit event,
where the same proposal recurring across unrelated incidents is the evidence
that building it is worth the effort.

The field is inert by construction. No gate reads it, and an id that is not
already allowlisted still fails action_not_allowlisted exactly as before -
naming a remediation and being granted one stay different things, and only the
second needs a human to change a deployment. It is rejected outright alongside
a requested action: the field reports that nothing fit, so something fitting
contradicts it, and allowing both would invite a rationale to be attached to a
request the gates must judge on evidence alone. The key is optional rather
than required so a response written before today still validates unchanged,
and unknown keys are still refused.

Also names reclaim_workspace_storage, clear_stuck_agent_pods and
abort_hung_build in KNOWN_ACTION_LABELS. They were reporting as "unknown" in
metrics on any deployment that had not enabled them, which is the one case
where you most want to see the real name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:15:08 -03:00
codex
011bc2c993 fix(hermes): give a non-model finding an honest headline and label
All checks were successful
Tests / Declarative: Post Actions passed: 1183
Two remaining misattributions in the artifact a human reads first. The summary
line opened with "Hermes auto-triage classified" even when no run happened,
and the hung-build path fell through to the generic "undiagnosed" label
because HUNG_CLASSIFICATION was defined and never wired, so lesavka issue #3
was titled 'undiagnosed' for a build that plainly overran.

The headline now reads "Ariadne recorded incident X as Y" when there was no
run, and a caller may supply an explicit classification that is used only when
no diagnosis produced one - a real diagnosis is never overridden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 06:20:13 -03:00
codex
dca283a17b fix(hermes): stop crediting a diagnosis that never happened
The hung-build path escalates without calling a model, but the issue footer
said unconditionally that the finding came "from a Hermes Agent diagnosis
(run `unknown`)". Live example: lesavka issue #3, filed for a build that
overran, attributed a direct observation to a model conclusion that was never
requested. That misrepresents where the finding came from, which matters most
in exactly the artifact a human reads first.

Attribute to Ariadne when there was no run, and carry the recorded console
text into the body so the issue says what was actually observed instead of
leaving "Why a human is needed: hung_build" as the whole explanation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 06:01:27 -03:00
codex
950d014707 feat(hermes-triage): real services get both an issue and a patch proposal
The code path was gated on a single job id, so homegrown services could
never produce a pull request. Escalated incidents on mapped repositories
now additionally attempt a bounded patch proposal, and the filed issue
links it.

- hermes_code_flow.propose_for_incident: additive entry point invoked only
  from the escalation branch, so an auto-remediated failure never also
  gets a patch and a failed Hermes run never spends tokens on one
- three gates before any HTTP call: code enabled, not the legacy demo job,
  and the job has a repo mapping; unmapped jobs make zero calls
- never raises: a failed proposal cannot change the incident outcome or
  break the tick
- issue body links the proposal when a pull request was opened
- propose_code_fix added to the bounded action-label set

The legacy demo-job short-circuit is untouched and all of its tests pass
unchanged.

8 new tests; 480 pass in the hermes suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:29:54 -03:00
codex
6da560810f feat(hermes-triage): Gitea issues for escalations, patch proposals for real repos
Two capabilities that make triage useful outside the demo surface.

Issues: when triage concludes a human is needed, file an issue in the
failing service's own repository carrying classification, confidence, the
facts with their sources, the inferences and a Jenkins link, plus a footer
stating Hermes has no write access and nothing was changed. Opt-in per job
via a repo map, deduplicated by job+classification so a repeatedly failing
job yields one issue per kind of failure rather than one per build, and
capped per tick. Disabled by default.

Real-repo patches: candidate files are selected from the console failure
regions (Python, Rust and JS/TS reference patterns), filtered to each
repo's allowed prefixes and suffixes, ranked earliest-failure-first with
source preferred over test files, and fetched whole - never truncated,
because a patch anchor must match exactly. Per-job owner/repo/base-branch
resolution; the patch is validated against the file the model actually
chose, and an unlisted path is rejected.

Legacy single-repo demo behaviour is preserved unchanged.

131 new tests; 472 pass in the hermes suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:04:32 -03:00