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
5 changes: 3 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ mutation($threadId: ID!) {

Issue-level Copilot comments (those in `issues/<N>/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/<owner>/<repo>/pulls/<N> -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/<b>/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.
Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <path>` 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

Expand Down