---
name: cross-repo-change
description: Coordinate changes that touch both the aweb OSS repo and the hosted ac repo. OSS lands first; the hosted repository owns dependency derivation and deployment.
---

# Cross-repo change coordination

Changes that affect both aweb (OSS) and ac (hosted SaaS) must be
coordinated carefully because ac embeds aweb as a PyPI package.

## Read this first: shippability

**A released client is a permanent constraint.** Once a client that
sends X is in the field, the server must accept X until you can prove
nothing sends it.

Everything below about atomic deploys applies to **aweb ↔ ac only**. Every
other consumer — the channel plugin, pi, the `aw` CLI, self-hosted servers —
updates on its own schedule. A running agent
keeps its loaded module until the process **restarts**, which for a
long-lived agent may be never.

**Default: additive only.** You may ADD an accepted shape. You may NOT:

- remove an accepted shape,
- make an optional field required,
- reject a shape that is in the field,
- narrow a type or add a stricter validator to an existing field.

**The only exception:** you may narrow a contract if you can NAME every
consumer and show each deploys **atomically** with this change — same
image, same deploy, same instant. "We will release the client right
after" is not atomic. Put the justification in the commit message.
Today exactly one boundary qualifies: aweb ↔ ac.

**Removal is a separate, later change.** Remove support for an old shape
only after MEASURING that nothing sends it. If you cannot measure it,
you cannot remove it.

**Before merge, answer:** *what is deployed today, and does this still
work against it?* Check against PUBLISHED ARTIFACTS — `npm pack` the
tarball and read the shipped bundle, or read the released tag — never
against your own source. Your source and the published client disagree;
that disagreement is the entire risk.

Why this section exists: on 2026-07-26 a chat mark-read change made a new
field required and explicitly rejected the old one. Every published client
sent the old field, so deploying it would have broken every client in the
world. It was faithful to the anti-pattern below, which is correct for the
cloud boundary and catastrophic for clients. The process was not ignored —
it was followed.

## Principle (aweb ↔ ac only)

The cloud imports aweb. Both deploy in the same Docker image.
There is no transition period — when the cloud pins a new aweb
version, both the old and new code deploy atomically.

This is the ONLY boundary with that property. Do not generalise it.

## Sequence

1. **Design** — agree on the contract change between OSS and cloud.
   Identify which side goes first.

2. **OSS first** — land the OSS change on aweb main after the relevant focused
   tests pass.

3. **Release OSS** — run `make release-candidate TAGS='...'` in aweb, then push
   each resulting tag separately. `docs/release.md` is authoritative. Wait for
   the exact package versions to be public.

4. **Cloud pins** — update and review `backend/pyproject.toml` and
   `backend/uv.lock` to the exact public OSS versions on AC `main`.

5. **Cloud candidate** — land the cloud-side source change on AC `main`, then
   run `make release-candidate VERSION=X.Y.Z`. Push the resulting `vX.Y.Z` tag;
   AC never publishes OSS artifacts.

6. **Verify** — cloud tests pass against the real aweb package (not
   editable/sibling source).

## Anti-patterns

- Do NOT land the cloud change before the OSS release. The cloud
  CI will fail on import errors.
- Do NOT use editable/sibling source installs as a permanent
  workaround. They mask version pinning issues.
- Do NOT accept both old and new formats "during transition" when
  both sides deploy atomically. Pick one format. **This applies to
  aweb ↔ ac and nowhere else.** Before invoking it, confirm
  every consumer of the contract deploys in that same image. If any
  consumer is a published client — channel, pi, `aw` — or a
  self-hosted server, the rule INVERTS: accept both, prefer the new
  one, and remove the old only after measuring. See "Read this
  first: shippability".
- Do NOT change a contract without enumerating what currently sends
  it. For the channel that check is small — its entire write surface
  is two calls, one of which has no body — so there is no excuse for
  skipping it.

## Hosted schema mirrors

The `ac` repository maintains its own copy of the OSS schemas. The cloud's
`migration_paths.py` points at `backend/src/aweb_cloud/migrations/aweb/`
and `migrations/server/` — NOT at the OSS package's migrations in the
installed `.venv`. So when an OSS release adds or alters a schema the
cloud uses, the cloud needs a paired mirror migration with a matching
ALTER/CREATE.

The migrations to mirror can live in DIFFERENT OSS tree subdirectories
within the same release. Easy to miss the second one when walking commit
by commit:

- `awid/src/awid_service/migrations/*.sql` → mirror in cloud as
  `migrations/aweb/`.
- `server/src/aweb/migrations/aweb/*.sql` → also mirror in cloud as
  `migrations/aweb/` (same target dir, different OSS source dir).

When reviewing a cross-repo bundle, grep ALL OSS migration trees touched
in the bundle, not just the first one you find. Pattern-blindness on the
second mirror is the failure mode.

Each mirror file should be a one-statement ALTER/CREATE matching the OSS
content, with a header comment naming the OSS source path so the lineage
is explicit.

## Example: aweb-aaje (proxy auth team_id format)

1. OSS: changed X-Team-ID validation from UUID to colon-form,
   deleted _resolve_proxy_team_id (cde8889, 0fbe3d9, 78794ff)
2. Released: aweb 1.10.1
3. Hosted repo: pinned 1.10.1 and changed the bridge to send colon-form
   (2761d0a5, 1f6e4797)
4. Both deploy together in the cloud Docker image

## Notes

- The hosted `ac` repo is cloned at `../ac` relative to the aweb
  workspace.
- Ownership and review routing live in the active team instructions
  and `aw workspace status`; do not rely on names written here.
