Skip to content

fix: share transition history across a pull request's events, and release 0.0.5 - #52

Merged
maxigimenez merged 1 commit into
mainfrom
fix/pr-review-requested-transitions
Sep 7, 2026
Merged

maxigimenez merged 1 commit into
mainfrom
fix/pr-review-requested-transitions

Conversation

@maxigimenez

Copy link
Copy Markdown
Owner

A route on pr_review_requested matching reviewersAdded could never fire. Not intermittently — never, including the Reviewer agent template this ships in the route catalog.

Reported against this config, which is correct as written:

{
  "trigger": { "type": "pr_review_requested", "provider": "github", "projectId": "trackside" },
  "guard":   { "refire": "per-change", "markers": true },
  "match":   { "reviewersAdded": { "any": ["EomiAIBot"] } }
}

The bug

Observations were keyed <triggerType>:<ref>, so every trigger type kept its own transition baseline. But pr_review_requested is only emitted once a reviewer is outstanding — so that baseline came into existence at the exact moment of the transition it existed to detect:

Cycle Reviewers Event emitted? changes Route
1 — no — —
2 EomiAIBot added yes, first sight undefined → suppressed ✗
3 EomiAIBot still there yes reviewersAdded: [] ✗

First sight reporting no changes is the backlog guard and is correct; the mistake was giving this event type its own baseline at all.

Every unit test passed because they all hand changes to the rule engine directly. None ran collect → observe → match, which is the only place the two halves disagree.

The fix

  • What changed belongs to the item, not to the trigger type it raised. Observations are keyed on ref alone and the poll loop observes once per item per cycle, so both events a pull request raises read one history. pr_event is emitted for every open PR whether or not a review is outstanding, which is what makes its baseline the right one to share.
  • migrateObservationKeys promotes existing <type>:<ref> rows on boot, so an upgrade does not reseed a history the install already had and go quiet for a cycle. pr_event wins where both rows exist.
  • test/routing/trigger-pipeline.test.ts wires collection, observation and matching together as main() does, and covers the request, the still-outstanding request, the re-request after a review, and first sight of a PR whose reviewer predates it.

Debuggability

This is the reason the bug survived. The cycle summary's no-route is equally consistent with a typo'd login, a missing target, and a transition that was never recorded.

  • explainRule names the one clause that rejected an event, in the operator's vocabulary. matchesRule is now defined as explainRule(...) === undefined rather than written beside it, so the explanation cannot drift from the decision.
  • sentinel0 explain [--project <id>] [--ref <owner/repo#n>], over a new GET /routes/explain, re-collects live triggers and reports every route's verdict per item:
  - acme/www#42 pr_review_requested  Fix the export
      changed  nothing changed since the last poll
      skips    Reviewer agent: match.reviewersAdded wants any of [EomiAIBot]; nothing
               changed there this cycle (nothing)
  • It reads history through the new changesSince and never observe: a diagnostic that advanced the baseline would consume the transition being asked about and answer differently the second time it was put.
  • The output says plainly that a matching route may still not run — the dedupe ledger, one-run-per-agent and agent resolution all come after matching, and the cycle summary reports those as duplicate, agent-busy and unknown-agent.

Release

0.0.5 across the workspace via pnpm version:set, including the internal @sentinel0/* pins in the published cli package.

Verification

  • 550 tests pass across all five packages; lint and build clean on Node 23.
  • The pr_review_requested failure was reproduced through the real pipeline before the fix (the new pipeline test fails against the old keying) and passes after.
  • The explain endpoint is tested against a live Fastify instance, including that it answers identically when asked twice.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QNEqbFkMFoPiiVnJGxpjj2

…ease 0.0.5

A route on `pr_review_requested` matching `reviewersAdded` could never fire.
Not intermittently — never, including the "Reviewer agent" template this ships
in the route catalog.

Observations were keyed `<triggerType>:<ref>`, so every trigger type kept its
own transition baseline. But `pr_review_requested` is only emitted once a
reviewer is outstanding, so that baseline came into existence at the exact
moment of the transition it existed to detect: the cycle a reviewer is added is
the row's first sight, which deliberately reports no changes, and by the next
cycle the reviewer is no longer new.

Every unit test passed because they all hand `changes` to the rule engine
directly. None ran collect -> observe -> match, which is the only place the
two halves disagree.

- What changed belongs to the *item*, not to the trigger type it raised.
  Observations are keyed on `ref` alone and the poll loop observes once per
  item per cycle, so both events a pull request raises read one history.
  `pr_event` is emitted for every open PR whether or not a review is
  outstanding, which is what makes its baseline the right one to share.
- `migrateObservationKeys` promotes existing `<type>:<ref>` rows on boot, so an
  upgrade does not reseed a history the install already had and go quiet for a
  cycle. `pr_event` wins where both rows exist.
- `test/routing/trigger-pipeline.test.ts` wires collection, observation and
  matching together as `main()` does, and covers the request, the outstanding
  request, the re-request, and first sight of a PR whose reviewer predates it.

Debuggability, which is the reason this survived: the cycle summary's
`no-route` is equally consistent with a typo'd login, a missing target, and a
transition that was never recorded.

- `explainRule` names the one clause that rejected an event, in the operator's
  vocabulary. `matchesRule` is now *defined as* `explainRule(...) === undefined`
  rather than written beside it, so the explanation cannot drift from the
  decision.
- `sentinel0 explain [--project <id>] [--ref <owner/repo#n>]`, over a new
  `GET /routes/explain`, re-collects live triggers and reports every route's
  verdict per item. It reads history through the new `changesSince` and never
  `observe`: a diagnostic that advanced the baseline would consume the
  transition being asked about and answer differently the second time.
- The output says plainly that a matching route may still not run — the dedupe
  ledger, one-run-per-agent and agent resolution all come after matching.

CLAUDE.md and docs/routes.md carry both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNEqbFkMFoPiiVnJGxpjj2
@maxigimenez
maxigimenez merged commit e91614a into main Sep 7, 2026
2 checks passed
@maxigimenez
maxigimenez deleted the fix/pr-review-requested-transitions branch September 7, 2026 16:22
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.

1 participant