From 1923f8e875dfe24b488f9cfe2ef799b1d8fcdc87 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 18 Jul 2026 16:44:53 -0700 Subject: [PATCH 1/2] Carried files must not reference the template repo; mechanize the check The private-template reference has spread to 13 of 20 downstream repos, 8 of them public. The dominant instance is a stale copilot-instructions paragraph telling agents to report drift upstream, which hard-links the private template twice - and links an AGENTS.md section the hub no longer has, so it 404s even with access. The #344 rule barred sibling-repo references but arguably let the hub through as "the subject". It now says explicitly that a carried file names the template in neither prose nor link: the template is private, so the link 404s for that repo's readers, and the flow is machinery a consumer should never see. Where a carried file must express a template-level behavior ("report a rule discrepancy upstream"), it states the behavior and not the destination; the maintainer supplies the destination out of band. Mechanized in spec/audit.py rather than left to review, since this spread silently across the fleet: the two agent-instruction files are checked for the template name. The name is derived from the git remote, not the checkout directory - a differently-named clone would otherwise stop detecting silently, the fail-open case the check exists to prevent. Verified: flags PlexCleaner (copilot-instructions) and ESPHome-Config (AGENTS.md); Vantage-Config clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- spec/audit.py | 23 +++++++++++++++++++++++ spec/project-types.json | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d4206249..954660a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,7 +105,7 @@ Clarify devcontainer setup steps in README ## Documentation Style Conventions -- **Carried files reference no other fleet repo unless that repo is the subject.** Every file the fleet carries verbatim - `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, `README.md`, `.github/copilot-instructions.md`, the `repo-config/` and `spec/` files, the carried `AUDIT.md` - is **repo-agnostic**: it describes the shape or pattern self-containedly, names no sibling fleet repo as an illustrative example, and adds no link (inline URL or reference definition) to another fleet repo. This bans only *cross-repo* references - ordinary links to files within the same repo, to anchors, and to external sites are unaffected. Two reasons: any fleet repo may be **private**, so a cross-repo link 404s in a public carrier; and a cross-repo reference **couples** the repos and rots as they diverge. A repo name is allowed only when that repo *is* the subject (the file is about it). To point at a current good example, name it in the **onboarding/conformance issue** (one-off, not carried) or the hub-only `reports/conformance-matrix.md` (a fleet tracking table, not carried) - never in a carried doc. This pairs with the present-tense rule below: state the current shape, not a history of which repo it came from. +- **Carried files reference no other fleet repo unless that repo is the subject.** Every file the fleet carries verbatim - `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, `README.md`, `.github/copilot-instructions.md`, the `repo-config/` and `spec/` files, the carried `AUDIT.md` - is **repo-agnostic**: it describes the shape or pattern self-containedly, names no sibling fleet repo as an illustrative example, and adds no link (inline URL or reference definition) to another fleet repo. **This includes the template repo itself.** A carried file names it in neither prose nor a link: the template is private, so any reference 404s for a downstream repo's users, and the template flow is machinery a consumer of that repo should never have to see. Where a carried file needs to express a template-level behavior - "report a rule discrepancy upstream" - state the behavior, not the destination; the maintainer supplies the destination out of band. This bans only *cross-repo* references - ordinary links to files within the same repo, to anchors, and to external sites are unaffected. Two reasons: any fleet repo may be **private**, so a cross-repo link 404s in a public carrier; and a cross-repo reference **couples** the repos and rots as they diverge. A repo name is allowed only when that repo *is* the subject (the file is about it). To point at a current good example, name it in the **onboarding/conformance issue** (one-off, not carried) or the hub-only `reports/conformance-matrix.md` (a fleet tracking table, not carried) - never in a carried doc. This pairs with the present-tense rule below: state the current shape, not a history of which repo it came from. ### Markdown diff --git a/spec/audit.py b/spec/audit.py index e01f8861..616d8990 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -40,6 +40,21 @@ def load(rel): return json.loads((ROOT / rel).read_text(encoding="utf-8")) +def hub_name(): + """This repo's name on the remote - what a carried file must never reference. + + Read from origin rather than the checkout directory, which a differently-named clone would + silently break; a silent miss here is the fail-open case the check exists to prevent. + """ + r = subprocess.run(["git", "config", "--get", "remote.origin.url"], capture_output=True, text=True, cwd=ROOT) + if r.returncode == 0 and r.stdout.strip(): + return r.stdout.strip().rstrip("/").removesuffix(".git").split("/")[-1] + return ROOT.name + + +HUB_NAME = hub_name() + + def gh(path, ok404=False): """GET a REST path via gh; parsed JSON, or None on 404 when ok404. @@ -183,6 +198,14 @@ def audit_repo(entry, spec): for name in sorted(present - claimed_names): findings.append(("DRIFT", f"secrets: {name} in the {store} store is claimed by no applicable mechanism (stale?)")) + # --- Carried files must not reference the template repo --- + # The template is private, so a reference 404s for this repo's users and exposes machinery they cannot + # follow. Checks the agent-instruction files, where a stale "report drift upstream" paragraph spread. + for path in ("AGENTS.md", ".github/copilot-instructions.md"): + doc = gh(f"repos/{slug}/contents/{path}?ref={ground}", ok404=True) + if doc and doc.get("content") and HUB_NAME in base64.b64decode(doc["content"]).decode("utf-8", "replace"): + findings.append(("DRIFT", f"carried: {path} references the template repo by name or link (private - 404s for this repo's readers; state the behavior, not the destination)")) + # --- Dependabot ecosystem coverage --- # A repo's tree implies Dependabot ecosystems it must track: github-actions when it ships workflows # (the action versions they reference otherwise go stale, and a merge-bot then has no PRs to auto-merge), diff --git a/spec/project-types.json b/spec/project-types.json index 6e3966bd..6cd13b8d 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -144,7 +144,7 @@ { "id": "recurring.charset", "verdict": "letter", "assert": "ASCII only in agent-authored text: no em-dash (use a spaced hyphen), no smart quotes, no stray non-ASCII.", "intentRef": "AGENTS.md#character-set" }, { "id": "recurring.spelling", "verdict": "letter", "assert": "US English spelling; the shared cspell.json sets language en-US (a bare en accepts British spellings too).", "intentRef": "CODESTYLE.md#markdown-and-spelling" }, { "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow .editorconfig, which carries a global [*] end_of_line default plus LF pins for execution-sensitive files (shell, Dockerfiles, shebang-executable .py by path), plus workflow YAML in .github/workflows/* enforced by editorconfig-checker in CI; a per-extension-only form lacking the global [*] default is a drift finding. The global default is CRLF for release repos, or the consuming application's native platform for an operational (config) repo as recorded in the registry lineEndings field (LF for Linux-native/container config e.g. ESPHome/Home Assistant, CRLF for a Windows-native editor e.g. Vantage/Design Center) - do not re-normalize such a repo to CRLF. Edits preserve the file's endings.", "intentRef": "AGENTS.md#line-endings" }, - { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, CODESTYLE.md, WORKFLOW.md, README.md, .github/copilot-instructions.md, repo-config/README.md, repo-config/develop.json, repo-config/main.json, spec/secrets.json, the carried AUDIT.md) names no sibling fleet repo as an illustrative example and adds no link (inline URL or reference definition) to another fleet repo; only cross-repo references are banned - same-repo file/anchor links and external links are fine. A repo name appears only where that repo is the file's subject. Rationale: a cross-repo link 404s in a public carrier when the target is private, and couples the repos. A current example is named in the onboarding/conformance issue or the hub-only reports/conformance-matrix.md, never a carried doc.", "intentRef": "AGENTS.md#documentation-style-conventions" } + { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, CODESTYLE.md, WORKFLOW.md, README.md, .github/copilot-instructions.md, repo-config/README.md, repo-config/develop.json, repo-config/main.json, spec/secrets.json, the carried AUDIT.md) names no other fleet repo as an illustrative example and adds no link (inline URL or reference definition) to one - including the template repo itself, which is private, so a reference 404s for the carrying repo's readers and exposes template machinery they cannot follow; state the behavior, not the destination. spec/audit.py mechanically checks the two agent-instruction files for the template name; only cross-repo references are banned - same-repo file/anchor links and external links are fine. A repo name appears only where that repo is the file's subject. Rationale: a cross-repo link 404s in a public carrier when the target is private, and couples the repos. A current example is named in the onboarding/conformance issue or the hub-only reports/conformance-matrix.md, never a carried doc.", "intentRef": "AGENTS.md#documentation-style-conventions" } ] }, "readme-structure": { From 0e19c73fbf99f84b9686b9a14e5cf3e567245c8a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 18 Jul 2026 16:48:45 -0700 Subject: [PATCH 2/2] Match the template name case-insensitively; announce the fallback (Copilot #359) Two ways the check could miss silently: - The match was case-sensitive, so a lowercased URL (github.com/ptr727/projecttemplate) would pass. Compares casefolded now; GitHub treats repo names case-insensitively. - hub_name()'s directory-name fallback was itself silent, so a fork, worktree, or remoteless checkout would degrade the check with no signal. It now reports whether the name came from the remote, and the run prints a stderr warning when it did not. Both are the fail-open case this check exists to prevent, so neither should be quiet about it. Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/audit.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 616d8990..829004a2 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -41,18 +41,19 @@ def load(rel): def hub_name(): - """This repo's name on the remote - what a carried file must never reference. + """This repo's name on the remote, and whether it came from the remote. - Read from origin rather than the checkout directory, which a differently-named clone would - silently break; a silent miss here is the fail-open case the check exists to prevent. + Read from origin rather than the checkout directory, which a differently-named clone, a fork, or a + worktree without an origin would silently break. The caller announces the directory-name fallback: + a silently degraded match is the fail-open case this check exists to prevent. """ r = subprocess.run(["git", "config", "--get", "remote.origin.url"], capture_output=True, text=True, cwd=ROOT) if r.returncode == 0 and r.stdout.strip(): - return r.stdout.strip().rstrip("/").removesuffix(".git").split("/")[-1] - return ROOT.name + return r.stdout.strip().rstrip("/").removesuffix(".git").split("/")[-1], True + return ROOT.name, False -HUB_NAME = hub_name() +HUB_NAME, HUB_NAME_FROM_REMOTE = hub_name() def gh(path, ok404=False): @@ -203,7 +204,7 @@ def audit_repo(entry, spec): # follow. Checks the agent-instruction files, where a stale "report drift upstream" paragraph spread. for path in ("AGENTS.md", ".github/copilot-instructions.md"): doc = gh(f"repos/{slug}/contents/{path}?ref={ground}", ok404=True) - if doc and doc.get("content") and HUB_NAME in base64.b64decode(doc["content"]).decode("utf-8", "replace"): + if doc and doc.get("content") and HUB_NAME.lower() in base64.b64decode(doc["content"]).decode("utf-8", "replace").lower(): findings.append(("DRIFT", f"carried: {path} references the template repo by name or link (private - 404s for this repo's readers; state the behavior, not the destination)")) # --- Dependabot ecosystem coverage --- @@ -280,7 +281,10 @@ def main(): run_utc = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") hub = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, cwd=ROOT) hub_sha = hub.stdout.strip() if hub.returncode == 0 else "unknown" - print(f"audit run {run_utc} | hub {hub_sha}\n") + print(f"audit run {run_utc} | hub {hub_sha}") + if not HUB_NAME_FROM_REMOTE: + print(f"warning: no git remote; template-reference check falls back to the directory name '{HUB_NAME}' and may miss", file=sys.stderr) + print() hard = 0 for entry in repos: