Skip to content

Latest commit

 

History

History
211 lines (157 loc) · 20.8 KB

AGENTS.md

File metadata and controls

211 lines (157 loc) · 20.8 KB

Project Structure

This project is a Copier template used to generate other copier templates. It is the "grandparent" of actual instantiated application/library repositories.

Code Guidelines

Code Style

  • Comments should be used very rarely. Code should generally express its intent.
  • Never write a one-line docstring — either the name is sufficient or the behavior warrants a full explanation.
  • Don't name a value that is used once where the literal is already self-explanatory at the point of use — the extra variable adds a line and a hop without adding meaning.
  • Prefer keyword-only parameters (unless a very clear single-argument function): use * in Python signatures and destructured options objects in TypeScript.
  • When disabling a linting rule with an inline directive, provide a comment at the end of the line (or on the line above for tools that don't allow extra text after an inline directive) describing the reasoning for disabling the rule.
  • Avoid telling the type checker what a type is rather than letting it prove it. This includes type assertions (as SomeType in TypeScript, cast() in Python) and variable annotations that override inference. Prefer approaches that let the type checker verify the type itself: isinstance/instanceof narrowing, restructuring code so the correct type flows naturally, or using discriminated unions. When there is genuinely no alternative, add a comment explaining why the workaround is necessary and why it is safe.
  • In non-test code — anything coverage measures — avoid || (TypeScript) or or (Python) in if/elif conditions, and avoid x in ['a', 'b']-style membership tests. Coverage tools treat these as a single branch, silently masking untested paths and producing false 100% branch coverage. Use separate if/elif branches instead so each condition is independently covered. Files under a test directory are exempt because coverage never measures them, so collapsing a condition there hides nothing.

Python

  • Always include type hints.
  • Respect the pyrefly unused-call-result check; assign unneeded return values to _
  • In non-test code, prefer explicit if/else (or a for loop with if/return) over one-line forms that collapse branches: a ternary, d.get(key, default), returning a boolean expression return b > 5/return bool(b), or any()/all() over a generator in place of a loop. coverage.py tracks branches as line-to-line arcs, so a single-line expression hides the untaken path.
  • In non-test code, when filtering logic combines multiple and-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit if/continue branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced.

Testing

  • Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default.

  • Never manually start services prior to running E2E tests. The test harness boots and tears down its own services (backend, frontend, supporting services) via session fixtures.

  • When iterating on a single test, run that test in isolation first and confirm it is in the expected state (red or green) before widening to the full suite. Use the most targeted invocation available: a specific test function for Python (e.g. uv run pytest path/to/test.py::test_name --no-cov) or a file path and name filter for TypeScript (e.g. pnpm test-unit path/to/test.spec.ts -t "test name" --no-coverage). Only run the full suite once the target test behaves as expected.

  • Test coverage requirements are usually at 100%, so when running a subset of tests, always disable test coverage to avoid the test run failing for insufficient coverage.

  • Avoid magic values in comparisons in tests in all languages (like ruff rule PLR2004 specifies). Note: 1 and 0 are not magic numbers (according to PLR2004)

  • Prefer using random values in tests rather than arbitrary ones (e.g. the faker library, uuids, random.randint) when possible. For enums, pick randomly rather than hardcoding one value.

  • Avoid loops in tests — assert each item explicitly so failures pinpoint the exact element. When verifying a condition across all items in a collection, collect the violations into a list and assert it's empty (e.g., assert [x for x in items if bad_condition(x)] == []).

  • When a test's final assertion is an absence (e.g., element is null, list is empty, modal is closed), include a prior presence assertion confirming the expected state existed before the action that removed it. A test whose only assertion is an absence check can pass vacuously if setup silently failed.

  • When asserting a mock or spy was called with specific arguments, always constrain as tightly as possible. In order of preference: (1) assert called exactly once with those args (assert_called_once_with in Python, toHaveBeenCalledExactlyOnceWith in Vitest/Jest); (2) if multiple calls are expected, assert the total call count and use a positional or last-call assertion (nthCalledWith, lastCalledWith / assert_has_calls with call_args_list[n]); (3) plain "called with at any point" (toHaveBeenCalledWith, assert_called_with) is a last resort only when neither the call count nor the call order can reasonably be constrained.

  • When asserting an exception is raised, verify the error message includes all key constructor arguments — not just one identifying field. This ensures the error message is fully populated and catches cases where arguments are swapped or missing. In Python: use the match parameter in pytest.raises. In TypeScript: use a regex or substring in toThrow, or catch and assert on error properties individually.

  • Name tests with Given/When/Then where each clause means a specific thing: When names the single action under test plus the input that distinguishes this case; Given names only preconditions established before that action (fixtures, mocks, prior state) and is omitted when there are none; Then names the asserted outcome.

  • A Given or When clause shared by every test in a group belongs on the enclosing scope (a pytest class, a describe block) rather than repeated in each test name. The chain from the outermost scope through the test name must read as one complete Given/When/Then, and a scope carrying a clause must state it explicitly (TestWhenFooInvoked, describe("Given foo mocked to succeed")).

  • When an exception has a fixed message with no variable data, prefer a specific exception subclass over match — the subclass type is the full assertion, and matching a hardcoded string duplicates the exception class without adding test value. In this case, suppress PT011 with an inline # noqa: PT011 comment explaining why.

  • Structure each test body in this order, with a single blank line separating each section:

    1. Constants — random/faker values and test data objects
    2. Mocks/spies — all spy and patch setups
    3. Arrange — setup calls that establish the precondition (mounting, pre-act interactions, etc.)
    4. (blank line)
    5. Act — the action under test
    6. (blank line)
    7. State capture — variables extracted from the system under test purely for use in assertions (DOM queries, return values, captured state)
    8. (blank line)
    9. Assertions

    For tests with multiple interaction steps (e.g. E2E or complex flows), repeat the Act → State capture → Assertions cycle, with a blank line between each cycle.

    Keep blank lines to a minimum: only where they separate meaningful sections to enhance code readability.

Python Testing

  • Do not apply the keyword-only parameter rule (*) to test functions or fixtures — pytest injects its parameters, so * has no effect.
  • When using mocker.spy on a class-level method (including inherited ones), the spy records the unbound call, so assertions need ANY as the first argument to match self: spy.assert_called_once_with(ANY, expected_arg)
  • Before writing new mock/spy helpers, check the tests/unit/ folder for pre-built helpers in files like fixtures.py or *mocks.py
  • When a test needs a fixture only for its side effects (not its return value), use @pytest.mark.usefixtures(fixture_name.__name__) instead of adding an unused parameter with a noqa comment
  • Use __name__ instead of string literals when referencing functions/methods (e.g., mocker.patch.object(MyClass, MyClass.method.__name__), pytest.mark.usefixtures(my_fixture.__name__)). This enables IDE refactoring tools to catch renames.
  • When using the faker library, prefer the pytest fixture (provided by the faker library) over instantiating instances of Faker.
  • Choosing between cassettes and mocks: At the layer that directly wraps an external API or service, strongly prefer VCR cassette-recorded interactions (via pytest-recording/vcrpy) — they capture real HTTP traffic and verify the wire format, catching integration issues that mocks would miss. At layers above that (e.g. business logic, route handlers), mock the wrapper layer instead (e.g. mocker.patch.object(ThresholdsRepository, ...)) — there is no value in re-testing the HTTP interaction from higher up.
  • Never hand-write VCR cassette YAML files. Cassettes must be recorded from real HTTP interactions by running the test once with --record-mode=once against a live external service: uv run pytest --record-mode=once <test path> --no-cov. The default mode is none — a missing cassette will cause an error, which is expected until recorded.
  • Never hand-edit syrupy snapshot files. Snapshots are auto-generated — to create or update them, run uv run pytest --snapshot-update <test path> --no-cov. A missing snapshot causes the test to fail, which is expected until you run with --snapshot-update. When a snapshot mismatch occurs, fix the code if the change was unintentional; run --snapshot-update if it was intentional.
  • Never hand-write or hand-edit pytest-reserial .jsonl recording files. Recordings must be captured from real serial port traffic by running the test with --record while the device is connected: uv run pytest --record <test path> --no-cov. The default mode replays recordings — a missing recording causes an error, which is expected until recorded against a live device.

Frontend Testing

  • When a data-testid identifies one of many rendered entities, interpolate that entity's stable identifier as the dynamic value, not its display label — prefer an ID (item.itemId, record.sha) whenever the entity has one, since labels collide and change. Where the identifier is human-readable and no ID exists, that name is the key.
  • In DOM-based unit tests, scope queries to the tightest relevant container. Only query document or document.body directly to find the top-level portal/popup element (e.g. a Reka UI dialog via [role="dialog"][data-state="open"]); all further queries should run on that element, not on document.body again. Browser automation (e.g. Playwright) fails an ambiguous single-target locator outright, so a unique data-testid looked up from the page is enough there.

Agent Implementations & Configurations

Memory and Rules

  • Before saving any memory or adding any rule, explicitly ask the user whether the concept should be: (1) added to AGENTS.md as a general rule applicable across all projects, (2) added to AGENTS.md as a rule specific to this project, or (3) stored as a temporary local memory only relevant to the current active work. The devcontainer environment is ephemeral, so local memory files are rarely the right choice.

Public Repositories

  • Before posting to a repository (PR or issue text, comments, commit messages, file contents), check its visibility (gh repo view <owner>/<repo> --json visibility). In a public repository, never name, link to, or quote a private repository. Still say when relevant work happened in one, such as testing in a private repo that depends on this one, but anonymize it ("tested in a private repo") and give only the details needed to make the point.

Tooling

  • Before hand-assembling a multi-step workflow, run task --list — it is probably already a task. Definitions live in .config/taskfiles/; the root Taskfile.yaml only includes them.
  • The bans on pnpm --prefix, uv --directory and direct tool invocation apply to commands you type, not to task definitions: a task's commands run with the repo root as their working directory by default. Prefer adding or extending a task over typing the long form.
  • Linting and type-checking stay with pre-commit run <hook-id> rather than a task, so that what you run is exactly what CI runs.
  • cd into a subdirectory is auto-approved; navigating up (cd ..) or to an absolute path (cd /some/path) requires a user permission prompt. Minimize such navigation: run pre-commit from whichever subdirectory you're already in (it walks up to find .pre-commit-config.yaml).
  • ❌ Never use python3 or python directly. ✅ Always use uv run python for Python commands.
  • ❌ Never use python3/python for one-off data tasks. ✅ Use jq for JSON parsing, standard shell builtins for string manipulation. Only reach for uv run python when no dedicated tool covers the need.
  • ❌ Never use uv run python -c "import ...; print(...)" or inspect to introspect Python source. ✅ Read source files directly or grep for symbols — the code is on disk and can be read without running it.
  • Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior.
  • For frontend tests, run commands via pnpm scripts from frontend/package.json — never invoke tools directly (not pnpm exec , npx , etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ...
  • For linting and type-checking, prefer pre-commit run <hook-id> over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: typescript-check, eslint, pyrefly, ruff, ruff-format.
  • Never rely on IDE diagnostics for ruff warnings — the IDE may not respect the project's ruff.toml config. Run pre-commit run ruff -a to get accurate results.
  • Never use pnpm --prefix <path> or uv --directory <path> to target a different directory — these flags break the permission allow-list matcher the same way chained cd && commands do. Instead, rely on the working directory already being correct (the cwd persists between Bash tool calls), or issue a plain cd <path> as a separate prior tool call to reposition before running the command.
  • Never use backslash line continuations in shell commands — always write the full command on a single line. Backslashes break the permission allow-list matcher.
  • Never manually edit files in any generated/ folder. These files are produced by codegen tooling (typically Kiota) and any manual changes will be overwritten. If a generated file needs to change, update the source (e.g. the OpenAPI schema) and re-run the generator.
  • Leave import statement ordering and unused-import removal to pre-commit — don't edit them yourself.
  • In GitHub Codespaces, the injected GITHUB_TOKEN is a repo-scoped app token, so gh/git cannot reach other repos in the org. It lives in the container process environment (not any dotfile), so unset inside a Bash tool call does not persist. To grant broader access, ask the user to run env -u GITHUB_TOKEN -u GH_TOKEN gh auth login --hostname github.com --git-protocol https --web in their shell (the --web flags are required — gh auth login alone needs a TTY that the Bash tool does not provide, and it refuses to store credentials while GITHUB_TOKEN is set). Afterwards, prefix your own gh/git calls with env -u GITHUB_TOKEN -u GH_TOKEN so they use the stored GitHub CLI credentials instead of the app token — GH_TOKEN also has to be cleared because gh gives it precedence over both GITHUB_TOKEN and stored credentials. Where those credentials live depends on the environment (gh prefers the OS credential store and the config dir follows GH_CONFIG_DIR), but in these devcontainers they usually land in plain text at ~/.config/gh/hosts.yml; gh auth status reports the active source.

Work Tracking with bd (beads)

IMPORTANT: Use bd (beads) to break down and track your own work while implementing a ticket. Within a working session it is the only place to track that breakdown — do NOT use markdown TODOs, task lists, or other scratch tracking methods.

bd is not the team's ticket tracker. It is a local tool for a developer and an AI agent working together: bd init --stealth adds .beads/ to .git/info/exclude, so neither the database nor its JSONL export is committed, and nothing recorded in bd is visible to reviewers, teammates, or CI. The ticket a session is implementing lives in whatever system the team owning the repository uses — GitHub Issues, Jira, Linear, or something else.

Why bd?

  • Dependency-aware: Track blockers and relationships between the pieces of work in a session
  • Agent-optimized: JSON output, ready work detection, discovered-from links
  • Keeps session breakdown out of the team's ticket tracker, and out of the codebase

Quick Start

Check for ready work:

bd ready --json

Create new issues:

bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json
bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:<parent-id> --json

Claim and update:

bd update <id> --status in_progress --json
bd update <id> --priority 1 --json

Complete work:

bd close <id> --reason "Completed" --json

Creating human readable file: After every CRUD command on an issue, export it:

bd export -o [relative path to repository root]/.claude/.beads/issues-dump.jsonl

Issue Types

  • bug - Something broken
  • feature - New functionality
  • task - Work item (tests, docs, refactoring)
  • epic - Large feature with subtasks
  • chore - Maintenance (dependencies, tooling)
  • decision - An architectural decision (aliases: dec, adr)

Priorities

  • 0 - Critical (security, data loss, broken builds)
  • 1 - High (major features, important bugs)
  • 2 - Medium (default, nice-to-have)
  • 3 - Low (polish, optimization)
  • 4 - Backlog (future ideas)

Workflow for AI Agents

  1. Check ready work: bd ready shows unblocked issues
  2. Claim your task: bd update <id> --status in_progress
  3. Work on it: Implement, test, document
  4. Discover new work? Create linked issue:
    • bd create "Found bug" --description="Details about what was found" -p 1 --deps discovered-from:<parent-id>
  5. Complete: bd close <id> --reason "Done"

Important Rules

  • ✅ Use bd for ALL tracking of your own work within a session
  • ✅ Always use --json flag for programmatic use
  • ✅ Link discovered work with discovered-from dependencies
  • ✅ Check bd ready before asking "what should I work on?"
  • ❌ Do NOT create markdown TODO lists to track session work
  • ❌ Do NOT stand up a second scratch tracking system alongside bd
  • ❌ Do NOT propose bd for work that needs tracking beyond the session — a bd issue is invisible to everyone else. Suggest a code comment, or a ticket in the team's own tracker, and do not assume which tracker that is

For more details, run bd quickstart or see https://github.com/gastownhall/beads.

Copier-Managed Files

Files generated from upstream copier templates are listed in .config/.copier-managed-files.json (if present). The manifest has a templates array — one entry per template in the chain. Each entry has src (the template URL), managed_files (files it placed), and optionally parent_src (the template that generated it, forming the chain).

When a file appears in multiple entries, the last entry in the array is the authoritative owner for backporting — it is the most-specific template in the chain. parent_src lets you trace the full chain upward to the root.

When reviewing a PR that modifies a file listed in the manifest, check whether the change is generic enough to backport to the owning template. If so, add a single summary comment (not one per file) noting which files are template-managed and which template repo the changes should be backported to.