Skip to content

The rest of the Ready issue rule, and saying why an issue was passed over - #296

Merged
JacobStephens2 merged 3 commits into
issue-212from
issue-283
Oct 1, 2026
Merged

JacobStephens2 merged 3 commits into
issue-212from
issue-283

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

The rest of the Ready issue rule, and one line on stderr for each ready-for-agent issue a Pickup run passed over.

 for each open issue labelled ready-for-agent, lowest number first
   if it has an Unready Ticket label      -> "#18 labelled needs-info"
   if it carries a Claim                  -> "#19 labelled in-progress"
+  read its parent, blockers and timeline (one GraphQL query)
+  if it is a sub-issue                   -> "#21 is a Ticket of #20, which is not ready"
+                                            (no line when its Spec is a Ready issue)
+  if it is labelled base-fix             -> "#26 labelled base-fix"
+  if it has an open blocker              -> "#30 blocked by #29"
   if it was started                      -> "#31 already started: issue-31 is on origin"
+  if it was shaped under 10 minutes ago  -> "#34 not settled: labelled ready-for-agent less than 10 minutes ago"
   else take it: "taking Ready issue #22 ..."
 none taken: "no Ready issue on <owner>/<repo>"

Shaped means the latest of ready-for-agent being applied, a sub-issue added or removed, and a "blocked by" link added or removed, read from the issue's timeline.

src/
├── pickup.rs    # the search: where each candidate stands, and its line
├── github.rs    # candidate(): parent, sub-issue count, open blockers, last shaped
├── branch.rs    # started() now says what shows it: the Issue branch or the pull request
├── base_fix.rs  # is_issue(): the base-fix label's one home
└── main.rs      # help states the whole rule
tests/
├── pickup_run.rs   # end-to-end cases for each reason and its line
├── fakes/gh.rs     # the candidate query, an issue's parent, `gh fake timeline`
└── support/mod.rs  # issue_blocked_by, issue_timeline (minutes ago, relative to now)

Seam under test: the thirdshift binary run as a scenario against the fake gh and the fake claude. No new seam.

Whether an issue is a Spec now comes from the same query, so the separate Tickets query before the dispatch is gone.

Closes #283

Evidence

  • Before: an_issue_that_also_has_a_label_that_makes_an_unready_ticket_is_not_taken, asked for the line, failed:

    left:  "thirdshift: no Ready issue on acme/widgets\n"
    right: "thirdshift: #7 labelled ready-for-human\nthirdshift: no Ready issue on acme/widgets\n"
    

    After: cargo test --test pickup_run: 41 passed, 0 failed. The new cases:

    a_ready_for_agent_sub_issue_is_never_run_on_its_own_whatever_its_specs_labels
    a_ticket_numbered_below_its_spec_names_the_spec_that_is_not_ready
    a_spec_that_is_a_ready_issue_is_taken_and_its_tickets_get_no_lines
    an_issue_labelled_base_fix_is_not_taken
    an_issue_with_an_open_blocker_is_not_taken_and_with_that_blocker_closed_it_is
    every_open_blocker_is_named_in_the_line
    an_issue_labelled_ready_for_agent_less_than_ten_minutes_ago_is_not_taken
    an_issue_labelled_ready_for_agent_more_than_ten_minutes_ago_is_taken
    a_spec_labelled_long_ago_whose_sub_issues_or_blockers_changed_less_than_ten_minutes_ago_is_not_taken
    an_issue_passed_over_for_being_unsettled_is_taken_by_a_later_pass_once_it_has_settled
    each_passed_over_issue_has_one_line_with_the_first_reason_that_applies
    when_a_later_issue_is_ready_the_earlier_ones_lines_are_printed_and_it_is_taken
    
  • Full cargo test: every suite passes. cargo fmt --check and cargo clippy --all-targets -- -D warnings are clean.

  • The candidate query was run read-only against this repository on real GitHub (Pickup runs: thirdshift takes ready-for-agent issues on its own #212, The rest of the Ready issue rule, and saying why an issue was passed over #283). It returned the parent, the blockers with their states, the LabeledEvents and the last sub-issue or "blocked by" event, in the shape the code reads. A label given when an issue is created does produce a LabeledEvent.

Unaddressed findings

Standards

  • The open-blockers loop appears in both tickets() and candidate(). Skipped: the two read different things (every blocker and the open ones, against the open ones only), and each is four lines.
  • "candidate" names both the listed issues in pickup.rs and github::Candidate, bound as read; at is an index in Search and a timestamp closure in github::candidate. Skipped: I renamed stand to standing_of, but found no better name for the type than the one that says whose facts they are.
  • candidates and standings are parallel vectors tied by index, and first_ready returns (ListedIssue, bool). Skipped: both are private to Search, and a wrapper type would add more than it removes.
  • Search::line both words a line and works out another candidate's standing. Skipped: the look-ahead exists only to decide whether a Ticket gets a line, so it sits with the line.
  • github::candidate(issue, label) takes a label only one caller gives. Skipped: it keeps ready-for-agent out of github.rs, as open_issues_labelled(repo, label) does.

Spec

  • Tickets of a Claimed Spec get a line every pass. Once a Spec is in-progress, each of its Tickets still labelled ready-for-agent prints #N is a Ticket of #S, which is not ready on every pass. Skipped: it is what The rest of the Ready issue rule, and saying why an issue was passed over #283 asks for ("A ready-for-agent Ticket whose Spec is not a Ready issue gets a line naming the Spec"). Whether to quiet it is a question for the Spec, Pickup runs: thirdshift takes ready-for-agent issues on its own #212.
  • ready-for-agent being applied is read from the last 100 label events only. An issue whose label is older than its last 100 label events, or that has no such event, counts as settled. Skipped: that is the safe direction for an old label, and it keeps the search to one query per issue.

Merge Danger

Door: two-way

A revert restores the earlier search. Nothing is written to GitHub by the search itself.

Blast Radius: pickup

Only thirdshift pickup changes: it takes fewer issues than before (sub-issues, base-fix issues, blocked issues and issues shaped in the last ten minutes are now passed over), makes one more GraphQL query per ready-for-agent issue it looks at, and prints more lines on stderr. A released Claim puts ready-for-agent back, which is a new label event, so that issue waits ten minutes before a pass takes it again. branch::started and the label helpers changed shape, with the full suite passing.

🤖 Generated with Claude Code

JacobStephens2 and others added 3 commits October 1, 2026 19:11
…un passed over

A ready-for-agent issue is also not a Ready issue when it has a parent
issue, is labelled base-fix, has an open blocker, or is not settled: ten
minutes since ready-for-agent was applied and since a sub-issue or a
"blocked by" link was last added or removed, read from its timeline.
Each issue passed over gets one line on stderr with the first reason
that applies, before the line that says what the pass did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… whatever its case; the settle time in the line from its constant; the reason order tested through to not settled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…side one home for a label whatever its case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit 3c0e201 into issue-212 Oct 1, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-283 branch October 1, 2026 19:35
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