Skip to content

feat: replace claim leases with advisory reservations (v3) - #40

Merged
bayleafwalker merged 109 commits into
mainfrom
codex/p23-vuoro-adapter-kit
Aug 15, 2026
Merged

feat: replace claim leases with advisory reservations (v3)#40
bayleafwalker merged 109 commits into
mainfrom
codex/p23-vuoro-adapter-kit

Conversation

@bayleafwalker

Copy link
Copy Markdown
Owner

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 said
the 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.

  • reserve always commits, returning conflict,
    conflicting_reservations, conflict_severity (warning only for
    execution-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 the
    item's active execution reservations, recording a reason and an audit
    event. "Override" read as bypassing an authorization check, the exact
    concept v3 deletes.
  • Roles become the work relationship: execution / verification /
    observation. That is what makes an overlap classifiable. coordinate was
    orchestration context rather than a relationship to the item, so it and
    inspect fold into observation.
  • Activity is session-attributed and mostly implicit: last_activity_at
    advances on successful item-scoped mutations by the reserving session, never
    on reads, 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 to reservation_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".

Schema cutover — read before merging

This PR is not safe to deploy by merging alone. It carries a destructive,
deployment-owned migration.

  • SQLite 22 and PostgreSQL 12 drop idx_reservation_active_execute and
    rewrite role values in place.
  • PostgreSQL 10 (earlier in the branch) drops the live claim relation;
    claim_history is the only survivor, with claim_token nulled out.
  • MINIMUM_SCHEMA_VERSION is raised to 12, equal to CURRENT_SCHEMA_VERSION.
    The previous floor of 5 was a false promise: reservation storage arrived in
    8, claim disappeared in 10, and this correction is 12 — a client could
    pass 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/pg on 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 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 that
originated 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_evidence was not taken,
because it exercises work.pilot.cutover-evidence, a surface 21b6984 removed
here as unreachable and broken.

Not covered by this PR

  • #1164 row 12 (direct-remote circuit breaker) is a release/deployment gate.
    Merging is fine if merge does not publish; do not run the destructive schema
    cutover before it closes.
  • Cross-repo guidance mirrors (agentops skills, bootstrap-template docs) are
    prepared separately.

🤖 Generated with Claude Code

actionq-dispatcher added 30 commits August 13, 2026 19:45
actionq-dispatcher and others added 27 commits August 14, 2026 18:57
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>
@bayleafwalker
bayleafwalker marked this pull request as ready for review August 15, 2026 19:10
@bayleafwalker
bayleafwalker merged commit 15afc87 into main Aug 15, 2026
4 checks passed
@bayleafwalker
bayleafwalker deleted the codex/p23-vuoro-adapter-kit branch August 19, 2026 18:38
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