sample-aws-genai-db-modernizer
AGENTS.md14 starsChanged 8 days agoMIT-0View on GitHub
Working guide for coding agents and power users of this repository. It is a map, not a manual: human onboarding lives in CONTRIBUTING.md and the CI scripts are documented in ci/README.md. Running a pipeline command? If this session was started to run /modernize or any other .claude/commands/*.md command, that command file is authoritative. Nothing here adds steps, questions or tools to it.
- Installs packages
- Commits and pushes
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 repositoryOne other file it gives its agents.
- CLAUDE.mdCLAUDE.md
The file
We split this AGENTS.md into the rules it gives an agent
12 rules. About 0 are common, 5 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 49
Every phase that can use a model splits into three pieces, so the same code serves Bedrock, Claude Code and fully deterministic runs:
- Very specific: line 92
Run uv sync in each worktree, then the SDK install from Setup (uv sync removes it).
- Distinctive: line 95
Before fanning out, list each issue's files.
- Very specific: line 133
Read e2e-llm results in test-results/llm-<mode>-<fixture>/: results.json (one merged row: checks, cost, turns, pass, the per-agent …
- Very specific: line 145
After the run, check-model fails it when the transcript's model is not ANTHROPIC_MODEL: a claude wrapper script on PATH can choose its own …
- Very specific: line 151
If the UI can't start, it falls back to chat and keeps going rather than failing the run.
- Very specific: line 160
Every command, and every dispatch's task text, carries this tool-use rule word for word (enforced by …
- Very specific: line 260
Every engine change is a Move: only move_ledger.apply / apply_to_assignment write assigned_engine and tied_engines, with no exceptions …
- Distinctive: line 274
Never bend a sample, fixture or expectation to keep a result.
- Distinctive: line 284
Every follow-up or known limit listed in a review or PR becomes an issue before the PR merges, and joins an epic as a sub-issue …
- Very specific: line 295
Never add Co-Authored-By trailers (also hook-enforced, see above).
- Distinctive: line 296
Write tests first; keep PRs small.
≈ “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
Working guide for coding agents and power users of this repository. It is a map, not a manual: human onboarding lives in CONTRIBUTING.md and the CI scripts are documented in ci/README.md.
Running a pipeline command? If this session was started to run
/modernizeor any other.claude/commands/*.mdcommand, that command file is authoritative. Nothing here adds steps, questions or tools to it.
Repo map
| Path | What lives there |
src/agents/ | Pipeline agents: collector/, analysis/ (one per engine), referee/ (triage, reality check, synthesis), schema_design/, load_test/; prompt_framing.py |
src/contracts/ | Pydantic models for every agent's input and output (contract changes: see CONTRIBUTING.md) |
src/tools/ | Deterministic tools: analysis/, schema/, validation/, database/, aws/ |
src/report/ | Deliverable renderers (engineering Markdown, landscape executive report PDF) from one synthesis report.json; escaping.py |
src/shared/ | Cross-cutting helpers, including engine_names.py (engine display names) |
src/catalog/ | Rule catalog (ADR-031 Decision 8): engines/ one TOML per engine (display name, colours, status, role), patterns/ one TOML per pattern type (per-engine fit score, anti-pattern severity, penalty, why), gaps/ one TOML per relational-move gap rule with aurora_versions.toml and aurora_postgresql_extensions.toml (#471, #1078, read at synthesis, never by routing<!-- claim: tests/unit/agents/referee/test_routing_never_reads_gaps.py::test_no_routing_module_reaches_the_gap_check, tests/unit/agents/referee/test_synthesis_relational_gaps.py::test_synthesis_writes_the_gaps_of_the_retained_aurora_engine -->), write_intents/ one TOML per write intent with carried_writes.toml (#1362, the target-native way to serve a write-group member its engine fits poorly; designer input and risk register, never routing), loaded by src/shared/catalog/; scripts/render_catalog.py generates the UI's engineCatalog.generated.js and docs/architecture/rule-catalog.md (--check runs in lint) |
src/skills/ | Markdown prompts (data-modeling and PE-review guidance per engine) that agents and commands load |
src/orchestrator/, src/storage/, src/graph/ | Local phase orchestrator, artifact store and assignment versioning, context graph (with explorer.py/explorer_overlays.py, the graph explorer's bounded read-only queries) |
src/api/, src/ui/ | FastAPI backend and React UI (the results page, with the wave plan and the deliverables download panel; the experimental graph explorer at /analysis/explore/<job_id>, off unless the API runs with MODERNIZER_EXPERIMENTAL_GRAPH_UI=1: start it with scripts/start_local_ui.py --experimental-graph-explorer or make explore JOB=<id>, never in headless runs; user guide docs/explore-the-graph.md) |
src/assessment/ | The public library entry point assess(request) -> result: typed request and result, the stable surface (summary, statements[], groups[]) over the internal contracts (docs/contracts/assess.md) |
src/atx_orchestrator/ | AWS Transform integration |
scripts/ | CLI entry points (run_*.py) the commands call, plus validators and helpers |
.claude/commands/ | Claude Code slash commands (/modernize, /analyze-<engine>, /design-schema-<engine>, /reality-check, /synthesize, /explore for questions about an assessed job, ...) |
.claude/agents/ | Subagent definitions: pr-reviewer.md, and dynamodb-group-draft.md (the DynamoDB group drafts' shared, cached prompt; generated by scripts/render_group_draft_agent.py --write, --check runs in lint) |
.claude/settings.ci.json | Permission allowlist for headless runs |
ci/ | CI-agnostic entry points (lint.sh, test.sh, e2e.sh, e2e-llm.sh) and the quality judge (ci/llm/) |
tests/ | unit/, contract/, property/, graph/ (gated), e2e/ (deterministic end-to-end), integration/ (live stack, not gated) |
contrib/ | Quarantined third-party snapshot (cdc-elasticache-prototype/, #1412): never built, imported or tested; ported piece by piece; tests/unit/test_contrib_quarantine.py guards it |
benchmarks/ | Assessment quality benchmark: scores pipeline output against reviewed answer keys (ADR-030); how to run it: benchmarks/README.md |
docs/examples/ | Sample fixtures: wordpress/ and discourse/ |
mkdocs.yml, docs/ (minus exclude_docs) | Help site sources (scripts/build_docs_sample.py renders the sample report into it; .github/workflows/docs.yml builds and deploys it) |
How commands, skills and scripts fit together
- A command (
.claude/commands/<name>.md) tells Claude Code whichuv run python scripts/<script>.py ...to run and whichsrc/skills/*.mdprompt to follow. The scripts do all deterministic work and write artifacts. /modernizeis a lightweight orchestrator: it reads.modernizer-state.jsonand script stdout, and dispatches one fresh subagent per LLM phase.uv run python scripts/validate_skills.pychecks that every script andsrc/skills/path a command names exists (also part ofmake lint).
The LLM seam pattern
Every phase that can use a model splits into three pieces, so the same code serves Bedrock, Claude Code and fully deterministic runs:
run_*_deterministic(...): the complete result with no model call (e.g.run_reality_check_deterministic,run_synthesis_deterministic).prepare_*_llm_input(det): the payload a model reasons over (prepare_reality_check_llm_input,prepare_synthesis_llm_input,prepare_schema_design_input).apply_*_llm_output(det, llm_output): merges and validates the model's answer on top of the deterministic result (apply_reality_check_llm_output,apply_synthesis_llm_output; schema design usesfinalize_schema_design).
Analysis agents expose the same shape (see the docstrings in
src/agents/analysis/aurora_*_analysis_agent.py).
The three LLM modes (--llm-mode)
| Mode | Who reasons | Notes |
external | Claude Code (the default for run_assessment.py) | The script writes llm_input.json and stops (awaiting_llm); the command writes the response, then runs --finalize / --resume-reality-check |
bedrock | Strands agents on Amazon Bedrock | run_assessment.py --file <f> --all -y --llm-mode bedrock |
none | Nobody | Deterministic only, no model calls |
With --llm-mode none: reality check keeps its deterministic result,
synthesis keeps the deterministic executive summary, and schema design is
skipped with reason llm_mode=none: every schema designer needs a model
(every designer needs a model). run_assessment.py always runs the analysis
phase deterministically, whatever --llm-mode says.
Setup
./scripts/setup_dev.sh # uv sync --extra dev, the AWS Transform SDK, pre-commit hooks
uv sync --extra dev --extra e2e # add the e2e extra (Playwright, pypdf)
uv sync --extra docs # add the docs extra (mkdocs, mkdocs-material) to build the help site
uv pip install -r src/atx_orchestrator/requirements-sdk.txt # the AWS Transform SDK (4 pinned packages) the atx_orchestrator tests need; repeat after any uv sync, which removes itThe UI needs Node 22 (npm ci, then npm run build, in src/ui/); ci/e2e.sh
and ci/e2e-llm.sh build it for you.
Parallel worktrees
- Run
uv syncin each worktree, then the SDK install from Setup (uv syncremoves it); a worktree's.venvis its own. - The main clone's editable install points at its own
src/, so if you reuse it from another worktree, setPYTHONPATHandUV_NO_SYNC=1instead. - Before fanning out, list each issue's files. Issues that share a file are not independent: land the shared extraction first, decide the merge order, then parallelize the rest.
- Regenerate generated baselines (the routing snapshot, the contract schema
snapshot) on top of main on every rebase; never hand-merge them.
uv run python scripts/regenerate_baselines.py --checknames the stale ones and their commands;uv run python scripts/regenerate_baselines.py --reason "why it moved (#NNN)"regenerates the stale ones (#1163).
Which test tier for which change
| Tier | Command | Checks | Cost / time |
| Lint | make lint (all pre-commit hooks) or ./ci/lint.sh (ruff, black, isort, mypy, validate_skills.py); ./ci/security.sh (semgrep with the rules ci/security.toml pins, gitleaks, bandit; --quick for the changed files) <!-- claim: tests/unit/ci/test_security_parity.py::test_semgrep_engine_and_rule_sources_are_pinned, tests/unit/ci/test_security.py::test_select_files_quick_asks_for_the_changed_files --> | style, types, markdownlint, secrets, command references, security findings | free: ci/lint.sh seconds, make lint about 2 minutes |
| Unit/contract | make test / ./ci/test.sh -q (the full suites; needs the AWS Transform SDK, see Setup); while iterating, targeted tests, e.g. uv run pytest <paths> -n 4 | unit, contract, property, graph; --fail-on-skip, no network | free; the full suite takes minutes (15-20 on a laptop with parallel worktrees running; measured 924-1229 s) |
| Deterministic e2e | make e2e (or ./ci/e2e.sh) | full pipeline on both samples with no model, HTML in Chromium + WebKit, PDF, UI smoke, the built help site | free, minutes (UI build + browsers) |
| Headless LLM e2e | make e2e-llm (both mode by default, E2E_LLM_FIXTURE=wordpress by default) | real /modernize --auto run, the same deliverable checks, a rubric judge; per PR only when ./ci/needs_llm_run.sh prints e2e-llm: required (prompt tier), otherwise the maintainers' nightly run on main covers it | real tokens, both mode: wordpress about $4 and roughly 6 minutes; discourse about $18-20 and 14-15 minutes (session cap E2E_LLM_MAX_BUDGET_USD, default 50) |
| Real-run replay | ./ci/replay.sh (also run by ./ci/pre_merge.sh and the replay e2e shard) | the saved real-model run in tests/fixtures/real_runs/: its transcript checks and the external-mode e2e checks over its report, re-rendered by the current code; it does not re-run synthesis or the facts layer (refresh the fixture after such a change, see ci/README.md) | free, about 15 seconds, dev extra only |
| e2e-llm dry run | E2E_LLM_DRY_RUN=1 E2E_LLM_TRANSCRIPT=<transcript.jsonl> ./ci/e2e-llm.sh chat wordpress | everything after the model call, replayed from a saved transcript | free, needs an existing job under artifacts/ |
- Python change in
src/orscripts/:make lint(or./ci/lint.sh),./ci/pre_merge.sh --quickand the targeted tests for what you touched, with at most 4 workers (-n 4). Parallel worktrees compete for CPU, so keep the worker count low while iterating. Before asking for review, run the full./ci/test.sh -q(./ci/pre_merge.shwithout--quickdoes) and make it green: the unit, contract and property suites are not CI-only. Only the e2e and real-model tiers (make e2e,make e2e-llm) are left to CI unless the change needs them (below). Ifci/test.shstops at once with a missing-SDK line, run theuv pip install -r src/atx_orchestrator/requirements-sdk.txtit prints (./scripts/setup_dev.shis for first setup; it changes system git config). - Before pushing or asking for a merge:
./ci/pre_merge.sh(fetch, behind count, conflict and contract-version checks, new tests against main, lint, the security scan, tests,needs_llm_runverdict); see CONTRIBUTING.md's "Working with a fast-moving main".<!-- claim: tests/unit/ci/test_security_parity.py::test_pre_merge_runs_the_script_quick_on_the_branch_and_full_otherwise --> - A red required check in any CI environment stops merges, even when it fails on code the PR didn't touch: file it and fix it first.
- A
tests/e2etest that works only on the deterministic job (it skips whenrun.external) needs@pytest.mark.deterministic, ore2e-llmfails on--fail-on-skip;tests/unit/ci/test_e2e_deterministic_markers.pyenforces it.<!-- claim: tests/unit/ci/test_e2e_deterministic_markers.py::test_unmarked_e2e_tests_never_reach_a_skip --> - Pipeline output or
src/report/: addmake e2e(oruv run pytest tests/e2e/test_pipeline.py -p no:xdist -qfor a quick pass). make e2e-llmwhen./ci/needs_llm_run.shprintse2e-llm: required(its tier lists are the source of truth;bedrock-smokecovers Bedrock call sites).requiredis the prompt tier: files that write, word, frame or tag text sent to a model (skills, commands, prompt and request builders, framing, catalog prose that reaches a model word for word, model-output contracts, model settings); it needs a result line in PR Evidence.nightlyis pipeline logic whose output a model only reads as data (routing, scoring, pattern catalogs, collectors, synthesis math) and the model-call plumbing: no per-PR run and no result line; the maintainers' nightly run onmaincovers it. Unsure of a path's tier? Treat it as prompt tier (ci/README.md).E2E_LLM_MODEischat,uiorboth;E2E_LLM_FIXTUREiswordpress(fast iteration) ordiscourse(scale).- Read e2e-llm results in
test-results/llm-<mode>-<fixture>/:results.json(one merged row: checks, cost, turns,pass, the per-agenttoken_usagetable and soft-budgetwarnings; compare runs withci/llm/compare_costs.py),judge.json(rubric scores, rubric inci/llm/rubric.md),transcript.jsonl,steps.json,deliverables/,job-logs/. - Tests never call a real model; patch the model entry points.
Headless rules (/modernize --auto)
ci/e2e-llm.sh runs claude -p "/modernize <collection.json> --auto --mode <chat|ui|both>"
with --permission-mode dontAsk and .claude/settings.ci.json.
E2E_LLM_TARGET_DATABASES=dynamodb,aurora_postgresqlmakes it a scoped run (--target-databases, the customer's engine scope); discourse scoped that way costs about $56 (44 DynamoDB parts), so raiseE2E_LLM_MAX_BUDGET_USDfor it.- After the run,
check-modelfails it when the transcript's model is notANTHROPIC_MODEL: aclaudewrapper script onPATHcan choose its own model, andCLAUDE_BINpointing at the claude CLI itself fixes it. --autonever asks the user anything;/modernizenever asks an experience-mode question at all. Default (and--autoalone) meansboth(chat + local UI);--mode chatis chat only,--mode uiis UI-first (chat gives only the approval-gate numbers, UI pointers, and the result line -- no phase summaries). If the UI can't start, it falls back to chat and keeps going rather than failing the run. UI pointers quote the job page'sui_urlfromscripts/start_local_ui.py --page <page> --job-id <id>, never the UI root.- The run ends with exactly one line
MODERNIZE_RESULT: complete job_id=<id> db=<db> mode=<mode>orMODERNIZE_RESULT: failed phase=<phase> reason=<one line>. - The headless session may have no Grep tool, and
grepthrough Bash is denied. Search withuv run python scripts/search_artifacts.py <regex> <path>(read-only, repo-contained, capped output). Every command, and every dispatch's task text, carries this tool-use rule word for word (enforced by
tests/unit/scripts/test_modernize_command.py):<!-- claim: tests/unit/scripts/test_modernize_command.py::test_every_command_carries_the_tool_use_rule, tests/unit/scripts/test_modernize_command.py::test_every_dispatch_task_text_carries_the_tool_use_rule -->Read files with the Read tool (use
offset/limitfor large files). Search file contents withuv run python scripts/search_artifacts.py <regex> <path>(or the Grep tool if this session has one). Use Bash only for the documenteduv run python scripts/…commands; never usecat,jq,python3 -c,sed,ls,cdchains, heredocs orgrep..claude/settings.ci.jsonallows only the pipeline'suv run python scripts/...commands,Edit/Writeunderartifacts/**and.modernizer-state.json,Agent, and the dispatched skills; it denies web access,git push/commit,rm,curland secret paths. A new script a command runs needs an allow rule there (rationale for each entry: ci/README.md).- Subagents nest one level deep: only the orchestrator dispatches, and every dispatch says "Do not dispatch subagents yourself."
- Waiting Rule (in
modernize.md): never end a turn waiting unless a dispatched subagent is still running. - Script stdout stays compact for headless sessions: JSON status lines only.
run_assessment.pywrites its progress toartifacts/<db>/<job>/_logs/run_assessment.logand prints a final{"log": ...}line (--verboserestores full stdout). scripts/print_summary.pyis the one pipeline script whose stdout is Markdown, not a JSON status line:/modernizequotes its--gateand--finaloutput verbatim instead of composing the approval-gate block and the final summary, and a failure is one JSON{"status": "error", ...}line. With--mode ui|bothboth blocks carry the job pages' URLs; the headless check fails a run that never quotes them, or quotes the gate's only after schema design starts (check_ui_pointers).- Allowlisted scripts refuse paths outside the repo under
MODERNIZER_CI_SANDBOX=1(scripts/_sandbox.py).
Where artifacts live
Local runs write under artifacts/<db>/<job>/ (ARTIFACT_DIR, default
./artifacts):
collector/output.json,collector/sql_shadow.json(sqlglot facade vs regex table references; the facade drivestables_accessed, #557, #553),referee-triage/triage.jsoncollector/routine_sources.jsonandcollector/routine_sources/part-<n>.json.gz: each PostgreSQL, MySQL and MariaDB routine'sCREATE, literals included (#1652;exactsays whether it is the server's own text); the one unredacted part of a job, which no report, model input or API response readsanalysis-<engine>/analysis.json(+decision-trace.json)assignment/v<N>/assignment.json: versioned; reality check and customer edits write a new version, never overwrite oneassignment/v<N>/access_patterns.json: each query's access-pattern class, class scores and parity-anchor notes, as routing scored it (#728, #936), its redesigned class and the keys routing assumed when a redesign applies, and its anti-pattern findings, each fixable by redesign or intrinsic (#1231), and the score dimensions routing combined per scored engine (pattern, cost, base, applied penalty, score; per-table complexity, performance and weights), a record routing never reads (#1471)reality-check/(llm_input.json,output.jsonfor the latest run, andv<N>/output.jsonfor each completed run, keyed on the assignment version it wrote or, when it changed nothing, checked; readers take the record of the run the effective assignment descends from, never one from another lineage, #193, #215)schema-<engine>/v<N>/schema_output.json,synthesis/v<N>/report.jsonplus a per-version copy of the rendered files, keyed on the assignment versiondeliverables/: the customer folderscripts/run_report.pyfills for the latest version, with stable names and anindex.md; the one list of its files isFOLDER_ENTRIESinsrc/report/deliverables.py, andtests/unit/docs/test_deliverables_lists.pykeeps every docs/command list in step with it.<!-- claim: tests/unit/docs/test_deliverables_lists.py::test_the_documented_set_is_the_set_the_code_writes, tests/unit/docs/test_deliverables_lists.py::test_each_list_names_exactly_the_documented_set -->Model I/O and traces never go theredecisions/(recorded approvals),_logs/(script logs)
artifacts/.trash/ (outside any job folder): job folders being deleted by
DELETE /assessments/{job_id}/artifacts; leftover tombstones older than an
hour are removed, up to 5 per later delete call.
.modernizer-state.json at the repo root is the job cursor, not the source of
truth.
Key invariants
- Assignments map queries to engines. A table can be served by several
engines; the single recommended engine in
table_mappingsis not ownership. - Deliverables agree. The executive report, the engineering report and the UI
render the same
report.json; a fact changed in one must change in all. Engine names come fromsrc/shared/engine_names.py(display_engine), never a local copy. - Renderers only format. Synthesis computes each customer-facing figure once into
report.json'sfacts; renderers read it and never count or total it, in Python (tests/unit/report/test_renderers_only_format.py) and in the UI (src/ui/src/utils/__tests__/rendererOnlyFormat.test.js).<!-- claim: tests/unit/report/test_renderers_only_format.py::test_no_renderer_does_arithmetic_on_a_fact, src/ui/src/utils/__tests__/rendererOnlyFormat.test.js -->A new figure goes intosrc/shared/report_facts.pyfirst (ADR-032). - Untrusted text is data. Customer and LLM text is wrapped with
frame_untrusted(src/agents/prompt_framing.py) in prompts. JSON a model reads instead (an external session's request files, the AWS Transform orchestrator's report and routing-summary tool results) carries anuntrusted_data_noticekey viawith_untrusted_notice: the first key, except in the reality-check request, where it followsread_pages. Both are escaped with the helpers insrc/report/escaping.py(md_cell,md_text,md_code,mermaid_label, ...) when rendered. Generated DDL types pass an allowlist (validate_aurora_typeinsrc/contracts/aurora_design_delta.py), so collector text never reaches DDL unchecked.<!-- claim: tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_a_type_source_that_returns_an_unchecked_type_is_refused, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_column_ddl_is_built_in_one_place_and_it_runs_the_allowlist, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_every_ddl_column_type_is_the_allowlists_answer, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_collector_text_in_generated_ddl_is_only_quoted_or_commented, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_step1_ddl_takes_the_source_schema_and_database_only_as_plain_names, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_step1_refuses_a_source_database_that_is_not_a_plain_name, tests/unit/tools/schema/aurora_common/test_collector_text_never_reaches_ddl.py::test_every_ddl_writer_is_reviewed --> - Engine/query assignment is deterministic; a model may explain it, not decide it.
Routing changes
The routing model lives in one place, ADR-031. This section is a pointer, not a restatement.
- The query is the unit everywhere: routing, waves, deliverable figures, cost
and scope. A table's
primary_engineis a summary label. - There is one scorer:
query_scoring.score_query_for_engine, used withScoringContext.from_pipeline. Never add a private fit function. - A query's pattern dimension is its own access-pattern class's score
(
query_pattern.QueryPatternModel, ADR-031 Decision 14 Amendments 5 and 7), never its tables' pattern scores. - Ties go through
tie_break.break_tie, usingsettled_countsonly.test_no_engine_bias.pyfails an engine picked, outside it and outside the test's reviewed allowlist, by name order, query count, set iteration order, a literal engine or the first element seen.<!-- claim: tests/unit/agents/referee/test_no_engine_bias.py::test_no_unallowlisted_engine_bias_in_referee, tests/unit/agents/referee/test_no_engine_bias.py::test_rule_catches_sorted_index0_name_order_pick, tests/unit/agents/referee/test_no_engine_bias.py::test_rule_catches_min_max_keyed_by_count, tests/unit/agents/referee/test_no_engine_bias.py::test_rule_catches_next_iter_over_a_set_literal, tests/unit/agents/referee/test_no_engine_bias.py::test_rule_catches_literal_engine_preference_assignment, tests/unit/agents/referee/test_no_engine_bias.py::test_rule_catches_for_loop_keeps_first_then_breaks -->Engine removal keeps the source-compatible engine when it scores every query at least as well, unless the alternative is at least 40% and $100/mo cheaper (tie_break.source_compatible_engine_survives, ADR-031 Decision 13).<!-- claim: tests/unit/agents/referee/test_source_compatible_tie_survival.py::TestTheIssueRepro, tests/unit/agents/referee/test_source_compatible_tie_survival.py::TestTheCostException, tests/unit/agents/referee/test_tie_break.py::TestCheaperEngineDisplacesSourceCompatible --> - An override onto an engine that can't serve the query is rejected.
- A table has one writer (ADR-031 Amendment 9): its writes and locking reads form a write group placed as one unit with any co-dependency group it touches (
write_groups.placement_groups) in every pass; its reads stay per query, as CDC replicas. - Every engine change is a Move: only
move_ledger.apply/apply_to_assignmentwriteassigned_engineandtied_engines, with no exceptions (ADR-031 Amendment 3,test_only_the_ledger_writes_engines.py).<!-- claim: tests/unit/agents/referee/test_only_the_ledger_writes_engines.py::test_only_the_move_ledger_writes_engines_and_ties --> - Explain drift with the routing snapshot, and regenerate it on rebase. Never hand-merge it.
Before asking for review
Self-review checklist, from findings that recur in review. Each item says what to do, then why.
- Every new test fails on main. Run it against
origin/main's code; the reviewer will, and a test that passes there guards nothing../ci/pre_merge.sh(also with--quick) runs the branch's new and changed tests on main's code and lists the ones that pass there; mark a test that guards behaviour main already has@pytest.mark.passes_on_main(see ci/README.md). - Revert each part of the fix and check that some test fails. If none fails, that part is untested.
./ci/pre_merge.sh --mutationdoes it per committedsrc/andscripts/hunk (ci/hunk_revert.py, minutes) and lists the hunks no new or changed test notices; it skips only Python comment and whitespace hunks. - Cover the negative and edge cases: what must not change, ties, empty input, ambiguity. Bugs hide where the happy path does not look.
- The PR body states only what this branch shows: its own test counts, timings and drift. "Covered by existing tests" names the tests. Numbers from another branch mislead the reviewer.
- Never bend a sample, fixture or expectation to keep a result. Fix the cause, or change the expectation with a per-query explanation (see Routing changes).
- After renaming an id, field or capability, old saved artifacts must still load (map the legacy value), with a test that loads an old file. Saved jobs outlive the code that wrote them.
tests/fixtures/old_jobs/holds a real older release's job;tests/contract/test_old_job_loads.pyloads it and fails when a stored engine id, access class, pattern key, catalog pattern id, capability or move reason code no longer resolves (other vocabularies are not checked). Fix it with a legacy-map entry; keep the fixture as the release wrote it (CONTRIBUTING.md, "Renaming an id or an enum value"). - Check the change against every ADR decision it touches, not just the one in the issue. ADR text must never claim more than a guard enforces: each behaviour sentence the PR adds or changes in the docs, an ADR or a command file names its test, with a claim marker in the page and a row in the PR's Claims table, or is cut (CONTRIBUTING.md, "Claims name their test").
- Make timing and resource checks relative (a ratio against a same-process reference), so local runs match CI.
- Run
./ci/pre_merge.shbefore pushing: it checks conflicts, contract versions, new tests against main, lint, the security scan, tests and theneeds_llm_runverdict in one pass (see ci/README.md).<!-- claim: tests/unit/ci/test_security_parity.py::test_pre_merge_runs_the_script_quick_on_the_branch_and_full_otherwise --> - Pass the source dialect or engine wherever SQL is parsed. A default dialect silently misparses other engines' syntax.
- Fill in the PR template's "Maintainer decision this implements" with a link to the issue comment that recorded the decision (or "None"), so the reviewer can check the PR against it.
Conventions
- Every follow-up or known limit listed in a review or PR becomes an issue before the PR merges, and joins an epic as a sub-issue (CONTRIBUTING.md, "Follow-ups").
- Issues use the templates in
.github/ISSUE_TEMPLATE/:[Bug] ...(labelbug) or[Feature] ...(labelenhancement), every section filled in. - One commit per issue: a short Conventional Commits subject (at most 72
characters) plus a 2-4 line body (a line or two each on the problem and
the fix), ending
Closes #N(orRefs #Nfor follow-ups). The full explanation, review notes and test results go in the issue and the PR, not the commit (see CONTRIBUTING.md's Commit Message Format). Acheck-commit-messagepre-commit hook andci/check_commits.shenforce this; merge (also in a linked worktree, #1968) and fixup/squash/amend commits are skipped (#570).<!-- claim: tests/unit/scripts/test_check_commit_message.py::test_merge_message_in_a_worktree_merge_is_skipped, tests/unit/scripts/test_conventional_commit_hook.py::test_merge_in_progress_in_a_worktree_skips_the_conventional_check --> - Claude Code hooks in
scripts/hooks/(wired in.claude/settings.jsonand.claude/settings.ci.json) refusegit commit --no-verify, hand edits of generated baselines and of contract versions (declare a bump withscripts/bump_contract.py), schema design before/modernizehas run the approval gate on its own, a Bash call that prints a cloud credential helper's output, a push from a fork PR's worktree to a repository other than the fork and, in headless runs, any Bash call that breaks the tool-use rule and a/modernize --autoturn that would end with nothing running and noMODERNIZE_RESULTline; see ci/README.md. - Never add
Co-Authored-Bytrailers (also hook-enforced, see above); never use--no-verify.<!-- claim: tests/unit/scripts/test_check_commit_message.py::test_co_authored_by_trailer_fails_even_with_a_valid_closes_trailer --> - Write tests first; keep PRs small; open them against
main. - Public wording everywhere (issues, commits, docs, code): this is a public sample repository, so no internal system names or internal links.
- The "Maintainer Decision" label marks an issue or PR waiting for a maintainer, and blocks a PR from merging. Agents may add it; agents never remove it unless a human explicitly says to in that session.
- Maintainers mirroring this repo into a deployment target: see
CONTRIBUTING.md's Maintainer Sync section (
scripts/maintainer-sync.sh).
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.

