* fix(rank): retry postings that could not be fetched instead of expiring them
A posting /rank could not fetch (403, login wall, 404, timeout, empty
page) and one that is really closed both became `expired`, which is
terminal: `candidates` only selected `new`, so a transient failure on
ranking day removed a live job from the pipeline and the report
presented it as closed.
Shape per the maintainer's decisions on #534:
- Scoring agents return a third status, `unreachable` (with a short
`fetch_error`), after exhausting the 09-web-research.md escalation
order; `expired` stays for positive evidence of closure (closed page,
410, redirect to a listing page, passed deadline). A 404 is
`unreachable` first (#538 is that case).
- `candidates` selects `unreachable` alongside `new`. `apply` increments
`fetch_attempts`, keeps `first_unreachable` (UTC timestamp) and stores
`last_fetch_error` (short reason, no page text). After 3 attempts with
at least 48 hours between the first and the last it writes `expired`
with `expired_reason: "unreachable"` and returns the job once under
`gave_up`, which Step 5 prints under "Gave up, check manually" with
its URL. A later successful fetch, an explicit `expired` or a passed
deadline clears the retry fields. `--all` still revives everything.
- rank_state.py owns the status set (`AGENT_STATUSES`); `apply` rejects
any other status instead of reporting a misleading missing score, and
test_rank_command.py asserts rank.md's JSON contract names the same
set. A new `--now` option makes the 48-hour floor testable.
- rank.md's step text (Step 2 rules and contract, rule 6's --all note,
Step 4 fields, Step 5 report, Important Rule 1) updated together.
CHANGELOG under Changed: no existing entry carries the new status, so
no migration.
Reported by @ai-tvang in #534, who also proposed the time floor and the
give-up listing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(rank): expire an unreachable posting whose deadline has passed
Review on #564:
- The unreachable branch in apply returned before the deadline check the
scored path runs, and candidates hands out `new` entries whose stored
deadline has passed. So a posting that closed on 2026-09-20 and
returned 403 on 2026-10-05 was retried, then listed under "Gave up,
check manually" as possibly live. A passed stored deadline is now
checked first and takes the expired path, dropping the retry fields,
as on master and under rule 5. New test pins it.
- 09-web-research.md step 5 said that in /rank an unretrievable posting
is marked `expired`, so an agent following it could write a 403 off
and bring #534 back. It now says `unreachable` (framework_version
1.1.2 -> 1.1.3); the CHANGELOG entry says so.
- The report shows `attempt N` instead of `N/3`, which read 4/3 while a
job waited out the 48-hour floor, and job-scraper's dedup note names
`unreachable` alongside `ranked`/`expired`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>