From aa6225926481f90dcc8d7600fb14cf403a5d2c52 Mon Sep 17 00:00:00 2001 From: PBNZ <93853627+PBNZ@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:30:01 +1200 Subject: [PATCH 1/4] fix(new-repo): commit identity and {{author}} default to the GitHub handle The step-5 recipe resolved user.name as `.name // .login` -- the real profile name on any account that has one -- under a heading that promised anonymity; only the email was anonymised. Prefer `.login` for both fields, make an explicit real identity the recorded opt-in direction, and flip the `{{author}}` interview default from a personal name to the handle. Refs #26 Co-Authored-By: Claude Fable 5 --- plugins/repokit/skills/new-repo/SKILL.md | 24 ++++++++++++------- .../new-repo/references/placeholders.md | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/repokit/skills/new-repo/SKILL.md b/plugins/repokit/skills/new-repo/SKILL.md index 1799bdb..34fd8fd 100644 --- a/plugins/repokit/skills/new-repo/SKILL.md +++ b/plugins/repokit/skills/new-repo/SKILL.md @@ -29,7 +29,11 @@ Gather these from the user's arguments / message, else ask — keep it to load-b - **type** — one of: `powershell-module`, `docker-compose`, `power-platform-connectors`, `skill-plugin`, `collection`, `mcp-server`, `app-ts`, `app-python`, `script-collection`. - **visibility** — `private` (= Core tier), `public` (= +Public), or `published` (= +Published). -- **author** — default `Peter Braun` (`PBNZ`). +- **author** — the identity stamped wherever an author/owner/copyright value is written. Default: + the **GitHub handle** of the gh-authenticated user (`gh api user --jq .login`; no `gh` → ask). + A real personal name only when the user explicitly chooses one for this repo — that choice is a + declared variance: record it in ADR-0001 and give it a START-HERE row (see the standard's + *Author identity*). - **license** — default `Apache-2.0`. - **living-docs add-on** — yes/no, default **no**. Ask: *"Will this repo's docs track live operational state (deployed resources, scheduled jobs, long-running migrations)?"* If yes, the @@ -88,16 +92,20 @@ want to continue. 5. **Initialise git** in the new repo directory: - **Default branch `main`, never `master`:** `git init -b main` (`-b` needs git >= 2.28; if it errors, run `git init` then `git branch -m main`). - - **Commit identity -- anonymous by default.** So a repo that later goes public never leaks a - personal email, set a **repo-local** (not `--global`) identity using the GitHub *noreply* - address. Resolve it for the gh-authenticated user and apply it locally: + - **Commit identity -- the handle + noreply, anonymous by default.** So a repo that later goes + public never leaks a real name *or* a personal email, set a **repo-local** (not `--global`) + identity from the GitHub **handle** and the GitHub *noreply* address -- both fields, not just + the email (a leaked file can be edited; a leaked commit identity survives until a history + rewrite). Resolve them for the gh-authenticated user and apply locally: ``` - gh api user --jq '"\(.name // .login)\t\(.id)+\(.login)@users.noreply.github.com"' - git config user.name "" + gh api user --jq '"\(.login)\t\(.id)+\(.login)@users.noreply.github.com"' + git config user.name "" git config user.email "+@users.noreply.github.com" ``` - Use a **real** name/email only if the user explicitly asked (you may ask, but the default is the - noreply address). No `gh` available? Fall back to `@users.noreply.github.com`, or ask. + Use a **real** name/email only if the user explicitly asked -- you may ask, but the default + for both fields is the handle + noreply, and an explicit real identity is recorded in + ADR-0001 (see the standard's *Author identity*). No `gh` available? Ask for the handle and + fall back to `@users.noreply.github.com`. - **Stage + commit:** `git add -A -f` -- the `-f` force-adds the stamped files past any global gitignore (e.g. a `*private*` rule that would silently drop a `Private/` dir; safe here, the tree holds only what you stamped, and the repo's own `.gitignore` governs later additions) -- then one diff --git a/plugins/repokit/skills/new-repo/references/placeholders.md b/plugins/repokit/skills/new-repo/references/placeholders.md index 42f9d9f..620dbef 100644 --- a/plugins/repokit/skills/new-repo/references/placeholders.md +++ b/plugins/repokit/skills/new-repo/references/placeholders.md @@ -8,7 +8,7 @@ |-------|---------| | `{{name}}` | repo / directory name (kebab-case) | | `{{description}}` | one-line description | -| `{{author}}` | author name (default `Peter Braun`) | +| `{{author}}` | author identity (default: the GitHub **handle**, `gh api user --jq .login`; a real personal name only on the user's explicit request — see the standard's *Author identity*) | | `{{license}}` | SPDX licence id (default `Apache-2.0`) | | `{{type}}` | the chosen repo type | | `{{tier}}` | resolved tier label (`Core`, `Core + Public`, or `Core + Public + Published`) | From 839d59f5c59ff78f2bbc3c12d10b2c21d6c87462 Mon Sep 17 00:00:00 2001 From: PBNZ <93853627+PBNZ@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:34:02 +1200 Subject: [PATCH 2/4] feat(standard): state the author-identity convention and enforce it The handle-over-real-name expectation becomes a stated convention: a new "Author identity" section in the-standard.md (with the opt-in path -- a real name is a per-repo declared variance, recorded in an ADR and a START-HERE row), the commit-identity section covers both fields, and the pre-commit checklist extends the contact-info item to names. Reintroduction is now caught, not review-dependent: repokit-check.ps1 gains check 6 -- repo-local git identity and commit history (bounded by the adoption marker) must be handle + noreply unless the variance row is declared -- with smoke coverage for the pass/fail/variance cases. The optional bracketed-grep tree guard is documented with its trade-off (the pattern spells the name) rather than stamped into scaffolds. Also stamps the licence copyright line from {{year}}/{{author}} instead of a hardcoded handle, adds the identity ground rule to the scaffolded AGENTS.md, and allowlists GitHub's own web-flow service address in the private-contact scan. Refs #25 Co-Authored-By: Claude Fable 5 --- .../new-repo/templates/core/AGENTS.md.tmpl | 3 + .../templates/core/scripts/repokit-check.ps1 | 70 +++++++++++++++++++ .../public/{LICENSE => LICENSE.tmpl} | 2 +- .../standard/commit-conventions.md | 12 ++-- .../standard/pre-commit-checklist.md | 4 +- .../repo-standard/standard/the-standard.md | 43 ++++++++++++ scripts/check_no_private_contact.py | 4 +- scripts/smoke_test_repokit_check.ps1 | 32 ++++++++- 8 files changed, 162 insertions(+), 8 deletions(-) rename plugins/repokit/skills/new-repo/templates/public/{LICENSE => LICENSE.tmpl} (99%) diff --git a/plugins/repokit/skills/new-repo/templates/core/AGENTS.md.tmpl b/plugins/repokit/skills/new-repo/templates/core/AGENTS.md.tmpl index a7ad47b..a7daa82 100644 --- a/plugins/repokit/skills/new-repo/templates/core/AGENTS.md.tmpl +++ b/plugins/repokit/skills/new-repo/templates/core/AGENTS.md.tmpl @@ -17,6 +17,9 @@ checklists, use the `repo-standard` skill (RepoKit: `/plugin marketplace add PBN reads `CLAUDE.md`, not `AGENTS.md`). - Conventional Commits; one concern per PR. Record notable decisions as ADRs in `docs/adr/`. - Ceremony scales by visibility — this repo is at the **{{tier}}** tier. +- The author is the **GitHub handle** everywhere, including the repo-local commit identity + `/new-repo` configured — don't override it with a real name/email unless this repo declares + that variance (the `repo-standard` skill's `standard/the-standard.md`, *Author identity*). - Humans + agents sharing an issue board: follow the `repo-standard` skill's `standard/agent-collaboration.md` — cards move when state changes, run the session preflight before board work, sign agent-authored output. diff --git a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 index 1d22b5e..2585bcf 100644 --- a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 +++ b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 @@ -10,6 +10,9 @@ # 4. An ADR directory (or declared substitute) exists. # 5. A resume-state row exists in the START-HERE map (docs/CHECKPOINT.md or a declared # substitute) — the row is mandatory. +# 6. The commit identity is the GitHub handle + noreply email (see the standard, "Author +# identity") — repo-local config and history since the adoption marker — unless the +# START-HERE map declares an author-identity variance. # # Usage: pwsh scripts/repokit-check.ps1 [-RepoRoot ] # Exit code: 0 = compliant, 1 = one or more failures. @@ -118,6 +121,73 @@ if ($resumeRows.Count -gt 0) { Fail 'No resume-state row in the START-HERE map (docs/CHECKPOINT.md or a declared substitute is required at Core)' } +# --- 6. Author identity (privacy default) --------------------------------------------------- +# The standard ("Author identity"): the author is the GitHub handle + noreply email in both +# identity fields. A declared variance row switches the check off; the adoption marker bounds +# the history scan; repos without git (or with no commits yet) skip cleanly. +function Test-AnonymousIdentity([string]$Name, [string]$Email) { + if ($Email -eq 'noreply@github.com') { return $true } # GitHub's own web-flow identity + if ($Email -match '^(\d+\+)?(?[^@]+)@users\.noreply\.github\.com$') { + return ($Name -eq $Matches['login']) # the handle, nothing else + } + return $false +} + +if ((Get-MapRows '(?i)author\s+identity').Count -gt 0) { + Pass 'Author-identity variance declared in the START-HERE map - identity check skipped' +} elseif (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Warn 'git not available - author-identity check skipped' +} else { + git -C $RepoRoot rev-parse --git-dir *> $null + if ($LASTEXITCODE -ne 0) { + Warn 'Not a git repository - author-identity check skipped' + } else { + # 6a. The configured repo-local identity — what the *next* commit will use. + $cfgName = git -C $RepoRoot config --local --get user.name + $cfgEmail = git -C $RepoRoot config --local --get user.email + if ($cfgName -or $cfgEmail) { + if (Test-AnonymousIdentity $cfgName $cfgEmail) { + Pass "Repo-local git identity is handle + noreply ($cfgName)" + } else { + Fail "Repo-local git identity is not handle + noreply: $cfgName <$cfgEmail> - fix it, or declare an author-identity variance row" + } + } + # 6b. Identities already in history, bounded by the adoption marker when one exists. + $range = 'HEAD' + $agentsRaw = '' + if (Test-Path -LiteralPath $agentsPath -PathType Leaf) { + $agentsRaw = Get-Content -LiteralPath $agentsPath -Raw + } + if ($agentsRaw -match 'RepoKit adopted:[^\r\n]*?`([0-9a-fA-F]{7,40})`') { + $adoptSha = $Matches[1] + git -C $RepoRoot rev-parse --verify --quiet "$adoptSha^{commit}" *> $null + if ($LASTEXITCODE -eq 0) { + $range = "$adoptSha..HEAD" + } else { + Warn "Adoption marker commit $adoptSha not in this clone (shallow?) - scanning the history that is available" + } + } + $idLines = @(git -C $RepoRoot log $range --format='%an%x09%ae%x09%cn%x09%ce' 2>$null) + if ($LASTEXITCODE -ne 0) { + Warn 'No commits yet - history identity check skipped' + } else { + $bad = [System.Collections.Generic.HashSet[string]]::new() + foreach ($line in $idLines) { + $an, $ae, $cn, $ce = $line -split "`t" + if (-not (Test-AnonymousIdentity $an $ae)) { [void]$bad.Add("$an <$ae>") } + if (-not (Test-AnonymousIdentity $cn $ce)) { [void]$bad.Add("$cn <$ce>") } + } + if ($bad.Count -eq 0) { + Pass "Commit identities are handle + noreply ($($idLines.Count) commit(s) scanned)" + } else { + foreach ($id in $bad) { + Fail "Commit identity is not handle + noreply: $id - see the standard, 'Author identity' (a real identity needs a declared variance row)" + } + } + } + } +} + # --- Summary ------------------------------------------------------------------------------- if ($script:failures -gt 0) { Write-Host "repokit-check: $($script:failures) failure(s)" diff --git a/plugins/repokit/skills/new-repo/templates/public/LICENSE b/plugins/repokit/skills/new-repo/templates/public/LICENSE.tmpl similarity index 99% rename from plugins/repokit/skills/new-repo/templates/public/LICENSE rename to plugins/repokit/skills/new-repo/templates/public/LICENSE.tmpl index b62079c..e2e3a03 100644 --- a/plugins/repokit/skills/new-repo/templates/public/LICENSE +++ b/plugins/repokit/skills/new-repo/templates/public/LICENSE.tmpl @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 PBNZ + Copyright {{year}} {{author}} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/plugins/repokit/skills/repo-standard/standard/commit-conventions.md b/plugins/repokit/skills/repo-standard/standard/commit-conventions.md index 1674de7..008196b 100644 --- a/plugins/repokit/skills/repo-standard/standard/commit-conventions.md +++ b/plugins/repokit/skills/repo-standard/standard/commit-conventions.md @@ -127,7 +127,11 @@ Optional CI nudge (warn, not fail — copy into any workflow): ## Commit identity -Commits use the GitHub **noreply** email (`+@users.noreply.github.com`), set -**repo-locally**, so a repo that goes public never exposes a personal address. Use a real -name/email only when the user explicitly wants it. `/new-repo` sets this on the scaffold commit; keep -it for later commits too. +Commits are authored as the GitHub **handle** with the GitHub **noreply** email +(`login `), set **repo-locally**, so a repo that goes public +never exposes a real name or a personal address. The name is the half that gets missed: an email +reads as obviously an address, but `user.name` set to a real name leaks just as permanently — +a commit identity leaves history only via a rewrite. Use a real name/email only when the user +explicitly wants it — a declared variance (see [`the-standard.md`](the-standard.md), *Author +identity*). `/new-repo` sets this on the scaffold commit; keep it for later commits too — +`scripts/repokit-check.ps1` verifies both fields. diff --git a/plugins/repokit/skills/repo-standard/standard/pre-commit-checklist.md b/plugins/repokit/skills/repo-standard/standard/pre-commit-checklist.md index 3ce6c89..4770fe7 100644 --- a/plugins/repokit/skills/repo-standard/standard/pre-commit-checklist.md +++ b/plugins/repokit/skills/repo-standard/standard/pre-commit-checklist.md @@ -5,7 +5,9 @@ Run through this before every commit. - [ ] **Builds / lints clean** for this repo type (see `testing-matrix.md`). - [ ] **Tests pass** for the code you touched. - [ ] **Every changed line traces to the task.** No drive-by edits; surgical changes only. -- [ ] **No secrets, tokens, or private contact info** (no email addresses) in the diff. +- [ ] **No secrets, tokens, or private identity** in the diff — no email addresses, no real + personal names: the author is the GitHub handle unless this repo declares otherwise (see + `the-standard.md`, *Author identity*). - [ ] **`CHANGELOG.md` updated** under `## [Unreleased]` if the change is user-visible. - [ ] **Docs move together** *(living-docs repos)* — if the change alters anything a doc states (status, resources, counts, dates), update `docs/STATE.json` in the same commit, run diff --git a/plugins/repokit/skills/repo-standard/standard/the-standard.md b/plugins/repokit/skills/repo-standard/standard/the-standard.md index 4c21b8a..c2fa315 100644 --- a/plugins/repokit/skills/repo-standard/standard/the-standard.md +++ b/plugins/repokit/skills/repo-standard/standard/the-standard.md @@ -140,6 +140,47 @@ many of these names are read by tools that require an exact case. follow the language/ecosystem idiom; never rename a convention-bearing file just to make the tree look uniform. +## Author identity + +The **GitHub handle** (account or org login) identifies the author everywhere an +author/owner/copyright value is written: the git commit identity, module/package metadata (a +`.psd1` `Author` field, an npm `author`), the licence copyright line, ADRs, docs. Commits pair +the handle with the GitHub **noreply** email, set repo-locally — see +[`commit-conventions.md`](commit-conventions.md) (*Commit identity*). + +Why: the promotion path is private-now/public-later, so by the time visibility flips there must +be nothing personal left to scrub. File contents can be edited out; a real name in commit +history is permanent short of a rewrite and force-push. The cheap moment to get this right is +commit #1 — which is why `/new-repo` defaults every identity value to the handle. + +A real personal name is **opt-in, per repo** — the user's explicit choice (say, legal +attribution on a published package), never a default. That choice is a *declared variance*: +record it in an ADR and add a START-HERE row (e.g. "Author identity → real name by choice, +ADR-NNNN") so an audit reads it as a decision, not a leak. + +**What catches reintroduction.** `scripts/repokit-check.ps1` fails when the repo-local git +identity, or any commit since the adoption marker, uses a non-noreply email or an author name +that isn't the handle — unless the variance row above is declared. The self-check cannot know +your name, so it cannot catch it in *file contents*; a repo that wants a tree guard adds a CI +grep for the specific name, bracketed so the step never matches its own source, and matching +the first name alone as well as the pair (the case a "First Last" grep misses is +`/home//...` in a runbook): + +```yaml +- name: No real personal name in the tree + run: | + if git grep -Iin -e '[J]ane' -e '[D]oe' -- . ; then + echo "::error::A real personal name appears above. Identify the author by handle." + exit 1 + fi +``` + +State the trade-off when offering this guard: the bracket defeats the grep, not a reader — the +pattern still spells the name. It fits a repo that stays private, or a public one where the +name-to-handle association is already public and the goal is *no new occurrences*. Where the +association itself is the secret, keep the pattern out of the tree (a repository secret works) +and rely on the identity check plus review. + ## Promotion path Private → public → published just **switches on the next layer** over the *same* structure. Moving @@ -193,5 +234,7 @@ RepoKit adopted: 2026-07-19 (``) — history before this commit preda - **Do** keep private repos at Core. **Don't** force public/published governance onto them. - **Do** record notable decisions as ADRs. **Don't** rely on commit messages alone for rationale. - **Do** keep `SKILL.md` files and templates as plain prose — no secrets, no email addresses. +- **Do** identify the author by the GitHub handle — commits, metadata, copyright lines. A real + name is a per-repo opt-in, declared in the START-HERE map (see *Author identity*). - **Do** delete superseded doc content — git keeps the history. **Don't** keep "superseded by v2" annotations or before/after duplicates in living docs (see `living-docs.md`). diff --git a/scripts/check_no_private_contact.py b/scripts/check_no_private_contact.py index 8456860..b619277 100644 --- a/scripts/check_no_private_contact.py +++ b/scripts/check_no_private_contact.py @@ -23,11 +23,13 @@ "LICENSE", } -# Literal strings that are allowed (structured URLs, not personal contacts). +# Literal strings that are allowed (structured URLs and GitHub service +# addresses, not personal contacts). ALLOWED_SUBSTRINGS = ( "apache.org", "noreply.github.com", "users.noreply.github.com", + "noreply@github.com", # GitHub's own web-flow committer identity ) diff --git a/scripts/smoke_test_repokit_check.ps1 b/scripts/smoke_test_repokit_check.ps1 index 320b14e..3b16bf3 100644 --- a/scripts/smoke_test_repokit_check.ps1 +++ b/scripts/smoke_test_repokit_check.ps1 @@ -4,7 +4,9 @@ # # Builds a minimal compliant fixture repo and asserts the check passes, then proves the check # FAILS on each drift case from the retrospective (refs #16): missing shim, non-importing shim, -# broken START-HERE path, missing changelog, missing ADR dir, missing resume-state row. +# broken START-HERE path, missing changelog, missing ADR dir, missing resume-state row — plus +# the author-identity cases (refs #25/#26): handle + noreply passes, a real name or personal +# email fails, and a declared variance row switches the check off. $ErrorActionPreference = 'Stop' @@ -77,6 +79,34 @@ $d = New-Fixture Set-Content (Join-Path $d 'AGENTS.md') (($agentsTemplate -split "`n") -notmatch 'resume state' -join "`n") Assert-Check 'missing resume-state row fails' $d 1 +# --- Author-identity cases (refs #25/#26) — these fixtures are real git repos. --------------- +function New-GitFixture([string]$UserName, [string]$UserEmail) { + $d = New-Fixture + git -C $d init -q -b main + git -C $d config core.autocrlf false # throwaway fixture — silence Windows CRLF warnings + git -C $d config user.name $UserName + git -C $d config user.email $UserEmail + git -C $d config commit.gpgsign false + git -C $d add -A + git -C $d commit -q -m 'chore: fixture' | Out-Null + return $d +} + +# 8. The privacy default passes: handle as the name, noreply as the email. +Assert-Check 'handle + noreply identity passes' (New-GitFixture 'octocat' 'octocat@users.noreply.github.com') 0 + +# 9. A real name with the anonymous email fails — the exact #26 signature (the email hides, +# the name leaks). +Assert-Check 'real-name author fails' (New-GitFixture 'Octo Cat' '583231+octocat@users.noreply.github.com') 1 + +# 10. A personal email fails. (Concatenated so the private-contact scan never matches this file.) +Assert-Check 'personal email fails' (New-GitFixture 'octocat' ('octocat@' + 'example.com')) 1 + +# 11. The same real identity with a declared variance row passes — a decision, not a leak. +$d = New-GitFixture 'Octo Cat' ('octocat@' + 'example.com') +Add-Content (Join-Path $d 'AGENTS.md') '| Author identity | real name by choice (ADR-0001) |' +Assert-Check 'declared author-identity variance passes' $d 0 + if ($script:failed -gt 0) { Write-Host "smoke_test_repokit_check: $script:failed failure(s)"; exit 1 } Write-Host 'smoke_test_repokit_check: all cases passed' # Explicit success exit: the last negative case leaves $LASTEXITCODE = 1, and GitHub's pwsh From 61dd525893d77e744e4be734418e346508e5a000 Mon Sep 17 00:00:00 2001 From: PBNZ <93853627+PBNZ@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:34:55 +1200 Subject: [PATCH 3/4] docs: ADR-0009, changelog and checkpoint for the author-identity batch Refs #25, #26 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 16 ++++++ docs/CHECKPOINT.md | 13 +++-- ...09-author-identity-handle-not-real-name.md | 56 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 docs/adr/0009-author-identity-handle-not-real-name.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e16160..ffe9720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The standard states the **author-identity convention** (#25, ADR-0009): the GitHub handle + identifies the author everywhere an author/owner/copyright value is written; a real personal + name is a per-repo opt-in recorded as an ADR plus a START-HERE variance row. + `repokit-check.ps1` now fails on a non-anonymous repo-local git identity or commit history + (bounded by the adoption marker, switched off by the variance row), and the standard + documents an optional bracketed-grep CI tree guard with its trade-off stated. + +### Fixed + +- `/new-repo` no longer configures the commit identity from the GitHub profile's real name + (#26): the recipe prefers the **handle** (`.login`) for `user.name`, the `{{author}}` + interview default is the handle instead of a personal name, and the licence template's + copyright line is stamped from `{{year}}`/`{{author}}` rather than hardcoded. + ## [0.5.0] - 2026-07-20 ### Added diff --git a/docs/CHECKPOINT.md b/docs/CHECKPOINT.md index ae490f2..655df6f 100644 --- a/docs/CHECKPOINT.md +++ b/docs/CHECKPOINT.md @@ -3,10 +3,13 @@ Resume state: what a fresh session needs to pick this repo up. Keep every line current — the pre-commit checklist has the tripwire, and stale entries are worse than none. -- Last updated: 2026-07-20 -- Status: DONE -- In progress: nothing — board-commissioning batch (#21–#23, PR #24, ADR-0008) merged and - closed; release 0.5.0 cut (board-commissioning standards) -- Next step: paused — nothing pending +- Last updated: 2026-08-12 +- Status: IN PROGRESS +- In progress: two branches ready for local review — `fix/author-identity` (#25 + #26, + ADR-0009: handle-over-real-name defaults, repokit-check identity check) and + `fix/session-end-branch-check` (#28: content-based branch-landed check); owner pushes and + opens the PRs (`Refs #NN`, no closing keywords) +- Next step: review both branches, push, open two PRs, merge, verify, close #25/#26/#28; + `[Unreleased]` then holds a shippable standards batch — cut 0.6.0 One line per fact, current state only — history lives in `CHANGELOG.md` and git. diff --git a/docs/adr/0009-author-identity-handle-not-real-name.md b/docs/adr/0009-author-identity-handle-not-real-name.md new file mode 100644 index 0000000..f956d0c --- /dev/null +++ b/docs/adr/0009-author-identity-handle-not-real-name.md @@ -0,0 +1,56 @@ +# ADR-0009: Author identity — the GitHub handle everywhere, a real name only as a declared variance + +- **Status:** accepted +- **Date:** 2026-08-12 + +## Context + +Auditing a public `/new-repo`-scaffolded repo against 0.5.0 produced two field reports (#25, +#26): the standard's anonymity story was half-built. The commit *email* was anonymised (the +GitHub noreply address), but the recipe under the heading "Commit identity -- anonymous by +default" resolved the commit *name* as `.name // .login` — the real profile name, on any +account that has one. The `{{author}}` interview default was a real personal name, the licence +template hardcoded one specific handle, and nothing — self-check or CI — caught a real name +reintroduced later. The name is the expensive half: file contents can be edited, but a commit +identity leaves history only via a rewrite and force-push, which defeats RepoKit's +private-now/public-later promotion path. + +## Decision + +Privacy-first identity by default; a real name is a per-repo, user-explicit opt-in recorded as +a declared variance. Both issues accepted, landed as one batch (one commit per issue, +`Refs #NN`), with the guard adapted: + +- **The stated convention** — `the-standard.md` gains an *Author identity* section: the GitHub + handle identifies the author everywhere an author/owner/copyright value is written (commit + identity, module metadata, copyright lines, ADRs, docs). The opt-in path reuses the existing + variance mechanism — an ADR records the choice and a START-HERE row declares it — so a real + name reads as a decision, not a leak. `commit-conventions.md` now covers both identity + fields; the pre-commit checklist's contact-info item extends to names. +- **The recipe** — `/new-repo` step 5 prefers `.login` for `user.name` (was `.name // .login`), + and the `{{author}}` interview default is the handle, not a personal name. The licence + copyright line is stamped from `{{year}}`/`{{author}}` instead of a hardcoded value. The + scaffolded `AGENTS.md` tells later sessions to leave the configured identity alone — a + behaviour an agent must be told, not a file it can lint. +- **The guard** — `repokit-check.ps1` gains check 6: the repo-local git identity and every + commit identity since the adoption marker must be handle + noreply (GitHub's own web-flow + identity allowed); the declared variance row switches the check off. This runs wherever the + self-check already runs (locally and via the one-line CI step), so reintroduction no longer + relies on a human spotting it in review. +- **Adapted, not adopted:** #26's tree-grep CI guard (bracketed patterns for a specific name) + is documented in *Author identity* as an optional per-repo recipe rather than stamped into + scaffolds — the bracket defeats the grep, not a reader, so stamping the pattern into every + potentially-public repo would leak by default the very thing it guards. The trade-off and + the secret-based alternative are stated where the recipe is offered. + +Issues are **not** auto-closed — the human verifies each after merge. + +## Consequences + +Scaffolds carry no real personal name in any tier's output, and the commit identity is +anonymous in both fields from commit #1. The self-check grows its first git-history check; +repos without git, with no commits yet, or running from a shallow clone degrade to a warning +rather than a failure. A repo that genuinely wants real-name attribution pays one ADR and one +START-HERE row — the same price as any other declared variance. What remains uncatchable by +machinery is a real name in file contents on a repo that never opted in and never added the +tree guard; that stays with the pre-commit checklist and review. From 756941912326dd8651f023b08bb6b336fe8357e2 Mon Sep 17 00:00:00 2001 From: PBNZ <93853627+PBNZ@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:13:14 +1200 Subject: [PATCH 4/4] fix(standard): web-flow identity requires GitHub's own name (review follow-up) Copilot review on PR #29: noreply@github.com passed with any name, so a real name paired with the web-flow email slipped through -- now only "GitHub " passes, with smoke cases in both directions. Also mirrors the documented git < 2.28 init fallback in the smoke fixture and refreshes the checkpoint to post-push state. Refs #25 Co-Authored-By: Claude Fable 5 --- docs/CHECKPOINT.md | 14 +++++++------- .../templates/core/scripts/repokit-check.ps1 | 4 +++- scripts/smoke_test_repokit_check.ps1 | 8 +++++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/CHECKPOINT.md b/docs/CHECKPOINT.md index 655df6f..01c042f 100644 --- a/docs/CHECKPOINT.md +++ b/docs/CHECKPOINT.md @@ -3,13 +3,13 @@ Resume state: what a fresh session needs to pick this repo up. Keep every line current — the pre-commit checklist has the tripwire, and stale entries are worse than none. -- Last updated: 2026-08-12 +- Last updated: 2026-08-13 - Status: IN PROGRESS -- In progress: two branches ready for local review — `fix/author-identity` (#25 + #26, - ADR-0009: handle-over-real-name defaults, repokit-check identity check) and - `fix/session-end-branch-check` (#28: content-based branch-landed check); owner pushes and - opens the PRs (`Refs #NN`, no closing keywords) -- Next step: review both branches, push, open two PRs, merge, verify, close #25/#26/#28; - `[Unreleased]` then holds a shippable standards batch — cut 0.6.0 +- In progress: standards batch in review — [PR #29](https://github.com/PBNZ/repo-kit/pull/29) + (#25 + #26, ADR-0009: handle-over-real-name defaults, repokit-check identity check) and + [PR #30](https://github.com/PBNZ/repo-kit/pull/30) (#28: content-based branch-landed check); + CI green, Copilot review addressed on both +- Next step: owner merges #29 and #30 and closes #25/#26/#28 after verifying; then cut 0.6.0 — + rename `[Unreleased]`, bump `plugin.json` + `marketplace.json`, tag `v0.6.0`, push One line per fact, current state only — history lives in `CHANGELOG.md` and git. diff --git a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 index 2585bcf..d94b30e 100644 --- a/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 +++ b/plugins/repokit/skills/new-repo/templates/core/scripts/repokit-check.ps1 @@ -126,7 +126,9 @@ if ($resumeRows.Count -gt 0) { # identity fields. A declared variance row switches the check off; the adoption marker bounds # the history scan; repos without git (or with no commits yet) skip cleanly. function Test-AnonymousIdentity([string]$Name, [string]$Email) { - if ($Email -eq 'noreply@github.com') { return $true } # GitHub's own web-flow identity + # GitHub's own web-flow identity is exactly "GitHub " — a real name + # paired with that email must not slip through. + if ($Email -eq 'noreply@github.com') { return ($Name -eq 'GitHub') } if ($Email -match '^(\d+\+)?(?[^@]+)@users\.noreply\.github\.com$') { return ($Name -eq $Matches['login']) # the handle, nothing else } diff --git a/scripts/smoke_test_repokit_check.ps1 b/scripts/smoke_test_repokit_check.ps1 index 3b16bf3..0eab61f 100644 --- a/scripts/smoke_test_repokit_check.ps1 +++ b/scripts/smoke_test_repokit_check.ps1 @@ -82,7 +82,8 @@ Assert-Check 'missing resume-state row fails' $d 1 # --- Author-identity cases (refs #25/#26) — these fixtures are real git repos. --------------- function New-GitFixture([string]$UserName, [string]$UserEmail) { $d = New-Fixture - git -C $d init -q -b main + git -C $d init -q -b main 2>$null + if ($LASTEXITCODE -ne 0) { git -C $d init -q; git -C $d branch -m main } # git < 2.28 git -C $d config core.autocrlf false # throwaway fixture — silence Windows CRLF warnings git -C $d config user.name $UserName git -C $d config user.email $UserEmail @@ -107,6 +108,11 @@ $d = New-GitFixture 'Octo Cat' ('octocat@' + 'example.com') Add-Content (Join-Path $d 'AGENTS.md') '| Author identity | real name by choice (ADR-0001) |' Assert-Check 'declared author-identity variance passes' $d 0 +# 12. GitHub's own web-flow identity passes — but only with GitHub's own name (13): a real +# name paired with the web-flow email must not slip through. +Assert-Check 'web-flow identity passes' (New-GitFixture 'GitHub' 'noreply@github.com') 0 +Assert-Check 'real name on the web-flow email fails' (New-GitFixture 'Octo Cat' 'noreply@github.com') 1 + if ($script:failed -gt 0) { Write-Host "smoke_test_repokit_check: $script:failed failure(s)"; exit 1 } Write-Host 'smoke_test_repokit_check: all cases passed' # Explicit success exit: the last negative case leaves $LASTEXITCODE = 1, and GitHub's pwsh