Skip to content

feat: squabble board (estate needs-me board) and squabble inbox-sweep - #123

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/board-and-inbox-sweep
Oct 1, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
feat/board-and-inbox-sweep

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

What

Two new subcommands, so the owner sees only what needs them: no paging through hundreds of merged-PR notifications, and no hunting for the few PRs that are really blocked.

squabble board

  • Enumerates every non-archived repo of each owner. Defaults are hyperpolymath and metadatastician.
  • Reads the merge gate of each repo with open PRs, taking the union of rulesets and classic branch protection, since Ref.rules omits classic protection. It also reads every open PR.
  • Places each PR in exactly one bucket: Needs you · Agent work · Landing by itself · Stale — close? (pure classifier in squabble-core::board).
  • Renders Markdown under the 65536-byte issue-body limit. --publish owner/repo#N rewrites that issue's body: it never comments, so it sends no notifications. It then checks that the stored body equals the one sent, because rc=0 is not evidence.
  • Recovers from batch failures: a whole-batch failure (HTTP 502, or "no data") is split in half recursively, down to one repo with one retry. Listing pages retry transport failures. Anything still unreadable is named at the top of the board, and the command exits 6.

squabble inbox-sweep

  • Lists every thread still in the inbox (GET /notifications?all=true, paginated). It resolves PR and issue state in GraphQL batches.
  • Clears only threads whose subject is merged or closed: it unsubscribes, then marks the thread done. Unknown state, unread state and non-PR/issue subjects are kept.
  • Dry run by default. --apply writes, stops at a REST --reserve read from the response header, and names every due thread it left (as a set, not a count). Exits 6 if any were left.
  • Needs a user token with the notifications scope; an App token cannot read notifications.

Evidence

  • just quality: exit 0, 302 tests passed, 0 failed (fmt-check, clippy -D warnings, test).
  • New tests:
    • Generated GraphQL is schema-validated against the vendored schema, with a negative control for each query.
    • Planted CLEAN, armed and no-gate PRs each land in the right bucket.
    • A heavy repo no longer sinks its batch. Mutant check: disabling the split makes one_heavy_repo_does_not_sink_its_batch fail.
    • The sweep unsubscribes before marking done, a failed unsubscribe keeps the thread, and the reserve stop names the tail.
  • Live board, 2026-10-01T14:2xZ:
    • Coverage: 398 + 50 repos, 458 + 54 open PRs, all placed, 0 unreadable (the run before the split/retry fix had 20 unreadable).
    • Buckets: Needs you 139 (126 of them "no required check on the default branch") · Agent work 371 · Landing 2 · Stale 0.
    • Counts reconcile with gh search prs: 458 = 458, and 55 = 54 + 1 PR in an archived repo, which the board excludes by design.

Not in this PR

  • The hourly systemd user timer that runs inbox-sweep --apply, then board --publish. It lives in the owner's developer/scripts, outside this repo.
  • Auto-merge arming and the layered ruleset canon, which come in later increments. Repos with no required check are listed on the board, not armed: arming there would merge at once.

🤖 Generated with Claude Code

https://claude.ai/code/session_0196aKTfvS6vPQVp2LPoXbjP

`board` enumerates every non-archived repo of each owner, reads the
merge gate (rulesets ∪ classic protection) and open PRs of repos that
have any, and places each PR in one bucket: needs you / agent work /
landing by itself / stale. Renders Markdown under the 65536-byte issue
body limit and, with --publish owner/repo#N, rewrites that issue's body
(never comments) and checks the stored body equals the one sent.
A whole-batch failure (502, no data) is split and retried down to one
repo; listing pages retry transport failures. Exit 6 = incomplete, with
the unreadable repos named at the top of the board.

`inbox-sweep` lists every notification thread still in the inbox,
resolves PR/issue state in GraphQL batches, and clears (unsubscribe,
then mark done) only threads whose subject is merged or closed. Dry run
by default; --apply writes, stops at a REST reserve, and names every
due thread it left. Unknown state and non-PR/issue subjects are kept.

Live board 2026-10-01: 398+50 repos, 458+54 open PRs, all placed,
0 unreadable; counts reconcile with `gh search prs` (the one remaining
difference is a PR in an archived repo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aKTfvS6vPQVp2LPoXbjP
@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 1, 2026 14:25
@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 75119691-9698-4447-af6d-9d382ba6ae5e

📥 Commits

Reviewing files that changed from the base of the PR and between bf00c8e and e1caeaf.

📒 Files selected for processing (11)
  • crates/squabble-cli/src/board.rs
  • crates/squabble-cli/src/inbox.rs
  • crates/squabble-cli/src/main.rs
  • crates/squabble-core/src/board.rs
  • crates/squabble-core/src/inbox.rs
  • crates/squabble-core/src/lib.rs
  • crates/squabble-forge/graphql/board_repo.graphql
  • crates/squabble-forge/graphql/estate_repos.graphql
  • crates/squabble-forge/src/board.rs
  • crates/squabble-forge/src/inbox.rs
  • crates/squabble-forge/src/lib.rs
 ________________________________________________________
< Code review is a dish best served cold. Like a carrot. >
 --------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath merged commit 2f9434b into main Oct 1, 2026
49 of 50 checks passed
@hyperpolymath
hyperpolymath deleted the feat/board-and-inbox-sweep branch October 1, 2026 14:26
hyperpolymath added a commit that referenced this pull request Oct 1, 2026
Follow-up to #123.

## Why
`GET /notifications?all=true` also lists threads that are already
**done**, and the REST notification object has no done field. I measured
this live on `hyperpolymath/HOL`:
- `inbox-sweep --apply --repo hyperpolymath/HOL` cleared 4 of 4 due
threads, and both DELETEs returned 204.
- PR #7's subscription went `true` → 404, while open PR #11 stayed
subscribed.
- The **web inbox** for that repo then showed only #11 (the positive
control). The other four were gone.
- But `all=true` still listed all five threads unchanged three minutes
later.

Without memory, every hourly run would therefore re-clear the same ~640
threads: about 1300 REST writes against a 5000/h budget that other
sessions share.

## What
`--state <path>` stores `{thread id: updated_at}`:
- An entry is recorded only after both DELETEs return 204. The file is
written atomically (temp file + rename).
- A thread whose `updated_at` is unchanged is skipped before any GraphQL
read or REST write.
- New activity, which GitHub uses to bring a done thread back to the
inbox, makes it eligible again.
- Entries for threads that are no longer listed are pruned, so the file
stays bounded.
- A missing file means empty state. A corrupt file exits 2: an empty
state would silently re-clear everything.

## Evidence
- `just quality`: exit 0, 310 passed, 0 failed.
- New tests: a second sweep using the saved state performs **zero**
writes; a bumped `updated_at` is re-cleared; stale entries are pruned;
the state file round-trips and a corrupt one is refused.
- Mutant: replacing the skip predicate with `false` fails 2 of the new
tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0196aKTfvS6vPQVp2LPoXbjP

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Oct 1, 2026
Resolve crates/squabble-cli/src/main.rs as a union: main added
`board`/`inbox-sweep` (exit 6, #123/#124) where this branch added
`verify-satisfied` (exit 5, #119). Both modules, dispatch arms, usage
lines and exit-code docs are kept; codes listed in numeric order.

cargo fmt --check, clippy -D warnings and cargo test --workspace (279) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5CoaksP2Bg21HpDCgFgwS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant