Skip to content

fix(watcher): read review summaries and skip bot-authored comments - #570

Draft
eyalgolan wants to merge 7 commits into
mainfrom
no-human/8fe972af-2
Draft

eyalgolan wants to merge 7 commits into
mainfrom
no-human/8fe972af-2

Conversation

@eyalgolan

@eyalgolan eyalgolan commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review passed (3 rounds) · Tests failed (13579 passed, 1 failed) · Merge policy not ready

Evidence

Check Result
Independent review ✅ PASSED — 3 rounds · proof
Verifiers ✅ 2 of 2 satisfied · proof
Tests ❌ FAIL — 13579 passed, 1 failed, 0 errors · proof
CI success
Merge policy ❌ not ready — 1 of 6 rules failed: tests_ran_and_passed · proof
2 findings raised and addressed across review rounds
  • diff grows two frozen-budget files, reddening structural gate — tests/test_structural_budget.py::test_no_frozen_entry_has_grown fails: 'config.py: frozen 3689,
  • pre-review test run — the harness's own pre-review test run was RED — failing: tests/test_funnel_eval.py::test_a_wedged_holdout_is_killed_by_process_group, test
Verifiers (2)
  • ✅ tests-assert-something — 3 files
  • ✅ no-unvalidated-status-write — 1 file
1 failing test
  • tests/test_lane_conformance.py::test_the_js_implementation_agrees_on_every_shared_case
Merge-ready policy (6 rules, source: default)
  • ✅ review_passed — review PASSED on head
  • ❌ tests_ran_and_passed — tests: 1 failed of 13580 run
  • ✅ tamper_guard_clear — tamper guard did not fire
  • ✅ repro_gate — repro gate pass
  • ✅ verifiers_all_satisfied — 2 verifiers, none failed
  • ✅ ci — ci: success

Acceptance criteria

  • A CHANGES_REQUESTED review whose body is the only feedback wakes the task with that body, and a COMMENTED review with a non-empty body wakes it too. An APPROVED review body does not.
  • A comment or review whose author carries user.type == 'Bot' is not treated as human feedback by default, regardless of its login suffix. A new documented config setting lets a user opt a named bot back in, and a test covers the opt-in.
  • A review body carrying the agent's own marker is filtered, and a review whose submitted_at is at or before the cursor is not fresh — reviews carry no created_at, so the freshness check must read submitted_at.
  • A review body plus two line comments from the same review produce exactly one resume and one revision-round increment, not three. The test asserts both counts.

Changes

Final Report

Context: This attempt resumed a task where (a) the branch merge with base was already completed and committed (b19ffb51), carrying forward the prior implementation of review-summary fetching, bot-comment classification, and the frozen-budget fix; and (b) a human/reviewer send-back identified one confirmed crash path in the new reviews block that needed a precisely-scoped fix.

Root cause: In fetch_github_pr_comments (src/no_human/vcs/pr_watcher.py), three separate loops used x.get("user", {}).get("login"). dict.get(key, default) only substitutes the default when the key is absent; GitHub sends "user": null for a review/comment whose author deleted their account — a present key with value None — so .get("user", {}) still returns None, and .get("login") on it raises AttributeError, uncaught, taking out the entire fetch (no line comments, no issue comments, no other reviews reach the caller).

Fix (all in src/no_human/vcs/pr_watcher.py, confirmed via git diff):

  • Line-comments block (/pulls/{n}/comments): user = c.get("user") or {}, then user.get("login"/"type").
  • Issue-comments block (/issues/{n}/comments): same pattern.
  • Reviews block (/pulls/{n}/reviews): same pattern — this is the block the send-back pointed at directly.
  • No try/except was added anywhere (as instructed) — the fetch still raises loudly on any other unexpected shape; only this specific null-user hazard is neutralized.
Rest of the coder's report (7 more paragraphs)

Test added: tests/test_pr_review_summaries.py::test_deleted_account_null_user_review_does_not_crash_the_fetch — uses a new fixture entry reviews.json[6] ("user": null, CHANGES_REQUESTED, non-empty body). Verified as a genuine positive control: reverting just the reviews-block fix reproduces AttributeError: 'NoneType' object has no attribute 'get' at the exact r.get("user", {}).get("login") line; with the fix restored, all 13 tests in the file pass, and the fetch returns the null-user review (author="unknown", author_type="") plus the other reviews/line-comments/issue-comments unaffected.

Non-blocking cleanup done: renamed test_review_has_no_created_at_field → test_review_fixture_pins_no_created_at_field with a docstring clarifying it only pins fixture shape and pointing at the actual behavioral test (test_review_at_or_before_the_cursor_is_not_fresh) for submitted_at coverage.

Evidence:

uv run pytest tests/test_pr_review_summaries.py -q -n 4
13 passed in 26.64s

uv run pytest tests/test_pr_review_summaries.py tests/test_pr_watcher.py tests/test_config.py \
  tests/test_structural_budget.py tests/test_blockers.py tests/test_wake_comment_conflict_precedence.py \
  tests/test_wake_conflict.py tests/test_wake_pr_closed_repair.py tests/test_wake_tick_does_not_stall_scheduler.py \
  tests/test_pr_watcher_cli_timeout.py -q -n 4
363 passed in 23.44s

Positive-control check (reverted fix → test fails with the predicted AttributeError at the exact line; restored fix → passes).

(a paragraph matched a filtered-phrase list and was removed)

Diff scope this session: src/no_human/vcs/pr_watcher.py, tests/fixtures/github_pr_reviews/reviews.json, tests/test_pr_review_summaries.py — nothing else touched; config.py/wake.py (frozen-budget files) untouched this session, matching the already-merged frozen pins (3697 / 2802 lines, both confirmed via wc -l and tests/test_structural_budget.py passing).

  • MET — A CHANGES_REQUESTED review whose body is the only feedback wakes the task with that body, and a COMMENTED review with a non-empty body wakes it too. An APPROVED review body does not. — evidence: src/no_human/vcs/pr_watcher.py:238-259 (state filter + body-filter + PrComment emission); tests/test_pr_review_summaries.py:72-96 (test_changes_requested_review_body_is_returned_as_feedback, test_commented_review_with_body_is_returned, `tes

(summary truncated at 4000 characters — full report: nh task show <task-id>)

⚠️ Unresolved: You've hit your weekly limit · resets 5am (Asia/Jerusalem) ('personal' subscription)

⚠️ 3 assumptions made on your behalf — verify at review
  • Q: What defines 'the agent's own marker' referenced in the acceptance criteria for filtering? Is it a string pattern typically found in review bodies, a configuration value, or something derived from the reviewing user/bot identity? A: A string pattern or identifier constant embedded in review/comment bodies that the agent includes when it authors its own feedback (e.g., a bot signature, footer marker, or unique string literal). This marker allows the system to recognize and filter out the agent's own reviews to prevent self-referential feedback loops. (assumption)
  • Q: Where should the bot opt-in configuration be stored (e.g., a specific config file path, environment variable name, or code constant), and what structure should it use (e.g., a list of bot login strings, a YAML/JSON dict, or regex patterns)? A: A configuration file (YAML or JSON format, stored in the project root or standard config directory) containing a list of bot login strings that should be treated as human feedback despite having user.type == 'Bot'. The structure should support a simple array or dict of login names for efficient lookup. Alternatively, as an environment variable if following that pattern elsewhere in the project. (assumption)
  • Q: Should the new bot opt-in configuration replace the existing ignore_comment_authors mechanism, extend it, or be implemented as a completely separate new setting? A: Implement as a separate new configuration setting alongside ignore_comment_authors rather than replacing or merging with it. They serve complementary but opposite purposes: ignore_comment_authors blocks feedback from specified users, while the bot opt-in explicitly allows certain bot-typed users. Keeping them separate preserves clarity and makes config intent explicit. (assumption)

How I verified this

Tests (9 runs, last shown) — cd /Users/eyalgolan/.<redacted>/worktrees/8fe972afa47745fb9da3387b878dd0c4.7034.0405e863 uv run pytest tests/test_pr_review_summaries.py tests/test_pr_watcher.py tests/test_config.py tests/test_structural_ [... 115 of 458 characters omitted from the middle ...] est_wake_pr_closed_repair.py tests/test_wake_tick_does_not_stall_scheduler.py tests/test_pr_watcher_cli_timeout.py -q -n 4 2>&1 | tail -15 · full log
warning: `VIRTUAL_ENV=/Users/eyalgolan/git/<redacted>-public/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
bringing up nodes...
bringing up nodes...

........................................................................ [ 19%]
........................................................................ [ 39%]
........................................................................ [ 59%]
........................................................................ [ 79%]
........................................................................ [ 99%]
...                                                                      [100%]
363 passed in 23.44s

9 commands recorded while working. Full verification log: ~/.no_human/artifacts/8fe972afa47745fb9da3387b878dd0c4/verification-attempt-5.md — nh logs 8fe972af; the same log, every command with its captured output, is posted as this PR's How I verified this comment when posting succeeds. Whether it passed is in the Evidence table above: no entry here asserts a pass or a fail.

UI evidence

Visual proof skipped: playwright not installed - run nh doctor --fix-walks to enable


Opened by no_human (attempt 5, no-human/8fe972af-2 → main). It never merges: review and merge this yourself, or run nh approve 8fe972af.

@eyalgolan

Copy link
Copy Markdown
Contributor Author

How I verified this

4 commands recorded - as recorded (shortened, folded onto one line), grouped by kind. No entry asserts a pass or a fail: read the output. Not necessarily everything the session ran.

test

  • uv run pytest tests/test_structural_budget.py -q 2>&1 | tail -40
warning: `VIRTUAL_ENV=/Users/eyalgolan/git/<redacted>-public/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
...................                                                      [100%]
19 passed in 2.44s
  • uv run pytest tests/test_funnel_eval.py::test_a_wedged_holdout_is_killed_by_process_group -q 2>&1 | tail -60
warning: `VIRTUAL_ENV=/Users/eyalgolan/git/<redacted>-public/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
.                                                                        [100%]
1 passed in 6.86s
  • uv run pytest tests/test_pr_review_summaries.py tests/test_pr_watcher.py tests/test_pr_ci_watch.py tests/test_config.py tests/test_comment_poster.py tests/test_reviewer.py tests/test_structural_budget.py tests/test_wake.py -q -n 4 2>&1 | tail -60
warning: `VIRTUAL_ENV=/Users/eyalgolan/git/<redacted>-public/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
bringing up nodes...
bringing up nodes...


no tests ran in 2.97s
  • uv run pytest tests/test_structural_budget.py::test_no_frozen_entry_has_grown -q 2>&1 | tail -10
warning: `VIRTUAL_ENV=/Users/eyalgolan/git/<redacted>-public/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
.                                                                        [100%]
1 passed in 1.37s

Not verified: everything below is a limit of this section, listed whether or not it bit this attempt.

  • no command recognised as e2e, http, typecheck, lint, build was recorded
  • an entry shows that a command LINE was submitted to the shell and what came back - never that the check recognised inside it RAN, and never that it was the RIGHT command: pytest -k test_nothing prints a clean run, and a recorded command line may name a check the shell never reached yet is still counted - TEN SHAPES WERE DRIVEN against bash 3.2.57 with the check replaced by a marker-printing stub and the marker was absent in every one: a failed &&, a taken ||, an exit, an exec, an exit inside a sourced script, a syntax error that aborts the REST of the line, a multi-line if false, a case that matches nothing, set -e aborting an earlier command, and set -u on an unset variable; that list is MEASURED, NOT EXHAUSTIVE, because this module is not bash, so a kind this section does NOT list as missing is a kind some recorded line named, which is not the same as a kind that ran
  • the text is the coder's: the session chose the command string and, through echo/printf, can choose the output too. Both are shown as inert text, and no entry ASSERTS a pass, a fail, or an exit status - pytest -q | tail -3 exits with tail's status, Error: Exit code 1 is a line IN THE OUTPUT, and where the harness reported a timeout or an interruption instead of output that report is appended to the captured text in square brackets. Read the output
  • recognition reads the command line ONLY - it never looks inside what a command runs, so bash -c 'uv run pytest -q' leaves no receipt at all while make test leaves one that names make and not the recipe it ran; and the other way, a check merely NAMED in a heredoc body, or in a quoted string that happens to spell a shell separator, can be recorded as though it ran
  • commands run inside a spawned subagent are deliberately excluded, so delegated work leaves no receipt here; a command the harness refused to run (blocked, or permission denied) leaves none, because it never ran; and only a command the HARNESS backgrounded leaves no receipt at all - it hands back a task id instead of output. A trailing & YOU wrote is NOT that and is NOT excluded: pytest -q & is recorded and headed test, and may still have been running when the harness returned
  • the COMMAND and the output are both redacted and bounded before they are stored - an excerpt is not the full log, a credential-shaped string may have been masked out of either, a command over 400 characters is shortened in the middle, each command is displayed on ONE line with its newlines folded to spaces (so it may not re-run as written), and invisible and direction-changing characters are stripped before display; look-alike letters are NOT detected
  • nothing here checks that these commands exercise the diff - no receipt is compared against the files this PR changes; no interactive UI check was performed (no_human never drives a browser at your change except testing/ui_evidence.py's walk, reported as its own evidence, not a receipt; the only other page it drives is a CI server's login form, and the board it opens without driving, so an e2e entry is the project's harness printing its result, not a human-style walkthrough); and no_human's own test run, CI, and the independent review are separate signals - this section covers only the coder session's own commands
  • at most 200 receipts are recorded per attempt; past that the observer stops recording, and this section says so above when the limit was reached

See the PR body's Evidence table for the orchestrator's own test run.

@eyalgolan

Copy link
Copy Markdown
Contributor Author

Independent review — PASSED (2 rounds) on ddb9159

A different model, fresh context, commit, push and merge refused at the tool call, told to refute "done". This is the checklist the gate decided on; no_human never merges — a human does.

Severity Finding Where Note
✅ funnel_eval red, unrelated to diff — Heads up that test_funnel_eval's process-group holdout test is red on this branch but green on base. Nothing in this diff goes near the eval harness or process-

@eyalgolan eyalgolan changed the title GitHub review summaries never reach the coder; bot comments count fix(watcher): read review summaries and skip bot-authored comments Sep 24, 2026
# Conflicts:
#	RELEASE_MANIFEST.txt
#	tests/test_structural_budget.py

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants