Retrospective: cross-platform, dev-tooling, and agent-workflow learnings to encode as template rules
Over a multi-day effort I converged two repos in this family and cut a release: ptr727/homeassistant-purpleair (a HACS integration, dependency-locked to Linux) and ptr727/aiopurpleair (a pure-Python uv library published to PyPI). This issue is a retrospective of everything that bit us, what we tried, what worked, and the decisions we made on genuinely-ambiguous cases.
Because ProjectTemplate is moving to a rules / AGENTS / automated-validation-only model (no sample projects to keep in sync), each learning is framed as a rule to encode + where it belongs, not code to copy. Categories used throughout: 🟥 MUST DO, 🟩 WHAT WORKED, 🟧 FAILED / MISTAKE CORRECTED, 🟦 NICE TO HAVE, 🐛 EXTERNAL-TOOL GOTCHA. A file-mapping table is at the end so the template agent can ingest each item into the right rule surface.
0. Headline decision framework — the one thing to internalize
🟥 A project's cross-platform support ceiling is set by its dependencies, not by tooling effort. Decide Win+Linux+macOS vs Linux-only per repo, from that ceiling — before writing any dev tooling.
- Home Assistant integration → Linux-only. HA Core has POSIX-only dependencies and does not run on Windows natively;
hass and the HA test harness won't run there. Even maximal tooling can only deliver lint-only on Windows-native, and that still forces WSL2/devcontainer for run/test — i.e. high effort, partial result, and it doesn't remove the dependency it was meant to avoid. Poor ROI → declare Linux / WSL2 / devcontainer only.
- Pure-Python uv library → cross-platform for free. Everything runs through
uv run (identical on every OS) and there are no bash dev scripts; restricting it would buy nothing. Keep it Linux/macOS/Windows.
- The right answer is therefore per-repo, not a uniform family policy. Encode a short decision procedure in the template, not a single stance.
- "Linux-only" does not preclude a VS Code-first experience — that experience lives inside WSL2/the devcontainer. Don't conflate "supported OS" with "supported editor workflow."
- The "no excuses" corollary: whichever stance a repo takes, AGENTS/CODESTYLE/copilot-instructions must state it explicitly so an agent can never hand-wave a rule ("I couldn't run
scripts/lint because I'm on Windows"). If a rule assumes bash, the docs must also assert dev is Linux-only, so the rule is always honorable in a supported environment.
- Corollary on agents: bash
scripts/* serve Linux agents/CLI only. A Windows agent uses PowerShell/python, not bash. Never claim "agents use the scripts" universally — it's only true on Linux/WSL/devcontainer.
Template rule shape: a CODESTYLE.md / AGENTS.md section "Supported development platforms" with (a) the decision procedure (what does your runtime/dep stack support?), (b) a required explicit statement per repo, (c) the VS-Code-first-≠-cross-platform note.
1. Line endings / .editorconfig / .gitattributes
This was the single most time-consuming recurring class of issue.
- 🟩 Policy that works:
.editorconfig end_of_line = crlf as the [*] default (so files are correct on Windows too), with LF pinned for execution-sensitive files: [*.sh], [scripts/*], and Dockerfiles. .gitattributes uses * -text (git does not normalize; the editor's bytes are committed) plus explicit *.sh/scripts/*/Dockerfile text eol=lf so git enforces LF on those regardless of editor.
- 🟧 The recurring failure: files drift to LF stragglers against the CRLF policy (README/HISTORY/DEVELOPMENT, and config JSON like
tasks.json, cspell.json, *.code-workspace). The user's editor auto-flips LF→CRLF on save, so a tiny content edit shows up as a whole-file diff, and the actual edit is invisible without git diff --ignore-all-space.
- 🟥 Auditing correctly: do not trust the
file command, and do not naively parse git ls-files --eol (its attr/ column has multiple tokens, which shifts field-splitting and produces false positives — this bit me twice). Byte-check each tracked text file for \r\n vs bare \n (skip binaries via a NUL check). Idempotent normalization: b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n").
- 🟥 Editing CRLF files with agent tools: a single-line (within-line) string replace is EOL-safe; but multi-line inserts and new files must emit
\r\n or you create mixed endings. Do multi-line edits via a small python byte-rewrite, not a naive \n insert.
- 🐛
.code-workspace is JSONC (has // comments) — json.load fails on it; strip line comments before parsing/validating.
- 🟦 A one-shot
git add --renormalize . + committed EOL sweep, plus a CI check (git ls-files --eol audit), would prevent the straggler drift entirely. Worth a template validation rule.
Where it belongs: .editorconfig, .gitattributes (the canonical pair), a CODESTYLE "Line endings" rule, and a CI/validation check that byte-audits EOL compliance.
2. VS Code tasks + workspace (template currently has no .vscode/)
- 🟧 The 4-terminals failure: a "Lint" task built as a compound task (
dependsOn + dependsOrder: sequence) where each sub-task had presentation.panel: "dedicated" spawns one terminal per tool (ruff/mypy/pyright → 4 windows). Users expect one cohesive output, not to hunt across panels for the final state.
- 🟩 Fix that works: make Lint/Fix single
type: shell tasks that chain the tools in one command; use presentation: { panel: "shared", clear: true } so it's one reusable terminal. Reserve dependsOn/compound for genuine cross-task barriers.
- 🐛 Shell-operator portability:
&& works in bash/zsh/cmd/PowerShell 7, but not Windows PowerShell 5.1. For a cross-platform repo, either accept pwsh7/cmd or use per-item single-command tasks; for a Linux-only repo it's a non-issue.
- 🟧 Tasks that call bash scripts need the env activated. A
type: shell task running bash -c scripts/lint does not reliably inherit an activated .venv — this produced the classic ruff: command not found even with the right interpreter selected. Fix: the script self-activates .venv (a source .venv/bin/activate guard) instead of depending on VS Code to activate it. This is the durable fix; interpreter selection alone is not enough.
- 🟧 Hardcoded interpreter path is POSIX-only.
python.defaultInterpreterPath: "${workspaceFolder}/.venv/bin/python" breaks on Windows (.venv/Scripts/python.exe) and contradicts any cross-platform claim. Remove it and rely on the Python extension auto-detecting .venv (works on both layouts). Only exception: a Linux-only devcontainer may point at the feature python.
- 🐛 Interpreter path vs provisioning mismatch: the workspace pointed at
.venv/bin/python while scripts/setup installed into system python and never created a .venv — so the interpreter setting was broken in every environment. Keep the workspace interpreter and the provisioning script telling the same story.
- 🟦 Add a template
.vscode/tasks.json (single-task Lint/Fix/Test, panel: shared) + a .code-workspace with cross-platform-safe settings, as an opt-in surface.
Where it belongs: a new .vscode/ template surface + a CODESTYLE "Editor tasks" rule.
3. Python env: pip vs uv (and how they coexist)
- 🟩 uv is the better dev experience:
uv venv creates the .venv (which then matches the workspace interpreter path), uv pip install -r requirements*.txt populates it, tools resolve reliably. It fixed the whole "lint task can't find ruff" class.
- 🟥 Keep
requirements*.txt as the single dependency source and split by concern, not tool: CI installs via pip (mandatory where the dependency set can't be locked — e.g. a Home Assistant version matrix that overrides homeassistant==X per leg), while local dev installs the same files via uv pip. No pyproject/lock duplication, no drift. Encode this "pip where required (the matrix), uv for the dev loop" split as the rule.
- 🟧
uv run needs a project. For a bare .venv + requirements.txt layout (no pyproject.toml, e.g. an HA integration), uv run <tool> is the wrong primitive — activate .venv in the scripts instead. uv run is correct only for real uv projects (the library).
- 🐛 Bootstrapping uv: prefer a pinned
pip install "uv==X.Y.Z" (deterministic, from PyPI over TLS) over curl -LsSf https://astral.sh/uv/install.sh | sh (installs "latest", non-deterministic, inconsistent with any SHA-pinned tooling in the same script). But don't call that "hash-verified" — plain pip install does not verify pinned hashes without --require-hashes; the accurate claim is "pinned version, fetched from PyPI." Also: a version pin that only applies when the tool is absent should say so (an already-present newer uv is used as-is; don't force-downgrade).
- 🐛 Editable-install version assertion trap: if
_version.py is a build-time placeholder (__version__ = "0.0.0", overwritten by NBGV/sed in CI), then an editable checkout always reports 0.0.0, so any installed == pin assertion fails unconditionally, on every branch. Skip the assertion for the placeholder (the checkout is the latest source by design). This masqueraded as a "wrong clone branch" bug for a while — the real cause was the placeholder.
Where it belongs: CODESTYLE "Toolchain" + the provisioning-script rules; a template scripts/setup pattern (uv venv + uv pip install, self-activating lint/fix/develop).
4. Versioning + release (NBGV) conventions
- 🟥 Docs reference the 2-digit
major.minor line (e.g. "Version 1.0"), never a 3-digit build. NBGV owns the patch/build; version.json version is bumped only on functionality changes. A well-meaning edit that "corrected" 1.0 → 1.0.0 in the changelog was wrong and had to be reverted — and it created a genuine release blocker.
- 🟧
versionHeightOffset: -1 only yields a clean .0 first release if you release at git height 1. Height runs up during development, so by release time the version was 1.0.7, not 1.0.0. Don't promise "clean 1.0.0" from the offset; treat the patch as NBGV's.
- 🐛 Detached-HEAD gotcha:
nbgv get-version on a detached origin/main reports a -g<sha> prerelease suffix because the checkout doesn't match publicReleaseRefSpec ^refs/heads/main$. The real published version (clean X.Y.Z) is what CI computes on the branch ref — don't panic at the local -g suffix.
- 🟥 PyPI versions are immutable and non-reusable — once
1.0.0 is published (even if later deleted), you can't re-upload it. Check the PyPI release list before assuming a target number; skip-existing will silently no-op a collision.
- 🟩 Release mechanics: feature→develop = squash; develop→main = merge-commit (mismatched flag is rejected by branch protection). The develop→main merge is the release cut → triggers the publish workflow (OIDC → PyPI). Gate the irreversible publish behind explicit human go-ahead.
- 🟥 Issue-closing keywords (
Closes #N) go in the develop→main promotion PR, not the feature/develop PR — GitHub only auto-closes from the default-branch merge.
Where it belongs: WORKFLOW.md (release flow) + a CODESTYLE/AGENTS "Versioning & changelog" rule.
5. GitHub / gh / Copilot-review mechanics (bake into the runbook)
- 🐛
gh pr edit --body/--title silently fails (it touches the deprecated Projects-classic projectCards GraphQL field and errors before applying — silently). This caused a stale PR description to survive many review rounds. Use gh api -X PATCH repos/<o>/<r>/pulls/<N> -F body=@file and verify it took.
- 🟥 "Copilot only re-flags what's genuinely still present, or a conversation that wasn't addressed." This user rule was consistently correct: when the same finding recurs, fix it at the code level (even a clarifying comment) rather than dismissing it as stale. The recurring "stale PR description" and "migration warning has no downgrade note" cases were all real.
- 🟩 Copilot dance mechanics (reliable): re-request via GraphQL
requestReviews(botIds: [<copilot bot node id>], union: true); reply+resolve via addPullRequestReviewThreadReply + resolveReviewThread. Reviewer login differs by API — GraphQL: copilot-pull-request-reviewer; REST: copilot-pull-request-reviewer[bot]. A formal review with zero inline comments is a clean pass, not a missing review.
- 🟩 Stale threads show
line: null (the code moved) — resolve them; the fix already landed. MERGEABLE/BLOCKED is usually just unresolved review threads (the ruleset requires thread resolution) → resolving them → CLEAN. Main uses rulesets, not classic branch protection (the classic protection REST endpoint 404s — read the ruleset instead).
- 🐛 Poll/push race: capturing a PR head SHA immediately after a push can read the old head; re-read after the push registers, or the whole poll evaluates the stale head.
- 🐛 Copilot is sometimes wrong — e.g. it claimed
actionlint -color "requires a value" (it's a boolean flag; the auto|always|never form is grep/ripgrep's). Verify before fixing; decline with evidence when it's wrong (that's not the same as dismissing as stale).
Where it belongs: copilot-instructions.md "Review Runbook" + AGENTS "PR etiquette."
6. Local CLI validation via Docker (user-driven learning)
- 🟥 Run the docs-lint CLIs locally; don't defer to CI because a tool isn't installed. When
cspell/markdownlint-cli2/actionlint/shellcheck aren't on PATH, run the official Docker image (identical binary). This is now a permanent AGENTS note and should be a template rule.
- 🐛
docker run -v "$PWD:/workdir" needs -w /workdir. cspell and markdownlint-cli2 images default their WORKDIR to /workdir (so they appear to work), but actionlint (WORKDIR /) and shellcheck (unset) run from / and can't resolve relative paths — verified by exit-3 failures. Always set -w /workdir.
Where it belongs: AGENTS "Local validation" + copilot-instructions.
7. Reference links / markdown hygiene
- 🟩 Convention that works: reference-style links with definitions at the file bottom, grouped (Shields / Other), sorted, consistent names across repos, no undefined refs, no unused defs. Single-use relative links to local files may stay inline.
- 🟥 URLs inside fenced code blocks stay inline — reference links don't work in code blocks. A PR description that claimed "every inline URL was extracted" was wrong for a code-block URL; either exempt code blocks in the wording or reword the line.
- 🐛 Link-integrity false positives: naive
[shortcut] regex flags Python list literals / example output inside code blocks (["a","b"], [...]) as "undefined refs." Exclude fenced code before checking.
- 🟥 Removing a link reference orphans its definition — remove both, or you fail the "no unused defs" rule.
Where it belongs: CODESTYLE "Markdown" + a validation check.
8. Agent-workflow / process learnings
- 🟥 Always check the working tree for the user's uncommitted edits before committing, and ask whether to include them. The maintainer hand-edits README/HISTORY live (often with the editor's LF→CRLF flip on top). A commit that ignores those strands their work or bundles half-finished prose. (Written to memory this session.)
- 🟥 When a user instruction rests on a premise you can disprove, surface the contradiction rather than blindly executing or refusing. Example: "delete
scripts/lint/fix, they're redundant with VS Code tasks" — but the tasks called those scripts, and CLI/agents/CI-mirror also depend on them; the real goal turned out to be cross-platform + cohesive-terminal, reached by a different change.
- 🟩 Branch strategy under an unmerged PR: when
develop is behind an open PR, fold the new change into that PR (or base off it) rather than branching off stale develop — avoids whole-file conflicts, especially with the CRLF-diff amplification.
- 🟥 Cross-repo consistency: a finding in one repo → verify/fix in the sibling (grammar, EOL, link naming, the
-w /workdir fix all propagated this way).
- 🟧
Co-Authored-By conflict: the repo convention forbids Co-Authored-By unless asked, which conflicts with a generic agent-harness default that adds it. The repo convention wins — encode it explicitly so agents don't add the trailer.
- 🟦 Sprawl warning: a single PR that accumulates many unrelated concerns goes through many Copilot rounds and never feels "done." Scope PRs; a template lint could warn on PRs touching N unrelated top-level areas.
Where it belongs: AGENTS "Working with the maintainer" + WORKFLOW.
Ingestion map (learning → target surface → rule form)
| # |
Theme |
Target file(s) |
Rule form |
| 0 |
Cross-platform ROI (per-repo) |
AGENTS.md, CODESTYLE.md |
"Supported development platforms" decision procedure + required explicit statement |
| 1 |
Line endings |
.editorconfig, .gitattributes, CODESTYLE.md |
Canonical CRLF-default + LF pins; byte-audit CI check |
| 2 |
VS Code tasks/workspace |
new .vscode/, CODESTYLE.md |
Single-task Lint/Fix (panel: shared); no hardcoded interpreter path; scripts self-activate .venv |
| 3 |
pip vs uv |
CODESTYLE.md, scripts/setup pattern |
"uv for dev, pip where the matrix requires it"; single requirements*.txt source |
| 4 |
NBGV versioning/release |
WORKFLOW.md, CODESTYLE.md |
2-digit docs version; squash-vs-merge; Closes #N on promotion PR |
| 5 |
gh/Copilot mechanics |
copilot-instructions.md, AGENTS.md |
gh pr edit → REST PATCH; re-flag rule; login-by-API; rulesets not classic |
| 6 |
Docker CLI validation |
AGENTS.md |
Run official images with -w /workdir |
| 7 |
Markdown/link hygiene |
CODESTYLE.md |
Grouped/sorted refs; code-block URL exemption; no orphaned defs |
| 8 |
Agent workflow |
AGENTS.md, WORKFLOW.md |
Check user's uncommitted edits; surface false premises; cross-repo sync; Co-Authored-By |
Hints for the template agent ingesting this
- Treat 🟥 MUST DO items as candidate validation rules (something in
spec/registry/repo-config can assert them), not just prose.
- Items marked 🐛 are external-tool behaviors — put them in the copilot/AGENTS runbook with the exact command, because an agent will otherwise rediscover them the hard way (each cost real round-trips here).
- The cross-platform ROI framework (§0) is the highest-leverage addition: it prevents wasted effort chasing Windows-native support that the dependency stack can't deliver. Encode it as a decision procedure, not a fixed stance.
- Several items (EOL byte-audit, link integrity, EOL compliance, "docs use 2-digit version") are mechanically checkable — good candidates for the automated-validation surface the template is moving toward.
Filed as a retrospective from the PurpleAir ecosystem work (homeassistant-purpleair + aiopurpleair). Happy to split any section into its own tracking issue or draft the concrete rule text for a given file.
Retrospective: cross-platform, dev-tooling, and agent-workflow learnings to encode as template rules
Over a multi-day effort I converged two repos in this family and cut a release:
ptr727/homeassistant-purpleair(a HACS integration, dependency-locked to Linux) andptr727/aiopurpleair(a pure-Python uv library published to PyPI). This issue is a retrospective of everything that bit us, what we tried, what worked, and the decisions we made on genuinely-ambiguous cases.Because ProjectTemplate is moving to a rules / AGENTS / automated-validation-only model (no sample projects to keep in sync), each learning is framed as a rule to encode + where it belongs, not code to copy. Categories used throughout: 🟥 MUST DO, 🟩 WHAT WORKED, 🟧 FAILED / MISTAKE CORRECTED, 🟦 NICE TO HAVE, 🐛 EXTERNAL-TOOL GOTCHA. A file-mapping table is at the end so the template agent can ingest each item into the right rule surface.
0. Headline decision framework — the one thing to internalize
🟥 A project's cross-platform support ceiling is set by its dependencies, not by tooling effort. Decide Win+Linux+macOS vs Linux-only per repo, from that ceiling — before writing any dev tooling.
hassand the HA test harness won't run there. Even maximal tooling can only deliver lint-only on Windows-native, and that still forces WSL2/devcontainer for run/test — i.e. high effort, partial result, and it doesn't remove the dependency it was meant to avoid. Poor ROI → declare Linux / WSL2 / devcontainer only.uv run(identical on every OS) and there are no bash dev scripts; restricting it would buy nothing. Keep it Linux/macOS/Windows.scripts/lintbecause I'm on Windows"). If a rule assumes bash, the docs must also assert dev is Linux-only, so the rule is always honorable in a supported environment.scripts/*serve Linux agents/CLI only. A Windows agent uses PowerShell/python, not bash. Never claim "agents use the scripts" universally — it's only true on Linux/WSL/devcontainer.Template rule shape: a
CODESTYLE.md/AGENTS.mdsection "Supported development platforms" with (a) the decision procedure (what does your runtime/dep stack support?), (b) a required explicit statement per repo, (c) the VS-Code-first-≠-cross-platform note.1. Line endings /
.editorconfig/.gitattributesThis was the single most time-consuming recurring class of issue.
.editorconfigend_of_line = crlfas the[*]default (so files are correct on Windows too), with LF pinned for execution-sensitive files:[*.sh],[scripts/*], and Dockerfiles..gitattributesuses* -text(git does not normalize; the editor's bytes are committed) plus explicit*.sh/scripts/*/Dockerfile text eol=lfso git enforces LF on those regardless of editor.tasks.json,cspell.json,*.code-workspace). The user's editor auto-flips LF→CRLF on save, so a tiny content edit shows up as a whole-file diff, and the actual edit is invisible withoutgit diff --ignore-all-space.filecommand, and do not naively parsegit ls-files --eol(itsattr/column has multiple tokens, which shifts field-splitting and produces false positives — this bit me twice). Byte-check each tracked text file for\r\nvs bare\n(skip binaries via a NUL check). Idempotent normalization:b.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n").\r\nor you create mixed endings. Do multi-line edits via a small python byte-rewrite, not a naive\ninsert..code-workspaceis JSONC (has//comments) —json.loadfails on it; strip line comments before parsing/validating.git add --renormalize .+ committed EOL sweep, plus a CI check (git ls-files --eolaudit), would prevent the straggler drift entirely. Worth a template validation rule.Where it belongs:
.editorconfig,.gitattributes(the canonical pair), a CODESTYLE "Line endings" rule, and a CI/validation check that byte-audits EOL compliance.2. VS Code tasks + workspace (template currently has no
.vscode/)dependsOn+dependsOrder: sequence) where each sub-task hadpresentation.panel: "dedicated"spawns one terminal per tool (ruff/mypy/pyright → 4 windows). Users expect one cohesive output, not to hunt across panels for the final state.type: shelltasks that chain the tools in one command; usepresentation: { panel: "shared", clear: true }so it's one reusable terminal. ReservedependsOn/compound for genuine cross-task barriers.&&works in bash/zsh/cmd/PowerShell 7, but not Windows PowerShell 5.1. For a cross-platform repo, either accept pwsh7/cmd or use per-item single-command tasks; for a Linux-only repo it's a non-issue.type: shelltask runningbash -c scripts/lintdoes not reliably inherit an activated.venv— this produced the classicruff: command not foundeven with the right interpreter selected. Fix: the script self-activates.venv(asource .venv/bin/activateguard) instead of depending on VS Code to activate it. This is the durable fix; interpreter selection alone is not enough.python.defaultInterpreterPath: "${workspaceFolder}/.venv/bin/python"breaks on Windows (.venv/Scripts/python.exe) and contradicts any cross-platform claim. Remove it and rely on the Python extension auto-detecting.venv(works on both layouts). Only exception: a Linux-only devcontainer may point at the feature python..venv/bin/pythonwhilescripts/setupinstalled into system python and never created a.venv— so the interpreter setting was broken in every environment. Keep the workspace interpreter and the provisioning script telling the same story..vscode/tasks.json(single-task Lint/Fix/Test,panel: shared) + a.code-workspacewith cross-platform-safe settings, as an opt-in surface.Where it belongs: a new
.vscode/template surface + a CODESTYLE "Editor tasks" rule.3. Python env: pip vs uv (and how they coexist)
uv venvcreates the.venv(which then matches the workspace interpreter path),uv pip install -r requirements*.txtpopulates it, tools resolve reliably. It fixed the whole "lint task can't find ruff" class.requirements*.txtas the single dependency source and split by concern, not tool: CI installs via pip (mandatory where the dependency set can't be locked — e.g. a Home Assistant version matrix that overrideshomeassistant==Xper leg), while local dev installs the same files viauv pip. Nopyproject/lock duplication, no drift. Encode this "pip where required (the matrix), uv for the dev loop" split as the rule.uv runneeds a project. For a bare.venv+requirements.txtlayout (nopyproject.toml, e.g. an HA integration),uv run <tool>is the wrong primitive — activate.venvin the scripts instead.uv runis correct only for real uv projects (the library).pip install "uv==X.Y.Z"(deterministic, from PyPI over TLS) overcurl -LsSf https://astral.sh/uv/install.sh | sh(installs "latest", non-deterministic, inconsistent with any SHA-pinned tooling in the same script). But don't call that "hash-verified" — plainpip installdoes not verify pinned hashes without--require-hashes; the accurate claim is "pinned version, fetched from PyPI." Also: a version pin that only applies when the tool is absent should say so (an already-present newer uv is used as-is; don't force-downgrade)._version.pyis a build-time placeholder (__version__ = "0.0.0", overwritten by NBGV/sed in CI), then an editable checkout always reports0.0.0, so anyinstalled == pinassertion fails unconditionally, on every branch. Skip the assertion for the placeholder (the checkout is the latest source by design). This masqueraded as a "wrong clone branch" bug for a while — the real cause was the placeholder.Where it belongs: CODESTYLE "Toolchain" + the provisioning-script rules; a template
scripts/setuppattern (uv venv +uv pip install, self-activating lint/fix/develop).4. Versioning + release (NBGV) conventions
major.minorline (e.g. "Version 1.0"), never a 3-digit build. NBGV owns the patch/build;version.jsonversionis bumped only on functionality changes. A well-meaning edit that "corrected"1.0→1.0.0in the changelog was wrong and had to be reverted — and it created a genuine release blocker.versionHeightOffset: -1only yields a clean.0first release if you release at git height 1. Height runs up during development, so by release time the version was1.0.7, not1.0.0. Don't promise "clean 1.0.0" from the offset; treat the patch as NBGV's.nbgv get-versionon a detachedorigin/mainreports a-g<sha>prerelease suffix because the checkout doesn't matchpublicReleaseRefSpec ^refs/heads/main$. The real published version (cleanX.Y.Z) is what CI computes on the branch ref — don't panic at the local-gsuffix.1.0.0is published (even if later deleted), you can't re-upload it. Check the PyPI release list before assuming a target number;skip-existingwill silently no-op a collision.Closes #N) go in the develop→main promotion PR, not the feature/develop PR — GitHub only auto-closes from the default-branch merge.Where it belongs: WORKFLOW.md (release flow) + a CODESTYLE/AGENTS "Versioning & changelog" rule.
5. GitHub /
gh/ Copilot-review mechanics (bake into the runbook)gh pr edit --body/--titlesilently fails (it touches the deprecated Projects-classicprojectCardsGraphQL field and errors before applying — silently). This caused a stale PR description to survive many review rounds. Usegh api -X PATCH repos/<o>/<r>/pulls/<N> -F body=@fileand verify it took.requestReviews(botIds: [<copilot bot node id>], union: true); reply+resolve viaaddPullRequestReviewThreadReply+resolveReviewThread. Reviewer login differs by API — GraphQL:copilot-pull-request-reviewer; REST:copilot-pull-request-reviewer[bot]. A formal review with zero inline comments is a clean pass, not a missing review.line: null(the code moved) — resolve them; the fix already landed.MERGEABLE/BLOCKEDis usually just unresolved review threads (the ruleset requires thread resolution) → resolving them →CLEAN. Main uses rulesets, not classic branch protection (the classic protection REST endpoint 404s — read the ruleset instead).actionlint -color"requires a value" (it's a boolean flag; theauto|always|neverform is grep/ripgrep's). Verify before fixing; decline with evidence when it's wrong (that's not the same as dismissing as stale).Where it belongs: copilot-instructions.md "Review Runbook" + AGENTS "PR etiquette."
6. Local CLI validation via Docker (user-driven learning)
cspell/markdownlint-cli2/actionlint/shellcheckaren't on PATH, run the official Docker image (identical binary). This is now a permanent AGENTS note and should be a template rule.docker run -v "$PWD:/workdir"needs-w /workdir.cspellandmarkdownlint-cli2images default their WORKDIR to/workdir(so they appear to work), but actionlint (WORKDIR/) and shellcheck (unset) run from/and can't resolve relative paths — verified by exit-3 failures. Always set-w /workdir.Where it belongs: AGENTS "Local validation" + copilot-instructions.
7. Reference links / markdown hygiene
[shortcut]regex flags Python list literals / example output inside code blocks (["a","b"],[...]) as "undefined refs." Exclude fenced code before checking.Where it belongs: CODESTYLE "Markdown" + a validation check.
8. Agent-workflow / process learnings
scripts/lint/fix, they're redundant with VS Code tasks" — but the tasks called those scripts, and CLI/agents/CI-mirror also depend on them; the real goal turned out to be cross-platform + cohesive-terminal, reached by a different change.developis behind an open PR, fold the new change into that PR (or base off it) rather than branching off staledevelop— avoids whole-file conflicts, especially with the CRLF-diff amplification.-w /workdirfix all propagated this way).Co-Authored-Byconflict: the repo convention forbidsCo-Authored-Byunless asked, which conflicts with a generic agent-harness default that adds it. The repo convention wins — encode it explicitly so agents don't add the trailer.Where it belongs: AGENTS "Working with the maintainer" + WORKFLOW.
Ingestion map (learning → target surface → rule form)
.editorconfig,.gitattributes, CODESTYLE.md.vscode/, CODESTYLE.mdpanel: shared); no hardcoded interpreter path; scripts self-activate.venvscripts/setuppatternrequirements*.txtsourceCloses #Non promotion PRgh pr edit→ REST PATCH; re-flag rule; login-by-API; rulesets not classic-w /workdirHints for the template agent ingesting this
spec/registry/repo-configcan assert them), not just prose.Filed as a retrospective from the PurpleAir ecosystem work (homeassistant-purpleair + aiopurpleair). Happy to split any section into its own tracking issue or draft the concrete rule text for a given file.