From 022e1654a6164ca61d83237a6f5f21906107b82e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 16 Oct 2026 16:33:04 -0700 Subject: [PATCH 1/5] Refer to an Enumeration Instead of Stating an Incidental Count Add an "Incidental Counts" rule to GOVERNANCE.md "Documentation Style Conventions", carried whole into comment-and-doc-style as a generated include, and point fleet-code-review at it. Remove the incidental counts the carried text already held. 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 | 8 ++--- .../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 | 27 +++++++++++------ .../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 | 4 +-- .agents/skills/session-handoff/SKILL.md | 20 ++++++------- .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 | 8 ++--- .../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 | 27 +++++++++++------ .../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 | 4 +-- .../skills/session-handoff/SKILL.md | 20 ++++++------- .../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 | 8 ++--- .../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 | 27 +++++++++++------ .../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 | 4 +-- .github/skills/session-handoff/SKILL.md | 20 ++++++------- .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 | 30 +++++++++++-------- WORKFLOW.md | 18 +++++------ scripts/README.md | 2 +- scripts/handoff.py | 2 +- 79 files changed, 241 insertions(+), 204 deletions(-) diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index 8ec25a370..db119eadf 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 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. `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..5624ae4da 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 @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records @@ -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..b2cca0815 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. +These qualifications 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..fe1ab7f61 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -14,7 +14,9 @@ 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 whenever a + sentence or comment states how many items a list, a table, or a set of cases holds, since that + count goes stale at the next edit to the enumeration. --- # Comment and Doc Style @@ -65,7 +67,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 +201,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 +These facts 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 +222,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 neither 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 @@ -240,7 +242,7 @@ other kind reads exactly like one. Neither carve-out above lifts this ban. Item 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 +296,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 +386,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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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..991ed3983 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, 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..3f0899127 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -68,7 +68,7 @@ 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, 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. Those 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..8c65ce0ef 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 @@ -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 +These things 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,7 +290,7 @@ 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 +their own scope. A session working a named lane passes `--track` on each 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. @@ -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 them, 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..f8e8e258b 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. These things make 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..18f43e6e6 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 it all 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..4fb06c162 100644 --- a/.claude-plugin/fleet-skills/.source-digests/agent-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/agent-conduct @@ -1 +1 @@ -8fd3e8fc02a6a212 +fd06cf6db7ab4aa0 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..78acdcc56 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -aac3800c3f96ce19 +18500edf5d9e7b3d 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..fd19cd5b4 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 +2532d810882ae917 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..3135ebdc7 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 +52ab3848ac8b65a0 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..833f3a4dc 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -f4eff2621ffb5828 +80672d8ef28cbae6 diff --git a/.claude-plugin/fleet-skills/.source-digests/session-handoff b/.claude-plugin/fleet-skills/.source-digests/session-handoff index b63c2d958..2ba32243a 100644 --- a/.claude-plugin/fleet-skills/.source-digests/session-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/session-handoff @@ -1 +1 @@ -5daeb44bdd891a90 +b1b72f730317467e diff --git a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff index 05bbf8d5b..3df6351b2 100644 --- a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff @@ -1 +1 @@ -66316cebf3923233 +da642cd70d215ec6 diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 109e01eab..598843f19 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -82e59e34aeb7fac2 +b9eeafd90d36fb1f diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index 8ec25a370..db119eadf 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 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. `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..5624ae4da 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 @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records @@ -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..b2cca0815 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. +These qualifications 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..fe1ab7f61 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,9 @@ 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 whenever a + sentence or comment states how many items a list, a table, or a set of cases holds, since that + count goes stale at the next edit to the enumeration. --- # Comment and Doc Style @@ -65,7 +67,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 +201,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 +These facts 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 +222,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 neither 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 @@ -240,7 +242,7 @@ other kind reads exactly like one. Neither carve-out above lifts this ban. Item 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 +296,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 +386,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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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..991ed3983 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, 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..3f0899127 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -68,7 +68,7 @@ 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, 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. Those 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..8c65ce0ef 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 @@ -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 +These things 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,7 +290,7 @@ 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 +their own scope. A session working a named lane passes `--track` on each 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. @@ -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 them, 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..f8e8e258b 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. These things make 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..18f43e6e6 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 it all 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..db119eadf 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 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. `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..5624ae4da 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 @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records @@ -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..b2cca0815 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. +These qualifications 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..fe1ab7f61 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -14,7 +14,9 @@ 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 whenever a + sentence or comment states how many items a list, a table, or a set of cases holds, since that + count goes stale at the next edit to the enumeration. --- # Comment and Doc Style @@ -65,7 +67,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 +201,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 +These facts 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 +222,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 neither 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 @@ -240,7 +242,7 @@ other kind reads exactly like one. Neither carve-out above lifts this ban. Item 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 +296,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 +386,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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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..991ed3983 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, 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..3f0899127 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -68,7 +68,7 @@ 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, 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. Those 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..8c65ce0ef 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 @@ -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 +These things 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,7 +290,7 @@ 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 +their own scope. A session working a named lane passes `--track` on each 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. @@ -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 them, 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..f8e8e258b 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. These things make 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..18f43e6e6 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 it all 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..b4c1ed272 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. The chain closes each way a scratch file fails. 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..408669e50 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 each 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. ### 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..f40ef4591 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 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. `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 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. - **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. @@ -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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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. Neither 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..216400c1a 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 it all 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 three times with its `mode` input set to `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`, 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 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 a3be034b3eb8a8d4567fd485069173494530bc56 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:40:35 -0700 Subject: [PATCH 2/5] Fix Antecedents Left Dangling by the Count Removals Rebind pronouns that pointed at the wrong noun once a count was gone, finish the half-removed counts later in the same paragraphs, keep the ordinal-structured count in backlog-burndown, apply the rule to the References carve-outs, and drop "a version" from the rule's list of counts the reader needs, since a version is not an enumeration size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agents/skills/agent-conduct/SKILL.md | 2 +- .agents/skills/backlog-burndown/SKILL.md | 2 +- .../references/release-publish-mechanics.md | 2 +- .agents/skills/comment-and-doc-style/SKILL.md | 13 ++++++------- .agents/skills/repo-worktree/SKILL.md | 4 ++-- .agents/skills/session-handoff/SKILL.md | 4 ++-- .agents/skills/unattended-handoff/SKILL.md | 2 +- .../references/test-methodology.md | 2 +- .../fleet-skills/.source-digests/agent-conduct | 2 +- .../fleet-skills/.source-digests/backlog-burndown | 2 +- .../.source-digests/branching-and-release-model | 2 +- .../.source-digests/comment-and-doc-style | 2 +- .../fleet-skills/.source-digests/repo-worktree | 2 +- .../fleet-skills/.source-digests/session-handoff | 2 +- .../fleet-skills/.source-digests/unattended-handoff | 2 +- .../.source-digests/workflow-ci-contract | 2 +- .../fleet-skills/skills/agent-conduct/SKILL.md | 2 +- .../fleet-skills/skills/backlog-burndown/SKILL.md | 2 +- .../references/release-publish-mechanics.md | 2 +- .../skills/comment-and-doc-style/SKILL.md | 13 ++++++------- .../fleet-skills/skills/repo-worktree/SKILL.md | 4 ++-- .../fleet-skills/skills/session-handoff/SKILL.md | 4 ++-- .../fleet-skills/skills/unattended-handoff/SKILL.md | 2 +- .../references/test-methodology.md | 2 +- .github/skills/agent-conduct/SKILL.md | 2 +- .github/skills/backlog-burndown/SKILL.md | 2 +- .../references/release-publish-mechanics.md | 2 +- .github/skills/comment-and-doc-style/SKILL.md | 13 ++++++------- .github/skills/repo-worktree/SKILL.md | 4 ++-- .github/skills/session-handoff/SKILL.md | 4 ++-- .github/skills/unattended-handoff/SKILL.md | 2 +- .../references/test-methodology.md | 2 +- AGENTS.md | 2 +- CODESTYLE.md | 2 +- GOVERNANCE.md | 10 +++++----- WORKFLOW.md | 4 ++-- 36 files changed, 62 insertions(+), 65 deletions(-) diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index db119eadf..f31e13231 100644 --- a/.agents/skills/agent-conduct/SKILL.md +++ b/.agents/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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 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. diff --git a/.agents/skills/backlog-burndown/SKILL.md b/.agents/skills/backlog-burndown/SKILL.md index 5624ae4da..631c1bc7f 100644 --- a/.agents/skills/backlog-burndown/SKILL.md +++ b/.agents/skills/backlog-burndown/SKILL.md @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records 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 b2cca0815..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 @@ -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. -These 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/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index fe1ab7f61..51772e1d6 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -14,9 +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). Also triggers whenever a - sentence or comment states how many items a list, a table, or a set of cases holds, since that - count goes stale at the next edit to the enumeration. + 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 @@ -201,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. -These facts 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 @@ -222,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. -The carve-outs below are 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 @@ -238,7 +237,7 @@ 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. @@ -388,7 +387,7 @@ Clarify Devcontainer Setup Steps in README <!-- 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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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. +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 --> diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 3f0899127..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 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 diff --git a/.agents/skills/session-handoff/SKILL.md b/.agents/skills/session-handoff/SKILL.md index 8c65ce0ef..1e09c157d 100644 --- a/.agents/skills/session-handoff/SKILL.md +++ b/.agents/skills/session-handoff/SKILL.md @@ -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 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 @@ -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. -These 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 diff --git a/.agents/skills/unattended-handoff/SKILL.md b/.agents/skills/unattended-handoff/SKILL.md index f8e8e258b..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. These 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/references/test-methodology.md b/.agents/skills/workflow-ci-contract/references/test-methodology.md index 18f43e6e6..3ba870bb6 100644 --- a/.agents/skills/workflow-ci-contract/references/test-methodology.md +++ b/.agents/skills/workflow-ci-contract/references/test-methodology.md @@ -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 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 it all 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 4fb06c162..64dcfeb51 100644 --- a/.claude-plugin/fleet-skills/.source-digests/agent-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/agent-conduct @@ -1 +1 @@ -fd06cf6db7ab4aa0 +fe6b18daf82cf0ce diff --git a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown index 78acdcc56..4707bb7e8 100644 --- a/.claude-plugin/fleet-skills/.source-digests/backlog-burndown +++ b/.claude-plugin/fleet-skills/.source-digests/backlog-burndown @@ -1 +1 @@ -18500edf5d9e7b3d +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 fd19cd5b4..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 @@ -2532d810882ae917 +af770918d60e7ecc 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 3135ebdc7..295c35fe4 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 @@ -52ab3848ac8b65a0 +1d902f7abb93b2d4 diff --git a/.claude-plugin/fleet-skills/.source-digests/repo-worktree b/.claude-plugin/fleet-skills/.source-digests/repo-worktree index 833f3a4dc..1021f3289 100644 --- a/.claude-plugin/fleet-skills/.source-digests/repo-worktree +++ b/.claude-plugin/fleet-skills/.source-digests/repo-worktree @@ -1 +1 @@ -80672d8ef28cbae6 +010e5fbece90a0b9 diff --git a/.claude-plugin/fleet-skills/.source-digests/session-handoff b/.claude-plugin/fleet-skills/.source-digests/session-handoff index 2ba32243a..40f42d21b 100644 --- a/.claude-plugin/fleet-skills/.source-digests/session-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/session-handoff @@ -1 +1 @@ -b1b72f730317467e +93d1bc3f5e9d4039 diff --git a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff index 3df6351b2..ed40090d1 100644 --- a/.claude-plugin/fleet-skills/.source-digests/unattended-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/unattended-handoff @@ -1 +1 @@ -da642cd70d215ec6 +680119285f154228 diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 598843f19..4ad8ce7f7 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -b9eeafd90d36fb1f +747f86164bec8c5f diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index db119eadf..f31e13231 100644 --- a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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 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. diff --git a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md index 5624ae4da..631c1bc7f 100644 --- a/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records 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 b2cca0815..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 @@ -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. -These 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/comment-and-doc-style/SKILL.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md index fe1ab7f61..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,9 +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). Also triggers whenever a - sentence or comment states how many items a list, a table, or a set of cases holds, since that - count goes stale at the next edit to the enumeration. + 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 @@ -201,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. -These facts 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 @@ -222,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. -The carve-outs below are 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 @@ -238,7 +237,7 @@ 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. @@ -388,7 +387,7 @@ Clarify Devcontainer Setup Steps in README <!-- 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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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. +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 --> diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 3f0899127..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 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 diff --git a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md index 8c65ce0ef..1e09c157d 100644 --- a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md @@ -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 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 @@ -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. -These 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 diff --git a/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/unattended-handoff/SKILL.md index f8e8e258b..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. These 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/references/test-methodology.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md index 18f43e6e6..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 @@ -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 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 it all 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 db119eadf..f31e13231 100644 --- a/.github/skills/agent-conduct/SKILL.md +++ b/.github/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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 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. diff --git a/.github/skills/backlog-burndown/SKILL.md b/.github/skills/backlog-burndown/SKILL.md index 5624ae4da..631c1bc7f 100644 --- a/.github/skills/backlog-burndown/SKILL.md +++ b/.github/skills/backlog-burndown/SKILL.md @@ -194,7 +194,7 @@ for, and it binds harder than any throughput target. stops there and reports, rather than dispatching against a stale answer, and stopping rather than deferring is what the cleanup and promotion steps need too, since both read the same remote. - Those enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is + Those three enumerate the branches, with one gap: a branch in the standalone clone `repo-worktree` allows as a fallback is reached only once it is pushed, since `git branch -r` inventories the remote rather than this repository's checkouts. The claim comments are what say which files each branch holds, since a branch name says nothing about a file set, an unpushed branch has no pull request diff to read, and no diff of any branch reports the predicted set a claim records 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 b2cca0815..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 @@ -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. -These 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/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md index fe1ab7f61..51772e1d6 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -14,9 +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). Also triggers whenever a - sentence or comment states how many items a list, a table, or a set of cases holds, since that - count goes stale at the next edit to the enumeration. + 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 @@ -201,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. -These facts 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 @@ -222,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. -The carve-outs below are 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 @@ -238,7 +237,7 @@ 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. @@ -388,7 +387,7 @@ Clarify Devcontainer Setup Steps in README <!-- 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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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. +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 --> diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 3f0899127..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 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 diff --git a/.github/skills/session-handoff/SKILL.md b/.github/skills/session-handoff/SKILL.md index 8c65ce0ef..1e09c157d 100644 --- a/.github/skills/session-handoff/SKILL.md +++ b/.github/skills/session-handoff/SKILL.md @@ -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 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 @@ -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. -These 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 diff --git a/.github/skills/unattended-handoff/SKILL.md b/.github/skills/unattended-handoff/SKILL.md index f8e8e258b..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. These 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/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md index 18f43e6e6..3ba870bb6 100644 --- a/.github/skills/workflow-ci-contract/references/test-methodology.md +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -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 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 it all 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 b4c1ed272..823568182 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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. The chain closes each way a scratch file fails. 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. diff --git a/CODESTYLE.md b/CODESTYLE.md index 408669e50..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 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 each 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 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index f40ef4591..0d30589ce 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.** The conditions below 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 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,7 +32,7 @@ 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. 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 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.** 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. @@ -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 @@ -182,7 +182,7 @@ ASD-STE100's structural half is the adopted house style: short sentences, one in ### 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, a version, or a measured result. Such a count names its source, such as the constant, the manifest, 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. +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 @@ -286,7 +286,7 @@ 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. 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 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. diff --git a/WORKFLOW.md b/WORKFLOW.md index 216400c1a..36b87c5a4 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -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 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 it all 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 @@ -301,7 +301,7 @@ 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 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`, 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. +- **`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 --> From e01150a633d7de3d0790a3a6776ab55c70dc0413 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:42:19 -0700 Subject: [PATCH 3/5] Name the Noun "Each" Counts in session-handoff Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agents/skills/session-handoff/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digests/session-handoff | 2 +- .claude-plugin/fleet-skills/skills/session-handoff/SKILL.md | 2 +- .github/skills/session-handoff/SKILL.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/skills/session-handoff/SKILL.md b/.agents/skills/session-handoff/SKILL.md index 1e09c157d..4d8e4178d 100644 --- a/.agents/skills/session-handoff/SKILL.md +++ b/.agents/skills/session-handoff/SKILL.md @@ -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 each 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 diff --git a/.claude-plugin/fleet-skills/.source-digests/session-handoff b/.claude-plugin/fleet-skills/.source-digests/session-handoff index 40f42d21b..7042e8e26 100644 --- a/.claude-plugin/fleet-skills/.source-digests/session-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/session-handoff @@ -1 +1 @@ -93d1bc3f5e9d4039 +1ea7ef4596eb8b14 diff --git a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md index 1e09c157d..4d8e4178d 100644 --- a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md @@ -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 each 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 diff --git a/.github/skills/session-handoff/SKILL.md b/.github/skills/session-handoff/SKILL.md index 1e09c157d..4d8e4178d 100644 --- a/.github/skills/session-handoff/SKILL.md +++ b/.github/skills/session-handoff/SKILL.md @@ -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 each 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 From 6ff068a0f265752777b0bd561a8c821d8e34a9dc Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:36:27 -0700 Subject: [PATCH 4/5] Give the Two Rewritten Fragments a Verb Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agents/skills/agent-conduct/SKILL.md | 2 +- .../comment-and-doc-style/references/carried-doc-references.md | 2 +- .claude-plugin/fleet-skills/.source-digests/agent-conduct | 2 +- .../fleet-skills/.source-digests/comment-and-doc-style | 2 +- .claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md | 2 +- .../comment-and-doc-style/references/carried-doc-references.md | 2 +- .github/skills/agent-conduct/SKILL.md | 2 +- .../comment-and-doc-style/references/carried-doc-references.md | 2 +- GOVERNANCE.md | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index f31e13231..45c60c1f1 100644 --- a/.agents/skills/agent-conduct/SKILL.md +++ b/.agents/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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. +- **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. 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 991ed3983..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 -The following, 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/.source-digests/agent-conduct b/.claude-plugin/fleet-skills/.source-digests/agent-conduct index 64dcfeb51..d241073ab 100644 --- a/.claude-plugin/fleet-skills/.source-digests/agent-conduct +++ b/.claude-plugin/fleet-skills/.source-digests/agent-conduct @@ -1 +1 @@ -fe6b18daf82cf0ce +5e3c2f4aee886b23 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 295c35fe4..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 @@ -1d902f7abb93b2d4 +39aa8ed8d7eda42b diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index f31e13231..45c60c1f1 100644 --- a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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. +- **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. 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 991ed3983..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 -The following, 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/agent-conduct/SKILL.md b/.github/skills/agent-conduct/SKILL.md index f31e13231..45c60c1f1 100644 --- a/.github/skills/agent-conduct/SKILL.md +++ b/.github/skills/agent-conduct/SKILL.md @@ -102,7 +102,7 @@ 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.** The conditions below 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. +- **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. 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 991ed3983..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 -The following, 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/GOVERNANCE.md b/GOVERNANCE.md index 0d30589ce..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.** The conditions below 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. +- **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. From 7ccdd594a49ecf32743f3e5d4a5b3af4b3545591 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:38:13 -0700 Subject: [PATCH 5/5] Rebind the session-handoff Pronouns the Count Removal Orphaned Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agents/skills/session-handoff/SKILL.md | 6 +++--- .claude-plugin/fleet-skills/.source-digests/session-handoff | 2 +- .claude-plugin/fleet-skills/skills/session-handoff/SKILL.md | 6 +++--- .github/skills/session-handoff/SKILL.md | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.agents/skills/session-handoff/SKILL.md b/.agents/skills/session-handoff/SKILL.md index 4d8e4178d..235ed135f 100644 --- a/.agents/skills/session-handoff/SKILL.md +++ b/.agents/skills/session-handoff/SKILL.md @@ -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 that accepts -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 @@ -309,7 +309,7 @@ comments on whatever link that lane has, closing it too where it was open. - **`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 every step. **`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 diff --git a/.claude-plugin/fleet-skills/.source-digests/session-handoff b/.claude-plugin/fleet-skills/.source-digests/session-handoff index 7042e8e26..141430d17 100644 --- a/.claude-plugin/fleet-skills/.source-digests/session-handoff +++ b/.claude-plugin/fleet-skills/.source-digests/session-handoff @@ -1 +1 @@ -1ea7ef4596eb8b14 +8f9da4cca73d8b01 diff --git a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md index 4d8e4178d..235ed135f 100644 --- a/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/session-handoff/SKILL.md @@ -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 that accepts -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 @@ -309,7 +309,7 @@ comments on whatever link that lane has, closing it too where it was open. - **`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 every step. **`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 diff --git a/.github/skills/session-handoff/SKILL.md b/.github/skills/session-handoff/SKILL.md index 4d8e4178d..235ed135f 100644 --- a/.github/skills/session-handoff/SKILL.md +++ b/.github/skills/session-handoff/SKILL.md @@ -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 that accepts -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 @@ -309,7 +309,7 @@ comments on whatever link that lane has, closing it too where it was open. - **`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 every step. **`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