Skip to content

thirdshift pickup: take the lowest-numbered Ready issue and run it - #293

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

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

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

thirdshift pickup starts a Pickup run: one pass, with no Issue URL, that takes the lowest-numbered Ready issue and dispatches it as thirdshift <Issue URL> would.

pickup [merge|no-merge] [base-fix|no-base-fix] [parallel <n>] [email [<address>]|no-email] [base <branch>]
  User config
  launch::start(base)                    # shared with an Architect run
    origin is a GitHub repository, git identity, Base branch on origin and not behind the local copy
    try the per-repository lock          -> skipped: "an Architect run or a Pickup run is already running on <owner>/<repo>"
  list open issues labelled ready-for-agent, lowest number first
    take the first with no Unready Ticket label, no Claim (in-progress),
    and never started (no Issue branch on origin, no pull request from one, in any state)
                                         -> skipped: "no Ready issue on <owner>/<repo>"
  "taking Ready issue #<n> "<title>", as thirdshift <Issue URL> would"
  dispatch: a Spec run if it has sub-issues, a Run otherwise, on the Pickup run's Base branch
    parallel <n> dropped when it is not a Spec
    that run makes the Claim, and its ending is the Pickup run's

A skipped pass exits 0, with stdout empty and that one line on stderr.

 src/
+├── launch.rs      # how a run with no Issue URL starts: its repository, Base branch, checks and lock (moved out of architect.rs)
+├── pickup.rs      # the search for the Ready issue, and why a pass is skipped
 ├── architect.rs   # starts through launch::start; its skip reason now covers both kinds of run
 ├── args.rs        # Command::Pickup, parse_pickup
 ├── branch.rs      # started(): was an issue ever started, off the same reads as Issue branch selection
 ├── claim.rs       # is_on(): does an issue carry a Claim
 ├── main.rs        # pickup(), and one dispatched() for an Architect run and a Pickup run; help
 └── run.rs         # StartedBy::ArchitectRun -> StartedBy::Dispatch, for both

Two things beyond the letter of the Ticket, both needed for "exactly as thirdshift <Issue URL> would":

  • After an Inherited failure, the command offered is thirdshift <Issue URL> … base-fix, without parallel for an issue that is not a Spec, since that command would fail by hand.
  • Until One Run notification for a Pickup run that took an issue #285, the dispatched run sends its Run notification as it would by hand, its checks made once the Ready issue is found and before the Claim. A skipped pass sends none.

Two changes reach an Architect run: its skip reason is reworded to cover both, and its detached-HEAD message is now the shared one, HEAD is detached; check out the branch the work should be based on, or name it with base <branch>. An Unready Ticket label now counts whatever its case, for a Spec run's Tickets and an Architect plan too.

Seams tested: the thirdshift binary in a scenario against the fake gh and claude (tests/pickup_run.rs, new; tests/command_surface.rs; tests/architect_run.rs), and args::parse. The fake gh needed nothing new: it already listed issues by label.

Evidence

  • Before: thirdshift pickup was an argument error (not a GitHub issue URL: pickup), and the first case failed:

    the_lower_numbered_of_two_ready_issues_is_run_and_the_other_left_untouched ... FAILED
    no PR from issue-7: []
    

    After: cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test all pass, the last run once at the end:

    tests/pickup_run.rs       30 passed
    tests/architect_run.rs    74 passed
    tests/command_surface.rs  27 passed
    unit tests               108 passed
    every other suite         passed, 0 failed
    

    What the Pickup run cases assert, each by exit code, stdout, stderr and the labels on the fake GitHub:

    two Ready issues #7 and #9        -> #7 run, PR URL on stdout, #7 in-progress, #9 still ready-for-agent
    sub-issues                        -> Spec run, Spec PR ready; no sub-issues -> Run
    no ready-for-agent / closed       -> skipped, "no Ready issue on acme/widgets", no session
    + ready-for-human, needs-info, wontfix, needs-triage, Needs-Info, in-progress -> not taken
    issue-7 or issue-7-branch-2 on origin; PR from issue-7 OPEN, MERGED or CLOSED -> not taken (#70 is still taken)
    Architect run mid-review          -> pickup skipped, no gh call made
    Pickup run mid-dispatch           -> pickup and architect both skipped with the shared line
    merge, no-merge, base-fix, no-base-fix, merge.always, base.fix, spec.parallel -> reach the dispatched run
    parallel 1 on a Spec              -> Tickets one at a time; parallel 2 on a non-Spec -> ignored, exit 0
    base develop from main or a detached HEAD -> PR targets develop, branch is off develop
    detached HEAD, local base ahead, base not on origin, no git identity, non-GitHub origin -> exit 1, no gh call, labels unchanged
    focus, --plan-only, an Issue URL, repeated or contradictory flags -> exit 2 with the help
    

Unaddressed findings

Standards

  • "Is it a Spec" is decided twice (pickup.rs reads the sub-issues for Taken.is_spec, then run.rs reads them again). Skipped: the command a Base fix is offered with is built before the dispatched run starts, and must already leave parallel out for an issue that is not a Spec. It costs one extra query a pass.
  • Test helpers copied from tests/architect_run.rs (agent_opens_pr, pr_from, assert_ended_with_pr, the Resend constants). Skipped: one copy per test file is the repository's idiom, and moving them to tests/support is a change across six files that is not this Ticket's.

Spec

  • Thin coverage of the User config's defaults: no Pickup run case sets launch.pull or logs.dir, and the Base fix cases dispatch a Run, not a Spec run. Skipped: both kinds of run go through the one dispatched() in main.rs, and the Architect run's cases cover those. A spec.parallel case was added.
  • Scope beyond the Ticket: the retry command after an Inherited failure, and tests that pin the interim Run notification. Kept: the first is what makes the offered command work by hand, and the second is what "behaves as it would by hand" means until One Run notification for a Pickup run that took an issue #285, which will rewrite those cases.

Merge Danger

Door: two-way

A new command and an internal move. Reverting removes pickup and puts the lock back in architect.rs. The lock file keeps its path, ~/.thirdshift/architect-locks/, so a thirdshift from before and one from after still exclude each other.

Blast Radius: contained

Nothing runs a Pickup run unless someone types thirdshift pickup. An Architect run's two reworded lines could break a script that matches its stderr text. An issue labelled with an Unready Ticket label in another case, such as Needs-Info, is now left alone by a Spec run where it was run before.

Closes #282

🤖 Generated with Claude Code

JacobStephens2 and others added 3 commits October 1, 2026 18:30
… and runs it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow a run with no Issue URL starts as its own type, Unready labels whatever their case, the glossary's Base branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit 56bf1f2 into issue-212 Oct 1, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-282 branch October 1, 2026 18:49
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