harborclerk / alembic
AGENTS.md3 starsChanged 31 days agoMITView on GitHub
Async Alembic migrations against PostgreSQL 18 with vector, pg_trgm, and citext. The current schema is whatever versions/ produces; the table list in docs/architecture.md is generated from SQLAlchemy metadata, not hand-written. Both postgresql.ENUM and sa.Enum send the Python member .name, not its .value. So this works: and this silently produces ADMIN in SQL against a type whose label is admin: chunks.embedding is vector(768) (Granite-R2). schema_metadata records the embedding model and dimension, and startup refuses to run against a mismatch. Changing the…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository9 other files it gives its agents.
- AGENTS.mdAGENTS.md
- frontend/AGENTS.mdAGENTS.md
- macos/AGENTS.mdAGENTS.md
- acceptanceSkill
- benchmarkSkill
- build-macosSkill
- model-refreshSkill
- verifySkill
- harbor-clerkSkill
The file
We split this AGENTS.md into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 113
When the change rewrites chunks anyway, build the new index in the same transaction, not concurrently (0011): ACCESS EXCLUSIVE is already …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md — alembic
Async Alembic migrations against PostgreSQL 18 with vector, pg_trgm, and
citext. The current schema is whatever versions/ produces; the table list in
docs/architecture.md is generated from SQLAlchemy metadata, not hand-written.
Enum member names must be lowercase
Both postgresql.ENUM and sa.Enum send the Python member .name, not its
.value. So this works:
class Role(enum.Enum):
admin = "admin" # correct — name matches the PG labeland this silently produces ADMIN in SQL against a type whose label is admin:
class Role(enum.Enum):
ADMIN = "admin" # wrongEmbedding dimension is pinned by a sentinel
chunks.embedding is vector(768) (Granite-R2). schema_metadata records the
embedding model and dimension, and startup refuses to run against a
mismatch. Changing the model is a migration plus a re-embed, not a config
edit — a dimension change invalidates every stored vector.
No tenant_id
Single-tenant by design. No tenant table, no tenant column, not in a migration, not "for later".
created_at is not an insertion order
server_default=now() is the transaction's start, so every row one
transaction writes ties on it, and the ids are random UUIDs. Ordering by
created_at alone returns ties in heap order. chat_stream writes a whole turn
in one transaction, which sent tool results before their calls (#809):
chat_messages.seq (0010) is an identity, and every reader orders by
(created_at, seq); tests/test_chat_message_order.py guards both. A new
table whose readers need the order rows were written in needs the same key.
Log-table conventions
Audit and request logs follow a shape established by imap_command_log, and new
ones should match so a single admin view can serve them:
- table named
<domain>_log - typed FK columns (
account_id,api_key_id) rather than opaque strings created_atindexed, because the retention reaper scans itdetailsas JSONB for variable payloads; scalar columns for fixed shapes- an explicit per-table retention policy
Generated columns
chunks.fts_en and chunks.fts_fr are generated stored tsvector columns
with GIN indexes — they are computed by PostgreSQL, so never write to them from
application code. Since 0011 (#828; 0009, #825, before it) they are computed
over hc_fold_search_text(chunk_text): under LC_CTYPE C (the Mac app) the
parser reads every non-ASCII character, an NBSP or a curly apostrophe, as a
letter. Build every tsquery against them through search.websearch_tsquery,
which folds the query the same way (text_contains.fold_search_text, held to
the SQL function by tests/test_search_fold.py). The model's Computed(...)
text is the migration's, verbatim (a test compares them).
Changing a stored generated expression (ALTER COLUMN ... SET EXPRESSION)
rewrites chunks under ACCESS EXCLUSIVE and rebuilds every index on it,
the HNSW index included (40,000 chunks: 0009 about 26 s, 16 s of it HNSW, when
measured for #827; 0011 and 0009 both 39-40 s on one machine on one day). The
time of an expression in a generated column is paid once per column per row:
0011's fold costs 1.9 s per 40,000 chunks against 0.6 s for 0008's. A rewrite
also rebuilds, and so validates, an INVALID index left by an interrupted
concurrent build. Killing the alembic client does not stop its backend: the lock
lasts for the whole rewrite unless the backend is cancelled. env.py names its
session from HC_MIGRATION_APPLICATION_NAME (harbor_clerk.migration_session),
and anything that runs alembic with a deadline cancels by that name, as the
/system/run-migrations route does.
Migration runs are serial; wait for the lock by polling
env.py holds a session-level advisory lock (MIGRATION_LOCK_KEY in
harbor_clerk.migration_session) for the whole run, so a second runner waits
for the one in progress and then finds head, instead of rewriting chunks
again. Every online alembic command goes through it: alembic current and
alembic stamp now wait behind a running migration. The Run Migrations route
refuses with 409 while the lock is held rather than queueing.
Never wait with pg_advisory_lock. A backend blocked in it holds a
snapshot; CREATE INDEX CONCURRENTLY (0008) waits for every older snapshot in
the database while the waiter waits for the lock the build's session holds: a
deadlock, and the detector killed one of the two runs (the builder, mid-build,
leaves an INVALID index). env.py polls pg_try_advisory_lock once a second and
ends the transaction after every try. A transaction left open across tries is
worse: the deadlock becomes an undetected hang, because the build waits for
that transaction to end.
Functions under an expression index are never redefined in place
ix_chunks_chunk_text_fold_trgm indexes hc_fold_search_text(chunk_text),
and fts_en / fts_fr store values computed with it (0011, #828). Both hold
values computed with the function body of the day they were written; CREATE
OR REPLACE on that function leaves every stored entry stale, and the planner
keeps using the index, so text_contains and keyword search silently miss
rows. A changed fold is a new function name in a new migration that builds
a new index on it, resets the column expressions, and drops the old ones, as
0011 did to 0008's hc_fold_whitespace. Each fold migration creates its
function only if absent and raises if an existing one's definition differs.
DROP FUNCTION ... CASCADE on it drops both FTS columns.
When the change rewrites chunks anyway, build the new index in the same
transaction, not concurrently (0011): ACCESS EXCLUSIVE is already held, a
concurrent build would be rebuilt by the rewrite, and an interrupted run rolls
back whole with no INVALID leftover. Drop the old index before the rewrite so
it is not rebuilt only to be dropped. Expression-index statistics come only
from an ANALYZE with no column list.
Concurrent index builds must survive an interrupted attempt
An interrupted CREATE INDEX CONCURRENTLY leaves an INVALID index and the
revision unstamped; the retry's IF NOT EXISTS then skips it. Every startup
path retries on its own (MigrationRunner, Docker restart), so a migration that
builds concurrently drops an invalid leftover first and checks indisvalid
before dropping anything the new index replaces (0008's
_build_index_concurrently).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

