diff --git a/.editorconfig b/.editorconfig index 56692ffc..7e5c5b76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -41,8 +41,9 @@ indent_size = 2 [*.sh] end_of_line = lf -# Python - LF so shebang scripts stay executable -[*.py] +# Python is CRLF by the `[*]` default (universal newlines; commonly edited on Windows). Pin LF +# only for a `.py` executed directly via its shebang, by path - here the CI validation entry point. +[spec/validate.py] end_of_line = lf # Dockerfiles - CRLF breaks RUN heredocs and line continuations diff --git a/.gitattributes b/.gitattributes index 046e12af..84d171fe 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,7 +10,11 @@ # and `--renormalize`. Any repo whose tooling ships extensionless scripts adds the matching path pin, e.g. s6-overlay # init `Docker/s6-overlay/** text eol=lf` or husky/git hooks `.husky/pre-commit text eol=lf`. *.sh text eol=lf -*.py text eol=lf + +# Vanilla `.py` follows the CRLF default - Python's universal newlines accept CRLF, and it is +# commonly edited on Windows. Pin LF only for a `.py` executed directly via its shebang, by path - +# here the CI validation entry point; do not re-add a blanket `*.py text eol=lf`. +spec/validate.py text eol=lf # Dockerfiles must be LF - a CRLF breaks RUN heredocs and line continuations. Dockerfile text eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e1aa25ef..3397e051 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -139,6 +139,13 @@ mutation($threadId: ID!) { Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. +### PR Edits and Merge-State Gotchas + +- **`gh pr edit --title/--body` is broken here.** It touches the deprecated Projects-classic `projectCards` GraphQL field and **exits non-zero without applying the change** (a stale PR description then survives review rounds). Edit the title/body via the API and verify it took: GraphQL `updatePullRequest(input: { pullRequestId, title, body })`, or REST `gh api -X PATCH repos///pulls/ -F body=@body.md` (the `@` reads the body from a file - name it explicitly, not the literal `file`). +- **`main`/`develop` use rulesets, not classic branch protection.** The classic protection REST endpoint (`repos/.../branches//protection`) 404s - read the ruleset instead. A `mergeStateStatus` of `BLOCKED` on a green PR is usually just **unresolved review threads** (the ruleset requires thread resolution); resolving them moves it to `CLEAN`. (`BLOCKED` is a `mergeStateStatus` value; don't confuse it with the separate `mergeable` field's `MERGEABLE`/`CONFLICTING`, which reports merge conflicts, not review gates.) +- **Push -> head-SHA read race.** A `headRefOid` read taken immediately after a push can return the **old** head; re-read after the push registers, or a coverage poll evaluates the stale SHA. +- **Copilot is sometimes factually wrong** (e.g. it claimed `actionlint -color` "requires a value" - it is a boolean flag). Verify a finding before fixing; decline with evidence when it is wrong - that is distinct from dismissing a still-present finding as stale. + Reply-body conventions: - Accepted bug/style fix: include fixing commit SHA and a one-line summary. diff --git a/AGENTS.md b/AGENTS.md index 58b5721b..657c00be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ Treat this file as authoritative for everything else; don't restate its rules el ## Git and Commit Rules - **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound - it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. +- **Check the working tree for the maintainer's own uncommitted edits before committing.** The maintainer hand-edits files live (often `README.md`/`HISTORY.md`, sometimes with the editor's LF->CRLF flip on top). Review `git status` first; if there are changes you did not make, ask whether to include them rather than bundling half-finished work or stranding it in an unrelated commit. - **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration - `git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent (loaded `ssh-agent` for SSH, or `gpg-agent` for GPG). If signing is not configured in the environment, **do not commit** - surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L` or the GPG equivalent). **Signing must be live before the *first* commit, not retrofitted.** Turning on `Require signed commits` against a branch that already has unsigned commits forces a rewrite of that entire history to re-sign it - changing every commit SHA and making whoever does the rewrite the committer and signer of every commit (a rebase preserves the `author` field but not the original signatures; you cannot sign another contributor's commits for them). During new-repo setup, never create commits until signing is verified. - **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. @@ -111,13 +112,14 @@ Applies to code and workflow (`#`) comments alike. ### Line Endings -- **[`.editorconfig`](./.editorconfig) defines the correct line ending per file type:** **CRLF** for `.md`, `.cs`, XML/`.csproj`/`.props`/`.targets`, `.yml`/`.yaml`, `.json`, and `.cmd`/`.bat`/`.ps1`; **LF** for `.sh`. `.gitattributes` is `* -text`, so git stores the exact bytes you commit and will **not** normalize endings for you. +- **[`.editorconfig`](./.editorconfig) sets the line ending:** `[*] end_of_line = crlf` is the **default** - every file type is CRLF unless pinned otherwise - with **LF** pinned for the execution-sensitive exceptions - `*.sh`, Dockerfiles, and any individual `.py` executed directly via its shebang (pinned **by path**, e.g. `spec/validate.py`; vanilla `.py` stays CRLF, since Python's universal newlines accept it and it is commonly edited on Windows). Only the LF exceptions are declared; the redundant per-type CRLF rules are intentionally omitted. `.gitattributes` mirrors it: `* -text` (git stores the exact bytes you commit and will **not** normalize) plus the matching LF pins. - **Choosing an ending for a new file type:** CRLF is the **default** - cross-platform editors on Windows produce it, and it is harmless on Linux for everything except shell. Use LF only when the type **requires** it or CRLF **breaks how it is consumed**: executable scripts/shebangs (`*.sh`, s6, husky), Dockerfiles (CRLF breaks `RUN` heredocs/continuations), and tool-owned formats with a native LF ending (KiCad). **YAML stays CRLF** - GitHub Actions' parser tolerates it and these repos run it without breakage (a repo that also runs yamllint sets `new-lines: disable` to defer to `.editorconfig`). Distinguish where a file is *consumed* from where it is *edited*: consumption on Linux alone does not force LF. - **Scripts and extensionless executables must be LF - and pinned in `.gitattributes`, not just configured.** A CRLF shebang (`#!/usr/bin/env bash\r`) breaks execution. `.editorconfig` sets `[*.sh] = lf`, but that extension-based rule does not match **extensionless** executables (s6 service scripts `run`/`up`/`finish`, husky/git hook scripts like `.husky/pre-commit`), and `* -text` enforces nothing - so a broad normalization pass or an editor can silently flip them to CRLF (it has). `.gitattributes` is the enforcement layer: it carries `*.sh text eol=lf`, and any repo whose tooling ships extensionless scripts **adds the matching path pin** - e.g. `Docker/s6-overlay/** text eol=lf` for s6 init, `.husky/pre-commit text eol=lf` for husky hooks - so git holds them at LF on checkout and `--renormalize`. This pin is mandatory for any repo that overrides s6 init, uses husky/git hooks, or otherwise ships executable scripts. The same explicit-pin rule extends to **tool-owned file formats the base config doesn't key on**: pin them to whatever ending the tool reads and writes so a normalization sweep can't churn them - e.g. KiCad project/footprint/3D files (`*.kicad_mod`, `*.kicad_sym`, `*.step`), which KiCad writes LF (`*.kicad_mod text eol=lf`, ...). The principle is general: a file class the `.editorconfig` extension rules and `* -text` don't cover needs an explicit `.gitattributes` pin matching its tool's native ending. - **New files:** create them with the `.editorconfig`-mandated ending. -- **Editing an existing file:** **preserve the file's current line endings** - do not reflow them as a side effect of a content change, even if the file is already non-compliant. A tool that rewrites a file in text mode (a script, a bulk find/replace) can silently flip CRLF to LF and turn a one-line change into a whole-file diff. After any programmatic edit, verify before staging: `git diff --stat` should touch only the lines you changed, and `file ` should report the file's expected ending. If a diff balloons to the whole file, you flipped the endings - restore them and re-stage. +- **Editing an existing file:** **preserve the file's current line endings** - do not reflow them as a side effect of a content change, even if the file is already non-compliant. A tool that rewrites a file in text mode (a script, a bulk find/replace) can silently flip CRLF to LF and turn a one-line change into a whole-file diff. After any programmatic edit, verify before staging: `git diff --stat` should touch only the lines you changed, and a byte check should confirm the expected ending (`file` is unreliable here - see Auditing below). If a diff balloons to the whole file, you flipped the endings - restore them and re-stage. - **Fixing a non-compliant file:** bring it to its `.editorconfig` ending as a **deliberate** change, and prefer to isolate it in its own EOL-only commit so the churn is reviewable. When a broader maintenance change has to normalize endings alongside content edits (a repo-wide cleanup sometimes does), call it out explicitly in the commit/PR description and verify the content separately with `git diff --ignore-cr-at-eol`. -- **Both `.editorconfig` and `.gitattributes` are required.** [`.editorconfig`](./.editorconfig) **and** [`.gitattributes`](./.gitattributes) together govern line endings. A repo missing either file, or one whose `.editorconfig` sets `end_of_line` only under `[*.md]` instead of carrying the full per-extension rules, will accumulate files mixed between LF and CRLF - the exact failure these two files prevent. Carry both files **whole** (the `[*.cs]` block is inert without `.cs` files), including the `*.sh text eol=lf` pin and any extensionless-script path pins. Adopting `.gitattributes` for the first time requires a one-time normalization pass. +- **Auditing line endings - don't trust `file` or naive `git ls-files --eol`.** The authoritative check is a **byte scan** that classifies by which endings are present: **CRLF-only** (every `\n` is preceded by `\r`), **LF-only** (no `\r`), or **mixed** (both forms present) - flag mixed explicitly rather than lumping it in with CRLF; skip binaries via a NUL-byte check. `file` mislabels some types (it reports a CRLF `.json`/`.code-workspace` as plain "JSON text data" with no CRLF note), and `git ls-files --eol`'s `attr/` column holds multiple tokens that shift naive field-splitting into false positives. Scope a repo-wide audit to `git ls-files` plus `git ls-files --others --exclude-standard` - never a raw `find`, which sweeps self-ignoring caches (`.mypy_cache`, `.artifacts`). Idempotent normalize: `b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")`. A single within-line string replace is EOL-safe, but an agent tool that inserts **multiple lines** or writes a **new file** into a CRLF file must emit `\r\n` - a naive `\n` insert creates mixed endings. `.code-workspace` is JSONC (it has `//` comments); strip them before JSON-parsing it. +- **Both `.editorconfig` and `.gitattributes` are required.** [`.editorconfig`](./.editorconfig) **and** [`.gitattributes`](./.gitattributes) together govern line endings. A repo missing either file, or one whose `.editorconfig` sets no global `end_of_line` default (e.g. declares it only under `[*.md]`), will accumulate files mixed between LF and CRLF - the exact failure these two files prevent. The canonical form is a `[*] end_of_line = crlf` default plus the LF exception pins, mirroring `.gitattributes`. Carry both files **whole** (the `[*.cs]` block is inert without `.cs` files), including the `*.sh text eol=lf` pin and any extensionless-script path pins. Adopting `.gitattributes` for the first time requires a one-time normalization pass. ### Quantitative Claims