diff --git a/AGENTS.md b/AGENTS.md
index f1d7fc622..7a2c80cca 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -35,6 +35,20 @@ before completion.
- Write Markdown prose one sentence per line, with no hard wrapping, so diffs
stay readable.
+### Fixing bugs
+
+Reproduce a bug with a failing test before fixing it, including bugs raised in review.
+Show the test failing without the fix and passing with it.
+
+### Design before implementation
+
+Once a design is agreed, write it up as a spec and break it into tickets before opening an implementation PR.
+
+### Maintenance branches
+
+`release/2.2` is maintained separately from `main` and has a different architecture.
+A fix needed on both is implemented and reviewed against each branch, not cherry-picked.
+
### Release documentation
For user-facing or notable changes, update the `CHANGELOG.md` `[Unreleased]`
@@ -42,6 +56,16 @@ section using Keep a Changelog categories and user-facing language. For a
significant release, prepare `ANNOUNCEMENT.md` for GitHub Discussions and
remove that temporary file after publishing it.
+## Commits and pull requests
+
+This repository is public.
+
+- Never put private Linear issue keys (`CIP-####`) or Linear links in PR titles, PR bodies, PR comments, commit messages, branch names or code comments. See `docs/agents/issue-tracker.md`.
+- Never put links to private agent sessions or threads (for example claude.ai session or project URLs, or `Claude-Session:` trailers) in PRs, PR comments or commit messages.
+- Every commit pushed to a PR must be signed and show as verified on GitHub; `main` requires it. This is separate from the DCO sign-off in `CONTRIBUTING.md`.
+- Never rewrite history with `git filter-branch`, which drops signatures. To reword or re-sign your own branch, use `git rebase --force-rebase --gpg-sign ` and check each commit with `git cat-file commit ` before force-pushing with a lease.
+- When a PR has a review in progress, comment on it before pushing: say what is coming and whether it is a fast-forward.
+
## Pull request CI ownership
When a task authorizes pushing changes to a pull request, use passing required
@@ -53,8 +77,11 @@ After every push:
1. The main agent must spawn a background subagent whose role is **monitor
only**. The monitor may inspect the PR, wait for checks with
`gh pr checks --watch`, and collect failed-run logs with read-only `gh`
- commands. It must not edit files, run formatting that changes files, commit,
- push, or perform any other repository mutation.
+ commands. Where GitHub's GraphQL API is unavailable (`gh pr checks` then
+ fails), read check state over REST with
+ `gh api repos/cipherstash/proxy/commits//check-runs`. It must not
+ edit files, run formatting that changes files, commit, push, or perform any
+ other repository mutation.
2. The monitor must return the final check state and, for failures, the failed
check names, run identifiers, and relevant failure output to the main agent.
3. The main agent is the sole writer. It must diagnose the failure, edit the
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index ad644368b..37784304e 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -436,6 +436,13 @@ To run integration tests:
mise run test:integration
```
+`mise run test` (which CI runs) stops at the first stage that fails.
+The stages run in this order: hygiene, unit tests, then the integration stages (psql, Rust integration, multitenant, showcase, Go, Python).
+A failing unit test therefore means no integration test runs, so a red CI run says nothing about the integration suite.
+To show that an integration test fails on its own, push it in a commit without any failing unit test.
+
+Integration tests run Proxy with `CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS` unset, which is the production default (off).
+
#### Integration tests
Integration tests are defined in `tests/tasks/test/`.
@@ -795,6 +802,17 @@ Struct definitions make error message strings slightly clearer.
Note: not all errors do this at the moment, and we will change over time.
+## Merging into main
+
+A pull request into `main` needs:
+
+- the `Build (Rust MSRV)` check from `test.yml` to pass on the PR. A manually dispatched run of the workflow does not satisfy it;
+- one approving review;
+- signed commits.
+
+Pushing after an approval does not dismiss it.
+The ruleset also requires an extra approval for unattributed changes, so a commit pushed after the approval by an account other than the PR's author can leave the PR blocked until it is approved again.
+
## Releasing
Releases are published via GitHub Actions when a GitHub release is created.
@@ -846,6 +864,20 @@ git push origin vX.Y.Z
gh release create vX.Y.Z --generate-notes
```
+### Patch releases of an older line (for example 2.2.x)
+
+Patch releases of an older major or minor line are cut from its maintenance branch (for example `release/2.2`), not from `main`.
+`mise run release` refuses any branch but `main`, so use the manual steps, with the prepare-release PR opened against the maintenance branch and the tag created on its merged commit.
+
+The release workflow runs from the tagged commit, so it uses that branch's `release.yml`.
+`docker/metadata-action` defaults to `latest=auto`, which tags the image `cipherstash/proxy:latest` for a release tag.
+Unless the branch's `release.yml` sets `flavor: latest=false` on its metadata steps, an older-line release moves `latest` off the current line.
+Create the GitHub release with `--latest=false`, and pass `--notes-start-tag` with the previous tag on that line so the generated notes do not diff against a newer line's tag:
+
+```bash
+gh release create v2.2.6 --target release/2.2 --latest=false --generate-notes --notes-start-tag v2.2.5
+```
+
### Re-releasing a version
If a release fails and you need to re-release the same version:
diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md
index ed19719a1..5674cf0b8 100644
--- a/docs/agents/issue-tracker.md
+++ b/docs/agents/issue-tracker.md
@@ -64,8 +64,12 @@ The Product Engineering workflow:
## Linking PRs to issues
-Reference the issue key in the PR title or body (e.g. `CIP-3233`) so Linear's GitHub
-integration attaches the PR to the issue automatically.
+This repository is public and Linear is private.
+Never put a Linear issue key (`CIP-####`) or a `linear.app` link in a PR title, PR body, PR comment, commit message, branch name or code comment.
+Describe the change in its own words instead.
+
+Link the other way: add the PR URL to the Linear issue as a link.
+Before opening a PR or pushing, check the PR text and `git log ..HEAD --format=%B` for `CIP-`.
## PRs as a request surface
diff --git a/packages/cipherstash-proxy-integration/src/select/unmappable.rs b/packages/cipherstash-proxy-integration/src/select/unmappable.rs
index 194a3cd43..b4c8e8e2a 100644
--- a/packages/cipherstash-proxy-integration/src/select/unmappable.rs
+++ b/packages/cipherstash-proxy-integration/src/select/unmappable.rs
@@ -5,7 +5,9 @@ mod tests {
///
/// Tests unmappble statements return an error in tests.
///
- /// `enable_mapping_errors` should be `true` in the test configuration.`
+ /// Integration tests run with `enable_mapping_errors` off (the default), so the type
+ /// check failure is not raised by Proxy; the statement is forwarded and PostgreSQL
+ /// rejects it because the table does not exist.
///
/// Test ensures that unmappable SQL statements return an error
///
diff --git a/packages/cipherstash-proxy/CONTEXT.md b/packages/cipherstash-proxy/CONTEXT.md
index 3d318b1d3..dadf7774c 100644
--- a/packages/cipherstash-proxy/CONTEXT.md
+++ b/packages/cipherstash-proxy/CONTEXT.md
@@ -60,6 +60,7 @@ statement is forwarded only for a named reason: `SET CIPHERSTASH.UNSAFE_DISABLE_
was issued on this connection, EQL Mapper does not type check its kind, it touches no
encrypted column, or the **type-check fallback**. These are not interchangeable.
Name the condition rather than saying "passthrough" alone.
+Kinds EQL Mapper does not type check include `DO` and `CALL`: Proxy cannot see the SQL their bodies run, so they are forwarded as written, which is a known limitation.
**Route**:
What Proxy does with one statement from a Query or Parse: map it, forward it for a named
@@ -155,6 +156,8 @@ state.
An immutable, monotonically versioned pair of database structure and column encryption
metadata loaded from PostgreSQL. The pair is published atomically because a table without its
encryption policy (or an encryption policy without its table) is not a valid observable state.
+It holds the tables in the schemas on the search path of Proxy's own loading connection, each recorded with its schema.
+A bare table name found in more than one of those schemas does not resolve, because a client can change its `search_path` without Proxy seeing it; when encrypted columns are configured, a write that uses it is refused.
**Transaction schema overlay**:
The confirmed effects of successful DDL executions in one connection's current transaction.
diff --git a/tests/mise.tcp.toml b/tests/mise.tcp.toml
index a017e33a7..d1a5fe8c1 100644
--- a/tests/mise.tcp.toml
+++ b/tests/mise.tcp.toml
@@ -3,5 +3,4 @@ CS_DATABASE__HOST = "postgres"
CS_DATABASE__PORT = "5532"
CS_PROMETHEUS__ENABLED = "true"
CS_LOG__LEVEL = "debug"
-CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS = "true"
CS_DEVELOPMENT__DISABLE_MAPPING = "false"
diff --git a/tests/mise.tls.toml b/tests/mise.tls.toml
index 7de9349bd..02fee6923 100644
--- a/tests/mise.tls.toml
+++ b/tests/mise.tls.toml
@@ -6,4 +6,3 @@ CS_TLS__TYPE = "Path"
CS_TLS__CERTIFICATE_PATH = "/etc/cipherstash-proxy/server.cert"
CS_TLS__PRIVATE_KEY_PATH = "/etc/cipherstash-proxy/server.key"
CS_SERVER__REQUIRE_TLS = "true"
-CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS = "true"