Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v0.0.104
_commit: v0.0.106
_src_path: gh:LabAutomationAndScreening/copier-base-template.git
description: Copier template for creating Python libraries and executables
install_claude_cli: true
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@
"initializeCommand": "sh .devcontainer/initialize-command.sh",
"onCreateCommand": "sh .devcontainer/on-create-command.sh",
"postStartCommand": "sh .devcontainer/post-start-command.sh"
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 0f1c7f94 # spellchecker:disable-line
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): c4997eda # spellchecker:disable-line
}
2 changes: 1 addition & 1 deletion .devcontainer/install-ci-tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import tempfile
from pathlib import Path

UV_VERSION = "0.10.10"
UV_VERSION = "0.10.12"
PNPM_VERSION = "10.32.1"
COPIER_VERSION = "==9.14.0"
COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3"
Expand Down
11 changes: 8 additions & 3 deletions .github/reusable_workflows/build-docker-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ on:
artifact-name:
description: 'The name of the uploaded artifact of the image tarball'
value: ${{ jobs.build-image.outputs.artifact-name }}
full-image-tag:
description: 'The full image tag used for the built image (repository/name:context-hash)'
value: ${{ jobs.build-image.outputs.full-image-tag }}

permissions:
id-token: write
Expand All @@ -47,6 +50,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
artifact-name: ${{ steps.calculate-build-context-hash.outputs.image_name_no_slashes }}
full-image-tag: ${{ steps.calculate-build-context-hash.outputs.full_image_tag }}
steps:
- name: Parse ECR URL
if: ${{ inputs.push-role-name != 'no-push' }}
Expand Down Expand Up @@ -89,6 +93,7 @@ jobs:
IMAGE_NAME_NO_SLASHES="${IMAGE_NAME_WITH_NAMESPACE//\//-}"
echo "image_name_no_slashes=${IMAGE_NAME_NO_SLASHES}" >> "$GITHUB_OUTPUT"
echo "Image name without slashes: ${IMAGE_NAME_NO_SLASHES}"
echo "full_image_tag=${{ inputs.repository }}/${{ inputs.image_name }}:context-${BUILD_HASH}" >> "$GITHUB_OUTPUT"

- name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658
if: ${{ inputs.push-role-name != 'no-push' }}
Expand All @@ -114,7 +119,7 @@ jobs:
- name: Pull existing image to package as artifact
if: ${{ inputs.save-as-artifact && steps.check-if-exists.outputs.status == 'found' }}
run: |
docker pull ${{ inputs.repository }}/${{ inputs.image_name }}:${{ steps.calculate-build-context-hash.outputs.build_context_tag }}
docker pull ${{ steps.calculate-build-context-hash.outputs.full_image_tag }}

- name: Set up Docker Buildx
if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.check-if-exists.outputs.status == 'notfound' }}
Expand All @@ -129,7 +134,7 @@ jobs:
context: ${{ inputs.context }}
push: ${{ inputs.push-role-name != 'no-push' && steps.check-if-exists.outputs.status == 'notfound' }}
load: ${{ inputs.save-as-artifact }} # make the image available later for the `docker save` step
tags: ${{ inputs.repository }}/${{ inputs.image_name }}:${{ steps.calculate-build-context-hash.outputs.build_context_tag }}
tags: ${{ steps.calculate-build-context-hash.outputs.full_image_tag }}

- name: Add git sha tag
if: ${{ inputs.push-role-name != 'no-push' }}
Expand All @@ -147,7 +152,7 @@ jobs:

- name: Save Docker Image as tar
if: ${{ inputs.save-as-artifact }}
run: docker save -o ${{ steps.calculate-build-context-hash.outputs.image_name_no_slashes }}.tar ${{ inputs.repository }}/${{ inputs.image_name }}:${{ steps.calculate-build-context-hash.outputs.build_context_tag }}
run: docker save -o ${{ steps.calculate-build-context-hash.outputs.image_name_no_slashes }}.tar ${{ steps.calculate-build-context-hash.outputs.full_image_tag }}

- name: Upload Docker Image Artifact
if: ${{ inputs.save-as-artifact }}
Expand Down
22 changes: 21 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# 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 sort or remove imports manually — pre-commit handles it.
- Always include type hints for pyright in Python
- Respect the pyright rule reportUnusedCallResult; assign unneeded return values to `_`
- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Apply the same wording cleanup here for consistency.

Replace “a very clear single-argument function” with “a clear single-argument function.”

✏️ Suggested edit
-- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
+- Prefer keyword-only parameters (unless a clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
- Prefer keyword-only parameters (unless a clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
🧰 Tools
🪛 LanguageTool

[style] ~14-~14: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... Prefer keyword-only parameters (unless a very clear single-argument function): use * in P...

(EN_WEAK_ADJECTIVE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` at line 14, Update the sentence in AGENTS.md that currently reads
"Prefer keyword-only parameters (unless a very clear single-argument function):
use `*` in Python signatures and destructured options objects in TypeScript." to
replace "a very clear single-argument function" with "a clear single-argument
function" so it reads "Prefer keyword-only parameters (unless a 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.

## Testing

- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types by default.
- 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)
Expand All @@ -16,22 +25,33 @@
- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change.

### Python Testing

- 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.
- **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.

# 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.

## Tooling

- Always use `uv run python` instead of `python3` or `python` when running Python commands.
- Prefer dedicated shell tools over `python3`/`python` for simple one-off tasks: use `jq` for JSON parsing, standard shell builtins for string manipulation, etc. Only reach for `python3` when no simpler tool covers the need.
- Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior.
- For frontend work, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec <tool>, npx <tool>, etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ...
- For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec <tool>, npx <tool>, 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`, `pyright`, `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.
- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use absolute paths instead of `cd` to avoid needing to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts
- 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.

<!-- BEGIN BEADS INTEGRATION -->
## Issue Tracking with bd (beads)
Expand Down
4 changes: 2 additions & 2 deletions extensions/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ class ContextUpdater(ContextHook):

@override
def hook(self, context: dict[Any, Any]) -> dict[Any, Any]:
context["uv_version"] = "0.10.10"
context["uv_version"] = "0.10.12"
context["pnpm_version"] = "10.32.1"
context["pre_commit_version"] = "4.5.1"
context["pyright_version"] = ">=1.1.408"
context["pytest_version"] = ">=9.0.2"
context["pytest_randomly_version"] = ">=4.0.1"
context["pytest_cov_version"] = ">=7.0.0"
context["pytest_cov_version"] = ">=7.1.0"
context["ty_version"] = ">=0.0.23"
context["copier_version"] = "==9.14.0"
context["copier_template_extensions_version"] = "==0.3.3"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies = [

# Managed by upstream template
"pytest>=9.0.2",
"pytest-cov>=7.0.0",
"pytest-cov>=7.1.0",
"pytest-randomly>=4.0.1",
"pyright[nodejs]>=1.1.408",
"ty>=0.0.23",
Expand Down
22 changes: 21 additions & 1 deletion template/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Project Structure

This project is a Python library.

# 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 sort or remove imports manually — pre-commit handles it.
- Always include type hints for pyright in Python
- Respect the pyright rule reportUnusedCallResult; assign unneeded return values to `_`
- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten wording by removing the weak intensifier.

Consider replacing “a very clear single-argument function” with “a clear single-argument function” for more direct style.

✏️ Suggested edit
-- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
+- Prefer keyword-only parameters (unless a clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
- Prefer keyword-only parameters (unless a clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript.
🧰 Tools
🪛 LanguageTool

[style] ~14-~14: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... Prefer keyword-only parameters (unless a very clear single-argument function): use * in P...

(EN_WEAK_ADJECTIVE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/AGENTS.md` at line 14, Update the phrasing in the AGENTS.md sentence
that currently reads "a very clear single-argument function" to instead read "a
clear single-argument function" so the guideline uses a more direct tone; locate
the line containing "Prefer keyword-only parameters (unless a very clear
single-argument function): use `*` in Python signatures and destructured options
objects in TypeScript." and replace the phrase accordingly.

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

## Testing

- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types by default.
- 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)
Expand All @@ -16,22 +25,33 @@
- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change.

### Python Testing

- 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.
- **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.

# 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.

## Tooling

- Always use `uv run python` instead of `python3` or `python` when running Python commands.
- Prefer dedicated shell tools over `python3`/`python` for simple one-off tasks: use `jq` for JSON parsing, standard shell builtins for string manipulation, etc. Only reach for `python3` when no simpler tool covers the need.
- Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior.
- For frontend work, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec <tool>, npx <tool>, etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ...
- For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec <tool>, npx <tool>, 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`, `pyright`, `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.
- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use absolute paths instead of `cd` to avoid needing to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts
- 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.

<!-- BEGIN BEADS INTEGRATION -->
## Issue Tracking with bd (beads)
Expand Down
Loading
Loading