feat: detect what is waiting on the operator #449

Merged
joshtronic merged 5 commits from feat/439-needsyou-detection into master 2026-07-28 22:51:13 +00:00
Collaborator

What this PR does

Detection half of #439. Delivery (email) is a separate change — the first attempt at this issue tried both at once and blocked at 792 lines against the 400 cap.

  • lib/needsyou.sh — the waiting-on-you set, its event semantics, and a human-readable description
  • a throttled scan in bin/tick.sh that builds the live set and logs what is NEW
  • bin/test-needsyou.sh — 46 checks

Review round 2 took the branch from ~380 to 458 net lines, past the 400 cap. The rework path does not run the scope gate, so this is a note rather than a block: the 73 added lines are the scan-blindness fix below, its regression tests, and the three defensiveness changes the review asked for. There is nothing in here I can cut without dropping coverage or the comments that explain the verdict table. Say the word and I will split the needsyou_pr_numbers fix into its own PR ahead of this one.

Event-driven, not periodic

A recurring digest that usually says "nothing needs you" trains the reader to stop opening it, and then it fails exactly when it matters. So only additions are announceable — removals are silent, and an unchanged set says nothing at all.

The dry-run found the scan blind

The first version of this description claimed a dry-run showed debtmom.com#1 reading as needing the human. The reviewer caught that this was run against an earlier ! automerge_will_take predicate and never re-run after the redesign. Re-running it against the shipped code found zero — and not because the fleet was quiet.

forgejo_list_open_bot_prs answers with a JSON array. The scan read that text line by line, so needsyou_pr_why was called with [, { and "number": 449, and never once with a PR number. The feature detected nothing at all, on every repo, and the only reason that read as plausible is that "zero things need you" is a legitimate answer.

needsyou_pr_numbers now pulls .number out the way do_automerge_tick does, and drops WIP: checkpoint PRs — that is Igor mid-task, not your turn, and such a PR can still carry the verdict it had from before it checkpointed.

Honest dry-run against the fixed scan, today: 2 open bot PRs fleet-wide, 0 needing you.

item state answer
debtmom.com#1 no shadow verdict recorded reviewer's turn — silent
igor#449 (this PR) REQUEST_CHANGES, round 2 of 3 Igor's turn — silent

Plus 2 open issues fleet-wide, neither carrying a Status/* label, so 0 parked on you. Zero items today, and every zero is checkable against reality.

The decision worth reviewing

Which PR states are your turn is a hand-enumerated verdict table, not a derived one. The first cut asked ! automerge_will_take, which is much broader than "you are the blocker": it is also true of a PR nobody has reviewed yet (the shadow reviewer's turn) and of one inside the rework loop (Igor's turn, for up to 3 rounds). Both flip in and out of the set on their own, so announcing them is exactly the noise this feature exists to avoid.

So needsyou_pr_why enumerates instead, mirroring do_review_tick's own routing — unreviewed goes to the reviewer, APPROVE to nobody unless the repo pins itself to a human, COMMENT to you, REQUEST_CHANGES to Igor for rounds 1–3 and then to you.

The tradeoff to scrutinise is drift. That table is a second reading of the merge policy, and if the policy moves the table has to move with it. It lives in one function, is unit-tested state by state, and carries a comment saying why — but nothing enforces the correspondence. If that is the wrong call, deriving it from automerge_will_take and then subtracting the reviewer's-turn and rework-loop cases is the alternative.

Scanned, not hooked. The cheaper design hooks where the loop already decides — review_request_human() and agent-block.sh — at zero API cost. I did not, because items must also leave the set when you deal with them. A hook can only ever add; without removal an item stays "already known" forever and a genuine recurrence is never announced again. A scan is idempotent, self-correcting, and picks up anything already queued before this ships.

Throttled to every 20th tick. It costs a PR-list and an issue-list call for every repo in the analysis set — 2N, exactly the fleet-sweep cost #441 was about — plus one agent.json fetch per repo that actually has an open bot PR. Gating the issue call the way the agent.json fetch is gated would not work: an issue can be parked on you in a repo with no bot PR at all.

needsyou_merge preserves each item's since across scans. Without that every scan resets the clock and nothing can ever be reported as having waited days, which is most of the value.

Test plan

  • bash bin/test-needsyou.sh — 46 checks: additions vs removals, an unchanged set announcing nothing, first-ever scan, since preserved across scans, an item that left and returned resetting its clock, wait-time formatting (m/h/d), which verdicts are whose turn, WIP: drafts dropped, and malformed input degrading to empty rather than crashing
  • make test and make lint pass; bash -n clean on tick.sh
  • Predicate dry-run against live fleet data, re-run against the shipped needsyou_pr_why — the table above
  • Glue exercised end-to-end: lifted needsyou_scan_set + needsyou_pass verbatim out of tick.sh, ran them against live fleet data with a throwaway state file. Five passes: live state announces nothing, an unchanged rescan is silent, forcing igor#449 to COMMENT announces it once, rescanning is silent again, withdrawing the verdict drops it from the set with no output. Manual — not in CI, see below.

Not in this PR

Delivery. The next change wires the additions into the existing ship-report mail path so the arrival of the mail is the signal.

Also not covered by CI: needsyou_scan_set and needsyou_pass themselves. Every unit test targets the pure helpers in lib/needsyou.sh; the state-file write, the mktemp/mv, and the API glue are verified only by the manual run above — and that glue is exactly where this round's bug lived. The delivery PR should move the scan into lib/needsyou.sh behind stubbable seams and cover it, the way test-automerge.sh stubs _fj.

Part of #439

## What this PR does Detection half of #439. Delivery (email) is a **separate change** — the first attempt at this issue tried both at once and blocked at **792 lines** against the 400 cap. - [x] `lib/needsyou.sh` — the waiting-on-you set, its event semantics, and a human-readable description - [x] a throttled scan in `bin/tick.sh` that builds the live set and logs what is NEW - [x] `bin/test-needsyou.sh` — 46 checks Review round 2 took the branch from ~380 to **458 net lines**, past the 400 cap. The rework path does not run the scope gate, so this is a note rather than a block: the 73 added lines are the scan-blindness fix below, its regression tests, and the three defensiveness changes the review asked for. There is nothing in here I can cut without dropping coverage or the comments that explain the verdict table. Say the word and I will split the `needsyou_pr_numbers` fix into its own PR ahead of this one. ## Event-driven, not periodic A recurring digest that usually says "nothing needs you" trains the reader to stop opening it, and then it fails exactly when it matters. So only **additions** are announceable — removals are silent, and an unchanged set says nothing at all. ## The dry-run found the scan blind The first version of this description claimed a dry-run showed `debtmom.com#1` reading as needing the human. The reviewer caught that this was run against an earlier `! automerge_will_take` predicate and never re-run after the redesign. Re-running it against the shipped code found zero — and not because the fleet was quiet. `forgejo_list_open_bot_prs` answers with a JSON **array**. The scan read that text line by line, so `needsyou_pr_why` was called with `[`, ` {` and `"number": 449,` and never once with a PR number. The feature detected nothing at all, on every repo, and the only reason that read as plausible is that "zero things need you" is a legitimate answer. `needsyou_pr_numbers` now pulls `.number` out the way `do_automerge_tick` does, and drops `WIP:` checkpoint PRs — that is Igor mid-task, not your turn, and such a PR can still carry the verdict it had from before it checkpointed. Honest dry-run against the fixed scan, today: **2 open bot PRs fleet-wide, 0 needing you.** | item | state | answer | |---|---|---| | `debtmom.com#1` | no shadow verdict recorded | reviewer's turn — silent | | `igor#449` (this PR) | `REQUEST_CHANGES`, round 2 of 3 | Igor's turn — silent | Plus 2 open issues fleet-wide, neither carrying a `Status/*` label, so 0 parked on you. Zero items today, and every zero is checkable against reality. ## The decision worth reviewing **Which PR states are your turn is a hand-enumerated verdict table, not a derived one.** The first cut asked `! automerge_will_take`, which is much broader than "you are the blocker": it is also true of a PR nobody has reviewed yet (the shadow reviewer's turn) and of one inside the rework loop (Igor's turn, for up to 3 rounds). Both flip in and out of the set on their own, so announcing them is exactly the noise this feature exists to avoid. So `needsyou_pr_why` enumerates instead, mirroring `do_review_tick`'s own routing — unreviewed goes to the reviewer, `APPROVE` to nobody unless the repo pins itself to a human, `COMMENT` to you, `REQUEST_CHANGES` to Igor for rounds 1–3 and then to you. **The tradeoff to scrutinise is drift.** That table is a second reading of the merge policy, and if the policy moves the table has to move with it. It lives in one function, is unit-tested state by state, and carries a comment saying why — but nothing enforces the correspondence. If that is the wrong call, deriving it from `automerge_will_take` and then subtracting the reviewer's-turn and rework-loop cases is the alternative. **Scanned, not hooked.** The cheaper design hooks where the loop already decides — `review_request_human()` and `agent-block.sh` — at zero API cost. I did not, because items must also **leave** the set when you deal with them. A hook can only ever add; without removal an item stays "already known" forever and a genuine recurrence is never announced again. A scan is idempotent, self-correcting, and picks up anything already queued before this ships. Throttled to every 20th tick. It costs a PR-list **and** an issue-list call for every repo in the analysis set — 2N, exactly the fleet-sweep cost #441 was about — plus one `agent.json` fetch per repo that actually has an open bot PR. Gating the issue call the way the `agent.json` fetch is gated would not work: an issue can be parked on you in a repo with no bot PR at all. `needsyou_merge` preserves each item's `since` across scans. Without that every scan resets the clock and nothing can ever be reported as having waited days, which is most of the value. ## Test plan - [x] `bash bin/test-needsyou.sh` — 46 checks: additions vs removals, an unchanged set announcing nothing, first-ever scan, `since` preserved across scans, an item that left and returned resetting its clock, wait-time formatting (m/h/d), which verdicts are whose turn, `WIP:` drafts dropped, and malformed input degrading to empty rather than crashing - [x] `make test` and `make lint` pass; `bash -n` clean on `tick.sh` - [x] **Predicate dry-run against live fleet data**, re-run against the shipped `needsyou_pr_why` — the table above - [x] **Glue exercised end-to-end**: lifted `needsyou_scan_set` + `needsyou_pass` verbatim out of `tick.sh`, ran them against live fleet data with a throwaway state file. Five passes: live state announces nothing, an unchanged rescan is silent, forcing `igor#449` to `COMMENT` announces it once, rescanning is silent again, withdrawing the verdict drops it from the set with no output. Manual — not in CI, see below. ## Not in this PR Delivery. The next change wires the additions into the existing ship-report mail path so the arrival of the mail is the signal. Also not covered by CI: `needsyou_scan_set` and `needsyou_pass` themselves. Every unit test targets the pure helpers in `lib/needsyou.sh`; the state-file write, the `mktemp`/`mv`, and the API glue are verified only by the manual run above — and that glue is exactly where this round's bug lived. The delivery PR should move the scan into `lib/needsyou.sh` behind stubbable seams and cover it, the way `test-automerge.sh` stubs `_fj`. Part of #439
feat: detect what is waiting on the operator
All checks were successful
Lint / check-sync (push) Successful in 5s
Lint / check-sync (pull_request) Successful in 5s
afb2bb3c1d
Half of #439. Detection only -- delivery (email) is a separate change.
The first attempt at this issue tried both at once and blocked at 792
lines against the 400 cap.

Event-driven, not periodic. A recurring digest that usually says
"nothing needs you" trains the reader to stop opening it, and then it
fails exactly when it matters. Only ADDITIONS to the set are
announceable; removals are silent and an unchanged set says nothing.

A PR needs the human exactly when auto-merge will NOT take it. Written
as the negation of automerge_will_take rather than a second copy of the
rule -- that predicate already encodes "APPROVE and the repo is not
pinned to a human", it is already unit-tested, and a duplicate would
drift from it. Issues qualify on Status/Blocked or Status/Need More Info.

Scanned rather than hooked at the emit points (review_request_human,
agent-block.sh), because items must also LEAVE the set when he deals
with them. A hook can only add; without removal an item stays "already
known" forever and a genuine recurrence is never announced again. A scan
is idempotent, self-correcting, and picks up anything that was already
queued before this shipped.

Throttled to every 20th tick: it costs one PR-list call per eligible
repo, which is exactly the fleet-sweep cost igor#441 was about, so it
must not run every tick.

needsyou_merge preserves each item's `since` across scans. Without that
every scan resets the clock and nothing can ever be reported as having
waited days -- which is most of the value.

Two bugs caught before they shipped, both mine:
- The throttle was placed above the cascade prelude, where CASCADE_TICK
  is not yet assigned. Under `set -u` the modulo would have aborted
  EVERY tick.
- An `[ -n "$pr" ] && [ ... ] || continue` -- the same SC2015 shape
  fixed in igor#423.

Verified the predicate against live fleet data: of every open PR, only
debtmom.com#1 reads as needing the human (no shadow verdict, so
auto-merge will not take it), which matches reality.
Author
Collaborator

🤖 Review — REQUEST_CHANGES (automated)

CI for afb2bb3c: success

Detection-only half of the "what's waiting on the operator" feature: a new pure-JSON lib (lib/needsyou.sh), a throttled scan in bin/tick.sh, and 21 unit assertions. Scope (~190 lines), CI green, and the checklist is honest — I recounted the assertions in bin/test-needsyou.sh and got exactly 21, and every checked item corresponds to real code in the diff. The pure functions are well-factored and the since-preservation semantics are correct.

What blocks it is the classification predicate itself, which the description explicitly asks to be reviewed.

Blocking

1. bin/tick.sh:~4100 — "auto-merge will not take it" is much broader than "the human is the blocker".

automerge_will_take "$repo" "$verdict" && continue
...
why="shadow verdict ${verdict:-none} -- auto-merge will not take it"

!automerge_will_take is true for at least three distinct states, only one of which is the operator:

  • verdict = none (not yet reviewed) → waiting on the reviewer, not the human. Every freshly opened bot PR sits in this state; with a 20-tick scan a good fraction will be sampled before review lands and announced as "needs you", then silently leave.
  • verdict = REQUEST_CHANGES → waiting on Igor's own rework loop (up to 3 rounds before it escalates). The human is explicitly not the blocker during those rounds, yet each rework round that flips the item out and back in re-announces it, because needsyou_merge deliberately resets since on return.
  • verdict = COMMENT, or automerge_require_human → genuinely the human. ✔

This contradicts the PR's own stated design goal ("a digest that usually says nothing needs you trains the reader to stop opening it"). The live dry-run cited in the description is a single snapshot of a fleet whose PRs happened to already be reviewed; it doesn't exercise the transient states above. Fix looks like: enumerate the verdicts that actually mean "human", or gate on an escalation/age threshold rather than on the negation of the merge predicate. If the intent really is "anything auto-merge won't take, because you're the fallback", say so in the why string and the docs — but then the noise argument needs revisiting.

2. No test covers the only new decision logic in this PR. needsyou_scan_set() and needsyou_pass() live inline in tick.sh and are untestable as written; the 21 assertions all exercise the pure set algebra in lib/needsyou.sh, which is the part least likely to be wrong. The two things the description calls "worth reviewing" — the PR predicate and the Status/Blocked / Status/Need More Info issue filter — have zero automated coverage. Lifting the per-item classification into a pure function in lib/needsyou.sh (taking repo/verdict/require_human → why-string or empty) would make finding #1 a one-line test.

3. bin/tick.shrepo is not guarded against the literal string null.

repo=$(jq -r '.full_name' <<<"$repo_line" 2>/dev/null); [ -n "$repo" ] || continue

If a line parses as JSON but lacks .full_name, jq -r emits null, which passes [ -n ], and the loop then issues forgejo_list_open_bot_prs null and GET /repos/null/issues. The PR loop three lines below already checks [ "$pr" = "null" ] — apply the same check here.

Non-blocking notes

  • lib/needsyou.sh:needsyou_mergelocal prev="${1:-\{\}}" is dead/wrong: the file's own _needsyou_obj comment explains that \{\} reaches jq verbatim as an invalid document. It's harmless only because _needsyou_obj sanitises it afterwards. Drop the default to ${1:-} for consistency with needsyou_added/needsyou_removed.
  • needsyou_scan_set declares key in local but never assigns it.
  • The throttle rationale says "one PR-list call per eligible repo", but the scan also makes an issue-list call per repo — the fleet-sweep cost is 2×N, not N. Worth correcting the comment at tick.sh:~4088 since it's the justification for NEEDSYOU_SCAN_EVERY=20.
  • needsyou_describe will render a negative age (-1m) if since > now (clock skew / hand-edited state). Cheap to clamp at 0.
  • If discretionary-state.json is ever unparseable, the jq ... > "$tmp" write is skipped silently and prev reads as {} forever, so every scan re-announces the whole set. Consider logging the write failure.
  • Throttle placement after the cascade prelude is correct and the CASCADE_TICK/set -u note is accurate; the new block sits between two existing comment sections without orphaning anything.

Independent review by the harness on claude-opus-5 (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.

### 🤖 Review — `REQUEST_CHANGES` _(automated)_ CI for `afb2bb3c`: **success** Detection-only half of the "what's waiting on the operator" feature: a new pure-JSON lib (`lib/needsyou.sh`), a throttled scan in `bin/tick.sh`, and 21 unit assertions. Scope (~190 lines), CI green, and the checklist is honest — I recounted the assertions in `bin/test-needsyou.sh` and got exactly 21, and every checked item corresponds to real code in the diff. The pure functions are well-factored and the `since`-preservation semantics are correct. What blocks it is the classification predicate itself, which the description explicitly asks to be reviewed. ## Blocking **1. `bin/tick.sh:~4100` — "auto-merge will not take it" is much broader than "the human is the blocker".** ```sh automerge_will_take "$repo" "$verdict" && continue ... why="shadow verdict ${verdict:-none} -- auto-merge will not take it" ``` `!automerge_will_take` is true for at least three distinct states, only one of which is the operator: - `verdict = none` (not yet reviewed) → waiting on the **reviewer**, not the human. Every freshly opened bot PR sits in this state; with a 20-tick scan a good fraction will be sampled before review lands and announced as "needs you", then silently leave. - `verdict = REQUEST_CHANGES` → waiting on **Igor's own rework loop** (up to 3 rounds before it escalates). The human is explicitly not the blocker during those rounds, yet each rework round that flips the item out and back in re-announces it, because `needsyou_merge` deliberately resets `since` on return. - `verdict = COMMENT`, or `automerge_require_human` → genuinely the human. ✔ This contradicts the PR's own stated design goal ("a digest that usually says nothing needs you trains the reader to stop opening it"). The live dry-run cited in the description is a single snapshot of a fleet whose PRs happened to already be reviewed; it doesn't exercise the transient states above. Fix looks like: enumerate the verdicts that actually mean "human", or gate on an escalation/age threshold rather than on the negation of the merge predicate. If the intent really is "anything auto-merge won't take, because you're the fallback", say so in the `why` string and the docs — but then the noise argument needs revisiting. **2. No test covers the only new decision logic in this PR.** `needsyou_scan_set()` and `needsyou_pass()` live inline in `tick.sh` and are untestable as written; the 21 assertions all exercise the pure set algebra in `lib/needsyou.sh`, which is the part least likely to be wrong. The two things the description calls "worth reviewing" — the PR predicate and the `Status/Blocked` / `Status/Need More Info` issue filter — have zero automated coverage. Lifting the per-item classification into a pure function in `lib/needsyou.sh` (taking repo/verdict/require_human → why-string or empty) would make finding #1 a one-line test. **3. `bin/tick.sh` — `repo` is not guarded against the literal string `null`.** ```sh repo=$(jq -r '.full_name' <<<"$repo_line" 2>/dev/null); [ -n "$repo" ] || continue ``` If a line parses as JSON but lacks `.full_name`, `jq -r` emits `null`, which passes `[ -n ]`, and the loop then issues `forgejo_list_open_bot_prs null` and `GET /repos/null/issues`. The PR loop three lines below already checks `[ "$pr" = "null" ]` — apply the same check here. ## Non-blocking notes - `lib/needsyou.sh:needsyou_merge` — `local prev="${1:-\{\}}"` is dead/wrong: the file's own `_needsyou_obj` comment explains that `\{\}` reaches jq verbatim as an invalid document. It's harmless only because `_needsyou_obj` sanitises it afterwards. Drop the default to `${1:-}` for consistency with `needsyou_added`/`needsyou_removed`. - `needsyou_scan_set` declares `key` in `local` but never assigns it. - The throttle rationale says "one PR-list call per eligible repo", but the scan also makes an issue-list call per repo — the fleet-sweep cost is 2×N, not N. Worth correcting the comment at `tick.sh:~4088` since it's the justification for `NEEDSYOU_SCAN_EVERY=20`. - `needsyou_describe` will render a negative age (`-1m`) if `since > now` (clock skew / hand-edited state). Cheap to clamp at 0. - If `discretionary-state.json` is ever unparseable, the `jq ... > "$tmp"` write is skipped silently and `prev` reads as `{}` forever, so every scan re-announces the whole set. Consider logging the write failure. - Throttle placement after the cascade prelude is correct and the `CASCADE_TICK`/`set -u` note is accurate; the new block sits between two existing comment sections without orphaning anything. --- <sub>Independent review by the harness on `claude-opus-5` (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.</sub> <!-- review sha=afb2bb3c1d8d4904068d882e60b4a8794a5f8fee verdict=REQUEST_CHANGES ci=success -->
igor added spent time 2026-07-28 17:21:06 +00:00
1 minute 56 seconds
igor self-assigned this 2026-07-28 17:21:06 +00:00
Author
Collaborator

🔧 Rework — round 1 (automated)

Addressed the review on claude-opus-5 at effort high — 1 new commit(s).

### 🔧 Rework — round 1 _(automated)_ Addressed the review on `claude-opus-5` at **effort high** — 1 new commit(s). <!-- audit:rework round=1 effort=high -->
igor removed their assignment 2026-07-28 17:28:31 +00:00
igor added spent time 2026-07-28 17:28:32 +00:00
6 minutes 1 second
fix: narrow "needs you" to the states where you are actually the blocker
All checks were successful
Lint / check-sync (push) Successful in 5s
Lint / check-sync (pull_request) Successful in 5s
3a6091c2ce
The scan classified a PR as waiting on the operator whenever auto-merge
would not take it. That is three states, and only one of them is his:
an unreviewed PR is the shadow reviewer's turn, and a REQUEST_CHANGES
inside the rework loop is Igor's for up to three rounds. Both flip in
and out of the set on their own, so both would be announced and then
silently leave -- the every-scan noise this feature exists to avoid.

The verdicts that mean HUMAN are now enumerated in needsyou_pr_why,
mirroring do_review_tick's own routing (COMMENT, an APPROVE on a
human-pinned repo, an escalated or CI-unverifiable REQUEST_CHANGES).
The issue filter moves to needsyou_issue_lines. Both are pure and
unit-tested, so the only decision logic in the change is now covered:
17 new assertions, 38 total.

Also from review: guard the repo name against a literal "null" the way
the PR loop already does, clamp a negative age at 0, drop the dead
`\{\}` default and the unassigned local, log a state-write failure
rather than silently re-announcing the whole set forever, and correct
the throttle rationale -- the sweep costs 2N calls, not N.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Review — REQUEST_CHANGES (automated)

CI for 3a6091c2: success

Detection-only half of igor#439: a new lib/needsyou.sh (pure JSON helpers), a throttled scan/announce pass in bin/tick.sh, and a self-contained unit test. The library code itself is careful and well tested; what blocks is that the PR description makes verification claims the shipped code contradicts, plus one ordering risk in tick.sh I can't verify from the diff.

Blocking

1. The checked "Predicate dry-run against live fleet data" claim contradicts the shipped predicate.
The description states: "across every open PR in the fleet, only debtmom.com#1 reads as needing the human (verdict none, so auto-merge will not take it)". But lib/needsyou.sh:needsyou_pr_why has no none branch — an unrecognised/none verdict falls through the case and returns empty — and bin/test-needsyou.sh asserts exactly that:

eq "so is a PR whose verdict never parsed" "" "$(needsyou_pr_why "none" false 0 true)"

So the one item the dry-run says would be announced would not be announced by this code. The dry-run was evidently run against the earlier ! automerge_will_take predicate and never re-run after the redesign. That matters beyond bookkeeping: the honest reading is that the shipped predicate currently matches nothing in the fleet, and the only evidence offered that it does anything is stale. Please re-run the dry-run against needsyou_pr_why as written and report the real result (even if it's "zero items today"), or drop the checkbox.

2. "Two decisions worth reviewing" describes a design that isn't in the diff.
The description says the predicate is "expressed as the negation of automerge_will_take rather than a second copy of the rule… That predicate already encodes…". The code says the opposite, at length (lib/needsyou.sh, comment above needsyou_pr_why): "The first cut of this asked ! automerge_will_take, which is much broader… So the verdicts that mean HUMAN are enumerated instead." The human reads the description to decide what to scrutinise; here it points at the wrong design and the wrong risk (a hand-enumerated verdict table that can drift from the merge policy — which is the tradeoff actually worth reviewing). Update the description to match what shipped.

3. Unverifiable: is ANALYSIS_REPOS_JSON assigned at the call site?
needsyou_scan_set ends with done <<<"$ANALYSIS_REPOS_JSON" (bin/tick.sh, new block), and needsyou_pass is invoked immediately after CASCADE_TICK=…, before the cascade stages run. Under set -u — which the PR itself notes is in force, and which already produced one of the two self-caught bugs — an unassigned ANALYSIS_REPOS_JSON at that point aborts the entire tick, and because the pass only fires on every 20th tick that failure would be rare and confusing. I can't see where that variable is set from the diff. Either confirm it's assigned above line ~4239, or make it defensive (<<<"${ANALYSIS_REPOS_JSON:-}") so a missing/late assignment degrades to an empty scan rather than killing the tick.

Non-blocking

  • Cost is understated in the description. The body says the scan "costs one PR-list call per eligible repo"; the code comment (correctly) says 2N — a PR list and an unconditional _fj GET /repos/…/issues per repo, the issue call being paid even for repos with no bot PRs. The throttle rationale (igor#441) is argued from the smaller number. Consider gating the issue list the same way automerge_require_human/maintenance_repo_validated are gated, or at least fix the number.
  • "21 assertions" — I count roughly 38 eq/has checks in bin/test-needsyou.sh. Undercount, harmless, but the checklist should be accurate.
  • needsyou_scan_set / needsyou_pass have no test coverage. All 38 assertions target the pure helpers; the parts that touch the state file, mktemp/mv, and the API glue — i.e. where the two self-caught bugs lived — are exercised only in production. Not a blocker for a detection-only PR, but worth a note in the follow-up.
  • needsyou_removed is defined and tested but never called. Documented as "available for a caller"; fine, just flagging the dead path.
  • needsyou_item failure mode: a non-numeric since makes jq fail and the function prints {}, which the scan then folds into the set under a valid key — needsyou_describe would render null#null. Only reachable via a caller bug (the scan always passes 0), so low priority.

What I did check and found clean

  • Scope: single issue, no unrelated refactors, no tests or working code deleted to fit budget; ~380 added lines.
  • No CI/workflow changes; no secrets, injection, or unsafe input handling. _fj output is guarded by _needsyou_arr, state reads by _needsyou_obj.
  • needsyou_merge since-preservation semantics are correct ($p[.key].since // $now, new keys take $now, returning items reset — all directly asserted).
  • automerge_require_human … && require_human=true is safe under set -e (failing left side of an AND-list doesn't trigger exit).
  • CI is green on the head commit.

Independent review by the harness on claude-opus-5 (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.

### 🤖 Review — `REQUEST_CHANGES` _(automated)_ CI for `3a6091c2`: **success** Detection-only half of igor#439: a new `lib/needsyou.sh` (pure JSON helpers), a throttled scan/announce pass in `bin/tick.sh`, and a self-contained unit test. The library code itself is careful and well tested; what blocks is that the PR description makes verification claims the shipped code contradicts, plus one ordering risk in `tick.sh` I can't verify from the diff. ## Blocking **1. The checked "Predicate dry-run against live fleet data" claim contradicts the shipped predicate.** The description states: *"across every open PR in the fleet, only `debtmom.com#1` reads as needing the human (verdict `none`, so auto-merge will not take it)"*. But `lib/needsyou.sh:needsyou_pr_why` has no `none` branch — an unrecognised/`none` verdict falls through the `case` and returns empty — and `bin/test-needsyou.sh` asserts exactly that: ``` eq "so is a PR whose verdict never parsed" "" "$(needsyou_pr_why "none" false 0 true)" ``` So the one item the dry-run says would be announced would **not** be announced by this code. The dry-run was evidently run against the earlier `! automerge_will_take` predicate and never re-run after the redesign. That matters beyond bookkeeping: the honest reading is that the shipped predicate currently matches **nothing** in the fleet, and the only evidence offered that it does anything is stale. Please re-run the dry-run against `needsyou_pr_why` as written and report the real result (even if it's "zero items today"), or drop the checkbox. **2. "Two decisions worth reviewing" describes a design that isn't in the diff.** The description says the predicate is *"expressed as the negation of `automerge_will_take` rather than a second copy of the rule… That predicate already encodes…"*. The code says the opposite, at length (`lib/needsyou.sh`, comment above `needsyou_pr_why`): *"The first cut of this asked `! automerge_will_take`, which is much broader… So the verdicts that mean HUMAN are enumerated instead."* The human reads the description to decide what to scrutinise; here it points at the wrong design and the wrong risk (a hand-enumerated verdict table that *can* drift from the merge policy — which is the tradeoff actually worth reviewing). Update the description to match what shipped. **3. Unverifiable: is `ANALYSIS_REPOS_JSON` assigned at the call site?** `needsyou_scan_set` ends with `done <<<"$ANALYSIS_REPOS_JSON"` (bin/tick.sh, new block), and `needsyou_pass` is invoked immediately after `CASCADE_TICK=…`, before the cascade stages run. Under `set -u` — which the PR itself notes is in force, and which already produced one of the two self-caught bugs — an unassigned `ANALYSIS_REPOS_JSON` at that point aborts the entire tick, and because the pass only fires on every 20th tick that failure would be rare and confusing. I can't see where that variable is set from the diff. Either confirm it's assigned above line ~4239, or make it defensive (`<<<"${ANALYSIS_REPOS_JSON:-}"`) so a missing/late assignment degrades to an empty scan rather than killing the tick. ## Non-blocking - **Cost is understated in the description.** The body says the scan *"costs one PR-list call per eligible repo"*; the code comment (correctly) says 2N — a PR list **and** an unconditional `_fj GET /repos/…/issues` per repo, the issue call being paid even for repos with no bot PRs. The throttle rationale (igor#441) is argued from the smaller number. Consider gating the issue list the same way `automerge_require_human`/`maintenance_repo_validated` are gated, or at least fix the number. - **"21 assertions"** — I count roughly 38 `eq`/`has` checks in `bin/test-needsyou.sh`. Undercount, harmless, but the checklist should be accurate. - **`needsyou_scan_set` / `needsyou_pass` have no test coverage.** All 38 assertions target the pure helpers; the parts that touch the state file, `mktemp`/`mv`, and the API glue — i.e. where the two self-caught bugs lived — are exercised only in production. Not a blocker for a detection-only PR, but worth a note in the follow-up. - **`needsyou_removed`** is defined and tested but never called. Documented as "available for a caller"; fine, just flagging the dead path. - **`needsyou_item` failure mode:** a non-numeric `since` makes `jq` fail and the function prints `{}`, which the scan then folds into the set under a valid key — `needsyou_describe` would render `null#null`. Only reachable via a caller bug (the scan always passes `0`), so low priority. ## What I did check and found clean - Scope: single issue, no unrelated refactors, no tests or working code deleted to fit budget; ~380 added lines. - No CI/workflow changes; no secrets, injection, or unsafe input handling. `_fj` output is guarded by `_needsyou_arr`, state reads by `_needsyou_obj`. - `needsyou_merge` `since`-preservation semantics are correct (`$p[.key].since // $now`, new keys take `$now`, returning items reset — all directly asserted). - `automerge_require_human … && require_human=true` is safe under `set -e` (failing left side of an AND-list doesn't trigger exit). - CI is green on the head commit. --- <sub>Independent review by the harness on `claude-opus-5` (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.</sub> <!-- review sha=3a6091c2ce017123e2fd161691c67ba0b4005277 verdict=REQUEST_CHANGES ci=success -->
igor added spent time 2026-07-28 17:31:27 +00:00
2 minutes 5 seconds
igor self-assigned this 2026-07-28 17:31:27 +00:00
Author
Collaborator

🔧 Rework — round 2 (automated)

Addressed the review on claude-opus-5 at effort xhigh — 1 new commit(s).

### 🔧 Rework — round 2 _(automated)_ Addressed the review on `claude-opus-5` at **effort xhigh** — 1 new commit(s). <!-- audit:rework round=2 effort=xhigh -->
igor removed their assignment 2026-07-28 17:46:56 +00:00
igor added spent time 2026-07-28 17:46:57 +00:00
14 minutes 18 seconds
fix: make the needs-you scan actually see a PR
All checks were successful
Lint / check-sync (push) Successful in 5s
Lint / check-sync (pull_request) Successful in 5s
864e6f5417
The dry-run the PR description offered as evidence was run against the
earlier `! automerge_will_take` predicate, not the one that shipped.
Re-run against the shipped code it found nothing -- and not because the
fleet was quiet. `forgejo_list_open_bot_prs` answers with a JSON ARRAY,
and the scan read that text line by line, so `needsyou_pr_why` was called
with "[", "  {" and '"number": 449,' and never once with a PR number.

`needsyou_pr_numbers` pulls `.number` out the way do_automerge_tick does,
and drops `WIP:` checkpoint PRs for the same reason the rework loop is
dropped: that is Igor mid-task, not the operator's turn, and such a PR can
still carry the verdict it had before it checkpointed.

Fixing it also makes the emptiness test below it mean something. "[]" is a
non-empty string, so gating on the raw payload paid for the agent.json
fetch on every repo in the fleet rather than only where a PR exists.

Also from review:

- `<<<"${ANALYSIS_REPOS_JSON:-}"`. The validation sweep assigns it well
  above the call site, but this is the one fleet loop that runs from the
  cascade prelude, so it is the likeliest to end up above that assignment
  later. Under `set -u` that aborts the tick, and only every 20th tick --
  a rare, confusing failure.
- A non-numeric `since` no longer voids `needsyou_item` into a bare {},
  which the scan would fold in under a valid key and describe as
  "null#null".
- The cost comment now says what it actually costs: 2N, plus one
  agent.json fetch per repo that has an open bot PR.

Dry-run re-run against the fixed scan: 2 open bot PRs fleet-wide, 0
needing the human. debtmom.com#1 has no shadow verdict (the reviewer's
turn) and igor#449 is REQUEST_CHANGES at round 2 (Igor's turn). Both
correctly silent, and both checkable against reality.
Author
Collaborator

🤖 Review — REQUEST_CHANGES (automated)

CI for 864e6f54: success

Detection half of igor#439: a new lib/needsyou.sh with pure set/verdict helpers, a throttled fleet scan wired into bin/tick.sh's cascade prelude, and 46 unit checks. The pure helpers are genuinely good — the needsyou_pr_why enumeration is well-reasoned and the description is unusually honest (I recounted the test file: 46 checks, matches the claim; the scope overrun is self-declared rather than hidden). What blocks me is that the layer where last round's bug lived is still the layer with zero coverage, and it still contains the same unverified assumption.

Blocking

1. needsyou_scan_set iterates ANALYSIS_REPOS_JSON line-by-line — the exact pattern that made the last version blind (bin/tick.sh, done <<<"${ANALYSIS_REPOS_JSON:-}" with jq -r '.full_name // empty' <<<"$repo_line"). This only works if that variable is newline-delimited compact JSON objects. If it is a JSON array — pretty-printed or not — every repo_line is [, {, "full_name": ... and the outer loop yields zero repos, i.e. the feature is silently dead fleet-wide again, and "0 items need you" is once more a plausible-looking answer. I cannot verify the format from this diff (the assignment isn't in it). The manual end-to-end run does not cover this: the description says the functions were lifted verbatim out of tick.sh and run with a hand-built environment, so the harness supplied whatever format the author assumed rather than what tick.sh actually assigns. Fixed looks like: point to the assignment site and confirm the shape, or normalise defensively (jq -c '.[]?' <<<"$ANALYSIS_REPOS_JSON" style, matching whatever needsyou_pr_numbers now does for pulls).

2. The glue is uncovered, and the author says so. Every one of the 46 checks targets pure helpers. needsyou_scan_set and needsyou_pass — the state-file write, the mktemp/mv, the API fan-out, the ANALYSIS_REPOS_JSON loop above — have no test at all. Deferring that to the delivery PR is not acceptable in a round that exists because an untested glue bug shipped and read as a legitimate zero. The author already names the pattern (test-automerge.sh stubs _fj); at minimum stub _fj / forgejo_list_open_bot_prs / automerge_require_human / review_rework_rounds and assert that needsyou_scan_set produces the right keys for a two-repo fixture. That single test would have caught both this round's bug and finding #1.

3. Scope: 458 net lines against the ~400 cap. Declared honestly, but declared is not waived. The author's own offer is the right fix: land needsyou_pr_numbers + its regression tests as a small stacked PR first, then the rest under budget.

Non-blocking, but I'd want these addressed

  • Scan failure is indistinguishable from "nothing is waiting." forgejo_list_open_bot_prs ... 2>/dev/null || true and _fj GET ... || true both degrade to empty, and needsyou_pass then persists a merged set that has dropped those items — losing their since. One transient API blip silently removes an item the operator hasn't touched, and the next successful scan re-announces it as new. That is precisely the "notification the reader learns to ignore" failure the design section argues against. Consider skipping the state write entirely when the scan produced no repos / no successful list calls, rather than persisting an empty set.
  • Inconsistent error defensiveness against set -e. prev=$(jq -c '.needsyou // {}' "$sf" 2>/dev/null || echo '{}') is guarded; verdict=$(jq -r ... "$sf" 2>/dev/null) and out=$(jq -c ... <<<"$out") in needsyou_scan_set are not. If tick.sh runs under set -e, a malformed discretionary-state.json aborts the whole tick from inside the scan. Add the same || echo ... fallbacks.
  • Test plan overclaims wait-time coverage. It says formatting (m/h/d); the tests assert waiting 1d, waiting 30m, waiting 0m — there is no hours case, and the 60/1440 boundaries are untested. Small, but it's a checklist item that doesn't fully match the diff.
  • needsyou_describe's if $i == null then empty is dead code — ($s[$k] // empty) as $i already emits nothing for a missing key. Harmless, but the comment/branch implies a guard that isn't doing anything.
  • _needsyou_obj accepts any valid JSON, not just objects: _needsyou_obj '[]' returns [], and needsyou_added then reports array indices as keys. Tightening it to jq -ce 'type == "object"' (mirroring _needsyou_arr) closes that.

What's good

  • needsyou_pr_why as a hand-enumerated table rather than ! automerge_will_take is the right call and is argued for honestly, including the drift risk. The state-by-state tests make the table's intent legible.
  • Dropping WIP: checkpoints, and the reasoning that such a PR can carry a stale verdict, is a sharp catch.
  • Preserving since across scans via merge rather than replace, with a test that an item which left and returned resets its clock.
  • Nothing here touches CI config, secrets, or auth; no injection or unsafe-input surface (all API payloads go through jq with --arg/--argjson, never eval'd). CI is green.

Independent review by the harness on claude-opus-5 (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.

### 🤖 Review — `REQUEST_CHANGES` _(automated)_ CI for `864e6f54`: **success** Detection half of igor#439: a new `lib/needsyou.sh` with pure set/verdict helpers, a throttled fleet scan wired into `bin/tick.sh`'s cascade prelude, and 46 unit checks. The pure helpers are genuinely good — the `needsyou_pr_why` enumeration is well-reasoned and the description is unusually honest (I recounted the test file: **46 checks, matches the claim**; the scope overrun is self-declared rather than hidden). What blocks me is that the layer where last round's bug lived is still the layer with zero coverage, and it still contains the same unverified assumption. ## Blocking **1. `needsyou_scan_set` iterates `ANALYSIS_REPOS_JSON` line-by-line — the exact pattern that made the last version blind** (`bin/tick.sh`, `done <<<"${ANALYSIS_REPOS_JSON:-}"` with `jq -r '.full_name // empty' <<<"$repo_line"`). This only works if that variable is newline-delimited compact JSON objects. If it is a JSON array — pretty-printed or not — every `repo_line` is `[`, ` {`, `"full_name": ...` and the outer loop yields zero repos, i.e. the feature is silently dead fleet-wide again, and "0 items need you" is once more a plausible-looking answer. I cannot verify the format from this diff (the assignment isn't in it). The manual end-to-end run does **not** cover this: the description says the functions were *lifted verbatim out of* `tick.sh` and run with a hand-built environment, so the harness supplied whatever format the author assumed rather than what tick.sh actually assigns. Fixed looks like: point to the assignment site and confirm the shape, or normalise defensively (`jq -c '.[]?' <<<"$ANALYSIS_REPOS_JSON"` style, matching whatever `needsyou_pr_numbers` now does for pulls). **2. The glue is uncovered, and the author says so.** Every one of the 46 checks targets pure helpers. `needsyou_scan_set` and `needsyou_pass` — the state-file write, the `mktemp`/`mv`, the API fan-out, the `ANALYSIS_REPOS_JSON` loop above — have no test at all. Deferring that to the delivery PR is not acceptable in a round that exists *because* an untested glue bug shipped and read as a legitimate zero. The author already names the pattern (`test-automerge.sh` stubs `_fj`); at minimum stub `_fj` / `forgejo_list_open_bot_prs` / `automerge_require_human` / `review_rework_rounds` and assert that `needsyou_scan_set` produces the right keys for a two-repo fixture. That single test would have caught both this round's bug and finding #1. **3. Scope: 458 net lines against the ~400 cap.** Declared honestly, but declared is not waived. The author's own offer is the right fix: land `needsyou_pr_numbers` + its regression tests as a small stacked PR first, then the rest under budget. ## Non-blocking, but I'd want these addressed - **Scan failure is indistinguishable from "nothing is waiting."** `forgejo_list_open_bot_prs ... 2>/dev/null || true` and `_fj GET ... || true` both degrade to empty, and `needsyou_pass` then persists a `merged` set that has *dropped* those items — losing their `since`. One transient API blip silently removes an item the operator hasn't touched, and the next successful scan re-announces it as new. That is precisely the "notification the reader learns to ignore" failure the design section argues against. Consider skipping the state write entirely when the scan produced no repos / no successful list calls, rather than persisting an empty set. - **Inconsistent error defensiveness against `set -e`.** `prev=$(jq -c '.needsyou // {}' "$sf" 2>/dev/null || echo '{}')` is guarded; `verdict=$(jq -r ... "$sf" 2>/dev/null)` and `out=$(jq -c ... <<<"$out")` in `needsyou_scan_set` are not. If `tick.sh` runs under `set -e`, a malformed `discretionary-state.json` aborts the whole tick from inside the scan. Add the same `|| echo ...` fallbacks. - **Test plan overclaims wait-time coverage.** It says formatting `(m/h/d)`; the tests assert `waiting 1d`, `waiting 30m`, `waiting 0m` — there is no hours case, and the 60/1440 boundaries are untested. Small, but it's a checklist item that doesn't fully match the diff. - `needsyou_describe`'s `if $i == null then empty` is dead code — `($s[$k] // empty) as $i` already emits nothing for a missing key. Harmless, but the comment/branch implies a guard that isn't doing anything. - `_needsyou_obj` accepts any valid JSON, not just objects: `_needsyou_obj '[]'` returns `[]`, and `needsyou_added` then reports array *indices* as keys. Tightening it to `jq -ce 'type == "object"'` (mirroring `_needsyou_arr`) closes that. ## What's good - `needsyou_pr_why` as a hand-enumerated table rather than `! automerge_will_take` is the right call and is argued for honestly, including the drift risk. The state-by-state tests make the table's intent legible. - Dropping `WIP:` checkpoints, and the reasoning that such a PR can carry a stale verdict, is a sharp catch. - Preserving `since` across scans via merge rather than replace, with a test that an item which left and returned resets its clock. - Nothing here touches CI config, secrets, or auth; no injection or unsafe-input surface (all API payloads go through `jq` with `--arg`/`--argjson`, never eval'd). CI is green. --- <sub>Independent review by the harness on `claude-opus-5` (effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.</sub> <!-- review sha=864e6f54170588c8c85773fc0867263c593b707b verdict=REQUEST_CHANGES ci=success -->
igor added spent time 2026-07-28 17:50:35 +00:00
2 minutes 31 seconds
igor self-assigned this 2026-07-28 17:50:35 +00:00
Author
Collaborator

🔧 Rework — round 3 (automated)

Addressed the review on claude-opus-5 at effort max — 2 new commit(s).

### 🔧 Rework — round 3 _(automated)_ Addressed the review on `claude-opus-5` at **effort max** — 2 new commit(s). <!-- audit:rework round=3 effort=max -->
igor removed their assignment 2026-07-28 18:11:08 +00:00
igor added spent time 2026-07-28 18:11:08 +00:00
19 minutes 17 seconds
Addresses the shadow review on #449.

Blocking #1 -- ANALYSIS_REPOS_JSON shape. tick.sh assigns it as
`jq -c '.[]' <<<"$ALL_REPOS"` (see its "Analysis set:" comment), so
the line-by-line read is correct today. Normalised anyway via
_needsyou_repo_lines: an array, pretty-printed or compact, now flattens
instead of yielding "[" and never a repo.

Blocking #2 -- the glue had no test. needsyou_scan_set and needsyou_pass
moved from bin/tick.sh into lib/needsyou.sh, where bin/test-needsyou.sh
stubs forgejo_list_open_bot_prs, _fj, automerge_require_human,
maintenance_repo_validated, review_rework_rounds, discretionary_state_file
and log against a two-repo fixture. 46 -> 70 checks.

Also: a scan that did not finish is now distinguishable from one that
found nothing. forgejo_list_open_bot_prs is `_fj | jq`, so its exit
status is jq's -- a failed fetch returns rc 0 with empty output, which
exit-status checks cannot catch. _needsyou_listed gates on the payload
parsing as an array instead; needsyou_scan_set returns nonzero on any
unanswered call, and needsyou_pass then keeps the previous set rather
than persisting one with the items' `since` dropped.

Plus the smaller asks: || fallbacks on the two unguarded jq calls,
_needsyou_obj tightened to type == "object", the dead null branch in
needsyou_describe removed, and the 60/1440-minute boundaries tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test: make the errexit check on the pass non-vacuous
All checks were successful
Lint / check-sync (push) Successful in 6s
Lint / check-sync (pull_request) Successful in 5s
365bb32ce5
The first version wrapped it as `if ( set -euo pipefail; needsyou_pass )`,
which cannot fail: bash suppresses errexit for the whole of a command it
is testing, subshell and called functions included. Run as a statement
with the status read afterwards, it catches the real thing -- reverting
the guard on needsyou_scan_set's return makes the incomplete-scan path
abort the tick with rc 1, and the check goes red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Review — COMMENT (automated)

CI for 365bb32c: success

Detection-only half of #439: a new lib/needsyou.sh (pure predicates + a fleet scan), a 271-line unit suite, and a throttled needsyou_pass call in the cascade prelude. CI is green and I found no defect I can prove from the diff — but I can't approve it: the diff is ~625 added lines against the 400 cap, the description is materially stale relative to what shipped, and every integration seam is stubbed by the same author who wrote the code, in exactly the layer that shipped blind last round.

What I can't verify from the diff (why this isn't an APPROVE)

needsyou_scan_set reaches five things defined outside this diff, and the new suite stubs all five. A signature or shape mismatch reproduces last round's bug class — the feature reads as "nothing needs you" while the tests stay green:

  • _fj GET "/repos/${repo}/issues?state=open&type=issues&limit=50" — the stub matches on "$2", so it encodes _fj <METHOD> <PATH>. Please confirm against the real _fj.
  • forgejo_list_open_bot_prs "$repo" "${BOT_USER:-}" — called with two args; the stub only reads $1. Harmless if the real arity is 1, wrong if arg 2 means something else.
  • review_rework_rounds "${repo}#${pr}" — stub returns 0. This is the silent one: needsyou_pr_why coerces a non-numeric round to 0, so a wrong key/arity means escalated REQUEST_CHANGES PRs are never announced, with no error anywhere.
  • .review["repo#pr"].verdict in the discretionary state file — also silent on mismatch (verdict reads "" → "reviewer's turn" → nothing announced, forever).
  • automerge_require_human / maintenance_repo_validated polarity (rc 0 = "pinned" / "validated").

Also unverifiable: the call site's comment claims ANALYSIS_REPOS_JSON is "assigned well above by the validation sweep" (bin/tick.sh, new block after CASCADE_TICK=). The ${ANALYSIS_REPOS_JSON:-} guard plus the incomplete-scan return means a wrong ordering fails loudly (needs-you: scan incomplete every 20th tick) — good design — but I can't confirm the ordering itself.

Description vs. diff

The checkboxes all correspond to real work; the prose is stale in the understating direction, which makes it an unreliable map rather than a fabrication:

  • "46 checks" — I count ~72 assertions in bin/test-needsyou.sh.
  • "Not in this PR → Also not covered by CI: needsyou_scan_set and needsyou_pass themselves … verified only by the manual run above" — flatly contradicted by the -- the glue -- section of the same diff, which stubs the seams and covers both functions, the state write, the set -euo pipefail path, and the incomplete-scan bail. That paragraph should be deleted, not left telling the human a gap exists that was closed.
  • Test-plan bullet "lifted needsyou_scan_set + needsyou_pass verbatim out of tick.sh" — those functions live in lib/needsyou.sh in the shipped diff.
  • "458 net lines" — git diff here is 625 insertions, 0 deletions across three files. If the repo's scope gate counts non-comment/non-blank lines, 458 is plausible as that metric, but the human should know the raw number.

Scope

625 added lines against a ~400 cap, self-acknowledged. I'm explicitly not asking for it to be trimmed — the overage is tests and the comments that justify the verdict table, which the contract says never to cut. But the split decision is the human's, and the author has already offered one. Worth noting the natural seam is lib+tests vs. the tick.sh wiring, not the needsyou_pr_numbers fix.

Design / coverage observations (non-blocking)

  • Approved-but-stuck PRs never enter the set. needsyou_pr_why returns empty for APPROVE unless the repo is human-pinned. A PR approved days ago that auto-merge can't take (red CI, merge conflict, protected branch) is nobody's turn under this table, forever. That looks like a real "you are the blocker" case the enumeration drops. The description defends the table against derivation, but doesn't address this hole.
  • Drift, as the author flags: the table is a second reading of the merge policy with nothing enforcing correspondence. This repo already has check-sync.sh enforcing exactly that kind of contract for OUTCOME sentinels and cascade stages — a similar gate is the obvious follow-up.
  • Prelude, not a cascade stage. Running 2N+ synchronous API calls before stage dispatch means the 20th tick pays the fleet sweep before doing any work, and it sits outside the OUTCOME/starvation bookkeeping check-sync.sh enforces for stages. The stated reason (CASCADE_TICK isn't assigned yet) is fair; flagging it as an architecture call for the human.
  • limit=50 on the issue list — a repo with >50 open issues can hide a Status/Blocked one with no signal.
  • NEEDSYOU_SCAN_EVERY=0 in the environment makes the throttle a division by zero ($(( CASCADE_TICK % NEEDSYOU_SCAN_EVERY ))). Exempt from errexit inside the if, so it degrades to noise, but a -gt 0 guard is one line.
  • needsyou_removed is defined and tested but unused in production.
  • ${CHECKPOINT_WIP_PREFIX:-WIP: } — if the repo's real constant has another name/value, the WIP filter silently stops matching. Failure mode is visible (a checkpoint PR gets announced), so low risk.

What's good

The _needsyou_obj / _needsyou_arr type checks (not just parse checks) close the keys-on-an-array hole; _needsyou_listed correctly distinguishes "list call didn't answer" from "no bot PRs" given _fj | jq returns jq's status; needsyou_pass throwing away a partial scan rather than persisting it is the right call and is tested; the errexit_rc helper with its comment about if ( set -e; … ) suppressing errexit is a genuinely subtle trap correctly avoided; jq is used with --arg/--argjson throughout, no injection surface. No security concerns.


Independent review by the harness on claude-opus-5 (effort: max). The human reviewer is requested once Igor has reviewed; a human still merges.

### 🤖 Review — `COMMENT` _(automated)_ CI for `365bb32c`: **success** Detection-only half of #439: a new `lib/needsyou.sh` (pure predicates + a fleet scan), a 271-line unit suite, and a throttled `needsyou_pass` call in the cascade prelude. CI is green and I found no defect I can prove from the diff — but I can't approve it: the diff is ~625 added lines against the 400 cap, the description is materially stale relative to what shipped, and every integration seam is stubbed by the same author who wrote the code, in exactly the layer that shipped blind last round. ## What I can't verify from the diff (why this isn't an APPROVE) `needsyou_scan_set` reaches five things defined outside this diff, and the new suite stubs all five. A signature or shape mismatch reproduces last round's bug class — the feature reads as "nothing needs you" while the tests stay green: - `_fj GET "/repos/${repo}/issues?state=open&type=issues&limit=50"` — the stub matches on `"$2"`, so it encodes `_fj <METHOD> <PATH>`. Please confirm against the real `_fj`. - `forgejo_list_open_bot_prs "$repo" "${BOT_USER:-}"` — called with two args; the stub only reads `$1`. Harmless if the real arity is 1, wrong if arg 2 means something else. - `review_rework_rounds "${repo}#${pr}"` — stub returns `0`. This is the **silent** one: `needsyou_pr_why` coerces a non-numeric round to 0, so a wrong key/arity means escalated `REQUEST_CHANGES` PRs are never announced, with no error anywhere. - `.review["repo#pr"].verdict` in the discretionary state file — also silent on mismatch (verdict reads `""` → "reviewer's turn" → nothing announced, forever). - `automerge_require_human` / `maintenance_repo_validated` polarity (rc 0 = "pinned" / "validated"). Also unverifiable: the call site's comment claims `ANALYSIS_REPOS_JSON` is "assigned well above by the validation sweep" (`bin/tick.sh`, new block after `CASCADE_TICK=`). The `${ANALYSIS_REPOS_JSON:-}` guard plus the incomplete-scan return means a wrong ordering fails *loudly* (`needs-you: scan incomplete` every 20th tick) — good design — but I can't confirm the ordering itself. ## Description vs. diff The checkboxes all correspond to real work; the prose is stale in the *understating* direction, which makes it an unreliable map rather than a fabrication: - "**46 checks**" — I count ~72 assertions in `bin/test-needsyou.sh`. - "**Not in this PR → Also not covered by CI: `needsyou_scan_set` and `needsyou_pass` themselves … verified only by the manual run above**" — flatly contradicted by the `-- the glue --` section of the same diff, which stubs the seams and covers both functions, the state write, the `set -euo pipefail` path, and the incomplete-scan bail. That paragraph should be deleted, not left telling the human a gap exists that was closed. - Test-plan bullet "lifted `needsyou_scan_set` + `needsyou_pass` verbatim out of `tick.sh`" — those functions live in `lib/needsyou.sh` in the shipped diff. - "**458 net lines**" — `git diff` here is **625 insertions, 0 deletions** across three files. If the repo's scope gate counts non-comment/non-blank lines, 458 is plausible as *that* metric, but the human should know the raw number. ## Scope 625 added lines against a ~400 cap, self-acknowledged. I'm explicitly **not** asking for it to be trimmed — the overage is tests and the comments that justify the verdict table, which the contract says never to cut. But the split decision is the human's, and the author has already offered one. Worth noting the natural seam is lib+tests vs. the `tick.sh` wiring, not the `needsyou_pr_numbers` fix. ## Design / coverage observations (non-blocking) - **Approved-but-stuck PRs never enter the set.** `needsyou_pr_why` returns empty for `APPROVE` unless the repo is human-pinned. A PR approved days ago that auto-merge can't take (red CI, merge conflict, protected branch) is nobody's turn under this table, forever. That looks like a real "you are the blocker" case the enumeration drops. The description defends the table against *derivation*, but doesn't address this hole. - **Drift**, as the author flags: the table is a second reading of the merge policy with nothing enforcing correspondence. This repo already has `check-sync.sh` enforcing exactly that kind of contract for OUTCOME sentinels and cascade stages — a similar gate is the obvious follow-up. - **Prelude, not a cascade stage.** Running 2N+ synchronous API calls before stage dispatch means the 20th tick pays the fleet sweep before doing any work, and it sits outside the OUTCOME/starvation bookkeeping `check-sync.sh` enforces for stages. The stated reason (`CASCADE_TICK` isn't assigned yet) is fair; flagging it as an architecture call for the human. - `limit=50` on the issue list — a repo with >50 open issues can hide a `Status/Blocked` one with no signal. - `NEEDSYOU_SCAN_EVERY=0` in the environment makes the throttle a division by zero (`$(( CASCADE_TICK % NEEDSYOU_SCAN_EVERY ))`). Exempt from errexit inside the `if`, so it degrades to noise, but a `-gt 0` guard is one line. - `needsyou_removed` is defined and tested but unused in production. - `${CHECKPOINT_WIP_PREFIX:-WIP: }` — if the repo's real constant has another name/value, the WIP filter silently stops matching. Failure mode is visible (a checkpoint PR gets announced), so low risk. ## What's good The `_needsyou_obj` / `_needsyou_arr` type checks (not just parse checks) close the `keys`-on-an-array hole; `_needsyou_listed` correctly distinguishes "list call didn't answer" from "no bot PRs" given `_fj | jq` returns jq's status; `needsyou_pass` throwing away a partial scan rather than persisting it is the right call and is tested; the `errexit_rc` helper with its comment about `if ( set -e; … )` suppressing errexit is a genuinely subtle trap correctly avoided; jq is used with `--arg`/`--argjson` throughout, no injection surface. No security concerns. --- <sub>Independent review by the harness on `claude-opus-5` (effort: max). The human reviewer is requested once Igor has reviewed; a human still merges.</sub> <!-- review sha=365bb32ce5fa8960fce9cdd01a52e350700256b9 verdict=COMMENT ci=success -->
igor added spent time 2026-07-28 20:05:57 +00:00
9 minutes 10 seconds
joshtronic approved these changes 2026-07-28 22:51:08 +00:00
joshtronic deleted branch feat/439-needsyou-detection 2026-07-28 22:51:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Total time spent: 55 minutes 18 seconds
igor
55 minutes 18 seconds
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshtronic/igor!449
No description provided.