feat: replace claim leases with advisory reservations (v3) - #40
Merged
Conversation
added 30 commits
August 13, 2026 19:45
Physical claim-core cutover (retirement plan step 1). The credential-bearing claim runtime had no live callers left: every public entry point was retired in earlier commits, leaving db.py/pg.py wrappers that only called each other. - Delete sprintctl/claimcore.py. - Remove the claim wrapper blocks, adapters, and imports from db.py and pg.py (create/heartbeat/release/handoff/list/find, proof checks, purge_expired_claims, and the repo arbitration clock). - Remove the claim row serializers and identity-status constants from rows.py. The live `claim` relation, `claim_history`, and the export/import/recovery table lists are untouched: they are archive evidence removed by the schema cutover (step 2), not by this change. Tests: claim-runtime coverage is replaced rather than dropped where the behaviour survives -- stale-reservation sweep, reservation takeover with a rejected stale touch, and claim-handoff payload canonicalization as archive-only evidence. A conftest `seed_legacy_claim` helper seeds legacy rows by SQL for the archive/export/migration tests that still need them. 1216 passed, 156 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retiring the claim runtime dropped `lock_capability_arbitration()` and the active-capability check that `create_claim` performed. Maintenance activation still gates on "zero active reservations", but nothing gated the other direction, so the window was only half protected: - On PostgreSQL nothing serialized the two paths at all. An `activate` that counted zero reservations and a concurrent `reserve()` could both commit, leaving a live reservation under an active capability. - On both backends `reserve()` succeeded while a capability was already active, which the retired claim path rejected outright. `reserve()` now takes the same repo-scoped `pg_advisory_xact_lock` the claim path held (SQLite relies on BEGIN IMMEDIATE's whole-database lock, as before) and rejects admission with `ReservationConflict` while an active or observing capability is unexpired. The two PostgreSQL arbitration tests are converted from claims to reservations rather than dropped, and a SQLite test covers the newly restored direction of the gate. 1217 passed, 156 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the application/sync half of the claim-core cutover. The proof verification these credentials fed (`authority._resolve_credential` / `_verify_claim_secret`) was removed with the claim authority helpers, leaving a resolver that revealed secrets into a parameter nothing read. Removed: - `credentials` threading through `arbitrate_command` -> `_apply_command` -> `_handle_item`; none of them read it. - `WorkApplication.credential_resolver` and `_credentials`, and the `CommandArbiter` slot they filled. - `make_transient_credential_resolver`, `CredentialResolver`, `TransientCredentialCarrier`, `_CLAIM_CREDENTIAL_REF_FIELDS`, and `InvocationContext.transient_credentials`. `sync.synchronize_outbox` keeps its `credential_resolver` parameter: it is not dead. Declining a record still leaves that command and every later one pending (covered by tests/pg/test_authority.py), so it stays as an upload-readiness gate, now documented as one -- the mapping it returns is no longer consumed. The `invocation/v2` wire field stays accepted-and-ignored on the Vuoro side; removing it is a published-protocol change, not part of this cutover. 1213 passed, 156 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_credential_ref, _positive_ttl, and _canonical_claim_metadata validated payload fields that only claim command types carried. No record type in SPRINTCTL_RECORD_TYPE_CLASSES reaches them any more, and _strict_fields rejects unknown payload keys, so no future payload can either. _SECRET_FIELD_NAMES keeps "claim_token": _reject_secret_material is a defensive name-based guard, not a claim contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sidecars under .sprintctl/authority-credentials/ retained transient claim proofs so a lost response could be retried. Nothing has written one since claim arbitration was retired: store_pending_authority_credential had no production callers, so every load returned None. That made the served sync gate unreachable rather than merely unused -- it stopped at the first command whose payload named a *credential_ref with no matching sidecar, and no authority command payload contract admits such a field. pending_command_event_ids is kept in the JSON and text output for shape parity with synchronize_outbox's report, now always empty, with a comment saying why. Also drops the dead transient_credentials parameters from served batch_apply and lifecycle_arbitrate; no caller passed either. _canonical_event_id and _require_private survive as guards on the terminal-decision receipts, so their messages no longer say "credential", and a new test covers the 0700/0600 modes, receipt validation, and event_id traversal rejection on that path -- those properties were only asserted through the removed credential tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…docs _merge_runtime_exports filtered out "claim", "claim_*", and "_claim_*" names so retired claim helpers would not leak into the shared CLI namespace. No command module exports such a name any more (verified by walking sprintctl.commands), so the filter excluded nothing. served.py's item_note docstring cross-referenced :func:`claim_start`, which no longer exists, and lifecycle_arbitrate described claim arbitration as "not-yet-wired" rather than retired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three assertions in test_schema.py hardcoded the applied-migration list as [2..7] and the resulting schema_version as 7. Migrations 8 (reservation) and 9 (claim_history) landed without updating them, so the tests were already failing -- invisibly, because tests/pg/ skips itself whenever SPRINTCTL_TEST_PG_URL is unset. They now derive the range from pg_migrations.CURRENT_SCHEMA_VERSION and will not rot when migration 10 lands. test_interleaved_legacy_offsets_backfill also failed for legacy_version=2 with UndefinedTable: work_item. The version-1 path replays the canonical PG_DDL, but the version-2 path does not, so the synthetic fixture must stand in for the base tables a real version-2+ deployment already has -- the same reason the existing `ref` stub is there. Migration 8's foreign key needs work_item and migration 9's LIKE needs claim, so both are now stubbed, guarded to legacy_version >= 2 so CREATE TABLE IF NOT EXISTS in the version-1 replay is not shadowed by a partial stub. Verified against a disposable PostgreSQL 16.13: tests/pg/test_schema.py 11 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pg.touch_reservation collapsed "not found" and "not active" into one "Reservation #N is not active" message, so a missing reservation was misreported and the caller never learned which state blocked the touch. SQLite already distinguishes both and names the state; PostgreSQL now matches. reassign_reservation was already identical on both backends. The converted partition test covers it: a session displaced by an override now learns its reservation "is interrupted" rather than the ambiguous "is not active". Also retires this module's two claim-lifecycle tests. They drove claim.acquire/renew/handoff/release, record types that no longer have a payload contract at all. Their surviving policy -- no proof material in a command payload -- moves to tests/test_authority_contracts.py, where it is asserted against every record type that still exists and runs in the default suite rather than only under a PostgreSQL rehearsal. Verified against a disposable PostgreSQL 16.13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the handoff's step 3. Each group was converted where the property survives v3 and deleted where the plan drops it outright. Converted: - test_maintain: purge_expired_claims -> sweep_stale_reservations, plus a negative case. sweep_stale_reservations had no PostgreSQL coverage at all, so this closes a real gap rather than just relocating one. - test_maintain: the lease_epoch rotation history test becomes reassign + override, proving ownership changes still accumulate rows instead of rewriting one in place -- the auditability the epoch counter provided, without a secret. - test_work_item: a reserved item's status change still uses ordinary CAS, i.e. a reservation is advisory and never gates a transition. - test_remote_recovery: asserts the reservation ledger survives a snapshot. The claim row is now seeded with raw SQL rather than dropped, because write_recovery_snapshot still strips ownership out of it; that coverage should outlive the API, not the relation. - test_work_application_pg: the served concurrency test targets work.reservation.reserve. Its claim ancestor also proved idempotent replay of the winning command; reservations have no durable decision ledger, so the docstring states the narrower property rather than implying the old one still holds. - test_work_application_pg: actor binding keeps the nested-actor case on item.transition and drops claim-agent-mismatch, which no surviving payload contract can produce. Deleted: claim renew/release/handoff served tests and their command builders (TTL, proof, and rotation are all dropped invariants). Also removes an orphaned 49-line block that a previous retirement pass left glued onto the end of test_served_lifecycle_retry_and_stale_basis_are_durable after deleting its `def` line. It ran after store.conn.close() and failed with OperationalError whenever the suite ran against a real database. Verified against a disposable PostgreSQL 16.13: tests/pg/ and tests/test_work_application_pg.py 143 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the retired claim-ownership protocol with the credential-free reservation model. Update context/handoff contracts, served parity, migration history, doc refs, and capability close flow to match the current CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update the agent integration guide, README, work loop, project integration, resume, daily loop, agent-assisted, interoperability, coordinator mode, and takeup docs to use credential-free reservations instead of the retired claim CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update repo template, bootstrap prompts/workflow, alias pack, agent prompt snippets, AGENTS.md sample, and editor integration examples to use sprintctl reservation instead of the retired claim commands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reservation docs rewrite renamed two documents and two section headings, which broke six assertions that pinned the old names: - claim-discipline.md -> reservation-discipline.md, and its link label - claim-ownership.md -> reservation-model.md - "## Claim helpers (explicit proof retained)" -> "## Reservation helpers (explicit handle retained)" - "## 2. Claim-and-execute snippet" -> "## 2. Reserve-and-execute" docs/customization.md still linked the old filename. It sits outside the directories the rewrite covered, so its link was left dangling. Two assertions were wrong rather than merely stale: - The capability-receipt reference pinned `sprintctl sprint status --id <id> --status closed --actor <actor> --json`, which cannot run: --expected-revision is required for a direct sprint transition (commands/work.py raises UsageError without it). The documented example now reads the revision from `sprint show --json` first -- verified against the CLI, that key is emitted. The test pins the flags it cares about instead of a full command string. - The protocol contract asserted "work-item row lock is the arbitration point". That has not been true since reservations replaced claims: idx_reservation_active_execute is what enforces one active execute reservation per item, on both backends, and the surrounding serialization differs (BEGIN IMMEDIATE on SQLite, a repo-scoped pg_advisory_xact_lock plus SELECT FOR UPDATE on PostgreSQL, taken because maintenance activation gates on a count no index can enforce). Corrected in the document and in the test, and the prose assertions are now whitespace-normalized so a reflow does not read as a deletion. Full suite: 1378 passed, 4 skipped against a disposable PostgreSQL; 1235 passed, 147 skipped without one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the handoff's step 2. Both migrations repeat the archive step before dropping, because a deployment can write claims between the archive migration and this one; the inserts are keyed on id (SQLite) and (repo_id, id) (PostgreSQL) so a replay cannot duplicate a row. Nothing references claim by foreign key and dropping the table removes its indexes with it. claim_token is nulled across claim_history. The tokens are already inert, but the archive exists to record who held what and when, not to retain proof material, and V3-4 calls for dropping the column outright. Every other column is preserved verbatim. Table lists follow: recovery, export, NDJSON, referential-integrity, identity-sequence, VACUUM, and test-scope cleanup now name claim_history where they named claim. Two gaps surfaced while doing that: - pg_testing.REPO_TABLES never listed reservation. It survived on the work_item cascade, which claim_history does not have -- CREATE TABLE ... LIKE copies checks and indexes, never foreign keys. Both are now listed explicitly rather than relying on which one cascades. - write_recovery_snapshot never interrupted active reservations. The claim path closed active claims for exactly this reason and the rule was not ported when reservations replaced them, so a recovered database read as though the pre-recovery session still held its work. The reservation protocol document already stated the intended behavior; the code now matches it, and the recovery report counts interruptions instead of closed claims. Replay hardening: _add_column_if_missing, _migration_19, and _migration_20 now tolerate an absent claim relation. Rolling schema_version back on an already-cut-over database used to abort with "no such table: claim" -- the same tolerance the surrounding IF NOT EXISTS statements already had. Hardcoded ledger expectations in test_pg_bootstrap are derived from CURRENT_SCHEMA_VERSION, the same fix already applied to test_schema. Rehearsed against a disposable PostgreSQL 16.13 built to a real schema 9 with two live claims, one written after the archive migration: migration 10 archived both, redacted both tokens, dropped the relation, was a no-op on re-run, and the compatibility handshake accepted version 10. tests/test_core.py covers the SQLite equivalent from a schema-19 database. Suites: 1380 passed, 4 skipped with PostgreSQL; 1237 passed, 147 skipped without. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `pilot` command group was unregistered from the root CLI in d5b67e4, and sprintctl/pilot.py and sprintctl/cutover.py were deleted in 6183d6b, but ~380 lines of the group survived in commands/operations.py: the group itself, status/enable/disable/verify/sync/cutover-evidence, and their helpers. They are unreachable and also broken -- `_pilot` and `_cutover` are not bound anywhere in the module, so every one of them would raise NameError if it could be invoked. Also removes work.py's orphaned _pilot_status_payload (no callers at all) and served.py's cutover_evidence facade, whose route was withdrawn from the served catalog by the same retirement. An earlier dead-code sweep missed this island because Click decorators (@pilot.command) make the functions look referenced. The Phase 28 operator procedure that drove the surface moves to docs/archive/ with a banner, since it can no longer be executed. It had no inbound links left once operations.py stopped citing it. Two catalog pins in test_vuoro_work_adapter_integration are re-pinned. They were set on 2026-08-13 and the claim -> reservation cutover landed on 2026-08-14: aeace4d added six reservation operations and 1a06d1e removed five claim operations, a net +1 at both schema versions, with schema 7 still gating exactly the three work.maintenance.resource.* operations. The drift was invisible because the module skips itself unless httpx, vuoro_client, and vuoro_service are all installed. The comment now says these pins must move in the same commit as a catalog change, never to make a red test pass. The test for the retired work.pilot.cutover-evidence operation is deleted with the operation. Suites: 1250 passed, 146 skipped; 1393 passed, 3 skipped with PostgreSQL and the Vuoro integration extras installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Recovery provenance was one synthetic recovery.completed event appended
per recovered sprint. A recovery is a property of the database, not of
each sprint inside it, so that form both scaled with sprint count and put
an operational fact into the append-only business event log.
SQLite migration 21 and PostgreSQL migration 11 add recovery_record, and
write_recovery_snapshot writes exactly one row inside the same
transaction as the data. The plan's invariant -- "provenance is atomic
with data" -- is unchanged and now covered by a test that asserts a
failed recovery leaves neither rows nor a record.
recovery_record is deliberately absent from the recovery and export table
lists. It is this database's own operational provenance ("was I
recovered, when, from where"); carrying the source's records across would
conflate the source's history with this database's. It is present on both
backends for schema parity, in check_integrity's counts, and in the
PostgreSQL test-scope cleanup.
Existing recovery.completed events are left in place. The event history
is append-only, so past recoveries stay legible where they were recorded;
only new recoveries use the record.
doctor's local schema probe now reports recovered_from -- a recovered
database is a new authority instance, and an operator diagnosing one
needs to know that before trusting anything else it reports. A database
predating migration 21 has no such table, which the probe reports as
absent rather than treating as an error.
The recover-from-remote parity report drops its "+N recovery.completed"
line: events now recover one-for-one.
Rehearsed against a disposable PostgreSQL 16.13 built to a real schema
10: migration 11 created the relation with the expected columns and
indexes, was a no-op on re-run, and the compatibility handshake accepted
version 11.
Suites: 1253 passed, 146 skipped; 1396 passed, 3 skipped with PostgreSQL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The byte-exact catalog pins live in test_vuoro_work_adapter_integration, which skips itself unless httpx, vuoro_client and vuoro_service are all installed. vuoro_service has no published wheel -- only the client does, as an attested release pinned by the `served` extra -- so on an ordinary checkout that module never runs. That is how those pins silently drifted for two days across the claim -> reservation cutover. WORK_OPERATION_CONTRACTS is a plain tuple in sprintctl.vuoro_adapter with no Vuoro dependency, so pinning the operation names there runs on every checkout. It cannot catch a schema-shape change inside one operation, but it does catch an operation being added, removed, or renamed, which is what actually drifted. Verified by removing an entry and watching it fail. Also guards that the withdrawn claim and pilot operations do not reappear. Deliberately not fixed by declaring vuoro-service as a dependency: the served extra pins an attested release wheel by digest specifically so no mutable source checkout participates in installation, and sprintctl is a client of the Vuoro service, not a host of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pushed reservation model contradicted itself. The protocol said there was no enforced exclusivity and that overlapping reservations were the concurrency-tested behavior; a partial unique index and reserve()'s conflict branch said the opposite, and the tests pinned the index. Exclusivity wins in that fight, so v3 shipped a lease under a different noun. Overlap is now recorded, not refused. reserve() always commits and returns conflict / conflicting_reservations / conflict_severity, with `warning` reserved for execution-beside-execution. Refusing the second session never stopped it working -- it only kept it out of the ledger, which is the one outcome a coordination ledger must not produce. idx_reservation_active_execute is dropped in SQLite 22 and PostgreSQL 12. Displacement survives as an explicit act: --interrupt-existing, scoped to the item's active execution reservations, recording "interrupted by <actor> (<session>)" plus an audit event. It is deliberately not called --override, which reads as bypassing an authorization check -- the exact concept v3 deletes. Verification and observation reservations are left alone: a takeover replaces whoever claims to be doing the work, not everybody else's signals. Roles become the work relationship -- execution, verification, observation -- because that is what makes an overlap classifiable. `coordinate` was never a relationship to the item (orchestration is session and project context), so coordinators and `inspect` fold into observation. Legacy names normalize on input and are rewritten by both migrations. Activity stops measuring remembered ceremony. last_activity_at now advances implicitly on successful item-scoped mutations attributed to the reservation's session -- status, edit, note, ref, dep -- never on reads and never on a bare actor-name match. `reservation touch` remains for work outside sprintctl. Explicit-touch-only had quietly implemented a very relaxed heartbeat while insisting it was not one. Staleness horizons move out of the model into reservation_policy: the ledger stores facts, and what an age means is operator policy (4h display, 7d sweep, both env-overridable). Seven days means "an explicitly invoked maintain sweep may interrupt reservations older than this", never "something expires in the background". Finally, the PostgreSQL floor was a false promise. MINIMUM_SCHEMA_VERSION was 5 while reservation storage arrived in 8, the live claim relation only disappeared in 10, and this correction is 12 -- a client could pass the handshake against a schema that cannot service its first reservation call. The v0.3 release is a coordinated cutover, so the runtime admits exactly the schema it was built against. stage_schema5_maintenance_bridge had asserted full compatibility as its post-condition, which can no longer hold at schema 5; it now verifies the bridge it actually installed. Tests: 1411 passed, 4 skipped, including tests/pg against a disposable PostgreSQL. New coverage pins overlap on both backends from independent connections, takeover scoping, session-attributed activity, policy horizons, and migration 12 replayed against a real parser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The protocol document asserted both that no exclusivity was enforced and that a partial unique index was the arbitration point. With the index gone, the second claim goes with it, and the surrounding guidance follows: reserve reports overlap, roles are the work relationship, activity is session- attributed and mostly implicit, and staleness horizons belong to operator policy rather than to the model. The parity section now records what the backends actually still serialize -- a repo-scoped lock held because maintenance activation gates on a *count* of active reservations, which no index can enforce -- so the one remaining refusal is legible as a property of the repository rather than of who else is working on the item. Q1-Q4 and Q7 are recorded as resolved in the v3 plan, together with the schema admission floor raised during review. The doc-contract test that pinned the exclusivity sentence now pins its absence, so the claim cannot quietly return. Also corrected in passing: served-command-parity and the work-adapter inventory still described reservation operations as work.claim.* arbitration, and the SQLite migration history stopped at 14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main's only commit since the merge base is 75fd4a7, the squash-merge of PR #39 -- work that originated on this branch. Merging it back is therefore bookkeeping: the resulting tree is byte-identical to this branch before the merge (`git diff HEAD` after resolution is empty). Three conflicts, all from the squash re-presenting content this branch has since moved past: - tests/test_adapter_kit_migration.py: catalog counts. Kept this branch's 47/44; main's 46/43 predate the reservation catalog. - sprintctl/vuoro_adapter.py: the squash re-added catalog_operation_specs and _RESOURCE_OPERATIONS verbatim beside the copies this branch already has. Dropped the duplicate; Python would otherwise have silently used whichever definition came last. - tests/test_vuoro_work_adapter_integration.py: kept this branch's byte pins, and deliberately did not take main's new test_preexisting_generic_client_discovers_cutover_evidence. That test exercises work.pilot.cutover-evidence, a surface 21b6984 removed here as unreachable and broken. Taking it would have reintroduced a test for code this branch deleted. If the pilot retirement is ever reconsidered, that test comes back with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review findings against the reservation correction. The headline feature was half-wired and one parity claim in the docs was false. Implicit activity never fired for served clients. The catalog gained an optional session_id on seven operations, but nothing populated it: served.py had no occurrence of the field, so the authority's `if not session_id: return` always won, and the CLI returns from its served branch before the direct-path helper can run. Rather than thread the argument through six facade signatures -- where the next served operation would silently drop it again -- clients now attach it centrally in _invoke_operation, driven by the same operation set the authority consumes. That set moves to sprintctl.reservation, keyed by the argument each operation uses to name its item, because the catalog is not uniform: work.event.add scopes itself with work_item_id while the item operations use item_id. Reading only item_id made event.add a silent no-op despite being listed as activity-bearing. Direct-CLI dep add/remove and event add never called the helper at all; they do now, so the direct and served paths agree with the documented set instead of each honouring a different subset of it. Net effect of the bug: agents were pushed back to explicit `reservation touch`, the exact ceremony Q3 set out to remove -- while the protocol document told them they did not have to. PostgreSQL appended no reservation events at all. Pre-existing, but this branch newly asserted that both backends record the lifecycle "so the facades cannot drift" and that --interrupt-existing "emits a durable audit event". Neither held on PG: reserve, reassign, release, and sweep all committed silently, so handoff bundles and usage --context showed reservation state changing with no attribution. Since reservations carry no credential, that trail is the only record of who displaced whom, so PG now appends the same events SQLite does, pinned on both backends. touch deliberately stays event-free: it moves a clock, and an event per bump would rebuild the heartbeat log v3 deleted. Smaller findings: - _MAINTENANCE_TABLES omitted `reservation` and `recovery_record`, so integrity and vacuum reports described a different repository depending on backend. A parity test now compares the two table sets directly. - Reservation policy was published under two names for one value (interrupt_after_days vs maintenance_interrupt_after_days), with the arithmetic hand-rolled at the second site. describe() is now the single surface both splat, so the next horizon added cannot reach only one of them. - LEGACY_REMOTE_COMMAND_PARITY collapsed touch/reassign/release into a row naming only reassign, while the doc it advertises lists all three. - Dead pilot-era shadow helpers in commands/work.py (~80 lines) referencing a module this branch deleted. The copies in commands/operations.py are live and stay. - doctor captured recovery provenance but only rendered it in --json, so the operator most likely to need "this is a recovered authority instance" was the least likely to see it. - One duplicated claim_history assertion from the rename. - Prose still said "coordinate reservation" / "worker execute reservations". Also documented that the CLI accepts only current role names and fails loudly on retired ones, which is a decision rather than an oversight: the served API still folds legacy names in for un-updated clients. Tests: 1421 passed, 4 skipped, including tests/pg against a disposable PostgreSQL. New coverage pins served session attribution across every activity-bearing facade, the work.event.add key, session-not-actor attribution, the audit trail on both backends, and integrity table parity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reservation model is a breaking change, so it takes the minor version rather than another patch: the PostgreSQL schema floor is now equal to the current version (12), the served catalog dropped the claim operations, and the CLI's role vocabulary changed. A 0.2.x consumer cannot talk to a 0.3.0 authority, and this makes that legible in the version alone. Bumps the four places a release reads: pyproject, __version__, the lock's own package entry, and the release contract's pinned RELEASE_VERSION, which the tag-triggered workflow validates the built wheel against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The v3 reservation tract: sprintctl's credential-bearing claim lease is
replaced by an advisory reservation ledger, and the machinery that supported
the lease — tokens, rotation, TTL/heartbeat, recovery sidecars,
lease_epoch,proof-gated mutation — is deleted rather than preserved. 107 commits,
+15,044 / −27,742.
Mutation safety moves to expected-revision compare-and-swap, command
idempotency, and served-authority serialization. Execution ownership stays
with the dispatcher (ActionQ execution IDs, git worktrees). Recovery collapses
to "new authority instance, re-reserve".
Background:
docs/plans/v3-reservation-model-plan.md(model, sequencing,resolved questions) and
docs/protocols/reservation-model.md(the contract).The correction the last two commits make
The pushed model contradicted itself: the protocol said no exclusivity was
enforced and that overlapping reservations were the concurrency-tested
behavior, while a partial unique index and
reserve()'s conflict branch saidthe opposite — and the tests pinned the index. Exclusivity won that fight, so
v3 was shipping a lease under a different noun.
Resolved as: overlap is recorded and reported, never refused.
reservealways commits, returningconflict,conflicting_reservations,conflict_severity(warningonly forexecution-beside-execution). Refusing a second session never stopped it
working — it only kept it out of the ledger.
--override→--interrupt-existing: a deliberate takeover, scoped to theitem's active
executionreservations, recording a reason and an auditevent. "Override" read as bypassing an authorization check, the exact
concept v3 deletes.
execution/verification/observation. That is what makes an overlap classifiable.coordinatewasorchestration context rather than a relationship to the item, so it and
inspectfold intoobservation.last_activity_atadvances on successful item-scoped mutations by the reserving session, never
on reads, never on a bare actor-name match.
reservation touchremains forwork outside sprintctl. Explicit-touch-only had quietly implemented a very
relaxed heartbeat while insisting it was not one.
reservation_policy(4h display, 7d sweep, bothenv-overridable). Seven days means "an explicitly invoked
maintain sweepmay interrupt reservations older than this", never "something expires in the
background".
Schema cutover — read before merging
This PR is not safe to deploy by merging alone. It carries a destructive,
deployment-owned migration.
idx_reservation_active_executeandrewrite role values in place.
claimrelation;claim_historyis the only survivor, withclaim_tokennulled out.MINIMUM_SCHEMA_VERSIONis raised to 12, equal toCURRENT_SCHEMA_VERSION.The previous floor of 5 was a false promise: reservation storage arrived in
8,
claimdisappeared in 10, and this correction is 12 — a client couldpass the handshake against a schema that cannot service its first
reservation call. This release admits exactly the schema it was built
against; widen it later if a rollout needs it.
Cutover is one maintenance window: quiesce writes → PITR/backup point →
migrate → deploy runtime → update clients → verify handshake, catalog,
reserve/overlap/activity/release, recovery provenance → reopen. Rollback is
restore, not downgrade — there is no down migration that reconstructs
claim.Verification
1411 passed, 4 skipped, against the merged tree, including
tests/pgon adisposable PostgreSQL. New coverage pins overlap on both backends from
independent connections, takeover scoping, session-attributed activity,
policy horizons, and migration 12 replayed against a real parser on a
throwaway schema. The doc-contract test now pins the absence of the
exclusivity claim so it cannot quietly return.
Merge-with-main note
main's only commit since the merge base is the squash of PR #39 — work thatoriginated on this branch — so merging it back was bookkeeping and the tree is
unchanged by it. Three conflicts came from that squash re-presenting content
this branch has moved past; the merge commit records each resolution. One is a
judgement call worth a reviewer's eye: main's new
test_preexisting_generic_client_discovers_cutover_evidencewas not taken,because it exercises
work.pilot.cutover-evidence, a surface 21b6984 removedhere as unreachable and broken.
Not covered by this PR
Merging is fine if merge does not publish; do not run the destructive schema
cutover before it closes.
prepared separately.
🤖 Generated with Claude Code