Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions docs/CHECKPOINT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-13
- Status: IN PROGRESS
- 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.
56 changes: 56 additions & 0 deletions docs/adr/0009-author-identity-handle-not-real-name.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 16 additions & 8 deletions plugins/repokit/skills/new-repo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "<name-or-login>"
gh api user --jq '"\(.login)\t\(.id)+\(.login)@users.noreply.github.com"'
git config user.name "<login>"
git config user.email "<id>+<login>@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 `<login>@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 `<login>@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
Expand Down
2 changes: 1 addition & 1 deletion plugins/repokit/skills/new-repo/references/placeholders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down
3 changes: 3 additions & 0 deletions plugins/repokit/skills/new-repo/templates/core/AGENTS.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>]
# Exit code: 0 = compliant, 1 = one or more failures.
Expand Down Expand Up @@ -118,6 +121,75 @@ 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) {
# GitHub's own web-flow identity is exactly "GitHub <noreply@github.com>" — 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+\+)?(?<login>[^@]+)@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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ Optional CI nudge (warn, not fail — copy into any workflow):

## Commit identity

Commits use the GitHub **noreply** email (`<id>+<login>@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 <id+login@users.noreply.github.com>`), 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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading