Changes touching this path

  • the forge learns runners and jobs (#2157, ADR 0091, map #2099, first of three slices of #2129): migration 0018 adds the runner and job tables, both repo-scoped, so both join 0014 row-security set and take 0017 forge_is_bound_to_one_repo policy verbatim in shape, and tests/rls.rs ENABLED_TABLES names them, which is the set equality that forces the decision rather than a list nobody maintains. A runner is its OWN keypair under NO account and that is the security property expressed as schema: require_pusher and the proposal door both resolve a signer through account_key, so a runner key is refused at push and propose BY ABSENCE, where the same key on the owner account would have been a full pusher since require_pusher admits any live key of the account; the row is written from an envelope the namespace owner signed, which is a proof needing no browser session, so tests/account_tier.rs is green UNAMENDED. A job identity is the triple version, trigger, kind, so creation is idempotent by the primary key; member and approved are computed in at creation so a later membership change cannot re-authorize queued work; a claim is claimed_by plus a lease the CLAIMANT names, exclusive by FOR UPDATE SKIP LOCKED in the driver and one lock across find-and-write in the reference store; an expired lease returns the job by a READ-TIME predicate, so there is no reaper to fall behind; a verdict is accepted whatever the lease says, because the runner did the work and a bookkeeping deadline must not discard it; a job is born unclaimed and unfinished and a creation carrying either is refused, since those are the two writes that have to be exclusive. Trigger and Kind move from loot-cli to loot_net::pipeline, the one crate the forge and the CLI both already depend on, re-exported so every path reads unchanged: the forge stores what the CLI writes, and two copies of one vocabulary would be a list to keep in step. Capabilities is exactly Kind as a set, and a bit this build cannot name is REFUSED rather than dropped, because reading a newer runner row as covering less would hand it jobs it cannot do. Five conformance cases join the roll call, so both stores answer one contract and neither can name a subset. Measured: bash ci/local.sh green end to end against Postgres 18, 129 test binaries, 0 failed and 0 SKIPPED, which is what proves the driver, the migration and both policies rather than only the reference store; the claim exclusivity and the lease expiry are proved there on real Postgres, and an EXPLAIN plan under contention is NOT measured here and is owed to #2158 where the poll load is. Three refusals found by the run rather than by thought: the destination census flagged a fixture wake-up URL, allowlisted with the reason that it is a value the store round-trips and never an address anything dials, which took the allowlist to ten and moved ADR 0074 spelled count; the shipped-predicate pin read migration 17 alone, so it now reads the migration that CREATED each table rather than recording which came from where; and the binding fixture seeds a runner and a job row per repo, without which its controls were zero and the refusal below them would have passed for the wrong reason. Five pins each broken once by a named mutation, red at 1 failed then green at 0 failed with the count above zero: the claimable predicate widened, the lease made to hold forever, retirement re-dated, the born-claim refusal disabled, and an unknown capability bit accepted. No routes, no wire and no verb here; those are #2158 and #2159. No FORMAT_MAJOR move (#2157) ea4d34a5 · dbf3dbe6…

Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.