All checks were successful
deploy / deploy (push) Successful in 6m1s
Closes #3 and #4. Both specs had gaps that only appeared on contact, which is worth recording because it is evidence for the plan-quality question in #10: - `enqueue(&[DiscoveredIssue])` cannot know a JobKind. It is derivable from labels, so the store now carries the LabelProtocol and core gained `job_kind_for`. The precedence when an operator applies several mode labels had to be decided: plan beats implement, because planning produces the implementation children and so loses nothing, while the reverse silently discards the decomposition that was also asked for. - `claim_next(worker, allowed_lanes)` had no lane to filter on. Migration 0002 adds one, recorded at enqueue by `routing::lane_for` — the same function `route` now delegates to, so the two cannot drift. Deriving it at claim time instead would mean a forge request per claim, since labels are not stored. - `list_opted_in_issues` returned a bare Vec with nowhere to put the ETag that #4's own step 3 requires, so a caller could not make the next poll conditional. It returns an `IssuePage` now, which also carries `not_modified` — a 304 is not an empty repo, and a poller that conflated them would treat every quiet poll as every issue having disappeared. The claim is one statement: a CTE takes the row lock with SKIP LOCKED and the update writes the claim, so select and update share a transaction without managing one by hand. Returning a job to pending clears the claim, because `pending_holds_no_claim` refuses the half-done version — the database is what makes lease expiry safe rather than the code remembering to. Enum values round-trip through serde rather than a hand-written match, so the schema's check constraints and the Rust types are provably one vocabulary. Also replaced a test from #2 that asserted exactly one migration exists. It failed on the first legitimate migration, which teaches people to edit the assertion rather than think. It now asserts what it was reaching for: versions unique and ascending. Verified against Postgres 18 — 15 database tests covering enqueue idempotency across repeated polls, lane filtering including the agent:oc override, claim metadata, lease expiry and reclaim, terminal jobs refusing transition, renewal requiring you still hold the claim, and re-routing a queued job when its labels change. Plus 12 mock-forge tests: If-None-Match sent and 304 distinguished from empty, ETag surfaced, 429 retried but bounded, 503 retried then succeeding, 404 not retried, pull requests filtered out, and every unimplemented write failing without touching the forge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6