mirror of
https://github.com/daymade/claude-code-skills.git
synced 2026-09-14 16:15:21 +08:00
fix(git-safety-net): enforce single-writer SHA handoff (#437)
This commit is contained in:
@@ -491,7 +491,7 @@
|
||||
"description": "Audits, preserves, recovers, and safely retires local Git state: unpushed or wrong-branch commits, dirty or detached worktrees, forgotten duplicate clones of the same repo, untracked work no bundle can back up, orphaned stashes, dangling commits, stale branches, and squash/rebase merge uncertainty. Use when the user fears work was lost; asks to recover a commit or branch; asks whether a worktree, clone, or scratch directory can be deleted; wants everything converged onto one main branch; or needs proof that cleanup will not drop work. Use it even after an audit reported clean — the usual gap is scope: every in-repo command is blind to a second clone elsewhere on disk. Triggers on \"did I lose work\", \"is everything merged\", \"is anything else lost\", \"safe to delete this clone\", \"clean up old branches/stashes\", \"only keep one main branch\", \"git reflog\", \"dangling commits\", \"分支灾难\", \"误删分支/commit\", \"worktree 能删吗\", \"还有没有丢的东西\", \"只保留一个主分支\". Covers local-Git forensics, not GitHub PR/API operations or routine sync.",
|
||||
"source": "./git-safety-net",
|
||||
"strict": false,
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"category": "developer-tools",
|
||||
"keywords": [
|
||||
"git",
|
||||
|
||||
@@ -48,6 +48,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
an order-inverted stitched quote — both fixed and re-verified pre-ship.
|
||||
|
||||
### Fixed
|
||||
- **git-safety-net** v1.13.0 → v1.14.0: replace the over-broad “one worktree per
|
||||
concurrent session” prescription with an authority-first, single-writer shared-checkout
|
||||
contract. Mode D now treats worktrees as explicitly authorized named exceptions, preserves
|
||||
narrow higher-authority stash contracts instead of declaring a universal ban, stops every
|
||||
repository mutation while another writer is active, freezes handoffs and merges by exact
|
||||
local/remote SHA, makes completion an AND gate over session-owned bytes,
|
||||
remote containment, and attributed residuals, and keeps cleanup in separately authorized
|
||||
Mode E. The prevention reference now counts any scheduled job that can write the checkout as a
|
||||
writer even when paths are disjoint: an idle process snapshot is not a lock, so existing
|
||||
coordination must quiesce it and transfer exclusive ownership before Git mutation; stopping,
|
||||
leasing, or rescheduling automation remains a separate design decision. The reachable Mode E
|
||||
convergence workflow now applies the same gate to alternate-index commits, ref/bundle writes,
|
||||
push/PR, and fetch; parallel verification agents receive frozen SHAs and never move
|
||||
remote-tracking refs. Bundle-relative helpers are no longer presented as commands assumed to
|
||||
exist on `PATH`.
|
||||
- **peer-message** v1.1.0 → v1.1.1: stop Claude receiver-evidence verification from collapsing unreadable transcripts or malformed matching JSONL into ordinary `unverified`. The verifier now keeps scanning other candidates and later lines for valid enqueue evidence, but fails loudly if no valid match exists and any evidence read/parse error occurred; a clean, fully readable miss remains `unverified`. Four deterministic regression cases cover read failure, malformed matching JSON, later valid evidence, and the healthy-miss control.
|
||||
- **peer-message** v1.0.0 → v1.0.1: make documentation ownership executable instead of duplicative. `SKILL.md` keeps only routing, stable prerequisites, safety, and owner pointers; `peer.py --help` owns CLI syntax; the protocol reference owns addressing, envelopes, receipt, exit, transport, and verification semantics; the official-feature reference owns volatile product interfaces and inbound mechanics. README/README.zh-CN and `CLAUDE.md` point to those owners, while the changelog and private review stop persisting derived test, session, reachability, and file totals.
|
||||
- **prior-work-retrieval / claude-switch-models-setup** (daymade-claude-code v3.7.9 → v3.7.13): remove `uv run` from the synchronous Claude/Codex prior-work hook entrypoint and require the profile-convergence SessionStart hook to use an absolute direct-Python command. The prior-work wrapper now also fails closed when that runtime is missing or a relative override is supplied, rather than falling back to PATH `python3`. Repository and Skill contracts distinguish package-manager-free hook launch from explicit `uv` retrieval, validation, and test lifecycles. Shared UV cache cleanup or lock contention can no longer stall every PreToolUse decision or prevent profile repair.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Security scan passed
|
||||
Scanned at: 2026-08-30T16:16:34.478783+00:00
|
||||
Scanned at: 2026-09-01T17:08:37.265263+00:00
|
||||
Tool: gitleaks + pattern-based validation
|
||||
Content hash: 2ea4e41633c9c742214641d281c779b59c8f6a51433de58845394b96d073df49
|
||||
Content hash: 6f1f1aa202f59021bfa7cf1415aab6b57dcff8434b393e6415da9c8bee52e93a
|
||||
|
||||
+77
-55
@@ -264,15 +264,19 @@ scripts/git_verify_branch_merged.sh <branch> [<base>] # base defaults to origi
|
||||
This mode is the one direction where a stale base is *unsafe* (rule 1): judged against yesterday's
|
||||
`origin/main`, a branch whose content landed hours ago still reads UNMERGED, and "rescuing" it
|
||||
re-applies an older version over whatever was built on top. The script fetches first for exactly
|
||||
that reason — but if the fetch fails it falls back to cached refs and says so **on stderr only**.
|
||||
that reason. Because fetch moves remote-tracking refs, run it only after existing coordination has
|
||||
quiesced every checkout writer and transferred exclusive ownership. If that cannot happen, stay
|
||||
read-only and report that the merge verdict is unavailable. If the fetch itself fails after
|
||||
ownership transfer, the script falls back to cached refs and says so **on stderr only**.
|
||||
Treat that line as a blocker, not a footnote: rerun once the network is back before acting on the
|
||||
verdict. Comparing by hand (`git diff origin/main <branch>`, `git log origin/main..<branch>`) has
|
||||
no such safety net at all — fetch yourself first, every time.
|
||||
no such safety net at all — the sole writer must refresh authority first.
|
||||
|
||||
It reports **MERGED (ancestor)** or **MERGED (content contained)** — safe to delete — versus
|
||||
It reports **MERGED (ancestor)** or **MERGED (content contained)** — content-safe for a separately
|
||||
authorized Mode E deletion gate — versus
|
||||
**UNMERGED / NEEDS REVIEW**, listing the files the branch would still change. The verdict is sound,
|
||||
not heuristic: it does a trial 3-way merge of the branch *into* the base with `git merge-tree`
|
||||
(in memory, no checkout) and only says "safe to delete" when that merge changes nothing — so a
|
||||
(in memory, no checkout) and only reports content containment when that merge changes nothing — so a
|
||||
squash-merged branch reads MERGED despite a nonzero commit count, while a revert/edit/new-file the
|
||||
base lacks reads UNMERGED. It is **safety-biased**: anything it can't prove contained is reported
|
||||
for review, because a false "merged" loses work while a false "unmerged" only costs a look. Full
|
||||
@@ -286,52 +290,61 @@ re-checked): **[references/merge_verification.md](references/merge_verification.
|
||||
The habits that keep a branch tangle from ever stranding work:
|
||||
**[references/prevention_practices.md](references/prevention_practices.md)**. The load-bearing few:
|
||||
|
||||
- **Commit before you switch — neither `git stash` nor `git worktree`.** Uncommitted work is what
|
||||
gets stranded: a `git stash` you later can't find, or edits a `switch` buries. Commit each line
|
||||
of work to its own branch and push it early (a committed, pushed branch can't be orphaned), then
|
||||
bring it where you need it *live* by merging — not by stashing, and not by spinning up a second
|
||||
`git worktree` checkout (which is one more place to forget work and won't even have your
|
||||
gitignored deps). A shared working tree with commit-then-switch discipline is the safe default.
|
||||
- **If you truly need a second checkout, make it a worktree — never a second `git clone`.** Both
|
||||
are extra places to forget work, which is why commit-then-switch above is still the default. But
|
||||
the failure modes are not equal: a linked worktree announces itself in `git worktree list`, so
|
||||
every audit finds it, while an independent clone is invisible to every command run from the
|
||||
original repository. Choosing `clone` for a few days of parallel work quietly opts out of all
|
||||
the safety tooling. When a clone already exists (a colleague made it, a script made it, you
|
||||
inherited it), register it somewhere the team actually reads and retire it the day it's done —
|
||||
and until then, treat it as an audit target in its own right, not as a scratch directory.
|
||||
- **Push a work-in-progress branch to a remote early.** The one commit only on a local branch is
|
||||
the only commit that a dead laptop actually loses.
|
||||
- **Read the current collaboration contract before prescribing topology.** An explicit user or
|
||||
project decision about shared checkouts, worktrees, branches, or contribution flow outranks this
|
||||
generic guidance. Do not turn one messy audit into a permanent "one worktree per session" rule.
|
||||
- **One physical checkout gets one writer; parallel agents and sessions stay read-only.** A topic
|
||||
branch inside the same checkout does not isolate the shared working files, current branch, or
|
||||
index. Writer ownership comes from the repository's task/coordination contract, not a guessed
|
||||
file list. If ownership is unclear or another writer is active, do not mutate the checkout.
|
||||
- **Commit before switching and push WIP early.** Prefer a remote-backed commit over stash
|
||||
juggling, but preserve a higher-authority narrow stash exception; never use an unscoped stash
|
||||
to make a dirty checkout look ready.
|
||||
- **Worktrees are explicitly authorized, named exceptions — not the standing default.** They
|
||||
isolate working files, `HEAD`, and index but still share refs, stashes, object storage, config,
|
||||
and hooks, and do not copy ignored dependencies. When approved, a linked worktree is safer than
|
||||
an invisible independent clone but remains a separately audited retirement target.
|
||||
- **Handoff and merge by exact commit, then finish with an AND gate.** Record branch, local `HEAD`,
|
||||
and fresh remote tip; require them to equal the handoff SHA. Direct merges name that SHA, not the
|
||||
branch. Hosted merges use an expected-head-SHA precondition when available, or an immediately
|
||||
preceding hosted head readback that must still equal the handoff SHA. Every session-owned byte
|
||||
must be in that remote-backed commit, and every residual path must be enumerated and attributed.
|
||||
- **A process snapshot is not a lock, and a merge is not cleanup authority.** Any scheduler that
|
||||
can write this checkout counts as a writer even when its paths are disjoint. Before Git mutation,
|
||||
the project's existing coordination must prove it quiescent and transfer exclusive ownership;
|
||||
without that mechanism, stay read-only and report the gap. Do not stop, reconfigure, or invent a
|
||||
lease for automation under this generic Skill. Retire refs or checkouts only through separately
|
||||
authorized Mode E evidence.
|
||||
- **Confirm the current branch before committing** (`git branch --show-current`) — a fix committed
|
||||
onto the wrong feature branch is invisible to its real PR and easy to lose on cleanup.
|
||||
- **In a shared tree, never aim a destructive command at "the current branch" — name the branch
|
||||
explicitly.** `reset --hard`, `merge`, and `rebase` all act on *whatever is checked out at the
|
||||
instant they run*, so a branch check is stale the moment it returns: a parallel session can
|
||||
`switch` in between, and your command lands on **their** branch. This is the inverse of the
|
||||
bullet below (that one protects *your* work from *their* switch; this one protects *theirs*
|
||||
from *your* command), and re-checking harder does not fix it — the race is inherent. Use the
|
||||
checkout-independent forms instead, which name their target and never touch the working tree:
|
||||
- **Never race another writer with checkout-relative mutation.** If another writer is active, stop
|
||||
until the repository's coordination system transfers exclusive write ownership. After transfer,
|
||||
name the exact ref and object when repairing or advancing state; do not rely on whichever branch
|
||||
happens to be checked out. `reset --hard`, `merge`, and `rebase` all act on *whatever is checked
|
||||
out at the instant they run*. Use checkout-independent forms for ref repair when they match the
|
||||
authorized outcome:
|
||||
```bash
|
||||
git branch -f <branch> <target> # instead of: switch <branch> && reset --hard <target>
|
||||
git fetch origin <branch>:<branch> # fast-forward a branch you are not on
|
||||
git push origin <sha>:refs/heads/<branch>
|
||||
```
|
||||
Real incident: a `reset --hard origin/main` issued seconds after `git branch --show-current`
|
||||
said `main` landed on a parallel session's feature branch and moved it back two commits; the
|
||||
follow-up "repair" then missed *again* because the tree had been switched a second time.
|
||||
`git branch -f` fixed both in one shot precisely because it never consults the checkout.
|
||||
- **If a parallel session switched the shared tree onto its branch** and stranded your uncommitted
|
||||
work there, don't commit onto their branch — carry your edits to a branch off the base
|
||||
(`git checkout origin/main -b …`, after `git diff --quiet` proves your files match across bases),
|
||||
commit only your explicit paths, then switch the tree back to their branch to restore their state.
|
||||
- **If a parallel session is *actively* writing the shared tree** — files keep appearing while you
|
||||
work — don't `switch`, `add`, or `reset` at all: each would either strand their uncommitted work
|
||||
or trip a worktree guard. When your own change is self-contained (new files, or edits that belong
|
||||
on `origin/main` rather than on their in-progress tree), build the commit with plumbing that never
|
||||
touches the working tree, then push it to a branch and open a PR. Freeze every candidate as the
|
||||
exact Git entry tuple `(mode, object ID, path)` — bytes alone are insufficient because `100755`,
|
||||
`120000`, and `160000` carry executable, symlink, and gitlink behavior. The safest source is an
|
||||
immutable candidate commit:
|
||||
Real incident: a `reset --hard origin/main` issued while another session still owned the checkout
|
||||
landed on that session's feature branch and moved it back two commits. The correct first action is
|
||||
to stop and transfer ownership; once transferred, an explicitly targeted ref repair avoids making
|
||||
checkout position part of the operation.
|
||||
- **If a parallel session previously switched the shared tree and stranded your uncommitted work,**
|
||||
do not mutate it until that session is quiescent and exclusive ownership has transferred. Then
|
||||
follow the incident-only relocation procedure in the prevention reference: prove your files match
|
||||
across bases, commit only explicit paths, and restore the prior branch before handing ownership
|
||||
back. Branch deletion remains a separately authorized Mode E action.
|
||||
- **If a parallel session is *actively* writing the shared tree, all repository mutation stops.**
|
||||
Do not `switch`, `add`, `reset`, create commits with a temporary index, update refs, or push. Use
|
||||
the repository's coordination system to quiesce that writer and transfer exclusive ownership; if
|
||||
none exists, report the gap and preserve the current evidence. Once you are the sole writer, an
|
||||
object-store-only commit can keep attributable foreign WIP out of the shared index and working
|
||||
tree. Freeze every candidate as the exact Git entry tuple `(mode, object ID, path)` — bytes alone
|
||||
are insufficient because `100755`, `120000`, and `160000` carry executable, symlink, and gitlink
|
||||
behavior. The safest source is an immutable candidate commit:
|
||||
```bash
|
||||
candidate_ref=<immutable-candidate-commit-oid>
|
||||
candidate_path=path/to/file
|
||||
@@ -340,7 +353,7 @@ The habits that keep a branch tangle from ever stranding work:
|
||||
candidate_oid=$(printf '%s\n' "$candidate_entry" | awk 'NR == 1 { print $3 }')
|
||||
test -n "$candidate_mode" && test -n "$candidate_oid" || exit 1
|
||||
|
||||
candidate_index=$(mktemp /tmp/tinkle_git_index.XXXXXX)
|
||||
candidate_index=$(mktemp /tmp/git_safety_candidate_index.XXXXXX)
|
||||
export GIT_INDEX_FILE="$candidate_index" # the tree's real index is untouched
|
||||
git read-tree origin/main # start from the pushed base, not the dirty tree
|
||||
git update-index --add --cacheinfo "$candidate_mode,$candidate_oid,$candidate_path"
|
||||
@@ -355,15 +368,17 @@ The habits that keep a branch tangle from ever stranding work:
|
||||
applying that route to a symlink or submodule. For those entry types, first freeze an immutable
|
||||
candidate commit and copy its mode/object tuple as above. Never source an entry from a shared path
|
||||
that another session is editing. The sequence reads and writes only the object store and a
|
||||
throwaway index, so `git status` in the shared tree is byte-for-byte unchanged. `commit-tree`
|
||||
throwaway index, so `git status` in the shared tree is byte-for-byte unchanged. It is a
|
||||
sole-writer preservation technique, not permission to mutate while someone else owns the repo.
|
||||
`commit-tree`
|
||||
does not run the normal `git commit` hook path: execute the repository's exact pre-commit/security
|
||||
gates against the candidate before push, and still let pre-push run. This is the escape hatch for
|
||||
when commit-then-switch is off the table because someone else holds the tree.
|
||||
gates against the candidate before push, and still let pre-push run. Use it only after ownership
|
||||
transfer, when preserved foreign WIP makes checkout switching or shared-index staging unsuitable.
|
||||
- **A bare `git commit` snapshots the *whole* index, not just what you staged — and a commit that
|
||||
bypassed the index leaves a trap in it.** Moving the **current** branch without updating the
|
||||
shared index advances HEAD while the index stays on its old baseline — via `commit-tree` +
|
||||
`update-ref` on that branch, or a `git commit` through a temporary `GIT_INDEX_FILE`. (The
|
||||
push-to-another-branch escape hatch above moves no *local* ref, so it leaves no drift.) Every
|
||||
sole-writer push-to-another-branch path above moves no *local* ref, so it leaves no drift.) Every
|
||||
file the new commit introduced then shows as a *staged deletion* (`git status` prints `D `
|
||||
lines plus matching `??` untracked entries). `git commit -- <path>` neither creates nor repairs
|
||||
this drift — it only updates its own paths. The drift detonates on anyone's next bare
|
||||
@@ -447,9 +462,11 @@ one branch into a repo export.
|
||||
|
||||
For a multi-branch "only one main" cleanup while other sessions may still commit or open PRs, read
|
||||
**[references/merge_verification.md](references/merge_verification.md)** § Converging many branches
|
||||
to one main under active concurrency before Step 3. It adds the moving-ref inventory, dirty-WIP
|
||||
preservation, immutable-candidate, duplicate-PR, and final branch-count gates that a single-branch
|
||||
retirement does not need.
|
||||
to one main through single-writer windows before Step 3. While another writer is active, that route
|
||||
is read-only: fetch, object/ref creation, bundle export, push/PR, and deletion wait for existing
|
||||
coordination to prove quiescence and transfer exclusive ownership through final readback. The
|
||||
reference adds the moving-ref inventory, dirty-WIP preservation, immutable-candidate, duplicate-PR,
|
||||
and final branch-count gates that a single-branch retirement does not need.
|
||||
|
||||
**Step 3 — destroy, in the safe order:**
|
||||
|
||||
@@ -521,6 +538,10 @@ in place; the bundle restores full history via `git fetch <file>.bundle <branch>
|
||||
|
||||
## Scripts (execute these; they are non-destructive unless noted)
|
||||
|
||||
Every `scripts/...` path below is relative to this Skill's bundle root, not a command promised on
|
||||
`PATH` or in the target repository. Resolve the loaded Skill directory and invoke the bundled path;
|
||||
never tell a user to run bare `git_verify_branch_merged.sh` unless `command -v` actually finds it.
|
||||
|
||||
| Script | Does | Mutates? |
|
||||
|---|---|---|
|
||||
| `scripts/git_find_all_checkouts.sh [root ...]` | Find every checkout of this repo on the machine — including independent clones invisible to `git worktree list` — and flag uncommitted/untracked/unpushed work, remote-cache age, and borrowed alternates object stores | Nothing (read-only, no fetch) |
|
||||
@@ -534,10 +555,11 @@ in place; the bundle restores full history via `git fetch <file>.bundle <branch>
|
||||
All six run from the repository root. They use read-only enumeration/configuration commands such as
|
||||
`find`, `config`, `symbolic-ref`, `submodule status`, `status`, `cat-file`, `rev-list`, `rev-parse`,
|
||||
`fsck`, `for-each-ref`, and `remote get-url`; plus scoped `fetch`, `archive`, `bundle create/verify`,
|
||||
metadata hashing/archive, and (preserve only) `update-ref` where each script's table row says so — never
|
||||
`checkout`, `reset`, `push`, `stash drop`, `branch -d`, or `gc`, so they are safe to run in a
|
||||
dirty tree or alongside other agents. `git_find_all_checkouts.sh` additionally never fetches, so
|
||||
it works offline and behind a proxy.
|
||||
metadata hashing/archive, and (preserve only) `update-ref` where each script's table row says so.
|
||||
Only `git_find_all_checkouts.sh` is repository-read-only and safe beside read-only agents; it never
|
||||
fetches, so it also works offline and behind a proxy. Any helper that fetches, writes backup state,
|
||||
or adds refs runs only after existing coordination transfers exclusive writer ownership. None of
|
||||
the helpers authorizes `checkout`, `reset`, `push`, `stash drop`, `branch -d`, or `gc`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- Pick the diff FORM from the question you're asking (two-dot vs three-dot)
|
||||
- Why safety-biased: a false "merged" loses work, a false "unmerged" only costs a look
|
||||
- Manual-only investigation hints (do NOT auto-decide on these)
|
||||
- Converging many branches to one main under active concurrency
|
||||
- Converging many branches to one main through single-writer windows
|
||||
- Independent clone retirement — preserve refs, metadata, and borrowed objects
|
||||
- Worktree retirement — prove the checkout is disposable before removal
|
||||
- Adversarial multi-agent verification (for a whole repo of branches)
|
||||
@@ -75,9 +75,10 @@ base's parallel work is the point. It is the wrong form for "what does the base
|
||||
|
||||
For each branch, the script decides among three outcomes:
|
||||
|
||||
- **MERGED (ancestor)** — in the base's history. Delete freely.
|
||||
- **MERGED (ancestor)** — in the base's history. Content containment is proven; deletion still
|
||||
requires the separately authorized Mode E target, current ref equality, and preservation gate.
|
||||
- **MERGED (content contained)** — a trial merge into the base changes nothing; the "commits
|
||||
ahead" count is a squash/rebase artifact. Delete freely.
|
||||
ahead" count is a squash/rebase artifact. This proves content containment, not deletion authority.
|
||||
- **UNMERGED / NEEDS REVIEW** — a trial merge *would* change the base, so the branch carries
|
||||
content the base does not already have (a genuinely new/edited/reverted/deleted file). Review
|
||||
the listed contribution before deleting.
|
||||
@@ -214,12 +215,15 @@ most likely to be restored on instinct.
|
||||
|
||||
Same safety bias as everywhere else in this skill: prove supersession per item, or keep the item.
|
||||
|
||||
## Converging many branches to one main under active concurrency
|
||||
## Converging many branches to one main through single-writer windows
|
||||
|
||||
Use this READ-DO sequence when the outcome is not one deletion but a repository-wide convergence:
|
||||
keep every unique behavior, preserve current WIP, and leave exactly one maintained `main`. A branch
|
||||
list is a moving snapshot while other sessions are alive, so the start-of-task audit cannot double
|
||||
as the deletion gate.
|
||||
as the deletion gate. Another active writer makes this sequence read-only: do not fetch, create
|
||||
objects, update refs, export bundles, push/open PRs, or delete until the repository's existing
|
||||
coordination proves quiescence and transfers exclusive writer ownership. Hold that ownership
|
||||
through commit and final readback. If it is lost, stop and restart from a fresh authority snapshot.
|
||||
|
||||
The executing agent owns only the explicitly authorized slice of this sequence, not every object
|
||||
the inventory reveals. `--verify-current` mechanically decides only whether exact ref tips stayed
|
||||
@@ -239,9 +243,14 @@ Before interpreting the inventory, partition objects into three sets:
|
||||
Generic phrases such as "take over", "continue", or "finish this" do not move an object between
|
||||
sets. The user must name the additional object or otherwise make the expansion unambiguous.
|
||||
|
||||
Record the exact local and remote-tracking refs, then query the hosting service for its current
|
||||
branch list and PR heads. Keep the two inventories separate: remote-tracking refs are a Git cache;
|
||||
the hosting API is authority for branches that exist on the server. Record every exact tip SHA.
|
||||
Before spawning reviewers or interpreting refs, the sole writer performs one authority refresh,
|
||||
including any required fetch, then freezes the exact local and remote-tracking refs and queries the
|
||||
hosting service for its current branch list and PR heads. Keep the two inventories separate:
|
||||
remote-tracking refs are a Git cache; the hosting API is authority for branches that exist on the
|
||||
server. Record every exact tip SHA and give reviewers those immutable SHAs. If another writer is
|
||||
active and ownership cannot transfer, hosting/API and existing immutable-object reads may inventory
|
||||
what is already known, but the inability to refresh authority is a reported gap, not permission to
|
||||
fetch concurrently.
|
||||
|
||||
Classify each change-authorized non-main ref by content. Use the trial-merge verdict first. For
|
||||
NEEDS REVIEW refs, walk the supersession ladder above and open distinctive code/tests at authority.
|
||||
@@ -250,17 +259,19 @@ Merge or adapt the smallest unique behavior; never merge an old whole branch mer
|
||||
many `+` commits or a compelling name. Report inspect-only and excluded refs separately without
|
||||
turning their existence into an action item.
|
||||
|
||||
### 2. Build keeper commits without touching a shared writer
|
||||
### 2. Build keeper commits after exclusive ownership transfers
|
||||
|
||||
When another session is actively changing files, do not switch the shared checkout or use its real
|
||||
index. Build from the freshly fetched base with Mode D's alternate-index plumbing. Preserve each
|
||||
candidate as an exact `(mode, object ID, path)` tuple from an immutable commit; copying only blob
|
||||
bytes can silently strip executable (`100755`), symlink (`120000`), or gitlink (`160000`) behavior.
|
||||
An owned temporary regular file may be hashed only after its intended `100644`/`100755` mode is
|
||||
verified explicitly; symlinks and submodules must use the immutable-entry route. Never hash the
|
||||
shared worktree, which can silently capture someone else's in-progress bytes. Run the candidate's
|
||||
When another session or scheduler is actively changing files, do not switch, use the real index,
|
||||
create objects with an alternate index, update refs, or publish a PR. Wait for the existing
|
||||
coordination mechanism to quiesce that writer and transfer exclusive ownership. Once transferred,
|
||||
build from the freshly fetched frozen base with Mode D's sole-writer alternate-index technique.
|
||||
Preserve each candidate as an exact `(mode, object ID, path)` tuple from an immutable commit;
|
||||
copying only blob bytes can silently strip executable (`100755`), symlink (`120000`), or gitlink
|
||||
(`160000`) behavior. An owned temporary regular file may be hashed only after its intended
|
||||
`100644`/`100755` mode is verified explicitly; symlinks and submodules must use the immutable-entry
|
||||
route. Never hash a shared-worktree path attributed to someone else's WIP. Run the candidate's
|
||||
deterministic tests and the exact hook/security gates that a normal commit would have run before
|
||||
opening the PR.
|
||||
opening the PR, and retain exclusive ownership through push and final readback.
|
||||
|
||||
After a squash merge, do not compare commit SHAs: GitHub creates a new base-branch commit. If the
|
||||
base did not otherwise move, equal tree IDs prove byte-identical landing. If it did move, compare
|
||||
@@ -269,6 +280,10 @@ GitHub-side duplicate/superseded PR handling belongs to the `github-ops` skill.
|
||||
|
||||
### 3. Preserve refs and dirty WIP through different channels
|
||||
|
||||
This section creates refs, objects, external backups, or hosted state. Run it only while the
|
||||
exclusive writer window from Step 1 remains valid. If another writer resumes, stop before the next
|
||||
mutation; the frozen evidence remains useful, but it does not authorize continuing.
|
||||
|
||||
Create a repository-external bundle containing only the branches/refs whose deletion is authorized,
|
||||
then verify it. The bundle is the ref manifest: immediately before deletion run:
|
||||
|
||||
@@ -305,14 +320,15 @@ with checkout materialization. Move that exact path to the verified external bac
|
||||
clean base, then restore the saved bytes; it should naturally become a tracked modification. Never
|
||||
drop it because "main now has a file with that name."
|
||||
|
||||
If another writer is still active, leave the real HEAD/index/worktree alone and postpone local
|
||||
branch convergence. Publishing an isolated PR is safe; switching the shared checkout is not. When
|
||||
an exclusive window exists, update only paths proven clean, or restore the complete verified WIP
|
||||
set after materializing the new base.
|
||||
If another writer is active, leave the real HEAD/index/worktree, object store, refs, and hosted
|
||||
branches unchanged and postpone both local convergence and PR publication. When the existing
|
||||
coordination restores an exclusive window, restart at Step 1, update only paths proven clean, or
|
||||
restore the complete verified WIP set after materializing the new base.
|
||||
|
||||
### 4. Re-freeze immediately before deletion
|
||||
|
||||
Fetch again, re-query hosting branches/PRs, and re-enumerate local refs. Compare the result with the
|
||||
While retaining exclusive writer ownership, fetch again, re-query hosting branches/PRs, and
|
||||
re-enumerate local refs. Compare the result with the
|
||||
bundle heads. A new branch, changed tip, or late PR inside the change-authorized set is new evidence:
|
||||
stop, classify its unique behavior, and rebuild the bundle. A newly discovered collaborator object
|
||||
does not silently join that set; record it as inspect-only or excluded and rebuild only if the next
|
||||
@@ -516,6 +532,7 @@ high-stakes "is *everything* merged?" verdict, fan out:
|
||||
never by commit count. Report per branch: MERGED / **UNMERGED / NEEDS REVIEW (with the file(s)
|
||||
the trial merge would change)**."
|
||||
3. **Lock them read-only** (see rules below) so concurrent agents don't corrupt each other's tree.
|
||||
The sole writer refreshes authority first and hands them frozen SHAs; reviewers never fetch.
|
||||
4. **Counter-review every finding yourself.** An agent's "UNMERGED" is a *hypothesis*: re-run the
|
||||
trial-merge / inspect the specific files before believing it (agents produce false positives
|
||||
too). An agent's "all merged" is only as good as its method — spot-check that it judged by
|
||||
@@ -530,12 +547,13 @@ a subagent cannot spawn subagents.
|
||||
|
||||
Put these in every agent's prompt — they are what make parallel verification safe and correct:
|
||||
|
||||
- **Read-only, always.** Only `fetch --quiet`, `merge-base`, `merge-tree`, `diff`, `log`, `show`,
|
||||
`cat-file`, `rev-list`, `rev-parse`, `ls-tree`, `for-each-ref`, `branch -r --contains`. **Never**
|
||||
`checkout`, `switch`, `reset`, `rebase`, `commit`, `push`, `update-ref`, or `gc`. Multiple agents
|
||||
share one working tree; a single `checkout` corrupts everyone else's run.
|
||||
- **Explicit refs only** (`origin/main`, `origin/<branch>`) so nothing depends on the current
|
||||
checkout.
|
||||
- **Read-only, always.** Only `merge-base`, `merge-tree`, `diff`, `log`, `show`, `cat-file`,
|
||||
`rev-list`, `rev-parse`, `ls-tree`, `for-each-ref`, `branch -r --contains`. **Never** `fetch`,
|
||||
`checkout`, `switch`, `reset`, `rebase`, `commit`, `push`, `update-ref`, or `gc`. The sole writer
|
||||
refreshes once before fan-out and supplies immutable SHAs; a reviewer does not move even a
|
||||
remote-tracking ref.
|
||||
- **Explicit frozen SHAs first.** Use the supplied immutable object IDs for conclusions. Named refs
|
||||
(`origin/main`, `origin/<branch>`) may be displayed for attribution but are not merge authority.
|
||||
- **Judge by content via the trial merge, not by counts** — restate the check in the prompt.
|
||||
- **Return structured per-branch verdicts with the file(s) the trial merge would change for any
|
||||
UNMERGED**, not prose.
|
||||
|
||||
@@ -4,10 +4,14 @@ Each practice below maps to a specific way work actually gets lost. They are che
|
||||
ceremony; adopt the ones whose failure mode you're exposed to.
|
||||
|
||||
## Contents
|
||||
- Parallel / multi-branch work: commit before you switch (not stash, not worktree)
|
||||
- Choose topology from current authority
|
||||
- Shared checkout and concurrent sessions: one writer
|
||||
- Exact-SHA handoff and scoped completion
|
||||
- Known automated writers are not session-owned WIP
|
||||
- Parallel / multi-branch work: commit before switching; exceptions follow current authority
|
||||
- Push work-in-progress branches early
|
||||
- Confirm the branch before every commit
|
||||
- Relocate your work when a parallel session switched the shared tree under you
|
||||
- Recover stranded work after a parallel session switched the shared tree
|
||||
- Audit before rebase / branch-delete
|
||||
- Audit every authorized worktree before retirement
|
||||
- Snapshot before any history rewrite
|
||||
@@ -15,7 +19,117 @@ ceremony; adopt the ones whose failure mode you're exposed to.
|
||||
- Commit-scope hygiene (don't sweep unrelated staged work)
|
||||
- Set a wider reflog safety window once
|
||||
|
||||
## Parallel / multi-branch work: commit before you switch (not stash, not worktree)
|
||||
## Choose topology from current authority
|
||||
|
||||
Before recommending branches, worktrees, or a second checkout, read the current user and project
|
||||
instructions. Their explicit collaboration and contribution model wins. A generic post-incident
|
||||
lesson cannot silently replace a user's existing no-worktree decision, a repository's PR-only
|
||||
flow, or a task registry's single-writer rule.
|
||||
|
||||
Default here: keep one maintained checkout and commit before switching. Do **not** turn worktrees
|
||||
into a standing rule for every concurrent session. When the current authority explicitly approves
|
||||
a named second checkout and simultaneous writing truly requires it, prefer a linked worktree over
|
||||
an independent clone because audits can discover it. That exception does not make it independent:
|
||||
linked worktrees separate working files, `HEAD`, and index, but share refs, stashes, object storage,
|
||||
config, and hooks; ignored dependencies and local-only assets are not copied.
|
||||
|
||||
## Shared checkout and concurrent sessions: one writer
|
||||
|
||||
**Failure mode:** separate sessions edit different files and assume they are independent, but they
|
||||
share the checkout's current branch and index. One session can switch the branch under another;
|
||||
one bare commit snapshots every staged entry, including another session's work or a phantom `D`
|
||||
left by an index-bypassing commit. File ownership alone cannot close that race.
|
||||
|
||||
**Prevention:** one physical checkout has one writer. Parallel agents or sibling sessions may do
|
||||
read-only investigation, but they do not mutate files, refs, index, stash, or working-tree state.
|
||||
Writer ownership comes from the repository's existing task/coordination system. If that authority
|
||||
is unavailable, another writer is active, or foreign dirty paths cannot be attributed, stop the
|
||||
write path; do not create a branch, stash the tree, or "just stage your files" as a workaround.
|
||||
|
||||
When you hold write ownership, keep the stage-to-commit interval bounded and inspect the complete
|
||||
index, not merely the paths you just added:
|
||||
|
||||
```bash
|
||||
git -C <absolute-repo> branch --show-current
|
||||
git -C <absolute-repo> add -- <exact-path-1> <exact-path-2>
|
||||
git -C <absolute-repo> diff --cached --name-status
|
||||
git -C <absolute-repo> diff --cached --stat
|
||||
git -C <absolute-repo> commit -m "<message>"
|
||||
```
|
||||
|
||||
Every staged status letter must match the intended change. In particular, an unfamiliar `D` is a
|
||||
stop signal, not a harmless leftover. Follow the repository's contribution policy for push/PR;
|
||||
this prevention Skill does not widen push or merge authority.
|
||||
|
||||
Prefer a WIP commit and early remote copy to stash juggling. Do not rewrite an existing narrow
|
||||
stash exception as an absolute prohibition: if the current contract permits it, only the single
|
||||
writer may use its exact absolute-repository and explicit-file form, such as
|
||||
`git -C <absolute-repo> stash push [options] -- <exact-file>...`. An unscoped stash remains invalid.
|
||||
|
||||
## Exact-SHA handoff and scoped completion
|
||||
|
||||
A branch name is a routing label, not a frozen deliverable. The writer can add another commit after
|
||||
handoff, and linked worktrees share that ref. A later `merge <topic>` may therefore merge bytes the
|
||||
integrator never reviewed.
|
||||
|
||||
Freeze and read back the handoff:
|
||||
|
||||
```bash
|
||||
topic_branch=$(git -C <absolute-repo> branch --show-current)
|
||||
topic_sha=$(git -C <absolute-repo> rev-parse HEAD)
|
||||
remote_sha=$(git -C <absolute-repo> ls-remote origin "refs/heads/$topic_branch" | awk 'NR == 1 {print $1}')
|
||||
test -n "$remote_sha" && test "$topic_sha" = "$remote_sha"
|
||||
```
|
||||
|
||||
Report the absolute checkout path, branch, and `topic_sha`. Immediately before the merge, the
|
||||
integrator must re-read the intended remote tip and require it to equal `topic_sha`. A direct merge
|
||||
names the frozen object, not the branch:
|
||||
|
||||
```bash
|
||||
current_remote_sha=$(git -C <absolute-repo> ls-remote origin "refs/heads/$topic_branch" | awk 'NR == 1 {print $1}')
|
||||
test -n "$current_remote_sha" && test "$current_remote_sha" = "$topic_sha"
|
||||
git -C <absolute-repo> merge "$topic_sha"
|
||||
```
|
||||
|
||||
For a hosted PR, use the platform's expected-head-SHA precondition when it exists. Otherwise make a
|
||||
fresh hosted head-SHA readback the immediately preceding step and abort instead of merging if it no
|
||||
longer equals `topic_sha`. A moved ref means "handoff expired," not "take whatever is newest."
|
||||
|
||||
Completion requires all of these, never an OR between them:
|
||||
|
||||
1. every session-owned tracked and untracked byte is present in the handed-off commit;
|
||||
2. the exact commit is present on the intended remote, proven by independent readback;
|
||||
3. every residual path in the checkout is enumerated and attributed;
|
||||
4. no branch, stash, worktree, or remote ref is deleted as an implicit completion step.
|
||||
|
||||
The claim is intentionally scoped. A checkout can remain dirty because an authorized automation
|
||||
writer or another named owner left unrelated paths. That does not make the session incomplete, but
|
||||
it forbids claiming the **whole repository** is clean or sweeping the residuals into this commit.
|
||||
Retirement is a separate Mode E task with new authority and fresh evidence.
|
||||
|
||||
## Known automated writers are not session-owned WIP
|
||||
|
||||
**Failure mode:** an integration session checks that a scheduled writer is idle, then treats the
|
||||
next few commands as exclusive. The job starts after the check and writes during stage, merge, or
|
||||
final verification. A process snapshot is an observation, not a lock.
|
||||
|
||||
Do not stop, disable, or reconfigure an authorized job merely to make the generic Git routine easy;
|
||||
that is a new operations decision. A scheduler that can write anywhere in this physical checkout is
|
||||
still a writer even when its usual paths are disjoint from the current task. Before any Git mutation,
|
||||
the project's existing coordination mechanism must prove it quiescent and transfer exclusive writer
|
||||
ownership through commit and final readback. A process snapshot alone cannot do that. Then:
|
||||
|
||||
- read the project's owner contract for the generated paths;
|
||||
- do not manually edit generated files or co-stage them with an unrelated task;
|
||||
- when the current task explicitly owns one generated batch and exclusive ownership has transferred,
|
||||
stage only its exact paths, complete the Git operation, then re-read the working tree after handing
|
||||
ownership back because the next batch may already have arrived;
|
||||
- if no existing mechanism can prove quiescence and transfer ownership, stay read-only, enumerate
|
||||
the residual paths, and report the gap rather than treating disjoint paths or an idle check as
|
||||
mutual exclusion;
|
||||
- treat a new lock, lease, pause, or schedule change as a separately authorized design.
|
||||
|
||||
## Parallel / multi-branch work: commit before switching; exceptions follow current authority
|
||||
|
||||
**Failure mode:** the classic disaster is `git stash` → switch branch → work → `git stash` again →
|
||||
rebase → switch back. Each `stash` that gets superseded or dropped orphans a commit; after a busy
|
||||
@@ -30,27 +144,32 @@ stashed away or stranded:
|
||||
```bash
|
||||
# instead of `git stash` before switching:
|
||||
git switch -c <branch-for-this-work> # a branch for this line of work
|
||||
git add <the paths for THIS work> && git commit -m "wip: ..."
|
||||
git add -- <the paths for THIS work>
|
||||
git diff --cached --name-status # inspect the whole shared index
|
||||
git diff --cached --stat
|
||||
git commit -m "wip: ..."
|
||||
git push -u origin <branch-for-this-work> # early; re-push as you go
|
||||
git switch <other-branch> # nothing left behind — no stash to drop
|
||||
```
|
||||
|
||||
Then bring the work back to wherever you need it **live in the working tree** by merging — not by
|
||||
fishing it out of a stash and not from a second checkout:
|
||||
After sole-writer ownership has transferred to the integrator, bring the frozen work back wherever
|
||||
it is needed **live in the working tree** by merging the reviewed SHA — not by fishing it out of a
|
||||
stash or following a movable branch name:
|
||||
|
||||
```bash
|
||||
git switch <target-branch>
|
||||
git merge <branch-for-this-work> # the work is now in THIS working tree too
|
||||
git merge "$topic_sha" # exact reviewed object, not a movable ref
|
||||
```
|
||||
|
||||
**Deliberately avoided here — two tempting shortcuts that both cause the loss this skill exists to prevent:**
|
||||
**Deliberately avoided here — shortcuts that cause the loss this skill exists to prevent:**
|
||||
|
||||
- **`git stash` + switch juggling** — orphans stashes (the failure mode above). Commit instead; a
|
||||
commit on a branch never silently disappears from `git stash list`.
|
||||
- **`git worktree`** — a second checkout is one more place to leave work in and forget, it does
|
||||
**not** copy gitignored dependencies (`node_modules`, `.venv`), so tools/tests run there fail on
|
||||
the missing deps, and it can hand back a stale checkout of an older commit. A shared working tree
|
||||
with disciplined *commit-then-switch* is safer and simpler than juggling worktrees.
|
||||
- **Defaulting every concurrent session to `git worktree`** — a second checkout is one more place
|
||||
to leave work in and forget, it does **not** copy gitignored dependencies (`node_modules`,
|
||||
`.venv`), and it still shares refs/stashes/config/hooks. A shared checkout with one writer and
|
||||
disciplined *commit-then-switch* is safer unless current authority explicitly approves the named
|
||||
worktree exception described above.
|
||||
- **`git clone --shared` as temporary isolation** — the clone's refs and object ownership split:
|
||||
`.git/objects/info/alternates` borrows objects from the source while the clone owns its refs.
|
||||
Git's official documentation warns that source maintenance can prune those borrowed objects and
|
||||
@@ -91,10 +210,11 @@ to, and gets deleted along with the wrong branch during cleanup.
|
||||
git branch --show-current # is this where this change belongs?
|
||||
```
|
||||
|
||||
If you commit to the wrong branch anyway, it's recoverable: `git log` the sha, `git branch
|
||||
correct-branch <sha>`, then remove it from the wrong branch — but confirming up front is free.
|
||||
If you commit to the wrong branch anyway, it's recoverable: `git log` the SHA, then create a
|
||||
preserving ref with `git branch correct-branch <sha>`. Leave removal from the wrong branch to
|
||||
separately authorized Mode E retirement; confirming up front is free.
|
||||
|
||||
## Relocate your work when a parallel session switched the shared tree under you
|
||||
## Recover stranded work after a parallel session switched the shared tree
|
||||
|
||||
**Failure mode:** two agents share one working tree. While you were editing, a *parallel* session
|
||||
ran `git switch` and moved the shared tree onto **its** feature branch — so your uncommitted changes
|
||||
@@ -103,8 +223,13 @@ switched, so "commit before you switch" never got a chance to fire. A naive `git
|
||||
commit` here buries your work inside the other branch's PR (wrong attribution, wrong review) and can
|
||||
sweep in their file; committing onto their branch also couples your change to their merge.
|
||||
|
||||
**Fix — carry your uncommitted work onto a branch off the base, commit only your paths, then put the
|
||||
tree back exactly where the other session left it:**
|
||||
**Incident-only recovery:** do not run the sequence below while the other writer is active. First
|
||||
use the repository's coordination system to quiesce that writer, freeze the observed dirty paths,
|
||||
and transfer exclusive write ownership. If no such authority exists, stop with the evidence intact;
|
||||
do not treat checkout plumbing as a concurrency loophole.
|
||||
|
||||
Once exclusive ownership is established, carry your uncommitted work onto a branch off the base,
|
||||
commit only your paths, then put the tree back exactly where the other session left it:
|
||||
|
||||
```bash
|
||||
# 1. See what the hijacked branch is, and prove YOUR files are safe to carry across the switch.
|
||||
@@ -119,11 +244,10 @@ git checkout origin/main -b fix/your-work
|
||||
# 3. Commit ONLY your explicit paths — never `git add -A`; the other session's file is still here.
|
||||
git add <your-path-1> <your-path-2>
|
||||
git diff --cached --name-only # verify: only yours, not their file
|
||||
git commit -m "…" # then push / PR / merge as normal
|
||||
git commit -m "…" # freeze/push this SHA; never merge the branch name
|
||||
|
||||
# 4. Restore the other session's state: put the shared tree back on their branch.
|
||||
# 4. Restore the other session's state before handing write ownership back.
|
||||
git checkout <their-branch> # their uncommitted file carries back untouched
|
||||
git branch -d fix/your-work # safe once merged (the branch tracked origin/main)
|
||||
```
|
||||
|
||||
**Why this and not the alternatives:** `git stash` to move your edits risks the orphaned-stash loss
|
||||
@@ -133,6 +257,11 @@ branch's tip and the base, which is exactly the condition under which `checkout
|
||||
uncommitted edits with no conflict (if it reports a difference, stop and resolve it deliberately
|
||||
rather than forcing the switch). Step 4 is correctness, not just courtesy: the parallel session
|
||||
expects to find its own branch checked out with its work intact, exactly as it left it.
|
||||
Branch deletion is not part of this recovery; it requires separately authorized Mode E evidence.
|
||||
After Step 3, return to **Exact-SHA handoff and scoped completion** above: record the commit as
|
||||
`topic_sha`, push and read back that exact object, use `git merge "$topic_sha"` for a direct merge,
|
||||
and require the hosted expected-head-SHA gate (or immediately preceding hosted head readback) for a
|
||||
PR merge. Any branch-tip drift expires the handoff.
|
||||
|
||||
## Audit before rebase / branch-delete
|
||||
|
||||
@@ -180,7 +309,7 @@ the pre-rewrite commits.
|
||||
**Prevention:** a throwaway backup branch makes the whole operation reversible:
|
||||
|
||||
```bash
|
||||
git branch backup/pre-rewrite # points at the current tip; delete once you're happy
|
||||
git branch backup/pre-rewrite # points at the current tip; retire later through Mode E
|
||||
```
|
||||
|
||||
If the rewrite goes wrong, `git reset --hard backup/pre-rewrite` restores it exactly.
|
||||
|
||||
Reference in New Issue
Block a user