From 9ea728ab6784f5854331fc9dbbd640707e7a2b0f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 13 Aug 2026 07:09:04 -0700 Subject: [PATCH 1/5] Package Comment and Doc Style as a Skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the hub-docs-to-Skills conversion (Phase 0/1 shipped in #676, Phase 2 in #677). ## What this adds - comment-and-doc-style, extracted from CODESTYLE.md's General section (Tooling Names and Casing, Markdown and Spelling) plus GOVERNANCE.md's Documentation Style Conventions and Pull Request Title and Commit Message Conventions sections: naming tools in prose, Markdown linting and spelling, Markdown formatting, comment philosophy, the ASCII character-set tiers, line endings, the carried-file coordination reference ban, and PR title/commit message conventions. - Three references/ files for progressive disclosure: line-endings.md (the full CRLF/LF policy), carried-doc-references.md (the full coordination-reference rule and its two exceptions), and markdown-links.md (the full reference-style-link convention). ## What changed in existing files - CODESTYLE.md's General section: Tooling Names and Casing and Markdown and Spelling shrink to pointers at the skill. Clean-Compile Verification and Analyzer Diagnostics and Suppressions are untouched, they are build-gate topics, not comment or doc style. - GOVERNANCE.md's Pull Request Title and Commit Message Conventions and Documentation Style Conventions sections shrink to pointers, per this project's standing decision that a skill becomes the sole canonical content for its topic once one exists. The Comments, Character Set, and Line Endings headings stay in place (spec/files.json and spec/project-types.json anchor to them by name), their content is now a one-line pointer each. - AGENTS.md's Where the Rules Live table routes both rows to the new skill. - .claude-plugin/fleet-skills/ regenerated via scripts/build_dist.py to carry the new skill alongside the three from Phases 1 and 2. ## Verification performed - scripts/prose_lint.py, CRLF byte-integrity (all touched files remain 100% CRLF), editorconfig-checker (docker), spec/validate.py, markdownlint-cli2, scripts/test_build_dist.py + scripts/test_skills_install.py (46 tests) all clean. - scripts/build_dist.py --check confirms the generated plugin is current relative to .agents/skills/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 --- .agents/skills/comment-and-doc-style/SKILL.md | 196 ++++++++++++++++++ .../references/carried-doc-references.md | 61 ++++++ .../references/line-endings.md | 132 ++++++++++++ .../references/markdown-links.md | 64 ++++++ .../fleet-skills/.claude-plugin/plugin.json | 1 + .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/comment-and-doc-style/SKILL.md | 196 ++++++++++++++++++ .../references/carried-doc-references.md | 61 ++++++ .../references/line-endings.md | 132 ++++++++++++ .../references/markdown-links.md | 64 ++++++ AGENTS.md | 4 +- CODESTYLE.md | 11 +- GOVERNANCE.md | 92 +------- 13 files changed, 919 insertions(+), 97 deletions(-) create mode 100644 .agents/skills/comment-and-doc-style/SKILL.md create mode 100644 .agents/skills/comment-and-doc-style/references/carried-doc-references.md create mode 100644 .agents/skills/comment-and-doc-style/references/line-endings.md create mode 100644 .agents/skills/comment-and-doc-style/references/markdown-links.md create mode 100644 .claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md create mode 100644 .claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md create mode 100644 .claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md create mode 100644 .claude-plugin/fleet-skills/skills/comment-and-doc-style/references/markdown-links.md diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md new file mode 100644 index 00000000..1e654160 --- /dev/null +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -0,0 +1,196 @@ +--- +name: comment-and-doc-style +description: >- + Governs prose, comment, Markdown, character-set, line-ending, and PR-title/commit-message + conventions for every ptr727/ProjectTemplate fleet repo. Use this whenever writing or editing a + code comment, workflow comment, Markdown doc, commit message, or PR title, whenever choosing + which characters to type in agent-authored text, whenever the file being edited is CRLF, and + whenever naming a tool in prose or docs. Triggers even when the task looks purely mechanical, + such as "just fix a typo" or "add a one-line comment", because the fleet's ASCII character-set + tiers, no-semicolon rule, comment-growth discipline, and CRLF-preservation rule are each easy to + violate without noticing: an em dash slipped into a sentence, a comment that grew by one more + clause, or a text-mode edit that silently flattens a CRLF file to LF. Also triggers when + authoring a new Markdown file (reference-style links, Table of Contents, present tense), when a + carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, + .github/copilot-instructions.md) is being edited (no coordination references to the template or + a sibling repo), and when writing a PR title or commit message (imperative subject, no vague + titles, no unsolicited Co-Authored-By, no release-bump magnitude). +--- + +# Comment and Doc Style + +## Why this exists + +These are the fleet's mechanical prose rules, kept in one place instead of re-derived per repo or +per session: how to write a comment, which characters an agent may type, how a Markdown file is +structured, how a carried instruction file may reference the hub, and how a PR title or commit +message reads. None of these are matters of taste. Each is checked, by `prose_lint.py`, +`editorconfig-checker`, `markdownlint`, `cspell`, or a human reviewer, and each has been the exact +subject of a real review finding. + +## Naming tools in prose + +Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.Net`), +`CSharpier`, `ruff`, `pyright`, `uv`. Do not invent personal variants. + +## Markdown files: linting and spelling + +- **Markdown lints clean, repo-wide.** Every `.md` file is error and warning free via + `markdownlint-cli2` against the shared `.markdownlint-cli2.jsonc`. A rule it deliberately + disables (for example `MD013` line length) stays disabled, do not "fix" it. `MD033` inline HTML + stays enabled: HTML comments, and `details`/`summary` (no Markdown equivalent for a + collapsible), are allowed, everything else with a native Markdown equivalent uses the Markdown. +- **Spelling is US English**, checked by CSpell against the shared `cspell.json` + (`"language": "en-US"`, so a British spelling is flagged). Add a project term to `cspell.json`'s + `words` list, never to a `.code-workspace`'s own `cspell.words` block. +- **CI's spelling gate covers `README.md` and `HISTORY.md` only**, deliberately not every `.md` + file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). + A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no + `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md + cspell line together, all three or none. +- **`HISTORY.md` mirrors the README's opening**: the same `# `, the same tagline verbatim + (the first line after the README's H1), then its own `## Release History`. It never repeats a + paragraph below the README's tagline. +- **"Markdown" is a proper noun in prose** (a Markdown file, a Markdown-only repo), lowercase only + for what a machine reads: a tool or package name (`markdownlint`), a settings key, a heading + anchor, a file extension. + +## Markdown formatting + +- **Reference-style links everywhere**, except the four files read one section at a time rather + than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, `.github/copilot-instructions.md`. + Those keep inline links so a target resolves where it is read. Every other Markdown file defines + every URI at the bottom, grouped by type under an HTML-comment header, each group alphabetized + by reference name rather than by the full definition line (a name that is a prefix of another + sorts first, `[governance]` above `[governance-branching-model]`). A URL inside a fenced code + block stays inline. See `references/markdown-links.md` for the full grouping and naming + convention. +- **Table of Contents**: generated by the Markdown All in One extension on save, never + hand-authored or hand-edited. Exclude a heading with an inline `<!-- omit from toc -->` marker. +- **One logical paragraph per line**, no hard-wrap line-length limit. For an intentional line + break within a block (stacked badges, status lines), end the line with a trailing backslash + rather than trailing whitespace. +- **Headings use the PR-title casing rule** below. +- **Write in the present tense.** State what *is*, never a change from a prior state ("X does Y", + not "X now does Y" or "X no longer does Z"). This applies to docs and code/workflow comments + alike. Before/after framing belongs in changelogs, commit messages, and PR descriptions, where + the prior state is the point. +- **When a behavior changes, grep for prose asserting the old one.** Comments, diagram labels, + workflow-input descriptions, and audit statements elsewhere may still describe the prior + behavior, and each was accurate when written. No linter catches a claim that is merely untrue, + so this sweep is the only mechanism that will. + +## Comments + +Applies to code and workflow (`#`) comments alike. + +- Comment only when the code does not explain itself, or the logic is genuinely complex. + Self-evident code needs no comment. +- State only the non-obvious *why*, for the human reading *this* project's code now. No + cross-project references, no historic or design narrative, no rule citations (governance lives + in `GOVERNANCE.md`, not echoed inline). +- **Keep it short**: one line is the default. A second line is earned only by a constraint the + code cannot otherwise carry. +- **Structured, not prose**: one sentence per line, never wrapped across lines, never a + multi-sentence run-on. A comment that genuinely needs several sentences is several lines, each + one sentence. +- A comment line opening prose starts with a capital. A trailing label, or the version pin an + action-pinning rule requires, does not. +- Mark a sub-topic with `-` after the comment marker (`# -`), only for genuine parallel sub-items + hanging off a lead line, never a continuation of one thought. +- **No file, class, or type header summary blocks.** A type or file gets a comment only for a + specific non-obvious point, never a block restating what it contains (a license or provenance + header a tool or policy requires is not a summary and is unaffected). +- **Never let a comment grow across edits.** Touching code near an existing comment means the + comment comes out the same length or shorter, never one more clause of rationale appended. + +A continuation stays unindented, one sentence per line: + +```text +# Change gate for the compile tests. +# An esp-idf build costs minutes, so gate on what each test covers. +# A diff that cannot be computed runs everything. +``` + +Sub-topics take a `-` after the comment marker, each elaborating a distinct item named in the lead: + +```text +# Source lint plus change-gated compile tests. +# - compile-test builds the external component. +# - template-compile-test builds one example device per template. +``` + +## Character set + +Agent-authored text (docs, code, comments, commit messages, PR descriptions) is ASCII by default. +A non-ASCII character is judged by tier, and one that fits no tier is a finding, never a silent +pass. + +| Tier | Rule | Examples | +| --- | --- | --- | +| 1, never legitimate | Restructure the sentence instead of substituting an ASCII lookalike | em/en dash to a comma or two sentences (never a spaced hyphen), arrows to `->`/`=>`, curly quotes to straight quotes, ellipsis to `...`, bullet to `-` | +| 2, legitimate only next to a number | Keep next to a number, a tier-3 symbol, or another tier-2 operator, otherwise use the ASCII form | keep `<=35` in a threshold table, write `<=`, `>=`, `!=`, `+/-`, `x`, `/` in flowing prose | +| 3, always legitimate | Keep, never approximate away or spell out | micro, degree, ohm, pi, superscript two and three, section symbols | +| developer-typed | Never strip, and not a license for the agent to add its own | emoji a developer deliberately placed for emphasis | + +- **No semicolon in agent-authored prose.** Recast a mid-sentence semicolon as a comma or as two + sentences. A semicolon separating items in a list that already contains commas, or a statement + terminator in code, is unaffected. +- **No spaced hyphen joining or interrupting a sentence** (` - `, or the paired aside ` - x - `). + Recast as a comma, two sentences, or parentheses. A hyphen inside a compound word, a leading + list marker, a range, and the `- **Label** - explanation` bullet separator are unaffected. +- **In carried verbatim content, fix the whole class at the hub**, not one instance, since a + downstream repo cannot edit a section byte-matched against the hub. Everywhere else, correct as + each file is next edited, not swept. + +## Line endings + +This repo's default is CRLF (`[*] end_of_line = crlf` in `.editorconfig`), with LF pinned for +execution-sensitive files (`*.sh`, Dockerfiles, a shebang-executed `.py` pinned by path). +**Preserve a file's existing line ending when editing it, never reflow as a side effect of a +content change.** A text-mode tool, including a naive programmatic write, can silently flip CRLF +to LF and turn a one-line change into a whole-file diff. After any programmatic edit, verify with +`git diff --stat` (it should touch only the lines you changed) and a byte scan, `file` and a naive +`git ls-files --eol` are both unreliable here. Idempotent normalize: +`b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")`. The full policy, choosing an ending for a new +file type, operational-repo overrides, extensionless-script pins, and auditing, is in +`references/line-endings.md`. + +## Carried files reference no coordination machinery + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +(in prose or a link), and never name a sibling fleet repo as an illustrative example. State the +behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies +the destination out of band. A contextually relevant link to a related project (the image this +config feeds, a library this depends on) is not a coordination reference and is expected. The full +exceptions, a verbatim section that must name the hub to do its job, and a pointer to a +hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. + +## PR titles and commit messages + +- **Format**: an imperative subject, 72 characters or fewer, no trailing period ("Add 24-hour + PM2.5 average sensor", not "Added X" or "Adds X"). An optional body, blank-line separated, + explains *why* the change is being made when that is non-obvious, the diff already shows *what*. +- **Rules**: no vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` + titles are fine as-is. No `Co-Authored-By:` lines unless the developer explicitly asks. No + release-bump magnitude in the title ("minor", "patch", "release v0.2.0"), Nerdbank.GitVersioning + computes the next version from `version.json` and git history, a dependency version in a + dependency-bump title is fine and expected. US English spelling, and title case with lowercase + short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from), a hyphenated + compound capitalizes both parts unless the second is a short preposition (*Built-in*, + *EPA-Corrected*, *24-Hour*). + +```text +Add structured logging extensions to library +Pin softprops/action-gh-release to commit SHA +Drop net8.0 multi-targeting from console project +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify devcontainer setup steps in README +``` + +## Quantitative claims + +A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) +is verified against current code before it is written. When a doc number is derived from a code +constant, mark the dependency in a source-code comment so the next editor knows to update both. diff --git a/.agents/skills/comment-and-doc-style/references/carried-doc-references.md b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md new file mode 100644 index 00000000..111d6e88 --- /dev/null +++ b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md @@ -0,0 +1,61 @@ +# Carried Files Carry No Coordination References + +Full detail for the "Carried files reference no coordination machinery" rule in `SKILL.md`. Load +this when editing one of the carried files themselves, not when writing an ordinary repo-owned +doc. + +## Which files this governs + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template +content only. A repo's own `README.md` and topical docs are its own content, never carried +verbatim, and this rule does not reach them. + +## What is banned + +Two things, in the files above: + +1. **Any reference to the template repo**, in prose or in a link. The coordination flow that + produced a carried file is machinery a consumer of that repo should never have to see, and + naming where a file came from is exactly the derived-from framing the present-tense rule (in + `SKILL.md`'s "Markdown formatting" section) independently forbids. Where a carried file must + express a template-level behavior ("report a rule discrepancy upstream"), state the behavior + rather than the destination. The maintainer supplies the destination out of band. +2. **A sibling fleet repo named as an illustrative example** ("repo X does it this way", "see repo + Y's adoption"), which couples the repos and rots as they diverge. To point at a current good + example, name it in the onboarding or conformance issue, never in a carried doc. + +## The two exceptions + +**The first exception is a verbatim section**, and `AGENTS.md` "Fleet Bootstrap" is why it exists. +That section's whole function is to name where the canonical rules live, for an agent in a +repository whose carried copies are stale, partial, or absent, which is exactly when no other file +present can say it. Its bytes are fixed fleet-wide, so a repository cannot edit the reference out +without failing the verbatim check instead, and a rule banning it would be unsatisfiable rather +than merely strict. The exception is scoped to the verbatim region and never leaks past it: the +same document's own prose, outside that region, is governed normally. A reference that reaches a +verbatim section is a defect in the canonical, fixed once at the source rather than reported +against every repository carrying it. + +**The second exception is a hub-hosted tool the reader is told to run**, which is a different kind +of reference. A rule naming a gate, a script, or a reference snippet the reader executes or copies +states an instruction rather than a provenance, and an instruction with no destination is +unfollowable, which is precisely how a pointer in carried text comes to read as decorative. The +test is whether the reference is something the reader *does* or something that *happened to this +file*: where the content came from stays out, what the reader runs stays in. Such a pointer names +the hub's canonical rather than this repository's provenance, so it is the hub's to keep resolving +and never a repository's to edit out or re-point at a local path. What is reached rather than +carried, and how, is `GOVERNANCE.md` "Hub-Hosted Tooling". In `AGENTS.md` and `GOVERNANCE.md` this +belongs in verbatim rule text, the same region the first exception already covers, so the whole +fleet reads one wording and no repository is asked to answer for a reference it did not write. + +## What is not a coordination reference + +**A contextually relevant link to a related project is expected, not banned.** Where another repo +is part of this repo's subject matter (the image that consumes this config, the builder that +generates this hardware, a library this depends on), link it normally. The test is whether the +link serves a reader of *this* repo's content, not whether the target happens to be in the fleet. + +This pairs with the present-tense rule: state the current shape, not a history of which repo it +came from. diff --git a/.agents/skills/comment-and-doc-style/references/line-endings.md b/.agents/skills/comment-and-doc-style/references/line-endings.md new file mode 100644 index 00000000..7791569f --- /dev/null +++ b/.agents/skills/comment-and-doc-style/references/line-endings.md @@ -0,0 +1,132 @@ +# Line Ending Policy + +Full detail for the "Line endings" rule in `SKILL.md`. Load this when choosing an ending for a +new file type, working in an operational (config) repo, pinning an extensionless executable, or +auditing a repo's endings, not for an ordinary content edit to an existing file (the SKILL.md +summary, preserve the existing ending and verify with a byte scan, covers that case). + +## The defaults + +- **[`.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, for example `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 committed and does not + normalize) plus the matching LF pins. +- **Both files are required together.** `.editorconfig` governs the editor, `.gitattributes` + governs git (checkout, commit, `--renormalize`). A repo missing either file, or whose + `.editorconfig` sets no global `end_of_line` default (for example declares it only under + `[*.md]`), accumulates files mixed between LF and CRLF, the exact failure these two files + prevent together. Carry both files whole (an inert `[*.cs]` block costs nothing in a non-.NET + repo), including the `*.sh text eol=lf` pin and any extensionless-script path pins. + +## Choosing an ending for a new file type + +CRLF is the default, since 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 and shebangs (`*.sh`, s6, husky), Dockerfiles (CRLF breaks `RUN` +heredocs and line continuations), and tool-owned formats with a native LF ending (KiCad). +Non-workflow YAML stays CRLF, GitHub Actions' parser tolerates it (a repo also running yamllint +sets `new-lines: disable` to defer to `.editorconfig`). Workflow YAML +(`.github/workflows/*.{yml,yaml}`) is pinned LF in `.editorconfig`, because Dependabot and Actions +rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This LF +class is not backed by a `.gitattributes` pin, git keeps `* -text`, and CI's `editorconfig-checker` +(EOL-only) catches a mismatch instead. + +Distinguish where a file is *consumed* from where it is *edited*, consumption on Linux alone does +not force LF. A config or pattern file consumed by a Linux tool stays CRLF when the tool tolerates +a trailing CR: `.dockerignore` and `.gitignore` are CRLF (their parsers strip the CR), and only a +*Dockerfile*, interpreted and broken by a CR in a `RUN` heredoc or line continuation, is LF. + +## Operational (config) repos + +The global default follows the consuming application's native platform, not the fleet CRLF +default. A config repo (registry `workflowModel: operational`) is a view into an application's +configuration directory, often the exact tree mounted into that app's container, so its files use +the ending the app itself reads and writes, and forcing the fleet CRLF default would fight the +app. Set the `[*] end_of_line` default to the app's native ending and record it in the registry +`lineEndings` field (`lf` or `crlf`): LF for a Linux-native app whose config lives in a Linux +container (ESPHome, Home Assistant, a devcontainer-only or HACS config), CRLF for a Windows-native +editor, for example Vantage InFusion config edited by Design Center on Windows. The +execution-sensitive LF pins (`*.sh`, Dockerfiles, workflow YAML) still apply on top, and +`.gitattributes` still mirrors the chosen default. This override is for operational repos only, +`release` repos keep the `[*] end_of_line = crlf` fleet default above. Do not re-normalize an +operational repo to the fleet default, that is exactly the over-normalization these per-repo +endings exist to prevent. + +**Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed +repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited +subtree), the clean answer is a repo per consumer, each single-platform with its own +`lineEndings`. For example a controller config edited by a Windows-native editor lives in its own +CRLF repo, not as a subtree inside a Linux `lf` config repo. Fallback only if a subtree genuinely +cannot be split out: keep the global default at the primary consumer and pin the odd subtree with +an `.editorconfig` path override (for example `[<subtree>/**] end_of_line = crlf`) matching its +consumer. The global `* -text` in `.gitattributes` already preserves those bytes, no extra git pin +is needed. + +## 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 or 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. `.gitattributes` is the +enforcement layer: it carries `*.sh text eol=lf`, and any repo whose tooling ships extensionless +scripts adds the matching path pin (`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 or git +hooks, or otherwise ships executable scripts. The same explicit-pin rule extends to tool-owned +file formats the base config does not key on: pin them to whatever ending the tool reads and +writes, for example KiCad project, footprint, and 3D files (`*.kicad_mod`, `*.kicad_sym`, +`*.step`), which KiCad writes LF. + +**Pair each such pin with a matching `.editorconfig` override**, since the git pin alone is not +enough, `.gitattributes` governs git while the editor follows `.editorconfig`, where the default +still applies to any file no extension rule covers. Give every extensionless executable an +editorconfig LF override beside its `.gitattributes` pin (`[.husky/pre-commit] end_of_line = lf`), +and for a byte-preserve data directory (downloaded or opaque source whose exact bytes the consumer +may depend on) disable all editor normalization, not just EOL: `[<dir>/*]` with `charset = unset`, +`end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` +is EditorConfig's spec-defined special value that removes an inherited property). + +## Editing discipline + +- **New files**: create with the `.editorconfig`-mandated ending. +- **Editing an existing file**: preserve its 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. If a diff balloons to the whole file, the endings flipped, 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, call it out explicitly in + the commit or PR description and verify the content separately with + `git diff --ignore-cr-at-eol`. + +## Auditing + +Don't trust `file` or a naive `git ls-files --eol`. The authoritative check is a byte scan that +classifies by which endings are present: CRLF-only (every `\n` preceded by `\r`), LF-only (no +`\r`), or mixed (both forms present). Flag mixed explicitly rather than lumping it in with CRLF, +and skip binaries via a NUL-byte check. `file` mislabels some types (it reports a CRLF `.json` or +`.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 a tool that inserts multiple lines or writes a new file into a +CRLF file must emit `\r\n`, since a naive `\n` insert creates mixed endings. `.code-workspace` is +JSONC (it has `//` comments), so strip them before JSON-parsing it. + +Editing CRLF files programmatically with a regex has a sharper trap: `.` matches `\r`, so a +captured line keeps its carriage return and rejoining with `\r\n` yields `CRCRLF`. A text-mode +rewrite has the mirror failure, silently flattening CRLF to LF. Prefer line-based edits +(`splitlines(keepends=True)`) or literal replacement over regex reassembly. In Python the +text-mode failure is the default: `Path.read_text()` decodes through universal newlines and +`write_text()` writes `\n` back, so a read-edit-write round trip flattens the whole file while the +edit itself looks correct. Pass `newline=''` to both, or work in bytes. diff --git a/.agents/skills/comment-and-doc-style/references/markdown-links.md b/.agents/skills/comment-and-doc-style/references/markdown-links.md new file mode 100644 index 00000000..e7251ccb --- /dev/null +++ b/.agents/skills/comment-and-doc-style/references/markdown-links.md @@ -0,0 +1,64 @@ +# Reference-Style Links + +Full detail for the "Markdown formatting" reference-style-links rule in `SKILL.md`. Load this +when actually authoring or reorganizing a Markdown file's link definitions, not for a small +in-place prose edit. + +## Where the rule applies + +Every Markdown file in the repo uses reference-style links only, except the four files that are +read one section at a time rather than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, +and `.github/copilot-instructions.md`. Those keep inline `[text](uri)` links, since a reader +jumping straight to one section needs the target to resolve where it is, while a definition parked +at the bottom of the file is never reached. The exception is that closed list of four files, never +a category to argue from case by case. Every other Markdown file follows the rule regardless of +its audience. + +## The definition block + +Every URI, an internal path, an anchor, an external URL, or a shield image, is defined at the +bottom of the file, split into groups by type under an HTML-comment header, for example: + +```markdown +<!-- Shields --> + +[license-shield]: https://img.shields.io/... + +<!-- Repo --> + +[governance]: ./GOVERNANCE.md +[governance-branching-model]: ./GOVERNANCE.md#branching-model + +<!-- External --> + +[markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 +``` + +Within a group, definitions are alphabetized by **reference name alone**, the text inside the +brackets, never by the whole definition line. Where one name is a prefix of another, the shorter +one sorts first: `[governance]` above `[governance-branching-model]`, `[repo-config]` above +`[repo-config-settings]`. Sorting the full line instead inverts every such pair, because `-` +precedes `]` in byte order, so the two readings disagree on exactly the names a reader looks up +together, and a plain `sort -c` over the block passes on the inverted order regardless. + +## Naming a reference + +Reference names are contextual and encode both the target and its group: + +- `foo-shield` for a shield image +- `foo-link` for an external URL +- a bare `foo` for a local path or anchor + +For example `[license-shield]`, `[releases-link]`, `[repo-config]`. Never a numeric name (`[1]`) +and never an opaque one. + +## Mechanics + +- No inline `[text](uri)` targets in prose, in any file outside the four-file exception above. +- **A URL inside a fenced code block stays inline.** Reference links do not resolve inside a code + block, so do not extract it there, and exclude fenced code from any link-integrity check + (bracket literals like `["a", "b"]` otherwise read as undefined references). +- **Removing a link also removes its reference definition.** An orphaned definition fails the + no-unused-defs rule. +- The one exception to "no inline links" is the Table of Contents, whose entries stay inline + anchor links, since the ToC extension generates them that way and they are never hand-edited. diff --git a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json index de44ba5f..6a31752b 100644 --- a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json +++ b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json @@ -7,6 +7,7 @@ }, "skills": [ "./skills/carried-instruction-file-guard", + "./skills/comment-and-doc-style", "./skills/copilot-instructions-keeper", "./skills/pr-review-conduct" ] diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index e71b9dc0..80f06874 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -5fb858e6b994c4d2 +e561f9c04d4b184a diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md new file mode 100644 index 00000000..1e654160 --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md @@ -0,0 +1,196 @@ +--- +name: comment-and-doc-style +description: >- + Governs prose, comment, Markdown, character-set, line-ending, and PR-title/commit-message + conventions for every ptr727/ProjectTemplate fleet repo. Use this whenever writing or editing a + code comment, workflow comment, Markdown doc, commit message, or PR title, whenever choosing + which characters to type in agent-authored text, whenever the file being edited is CRLF, and + whenever naming a tool in prose or docs. Triggers even when the task looks purely mechanical, + such as "just fix a typo" or "add a one-line comment", because the fleet's ASCII character-set + tiers, no-semicolon rule, comment-growth discipline, and CRLF-preservation rule are each easy to + violate without noticing: an em dash slipped into a sentence, a comment that grew by one more + clause, or a text-mode edit that silently flattens a CRLF file to LF. Also triggers when + authoring a new Markdown file (reference-style links, Table of Contents, present tense), when a + carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, + .github/copilot-instructions.md) is being edited (no coordination references to the template or + a sibling repo), and when writing a PR title or commit message (imperative subject, no vague + titles, no unsolicited Co-Authored-By, no release-bump magnitude). +--- + +# Comment and Doc Style + +## Why this exists + +These are the fleet's mechanical prose rules, kept in one place instead of re-derived per repo or +per session: how to write a comment, which characters an agent may type, how a Markdown file is +structured, how a carried instruction file may reference the hub, and how a PR title or commit +message reads. None of these are matters of taste. Each is checked, by `prose_lint.py`, +`editorconfig-checker`, `markdownlint`, `cspell`, or a human reviewer, and each has been the exact +subject of a real review finding. + +## Naming tools in prose + +Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.Net`), +`CSharpier`, `ruff`, `pyright`, `uv`. Do not invent personal variants. + +## Markdown files: linting and spelling + +- **Markdown lints clean, repo-wide.** Every `.md` file is error and warning free via + `markdownlint-cli2` against the shared `.markdownlint-cli2.jsonc`. A rule it deliberately + disables (for example `MD013` line length) stays disabled, do not "fix" it. `MD033` inline HTML + stays enabled: HTML comments, and `details`/`summary` (no Markdown equivalent for a + collapsible), are allowed, everything else with a native Markdown equivalent uses the Markdown. +- **Spelling is US English**, checked by CSpell against the shared `cspell.json` + (`"language": "en-US"`, so a British spelling is flagged). Add a project term to `cspell.json`'s + `words` list, never to a `.code-workspace`'s own `cspell.words` block. +- **CI's spelling gate covers `README.md` and `HISTORY.md` only**, deliberately not every `.md` + file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). + A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no + `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md + cspell line together, all three or none. +- **`HISTORY.md` mirrors the README's opening**: the same `# <Title>`, the same tagline verbatim + (the first line after the README's H1), then its own `## Release History`. It never repeats a + paragraph below the README's tagline. +- **"Markdown" is a proper noun in prose** (a Markdown file, a Markdown-only repo), lowercase only + for what a machine reads: a tool or package name (`markdownlint`), a settings key, a heading + anchor, a file extension. + +## Markdown formatting + +- **Reference-style links everywhere**, except the four files read one section at a time rather + than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, `.github/copilot-instructions.md`. + Those keep inline links so a target resolves where it is read. Every other Markdown file defines + every URI at the bottom, grouped by type under an HTML-comment header, each group alphabetized + by reference name rather than by the full definition line (a name that is a prefix of another + sorts first, `[governance]` above `[governance-branching-model]`). A URL inside a fenced code + block stays inline. See `references/markdown-links.md` for the full grouping and naming + convention. +- **Table of Contents**: generated by the Markdown All in One extension on save, never + hand-authored or hand-edited. Exclude a heading with an inline `<!-- omit from toc -->` marker. +- **One logical paragraph per line**, no hard-wrap line-length limit. For an intentional line + break within a block (stacked badges, status lines), end the line with a trailing backslash + rather than trailing whitespace. +- **Headings use the PR-title casing rule** below. +- **Write in the present tense.** State what *is*, never a change from a prior state ("X does Y", + not "X now does Y" or "X no longer does Z"). This applies to docs and code/workflow comments + alike. Before/after framing belongs in changelogs, commit messages, and PR descriptions, where + the prior state is the point. +- **When a behavior changes, grep for prose asserting the old one.** Comments, diagram labels, + workflow-input descriptions, and audit statements elsewhere may still describe the prior + behavior, and each was accurate when written. No linter catches a claim that is merely untrue, + so this sweep is the only mechanism that will. + +## Comments + +Applies to code and workflow (`#`) comments alike. + +- Comment only when the code does not explain itself, or the logic is genuinely complex. + Self-evident code needs no comment. +- State only the non-obvious *why*, for the human reading *this* project's code now. No + cross-project references, no historic or design narrative, no rule citations (governance lives + in `GOVERNANCE.md`, not echoed inline). +- **Keep it short**: one line is the default. A second line is earned only by a constraint the + code cannot otherwise carry. +- **Structured, not prose**: one sentence per line, never wrapped across lines, never a + multi-sentence run-on. A comment that genuinely needs several sentences is several lines, each + one sentence. +- A comment line opening prose starts with a capital. A trailing label, or the version pin an + action-pinning rule requires, does not. +- Mark a sub-topic with `-` after the comment marker (`# -`), only for genuine parallel sub-items + hanging off a lead line, never a continuation of one thought. +- **No file, class, or type header summary blocks.** A type or file gets a comment only for a + specific non-obvious point, never a block restating what it contains (a license or provenance + header a tool or policy requires is not a summary and is unaffected). +- **Never let a comment grow across edits.** Touching code near an existing comment means the + comment comes out the same length or shorter, never one more clause of rationale appended. + +A continuation stays unindented, one sentence per line: + +```text +# Change gate for the compile tests. +# An esp-idf build costs minutes, so gate on what each test covers. +# A diff that cannot be computed runs everything. +``` + +Sub-topics take a `-` after the comment marker, each elaborating a distinct item named in the lead: + +```text +# Source lint plus change-gated compile tests. +# - compile-test builds the external component. +# - template-compile-test builds one example device per template. +``` + +## Character set + +Agent-authored text (docs, code, comments, commit messages, PR descriptions) is ASCII by default. +A non-ASCII character is judged by tier, and one that fits no tier is a finding, never a silent +pass. + +| Tier | Rule | Examples | +| --- | --- | --- | +| 1, never legitimate | Restructure the sentence instead of substituting an ASCII lookalike | em/en dash to a comma or two sentences (never a spaced hyphen), arrows to `->`/`=>`, curly quotes to straight quotes, ellipsis to `...`, bullet to `-` | +| 2, legitimate only next to a number | Keep next to a number, a tier-3 symbol, or another tier-2 operator, otherwise use the ASCII form | keep `<=35` in a threshold table, write `<=`, `>=`, `!=`, `+/-`, `x`, `/` in flowing prose | +| 3, always legitimate | Keep, never approximate away or spell out | micro, degree, ohm, pi, superscript two and three, section symbols | +| developer-typed | Never strip, and not a license for the agent to add its own | emoji a developer deliberately placed for emphasis | + +- **No semicolon in agent-authored prose.** Recast a mid-sentence semicolon as a comma or as two + sentences. A semicolon separating items in a list that already contains commas, or a statement + terminator in code, is unaffected. +- **No spaced hyphen joining or interrupting a sentence** (` - `, or the paired aside ` - x - `). + Recast as a comma, two sentences, or parentheses. A hyphen inside a compound word, a leading + list marker, a range, and the `- **Label** - explanation` bullet separator are unaffected. +- **In carried verbatim content, fix the whole class at the hub**, not one instance, since a + downstream repo cannot edit a section byte-matched against the hub. Everywhere else, correct as + each file is next edited, not swept. + +## Line endings + +This repo's default is CRLF (`[*] end_of_line = crlf` in `.editorconfig`), with LF pinned for +execution-sensitive files (`*.sh`, Dockerfiles, a shebang-executed `.py` pinned by path). +**Preserve a file's existing line ending when editing it, never reflow as a side effect of a +content change.** A text-mode tool, including a naive programmatic write, can silently flip CRLF +to LF and turn a one-line change into a whole-file diff. After any programmatic edit, verify with +`git diff --stat` (it should touch only the lines you changed) and a byte scan, `file` and a naive +`git ls-files --eol` are both unreliable here. Idempotent normalize: +`b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")`. The full policy, choosing an ending for a new +file type, operational-repo overrides, extensionless-script pins, and auditing, is in +`references/line-endings.md`. + +## Carried files reference no coordination machinery + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +(in prose or a link), and never name a sibling fleet repo as an illustrative example. State the +behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies +the destination out of band. A contextually relevant link to a related project (the image this +config feeds, a library this depends on) is not a coordination reference and is expected. The full +exceptions, a verbatim section that must name the hub to do its job, and a pointer to a +hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. + +## PR titles and commit messages + +- **Format**: an imperative subject, 72 characters or fewer, no trailing period ("Add 24-hour + PM2.5 average sensor", not "Added X" or "Adds X"). An optional body, blank-line separated, + explains *why* the change is being made when that is non-obvious, the diff already shows *what*. +- **Rules**: no vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` + titles are fine as-is. No `Co-Authored-By:` lines unless the developer explicitly asks. No + release-bump magnitude in the title ("minor", "patch", "release v0.2.0"), Nerdbank.GitVersioning + computes the next version from `version.json` and git history, a dependency version in a + dependency-bump title is fine and expected. US English spelling, and title case with lowercase + short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from), a hyphenated + compound capitalizes both parts unless the second is a short preposition (*Built-in*, + *EPA-Corrected*, *24-Hour*). + +```text +Add structured logging extensions to library +Pin softprops/action-gh-release to commit SHA +Drop net8.0 multi-targeting from console project +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify devcontainer setup steps in README +``` + +## Quantitative claims + +A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) +is verified against current code before it is written. When a doc number is derived from a code +constant, mark the dependency in a source-code comment so the next editor knows to update both. diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md new file mode 100644 index 00000000..111d6e88 --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md @@ -0,0 +1,61 @@ +# Carried Files Carry No Coordination References + +Full detail for the "Carried files reference no coordination machinery" rule in `SKILL.md`. Load +this when editing one of the carried files themselves, not when writing an ordinary repo-owned +doc. + +## Which files this governs + +`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, +the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template +content only. A repo's own `README.md` and topical docs are its own content, never carried +verbatim, and this rule does not reach them. + +## What is banned + +Two things, in the files above: + +1. **Any reference to the template repo**, in prose or in a link. The coordination flow that + produced a carried file is machinery a consumer of that repo should never have to see, and + naming where a file came from is exactly the derived-from framing the present-tense rule (in + `SKILL.md`'s "Markdown formatting" section) independently forbids. Where a carried file must + express a template-level behavior ("report a rule discrepancy upstream"), state the behavior + rather than the destination. The maintainer supplies the destination out of band. +2. **A sibling fleet repo named as an illustrative example** ("repo X does it this way", "see repo + Y's adoption"), which couples the repos and rots as they diverge. To point at a current good + example, name it in the onboarding or conformance issue, never in a carried doc. + +## The two exceptions + +**The first exception is a verbatim section**, and `AGENTS.md` "Fleet Bootstrap" is why it exists. +That section's whole function is to name where the canonical rules live, for an agent in a +repository whose carried copies are stale, partial, or absent, which is exactly when no other file +present can say it. Its bytes are fixed fleet-wide, so a repository cannot edit the reference out +without failing the verbatim check instead, and a rule banning it would be unsatisfiable rather +than merely strict. The exception is scoped to the verbatim region and never leaks past it: the +same document's own prose, outside that region, is governed normally. A reference that reaches a +verbatim section is a defect in the canonical, fixed once at the source rather than reported +against every repository carrying it. + +**The second exception is a hub-hosted tool the reader is told to run**, which is a different kind +of reference. A rule naming a gate, a script, or a reference snippet the reader executes or copies +states an instruction rather than a provenance, and an instruction with no destination is +unfollowable, which is precisely how a pointer in carried text comes to read as decorative. The +test is whether the reference is something the reader *does* or something that *happened to this +file*: where the content came from stays out, what the reader runs stays in. Such a pointer names +the hub's canonical rather than this repository's provenance, so it is the hub's to keep resolving +and never a repository's to edit out or re-point at a local path. What is reached rather than +carried, and how, is `GOVERNANCE.md` "Hub-Hosted Tooling". In `AGENTS.md` and `GOVERNANCE.md` this +belongs in verbatim rule text, the same region the first exception already covers, so the whole +fleet reads one wording and no repository is asked to answer for a reference it did not write. + +## What is not a coordination reference + +**A contextually relevant link to a related project is expected, not banned.** Where another repo +is part of this repo's subject matter (the image that consumes this config, the builder that +generates this hardware, a library this depends on), link it normally. The test is whether the +link serves a reader of *this* repo's content, not whether the target happens to be in the fleet. + +This pairs with the present-tense rule: state the current shape, not a history of which repo it +came from. diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md new file mode 100644 index 00000000..7791569f --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md @@ -0,0 +1,132 @@ +# Line Ending Policy + +Full detail for the "Line endings" rule in `SKILL.md`. Load this when choosing an ending for a +new file type, working in an operational (config) repo, pinning an extensionless executable, or +auditing a repo's endings, not for an ordinary content edit to an existing file (the SKILL.md +summary, preserve the existing ending and verify with a byte scan, covers that case). + +## The defaults + +- **[`.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, for example `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 committed and does not + normalize) plus the matching LF pins. +- **Both files are required together.** `.editorconfig` governs the editor, `.gitattributes` + governs git (checkout, commit, `--renormalize`). A repo missing either file, or whose + `.editorconfig` sets no global `end_of_line` default (for example declares it only under + `[*.md]`), accumulates files mixed between LF and CRLF, the exact failure these two files + prevent together. Carry both files whole (an inert `[*.cs]` block costs nothing in a non-.NET + repo), including the `*.sh text eol=lf` pin and any extensionless-script path pins. + +## Choosing an ending for a new file type + +CRLF is the default, since 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 and shebangs (`*.sh`, s6, husky), Dockerfiles (CRLF breaks `RUN` +heredocs and line continuations), and tool-owned formats with a native LF ending (KiCad). +Non-workflow YAML stays CRLF, GitHub Actions' parser tolerates it (a repo also running yamllint +sets `new-lines: disable` to defer to `.editorconfig`). Workflow YAML +(`.github/workflows/*.{yml,yaml}`) is pinned LF in `.editorconfig`, because Dependabot and Actions +rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This LF +class is not backed by a `.gitattributes` pin, git keeps `* -text`, and CI's `editorconfig-checker` +(EOL-only) catches a mismatch instead. + +Distinguish where a file is *consumed* from where it is *edited*, consumption on Linux alone does +not force LF. A config or pattern file consumed by a Linux tool stays CRLF when the tool tolerates +a trailing CR: `.dockerignore` and `.gitignore` are CRLF (their parsers strip the CR), and only a +*Dockerfile*, interpreted and broken by a CR in a `RUN` heredoc or line continuation, is LF. + +## Operational (config) repos + +The global default follows the consuming application's native platform, not the fleet CRLF +default. A config repo (registry `workflowModel: operational`) is a view into an application's +configuration directory, often the exact tree mounted into that app's container, so its files use +the ending the app itself reads and writes, and forcing the fleet CRLF default would fight the +app. Set the `[*] end_of_line` default to the app's native ending and record it in the registry +`lineEndings` field (`lf` or `crlf`): LF for a Linux-native app whose config lives in a Linux +container (ESPHome, Home Assistant, a devcontainer-only or HACS config), CRLF for a Windows-native +editor, for example Vantage InFusion config edited by Design Center on Windows. The +execution-sensitive LF pins (`*.sh`, Dockerfiles, workflow YAML) still apply on top, and +`.gitattributes` still mirrors the chosen default. This override is for operational repos only, +`release` repos keep the `[*] end_of_line = crlf` fleet default above. Do not re-normalize an +operational repo to the fleet default, that is exactly the over-normalization these per-repo +endings exist to prevent. + +**Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed +repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited +subtree), the clean answer is a repo per consumer, each single-platform with its own +`lineEndings`. For example a controller config edited by a Windows-native editor lives in its own +CRLF repo, not as a subtree inside a Linux `lf` config repo. Fallback only if a subtree genuinely +cannot be split out: keep the global default at the primary consumer and pin the odd subtree with +an `.editorconfig` path override (for example `[<subtree>/**] end_of_line = crlf`) matching its +consumer. The global `* -text` in `.gitattributes` already preserves those bytes, no extra git pin +is needed. + +## 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 or 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. `.gitattributes` is the +enforcement layer: it carries `*.sh text eol=lf`, and any repo whose tooling ships extensionless +scripts adds the matching path pin (`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 or git +hooks, or otherwise ships executable scripts. The same explicit-pin rule extends to tool-owned +file formats the base config does not key on: pin them to whatever ending the tool reads and +writes, for example KiCad project, footprint, and 3D files (`*.kicad_mod`, `*.kicad_sym`, +`*.step`), which KiCad writes LF. + +**Pair each such pin with a matching `.editorconfig` override**, since the git pin alone is not +enough, `.gitattributes` governs git while the editor follows `.editorconfig`, where the default +still applies to any file no extension rule covers. Give every extensionless executable an +editorconfig LF override beside its `.gitattributes` pin (`[.husky/pre-commit] end_of_line = lf`), +and for a byte-preserve data directory (downloaded or opaque source whose exact bytes the consumer +may depend on) disable all editor normalization, not just EOL: `[<dir>/*]` with `charset = unset`, +`end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` +is EditorConfig's spec-defined special value that removes an inherited property). + +## Editing discipline + +- **New files**: create with the `.editorconfig`-mandated ending. +- **Editing an existing file**: preserve its 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. If a diff balloons to the whole file, the endings flipped, 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, call it out explicitly in + the commit or PR description and verify the content separately with + `git diff --ignore-cr-at-eol`. + +## Auditing + +Don't trust `file` or a naive `git ls-files --eol`. The authoritative check is a byte scan that +classifies by which endings are present: CRLF-only (every `\n` preceded by `\r`), LF-only (no +`\r`), or mixed (both forms present). Flag mixed explicitly rather than lumping it in with CRLF, +and skip binaries via a NUL-byte check. `file` mislabels some types (it reports a CRLF `.json` or +`.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 a tool that inserts multiple lines or writes a new file into a +CRLF file must emit `\r\n`, since a naive `\n` insert creates mixed endings. `.code-workspace` is +JSONC (it has `//` comments), so strip them before JSON-parsing it. + +Editing CRLF files programmatically with a regex has a sharper trap: `.` matches `\r`, so a +captured line keeps its carriage return and rejoining with `\r\n` yields `CRCRLF`. A text-mode +rewrite has the mirror failure, silently flattening CRLF to LF. Prefer line-based edits +(`splitlines(keepends=True)`) or literal replacement over regex reassembly. In Python the +text-mode failure is the default: `Path.read_text()` decodes through universal newlines and +`write_text()` writes `\n` back, so a read-edit-write round trip flattens the whole file while the +edit itself looks correct. Pass `newline=''` to both, or work in bytes. diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/markdown-links.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/markdown-links.md new file mode 100644 index 00000000..e7251ccb --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/markdown-links.md @@ -0,0 +1,64 @@ +# Reference-Style Links + +Full detail for the "Markdown formatting" reference-style-links rule in `SKILL.md`. Load this +when actually authoring or reorganizing a Markdown file's link definitions, not for a small +in-place prose edit. + +## Where the rule applies + +Every Markdown file in the repo uses reference-style links only, except the four files that are +read one section at a time rather than end to end: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, +and `.github/copilot-instructions.md`. Those keep inline `[text](uri)` links, since a reader +jumping straight to one section needs the target to resolve where it is, while a definition parked +at the bottom of the file is never reached. The exception is that closed list of four files, never +a category to argue from case by case. Every other Markdown file follows the rule regardless of +its audience. + +## The definition block + +Every URI, an internal path, an anchor, an external URL, or a shield image, is defined at the +bottom of the file, split into groups by type under an HTML-comment header, for example: + +```markdown +<!-- Shields --> + +[license-shield]: https://img.shields.io/... + +<!-- Repo --> + +[governance]: ./GOVERNANCE.md +[governance-branching-model]: ./GOVERNANCE.md#branching-model + +<!-- External --> + +[markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 +``` + +Within a group, definitions are alphabetized by **reference name alone**, the text inside the +brackets, never by the whole definition line. Where one name is a prefix of another, the shorter +one sorts first: `[governance]` above `[governance-branching-model]`, `[repo-config]` above +`[repo-config-settings]`. Sorting the full line instead inverts every such pair, because `-` +precedes `]` in byte order, so the two readings disagree on exactly the names a reader looks up +together, and a plain `sort -c` over the block passes on the inverted order regardless. + +## Naming a reference + +Reference names are contextual and encode both the target and its group: + +- `foo-shield` for a shield image +- `foo-link` for an external URL +- a bare `foo` for a local path or anchor + +For example `[license-shield]`, `[releases-link]`, `[repo-config]`. Never a numeric name (`[1]`) +and never an opaque one. + +## Mechanics + +- No inline `[text](uri)` targets in prose, in any file outside the four-file exception above. +- **A URL inside a fenced code block stays inline.** Reference links do not resolve inside a code + block, so do not extract it there, and exclude fenced code from any link-integrity check + (bracket literals like `["a", "b"]` otherwise read as undefined references). +- **Removing a link also removes its reference definition.** An orphaned definition fails the + no-unused-defs rule. +- The one exception to "no inline links" is the Table of Contents, whose entries stay inline + anchor links, since the ToC extension generates them that way and they are never hand-edited. diff --git a/AGENTS.md b/AGENTS.md index c47393b2..d78357dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,8 +76,8 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R | A live config repo rather than a code repo | `Operational Repositories` | | Onboarding a repo or running a conformance sweep | `Repository Onboarding and Conformance` (hub only, not carried) | | Running a fleet gate, the review digest, or the config script | `Hub-Hosted Tooling` | -| Writing a commit message or pull request title | `Pull Request Title and Commit Message Conventions` | -| Any prose, comment, doc, or line-ending change | `Documentation Style Conventions` | +| Writing a commit message or pull request title | `Pull Request Title and Commit Message Conventions`, packaged as the `comment-and-doc-style` Skill | +| Any prose, comment, doc, or line-ending change | `Documentation Style Conventions`, packaged as the `comment-and-doc-style` Skill | | Proving work actually happened | `Verification Discipline` | | Requesting, answering, or closing a review | `PR Review Etiquette`, packaged as the `pr-review-conduct` Skill | | Reporting progress or asking the user something | `Communicating with the User` | diff --git a/CODESTYLE.md b/CODESTYLE.md index 5a07f674..efab5c12 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -10,7 +10,7 @@ These rules apply to every language in the repo. ### Tooling Names and Casing -Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.Net`), `CSharpier`, `ruff`, `pyright`, `uv`. Don't invent personal variants. +Use each tool's official casing in task labels, docs, and prose, per the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub (not a repo-relative link, that path is hub-local and not carried into every fleet repo). ### Clean-Compile Verification @@ -31,13 +31,7 @@ Each language defines a **clean-compile** verification: the combination of build ### Markdown and Spelling -These apply repo-wide, in every directory: - -1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no Markdown equivalent, every other element is flagged, and anything with a native Markdown equivalent uses the Markdown. Fix violations at the source rather than disabling rules. -2. **Spelling**: All spelling must be clean via the CSpell VS Code integration, and words must be correctly spelled in **US English** (the repo-wide convention, per [GOVERNANCE.md][governance]). The shared `cspell.json` sets `"language": "en-US"` so British spellings are flagged, where a bare `"en"` accepts both US and British and silently passes the wrong spelling. Project-specific terms go in the shared `cspell.json` `words` list, the single source of truth the extension, CLI, and CI all read. The `.code-workspace` must **not** carry its own `cspell.words`/`cSpell.words` block, and when externalizing words into `cspell.json`, delete any word list left in the workspace (a leftover one duplicates the list and silently drifts). -3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only**, because these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many Markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, Markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but README + HISTORY are the default. Keep the CI workflow, the `Lint: Spelling` VS Code task, and the GOVERNANCE.md cspell one-liner on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone, since cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md`, which does not choke on technical terms. -4. **`HISTORY.md` mirrors the README opening**: `HISTORY.md` is the maintainer-curated changelog and opens as the README's twin, carrying the same `# <Title>` (without the README's ToC-omit comment) and the same **tagline** copied verbatim, then a `## Release History` section. The tagline is the first line after the README's H1, and it is the whole of the mirror: a README may carry further paragraphs below it, explaining the project to a reader before the fold, and the changelog does not repeat them, because it opens on the identity and then goes straight to the releases. The mirrored opening keeps the project identity consistent for a reader who lands on the changelog directly. The audit checks that the title and the tagline match the README, with HTML comments stripped. -5. **"Markdown" is the format's name**: The format is a proper noun, so prose capitalizes it, meaning a Markdown file, a Markdown link, and the Markdown a surface renders. Lowercase is for the strings a machine reads and for nothing else: a tool or package name (`markdownlint`, `markdownlint-cli2`, `yzhang.markdown-all-in-one`), a settings key (`markdown.extension.toc.levels`), a heading anchor (`#markdown-and-spelling`), an identifier in code, and a file extension. A hyphenated compound in prose is prose, so it capitalizes too (Markdown-only), which is the boundary a mechanical sweep gets wrong, since it reads the hyphen as the mark of an identifier. What this settles is the mix rather than either spelling, because a file carrying both gives the next author no default to follow and a reviewer a finding to raise on whichever one it wrote last. The rule lives here because every repo carries this file, so the convention arrives with it rather than being re-decided per repo. +These apply repo-wide, in every directory: Markdown lints clean via `markdownlint-cli2` against the shared config, spelling is US English via CSpell against the shared `cspell.json`, the CI spelling gate covers `README.md` and `HISTORY.md` only, `HISTORY.md` mirrors the README's opening, and "Markdown" is a proper noun in prose. The full rules are in the `comment-and-doc-style` Skill referenced above. ## .NET @@ -508,7 +502,6 @@ Bash, and only where a program cannot be Python: a bootstrap that installs the i [history]: ./HISTORY.md [line-endings]: ./GOVERNANCE.md#line-endings [markdown-and-spelling]: #markdown-and-spelling -[markdownlint-cli2]: ./.markdownlint-cli2.jsonc [readme]: ./README.md [repo-config-configure]: ./repo-config/configure.sh [root]: ./.editorconfig diff --git a/GOVERNANCE.md b/GOVERNANCE.md index fd78a56a..26876aa6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -137,105 +137,27 @@ The fleet's tooling lives in the hub once and a repository runs it from there ra ## Pull Request Title and Commit Message Conventions -### Format +A PR title and a commit message share one contract: an imperative subject, 72 characters or fewer, no trailing period, no vague titles like `update stuff` or `wip` (Dependabot's `Bump X from Y to Z` is fine as-is), no unsolicited `Co-Authored-By:` lines, and no release-bump magnitude in the title, since Nerdbank.GitVersioning computes the next version from `version.json` and git history. -- Imperative subject summarizing the change, <=72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".) -- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. - -### Rules - -- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine, so keep them.) -- Don't add `Co-Authored-By:` lines unless the developer explicitly asks. -- Don't put release-bump magnitude in the title, so no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. -- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*). - -### Examples - -```text -Add structured logging extensions to library -Pin softprops/action-gh-release to commit SHA -Drop net8.0 multi-targeting from console project -Bump xunit.v3 from 3.2.2 to 3.3.0 -Clarify devcontainer setup steps in README -``` +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full format, rules, and examples, since this section only points to it now rather than carrying the contract text itself. ## Documentation Style Conventions -- **Carried files carry no coordination references.** In the files the fleet carries (`AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, the `repo-config/` and `spec/` files, the carried `AUDIT.md`), two things are banned. **Any reference to the template repo**, in prose or in a link: the coordination flow is machinery a consumer of that repo should never have to see, and naming where a file came from is the derived-from framing the present-tense rule below independently forbids. Where a carried file must express a template-level behavior ("report a rule discrepancy upstream"), state the behavior rather than the destination. **The first exception is a verbatim section, and `AGENTS.md` "Fleet Bootstrap" is why it exists.** That section's whole function is to name where the canonical rules live, for an agent in a repository whose carried copies are stale, partial, or absent, which is exactly when no other file present can say it. Its bytes are fixed fleet-wide, so a repository cannot edit the reference out without failing the verbatim check instead, and a rule banning it would be unsatisfiable rather than merely strict. The exception is scoped to the verbatim region and never leaks past it: the same document's own prose is governed normally, so a repository that names the destination in a paragraph it owns is still in breach. A reference that reaches a verbatim section is a defect in the canonical, fixed once at the source rather than reported against every repository carrying it. The maintainer supplies the destination out of band. **The second exception is a hub-hosted tool the reader is told to run, which is a different kind of reference.** A rule naming a gate, a script, or a reference snippet the reader executes or copies states an instruction rather than a provenance, and an instruction with no destination is unfollowable, which is precisely how a pointer in carried text comes to read as decorative. The test is whether the reference is something the reader does or something that happened to this file, so where the content came from stays out and what the reader runs stays in. Such a pointer names the hub's canonical rather than this repository's provenance, so it is the hub's to keep resolving and never a repository's to edit out or to re-point at a local path, which is what left the reference resolving differently in every copy before. In `AGENTS.md` and `GOVERNANCE.md` it belongs in verbatim rule text, the same region the first exception already covers, so the whole fleet reads one wording and no repository is asked to answer for a reference it did not write. What is reached rather than carried, and how, is in [Hub-Hosted Tooling](#hub-hosted-tooling). And **a sibling fleet repo named as an illustrative example** ("repo X does it this way", "see repo Y's adoption"), which couples the repos and rots as they diverge. To point at a current good example, name it in the onboarding or conformance issue, never in a carried doc. **A contextually relevant link to a related project is not a coordination reference, and is expected.** Where another repo is part of this repo's subject matter (the image that consumes this config, the builder that generates this hardware, a library this depends on), link it normally. The test is whether the link serves a reader of *this* repo's content, not whether the target happens to be in the fleet. This rule governs carried template content. A repo's own `README.md` and topical docs are its own content, not carried verbatim, and it does not reach them. This pairs with the present-tense rule below: state the current shape, not a history of which repo it came from. - -### Markdown +The fleet's prose and formatting contract: what a carried file may reference, how Markdown links, headings, and tense are structured, the comment philosophy, the ASCII character-set tiers, the line-ending policy, and how a quantitative claim in a doc stays honest. Applies to docs and code/workflow comments alike. -- **Reference-style links in human-facing docs.** Every Markdown file uses reference-style links only, **except** the four agent-instruction files that keep inline links: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, and `.github/copilot-instructions.md`. Those are read one section at a time rather than end to end, so an inline target resolves where it is read while a definition at the bottom of the file is never reached. The exception is that closed list rather than a category to argue from, so every other Markdown file follows the rule whatever its audience. Under the rule: every URI (internal path, anchor, external URL, or shield image) is defined at the **bottom of the file**, split into groups by type under an HTML-comment header (e.g. `<!-- Shields -->`, `<!-- Workflow -->`, `<!-- Repo -->`, `<!-- External -->`) with each group's definitions alphabetized by reference name. **The sort key is the reference name alone**, the text inside the brackets, never the whole definition line: where one name is a prefix of another, the shorter one sorts first (`[governance]` above `[governance-branching-model]`, `[repo-config]` above `[repo-config-settings]`). Sorting the line instead inverts every such pair, because `-` precedes `]` in byte order, so the two readings disagree on exactly the names a reader looks up together, and a plain `sort -c` over the block passes on the inverted order. **Reference names are contextual and encode the target and its group**: `foo-shield` for a shield image, `foo-link` for an external URL, and a bare `foo` for a local path or anchor (e.g. `[license-shield]`, `[releases-link]`, `[repo-config]`), never numeric (`[1]`) or opaque. No inline `[text](uri)` targets in prose. **A URL inside a fenced code block stays inline**, because reference links do not resolve in code blocks, so do not extract it, and exclude fenced code from any link-integrity check (bracket literals like `["a", "b"]` otherwise read as undefined references). **Removing a link also removes its reference definition**, since an orphaned definition fails the no-unused-defs rule. The one exception is the Table of Contents, whose entries stay inline anchor links (see Table of Contents below). -- **Table of Contents.** Generate it with the Markdown All in One extension, which fills and auto-updates the list on save, so leave the `## Table of Contents` heading for the extension to populate and never hand-author or hand-edit the entries. Exclude a heading with an inline `<!-- omit from toc -->` marker on it (the badge/build header block and the `## Table of Contents` heading itself carry it). The workspace sets which heading levels appear. -- One logical paragraph per line, with no hard-wrap line-length limit. For an intentional hard line break within a block (stacked badges, status, or license lines), end the line with a trailing backslash (`\`). This explicit form is preferred over trailing whitespace and is not treated as a paragraph split. -- Headings follow the title-case-with-short-bind-words rule from the PR-title section. -- **Write in the present tense, describing only the current state.** The reader has no knowledge beyond what they are reading, so state what *is* (what to know, do, follow, or avoid), never a change from a prior state. Write "X does Y", never "X *now* does Y", "X *no longer* does Z", "X *still* does W", or "changed/switched/restored to Y". This applies to docs and code/workflow comments alike. Before/after framing belongs in changelogs, commit messages, and PR descriptions, where the prior state is the point, not in `README.md`, `AGENTS.md`, or other living docs. -- **When you change a behavior, search for prose that asserts the old one.** Updating the guarantee or rule you are consciously editing is not enough: comments, diagram labels, reusable-workflow input descriptions, and audit statements elsewhere may still describe the prior behavior, and each was accurate when written. Grep for the old behavior's distinctive phrasing and fix every instance. No linter catches this, because markdownlint, cspell, actionlint, and editorconfig-checker all pass on a claim that is merely untrue, so the sweep is the only mechanism that will. This is the maintenance counterpart to the present-tense rule above: that one governs how to phrase a doc, this one how to keep it true when the behavior underneath it moves. +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules, since this section only points to it now rather than carrying the contract text itself. ### Comments -Applies to code and workflow (`#`) comments alike. - -- Comment only when the code does not explain itself or the logic is genuinely complex. Self-evident code needs no comment. -- Write for the human reading *this* project's code now: state only the non-obvious *why*. No cross-project references (do not name other repos), no historic or design narrative, no rule citations, since governance lives in this file, not echoed inline. -- **Keep it short.** One line is the default. A comment earns a second line only by carrying a constraint the code cannot. Most comments are one sentence, and never restate *what* the code does, since a well-named symbol already says it. -- **Structured, not prose: one sentence per line, and never wrap a sentence across lines.** No block paragraphs and no multi-sentence run-ons. A comment that genuinely needs several sentences is several lines, each a single sentence. A sentence too long for one sensible line is too long, so split the thought. -- **A comment line that opens prose starts with a capital.** A line opening in lowercase reads as the continuation of the one above it, so a sentence that genuinely starts there is capitalized. A trailing fragment that annotates the code on its line is a label rather than a sentence, and the version pin the action-pinning rule requires is one, so neither takes a capital. Where the first word is a tool or identifier whose own casing is lowercase, restructure so the sentence does not open on it rather than capitalizing the name against its official spelling. -- **A multi-line comment shows whether it is a continuation or a list.** A continuation of the same topic stays unindented, one sentence per line. Mark a sub-topic with a `-` after the comment marker (`# -`, `// -`), and only for genuine sub-topics: parallel items hanging off a lead line, never a continuation of one thought. -- **No class-, type-, or file-header summary comment blocks.** A type or file gets a comment only for a specific non-obvious point, kept terse, never a block summarizing what the file contains or what the class is for. A summary restates the declaration below it, goes stale as the file grows, and is the file-scope form of the design narrative and verbosity creep this section already bans. A license or provenance header a tool or policy requires is not a summary and is unaffected. -- **Do not grow a comment across edits.** When you touch code near an existing comment, the comment must come out **same length or shorter**, never appending "one more clause" of rationale. If a block comment has crept to multiple sentences of prose, cut it back to its single load-bearing point as part of your change. Verbosity creep is the specific regression to prevent: every iteration that adds a clause is a regression, not an improvement. - -A continuation stays unindented, one sentence per line: - -```text -# Change gate for the compile tests. -# An esp-idf build costs minutes, so gate on what each test covers. -# A diff that cannot be computed runs everything. -``` - -Sub-topics take a `-` after the comment marker, each elaborating a distinct item named in the lead: - -```text -# Source lint plus change-gated compile tests. -# - compile-test builds the external component. -# - template-compile-test builds one example device per template. -``` +The full comment philosophy, what earns one, structure, capitalization, growth discipline, is in the `comment-and-doc-style` Skill referenced above. ### Character Set -Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR descriptions. A non-ASCII character is read against three tiers, because whether one is typography or meaning depends on where it sits. A character in no tier is a finding rather than a silent pass. - -- **Tier 1, never legitimate.** Typography carrying no meaning its ASCII form loses. Remove on sight: - - em dash (U+2014) and en dash (U+2013) -> **restructure the sentence**. Two sentences, or a comma. Do not substitute a spaced hyphen. - - right arrow (U+2192) -> `->`, double arrow (U+21D2) -> `=>` - - curly quotes (U+2018/U+2019/U+201C/U+201D) -> straight `'` and `"` - - ellipsis (U+2026) -> `...`, bullet (U+2022) -> `-` - - no-break space (U+00A0) -> a space, non-breaking hyphen (U+2011) -> `-` -- **Tier 2, legitimate only next to a number.** Relational and arithmetic operators: U+2264, U+2265, U+2260, U+00B1, U+2212, U+00D7, U+00F7, U+00B7. Keep one when an adjacent non-space token is a number, a tier-3 symbol, or another tier-2 operator, so a threshold table or a measured range reads as the range it is. In flowing prose write the ASCII form: `<=`, `>=`, `!=`, `+/-`, `-`, `x`, `/`. A U+2264 directly before `35` in a table of sensor thresholds is the range it describes and stays. The same character between two words, as in a sentence about one check running before another, is prose and takes `<=`. -- **Tier 3, always legitimate.** Scientific and unit symbols whose ASCII form would be a lie: micro (U+00B5), degree (U+00B0), ohm (U+2126), pi (U+03C0), superscript two and three (U+00B2, U+00B3), section (U+00A7). Keep the symbol. Do not approximate it away, and do not spell it out. -- **Unicode the developer deliberately typed** stays regardless of tier, such as emoji used for emphasis or as callout markers, for example the warning markers a maintainer placed in `README.md`. Never strip the developer's own characters. The carve-out governs what an agent may rewrite rather than what the gate reports, so an un-tiered character of this kind is still a `charset-unknown` finding until someone classifies it. It covers developer-authored text, and is not a license for the agent to add emoji. -- **An unrecognized non-ASCII character is reported, not allowed.** Classify it into a tier above before using it. A gate that passes whatever it does not recognize stops gating as the character set grows, which is the silent-narrowing failure named under "Verification Discipline". -- **No spaced hyphen joining or interrupting a sentence.** The em-dash-style clause break ` - `, and the paired aside ` - x - `, are both recast: a comma where the clauses are short and closely linked, two sentences where they are not, or parentheses for a genuine aside. This is the same construction the tier-1 em dash is restructured into, so allowing its ASCII spelling would keep the shape and only change the character. A hyphen inside a compound word, a leading list marker, a range, and the `- **Label** - explanation` separator that opens a governed bullet keep their standard use: the last is structurally a colon, and flagging it would restructure the document format rather than the prose. Existing prose is corrected as each file is next edited, not swept. -- **No semicolon in agent-authored prose.** A mid-sentence semicolon joining clauses is recast as a comma or as two sentences. A semicolon separating items in a list that already contains commas keeps its standard use, and a statement terminator in **code** is untouched. A colon introducing an explanation, elaboration, or list is not a semicolon and is unaffected. Existing prose is corrected as each file is next edited, not swept. -- **In carried verbatim content the hub sweeps a class rather than waiting for the next edit.** Correct-as-you-next-edit assumes someone able to edit the file, and a downstream repo cannot edit a verbatim section, since its copy is byte-matched against the hub's. So a review finding against carried rule text is fixed at the hub, for the whole class at once, and re-vendored. Fixing one instance leaves the class to resurface in a different section on the next downstream review, where it still cannot be answered. This is scoped to verbatim content. A file a repo owns, or one carried at `intent` fidelity, is corrected as it is next edited like any other prose. +The full ASCII tier system (never legitimate, legitimate next to a number, always legitimate, developer-typed Unicode) and the semicolon and spaced-hyphen rules are in the `comment-and-doc-style` Skill referenced above. ### Line Endings -- **[`.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**, since 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). **Non-workflow YAML stays CRLF**, since GitHub Actions' parser tolerates it (a repo that also runs yamllint sets `new-lines: disable` to defer to `.editorconfig`). **Workflow YAML (`.github/workflows/*.{yml,yaml}`) is pinned LF** in `.editorconfig`, because Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This is an LF class **not** backed by a `.gitattributes` pin: git keeps `* -text` (no normalization), and CI's `editorconfig-checker` (EOL-only) catches a mismatch instead. Distinguish where a file is *consumed* from where it is *edited*: consumption on Linux alone does not force LF. A config or pattern file consumed by a Linux tool stays CRLF when the tool tolerates a trailing CR: `.dockerignore` and `.gitignore` are CRLF (their parsers strip the CR), and only a *Dockerfile*, interpreted and broken by a CR in a `RUN` heredoc or line continuation, is LF. -- **Operational (config) repos: the global default follows the consuming application's native platform, not the fleet CRLF default.** A config repo (registry `workflowModel: operational`) is a *view into an application's configuration directory*, often the exact tree mounted into that app's container, so its files must use the ending the app itself reads and writes, and forcing the fleet CRLF default would fight the app. Set the `[*] end_of_line` default to the app's native ending and record it in the registry `lineEndings` field (`lf` | `crlf`): **LF** for a Linux-native app whose config lives in a Linux container (ESPHome, Home Assistant, a devcontainer-only or HACS config), and **CRLF** for a Windows-native editor, for example Vantage InFusion config edited by Design Center on Windows. The execution-sensitive LF pins (`*.sh`, Dockerfiles, workflow YAML) still apply on top, and `.gitattributes` still mirrors the chosen default. This override is for operational repos only, and `release` repos keep the `[*] end_of_line = crlf` fleet default above. Do **not** re-normalize such a repo to the fleet default, which is exactly the over-normalization these per-repo endings prevent. - - **Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited subtree), the clean answer is a repo per consumer, each single-platform with its own `lineEndings`. For example a controller config edited by a Windows-native editor (CRLF) lives in its own repo, **not** as a subtree inside a Linux-`lf` config repo. That keeps each repo's default, CI, and checkout matched to one platform and avoids per-path EOL machinery entirely. **Fallback only if a subtree genuinely cannot be split out:** keep the global default at the primary consumer and pin the odd subtree with an `.editorconfig` path override (e.g. `[<subtree>/**] end_of_line = crlf`) matching its consumer, treated like any tool-owned format. The global `* -text` in `.gitattributes` already preserves those bytes, so no extra git pin is needed. -- **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** (`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, for example 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. -- **Pair each such pin with a matching `.editorconfig` override, since the git pin alone is not enough.** `.gitattributes` governs **git** (checkout, commit, `--renormalize`), while the **editor** follows `.editorconfig`, where the `[*] end_of_line = crlf` default still applies to any file no extension rule covers. So even with the git pin, the editor writes a CRLF shebang into an extensionless hook (breaking it when run from the working tree) or re-ends/trims a byte-sensitive data file. Give every extensionless **executable** an editorconfig LF override beside its `.gitattributes` pin (`[.husky/pre-commit] end_of_line = lf`), and for a **byte-preserve data directory** (downloaded or opaque source whose exact bytes the consumer may depend on) disable *all* editor normalization, not just EOL: `[<dir>/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` is EditorConfig's spec-defined special value that removes an inherited property, so the editor enforces neither the global `charset` nor `end_of_line` on that path). Keep these overrides with the line-ending governance (above any `.NET-only` divider), not in the language-style section. -- **New files:** create them with the `.editorconfig`-mandated ending. -- **Editing an existing file:** **preserve the file's current line endings**, and 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, per Auditing below). If a diff balloons to the whole file, you flipped the endings, so 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`. -- **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, and 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`, since a naive `\n` insert creates mixed endings. `.code-workspace` is JSONC (it has `//` comments), so 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 - -- Any quantitative claim in `README.md` (counts, sizes, version floors, supported platforms) must be verified against current code. If a doc number is derived from a code constant, mark the dependency in a source-code comment so the next editor knows to update both. +The full CRLF/LF policy (`.editorconfig` and `.gitattributes` defaults and pins, choosing an ending for a new file type, operational-repo overrides, editing discipline, and auditing) is in the `comment-and-doc-style` Skill referenced above. ## Verification Discipline From 9542befd26b7d8bc28a38cae81012344476d6305 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:18:22 -0700 Subject: [PATCH 2/5] Fix Copilot findings and CI failure on the comment-and-doc-style skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both raised by the round 1 review of #678. - references/line-endings.md carried an inline Markdown link to .editorconfig, violating the skill's own reference-style-links rule (Copilot finding, present in the hub copy and the generated dist copy). Dropped the link, matching every other skill file's zero-link precedent. - scripts/test_prose_lint.py's TestGovernanceCoupling parses GOVERNANCE.md's Character Set bullets by heading and cross-checks the U+XXXX codepoints named there against prose_lint.py's TIER1/TIER2/ TIER3 tables, so the doc drives the code rather than a copy of the doc drifting from it. Trimming GOVERNANCE.md's Character Set to a pointer broke that parse (CI: "the Tier 1 bullet moved, so the parse is blind"). Repointed the test at the skill, the section's new canonical home, and restored the tier bullets there with their exact codepoints (they had been condensed into a table that dropped the U+XXXX notation the test keys on). Removed the now-unused GOVERNANCE constant. ## Verification performed scripts/test_prose_lint.py (223 tests), scripts/prose_lint.py, editorconfig-checker (docker), markdownlint-cli2, spec/validate.py, scripts/build_dist.py --check, scripts/test_build_dist.py (19 tests) all clean. CRLF byte-integrity confirmed on every touched Markdown file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- .agents/skills/comment-and-doc-style/SKILL.md | 39 +++++++++++++------ .../references/line-endings.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/comment-and-doc-style/SKILL.md | 39 +++++++++++++------ .../references/line-endings.md | 2 +- scripts/test_prose_lint.py | 21 +++++----- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index 1e654160..4ba93310 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -122,17 +122,34 @@ Sub-topics take a `-` after the comment marker, each elaborating a distinct item ## Character set -Agent-authored text (docs, code, comments, commit messages, PR descriptions) is ASCII by default. -A non-ASCII character is judged by tier, and one that fits no tier is a finding, never a silent -pass. - -| Tier | Rule | Examples | -| --- | --- | --- | -| 1, never legitimate | Restructure the sentence instead of substituting an ASCII lookalike | em/en dash to a comma or two sentences (never a spaced hyphen), arrows to `->`/`=>`, curly quotes to straight quotes, ellipsis to `...`, bullet to `-` | -| 2, legitimate only next to a number | Keep next to a number, a tier-3 symbol, or another tier-2 operator, otherwise use the ASCII form | keep `<=35` in a threshold table, write `<=`, `>=`, `!=`, `+/-`, `x`, `/` in flowing prose | -| 3, always legitimate | Keep, never approximate away or spell out | micro, degree, ohm, pi, superscript two and three, section symbols | -| developer-typed | Never strip, and not a license for the agent to add its own | emoji a developer deliberately placed for emphasis | - +Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR +descriptions. A non-ASCII character is read against three tiers, because whether one is +typography or meaning depends on where it sits. A character in no tier is a finding rather than a +silent pass. + +- **Tier 1, never legitimate.** Typography carrying no meaning its ASCII form loses. Remove on + sight: + - em dash (U+2014) and en dash (U+2013) to a restructured sentence, two sentences or a comma, + never a spaced hyphen + - right arrow (U+2192) to `->`, double arrow (U+21D2) to `=>` + - curly quotes (U+2018/U+2019/U+201C/U+201D) to straight `'` and `"` + - ellipsis (U+2026) to `...`, bullet (U+2022) to `-` + - no-break space (U+00A0) to a space, non-breaking hyphen (U+2011) to `-` +- **Tier 2, legitimate only next to a number.** Relational and arithmetic operators: U+2264, + U+2265, U+2260, U+00B1, U+2212, U+00D7, U+00F7, U+00B7. Keep one when an adjacent non-space token + is a number, a tier-3 symbol, or another tier-2 operator, so a threshold table or a measured + range reads as the range it is. In flowing prose write the ASCII form: `<=`, `>=`, `!=`, `+/-`, + `-`, `x`, `/`. A tier-2 operator directly before a number in a table of thresholds is the range + it describes and stays, the same character between two words in a sentence is prose and takes + the ASCII form. +- **Tier 3, always legitimate.** Scientific and unit symbols whose ASCII form would be a lie: + micro (U+00B5), degree (U+00B0), ohm (U+2126), pi (U+03C0), superscript two and three (U+00B2, + U+00B3), section (U+00A7). Keep the symbol, never approximate it away or spell it out. +- **Unicode a developer deliberately typed** stays regardless of tier, such as emoji used for + emphasis or as callout markers. Never strip a developer's own characters, this is developer + authored text and not a license for the agent to add its own. +- **An unrecognized non-ASCII character is reported, not allowed.** Classify it into a tier above + before using it. - **No semicolon in agent-authored prose.** Recast a mid-sentence semicolon as a comma or as two sentences. A semicolon separating items in a list that already contains commas, or a statement terminator in code, is unaffected. diff --git a/.agents/skills/comment-and-doc-style/references/line-endings.md b/.agents/skills/comment-and-doc-style/references/line-endings.md index 7791569f..63f47774 100644 --- a/.agents/skills/comment-and-doc-style/references/line-endings.md +++ b/.agents/skills/comment-and-doc-style/references/line-endings.md @@ -7,7 +7,7 @@ summary, preserve the existing ending and verify with a byte scan, covers that c ## The defaults -- **[`.editorconfig`](../../../../.editorconfig) sets the line ending.** `[*] end_of_line = crlf` +- **`.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, for example `spec/validate.py`, vanilla `.py` stays CRLF, since diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 80f06874..fbb6d071 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -e561f9c04d4b184a +64d472ca79ed5c58 diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md index 1e654160..4ba93310 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md @@ -122,17 +122,34 @@ Sub-topics take a `-` after the comment marker, each elaborating a distinct item ## Character set -Agent-authored text (docs, code, comments, commit messages, PR descriptions) is ASCII by default. -A non-ASCII character is judged by tier, and one that fits no tier is a finding, never a silent -pass. - -| Tier | Rule | Examples | -| --- | --- | --- | -| 1, never legitimate | Restructure the sentence instead of substituting an ASCII lookalike | em/en dash to a comma or two sentences (never a spaced hyphen), arrows to `->`/`=>`, curly quotes to straight quotes, ellipsis to `...`, bullet to `-` | -| 2, legitimate only next to a number | Keep next to a number, a tier-3 symbol, or another tier-2 operator, otherwise use the ASCII form | keep `<=35` in a threshold table, write `<=`, `>=`, `!=`, `+/-`, `x`, `/` in flowing prose | -| 3, always legitimate | Keep, never approximate away or spell out | micro, degree, ohm, pi, superscript two and three, section symbols | -| developer-typed | Never strip, and not a license for the agent to add its own | emoji a developer deliberately placed for emphasis | - +Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR +descriptions. A non-ASCII character is read against three tiers, because whether one is +typography or meaning depends on where it sits. A character in no tier is a finding rather than a +silent pass. + +- **Tier 1, never legitimate.** Typography carrying no meaning its ASCII form loses. Remove on + sight: + - em dash (U+2014) and en dash (U+2013) to a restructured sentence, two sentences or a comma, + never a spaced hyphen + - right arrow (U+2192) to `->`, double arrow (U+21D2) to `=>` + - curly quotes (U+2018/U+2019/U+201C/U+201D) to straight `'` and `"` + - ellipsis (U+2026) to `...`, bullet (U+2022) to `-` + - no-break space (U+00A0) to a space, non-breaking hyphen (U+2011) to `-` +- **Tier 2, legitimate only next to a number.** Relational and arithmetic operators: U+2264, + U+2265, U+2260, U+00B1, U+2212, U+00D7, U+00F7, U+00B7. Keep one when an adjacent non-space token + is a number, a tier-3 symbol, or another tier-2 operator, so a threshold table or a measured + range reads as the range it is. In flowing prose write the ASCII form: `<=`, `>=`, `!=`, `+/-`, + `-`, `x`, `/`. A tier-2 operator directly before a number in a table of thresholds is the range + it describes and stays, the same character between two words in a sentence is prose and takes + the ASCII form. +- **Tier 3, always legitimate.** Scientific and unit symbols whose ASCII form would be a lie: + micro (U+00B5), degree (U+00B0), ohm (U+2126), pi (U+03C0), superscript two and three (U+00B2, + U+00B3), section (U+00A7). Keep the symbol, never approximate it away or spell it out. +- **Unicode a developer deliberately typed** stays regardless of tier, such as emoji used for + emphasis or as callout markers. Never strip a developer's own characters, this is developer + authored text and not a license for the agent to add its own. +- **An unrecognized non-ASCII character is reported, not allowed.** Classify it into a tier above + before using it. - **No semicolon in agent-authored prose.** Recast a mid-sentence semicolon as a comma or as two sentences. A semicolon separating items in a list that already contains commas, or a statement terminator in code, is unaffected. diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md index 7791569f..63f47774 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/line-endings.md @@ -7,7 +7,7 @@ summary, preserve the existing ending and verify with a byte scan, covers that c ## The defaults -- **[`.editorconfig`](../../../../.editorconfig) sets the line ending.** `[*] end_of_line = crlf` +- **`.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, for example `spec/validate.py`, vanilla `.py` stays CRLF, since diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index f59ab50d..b587ad37 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -24,7 +24,7 @@ import prose_lint REPO = Path(__file__).resolve().parent.parent -GOVERNANCE = REPO / 'GOVERNANCE.md' +COMMENT_AND_DOC_STYLE_SKILL = REPO / '.agents' / 'skills' / 'comment-and-doc-style' / 'SKILL.md' # Bait assembled from two literals, so this module never holds the pattern it feeds the gate. # A file full of rejected input would otherwise report itself. @@ -132,15 +132,17 @@ def test_each_tier_covers_a_plausible_number_of_characters(self) -> None: class TestGovernanceCoupling(unittest.TestCase): """The rule text drives the tables, rather than a copy of the rule driving them. - These are the cases that catch an incomplete or mis-tiered table, which no bait built from the - tables themselves can do: bait proves the matching works, not that the data is right. + The Character Set rule text lives in the comment-and-doc-style Skill, GOVERNANCE.md's own + section is a pointer at it, per the "skill becomes sole canonical content" decision. These are + the cases that catch an incomplete or mis-tiered table, which no bait built from the tables + themselves can do: bait proves the matching works, not that the data is right. """ def setUp(self) -> None: - self.doc = GOVERNANCE.read_text(encoding='utf-8') - section = re.search(r'^### Character Set$(.*?)^### ', self.doc, re.MULTILINE | re.DOTALL) + self.doc = COMMENT_AND_DOC_STYLE_SKILL.read_text(encoding='utf-8') + section = re.search(r'^## Character set$(.*?)^## ', self.doc, re.MULTILINE | re.DOTALL) if section is None: - self.fail('the Character Set heading moved, so the parse is blind') + self.fail('the Character set heading moved, so the parse is blind') self.section = section.group(1) def tier_codepoints(self, label: str) -> set[int]: @@ -1342,9 +1344,10 @@ def test_the_repo_is_clean_of_british_spellings(self) -> None: class TestCarriedContent(unittest.TestCase): """Content the fleet copies byte-matched, which only the hub can ever correct. - GOVERNANCE "Character Set" states the obligation: correct-as-you-next-edit assumes someone - able to edit the file, and a downstream repo cannot edit a verbatim one, since its copy is - byte-matched against the hub's. So the hub sweeps the class and re-vendors. That makes a + The comment-and-doc-style Skill's "Character set" section states the obligation: + correct-as-you-next-edit assumes someone able to edit the file, and a downstream repo cannot + edit a verbatim one, since its copy is byte-matched against the hub's. So the hub sweeps the + class and re-vendors. That makes a finding in a verbatim file different in kind from the tree-wide backlog: it is not a correction owed by whoever next edits the file, it is one no downstream repo can make at all. """ From 23057d23d1ac973d718e029cff651261a8645d07 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:24:03 -0700 Subject: [PATCH 3/5] Fix two suppressed prose findings on test_prose_lint.py docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 review of #678, both suppressed (no thread since the docstring lines the prior fix touched carried no open thread of their own). - TestGovernanceCoupling's docstring joined two independent clauses with a bare comma (comma splice). Split into two sentences. - TestCarriedContent's docstring left "That makes a" as an orphaned two-word fragment line from an earlier edit. Rewrapped the paragraph to natural line width, matching how the rest of the file wraps docstring prose. ## Verification performed scripts/test_prose_lint.py (223 tests), scripts/prose_lint.py, editorconfig-checker (docker) all clean. LF byte-integrity confirmed (this file is pinned LF in .gitattributes). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- scripts/test_prose_lint.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index b587ad37..2b950932 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -132,10 +132,11 @@ def test_each_tier_covers_a_plausible_number_of_characters(self) -> None: class TestGovernanceCoupling(unittest.TestCase): """The rule text drives the tables, rather than a copy of the rule driving them. - The Character Set rule text lives in the comment-and-doc-style Skill, GOVERNANCE.md's own - section is a pointer at it, per the "skill becomes sole canonical content" decision. These are - the cases that catch an incomplete or mis-tiered table, which no bait built from the tables - themselves can do: bait proves the matching works, not that the data is right. + The Character Set rule text lives in the comment-and-doc-style Skill. + GOVERNANCE.md's own section is a pointer at it, per the "skill becomes sole canonical + content" decision. These are the cases that catch an incomplete or mis-tiered table, which + no bait built from the tables themselves can do: bait proves the matching works, not that + the data is right. """ def setUp(self) -> None: @@ -1346,10 +1347,11 @@ class TestCarriedContent(unittest.TestCase): The comment-and-doc-style Skill's "Character set" section states the obligation: correct-as-you-next-edit assumes someone able to edit the file, and a downstream repo cannot - edit a verbatim one, since its copy is byte-matched against the hub's. So the hub sweeps the - class and re-vendors. That makes a - finding in a verbatim file different in kind from the tree-wide backlog: it is not a - correction owed by whoever next edits the file, it is one no downstream repo can make at all. + edit a verbatim one, since its copy is byte-matched against the hub's. + So the hub sweeps the class and re-vendors. + That makes a finding in a verbatim file different in kind from the tree-wide backlog: it is + not a correction owed by whoever next edits the file, it is one no downstream repo can make + at all. """ def verbatim_paths(self) -> list[Path]: From 83e510d0f723de92ad45cf69da32ca302f9cd421 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:29:24 -0700 Subject: [PATCH 4/5] Fix round 3 findings: pointer wording and a misleading test class name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 3 review of #678. - GOVERNANCE.md's three skill-pointer paragraphs (Pull Request Title and Commit Message Conventions, Documentation Style Conventions, and the pre-existing PR Review Etiquette from #677) each said the section "only points to it now rather than carrying the contract text itself" directly under a paragraph that does summarize the contract, reading as a self-contradiction. Reworded all three the same way: the paragraph above is a sketch, the skill carries the full text. Swept the pre-existing PR Review Etiquette instance too, not just the two new ones the review flagged, since it is the same construction in the same file. - Renamed TestGovernanceCoupling to TestCommentAndDocStyleSkillCoupling in test_prose_lint.py, since the prior commit repointed it at the Skill and the old name no longer describes what it couples to. ## Verification performed scripts/test_prose_lint.py (223 tests), scripts/prose_lint.py, editorconfig-checker (docker), markdownlint-cli2, spec/validate.py all clean. CRLF byte-integrity confirmed on GOVERNANCE.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- GOVERNANCE.md | 6 +++--- scripts/test_prose_lint.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 26876aa6..78c8fad4 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -139,13 +139,13 @@ The fleet's tooling lives in the hub once and a repository runs it from there ra A PR title and a commit message share one contract: an imperative subject, 72 characters or fewer, no trailing period, no vague titles like `update stuff` or `wip` (Dependabot's `Bump X from Y to Z` is fine as-is), no unsolicited `Co-Authored-By:` lines, and no release-bump magnitude in the title, since Nerdbank.GitVersioning computes the next version from `version.json` and git history. -This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full format, rules, and examples, since this section only points to it now rather than carrying the contract text itself. +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the full format, rules, and examples. ## Documentation Style Conventions The fleet's prose and formatting contract: what a carried file may reference, how Markdown links, headings, and tense are structured, the comment philosophy, the ASCII character-set tiers, the line-ending policy, and how a quantitative claim in a doc stays honest. Applies to docs and code/workflow comments alike. -This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules, since this section only points to it now rather than carrying the contract text itself. +This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the full rules. ### Comments @@ -183,7 +183,7 @@ The checks that separate work actually done from work that merely reports succes The provider-agnostic review-loop contract every fleet repo follows: request a review on every push, confirm it covers the current head SHA and the full diff rather than only part of it, triage every finding including the low-confidence ones a review body collapses rather than threads, and reply and resolve. Never merge on a green or CLEAN merge state alone, since that field can go clean once checks pass and every known thread is resolved while still saying nothing about whether the review covered the current head SHA, read the full diff, or left a suppressed finding, which opens no thread at all, unanswered. -This is packaged as the `pr-review-conduct` Skill at `.agents/skills/pr-review-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the merge gate, the expected loop, and how a finding is closed, since this section only points to it now rather than carrying the contract text itself. +This is packaged as the `pr-review-conduct` Skill at `.agents/skills/pr-review-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the merge gate, the expected loop, and how a finding is closed. The provider-specific mechanics this contract needs to actually drive GitHub Copilot, how to request a review, poll for it, match the suppressed-findings heading, verify coverage, and reply or resolve a thread without a hand-typed id, live in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) "GitHub Copilot Review Runbook", unaffected by this pointer. That content is read directly by the Copilot reviewer bot, which cannot read a Skill, so it stays fully written out rather than being packaged the same way. diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index 2b950932..542b5a37 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -129,7 +129,7 @@ def test_each_tier_covers_a_plausible_number_of_characters(self) -> None: self.assertGreaterEqual(len(table), floor) -class TestGovernanceCoupling(unittest.TestCase): +class TestCommentAndDocStyleSkillCoupling(unittest.TestCase): """The rule text drives the tables, rather than a copy of the rule driving them. The Character Set rule text lives in the comment-and-doc-style Skill. From 7496b7bd2da2e0d66537e438c63337fb60302c6b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:21:33 -0700 Subject: [PATCH 5/5] Fix round 4 finding: comment rule pointed at the file it now supersedes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 4 review of #678. The skill's own Comments rule said "governance lives in GOVERNANCE.md, not echoed inline", but this PR moves the canonical comment rules into the skill itself, so the pointer named the file the skill just superseded. Reworded to point at the fleet's instruction set rather than a specific file, so it does not go stale the next time content moves between files. ## Verification performed scripts/test_prose_lint.py (223 tests), scripts/prose_lint.py, editorconfig-checker (docker), markdownlint-cli2, scripts/build_dist.py --check, scripts/test_build_dist.py (19 tests) all clean. CRLF byte-integrity confirmed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- .agents/skills/comment-and-doc-style/SKILL.md | 4 ++-- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/comment-and-doc-style/SKILL.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index 4ba93310..d5a1440f 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -87,8 +87,8 @@ Applies to code and workflow (`#`) comments alike. - Comment only when the code does not explain itself, or the logic is genuinely complex. Self-evident code needs no comment. - State only the non-obvious *why*, for the human reading *this* project's code now. No - cross-project references, no historic or design narrative, no rule citations (governance lives - in `GOVERNANCE.md`, not echoed inline). + cross-project references, no historic or design narrative, no rule citations. Governance lives + in the fleet's own instruction set, not echoed inline. - **Keep it short**: one line is the default. A second line is earned only by a constraint the code cannot otherwise carry. - **Structured, not prose**: one sentence per line, never wrapped across lines, never a diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index fbb6d071..53a36c89 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -64d472ca79ed5c58 +1d466f7f6d091eb9 diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md index 4ba93310..d5a1440f 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md @@ -87,8 +87,8 @@ Applies to code and workflow (`#`) comments alike. - Comment only when the code does not explain itself, or the logic is genuinely complex. Self-evident code needs no comment. - State only the non-obvious *why*, for the human reading *this* project's code now. No - cross-project references, no historic or design narrative, no rule citations (governance lives - in `GOVERNANCE.md`, not echoed inline). + cross-project references, no historic or design narrative, no rule citations. Governance lives + in the fleet's own instruction set, not echoed inline. - **Keep it short**: one line is the default. A second line is earned only by a constraint the code cannot otherwise carry. - **Structured, not prose**: one sentence per line, never wrapped across lines, never a