diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index 00935579..a1088902 100644 --- a/.agents/skills/agent-conduct/SKILL.md +++ b/.agents/skills/agent-conduct/SKILL.md @@ -19,6 +19,7 @@ Read `GOVERNANCE.md` "Verification Discipline" before reporting success on anyth - **Run the repo's whole lint gate before every push**, not the parts that look relevant, because the tool most likely to catch a change is often the one it seems least about. - **A launched process is not a result.** Report the output the wait produced, and where it produced none, that absence is the report. Never name an external cause the record does not carry. - **A local clone is not the branch it names.** Fetch immediately before reading, or read the live ref, and name the ref and commit in any finding a local read produced. +- **A "does not exist" claim names the branch it was checked against.** A worktree's default branch is not necessarily the one the content lives on: in-flight content on a `release`-model repo lands on `develop` before `main`, per `GOVERNANCE.md` "Branching Model," so check that branch before reporting anything absent repo-wide. - **A test asserts the mechanism it names, and a gate has to be watched failing.** A case that passes for an incidental reason is worse than no case, because it is later cited as evidence. - **Platform-specific code is verified only on the platform it runs on.** Reasoning about PowerShell, macOS, or WSL-specific behavior from a different host is not verification, however closely it matches an already-tested equivalent elsewhere. State an untested structural match as exactly that, never in the words used for a tested fact, and when no agent in the loop has access to the target platform, say so and defer or ship it labeled unverified. diff --git a/.agents/skills/git-commit-conventions/SKILL.md b/.agents/skills/git-commit-conventions/SKILL.md index 7ff9e43b..9790f987 100644 --- a/.agents/skills/git-commit-conventions/SKILL.md +++ b/.agents/skills/git-commit-conventions/SKILL.md @@ -32,6 +32,12 @@ scope-widened commit, a rewritten shared history, a destructive reset). developer unless the developer has explicitly authorized committing for the current ask ("commit this", "open a PR"). Authorization is scope-bound: it covers the commits that specific task needs, not a blanket license for the rest of the session. +- **Stage by explicit path, never `git add -A` or `git add .`.** A blanket add stages whatever + else happens to be in the tree, and what it sweeps in is another task's uncommitted work, + landing in a commit whose subject never mentions it, committed by a session that never saw it. + That sweep has happened, which is why task isolation exists (the `repo-worktree` skill), and + isolation makes a shared tree rare rather than impossible. Name the files this task changed, + and let anything else stay unstaged. - **"Commit" means commit and push.** An authorization to commit carries the push to the feature branch the work belongs on, because nothing reviews a local commit. The Copilot review loop, the required status checks, and the maintainer all read the remote, so stopping at `git commit` @@ -39,10 +45,14 @@ scope-widened commit, a rewritten shared history, a destructive reset). progress while none of the gates have run. Push to the feature branch, never to a protected branch, and never with `--force`. Holding a commit locally is the narrower case: it happens when the developer asks for it, not by default. -- **Check `git status` for the maintainer's own uncommitted edits before committing.** The - maintainer hand-edits files live, often `README.md`/`HISTORY.md`, sometimes with an editor's - LF -> CRLF flip on top. If there are changes not made this session, ask whether to include them - rather than bundling half-finished work or stranding it in an unrelated commit. +- **Check `git status` before committing, and treat any change this session did not make as a + stop.** The maintainer hand-edits files live, often `README.md`/`HISTORY.md`, sometimes with an + editor's LF -> CRLF flip on top, and a sibling agent session sharing the tree leaves its edits + the same way. Whoever the author is, a change this session did not make is never bundled: ask + whether to include it, or leave it unstaged and say so, rather than committing half-finished + work or stranding it in an unrelated commit. An unexpected change in the tree is also the + signal to re-check isolation per the `repo-worktree` skill, since it may mean another task is + live in this checkout. ## Signing, verified not configured diff --git a/.agents/skills/operational-vs-release-workflow/SKILL.md b/.agents/skills/operational-vs-release-workflow/SKILL.md index 4a0cb078..ad30d26c 100644 --- a/.agents/skills/operational-vs-release-workflow/SKILL.md +++ b/.agents/skills/operational-vs-release-workflow/SKILL.md @@ -41,6 +41,7 @@ rather than guessing from the repo's contents. ## Branching (release model) +- **GitHub's repository setting for "default branch" reads `main`, but `develop` is where work starts and where in-flight content lives.** A worktree or clone that defaults to "the default branch" lands on `main` and can silently miss content that has merged to `develop` but not yet been promoted. Before branching off a change, or asserting something absent from this repo, check `develop`, not just whichever branch a tool defaulted to. See GOVERNANCE.md "Verification Discipline" on naming the branch a "does not exist" claim was checked against, and the `repo-worktree` skill, which owns the worktree-creation moment this base-branch choice is made at. - `develop` is the integration branch. Feature branches -> `develop` is **squash-only**, which keeps `develop` linear. - `develop -> main` is **merge-commit only** (no squash, no rebase). Merge commits preserve diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md new file mode 100644 index 00000000..93ed6901 --- /dev/null +++ b/.agents/skills/repo-worktree/SKILL.md @@ -0,0 +1,109 @@ +--- +name: repo-worktree +description: >- + Mandates and mechanizes task isolation in ptr727/ProjectTemplate fleet repos: every task, + including a continuation of a prior session's task, creates its own git worktree on its own + feature branch before its first file edit, based on the branch work starts on (develop on both + fleet workflow models unless the task is explicitly about main-only content, never whichever + branch a tool defaulted to). Also wraps the mechanics: + creating a worktree with git worktree add, the fleet layout convention, listing what is in + flight, and removing a worktree and its branch after merge. Use this whenever about to create + or edit files in a fleet repo, whenever starting or resuming a task, whenever the task's + branch is already checked out in a shared checkout, and whenever creating, listing, or + removing a worktree. Triggers even when the session was launched in the primary checkout or + the change looks like a one-line fix, because the primary checkout is the maintainer's own + surface and the incident this guards against was two sessions sharing one checkout, each + session's blanket add committing the other's uncommitted files. +--- + +# Repo Worktree + +## Why This Exists + +Two agent sessions once ran concurrently in the same primary checkout, on the same feature +branch, neither knowing the other was in the tree. One session's commits swept in the other +session's uncommitted files, so two commits landed carrying work their subjects never mention, +committed by a task that never saw it. No rule fired at the moment it was violated, which is the +first file edit: the commit-time and review-time skills all run after a sweep has already +happened. This skill is that missing task-start surface. `GOVERNANCE.md` "Repository Boundaries +and Write Safety" keeps the isolation law and wins on any disagreement, and the mechanics below +are this skill's own content. + +## The Mandate + +- **Every task isolates into its own worktree before its first file edit.** All new work begins + by creating a unique worktree (or clone) on its own feature branch. The primary checkout is + the maintainer's own surface, so a session launched there isolates before writing rather than + after noticing contention. +- **A continuation re-isolates.** A session resuming prior work finds its branch already checked + out somewhere and naturally resumes there, and that instinct is the hazard: a branch sitting + checked out in a shared tree is exactly how two sessions end up in one checkout. Create a + fresh worktree for the continuation and check the branch out there. +- **The moment is the first file edit, not the commit.** By commit time another task's + uncommitted work can already be swept into the staging area, so isolating late protects + nothing. Reading anywhere is fine, and the worktree exists before the first write. +- **Someone else's tree stays theirs.** A branch that changes when nothing you did changed it, + or an edit of yours reverted with no conflict, means another task is live in that tree, and + the response is to stop rather than to re-apply the edit, per `GOVERNANCE.md` "Repository + Boundaries and Write Safety". + +## The Base Branch + +Base the worktree on the branch work starts on for the repository's model, not on whichever +branch a tool defaulted to. GitHub's own "default branch" setting reads `main`, but on both +fleet workflow models work starts on `develop`, so a worktree defaulted to "the default branch" +lands on `main` and silently misses everything merged to `develop` but not yet promoted. Branch +from `develop` unless the task is explicitly about `main`-only content, per `GOVERNANCE.md` +"Branching Model". Fetch immediately before creating and base on the remote ref, because a clone +is whatever it last fetched rather than the branch it names. + +## Creating a Worktree + +The fleet layout convention keeps every base clone and every in-flight task visible in one +place, with no owner segment since every repo here is under one owner: + +```text +~/repos/ base clone, on its default/working branch +~/repos/worktrees/- one worktree per in-flight task, own branch +``` + +```sh +git -C ~/repos/ fetch origin develop +git -C ~/repos/ worktree add ~/repos/worktrees/- -b origin/develop +``` + +A continuation attaches the task's existing branch rather than forking a fresh one: + +```sh +git -C ~/repos/ fetch origin +git -C ~/repos/ worktree add ~/repos/worktrees/- +``` + +When the base clone holds only the remote-tracking ref, the same command creates the local +branch tracking `origin/` through git's ordinary checkout guessing, so a fresh +clone needs no separate branch setup. Git refuses to attach a branch that is already checked +out somewhere else, and that refusal is the mandate working, since the branch sitting checked +out in a shared tree is the hazard the continuation rule exists for. Return that checkout to +its own working branch first when its tree is clean, and stop when it is not, because a dirty +tree there may be another task's uncommitted work. + +A machine not yet migrated to this layout still isolates exactly the same way, since the mandate +is the isolation rather than the path: create the worktree beside whatever layout the machine +has, and note that the base clone may live elsewhere than `~/repos/`. + +Claude Code's own `EnterWorktree` tool acts only on an explicit instruction from the user or the +project instructions, which is why the carried rules state this mandate in so many words. Given +a `name`, it creates the worktree under `.claude/worktrees/` inside the repo and bases it on the +GitHub default branch, which is the wrong path and the wrong base here. Create the worktree with +`git worktree add` as above, then attach with `EnterWorktree` `path:`, not `name:`. + +## Listing and Cleanup + +- `git worktree list`, run in any checkout of a repo, names that repo's base clone and every + worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is + in flight across the whole fleet. +- After the task's pull request merges, remove the worktree and its branch from the base clone: + `git worktree remove ~/repos/worktrees/-`, then `git branch -d `. +- A worktree that refuses removal is dirty, and force is not the fix: look at what is + uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, + per the `git-commit-conventions` skill. diff --git a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json index aac58e07..00016e2f 100644 --- a/.claude-plugin/fleet-skills/.claude-plugin/plugin.json +++ b/.claude-plugin/fleet-skills/.claude-plugin/plugin.json @@ -17,6 +17,7 @@ "./skills/operational-vs-release-workflow", "./skills/pr-review-conduct", "./skills/python-codestyle", + "./skills/repo-worktree", "./skills/resync-a-repo", "./skills/shell-codestyle", "./skills/skill-lifecycle", diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index ca25fa31..f0ee8330 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -787c26fa10640eb0 +e4a4146ddbea9745 diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index 00935579..a1088902 100644 --- a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md @@ -19,6 +19,7 @@ Read `GOVERNANCE.md` "Verification Discipline" before reporting success on anyth - **Run the repo's whole lint gate before every push**, not the parts that look relevant, because the tool most likely to catch a change is often the one it seems least about. - **A launched process is not a result.** Report the output the wait produced, and where it produced none, that absence is the report. Never name an external cause the record does not carry. - **A local clone is not the branch it names.** Fetch immediately before reading, or read the live ref, and name the ref and commit in any finding a local read produced. +- **A "does not exist" claim names the branch it was checked against.** A worktree's default branch is not necessarily the one the content lives on: in-flight content on a `release`-model repo lands on `develop` before `main`, per `GOVERNANCE.md` "Branching Model," so check that branch before reporting anything absent repo-wide. - **A test asserts the mechanism it names, and a gate has to be watched failing.** A case that passes for an incidental reason is worse than no case, because it is later cited as evidence. - **Platform-specific code is verified only on the platform it runs on.** Reasoning about PowerShell, macOS, or WSL-specific behavior from a different host is not verification, however closely it matches an already-tested equivalent elsewhere. State an untested structural match as exactly that, never in the words used for a tested fact, and when no agent in the loop has access to the target platform, say so and defer or ship it labeled unverified. diff --git a/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md b/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md index 7ff9e43b..9790f987 100644 --- a/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md @@ -32,6 +32,12 @@ scope-widened commit, a rewritten shared history, a destructive reset). developer unless the developer has explicitly authorized committing for the current ask ("commit this", "open a PR"). Authorization is scope-bound: it covers the commits that specific task needs, not a blanket license for the rest of the session. +- **Stage by explicit path, never `git add -A` or `git add .`.** A blanket add stages whatever + else happens to be in the tree, and what it sweeps in is another task's uncommitted work, + landing in a commit whose subject never mentions it, committed by a session that never saw it. + That sweep has happened, which is why task isolation exists (the `repo-worktree` skill), and + isolation makes a shared tree rare rather than impossible. Name the files this task changed, + and let anything else stay unstaged. - **"Commit" means commit and push.** An authorization to commit carries the push to the feature branch the work belongs on, because nothing reviews a local commit. The Copilot review loop, the required status checks, and the maintainer all read the remote, so stopping at `git commit` @@ -39,10 +45,14 @@ scope-widened commit, a rewritten shared history, a destructive reset). progress while none of the gates have run. Push to the feature branch, never to a protected branch, and never with `--force`. Holding a commit locally is the narrower case: it happens when the developer asks for it, not by default. -- **Check `git status` for the maintainer's own uncommitted edits before committing.** The - maintainer hand-edits files live, often `README.md`/`HISTORY.md`, sometimes with an editor's - LF -> CRLF flip on top. If there are changes not made this session, ask whether to include them - rather than bundling half-finished work or stranding it in an unrelated commit. +- **Check `git status` before committing, and treat any change this session did not make as a + stop.** The maintainer hand-edits files live, often `README.md`/`HISTORY.md`, sometimes with an + editor's LF -> CRLF flip on top, and a sibling agent session sharing the tree leaves its edits + the same way. Whoever the author is, a change this session did not make is never bundled: ask + whether to include it, or leave it unstaged and say so, rather than committing half-finished + work or stranding it in an unrelated commit. An unexpected change in the tree is also the + signal to re-check isolation per the `repo-worktree` skill, since it may mean another task is + live in this checkout. ## Signing, verified not configured diff --git a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/SKILL.md b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/SKILL.md index 4a0cb078..ad30d26c 100644 --- a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/SKILL.md @@ -41,6 +41,7 @@ rather than guessing from the repo's contents. ## Branching (release model) +- **GitHub's repository setting for "default branch" reads `main`, but `develop` is where work starts and where in-flight content lives.** A worktree or clone that defaults to "the default branch" lands on `main` and can silently miss content that has merged to `develop` but not yet been promoted. Before branching off a change, or asserting something absent from this repo, check `develop`, not just whichever branch a tool defaulted to. See GOVERNANCE.md "Verification Discipline" on naming the branch a "does not exist" claim was checked against, and the `repo-worktree` skill, which owns the worktree-creation moment this base-branch choice is made at. - `develop` is the integration branch. Feature branches -> `develop` is **squash-only**, which keeps `develop` linear. - `develop -> main` is **merge-commit only** (no squash, no rebase). Merge commits preserve diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md new file mode 100644 index 00000000..93ed6901 --- /dev/null +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -0,0 +1,109 @@ +--- +name: repo-worktree +description: >- + Mandates and mechanizes task isolation in ptr727/ProjectTemplate fleet repos: every task, + including a continuation of a prior session's task, creates its own git worktree on its own + feature branch before its first file edit, based on the branch work starts on (develop on both + fleet workflow models unless the task is explicitly about main-only content, never whichever + branch a tool defaulted to). Also wraps the mechanics: + creating a worktree with git worktree add, the fleet layout convention, listing what is in + flight, and removing a worktree and its branch after merge. Use this whenever about to create + or edit files in a fleet repo, whenever starting or resuming a task, whenever the task's + branch is already checked out in a shared checkout, and whenever creating, listing, or + removing a worktree. Triggers even when the session was launched in the primary checkout or + the change looks like a one-line fix, because the primary checkout is the maintainer's own + surface and the incident this guards against was two sessions sharing one checkout, each + session's blanket add committing the other's uncommitted files. +--- + +# Repo Worktree + +## Why This Exists + +Two agent sessions once ran concurrently in the same primary checkout, on the same feature +branch, neither knowing the other was in the tree. One session's commits swept in the other +session's uncommitted files, so two commits landed carrying work their subjects never mention, +committed by a task that never saw it. No rule fired at the moment it was violated, which is the +first file edit: the commit-time and review-time skills all run after a sweep has already +happened. This skill is that missing task-start surface. `GOVERNANCE.md` "Repository Boundaries +and Write Safety" keeps the isolation law and wins on any disagreement, and the mechanics below +are this skill's own content. + +## The Mandate + +- **Every task isolates into its own worktree before its first file edit.** All new work begins + by creating a unique worktree (or clone) on its own feature branch. The primary checkout is + the maintainer's own surface, so a session launched there isolates before writing rather than + after noticing contention. +- **A continuation re-isolates.** A session resuming prior work finds its branch already checked + out somewhere and naturally resumes there, and that instinct is the hazard: a branch sitting + checked out in a shared tree is exactly how two sessions end up in one checkout. Create a + fresh worktree for the continuation and check the branch out there. +- **The moment is the first file edit, not the commit.** By commit time another task's + uncommitted work can already be swept into the staging area, so isolating late protects + nothing. Reading anywhere is fine, and the worktree exists before the first write. +- **Someone else's tree stays theirs.** A branch that changes when nothing you did changed it, + or an edit of yours reverted with no conflict, means another task is live in that tree, and + the response is to stop rather than to re-apply the edit, per `GOVERNANCE.md` "Repository + Boundaries and Write Safety". + +## The Base Branch + +Base the worktree on the branch work starts on for the repository's model, not on whichever +branch a tool defaulted to. GitHub's own "default branch" setting reads `main`, but on both +fleet workflow models work starts on `develop`, so a worktree defaulted to "the default branch" +lands on `main` and silently misses everything merged to `develop` but not yet promoted. Branch +from `develop` unless the task is explicitly about `main`-only content, per `GOVERNANCE.md` +"Branching Model". Fetch immediately before creating and base on the remote ref, because a clone +is whatever it last fetched rather than the branch it names. + +## Creating a Worktree + +The fleet layout convention keeps every base clone and every in-flight task visible in one +place, with no owner segment since every repo here is under one owner: + +```text +~/repos/ base clone, on its default/working branch +~/repos/worktrees/- one worktree per in-flight task, own branch +``` + +```sh +git -C ~/repos/ fetch origin develop +git -C ~/repos/ worktree add ~/repos/worktrees/- -b origin/develop +``` + +A continuation attaches the task's existing branch rather than forking a fresh one: + +```sh +git -C ~/repos/ fetch origin +git -C ~/repos/ worktree add ~/repos/worktrees/- +``` + +When the base clone holds only the remote-tracking ref, the same command creates the local +branch tracking `origin/` through git's ordinary checkout guessing, so a fresh +clone needs no separate branch setup. Git refuses to attach a branch that is already checked +out somewhere else, and that refusal is the mandate working, since the branch sitting checked +out in a shared tree is the hazard the continuation rule exists for. Return that checkout to +its own working branch first when its tree is clean, and stop when it is not, because a dirty +tree there may be another task's uncommitted work. + +A machine not yet migrated to this layout still isolates exactly the same way, since the mandate +is the isolation rather than the path: create the worktree beside whatever layout the machine +has, and note that the base clone may live elsewhere than `~/repos/`. + +Claude Code's own `EnterWorktree` tool acts only on an explicit instruction from the user or the +project instructions, which is why the carried rules state this mandate in so many words. Given +a `name`, it creates the worktree under `.claude/worktrees/` inside the repo and bases it on the +GitHub default branch, which is the wrong path and the wrong base here. Create the worktree with +`git worktree add` as above, then attach with `EnterWorktree` `path:`, not `name:`. + +## Listing and Cleanup + +- `git worktree list`, run in any checkout of a repo, names that repo's base clone and every + worktree with its branch. On the convention layout, one `ls ~/repos/worktrees/` reads what is + in flight across the whole fleet. +- After the task's pull request merges, remove the worktree and its branch from the base clone: + `git worktree remove ~/repos/worktrees/-`, then `git branch -d `. +- A worktree that refuses removal is dirty, and force is not the fix: look at what is + uncommitted in it first, since discarding uncommitted work runs only on explicit instruction, + per the `git-commit-conventions` skill. diff --git a/AGENTS.md b/AGENTS.md index b1f25247..fff1fd30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R | --- | --- | | Why the rules are shaped this way | `Foundational Principles` | | Recording a durable lesson or updating governance | `Durable Knowledge and Self-Improvement`, surfaced at its decision moment by the `agent-conduct` Skill, and the section keeps the full rules | -| Any push, API mutation, comment, label, or merge, or which checkout the work happens in | `Repository Boundaries and Write Safety` | +| Any push, API mutation, comment, label, or merge, or which checkout the work happens in | `Repository Boundaries and Write Safety`, its task-isolation rule surfaced at the task-start moment by the `repo-worktree` Skill, and the section keeps the full rules | | Quoting data into a comment, commit, test, or doc | `Representative Data in Agent-Authored Text` | | Committing, signing, rebasing, force-pushing | `Git and Commit Rules`, packaged as the `git-commit-conventions` Skill | | Branch choice, promotion, keeping branches in sync | `Branching Model`, packaged as the `operational-vs-release-workflow` Skill | @@ -97,6 +97,6 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R | The About panel, description, or repo toggles | `Repository Details` | | Where a file belongs in the tree | `Repository Layout` | -A row above with no Skill annotation is doc-only by decision, not by omission. A Skill surfaces rules at a trigger moment, and each unannotated section either binds always or carries no moment narrower than reading it: `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 must bind even when no Skill fires (the `gh-write-guard` hook and the host-wide instruction blocks the agent-safety installer maintains are their enforcement layer), 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 with no Skill annotation is doc-only by decision, not by omission. A Skill surfaces rules at a trigger moment, and each unannotated section either binds always or carries no moment narrower than reading it: `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 must bind even when no Skill fires (the `gh-write-guard` hook and the host-wide instruction blocks the agent-safety installer maintains are their enforcement layer, and the one moment in the boundaries section narrow enough to surface, isolating into a worktree at task start, gets the `repo-worktree` Skill on top of that law rather than instead of it), 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 `fleet-conformance-check`, 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. 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. +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 `fleet-conformance-check`, 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. 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/GOVERNANCE.md b/GOVERNANCE.md index d611f11d..5473a184 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -29,6 +29,7 @@ A state-changing GitHub call is the highest-blast-radius thing an agent does her - **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. - **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, and a branch switch carries it into an unrelated change. 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. +- **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. - **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, 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 @@ -41,10 +42,7 @@ Agent-authored text illustrates with data the agent constructed, never with data ## Git and Commit Rules -The fleet's mechanical git rules: default to staging rather than committing, commit means commit -and push, every commit is signed and carries the committer's own verified GitHub `noreply` -identity, never force push, a history rewrite re-identifies only the commits it touches that -aren't yours, and destructive git commands run only on explicit instruction. +The fleet's mechanical git rules: default to staging rather than committing, stage by explicit path only and never with a blanket add, commit means commit and push, every commit is signed and carries the committer's own verified GitHub `noreply` identity, never force push, a history rewrite re-identifies only the commits it touches that aren't yours, and destructive git commands run only on explicit instruction. This is packaged as the `git-commit-conventions` Skill at `.agents/skills/git-commit-conventions/SKILL.md` in the hub, not a repo-relative link since that @@ -57,6 +55,7 @@ Two workflow models, set per repo by the registry `workflowModel` field. Most re `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 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 release), covered as a delta rather than a separate model. @@ -166,6 +165,7 @@ The checks that separate work actually done from work that merely reports succes - **Never edit an active `.code-workspace` file.** A workspace file rewritten on disk can make VS Code reload the window, and a reload destroys the running agent session's context, so the work in flight is lost with nothing to catch it, and the trigger is not fully characterized (an agent's edit has caused the reload where a human's identical edit did not). Surface the needed change for the maintainer to apply by hand. - **A green check is not evidence the work happened.** A skipped job and a passing job are indistinguishable in the aggregated required check. When a job exists to exercise something, confirm from its log that it ran and produced the output it promises. - **A local clone is not the branch it names, it is whatever that clone last fetched.** Reading a checkout on disk answers what that clone last saw, so a finding taken from one carries a date nobody stated, and two failures of exactly that shape are on record from one session: a repository reported as still drifted on a file whose fix had already merged, and a repository reported as missing a file it carries because the checkout sat on an older branch. Read the live ref through the API where the claim will be acted on, or fetch immediately before reading, and name the ref and the commit in any finding a local read produced. A clone stays the right tool for anything needing history or a build, which an API read cannot give. +- **A "does not exist" claim names the branch it was checked against.** A worktree or checkout answers for whichever ref it was built from, and that ref is not necessarily the one the content lives on: a `release`-model repo carries in-flight content on `develop`, per "Branching Model" above, well before it reaches `main`, so a worktree defaulted to the fleet's default branch can hold nothing while the repository holds everything. Before reporting a file, a directory, or a piece of content as absent anywhere in a repo, check it against the branch the repo's own model designates as current for that kind of content, not only whichever branch a worktree or checkout happened to default to, and name the branch the negative claim was checked against in the finding itself. - **A launched process is not a result, and a cause nobody observed is not a diagnosis.** "The watcher is armed" names a process rather than a finding, so what gets reported is the output that process produced, and where it produced none, that absence is the report. The failure it prevents is an agent standing still on a condition that was met half an hour earlier, having announced the wait and never read it. Naming an external cause for such a stall afterwards, a throttle or a quota that appears nowhere in the record, turns a local defect into a story about someone else and closes the investigation on the wrong party, so read the record for the cause before naming one, and where the record does not carry it, report the cause as unknown. - **A workflow change is only fully exercised by CI.** Extracting a `run:` block and executing it locally validates the script and nothing else, because `secrets: inherit`, `permissions:`, `needs:` wiring, and reusable-workflow inputs resolve only in a real run. - **Platform-specific code is "verified" only on the platform it runs on.** PowerShell on Windows, a macOS-only `mktemp`/`ssh-agent` behavior, a WSL-specific path quirk: an agent reasoning about such code from a different host, however carefully, has not executed it, and reasoning by structural analogy to an already-tested equivalent on another platform ("the POSIX version works, so the PowerShell version should too") is a plausible first pass, not verification. State it as exactly that, an unverified structural match, and never in the same words used for a tested fact. When no agent in the loop has access to the target platform, say so, and either defer the platform-specific portion to a human or an agent that has that access, or ship it clearly labeled unverified. diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 40537407..002aa729 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -8,7 +8,7 @@ Into `~/.claude/` (or `%USERPROFILE%\.claude\` on Windows): - **`hooks/gh-write-guard.py`**: a PreToolUse hook that denies two classes of dangerous action. First, the GitHub **write** footguns behind the cross-repo comment incident: a state-changing `gh` call whose output is discarded, a GraphQL mutation passing a **literal** node id instead of a `$variable`, and a `gh` write whose explicit target is under an owner other than the checkout origin's. Sibling repositories under the same owner are allowed, since the harm this guards is reaching a stranger's repository rather than working across one maintainer's own fleet, and a different owner is allowed only when the maintainer names it in `GH_WRITE_GUARD_ALLOW` (an `owner/repo` list, where `owner/*` grants a whole owner). That variable is read from the environment the session was launched with, which is the one channel an agent cannot set for itself: a hook runs as its own process, so an inline `VAR=x cmd` prefix or an `export` inside a Bash call never reaches it. Second, a **git operation that bypasses a repository protection**. The branch-rule cases (a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked) are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no per-repo configuration. A push to a protected-default branch fails closed when its rules cannot be determined, either because the API is unreachable or because the checkout's origin cannot be resolved to query them. The explicit-bypass flags are denied **unconditionally**, since the flag is itself the bypass and needs no branch query: `gh pr merge --admin` overrides the server-side merge gate, and `git commit`/`git push --no-verify` skips the local git hooks. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. - **Two marker-delimited blocks of host-wide content in `CLAUDE.md`**, loaded into every session on the machine (including ad-hoc work outside any project), installed and updated independently so one can change without rewriting the other. - - The `agent-safety` block carries restrictions alone, so nothing in it can widen a permission. `Repository Boundaries and Write Safety` mirrors the committed `GOVERNANCE.md` section of the same name, carrying its write rules and its rule that a task works in its own checkout as behavioral guidance. `Data in Agent-Authored Text` mirrors `GOVERNANCE.md` "Representative Data in Agent-Authored Text", so an agent illustrates with constructed data rather than data observed on the machine. `Authorization Scope and Memory Hygiene` bounds how a granted permission is recorded and read. Each mirrors a rule that otherwise reaches fleet repos only. + - The `agent-safety` block carries restrictions alone, so nothing in it can widen a permission. `Repository Boundaries and Write Safety` mirrors the committed `GOVERNANCE.md` section of the same name, carrying as behavioral guidance its write rules and its rules that a task works in its own checkout and isolates into its own worktree before its first file edit. `Data in Agent-Authored Text` mirrors `GOVERNANCE.md` "Representative Data in Agent-Authored Text", so an agent illustrates with constructed data rather than data observed on the machine. `Authorization Scope and Memory Hygiene` bounds how a granted permission is recorded and read. Each mirrors a rule that otherwise reaches fleet repos only. - The `fleet-bootstrap` block carries `Fleet Governance Entry Point`, which names the template repository and routes by the state a repository is actually in. It is separate precisely because it enables rather than restricts, and the safety block's own text says nothing in it widens a permission, so merging the two would contradict that. It is host-wide rather than per repository because the repositories that most need it are the ones carrying no instruction set to point the way, and it mirrors the byte-locked `AGENTS.md` "Fleet Bootstrap" section that a conformant repository carries. - **The permission rules this kit owns, merged into `settings.json`** beside the hook registration. Each is declared as a prefix and a rule, and a re-run drops every rule the prefix owns before adding the current one, so a rule whose spelling changes updates in place rather than accumulating beside the version it replaced. Ownership requires a rule-syntax delimiter after the prefix, since the prefix ends at the script name and a bare prefix test would also claim a longer path such as `pr_review.py-custom`, so a rule written by hand for a different script is never touched. These widen rather than restrict, which is why they are their own component for the same reason the `fleet-bootstrap` block is separate from the `agent-safety` one. Today the list holds one rule, for `scripts/pr_review.py`, the review loop's reply and resolve. Driving that loop by hand needs a raw GraphQL mutation carrying a node id, which is the shape that reached a stranger's repository, where the script queries the id itself and takes no argument an id fits in. What the rule decides is which command runs without a prompt, and it matches the command text rather than the directory the command runs in, so it reaches a `scripts/pr_review.py` in any checkout that carries one. An absolute path would not narrow that, since the hub is reached as a checkout of the caller's own and its location differs per task, so pinning one path would name a checkout the next task does not use. What bounds it is the rule that an agent reaches the hub as a checkout of its own, fetched immediately before it is read, rather than a copy it happens to find on disk, which the `fleet-bootstrap` block beside this carries and [`GOVERNANCE.md`][governance] "Hub-Hosted Tooling" states in full. diff --git a/host-setup/agent-safety/claude-md-safety.md b/host-setup/agent-safety/claude-md-safety.md index 79ce9928..3a465767 100644 --- a/host-setup/agent-safety/claude-md-safety.md +++ b/host-setup/agent-safety/claude-md-safety.md @@ -7,6 +7,7 @@ A `gh` / GitHub API write runs under the logged-in identity, so a mis-targeted w - **Never fabricate, guess, or reuse an identifier passed to a write.** Every id a write consumes (a node id, a numeric id, a thread or comment id) is captured from a live query in the same session into a variable and passed from there. Ids resolve globally, so a wrong-but-valid id does not fail. It writes to the wrong target, in another repository. If a query returns no id, stop rather than invent one. - **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, and never append an output-discarding or force-success tail (for example `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `|| true`, `|| :`, `|| echo`) to a mutation. A write that appears to fail is verified, not assumed harmless, because it may have succeeded on the server. - **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. A blanket `git add -A`, a `git reset --hard`, and a branch switch each cross the boundary while being correct in isolation, so a mutating command takes an absolute path, or a `cd` to one in the same invocation, rather than the working directory it inherited. A branch that changes when nothing you did changed it, or an edit of yours reverted with no conflict, means another task is live in that tree, and the response is to stop rather than to re-apply the edit. +- **A task isolates into its own git worktree before its first file edit, and a continuation re-isolates.** All new work, a continuation of a prior session's task included, begins by creating a unique worktree (or clone) on its own feature branch, based on the branch work starts on for that repository rather than whichever branch a tool defaulted to. The primary checkout is the maintainer's own surface, so a session launched there isolates before writing rather than after noticing contention. Staging is by explicit path, never `git add -A` or `git add .`, since a blanket add is how a shared tree turns into a mis-scoped commit. A committed repo's `GOVERNANCE.md` "Repository Boundaries and Write Safety" and the hub's `repo-worktree` skill carry the full rule and the mechanics. ## Data in Agent-Authored Text (Any Project, Every Session)