diff --git a/.agents/skills/worktrees-pnpm/SKILL.md b/.agents/skills/worktrees-pnpm/SKILL.md index b571fce3..6a4e0308 100644 --- a/.agents/skills/worktrees-pnpm/SKILL.md +++ b/.agents/skills/worktrees-pnpm/SKILL.md @@ -15,7 +15,7 @@ problem in this repo. ```bash git fetch origin +refs/heads/main:refs/remotes/origin/main # explicit refspec; see the hook for why -git worktree add -b .agents/.worktrees/ origin/main # or an existing , without -b +git worktree add --no-track -b .agents/.worktrees/ origin/main # or an existing , without -b cd .agents/.worktrees/ git rev-list --count HEAD..origin/main # anything above 0: git rebase origin/main pnpm install # ← not optional, and after the rebase, which can change the lockfile @@ -149,8 +149,10 @@ main checkout are not enough on their own, so a `pre-commit` hook makes it a har `scripts/refuse-main-checkout.sh` exits non-zero when `git rev-parse --git-dir` equals `--git-common-dir`, which is true only in the primary checkout, never in a linked worktree. It checks the checkout, not the branch, so a feature branch checked out in the primary tree is -refused too. It skips when `CI` is set, because a CI runner commits from a plain clone (version -PRs, review bots), and a plain clone is a primary checkout. With Husky, run it first in +refused too. It skips when `GITHUB_ACTIONS=true`, because a runner commits from a plain clone +(version PRs, review bots), and a plain clone is a primary checkout. It keys on the variable the +runner sets itself rather than `CI`, which any shell can export as an unnamed bypass. A repo on +another CI system adds that system's own marker. With Husky, run it first in `.husky/pre-commit`: ```sh @@ -160,7 +162,8 @@ npx lint-staged Without Husky, call it from `.git/hooks/pre-commit`, or from whatever hook manager the repo uses. A person who really means to commit in the primary checkout runs `git commit --no-verify`. -An agent never does that to get past this guard. +An agent never gets past it, whether by passing `--no-verify`, setting `GITHUB_ACTIONS`, or any +other means; a refusal means it is in the wrong checkout, so it moves to a worktree. ## Every root-level tool must ignore the worktrees directory diff --git a/.agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh b/.agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh index f02a708f..442e231f 100755 --- a/.agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh +++ b/.agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh @@ -12,13 +12,16 @@ # directory. This does not care what branch is checked out, so it also stops a # commit to a feature branch that someone checked out in the primary tree. # -# Not enforced in CI: the runners commit from a plain clone (changeset version -# PRs, review actions), and a plain clone is a primary checkout. +# Not enforced on GitHub Actions: the runners commit from a plain clone +# (changeset version PRs, review actions), and a plain clone is a primary +# checkout. The skip keys on GITHUB_ACTIONS, which the runner sets itself, +# rather than CI, which any shell exports and which no instruction names, so it +# would be a quieter bypass than --no-verify. # -# Escape hatch for a deliberate commit here: git commit --no-verify +# Escape hatch for a person's deliberate commit here: git commit --no-verify set -eu -if [ -n "${CI:-}" ]; then +if [ "${GITHUB_ACTIONS:-}" = "true" ]; then exit 0 fi @@ -37,9 +40,12 @@ Refusing to commit in the primary checkout ($top). Work in a worktree instead: git fetch origin +refs/heads/main:refs/remotes/origin/main - git worktree add -b .agents/.worktrees/ origin/main + git worktree add --no-track -b .agents/.worktrees/ origin/main cd .agents/.worktrees/ && pnpm install +--no-track keeps the branch from tracking origin/main, so a bare git push +cannot target main. + To commit here deliberately: git commit --no-verify EOF exit 1 diff --git a/AGENTS.md b/AGENTS.md index f5d7b99d..f1a2aa91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,17 +106,17 @@ This is where taskless/cli differs from the other Taskless repos, which run the ```bash git fetch origin +refs/heads/main:refs/remotes/origin/main -git worktree add -b .agents/.worktrees/ origin/main +git worktree add --no-track -b .agents/.worktrees/ origin/main cd .agents/.worktrees/ && pnpm install ``` -The refspec is spelled out because a bare `git fetch origin main` only updates `origin/main` when `remote.origin.fetch` covers it, which a single-branch clone of another branch does not. Branch from `origin/main`, never the local `main`: the local branch is whatever the main checkout was last pulled to. +The refspec is spelled out because a bare `git fetch origin main` only updates `origin/main` when `remote.origin.fetch` covers it, which a single-branch clone of another branch does not. Branch from `origin/main`, never the local `main`: the local branch is whatever the main checkout was last pulled to. `--no-track` keeps the new branch from tracking `origin/main`, so a bare `git push` cannot target `main`. The three rules that cause the most damage when missed: - **A worktree starts from the latest `origin/main`, checked before any work begins.** Confirm `git rev-parse --is-shallow-repository` prints `false` (a shallow clone cannot answer the next question; see the shallow-clone entry above), run `git fetch origin +refs/heads/main:refs/remotes/origin/main` (the explicit refspec, for the reason above), then `git rev-list --count HEAD..origin/main`. Anything above `0` means the worktree is behind: rebase onto `origin/main` (`git rebase origin/main`) before reading code, making changes, or running `pnpm install` (a rebase can change the lockfile), unless the work was explicitly asked to start from another branch or commit. The `WorktreeCreate` hook already branches from a freshly fetched `origin/main`, but a worktree made any other way (by hand, by another tool, or by a session board) is cut from whatever the main checkout was last pulled to. One started 68 commits behind `main`, and nothing flagged it: stale code reads exactly like current code. Another started 156 behind and never saw this rule at all, because the rule landed after its base: **a stale tree carries stale conventions**, so nothing in the repo can warn a checkout that is already behind. That is why the fix belongs in whatever creates the worktree (the hook, a session board, your own hands), and why this check comes before reading anything else here. - **A worktree gets its own empty `node_modules`.** `git worktree add` is not finished until `pnpm install` has run inside it. Without it, `.husky/_` does not exist in the worktree (the install generates it; it is not tracked), and since `core.hooksPath` points there, `git commit` runs no hooks at all: `lint-staged` and the primary-checkout guard are both skipped, silently. Every `pnpm` script fails too. A missing `prettier` here once cost an agent an hour of dead-end workarounds. There is no `pnpm worktree` command; `git worktree` is the tool. -- **NEVER point an agent at the main repo path** (e.g. `/Users//code/taskless/skills`). It will `cd` there and run git commands and edits in the **main** checkout, defeating isolation. It can create and check out a branch in your working tree, silently switching your session off its own branch. Tell the agent to work in **its assigned worktree** (`$PWD`) and pass only relative paths plus GitHub identifiers (`owner/repo`). The `pre-commit` hook enforces the commit half of this: it refuses any commit made in the primary checkout (outside CI), whatever branch is checked out there. `git commit --no-verify` is the deliberate override; an agent never uses it to get past this guard. +- **NEVER point an agent at the main repo path** (e.g. `/Users//code/taskless/skills`). It will `cd` there and run git commands and edits in the **main** checkout, defeating isolation. It can create and check out a branch in your working tree, silently switching your session off its own branch. Tell the agent to work in **its assigned worktree** (`$PWD`) and pass only relative paths plus GitHub identifiers (`owner/repo`). The `pre-commit` hook enforces the commit half of this: it refuses any commit made in the primary checkout (except on GitHub Actions runners), whatever branch is checked out there. An agent never gets past it, whether by passing `--no-verify`, setting `GITHUB_ACTIONS`, or any other means; a refusal means you are in the wrong checkout, so move to a worktree. `--no-verify` is for a person's deliberate commit only. **Do not offer to fast-forward or pull the local `main`.** Nothing here reads it: worktrees branch from a freshly fetched `origin/main`, and every freshness check compares against `origin/main`. So "want me to fast-forward `main`?" at the end of a task asks for a step that changes nothing. The one time it is worth mentioning is right after this session's own work has been rebased and merged to `main`, and even then mention it once, as optional, not as a closing step.