diff --git a/.claude/skills/juju/SKILL.md b/.claude/skills/juju/SKILL.md new file mode 100644 index 000000000..f44a80090 --- /dev/null +++ b/.claude/skills/juju/SKILL.md @@ -0,0 +1,49 @@ +--- +name: juju +description: "Develop and test against a real Juju controller" +--- + +# Juju Skill + +Use Juju to develop and test changes in the Ops project. Works on Linux, ideally in a sandboxed environment. + +## Quickstart + +```bash +sudo concierge prepare -p dev # Installs all the required packages and bootstraps local clouds and Juju itself +juju switch concierge-lxd # For a machine charm +juju switch concierge-k8s # For a Kubernetes charm +juju add-model claude-[random ID] # Use a consistent prefix +``` + +After adding a model ALWAYS tell the user the name of the model and how they can inspect the status, filling in the details: + +``` +To inspect the status of the model: + juju status -m [contoller-name]:[model-name] + +Or to see the Juju logs: + juju debug-log -m [controller-name]:[model-name] +``` + +This must ALWAYS be printed right after a session was started and once again at the end of the tool loop. But the earlier you send it, the happier the user will be. + +## Watching output + +Use `juju debug-log -m [controller-name]:[model-name]`. See `juju debug-log --help` for details. + +## Important commands + +- `charmcraft pack` - use this to pack a charm so that it can be deployed +- `juju deploy -m [controller-name]:[model-name] ./path-to-charm.charm` - deploy a charm -- see `--help` for information about providing resources +- `juju status --format json -m [controller-name]:[model-name]` - get information about the status of the deployed charms +- `juju actions` and `juju run` - run an action that a charm defines +- `juju config` - set configuration for a charm +- `juju integrate` - create a relation between two charms +- `juju ssh` - open a SSH connection to a charm container, see `--help` for details + +## Cleanup + +- Remove a model when done: `juju destroy-model [controller-name]:[model-name]` +- Do *not* remove the controller unless explicitly requested +- Do *not* remove any of the packages installed by Concierge unless explicitly requested diff --git a/.claude/skills/tmux/SKILL.md b/.claude/skills/tmux/SKILL.md new file mode 100644 index 000000000..af85a2751 --- /dev/null +++ b/.claude/skills/tmux/SKILL.md @@ -0,0 +1,106 @@ +--- +name: tmux +description: "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output." +license: Vibecoded +source: https://github.com/mitsuhiko/agent-commands/blob/main/skills/tmux/SKILL.md +--- + +# tmux Skill + +Use tmux as a programmable terminal multiplexer for interactive work. Works on Linux and macOS with stock tmux; avoid custom config by using a private socket. + +## Quickstart (isolated socket) + +```bash +SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets # well-known dir for all agent sockets +mkdir -p "$SOCKET_DIR" +SOCKET="$SOCKET_DIR/claude.sock" # keep agent sessions separate from your personal tmux +SESSION=claude-python # slug-like names; avoid spaces +tmux -S "$SOCKET" new -d -s "$SESSION" -n shell +tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'python3 -q' Enter +tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 # watch output +tmux -S "$SOCKET" kill-session -t "$SESSION" # clean up +``` + +After starting a session ALWAYS tell the user how to monitor the session by giving them a command to copy paste: + +``` +To monitor this session yourself: + tmux -S "$SOCKET" attach -t claude-lldb + +Or to capture the output once: + tmux -S "$SOCKET" capture-pane -p -J -t claude-lldb:0.0 -S -200 +``` + +This must ALWAYS be printed right after a session was started and once again at the end of the tool loop. But the earlier you send it, the happier the user will be. + +## Socket convention + +- Agents MUST place tmux sockets under `CLAUDE_TMUX_SOCKET_DIR` (defaults to `${TMPDIR:-/tmp}/claude-tmux-sockets`) and use `tmux -S "$SOCKET"` so we can enumerate/clean them. Create the dir first: `mkdir -p "$CLAUDE_TMUX_SOCKET_DIR"`. +- Default socket path to use unless you must isolate further: `SOCKET="$CLAUDE_TMUX_SOCKET_DIR/claude.sock"`. + +## Targeting panes and naming + +- Target format: `{session}:{window}.{pane}`, defaults to `:0.0` if omitted. Keep names short (e.g., `claude-py`, `claude-gdb`). +- Use `-S "$SOCKET"` consistently to stay on the private socket path. If you need user config, drop `-f /dev/null`; otherwise `-f /dev/null` gives a clean config. +- Inspect: `tmux -S "$SOCKET" list-sessions`, `tmux -S "$SOCKET" list-panes -a`. + +## Finding sessions + +- List sessions on your active socket with metadata: `./tools/find-sessions.sh -S "$SOCKET"`; add `-q partial-name` to filter. +- Scan all sockets under the shared directory: `./tools/find-sessions.sh --all` (uses `CLAUDE_TMUX_SOCKET_DIR` or `${TMPDIR:-/tmp}/claude-tmux-sockets`). + +## Sending input safely + +- Prefer literal sends to avoid shell splitting: `tmux -L "$SOCKET" send-keys -t target -l -- "$cmd"` +- When composing inline commands, use single quotes or ANSI C quoting to avoid expansion: `tmux ... send-keys -t target -- $'python3 -m http.server 8000'`. +- To send control keys: `tmux ... send-keys -t target C-c`, `C-d`, `C-z`, `Escape`, etc. + +## Watching output + +- Capture recent history (joined lines to avoid wrapping artifacts): `tmux -L "$SOCKET" capture-pane -p -J -t target -S -200`. +- For continuous monitoring, poll with the helper script (below) instead of `tmux wait-for` (which does not watch pane output). +- You can also temporarily attach to observe: `tmux -L "$SOCKET" attach -t "$SESSION"`; detach with `Ctrl+b d`. +- When giving instructions to a user, **explicitly print a copy/paste monitor command** alongside the action don't assume they remembered the command. + +## Spawning Processes + +Some special rules for processes: + +- when asked to debug, use lldb by default +- when starting a python interactive shell, always set the `PYTHON_BASIC_REPL=1` environment variable. This is very important as the non-basic console interferes with your send-keys. + +## Synchronizing / waiting for prompts + +- Use timed polling to avoid races with interactive tools. Example: wait for a Python prompt before sending code: + ```bash + ./tools/wait-for-text.sh -t "$SESSION":0.0 -p '^>>>' -T 15 -l 4000 + ``` +- For long-running commands, poll for completion text (`"Type quit to exit"`, `"Program exited"`, etc.) before proceeding. + +## Interactive tool recipes + +- **Python REPL**: `tmux ... send-keys -- 'python3 -q' Enter`; wait for `^>>>`; send code with `-l`; interrupt with `C-c`. Always with `PYTHON_BASIC_REPL`. +- **gdb**: `tmux ... send-keys -- 'gdb --quiet ./a.out' Enter`; disable paging `tmux ... send-keys -- 'set pagination off' Enter`; break with `C-c`; issue `bt`, `info locals`, etc.; exit via `quit` then confirm `y`. +- **Other TTY apps** (ipdb, psql, mysql, node, bash): same pattern—start the program, poll for its prompt, then send literal text and Enter. + +## Cleanup + +- Kill a session when done: `tmux -S "$SOCKET" kill-session -t "$SESSION"`. +- Kill all sessions on a socket: `tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`. +- Remove everything on the private socket: `tmux -S "$SOCKET" kill-server`. + +## Helper: wait-for-text.sh + +`./tools/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout. Works on Linux/macOS with bash + tmux + grep. + +```bash +./tools/wait-for-text.sh -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000] +``` + +- `-t`/`--target` pane target (required) +- `-p`/`--pattern` regex to match (required); add `-F` for fixed string +- `-T` timeout seconds (integer, default 15) +- `-i` poll interval seconds (default 0.5) +- `-l` history lines to search from the pane (integer, default 1000) +- Exits 0 on first match, 1 on timeout. On failure prints the last captured text to stderr to aid debugging. diff --git a/.claude/skills/tmux/tools/find-sessions.sh b/.claude/skills/tmux/tools/find-sessions.sh new file mode 100644 index 000000000..cc3cdb100 --- /dev/null +++ b/.claude/skills/tmux/tools/find-sessions.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern] + +List tmux sessions on a socket (default tmux socket if none provided). + +Options: + -L, --socket tmux socket name (passed to tmux -L) + -S, --socket-path tmux socket path (passed to tmux -S) + -A, --all scan all sockets under CLAUDE_TMUX_SOCKET_DIR + -q, --query case-insensitive substring to filter session names + -h, --help show this help +USAGE +} + +socket_name="" +socket_path="" +query="" +scan_all=false +socket_dir="${CLAUDE_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/claude-tmux-sockets}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -L|--socket) socket_name="${2-}"; shift 2 ;; + -S|--socket-path) socket_path="${2-}"; shift 2 ;; + -A|--all) scan_all=true; shift ;; + -q|--query) query="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then + echo "Cannot combine --all with -L or -S" >&2 + exit 1 +fi + +if [[ -n "$socket_name" && -n "$socket_path" ]]; then + echo "Use either -L or -S, not both" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +list_sessions() { + local label="$1"; shift + local tmux_cmd=(tmux "$@") + + if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then + echo "No tmux server found on $label" >&2 + return 1 + fi + + if [[ -n "$query" ]]; then + sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)" + fi + + if [[ -z "$sessions" ]]; then + echo "No sessions found on $label" + return 0 + fi + + echo "Sessions on $label:" + printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do + attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached") + printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created" + done +} + +if [[ "$scan_all" == true ]]; then + if [[ ! -d "$socket_dir" ]]; then + echo "Socket directory not found: $socket_dir" >&2 + exit 1 + fi + + shopt -s nullglob + sockets=("$socket_dir"/*) + shopt -u nullglob + + if [[ "${#sockets[@]}" -eq 0 ]]; then + echo "No sockets found under $socket_dir" >&2 + exit 1 + fi + + exit_code=0 + for sock in "${sockets[@]}"; do + if [[ ! -S "$sock" ]]; then + continue + fi + list_sessions "socket path '$sock'" -S "$sock" || exit_code=$? + done + exit "$exit_code" +fi + +tmux_cmd=(tmux) +socket_label="default socket" + +if [[ -n "$socket_name" ]]; then + tmux_cmd+=(-L "$socket_name") + socket_label="socket name '$socket_name'" +elif [[ -n "$socket_path" ]]; then + tmux_cmd+=(-S "$socket_path") + socket_label="socket path '$socket_path'" +fi + +list_sessions "$socket_label" "${tmux_cmd[@]:1}" diff --git a/.claude/skills/tmux/tools/wait-for-text.sh b/.claude/skills/tmux/tools/wait-for-text.sh new file mode 100644 index 000000000..56354be83 --- /dev/null +++ b/.claude/skills/tmux/tools/wait-for-text.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: wait-for-text.sh -t target -p pattern [options] + +Poll a tmux pane for text and exit when found. + +Options: + -t, --target tmux target (session:window.pane), required + -p, --pattern regex pattern to look for, required + -F, --fixed treat pattern as a fixed string (grep -F) + -T, --timeout seconds to wait (integer, default: 15) + -i, --interval poll interval in seconds (default: 0.5) + -l, --lines number of history lines to inspect (integer, default: 1000) + -h, --help show this help +USAGE +} + +target="" +pattern="" +grep_flag="-E" +timeout=15 +interval=0.5 +lines=1000 + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2-}"; shift 2 ;; + -p|--pattern) pattern="${2-}"; shift 2 ;; + -F|--fixed) grep_flag="-F"; shift ;; + -T|--timeout) timeout="${2-}"; shift 2 ;; + -i|--interval) interval="${2-}"; shift 2 ;; + -l|--lines) lines="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ -z "$target" || -z "$pattern" ]]; then + echo "target and pattern are required" >&2 + usage + exit 1 +fi + +if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then + echo "timeout must be an integer number of seconds" >&2 + exit 1 +fi + +if ! [[ "$lines" =~ ^[0-9]+$ ]]; then + echo "lines must be an integer" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +# End time in epoch seconds (integer, good enough for polling) +start_epoch=$(date +%s) +deadline=$((start_epoch + timeout)) + +while true; do + # -J joins wrapped lines, -S uses negative index to read last N lines + pane_text="$(tmux capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)" + + if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then + exit 0 + fi + + now=$(date +%s) + if (( now >= deadline )); then + echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2 + echo "Last ${lines} lines from $target:" >&2 + printf '%s\n' "$pane_text" >&2 + exit 1 + fi + + sleep "$interval" +done diff --git a/.claude/subagents/doc-agent.md b/.claude/subagents/doc-agent.md new file mode 100644 index 000000000..814a85e77 --- /dev/null +++ b/.claude/subagents/doc-agent.md @@ -0,0 +1,69 @@ +# Expert technical writer + +You are an expert technical writer for the Ops project. + +## Your role +- You are fluent in Markdown, particularly Myst-Markdown, and also Sphinx, and can read Python code +- You write for a developer audience, focusing on clarity and practical examples +- Your task: read code from `ops/`, `testing/src/scenario/`, and `tracing/ops_tracing/` and generate or update documentation in `docs/` - you may also write and edit reference documentation that is found in the Python docstrings in those folders + +Follow the Diátaxis framework for documentation structure: +- **Tutorials** (`docs/tutorial/`) - Learning-oriented +- **How-to guides** (`docs/howto/`) - Task-oriented +- **Reference** - Generated from docstrings +- **Explanation** (`docs/explanation/`) - Understanding-oriented + +## Project knowledge +- **Tech Stack:** Python, Markdown, Sphinx +- **File Structure:** + - `ops/` – Core 'ops' code (you READ from here) + - `testing/src/scenario/` - Framework for writing tests for charm that use ops (you READ from here) + - `tracing/ops_tracing/` - An optional extra to provide tracing for charms (you READ from here) + - `docs/` – All documentation (you WRITE to here), other than the API reference docs + - `test/`, `testing/tests`, `tracing/test` – Unit, integration, and other tests (you IGNORE these) + +## Commands you can use + +All of these are run in the `docs/` directory. + +* build: `make html` +* clean built doc files: `make clean-doc` +* clean full environment: `make clean` +* check links: `make linkcheck` +* check markdown: `make lint-md` +* check spelling: `make spelling` +* check spelling (without building again): `make spellcheck` +* check accessibility: `make pa11y` +* check style guide compliance: `make vale` +* check metrics for documentation: `make allmetrics` + +## Documentation practices + +- Use short sentences, ideally with one or two clauses. +- Use headings to split the doc into sections. Make sure that the purpose of each section is clear from its heading. +- Avoid a long introduction. Assume that the reader is only going to scan the first paragraph and the headings. +- Avoid background context unless it's essential for the reader to understand. + +Recommended tone: + +- Use a casual tone, but avoid idioms. Common contractions such as "it's" and "doesn't" are great. +- Use "we" to include the reader in what you're explaining. +- Avoid passive descriptions. If you expect the reader to do something, give a direct instruction. + +Read [STYLE.md](../../STYLE.md) for more guidance on documentation. + +## Inter-sphinx + +When linking to external documentation, use inter-sphinx links whenever they are already configured for the project. These include: + + * `python`: Python language and standard library documentation + * `jubilant`: https://documentation.ubuntu.com/jubilant + * `juju`: https://documentation.ubuntu.com/juju/3.6 + * `charmcraft`: https://documentation.ubuntu.com/charmcraft/latest + * `charmlibs`: https://documentation.ubuntu.com/charmlibs/ + * `pebble`: https://documentation.ubuntu.com/pebble + +## Boundaries +- ✅ **Always do:** Write new files to `docs/`, follow the style examples, run the build to ensure there are no errors +- ⚠️ **Ask first:** Before modifying existing documents in a major way +- 🚫 **Never do:** Modify code, edit config files, change the build process diff --git a/.claude/subagents/lint-agent.md b/.claude/subagents/lint-agent.md new file mode 100644 index 000000000..ef876f5da --- /dev/null +++ b/.claude/subagents/lint-agent.md @@ -0,0 +1,20 @@ +# Formatting and lint fixer + +You are a senior engineer focused on ensuring that code across the Ops project is consistent in terms of style and formatting, and that there are no linting issues. + +## Your role +- Format code +- Fix import order +- Enforce naming conventions +- Ensure type annotations are present and correct + +## Tools you can use +- **Format**: `tox -e format` +- **Check and lint**: `tox -e lint` + +If running `ruff` outside of `tox`, note that `--preview` is used. `--fix` and `--unsafe-fixes` can be used when helpful. + +## Boundaries +- ✅ **Always:** Ensure that `tox -e lint` runs without any errors +- ⚠️ **Ask first:** Editing [pyproject.toml](../../pyproject.toml) or adding `noqa` directives. +- 🚫 **Never:** Write new code or change tests diff --git a/.claude/subagents/test-runner.md b/.claude/subagents/test-agent.md similarity index 76% rename from .claude/subagents/test-runner.md rename to .claude/subagents/test-agent.md index 28b7111c9..4bf5fb53c 100644 --- a/.claude/subagents/test-runner.md +++ b/.claude/subagents/test-agent.md @@ -26,23 +26,17 @@ tox -e pebble # Real Pebble tests tox -e lint # Type checking and linting ``` -## Test Patterns -- Use `ops.testing.Context` for charm behavior tests -- Follow State → Event → State pattern -- Test both success and error paths -- Include edge cases and validation - ## Workflow 1. Identify which tests to run based on changes 2. Execute tests and capture output -3. Analyze failures with full traceback context +3. Analyse failures with full traceback context 4. Suggest specific fixes with file:line references 5. Re-run tests to verify fixes 6. Check coverage if needed ## Key Files -- `test/` - Unit tests mirroring source structure -- `test/conftest.py` - Pytest configuration +- `test/` - Tests for the ops package - `test/charms/` - Test charm implementations -- `testing/src/scenario/` - Testing framework source +- `testing/src/scenario/` - Tests for the ops-scenario package +- `tracing/test/` - Tests for the ops-tracing package - `tox.ini` - Test environment configuration diff --git a/.github/agents/doc-agent.md b/.github/agents/doc-agent.md new file mode 120000 index 000000000..906460906 --- /dev/null +++ b/.github/agents/doc-agent.md @@ -0,0 +1 @@ +.claude/subagents/doc-agent.md \ No newline at end of file diff --git a/.github/agents/lint-agent.md b/.github/agents/lint-agent.md new file mode 120000 index 000000000..1ffca5b2d --- /dev/null +++ b/.github/agents/lint-agent.md @@ -0,0 +1 @@ +.claude/subagents/lint-agent.md \ No newline at end of file diff --git a/.github/agents/test-agent.md b/.github/agents/test-agent.md new file mode 120000 index 000000000..a4b7f3028 --- /dev/null +++ b/.github/agents/test-agent.md @@ -0,0 +1 @@ +.claude/subagents/test-agent.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..e94a957aa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,95 @@ +# Copilot Instructions for Ops Library Code Review + +These instructions are specifically for GitHub Copilot when reviewing code in the `ops` library repository. For more general instructions ignore this file and refer to [AGENTS.md](../AGENTS.md). + +Remember: The ops library is foundational infrastructure. Prioritise stability, clarity, and maintainability over cleverness. + +## Code Review Focus Areas + +### Python Style and Standards + +For the most part, style and coding standards are enforced by ruff and do *not* need to be considered in code review. There are some additional recommendations in [STYLE.md](../STYLE.md) that should be followed. + +### Import modules, not objects + +```python +import subprocess + +import ops + +class MyCharm(ops.CharmBase): + def _pebble_ready(self, event: ops.PebbleReadyEvent) -> None: + subprocess.run(['echo', 'foo']) + +# However, "from typing import Foo" is okay to avoid verbosity +from typing import Optional, Tuple +counts: Optional[Tuple[str, int]] +``` + +Imports always appear at the top of the file, grouped in the following order with a blank line between each group, and using relative imports within the package: + +```python +import sys + +import yaml + +from . import charm +``` + +### Docs and docstrings + +#### Avoid the negative, state conditions in the positive + +- Avoid: "If the command doesn't exit within 1 second, the start is considered successful." (Negative) +- Prefer: "If the command stays running for the 1-second window, the start is considered successful." (Positive) + +#### Avoid passive, be active + +- Avoid: "A minimal check is created using the default values" +- Prefer: "Create a minimal check, using the default values" + +#### Avoid subjective, be objective + +- Avoid: "This can be done easily using ..." +- Prefer: "You can do this using ..." + +- Avoid: "This can be easily configured by ..." +- Prefer: "We can configure this using ..." + +- Avoid: "Simply run the command ..." +- Prefer: "Run the command ..." + +#### Use British English + +For example: "colour" rather than "color", "labelled" rather than "labeled", "serialise" rather than "serialize". + +#### Spell out abbreviations + +- "for example" rather than "e.g." +- "that is" rather than "i.e." +- "and so on" rather than "etc" +- "unit testing" rather than UT, and so on + +However, it's okay to use acronyms that are very well known in our domain, like HTTP or JSON or RPC. + +#### Use sentence case in headings + +- Prefer: `## Use sentence case for headings` +- Avoid: `## Use Title Case for Headings`. + +#### YAML + +- Use quotes for strings: This is especially important if a string contains special characters or starts with a number. +- Indentation: Always use spaces and be consistent with the number of spaces throughout the same file (two, unless the file already uses a different number). +- Use comments: Comments will help you and others understand what that data is used for. + +## Common Review Patterns + +What to Look For: + +- **Missing or inadequate tests** for new functionality +- **Breaking changes** (almost never appropriate and should always be called out) +- **Performance regressions** in critical paths +- **Resource leaks** or missing cleanup +- **Security vulnerabilities** in input handling +- **Inconsistent error handling** patterns diff --git a/AGENTS.md b/AGENTS.md index 6cefba86f..bd3fb9f97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,9 +130,11 @@ def new_feature(): Don't document Ops version changes in docstrings - that's in the changelog. -## Pull Request Guidelines +## Commit and Pull Request Guidelines -See CONTRIBUTING.md for more details. +See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. + +Use informative, detailed, conventional-commit styled commit messages as you work through a change. Each commit should be self-contained, building up to the overall PR story. Follow conventional commit style in PR titles: - `feat:` - New feature