diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 817a4729..97e880d6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -354,6 +354,11 @@ After the final push, sweep-resolve stale older threads for removed code paths. - **Proved against** - the 25 most recent merged pull requests as of `develop` at `756a53e`, the corpus on which the anchored verb form that ships instead raises one reference, and that one true. - **Delete when** - `claims` stops reading a description for commit references. +- **"The agent check branches" in `STANDUP.md` section 0 is a subject-verb disagreement, and should read "The agent checks branches".** Raised as a suppressed finding against a line the change under review only touched as diff context. + - **Disproved by** - reading the sentence against the snippet it describes. The subject is the noun phrase "the agent check", meaning the check for the signing agent, and "branches" is its verb, which is what the `if [ ... = ssh ]; then ssh-add -L; else gpg --list-secret-keys; fi` line does. The proposed reading needs "branches" as a plural noun, and the paragraph is section 0, before a repository exists, where the alternatives it names are the SSH and GPG forms rather than refs. + - **Proved against** - the paragraph following the agent snippet in `STANDUP.md` section 0 on `develop` at `676a2bd`, unchanged since `77be3a3`. + - **Delete when** - the sentence is reworded for any reason, since the entry is about this phrasing rather than about the rule it states. + ## When in Doubt Read [AGENTS.md](../AGENTS.md) to find the section that governs your change, and [GOVERNANCE.md](../GOVERNANCE.md) for the rule text itself. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the relevant language section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions, and keep those focused on the change itself. diff --git a/STANDUP.md b/STANDUP.md index 7c5dd1a6..3300dbb9 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -24,15 +24,15 @@ if [ "$(git config --global --get gpg.format)" = ssh ]; then ssh-add -L; else gp The agent check branches rather than listing both forms, because they are alternatives and running the wrong one fails on a correctly configured host: an SSH host need not have `gpg` installed at all. Signing is **SSH or GPG**, so judge the format and its agent together rather than requiring `ssh`: what matters is that the configured format has a matching agent holding the key, which is the check [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] prescribes. Any of these wrong or absent is a **host** misconfiguration to surface to the maintainer ([`docs/host-setup.md`][host-setup] is the setup procedure), not something to patch per repo. Patching it locally hides a broken host that then produces wrong identities in every other repo on that machine. -After `git init` and before the first commit, confirm the repo added no override of its own. This one needs a repository, since `--local` fails outside one: +After `git init` and before the first commit, confirm the repo added no override of its own. This one needs a repository, since `--local` fails outside one. Read it here and run it in section 0B, which places it between the init and the first commit, so nothing here is a prompt to init early: ```shell git config --local --get user.email || true # expect no output ``` -**The finding is a printed value, never the exit code.** An unset key prints nothing and exits `1`, so the passing case is a non-zero exit with empty output, and reading the exit status as failure inverts the check. The tolerant tail is in the snippet above so a copy into a `set -e` script does not abort on the expected case. +**The finding is a printed value, never the exit code.** An unset key prints nothing and makes `git config --get` exit `1`, so reading that as failure inverts the check, and the tolerant tail turns it into a zero exit in any case, which leaves empty output as the whole of the passing result. The tail is in the snippet above so a copy into a `set -e` script does not abort on the expected case. -After the first commit, confirm it took with `git log -1 --format='%G? author=%an <%ae> committer=%cn <%ce>'`, so the passing result is `G` plus the expected `noreply` address in **both** identities. Read both rather than the author alone: the rule governs the `author` and the `committer` together, GitHub verifies the signature against the **committer**, and a rebase, amend, or cherry-pick rewrites the committer while leaving the author untouched, which is exactly the case an author-only check passes and should not. `git verify-commit HEAD` is the pass/fail form, exiting non-zero on a bad signature and writing its "Good signature" line to stderr rather than emitting a status letter. +After the first commit, confirm it took with `git log -1 --format='%G? author=%an <%ae> committer=%cn <%ce>'`, so the passing result is `G` plus the expected `noreply` address in **both** identities. Read both rather than the author alone: the rule governs the `author` and the `committer` together, GitHub verifies the signature against the **committer**, and a rebase, amend, or cherry-pick rewrites the committer while leaving the author untouched, which is exactly the case an author-only check passes and should not. `git verify-commit HEAD` is the pass/fail form, exiting non-zero on a bad signature and writing its "Good signature" line to stderr rather than emitting a status letter. Section 0B's block runs this line too, in the position described here, which is the same split as the check above. ## 0A. Hand Over What Only the Maintainer Can Supply @@ -47,6 +47,36 @@ After the first commit, confirm it took with `git log -1 --format='%G? author=%a **Escalate a blocking prerequisite the moment it is found, rather than carrying it.** In a task list a pending task and a blocking prerequisite look identical, and the second quietly becomes the first as work continues around it. Stop at the step that needs the missing input and say which input it is. +## 0B. Create the Branches, Before the First Standup Commit + +**Create both long-lived branches empty and do the whole standup on a feature branch off `develop`.** An agent that starts committing onto whichever branch `git init` produced is writing the repo's permanent history, and every step below is exploratory work that has no business being permanent. Doing this first means nothing ever has to be cleaned off `main` or `develop`, because the only commit either carries is the empty one it starts from and every change after it arrives by pull request. + +1. **Create `main` carrying nothing.** A git branch cannot exist without a commit, so carrying nothing means exactly one signed empty root commit, and section 0's signing window applies to it like any other. +2. **Create `develop` from `main`**, also carrying nothing, so the two start level. +3. **Create the first feature branch from `develop`** and run every step below on it, through the audit in step 5. +4. **Add the repository on GitHub and apply the configuration while still on that branch**, which is step 4 and needs no branch of its own. +5. **Open a normal pull request into `develop`** when the standup is done, and let it squash like any other change. + +Steps 1 through 3 are the block below, run before the repository exists on GitHub. It carries the procedure's only `git init`, so section 0 is read as its `--global` checks running ahead of this block and its two repository-scoped checks sitting inside it, rather than as an init of its own to run first. + +```shell +git init # The host default may be master, which the rename below corrects. +git config --local --get user.email || true # Section 0's override check, whose passing case is no output. +git commit --allow-empty --message "Initial commit" # The one signed empty root commit. +git log -1 --format='%G? author=%an <%ae> committer=%cn <%ce>' # Section 0's verification of that commit. +git branch --move --force main # Renames whichever branch git init produced, in a repo holding only it. +git branch develop # From main, so the two start level. +git checkout -b "" develop # Every step below runs here. +``` + +The rename runs unconditionally rather than behind a test of `init.defaultBranch`, because forcing it is correct whether the host produced `master` or `main` and a conditional is one more thing to get wrong. What makes the force safe is where the block runs, in a repository holding one branch and one commit, so it is a rename with nothing to collide with rather than a general-purpose one. In a repository that already carries a `main` the same line overwrites that branch, which is why this block belongs to a fresh `git init` and never to a repair. Every command here predates `git switch` and `git init --initial-branch`, so the block needs no minimum version, which matters because [`docs/host-setup.md`][host-setup] checks that `git` is present and states no floor. The placeholder is quoted for the reason step 4 gives, that an unquoted `<` is input redirection. Both of section 0's checks sit in the block rather than beside it, placed where that section requires them rather than left to a reader to interpolate, and each is read as section 0 reads it. On the second line the finding is the printed value and never the exit status, so the passing case is empty output. Reading `$?` there answers nothing, because `git config --get` exits non-zero on the unset key that is the wanted result and the tolerant tail then returns zero regardless, which is what keeps a copy of the block inside a `set -e` script from aborting on the expected case. On the fourth the passing result is `G` beside the noreply address in the author and the committer both, which reads the empty root commit as the first commit the signing window covers rather than as an exception to it. Push `main` and `develop` once the repository exists and **before** step 4 applies the rulesets, since [`repo-config/main.json`][repo-config-main] carries a `pull_request` rule, so an applied ruleset blocks the direct push that would otherwise seed the branch. Ordering it this way rather than relying on a bypass is deliberate, because who may bypass a ruleset is a human decision no payload declares, and `repo-config/configure.sh` reads the live list and preserves it rather than asserting one, so an agent cannot know from the payload whether a bypass exists. + +**Committing onto `develop` and squashing afterwards does not work**, because `non_fast_forward` is set on both `develop` payloads and rewriting that history is exactly what the rule rejects. This is not hypothetical, since a repo stood up that way was correctly blocked at the point the history needed rewriting, with the standup already written into the branch it had to be lifted off. + +**The protection is uneven, so on an operational repo this instruction is the only thing holding the line.** A release repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries three rules, `deletion`, `non_fast_forward` and `required_signatures`, and none of them stops one. A conformant operational repo therefore accepts the commit that this step exists to prevent, and reports nothing wrong afterwards. + +**On a public repo the squash is the one chance to leave the exploratory history out.** Standup is where a wrong secret value, a throwaway credential, and a run of noise commits accumulate, and a squashed feature branch publishes the result rather than the route to it. + ## 1. Classify and Catalog Resolve the repo's type(s) with the [`AUDIT.md`][audit] section 2 detection rules, then write or repair its [`registry/repos.json`][repos] entry: `status`, `types[]`, `groundTruthBranch`, `hasDevelop`, `publish[]`, `requiredSecrets[]`, `consumerModel`, `releaseTrigger`, `workflowModel` (omit to take the `release` default), `configLayout`, and `driftNotes` that describe what the repo **actually is**. Run [`spec/validate.py`][validate] to confirm it classifies cleanly. The registry is ground truth about reality, not intent, and a `validate.py`-clean entry is still false if it disagrees with the live repo. @@ -163,6 +193,7 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and [readme-structure]: ./spec/readme-structure.md [repo-config]: ./repo-config/ [repo-config-carry]: ./docs/repo-config-carry.md +[repo-config-main]: ./repo-config/main.json [repo-config-readme]: ./repo-config/README.md [repos]: ./registry/repos.json [repos-schema]: ./registry/repos.schema.json diff --git a/TODO.md b/TODO.md index 7a1ef0bb..945b0ea7 100644 --- a/TODO.md +++ b/TODO.md @@ -172,23 +172,6 @@ One pull request reworking the README spec to the hand-crafted PlexCleaner shape - **Open** - The position of the sections the spec already names, since PlexCleaner places Questions or Issues immediately after the Table of Contents where the spec orders it ninth. - **Settled** - PlexCleaner's Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas and Sample Media Files are correctly repo-specific under the recurrence rule in [`spec/section-model.md`][section-model] and stay undeclared. -### Branch Bootstrap in the Standup - -One pull request giving [`STANDUP.md`][standup] the branch-creation step it lacks, so an agent stops committing onto whichever branch it finds. - -**State** `ready`. **Touches** [`STANDUP.md`][standup]. **Cost** one hub edit, hub-only, since the file is deliberately not carried. - -- **State the sequence that avoids every cleanup problem.** Nothing ever has to be cleaned off `main` or `develop`, because nothing reaches them without review. - - **Blocked by** - Nothing. - - **Issue** - [#490][issue-490] covered the human-gated half and is complete, so this is the remaining half rather than the whole gap. - - **Checked** - `develop` at `1ed0cc8` on 2026-08-03, where section 0A covers the prerequisites and nothing says how the branches come into being. - - **Open** - Nothing. - - **Settled** - The sequence is create `main` carrying nothing, create `develop` from it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the config while still on that branch, then open a normal pull request to `develop`. - - **Settled** - A git branch cannot exist without a commit, so carrying nothing means exactly one signed empty root commit, and section 0's signing window applies to it like any other. - - **Settled** - Committing onto `develop` and squashing afterwards does not work, since `non_fast_forward` is set on both `develop` payloads, and Blog was correctly blocked when it tried. - - **Settled** - The protection is uneven, since a `release` repo's `develop` payload carries a `pull_request` rule that blocks a direct commit outright while the operational payload carries only three rules, so on an operational repo only the instruction stands between the agent and an unfixable history. - - **Settled** - It matters on a public repo because the exploratory standup history is where secrets and noise commits accumulate, and squashing the feature branch is the one chance to leave them out. - ### Two Project Types and a Shared C++ Style One pull request extending the type model with the two types the fleet already needs, plus the shared style the `cpp` type has no canonical for. @@ -462,7 +445,6 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [issue-456]: https://github.com/ptr727/ProjectTemplate/issues/456 [issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 [issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489 -[issue-490]: https://github.com/ptr727/ProjectTemplate/issues/490 [issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509 [issue-519]: https://github.com/ptr727/ProjectTemplate/issues/519 [issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521