From d8d84913de0c9a9ed4ce78474f96b47e85fbdd67 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 09:59:52 -0700 Subject: [PATCH 1/8] Read the Hub's Own Main for _git_revisions, Not the Checkout's HEAD (#1021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1017. `_git_revisions()` ran `git log`/`git show`/`git ls-tree` with `cwd=ROOT` and no explicit revision, so it walked whatever branch the invoking checkout had checked out. This repo's own working checkouts are routinely on `develop`, so `hub_last_change()` (via `check_intent_staleness`) and `classify_verbatim()` (via `git_blob_in_file_history`) could judge a downstream copy against a develop-only commit that `main` never contained, misreporting it as trailing or modified. Adds `_hub_main_rev()`, which fetches `origin main` into ROOT's own object database and resolves it to a concrete SHA immediately before use (the same freshness pattern AGENTS.md documents for reaching the hub as a checkout of one's own), and defaults `_git_revisions()`/`git_blob_in_file_history()` to walk that SHA instead of the implicit HEAD. A `rev` parameter lets the `--selftest` fixtures keep exercising a throwaway local branch with no `origin` to fetch, so the offline engine self-test stays offline. Adds a `--selftest` case that reproduces the bug against a local upstream remote (develop ahead of main) and confirms the default now reads main; verified it fails without the fix and passes with it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Bug Fixes** * Hub content and Git history now consistently reflect the latest fetched main branch. * History results no longer include changes found only on the current development branch. * Stale-content checks now compare against the same branch revision as displayed content and history. * Git history lookups are more reliable and deterministic in offline repositories and test environments. * Historical scans now consistently use a resolved revision, improving result accuracy and reproducibility. --- spec/audit.py | 234 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 198 insertions(+), 36 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 030d46e9..d2eb2756 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -81,21 +81,51 @@ def load(rel): @functools.cache -def hub_tracked(): - """The hub's own git-tracked paths, which is what "hub-side" means for the hub-only comparison. - - git ls-files rather than a filesystem walk, since a walk picks up __pycache__ and a local .venv and - would make the result depend on working-tree state. - A non-zero exit raises rather than returning an empty set, which would read as "the hub tracks nothing" - and silently clear every hub-only finding. +def hub_tracked(rev=None): + """The hub's own tracked paths at the resolved `main` commit, which is what "hub-side" means + for the hub-only comparison. + + `git ls-tree -r` at that commit rather than `git ls-files` against ROOT's checked-out index + (a filesystem walk is avoided too, since it would pick up __pycache__ and a local .venv and + make the result depend on working-tree state), so a path present on `main` but absent on + `develop`, or the reverse, is not silently missed or falsely added + (ptr727/ProjectTemplate#1017 review). Filtered to regular-file modes (100644, 100755) the same + way `_git_revisions()` is: a directory, a symlink, or a submodule gitlink has no file content + to compare, and every caller here assumes a plain file at each returned path. `-z` NUL-delimits + the output so an unusual path is not C-quoted, which would otherwise return an escaped string + that matches nothing a caller compares it against. + + `rev` defaults to `_hub_main_rev()`. The --selftest fixture passes an explicit `rev` to check + against ROOT's own real tracked files without a network fetch, keeping the offline engine + self-test offline. + + A non-zero exit raises rather than returning an empty set, which would read as "the hub tracks + nothing" and silently clear every hub-only finding. """ - r = subprocess.run(["git", "ls-files"], cwd=ROOT, capture_output=True, text=True, check=False) + walk_rev = _hub_main_rev() if rev is None else rev + r = subprocess.run( + ["git", "ls-tree", "-r", "-z", walk_rev], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) if r.returncode != 0: - raise RuntimeError(f"git ls-files failed in {ROOT}: {r.stderr.strip() or 'non-zero exit'}") - return frozenset(r.stdout.splitlines()) + raise RuntimeError( + f"git ls-tree -r {walk_rev} failed in {ROOT}: {r.stderr.strip() or 'non-zero exit'}" + ) + paths = set() + for record in r.stdout.split("\0"): + if not record: + continue + meta, _, path = record.partition("\t") + mode = meta.split(None, 1)[0] if meta else None + if mode in ("100644", "100755"): + paths.add(path) + return frozenset(paths) -def hub_only_paths(spec): +def hub_only_paths(spec, rev=None): """Hub-tracked paths the manifest does not declare, so a downstream copy is hub-hosted content rather than a carry. This is the deletion detector: the manifest says what a repo carries, so a file the hub tracks and the @@ -109,6 +139,8 @@ def hub_only_paths(spec): hooks at .husky/pre-commit. So only a `retire` disposition in spec/divergences.json asserts a deletion, and an untriaged hit asks for the file to be read. + + `rev` is passed through to `hub_tracked()`; see its docstring. """ declared = {e["path"] for e in spec["files"]["baseline"]} tree_paths = set() @@ -116,11 +148,11 @@ def hub_only_paths(spec): root = declaration["source"].rstrip("/") + "/" tree_paths.update( path - for path in hub_tracked() + for path in hub_tracked(rev) if path.startswith(root) and tree_path_included(path.removeprefix(root), declaration["include"]) ) - return hub_tracked() - declared - tree_paths + return hub_tracked(rev) - declared - tree_paths def gap_dispositions(spec): @@ -162,14 +194,26 @@ def repo_tree(slug, ground_head): return None if entries is None else set(entries) -def git_blob_sha(content): - header = f"blob {len(content)}\0".encode() - return hashlib.sha1(header + content).hexdigest() - - @functools.cache def canonical_blob_sha(path): - return git_blob_sha((ROOT / path).read_bytes()) + """The hub's git blob identity for path, from the same resolved `main` commit + `_git_revisions()` and `_hub_main_rev()` walk, not from ROOT's checked-out working tree, + which is not necessarily `main` (ptr727/ProjectTemplate#1017 review). `git rev-parse` resolves + a `:` tree-ish straight to the blob object id, so no separate read-and-hash step is + needed. Raises OSError, matching a filesystem read's own contract, when path is absent from + that commit or is not a regular file there. + """ + rev = _hub_main_rev() + result = subprocess.run( + ["git", "rev-parse", f"{rev}:{path}"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise OSError(f"{path} is unreadable from the hub's main at {rev}: {result.stderr.strip()}") + return result.stdout.strip() def tree_path_included(path, patterns): @@ -1716,7 +1760,40 @@ def classify_verbatim(down_text, canon_text, past_texts): @functools.cache -def _git_revisions(rel_path): +def _hub_main_rev(): + """The hub's own `main`, fetched fresh from `origin` and resolved to a commit SHA. + + Reached as a checkout of one's own, fetched immediately before use, per AGENTS.md and + ptr727/ProjectTemplate#1017, rather than trusting ROOT's checked-out branch. `git fetch` + writes into ROOT's own object database, so a separate clone is not needed. Resolved to a + concrete SHA right away, not the mutable `FETCH_HEAD` pointer, so a later fetch elsewhere in + the process cannot move it mid-run. + + Cached: one fetch and resolve per run, reused for every rel_path read. + """ + fetch = subprocess.run( + ["git", "fetch", "-q", "origin", "main"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if fetch.returncode != 0: + raise RuntimeError(f"git fetch origin main failed in {ROOT}: {fetch.stderr.strip()}") + resolved = subprocess.run( + ["git", "rev-parse", "FETCH_HEAD"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if resolved.returncode != 0 or not resolved.stdout.strip(): + raise RuntimeError(f"git rev-parse FETCH_HEAD failed in {ROOT}: {resolved.stderr.strip()}") + return resolved.stdout.strip() + + +@functools.cache +def _git_revisions(rel_path, rev=None): """Every commit that touched rel_path in the hub's history, newest first, as (date, sha, text). `text` is None where rel_path has no file content at this revision: absent (deleted, checked @@ -1728,9 +1805,16 @@ def _git_revisions(rel_path): permission or encoding fluke, a corrupt object) raises instead of folding into the same None, so a real command fault cannot pass as an ordinary absence. Cached because one canonical's history is read once per fidelity/staleness check, then reused for every audited repo's copy. + + `rev` names the git revision walked. It defaults to the hub's own `main` via + `_hub_main_rev()` (ptr727/ProjectTemplate#1017) rather than the implicit HEAD of whatever + branch ROOT has checked out. The --selftest fixtures pass an explicit `rev` to exercise a + plain local branch in a throwaway repo with no `origin` to fetch, keeping the offline engine + self-test offline. """ + walk_rev = _hub_main_rev() if rev is None else rev r = subprocess.run( - ["git", "log", "--format=%cI %H", "--", rel_path], + ["git", "log", "--format=%cI %H", walk_rev, "--", rel_path], cwd=ROOT, capture_output=True, text=True, @@ -1788,6 +1872,18 @@ def git_file_history(rel_path): return [text for _, _, text in _git_revisions(rel_path) if text is not None] +def canonical_current_text(rel_path): + """The hub's current canonical content of rel_path, from the same `_git_revisions()` call + `git_file_history()` and `hub_last_change()` already make, so "current" and "history" can + never disagree about which commit they read (ptr727/ProjectTemplate#1017 review: reading + "current" from ROOT's working tree while history walked the resolved `main` SHA let a copy + that matches today's main misclassify as stale against its own most recent history entry). + None if rel_path has no history at `main`, or its newest revision has no file content there. + """ + revisions = _git_revisions(rel_path) + return revisions[0][2] if revisions else None + + def _last_effective_change(revisions): """The (date, sha) of the newest revision in `revisions` (newest-first (date, sha, text) triples for one file) whose content differs from its predecessor after normalize(), or the @@ -1818,10 +1914,17 @@ def _last_effective_change(revisions): @functools.cache -def git_blob_in_file_history(rel_path, blob_sha): - """Whether a blob occurred in a path's hub history.""" +def git_blob_in_file_history(rel_path, blob_sha, rev=None): + """Whether a blob occurred in a path's hub history. + + `rev` defaults to the hub's own `main` via `_hub_main_rev()` (ptr727/ProjectTemplate#1017) + for the same reason `_git_revisions` does: ROOT's checked-out branch is not necessarily + `main`, and a develop-only revision matching `blob_sha` must not read as "stale" against a + hub history `main` doesn't actually contain. + """ + walk_rev = _hub_main_rev() if rev is None else rev result = subprocess.run( - ["git", "log", "--format=%H", f"--find-object={blob_sha}", "--", rel_path], + ["git", "log", "--format=%H", f"--find-object={blob_sha}", walk_rev, "--", rel_path], cwd=ROOT, capture_output=True, text=True, @@ -1891,10 +1994,7 @@ def check_intent_staleness(slug, ground, path, canonical_rel, down_text): if hub_change is None: return [] if down_text is not None: - try: - canon_text = (ROOT / canonical_rel).read_text(encoding="utf-8", errors="replace") - except OSError: - canon_text = None + canon_text = canonical_current_text(canonical_rel) if canon_text is not None and content_hash(down_text) == content_hash(canon_text): return [] commits = gh(f"repos/{slug}/commits?path={path}&sha={ground}&per_page=1") @@ -1921,10 +2021,9 @@ def check_verbatim(label, down_text, canonical_rel, extract=None): and classify a mismatch as stale or modified via the canonical's git history. All findings are DRIFT: a byte diff is a hint to review, never proof of breakage. """ - try: - # Same decode policy as the downstream copy and the git history, so a stray byte can never make otherwise-equal content hash differently across the three sources. - canon_text = (ROOT / canonical_rel).read_text(encoding="utf-8", errors="replace") - except OSError: + # canonical_current_text() and git_file_history() both read the same _git_revisions() call, so a copy matching today's main can never mismatch against its own history's newest entry (ptr727/ProjectTemplate#1017 review). + canon_text = canonical_current_text(canonical_rel) + if canon_text is None: return [ ( "DRIFT", @@ -3306,7 +3405,7 @@ def _selftest(): ROOT = tmp_root_path try: _git_revisions.cache_clear() - revisions = _git_revisions(rel) + revisions = _git_revisions(rel, rev="HEAD") finally: ROOT = saved_root _git_revisions.cache_clear() @@ -3351,7 +3450,7 @@ def _selftest(): ROOT = tmp_root_path try: _git_revisions.cache_clear() - revisions = _git_revisions(rel) + revisions = _git_revisions(rel, rev="HEAD") finally: ROOT = saved_root _git_revisions.cache_clear() @@ -3403,7 +3502,7 @@ def _selftest(): ROOT = tmp_root_path try: _git_revisions.cache_clear() - revisions = _git_revisions(rel) + revisions = _git_revisions(rel, rev="HEAD") finally: ROOT = saved_root _git_revisions.cache_clear() @@ -3415,6 +3514,69 @@ def _selftest(): f" {'ok ' if got == want else 'FAIL'} want={want!s:<24} got={got!s:<24} _git_revisions: file-to-symlink transition" ) + # _git_revisions: the default rev reads origin's `main`, not ROOT's checked-out branch (ptr727/ProjectTemplate#1017). + # `origin` is a plain local path here, so the fetch stays offline. + with tempfile.TemporaryDirectory() as tmp_upstream, tempfile.TemporaryDirectory() as tmp_root: + tmp_upstream_path = pathlib.Path(tmp_upstream) + tmp_root_path = pathlib.Path(tmp_root) + rel = "hub-probe.txt" + for cmd in ( + ["git", "init", "-q", "-b", "main"], + ["git", "config", "user.email", "test@test.invalid"], + ["git", "config", "user.name", "test"], + ): + subprocess.run(cmd, cwd=tmp_upstream_path, check=True, capture_output=True) + (tmp_upstream_path / rel).write_text("main-content\n") + subprocess.run(["git", "add", rel], cwd=tmp_upstream_path, check=True, capture_output=True) + subprocess.run( + ["git", "commit", "-q", "-m", "add on main"], + cwd=tmp_upstream_path, + check=True, + capture_output=True, + ) + subprocess.run( + ["git", "clone", "-q", str(tmp_upstream_path), str(tmp_root_path)], + check=True, + capture_output=True, + ) + # A clone carries no committer identity of its own (no global config on a CI runner either), so this repo needs the same setup as tmp_upstream_path above. + for cmd in ( + ["git", "config", "user.email", "test@test.invalid"], + ["git", "config", "user.name", "test"], + ): + subprocess.run(cmd, cwd=tmp_root_path, check=True, capture_output=True) + subprocess.run( + ["git", "checkout", "-q", "-b", "develop"], + cwd=tmp_root_path, + check=True, + capture_output=True, + ) + (tmp_root_path / rel).write_text("develop-only-content\n") + subprocess.run(["git", "add", rel], cwd=tmp_root_path, check=True, capture_output=True) + subprocess.run( + ["git", "commit", "-q", "-m", "develop-only change"], + cwd=tmp_root_path, + check=True, + capture_output=True, + ) + saved_root = ROOT + ROOT = tmp_root_path + try: + _git_revisions.cache_clear() + _hub_main_rev.cache_clear() + revisions = _git_revisions(rel) + finally: + ROOT = saved_root + _git_revisions.cache_clear() + _hub_main_rev.cache_clear() + got = [text for _, _, text in revisions] + want = ["main-content\n"] + if got != want: + ok = False + print( + f" {'ok ' if got == want else 'FAIL'} want={want!s:<24} got={got!s:<24} _git_revisions: default rev reads origin's main, not the checked-out branch" + ) + # Region extraction and hashing: a forked github-release block must hash differently from the canonical. region = split_jobs(rel_ok).get("github-release") forked_region = split_jobs( @@ -4859,7 +5021,7 @@ def _selftest(): # The hub-only set is the manifest subtracted from the hub's tracked files, so a declared path must never appear in it. # Asserted against the live manifest rather than a fixture, since the failure this guards is a declared path leaking into the deletion list, which only the real pairing can show. declared = {e["path"] for e in load("spec/files.json")["baseline"]} - hub_only = hub_only_paths({"files": load("spec/files.json")}) + hub_only = hub_only_paths({"files": load("spec/files.json")}, rev="HEAD") leaked = sorted(declared & hub_only) if leaked or not hub_only: ok = False From 29998ff95314b5015aca43423a18ac0fc8d1a43d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:00:07 -0700 Subject: [PATCH 2/8] Document the Dependabot Self-Hosted-Runners Account Setting (#1022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1015. An account-wide GitHub setting, `Dependabot on self-hosted runners` at `https://github.com/settings/security_analysis`, routes Dependabot's own update jobs to a self-hosted runner pool. With no self-hosted runner registered on the account, those jobs queue forever and are cancelled after 24 hours, with no visible failure in the Actions API or in ordinary CI, only a `Self-hosted runner unavailable` message on the repo's own Dependabot page. GitHub never routes a public repo through this setting, so a public repo cannot show the symptom, which is why `ProjectTemplate` never surfaced it while every private repo under the account did. Adds the mechanical audit signal to AUDIT.md section 6 (a repo whose `dependabot-updates`/`update-graph` workflow runs are all `cancelled` with zero steps has this problem), and a stand-up-time check to STANDUP.md section 4 for private repos, per the reporter's own suggested locations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Documentation** * Added guidance for identifying automated dependency updates cancelled without running any steps. * Documented account-wide self-hosted runner settings and automatic-enablement controls for private repositories. * Explained how to verify matching runner registration and diagnose unavailable runners. * Clarified that public repositories and regular CI remain unaffected. * Included instructions to manually rerun updates that were queued or cancelled after settings are corrected. --- AUDIT.md | 2 ++ STANDUP.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/AUDIT.md b/AUDIT.md index 7be358cc..2e467be1 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -116,6 +116,8 @@ Run [`WORKFLOW.md`][workflow]'s methodology against the repo's **own** Actions: # then read dependabot.yml and confirm each present ecosystem has both a main and a develop target-branch entry ``` +- **Dependabot on self-hosted runners (account setting)** - a repo whose `dependabot-updates` or `update-graph` workflow runs are all `cancelled` with zero steps has this problem. The account-wide toggle at `https://github.com/settings/security_analysis`, `Dependabot on self-hosted runners`, routes Dependabot's own update jobs to a self-hosted runner pool. With none registered on the account, those jobs queue for up to 24 hours, then get cancelled. The cancelled-with-zero-steps pattern above is the only Actions-API-visible signal, not an explicit cause, and ordinary CI is unaffected. The account-setting root cause surfaces only as a `Self-hosted runner unavailable` message on the repo's own Dependabot page. GitHub never routes a public repo through this setting, so `ProjectTemplate` itself cannot show the symptom (ptr727/ProjectTemplate#1015). Detection stops there, like the rest of this audit. Remediation is a separate, manual action. Confirm the toggle, and `Automatically enable for new repositories` beside it, are both off, or register a matching self-hosted runner instead of disabling it. Disabling the toggle does not rerun jobs already queued. Each affected repo still needs its own manual `Check for Updates` click on its own Dependabot page. + ## 7. Verdict Model Per dimension, record `operational | not-operational | N/A`, each with a letter verdict and an intent verdict: diff --git a/STANDUP.md b/STANDUP.md index ce3feda8..6af8e687 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -206,6 +206,8 @@ Each is step 0A's escalation rather than something to work around. Run `repo-config/configure.sh apply owner/repo release|operational` from a hub checkout at `main`, naming the repo being stood up and its model, to apply the fleet settings, Dependabot security features, and two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config.md`][repo-config-doc]). Then run `repo-config/configure.sh check owner/repo release|operational` from the same checkout. Pass the model explicitly because the repository is outside the registry during this step. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once, which is why this step follows step 3 rather than preceding it. A ruleset requiring a name no run has ever reported leaves the first pull request waiting on a status nothing produces, and on an operational repo the `develop -> main` promotion is a pull request too, so the same wait applies there. +For a **private** repo, confirm the account-wide toggle at `https://github.com/settings/security_analysis`, `Dependabot on self-hosted runners`, is off, along with `Automatically enable for new repositories` beside it. If self-hosted routing is wanted instead, register a matching self-hosted runner rather than disabling the toggle. Left on with no self-hosted runner registered on the account, Dependabot's own update jobs queue for up to 24 hours, then get cancelled. That cancelled-with-zero-steps pattern is the only Actions-API-visible signal, not an explicit cause, and ordinary CI is unaffected. The account-setting root cause surfaces only as a `Self-hosted runner unavailable` message on the repo's own Dependabot page. GitHub never routes a public repo through this setting, so a public standup is unaffected (ptr727/ProjectTemplate#1015). A repo standing up from a **partial state** may already carry queued or cancelled jobs from before this check ran. Fixing the toggle does not rerun those. A manual `Check for Updates` click on the repo's own Dependabot page does. + ## 5. Verify: Run the Audit Run [`AUDIT.md`][audit] end to end. The repo is stood up only when it is **operational** (every applicable check passes) or its residual deltas are tracked in `reports//audit.md` plus an issue. Converge any drift through a Copilot-reviewed target PR ([`AUDIT.md`][audit] section 10), and the maintainer merges. A repo left partially set up and unrecorded is the exact failure this procedure exists to prevent. From 405ac15070604e7f9915fe881de59ee3af7ef701 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:00:17 -0700 Subject: [PATCH 3/8] Fix Four Real Bugs in Newly-Packaged Skills (#1023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the 'Real bugs' section of #928 (four of the twelve findings; the remaining findings are internal-inconsistency/accuracy/lower-confidence items left for a separate pass). 1. `comment-and-doc-style/references/line-endings.md`: `[/*]` only matches one path component under EditorConfig, so a nested file under a byte-preserve directory kept inherited normalization instead of the intended treatment. Now `[/**]`. 2. `operational-vs-release-workflow/references/branch-protection-and-promotion.md`: the stage-comparison example diffed `git show :2:f` / `:3:f` against a literal file named `f` rather than the `` the preceding command selects. Now `:2:` / `:3:`. 3. `resync-a-repo/SKILL.md` and `standup-a-repo/SKILL.md`: both gave `repo-config/configure.sh ... release|operational` as inline code with a bare pipe, not a valid mode argument as written. Now show one concrete mode with a note to substitute the other for an operational repo. 4. `dotnet-codestyle/references/conventions.md`: the `GetQuoteOfTheDayAsync` example declared `async Task` with an empty body, which doesn't compile (CS0161). Given a representative `await`/`return`. Regenerated both derived trees via `scripts/build_dist.py`; `--check` and `scripts/tests/test_build_dist.py` both pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Documentation** * Clarified recursive file-matching guidance for nested directories. * Improved asynchronous code examples with cancellation handling and category-specific output. * Corrected file-path usage in conflict-verification instructions. * Clarified separate configuration workflows for release and operational repositories. * Updated synchronized documentation references and source metadata. --- .../comment-and-doc-style/references/line-endings.md | 5 +++-- .../skills/dotnet-codestyle/references/conventions.md | 11 ++++++++++- .../references/branch-protection-and-promotion.md | 2 +- .agents/skills/resync-a-repo/SKILL.md | 5 +++-- .agents/skills/standup-a-repo/SKILL.md | 5 +++-- .claude-plugin/fleet-skills/.source-digest | 2 +- .../comment-and-doc-style/references/line-endings.md | 5 +++-- .../skills/dotnet-codestyle/references/conventions.md | 11 ++++++++++- .../references/branch-protection-and-promotion.md | 2 +- .../fleet-skills/skills/resync-a-repo/SKILL.md | 5 +++-- .../fleet-skills/skills/standup-a-repo/SKILL.md | 5 +++-- .../comment-and-doc-style/references/line-endings.md | 5 +++-- .../skills/dotnet-codestyle/references/conventions.md | 11 ++++++++++- .../references/branch-protection-and-promotion.md | 2 +- .github/skills/resync-a-repo/SKILL.md | 5 +++-- .github/skills/standup-a-repo/SKILL.md | 5 +++-- 16 files changed, 61 insertions(+), 25 deletions(-) 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 bdf0846a..8337cbcf 100644 --- a/.agents/skills/comment-and-doc-style/references/line-endings.md +++ b/.agents/skills/comment-and-doc-style/references/line-endings.md @@ -71,9 +71,10 @@ tool-owned format outside `.bat`/`.cmd`, or a byte-preserve data directory whose consumer may depend on), still pair a `.gitattributes` pin with a matching `.editorconfig` override, since the git pin alone is not enough there, `.gitattributes` governs git while the editor follows `.editorconfig`. For a byte-preserve directory, disable all editor normalization, -not just EOL: `[/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = +not just EOL: `[/**]` 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). +that removes an inherited property, and `**` is needed rather than `*` so a nested file under the +directory is covered too, since `*` excludes `/` and only matches one path component). ## Editing discipline diff --git a/.agents/skills/dotnet-codestyle/references/conventions.md b/.agents/skills/dotnet-codestyle/references/conventions.md index 5eb48540..2421ab73 100644 --- a/.agents/skills/dotnet-codestyle/references/conventions.md +++ b/.agents/skills/dotnet-codestyle/references/conventions.md @@ -122,5 +122,14 @@ parameters, return values, exceptions, and crefs. /// /// Thrown when is not a supported value. /// -public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} +public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) +{ + if (category is not ("motivational" or "humor")) + { + throw new ArgumentException($"Unsupported category: {category}", nameof(category)); + } + + await Task.Delay(0, cancellationToken); + return $"Quote for {category}"; +} ``` diff --git a/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index 597cd5f1..6d934c0e 100644 --- a/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -35,7 +35,7 @@ Two traps, both learned the hard way: `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take `develop`'s side for the EOL-conflicted files (`git checkout --theirs `) **after confirming each is content-identical modulo EOL, or that `develop` is a strict superset** - (`diff <(git show :2:f | tr -d '\r') <(git show :3:f | tr -d '\r')`), then open that branch into + (`diff <(git show ":2:" | tr -d '\r') <(git show ":3:" | tr -d '\r')`), then open that branch into `main`. Verify no genuine `main`-only content is dropped (build/test where the repo supports it). ## Why both rulesets omit "Require branches to be up to date before merging" diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index e291fb66..18962375 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -67,8 +67,9 @@ Preserve the evidence RESYNC.md section 2 requires, and do not leave the finding 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` from the hub at `main`, - then `apply` for what it reports, never from a carried copy. + `repo-config/configure.sh check "/" release` (substitute `operational` for an + operational repo) from the hub at `main`, then `apply` for what it reports, never from a + carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. Reconcile the registry entry (`status`, `types`, `releaseTrigger`, `workflowModel`, diff --git a/.agents/skills/standup-a-repo/SKILL.md b/.agents/skills/standup-a-repo/SKILL.md index 54c277a5..8269093d 100644 --- a/.agents/skills/standup-a-repo/SKILL.md +++ b/.agents/skills/standup-a-repo/SKILL.md @@ -75,8 +75,9 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same - command's `check` subcommand, never from a hand-built or carried copy. + `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main` and check with the same command's `check` subcommand, + never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 97ea0b63..160eee0d 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -b945e66c274cb82a +702692e5f8c8f60e 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 bdf0846a..8337cbcf 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 @@ -71,9 +71,10 @@ tool-owned format outside `.bat`/`.cmd`, or a byte-preserve data directory whose consumer may depend on), still pair a `.gitattributes` pin with a matching `.editorconfig` override, since the git pin alone is not enough there, `.gitattributes` governs git while the editor follows `.editorconfig`. For a byte-preserve directory, disable all editor normalization, -not just EOL: `[/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = +not just EOL: `[/**]` 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). +that removes an inherited property, and `**` is needed rather than `*` so a nested file under the +directory is covered too, since `*` excludes `/` and only matches one path component). ## Editing discipline diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md index 5eb48540..2421ab73 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md @@ -122,5 +122,14 @@ parameters, return values, exceptions, and crefs. /// /// Thrown when is not a supported value. /// -public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} +public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) +{ + if (category is not ("motivational" or "humor")) + { + throw new ArgumentException($"Unsupported category: {category}", nameof(category)); + } + + await Task.Delay(0, cancellationToken); + return $"Quote for {category}"; +} ``` diff --git a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index 597cd5f1..6d934c0e 100644 --- a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -35,7 +35,7 @@ Two traps, both learned the hard way: `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take `develop`'s side for the EOL-conflicted files (`git checkout --theirs `) **after confirming each is content-identical modulo EOL, or that `develop` is a strict superset** - (`diff <(git show :2:f | tr -d '\r') <(git show :3:f | tr -d '\r')`), then open that branch into + (`diff <(git show ":2:" | tr -d '\r') <(git show ":3:" | tr -d '\r')`), then open that branch into `main`. Verify no genuine `main`-only content is dropped (build/test where the repo supports it). ## Why both rulesets omit "Require branches to be up to date before merging" diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index e291fb66..18962375 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -67,8 +67,9 @@ Preserve the evidence RESYNC.md section 2 requires, and do not leave the finding 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` from the hub at `main`, - then `apply` for what it reports, never from a carried copy. + `repo-config/configure.sh check "/" release` (substitute `operational` for an + operational repo) from the hub at `main`, then `apply` for what it reports, never from a + carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. Reconcile the registry entry (`status`, `types`, `releaseTrigger`, `workflowModel`, diff --git a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md index 54c277a5..8269093d 100644 --- a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md @@ -75,8 +75,9 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same - command's `check` subcommand, never from a hand-built or carried copy. + `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main` and check with the same command's `check` subcommand, + never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/.github/skills/comment-and-doc-style/references/line-endings.md b/.github/skills/comment-and-doc-style/references/line-endings.md index bdf0846a..8337cbcf 100644 --- a/.github/skills/comment-and-doc-style/references/line-endings.md +++ b/.github/skills/comment-and-doc-style/references/line-endings.md @@ -71,9 +71,10 @@ tool-owned format outside `.bat`/`.cmd`, or a byte-preserve data directory whose consumer may depend on), still pair a `.gitattributes` pin with a matching `.editorconfig` override, since the git pin alone is not enough there, `.gitattributes` governs git while the editor follows `.editorconfig`. For a byte-preserve directory, disable all editor normalization, -not just EOL: `[/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = +not just EOL: `[/**]` 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). +that removes an inherited property, and `**` is needed rather than `*` so a nested file under the +directory is covered too, since `*` excludes `/` and only matches one path component). ## Editing discipline diff --git a/.github/skills/dotnet-codestyle/references/conventions.md b/.github/skills/dotnet-codestyle/references/conventions.md index 5eb48540..2421ab73 100644 --- a/.github/skills/dotnet-codestyle/references/conventions.md +++ b/.github/skills/dotnet-codestyle/references/conventions.md @@ -122,5 +122,14 @@ parameters, return values, exceptions, and crefs. /// /// Thrown when is not a supported value. /// -public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} +public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) +{ + if (category is not ("motivational" or "humor")) + { + throw new ArgumentException($"Unsupported category: {category}", nameof(category)); + } + + await Task.Delay(0, cancellationToken); + return $"Quote for {category}"; +} ``` diff --git a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index 597cd5f1..6d934c0e 100644 --- a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -35,7 +35,7 @@ Two traps, both learned the hard way: `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take `develop`'s side for the EOL-conflicted files (`git checkout --theirs `) **after confirming each is content-identical modulo EOL, or that `develop` is a strict superset** - (`diff <(git show :2:f | tr -d '\r') <(git show :3:f | tr -d '\r')`), then open that branch into + (`diff <(git show ":2:" | tr -d '\r') <(git show ":3:" | tr -d '\r')`), then open that branch into `main`. Verify no genuine `main`-only content is dropped (build/test where the repo supports it). ## Why both rulesets omit "Require branches to be up to date before merging" diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index e291fb66..18962375 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -67,8 +67,9 @@ Preserve the evidence RESYNC.md section 2 requires, and do not leave the finding 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` from the hub at `main`, - then `apply` for what it reports, never from a carried copy. + `repo-config/configure.sh check "/" release` (substitute `operational` for an + operational repo) from the hub at `main`, then `apply` for what it reports, never from a + carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. Reconcile the registry entry (`status`, `types`, `releaseTrigger`, `workflowModel`, diff --git a/.github/skills/standup-a-repo/SKILL.md b/.github/skills/standup-a-repo/SKILL.md index 54c277a5..8269093d 100644 --- a/.github/skills/standup-a-repo/SKILL.md +++ b/.github/skills/standup-a-repo/SKILL.md @@ -75,8 +75,9 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same - command's `check` subcommand, never from a hand-built or carried copy. + `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main` and check with the same command's `check` subcommand, + never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in From c6cff568a4c4d55b93d09ed6b20c1e4034cabb75 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:00:27 -0700 Subject: [PATCH 4/8] Mark the pyproject.toml Divergence-Ledger Gap as Tracked, Per #669 (#1024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #669, though it doesn't close it, the underlying track/appliesTo decision for `pyproject.toml` is still the maintainer's to make. Of the three `investigate`-disposition entries #669 named, two (`.github/workflows/publish-release.yml`, `.github/workflows/validate-task.yml`) have already converged to `retire` via the separate hub-hosted-reusable-workflows migration effort, unrelated to this issue. Only `pyproject.toml` remains at `investigate` with `tracking: null`. `reports/divergences.md` already shows this entry's carriers (Financial-Modeling, aiopurpleair, homeassistant-purpleair), confirming python repos do carry an equivalent, the fact the entry's own reason said was needed before a track/appliesTo decision. That decision (interface vs intent fidelity, and an `appliesTo` scope) surfaces many new findings fleet-wide per the entry's own reason, so I left it to the maintainer rather than making it unilaterally. Set `tracking` to the issue per the entry's own stated acceptable outcome: 'the entry keeps `investigate` and gains a `tracking` value pointing at this issue... the deferral becomes visible as a deferral rather than reading as an omission.' Regenerated `reports/divergences.md` via `spec/fidelity_honesty.py --report`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Documentation** * Updated divergence tracking metadata for `pyproject.toml`. * Clarified fleet-wide findings and existing carrier information. * Refreshed stale-copy counts and carrier lists in governance documentation, including HomeAutomation-Config. --- reports/divergences.md | 6 +++--- spec/divergences.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reports/divergences.md b/reports/divergences.md index 4e2eebed..612d53e8 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -10,7 +10,7 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### investigate -- **pyproject.toml** (manifest gap, carried by Financial-Modeling, aiopurpleair, homeassistant-purpleair) - The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent. +- **pyproject.toml** (manifest gap, carried by Financial-Modeling, aiopurpleair, homeassistant-purpleair) (tracking: ptr727/ProjectTemplate#669) - The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows carriers (ptr727/ProjectTemplate#669), so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's. ### retire @@ -51,12 +51,12 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur A past hub revision, not the current canonical - the audit already flags these as DRIFT. Copy the current file down. No judgment needed. - **AGENTS.md > Context and Delegation Discipline** (2): PhotoCleaner, PlexCleaner -- **AGENTS.md > Where the Rules Live** (6): Blog, ESPHome-Config, Financial-Modeling, HomeAssistant-Config, PhotoCleaner, PlexCleaner +- **AGENTS.md > Where the Rules Live** (7): Blog, ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PhotoCleaner, PlexCleaner - **GOVERNANCE.md > Repository Boundaries and Write Safety** (2): PhotoCleaner, PlexCleaner - **GOVERNANCE.md > Operational Repositories** (2): PhotoCleaner, PlexCleaner - **GOVERNANCE.md > Hub-Hosted Tooling** (2): PhotoCleaner, PlexCleaner - **GOVERNANCE.md > PR Review Etiquette** (5): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, PhotoCleaner, PlexCleaner -- **GOVERNANCE.md > Workflow YAML Conventions** (6): Blog, ESPHome-Config, Financial-Modeling, HomeAssistant-Config, PhotoCleaner, PlexCleaner +- **GOVERNANCE.md > Workflow YAML Conventions** (7): Blog, ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PhotoCleaner, PlexCleaner - **GOVERNANCE.md > Repository Details** (5): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, PhotoCleaner, PlexCleaner - **.markdownlint-cli2.jsonc** (11): DevKitCIoT, ESPHome-NonRoot, HolidayLights, KiCadLibrary, LanguageTags, MediaTools, NxWitness, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair diff --git a/spec/divergences.json b/spec/divergences.json index d7b42a6b..c8aa8379 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -20,7 +20,7 @@ { "path": "scripts/README.md", "disposition": "accepted", "reason": "A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's.", "tracking": null }, { "path": ".github/actionlint.yaml", "disposition": "accepted", "reason": "A path collision rather than a carry. HomeAutomation-Config's own copy declares self-hosted-runner labels (homelab, ubuntu-24.04) for its self-hosted CI runner, entirely different content from the hub's own file at this path, which configures $/ self-reference ignore rules for the hub's own workflows. Verified by reading both copies on 2026-08-25.", "tracking": null }, { "path": ".github/actions/validate/action.yml", "disposition": "accepted", "reason": "A path collision rather than a carry. HomeAutomation-Config's own copy overrides the interface-workflow validate hook, per RESYNC.md 'Apply, in This Order' item 4, 'Interface workflows': 'Honor the named contract... rather than copying bytes. The body is the repository's own.' It runs its CloudInit/ nested Python project through uv/ruff/pyright/pytest. The hub's own file at this same path is a different override, its own registry/spec self-test suite. A repo declaring its own .github/actions/validate/action.yml is the documented, intended override mechanism, not drift to reconcile. Verified by reading both copies on 2026-08-25.", "tracking": null }, - { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent.", "tracking": null }, + { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows carriers (ptr727/ProjectTemplate#669), so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's.", "tracking": "ptr727/ProjectTemplate#669" }, { "path": ".github/workflows/get-version-task.yml", "disposition": "retire", "reason": "The task is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no manifest entry and a downstream copy is retired rather than re-vendored. Every copy is the hub's own NBGV logic with nothing per-repo in it beyond the action pins Dependabot already owns. The carriers, read from the fleet on 2026-08-15, are ESPHome-NonRoot, NxWitness, PhotoCleaner, PlexCleaner, VSCode-Server-DotNetCore, KiCadLibrary, aiopurpleair, and homeassistant-purpleair. Delete the copy and reach the hub task by pin as each repo is next visited, per docs/reusable-workflows.md \"Adopting the Pure Functions\".", "tracking": null }, { "path": ".github/workflows/publish-plan-task.yml", "disposition": "retire", "reason": "The task is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no manifest entry and a downstream copy is retired rather than re-vendored. The carriers, read from the fleet on 2026-08-15, are ESPHome-NonRoot, NxWitness, and Utilities, and all three carry a strict subset of the canonical, missing the -E in set -Eeuo pipefail and the ::warning:: branch for an unrecognized actor pushing to main (WORKFLOW.md D8.4). Delete the copy and reach the hub task by pin as each repo is next visited, per docs/reusable-workflows.md \"Adopting the Pure Functions\".", "tracking": null }, { "path": ".github/workflows/validate-task.yml", "disposition": "retire", "reason": "The file is hub-hosted as a workflow_call task rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\" and docs/reusable-workflows.md \"Stage 2: The Gates\". The fleet doc-lint block, the language lint, the prose gate, and the repo gate move into the hub task, and a repo's own domain checks move into its own .github/actions/validate/action.yml hook instead, so a downstream copy is retired rather than re-vendored. The thirteen repos carrying a copy as of 2026-08-16 were PhotoCleaner, PlexCleaner, LanguageTags, Utilities, MediaTools, AudioCleaner, aiopurpleair, Financial-Modeling, Blog, ESPHome-NonRoot, NxWitness, VSCode-Server-DotNetCore, and HomeAutomation-Config, and the live current carrier list above may have moved on since. Delete the copy and adopt the caller stub in docs/reusable-workflows.md \"Adopting the Gates\" as each repo is next visited.", "tracking": null }, From c66ed555ff2c749b3630fb51928c905f1785d8ff Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:00:37 -0700 Subject: [PATCH 5/8] Regenerate reports/workflow-reuse.md, Per #1001 (#1025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1001. The carrier lists had drifted stale (last generated 2026-08-16 at hub `76f15b3`, nine days before CodeRabbit flagged the mismatch against `reports/divergences.md` on PR #1000). Confirmed with the maintainer that the two reports are meant to track current fleet state independently, rather than documented as intentionally-independent snapshots, so the fix is to regenerate rather than annotate the drift as expected. `python3 spec/workflow_reuse.py --report`, now at hub `5ce0374`. The `validate-task.yml` carrier count (9) now matches `reports/divergences.md`'s own live count for the same file, the specific mismatch #1001 named. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- reports/workflow-reuse.md | 79 ++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/reports/workflow-reuse.md b/reports/workflow-reuse.md index fd6b03be..6759cd42 100644 --- a/reports/workflow-reuse.md +++ b/reports/workflow-reuse.md @@ -1,12 +1,12 @@ # Fleet workflow reuse report -Generated by `python3 spec/workflow_reuse.py --report` at hub `76f15b3` - do not hand-edit. Each row reads a repo's ground-truth branch at generation time and compares it against the hub canonical of the same name after line-ending, action-pin, and job-needs normalization, per [`spec/fidelity-model.md`][fidelity-model] "Normalization". Git dates this file. The target model and the migration phases are in [`docs/reusable-workflows.md`][reusable-workflows]. +Generated by `python3 spec/workflow_reuse.py --report` at hub `5ce0374` - do not hand-edit. Each row reads a repo's ground-truth branch at generation time and compares it against the hub canonical of the same name after line-ending, action-pin, and job-needs normalization, per [`spec/fidelity-model.md`][fidelity-model] "Normalization". Git dates this file. The target model and the migration phases are in [`docs/reusable-workflows.md`][reusable-workflows]. ## Fleet Total -- **104 workflow files, 10,358 lines** across 20 downstream repos, 96 of them named for a hub canonical. No workflow at all in EspDinIoT. -- **3,654 lines (35%) are byte-identical to a hub canonical** after normalization, which is the confirmed duplication. The rest is mostly a per-repo edit of the same canonical rather than independent code. -- **Files reaching a hub reusable workflow or composite action through a pinned `uses:`: 3.** That is the state every carried copy converges to, so this number rises and the two above fall as the migration lands. +- **101 workflow files, 9,592 lines** across 20 downstream repos, 92 of them named for a hub canonical. No workflow at all in EspDinIoT. +- **3,513 lines (37%) are byte-identical to a hub canonical** after normalization, which is the confirmed duplication. The rest is mostly a per-repo edit of the same canonical rather than independent code. +- **Files reaching a hub reusable workflow or composite action through a pinned `uses:`: 19.** That is the state every carried copy converges to, so this number rises and the two above fall as the migration lands. ## Per Workflow @@ -14,16 +14,15 @@ Downstream copies of each hub canonical. A variant is a cluster of copies each a | File | Copies | Lines | Identical to hub | Variants | Callers | | --- | --- | --- | --- | --- | --- | -| `build-release-task.yml` | 9 | 1,709 | 891 | 6 | 0 | -| `test-pull-request.yml` | 20 | 1,661 | 497 | 14 | 1 | -| `merge-bot-pull-request.yml` | 16 | 1,626 | 203 | 8 | 1 | -| `publish-release.yml` | 17 | 1,374 | 416 | 13 | 1 | -| `validate-task.yml` | 13 | 1,214 | 442 | 11 | 0 | +| `build-release-task.yml` | 9 | 1,709 | 827 | 6 | 0 | +| `test-pull-request.yml` | 20 | 1,615 | 501 | 14 | 6 | +| `merge-bot-pull-request.yml` | 17 | 1,401 | 276 | 7 | 6 | +| `publish-release.yml` | 17 | 1,335 | 506 | 15 | 6 | +| `validate-task.yml` | 9 | 891 | 323 | 7 | 0 | | `build-docker-task.yml` | 4 | 512 | 266 | 4 | 0 | | `get-version-task.yml` | 7 | 439 | 342 | 4 | 0 | -| `publish-plan-task.yml` | 3 | 252 | 207 | 1 | 0 | -| `deploy-site-task.yml` | 1 | 191 | 113 | 1 | 0 | -| `run-codegen-pull-request-task.yml` | 2 | 161 | 130 | 1 | 0 | +| `publish-plan-task.yml` | 3 | 252 | 198 | 1 | 0 | +| `run-codegen-pull-request-task.yml` | 2 | 161 | 127 | 1 | 0 | | `check-upstream-version-task.yml` | 1 | 133 | 93 | 1 | 0 | | `run-periodic-codegen-pull-request.yml` | 2 | 48 | 29 | 2 | 0 | | `publish-docker-readme-task.yml` | 1 | 34 | 25 | 1 | 0 | @@ -40,12 +39,12 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu - 1: aiopurpleair - 1: homeassistant-purpleair - `test-pull-request.yml` - - 2: AudioCleaner, Financial-Modeling + - 1: AudioCleaner - 1: Blog - 2: DevKitCIoT, HolidayLights - 1: ESPHome-Config - 1: ESPHome-NonRoot - - 1: HomeAssistant-Config + - 2: Financial-Modeling, HomeAssistant-Config - 1: HomeAutomation-Config - 1: KiCadLibrary - 4: LanguageTags, MediaTools, Utilities, aiopurpleair @@ -55,19 +54,19 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu - 1: Vantage-Config - 1: homeassistant-purpleair - `merge-bot-pull-request.yml` - - 1: Blog - - 6: ESPHome-Config, HomeAssistant-Config, HomeAutomation-Config, PlexCleaner, Utilities, Vantage-Config + - 6: Blog, ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PhotoCleaner - 3: ESPHome-NonRoot, NxWitness, homeassistant-purpleair - 1: KiCadLibrary - 1: LanguageTags - 2: MediaTools, aiopurpleair - - 1: PhotoCleaner + - 3: PlexCleaner, Utilities, Vantage-Config - 1: VSCode-Server-DotNetCore - `publish-release.yml` - 1: Blog - - 2: ESPHome-Config, Vantage-Config + - 1: ESPHome-Config - 1: ESPHome-NonRoot - - 3: Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config + - 1: Financial-Modeling + - 2: HomeAssistant-Config, HomeAutomation-Config - 1: KiCadLibrary - 2: LanguageTags, MediaTools - 1: NxWitness @@ -75,15 +74,12 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu - 1: PlexCleaner - 1: Utilities - 1: VSCode-Server-DotNetCore + - 1: Vantage-Config - 1: aiopurpleair - 1: homeassistant-purpleair - `validate-task.yml` - 3: AudioCleaner, MediaTools, Utilities - - 1: Blog - 1: ESPHome-NonRoot - - 1: Financial-Modeling - - 1: HomeAssistant-Config - - 1: HomeAutomation-Config - 1: LanguageTags - 1: NxWitness - 1: PlexCleaner @@ -101,8 +97,6 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu - 1: homeassistant-purpleair - `publish-plan-task.yml` - 3: ESPHome-NonRoot, NxWitness, Utilities -- `deploy-site-task.yml` - - 1: Blog - `run-codegen-pull-request-task.yml` - 2: LanguageTags, NxWitness - `check-upstream-version-task.yml` @@ -117,34 +111,35 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu | Repo | Files | Lines | Identical to hub | Callers | Repo-local files | | --- | --- | --- | --- | --- | --- | -| AudioCleaner | 2 | 134 | 58 | 0 | - | -| Blog | 6 | 560 | 229 | 0 | `deploy-site.yml` | +| AudioCleaner | 2 | 134 | 57 | 0 | - | +| Blog | 4 | 220 | 97 | 4 | `deploy-site.yml` | | DevKitCIoT | 1 | 58 | 25 | 0 | - | -| ESPHome-Config | 3 | 450 | 61 | 0 | - | -| ESPHome-NonRoot | 11 | 1,154 | 451 | 0 | `check-upstream-dependency.yml`, `check-upstream-version.yml` | -| Financial-Modeling | 3 | 233 | 85 | 0 | - | +| ESPHome-Config | 3 | 327 | 113 | 3 | - | +| ESPHome-NonRoot | 11 | 1,154 | 457 | 0 | `check-upstream-dependency.yml`, `check-upstream-version.yml` | +| Financial-Modeling | 3 | 137 | 77 | 3 | - | | HolidayLights | 1 | 53 | 25 | 0 | - | -| HomeAssistant-Config | 4 | 247 | 89 | 0 | - | -| HomeAutomation-Config | 4 | 248 | 87 | 0 | - | -| KiCadLibrary | 6 | 772 | 198 | 0 | `build-datebadge-task.yml` | -| LanguageTags | 7 | 724 | 305 | 0 | - | -| MediaTools | 5 | 530 | 212 | 0 | - | -| NxWitness | 10 | 1,102 | 378 | 0 | `build-base-images-task.yml` | -| PhotoCleaner | 3 | 203 | 86 | 3 | - | -| PlexCleaner | 8 | 805 | 356 | 0 | `build-executable-task.yml` | -| Utilities | 6 | 621 | 294 | 0 | - | +| HomeAssistant-Config | 3 | 124 | 98 | 3 | - | +| HomeAutomation-Config | 4 | 164 | 99 | 3 | `test-homelab-runner.yml` | +| KiCadLibrary | 6 | 772 | 201 | 0 | `build-datebadge-task.yml` | +| LanguageTags | 7 | 724 | 280 | 0 | - | +| MediaTools | 5 | 530 | 188 | 0 | - | +| NxWitness | 10 | 1,102 | 375 | 0 | `build-base-images-task.yml` | +| PhotoCleaner | 3 | 203 | 85 | 3 | - | +| PlexCleaner | 8 | 805 | 362 | 0 | `build-executable-task.yml` | +| Utilities | 6 | 621 | 266 | 0 | - | | VSCode-Server-DotNetCore | 8 | 563 | 294 | 0 | - | | Vantage-Config | 3 | 229 | 59 | 0 | - | -| aiopurpleair | 6 | 611 | 222 | 0 | - | -| homeassistant-purpleair | 7 | 1,061 | 140 | 0 | `check-ha-version.yml`, `test-release-task.yml` | +| aiopurpleair | 6 | 611 | 220 | 0 | - | +| homeassistant-purpleair | 7 | 1,061 | 135 | 0 | `check-ha-version.yml`, `test-release-task.yml` | ## Repo-Local Workflows A workflow no hub canonical names. Each is either genuinely repo-specific, and stays, or a candidate for a hub task with a hook, and the design doc lists which. -- **Blog** `deploy-site.yml` (55 lines) +- **Blog** `deploy-site.yml` (70 lines) - **ESPHome-NonRoot** `check-upstream-dependency.yml` (111 lines) - **ESPHome-NonRoot** `check-upstream-version.yml` (41 lines) +- **HomeAutomation-Config** `test-homelab-runner.yml` (43 lines) - **KiCadLibrary** `build-datebadge-task.yml` (37 lines) - **NxWitness** `build-base-images-task.yml` (89 lines) - **PlexCleaner** `build-executable-task.yml` (107 lines) From 236df5ff2f6806b66fdd149e7ce802354b272b5c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:11:39 -0700 Subject: [PATCH 6/8] Fix Issue #928's Internal-Inconsistency Findings in Skills (#1026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #928 (the last un-addressed real findings: items 5-8 of the 'Internal inconsistencies' section; item 12 was already fixed on develop; items 9-11 are the reporter's own lower-confidence/judgment-call items, left open). Builds on PR #1023, which fixed the 'Real bugs' section. 5. `resync-a-repo/SKILL.md` and `skill-lifecycle/SKILL.md` instructed committing/pushing during their normal procedure unconditionally, conflicting with `git-commit-conventions`' 'default to staging, commit only when explicitly authorized' rule. Both now state the commit step needs the user's go-ahead. 6. `dotnet-codestyle/references/project-config.md`'s numbered property list omitted `Nullable` and `GenerateDocumentationFile`, both required elsewhere in the skill. Added as item 5, pointing to `references/conventions.md` for the XML documentation format. 7. `comment-and-doc-style/SKILL.md`'s own PR-title examples were written in sentence case throughout (both the inline "Add 24-hour PM2.5 average sensor" example and the five-line code block), contradicting the title-case rule stated immediately above them. Retitled to match (`net8.0`/`xunit.v3`/`devcontainer` stay lowercase as literal identifiers, the already-compliant Dependabot-style `Bump` line is unchanged). 8. `python-codestyle/SKILL.md`'s 'Local development loop' and 'Tests' sections, and `references/testing.md`, presented `uv run pytest` as the universal test command with no mention of the lint-only Scripts profile's `unittest` convention (already documented in `references/profiles.md`). Added qualifying notes pointing there rather than duplicating it. Regenerated both derived trees via `scripts/build_dist.py`; `--check` and `scripts/tests/test_build_dist.py` both pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit - **Documentation** - Clarified title-case conventions for pull requests and commit messages. - Added guidance for nullable reference types and XML documentation in .NET projects. - Improved Python development and testing instructions for build and lint-only project profiles. - Clarified authorization requirements before committing generated or audit-related updates. - **Chores** - Synchronized skill guidance across supported integrations and refreshed its version digest. --- .agents/skills/comment-and-doc-style/SKILL.md | 12 ++--- .../references/project-config.md | 4 ++ .agents/skills/python-codestyle/SKILL.md | 45 +++++++++++-------- .../python-codestyle/references/testing.md | 4 ++ .agents/skills/resync-a-repo/SKILL.md | 2 +- .agents/skills/skill-lifecycle/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/comment-and-doc-style/SKILL.md | 12 ++--- .../references/project-config.md | 4 ++ .../skills/python-codestyle/SKILL.md | 45 +++++++++++-------- .../python-codestyle/references/testing.md | 4 ++ .../skills/resync-a-repo/SKILL.md | 2 +- .../skills/skill-lifecycle/SKILL.md | 2 +- .github/skills/comment-and-doc-style/SKILL.md | 12 ++--- .../references/project-config.md | 4 ++ .github/skills/python-codestyle/SKILL.md | 45 +++++++++++-------- .../python-codestyle/references/testing.md | 4 ++ .github/skills/resync-a-repo/SKILL.md | 2 +- .github/skills/skill-lifecycle/SKILL.md | 2 +- 19 files changed, 130 insertions(+), 79 deletions(-) diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index aced18d4..af574e92 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -224,8 +224,8 @@ 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, +- **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 @@ -237,11 +237,11 @@ hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. *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 +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 +Clarify devcontainer Setup Steps in README ``` ## Quantitative claims diff --git a/.agents/skills/dotnet-codestyle/references/project-config.md b/.agents/skills/dotnet-codestyle/references/project-config.md index 8f6e8388..42b8fd1c 100644 --- a/.agents/skills/dotnet-codestyle/references/project-config.md +++ b/.agents/skills/dotnet-codestyle/references/project-config.md @@ -15,3 +15,7 @@ ``` + +5. **Nullable and XML documentation**: `enable`, + `true` (see `references/conventions.md` + for the XML documentation format every public surface needs). diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index afb345af..f7a7451e 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -48,7 +48,7 @@ declaration, versioning, VS Code config), see `references/profiles.md`. | [ruff][ruff-link] | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | | [pyright][pyright-link] | type checker (the default, a strict baseline) | `pyproject.toml` `[tool.pyright]` | | [mypy][mypy-link] | additional/alternate type checker (optional, the CI checker in a mypy-in-CI repo, required for Home Assistant) | `pyproject.toml` `[tool.mypy]` (or per home-assistant/core) | -| [pytest][docs-link] | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | +| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` | **Type checking targets strongly typed, deterministic code.** pyright in strict mode is the default baseline on first-party code (a repo may instead run mypy in CI and keep pyright @@ -72,7 +72,9 @@ inherently consistent. ## Local development loop -From inside the Python project directory: +From inside a **build**-profile Python project directory. A **lint-only** Scripts profile has no +`uv.lock` to sync and no pytest to run, substitute `uvx` per tool and `unittest` per the Two +Profiles section above: ```sh uv sync # creates .venv, installs deps + dev group @@ -85,15 +87,18 @@ uv run pytest # run tests uv build # produce wheel + sdist in ./dist (published packages only) ``` -The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the repo's type checker: -`uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both where the repo runs -both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are -documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell -chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. A working local hook is -strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical -`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters -Locally" for what the hook must cover and what its absence means. +The **build**-profile Python clean-compile is `uv run ruff format` + `uv run ruff check` + the +repo's type checker: `uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both +where the repo runs both (see Type checking above). Run it, plus `uv run pytest`, before +committing. A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` +equivalents, per Two Profiles above, and has no such command to run before committing beyond +those. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, +no `&&` shell chaining, so it runs the same on any task shell) is in the hub +`vscode-tasks-python.json` snippet. CI runs the same clean-compile commands as the authoritative +backstop. A working local hook is strongly suggested, not opt-in: wire the Python `pre-commit` +framework from the canonical `catalog/snippets/pre-commit/.pre-commit-config.yaml`. See +GOVERNANCE.md "Running the Linters Locally" for what the hook must cover and what its absence +means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before @@ -142,9 +147,12 @@ For comments, docstrings, full type-hint rules, naming, imports, and all pattern ## Tests -`uv run pytest`. One test file per module (`test_.py`), fixtures over setup/teardown, -fakes over mocks. Test the docstring's contract, not implementation details. See -`references/testing.md` for the full conventions. +`uv run pytest` for a build profile, `unittest` for a lint-only Scripts profile (see Two Profiles +above). One test file per module (`test_.py`). A build profile prefers fixtures over +`unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly, +since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test +the docstring's contract, not implementation details. See `references/testing.md` for the full +build-profile conventions, and `references/profiles.md` for the lint-only `unittest` conventions. ## Versioning @@ -159,10 +167,11 @@ Before pushing or opening a PR: - VS Code's Problems pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). -- The CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's type checker - (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as the local - loop above, run from the Python project directory (invoked as separate steps, not `&&`-chained, - so the runner shell is irrelevant). +- The **build**-profile CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's + type checker (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as + the local loop above, run from the Python project directory (invoked as separate steps, not + `&&`-chained, so the runner shell is irrelevant). A **lint-only** profile's CI gate is its `uvx` + equivalents plus its `unittest` suite, per `references/profiles.md`. - Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules, packaged as the `comment-and-doc-style` Skill. diff --git a/.agents/skills/python-codestyle/references/testing.md b/.agents/skills/python-codestyle/references/testing.md index c19ff9d7..54b756a8 100644 --- a/.agents/skills/python-codestyle/references/testing.md +++ b/.agents/skills/python-codestyle/references/testing.md @@ -1,5 +1,9 @@ # Python Testing Conventions +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest +against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are +in `references/profiles.md`. + Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index 18962375..83aea93d 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -82,4 +82,4 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report, done means measured, not applied. +commit the report per `git-commit-conventions`, done means measured, not applied. diff --git a/.agents/skills/skill-lifecycle/SKILL.md b/.agents/skills/skill-lifecycle/SKILL.md index 720c59ac..f866746c 100644 --- a/.agents/skills/skill-lifecycle/SKILL.md +++ b/.agents/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 160eee0d..3952a619 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -702692e5f8c8f60e +9bf75d7cd0da2253 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 aced18d4..af574e92 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 @@ -224,8 +224,8 @@ 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, +- **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 @@ -237,11 +237,11 @@ hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. *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 +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 +Clarify devcontainer Setup Steps in README ``` ## Quantitative claims diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/project-config.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/project-config.md index 8f6e8388..42b8fd1c 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/project-config.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/project-config.md @@ -15,3 +15,7 @@ ``` + +5. **Nullable and XML documentation**: `enable`, + `true` (see `references/conventions.md` + for the XML documentation format every public surface needs). diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index afb345af..f7a7451e 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -48,7 +48,7 @@ declaration, versioning, VS Code config), see `references/profiles.md`. | [ruff][ruff-link] | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | | [pyright][pyright-link] | type checker (the default, a strict baseline) | `pyproject.toml` `[tool.pyright]` | | [mypy][mypy-link] | additional/alternate type checker (optional, the CI checker in a mypy-in-CI repo, required for Home Assistant) | `pyproject.toml` `[tool.mypy]` (or per home-assistant/core) | -| [pytest][docs-link] | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | +| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` | **Type checking targets strongly typed, deterministic code.** pyright in strict mode is the default baseline on first-party code (a repo may instead run mypy in CI and keep pyright @@ -72,7 +72,9 @@ inherently consistent. ## Local development loop -From inside the Python project directory: +From inside a **build**-profile Python project directory. A **lint-only** Scripts profile has no +`uv.lock` to sync and no pytest to run, substitute `uvx` per tool and `unittest` per the Two +Profiles section above: ```sh uv sync # creates .venv, installs deps + dev group @@ -85,15 +87,18 @@ uv run pytest # run tests uv build # produce wheel + sdist in ./dist (published packages only) ``` -The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the repo's type checker: -`uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both where the repo runs -both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are -documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell -chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. A working local hook is -strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical -`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters -Locally" for what the hook must cover and what its absence means. +The **build**-profile Python clean-compile is `uv run ruff format` + `uv run ruff check` + the +repo's type checker: `uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both +where the repo runs both (see Type checking above). Run it, plus `uv run pytest`, before +committing. A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` +equivalents, per Two Profiles above, and has no such command to run before committing beyond +those. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, +no `&&` shell chaining, so it runs the same on any task shell) is in the hub +`vscode-tasks-python.json` snippet. CI runs the same clean-compile commands as the authoritative +backstop. A working local hook is strongly suggested, not opt-in: wire the Python `pre-commit` +framework from the canonical `catalog/snippets/pre-commit/.pre-commit-config.yaml`. See +GOVERNANCE.md "Running the Linters Locally" for what the hook must cover and what its absence +means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before @@ -142,9 +147,12 @@ For comments, docstrings, full type-hint rules, naming, imports, and all pattern ## Tests -`uv run pytest`. One test file per module (`test_.py`), fixtures over setup/teardown, -fakes over mocks. Test the docstring's contract, not implementation details. See -`references/testing.md` for the full conventions. +`uv run pytest` for a build profile, `unittest` for a lint-only Scripts profile (see Two Profiles +above). One test file per module (`test_.py`). A build profile prefers fixtures over +`unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly, +since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test +the docstring's contract, not implementation details. See `references/testing.md` for the full +build-profile conventions, and `references/profiles.md` for the lint-only `unittest` conventions. ## Versioning @@ -159,10 +167,11 @@ Before pushing or opening a PR: - VS Code's Problems pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). -- The CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's type checker - (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as the local - loop above, run from the Python project directory (invoked as separate steps, not `&&`-chained, - so the runner shell is irrelevant). +- The **build**-profile CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's + type checker (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as + the local loop above, run from the Python project directory (invoked as separate steps, not + `&&`-chained, so the runner shell is irrelevant). A **lint-only** profile's CI gate is its `uvx` + equivalents plus its `unittest` suite, per `references/profiles.md`. - Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules, packaged as the `comment-and-doc-style` Skill. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md b/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md index c19ff9d7..54b756a8 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md @@ -1,5 +1,9 @@ # Python Testing Conventions +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest +against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are +in `references/profiles.md`. + Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index 18962375..83aea93d 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -82,4 +82,4 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report, done means measured, not applied. +commit the report per `git-commit-conventions`, done means measured, not applied. diff --git a/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md b/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md index 720c59ac..f866746c 100644 --- a/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.github/skills/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md index aced18d4..af574e92 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -224,8 +224,8 @@ 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, +- **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 @@ -237,11 +237,11 @@ hub-hosted tool the reader runs, are in `references/carried-doc-references.md`. *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 +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 +Clarify devcontainer Setup Steps in README ``` ## Quantitative claims diff --git a/.github/skills/dotnet-codestyle/references/project-config.md b/.github/skills/dotnet-codestyle/references/project-config.md index 8f6e8388..42b8fd1c 100644 --- a/.github/skills/dotnet-codestyle/references/project-config.md +++ b/.github/skills/dotnet-codestyle/references/project-config.md @@ -15,3 +15,7 @@ ``` + +5. **Nullable and XML documentation**: `enable`, + `true` (see `references/conventions.md` + for the XML documentation format every public surface needs). diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index afb345af..f7a7451e 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -48,7 +48,7 @@ declaration, versioning, VS Code config), see `references/profiles.md`. | [ruff][ruff-link] | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | | [pyright][pyright-link] | type checker (the default, a strict baseline) | `pyproject.toml` `[tool.pyright]` | | [mypy][mypy-link] | additional/alternate type checker (optional, the CI checker in a mypy-in-CI repo, required for Home Assistant) | `pyproject.toml` `[tool.mypy]` (or per home-assistant/core) | -| [pytest][docs-link] | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | +| [pytest][docs-link] | test runner (build profile only, lint-only uses `unittest`) | `pyproject.toml` `[tool.pytest.ini_options]` | **Type checking targets strongly typed, deterministic code.** pyright in strict mode is the default baseline on first-party code (a repo may instead run mypy in CI and keep pyright @@ -72,7 +72,9 @@ inherently consistent. ## Local development loop -From inside the Python project directory: +From inside a **build**-profile Python project directory. A **lint-only** Scripts profile has no +`uv.lock` to sync and no pytest to run, substitute `uvx` per tool and `unittest` per the Two +Profiles section above: ```sh uv sync # creates .venv, installs deps + dev group @@ -85,15 +87,18 @@ uv run pytest # run tests uv build # produce wheel + sdist in ./dist (published packages only) ``` -The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the repo's type checker: -`uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both where the repo runs -both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are -documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell -chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. A working local hook is -strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical -`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters -Locally" for what the hook must cover and what its absence means. +The **build**-profile Python clean-compile is `uv run ruff format` + `uv run ruff check` + the +repo's type checker: `uv run pyright`, or `uv run mypy src` where mypy is the CI checker, or both +where the repo runs both (see Type checking above). Run it, plus `uv run pytest`, before +committing. A **lint-only** profile's clean-compile substitutes its `uvx` and `unittest` +equivalents, per Two Profiles above, and has no such command to run before committing beyond +those. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, +no `&&` shell chaining, so it runs the same on any task shell) is in the hub +`vscode-tasks-python.json` snippet. CI runs the same clean-compile commands as the authoritative +backstop. A working local hook is strongly suggested, not opt-in: wire the Python `pre-commit` +framework from the canonical `catalog/snippets/pre-commit/.pre-commit-config.yaml`. See +GOVERNANCE.md "Running the Linters Locally" for what the hook must cover and what its absence +means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before @@ -142,9 +147,12 @@ For comments, docstrings, full type-hint rules, naming, imports, and all pattern ## Tests -`uv run pytest`. One test file per module (`test_.py`), fixtures over setup/teardown, -fakes over mocks. Test the docstring's contract, not implementation details. See -`references/testing.md` for the full conventions. +`uv run pytest` for a build profile, `unittest` for a lint-only Scripts profile (see Two Profiles +above). One test file per module (`test_.py`). A build profile prefers fixtures over +`unittest`'s `setUp`/`tearDown` lifecycle hooks. A lint-only profile uses those hooks directly, +since `unittest` has no fixture-injection mechanism of its own. Fakes over mocks either way. Test +the docstring's contract, not implementation details. See `references/testing.md` for the full +build-profile conventions, and `references/profiles.md` for the lint-only `unittest` conventions. ## Versioning @@ -159,10 +167,11 @@ Before pushing or opening a PR: - VS Code's Problems pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). -- The CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's type checker - (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as the local - loop above, run from the Python project directory (invoked as separate steps, not `&&`-chained, - so the runner shell is irrelevant). +- The **build**-profile CI gate is `uv run ruff check`, `uv run ruff format --check`, the repo's + type checker (`uv run pyright` or `uv run mypy src`), and `uv run pytest`, the same commands as + the local loop above, run from the Python project directory (invoked as separate steps, not + `&&`-chained, so the runner shell is irrelevant). A **lint-only** profile's CI gate is its `uvx` + equivalents plus its `unittest` suite, per `references/profiles.md`. - Markdown in this directory follows CODESTYLE.md's repo-wide Markdown and Spelling rules, packaged as the `comment-and-doc-style` Skill. diff --git a/.github/skills/python-codestyle/references/testing.md b/.github/skills/python-codestyle/references/testing.md index c19ff9d7..54b756a8 100644 --- a/.github/skills/python-codestyle/references/testing.md +++ b/.github/skills/python-codestyle/references/testing.md @@ -1,5 +1,9 @@ # Python Testing Conventions +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest +against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are +in `references/profiles.md`. + Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index 18962375..83aea93d 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -82,4 +82,4 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report, done means measured, not applied. +commit the report per `git-commit-conventions`, done means measured, not applied. diff --git a/.github/skills/skill-lifecycle/SKILL.md b/.github/skills/skill-lifecycle/SKILL.md index 720c59ac..f866746c 100644 --- a/.github/skills/skill-lifecycle/SKILL.md +++ b/.github/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. From 1714b1b99337170bc7788e8df3414363aead647a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 10:53:18 -0700 Subject: [PATCH 7/8] Fix a Real UnicodeDecodeError Crash Review Found on the Promotion PR (#1028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per qodo's fresh review of PR #1027 (the develop -> main promotion diff), 3 findings: 1. **Real bug**: `hub_tracked()` requested NUL-delimited raw bytes from `git ls-tree -z` but decoded them with `subprocess`'s `text=True` (locale decoding), so a tracked filename with a byte invalid in that locale raised `UnicodeDecodeError` before the NUL-split ever ran, aborting the audit rather than enumerating the path. Read raw bytes instead and decode each record with `os.fsdecode()` (surrogateescape), matching the rest of Python's filesystem APIs. Verified by reproducing the crash with the old code against a synthetic non-UTF-8 filename, confirming the fix enumerates it correctly, and adding the case as a permanent `--selftest` regression. 2. `hub_only_paths()`'s new docstring used a semicolon as prose punctuation. Split into two sentences. 3. `canonical_blob_sha()`'s new docstring explained `git rev-parse` tree-ish resolution mechanics rather than stating the callable's behavior contract. Trimmed to the contract. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of files with non-standard or non-UTF-8 characters in their names. * Error messages from repository operations are now decoded more reliably, reducing confusing output. * Improved validation when identifying repository files, including clearer errors for missing paths and unsupported file types. * **Documentation** * Clarified documentation for path filtering and object identifier behavior. --- spec/audit.py | 88 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index d2eb2756..34f1d064 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -32,6 +32,7 @@ import hashlib import itertools import json +import locale import pathlib import re import subprocess @@ -103,25 +104,27 @@ def hub_tracked(rev=None): nothing" and silently clear every hub-only finding. """ walk_rev = _hub_main_rev() if rev is None else rev + # No text=True: -z's pathnames are raw bytes, and locale decoding here would crash on an invalid byte before the NUL-split below ever runs. + # Decode each record with a fixed utf-8/surrogateescape policy instead of os.fsdecode(), whose error handler is platform-dependent (surrogatepass on Windows) and still crashes on an arbitrary invalid byte there. r = subprocess.run( ["git", "ls-tree", "-r", "-z", walk_rev], cwd=ROOT, capture_output=True, - text=True, check=False, ) if r.returncode != 0: + stderr = r.stderr.decode("utf-8", errors="replace").strip() raise RuntimeError( - f"git ls-tree -r {walk_rev} failed in {ROOT}: {r.stderr.strip() or 'non-zero exit'}" + f"git ls-tree -r {walk_rev} failed in {ROOT}: {stderr or 'non-zero exit'}" ) paths = set() - for record in r.stdout.split("\0"): + for record in r.stdout.split(b"\0"): if not record: continue - meta, _, path = record.partition("\t") + meta, _, path = record.partition(b"\t") mode = meta.split(None, 1)[0] if meta else None - if mode in ("100644", "100755"): - paths.add(path) + if mode in (b"100644", b"100755"): + paths.add(path.decode("utf-8", errors="surrogateescape")) return frozenset(paths) @@ -140,7 +143,7 @@ def hub_only_paths(spec, rev=None): So only a `retire` disposition in spec/divergences.json asserts a deletion, and an untriaged hit asks for the file to be read. - `rev` is passed through to `hub_tracked()`; see its docstring. + `rev` is passed through to `hub_tracked()`. See its docstring. """ declared = {e["path"] for e in spec["files"]["baseline"]} tree_paths = set() @@ -198,14 +201,14 @@ def repo_tree(slug, ground_head): def canonical_blob_sha(path): """The hub's git blob identity for path, from the same resolved `main` commit `_git_revisions()` and `_hub_main_rev()` walk, not from ROOT's checked-out working tree, - which is not necessarily `main` (ptr727/ProjectTemplate#1017 review). `git rev-parse` resolves - a `:` tree-ish straight to the blob object id, so no separate read-and-hash step is - needed. Raises OSError, matching a filesystem read's own contract, when path is absent from - that commit or is not a regular file there. + which is not necessarily `main` (ptr727/ProjectTemplate#1017 review). Raises OSError, matching + a filesystem read's own contract, when path is absent from that commit or is not a regular + file there. """ rev = _hub_main_rev() + # Read via git ls-tree, not git rev-parse :, which resolves a directory to a tree object id just as readily as a file to a blob one, silently breaking the regular-file promise above. result = subprocess.run( - ["git", "rev-parse", f"{rev}:{path}"], + ["git", "ls-tree", rev, "--", path], cwd=ROOT, capture_output=True, text=True, @@ -213,7 +216,13 @@ def canonical_blob_sha(path): ) if result.returncode != 0: raise OSError(f"{path} is unreadable from the hub's main at {rev}: {result.stderr.strip()}") - return result.stdout.strip() + entry = result.stdout.strip() + if not entry: + raise OSError(f"{path} is absent from the hub's main at {rev}") + mode, _, sha = entry.partition("\t")[0].split() + if mode not in ("100644", "100755"): + raise OSError(f"{path} is not a regular file at {rev} (mode {mode})") + return sha def tree_path_included(path, patterns): @@ -3577,6 +3586,59 @@ def _selftest(): f" {'ok ' if got == want else 'FAIL'} want={want!s:<24} got={got!s:<24} _git_revisions: default rev reads origin's main, not the checked-out branch" ) + # hub_tracked(): a tracked filename with a byte the active locale rejects must round-trip rather than crash. + # git ls-tree -z is NUL-delimited raw bytes, and decoding it as text before the NUL-split (the bug review caught) raises UnicodeDecodeError instead of enumerating the path. + encoding = locale.getpreferredencoding(False) + invalid_bytes = None + for candidate in (b"\xff", b"\x80\x81", b"\xfe\xff"): + try: + candidate.decode(encoding) + except UnicodeDecodeError: + invalid_bytes = candidate + break + if invalid_bytes is None: + # No candidate is actually invalid under this host's active encoding: skip rather than asserting a regression the fixture cannot exercise here. + print(f" skip hub_tracked: no candidate byte sequence is invalid under {encoding!r}") + else: + bad_name = "bad-" + invalid_bytes.decode("utf-8", errors="surrogateescape") + "-name.txt" + with tempfile.TemporaryDirectory() as tmp_root: + tmp_root_path = pathlib.Path(tmp_root) + for cmd in ( + ["git", "init", "-q", "-b", "main"], + ["git", "config", "user.email", "test@test.invalid"], + ["git", "config", "user.name", "test"], + ): + subprocess.run(cmd, cwd=tmp_root_path, check=True, capture_output=True) + try: + (tmp_root_path / bad_name).write_text("x") + except OSError: + # This host's filesystem cannot represent the byte: skip rather than aborting the whole --selftest run over an environment limitation, not a code fault. + print(" skip hub_tracked: non-UTF-8 filename (host cannot create it)") + else: + subprocess.run( + ["git", "add", "-A"], cwd=tmp_root_path, check=True, capture_output=True + ) + subprocess.run( + ["git", "commit", "-q", "-m", "add invalid-utf8 name"], + cwd=tmp_root_path, + check=True, + capture_output=True, + ) + saved_root = ROOT + ROOT = tmp_root_path + try: + hub_tracked.cache_clear() + tracked = hub_tracked(rev="HEAD") + finally: + ROOT = saved_root + hub_tracked.cache_clear() + got = bad_name in tracked + if not got: + ok = False + print( + f" {'ok ' if got else 'FAIL'} want=True got={got!s:<24} hub_tracked: a non-UTF-8 filename round-trips instead of crashing" + ) + # Region extraction and hashing: a forked github-release block must hash differently from the canonical. region = split_jobs(rel_ok).get("github-release") forked_region = split_jobs( From 743fc8106844e9abc7cd6d2be4212a97110681ae Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 26 Aug 2026 11:27:13 -0700 Subject: [PATCH 8/8] Fix Six Findings From the Promotion PR's Fresh Review Pass (#1029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per CodeRabbit's fresh review of PR #1027 (the develop -> main promotion diff), 6 accepted findings (2 others declined with evidence directly on PR #1027): 1. `standup-a-repo/SKILL.md` instructed apply before check, the opposite of `resync-a-repo/SKILL.md`'s own correct check-then-apply order. Swapped to check-then-apply. 2. `dotnet-codestyle/references/conventions.md`'s `GetQuoteOfTheDayAsync` example used `Task.Delay(0, cancellationToken)`, which the .NET runtime completes immediately regardless of later cancellation (a documented zero-delay fast path), so the example did not actually demonstrate the cancellation contract its own XML doc promised. Added an explicit `ThrowIfCancellationRequested()` and switched to a non-zero delay. 3. `python-codestyle/references/testing.md`'s lint-only profile description read as if 'no uv.lock' were why pytest is unused, conflating two separate facts. Stated them separately. 4-5. `resync-a-repo/SKILL.md` and `skill-lifecycle/SKILL.md`: an earlier fix on PR #1026 (for a different reviewer's finding) trimmed the commit-authorization wording to a bare pointer, removing the conditionality itself, not only the restated substance. Restored a minimal 'once authorized' gate word alongside the pointer. 6. `spec/divergences.json`'s carrier-list sentence read as if the issue number were itself one of the carrier repos. Restructured into distinct sentences. Regenerated `reports/divergences.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Documentation** * Clarified cancellation handling in .NET examples. * Updated Python testing guidance for build and lint-only profiles. * Added authorization checkpoints before committing reports and skill updates. * Revised repository setup instructions to check configuration before applying changes. * Refined divergence documentation to remove redundant tracker references. * **Chores** * Refreshed the skills source digest to reflect the latest documentation updates. --- .../skills/dotnet-codestyle/references/conventions.md | 3 ++- .agents/skills/python-codestyle/references/testing.md | 6 +++--- .agents/skills/resync-a-repo/SKILL.md | 3 ++- .agents/skills/skill-lifecycle/SKILL.md | 2 +- .agents/skills/standup-a-repo/SKILL.md | 10 ++++++---- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/dotnet-codestyle/references/conventions.md | 3 ++- .../skills/python-codestyle/references/testing.md | 6 +++--- .../fleet-skills/skills/resync-a-repo/SKILL.md | 3 ++- .../fleet-skills/skills/skill-lifecycle/SKILL.md | 2 +- .../fleet-skills/skills/standup-a-repo/SKILL.md | 10 ++++++---- .../skills/dotnet-codestyle/references/conventions.md | 3 ++- .github/skills/python-codestyle/references/testing.md | 6 +++--- .github/skills/resync-a-repo/SKILL.md | 3 ++- .github/skills/skill-lifecycle/SKILL.md | 2 +- .github/skills/standup-a-repo/SKILL.md | 10 ++++++---- reports/divergences.md | 2 +- spec/divergences.json | 2 +- 18 files changed, 45 insertions(+), 33 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/references/conventions.md b/.agents/skills/dotnet-codestyle/references/conventions.md index 2421ab73..46897c81 100644 --- a/.agents/skills/dotnet-codestyle/references/conventions.md +++ b/.agents/skills/dotnet-codestyle/references/conventions.md @@ -129,7 +129,8 @@ public async Task GetQuoteOfTheDayAsync(string category, CancellationTok throw new ArgumentException($"Unsupported category: {category}", nameof(category)); } - await Task.Delay(0, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1, cancellationToken); return $"Quote for {category}"; } ``` diff --git a/.agents/skills/python-codestyle/references/testing.md b/.agents/skills/python-codestyle/references/testing.md index 54b756a8..0dae3fd4 100644 --- a/.agents/skills/python-codestyle/references/testing.md +++ b/.agents/skills/python-codestyle/references/testing.md @@ -1,8 +1,8 @@ # Python Testing Conventions -This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest -against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are -in `references/profiles.md`. +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` and does not +use pytest, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) +are in `references/profiles.md`. Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index 83aea93d..019e45de 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -82,4 +82,5 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report per `git-commit-conventions`, done means measured, not applied. +commit the report once authorized, per `git-commit-conventions`, done means measured, not +applied. diff --git a/.agents/skills/skill-lifecycle/SKILL.md b/.agents/skills/skill-lifecycle/SKILL.md index f866746c..aa853916 100644 --- a/.agents/skills/skill-lifecycle/SKILL.md +++ b/.agents/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then, once authorized, commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.agents/skills/standup-a-repo/SKILL.md b/.agents/skills/standup-a-repo/SKILL.md index 8269093d..f9d2b4f5 100644 --- a/.agents/skills/standup-a-repo/SKILL.md +++ b/.agents/skills/standup-a-repo/SKILL.md @@ -74,10 +74,12 @@ maintainer can supply what section 0A lists. inventing a shape. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub - repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an - operational repo) from the hub at `main` and check with the same command's `check` subcommand, - never from a hand-built or carried copy. + repository agree before running anything else here, then run + `repo-config/configure.sh check owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main`. A non-zero exit there means drift was found, not a + command failure. Review what it reports. Then run the same command's `apply` subcommand, which + idempotently reconciles the repo to the full committed configuration regardless of what `check` + reported, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 3952a619..ce2732c5 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -9bf75d7cd0da2253 +5ab0e6a26d537def diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md index 2421ab73..46897c81 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md @@ -129,7 +129,8 @@ public async Task GetQuoteOfTheDayAsync(string category, CancellationTok throw new ArgumentException($"Unsupported category: {category}", nameof(category)); } - await Task.Delay(0, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1, cancellationToken); return $"Quote for {category}"; } ``` diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md b/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md index 54b756a8..0dae3fd4 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/references/testing.md @@ -1,8 +1,8 @@ # Python Testing Conventions -This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest -against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are -in `references/profiles.md`. +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` and does not +use pytest, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) +are in `references/profiles.md`. Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index 83aea93d..019e45de 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -82,4 +82,5 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report per `git-commit-conventions`, done means measured, not applied. +commit the report once authorized, per `git-commit-conventions`, done means measured, not +applied. diff --git a/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md b/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md index f866746c..aa853916 100644 --- a/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then, once authorized, commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md index 8269093d..f9d2b4f5 100644 --- a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md @@ -74,10 +74,12 @@ maintainer can supply what section 0A lists. inventing a shape. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub - repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an - operational repo) from the hub at `main` and check with the same command's `check` subcommand, - never from a hand-built or carried copy. + repository agree before running anything else here, then run + `repo-config/configure.sh check owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main`. A non-zero exit there means drift was found, not a + command failure. Review what it reports. Then run the same command's `apply` subcommand, which + idempotently reconciles the repo to the full committed configuration regardless of what `check` + reported, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/.github/skills/dotnet-codestyle/references/conventions.md b/.github/skills/dotnet-codestyle/references/conventions.md index 2421ab73..46897c81 100644 --- a/.github/skills/dotnet-codestyle/references/conventions.md +++ b/.github/skills/dotnet-codestyle/references/conventions.md @@ -129,7 +129,8 @@ public async Task GetQuoteOfTheDayAsync(string category, CancellationTok throw new ArgumentException($"Unsupported category: {category}", nameof(category)); } - await Task.Delay(0, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(1, cancellationToken); return $"Quote for {category}"; } ``` diff --git a/.github/skills/python-codestyle/references/testing.md b/.github/skills/python-codestyle/references/testing.md index 54b756a8..0dae3fd4 100644 --- a/.github/skills/python-codestyle/references/testing.md +++ b/.github/skills/python-codestyle/references/testing.md @@ -1,8 +1,8 @@ # Python Testing Conventions -This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` to run pytest -against, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) are -in `references/profiles.md`. +This covers the **build** profile. A **lint-only** Scripts profile has no `uv.lock` and does not +use pytest, its testing conventions (`unittest`, `uvx coverage@latest run -m unittest discover`) +are in `references/profiles.md`. Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index 83aea93d..019e45de 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -82,4 +82,5 @@ One focused pull request per drift class, branched from the target's `develop`, push to a protected branch and never a hand edit outside a pull request. Close the review loop, per the `pr-review-conduct` skill, before asking the maintainer for merge permission. The maintainer merges, the agent drives to green and stops. Re-run the audit after the merge and -commit the report per `git-commit-conventions`, done means measured, not applied. +commit the report once authorized, per `git-commit-conventions`, done means measured, not +applied. diff --git a/.github/skills/skill-lifecycle/SKILL.md b/.github/skills/skill-lifecycle/SKILL.md index f866746c..aa853916 100644 --- a/.github/skills/skill-lifecycle/SKILL.md +++ b/.github/skills/skill-lifecycle/SKILL.md @@ -28,7 +28,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim 3. **Author the body per the `comment-and-doc-style` skill**: LF (the repo default), present tense, ASCII tiers, no semicolon in prose. Name hub paths as plain code spans rather than repo-relative links, because an installed copy resolves no repo path, and say "from a hub checkout" for anything the reader must run. 4. **Split bulk into `references/`** when the source doc is large: the SKILL.md carries the summary and the binding rules, and each `references/*.md` carries one topic read on demand, the shape `comment-and-doc-style` uses. 5. **Apply the doc-packaging pattern below in the same change** when the skill packages a law doc or one of its sections. -6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. +6. **Regenerate and commit all trees together**: `python3 scripts/build_dist.py`, then, once authorized, commit the source and both generated trees in one commit, per `git-commit-conventions`. CI runs `--check` on every pull request and fails a desynced distribution. `python3 scripts/tests/test_build_dist.py` covers the generator itself. 7. **Record the surfacing**: annotate the `AGENTS.md` "Where the Rules Live" row when the skill packages a GOVERNANCE section, or its closing paragraph when the skill is new content, so the map stays the one place coverage is read from. 8. **Refresh the machines after merge**: re-run `python3 scripts/skills_install.py` per machine, the cadence `docs/host-setup.md` "Fleet Skills Install" states. Until then every machine serves the previous skill set, which `--report` says. diff --git a/.github/skills/standup-a-repo/SKILL.md b/.github/skills/standup-a-repo/SKILL.md index 8269093d..f9d2b4f5 100644 --- a/.github/skills/standup-a-repo/SKILL.md +++ b/.github/skills/standup-a-repo/SKILL.md @@ -74,10 +74,12 @@ maintainer can supply what section 0A lists. inventing a shape. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub - repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release` (substitute `operational` for an - operational repo) from the hub at `main` and check with the same command's `check` subcommand, - never from a hand-built or carried copy. + repository agree before running anything else here, then run + `repo-config/configure.sh check owner/repo release` (substitute `operational` for an + operational repo) from the hub at `main`. A non-zero exit there means drift was found, not a + command failure. Review what it reports. Then run the same command's `apply` subcommand, which + idempotently reconciles the repo to the full committed configuration regardless of what `check` + reported, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood up only when it passes for its type, or its residual deltas are tracked in diff --git a/reports/divergences.md b/reports/divergences.md index 612d53e8..ca3ab12f 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -10,7 +10,7 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### investigate -- **pyproject.toml** (manifest gap, carried by Financial-Modeling, aiopurpleair, homeassistant-purpleair) (tracking: ptr727/ProjectTemplate#669) - The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows carriers (ptr727/ProjectTemplate#669), so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's. +- **pyproject.toml** (manifest gap, carried by Financial-Modeling, aiopurpleair, homeassistant-purpleair) (tracking: ptr727/ProjectTemplate#669) - The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows the carriers. The decision is tracked in ptr727/ProjectTemplate#669, so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's. ### retire diff --git a/spec/divergences.json b/spec/divergences.json index c8aa8379..5a16e7fa 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -20,7 +20,7 @@ { "path": "scripts/README.md", "disposition": "accepted", "reason": "A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's.", "tracking": null }, { "path": ".github/actionlint.yaml", "disposition": "accepted", "reason": "A path collision rather than a carry. HomeAutomation-Config's own copy declares self-hosted-runner labels (homelab, ubuntu-24.04) for its self-hosted CI runner, entirely different content from the hub's own file at this path, which configures $/ self-reference ignore rules for the hub's own workflows. Verified by reading both copies on 2026-08-25.", "tracking": null }, { "path": ".github/actions/validate/action.yml", "disposition": "accepted", "reason": "A path collision rather than a carry. HomeAutomation-Config's own copy overrides the interface-workflow validate hook, per RESYNC.md 'Apply, in This Order' item 4, 'Interface workflows': 'Honor the named contract... rather than copying bytes. The body is the repository's own.' It runs its CloudInit/ nested Python project through uv/ruff/pyright/pytest. The hub's own file at this same path is a different override, its own registry/spec self-test suite. A repo declaring its own .github/actions/validate/action.yml is the documented, intended override mechanism, not drift to reconcile. Verified by reading both copies on 2026-08-25.", "tracking": null }, - { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows carriers (ptr727/ProjectTemplate#669), so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's.", "tracking": "ptr727/ProjectTemplate#669" }, + { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the Python repos carry an equivalent. reports/divergences.md already shows the carriers. The decision is tracked in ptr727/ProjectTemplate#669, so the remaining call is the fleet-wide new-findings tradeoff, which stays the maintainer's.", "tracking": "ptr727/ProjectTemplate#669" }, { "path": ".github/workflows/get-version-task.yml", "disposition": "retire", "reason": "The task is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no manifest entry and a downstream copy is retired rather than re-vendored. Every copy is the hub's own NBGV logic with nothing per-repo in it beyond the action pins Dependabot already owns. The carriers, read from the fleet on 2026-08-15, are ESPHome-NonRoot, NxWitness, PhotoCleaner, PlexCleaner, VSCode-Server-DotNetCore, KiCadLibrary, aiopurpleair, and homeassistant-purpleair. Delete the copy and reach the hub task by pin as each repo is next visited, per docs/reusable-workflows.md \"Adopting the Pure Functions\".", "tracking": null }, { "path": ".github/workflows/publish-plan-task.yml", "disposition": "retire", "reason": "The task is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no manifest entry and a downstream copy is retired rather than re-vendored. The carriers, read from the fleet on 2026-08-15, are ESPHome-NonRoot, NxWitness, and Utilities, and all three carry a strict subset of the canonical, missing the -E in set -Eeuo pipefail and the ::warning:: branch for an unrecognized actor pushing to main (WORKFLOW.md D8.4). Delete the copy and reach the hub task by pin as each repo is next visited, per docs/reusable-workflows.md \"Adopting the Pure Functions\".", "tracking": null }, { "path": ".github/workflows/validate-task.yml", "disposition": "retire", "reason": "The file is hub-hosted as a workflow_call task rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\" and docs/reusable-workflows.md \"Stage 2: The Gates\". The fleet doc-lint block, the language lint, the prose gate, and the repo gate move into the hub task, and a repo's own domain checks move into its own .github/actions/validate/action.yml hook instead, so a downstream copy is retired rather than re-vendored. The thirteen repos carrying a copy as of 2026-08-16 were PhotoCleaner, PlexCleaner, LanguageTags, Utilities, MediaTools, AudioCleaner, aiopurpleair, Financial-Modeling, Blog, ESPHome-NonRoot, NxWitness, VSCode-Server-DotNetCore, and HomeAutomation-Config, and the live current carrier list above may have moved on since. Delete the copy and adopt the caller stub in docs/reusable-workflows.md \"Adopting the Gates\" as each repo is next visited.", "tracking": null },