From 3bb8896502b2a9c81f778874bde5ffc4d1f7ae2b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 16 Oct 2026 17:50:36 -0700 Subject: [PATCH 1/3] Refer to an Enumeration Instead of Stating an Incidental Count (#3168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Refs #3160. - Adds `GOVERNANCE.md` "Documentation Style Conventions" > "Incidental Counts". Text refers to an enumeration ("the following checks", "each case", "the rows below") rather than stating its size. The rule says outright that it holds in every fleet repository, for docs, code and workflow comments, and docstrings. A count stays only where the number is what the reader needs, such as a limit, a threshold, or a measured result, and it then names its source. A wrong incidental count is removed rather than corrected. - `comment-and-doc-style` carries the subsection whole as a generated include under "Quantitative claims", and its description gains a trigger for text that states how many items a list holds. - `fleet-code-review` gains one line: a finding that a stated count is wrong suggests removing it where it is incidental. - Sweeps the incidental counts out of `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, the skill sources, and the hub-only `scripts/README.md` and `scripts/handoff.py` docstring, which repeated one of the same sentences. Deliberately left as they are: - Counts that are the contribution: caps, timeouts, limits. - Pairs fixed by construction, such as the workflow models and the `.editorconfig` and `.gitattributes` pair. - Counts an ordinal structure later in the paragraph relies on ("the first two ... the third"). - Counts inside section titles other text links to, which need a heading rename. The rest of the sweep and those renames go to a follow-up issue. `CODESTYLE.md` "General" and `pr-review-conduct` are unchanged, since the `GOVERNANCE.md` section already governs code comments and the rule itself states how a wrong count is fixed. ## Verification - `scripts/build_dist.py --check`, `scripts/prose_lint.py --diff`, `scripts/repo_gate.py --check eol`, `spec/validate.py`: clean. - markdownlint, cspell, and editorconfig-checker through `scripts/docker_lint.py`: clean. - `python3 -m unittest discover -s tests`: 2887 tests OK, 7 skipped. - The opt-in `sentence-length` check reports only sentences that were over the cap before this branch and sit on a touched one-line paragraph. None of them is new wording. - `local-strict-review` ran three recorded passes. The first raised 14 findings and the second raised 1, almost all antecedents left dangling by a removed count. All were fixed, and the final pass raised none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .agents/skills/agent-conduct/SKILL.md | 12 +++---- .agents/skills/audit-a-repo/SKILL.md | 2 +- .agents/skills/backlog-burndown/SKILL.md | 6 ++-- .../branch-protection-and-promotion.md | 2 +- .../references/release-publish-mechanics.md | 4 +-- .../carried-instruction-file-guard/SKILL.md | 2 +- .agents/skills/comment-and-doc-style/SKILL.md | 28 ++++++++++------ .../references/carried-doc-references.md | 2 +- .agents/skills/dotnet-codestyle/SKILL.md | 6 ++-- .agents/skills/fleet-code-review/SKILL.md | 3 ++ .../references/history-rewrite.md | 2 +- .agents/skills/local-strict-review/SKILL.md | 4 +-- .agents/skills/merge-and-release/SKILL.md | 7 ++-- .../python-codestyle/references/code-style.md | 2 +- .agents/skills/repo-worktree/SKILL.md | 6 ++-- .agents/skills/session-handoff/SKILL.md | 24 +++++++------- .agents/skills/unattended-handoff/SKILL.md | 2 +- .agents/skills/workflow-ci-contract/SKILL.md | 2 +- .../references/test-methodology.md | 4 +-- .../.source-digests/agent-conduct | 2 +- .../fleet-skills/.source-digests/audit-a-repo | 2 +- .../.source-digests/backlog-burndown | 2 +- .../branching-and-release-model | 2 +- .../carried-instruction-file-guard | 2 +- .../.source-digests/comment-and-doc-style | 2 +- .../.source-digests/dotnet-codestyle | 2 +- .../.source-digests/fleet-code-review | 2 +- .../.source-digests/git-commit-conventions | 2 +- .../.source-digests/local-strict-review | 2 +- .../.source-digests/merge-and-release | 2 +- .../.source-digests/python-codestyle | 2 +- .../.source-digests/repo-worktree | 2 +- .../.source-digests/session-handoff | 2 +- .../.source-digests/unattended-handoff | 2 +- .../.source-digests/workflow-ci-contract | 2 +- .../skills/agent-conduct/SKILL.md | 12 +++---- .../fleet-skills/skills/audit-a-repo/SKILL.md | 2 +- .../skills/backlog-burndown/SKILL.md | 6 ++-- .../branch-protection-and-promotion.md | 2 +- .../references/release-publish-mechanics.md | 4 +-- .../carried-instruction-file-guard/SKILL.md | 2 +- .../skills/comment-and-doc-style/SKILL.md | 28 ++++++++++------ .../references/carried-doc-references.md | 2 +- .../skills/dotnet-codestyle/SKILL.md | 6 ++-- .../skills/fleet-code-review/SKILL.md | 3 ++ .../references/history-rewrite.md | 2 +- .../skills/local-strict-review/SKILL.md | 4 +-- .../skills/merge-and-release/SKILL.md | 7 ++-- .../python-codestyle/references/code-style.md | 2 +- .../skills/repo-worktree/SKILL.md | 6 ++-- .../skills/session-handoff/SKILL.md | 24 +++++++------- .../skills/unattended-handoff/SKILL.md | 2 +- .../skills/workflow-ci-contract/SKILL.md | 2 +- .../references/test-methodology.md | 4 +-- .github/skills/agent-conduct/SKILL.md | 12 +++---- .github/skills/audit-a-repo/SKILL.md | 2 +- .github/skills/backlog-burndown/SKILL.md | 6 ++-- .../branch-protection-and-promotion.md | 2 +- .../references/release-publish-mechanics.md | 4 +-- .../carried-instruction-file-guard/SKILL.md | 2 +- .github/skills/comment-and-doc-style/SKILL.md | 28 ++++++++++------ .../references/carried-doc-references.md | 2 +- .github/skills/dotnet-codestyle/SKILL.md | 6 ++-- .github/skills/fleet-code-review/SKILL.md | 3 ++ .../references/history-rewrite.md | 2 +- .github/skills/local-strict-review/SKILL.md | 4 +-- .github/skills/merge-and-release/SKILL.md | 7 ++-- .../python-codestyle/references/code-style.md | 2 +- .github/skills/repo-worktree/SKILL.md | 6 ++-- .github/skills/session-handoff/SKILL.md | 24 +++++++------- .github/skills/unattended-handoff/SKILL.md | 2 +- .github/skills/workflow-ci-contract/SKILL.md | 2 +- .../references/test-methodology.md | 4 +-- AGENTS.md | 12 +++---- CODESTYLE.md | 4 +-- GOVERNANCE.md | 32 +++++++++++-------- WORKFLOW.md | 18 +++++------ scripts/README.md | 2 +- scripts/handoff.py | 2 +- 79 files changed, 248 insertions(+), 214 deletions(-) diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index 8ec25a370..45c60c1f1 100644 --- a/.agents/skills/agent-conduct/SKILL.md +++ b/.agents/skills/agent-conduct/SKILL.md @@ -16,14 +16,14 @@ description: >- Where a sibling skill owns the moment, it wins: git-commit-conventions for committing, pr-review-conduct for review and merge claims, local-strict-review for the passes a push owes, comment-and-doc-style for prose. The GOVERNANCE.md sections it surfaces keep the full rules, - three of them carried here whole as generated includes. + several of them carried here whole as generated includes. --- # Agent Conduct ## Why This Exists -The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those three sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. +The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. ## Before Designing a Change @@ -102,18 +102,18 @@ Where a lesson lands, when it earns a mechanical hook, and where work blocked on - **A blocker filed in another repository is recorded in the repository whose work it blocks.** The binding moment is the one where the upstream issue is filed or, where it already exists, found, because that session is the one that knows what stopped and why. It owes a second issue in the repository that is waiting rather than only the first, and where the upstream issue already exists the local issue names that one and no second upstream issue is filed. The local issue states what this repository cannot do and why, in its own terms rather than as a pointer to read elsewhere, since a reader who has to open the upstream issue to learn whether it affects them opens every one of them. The local issue names the upstream one as its blocker, carries the `blocked` label, and carries whatever labels its own work would carry anyway. A comment on the upstream issue then names the local one in return, a comment rather than an edit to the body because a second repository may join the same blocker later and because the body is often not this session's to rewrite. That order, the upstream issue and then the local issue and then the backlink comment, leaves a partial failure as a record naming its blocker rather than as a blocker naming a record nobody wrote. **A handoff does not do this job.** It carries the blockers a round met, and it belongs to one track and closes with its successor, where the wait outlives every session that met it and belongs in the backlog the whole repository reads. - **The blocker record is written under the ordinary write rules, and the label on it is taken off deliberately.** Filing an issue and commenting on another are state-changing calls, so "Repository Boundaries and Write Safety" binds each of them exactly as it binds any other write, which keeps the upstream issue inside this owner and makes a blocker under a different owner a matter of explicit permission rather than of this rule. The `blocked` label is what every reader of this record selects on, so a repository not carrying it cannot host one until the fleet label set is applied there, which is a change to that repository's configuration rather than anything this rule writes, and a session that finds it missing reports that. The label comes off when the blocker clears, and the session closing the upstream issue is best placed to take it off, since the backlink comments naming every waiting repository are on the issue it is closing, while any later session that finds it cleared takes it off instead. A fix can land well before either of those, so the label lags the fix rather than tracking it, which is why a session meeting a `blocked` issue reads the state of the issue that issue's body names rather than the label. The local issue stays open when the label comes off, because the work it records still has to be done and is ordinary backlog from that moment on. - **A handoff parked on a maintainer decision carries the `blocked` label too.** Its blocker is a `decision` issue in this same repository rather than an issue elsewhere, and the parking comment on the handoff names it. A reader reads the named issue rather than the label, and the blocker clears when that issue loses its `decision` label. Unlike a cross-repository blocker, a session finding it cleared does not take the label off. It comes off only when a session hands the link back to be worked, so a loop running meanwhile never takes a link a present maintainer is still working. -- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those three conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. +- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** The conditions below must hold together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all of them (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" keeps the full rules, and the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, carries it whole as a generated include and surfaces it at its decision moments. -Two rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. +Some rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. ## When Work Stops on Another Repository -The rules for this moment are two bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. +The rules for this moment are bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. ## Delegation, in One Paragraph -The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. The two that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. +The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. Those that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. diff --git a/.agents/skills/audit-a-repo/SKILL.md b/.agents/skills/audit-a-repo/SKILL.md index e2718ab74..ef69d060e 100644 --- a/.agents/skills/audit-a-repo/SKILL.md +++ b/.agents/skills/audit-a-repo/SKILL.md @@ -8,7 +8,7 @@ description: >- ## Why This Exists -The audit is the fleet's measurement procedure, and the two failure shapes it guards against are both silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. +The audit is the fleet's measurement procedure, and the failure shapes it guards against are all silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. ## Before Measuring Anything diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index 53f93a66e..631c1bc7f 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists Asking for one issue to be fixed is `drive-pr`'s job and needs no skill above it. Asking for a -whole backlog to be worked down is a different problem, and three things about it are not obvious. +whole backlog to be worked down is a different problem, and several things about it are not obvious. Parallelism is bounded by file overlap rather than by agent count, so the grouping decides the throughput. The backlog is not a fixed list, since every review round files deferral issues that belong in the next round's ranking, so a plan made once is stale by its second round. And a @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. Three kinds of group are never tiered +named it for this run. These kinds of group are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds @@ -520,7 +520,7 @@ restores a line another session's write dropped. ## Ending the Run -The run ends at either of two points, and they are different endings. +The run ends at one of the points below, and they are different endings. - **The backlog is worked out**, meaning a full re-rank finds no open issue this skill can act on. That is not the same as zero open issues, since a backlog of nothing but maintainer questions and diff --git a/.agents/skills/branching-and-release-model/references/branch-protection-and-promotion.md b/.agents/skills/branching-and-release-model/references/branch-protection-and-promotion.md index 9ca227699..eae12059a 100644 --- a/.agents/skills/branching-and-release-model/references/branch-protection-and-promotion.md +++ b/.agents/skills/branching-and-release-model/references/branch-protection-and-promotion.md @@ -18,7 +18,7 @@ explicitly for a repository outside the registry. ## Executing a `develop -> main` promotion safely -Two traps, both learned the hard way: +These traps were learned the hard way: - **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` (and a repository's "Automatically delete head branches" toggle, which the fleet keeps off for diff --git a/.agents/skills/branching-and-release-model/references/release-publish-mechanics.md b/.agents/skills/branching-and-release-model/references/release-publish-mechanics.md index b0c3adf48..8b842510e 100644 --- a/.agents/skills/branching-and-release-model/references/release-publish-mechanics.md +++ b/.agents/skills/branching-and-release-model/references/release-publish-mechanics.md @@ -108,7 +108,7 @@ are intentionally not added. ## Recovering a failed registry push -A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the two recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. +A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. **Re-dispatch, available only while the tip has not moved.** A `workflow_dispatch` takes a ref rather than a commit, and D2.3 admits only `main` or `develop`, so what it builds is that branch's tip at dispatch time. While the tip is still the commit whose push failed, a re-dispatch rebuilds the same version and runs its push again, refreshing the release the way any dispatch does. @@ -116,7 +116,7 @@ This is a time-of-check-to-time-of-use race rather than a guarded operation: not **Re-run all jobs, available inside the window whatever the tip has done.** `gh run rerun ` replays the run under the original event's `GITHUB_SHA` and `GITHUB_REF` and re-executes every job rather than only the failed ones. The publisher pins the release task to that commit with `ref: ${{ github.sha }}`, so `get-version` recomputes the same version from the same commit and history, each build leaf checks out the `GitCommitId` that job emits, the package artifact D5.2 deleted is rebuilt and re-uploaded rather than missing when `publish-` downloads it, and that job retries the push it failed. The release itself needs nothing from the re-run, the failed run having already cut it, though on a dispatch-triggered run the re-run re-enters `github-release`, which refreshes the release per D4.4's dispatch leg and runs the `release-asset-*` delete with it per D5.2. A re-dispatch here would build the new tip instead, and NBGV derives the version from git height, so that is a further version and the one whose push failed never reaches the registry. -Three qualifications come with the re-run route. +The qualifications below come with the re-run route. - D4.4 and `WORKFLOW.md` 5B's S9 describe a re-run whose predecessor push **succeeded**, where the registry dedupes the second one. This is the case they do not cover, and its retried push is the first the registry ever receives for that version. - GitHub offers a re-run only within **30 days** of the initial run, and a repository's own **log** retention setting can be shorter, so the usable window is the shorter of the two. This is the run's own retention and is unrelated to D5.4's `retention-days: 1`, which bounds an uploaded artifact rather than the run. diff --git a/.agents/skills/carried-instruction-file-guard/SKILL.md b/.agents/skills/carried-instruction-file-guard/SKILL.md index c61872283..d20852942 100644 --- a/.agents/skills/carried-instruction-file-guard/SKILL.md +++ b/.agents/skills/carried-instruction-file-guard/SKILL.md @@ -21,7 +21,7 @@ description: >- A downstream repo's `AGENTS.md`/`GOVERNANCE.md`/`CODESTYLE.md`/`WORKFLOW.md` can hold two different kinds of content mixed in one file: sections that are stale copies of the hub's fleet-wide rules, and local rules the repo wrote for a fault the fleet has never seen elsewhere. Re-vendoring the hub's canonical version over the whole file deletes the second kind silently, because nothing about the diff looks wrong. This has actually happened: a resync replaced a repo's `AGENTS.md` wholesale with the hub's, and the repo's own local additions were gone with no error, no warning, and no review comment calling it out. -The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these four files, every time, regardless of how routine the request sounds. +The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these files, every time, regardless of how routine the request sounds. ## Before you touch any of these four files diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index 3e556340e..51772e1d6 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -14,7 +14,8 @@ description: >- carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md) is being edited (no coordination references to the template or a sibling repo), and when writing a PR title or commit message (imperative subject, no vague - titles, no unsolicited Co-Authored-By, no release-bump magnitude). + titles, no unsolicited Co-Authored-By, no release-bump magnitude). Also triggers when text + states how many items a list or a table holds, since that count goes stale. --- # Comment and Doc Style @@ -65,7 +66,7 @@ Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.N file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md - cspell line together, all three or none. + cspell line together, all of them or none. - **`HISTORY.md` mirrors the README's opening**: the same `# `, the same tagline verbatim (the first line after the README's H1), then its own `## Release History`. It never repeats a paragraph below the README's tagline. @@ -199,7 +200,7 @@ pull request carries the `comments` label and the gate stands down for that chan and `--allow-comments` does it for one run by hand. The label and the variable are separate deliberately, so a variable left exported reaches the commit and never the merge gate. -Three things about reaching those escapes read as a broken gate until they are known. On a public +The facts below about reaching those escapes read as a broken gate until they are known. On a public repository the lint job reads the label when it runs, and its log says "Read the pull request's labels live". A label added before then applies, and a rerun picks up one added since. In a private repository, or where the log lacks that line, the event's snapshot decides. It misses a label @@ -220,7 +221,7 @@ and the fleet's own rule documents: `AGENTS.md`, `AUDIT.md`, `CLAUDE.md`, `CODES are the repository's own narrative and keep their references, as do a commit message and a pull request body, which are the surfaces a reference belongs on. -Two carve-outs, each stated as a single case. Whatever neither of them affirmatively permits is +The carve-outs below are each stated as a single case. Whatever none of them affirmatively permits is banned by the paragraph above, which is the whole of the test and is why no list of banned cases follows. The first: **in a code or workflow comment, a URL naming an issue or a pull request on a public repository other than this one is a source citation and is permitted.** It does the same job @@ -236,11 +237,11 @@ citation beside a claim, which is the distinction this rule turns on. Separately, `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md` carry no three-part version and no commit SHA, full or abbreviated, whether a pin's value, an example, a minimum version, or a fixed constant, since a pin's copy goes stale at the next Dependabot bump and every -other kind reads exactly like one. Neither carve-out above lifts this ban. Item 3 of this skill's +other kind reads exactly like one. No carve-out above lifts this ban. Item 3 of this skill's carried-doc-references reference says what to write instead of each and carries the audit that flags a literal. -Three reasons, and the first decides it. +The reasons follow, and the first decides it. - **A reference is a second lookup, and the reader is already holding the file.** A comment earns its place by explaining the line under it to whoever reads that line now. A number they have to @@ -294,7 +295,7 @@ would report a fixture rather than a claim about this repository. ## Character set Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR -descriptions. A non-ASCII character is read against three tiers, because whether one is +descriptions. A non-ASCII character is read against the tiers below, because whether one is typography or meaning depends on where it sits. A character in no tier is a finding rather than a silent pass, a letter of a recorded name excepted per the bullet below. @@ -384,6 +385,13 @@ Clarify Devcontainer Setup Steps in README ## Quantitative claims -A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) -is verified against current code before it is written. When a doc number is derived from a code -constant, mark the dependency in a source-code comment so the next editor knows to update both. +<!-- include: GOVERNANCE.md > Incidental Counts --> + +Text refers to an enumeration rather than stating its size, so it writes "the following checks", "each case", or "the rows below". This holds in every fleet repository, for docs, code and workflow comments, and docstrings alike. A stated count is a second copy of what the list or table already shows, and nothing keeps the two copies in sync. Every later edit to the enumeration then owes an edit to the sentence, and a missed one surfaces as a review finding. A word such as "both" or "all three" is a count too, where the list it counts can grow. A count is written only where the number is what the reader needs, such as a limit, a threshold, or a measured result. Such a count names its source, such as the constant or the measurement, so a reader can re-derive it. A wrong incidental count is fixed by removing it rather than by correcting it, since a corrected number goes stale the same way. + +<!-- /include --> + +A `README.md` quantitative claim (a count, a size, a version floor, a supported-platform list) is +verified against current code before it is written. When a doc number +is derived from a code constant, mark the dependency in a source-code comment so the next editor +knows to update both. diff --git a/.agents/skills/comment-and-doc-style/references/carried-doc-references.md b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md index 9b66d1b3a..78f0091f1 100644 --- a/.agents/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md @@ -14,7 +14,7 @@ verbatim, and this rule does not reach them. ## What is banned -Three things, in the files above: +The following are banned in the files above: 1. **Any reference to the template repo**, in prose or in a link. The coordination flow that produced a carried file is machinery a consumer of that repo should never have to see, and diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index b13de18d1..8b95032a0 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -2,7 +2,7 @@ name: dotnet-codestyle description: >- Governs C#/.NET code style for ptr727/ProjectTemplate fleet repos: the zero-warnings build - policy and its three-task clean-compile chain, central Directory.Build.props/ + policy and its clean-compile task chain, central Directory.Build.props/ Directory.Packages.props configuration, C# language and naming conventions, XML documentation, analyzer suppression scope, the library-versus-application logging split, async and error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, and AOT-compatible @@ -32,10 +32,10 @@ real, and the language, naming, logging, and testing conventions. ### Zero warnings policy -All builds must complete without warnings, enforced three ways: +All builds must complete without warnings, enforced as follows: - **The `.NET Format` clean-compile task.** It chains `CSharpier Format` -> `.NET Build` -> - `dotnet format style --verify-no-changes`. A repo carries those three task definitions in its + `dotnet format style --verify-no-changes`. A repo carries those task definitions in its own `.vscode/tasks.json`, matching the canonical `vscode-tasks.json` snippet at `github.com/ptr727/ProjectTemplate/blob/main/catalog/snippets/configs/vscode-tasks.json`. Run the `.NET Format` task after any code change, before commit. To run it natively instead, diff --git a/.agents/skills/fleet-code-review/SKILL.md b/.agents/skills/fleet-code-review/SKILL.md index 1843d7829..58d0149f0 100644 --- a/.agents/skills/fleet-code-review/SKILL.md +++ b/.agents/skills/fleet-code-review/SKILL.md @@ -87,6 +87,9 @@ Review carried fleet content by intent and fidelity. A byte-locked reference to downstream repository does not carry is not a broken link. A substantive defect in canonical content remains a finding, with the fix located at its canonical source. +A finding that a stated count is wrong suggests removing the count where it is incidental, per +`GOVERNANCE.md` "Incidental Counts", rather than correcting the number. + ## Publish Every Finding Never suppress or hide a finding because confidence is low. Investigate until it is supported diff --git a/.agents/skills/git-commit-conventions/references/history-rewrite.md b/.agents/skills/git-commit-conventions/references/history-rewrite.md index b331a557e..66ab26500 100644 --- a/.agents/skills/git-commit-conventions/references/history-rewrite.md +++ b/.agents/skills/git-commit-conventions/references/history-rewrite.md @@ -9,7 +9,7 @@ key over a commit still committed by a bot (`dependabot[bot]`, `github-actions[b own web-flow does not match its committer and lands `unknown_key`/unverified, which a require-signed-commits rule then rejects. -Two gates keep committer and signature aligned: +These gates keep committer and signature aligned: 1. **Scope the rewrite to only the commits that must be modified.** By default those are the rewriter's own, whose committer already matches, so a commit that needs no change stays out of diff --git a/.agents/skills/local-strict-review/SKILL.md b/.agents/skills/local-strict-review/SKILL.md index f04d3d70a..64e9b8cf4 100644 --- a/.agents/skills/local-strict-review/SKILL.md +++ b/.agents/skills/local-strict-review/SKILL.md @@ -33,7 +33,7 @@ Dispatches one read-only subagent against this branch's full diff since it forke The subagent reads the full content of every file the diff and the untracked-file list touch, not just the hunks, since cross-file and whole-file context is exactly what incremental review misses. It reports findings only. It never fixes, stages, or commits anything. -Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out three traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. +Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out these traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. ## Running It @@ -109,7 +109,7 @@ A second pass under the same rule, run in the repository that authors canonical **The unit is what a reviewer reads whole**, and `spec/files.json` rather than the document decides which, down to which files carry units at all. `canonical_review.py list` names the whole set and is the authority on it, so the rules are not paraphrased here, where a paraphrase can only drift from them. In the ordinary case a unit is one level-two section of a carried Markdown canonical, and `sweep` names each one it wants exactly as `record` takes it. The pass reads that unit's whole current text rather than the diff that moved it, because reproducing the carrier's read is the entire point, and a diff with surrounding context is a different read the pass above has already done. -Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and all three are below. +Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and each is below. ```text Task: adversarial review of one canonical unit, read as a repository carrying it for the first diff --git a/.agents/skills/merge-and-release/SKILL.md b/.agents/skills/merge-and-release/SKILL.md index fabc25de5..a6a397746 100644 --- a/.agents/skills/merge-and-release/SKILL.md +++ b/.agents/skills/merge-and-release/SKILL.md @@ -21,8 +21,8 @@ description: >- ## Why This Exists -Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same two steps follow -every time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can +Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same steps follow every +time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can also change `.agents/skills` content this very session depends on, so the release step always carries a Skills refresh with it there, never a separate branch to ask about, an ambiguous "merge and release" on the hub must not leave the maintainer unsure whether Skills got refreshed. One @@ -156,8 +156,7 @@ skill covers all of it, scoped down by what the maintainer actually asks for. `docs/host-setup.md` "Fleet Skills Install" cadence. 8. Run cleanup regardless of how steps 5 through 7 ended, no release configured, a dispatch failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh all still - reach this step, the merge in step 3 already landed by then. Two parts, both required, neither - optional: + reach this step, the merge in step 3 already landed by then. Every part below is required: - The promotion PR's own worktree: fetch and prune, remove the worktree, then bring the base clone to current `develop`, checking `develop` out first only where it sits on another branch, and fast-forwarding it with `git merge --ff-only origin/develop`. Where that checkout diff --git a/.agents/skills/python-codestyle/references/code-style.md b/.agents/skills/python-codestyle/references/code-style.md index cca918e1b..45f355bcd 100644 --- a/.agents/skills/python-codestyle/references/code-style.md +++ b/.agents/skills/python-codestyle/references/code-style.md @@ -49,7 +49,7 @@ carries that as the host floor `scripts/host_gate.py` enforces, so 3.10+-only syntax (`X | None`, `match`, etc.) needs no quoting, no `typing.Union` fallback, and no `from __future__ import annotations` guard on that account alone. Add that import only when a real forward reference - needs it, per the bullet above. Three named exceptions carry a lower floor on purpose and say so + needs it, per the bullet above. Named exceptions carry a lower floor on purpose and say so themselves: `scripts/skills_install.sh` and the `install-skills.*` bootstrap scripts, which must run on whatever interpreter a host already has before this floor's toolchain exists to install one, and `spec/resolve_description.py`, which `repo-config/configure.sh`'s own bootstrap probe diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 9a71b3fef..46bbda6bb 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -66,9 +66,9 @@ from `develop` unless the task is explicitly about `main`-only content, per `GOV is whatever it last fetched rather than the branch it names. The base clone is a fetch source, not a place to do task work. `fetch` and `worktree add` run -against it for that purpose, and outside two steps below, nothing else mutates its working tree, +against it for that purpose, and outside the steps below, nothing else mutates its working tree, index, or HEAD while a task is in progress: never `checkout`, `pull`, `reset`, `commit`, or any -other such command. Those two are "Listing and Cleanup"'s own terminal step and "Creating a +other such command. The steps are "Listing and Cleanup"'s own terminal step and "Creating a Worktree"'s return of the base clone to its own working branch where a continuation finds the task branch checked out there. That distinction is the one a real incident missed, where an agent reused a primary checkout as the working directory itself rather than only as the source a @@ -125,7 +125,7 @@ list`, the fleet worktree directory, and IDE worktree discovery. It also leaves durable location the maintainer can inspect after the agent session ends. Before running that command, inspect the executor's active write boundaries. A linked worktree -requires write access to both of these locations: +requires write access to each of these locations: - the intended `~/repos/worktrees/<Repo>-<task-slug>` worktree directory - the base clone's `.git/worktrees/` administrative directory, which holds the index and locks diff --git a/.agents/skills/session-handoff/SKILL.md b/.agents/skills/session-handoff/SKILL.md index 1edc97bbc..235ed135f 100644 --- a/.agents/skills/session-handoff/SKILL.md +++ b/.agents/skills/session-handoff/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists -A handoff written to a scratch file fails three ways the chain closes. The file is not found where +The chain closes each way a handoff written to a scratch file fails. The file is not found where the next session looks. More than one candidate file is found and nothing says which one is current. And there is no history at all, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is precisely the record of what has already @@ -65,7 +65,7 @@ without trusting the parts of it that have gone stale. - **A platform lane is for work that needs its platform.** A platform track is one kept for a single host platform, such as `wsl` or `windows`. It takes an issue only when its defect reproduces, or its fix can be verified, on that platform alone. It also needs that no Linux host outside WSL can - both fix the issue and verify the fix. An issue meeting both goes to its platform's lane. Any + both fix the issue and verify the fix. An issue meeting each condition goes to its platform's lane. Any other issue goes to the ordinary backlog, since a lane costs a separate chain and a host-bound session. A defect in WSL interop, the `WSLInterop` binfmt handler, `/mnt/<drive>` paths, or `wslpath` reproduces only inside WSL. So does running a Windows `.exe` from Linux, although WSL @@ -73,7 +73,7 @@ without trusting the parts of it that have gone stale. path and ACL semantics, reproduces only on Windows. Either still goes to the backlog where a Linux host outside WSL verifies its fix, as a plain unit test of a path translation can. An issue in systemd user scopes, apt, a bash script, or a `pwsh` script run in a Linux container gets no lane - by its topic. The two conditions decide it like any other issue. Where only part of an issue needs + by its topic. The conditions decide it like any other issue. Where only part of an issue needs a platform, the session finding it files each such part as its own issue for its platform's lane. The original is narrowed to the rest, or closed in favor of those issues where nothing is left. A session that may not write issues names the unsplit issue in its report or handoff instead. An @@ -96,7 +96,7 @@ without trusting the parts of it that have gone stale. a given repository, read from each issue's metadata block rather than from the label, since an issue carrying the label and no block belongs to no readable track and blocks the count rather than joining it. Two or more on one track is a defect to report, never one to resolve by picking, - and a run interrupted between the three steps above is the one shape of it that `link` settles + and a run interrupted between the steps above is the one shape of it that `link` settles rather than a human. - **Zero open on a track is two different states.** The track has never had a handoff, or its lane was closed out and its newest link is closed. `new` chains onto the newest link either way, open @@ -170,7 +170,7 @@ comments. Read the chain before re-attempting anything. `resume` prints the current body and indexes the closed links behind it, and `chain --grep` searches the bodies it walks for a regular expression, which is how a session answers whether a path has already been tried without reading every round. -Three things bound that answer. The walk starts at one track's newest link and follows each +The points below bound that answer. The walk starts at one track's newest link and follows each marker's `previous=` from there, which is normally that one lane and is in fact whatever the markers name, so every link prints its own track and a predecessor on another lane shows up rather than passing unseen. The walk stops at `--limit` and names the link it stopped short of, so a capped @@ -255,9 +255,9 @@ it. ## The Parked Decision Queue The rules below are `GOVERNANCE.md` "Communicating with the User", carried whole so this skill works -in isolation. Two of them state the obligation: the one opening "A question filed as an issue is +in isolation. Some of them state the obligation: the one opening "A question filed as an issue is parked rather than asked" and the one opening "The session that writes a handoff presents the parked -queue in the same act". Three more state the form the questions take, the one opening "Ask every +queue in the same act". Others state the form the questions take, the one opening "Ask every question through the interface's prompt, never in prose", which also names the numbered list as the fallback where no prompt exists, the one opening "Raise work blocked on the user as a direct interactive prompt", which shapes the options, and the one opening "Lead every choice with a @@ -290,8 +290,8 @@ The mechanics are the hub's `scripts/handoff.py`, run from a hub checkout, per ` resolves in every repository and a chain read out of the wrong one is well formed. `current`, `resume`, `chain`, and `new` each take an optional `--track` defaulting to `default`, and `link` and `tracks` take none, since a pair of issue numbers and a whole-repository survey each name -their own scope. A session working a named lane passes `--track` on each of the four that accept -it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and +their own scope. A session working a named lane passes `--track` on each subcommand that +accepts it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and comments on whatever link that lane has, closing it too where it was open. - **`current` and `resume`** answer where a track stands, `resume` adding the body itself and an @@ -306,10 +306,10 @@ comments on whatever link that lane has, closing it too where it was open. place an issue on a track until its own block says which track that is. With `--json` it prints the same rows as an array of number, title, track, labels, and updatedAt, the track null for an issue whose block is absent or unreadable, which is what a picker ranks by. -- **`new`** performs as many of the chain's three steps as the track has links for. On a track +- **`new`** performs as many of the chain's steps as the track has links for. On a track with no link at all it creates and stops, since there is nothing to comment on or close. On one whose newest link is closed it creates and comments, since the close already happened. Only a - track with an open head takes all three. **`link`** finishes a run that stopped between them, + track with an open head takes every step. **`link`** finishes a run that stopped between those steps, and separately repairs a successor whose block names no predecessor, so its own writes are the comment, the close, and that body edit. The edit reaches only the repair case, since `new` embeds the predecessor in the block at create time, so a run interrupted after the create @@ -327,7 +327,7 @@ python3 "$handoff" tracks --repo OWNER/NAME [--json] ``` Read `--dry-run` output before the first real `new` of a session, since the run can close an issue. -It is accepted by `new` and `link`, the two subcommands that write, and by no other. +It is accepted by `new` and `link`, the subcommands that write, and by no other. Exit `0` is success, `1` a refusal the caller can act on, a usage error included, and `2` the command not having run to an answer, so a refusal and a failure to reach one never share a code. A diff --git a/.agents/skills/unattended-handoff/SKILL.md b/.agents/skills/unattended-handoff/SKILL.md index 5c1e41e59..f701ca677 100644 --- a/.agents/skills/unattended-handoff/SKILL.md +++ b/.agents/skills/unattended-handoff/SKILL.md @@ -25,7 +25,7 @@ description: >- The attended session, `session-handoff`'s "The Attended Session", needs the maintainer for every decision it meets. Most open issues need none, so a loop can work those while the maintainer is -away and hand back only the ones that do. Two things make that loop hard. Its orchestrator runs for +away and hand back only the ones that do. What follows makes that loop hard. Its orchestrator runs for hours, so any detail it reads is paid for again on every later round, and a loop that reads issues itself dies of its own context long before the backlog is empty. And a worker that meets a decision must stop without losing its work, in a state the maintainer's next attended session picks up with diff --git a/.agents/skills/workflow-ci-contract/SKILL.md b/.agents/skills/workflow-ci-contract/SKILL.md index 4689dde9d..8a53aa69f 100644 --- a/.agents/skills/workflow-ci-contract/SKILL.md +++ b/.agents/skills/workflow-ci-contract/SKILL.md @@ -37,7 +37,7 @@ description: >- ## The Contract Text -`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry three rules for reading a row, one of which is about a repository declaring more than one type, so read those there. +`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry rules for reading a row, one of which is about a repository declaring more than one type, so read those there. ## After Any Workflow Edit diff --git a/.agents/skills/workflow-ci-contract/references/test-methodology.md b/.agents/skills/workflow-ci-contract/references/test-methodology.md index 527e155dc..3ba870bb6 100644 --- a/.agents/skills/workflow-ci-contract/references/test-methodology.md +++ b/.agents/skills/workflow-ci-contract/references/test-methodology.md @@ -6,7 +6,7 @@ The section below is `WORKFLOW.md` section 5, whole. Its items and scenarios ans <!-- include: WORKFLOW.md > 5. Test Methodology --> -An agent verifies a project in three escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. +An agent verifies a project in escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. ### 5A. Static Audit (No Execution) @@ -44,7 +44,7 @@ Every probe here that opens a pull request, dispatches a workflow, or re-runs a - Open a trivial-change PR touching one target and confirm S1. *Caveat: the Docker leg logs in to the registry even on smoke and reads the buildcache, so it needs `DOCKER_HUB_*` secrets and cannot run on a fork PR (same-repo only).* - Per registry: after a real publish, query NuGet.org for the expected version + prerelease classification (and the `.snupkg` on the symbol server), and confirm a re-run added no duplicate. For PyPI read the built `dist/*` filenames out of the build job's log, `.dev0` off `develop` vs a plain version on the default branch. - Inspect the latest real publish's logs for `PublicRelease`/`SemVer2` per leg and confirm the artifact lifecycle (uploaded, consumed, deleted, with none left behind). -- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is four things, and each of them matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all four, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads all four back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. +- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is the following, and each item matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all of it, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads the evidence back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. ### Assessment diff --git a/.claude-plugin/fleet-skills/.source-digests/agent-conduct b/.claude-plugin/fleet-skills/.source-digests/agent-conduct index d84ef48e4..d241073ab 100644 --- a/.claude-plugin/fleet-skills/.source-digests/agent-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/agent-conduct @@ -1 +1 @@ -8fd3e8fc02a6a212 +5e3c2f4aee886b23 diff --git a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo index e0df6af4a..0827c9ff8 100644 --- a/.claude-plugin/fleet-skills/.source-digests/audit-a-repo +++ b/.claude-plugin/fleet-skills/.source-digests/audit-a-repo @@ -1 +1 @@ -80fffe4dc7a60134 +db506d0d9484243a diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 8a9281d7f..4707bb7e8 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -aac3800c3f96ce19 +c3f8d3afecf7d599 diff --git a/.claude-plugin/fleet-skills/.source-digests/branching-and-release-model b/.claude-plugin/fleet-skills/.source-digests/branching-and-release-model index d3b7afec8..575254ab4 100644 --- a/.claude-plugin/fleet-skills/.source-digests/branching-and-release-model +++ b/.claude-plugin/fleet-skills/.source-digests/branching-and-release-model @@ -1 +1 @@ -5523609158609424 +af770918d60e7ecc diff --git a/.claude-plugin/fleet-skills/.source-digests/carried-instruction-file-guard b/.claude-plugin/fleet-skills/.source-digests/carried-instruction-file-guard index dc73678b8..39bef72fb 100644 --- a/.claude-plugin/fleet-skills/.source-digests/carried-instruction-file-guard +++ b/.claude-plugin/fleet-skills/.source-digests/carried-instruction-file-guard @@ -1 +1 @@ -5b8dad228ecf08a0 +f15ffb2deb91ce4d diff --git a/.claude-plugin/fleet-skills/.source-digests/comment-and-doc-style b/.claude-plugin/fleet-skills/.source-digests/comment-and-doc-style index 8e469ab6e..33c13d6c2 100644 --- a/.claude-plugin/fleet-skills/.source-digests/comment-and-doc-style +++ b/.claude-plugin/fleet-skills/.source-digests/comment-and-doc-style @@ -1 +1 @@ -21bf1a2aa4749651 +39aa8ed8d7eda42b diff --git a/.claude-plugin/fleet-skills/.source-digests/dotnet-codestyle b/.claude-plugin/fleet-skills/.source-digests/dotnet-codestyle index f6d020a21..d7cd99700 100644 --- a/.claude-plugin/fleet-skills/.source-digests/dotnet-codestyle +++ b/.claude-plugin/fleet-skills/.source-digests/dotnet-codestyle @@ -1 +1 @@ -4e9d6c62e1680a27 +6b880f19cbeab6c9 diff --git a/.claude-plugin/fleet-skills/.source-digests/fleet-code-review b/.claude-plugin/fleet-skills/.source-digests/fleet-code-review index c1ba22f7d..055615cc3 100644 --- a/.claude-plugin/fleet-skills/.source-digests/fleet-code-review +++ b/.claude-plugin/fleet-skills/.source-digests/fleet-code-review @@ -1 +1 @@ -80db8cf4ce862aec +5f0ff9ce248cdb3e diff --git a/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions b/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions index 2c070319f..844685ffb 100644 --- a/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions +++ b/.claude-plugin/fleet-skills/.source-digests/git-commit-conventions @@ -1 +1 @@ -074ebf66f8ce7a8b +5d667d68d62f1f7d diff --git a/.claude-plugin/fleet-skills/.source-digests/local-strict-review b/.claude-plugin/fleet-skills/.source-digests/local-strict-review index d4f83be26..33176ecd2 100644 --- a/.claude-plugin/fleet-skills/.source-digests/local-strict-review +++ b/.claude-plugin/fleet-skills/.source-digests/local-strict-review @@ -1 +1 @@ -347b1f7398043440 +31135890d391c0b3 diff --git a/.claude-plugin/fleet-skills/.source-digests/merge-and-release b/.claude-plugin/fleet-skills/.source-digests/merge-and-release index 7feb90f6f..e99015190 100644 --- a/.claude-plugin/fleet-skills/.source-digests/merge-and-release +++ b/.claude-plugin/fleet-skills/.source-digests/merge-and-release @@ -1 +1 @@ -3861745c2d562eec +3053496c0d18bcac diff --git a/.claude-plugin/fleet-skills/.source-digests/python-codestyle b/.claude-plugin/fleet-skills/.source-digests/python-codestyle index 75e738476..6859deaab 100644 --- a/.claude-plugin/fleet-skills/.source-digests/python-codestyle +++ b/.claude-plugin/fleet-skills/.source-digests/python-codestyle @@ -1 +1 @@ -a54a634cf8810359 +047e89b805ed646f diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index 645232600..1021f3289 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -f4eff2621ffb5828 +010e5fbece90a0b9 diff --git a/.claude-plugin/fleet-skills/.source-digests/session-handoff b/.claude-plugin/fleet-skills/.source-digests/session-handoff index b63c2d958..141430d17 100644 --- a/.claude-plugin/fleet-skills/.source-digests/session-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/session-handoff @@ -1 +1 @@ -5daeb44bdd891a90 +8f9da4cca73d8b01 diff --git a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff index 05bbf8d5b..ed40090d1 100644 --- a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff @@ -1 +1 @@ -66316cebf3923233 +680119285f154228 diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 109e01eab..4ad8ce7f7 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -82e59e34aeb7fac2 +747f86164bec8c5f diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index 8ec25a370..45c60c1f1 100644 --- a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md @@ -16,14 +16,14 @@ description: >- Where a sibling skill owns the moment, it wins: git-commit-conventions for committing, pr-review-conduct for review and merge claims, local-strict-review for the passes a push owes, comment-and-doc-style for prose. The GOVERNANCE.md sections it surfaces keep the full rules, - three of them carried here whole as generated includes. + several of them carried here whole as generated includes. --- # Agent Conduct ## Why This Exists -The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those three sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. +The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. ## Before Designing a Change @@ -102,18 +102,18 @@ Where a lesson lands, when it earns a mechanical hook, and where work blocked on - **A blocker filed in another repository is recorded in the repository whose work it blocks.** The binding moment is the one where the upstream issue is filed or, where it already exists, found, because that session is the one that knows what stopped and why. It owes a second issue in the repository that is waiting rather than only the first, and where the upstream issue already exists the local issue names that one and no second upstream issue is filed. The local issue states what this repository cannot do and why, in its own terms rather than as a pointer to read elsewhere, since a reader who has to open the upstream issue to learn whether it affects them opens every one of them. The local issue names the upstream one as its blocker, carries the `blocked` label, and carries whatever labels its own work would carry anyway. A comment on the upstream issue then names the local one in return, a comment rather than an edit to the body because a second repository may join the same blocker later and because the body is often not this session's to rewrite. That order, the upstream issue and then the local issue and then the backlink comment, leaves a partial failure as a record naming its blocker rather than as a blocker naming a record nobody wrote. **A handoff does not do this job.** It carries the blockers a round met, and it belongs to one track and closes with its successor, where the wait outlives every session that met it and belongs in the backlog the whole repository reads. - **The blocker record is written under the ordinary write rules, and the label on it is taken off deliberately.** Filing an issue and commenting on another are state-changing calls, so "Repository Boundaries and Write Safety" binds each of them exactly as it binds any other write, which keeps the upstream issue inside this owner and makes a blocker under a different owner a matter of explicit permission rather than of this rule. The `blocked` label is what every reader of this record selects on, so a repository not carrying it cannot host one until the fleet label set is applied there, which is a change to that repository's configuration rather than anything this rule writes, and a session that finds it missing reports that. The label comes off when the blocker clears, and the session closing the upstream issue is best placed to take it off, since the backlink comments naming every waiting repository are on the issue it is closing, while any later session that finds it cleared takes it off instead. A fix can land well before either of those, so the label lags the fix rather than tracking it, which is why a session meeting a `blocked` issue reads the state of the issue that issue's body names rather than the label. The local issue stays open when the label comes off, because the work it records still has to be done and is ordinary backlog from that moment on. - **A handoff parked on a maintainer decision carries the `blocked` label too.** Its blocker is a `decision` issue in this same repository rather than an issue elsewhere, and the parking comment on the handoff names it. A reader reads the named issue rather than the label, and the blocker clears when that issue loses its `decision` label. Unlike a cross-repository blocker, a session finding it cleared does not take the label off. It comes off only when a session hands the link back to be worked, so a loop running meanwhile never takes a link a present maintainer is still working. -- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those three conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. +- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** The conditions below must hold together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all of them (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" keeps the full rules, and the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, carries it whole as a generated include and surfaces it at its decision moments. <!-- /include --> -Two rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. +Some rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. ## When Work Stops on Another Repository -The rules for this moment are two bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. +The rules for this moment are bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. ## Delegation, in One Paragraph -The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. The two that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. +The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. Those that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. diff --git a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md index e2718ab74..ef69d060e 100644 --- a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md @@ -8,7 +8,7 @@ description: >- ## Why This Exists -The audit is the fleet's measurement procedure, and the two failure shapes it guards against are both silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. +The audit is the fleet's measurement procedure, and the failure shapes it guards against are all silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. ## Before Measuring Anything diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index 53f93a66e..631c1bc7f 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists Asking for one issue to be fixed is `drive-pr`'s job and needs no skill above it. Asking for a -whole backlog to be worked down is a different problem, and three things about it are not obvious. +whole backlog to be worked down is a different problem, and several things about it are not obvious. Parallelism is bounded by file overlap rather than by agent count, so the grouping decides the throughput. The backlog is not a fixed list, since every review round files deferral issues that belong in the next round's ranking, so a plan made once is stale by its second round. And a @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. Three kinds of group are never tiered +named it for this run. These kinds of group are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds @@ -520,7 +520,7 @@ restores a line another session's write dropped. ## Ending the Run -The run ends at either of two points, and they are different endings. +The run ends at one of the points below, and they are different endings. - **The backlog is worked out**, meaning a full re-rank finds no open issue this skill can act on. That is not the same as zero open issues, since a backlog of nothing but maintainer questions and diff --git a/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/branch-protection-and-promotion.md b/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/branch-protection-and-promotion.md index 9ca227699..eae12059a 100644 --- a/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/branch-protection-and-promotion.md +++ b/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/branch-protection-and-promotion.md @@ -18,7 +18,7 @@ explicitly for a repository outside the registry. ## Executing a `develop -> main` promotion safely -Two traps, both learned the hard way: +These traps were learned the hard way: - **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` (and a repository's "Automatically delete head branches" toggle, which the fleet keeps off for diff --git a/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/release-publish-mechanics.md b/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/release-publish-mechanics.md index b0c3adf48..8b842510e 100644 --- a/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/release-publish-mechanics.md +++ b/.claude-plugin/fleet-skills/skills/branching-and-release-model/references/release-publish-mechanics.md @@ -108,7 +108,7 @@ are intentionally not added. ## Recovering a failed registry push -A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the two recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. +A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. **Re-dispatch, available only while the tip has not moved.** A `workflow_dispatch` takes a ref rather than a commit, and D2.3 admits only `main` or `develop`, so what it builds is that branch's tip at dispatch time. While the tip is still the commit whose push failed, a re-dispatch rebuilds the same version and runs its push again, refreshing the release the way any dispatch does. @@ -116,7 +116,7 @@ This is a time-of-check-to-time-of-use race rather than a guarded operation: not **Re-run all jobs, available inside the window whatever the tip has done.** `gh run rerun <id>` replays the run under the original event's `GITHUB_SHA` and `GITHUB_REF` and re-executes every job rather than only the failed ones. The publisher pins the release task to that commit with `ref: ${{ github.sha }}`, so `get-version` recomputes the same version from the same commit and history, each build leaf checks out the `GitCommitId` that job emits, the package artifact D5.2 deleted is rebuilt and re-uploaded rather than missing when `publish-<target>` downloads it, and that job retries the push it failed. The release itself needs nothing from the re-run, the failed run having already cut it, though on a dispatch-triggered run the re-run re-enters `github-release`, which refreshes the release per D4.4's dispatch leg and runs the `release-asset-*` delete with it per D5.2. A re-dispatch here would build the new tip instead, and NBGV derives the version from git height, so that is a further version and the one whose push failed never reaches the registry. -Three qualifications come with the re-run route. +The qualifications below come with the re-run route. - D4.4 and `WORKFLOW.md` 5B's S9 describe a re-run whose predecessor push **succeeded**, where the registry dedupes the second one. This is the case they do not cover, and its retried push is the first the registry ever receives for that version. - GitHub offers a re-run only within **30 days** of the initial run, and a repository's own **log** retention setting can be shorter, so the usable window is the shorter of the two. This is the run's own retention and is unrelated to D5.4's `retention-days: 1`, which bounds an uploaded artifact rather than the run. diff --git a/.claude-plugin/fleet-skills/skills/carried-instruction-file-guard/SKILL.md b/.claude-plugin/fleet-skills/skills/carried-instruction-file-guard/SKILL.md index c61872283..d20852942 100644 --- a/.claude-plugin/fleet-skills/skills/carried-instruction-file-guard/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/carried-instruction-file-guard/SKILL.md @@ -21,7 +21,7 @@ description: >- A downstream repo's `AGENTS.md`/`GOVERNANCE.md`/`CODESTYLE.md`/`WORKFLOW.md` can hold two different kinds of content mixed in one file: sections that are stale copies of the hub's fleet-wide rules, and local rules the repo wrote for a fault the fleet has never seen elsewhere. Re-vendoring the hub's canonical version over the whole file deletes the second kind silently, because nothing about the diff looks wrong. This has actually happened: a resync replaced a repo's `AGENTS.md` wholesale with the hub's, and the repo's own local additions were gone with no error, no warning, and no review comment calling it out. -The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these four files, every time, regardless of how routine the request sounds. +The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these files, every time, regardless of how routine the request sounds. ## Before you touch any of these four files 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 3e556340e..51772e1d6 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 @@ -14,7 +14,8 @@ description: >- carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md) is being edited (no coordination references to the template or a sibling repo), and when writing a PR title or commit message (imperative subject, no vague - titles, no unsolicited Co-Authored-By, no release-bump magnitude). + titles, no unsolicited Co-Authored-By, no release-bump magnitude). Also triggers when text + states how many items a list or a table holds, since that count goes stale. --- # Comment and Doc Style @@ -65,7 +66,7 @@ Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.N file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md - cspell line together, all three or none. + cspell line together, all of them or none. - **`HISTORY.md` mirrors the README's opening**: the same `# <Title>`, the same tagline verbatim (the first line after the README's H1), then its own `## Release History`. It never repeats a paragraph below the README's tagline. @@ -199,7 +200,7 @@ pull request carries the `comments` label and the gate stands down for that chan and `--allow-comments` does it for one run by hand. The label and the variable are separate deliberately, so a variable left exported reaches the commit and never the merge gate. -Three things about reaching those escapes read as a broken gate until they are known. On a public +The facts below about reaching those escapes read as a broken gate until they are known. On a public repository the lint job reads the label when it runs, and its log says "Read the pull request's labels live". A label added before then applies, and a rerun picks up one added since. In a private repository, or where the log lacks that line, the event's snapshot decides. It misses a label @@ -220,7 +221,7 @@ and the fleet's own rule documents: `AGENTS.md`, `AUDIT.md`, `CLAUDE.md`, `CODES are the repository's own narrative and keep their references, as do a commit message and a pull request body, which are the surfaces a reference belongs on. -Two carve-outs, each stated as a single case. Whatever neither of them affirmatively permits is +The carve-outs below are each stated as a single case. Whatever none of them affirmatively permits is banned by the paragraph above, which is the whole of the test and is why no list of banned cases follows. The first: **in a code or workflow comment, a URL naming an issue or a pull request on a public repository other than this one is a source citation and is permitted.** It does the same job @@ -236,11 +237,11 @@ citation beside a claim, which is the distinction this rule turns on. Separately, `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md` carry no three-part version and no commit SHA, full or abbreviated, whether a pin's value, an example, a minimum version, or a fixed constant, since a pin's copy goes stale at the next Dependabot bump and every -other kind reads exactly like one. Neither carve-out above lifts this ban. Item 3 of this skill's +other kind reads exactly like one. No carve-out above lifts this ban. Item 3 of this skill's carried-doc-references reference says what to write instead of each and carries the audit that flags a literal. -Three reasons, and the first decides it. +The reasons follow, and the first decides it. - **A reference is a second lookup, and the reader is already holding the file.** A comment earns its place by explaining the line under it to whoever reads that line now. A number they have to @@ -294,7 +295,7 @@ would report a fixture rather than a claim about this repository. ## Character set Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR -descriptions. A non-ASCII character is read against three tiers, because whether one is +descriptions. A non-ASCII character is read against the tiers below, because whether one is typography or meaning depends on where it sits. A character in no tier is a finding rather than a silent pass, a letter of a recorded name excepted per the bullet below. @@ -384,6 +385,13 @@ Clarify Devcontainer Setup Steps in README ## Quantitative claims -A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) -is verified against current code before it is written. When a doc number is derived from a code -constant, mark the dependency in a source-code comment so the next editor knows to update both. +<!-- include: GOVERNANCE.md > Incidental Counts --> + +Text refers to an enumeration rather than stating its size, so it writes "the following checks", "each case", or "the rows below". This holds in every fleet repository, for docs, code and workflow comments, and docstrings alike. A stated count is a second copy of what the list or table already shows, and nothing keeps the two copies in sync. Every later edit to the enumeration then owes an edit to the sentence, and a missed one surfaces as a review finding. A word such as "both" or "all three" is a count too, where the list it counts can grow. A count is written only where the number is what the reader needs, such as a limit, a threshold, or a measured result. Such a count names its source, such as the constant or the measurement, so a reader can re-derive it. A wrong incidental count is fixed by removing it rather than by correcting it, since a corrected number goes stale the same way. + +<!-- /include --> + +A `README.md` quantitative claim (a count, a size, a version floor, a supported-platform list) is +verified against current code before it is written. When a doc number +is derived from a code constant, mark the dependency in a source-code comment so the next editor +knows to update both. diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md index 9b66d1b3a..78f0091f1 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md @@ -14,7 +14,7 @@ verbatim, and this rule does not reach them. ## What is banned -Three things, in the files above: +The following are banned in the files above: 1. **Any reference to the template repo**, in prose or in a link. The coordination flow that produced a carried file is machinery a consumer of that repo should never have to see, and diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index b13de18d1..8b95032a0 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -2,7 +2,7 @@ name: dotnet-codestyle description: >- Governs C#/.NET code style for ptr727/ProjectTemplate fleet repos: the zero-warnings build - policy and its three-task clean-compile chain, central Directory.Build.props/ + policy and its clean-compile task chain, central Directory.Build.props/ Directory.Packages.props configuration, C# language and naming conventions, XML documentation, analyzer suppression scope, the library-versus-application logging split, async and error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, and AOT-compatible @@ -32,10 +32,10 @@ real, and the language, naming, logging, and testing conventions. ### Zero warnings policy -All builds must complete without warnings, enforced three ways: +All builds must complete without warnings, enforced as follows: - **The `.NET Format` clean-compile task.** It chains `CSharpier Format` -> `.NET Build` -> - `dotnet format style --verify-no-changes`. A repo carries those three task definitions in its + `dotnet format style --verify-no-changes`. A repo carries those task definitions in its own `.vscode/tasks.json`, matching the canonical `vscode-tasks.json` snippet at `github.com/ptr727/ProjectTemplate/blob/main/catalog/snippets/configs/vscode-tasks.json`. Run the `.NET Format` task after any code change, before commit. To run it natively instead, diff --git a/.claude-plugin/fleet-skills/skills/fleet-code-review/SKILL.md b/.claude-plugin/fleet-skills/skills/fleet-code-review/SKILL.md index 1843d7829..58d0149f0 100644 --- a/.claude-plugin/fleet-skills/skills/fleet-code-review/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/fleet-code-review/SKILL.md @@ -87,6 +87,9 @@ Review carried fleet content by intent and fidelity. A byte-locked reference to downstream repository does not carry is not a broken link. A substantive defect in canonical content remains a finding, with the fix located at its canonical source. +A finding that a stated count is wrong suggests removing the count where it is incidental, per +`GOVERNANCE.md` "Incidental Counts", rather than correcting the number. + ## Publish Every Finding Never suppress or hide a finding because confidence is low. Investigate until it is supported diff --git a/.claude-plugin/fleet-skills/skills/git-commit-conventions/references/history-rewrite.md b/.claude-plugin/fleet-skills/skills/git-commit-conventions/references/history-rewrite.md index b331a557e..66ab26500 100644 --- a/.claude-plugin/fleet-skills/skills/git-commit-conventions/references/history-rewrite.md +++ b/.claude-plugin/fleet-skills/skills/git-commit-conventions/references/history-rewrite.md @@ -9,7 +9,7 @@ key over a commit still committed by a bot (`dependabot[bot]`, `github-actions[b own web-flow does not match its committer and lands `unknown_key`/unverified, which a require-signed-commits rule then rejects. -Two gates keep committer and signature aligned: +These gates keep committer and signature aligned: 1. **Scope the rewrite to only the commits that must be modified.** By default those are the rewriter's own, whose committer already matches, so a commit that needs no change stays out of diff --git a/.claude-plugin/fleet-skills/skills/local-strict-review/SKILL.md b/.claude-plugin/fleet-skills/skills/local-strict-review/SKILL.md index f04d3d70a..64e9b8cf4 100644 --- a/.claude-plugin/fleet-skills/skills/local-strict-review/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/local-strict-review/SKILL.md @@ -33,7 +33,7 @@ Dispatches one read-only subagent against this branch's full diff since it forke The subagent reads the full content of every file the diff and the untracked-file list touch, not just the hunks, since cross-file and whole-file context is exactly what incremental review misses. It reports findings only. It never fixes, stages, or commits anything. -Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out three traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. +Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out these traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. ## Running It @@ -109,7 +109,7 @@ A second pass under the same rule, run in the repository that authors canonical **The unit is what a reviewer reads whole**, and `spec/files.json` rather than the document decides which, down to which files carry units at all. `canonical_review.py list` names the whole set and is the authority on it, so the rules are not paraphrased here, where a paraphrase can only drift from them. In the ordinary case a unit is one level-two section of a carried Markdown canonical, and `sweep` names each one it wants exactly as `record` takes it. The pass reads that unit's whole current text rather than the diff that moved it, because reproducing the carrier's read is the entire point, and a diff with surrounding context is a different read the pass above has already done. -Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and all three are below. +Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and each is below. ```text Task: adversarial review of one canonical unit, read as a repository carrying it for the first diff --git a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md index fabc25de5..a6a397746 100644 --- a/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md @@ -21,8 +21,8 @@ description: >- ## Why This Exists -Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same two steps follow -every time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can +Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same steps follow every +time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can also change `.agents/skills` content this very session depends on, so the release step always carries a Skills refresh with it there, never a separate branch to ask about, an ambiguous "merge and release" on the hub must not leave the maintainer unsure whether Skills got refreshed. One @@ -156,8 +156,7 @@ skill covers all of it, scoped down by what the maintainer actually asks for. `docs/host-setup.md` "Fleet Skills Install" cadence. 8. Run cleanup regardless of how steps 5 through 7 ended, no release configured, a dispatch failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh all still - reach this step, the merge in step 3 already landed by then. Two parts, both required, neither - optional: + reach this step, the merge in step 3 already landed by then. Every part below is required: - The promotion PR's own worktree: fetch and prune, remove the worktree, then bring the base clone to current `develop`, checking `develop` out first only where it sits on another branch, and fast-forwarding it with `git merge --ff-only origin/develop`. Where that checkout diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/references/code-style.md b/.claude-plugin/fleet-skills/skills/python-codestyle/references/code-style.md index cca918e1b..45f355bcd 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/references/code-style.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/references/code-style.md @@ -49,7 +49,7 @@ carries that as the host floor `scripts/host_gate.py` enforces, so 3.10+-only syntax (`X | None`, `match`, etc.) needs no quoting, no `typing.Union` fallback, and no `from __future__ import annotations` guard on that account alone. Add that import only when a real forward reference - needs it, per the bullet above. Three named exceptions carry a lower floor on purpose and say so + needs it, per the bullet above. Named exceptions carry a lower floor on purpose and say so themselves: `scripts/skills_install.sh` and the `install-skills.*` bootstrap scripts, which must run on whatever interpreter a host already has before this floor's toolchain exists to install one, and `spec/resolve_description.py`, which `repo-config/configure.sh`'s own bootstrap probe diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 9a71b3fef..46bbda6bb 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -66,9 +66,9 @@ from `develop` unless the task is explicitly about `main`-only content, per `GOV is whatever it last fetched rather than the branch it names. The base clone is a fetch source, not a place to do task work. `fetch` and `worktree add` run -against it for that purpose, and outside two steps below, nothing else mutates its working tree, +against it for that purpose, and outside the steps below, nothing else mutates its working tree, index, or HEAD while a task is in progress: never `checkout`, `pull`, `reset`, `commit`, or any -other such command. Those two are "Listing and Cleanup"'s own terminal step and "Creating a +other such command. The steps are "Listing and Cleanup"'s own terminal step and "Creating a Worktree"'s return of the base clone to its own working branch where a continuation finds the task branch checked out there. That distinction is the one a real incident missed, where an agent reused a primary checkout as the working directory itself rather than only as the source a @@ -125,7 +125,7 @@ list`, the fleet worktree directory, and IDE worktree discovery. It also leaves durable location the maintainer can inspect after the agent session ends. Before running that command, inspect the executor's active write boundaries. A linked worktree -requires write access to both of these locations: +requires write access to each of these locations: - the intended `~/repos/worktrees/<Repo>-<task-slug>` worktree directory - the base clone's `.git/worktrees/` administrative directory, which holds the index and locks diff --git a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md index 1edc97bbc..235ed135f 100644 --- a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists -A handoff written to a scratch file fails three ways the chain closes. The file is not found where +The chain closes each way a handoff written to a scratch file fails. The file is not found where the next session looks. More than one candidate file is found and nothing says which one is current. And there is no history at all, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is precisely the record of what has already @@ -65,7 +65,7 @@ without trusting the parts of it that have gone stale. - **A platform lane is for work that needs its platform.** A platform track is one kept for a single host platform, such as `wsl` or `windows`. It takes an issue only when its defect reproduces, or its fix can be verified, on that platform alone. It also needs that no Linux host outside WSL can - both fix the issue and verify the fix. An issue meeting both goes to its platform's lane. Any + both fix the issue and verify the fix. An issue meeting each condition goes to its platform's lane. Any other issue goes to the ordinary backlog, since a lane costs a separate chain and a host-bound session. A defect in WSL interop, the `WSLInterop` binfmt handler, `/mnt/<drive>` paths, or `wslpath` reproduces only inside WSL. So does running a Windows `.exe` from Linux, although WSL @@ -73,7 +73,7 @@ without trusting the parts of it that have gone stale. path and ACL semantics, reproduces only on Windows. Either still goes to the backlog where a Linux host outside WSL verifies its fix, as a plain unit test of a path translation can. An issue in systemd user scopes, apt, a bash script, or a `pwsh` script run in a Linux container gets no lane - by its topic. The two conditions decide it like any other issue. Where only part of an issue needs + by its topic. The conditions decide it like any other issue. Where only part of an issue needs a platform, the session finding it files each such part as its own issue for its platform's lane. The original is narrowed to the rest, or closed in favor of those issues where nothing is left. A session that may not write issues names the unsplit issue in its report or handoff instead. An @@ -96,7 +96,7 @@ without trusting the parts of it that have gone stale. a given repository, read from each issue's metadata block rather than from the label, since an issue carrying the label and no block belongs to no readable track and blocks the count rather than joining it. Two or more on one track is a defect to report, never one to resolve by picking, - and a run interrupted between the three steps above is the one shape of it that `link` settles + and a run interrupted between the steps above is the one shape of it that `link` settles rather than a human. - **Zero open on a track is two different states.** The track has never had a handoff, or its lane was closed out and its newest link is closed. `new` chains onto the newest link either way, open @@ -170,7 +170,7 @@ comments. Read the chain before re-attempting anything. `resume` prints the current body and indexes the closed links behind it, and `chain --grep` searches the bodies it walks for a regular expression, which is how a session answers whether a path has already been tried without reading every round. -Three things bound that answer. The walk starts at one track's newest link and follows each +The points below bound that answer. The walk starts at one track's newest link and follows each marker's `previous=` from there, which is normally that one lane and is in fact whatever the markers name, so every link prints its own track and a predecessor on another lane shows up rather than passing unseen. The walk stops at `--limit` and names the link it stopped short of, so a capped @@ -255,9 +255,9 @@ it. ## The Parked Decision Queue The rules below are `GOVERNANCE.md` "Communicating with the User", carried whole so this skill works -in isolation. Two of them state the obligation: the one opening "A question filed as an issue is +in isolation. Some of them state the obligation: the one opening "A question filed as an issue is parked rather than asked" and the one opening "The session that writes a handoff presents the parked -queue in the same act". Three more state the form the questions take, the one opening "Ask every +queue in the same act". Others state the form the questions take, the one opening "Ask every question through the interface's prompt, never in prose", which also names the numbered list as the fallback where no prompt exists, the one opening "Raise work blocked on the user as a direct interactive prompt", which shapes the options, and the one opening "Lead every choice with a @@ -290,8 +290,8 @@ The mechanics are the hub's `scripts/handoff.py`, run from a hub checkout, per ` resolves in every repository and a chain read out of the wrong one is well formed. `current`, `resume`, `chain`, and `new` each take an optional `--track` defaulting to `default`, and `link` and `tracks` take none, since a pair of issue numbers and a whole-repository survey each name -their own scope. A session working a named lane passes `--track` on each of the four that accept -it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and +their own scope. A session working a named lane passes `--track` on each subcommand that +accepts it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and comments on whatever link that lane has, closing it too where it was open. - **`current` and `resume`** answer where a track stands, `resume` adding the body itself and an @@ -306,10 +306,10 @@ comments on whatever link that lane has, closing it too where it was open. place an issue on a track until its own block says which track that is. With `--json` it prints the same rows as an array of number, title, track, labels, and updatedAt, the track null for an issue whose block is absent or unreadable, which is what a picker ranks by. -- **`new`** performs as many of the chain's three steps as the track has links for. On a track +- **`new`** performs as many of the chain's steps as the track has links for. On a track with no link at all it creates and stops, since there is nothing to comment on or close. On one whose newest link is closed it creates and comments, since the close already happened. Only a - track with an open head takes all three. **`link`** finishes a run that stopped between them, + track with an open head takes every step. **`link`** finishes a run that stopped between those steps, and separately repairs a successor whose block names no predecessor, so its own writes are the comment, the close, and that body edit. The edit reaches only the repair case, since `new` embeds the predecessor in the block at create time, so a run interrupted after the create @@ -327,7 +327,7 @@ python3 "$handoff" tracks --repo OWNER/NAME [--json] ``` Read `--dry-run` output before the first real `new` of a session, since the run can close an issue. -It is accepted by `new` and `link`, the two subcommands that write, and by no other. +It is accepted by `new` and `link`, the subcommands that write, and by no other. Exit `0` is success, `1` a refusal the caller can act on, a usage error included, and `2` the command not having run to an answer, so a refusal and a failure to reach one never share a code. A diff --git a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md index 5c1e41e59..f701ca677 100644 --- a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md @@ -25,7 +25,7 @@ description: >- The attended session, `session-handoff`'s "The Attended Session", needs the maintainer for every decision it meets. Most open issues need none, so a loop can work those while the maintainer is -away and hand back only the ones that do. Two things make that loop hard. Its orchestrator runs for +away and hand back only the ones that do. What follows makes that loop hard. Its orchestrator runs for hours, so any detail it reads is paid for again on every later round, and a loop that reads issues itself dies of its own context long before the backlog is empty. And a worker that meets a decision must stop without losing its work, in a state the maintainer's next attended session picks up with diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md index 4689dde9d..8a53aa69f 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md @@ -37,7 +37,7 @@ description: >- ## The Contract Text -`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry three rules for reading a row, one of which is about a repository declaring more than one type, so read those there. +`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry rules for reading a row, one of which is about a repository declaring more than one type, so read those there. ## After Any Workflow Edit diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md index 527e155dc..3ba870bb6 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md @@ -6,7 +6,7 @@ The section below is `WORKFLOW.md` section 5, whole. Its items and scenarios ans <!-- include: WORKFLOW.md > 5. Test Methodology --> -An agent verifies a project in three escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. +An agent verifies a project in escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. ### 5A. Static Audit (No Execution) @@ -44,7 +44,7 @@ Every probe here that opens a pull request, dispatches a workflow, or re-runs a - Open a trivial-change PR touching one target and confirm S1. *Caveat: the Docker leg logs in to the registry even on smoke and reads the buildcache, so it needs `DOCKER_HUB_*` secrets and cannot run on a fork PR (same-repo only).* - Per registry: after a real publish, query NuGet.org for the expected version + prerelease classification (and the `.snupkg` on the symbol server), and confirm a re-run added no duplicate. For PyPI read the built `dist/*` filenames out of the build job's log, `.dev0` off `develop` vs a plain version on the default branch. - Inspect the latest real publish's logs for `PublicRelease`/`SemVer2` per leg and confirm the artifact lifecycle (uploaded, consumed, deleted, with none left behind). -- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is four things, and each of them matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all four, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads all four back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. +- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is the following, and each item matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all of it, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads the evidence back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. ### Assessment diff --git a/.github/skills/agent-conduct/SKILL.md b/.github/skills/agent-conduct/SKILL.md index 8ec25a370..45c60c1f1 100644 --- a/.github/skills/agent-conduct/SKILL.md +++ b/.github/skills/agent-conduct/SKILL.md @@ -16,14 +16,14 @@ description: >- Where a sibling skill owns the moment, it wins: git-commit-conventions for committing, pr-review-conduct for review and merge claims, local-strict-review for the passes a push owes, comment-and-doc-style for prose. The GOVERNANCE.md sections it surfaces keep the full rules, - three of them carried here whole as generated includes. + several of them carried here whole as generated includes. --- # Agent Conduct ## Why This Exists -The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those three sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. +The fleet's conduct rules (verification before claiming done, asking instead of assuming, recording lessons, accounting at handoff for what was parked rather than asked, recording a blocker that lives in another repository) lived only in doc sections nothing surfaced at the moment of violation, so they were honored by whoever happened to have read them recently. This skill is the decision-moment surface. The full rules stay in `GOVERNANCE.md` ("Verification Discipline", "Communicating with the User", "Durable Knowledge and Self-Improvement"), which keeps authority, and each of those sections is carried here whole, as a generated include that `scripts/build_dist.py` fills from the section and holds to it, so the text that surfaces at the moment is the rule's own rather than a shorter list of it. The carried `AGENTS.md` "Context and Delegation Discipline" section is the always-on layer and is not carried here. A defect in included text is fixed in `GOVERNANCE.md` and regenerated, never edited in this file, per the `skill-lifecycle` Skill. ## Before Designing a Change @@ -102,18 +102,18 @@ Where a lesson lands, when it earns a mechanical hook, and where work blocked on - **A blocker filed in another repository is recorded in the repository whose work it blocks.** The binding moment is the one where the upstream issue is filed or, where it already exists, found, because that session is the one that knows what stopped and why. It owes a second issue in the repository that is waiting rather than only the first, and where the upstream issue already exists the local issue names that one and no second upstream issue is filed. The local issue states what this repository cannot do and why, in its own terms rather than as a pointer to read elsewhere, since a reader who has to open the upstream issue to learn whether it affects them opens every one of them. The local issue names the upstream one as its blocker, carries the `blocked` label, and carries whatever labels its own work would carry anyway. A comment on the upstream issue then names the local one in return, a comment rather than an edit to the body because a second repository may join the same blocker later and because the body is often not this session's to rewrite. That order, the upstream issue and then the local issue and then the backlink comment, leaves a partial failure as a record naming its blocker rather than as a blocker naming a record nobody wrote. **A handoff does not do this job.** It carries the blockers a round met, and it belongs to one track and closes with its successor, where the wait outlives every session that met it and belongs in the backlog the whole repository reads. - **The blocker record is written under the ordinary write rules, and the label on it is taken off deliberately.** Filing an issue and commenting on another are state-changing calls, so "Repository Boundaries and Write Safety" binds each of them exactly as it binds any other write, which keeps the upstream issue inside this owner and makes a blocker under a different owner a matter of explicit permission rather than of this rule. The `blocked` label is what every reader of this record selects on, so a repository not carrying it cannot host one until the fleet label set is applied there, which is a change to that repository's configuration rather than anything this rule writes, and a session that finds it missing reports that. The label comes off when the blocker clears, and the session closing the upstream issue is best placed to take it off, since the backlink comments naming every waiting repository are on the issue it is closing, while any later session that finds it cleared takes it off instead. A fix can land well before either of those, so the label lags the fix rather than tracking it, which is why a session meeting a `blocked` issue reads the state of the issue that issue's body names rather than the label. The local issue stays open when the label comes off, because the work it records still has to be done and is ordinary backlog from that moment on. - **A handoff parked on a maintainer decision carries the `blocked` label too.** Its blocker is a `decision` issue in this same repository rather than an issue elsewhere, and the parking comment on the handoff names it. A reader reads the named issue rather than the label, and the blocker clears when that issue loses its `decision` label. Unlike a cross-repository blocker, a session finding it cleared does not take the label off. It comes off only when a session hands the link back to be worked, so a loop running meanwhile never takes a link a present maintainer is still working. -- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those three conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. +- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** The conditions below must hold together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all of them (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" keeps the full rules, and the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, carries it whole as a generated include and surfaces it at its decision moments. <!-- /include --> -Two rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. +Some rules that bind at this moment have their homes elsewhere. A review flags an instance, so a fix covers the class, bounded to what the change touched or broke, stated under "Before Claiming Done" above. And a rule that keeps needing to be restated is a stale or missing skills install before it is a missing rule, per `AGENTS.md` "Where the Rules Live", with the `check-this-repo` Skill as the check. ## When Work Stops on Another Repository -The rules for this moment are two bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. +The rules for this moment are bullets in the `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" include under "When a Failure Surfaces a Lesson" above. The first, opening "A blocker filed in another repository is recorded in the repository whose work it blocks", states what the two issues are, what the local one says, the `blocked` label it carries, and the backlink the upstream issue takes in return. The second, opening "The blocker record is written under the ordinary write rules", states which write rules bind those writes, what a repository not carrying the `blocked` label owes before it can host the record, and when the label comes off. Read both there, since a restatement here would be a second copy and nothing would hold it to the first. ## Delegation, in One Paragraph -The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. The two that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. +The always-on rules live in `AGENTS.md` "Context and Delegation Discipline", loaded in every session, and are not restated here. Those that bind at a conduct moment are its rule on briefing a subagent and its rule on never tiering down the seat holding the judgment. diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md index e2718ab74..ef69d060e 100644 --- a/.github/skills/audit-a-repo/SKILL.md +++ b/.github/skills/audit-a-repo/SKILL.md @@ -8,7 +8,7 @@ description: >- ## Why This Exists -The audit is the fleet's measurement procedure, and the two failure shapes it guards against are both silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. +The audit is the fleet's measurement procedure, and the failure shapes it guards against are all silent: a repo judged conformant with no committed evidence, and an audit that quietly edits what it was supposed to measure. `AUDIT.md` in the hub is the procedure and keeps authority. This skill carries the rules that get skipped in practice and says which section owns each step. ## Before Measuring Anything diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index 53f93a66e..631c1bc7f 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists Asking for one issue to be fixed is `drive-pr`'s job and needs no skill above it. Asking for a -whole backlog to be worked down is a different problem, and three things about it are not obvious. +whole backlog to be worked down is a different problem, and several things about it are not obvious. Parallelism is bounded by file overlap rather than by agent count, so the grouping decides the throughput. The backlog is not a fixed list, since every review round files deferral issues that belong in the next round's ranking, so a plan made once is stale by its second round. And a @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. Three kinds of group are never tiered +named it for this run. These kinds of group are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds @@ -520,7 +520,7 @@ restores a line another session's write dropped. ## Ending the Run -The run ends at either of two points, and they are different endings. +The run ends at one of the points below, and they are different endings. - **The backlog is worked out**, meaning a full re-rank finds no open issue this skill can act on. That is not the same as zero open issues, since a backlog of nothing but maintainer questions and diff --git a/.github/skills/branching-and-release-model/references/branch-protection-and-promotion.md b/.github/skills/branching-and-release-model/references/branch-protection-and-promotion.md index 9ca227699..eae12059a 100644 --- a/.github/skills/branching-and-release-model/references/branch-protection-and-promotion.md +++ b/.github/skills/branching-and-release-model/references/branch-protection-and-promotion.md @@ -18,7 +18,7 @@ explicitly for a repository outside the registry. ## Executing a `develop -> main` promotion safely -Two traps, both learned the hard way: +These traps were learned the hard way: - **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` (and a repository's "Automatically delete head branches" toggle, which the fleet keeps off for diff --git a/.github/skills/branching-and-release-model/references/release-publish-mechanics.md b/.github/skills/branching-and-release-model/references/release-publish-mechanics.md index b0c3adf48..8b842510e 100644 --- a/.github/skills/branching-and-release-model/references/release-publish-mechanics.md +++ b/.github/skills/branching-and-release-model/references/release-publish-mechanics.md @@ -108,7 +108,7 @@ are intentionally not added. ## Recovering a failed registry push -A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the two recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. +A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window. **Re-dispatch, available only while the tip has not moved.** A `workflow_dispatch` takes a ref rather than a commit, and D2.3 admits only `main` or `develop`, so what it builds is that branch's tip at dispatch time. While the tip is still the commit whose push failed, a re-dispatch rebuilds the same version and runs its push again, refreshing the release the way any dispatch does. @@ -116,7 +116,7 @@ This is a time-of-check-to-time-of-use race rather than a guarded operation: not **Re-run all jobs, available inside the window whatever the tip has done.** `gh run rerun <id>` replays the run under the original event's `GITHUB_SHA` and `GITHUB_REF` and re-executes every job rather than only the failed ones. The publisher pins the release task to that commit with `ref: ${{ github.sha }}`, so `get-version` recomputes the same version from the same commit and history, each build leaf checks out the `GitCommitId` that job emits, the package artifact D5.2 deleted is rebuilt and re-uploaded rather than missing when `publish-<target>` downloads it, and that job retries the push it failed. The release itself needs nothing from the re-run, the failed run having already cut it, though on a dispatch-triggered run the re-run re-enters `github-release`, which refreshes the release per D4.4's dispatch leg and runs the `release-asset-*` delete with it per D5.2. A re-dispatch here would build the new tip instead, and NBGV derives the version from git height, so that is a further version and the one whose push failed never reaches the registry. -Three qualifications come with the re-run route. +The qualifications below come with the re-run route. - D4.4 and `WORKFLOW.md` 5B's S9 describe a re-run whose predecessor push **succeeded**, where the registry dedupes the second one. This is the case they do not cover, and its retried push is the first the registry ever receives for that version. - GitHub offers a re-run only within **30 days** of the initial run, and a repository's own **log** retention setting can be shorter, so the usable window is the shorter of the two. This is the run's own retention and is unrelated to D5.4's `retention-days: 1`, which bounds an uploaded artifact rather than the run. diff --git a/.github/skills/carried-instruction-file-guard/SKILL.md b/.github/skills/carried-instruction-file-guard/SKILL.md index c61872283..d20852942 100644 --- a/.github/skills/carried-instruction-file-guard/SKILL.md +++ b/.github/skills/carried-instruction-file-guard/SKILL.md @@ -21,7 +21,7 @@ description: >- A downstream repo's `AGENTS.md`/`GOVERNANCE.md`/`CODESTYLE.md`/`WORKFLOW.md` can hold two different kinds of content mixed in one file: sections that are stale copies of the hub's fleet-wide rules, and local rules the repo wrote for a fault the fleet has never seen elsewhere. Re-vendoring the hub's canonical version over the whole file deletes the second kind silently, because nothing about the diff looks wrong. This has actually happened: a resync replaced a repo's `AGENTS.md` wholesale with the hub's, and the repo's own local additions were gone with no error, no warning, and no review comment calling it out. -The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these four files, every time, regardless of how routine the request sounds. +The fix is not "be careful." Being careful is what failed the first time. The fix is a mechanical check you run before any overwrite touches one of these files, every time, regardless of how routine the request sounds. ## Before you touch any of these four files diff --git a/.github/skills/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md index 3e556340e..51772e1d6 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -14,7 +14,8 @@ description: >- carried instruction file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md) is being edited (no coordination references to the template or a sibling repo), and when writing a PR title or commit message (imperative subject, no vague - titles, no unsolicited Co-Authored-By, no release-bump magnitude). + titles, no unsolicited Co-Authored-By, no release-bump magnitude). Also triggers when text + states how many items a list or a table holds, since that count goes stale. --- # Comment and Doc Style @@ -65,7 +66,7 @@ Use each tool's official casing in task labels, docs, and prose: `.NET` (not `.N file, so a new topical doc is not spell-gated in CI (the editor extension still flags it live). A repo may widen its own CI list, README plus HISTORY is the default. A repo shipping no `HISTORY.md` drops it from the CI workflow, the `Lint: Spelling` task, and the GOVERNANCE.md - cspell line together, all three or none. + cspell line together, all of them or none. - **`HISTORY.md` mirrors the README's opening**: the same `# <Title>`, the same tagline verbatim (the first line after the README's H1), then its own `## Release History`. It never repeats a paragraph below the README's tagline. @@ -199,7 +200,7 @@ pull request carries the `comments` label and the gate stands down for that chan and `--allow-comments` does it for one run by hand. The label and the variable are separate deliberately, so a variable left exported reaches the commit and never the merge gate. -Three things about reaching those escapes read as a broken gate until they are known. On a public +The facts below about reaching those escapes read as a broken gate until they are known. On a public repository the lint job reads the label when it runs, and its log says "Read the pull request's labels live". A label added before then applies, and a rerun picks up one added since. In a private repository, or where the log lacks that line, the event's snapshot decides. It misses a label @@ -220,7 +221,7 @@ and the fleet's own rule documents: `AGENTS.md`, `AUDIT.md`, `CLAUDE.md`, `CODES are the repository's own narrative and keep their references, as do a commit message and a pull request body, which are the surfaces a reference belongs on. -Two carve-outs, each stated as a single case. Whatever neither of them affirmatively permits is +The carve-outs below are each stated as a single case. Whatever none of them affirmatively permits is banned by the paragraph above, which is the whole of the test and is why no list of banned cases follows. The first: **in a code or workflow comment, a URL naming an issue or a pull request on a public repository other than this one is a source citation and is permitted.** It does the same job @@ -236,11 +237,11 @@ citation beside a claim, which is the distinction this rule turns on. Separately, `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md` carry no three-part version and no commit SHA, full or abbreviated, whether a pin's value, an example, a minimum version, or a fixed constant, since a pin's copy goes stale at the next Dependabot bump and every -other kind reads exactly like one. Neither carve-out above lifts this ban. Item 3 of this skill's +other kind reads exactly like one. No carve-out above lifts this ban. Item 3 of this skill's carried-doc-references reference says what to write instead of each and carries the audit that flags a literal. -Three reasons, and the first decides it. +The reasons follow, and the first decides it. - **A reference is a second lookup, and the reader is already holding the file.** A comment earns its place by explaining the line under it to whoever reads that line now. A number they have to @@ -294,7 +295,7 @@ would report a fixture rather than a claim about this repository. ## Character set Agent-authored text is ASCII by default: documentation, code, comments, commit messages, and PR -descriptions. A non-ASCII character is read against three tiers, because whether one is +descriptions. A non-ASCII character is read against the tiers below, because whether one is typography or meaning depends on where it sits. A character in no tier is a finding rather than a silent pass, a letter of a recorded name excepted per the bullet below. @@ -384,6 +385,13 @@ Clarify Devcontainer Setup Steps in README ## Quantitative claims -A quantitative claim in `README.md` (a count, a size, a version floor, a supported-platform list) -is verified against current code before it is written. When a doc number is derived from a code -constant, mark the dependency in a source-code comment so the next editor knows to update both. +<!-- include: GOVERNANCE.md > Incidental Counts --> + +Text refers to an enumeration rather than stating its size, so it writes "the following checks", "each case", or "the rows below". This holds in every fleet repository, for docs, code and workflow comments, and docstrings alike. A stated count is a second copy of what the list or table already shows, and nothing keeps the two copies in sync. Every later edit to the enumeration then owes an edit to the sentence, and a missed one surfaces as a review finding. A word such as "both" or "all three" is a count too, where the list it counts can grow. A count is written only where the number is what the reader needs, such as a limit, a threshold, or a measured result. Such a count names its source, such as the constant or the measurement, so a reader can re-derive it. A wrong incidental count is fixed by removing it rather than by correcting it, since a corrected number goes stale the same way. + +<!-- /include --> + +A `README.md` quantitative claim (a count, a size, a version floor, a supported-platform list) is +verified against current code before it is written. When a doc number +is derived from a code constant, mark the dependency in a source-code comment so the next editor +knows to update both. diff --git a/.github/skills/comment-and-doc-style/references/carried-doc-references.md b/.github/skills/comment-and-doc-style/references/carried-doc-references.md index 9b66d1b3a..78f0091f1 100644 --- a/.github/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.github/skills/comment-and-doc-style/references/carried-doc-references.md @@ -14,7 +14,7 @@ verbatim, and this rule does not reach them. ## What is banned -Three things, in the files above: +The following are banned in the files above: 1. **Any reference to the template repo**, in prose or in a link. The coordination flow that produced a carried file is machinery a consumer of that repo should never have to see, and diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index b13de18d1..8b95032a0 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -2,7 +2,7 @@ name: dotnet-codestyle description: >- Governs C#/.NET code style for ptr727/ProjectTemplate fleet repos: the zero-warnings build - policy and its three-task clean-compile chain, central Directory.Build.props/ + policy and its clean-compile task chain, central Directory.Build.props/ Directory.Packages.props configuration, C# language and naming conventions, XML documentation, analyzer suppression scope, the library-versus-application logging split, async and error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, and AOT-compatible @@ -32,10 +32,10 @@ real, and the language, naming, logging, and testing conventions. ### Zero warnings policy -All builds must complete without warnings, enforced three ways: +All builds must complete without warnings, enforced as follows: - **The `.NET Format` clean-compile task.** It chains `CSharpier Format` -> `.NET Build` -> - `dotnet format style --verify-no-changes`. A repo carries those three task definitions in its + `dotnet format style --verify-no-changes`. A repo carries those task definitions in its own `.vscode/tasks.json`, matching the canonical `vscode-tasks.json` snippet at `github.com/ptr727/ProjectTemplate/blob/main/catalog/snippets/configs/vscode-tasks.json`. Run the `.NET Format` task after any code change, before commit. To run it natively instead, diff --git a/.github/skills/fleet-code-review/SKILL.md b/.github/skills/fleet-code-review/SKILL.md index 1843d7829..58d0149f0 100644 --- a/.github/skills/fleet-code-review/SKILL.md +++ b/.github/skills/fleet-code-review/SKILL.md @@ -87,6 +87,9 @@ Review carried fleet content by intent and fidelity. A byte-locked reference to downstream repository does not carry is not a broken link. A substantive defect in canonical content remains a finding, with the fix located at its canonical source. +A finding that a stated count is wrong suggests removing the count where it is incidental, per +`GOVERNANCE.md` "Incidental Counts", rather than correcting the number. + ## Publish Every Finding Never suppress or hide a finding because confidence is low. Investigate until it is supported diff --git a/.github/skills/git-commit-conventions/references/history-rewrite.md b/.github/skills/git-commit-conventions/references/history-rewrite.md index b331a557e..66ab26500 100644 --- a/.github/skills/git-commit-conventions/references/history-rewrite.md +++ b/.github/skills/git-commit-conventions/references/history-rewrite.md @@ -9,7 +9,7 @@ key over a commit still committed by a bot (`dependabot[bot]`, `github-actions[b own web-flow does not match its committer and lands `unknown_key`/unverified, which a require-signed-commits rule then rejects. -Two gates keep committer and signature aligned: +These gates keep committer and signature aligned: 1. **Scope the rewrite to only the commits that must be modified.** By default those are the rewriter's own, whose committer already matches, so a commit that needs no change stays out of diff --git a/.github/skills/local-strict-review/SKILL.md b/.github/skills/local-strict-review/SKILL.md index f04d3d70a..64e9b8cf4 100644 --- a/.github/skills/local-strict-review/SKILL.md +++ b/.github/skills/local-strict-review/SKILL.md @@ -33,7 +33,7 @@ Dispatches one read-only subagent against this branch's full diff since it forke The subagent reads the full content of every file the diff and the untracked-file list touch, not just the hunks, since cross-file and whole-file context is exactly what incremental review misses. It reports findings only. It never fixes, stages, or commits anything. -Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out three traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. +Review criteria are `fleet-code-review`'s "Review the Change" section, reused rather than restated here. Before any other reviewer sees the diff, this pass also calls out these traps: unguarded type coercions, TOCTOU/race conditions, and platform-specific behavior differences. Each is called out only where the code's actual deployment can produce it, per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort". The brief also asks the reviewer to search for an existing helper doing the job of each new one, per `GOVERNANCE.md` "Design Before Code". `fleet-code-review`'s separate "Publish Every Finding" section does not apply here. This skill's report contract below replaces it, since there is no PR to comment on and no coverage marker to close a review with. ## Running It @@ -109,7 +109,7 @@ A second pass under the same rule, run in the repository that authors canonical **The unit is what a reviewer reads whole**, and `spec/files.json` rather than the document decides which, down to which files carry units at all. `canonical_review.py list` names the whole set and is the authority on it, so the rules are not paraphrased here, where a paraphrase can only drift from them. In the ordinary case a unit is one level-two section of a carried Markdown canonical, and `sweep` names each one it wants exactly as `record` takes it. The pass reads that unit's whole current text rather than the diff that moved it, because reproducing the carrier's read is the entire point, and a diff with surrounding context is a different read the pass above has already done. -Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and all three are below. +Run it at the same model tier and in the same delegation shape as the pass above. The brief, the engine, and its flags each differ, and each is below. ```text Task: adversarial review of one canonical unit, read as a repository carrying it for the first diff --git a/.github/skills/merge-and-release/SKILL.md b/.github/skills/merge-and-release/SKILL.md index fabc25de5..a6a397746 100644 --- a/.github/skills/merge-and-release/SKILL.md +++ b/.github/skills/merge-and-release/SKILL.md @@ -21,8 +21,8 @@ description: >- ## Why This Exists -Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same two steps follow -every time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can +Once drive-pr (or a maintainer by hand) leaves a promotion PR ready, the same steps follow every +time: merge it, and usually dispatch the release it unblocks. In this hub a promotion can also change `.agents/skills` content this very session depends on, so the release step always carries a Skills refresh with it there, never a separate branch to ask about, an ambiguous "merge and release" on the hub must not leave the maintainer unsure whether Skills got refreshed. One @@ -156,8 +156,7 @@ skill covers all of it, scoped down by what the maintainer actually asks for. `docs/host-setup.md` "Fleet Skills Install" cadence. 8. Run cleanup regardless of how steps 5 through 7 ended, no release configured, a dispatch failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh all still - reach this step, the merge in step 3 already landed by then. Two parts, both required, neither - optional: + reach this step, the merge in step 3 already landed by then. Every part below is required: - The promotion PR's own worktree: fetch and prune, remove the worktree, then bring the base clone to current `develop`, checking `develop` out first only where it sits on another branch, and fast-forwarding it with `git merge --ff-only origin/develop`. Where that checkout diff --git a/.github/skills/python-codestyle/references/code-style.md b/.github/skills/python-codestyle/references/code-style.md index cca918e1b..45f355bcd 100644 --- a/.github/skills/python-codestyle/references/code-style.md +++ b/.github/skills/python-codestyle/references/code-style.md @@ -49,7 +49,7 @@ carries that as the host floor `scripts/host_gate.py` enforces, so 3.10+-only syntax (`X | None`, `match`, etc.) needs no quoting, no `typing.Union` fallback, and no `from __future__ import annotations` guard on that account alone. Add that import only when a real forward reference - needs it, per the bullet above. Three named exceptions carry a lower floor on purpose and say so + needs it, per the bullet above. Named exceptions carry a lower floor on purpose and say so themselves: `scripts/skills_install.sh` and the `install-skills.*` bootstrap scripts, which must run on whatever interpreter a host already has before this floor's toolchain exists to install one, and `spec/resolve_description.py`, which `repo-config/configure.sh`'s own bootstrap probe diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 9a71b3fef..46bbda6bb 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -66,9 +66,9 @@ from `develop` unless the task is explicitly about `main`-only content, per `GOV is whatever it last fetched rather than the branch it names. The base clone is a fetch source, not a place to do task work. `fetch` and `worktree add` run -against it for that purpose, and outside two steps below, nothing else mutates its working tree, +against it for that purpose, and outside the steps below, nothing else mutates its working tree, index, or HEAD while a task is in progress: never `checkout`, `pull`, `reset`, `commit`, or any -other such command. Those two are "Listing and Cleanup"'s own terminal step and "Creating a +other such command. The steps are "Listing and Cleanup"'s own terminal step and "Creating a Worktree"'s return of the base clone to its own working branch where a continuation finds the task branch checked out there. That distinction is the one a real incident missed, where an agent reused a primary checkout as the working directory itself rather than only as the source a @@ -125,7 +125,7 @@ list`, the fleet worktree directory, and IDE worktree discovery. It also leaves durable location the maintainer can inspect after the agent session ends. Before running that command, inspect the executor's active write boundaries. A linked worktree -requires write access to both of these locations: +requires write access to each of these locations: - the intended `~/repos/worktrees/<Repo>-<task-slug>` worktree directory - the base clone's `.git/worktrees/` administrative directory, which holds the index and locks diff --git a/.github/skills/session-handoff/SKILL.md b/.github/skills/session-handoff/SKILL.md index 1edc97bbc..235ed135f 100644 --- a/.github/skills/session-handoff/SKILL.md +++ b/.github/skills/session-handoff/SKILL.md @@ -23,7 +23,7 @@ description: >- ## Why This Exists -A handoff written to a scratch file fails three ways the chain closes. The file is not found where +The chain closes each way a handoff written to a scratch file fails. The file is not found where the next session looks. More than one candidate file is found and nothing says which one is current. And there is no history at all, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is precisely the record of what has already @@ -65,7 +65,7 @@ without trusting the parts of it that have gone stale. - **A platform lane is for work that needs its platform.** A platform track is one kept for a single host platform, such as `wsl` or `windows`. It takes an issue only when its defect reproduces, or its fix can be verified, on that platform alone. It also needs that no Linux host outside WSL can - both fix the issue and verify the fix. An issue meeting both goes to its platform's lane. Any + both fix the issue and verify the fix. An issue meeting each condition goes to its platform's lane. Any other issue goes to the ordinary backlog, since a lane costs a separate chain and a host-bound session. A defect in WSL interop, the `WSLInterop` binfmt handler, `/mnt/<drive>` paths, or `wslpath` reproduces only inside WSL. So does running a Windows `.exe` from Linux, although WSL @@ -73,7 +73,7 @@ without trusting the parts of it that have gone stale. path and ACL semantics, reproduces only on Windows. Either still goes to the backlog where a Linux host outside WSL verifies its fix, as a plain unit test of a path translation can. An issue in systemd user scopes, apt, a bash script, or a `pwsh` script run in a Linux container gets no lane - by its topic. The two conditions decide it like any other issue. Where only part of an issue needs + by its topic. The conditions decide it like any other issue. Where only part of an issue needs a platform, the session finding it files each such part as its own issue for its platform's lane. The original is narrowed to the rest, or closed in favor of those issues where nothing is left. A session that may not write issues names the unsplit issue in its report or handoff instead. An @@ -96,7 +96,7 @@ without trusting the parts of it that have gone stale. a given repository, read from each issue's metadata block rather than from the label, since an issue carrying the label and no block belongs to no readable track and blocks the count rather than joining it. Two or more on one track is a defect to report, never one to resolve by picking, - and a run interrupted between the three steps above is the one shape of it that `link` settles + and a run interrupted between the steps above is the one shape of it that `link` settles rather than a human. - **Zero open on a track is two different states.** The track has never had a handoff, or its lane was closed out and its newest link is closed. `new` chains onto the newest link either way, open @@ -170,7 +170,7 @@ comments. Read the chain before re-attempting anything. `resume` prints the current body and indexes the closed links behind it, and `chain --grep` searches the bodies it walks for a regular expression, which is how a session answers whether a path has already been tried without reading every round. -Three things bound that answer. The walk starts at one track's newest link and follows each +The points below bound that answer. The walk starts at one track's newest link and follows each marker's `previous=` from there, which is normally that one lane and is in fact whatever the markers name, so every link prints its own track and a predecessor on another lane shows up rather than passing unseen. The walk stops at `--limit` and names the link it stopped short of, so a capped @@ -255,9 +255,9 @@ it. ## The Parked Decision Queue The rules below are `GOVERNANCE.md` "Communicating with the User", carried whole so this skill works -in isolation. Two of them state the obligation: the one opening "A question filed as an issue is +in isolation. Some of them state the obligation: the one opening "A question filed as an issue is parked rather than asked" and the one opening "The session that writes a handoff presents the parked -queue in the same act". Three more state the form the questions take, the one opening "Ask every +queue in the same act". Others state the form the questions take, the one opening "Ask every question through the interface's prompt, never in prose", which also names the numbered list as the fallback where no prompt exists, the one opening "Raise work blocked on the user as a direct interactive prompt", which shapes the options, and the one opening "Lead every choice with a @@ -290,8 +290,8 @@ The mechanics are the hub's `scripts/handoff.py`, run from a hub checkout, per ` resolves in every repository and a chain read out of the wrong one is well formed. `current`, `resume`, `chain`, and `new` each take an optional `--track` defaulting to `default`, and `link` and `tracks` take none, since a pair of issue numbers and a whole-repository survey each name -their own scope. A session working a named lane passes `--track` on each of the four that accept -it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and +their own scope. A session working a named lane passes `--track` on each subcommand that +accepts it. Omitting it does more than read the wrong chain, since `new` then files onto `default` and comments on whatever link that lane has, closing it too where it was open. - **`current` and `resume`** answer where a track stands, `resume` adding the body itself and an @@ -306,10 +306,10 @@ comments on whatever link that lane has, closing it too where it was open. place an issue on a track until its own block says which track that is. With `--json` it prints the same rows as an array of number, title, track, labels, and updatedAt, the track null for an issue whose block is absent or unreadable, which is what a picker ranks by. -- **`new`** performs as many of the chain's three steps as the track has links for. On a track +- **`new`** performs as many of the chain's steps as the track has links for. On a track with no link at all it creates and stops, since there is nothing to comment on or close. On one whose newest link is closed it creates and comments, since the close already happened. Only a - track with an open head takes all three. **`link`** finishes a run that stopped between them, + track with an open head takes every step. **`link`** finishes a run that stopped between those steps, and separately repairs a successor whose block names no predecessor, so its own writes are the comment, the close, and that body edit. The edit reaches only the repair case, since `new` embeds the predecessor in the block at create time, so a run interrupted after the create @@ -327,7 +327,7 @@ python3 "$handoff" tracks --repo OWNER/NAME [--json] ``` Read `--dry-run` output before the first real `new` of a session, since the run can close an issue. -It is accepted by `new` and `link`, the two subcommands that write, and by no other. +It is accepted by `new` and `link`, the subcommands that write, and by no other. Exit `0` is success, `1` a refusal the caller can act on, a usage error included, and `2` the command not having run to an answer, so a refusal and a failure to reach one never share a code. A diff --git a/.github/skills/unattended-handoff/SKILL.md b/.github/skills/unattended-handoff/SKILL.md index 5c1e41e59..f701ca677 100644 --- a/.github/skills/unattended-handoff/SKILL.md +++ b/.github/skills/unattended-handoff/SKILL.md @@ -25,7 +25,7 @@ description: >- The attended session, `session-handoff`'s "The Attended Session", needs the maintainer for every decision it meets. Most open issues need none, so a loop can work those while the maintainer is -away and hand back only the ones that do. Two things make that loop hard. Its orchestrator runs for +away and hand back only the ones that do. What follows makes that loop hard. Its orchestrator runs for hours, so any detail it reads is paid for again on every later round, and a loop that reads issues itself dies of its own context long before the backlog is empty. And a worker that meets a decision must stop without losing its work, in a state the maintainer's next attended session picks up with diff --git a/.github/skills/workflow-ci-contract/SKILL.md b/.github/skills/workflow-ci-contract/SKILL.md index 4689dde9d..8a53aa69f 100644 --- a/.github/skills/workflow-ci-contract/SKILL.md +++ b/.github/skills/workflow-ci-contract/SKILL.md @@ -37,7 +37,7 @@ description: >- ## The Contract Text -`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry three rules for reading a row, one of which is about a repository declaring more than one type, so read those there. +`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry rules for reading a row, one of which is about a repository declaring more than one type, so read those there. ## After Any Workflow Edit diff --git a/.github/skills/workflow-ci-contract/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md index 527e155dc..3ba870bb6 100644 --- a/.github/skills/workflow-ci-contract/references/test-methodology.md +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -6,7 +6,7 @@ The section below is `WORKFLOW.md` section 5, whole. Its items and scenarios ans <!-- include: WORKFLOW.md > 5. Test Methodology --> -An agent verifies a project in three escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. +An agent verifies a project in escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. ### 5A. Static Audit (No Execution) @@ -44,7 +44,7 @@ Every probe here that opens a pull request, dispatches a workflow, or re-runs a - Open a trivial-change PR touching one target and confirm S1. *Caveat: the Docker leg logs in to the registry even on smoke and reads the buildcache, so it needs `DOCKER_HUB_*` secrets and cannot run on a fork PR (same-repo only).* - Per registry: after a real publish, query NuGet.org for the expected version + prerelease classification (and the `.snupkg` on the symbol server), and confirm a re-run added no duplicate. For PyPI read the built `dist/*` filenames out of the build job's log, `.dev0` off `develop` vs a plain version on the default branch. - Inspect the latest real publish's logs for `PublicRelease`/`SemVer2` per leg and confirm the artifact lifecycle (uploaded, consumed, deleted, with none left behind). -- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is four things, and each of them matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all four, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads all four back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. +- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is the following, and each item matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all of it, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads the evidence back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. ### Assessment diff --git a/AGENTS.md b/AGENTS.md index 87a67fdcd..823568182 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ **ProjectTemplate** exists for agent enablement across a fleet of related projects, so an agent works over all of them autonomously and productively, at repeatable quality, inside guardrails that keep the autonomy from doing harm. It holds the portable rules those projects follow, a machine-readable ground-truth spec ([`spec/`](./spec/)), a registry of the projects ([`registry/repos.json`](./registry/repos.json)), and an audit-agent instruction set ([`AUDIT.md`](./AUDIT.md)). It ships no sample application code. -This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). +This file is the entry point every coding agent reads first, and it holds only these: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A Skill that needs a rule's full text to work in isolation carries it as a generated include from the rule's home rather than as a copy, per the `skill-lifecycle` Skill, so the text cannot drift undetected. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. @@ -26,7 +26,7 @@ flowchart TD - **A repository with the instruction set, current or stale.** Follow the hub's `RESYNC.md`, which runs `AUDIT.md` end to end for the findings and then applies each one in an order that matters, since the rules govern what comes after them, a deletion must precede the re-vendor that would otherwise refresh the file, and only some findings are mechanically detectable at all. An audit that reports drift and stops is half the procedure. - **A repository that believes it is conformant.** Run the audit anyway, because conformance asserted without a report is conformance nobody can check. The hub commits that report under its own `reports/`, since a report written by the repository it measures is a claim rather than evidence, so a session in the repository being audited fixes its own drift in its own repository, files its findings about the hub as issues, and leaves the report to a hub-side audit rather than opening a hub pull request to write its own. This is the same procedure as the case above and is listed separately only because it is the one most often skipped. -Three rules bound every path above. **Read the hub's `main` branch as ground truth**, since that is the promoted and gated state, and read `develop` only to detect divergence. **Reach the hub as a checkout of your own and fetch it immediately before reading it**, because a clone is whatever it last fetched rather than the branch it names, and work only in that checkout rather than in one that another task is using, per [`GOVERNANCE.md`](./GOVERNANCE.md) "Repository Boundaries and Write Safety" and "Hub-Hosted Tooling". And **the audit is read-only**: it produces a report and never edits the repository it measures, so a fix is a separate, reviewable change. +The rules below bound every path above. **Read the hub's `main` branch as ground truth**, since that is the promoted and gated state, and read `develop` only to detect divergence. **Reach the hub as a checkout of your own and fetch it immediately before reading it**, because a clone is whatever it last fetched rather than the branch it names, and work only in that checkout rather than in one that another task is using, per [`GOVERNANCE.md`](./GOVERNANCE.md) "Repository Boundaries and Write Safety" and "Hub-Hosted Tooling". And **the audit is read-only**: it produces a report and never edits the repository it measures, so a fix is a separate, reviewable change. ## Context and Delegation Discipline @@ -37,7 +37,7 @@ An agent session is billed on the context it carries, not the work it does. Ever - **One deliverable, one session.** A session covers one branch and one deliverable, and ends when that work merges. A multi-step task is one deliverable and stays in one session. Two unrelated tasks are two sessions even when they run back to back. - **End a session at any of these, without being asked:** the branch changes, the pull request merges, or the next task is unrelated to the last. A review round is none of them. A loop still producing findings is the deliverable in progress, and a round count is not a reason to leave one open. - **A session orchestrating dispatched work is an exception, and a narrow one.** Its deliverable is the run rather than any branch, so it spans many branches and many merges by construction, and ending it at the first dispatched merge would end the run. The triggers above land on each dispatched task instead, one branch and one deliverable each, which is this rule applied rather than waived. What keeps the exception narrow is that such a session holds no branch of its own and authors none of the work it dispatches, so the file context every other session accumulates is context it never takes on, and it re-derives each round's state from live sources rather than holding it, per "Re-derive state, do not carry it" below. A session that starts editing the files a dispatched task would have edited is an ordinary one again and ends on the triggers above. -- **Hand off in an issue, never in a scratch file and never in context.** Close a session by filing the next link in the handoff chain of the repository holding the work the next session resumes, which for a session that stayed in one repository is this one, and a session that spanned several names the others in the handoff's state section. A track is a lane of work named by a short slug, `default` where a session names none, and a track in use holds exactly one open issue carrying the `handoff` label. The track and the predecessor are recorded in the issue body rather than in its title, so a retitled or hand-edited issue still chains and a reader can tell which lane an open issue belongs to. The new link names its predecessor that way, a forward-link comment then goes onto that predecessor, and the predecessor is closed last, in that order, so a failure part way leaves a discoverable new issue rather than a closed chain with no successor. Each of those three is an outward-facing write, so the write-safety rules in `GOVERNANCE.md` "Repository Boundaries and Write Safety" bind all three exactly as they bind any other write, the identifier rule most of all, since the link a comment and a close target is read live in the same run rather than remembered. The chain needs the `handoff` label to be findable at all, so a repository not carrying it cannot host one until the label exists there, which on a fleet repository is a change to that repository's configuration rather than anything a handoff writes. A session that cannot file a link reports that it could not hand off and leaves the previous link open, whether it is stopped by a repository carrying no such label, by a write it may not make, or by anything else. That is the one alternative this rule allows to a track still in use, and it is a report rather than a file, because a report says the round's record is missing while a file claims to be it. A track whose work is complete is closed out instead, its last link carrying the outcome as a comment and closed with no successor, which leaves the track no longer in use rather than breaking its chain. A scratch file fails three ways the chain closes. It is not found where the next session looks. More than one candidate is found and nothing says which is current. And it holds no history, so a later round re-runs a path an earlier round already tried and already wrote down, which is the one thing a handoff exists to prevent. The handoff carries the next steps in priority order, the external blockers and internal dependencies among them, the state a resume re-reads rather than trusts, listed so the resume knows what to re-read, the account of parked decisions that `GOVERNANCE.md` "Communicating with the User" requires, what the last round did, what not to repeat, and what was learned. That section states the account whole, and it requires the session to present those decisions as well as record them. **The size rule is stated per section.** An entry earns its place by being specific enough to change a later session's behavior, a section ranks what it keeps and drops whatever does not meet that bar, and what belongs somewhere durable goes there and appears here as one line and a pointer, a defect as an issue, a rule as rule text, a lesson as governance prose. The parked-decision account keeps the count and the ranked questions one round can carry, and names every issue past those by number alone, which is what keeps a queue larger than one round inside this rule. A summary held in context is re-billed until the session ends, a scratch file is read only on the machine holding it, and a closed link stays readable from any machine to every session after it. +- **Hand off in an issue, never in a scratch file and never in context.** Close a session by filing the next link in the handoff chain of the repository holding the work the next session resumes, which for a session that stayed in one repository is this one, and a session that spanned several names the others in the handoff's state section. A track is a lane of work named by a short slug, `default` where a session names none, and a track in use holds exactly one open issue carrying the `handoff` label. The track and the predecessor are recorded in the issue body rather than in its title, so a retitled or hand-edited issue still chains and a reader can tell which lane an open issue belongs to. The new link names its predecessor that way, a forward-link comment then goes onto that predecessor, and the predecessor is closed last, in that order, so a failure part way leaves a discoverable new issue rather than a closed chain with no successor. Each of those is an outward-facing write, so the write-safety rules in `GOVERNANCE.md` "Repository Boundaries and Write Safety" bind each of them exactly as they bind any other write, the identifier rule most of all, since the link a comment and a close target is read live in the same run rather than remembered. The chain needs the `handoff` label to be findable at all, so a repository not carrying it cannot host one until the label exists there, which on a fleet repository is a change to that repository's configuration rather than anything a handoff writes. A session that cannot file a link reports that it could not hand off and leaves the previous link open, whether it is stopped by a repository carrying no such label, by a write it may not make, or by anything else. That is the one alternative this rule allows to a track still in use, and it is a report rather than a file, because a report says the round's record is missing while a file claims to be it. A track whose work is complete is closed out instead, its last link carrying the outcome as a comment and closed with no successor, which leaves the track no longer in use rather than breaking its chain. A scratch file fails in ways the chain closes. It is not found where the next session looks. More than one candidate is found and nothing says which is current. And it holds no history, so a later round re-runs a path an earlier round already tried and already wrote down, which is the one thing a handoff exists to prevent. The handoff carries the next steps in priority order, the external blockers and internal dependencies among them, the state a resume re-reads rather than trusts, listed so the resume knows what to re-read, the account of parked decisions that `GOVERNANCE.md` "Communicating with the User" requires, what the last round did, what not to repeat, and what was learned. That section states the account whole, and it requires the session to present those decisions as well as record them. **The size rule is stated per section.** An entry earns its place by being specific enough to change a later session's behavior, a section ranks what it keeps and drops whatever does not meet that bar, and what belongs somewhere durable goes there and appears here as one line and a pointer, a defect as an issue, a rule as rule text, a lesson as governance prose. The parked-decision account keeps the count and the ranked questions one round can carry, and names every issue past those by number alone, which is what keeps a queue larger than one round inside this rule. A summary held in context is re-billed until the session ends, a scratch file is read only on the machine holding it, and a closed link stays readable from any machine to every session after it. - **Re-derive state, do not carry it.** "This session already has the context" is the signal to split, not to continue. Context that has gone stale is worse than absent, because a file read hundreds of requests ago no longer describes the file. - **Compaction is a fallback, not the strategy.** It restarts context from a floor and climbs again, where a fresh session starts from zero. @@ -84,8 +84,8 @@ If a rule you were given does not cover what you find, stop and report it. Do no ``` - **Wait in a background process, not in a poll loop.** A review or CI wait is a sequence of near-identical requests, each billed for whatever context it happens to carry. Run the wait as one backgrounded command rather than as a sequence of turns. -- **A wait separates three outcomes, and says which one it reached.** The condition was met, it has not been met yet, and the wait cannot reach it at all are three different results, and a backgrounded wait that emits nothing renders all three identically. Run the command once in the foreground and read its output before backgrounding it, because a wait is only as good as the command inside it, and an unsupported flag on the installed tool version exits non-zero with an empty stdout that every naive test reads as "nothing yet". Never let a fallback stand in for a failed command, since `|| echo '[]'`, `|| true`, and `2>/dev/null` convert an error into that same reading, which is the suppression the write-safety rules already forbid on a mutation. Make the wait emit on failure as loudly as on success, so silence means "still running" and nothing else, and bound it, so a condition that is never coming ends in a report rather than in another wait. -- **Never write a wait as an unbounded shell loop.** This is a prohibition rather than a preference. A loop that waits for something, with no bound anywhere in the command that runs it, is forbidden. That holds in a tool call, in a script, and in a brief handed to a subagent. The bound goes inside that command. The wait then says which of three things it found: the condition met, the bound reached, or the check itself failing. **Prefer the mechanism that already signals.** Where the dispatch mechanism reports a subagent's completion itself, polling that subagent's output file is a second channel. The answer is already on its way. **The agent that starts a wait owns the process it leaves.** A process a tool call leaves running survives the turn, the subagent, and the run that dispatched it. So a run that dispatched workers does not report itself done while it cannot say what it left running. +- **A wait separates its outcomes, and says which one it reached.** The condition was met, it has not been met yet, and the wait cannot reach it at all are different results, and a backgrounded wait that emits nothing renders them identically. Run the command once in the foreground and read its output before backgrounding it, because a wait is only as good as the command inside it, and an unsupported flag on the installed tool version exits non-zero with an empty stdout that every naive test reads as "nothing yet". Never let a fallback stand in for a failed command, since `|| echo '[]'`, `|| true`, and `2>/dev/null` convert an error into that same reading, which is the suppression the write-safety rules already forbid on a mutation. Make the wait emit on failure as loudly as on success, so silence means "still running" and nothing else, and bound it, so a condition that is never coming ends in a report rather than in another wait. +- **Never write a wait as an unbounded shell loop.** This is a prohibition rather than a preference. A loop that waits for something, with no bound anywhere in the command that runs it, is forbidden. That holds in a tool call, in a script, and in a brief handed to a subagent. The bound goes inside that command. The wait then says which of these it found: the condition met, the bound reached, or the check itself failing. **Prefer the mechanism that already signals.** Where the dispatch mechanism reports a subagent's completion itself, polling that subagent's output file is a second channel. The answer is already on its way. **The agent that starts a wait owns the process it leaves.** A process a tool call leaves running survives the turn, the subagent, and the run that dispatched it. So a run that dispatched workers does not report itself done while it cannot say what it left running. ## Where the Rules Live @@ -121,7 +121,7 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md) un | The About panel, description, or repo toggles | `Repository Details` | | Where a file belongs in the tree | `Repository Layout` | -A row above naming no Skill, or naming one only for part of its section, is doc-only by decision rather than by omission, and the reason differs by row. `Foundational Principles` is rationale read once rather than a procedure. `Repository Boundaries and Write Safety` and `Representative Data in Agent-Authored Text` are always-on law that binds whether or not a Skill fires, which is why the boundaries row names `repo-worktree` only for the one moment in it narrow enough to surface, isolating into a worktree at task start, on top of that law rather than instead of it. The `gh-write-guard` hook and the host-wide instruction blocks maintained by the hub's own agent-safety installer, hub-local at `host-setup/agent-safety/`, are the boundaries section's mechanical layer, while the data section has none, since no pattern decides it. `Running the Linters Locally (Known-Working Invocations)` is hub-only, so a carrier reaches it in a hub checkout rather than surfacing it. `Verification Discipline` carries its Skills on its other two rows. And `Hub-Hosted Tooling`, `Supported Development Platforms`, `Devcontainer`, `Editor and Tasks`, `Repository Details`, and `Repository Layout` are short reference sections a task reads at the moment it touches their subject, each already routed to by the procedures and Skills that need it. +A row above naming no Skill, or naming one only for part of its section, is doc-only by decision rather than by omission, and the reason differs by row. `Foundational Principles` is rationale read once rather than a procedure. `Repository Boundaries and Write Safety` and `Representative Data in Agent-Authored Text` are always-on law that binds whether or not a Skill fires, which is why the boundaries row names `repo-worktree` only for the one moment in it narrow enough to surface, isolating into a worktree at task start, on top of that law rather than instead of it. The `gh-write-guard` hook and the host-wide instruction blocks maintained by the hub's own agent-safety installer, hub-local at `host-setup/agent-safety/`, are the boundaries section's mechanical layer, while the data section has none, since no pattern decides it. `Running the Linters Locally (Known-Working Invocations)` is hub-only, so a carrier reaches it in a hub checkout rather than surfacing it. `Verification Discipline` carries its Skills on its other rows. And `Hub-Hosted Tooling`, `Supported Development Platforms`, `Devcontainer`, `Editor and Tasks`, `Repository Details`, and `Repository Layout` are short reference sections a task reads at the moment it touches their subject, each already routed to by the procedures and Skills that need it. Some of the rules above are also packaged as Claude Code / opencode / Codex Skills, hand-authored at `.agents/skills/` in the hub (not a repo-relative link here, since that path is hub-local and not carried into every fleet repo), so they surface automatically instead of needing to be re-read every session. `scripts/` is hub-hosted and reached rather than carried, per "Hub-Hosted Tooling", so run the installer from a hub checkout: `python3 scripts/skills_install.py` (or the `.sh`/`.ps1` wrapper) once per machine, from `github.com/ptr727/ProjectTemplate`, installs them for every repo touched from that machine. `python3 scripts/skills_install.py --report`, also from a hub checkout, says whether this machine is current. A rule that keeps needing to be restated is a sign the install is missing or stale, not that the rule does not exist. Keeping a repo's own carried `.github/copilot-instructions.md` in sync with the hub, without losing that repo's own "Disproved Claims" ledger entries in the process, is `copilot-instructions-keeper`, a skill about maintaining that file rather than a rule extracted from it, since the file itself is read directly by the Copilot bot and stays fully intact everywhere it is carried. Checking, from inside this repo's own session with no operator watching, whether this repo and this machine are actually current against the hub is `check-this-repo`, new content rather than a rule extracted from a section, the counterpart to `resync-a-repo` that needs no standing hub checkout or named target beyond the repo the session is already in, even though its own check fetches a hub checkout to reach `scripts/skills_install.py`. Opening a pull request against a repository outside this fleet, one the maintainer does not control, follows a different workflow entirely, new content rather than a rule extracted from a section, packaged as `upstream-contribution-workflow` and independent of the target repo's own type or workflow model. Isolating a task into its own worktree before its first file edit, with the base-branch choice, the layout convention, and the cleanup mechanics, is `repo-worktree`, the task-start surface of the `Repository Boundaries and Write Safety` law, which keeps the rule. Writing the handoff that "Session Scope" above requires, and resuming from one, is `session-handoff`, new content rather than a rule extracted from a section, since deciding what actually earns a place in each of the sections that rule names is judgment rather than a shape. It carries `GOVERNANCE.md` "Communicating with the User" whole as a generated include, that being the parked-decision account the rule owes, and it names the hub's `scripts/handoff.py` for the chain's mechanics. Its widest trigger is the one that earns it, about to re-attempt something a previous round may already have tried, since a session that does not know a chain exists never goes looking for one. Creating, changing, or retiring one of these skills is itself packaged as `skill-lifecycle`, hub-context only, since `.agents/skills/` exists only in the hub and the generated plugin tree is never hand-edited. diff --git a/CODESTYLE.md b/CODESTYLE.md index cb68b1809..fa23048c6 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses the `pre-commit` framework instead. Any repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, its per-clone enablement steps, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses the `pre-commit` framework instead. Any repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. Canonical shapes for Husky.Net and the `pre-commit` framework live in `catalog/snippets/` in the hub. That is not a repo-relative path, since it is hub-local and not carried into every fleet repo. What that gate must cover, its per-clone enablement steps, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions @@ -49,7 +49,7 @@ These apply repo-wide, in every directory: Markdown lints clean via `markdownlin *This section applies only to the .NET side. A repo with no .NET projects still carries it (the file is carried whole) and ignores it.* -The style guide for any .NET projects in this repo: the zero-warnings build policy and its three-task clean-compile chain, central `Directory.Build.props`/`Directory.Packages.props` configuration, C# language and naming conventions, XML documentation, analyzer suppression scope, the library-versus-application logging split, async and error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, the runner declaration, package references and version floor that an MTP-based test project needs under `WORKFLOW.md` D1.6, with the local diagnostic for a run that reports no tests, and AOT-compatible project configuration. +The style guide for any .NET projects in this repo: the zero-warnings build policy and its clean-compile task chain, central `Directory.Build.props`/`Directory.Packages.props` configuration, C# language and naming conventions, XML documentation, analyzer suppression scope, the library-versus-application logging split, async and error-handling patterns, xUnit v3 + AwesomeAssertions testing conventions, the runner declaration, package references and version floor that an MTP-based test project needs under `WORKFLOW.md` D1.6, with the local diagnostic for a run that reports no tests, and AOT-compatible project configuration. This is packaged as the `dotnet-codestyle` Skill at `.agents/skills/dotnet-codestyle/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the scope. Read the skill for the full rules, code examples, and mechanics. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 8cc07ad36..9038d571a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -20,7 +20,7 @@ The specific rules in this file implement a few governing principles. Read these - **A blocker filed in another repository is recorded in the repository whose work it blocks.** The binding moment is the one where the upstream issue is filed or, where it already exists, found, because that session is the one that knows what stopped and why. It owes a second issue in the repository that is waiting rather than only the first, and where the upstream issue already exists the local issue names that one and no second upstream issue is filed. The local issue states what this repository cannot do and why, in its own terms rather than as a pointer to read elsewhere, since a reader who has to open the upstream issue to learn whether it affects them opens every one of them. The local issue names the upstream one as its blocker, carries the `blocked` label, and carries whatever labels its own work would carry anyway. A comment on the upstream issue then names the local one in return, a comment rather than an edit to the body because a second repository may join the same blocker later and because the body is often not this session's to rewrite. That order, the upstream issue and then the local issue and then the backlink comment, leaves a partial failure as a record naming its blocker rather than as a blocker naming a record nobody wrote. **A handoff does not do this job.** It carries the blockers a round met, and it belongs to one track and closes with its successor, where the wait outlives every session that met it and belongs in the backlog the whole repository reads. - **The blocker record is written under the ordinary write rules, and the label on it is taken off deliberately.** Filing an issue and commenting on another are state-changing calls, so "Repository Boundaries and Write Safety" binds each of them exactly as it binds any other write, which keeps the upstream issue inside this owner and makes a blocker under a different owner a matter of explicit permission rather than of this rule. The `blocked` label is what every reader of this record selects on, so a repository not carrying it cannot host one until the fleet label set is applied there, which is a change to that repository's configuration rather than anything this rule writes, and a session that finds it missing reports that. The label comes off when the blocker clears, and the session closing the upstream issue is best placed to take it off, since the backlink comments naming every waiting repository are on the issue it is closing, while any later session that finds it cleared takes it off instead. A fix can land well before either of those, so the label lags the fix rather than tracking it, which is why a session meeting a `blocked` issue reads the state of the issue that issue's body names rather than the label. The local issue stays open when the label comes off, because the work it records still has to be done and is ordinary backlog from that moment on. - **A handoff parked on a maintainer decision carries the `blocked` label too.** Its blocker is a `decision` issue in this same repository rather than an issue elsewhere, and the parking comment on the handoff names it. A reader reads the named issue rather than the label, and the blocker clears when that issue loses its `decision` label. Unlike a cross-repository blocker, a session finding it cleared does not take the label off. It comes off only when a session hands the link back to be worked, so a loop running meanwhile never takes a link a present maintainer is still working. -- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those three conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. +- **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** The conditions below must hold together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all of them (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under `GOVERNANCE.md` "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. `GOVERNANCE.md` "Durable Knowledge and Self-Improvement" keeps the full rules, and the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, carries it whole as a generated include and surfaces it at its decision moments. @@ -32,10 +32,10 @@ A state-changing GitHub call is the highest-blast-radius thing an agent does her - **Provider connectors are read-only for fleet work.** Use a provider's GitHub connector for reads where it helps. Perform each GitHub mutation through the documented hub tool, or through authenticated `gh` where no tool owns the operation. This gives Codex, Claude, opencode, and a terminal session one write path with the same checks. It also avoids a connector mutation that predictably lacks repository authorization while the verified `gh` session already has it. A provider-specific instruction may explain how to reach the common path. It never replaces that path with its own mutation surface. - **Never fabricate, guess, or reuse an identifier passed to a write.** Capture every identifier a state-changing call consumes from a live query in the **same** session. This includes node, numeric, thread, and comment ids. Pass the captured value directly. Do not hand-type an id, recall it from another session, or copy it from documentation or an example. Ids commonly resolve **globally**, so a wrong-but-valid id does not fail. It writes to the wrong target, in someone else's repository. Apply the same rule to an identifier embedded in outward-facing text. Read the complete URL from the live object. Never construct a plausible link from an unverified id. If a query returns no id or URL, stop rather than invent one to proceed. - **A write is never a probe, and a write's output is never suppressed.** Never fire a state-changing call to see whether it works: decide it should happen, make it happen, and read the result. Never append output-discarding redirection or a force-success tail to a mutation (for example `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `|| true`, `|| :`, `|| echo`), because the write's output is exactly what must be read. A write that appears to fail is **verified, not assumed harmless**, because the operation may have succeeded on the server while the client reported an error, so confirm the actual state before retrying or moving on. The ban targets hiding a *failure*. An ad-hoc call's response is the only signal you get, so `>/dev/null 2>&1`, `|| true`, and `|| echo`, which swallow the error stream or force success, are never acceptable on one. A committed script under `set -e` is a narrow exception: it may send a write's *stdout* to `/dev/null` to drop the success-response noise, because stderr stays visible and a failed write still aborts loudly (the hub's own `repo-config/configure.sh` does exactly this, and a repository reaches it there rather than carrying a copy). The exception is stdout-only suppression inside a reviewed, fail-loud script, never `2>&1` or a force-success tail, and never an ad-hoc command. -- **A refused write is reported, never re-shaped, and the maintainer's say-so does not lift a refusal by the harness.** These are two different permissions and only one of them is the maintainer's to give. When the agent harness refuses a write, the maintainer authorizing it in conversation does not change the outcome, and the identical call is refused again, so a second attempt is not worth making and reading the second refusal as a flake is how an agent starts hunting for another shape of the same request. **That hunt is the failure this rule exists to stop.** Re-expressing a refused `gh` command as a raw `gh api -X POST` reaches the same endpoint with the same identity and the same blast radius, having defeated the one control that stopped it, and it is the more dangerous version because the agent believes it has permission. So a refused write is never re-attempted through a different API surface, a different tool, or a rephrasing, and it is never routed around by the agent writing itself a permission rule, which is self-authorization whatever the maintainer said. Two routes remain, both of them the maintainer's: they add the permission rule themselves, or they run the command themselves. Raise it as a blocked decision naming those two (see "Communicating with the User"), and where the work needs the result rather than the call, say what the agent will verify once the maintainer has run it. **A refusal is also a fact about the contract, not just about the session**: where a required verification can only be performed by a write the agent is refused, the document requiring it says so and names who runs it, since a check that is mandatory and unperformable is quietly dropped and then reported as done. +- **A refused write is reported, never re-shaped, and the maintainer's say-so does not lift a refusal by the harness.** These are two different permissions and only one of them is the maintainer's to give. When the agent harness refuses a write, the maintainer authorizing it in conversation does not change the outcome, and the identical call is refused again, so a second attempt is not worth making and reading the second refusal as a flake is how an agent starts hunting for another shape of the same request. **That hunt is the failure this rule exists to stop.** Re-expressing a refused `gh` command as a raw `gh api -X POST` reaches the same endpoint with the same identity and the same blast radius, having defeated the one control that stopped it, and it is the more dangerous version because the agent believes it has permission. So a refused write is never re-attempted through a different API surface, a different tool, or a rephrasing, and it is never routed around by the agent writing itself a permission rule, which is self-authorization whatever the maintainer said. The routes that remain are the maintainer's: they add the permission rule themselves, or they run the command themselves. Raise it as a blocked decision naming those routes (see "Communicating with the User"), and where the work needs the result rather than the call, say what the agent will verify once the maintainer has run it. **A refusal is also a fact about the contract, not just about the session**: where a required verification can only be performed by a write the agent is refused, the document requiring it says so and names who runs it, since a check that is mandatory and unperformable is quietly dropped and then reported as done. - **Each task runs in its own checkout, in its own directory, on its own feature branch.** The unit is the task rather than the agent, since one agent moving between two repositories meets the same hazard as two agents sharing one tree, and a rule written per agent permits exactly the case that goes wrong. The commands that cross the boundary are the ordinary ones rather than the reckless ones, and each is correct in isolation: a blanket `git add -A` sweeps another task's uncommitted work into the commit, a `git reset --hard` deletes it, a branch switch carries it into an unrelated change, and a `git stash pop` can pop another task's entry, since the stash is one list per repository shared by every worktree. The mechanical habit that holds the rule up is that a mutating command takes an absolute path, or a `cd` to one in the same invocation, rather than the working directory it inherited, because a read in the wrong directory is a wasted call and a write there is damage. No working directory scopes the shared stash list, so a task leaves the stash alone. - **A task isolates into its own worktree before its first file edit, and a continuation re-isolates.** All new work begins by creating a unique git worktree (or clone) on its own feature branch, based on the branch work starts on for the repository's model per "Branching Model", which is `develop` unless the task is explicitly about `main`-only content. The primary checkout is the maintainer's own surface, so a session launched there isolates before writing rather than after noticing contention, and a session resuming a prior task creates a fresh worktree rather than resuming wherever its branch happens to be checked out, since a branch sitting checked out in a shared tree is exactly how two sessions end up in one checkout. The moment this rule binds is the first file edit, because the commit-time and review-time checks all run after another task's uncommitted work can already be swept. The worktree mechanics, the layout convention, and the cleanup are packaged as the `repo-worktree` Skill at `.agents/skills/repo-worktree/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, and this section keeps the rule. For Claude Code sessions, the `gh-write-guard` hook now backstops a mechanical subset of this rule directly, denying a mutating git operation (`reset`, `add`, `commit`, and most of the rest, a documented handful of exemptions such as a fast-forward-only pull kept aside) run against a primary checkout rather than a worktree. Every other agent, and everything about isolation a hook cannot see (which checkout a read happens in, whether another task is live in the tree), still relies on this prose alone. The parenthetical "(or clone)" above names `repo-worktree`'s own standalone-clone fallback, used when a linked worktree is unavailable, and that fallback is itself structurally a primary checkout to the hook's own primary-vs-worktree test, so a session using it on Claude Code sets the hook's `GH_WRITE_GUARD_ALLOW_PRIMARY_CHECKOUT` grant for that session, per `repo-worktree`'s own instructions, rather than being silently denied the commits the fallback exists to make. -- **A checkout another task is live in is left rather than shared, and a footprint already left there is undone deliberately.** Two signals say someone else is in the tree, a branch that changes when nothing you did changed it, and an edit of yours reverted with no conflict, and the response to either is to stop rather than to re-apply the edit, which is the instinct and the wrong one. Leaving and cloning your own costs about a minute against an incident that costs the better part of an hour, so it is the cheap move rather than the cautious one. Once you have written there, leaving it alone arrives too late, so save your work aside in a scratch directory of your own, restore only the files you touched, verify the tree is clean, delete your branch from that clone, and then say plainly what was touched, since a regenerated report left behind reads as the other task's own and is committed by whoever runs the next blanket add. +- **A checkout another task is live in is left rather than shared, and a footprint already left there is undone deliberately.** These signals say someone else is in the tree, a branch that changes when nothing you did changed it, and an edit of yours reverted with no conflict, and the response to any of them is to stop rather than to re-apply the edit, which is the instinct and the wrong one. Leaving and cloning your own costs about a minute against an incident that costs the better part of an hour, so it is the cheap move rather than the cautious one. Once you have written there, leaving it alone arrives too late, so save your work aside in a scratch directory of your own, restore only the files you touched, verify the tree is clean, delete your branch from that clone, and then say plainly what was touched, since a regenerated report left behind reads as the other task's own and is committed by whoever runs the next blanket add. ## Representative Data in Agent-Authored Text @@ -58,7 +58,7 @@ Read the skill for the full rules. Two workflow models, set per repo by the registry `workflowModel` field. Most repos are `release`: squash-only feature branches into `develop`, merge-commit-only `develop -> main` -promotions, forward-only with no back-merges, and two promotion traps worth knowing before the +promotions, forward-only with no back-merges, and the promotion traps worth knowing before the first one (never delete `develop`, resolve an EOL-only conflict by taking `develop`'s side). **GitHub's own "default branch" repository setting reads `main`, but `develop` is where work starts and where in-flight content lives**, so a worktree or clone that defaults to "the default branch" lands on `main` and can silently miss content already merged to `develop` but not yet promoted. Branch from `develop`, on either workflow model, unless the task is explicitly about `main`-only content. **Operational** repos differ substantially (direct-to-`develop`, advisory CI, dispatch-only @@ -125,11 +125,11 @@ A repo whose own stacks or scripts read local runtime credentials from disk, mos ## Repository Onboarding and Conformance -Every fleet repo is a standard-style repo the hub audits **downward** against its declared type, the model the fleet uses because managing downstream divergence is too costly. Three obligations follow, and they are not optional: +Every fleet repo is a standard-style repo the hub audits **downward** against its declared type, the model the fleet uses because managing downstream divergence is too costly. These obligations follow, and they are not optional: -- **Definition of done is the audit.** Onboarding or materially changing a repo is complete only when it passes [`AUDIT.md`](./AUDIT.md) for its type, **or** has a hub-committed `reports/<repo>/audit.md` plus a tracking issue for the residual deltas. Do not leave a repo partially stood up and unrecorded, which is itself a defect. Stand a repo (or a new type) up with `STANDUP.md`, and verify it with [`AUDIT.md`](./AUDIT.md). The two share the same manifests, so a repo stood up correctly passes the audit by construction. A repo that is stood up already and has fallen behind the hub is brought back into line with `RESYNC.md`, which is the third procedure and the one a request to sync a repo with the hub means. It exists because a findings list does not state the order its remedies apply in, and one of them deletes a file rather than updating it. +- **Definition of done is the audit.** Onboarding or materially changing a repo is complete only when it passes [`AUDIT.md`](./AUDIT.md) for its type, **or** has a hub-committed `reports/<repo>/audit.md` plus a tracking issue for the residual deltas. Do not leave a repo partially stood up and unrecorded, which is itself a defect. Stand a repo (or a new type) up with `STANDUP.md`, and verify it with [`AUDIT.md`](./AUDIT.md). `STANDUP.md` and `AUDIT.md` share the same manifests, so a repo stood up correctly passes the audit by construction. A repo that is stood up already and has fallen behind the hub is brought back into line with `RESYNC.md`, which is the third procedure and the one a request to sync a repo with the hub means. It exists because a findings list does not state the order its remedies apply in, and one of them deletes a file rather than updating it. - **The registry is ground truth about reality, not intent.** After any conformance change, reconcile the repo's `registry/repos.json` entry (`status`, `types`, `releaseTrigger`, `workflowModel`, `driftNotes`) to what the repo actually is. `spec/validate.py` proves the catalog is self-consistent. It does **not** prove the catalog matches the live repo. A `validate.py`-clean entry can still be false, and a stale entry is a finding. -- **Every owned, non-fork repo carries a registry entry, full stop.** `spec/audit.py`'s fleet membership check (a full sweep, not a per-repo one) lists the owner's actual GitHub repos and flags any absent from the registry as a `DEFECT`, since every other tool here only ever iterates the registry and cannot see past it. An archived repo still gets an entry, `status: "archived"`, and an entry a maintainer has decided not to audit is `status: "excluded"` with a required `exclusionReason`. Both keep the decision visible rather than letting the repo disappear from the catalog. +- **Every owned, non-fork repo carries a registry entry, full stop.** `spec/audit.py`'s fleet membership check (a full sweep, not a per-repo one) lists the owner's actual GitHub repos and flags any absent from the registry as a `DEFECT`, since every other tool here only ever iterates the registry and cannot see past it. An archived repo still gets an entry, `status: "archived"`, and an entry a maintainer has decided not to audit is `status: "excluded"` with a required `exclusionReason`. Each status keeps the decision visible rather than letting the repo disappear from the catalog. - **Prove the docs suffice.** The onboarding docs are sufficient only when a context-free agent stands each supported repo shape (project type(s) plus workflow model) up from them alone. The cold-start self-test in `STANDUP.md` and the conformance matrix track that. A shape that cannot be stood up cold is a documentation defect, fixed in the hub, not worked around per repo. ## Hub-Hosted Tooling @@ -156,7 +156,7 @@ This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment ## Documentation Style Conventions -The fleet's prose and formatting contract, applied to docs and code/workflow comments alike. It governs what a carried file may reference, Markdown link, heading, and tense structure, and the comment philosophy. It also holds the ASCII character-set tiers, the line-ending policy, the sentence-structure house style, the ban on naming an issue, a pull request, or a commit in a comment, a docstring, or an instruction document, and the rule keeping a quantitative claim honest. +The fleet's prose and formatting contract, applied to docs and code/workflow comments alike. It governs what a carried file may reference, Markdown link, heading, and tense structure, and the comment philosophy. It also holds the ASCII character-set tiers, the line-ending policy, and the sentence-structure house style. It bans naming an issue, a pull request, or a commit in a comment, a docstring, or an instruction document. And it holds the rules keeping a quantitative claim honest and an incidental count out of the text. This is packaged as the `comment-and-doc-style` Skill at `.agents/skills/comment-and-doc-style/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. The summary above sketches the contract. Read the skill for the full rules. @@ -166,7 +166,7 @@ The full comment philosophy, what earns one, structure, capitalization, growth d ### References -No comment, no docstring, and no instruction document names an issue, a pull request, or a commit, and a commit message and a pull request body each carry theirs as usual. Two carve-outs. In a code or workflow comment, a URL naming an issue or a pull request on a public repository other than this one is a source citation and is permitted. And a record whose subject is the revision itself keeps it, which is what lets a disproved-claims entry in `.github/copilot-instructions.md` name the revision its proof was read against. Whatever neither carve-out affirmatively permits is banned by the sentence above them, which is the whole of the test. The reasons, the surfaces the ban reaches, the narrative files it leaves alone, and what to write instead are in the `comment-and-doc-style` Skill referenced above. Separately, `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md` carry no three-part version and no commit SHA, full or abbreviated, whether it is a pin's value, an illustrative example, a minimum version, or a fixed constant. A pin lives in the workflow or manifest that uses it, where Dependabot moves it, so a copy in prose is stale at the next bump, and every other kind reads exactly like one, which is why a check cannot tell them apart. A two-part language or runtime version, such as a minimum Python minor, is how such a rule is stated and stays. Each file names the mechanism instead, such as a reference SHA-pinned to a hub release, writes an example with a placeholder, such as `1.0.N`, names the manifest or skill that holds a minimum version rather than the number, and describes a fixed constant, such as the all-zero placeholder version, rather than quoting it. Neither carve-out above lifts this ban. +No comment, no docstring, and no instruction document names an issue, a pull request, or a commit, and a commit message and a pull request body each carry theirs as usual. The carve-outs follow. In a code or workflow comment, a URL naming an issue or a pull request on a public repository other than this one is a source citation and is permitted. And a record whose subject is the revision itself keeps it, which is what lets a disproved-claims entry in `.github/copilot-instructions.md` name the revision its proof was read against. Whatever no carve-out affirmatively permits is banned by the sentence above them, which is the whole of the test. The reasons, the surfaces the ban reaches, the narrative files it leaves alone, and what to write instead are in the `comment-and-doc-style` Skill referenced above. Separately, `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, and `WORKFLOW.md` carry no three-part version and no commit SHA, full or abbreviated, whether it is a pin's value, an illustrative example, a minimum version, or a fixed constant. A pin lives in the workflow or manifest that uses it, where Dependabot moves it, so a copy in prose is stale at the next bump, and every other kind reads exactly like one, which is why a check cannot tell them apart. A two-part language or runtime version, such as a minimum Python minor, is how such a rule is stated and stays. Each file names the mechanism instead, such as a reference SHA-pinned to a hub release, writes an example with a placeholder, such as `1.0.N`, names the manifest or skill that holds a minimum version rather than the number, and describes a fixed constant, such as the all-zero placeholder version, rather than quoting it. No carve-out above lifts this ban. ### Character Set @@ -180,6 +180,10 @@ The full CRLF/LF policy (`.editorconfig` and `.gitattributes` defaults and pins, ASD-STE100's structural half is the adopted house style: short sentences, one instruction per sentence, active voice, and imperative mood for procedure steps. Its controlled dictionary is deliberately not adopted. The full rules, the sentence word cap, and the opt-in `sentence-length` check that enforces the cap are in the `comment-and-doc-style` Skill referenced above. +### Incidental Counts + +Text refers to an enumeration rather than stating its size, so it writes "the following checks", "each case", or "the rows below". This holds in every fleet repository, for docs, code and workflow comments, and docstrings alike. A stated count is a second copy of what the list or table already shows, and nothing keeps the two copies in sync. Every later edit to the enumeration then owes an edit to the sentence, and a missed one surfaces as a review finding. A word such as "both" or "all three" is a count too, where the list it counts can grow. A count is written only where the number is what the reader needs, such as a limit, a threshold, or a measured result. Such a count names its source, such as the constant or the measurement, so a reader can re-derive it. A wrong incidental count is fixed by removing it rather than by correcting it, since a corrected number goes stale the same way. + ## Verification Discipline The checks that separate work actually done from work that merely reports success. A pattern that matches less still exits zero, and a gate that stops gating still reports success. @@ -282,11 +286,11 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers, which Dependabot bumps. It runs **editorconfig-checker** via Docker `:latest`, since its action only installs the CLI and the Docker one-liner is what actually runs the check. **shellcheck** and **shfmt** run the same way for a repo that carries a shell script, and **PSScriptAnalyzer** does for a repo that carries `.ps1` files. None of shellcheck, shfmt, and PSScriptAnalyzer has an action. For a repo that carries an action under `.github/actions/`, the repo gate's `composite-actions` check runs **shellcheck** in Docker over each bash or sh `run:` body. It also runs **check-jsonschema** through `uvx`, because actionlint reads workflows only. The `composite-actions` check runs shellcheck at warning severity without SC2154, weaker than for a `.sh` file. markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. -- **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo that already keeps a .NET tool manifest declaring it, since `dotnet tool restore` then `dotnet husky install` is what generates the `.husky/_/husky.sh` its hook sources, and the snippet ships no manifest of its own. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two shared doc gates. A repo drops the block for a language it does not have. A Docker, config, or docs repo therefore wires the doc gates alone, which is a mechanism rather than the absence of one. Which snippet a repo starts from is bounded by the toolchain it already keeps, never by which languages its checks cover. A snippet is a starting point rather than the only shape: a repo may wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, which is what this repository does. That path matters, since `parity.hooks` reads the tracked file at `.husky/pre-commit` or `.pre-commit-config.yaml` rather than a per-clone hooks path it cannot observe. +- **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Catalog snippets carry the canonical shape, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo that already keeps a .NET tool manifest declaring it, since `dotnet tool restore` then `dotnet husky install` is what generates the `.husky/_/husky.sh` its hook sources, and the snippet ships no manifest of its own. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. No snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the shared doc gates. A repo drops the block for a language it does not have. A Docker, config, or docs repo therefore wires the doc gates alone, which is a mechanism rather than the absence of one. Which snippet a repo starts from is bounded by the toolchain it already keeps, never by which languages its checks cover. A snippet is a starting point rather than the only shape: a repo may wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, which is what this repository does. That path matters, since `parity.hooks` reads the tracked file at `.husky/pre-commit` or `.pre-commit-config.yaml` rather than a per-clone hooks path it cannot observe. - **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to the working tree diff against `HEAD` rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. That scope is the working tree rather than the staged index under a plain git hook, so a partially staged file is judged on all of its edits. Under the `pre-commit` framework it is the staged state instead, since that runner sets unstaged changes aside before running a hook, which is measured rather than assumed. The pull request gate re-checks every line the branch changes against its base, which is the scope a review is judged on. A whole-repo check belongs in the hook too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out of the hook regardless, since it resolves a same-owner pin against the GitHub API. - **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. The prose gate adds one `api.github.com` commit lookup to its fetch, and those are the only network calls the doc gates make. An allow-list permitting only `raw.githubusercontent.com` still runs the gate, with the commit reported as unresolved. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. CI remains the authoritative run either way. - **The hub carries a second hook, `.husky/pre-push`, gating the local-review rule rather than lint.** It runs `scripts/local_review.py check`, refusing a branch push whose diff no recorded review pass covers, per "Verification Discipline" above. It fires only for a branch update, so a tag push and a branch delete pass through, and a branch holding no net content against its target has nothing for a review to cover. It refuses rather than guesses in every state it cannot speak for, since the engine reads the checkout it runs in while a push delivers a commit, and those describe the same content only sometimes. The order that keeps them describing the same content is to commit the change, then review the diff, record its receipt, and push, the receipt being untracked and so recorded after the last commit. A change that also carries the canonical ledger records those passes ahead of that commit instead, the ledger being tracked content the commit has to hold. It reads `develop` and accepts no override from the environment, since an environment variable is set inline on the very command being gated, by whoever is being gated, which is the same reason an authorization is never read from a channel the agent itself can set. The cost is that it measures a branch based on anything but `develop` against `develop` anyway, so its refusal there is not a verdict about that branch. Each refusal names its own case, and the `local-strict-review` Skill carries the fleet's one enumeration of them with what clears each, deliberately in one place rather than restated here. It folds the engine's three-valued exit honestly: a check that could not run blocks too, and says so in different words than a check that ran and found no pass, because a gate that waves a push through when it could not run is a gate that stopped gating. It is a backstop rather than a seal, and it is bypassable by construction: `--no-verify` is the documented route out of a pickle and is not the only one, since a git hook cannot police its own invocation. A Claude Code session meets a narrower surface, that flag being denied unconditionally by the agent-safety hook's explicit-bypass rule, and that denial is Claude Code's alone, since Codex and opencode carry no such hook yet. So the committed hook raises the cost of skipping the rule for one agent and lowers it for none, and the prose layer above stays the agent-agnostic one that actually binds. It is hub-only for now. `local_review.py` is hub-hosted per "Hub-Hosted Tooling", and a downstream repo reaches it as a hub checkout's copy run with its own worktree as the working directory, so a catalog snippet carrying this hook fleet-wide is a later step rather than part of this one. `canonical_review.py` is not run here at all, and nothing else gates a push or a pull request over a canonical unit either, that read being swept on a schedule instead, per "Verification Discipline" above. The hub's own `.github/actions/validate` composite action renders its coverage burn-down into every run's job summary and fails no pull request over what that shows, while still failing where the engine could not read the manifest or the ledger at all, that being a boundary rather than a verdict. -- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. +- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor. @@ -340,7 +344,7 @@ Contributors commit to this repo with signed commits. The SSH-signing setup live ## Editor and Tasks - **VS Code is the primary IDE, and the experience favors it.** Prefer VS Code tasks and launch configurations for building, running, and testing over ad-hoc shell scripts. A script is the fallback, not the default. -- The `.code-workspace` file carries the shared editor settings and the recommended-extension set. **All VS Code settings and extension recommendations live only here, never in a standalone `.vscode/settings.json` or `.vscode/extensions.json`** (`.vscode/` holds only `tasks.json` and `launch.json`). A **standard set** of extensions applies to every repo (markdownlint, cspell, editorconfig, markdown-all-in-one, better-todo-tree), kept to extensions that work without a separately installed tool. A repo shipping a language adds that language's **defaults** (.NET: csdevkit, csharpier. Python: python, pylance, ruff, mypy. Docker: the Docker extension). **Any repo can add further extensions** of its own on top of both. +- The `.code-workspace` file carries the shared editor settings and the recommended-extension set. **All VS Code settings and extension recommendations live only here, never in a standalone `.vscode/settings.json` or `.vscode/extensions.json`** (`.vscode/` holds only `tasks.json` and `launch.json`). A **standard set** of extensions applies to every repo (markdownlint, cspell, editorconfig, markdown-all-in-one, better-todo-tree), kept to extensions that work without a separately installed tool. A repo shipping a language adds that language's **defaults** (.NET: csdevkit, csharpier. Python: python, pylance, ruff, mypy. Docker: the Docker extension). **Any repo can add further extensions** of its own on top of them. - The Table of Contents is maintained by the Markdown All in One extension, and `markdown.extension.toc.levels` in the workspace sets which heading levels it includes (see the Markdown rules for the authoring convention and the `<!-- omit from toc -->` exclusion marker). - **Agents: editing a `.code-workspace` that a VS Code window has loaded as its workspace can reload that window and drop an agent session running in it.** "Verification Discipline" holds the rule. An agent leaves that edit to the maintainer while it knows a window with the file loaded hosts an agent session, the agent's own included. It treats any window but its own as hosting no session unless it knows otherwise. Where it still cannot tell whether the rule binds it, it asks the maintainer. An agent the rule does not bind edits the file directly. Opening the folder rather than the workspace, in every window holding the checkout, keeps a checkout's own file editable. @@ -357,12 +361,12 @@ Every repo's GitHub repository details (the About panel) follow a fixed conventi - [`AGENTS.md`](./AGENTS.md): the agent entry point, carrying context and delegation rules plus the map to the sections below. - [`CLAUDE.md`](./CLAUDE.md): imports `AGENTS.md`, since Claude Code reads `CLAUDE.md` and never `AGENTS.md` on its own. Carries no rule of its own. - [`GOVERNANCE.md`](./GOVERNANCE.md), [`CODESTYLE.md`](./CODESTYLE.md), [`WORKFLOW.md`](./WORKFLOW.md), [`AUDIT.md`](./AUDIT.md): the governance and audit docs. This file is the cross-cutting-rules authority. -- [`STANDUP.md`](./STANDUP.md), [`RESYNC.md`](./RESYNC.md): hub-only, the two fleet-conformance procedure docs at the root, one taking a repository from nothing to operational and the other bringing an already stood-up one back into line with the hub. `AGENTS.md` "Fleet Bootstrap" routes to them by name. +- [`STANDUP.md`](./STANDUP.md), [`RESYNC.md`](./RESYNC.md): hub-only, the fleet-conformance procedure docs at the root, one taking a repository from nothing to operational and one bringing an already stood-up one back into line with the hub. `AGENTS.md` "Fleet Bootstrap" routes to them by name. - [`spec/`](./spec/): hub-only, the machine-readable ground truth, covering project-type requirements, the file/section baseline, secrets, the host tool contract and its floors, and the preferred README structure. - [`registry/repos.json`](./registry/repos.json): hub-only, the fleet registry, holding every project, its type(s), and its status. - [`repo-config/`](./repo-config/): hub-only branch rulesets, fleet settings, documentation, and the apply script every repo is configured by, kept out of `.github/` (which is Actions-owned). - [`catalog/`](./catalog/): hub-only, reusable reference snippets (workflows, configs, devcontainers) the audit compares implementations against. -- [`.agents/`](./.agents/): hub-only, the source the fleet's Skills are authored in. `scripts/build_dist.py` builds it into the two generated trees below, and neither of those is hand-edited. +- [`.agents/`](./.agents/): hub-only, the source the fleet's Skills are authored in. `scripts/build_dist.py` builds it into the generated trees below, and none of those is hand-edited. - [`.github/skills/`](./.github/skills/): generated in the hub from `.agents/skills/` and carried verbatim to every repo, so a fix to a Skill belongs at that source rather than in any copy of this tree. - [`.claude-plugin/fleet-skills/`](./.claude-plugin/fleet-skills/): hub-only, the same Skills as a Claude plugin. The `marketplace.json` beside it names the plugin rather than the Skills, is hand-authored, and `build_dist.py` never writes it. - [`scripts/`](./scripts/): the fleet's lint and review tooling, with the deterministic checks also gating CI, for the documented rules no CI linter checks. Hosted here and reached rather than carried, per "Hub-Hosted Tooling". diff --git a/WORKFLOW.md b/WORKFLOW.md index b9d0e8069..36b87c5a4 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -4,13 +4,13 @@ The guide for CI/CD **workflows** (GitHub Actions): a deliberate mixture of arch Its defining principle: **it describes required outcomes.** Two repos may implement the same guarantee with different YAML wherever that guarantee names no construct, and where one is named, D6.1's `release-asset-<branch>-<target>` and D9.2's ruleset-bound job `name:` among them, matching it **is** the outcome. Section 4 is what a workflow must satisfy, and the verdict below is how that is judged. The style conventions that section 2 points at are part of that contract wherever section 4 states one as a guarantee, and a violation of one that it does is a defect on the same terms as any other. -Given this document and the `GOVERNANCE.md` sections it points at, an agent must be able to do three things to any project: +Given this document and the `GOVERNANCE.md` sections it points at, an agent must be able to do the following to any project: 1. **Audit** - statically check the structural fact each applicable guarantee implies, the style conventions among them (section 5A). 2. **Test** - trace the expected inputs/outputs (section 5B) and, where warranted, drive a live probe (section 5C). 3. **Assess** - render a verdict: **operational** (every *applicable* guarantee holds, every *applicable* scenario's predicted output equals the expected, and no 5C probe that was run contradicts either) or **not operational** (any *applicable* mismatch, which is a *defect*). -> **Canonical scope.** An overlap between this document and `GOVERNANCE.md` resolves **by subject**, never by blanket precedence. This document wins on the applicability and N/A rules (section 1), the pipeline architecture (section 3), the contract (section 4), the test methodology (section 5), and the per-project-type walkthroughs (section 6). `GOVERNANCE.md` wins on the workflow style conventions, at its "Workflow YAML Conventions", which section 2 points at rather than restating; on the release policy, at its "Release Model"; on the branching model, at its "Branching Model"; and on what an operational repo is, at its "Operational Repositories". Section 3 summarizes those last three rather than owning them. Each file names where it defers. +> **Canonical scope.** An overlap between this document and `GOVERNANCE.md` resolves **by subject**, never by blanket precedence. This document wins on the applicability and N/A rules (section 1), the pipeline architecture (section 3), the contract (section 4), the test methodology (section 5), and the per-project-type walkthroughs (section 6). `GOVERNANCE.md` wins on the workflow style conventions, at its "Workflow YAML Conventions", which section 2 points at rather than restating; on the release policy, at its "Release Model"; on the branching model, at its "Branching Model"; and on what an operational repo is, at its "Operational Repositories". Section 3 summarizes the release, branching, and operational-repo rules rather than owning them. Each file names where it defers. The guarantees are distilled from failures observed in practice. Section 4's preamble states how each item is written. @@ -18,10 +18,10 @@ The guarantees are distilled from failures observed in practice. Section 4's pre - **Contract, not implementation.** Conform to the *outcomes* in section 4. Shape, job names, and file layout may differ between repos wherever no guarantee names them, and where one does, as D9.2 does for the ruleset-bound job `name:`, that name is itself the outcome. - **Applicability.** A guarantee, or a 5B scenario, is **applicable** only if the repo contains the construct it governs: a given target, a transfer artifact, a registry push, a wrapper-version source. An item that governs an absent construct is **N/A**: record it as N/A and **exclude it from the verdict**. N/A is never a defect. Section 6 names which constructs each project type adds: start from the constructs every repo has, its pull request workflow among them, **union what every declared type adds**, and record an item N/A only where nothing in that union supplies its construct. Which triggers a construct carries is read from the workflow itself rather than from the type, per section 6. Owning the release task rather than calling the hub-hosted copy changes where a construct's evidence is cited, per 5A, never whether it is applicable. A near-empty pipeline (source-only) is mostly N/A and that is fine. -- **Operational is binary.** A workflow is operational only where section 5's Assessment records all three of its conjuncts met. A single applicable failure is a defect and makes the workflow **not operational**, whether it is an input/output mismatch or a static property of the committed source such as an unpinned action SHA, regardless of how clean the YAML looks. +- **Operational is binary.** A workflow is operational only where section 5's Assessment records every one of its conjuncts met. A single applicable failure is a defect and makes the workflow **not operational**, whether it is an input/output mismatch or a static property of the committed source such as an unpinned action SHA, regardless of how clean the YAML looks. - **Default branch.** Guarantees say "default branch" portably. It is implemented as the literal `main` in several places (the validate gate, the `prerelease` expression, and `version.json`'s `publicReleaseRefSpec`). These MUST all reference the repo's *actual* default branch. A divergence is a defect (D3.2). - **Two layers when auditing.** The pipeline splits into an **orchestrator** layer (the PR entry workflow, the publisher, and the version and release jobs) and a **build-leaf** layer (the `build-<target>` tasks, whether separate files or jobs inside the release task). Inputs like `github`/`dockerhub`/`expect_release_assets` live on the orchestrator. A leaf receives `ref`/`branch`/`smoke` and whatever else its own target needs, a derived `push` among them where that leaf pushes. A package target declares no push input on either layer, because section 3's `Output Seam by Destination` puts its push in a `publish-<target>` job in the publisher, gated by `needs:` rather than by a flag. Assert an input a guarantee names in the layer that declares it. -- **The three verbs.** Audit (static), Test (trace + probe), Assess (verdict). Section 5 gives the exact procedure. +- **The verbs.** Audit (static), Test (trace + probe), Assess (verdict). Section 5 gives the exact procedure. ## 2. Workflow Style Conventions @@ -215,7 +215,7 @@ The required behaviors, organized by domain. Each is a **MUST**, and its `Output ## 5. Test Methodology -An agent verifies a project in three escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. +An agent verifies a project in escalating modes, then renders a verdict. **Skip N/A items** (`WORKFLOW.md` section 1): a guarantee or scenario for an absent construct is recorded N/A, not failed. ### 5A. Static Audit (No Execution) @@ -253,7 +253,7 @@ Every probe here that opens a pull request, dispatches a workflow, or re-runs a - Open a trivial-change PR touching one target and confirm S1. *Caveat: the Docker leg logs in to the registry even on smoke and reads the buildcache, so it needs `DOCKER_HUB_*` secrets and cannot run on a fork PR (same-repo only).* - Per registry: after a real publish, query NuGet.org for the expected version + prerelease classification (and the `.snupkg` on the symbol server), and confirm a re-run added no duplicate. For PyPI read the built `dist/*` filenames out of the build job's log, `.dev0` off `develop` vs a plain version on the default branch. - Inspect the latest real publish's logs for `PublicRelease`/`SemVer2` per leg and confirm the artifact lifecycle (uploaded, consumed, deleted, with none left behind). -- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is four things, and each of them matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all four, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads all four back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. +- **The deploy ref gate (S13) is verified only by tripping it.** Dispatch the production environment from a non-default ref and expect the run to fail at the gate. The evidence is the following, and each item matters: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the production environment's deployment list carrying no deployment from the dispatched ref. Capture all of it, because a gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. **The agent prepares the command and reads the evidence back afterwards. It does not fire it.** The same split applies to any probe that acts on the deploy host directly, an outbound SSH exercising a forced command among them. ### Assessment @@ -270,7 +270,7 @@ Record the workflow **operational** when every *applicable* 5A item passes, ever Each type adds constructs to the pipeline, and the constructs are what decide a verdict. This section states what each type **adds**. Section 1's applicability rule turns that into the N/A set on its own: an item governing a construct the repo does not contain is N/A, and one governing a construct it contains is checked. No row here states an N/A list of its own, deliberately, so that reading one row can never take away a construct another row supplies. Walking the rows a repo's types select is the self-check that the contract holds for its shape. -Three rules govern reading a row, each of them because reading one row alone has produced a wrong verdict. +The rules below govern reading a row, each of them because reading one row alone has produced a wrong verdict. - **Types union, they do not choose.** A repo declaring more than one type contains the constructs of **every** type it declares, so an item is N/A only where no declared type supplies its construct. `source-only` beside another type is the case that catches a reader out: it adds no build target and takes none away. - **The trigger scenarios come from the publisher's own `on:` block, not from the type.** S5, S6 and S9 turn on which triggers the publisher actually carries, and two repos of one type routinely differ there. Section 5B's preamble owns that rule and it binds here. @@ -301,8 +301,8 @@ What each type adds beyond that, and how its leaf behaves, is below. - **`codegen`.** Adds a generation workflow that runs as a matrix over both branches and is deterministic from an external source (D8.2), carrying no per-run timestamp or GUID. It contributes no build target and no publish scenario of its own, so it changes no row of the table above. - **`csharp`, `python`, `cpp`, `docs`.** Add no pipeline construct. They decide what the validation job runs and what the repo's project configuration must hold, `csharp` the analyzer and central-MSBuild rules and D1.6's C# coverage leg, `python` the profile split and D1.6's Python one, `cpp` a shared `clang-format` feeding the lint gate, and `docs` a lint-only CI with no build or test. A repo declaring one of these and nothing else reaches only the scenarios its publisher and its pull request workflow already supply. - **`source-only`.** Adds no build leaf of its own. In a repo declaring no build-target type beside it, the publish job reaches the reusable release task with `github: true`, every `enable_*` input false, and `expect_release_assets: false`, producing tag + source zip + README + LICENSE with no asset download, the paths-filter matches nothing so a retained `smoke-build` job is **structurally always skipped**, and the repo may drop that never-running job. A repo declaring a build-target type as well enables that target instead, this row taking nothing away from it. NBGV and `version.json` own the tag. Validation remains the caller's own job reaching the reusable validator. The aggregator `needs:` that validation job (D1.2), and a retained `smoke-build` job `needs:` the `changes` job rather than the validation job. The publish job depends on the same reusable validation task the PR workflow runs, which prevents a dispatch from releasing a ref that fails validation. The release task carries the D6.1 `pattern:` download and the `release-asset-*` cleanup step (D5.1 to D5.3 and D5.5), so a repo contains them whether it owns that task or calls the hub-hosted copy, and is checked on them either way. **Owning it changes only where the evidence is cited**, in the repo's own file or at the SHA it pins, per section 5A's citation rule. Test, where the repo has a publisher: S7 covers the release, S8 the dispatch guard, and S10 the classification gate. -- **`hugo` (a static site deployed to a host the project owns).** Two independent surfaces, and keeping them apart is the point. The **release** is the `source-only` shape above, unchanged. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`. That task declares the `environment:` inside itself rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The caller still maps the crossing secrets explicitly under `secrets:`, `DEPLOY_SSH_PRIVATE_KEY` because the task declares it required, and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair, or the `SITE_EXTRA_AUTH_TOKEN_ID`/`SITE_EXTRA_AUTH_TOKEN` pair for a second site, only where a token-gated live check needs it, `secrets: inherit` not being used on a cross-repository call. For a secret the environment defines, the value the task's job reads is resolved by the task's own `environment:` binding from the caller's environment store, not taken from that mapping. That task hard-asserts two interfaces. It requires the environment to carry `SITE_BASE_URL`, `DEPLOY_SSH_USER`, `DEPLOY_SSH_HOST` and `DEPLOY_SSH_KNOWN_HOSTS` as variables and `DEPLOY_SSH_PRIVATE_KEY` as a secret, each non-empty, failing fast naming every missing one, and it requires `SITE_AUTH_TOKEN_ID` and `SITE_AUTH_TOKEN` to be mapped together or not at all, and the same of `SITE_EXTRA_AUTH_TOKEN_ID` and `SITE_EXTRA_AUTH_TOKEN`, which also need a non-empty `SITE_EXTRA_BASE_URL` variable. And it requires a **deploy hook** at `.github/actions/deploy/action.yml`, one composite action the caller owns, run three times with its `mode` input set to `build`, `prune` and `verify`. The hook declares all four inputs the task passes across those runs, `mode`, `bundle-path`, `release-id` and `environment`, and its `build` run leaves `releases/<release-id>/` and `current` under `bundle-path`, both of which the task asserts. The task re-asserts the environment *name*, `production` or `staging`, in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. The ref gate gets no counterpart re-assertion, so a caller reaching the task directly is trusted for the ref and not for the environment, which is a deliberate asymmetry rather than an omission. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs the hook's `build` invocation, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the hook's `prune` and `verify` invocations, the second checking the running host (D4.6) against the site's own URL contract. Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count in the `prune` hook, while a credential confined **write-only** can neither delete nor read back, so that repo's `prune` hook is a no-op, the prune is a host-side timer, and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. -- **Operational (a `workflowModel`, not a type).** A `workflowModel: operational` repo layers direct commits to `develop` onto the `source-only` release shape above. It has two workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard `source-only` publisher. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (`GOVERNANCE.md` "Workflow YAML Conventions"). A pull request uses `refs/pull/<n>/merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. Having no build target, such a repo never reaches S1, whose input is a target change, on any pull request, promotion and `develop` pull requests included. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. +- **`hugo` (a static site deployed to a host the project owns).** Two independent surfaces, and keeping them apart is the point. The **release** is the `source-only` shape above, unchanged. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`. That task declares the `environment:` inside itself rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The caller still maps the crossing secrets explicitly under `secrets:`, `DEPLOY_SSH_PRIVATE_KEY` because the task declares it required, and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair, or the `SITE_EXTRA_AUTH_TOKEN_ID`/`SITE_EXTRA_AUTH_TOKEN` pair for a second site, only where a token-gated live check needs it, `secrets: inherit` not being used on a cross-repository call. For a secret the environment defines, the value the task's job reads is resolved by the task's own `environment:` binding from the caller's environment store, not taken from that mapping. That task hard-asserts two interfaces. It requires the environment to carry `SITE_BASE_URL`, `DEPLOY_SSH_USER`, `DEPLOY_SSH_HOST` and `DEPLOY_SSH_KNOWN_HOSTS` as variables and `DEPLOY_SSH_PRIVATE_KEY` as a secret, each non-empty, failing fast naming every missing one, and it requires `SITE_AUTH_TOKEN_ID` and `SITE_AUTH_TOKEN` to be mapped together or not at all, and the same of `SITE_EXTRA_AUTH_TOKEN_ID` and `SITE_EXTRA_AUTH_TOKEN`, which also need a non-empty `SITE_EXTRA_BASE_URL` variable. And it requires a **deploy hook** at `.github/actions/deploy/action.yml`, one composite action the caller owns, run once for each `mode` input value, `build`, `prune` and `verify`. The hook declares every input the task passes across those runs, `mode`, `bundle-path`, `release-id` and `environment`, and its `build` run leaves `releases/<release-id>/` and `current` under `bundle-path`, each of which the task asserts. The task re-asserts the environment *name*, `production` or `staging`, in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. The ref gate gets no counterpart re-assertion, so a caller reaching the task directly is trusted for the ref and not for the environment, which is a deliberate asymmetry rather than an omission. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs the hook's `build` invocation, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the hook's `prune` and `verify` invocations, the second checking the running host (D4.6) against the site's own URL contract. Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count in the `prune` hook, while a credential confined **write-only** can neither delete nor read back, so that repo's `prune` hook is a no-op, the prune is a host-side timer, and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. +- **Operational (a `workflowModel`, not a type).** A `workflowModel: operational` repo layers direct commits to `develop` onto the `source-only` release shape above. It has the following workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard `source-only` publisher. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (`GOVERNANCE.md` "Workflow YAML Conventions"). A pull request uses `refs/pull/<n>/merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. Having no build target, such a repo never reaches S1, whose input is a target change, on any pull request, promotion and `develop` pull requests included. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. <!-- Repo --> diff --git a/scripts/README.md b/scripts/README.md index ccb48e6d7..36c8dc99a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -253,7 +253,7 @@ The match is on the block's heading rather than anywhere in the body, and on the ## `handoff.py` -The session handoff chain, one open issue per track in a repository, each link naming the one before it. Six subcommands: four reads and the two writes that move a chain forward. A handoff written to a scratch file fails three ways the chain closes: the file is not found where the next session looks, more than one candidate is found and nothing says which is current, and there is no history, so a later round re-runs a path an earlier round already tried and already wrote down. The judgment half, what belongs in a handoff and what does not, is the `session-handoff` Skill in [`.agents/skills/`][agents-skills], and this is only the mechanics. +The session handoff chain, one open issue per track in a repository, each link naming the one before it. Its subcommands are reads plus the writes that move a chain forward. The chain closes each way a handoff written to a scratch file fails: the file is not found where the next session looks, more than one candidate is found and nothing says which is current, and there is no history, so a later round re-runs a path an earlier round already tried and already wrote down. The judgment half, what belongs in a handoff and what does not, is the `session-handoff` Skill in [`.agents/skills/`][agents-skills], and this is only the mechanics. ```sh python3 scripts/handoff.py current --repo ptr727/ProjectTemplate --track default diff --git a/scripts/handoff.py b/scripts/handoff.py index 11dfe6b84..b668e68be 100755 --- a/scripts/handoff.py +++ b/scripts/handoff.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """The session handoff chain - one open issue per track, each link naming the one before it. -Why this exists: a handoff written to a file fails three ways that a chained issue does not. The +Why this exists: a handoff written to a file fails in ways that a chained issue does not. The file is not found where the next session looks. More than one candidate is found and nothing says which is current. And there is no history, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is the record of what has been attempted, and From 883cf6b18e99896fb835642e788008b0bf39a7e3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:38:24 -0700 Subject: [PATCH 2/3] Read Copilot's Count-First Overview Sections in pr_review.py (#3172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary A later revision of Copilot's second overview format opens each section summary on its count and names findings issues. `pr_review.py` stopped on each of these shapes with exit 43. It now reads them: - `2 previously missed issues` is the previously-missed section, whose findings open no thread. It is vetted, matched by `PREVIOUSLY_MISSED`, and counted from its leading count, so `previously_missed=N` names each finding. - `1 open issue` and `N open issues` are the open-findings section under a new noun. They map to the same role, so the section states the round's total and each entry is checked against the threads. - `**No open issues**` followed by an emoji is the bare stated zero, which printed `?` before. `heading_count` is now the one reader of a section heading's count, `(N)` or leading, used by `finding_count`, `section_entries`, and `sibling_heading`. `marker_blocks` accepts a non-heading line only on a `(N)` and only where the line does not open on a count. The comment stating the vetted list sizes is removed with the one test that kept its count in sync, since the count went stale on every new spelling. Refs #3167 Refs #3170 ## Verification - `tests/test_pr_review.py`: 850 tests pass. Every new test fails against the base script, and each later fix's test fails with that fix reverted. - ruff format and check, mypy, `prose_lint.py --diff` (default rules and `--check sentence-length`), and `repo_gate.py --check eol` are clean. - The shapes were confirmed against the real review bodies before writing the fixtures. Every title, path, and sentence in the fixtures is constructed. ## Open Local-Review Findings The five findings the first push listed here were fixed in 9722c346, e1d9f261, and b9941102, which also answer the three Copilot threads. The fix push's own final pass raised these, after its edit budget was spent, each to get an outcome in this pull request's review loop. 1. `introduced`: `unrecognized_in` compares the count of vetted previously-missed summaries with the count of blocks, while `marker_blocks` returns at most one block per region. Two previously-missed sections nested in one `Review details` wrapper then report an unread shape, exit 43, where `develop` reads one block holding both. The reverse can mask a miss. Not an observed shape. Outcome: filed as #3174, by the maintainer's choice. 2. `introduced`: `HEADING_OPENER` reads past a finding entry's severity badge and may now be followed by a count, so an entry titled `2 previously missed guards are re-added` inside another section reads as a previously-missed section. An entry with no badge titled with a number now bounds its section in `sibling_heading`. This is the class #3173 tracks, widened. Not an observed shape. Outcome: added to #3173, by the maintainer's choice. 3. `introduced`, design: `heading_count` is a third composition of the badge and emphasis reduction `normal` and `unwrap` also perform, and `LEADING_COUNT` is a second definition of a leading count beside `normal`'s. They differ on a form feed after the count. A shared reduction would close the class. Outcome: filed as #3174, by the maintainer's choice. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- scripts/pr_review.py | 155 +++++++++++++++++++----- tests/test_pr_review.py | 257 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 365 insertions(+), 47 deletions(-) diff --git a/scripts/pr_review.py b/scripts/pr_review.py index a2d5ae1a6..c79538ac2 100755 --- a/scripts/pr_review.py +++ b/scripts/pr_review.py @@ -433,8 +433,12 @@ # The second overview format's own section for a finding against code the pull request did not change. # Anchored to the opening of its line, past whatever markup a heading wears, rather than matched anywhere in it. # The wording is ordinary English, so a body-wide match reads a sentence such as `restores a guard previously missed (2) rounds ago` as this section. -# `marker_blocks` accepts a line on a count alone, which is what lets that sentence through, so the anchor is what separates a heading from prose naming one. +# `marker_blocks` accepts a line that is not a heading on a `(N)` alone, which is what lets that sentence through. +# So the anchor is what separates a heading from prose naming one. # The opener admits heading markup and anything outside ASCII, and nothing else. +# One count may follow it, since a later revision opens the heading on its count, as `2 previously missed issues`. +# A count opening a line is how a sentence opens too, so `marker_blocks` reads such a line as prose unless it is a heading. +# `OPENING_COUNT` reads that count past this same opener, so whatever markup the anchor admits, the prose check strips. # An emoji is what this format already puts on its verdict headings, and a spelling that admitted no such character read a heading wearing one as no section at all. # That failure is silent rather than loud, the heading itself being vetted and `normal` dropping the emoji before the comparison, so the digest reports every shape read while the findings reach nobody. # Widened to any non-word character instead, ordinary punctuation opened the run and a sentence such as `- Previously missed (2) findings were re-raised` became the section, printing the prose around it as the finding. @@ -443,12 +447,14 @@ # Two alternatives able to consume the same character make the run ambiguous, and the parse then doubles per character of it on a line that does not go on to say this, which is a hang rather than a slow read. # A body carrying 30 non-breaking spaces in a line took minutes, and every alternative here now opens on a character the others cannot. # An emphasis marker is admitted only where no space follows it, which is what tells `**Previously missed (1)**` from the list item `* Previously missed (2) findings`. -# A bullet is the one prose opener that is also heading markup, and `marker_blocks` accepts a line on a count alone, so the list item read as the section and printed the prose around it as findings. +# A bullet is the one prose opener that is also heading markup, and `marker_blocks` accepts a line on a `(N)` alone. +# So the list item read as the section and printed the prose around it as findings. # One character per iteration rather than a run of them, since a repeated group that can split a run of stars more than one way is ambiguous and parses exponentially in it. # What this still admits is non-ASCII punctuation, an em dash or a typographic quote opening a sentence, which reads that sentence as the section. # It is narrower than the spelling before it, which admitted ASCII punctuation as well, and the remedy for the rest is a class of non-ASCII symbols rather than the whole of non-ASCII. +HEADING_OPENER = r"(?:</?[A-Za-z][^>]*>|[ \t>#]|[*_](?![ \t])|[^\x00-\x7f])*" PREVIOUSLY_MISSED = re.compile( - r"^(?:</?[A-Za-z][^>]*>|[ \t>#]|[*_](?![ \t])|[^\x00-\x7f])*Previously missed\b", + rf"^{HEADING_OPENER}(?:[0-9]+[ \t]+)?Previously missed\b", re.IGNORECASE, ) # CodeRabbit's own equivalent, collapsed into the review body like `SUPPRESSED` rather than raised as an inline comment. @@ -664,7 +670,6 @@ def strip_fences( # A body is read for these rather than trusted, because every reader below keys on one of them. # A heading this script has no spelling for is a section it will not find, reported as absent. # That is the shape of all three failures already on record here, each caught after it landed. -# The lists are small because the output is regular: 10 headings, 13 summaries and 4 labels. # Two overview formats are carried rather than one, the second arriving after that measurement. # Its own markers are listed beside the first's, since a round in either format can land next. # Counts are normalized to `(N)` and non-ASCII is dropped before comparing, and `unvetted` folds letter case at the comparison itself. @@ -693,19 +698,29 @@ def strip_fences( "Open (N)": OPEN, "(N) open findings": TOTAL, "(N) open finding": TOTAL, + "(N) open issues": TOTAL, + "(N) open issue": TOTAL, "Resolved since last review (N)": RESOLVED, "(N) resolved since last review": RESOLVED, } -VETTED_SUMMARIES = set(OVERVIEW_SECTIONS) | { - "Pull request overview", - NARRATIVE_SUMMARY, - "Show a summary per file", - "File summaries", - "Review details", - "Suppressed comments (N)", - "Comments suppressed due to low confidence (N)", +PREVIOUSLY_MISSED_SUMMARIES = { "Previously missed (N)", + "(N) previously missed issues", + "(N) previously missed issue", } +VETTED_SUMMARIES = ( + set(OVERVIEW_SECTIONS) + | { + "Pull request overview", + NARRATIVE_SUMMARY, + "Show a summary per file", + "File summaries", + "Review details", + "Suppressed comments (N)", + "Comments suppressed due to low confidence (N)", + } + | PREVIOUSLY_MISSED_SUMMARIES +) VETTED_LABELS = {"Files reviewed", "Comments generated", "Review effort level", "Review effort"} # Inline emphasis around a marker is presentation rather than identity. # The second overview format wraps every `<summary>` in `<strong>` where the first wrapped none. @@ -746,7 +761,8 @@ def strip_fences( # The format states a count per severity, so a round stating `2 <medium> . 3 <low>` raised five. CCR_FINDINGS = re.compile(r"^ {0,3}\*\*Findings:\*\*(.*)$", re.MULTILINE) CCR_OPEN_FINDINGS = re.compile( - r"^ {0,3}\*\*(\d+) open findings?\*\*[ \t]*\r?$", re.MULTILINE | re.IGNORECASE + r"^ {0,3}\*\*(\d+|No) open (?:findings?|issues?)\*\*[ \t]*(?:[^\x00-\x7f\s]+[ \t]*)?\r?$", + re.MULTILINE | re.IGNORECASE, ) # The markup the format writes a count beside, replaced by one sentinel before a count is read. # Masked rather than matched around, because a badge carries digits of its own in its attributes. @@ -817,9 +833,16 @@ def strip_fences( SUMMARY = re.compile(r"<summary(?:\s+[A-Za-z][^>]*)?>(.*?)</summary>", re.DOTALL | re.IGNORECASE) TAGS = re.compile(r"</?(?:details|summary)>", re.IGNORECASE) COUNT = re.compile(r"\((\d+)\)") +# A count opening a line past any opener `PREVIOUSLY_MISSED` admits, which is how `marker_blocks` tells prose. +OPENING_COUNT = re.compile(rf"{HEADING_OPENER}[0-9]+[ \t]") +# A count opening a heading's text, read once `heading_count` has reduced its badge and emphasis as `normal` does. +# No tag is skipped here, since a finding entry's summary opens on a severity badge rather than on heading markup. +# A badge read as markup made an entry titled `2 low confidence results` state a count, and bound a section there. +LEADING_COUNT = re.compile(r"(?:[ \t\r\n#*_]|[^\x00-\x7f])*([0-9]+)[ \t\r\n]") # What makes a line a heading rather than prose mentioning the phrase, in either markup. -# A line that is neither still qualifies where it carries a count. -# The section has appeared as a bare line too, and a count is what prose does not carry. +# A line that is neither still qualifies where it carries a `(N)` and does not open on a count. +# The section has appeared as a bare line too, and a closing `(N)` is what prose does not carry. +# A count opening the line is not that signal, since a sentence opens on one as readily as a heading does. HEADING = re.compile(r"\s*(?:#{1,6}\s|<summary)", re.IGNORECASE) # How many of the newest reviews and comments both queries read. @@ -2395,8 +2418,7 @@ def section_entries(body: str) -> list[tuple[str, int | None, list[str]]]: role = roles.get(normal(summary).casefold()) if role is None: continue - digits = re.search(r"\d+", EMPHASIS.sub("", summary)) - count = counted(digits.group()) if digits else None + count = heading_count(summary) text = masked[start:end] own = "\n".join(text[a:b] for a, b in details_spans(tags[start:end])[1]) found.append((role, count, [ln for ln in own.splitlines() if ENTRY_LINE.match(ln)])) @@ -2417,8 +2439,9 @@ def stated_total(body: str, preamble_only: bool = False) -> int | None: `MARKUP_MASK`, for the reason `details_tags` gives, which keeps every offset the preamble needs. A later revision of the format drops the `**Findings:**` line and states its total as a bare - `**0 open findings**` line, read here the same way, since left unread it printed `?` and - suppressed the shortfall the total is compared for. + `**0 open findings**` line. That line is read here the same way, since left unread it printed + `?` and suppressed the shortfall the total is compared for. A later one still writes that zero as + `**No open issues**` and an emoji. That is the same stated zero. That revision writes the bare line only for a zero, measured over every body carrying it, and states any other total as the count opening its open-findings section, so where the preamble @@ -2430,7 +2453,10 @@ def stated_total(body: str, preamble_only: bool = False) -> int | None: opener = DETAILS_OPEN.search(CODE_SPAN.sub(MARKUP_MASK, stripped)) preamble = plain[: opener.start()] if opener else plain totals = [findings_on(m.group(1)) for m in CCR_FINDINGS.finditer(preamble)] - totals += [counted(m.group(1)) for m in CCR_OPEN_FINDINGS.finditer(preamble)] + totals += [ + 0 if m.group(1).casefold() == "no" else counted(m.group(1)) + for m in CCR_OPEN_FINDINGS.finditer(preamble) + ] found = [t for t in totals if t is not None] if not found and not preamble_only: found = [c for role, c, *_ in read_overview(body)[2] if role == TOTAL and c is not None] @@ -3647,6 +3673,10 @@ def unrecognized_in(body: str) -> list[str]: quoting a heading is not a review carrying one, and this script's own pull requests quote these lists in full. + A previously-missed summary that no reader finds as a section is reported too. Its spelling is + vetted wherever it appears, while the reader keys on how its line opens. A layout the reader + misses would otherwise hand its findings to nobody under `shapes=ok`. + A body carrying no heading at all is reported rather than passed, since every one of the 333 measured opens on a heading and a body with none is a format nothing here has seen. A refusal is the exemption, being a bare paragraph by design and already classified as one. @@ -3669,6 +3699,13 @@ def unrecognized_in(body: str) -> list[str]: for marker in (normal(s) for s in dict.fromkeys(SUMMARY.findall(plain))) if unvetted(marker, VETTED_SUMMARIES) and not finding_entry(marker) ] + missed = [ + marker + for marker in (normal(s) for s in SUMMARY.findall(plain)) + if not unvetted(marker, PREVIOUSLY_MISSED_SUMMARIES) + ] + if len(missed) > len(previously_missed_blocks(body)): + found.append(f"summary: {missed[0]}, vetted while no reader found its section") found += [ f"metadata label: {la}" for la in dict.fromkeys(labels) if unvetted(la, VETTED_LABELS) ] @@ -4266,6 +4303,25 @@ def heading_of(block: str) -> str: return m.group(1) if m and head.lower().startswith("<summary") else head.split("\n", 1)[0] +def heading_count(heading: str) -> int | None: + """The count a heading states, closing it as `(N)` or opening it. + + One reader serves every caller, since the format moved its count to the front of a summary. + A caller reading only one of the two places misses the other. A section bounded on `(N)` alone + ran into a count-first sibling and printed that sibling's findings twice. + + It takes the heading itself rather than the block, and reads all of it. Cut to its first line, + a summary split as `Open` and then `(2)` lost the count its role was vetted with. + + The badge and the emphasis are reduced first, as `normal` reduces them before vetting. The + count is then read from the same text the role was vetted on. A badge reduces to its word, which is + what keeps an entry titled `2 low confidence results` from stating a count. + """ + text = EMPHASIS.sub("", SEVERITY_BADGE.sub(badge_text, heading)) + m = COUNT.search(text) or LEADING_COUNT.match(text) + return counted(m.group(1)) if m else None + + # `<details>(.*?)</details>` lazily pairs each open with the *next* close, which is the innermost one once a shape nests, silently losing everything the outer wrapper still carries after it. # CodeRabbit's outside-diff section does exactly that: a file wrapper nested inside the section heading, itself wrapping a per-finding "Prompt for AI Agents" block three levels deep. DETAILS_TAG = re.compile( @@ -4395,8 +4451,13 @@ def marker_blocks(body: str, marker: re.Pattern[str], strip_blockquote: bool = F """Return the review body's sections whose own heading matches `marker`, each sliced from that heading through to the end of its own region. - Shared by `suppressed_blocks` and `outside_diff_blocks`. `strip_blockquote` affects heading - detection only, never the returned content. + Shared by `suppressed_blocks`, `previously_missed_blocks`, and `outside_diff_blocks`. + `strip_blockquote` affects heading detection only, never the returned content. A summary split + across lines is folded onto its opening line in the returned content too, per `fold_summaries`. + + A line that is not a heading qualifies on a `(N)` it carries, and only where it does not open + on a count. A count opening a line is how a heading states it and also how a sentence opens, + so `2 previously missed (3) rounds ago` is prose, not a section. """ if not body: return [] @@ -4412,15 +4473,53 @@ def marker_blocks(body: str, marker: re.Pattern[str], strip_blockquote: bool = F # Scanned line by line rather than read from a wrapper's own summary. # A heading can sit in the wrapper's body instead, where reading only the summary would miss it. scan_lines = "".join(masked[a:b] for a, b in spans).splitlines() + scan_lines, raw_lines = fold_summaries(scan_lines, raw_lines) if strip_blockquote: scan_lines = [BLOCKQUOTE.sub("", ln) for ln in scan_lines] for i, line in enumerate(scan_lines): - if marker.search(line) and (HEADING.match(line) or COUNT.search(line)): + bare = COUNT.search(line) and not OPENING_COUNT.match(line) + if marker.search(line) and (HEADING.match(line) or bare): blocks.append("\n".join(raw_lines[i : sibling_heading(scan_lines, i, marker)])) break return blocks +FOLD_OPENER = re.compile(r"[>\s]*<summary(?=[\s>]|$)", re.IGNORECASE) +FOLD_STOP = re.compile(r"\s*<(?:details|summary)(?=[\s>/]|$)", re.IGNORECASE) + + +def fold_summaries(scan_lines: list[str], raw_lines: list[str]) -> tuple[list[str], list[str]]: + """Both line lists with each `<summary>` split across lines folded onto its opening line. + + A heading is found line by line, so a summary whose text sits on lines of its own read as no + heading at all. `unrecognized_in` vetted its spelling meanwhile and reported nothing. The groups + come from the scanned copy and apply to both, so the two keep addressing the same lines. + + Only a line opening on `<summary` folds, and the fold stops short at a line opening another + wrapper, summary, or Markdown heading. A tag named mid-sentence otherwise folded every line to + the next close. A sibling heading caught inside that fold was then no longer at a line's start. + A continuation line's blockquote markers are dropped, being the quoting rather than the text. + """ + scan: list[str] = [] + raw: list[str] = [] + i = 0 + while i < len(scan_lines): + end = i + if FOLD_OPENER.match(scan_lines[i]) and "</summary>" not in scan_lines[i].lower(): + for j in range(i + 1, len(scan_lines)): + bare = BLOCKQUOTE.sub("", scan_lines[j].strip()) + if FOLD_STOP.match(bare) or MARKDOWN_HEADING.match(bare): + break + if "</summary>" in bare.lower(): + end = j + break + for source, target in ((scan_lines, scan), (raw_lines, raw)): + parts = [BLOCKQUOTE.sub("", ln.strip()) for ln in source[i + 1 : end + 1]] + target.append(" ".join([source[i].rstrip(), *parts])) + i = end + 1 + return scan, raw + + def sibling_heading(scan_lines: list[str], start: int, marker: re.Pattern[str]) -> int: """Where this block ends, which is the next sibling section's heading or the region's end. @@ -4449,7 +4548,7 @@ def sibling_heading(scan_lines: list[str], start: int, marker: re.Pattern[str]) """ for j in range(start + 1, len(scan_lines)): line = scan_lines[j] - if not (HEADING.match(line) and COUNT.search(line)): + if not (HEADING.match(line) and heading_count(heading_of(line)) is not None): continue if any(o is not marker and o.search(line) for o in SECTION_MARKERS): return j @@ -4508,13 +4607,15 @@ def outside_diff_blocks(body: str) -> list[str]: def finding_count(block: str) -> int: - """The heading's `(N)`, floored at one, since a block reported as zero reads as a clean pass. + """The heading's stated count, floored at one, since a block reported as zero reads as clean. + + A heading stating its count first, `2 previously missed issues`, is read by `heading_count` too. + Left unread, it fell to the floor and reported one finding where the round raised two. A run longer than a count carries falls to that same floor rather than crashing the digest, the block existing being what says there is at least one finding in it. """ - m = COUNT.search(heading_of(block)) - stated = counted(m.group(1)) if m else None + stated = heading_count(heading_of(block)) return max(stated, 1) if stated is not None else 1 diff --git a/tests/test_pr_review.py b/tests/test_pr_review.py index ed1061fd3..b06e12701 100755 --- a/tests/test_pr_review.py +++ b/tests/test_pr_review.py @@ -1337,8 +1337,8 @@ def test_no_exit_code_rides_on_it(self) -> None: self.assertEqual(0, code) def test_prose_naming_the_section_is_not_the_section(self) -> None: - """`marker_blocks` accepts a line on a count alone, so an unanchored marker reads an - ordinary English sentence carrying a number as a section this round raised. + """`marker_blocks` accepts a line that is not a heading on a `(N)` alone. An unanchored + marker then reads an ordinary English sentence carrying a number as a section this round raised. The observed wording is common enough to write by accident, and the result is a banner telling the reader a clean round raised a finding against code the branch never touched. @@ -1366,6 +1366,17 @@ def test_prose_naming_the_section_is_not_the_section(self) -> None: line = f"{opener}Previously missed (2) findings were re-raised this round." self.assertIsNone(pr_review.PREVIOUSLY_MISSED.match(line)) self.assertEqual([], pr_review.previously_missed_blocks(f"{OVERVIEW}\n{line}\n")) + counted = f"{opener}2 previously missed issues were re-raised this round." + self.assertIsNone(pr_review.PREVIOUSLY_MISSED.match(counted)) + for bare in ( + "2 previously missed issues were re-raised this round.", + "2 previously missed (3) rounds ago, the guard is back.", + "> 2 previously missed (3) rounds ago, the guard is back.", + "<strong>2 previously missed (3) rounds ago</strong>", + "<em>2 previously missed (3) rounds ago, the guard is back.</em>", + ): + with self.subTest(bare=bare): + self.assertEqual([], pr_review.previously_missed_blocks(f"{OVERVIEW}\n{bare}\n")) def test_the_heading_is_read_through_whatever_markup_it_wears(self) -> None: """What the anchor must admit, beside the prose above that it must not. @@ -1386,10 +1397,154 @@ def test_the_heading_is_read_through_whatever_markup_it_wears(self) -> None: # An emphasis marker is heading markup and a bullet is prose, and a space is what separates them, so both spellings are held here rather than only the one. ("italic, with no tags", "_Previously missed (1)_"), ("bold and italic", "***Previously missed (1)***"), + ( + "the count-first spelling", + "<summary><strong>2 previously missed issues</strong></summary>", + ), + ("a count-first markdown heading", "### 2 previously missed issues"), ): with self.subTest(case=label): self.assertIsNotNone(pr_review.PREVIOUSLY_MISSED.match(heading)) + def test_a_count_first_heading_is_the_section_and_states_its_count(self) -> None: + """A later revision opens the heading on its count, `2 previously missed issues`. + + Unread, the round exited 43 on an unvetted summary. Its findings open no thread, so they + reached the agent only through the raw body. Read without its count, the block fell to the + floor of one. It then reported one finding where the round raised two. + """ + body = counted_missed(2) + self.assertEqual([], pr_review.unrecognized_in(body)) + blocks = pr_review.previously_missed_blocks(body) + self.assertEqual(1, len(blocks)) + self.assertEqual(2, pr_review.finding_count(blocks[0])) + self.assertEqual([], pr_review.unrecognized_in(counted_missed(1))) + self.assertEqual( + 3, pr_review.finding_count(pr_review.previously_missed_blocks(counted_missed(3))[0]) + ) + long_run = counted_missed(1).replace( + "1 previously missed issue", "12345 previously missed issue" + ) + long_blocks = pr_review.previously_missed_blocks(long_run) + self.assertEqual(1, len(long_blocks)) + self.assertEqual(1, pr_review.finding_count(long_blocks[0])) + self.answer(payload([review(body=body)])) + out, _ = pr_review.digest("o", "r", 7) + self.assertIn("previously_missed=2 (on_head=2 earlier=0)", out) + self.assertIn("shapes=ok", out) + self.assertIn("Low severity Constructed missed finding 2", out) + + def test_a_summary_split_across_lines_is_still_the_section(self) -> None: + """A summary whose text sits on lines of its own is vetted by its spelling. A heading + found only within one line read it as no section, while `shapes=ok` reported nothing.""" + for label, summary in ( + ( + "its text on a line of its own", + "<summary>\n<strong>2 previously missed issues</strong>\n</summary>", + ), + ( + "its text set off by blank lines", + "<summary>\n\n<strong>2 previously missed issues</strong>\n\n</summary>", + ), + ( + "its emphasis in capitals", + "<summary><STRONG>2 previously missed issues</STRONG></summary>", + ), + ( + "its wording broken across lines", + "<summary><strong>2 previously\nmissed issues</strong></summary>", + ), + ): + with self.subTest(case=label): + body = counted_missed(2).replace( + "<summary><strong>2 previously missed issues</strong></summary>", summary + ) + self.assertEqual([], pr_review.unrecognized_in(body)) + blocks = pr_review.previously_missed_blocks(body) + self.assertEqual(1, len(blocks)) + self.assertEqual(2, pr_review.finding_count(blocks[0])) + self.assertIn("Constructed missed finding 2", blocks[0]) + + def test_a_vetted_summary_no_reader_finds_is_reported(self) -> None: + """The spelling is vetted wherever it appears, while the reader keys on how its line opens. + + A layout the reader misses would hand its findings to nobody under `shapes=ok`. So the + shortfall between the two is reported as an unread shape instead. + """ + body = counted_missed(2).replace( + "<strong>2 previously missed issues</strong>", + "<strong>2</strong> previously missed issues", + ) + self.assertEqual([], pr_review.previously_missed_blocks(body)) + found = pr_review.unrecognized_in(body) + self.assertEqual(1, len(found)) + self.assertIn("no reader found its section", found[0]) + + def test_an_entry_title_opening_on_a_number_bounds_no_section(self) -> None: + """An entry's summary opens on its severity badge, which is not heading markup. + + Read as markup, an entry titled `2 low confidence results are dropped` stated a count. That + entry then bounded the section, and the entries after it were dropped from it. + """ + body = counted_missed(3).replace( + "Constructed missed finding 2", "2 low confidence results are dropped" + ) + blocks = pr_review.previously_missed_blocks(body) + self.assertEqual(1, len(blocks)) + self.assertIn("Constructed missed finding 3", blocks[0]) + self.assertEqual(3, pr_review.finding_count(blocks[0])) + + def test_a_tag_named_in_prose_folds_nothing(self) -> None: + """Only a line opening on `<summary` folds. A finding naming the tag mid-sentence folded + every line to the next close, and the sibling heading caught inside the fold was lost.""" + shared = ( + f"{OVERVIEW}\n<details>\n<summary>Review details</summary>\n\n" + "### Suppressed comments (1)\n\n" + "The SUMMARY pattern misses a <summary class=x> opener.\n\n" + "<summary><strong>2 previously missed issues</strong></summary>\n\n" + "The previously missed finding.\n</details>\n" + ) + missed = pr_review.previously_missed_blocks(shared) + self.assertEqual(1, len(missed)) + self.assertIn("The previously missed finding.", missed[0]) + suppressed = pr_review.suppressed_blocks(shared) + self.assertEqual(1, len(suppressed)) + self.assertNotIn("The previously missed finding.", suppressed[0]) + + def test_a_section_ends_where_its_count_first_sibling_starts(self) -> None: + """A sibling bounds a section only where its heading states a count. + + A bound reading only `(N)` ran a suppressed block into a count-first previously-missed + sibling. The digest then printed that finding twice, once under the suppressed banner. + """ + shared = ( + f"{OVERVIEW}\n<details>\n<summary>Review details</summary>\n\n" + "### Suppressed comments (1)\n\nThe suppressed finding.\n\n" + "<details>\n<summary><strong>2 previously missed issues</strong></summary>\n\n" + "The previously missed finding.\n</details>\n</details>\n" + ) + suppressed = pr_review.suppressed_blocks(shared) + self.assertEqual(1, len(suppressed)) + self.assertIn("The suppressed finding.", suppressed[0]) + self.assertNotIn("The previously missed finding.", suppressed[0]) + self.answer(payload([review(body=shared)])) + out, _ = pr_review.digest("o", "r", 7) + self.assertEqual(1, out.count("The previously missed finding.")) + + def test_a_count_first_section_cancels_the_flagging_verdict_reading(self) -> None: + """The round flags a closer look and states no open issue. + + Its findings are the previously missed ones, which the digest now counts. So the round is + not a verdict naming nothing. + """ + self.answer(payload([review(body=counted_missed(2))])) + self.assertIsNone(pr_review.uncounted_verdict(payload([review(body=counted_missed(2))]))) + with contextlib.redirect_stdout(io.StringIO()) as printed: + code = pr_review.main(["status", "7", "--repo", "o/r"]) + self.assertNotIn("VERDICT WITH NO COUNTED FINDING", printed.getvalue()) + self.assertNotIn("UNRECOGNIZED", printed.getvalue()) + self.assertEqual(0, code) + def test_a_quoted_heading_is_not_a_section_this_round_raised(self) -> None: """Every other reader in this file strips a quotation before looking, and this one did not. @@ -1879,6 +2034,29 @@ def previously_missed( ) +def counted_missed(entries: int) -> str: + """The later revision's previously-missed section, its summary opening on the count. + + The round flags a closer look and states its zero as `**No open issues**`. The section nests + one badged entry per finding, each a `FILE:LINE` anchor and a priority-tagged paragraph. + That nesting is the observed one, and every title, path, and sentence is constructed, per + GOVERNANCE.md "Representative Data in Agent-Authored Text". + """ + noun = "issue" if entries == 1 else "issues" + found = "".join( + f"<details>\n<summary>{BADGE} Constructed missed finding {i}</summary>\n\n" + f"`a.py:{10 * i}`\n\n[P1] A constructed paragraph about the finding.\n</details>\n" + for i in range(1, entries + 1) + ) + block = ( + f"<details>\n<summary><strong>{entries} previously missed {noun}</strong></summary>\n\n" + f"In code that hasn't changed since last review\n\n{found}</details>\n\n<details>" + ) + return revised_with( + "**No open issues** \U0001f389", "### \U0001f535 Needs a closer look" + ).replace("<details>", block, 1) + + class TestCoverage(GqlCase): """The round that covered the head and read part of the diff, which is a clean pass elsewhere. @@ -4109,6 +4287,22 @@ def test_the_revision_s_open_findings_line_is_its_stated_total(self) -> None: ) self.assertIsNone(pr_review.stated_total(inside)) + def test_the_revision_s_no_open_issues_line_is_a_stated_zero(self) -> None: + """A later revision writes its zero as `**No open issues**` closed by an emoji, and unread + it printed `?` over a round stating nothing open. The line anchor still holds, so prose + naming the words, or a line going on past the emoji, states nothing.""" + for line, total in ( + ("**No open issues** \U0001f389", 0), + ("**No open issues**", 0), + ("**1 open issue**", 1), + ("**2 open issues**", 2), + ): + with self.subTest(line=line): + self.assertEqual(total, pr_review.stated_total(overview_v2_revised(line))) + for line in ("It had **No open issues** before.", "**No open issues** \U0001f389 yet"): + with self.subTest(line=line): + self.assertIsNone(pr_review.stated_total(overview_v2_revised(line))) + def test_a_total_split_across_severities_is_summed(self) -> None: """The format states a count per severity, so the first number alone undercounts. @@ -4651,23 +4845,6 @@ def test_a_phrase_rewrapped_across_comment_lines_reads_as_one_sentence(self) -> comment_flowed_text(wrapped), ) - def test_the_vetted_lists_hold_what_the_comment_beside_them_counts(self) -> None: - """The comment states the sizes, and adding an entry without it is how it goes stale.""" - source = comment_flowed_text(Path(pr_review.__file__).read_text(encoding="utf-8")) - stated = re.search( - r"the output is regular: (\d+) headings, (\d+) summaries and (\d+) labels", source - ) - if stated is None: - self.fail("the comment stating the vetted list sizes is no longer in the source") - self.assertEqual( - ( - len(pr_review.VETTED_HEADINGS), - len(pr_review.VETTED_SUMMARIES), - len(pr_review.VETTED_LABELS), - ), - tuple(int(g) for g in stated.groups()), - ) - COVERS_ONE = "<!-- fleet-review: reviewed=1 changed=1 findings=0 -->" @@ -4728,6 +4905,42 @@ def test_the_open_findings_section_states_the_round_s_total(self) -> None: body = revised_with(open_section(("6000000001", "6000000002", "6000000003"))) self.assertEqual(3, pr_review.stated_total(body)) + def test_the_open_issues_spelling_is_the_same_section(self) -> None: + """A later revision writes `1 open issue` where it wrote `1 open finding`, and exited 43. + + Its entries link the round's own threads, as the earlier spelling's do. So it is read as the + same section: vetted, stating the total, and each entry checked against the threads. + """ + for ids in (("6000000001",), ("6000000001", "6000000002")): + noun = "issue" if len(ids) == 1 else "issues" + with self.subTest(count=len(ids)): + body = revised_with(open_section(ids, noun=noun)) + self.assertEqual([], pr_review.unrecognized_in(body)) + self.assertEqual(len(ids), pr_review.stated_total(body)) + rd = review(body=body, rid="PRR_head") + threads = [thread(f"T{i}", rid="PRR_head", cid=c) for i, c in enumerate(ids)] + self.assertEqual(0, pr_review.unthreaded_entries(payload([rd], threads))) + lone = revised_with(open_section(("6000000009",), noun="issue")) + self.assertEqual(1, pr_review.unthreaded_entries(payload([review(body=lone)]))) + + def test_a_summary_split_across_lines_keeps_its_count(self) -> None: + """The role is vetted on the whole summary, so its count is read from the whole summary + too. Read from its first line alone, `Open` then `(2)` stated no count.""" + ids = ("6000000001", "6000000002", "6000000003") + split = revised_with(open_section(ids)).replace( + "<strong>3 open findings</strong>", "<strong>\n3 open findings\n</strong>" + ) + self.assertEqual([3], [c for r, c, _ in pr_review.section_entries(split) if r == "total"]) + self.assertEqual(3, pr_review.stated_total(split)) + for spelled in ("<STRONG>3 open findings</STRONG>", "<strong>3</strong> open findings"): + with self.subTest(spelled=spelled): + body = revised_with(open_section(ids)).replace( + "<strong>3 open findings</strong>", spelled + ) + self.assertEqual(3, pr_review.stated_total(body)) + closing = overview_v2().replace("<strong>Open (2)</strong>", "<strong>Open\n(2)</strong>") + self.assertEqual([2], [c for r, c, _ in pr_review.section_entries(closing) if r == "open"]) + def test_a_count_first_summary_outside_the_table_is_still_unvetted(self) -> None: """The table vets spellings, not any summary that opens on a count.""" body = revised_with(open_section(("6000000001",), noun="questions")) @@ -5015,7 +5228,11 @@ def test_fifty_outranks_a_stuck_check(self) -> None: def test_a_collapsed_finding_cancels_the_reading(self) -> None: """A suppressed or previously-missed block is a counted finding the digest already prints.""" - for summary in ("Previously missed (1)", "Suppressed comments (1)"): + for summary in ( + "Previously missed (1)", + "2 previously missed issues", + "Suppressed comments (1)", + ): with self.subTest(summary=summary): block = ( f"<details>\n<summary><strong>{summary}</strong></summary>\n\n" From 8198ee0a41cb9b1cf986cb10ff89bcd36f6fc886 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:58:47 -0700 Subject: [PATCH 3/3] Name the Never-Tiered-Down Groups in the Plural in backlog-burndown (#3177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Removing the incidental count in #3168 left "These kinds of group are never tiered below `frontier`" in `backlog-burndown`, which Copilot flagged on the promotion #3175 as an incorrect singular noun. It now reads "Groups of these kinds are never tiered below `frontier`", which still introduces the bullets that each open on "One touching" or "One whose". The generated distributions are regenerated. ## Verification - `scripts/build_dist.py --check` reports the distributions current. - `prose_lint.py --diff` (default rules and `--check sentence-length`) and markdownlint are clean. - Local strict review: 0 findings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .agents/skills/backlog-burndown/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digests/backlog-burndown | 2 +- .claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md | 2 +- .github/skills/backlog-burndown/SKILL.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index 631c1bc7f..b2a31609f 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. These kinds of group are never tiered +named it for this run. Groups of these kinds are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 4707bb7e8..5573af9d9 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -c3f8d3afecf7d599 +1ce0fe48128d5dac diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index 631c1bc7f..b2a31609f 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. These kinds of group are never tiered +named it for this run. Groups of these kinds are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index 631c1bc7f..b2a31609f 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -330,7 +330,7 @@ what it leaves to judgment here. The tier is chosen per group rather than defaul stronger tier produces better work and takes fewer review rounds to land it. That often costs less than a cheaper worker looping. The tier is `frontier` or `standard`, never `bulk`, since every worker judges its pull request's review findings. It is `highest` only where the maintainer -named it for this run. These kinds of group are never tiered +named it for this run. Groups of these kinds are never tiered below `frontier`: - One touching **carried canonical content**, as `GOVERNANCE.md` "Verification Discipline" bounds