Skip to content

fix(loop): census_error surfacing + pr-rebase gh field/fail-closed fixes (issue #187) - #216

Open
robercano-ghbot wants to merge 2 commits into
mainfrom
feat/issue-187-census-gh-parse-robustness
Open

fix(loop): census_error surfacing + pr-rebase gh field/fail-closed fixes (issue #187)#216
robercano-ghbot wants to merge 2 commits into
mainfrom
feat/issue-187-census-gh-parse-robustness

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Fixes #187 (narrowed scope — see below).

What this fixes

Two defects let the autonomous loop fail silently:

  1. pr-rebase.sh requested an invalid gh pr list --json field. baseRefOid doesn't exist on gh pr list in gh 2.46.0 (GraphQL-only), so gh printed "Unknown JSON field" and exited non-zero on every invocation. loop-census.sh silently swallowed that, pinning rebase_prs=0 forever — the Loop: react to post-open PR events — remote CI failure fixing, review-comment convergence, conflict-after-sibling-merge #96-part-3 rebase feature was a permanent no-op. Fixed by fetching the base branch's tip SHA once via gh api repos/OWNER/REPO/commits/BASE (every PR here shares the same base, so this is equivalent to the per-PR field). A failed fetch now degrades fail-closed (checked exit 1, zero candidates) rather than fail-open with an empty base_sha — the old fail-open shape would have silently re-emitted every CONFLICTING PR as a fresh attempt=1 candidate on every tick of an outage, defeating the anti-livelock budget.

  2. Nothing distinguished "census failed" from "nothing to do." Census now emits a greppable census_error=<stage> line whenever a gh/parse step genuinely fails or degrades, for every stage (repo-derive, open_prs — both exit-code and value-validated, open_pr_branches, open_issue_set, feedback_prs, ci_fix_prs, comment_fix_prs, rebase_prs, planned). Previously-unguarded gh calls no longer abort the script mid-contract under set -euo pipefail. loop-tick.sh folds these into the tick record's reason field (yielding to the existing ceiling_reason when both fire — verified by a dedicated precedence regression test), and cockpit.sh renders reason in the loop-health panel. A healthy tick emits zero census_error lines and is byte-identical to before.

Scope note

The original issue's items 1–2 (stray milestone output, priority ordering on milestone-assigned issues) were already resolved by #174's milestone-scoping feature, which landed on main after this issue was filed — extensive milestone fixtures already exist in loop-census.test.sh. This PR covers the issue's remaining items (3: never die into a silent action=none; 4: daemon-path validation) plus the pr-rebase.sh bug found while implementing item 3.

Validation

  • GATES_FILE=self/gates.json bash .claude/scripts/gate.sh build
  • GATES_FILE=self/gates.json bash .claude/scripts/gate.sh lint
  • self/smoke-fanout.sh (end-to-end fan-out scaffold) ✅
  • Every touched *.test.sh run individually — all pass: loop-census.test.sh (147), pr-rebase.test.sh (17), loop-ceilings.test.sh (53), loop-daemon.test.sh (113), cockpit.test.sh (131), loop-tick.test.sh (95).
  • Note: the aggregate gate.sh test (which loops over every *.test.sh sequentially) hangs in this sandboxed environment when it reaches loop-daemon.test.sh — confirmed to reproduce identically on unmodified main, i.e. a pre-existing environmental flake (resource contention under the full-suite sequential run), not something this PR introduces. Gates were verified by running build/lint/smoke-fanout plus every touched test file standalone instead (each passes cleanly in isolation).
  • Two independent reviewer lenses (correctness, tests) both APPROVE — see PR discussion.

Daemon-path verification (issue's item 4)

Not independently re-verified end-to-end in this session (the daemon's own re-attach/reachability path is exactly what the sandboxed-hang note above is about — see caveat). The census_error contract itself is covered by the per-stage fixtures above; a live tick's dispatch behavior should be spot-checked via the cockpit after merge.

🤖 Generated with Claude Code

robercano and others added 2 commits August 4, 2026 14:03
Two defects let the autonomous loop fail silently:

1. pr-rebase.sh requested `baseRefOid` in a `gh pr list --json` field
   list. That field does not exist on `gh pr list` in gh 2.46.0 (it is a
   GraphQL PullRequest field, surfaced only via `gh pr view`), so gh
   printed "Unknown JSON field" and exited non-zero on EVERY invocation.
   loop-census.sh swallowed that via `| grep -c . || true`, pinning
   rebase_prs=0 forever: the whole #96-part-3 rebase feature was a
   permanent no-op, plus stderr garbage on every tick. Every PR in this
   query shares `--base "$base"`, so the PR-level baseRefOid GitHub would
   report is by definition that base branch's current tip; fetch it once
   via `gh api repos/OWNER/REPO/commits/BASE --jq .sha` instead.

2. Nothing distinguished "census failed" from "nothing to do". Census
   now emits a greppable `census_error=<stage>` stdout contract line
   whenever a gh/parse step genuinely fails or degrades, previously
   unguarded gh calls no longer abort the script mid-contract under
   `set -euo pipefail`, loop-tick.sh folds the stages into the tick
   record's `reason` field, and cockpit.sh renders `reason` in the
   loop-health panel. A healthy tick emits zero census_error lines and
   is byte-identical to before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ion (issue #187)

Follow-up hardening on top of 83dec48's census_error surfacing, addressing
review-lens findings:

- pr-rebase.sh: a failed base-branch-tip fetch now degrades FAIL-CLOSED
  (checked exit 1, no candidates emitted) instead of fail-open with an
  empty base_sha — the old behavior would silently re-emit every
  CONFLICTING PR as a fresh attempt=1 candidate on every tick of a fetch
  outage, defeating the anti-livelock budget. loop-census.sh's existing
  exit-code guard now surfaces census_error=rebase_prs for this case.

- loop-census.sh: open_prs is now value-validated, not just exit-code
  checked — a gh SUCCESS with empty/non-numeric stdout (malformed --jq
  result) previously sailed past the guard and crashed the later
  `-eq 0` comparison with a bash "integer expression expected" error
  instead of a clean census_error.

- pr-rebase.test.sh's gh stub now validates the --json field list against
  gh 2.46.0's real allowlist, so a regression that reintroduces the
  invalid `baseRefOid` field fails the suite instead of silently passing.

- loop-daemon.test.sh: run_daemon_once_stripped_path now uses the same
  curated PATH as every other helper (previously a bare /usr/bin:/bin,
  which can resolve the host's real systemd-run/systemctl and hang when
  this suite runs from inside an active pr-loop-driver-* unit).

- Test coverage: per-stage census_error fixtures for every remaining
  census stage (repo-derive, open_pr_branches, open_issue_set,
  feedback_prs, ci_fix_prs, comment_fix_prs, planned), the open_prs
  non-numeric-success case, and a ceiling-vs-census_error precedence
  regression guard (ceiling_reason always wins).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@robercano-ghbot robercano-ghbot added the needs-human Loop escalated: attempt budget exhausted, manual triage required label Aug 4, 2026
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

fix(loop): census_error surfacing + pr-rebase gh field/fail-closed fixes (issue #187) (not yet reviewed)

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

Labels

needs-human Loop escalated: attempt budget exhausted, manual triage required

Projects

None yet

2 participants