# Roadmap

What has to happen between the 0.1.0 betas and the first release that is not one. Four lists: what still blocks a correct 0.1.0, what has only ever run against a stand-in and needs a real resource under it, which tests are missing, and what is experimental today and has to graduate or be cut. Everything here is a decision or a test, not a feature.

One definition runs through all of it. **Stable** means the full lifecycle - register, commit, fork, write, commit, promote, gc - is *exercised in CI* against the real system. **Experimental** means it has run against a fake, a local stand-in, or not at all. By that definition `file` and `icechunk` are stable on local storage, which is what CI runs; their S3, GCS and Azure paths are **not yet cloud-tested** and are labelled so in the README and the backends guide until section 2's runs happen. `tether backends` prints each kind's maturity, [add](reference/testing.ObjectDiff.html#tether.testing.ObjectDiff.add) prints a note for an experimental kind, and `--create` and `gc --delete-stores` print theirs.


# 1. Correctness blockers

The beta-exit work (0.1.0b4) closed the first review's findings, and then the second review's:

- **S1.** `secrets.toml`, `workspace.toml` and `ops.jsonl` are refused while the VCS tracks them, at `find` and at every refresh that sees a changed `workspace.toml` (each [open](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.open), each writing command); git 2.38 is enforced.
- **D1.** `gc` never releases a pin id any manifest names, and local paths are resolved fully (symlinks, trailing slashes, `file://`).
- **D2.** [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) compares fork heads with the bookmark's commit, not the working tree.
- **D3.** `undo ID` reverts only the fields still holding what the operation left, and refuses a [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new) whose bookmark the checkout still works on.
- **D4.** Lance's conditional fork never deletes or replaces a branch it did not check (a small window remains; below).
- **D8.** Saved plans are format 3, with a digest binding actions and context to their preconditions.
- **U1-U5.** Format version 5 and one migration carry a 0.1.0b3 dataset's identities, listings, Lance, Neon and directory states, and DuckLake paths forward; `gc` and [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify) skip manifests of removed kinds; 0.1.0b3 refuses a version 5 dataset.
- **jj template aliases.** tether's templates call keywords as methods, and the hostile-config fixture aliases them.
- **[new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new) against a branch being written.** `--adopt` binds to the branch existing and being the newest generation, and a `--shared` join to it builds on the bookmark's pin (0.1.0b6), so a branch a running service writes no longer fails "moved since the plan was made". Passed live on 10-08 in the lab-platform sandbox: a push to a running tether-mode preview (PR \#6) adopted every store's fork as it was, the Neon branch its Dagster daemon writes included, and the apply kept the same addresses. A reset (`--discard`) and `reuse` still bind to the head, which they depend on.

What remains:

| Blocker | What is missing | What settles it |
|----|----|----|
| **Real-service runs** for `neon`, `dolt`, and S3 through SeaweedFS | Neon has now run by hand against a real project (section 2, "Recorded runs"), which found the next two blockers; its [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) refusal, a 423 answer, protected pins, rate limits and the cost of a branch per pinning commit are still unobserved, and nothing runs in CI. Dolt has run only against fakes. The SeaweedFS-backed S3 test skips without `weed` on `PATH`, so CI has never run it. | The rest of the rows in section 2, once each, with the result recorded; `weed` in a CI image; a CI row where a credential or a container can live. |
| **`gc` cannot delete a Neon branch** | `plan_gc` reads no head for a [BRANCH_IS_STORAGE](reference/testing.Capability.html#tether.testing.Capability.BRANCH_IS_STORAGE) branch ("branch is storage; deleting reclaims its data"), so a `--force-prune` delete carries `head: None`. `apply_gc`'s preflight (`_require_head`) takes that to mean "the plan could not read this head" and raises [StalePlanError](reference/StalePlanError.html#tether.StalePlanError) whenever the head *can* be read - for Neon, always. `gc --prune-bookmarks --force-prune` therefore never deletes a retired bookmark's Neon branch, and fails before deleting anything else. The sandbox worked around it by deleting the branch through Neon's API before `gc`. | Record the head at plan time for storage branches too (one API call), or let a forced storage-branch delete skip the head check; a conformance test with a [BRANCH_IS_STORAGE](reference/testing.Capability.html#tether.testing.Capability.BRANCH_IS_STORAGE) backend. |
| **A writable Lance handle's address** | `open --json` prints a Lance handle as `<uri>#<branch>@v<N>` whether or not it is writable, the same form as a pinned read, so a consumer that reads `@vN` as a pin opens a writable fork read-only. Only the [read_only](reference/testing.Handle.html#tether.testing.Handle.read_only) field tells them apart. | Print a writable handle as `<uri>#<branch>` (its version is the open-time head, not a pin), or document that [read_only](reference/testing.Handle.html#tether.testing.Handle.read_only), not the address, decides. |
| **S3 Tables needs SigV4** | pyiceberg reaches the S3 Tables Iceberg REST catalog only with `pyiceberg[rest-sigv4]` (boto3). The `iceberg` extra installs `pyiceberg` alone, so a dataset on S3 Tables fails at its first catalog call. | Add `rest-sigv4` to the `iceberg` extra, or name it in the backends guide's Iceberg section. |
| **Cloud runs** for `file` and `icechunk` on S3, GCS and Azure | The object-store code paths (obstore client options, versioned objects, `s3_storage` with per-object credentials, [delete_store](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.delete_store) on a prefix) have run against in-memory stores and S3-compatible stand-ins only. | The S3, credentials, GCS and Azure rows in section 2. Until then the labels say "not yet cloud-tested". |
| **Lance's conditional fork** | Onto an existing branch, the head is compared and the branch then deleted and created again: a peer's branch created in that window can be replaced. A fork expecting "absent" is atomic. | A conditional branch update in Lance, then [CONDITIONAL_REF](reference/testing.Capability.html#tether.testing.Capability.CONDITIONAL_REF); until then the window is documented and Lance stays stable. |
| **Icechunk on local storage** | `reset_branch(from_snapshot_id=)` and `create_branch` are atomic only on object stores ([CONDITIONAL_REF](reference/testing.Capability.html#tether.testing.Capability.CONDITIONAL_REF) on `s3://` only). On a local or NFS-shared filesystem, racing processes can each win. | A lock tether takes around local-storage moves, or a refusal of `--shared` there; the docs say so meanwhile. |
| **jj repo-level aliases** | tether replaces the user config layer, but jj still loads repo- and workspace-level config. A repo-level alias of `if()`, of a lambda parameter, or of [files()](reference/vcs.GitObjectReader.html#tether.vcs.GitObjectReader.files) still changes what tether reads. | Refuse such aliases after `jj config list --repo`, or an upstream jj flag that skips repo config. |
| **Native handles across [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new)** | Every [open](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.open) follows the current bookmark, but a handle opened before another process's `new -b` keeps writing to its old branch. | A handle that checks the workspace's bookmark before it writes, or the documented "reopen after [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new)" stays. |
| **Windows** | Writing commands are refused where `fcntl` is missing, and a default [open](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.open) is read-only; the README says so. Nothing has run on Windows at all. | A decision: implement the checkout and repository locks on `msvcrt`, or ship 0.1.0 with writes refused. Either way a Windows CI row (section 3). |
| **jj version range** | The minimum is jj 0.43.0 (the version tether's tests run against locally); CI installs 0.45.1 only. Nothing between or beyond has run. | The version-matrix row in section 3. |
| **git [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote)/[merge](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.merge) under a detached `HEAD`** | With the default `ref = HEAD` the git backend moves the checked-out branch, and refuses a detached `HEAD`, which a colocated jj checkout always has. Documented; not fixed. | Decide whether to merge without a checkout (`merge-tree --write-tree`, `commit-tree`, a conditional `update-ref`) before 0.1.0, or keep the documented refusal. |
| **A teardown command** | Removing tether from a repository is a manual recipe in the Troubleshooting page: list and delete every `tether.<id>.*` ref per store. | A command, or the recipe stays and is exercised once by hand against every stable backend. |


# 2. Test against actual resources

Each line names the resource, what to run, and what would move as a result. The engine tests and conformance suites cover the logic; these cover the service. Run them once by hand before the release and, where a credential can live in CI, keep them running.

| Resource | What to run | What it decides |
|----|----|----|
| **S3 bucket** (any region; versioning on for one prefix) | `file` prefix and versioned-object objects: [add](reference/testing.ObjectDiff.html#tether.testing.ObjectDiff.add), [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit), `status --snapshot`, [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify), `open --rev`, drift after an overwrite, `allow_http` and the other allowlisted `storage_options`. `icechunk` repository at `s3://`: the story of section 2 of the worked examples (fork, write, promote, gc). `add --create` / `gc --delete-stores` on an `s3://` prefix - the [delete_store](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.delete_store) arm has only run against an obstore in-memory store. | `file`'s Addressable tier for versioned objects is real; Icechunk-on-S3 is real; both drop the "not yet cloud-tested" label; store-lifecycle graduation criterion 1. |
| **AWS credentials via `secrets.toml`** (a `profile`, a `role_arn`, literal keys, an `endpoint_url` to a SeaweedFS) | Two Icechunk objects in two accounts pinned by one [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit) (the configuration guide's "two identities" section). `tether open` and [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify) with `region` overrides. A [Repo](reference/Repo.html#tether.Repo) held open past the assumed role's expiry (the credentials refresh five minutes before it). | The per-object credential layer is real (it is core today; the reviews asked twice whether it should be). |
| **GCS and Azure containers** | `file` objects and prefixes through obstore (`gs://`, `az://`); a versioned object on each. | Whether the `file` backend's object-store claims hold beyond S3, or the docs narrow to S3. |
| **Neon project** (Free for the default unprotected pins, which still cap a project at 10 branches; a paid plan, which allows a few protected branches, for `protected_pins = true`) | The `neon` lifecycle against the control plane: [add](reference/testing.ObjectDiff.html#tether.testing.ObjectDiff.add), [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit) (a branch pin, protected when asked), [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new) + writable open (branch), [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) refusal path, `gc --prune-bookmarks` keeping a [BRANCH_IS_STORAGE](reference/testing.Capability.html#tether.testing.Capability.BRANCH_IS_STORAGE) branch, `--force-prune` deleting one, [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify) with a suspended compute, a 423 answer during a pin. The lineage / xid caveat in the caveats guide, observed. | `neon` stays experimental at 0.1.0 (below); this run decides what the note says. |
| **Iceberg catalog** (a REST catalog - Lakekeeper or Polaris in a container - and one object-store warehouse) | The `iceberg` lifecycle: snapshot pins, branch forks, [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) fast-forward, `log`, `add --pick`, a table with no snapshot yet, retention expiring a recorded state. pyiceberg 0.11 or newer. | `iceberg` graduates; the [RETENTION_BOUND](reference/testing.Capability.html#tether.testing.Capability.RETENTION_BOUND) claim is observed rather than asserted. |
| **Dolt server** (container, MySQL protocol) | The `dolt` lifecycle including a conflicting [merge](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.merge) (it must surface as [MergeConflict](reference/MergeConflict.html#tether.MergeConflict)), `HASHOF` pins, and a server whose `[uris]` entry is missing (no password may be sent). | `dolt` graduates. |
| **DuckLake catalog** (DuckDB with a Postgres or SQLite catalog and an object-store [data_path](reference/handles.DuckLakeHandle.html#tether.handles.DuckLakeHandle.data_path)) | Addressable reads by snapshot id; `log`; retention; a relative [metadata](reference/handles.DuckLakeHandle.html#tether.handles.DuckLakeHandle.metadata) path registered from another directory. | `ducklake` graduates. |
| **Postgres** (already in CI through `pytest-postgresql`) | Nothing new: the registry publish/import path runs against it. Run once against a managed Postgres (RDS, Neon) to catch permission and extension differences. | Registry graduation, together with a schema decision (below). |
| **A hosted git remote and a hosted jj remote** (GitHub; a jj-capable forge or a bare repository over SSH) | Two clones of one dataset: the two-clone scenario in `tests/test_store_lifecycle.py` against a real remote, including a `forget-workspace` on one side; `gc` in each clone before and after fetching (`keep-pin` until fetched, `--release-foreign` after); a bookmark pushed from one side and dropped on the other. `tether abandon` under `git` with a remote-tracking branch. A `git` object whose `remote` is configured, pinned from a clone. | Store-lifecycle graduation criterion 2; whether "gc only knows what this clone has fetched" needs more than documentation. |
| **A large local tree** (10^6 files) and **a large prefix** | [status](reference/backends.VerifyReport.html#tether.backends.VerifyReport.status) first and second fingerprint; [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit) with a listing; `diff --content`. Numbers into the performance guide. | The performance guide's claims are measured, not estimated. |


## Recorded runs

**The lab-platform sandbox**: one AWS account, with S3, S3 Tables and Neon; tether 0.1.0b4 and 0.1.0b5; 2026-09-23 to 2026-09-25. A web app's dataset had nine objects: Icechunk, Iceberg (S3 Tables' REST catalog), Lance, Delta and a `file` prefix, plus four databases in one Neon project. The dataset was pinned on `main`, then forked per pull request by CI (`new -b pr<N> --eager`). Each fork was written by that PR's preview services and retired with `gc --prune-bookmarks --force-prune`. [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) did not run: the app discards preview forks rather than landing them.

- **Worked:**
  - the baseline [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit): Icechunk and Lance tags; Iceberg, Delta and Neon by record; the `file` prefix;
  - `new --eager` forking every forkable store;
  - writes on the forks through the stores' own libraries: Iceberg appends on a table branch, Icechunk snapshots, Lance versions on a branch;
  - `status --snapshot` on `main` staying `clean` while the forks moved;
  - `open --writable` starting a Neon compute for the fork;
  - a `--discard` reset while nothing was writing;
  - `gc` releasing the Iceberg, Icechunk and Lance branches.
- **Found:**
  - the `gc`-on-Neon, [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new)-on-a-written-branch, Lance-address and S3 Tables rows in section 1;
  - `secrets.toml` replacing the committed Iceberg catalog table instead of merging into it (fixed in 0.1.0b5).
- **Observed:**
  - an Iceberg table needs a snapshot before it can be pinned;
  - deleting a Lance branch deletes its keys (`_refs/branches/<name>.json` and `tree/<name>/`), so a role that may not delete under the store cannot release a Lance fork. The backends guide should name that grant.

**The lab-platform sandbox, again**: tether 0.1.0b6 and 0.1.0b7; 2026-10-08 and 2026-10-09; the same nine objects, now with Icechunk 2.2. One pull request (PR \#6) ran a preview in each fork mode, under IAM that denies a pull request's roles every store's trunk and pins (lab-platform's `protect_trunk`). The sandbox's scripts retired the forks: the Neon branch through the Neon API (the `gc`-on-Neon row above still stands), the rest through tether. [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) did not run.

- **Worked:**
  - `new -b pr6 --eager` forking Lance, Icechunk, Iceberg and the four databases (one Neon branch) and pinning Delta and the `file` prefix, with no write to any trunk or pin;
  - `new pr6 --adopt` on a later push adopting every fork as it was, the Neon branch the preview's Dagster daemon writes included (the section 1 row this closed);
  - the preview's Dagster writing its forks: Lance on `tree/…pr6/`, an Icechunk branch, a snapshot on the Iceberg table branch;
  - releasing the Lance, Icechunk and Iceberg working branches;
  - `pull` and [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify) on `main` under a role only the default branch may assume, recording the databases' new LSN.
- **Observed:**
  - Icechunk 2.x keeps every branch and tag in one `repo` object, which creating a fork rewrites: a path-scoped grant cannot let a role fork a repository without also letting it move `main` (1.x's `refs/` could);
  - a Neon pin by record lasts only as long as the project's history (one day by default): once the database is written again, a fork from an older LSN fails ("LSN is older than cutoff"). Pinning must run more often than that window; the caveats guide should say so.


# 3. Tests to add

Rows that need no external resource, only time. Each becomes a job or a fixture in `tests/`.

| Test | What it runs | What it decides |
|----|----|----|
| **jj version matrix with a hostile user config** | The core loop, `gc`, `drop` and `undo` under jj 0.43.0 (the minimum), the CI pin (0.45.1) and the newest release, each with the hostile-config fixture from `tests/conftest.py` (colour forced on, `all()` and template keywords aliased, auto-tracking off, `log.showSignature`), and once more with those aliases at repo level. | Whether 0.43.0 stays the minimum, and how far the repo-level alias limit reaches. |
| **A hostile clone, end to end** | One fixture repository whose manifests try everything the trust boundary refuses: a relative or in-checkout git [path](reference/handles.GitHandle.html#tether.handles.GitHandle.path), a URL `remote`, a committed `[import] query`, `git_path`, an endpoint in `storage_options`, keys with `..` and `\`, a dataset id reused from another dataset naming the same store. Every command runs against it and must refuse without contacting anything it should not. | The security-model section of the configuration guide is a test, not a promise. |
| **Multi-checkout interleavings** | Two jj workspaces (and two git worktrees) of one dataset stepping through [commit](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.commit) vs `gc`, `new --shared` vs the first writable [open](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.open), `drop` vs [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new) on the dropped bookmark, `restore` vs a `--shared` peer's write, [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) vs a commit on the trunk, in every order the locks allow. | The repository lock and the `expected`-head moves hold under interleaving, not only in the two races the review reproduced. |
| **Windows** | The read-only commands ([status](reference/backends.VerifyReport.html#tether.backends.VerifyReport.status), [verify](reference/ObjectStatus.html#tether.ObjectStatus.verify), [diff](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.diff), `log`, `ops`, `gc --dry-run`) on a Windows runner, and the refusal message for a writing one; if the lock is implemented, the full suite. | The Windows decision in section 1. |
| **Every backend through the full conformance suite** | The `file` backend with its real capabilities ([DIFF](reference/testing.Capability.html#tether.testing.Capability.DIFF), [CREATE](reference/testing.Capability.html#tether.testing.Capability.CREATE), [ADDRESSABLE](reference/backends.Tier.html#tether.backends.Tier.ADDRESSABLE) for a versioned object), not the [FINGERPRINT](reference/testing.Capability.html#tether.testing.Capability.FINGERPRINT)-only override the in-repo harnesses use today; [delete_store](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.delete_store) refusing a non-empty store for every [CREATE](reference/testing.Capability.html#tether.testing.Capability.CREATE) backend. | The suite's claims cover what ships. |


# 4. Experimental today; graduate or cut before 0.1.0

| Feature | Where | Graduates when | The call for 0.1.0 |
|----|----|----|----|
| **`iceberg`** | `tether.experimental.backends.iceberg` | The 0.1.0b4 fixes (empty tables, pyiceberg 0.11) are in, and it has run by hand against S3 Tables' REST catalog (section 2's "Recorded runs"; the SigV4 extra in section 1); what is left is the catalog row in section 2 as a CI job (a REST catalog in a container) and the conformance suite passing against it. Then move the module to `tether/backends/`, set `MATURITY = "stable"`, update the backends guide and README. | **Next to graduate**, after its CI row. |
| **`ducklake`** | `tether.experimental.backends.ducklake` | The 0.1.0b4 fix (absolute [metadata](reference/handles.DuckLakeHandle.html#tether.handles.DuckLakeHandle.metadata) path) is in; what is left is the section 2 row as a CI job (DuckDB with the Postgres catalog `pytest-postgresql` already provides). | **Next to graduate**, after its CI row. |
| **`dolt`** | `tether.experimental.backends.dolt` | The 0.1.0b4 fixes (per-server credentials, merges inside a transaction) are in; what is left is the section 2 row against `dolt sql-server` in a container, as a CI job, and the conformance suite against it. | Graduates **after a real-server run**; otherwise ships experimental. |
| **`lakefs`** | removed in 0.1.0b4 | - | **Cut.** Every fork failed (lakeFS branch ids refuse the dots in `tether.ws.*` names), and [promote](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.promote) booked lakeFS's merge commit as a fast-forward; redesigning ref naming for one experimental kind was not worth it. A dataset whose history names a lakeFS object still works: `gc` and `verify --all-history` skip those manifests with a note, and their pins still count as references. |
| **`neon`** | `tether.experimental.backends.neon` | A live run of the section 2 row (partly done, section 2's "Recorded runs"; the `gc` and [new](reference/vcs.VcsAdapter.html#tether.vcs.VcsAdapter.new) blockers in section 1 come first), and a cost story users accept: one branch per pinning commit, unprotected by default, protected pins on paid plans only. | **Stays experimental** at 0.1.0, with the note. |
| **Registry** (`export`, `publish`, `import`; [tether.experimental.registry](reference/experimental.registry.html#tether.experimental.registry)) | [tether.experimental.registry](reference/experimental.registry.html#tether.experimental.registry) | The export schema is declared frozen (a `schema_version` in the bundle and a documented compatibility promise), one round trip has run against a managed Postgres, and `import --sync` has been used on a real dataset. Move to `tether.registry`. | **Stays experimental** past 0.1.0 with a stated horizon. It does not block the release: nothing else depends on it. |
| **Store lifecycle** (`add --create`, `Repo.create`, `gc --delete-stores`, `--store`; [tether.experimental.lifecycle](reference/experimental.lifecycle.html#tether.experimental.lifecycle)) | [tether.experimental.lifecycle](reference/experimental.lifecycle.html#tether.experimental.lifecycle); [Capability.CREATE](reference/testing.Capability.html#tether.testing.Capability.CREATE) in `tether.backends.base` | The five criteria in the module docstring: S3 [delete_store](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.delete_store) against a real bucket; the two-clone scenario against a real remote; one release cycle with no data-loss report; a second Forkable backend with [CREATE](reference/testing.Capability.html#tether.testing.Capability.CREATE); a decision on the touched index. Move the module to `tether/repo/_lifecycle.py`, drop the notes, re-home the CHANGELOG entry. | **Stays experimental** at 0.1.0. [Capability.CREATE](reference/testing.Capability.html#tether.testing.Capability.CREATE) stays declared (it is a per-backend fact with conformance coverage). |
| **Per-URI and per-object credentials** (`secrets.toml` `[uris.*]` / `[objects.*]`; [ObjectBackend.configure_secrets](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.configure_secrets) / [secrets_for](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.secrets_for)) | core (`tether.backends.base`, `Repo.backend_for`); the reviews asked twice whether it belongs in experimental | The two-identity run in section 2 has happened against real accounts (a `profile`, a `role_arn`, literal keys, an `endpoint_url`) for Icechunk and `file`, and no second resolution order was needed. Then it is simply core, and the configuration guide's "advanced" section is the contract. | Moves to `tether.experimental.credentials` behind [configure_secrets](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.configure_secrets) if the run finds a second order is needed; the kind-level `[backends.<kind>]` secrets stay core either way. Nothing users write in `secrets.toml` changes. |
| **The alpha upgrade path** (`tether.upgrade`) | `tether.upgrade` | Does not graduate: it is *removed* at 0.1.0 as announced. The removal checklist is below. | - |
| **`tether abandon`** | core (`Repo.abandon`) | Decided: it stays as the surgical form - commits off a bookmark you are keeping, `--gc` to release their pins in one action - while `drop` takes the whole line. The remaining question is only whether it behaves under `git` with remote-tracking branches (the two-clone and remote tests above). | Kept; documented next to `drop` as the pair they are. |


## Removing the upgrade path at 0.1.0

Before removal: publish the last beta, confirm `tether upgrade` from every alpha format and from 0.1.0b3 against it once more, and write the "install `tether-vcs==<last beta>`, upgrade, reinstall" message into the open-time error ([LAST_BETA_WITH_UPGRADE](reference/upgrade.LAST_BETA_WITH_UPGRADE.html#tether.upgrade.LAST_BETA_WITH_UPGRADE) names it). Then remove, in one revision. Outside `tether.upgrade` and `tests/upgrade/` (both go wholesale), every legacy-only site in `src/`, `tests/` and `user_guide/` carries the marker `Legacy, removed at 0.1.0` (a `#` comment in code, an HTML comment in the guide): `rg 'Legacy, removed at 0.1.0'` lists them, and none may remain. Saved plans need nothing: a plan applies only under the tether version that made it ([Plan.tether_version](reference/upgrade.Plan.html#tether.upgrade.Plan.tether_version), checked at load), so no release reads another's plans, and the old-plan compatibility (the `ref_head` alternative for an adopt, the format 1 and 2 refusals) is already gone.

- the `tether.upgrade` package and `tests/upgrade/`, the [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) CLI command, and the thin `Repo.plan_upgrade` / [apply_upgrade](reference/upgrade.apply_upgrade.html#tether.upgrade.apply_upgrade) / [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) delegates (with the `config_version` precondition kind only upgrade plans carry, and `key_digest6`, which only its ref renames call);
- `Repo.find(allow_outdated=)` and the `allow_outdated` constructor argument, which exist only so [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) can open an older dataset;
- [ObjectBackend.rename_pin](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.rename_pin) and [rename_working_ref](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.rename_working_ref) (and the Neon override), which only the ref-renaming migration calls;
- [working_ref_workspace](reference/working_ref_workspace.html#tether.working_ref_workspace) and the legacy per-workspace working-ref parsing (`gc --prune-bookmarks`'s "legacy branch" arm), the [UpgradeReport](reference/upgrade.UpgradeReport.html#tether.upgrade.UpgradeReport) re-export, and the Migrations section of `great-docs.yml`. Once unparsed, a per-workspace name would read as a bookmark slug with a dot in it, which `bookmark_slug` never makes: refuse those as unrecognized;
- `recover`'s listing of legacy per-workspace branches: [RecoveredRefs.legacy](reference/RecoveredRefs.html#tether.RecoveredRefs.legacy), its [legacy](reference/RecoveredRefs.html#tether.RecoveredRefs.legacy) JSON key (a documented output change, for the changelog), its `restore --at` step and CLI lines, the half of its test that covers them, and the recovery step in the troubleshooting guide;
- seeding the pin-ownership index (`tether-pinned.jsonl`) from the op logs (`Repo._seed_pinned`), for clones made before the index: **decide** then whether to move it into the last beta's [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) or keep it. Kept, nothing changes; moved, a clone that skipped that beta keeps every pin it made as foreign until `gc --release-foreign`, and the tests that unlink the index to exercise the seed go too;
- the [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) mentions in the guide, rewritten for the open-time message: the CLI guide's synopsis line and [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) section; the troubleshooting rows for [ConfigError](reference/ConfigError.html#tether.ConfigError) and `tether.toml is version 4`; the configuration guide's version, dataset-id and [write](reference/handles.MemoryHandle.html#tether.handles.MemoryHandle.write)-policy lines; the DuckLake locator row; the [rename_pin](reference/testing.ObjectBackend.html#tether.testing.ObjectBackend.rename_pin) row of the extending guide; the planned commands in `great-docs.yml` and the README; and the docstrings and `--prune-bookmarks` help that name legacy branches;
- freeze [CONFIG_VERSION](reference/upgrade.CONFIG_VERSION.html#tether.upgrade.CONFIG_VERSION) for 0.1.x.

Not part of the removal, though they mention older versions: the lenient `[dataset] id` parse of a version 1 `tether.toml` (so the open-time message, not a parse error, meets an alpha dataset); skipping history manifests of removed kinds (lakeFS) and Iceberg's volatile `metadata_location`, since history keeps those manifests; [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade) in the op log's [NOT_UNDOABLE](reference/oplog.OpEntry.html#tether.oplog.OpEntry.NOT_UNDOABLE), its summary, and [vcs_drift](reference/StatusReport.html#tether.StatusReport.vcs_drift) following its `rewritten_commits`, since op logs keep those entries; and the [accepts_expected](reference/testing.accepts_expected.html#tether.testing.accepts_expected) shim for third-party backends without the `expected` keyword, a backend-author decision of its own.


# 5. Before tagging 0.1.0

- Every backend the README matrix lists is either stable or marked *(experimental)* in the row; the matrix, the backends guide, and `tether backends` agree, and "stable" means exercised in CI everywhere it appears. `file` and `icechunk` either have their cloud rows run or keep the "not yet cloud-tested" note.
- The blockers in section 1 are each closed or decided, and the tests in section 3 run in CI.
- `tether.upgrade` is gone by the checklist above; [CONFIG_VERSION](reference/upgrade.CONFIG_VERSION.html#tether.upgrade.CONFIG_VERSION) is frozen for 0.1.x; a dataset from the last beta opens without [upgrade](reference/upgrade.upgrade.html#tether.upgrade.upgrade).
- The performance guide's numbers come from section 2's runs.
- The changelog's `0.1.0` entry lists what graduated, what stayed experimental, and what was removed, in those words.
