@effect-agent/pr-review 0.1.0-beta.24 → 0.1.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @effect-agent/pr-review
2
2
 
3
- A bounded, fail-closed GitHub pull-request reviewer built on the effect-agent
4
- public surface. Read-only Agents review host-partitioned pull-request evidence
3
+ A bounded, fail-closed GitHub pull-request reviewer built on effect-agent's public APIs. Read-only
4
+ Agents review host-partitioned pull-request evidence
5
5
  through typed ports; the host validates every finding anchor against the real
6
6
  diff and posts one review after the run settles.
7
7
 
@@ -15,6 +15,7 @@ truncated run posts nothing.
15
15
  import { Effect, Layer } from "effect";
16
16
  import {
17
17
  PrReview,
18
+ fullReviewExecutionContextLayer,
18
19
  gitHubReviewLayers,
19
20
  resolveReviewTarget,
20
21
  makeOpenAiReviewModel,
@@ -27,13 +28,22 @@ const program = Effect.gen(function* () {
27
28
  const target = yield* resolveReviewTarget({ repository: "acme/api", number: 123 });
28
29
  return yield* reviewer
29
30
  .run({ post: true })
30
- .pipe(Effect.provide(Layer.merge(gitHubReviewLayers(target), openAiClientLayer)));
31
+ .pipe(
32
+ Effect.provide(
33
+ fullReviewExecutionContextLayer("explicit direct full review").pipe(
34
+ Layer.provideMerge(Layer.merge(gitHubReviewLayers(target), openAiClientLayer)),
35
+ ),
36
+ ),
37
+ );
31
38
  });
32
39
  ```
33
40
 
34
41
  The run's requirement channel keeps every real dependency visible: the
35
- `PullRequestSource` and `ReviewPublisher` ports, the provider client, and the
36
- handler Layer of any extra tool you add. Anthropic is equally supported
42
+ `PullRequestSource`, `ReviewPublisher`, `ReviewAdjudicationHost`, and
43
+ `ReviewExecutionContext` ports, the provider client, and the handler Layer of
44
+ any extra tool you add. Direct callers provide an explicit full-review context
45
+ as above; the packaged action supplies its selected incremental or full range.
46
+ Anthropic is equally supported
37
47
  (`makeAnthropicReviewModel`, `anthropicClientLayer`), and the factory accepts
38
48
  any Effect AI Model.
39
49
 
@@ -53,44 +63,20 @@ const reviewer = PrReview.make({
53
63
  });
54
64
  ```
55
65
 
56
- Every knob widens what goes INTO the review. What leaves it is not
57
- configurable: model output is untrusted input, so finding anchors are
58
- re-validated against the parsed unified diff (invalid ones are demoted into
59
- the review body with the reason named, never trusted), the findings bound is
60
- enforced host-side, and publication happens only through the
61
- `ReviewPublisher` port after the run settles. `PrReview.makeFanOut` builds the
62
- assured host-scheduled variant. The host deterministically partitions bounded
63
- evidence, classifies high-risk units, requires a general discovery pass for
64
- every unit, adds a fresh specialist discovery pass for every unit, and uses
65
- deterministic host-classified risk categories as explicit focus labels. A
66
- fresh verifier must confirm or reject every discovered candidate. Every pass
67
- runs as a bounded evidence-only child dispatched directly by host code with
68
- Effect structured concurrency there is no coordinator model and no
69
- delegation tool, so nothing depends on a model copying the plan correctly. A
70
- pass that fails (child fault, malformed or misdirected output) is retried
71
- once; a pass that stays failed is reported and its unit's paths carry forward
72
- as retryable scope. Verifiers receive the exact candidate claims and the
73
- complete bounded unit, including neighboring evidence that can falsify a
74
- locally plausible claim; they do not receive discovery reasoning.
75
- The host builds publishable findings and concerns from exact confirmed
76
- candidate IDs and deduplicates byte-identical cross-pass claims in
77
- deterministic plan order; a discovery claim anchored outside its assigned
78
- evidence is discarded and counted, never published and never pass-fatal. The
79
- summary and verdict are composed deterministically from the confirmed
80
- severities. Shared `guidance` reaches both discovery and verification;
81
- `maxFindings` remains a host-enforced publication cap.
82
-
83
- GitHub may omit the `patch` field for large textual files as well as binary
84
- files. The GitHub source recovers a missing patch by reading bounded, strict
85
- UTF-8 base/head content: additions require the head, deletions require the
86
- base, and other changes require both. Reviewers receive that content through
87
- the ordinary diff-read tool with non-anchorable `B`/`H` line labels and must
88
- report defects as review-body concerns. Invalid UTF-8, binary NUL content,
89
- missing sides, and files beyond the per-side read bound leave a path with no
90
- reviewable textual evidence. Such paths keep input coverage incomplete and are
91
- carried for as long as they are part of the pull request — an unreviewable
92
- change must never authorize a green check; exclude them deliberately with
93
- ignore globs when that is intended.
66
+ Model output is untrusted. The host revalidates anchors, enforces `maxFindings`, and publishes only
67
+ after the run settles. `PrReview.makeFanOut` partitions bounded evidence and schedules general and
68
+ specialist discovery for every unit, followed by independent candidate verification. Verifiers
69
+ receive the exact claims and enough neighboring evidence to reject a plausible mistake, but no
70
+ discovery reasoning.
71
+
72
+ Each failed pass retries once, then carries its unit forward. The host accepts only confirmed
73
+ candidate IDs, discards anchors outside assigned evidence, deduplicates claims in plan order, and
74
+ derives the verdict from validated severities. Shared `guidance` reaches discovery and verification.
75
+
76
+ When GitHub omits a textual patch, the source reads bounded, strict UTF-8 base/head content and
77
+ labels it with non-anchorable `B`/`H` lines. Invalid UTF-8, binary NUL content, missing sides, and
78
+ oversized files remain unreviewable and keep input coverage incomplete. Exclude them with ignore
79
+ globs only when that is intentional.
94
80
 
95
81
  ## Assurance model
96
82
 
@@ -117,51 +103,52 @@ favors redundant work, but it cannot recognize every semantically risky
117
103
  change. The specialist pass is context-independent redundancy, not a claim of
118
104
  provider or model diversity. Running it for every unit prevents classifier
119
105
  silence from suppressing scrutiny; the category labels still cannot prove
120
- that every semantic risk was recognized. The legacy aggregate `coverage` field remains
121
- for compatibility; new hosts and UI use `inputCoverage` and `assurance`.
106
+ that every semantic risk was recognized. Hosts and UI use `inputCoverage` and `assurance`.
122
107
  The flat reviewer has path-input accounting but no independent verifier, so
123
108
  its assurance is `unverified` and the Action check cannot report success from
124
109
  that shape.
125
110
 
111
+ ## Maintainer adjudication
112
+
113
+ A maintainer can settle a finding without changing code, from the pull request itself:
114
+
115
+ - On a finding's inline thread, reply `/adjudicate accepted-risk|refuted|obsolete[: reason]`.
116
+ The thread names the target, so the verb alone suffices.
117
+ - For an unanchored concern, comment
118
+ `/adjudicate <disposition> "<exact concern title>"[: reason]` in the PR conversation. The quoted
119
+ title is required and must match exactly.
120
+
121
+ An adjudicated identity leaves active findings, verdict counts, and the check conclusion; it
122
+ renders in a collapsed "Adjudicated" section instead, and the final audit distinguishes fixed,
123
+ adjudicated, and still-open items. The reviewer prompt names each adjudication so the model does
124
+ not re-raise it without materially new evidence. Identity is exact — path, line range, and title
125
+ for findings, title alone for concerns — so a materially different finding at the same location is
126
+ untouched. Adjudications persist in the signed review state, and the skip-unchanged path re-reads
127
+ them, so an adjudication lifts a blocking check without a new commit.
128
+
129
+ **Authorization is fail-closed**: only comments whose `author_association` is OWNER, MEMBER, or
130
+ COLLABORATOR adjudicate. Anything else — third parties, bots, malformed commands — is ignored
131
+ (logged at debug). Later adjudications of the same identity win by comment creation order, bounded
132
+ at 20 stored entries (oldest dropped with a logged notice). If either GitHub listing surface fails,
133
+ or one thread exceeds the bounded authorized-command history, fresh collection is discarded and
134
+ the stored set stands unchanged.
135
+
136
+ **Rejected alternative — parsing free-text rebuttals** ("this is fine because …" replies): only an
137
+ explicit, authorized verb is auditable and fail-closed. Inferring intent from prose would let model
138
+ output or third-party comments silently suppress findings, and nobody could later say which comment
139
+ dismissed what.
140
+
126
141
  ## What a posted review looks like
127
142
 
128
- The body opens with a host-derived callout tier `[!CAUTION]` when any
129
- finding is blocking, `[!IMPORTANT]` for important findings, an ℹ️ blockquote
130
- for nits, `✅` for a clean approval computed from the validated severities,
131
- never from model prose. Directly under it, one host-derived stats line names
132
- the changeset size (`N files (+adds / −dels)`), the severity tally, and a
133
- deterministic 1–5 review-effort estimate computed from the changeset shape
134
- alone. Fan-out reviews add a second host-derived line naming input assignment,
135
- discovery/specialist/verification settlement, and candidate dispositions.
136
- Below the summary, a collapsed **📝 Walkthrough** table carries the
137
- model's one-sentence per-file change summaries — walkthrough paths are
138
- validated like finding anchors, so entries naming files outside the changeset
139
- are dropped. Under fan-out only summaries a
140
- successfully settled general pass actually reported for its own unit's paths
141
- survive, so a child cannot smuggle or invent entries. Non-anchored `concerns`
142
- (deletion plans, rollout sequencing,
143
- coverage gaps, scope questions — things with no diff line to point at) render
144
- as severity-tagged sections; demoted findings and findings carried from
145
- unchanged scope collapse into counted `<details>` sections.
146
-
147
- Each inline comment is headed by its severity plus an optional model-claimed
148
- category chip (`**[⚠️ important · security] …**`), carries a committable
149
- GitHub `suggestion` fence when independent verification settled the model's
150
- suggestion as committable replacement source, and ends with a
151
- collapsed **🤖 Prompt for AI agents** — a copy-paste instruction derived
152
- host-side from the validated finding, opening with a fixed preamble telling
153
- the receiving agent to treat the finding content as untrusted review data.
154
- The review body repeats every finding — anchored, demoted, and carried — in
155
- one consolidated prompt block so nothing needs to be collected by hand. The
156
- footer names the model binding, the observed token usage, and links to the
157
- workflow run; an invisible metadata comment pins the reviewed head commit so
158
- later readers know when line callouts have gone stale.
143
+ The host derives callouts, statistics, walkthroughs, inline comments, and the final prompt from
144
+ validated findings. Model prose cannot choose the verdict or smuggle paths outside the changeset.
145
+ The [Action guide](../../action/README.md) documents the rendered review and workflow outputs.
159
146
 
160
147
  ## Swap a port
161
148
 
162
149
  Tools observe the pull request only through `PullRequestSource`; publication
163
150
  happens only through `ReviewPublisher`. Provide your own Layers to review
164
- anything diff-shaped or publish anywhere else — the GitHub REST adapters are
151
+ any diff-shaped input or publish elsewhere. The GitHub REST adapters are
165
152
  one implementation, not the contract.
166
153
 
167
154
  ## Test what you adapted
@@ -176,131 +163,25 @@ import {
176
163
  ```
177
164
 
178
165
  Deterministic in-memory adapters for both ports plus prompt-keyed scripted
179
- models that walk the real tool surface no network, no credentials, every
180
- ordinary gate.
181
-
182
- ## Incremental Action reviews
183
-
184
- Every completed Action review that can be signed carries bounded, versioned,
185
- HMAC-authenticated continuity state: the exact PR/base/head lineage, profile
186
- and settled-scope fingerprints, the still-unresolved findings and concerns,
187
- any retryable unreviewed paths, and whether the run fully settled. The
188
- baseline is monotone by design — a flaky or failed pass carries exactly its
189
- own scope forward instead of freezing the baseline and reopening everything
190
- reviewed since (the non-converging loop of issue #131). A later Action run
191
- validates the state and reviews the GitHub comparison from that reviewed head
192
- to the current head plus the carried unreviewed paths, not the complete
193
- base...HEAD diff. When that head is not a git ancestor — a rebase, amend, or
194
- force-push — the host falls back to a two-dot tree comparison and reviews only
195
- the current PR paths whose contents actually changed, plus leftovers. Unchanged
196
- settled scope is not sent back to the model; unchanged unresolved findings
197
- remain active; changed or reverted paths invalidate their prior findings and
198
- receive fresh discovery and verification. An unchanged leftover from a failed
199
- pass retries only that failed stage and keeps its stored findings — it does
200
- not run a second general discovery.
201
- Whole overflow files are reviewed in bounded installments across pushes
202
- through the same carry; a single file beyond total plan capacity and
203
- undiffable paths stay explicitly incomplete instead — an unreviewable tail
204
- never moves behind a green check. Non-anchored concerns are carried conservatively
205
- until a full audit because they cannot be mapped safely to one path.
206
-
207
- The state marker must be terminal, signed with the configured stable
208
- `PR_REVIEW_STATE_SECRET`, authored by the configured review-posting bot, and
209
- pinned to the reviewed commit. The expected author defaults to
210
- `github-actions[bot]`; set `PR_REVIEW_AUTHOR_LOGIN` (or the Action's
211
- `review-author` input) to `<app-slug>[bot]` when posting with a custom GitHub
212
- App token. State lookup, authentication, schema, identity, ancestry, profile,
213
- and comparison checks are fail-closed for scope selection: missing, stale,
214
- incompatible, or truncated state/comparisons produce a visible full-diff
215
- fallback unless a two-dot content comparison can still name the changed PR
216
- paths. An ancestor base advance remains incremental and adds overlapping
217
- PR paths as affected context; a materially changed base lineage falls back to
218
- full unless the authenticated settled-scope fingerprint still matches or the
219
- content comparison intersects the current PR path set. The
220
- schema field retains the compatibility name `acceptedScopeFingerprint`. That
221
- fingerprint hashes the ignore-filtered effective diff, patchless base/head
222
- evidence, PR framing, and review-shaping profile while excluding commit IDs,
223
- base ancestry, and unified-diff hunk coordinates. Re-running the same head or a
224
- patch-equivalent rebase skips model execution by default ONLY when the stored
225
- state fully settled — a state carrying unreviewed scope is always retried.
226
- The preserved skip keeps its stored blocking/success conclusion and posts no
227
- duplicate review comments. Missing/corrupt state and any fingerprint or
228
- profile mismatch review conservatively.
229
-
230
- After a new state-bearing Action review posts, prior marker-bearing bot reviews
231
- are retired by default: their bodies become collapsed, superseded history,
232
- resolved findings are struck through, and matching inline comments are
233
- minimized as outdated. Only strictly older reviews from the same GitHub actor
234
- are eligible, so copied markers and newer concurrent reviews are untouched.
235
- The machine-state comments remain byte-identical and terminal, so an edited
236
- body still participates in incremental state recovery.
237
- This cosmetic pass is fail-open and can be disabled with the Action input
238
- `retire-stale-reviews: "false"`.
239
-
240
- Authentication is an explicit Effect service supplied by the Action host;
241
- WebCrypto import/sign/verify failures stay typed. The terminal marker is
242
- schema-branded and capped at 24,000 characters. If signing fails or state
243
- exceeds that bound, the completed review is posted without continuity state
244
- and with a bounded warning, so the next run safely performs a full review.
245
-
246
- `review-mode: final` is the explicit bounded merge-readiness audit. It ignores
247
- prior assurance state, plans fresh discovery over the full current PR
248
- diff, verifies new candidates, and resets the incremental baseline; normal
249
- `synchronize` events use `incremental` and do not perform this audit.
250
- Because equivalence is based on the textual review surface, a base change that
251
- alters runtime meaning without changing the diff/context is not detectable;
252
- request `final` mode (or disable unchanged skipping) for that case.
253
-
254
- ## Hosts
255
-
256
- - **GitHub Actions**: the repository ships a prebuilt node-runtime action
257
- supporting a committed review-profile document via `guidance-file` (this
258
- repository's own profile lives at `.github/review-guidance.md`)
259
- (`action/` at the repo root) — `uses` it with an API-key secret and nothing
260
- else. Fan-out is the Action default because the flat compatibility shape
261
- has no independent verifier and cannot settle review assurance. While a run executes it maintains one sticky, fail-open "review in
262
- progress" comment updated in place with the settled outcome
263
- (`progress-comment` input, default on; at-least-once with generation-fenced
264
- writes and best-effort duplicate cleanup — strict single-comment behavior
265
- comes from a per-PR workflow concurrency group), and its logs render one
266
- compact line per event (`log-level` input, default `Info`). For custom reviewers in
267
- CI, `@effect-agent/pr-review/action` exports `runReviewAction` (event
268
- resolution, typed draft/non-PR skips, bounded range selection, step
269
- outputs, and conservative check gate) to harness your own `reviewer.run`;
270
- pass `progressComment: true` to opt a custom harness into the sticky
271
- progress comment. A custom fingerprint-only harness remains source-compatible
272
- but cannot skip model work: unchanged skipping requires the profile fingerprint
273
- and authenticated state that prove the prior configured work settled.
274
- - **CLI**: `bun src/cli.ts --repo owner/name --pr 123 [--post] [--provider anthropic] [--fan-out]`
275
- (also exported as the `./cli` entry).
276
-
277
- Environment: `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` for the model,
278
- `PR_REVIEW_STATE_SECRET` to authenticate incremental state,
279
- `PR_REVIEW_AUTHOR_LOGIN` to match a custom review-posting bot (defaults to
280
- `github-actions[bot]`),
281
- `GITHUB_TOKEN` to post (optional for public-repository reads), and the
282
- standard `GITHUB_REPOSITORY` / `GITHUB_EVENT_PATH` / `GITHUB_API_URL`
283
- variables inside Actions.
166
+ models that call the real Tools. Tests need no network or credentials and exercise every ordinary
167
+ gate.
168
+
169
+ ## Action and CLI
170
+
171
+ The prebuilt GitHub Action handles signed incremental state, fail-closed scope selection, sticky
172
+ progress, stale-review retirement, and final audits. See its [setup and behavior guide](../../action/README.md)
173
+ and [input reference](../../action/action.yml). Custom hosts can import `runReviewAction` from
174
+ `@effect-agent/pr-review/action`.
175
+
176
+ Run the CLI with
177
+ `bun src/cli.ts --repo owner/name --pr 123 [--post] [--provider anthropic] [--fan-out]`.
284
178
 
285
179
  ## Bounds, spelled out
286
180
 
287
- Finite `AgentPolicy` on every definition plus run-level `UsageBudgetLimits`
288
- (tokens, tool calls, cost, duration). Reading either file version beyond 200k
289
- bytes or characters is refused typed; patchless content is reviewable only
290
- when its complete B/H rendering fits 220k characters. The changeset surface is
291
- bounded at 300 files:
292
- files beyond the bound are not fetched, and the review body reports
293
- `Input exposed N of M changed files` instead of claiming completeness.
294
- Fan-out capacity overflow is reported, never dropped. Every fan-out unit has
295
- at most 12 files, 12 complete evidence shards, and 240,000 evidence characters;
296
- one path may span multiple shards or units when necessary. If the eight-unit
297
- plan capacity is exhausted, every affected path and the exact shard count are
298
- reported, with a deterministic identifier sample bounded to one plan's capacity;
299
- at most eight units produce at most 24 attached children (general and
300
- specialist discovery plus one verification batch per unit), with child
301
- concurrency capped at four. Any blocking active finding fails the Action
302
- check. Any input gap—including an unassigned evidence shard—failed or
303
- exhausted configured pass, mismatched candidate batch, or unsettled
304
- verification is non-success rather than green. A settled
305
- clean result is evidence that these bounded passes completed, not proof that
306
- no defect exists.
181
+ Every definition has a finite `AgentPolicy` and run-level `UsageBudgetLimits`. The reviewer rejects
182
+ file versions beyond 200k bytes or characters and patchless B/H renderings beyond 220k characters.
183
+ It reads at most 300 changed files. Each fan-out unit holds at most 12 files, 12 complete evidence
184
+ shards, and 240,000 evidence characters. At most eight units produce 24 attached children with
185
+ child concurrency capped at four. The result reports every capacity overflow. Any blocking
186
+ finding, input gap, failed pass, exhausted pass, mismatched candidate batch, or unsettled
187
+ verification prevents a green check.
package/dist/action.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { L as PriorReviews, P as GitHubApiFailure, X as ReviewRetirementHost, cr as PullRequestSource, fr as ChangedFile, gt as ReviewMode, lr as PullRequestSourceFailure, sr as PullRequestMetadata, xt as ReviewStateAuthenticator, z as ReviewPublisher } from "./fan-out-Bi1v0VaU.mjs";
1
+ import { Br as ChangedFile, Cn as ReviewStateAuthenticator, Fr as PullRequestMetadata, Ir as PullRequestSource, L as PriorReviews, Lr as PullRequestSourceFailure, P as GitHubApiFailure, Q as ReviewRetirementHost, Qt as ReviewAdjudicationHost, gn as ReviewExecutionContext, vn as ReviewMode, z as ReviewPublisher } from "./fan-out-CMEsbFLk.mjs";
2
2
  import { EffortPosition, InvalidEffortInput, ReviewProgressReporter, ReviewProvider, ReviewRunOutcome, ReviewTargetUnresolved, RunReviewOptions } from "./index.mjs";
3
3
  import { Config, Effect, FileSystem, Schema } from "effect";
4
4
  import { BudgetExceeded } from "effect-agent";
@@ -92,29 +92,38 @@ type ReviewActionResult = {
92
92
  * The reviewer surface the action harness drives. `PrReview.make` and
93
93
  * `PrReview.makeFanOut` provide the state-selection effects. A fingerprint is
94
94
  * skip authority only when a profile fingerprint and authenticated review
95
- * state bind it to settled assurance; a fingerprint-only custom harness
96
- * remains source-compatible but deliberately runs instead of claiming green
97
- * assurance from unauthenticated prior-review text.
95
+ * state bind it to settled assurance.
98
96
  */
99
- interface HarnessedReviewer<E, R, FingerprintE, FingerprintR> {
100
- readonly run: (runOptions?: RunReviewOptions) => Effect.Effect<ReviewRunOutcome, E, R>;
97
+ interface HarnessedReviewerBase<E, R> {
98
+ /** The action composition root always supplies the selected run context. */
99
+ readonly run: (runOptions?: RunReviewOptions) => Effect.Effect<ReviewRunOutcome, E, R | ReviewExecutionContext>;
100
+ }
101
+ type HarnessedReviewer<E, R, FingerprintE, FingerprintR> = HarnessedReviewerBase<E, R> & ({
102
+ readonly fingerprint?: undefined;
103
+ readonly profileFingerprint?: undefined;
104
+ readonly snapshot?: undefined;
105
+ readonly filterFiles?: undefined;
106
+ } | {
101
107
  /** Current effective changeset fingerprint; not standalone skip authority. */
102
- readonly fingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
103
- readonly profileFingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
104
- readonly snapshot?: Effect.Effect<{
108
+ readonly fingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
109
+ readonly profileFingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
110
+ readonly snapshot: Effect.Effect<{
105
111
  readonly metadata: PullRequestMetadata;
106
112
  readonly files: ReadonlyArray<ChangedFile>;
107
- }, FingerprintE, FingerprintR> | undefined;
113
+ }, FingerprintE, FingerprintR>;
108
114
  readonly filterFiles?: ((files: ReadonlyArray<ChangedFile>) => ReadonlyArray<ChangedFile>) | undefined;
109
- }
115
+ });
110
116
  /**
111
117
  * Host-derived check conclusion; model verdict prose cannot weaken it.
112
118
  *
113
119
  * Blocking code findings outrank machinery gaps — they are the actionable
114
- * signal. A machinery gap (incomplete input, unsettled passes) concludes
115
- * `incomplete` with reasons that explicitly say the failure is reviewer-side
116
- * uncertainty carried forward for retry, never an invitation to change code.
117
- * The flat reviewer's constant `unverified` assurance is not a gap.
120
+ * signal. A retryable machinery gap (failed passes, capacity overflow)
121
+ * concludes `incomplete` with reasons that explicitly say the failure is
122
+ * reviewer-side uncertainty carried forward for retry, never an invitation to
123
+ * change code. Undiffable files are the deliberate exception: no retry can
124
+ * settle them, so their reason instructs removal or ignore globs instead of
125
+ * promising an automatic retry. The flat reviewer's constant `unverified`
126
+ * assurance is not a gap.
118
127
  */
119
128
  declare const concludeReviewOutcome: (outcome: ReviewRunOutcome) => {
120
129
  readonly conclusion: ReviewCheckConclusion;
@@ -154,7 +163,7 @@ declare const runReviewAction: <E, R, FingerprintE = never, FingerprintR = never
154
163
  } | {
155
164
  _tag: "Completed";
156
165
  outcome: ReviewRunOutcome;
157
- }, E | FingerprintE | Config.ConfigError | import("effect/PlatformError").PlatformError | PullRequestSourceFailure | ReviewGateFailed | ReviewTargetUnresolved, FileSystem.FileSystem | import("effect/unstable/http/HttpClient").HttpClient | ReviewStateAuthenticator | Exclude<FingerprintR, PriorReviews | PullRequestSource | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost> | Exclude<R, PriorReviews | PullRequestSource | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost>>;
166
+ }, E | FingerprintE | Config.ConfigError | import("effect/PlatformError").PlatformError | PullRequestSourceFailure | ReviewGateFailed | ReviewTargetUnresolved, FileSystem.FileSystem | import("effect/unstable/http/HttpClient").HttpClient | ReviewStateAuthenticator | Exclude<FingerprintR, PriorReviews | PullRequestSource | ReviewAdjudicationHost | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost> | Exclude<Exclude<R, PullRequestSource | ReviewExecutionContext>, PriorReviews | PullRequestSource | ReviewAdjudicationHost | ReviewProgressReporter | ReviewPublisher | ReviewRetirementHost>>;
158
167
  /**
159
168
  * The packaged, environment-driven action program: inputs from PR_REVIEW_*,
160
169
  * reviewer built from the packaged factory, provider client from the
package/dist/action.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { D as ReviewHeadComparison, E as ReviewExecutionContext, H as isLineageAncestor, J as validateReviewState, M as ReviewStateAuthenticator, U as selectReviewRange, W as selectedPullRequestSourceLayer, Xn as normalizeRepoRelativePath, Y as webCryptoReviewStateAuthenticatorLayer, a as PriorReviews, q as unavailableReviewStateAuthenticatorLayer, qn as PullRequestSource, x as retireStaleReviews } from "./github-C6jrBLA2.mjs";
2
- import { A as reviewBudgetLimits, C as PrReview, H as InvalidEffortInput, W as parseEffortPosition, a as anthropicClientLayer, c as makeOpenAiReviewModel, f as gitHubReviewLayers, g as ReviewProgressReporter, k as fanOutReviewBudgetLimits, l as openAiClientLayer, m as resolveReviewTarget, n as DEFAULT_PROVIDER, o as describeReviewModel, p as readGitHubEvent, s as makeAnthropicReviewModel } from "./providers-2Jao2ZAX.mjs";
1
+ import { An as validateReviewState, Cn as fullReviewSelection, En as selectedPullRequestSourceLayer, St as splitCarriedScope, Tn as selectReviewRange, Yt as retireStaleReviews, a as PriorReviews, cn as ReviewStateAuthenticator, gn as adjudicationIdentity, jn as webCryptoReviewStateAuthenticatorLayer, jt as collectReviewAdjudications, kn as unavailableReviewStateAuthenticatorLayer, nn as ReviewExecutionContext, rn as ReviewHeadComparison, vn as concernIdentity, vr as PullRequestSource, wn as isLineageAncestor, xr as normalizeRepoRelativePath, yn as findingIdentity } from "./github-NjgxGqwM.mjs";
2
+ import { A as reviewBudgetLimits, C as PrReview, H as InvalidEffortInput, W as parseEffortPosition, a as anthropicClientLayer, c as makeOpenAiReviewModel, f as gitHubReviewLayers, g as ReviewProgressReporter, k as fanOutReviewBudgetLimits, l as openAiClientLayer, m as resolveReviewTarget, n as DEFAULT_PROVIDER, o as describeReviewModel, p as readGitHubEvent, s as makeAnthropicReviewModel } from "./providers-CODZQCmL.mjs";
3
3
  import { n as compactReviewLoggingLayer } from "./logging-Q4j0oub-.mjs";
4
4
  import { Config, Console, Effect, FileSystem, Layer, Option, Redacted, Schema } from "effect";
5
5
  import { BudgetExceeded, UsageBudgetLimits } from "effect-agent";
@@ -127,7 +127,6 @@ const outcomeOutputs = (outcome, conclusion) => [
127
127
  ["verdict", outcome.review.verdict],
128
128
  ["input-coverage", outcome.inputCoverage.status],
129
129
  ["review-assurance", outcome.assurance.status],
130
- ["coverage", outcome.coverage.status],
131
130
  ["review-mode", outcome.reviewMode ?? "full"],
132
131
  ["review-reason", outcome.reviewReason ?? "direct full review"],
133
132
  ["inline-comments", String(outcome.plan.comments.length)],
@@ -137,18 +136,22 @@ const outcomeOutputs = (outcome, conclusion) => [
137
136
  ...outcome.usage === void 0 ? [] : [["input-tokens", String(outcome.usage.inputTokens)], ["output-tokens", String(outcome.usage.outputTokens)]],
138
137
  ["review-url", outcome.published?.url ?? ""]
139
138
  ];
140
- const outcomeSummary = (outcome, modelLabel, conclusion) => [
141
- "### Pull-request review",
142
- `- Check conclusion: **${conclusion}**`,
143
- `- Verdict: **${outcome.review.verdict}**`,
144
- `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
145
- `- Review assurance: **${outcome.assurance.status}** · general discovery ${outcome.assurance.completedGeneralDiscoveryPasses}/${outcome.assurance.requiredGeneralDiscoveryPasses} · specialist ${outcome.assurance.completedSpecialistPasses}/${outcome.assurance.requiredSpecialistPasses} · verification ${outcome.assurance.completedVerificationPasses}/${outcome.assurance.requiredVerificationPasses}`,
146
- `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
147
- ...outcome.unreviewedPaths.length === 0 ? [] : [`- Carried forward: ${outcome.unreviewedPaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`],
148
- ...modelLabel === void 0 ? [] : [`- Model: \`${modelLabel}\``],
149
- ...outcome.usage === void 0 ? [] : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`],
150
- ...outcome.published === void 0 ? ["- Dry run: nothing posted"] : [`- Posted: ${outcome.published.url}`]
151
- ];
139
+ const outcomeSummary = (outcome, modelLabel, conclusion) => {
140
+ const scope = splitCarriedScope(outcome);
141
+ return [
142
+ "### Pull-request review",
143
+ `- Check conclusion: **${conclusion}**`,
144
+ `- Verdict: **${outcome.review.verdict}**`,
145
+ `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
146
+ `- Review assurance: **${outcome.assurance.status}** · general discovery ${outcome.assurance.completedGeneralDiscoveryPasses}/${outcome.assurance.requiredGeneralDiscoveryPasses} · specialist ${outcome.assurance.completedSpecialistPasses}/${outcome.assurance.requiredSpecialistPasses} · verification ${outcome.assurance.completedVerificationPasses}/${outcome.assurance.requiredVerificationPasses}`,
147
+ `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
148
+ ...scope.retryablePaths.length === 0 ? [] : [`- Carried forward: ${scope.retryablePaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`],
149
+ ...scope.undiffablePaths.length === 0 ? [] : [`- Unreviewable: ${scope.undiffablePaths.length} path(s) with no reviewable diff (binary or oversized) — remove them from the pull request or exclude them with ignore globs`],
150
+ ...modelLabel === void 0 ? [] : [`- Model: \`${modelLabel}\``],
151
+ ...outcome.usage === void 0 ? [] : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`],
152
+ ...outcome.published === void 0 ? ["- Dry run: nothing posted"] : [`- Posted: ${outcome.published.url}`]
153
+ ];
154
+ };
152
155
  const skip = (reason) => Effect.gen(function* () {
153
156
  yield* Console.log(`Skipping review: ${reason}`);
154
157
  yield* writeActionOutputs([["skipped", "true"], ["skip-reason", reason]]);
@@ -170,15 +173,20 @@ const blockingReasons = (input) => [...input.findings.filter((finding) => findin
170
173
  * Host-derived check conclusion; model verdict prose cannot weaken it.
171
174
  *
172
175
  * Blocking code findings outrank machinery gaps — they are the actionable
173
- * signal. A machinery gap (incomplete input, unsettled passes) concludes
174
- * `incomplete` with reasons that explicitly say the failure is reviewer-side
175
- * uncertainty carried forward for retry, never an invitation to change code.
176
- * The flat reviewer's constant `unverified` assurance is not a gap.
176
+ * signal. A retryable machinery gap (failed passes, capacity overflow)
177
+ * concludes `incomplete` with reasons that explicitly say the failure is
178
+ * reviewer-side uncertainty carried forward for retry, never an invitation to
179
+ * change code. Undiffable files are the deliberate exception: no retry can
180
+ * settle them, so their reason instructs removal or ignore globs instead of
181
+ * promising an automatic retry. The flat reviewer's constant `unverified`
182
+ * assurance is not a gap.
177
183
  */
178
184
  const concludeReviewOutcome = (outcome) => {
179
185
  const machinery = [];
180
186
  if (outcome.inputCoverage.status === "incomplete" || outcome.assurance.status === "incomplete") {
181
- machinery.push(outcome.unreviewedPaths.length > 0 ? `review infrastructure did not settle — a reviewer-side gap, not a code defect; ${outcome.unreviewedPaths.length} path(s) are carried forward and retried automatically on the next run` : "review infrastructure did not settle — a reviewer-side gap, not a code defect");
187
+ const scope = splitCarriedScope(outcome);
188
+ if (scope.retryableGap) machinery.push(scope.retryablePaths.length > 0 ? `review infrastructure did not settle — a reviewer-side gap, not a code defect; ${scope.retryablePaths.length} path(s) are carried forward and retried automatically on the next run` : "review infrastructure did not settle — a reviewer-side gap, not a code defect");
189
+ if (scope.undiffablePaths.length > 0) machinery.push(`${scope.undiffablePaths.length} path(s) have no reviewable diff (binary or oversized) and no retry can settle them — remove them from the pull request or exclude them with ignore globs`);
182
190
  if (outcome.inputCoverage.status === "incomplete") machinery.push(...outcome.inputCoverage.reasons);
183
191
  if (outcome.assurance.status === "incomplete") machinery.push(...outcome.assurance.reasons);
184
192
  }
@@ -199,10 +207,10 @@ const concludeReviewOutcome = (outcome) => {
199
207
  reasons: []
200
208
  };
201
209
  };
202
- const concludeReviewState = (state) => {
210
+ const concludeReviewState = (state, adjudicated) => {
203
211
  const reasons = blockingReasons({
204
- findings: state.unresolvedFindings,
205
- concerns: state.unresolvedConcerns
212
+ findings: state.unresolvedFindings.filter((finding) => !adjudicated.has(findingIdentity(finding))),
213
+ concerns: state.unresolvedConcerns.filter((concern) => !adjudicated.has(concernIdentity(concern)))
206
214
  });
207
215
  return reasons.length > 0 ? {
208
216
  conclusion: "blocking",
@@ -213,16 +221,15 @@ const concludeReviewState = (state) => {
213
221
  };
214
222
  };
215
223
  const skipCoveredReview = Effect.fn("skipCoveredReview")(function* (input) {
216
- const result = concludeReviewState(input.state);
224
+ const adjudications = yield* collectReviewAdjudications(input.state.adjudications ?? []);
225
+ const result = concludeReviewState(input.state, new Set(adjudications.map(adjudicationIdentity)));
217
226
  yield* Console.log(`Skipping review of ${input.repository}#${input.pullRequestNumber}: ${input.reason}.`);
218
227
  yield* writeActionOutputs([
219
228
  ["skipped", "true"],
220
229
  ["skip-reason", input.reason],
221
- ...input.fingerprint === void 0 ? [] : [["fingerprint", input.fingerprint]],
222
230
  ["conclusion", result.conclusion],
223
231
  ["input-coverage", "complete"],
224
232
  ["review-assurance", "settled"],
225
- ["coverage", "complete"],
226
233
  ["review-mode", "incremental"]
227
234
  ]);
228
235
  yield* writeStepSummary([
@@ -258,14 +265,10 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
258
265
  return yield* Effect.gen(function* () {
259
266
  let selection;
260
267
  if (reviewer.profileFingerprint !== void 0) {
261
- const source = yield* PullRequestSource;
262
268
  const [snapshot, profileFingerprint, currentFingerprint] = yield* Effect.all([
263
- reviewer.snapshot ?? Effect.all({
264
- metadata: source.metadata,
265
- files: source.anchorFiles
266
- }),
269
+ reviewer.snapshot,
267
270
  reviewer.profileFingerprint,
268
- reviewer.fingerprint === void 0 ? Effect.succeed(void 0) : reviewer.fingerprint.pipe(Effect.map((fingerprint) => fingerprint), Effect.orElseSucceed(() => void 0))
271
+ reviewer.fingerprint.pipe(Effect.map((fingerprint) => fingerprint), Effect.orElseSucceed(() => void 0))
269
272
  ]);
270
273
  const { metadata, files: fullFiles } = snapshot;
271
274
  const history = options.priorReviews ?? (yield* PriorReviews);
@@ -283,13 +286,12 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
283
286
  failure: void 0
284
287
  })
285
288
  }));
286
- const equivalentPatchState = (options.reviewMode ?? "incremental") === "incremental" && options.skipUnchanged !== false && recovered.state !== void 0 && recovered.state.settled && currentFingerprint !== void 0 && validateReviewState(recovered.state, metadata, profileFingerprint) === void 0 && recovered.state.acceptedScopeFingerprint === currentFingerprint ? recovered.state : void 0;
289
+ const equivalentPatchState = (options.reviewMode ?? "incremental") === "incremental" && options.skipUnchanged !== false && recovered.state !== void 0 && recovered.state.settled && currentFingerprint !== void 0 && validateReviewState(recovered.state, metadata, profileFingerprint) === void 0 && recovered.state.settledScopeFingerprint === currentFingerprint ? recovered.state : void 0;
287
290
  if (equivalentPatchState !== void 0) return yield* skipCoveredReview({
288
291
  repository: target.repository,
289
292
  pullRequestNumber: target.number,
290
293
  reason: equivalentPatchState.reviewedHeadSha === metadata.headSha ? "the current head already has settled stored review assurance" : "the effective pull-request patch is unchanged since the last settled review",
291
- state: equivalentPatchState,
292
- fingerprint: currentFingerprint
294
+ state: equivalentPatchState
293
295
  });
294
296
  let comparison;
295
297
  let baseComparison;
@@ -346,6 +348,15 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
346
348
  state: selection.priorState
347
349
  });
348
350
  }
351
+ const executionContext = selection ?? (yield* Effect.gen(function* () {
352
+ const source = yield* PullRequestSource;
353
+ const [metadata, files] = yield* Effect.all([source.metadata, source.changedFiles]);
354
+ return fullReviewSelection({
355
+ reason: "explicit custom-reviewer full review without continuity selection",
356
+ files,
357
+ totalFiles: metadata.totalChangedFiles
358
+ });
359
+ }));
349
360
  yield* Console.log(`Reviewing ${target.repository}#${target.number} (${options.post === false ? "dry run" : "posting"})...`);
350
361
  const runUrl = yield* resolveRunUrl();
351
362
  const progress = options.progressComment === true && options.post !== false ? Option.some(yield* ReviewProgressReporter) : Option.none();
@@ -353,9 +364,9 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
353
364
  const headMetadata = yield* (yield* PullRequestSource).metadata.pipe(Effect.orElseSucceed(() => void 0));
354
365
  yield* progress.value.begin({
355
366
  headSha: headMetadata?.headSha,
356
- reviewMode: selection?.mode,
357
- reviewReason: selection?.reason,
358
- filesInScope: selection?.files.length,
367
+ reviewMode: executionContext.mode,
368
+ reviewReason: executionContext.reason,
369
+ filesInScope: executionContext.files.length,
359
370
  modelLabel: options.modelLabel,
360
371
  runUrl
361
372
  });
@@ -369,7 +380,8 @@ const runReviewAction = (reviewer, options = {}) => Effect.gen(function* () {
369
380
  runUrl,
370
381
  modelLabel: options.modelLabel
371
382
  }))) : runReview;
372
- const outcome = yield* selection === void 0 ? reviewEffect : reviewEffect.pipe(Effect.provide(selectedPullRequestSourceLayer(selection)), Effect.provideService(ReviewExecutionContext, selection));
383
+ const executionLayer = Layer.merge(Layer.succeed(ReviewExecutionContext)(executionContext), selectedPullRequestSourceLayer(executionContext));
384
+ const outcome = yield* reviewEffect.pipe(Effect.provide(executionLayer));
373
385
  yield* Console.log(`Review finished in ${outcome.turns} turn(s): verdict ${outcome.review.verdict}, ${outcome.plan.comments.length} inline comment(s), ${outcome.plan.demoted.length} demoted finding(s).`);
374
386
  if (outcome.published !== void 0) {
375
387
  yield* Console.log(`Posted ${outcome.published.event} review: ${outcome.published.url}`);