agentleFS
aws-samplesaws-samples/sample-aws-genai-db-modernizer/AGENTS.md

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.

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
  1. Distinctive:

    Every phase that can use a model splits into three pieces, so the same code serves Bedrock, Claude Code and fully deterministic runs:

    line 49
  2. Very specific:

    Run uv sync in each worktree, then the SDK install from Setup (uv sync removes it).

    line 92
  3. Distinctive:

    Before fanning out, list each issue's files.

    line 95
  4. Very specific:

    Read e2e-llm results in test-results/llm-<mode>-<fixture>/: results.json (one merged row: checks, cost, turns, pass, the per-agent …

    line 133
  5. Very specific:

    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 …

    line 145
  6. Very specific:

    If the UI can't start, it falls back to chat and keeps going rather than failing the run.

    line 151
  7. Very specific:

    Every command, and every dispatch's task text, carries this tool-use rule word for word (enforced by …

    line 160
  8. Very specific:

    Every engine change is a Move: only move_ledger.apply / apply_to_assignment write assigned_engine and tied_engines, with no exceptions …

    line 260
  9. Distinctive:

    Never bend a sample, fixture or expectation to keep a result.

    line 274
  10. Distinctive:

    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 …

    line 284
  11. Very specific:

    Never add Co-Authored-By trailers (also hook-enforced, see above).

    line 295
  12. Distinctive:

    Write tests first; keep PRs small.

    line 296

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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 /modernize or any other .claude/commands/*.md command, that command file is authoritative. Nothing here adds steps, questions or tools to it.

Repo map

PathWhat 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.jsonPermission 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 which uv run python scripts/<script>.py ... to run and which src/skills/*.md prompt to follow. The scripts do all deterministic work and write artifacts.
  • /modernize is a lightweight orchestrator: it reads .modernizer-state.json and script stdout, and dispatches one fresh subagent per LLM phase.
  • uv run python scripts/validate_skills.py checks that every script and src/skills/ path a command names exists (also part of make 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:

  1. run_*_deterministic(...): the complete result with no model call (e.g. run_reality_check_deterministic, run_synthesis_deterministic).
  2. prepare_*_llm_input(det): the payload a model reasons over (prepare_reality_check_llm_input, prepare_synthesis_llm_input, prepare_schema_design_input).
  3. 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 uses finalize_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)

ModeWho reasonsNotes
externalClaude 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
bedrockStrands agents on Amazon Bedrockrun_assessment.py --file <f> --all -y --llm-mode bedrock
noneNobodyDeterministic 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 it

The 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 sync in each worktree, then the SDK install from Setup (uv sync removes it); a worktree's .venv is its own.
  • The main clone's editable install points at its own src/, so if you reuse it from another worktree, set PYTHONPATH and UV_NO_SYNC=1 instead.
  • 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 --check names 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

TierCommandChecksCost / time
Lintmake 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 findingsfree: ci/lint.sh seconds, make lint about 2 minutes
Unit/contractmake 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 4unit, contract, property, graph; --fail-on-skip, no networkfree; the full suite takes minutes (15-20 on a laptop with parallel worktrees running; measured 924-1229 s)
Deterministic e2emake e2e (or ./ci/e2e.sh)full pipeline on both samples with no model, HTML in Chromium + WebKit, PDF, UI smoke, the built help sitefree, minutes (UI build + browsers)
Headless LLM e2emake 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 itreal 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 runE2E_LLM_DRY_RUN=1 E2E_LLM_TRANSCRIPT=<transcript.jsonl> ./ci/e2e-llm.sh chat wordpresseverything after the model call, replayed from a saved transcriptfree, needs an existing job under artifacts/
  • Python change in src/ or scripts/: make lint (or ./ci/lint.sh), ./ci/pre_merge.sh --quick and 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.sh without --quick does) 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). If ci/test.sh stops at once with a missing-SDK line, run the uv pip install -r src/atx_orchestrator/requirements-sdk.txt it prints (./scripts/setup_dev.sh is 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_run verdict); 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/e2e test that works only on the deterministic job (it skips when run.external) needs @pytest.mark.deterministic, or e2e-llm fails on --fail-on-skip; tests/unit/ci/test_e2e_deterministic_markers.py enforces it. <!-- claim: tests/unit/ci/test_e2e_deterministic_markers.py::test_unmarked_e2e_tests_never_reach_a_skip -->
  • Pipeline output or src/report/: add make e2e (or uv run pytest tests/e2e/test_pipeline.py -p no:xdist -q for a quick pass).
  • make e2e-llm when ./ci/needs_llm_run.sh prints e2e-llm: required (its tier lists are the source of truth; bedrock-smoke covers Bedrock call sites). required is 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. nightly is 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 on main covers it. Unsure of a path's tier? Treat it as prompt tier (ci/README.md).
  • E2E_LLM_MODE is chat, ui or both; E2E_LLM_FIXTURE is wordpress (fast iteration) or discourse (scale).
  • Read e2e-llm results in test-results/llm-<mode>-<fixture>/: results.json (one merged row: checks, cost, turns, pass, the per-agent token_usage table and soft-budget warnings; compare runs with ci/llm/compare_costs.py), judge.json (rubric scores, rubric in ci/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_postgresql makes it a scoped run (--target-databases, the customer's engine scope); discourse scoped that way costs about $56 (44 DynamoDB parts), so raise E2E_LLM_MAX_BUDGET_USD for it.
  • 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 model, and CLAUDE_BIN pointing at the claude CLI itself fixes it.
  • --auto never asks the user anything; /modernize never asks an experience-mode question at all. Default (and --auto alone) means both (chat + local UI); --mode chat is chat only, --mode ui is 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's ui_url from scripts/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> or MODERNIZE_RESULT: failed phase=<phase> reason=<one line>.
  • The headless session may have no Grep tool, and grep through Bash is denied. Search with uv 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/limit for large files). Search file contents with uv run python scripts/search_artifacts.py <regex> <path> (or the Grep tool if this session has one). Use Bash only for the documented uv run python scripts/… commands; never use cat, jq, python3 -c, sed, ls, cd chains, heredocs or grep.

  • .claude/settings.ci.json allows only the pipeline's uv run python scripts/... commands, Edit/Write under artifacts/** and .modernizer-state.json, Agent, and the dispatched skills; it denies web access, git push/commit, rm, curl and 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.py writes its progress to artifacts/<db>/<job>/_logs/run_assessment.log and prints a final {"log": ...} line (--verbose restores full stdout).
  • scripts/print_summary.py is the one pipeline script whose stdout is Markdown, not a JSON status line: /modernize quotes its --gate and --final output verbatim instead of composing the approval-gate block and the final summary, and a failure is one JSON {"status": "error", ...} line. With --mode ui|both both 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 drives tables_accessed, #557, #553), referee-triage/triage.json
  • collector/routine_sources.json and collector/routine_sources/part-<n>.json.gz: each PostgreSQL, MySQL and MariaDB routine's CREATE, literals included (#1652; exact says whether it is the server's own text); the one unredacted part of a job, which no report, model input or API response reads
  • analysis-<engine>/analysis.json (+ decision-trace.json)
  • assignment/v<N>/assignment.json: versioned; reality check and customer edits write a new version, never overwrite one
  • assignment/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.json for the latest run, and v<N>/output.json for 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.json plus a per-version copy of the rendered files, keyed on the assignment version
  • deliverables/: the customer folder scripts/run_report.py fills for the latest version, with stable names and an index.md; the one list of its files is FOLDER_ENTRIES in src/report/deliverables.py, and tests/unit/docs/test_deliverables_lists.py keeps 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 there
  • decisions/ (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_mappings is 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 from src/shared/engine_names.py (display_engine), never a local copy.
  • Renderers only format. Synthesis computes each customer-facing figure once into report.json's facts; 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 into src/shared/report_facts.py first (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 an untrusted_data_notice key via with_untrusted_notice: the first key, except in the reality-check request, where it follows read_pages. Both are escaped with the helpers in src/report/escaping.py (md_cell, md_text, md_code, mermaid_label, ...) when rendered. Generated DDL types pass an allowlist (validate_aurora_type in src/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_engine is a summary label.
  • There is one scorer: query_scoring.score_query_for_engine, used with ScoringContext.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, using settled_counts only. test_no_engine_bias.py fails 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_assignment write assigned_engine and tied_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.

  1. 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).
  2. Revert each part of the fix and check that some test fails. If none fails, that part is untested. ./ci/pre_merge.sh --mutation does it per committed src/ and scripts/ hunk (ci/hunk_revert.py, minutes) and lists the hunks no new or changed test notices; it skips only Python comment and whitespace hunks.
  3. Cover the negative and edge cases: what must not change, ties, empty input, ambiguity. Bugs hide where the happy path does not look.
  4. 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.
  5. 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).
  6. 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.py loads 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").
  7. 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").
  8. Make timing and resource checks relative (a ratio against a same-process reference), so local runs match CI.
  9. Run ./ci/pre_merge.sh before pushing: it checks conflicts, contract versions, new tests against main, lint, the security scan, tests and the needs_llm_run verdict 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 -->
  10. Pass the source dialect or engine wherever SQL is parsed. A default dialect silently misparses other engines' syntax.
  11. 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] ... (label bug) or [Feature] ... (label enhancement), 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 (or Refs #N for 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). A check-commit-message pre-commit hook and ci/check_commits.sh enforce 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.json and .claude/settings.ci.json) refuse git commit --no-verify, hand edits of generated baselines and of contract versions (declare a bump with scripts/bump_contract.py), schema design before /modernize has 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 --auto turn that would end with nothing running and no MODERNIZE_RESULT line; see ci/README.md.
  • Never add Co-Authored-By trailers (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).

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.