Repository navigation
fix: make a waiting review reachable, and stop explain hiding broken targets - #54
Merged
Merged
Conversation
…h did
Matching and targeting fail independently, and `explain` only ever reported the
first failing *match* clause. On a live runner that hid a second, unrelated
breakage behind the first:
skips Reviewer agent: match.reviewersAdded wants any of [EomiAIBot]; ...
The route was also addressing a GitHub identity that no Hermes profile claimed.
`hermes.profiles[].githubLogin` is the operator's declaration and nothing
verifies it; all four profiles on that machine had none, so `resolveAgent`
could never find one and the route would have died as `unknown-agent` on the
day it finally matched. Until then it read as an ordinary "did not match", and
two rounds of debugging went into the clause that was merely first.
- `resolveAgent` and `explainTarget` move into the rule engine and are shared
with the dispatcher rather than duplicated, so a diagnostic asking "could
this ever run" gets the answer the dispatcher would give.
- `explainTarget` separates "no agent has that identity" from "the agent is
disabled" -- identical symptom, different fix -- and, for the case that
actually occurred, names the profiles carrying no `githubLogin` and the file
to set it in. Preflight has warned about them all along, which is exactly how
easy that warning is to miss.
- `GET /routes/explain` evaluates the target for every route on every item
regardless of the match, and the CLI prints it in red as `broken` with a
pointer to `sentinel0 agents`.
Covered end to end at the API, in the shape that occurred: the match fails on
`reviewersAdded`, and the unresolvable target is reported alongside it rather
than instead of it.
Releases 0.0.7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNEqbFkMFoPiiVnJGxpjj2
A review request that is already outstanding could never be acted on. Not by waiting, and not by any operator action: - `reviewersAdded` fires only on the cycle that observes the request appear, so a review asked for before the route existed, or while the runner was down, is lost permanently. - Re-requesting does not recover it. A reviewer who has not yet reviewed never leaves `requested_reviewers`, so removing and re-adding them inside one ~25s poll window is invisible to the runner. Winning that race is not a thing to ask of an operator. The catalog already recorded this as an accepted limitation, and it should not have been one. Matching on the *state* -- "a review is outstanding" -- is always true while it is true, which is exactly what is needed. What made state clauses unusable was the dedupe key: a pull request's revision moves on every push, so under `per-change` each commit started another review round. `while-matched` separates "still waiting" from "asked again": claim the item on the first cycle it matches, hold the claim while it keeps matching, release it when it stops. - `dedupeKey` drops the revision, as `once` does, so pushes and replies cannot re-fire the route. - `releaseUnmatchedClaims` re-arms it once the condition lapses -- which for a review is the agent answering it, since GitHub clears the request and the trigger stops being raised at all. - A claim whose run has not settled is never released. Mid-run the item carries `sentinel0:in-progress` and so stops matching by construction, and releasing on that basis would re-arm the route against work it is still doing. - `collectEvents` now returns `undefined` on failure rather than `[]`. A tracker outage read as "nothing matches any more" would release every claim and replay the lot next cycle; the project is skipped instead, and the cycle summary says `unavailable`. - The shipped reviewer template moves to `while-matched` + `match.reviewers`. Enabling such a route acts on everything already matching. That is the point -- it is how a waiting pull request finally gets picked up -- and it is documented rather than hidden. Covered: fires with no transition to observe, holds one claim across pushes, re-arms after the request is answered and fires again when asked again, keeps the claim while the item still matches, never releases a running or approval-blocked run, re-arms after a failure so a retry is possible, and touches no other route's claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QNEqbFkMFoPiiVnJGxpjj2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent reasons the reviewer route could not run, both found on the live runner. Either alone is enough to keep it silent forever.
1. A review that is already outstanding is unreachable
This is the one that matters, and it is not a configuration mistake.
reviewersAddedfires only on the single cycle that observes the request appear. A review asked for before the route existed, or while the runner was down, is lost permanently.requested_reviewers, so removing and re-adding them inside one ~25s poll window is invisible. Winning that race is not a reasonable thing to ask of an operator.The catalog recorded this as an accepted limitation. It shouldn't have been one.
Matching on the state — "a review is outstanding" — is true for as long as it is true, which is exactly what's needed. What made state clauses unusable was the dedupe key: a PR's revision moves on every push, so under
per-changeeach commit started another review round.while-matchedseparates still waiting from asked again: claim the item on the first cycle it matches, hold the claim while it keeps matching, release it when it stops.dedupeKeydrops the revision, asoncedoes — pushes and replies cannot re-fire it.releaseUnmatchedClaimsre-arms the route when the condition lapses. For a review that's the agent answering it: GitHub clears the request and the trigger stops being raised at all.sentinel0:in-progressand so stops matching by construction; releasing on that basis would re-arm the route against work it is still doing.collectEventsreturnsundefinedon failure, not[]. A tracker outage read as "nothing matches any more" would release every claim and replay the lot. The project is skipped instead and the cycle summary saysunavailable.while-matched+match.reviewers.Enabling such a route acts on everything already matching. That is the point — it is how a waiting PR finally gets picked up — and it is documented rather than hidden.
2.
explainhid a broken target behind an unrelated clauseMatching and targeting fail independently, and
explainreported only the first failing match clause:True, but that route also addressed a GitHub identity no Hermes profile claims — all four profiles on the runner had no
githubLogin, soresolveAgentcould never find one and the route would have died asunknown-agentthe day it matched. Until then it read as an ordinary "did not match", and two rounds of debugging went into the clause that merely came first.resolveAgentandexplainTargetmove into the rule engine and are shared with the dispatcher, so the diagnostic gives the answer dispatch would give.explainTargetseparates no agent has that identity from the agent is disabled, and names the profiles carrying nogithubLoginplus the file to set it in.broken.Covered
while-matched: fires with no transition to observe · holds one claim across pushes · re-arms after the request is answered and fires again when asked again · keeps the claim while the item still matches · never releases a running or approval-blocked run · re-arms after a failure so a retry is possible · touches no other route's claims.Targeting: resolution by profile and by login, disabled vs missing, a target naming neither, and the exact production shape — match fails on
reviewersAddedand the unresolvable target is reported alongside it.593 tests pass across all five packages; lint and build clean.
Upgrading
Both fixes are needed, and so is one piece of configuration this cannot do for you: set
hermes.profiles[].githubLoginin~/.sentinel0/config.jsonso a profile declares which GitHub account it acts as. Nothing verifies it, preflight only warns, and without it the route resolves no agent.Then switch the existing route to
while-matched+match.reviewersand every PR already waiting on the agent is picked up on the next cycle — including ones that were previously unreachable.Releases
0.0.7.🤖 Generated with Claude Code
https://claude.ai/code/session_01QNEqbFkMFoPiiVnJGxpjj2