| name | git-discipline |
| description | Run BEFORE any git write (commit, push, merge) and before a stale checkout or branch read: exists? landed? — in this multi-agent repo the filesystem answers for someone else's branch, not yours, so `git show origin/main:<path>` is the only honest answer to "does X exist / did Y land". (Trigger note: this index derives keywords from the FIRST 12 non-stopword tokens of this description — `_triggers_from_text(limit=12)` — so the read-side terms above MUST stay in this opening sentence; when they weren't, this skill could only fire on writes while the trap fires on a READ, and it took three victims in one hour.) The full write surface still applies: commit, push, branch, switch, rebase, merge, open/close/merge a PR, re-trigger CI, "unblock #NNN", or diagnose a failing CI check, a "superseded/divergent" branch, or a "CI seems stuck" / green-but-won't-merge PR (decode mergeable_state FIRST — see §3a), a DIRTY/CONFLICTING mergeable_state stuck on a stale GitHub cache even when merge-base --is-ancestor + merge-tree prove 0 conflicts (base-toggle to force a recompute — see §3a-i), a stuck-DIRTY PR whose required pull_request checks never fire because GitHub won't build refs/pull/N/merge (advance the branch + base-toggle to rebuild the merge ref — see §3a-ii), or a recurring add/add conflict on a file two PRs both created (a dispatch-plan / evidence artifact under agi-proof — see §4b). This repo (tomyimkc/sophia-agi) has many concurrent advisors (Claude/Copilot/GLM/human) on many worktrees pushing to main around the clock, so a local checkout is a STALE snapshot the moment it is taken. Use even if the conversation already contains a diagnosis or a "you are on branch X" line — that may be stale.
|
| metadata | {"short-description":"Stale-snapshot / merge-blocker / verify-before-act discipline for this high-churn repo"} |
Git & merge discipline (this repo)
See also spark-cluster-ops §3-4 for the cluster addenda: the git-crypt
worktree/cherry-pick -c bypass, the failed-worktree add collision recovery
(branch -f → reset --hard), and shared-branch rebase reconcile.
This repo has many concurrent advisors pushing and merging to main constantly. Every
expensive mistake here came from acting on an unverified local picture: a fix PR built off
main that was 28 commits behind; a duplicate PR for a problem another advisor already fixed;
a "merge" blocked by unresolved review threads (not CI); a cancelled CI run misread as a real
failure. This skill is the cheap pre-flight that prevents that whole class of waste.
The canonical, fuller playbooks + ready-to-run scripts live in .agents/skills/ (shared with
the other agent tools). This skill is the Claude-Code entry point — it routes you to them and
states the rules. Do not re-implement the scripts; call them.
1. Before ANY git write — situational awareness (1 s, read-only)
python .agents/skills/git-operations/scripts/git_situational_awareness.py
python .agents/skills/git-operations/scripts/git_situational_awareness.py --json
Exit code is the verdict: 0 safe · 1 warnings (review, then proceed) · 2 blockers (stop).
It answers the six questions: where am I, is a sibling worktree on this branch, what's uncommitted,
am I stale vs origin/main, is there a competing PR on this branch, is main directly pushable.
It does not fetch — run git fetch origin --prune first when you need fresh remote truth.
2. Before any merge / unblock / CI-fix — merge pre-flight
git fetch origin --prune
python .agents/skills/multi-agent-merge-preflight/scripts/merge_blockers.py NNN
Re-verify the thing is still broken on origin/main (another advisor may have fixed it),
check for a competing PR before opening your own, and enumerate all merge blockers — not
just CI. The silent killer here is required_review_thread_resolution: true: green checks but
unresolved review threads still block the merge.
3. Verify before you act (the three false calls that happened here)
- "Divergent/superseded" by line count → wrong. Test ancestry + a trial merge, never line counts.
- "test: fail" → may be a cancelled run (job conclusion
cancelled, not failure) caused by
main moving mid-run. Read the job conclusion and re-trigger before "fixing" a ghost.
- A "bug" in a PR → reproduce it on the PR's current head against fresh
origin/main first.
Full reasoning + the exact gh api graphql thread-resolution recipe:
.agents/skills/pr-merge-verification/SKILL.md and .agents/skills/multi-agent-merge-preflight/SKILL.md.
3a. "CI seems stuck" — decode mergeable_state before touching anything (2026-07-06, PR #681)
A PR whose check-runs are ALL green can still sit unmergeable for hours and read as "stuck CI".
The check list is the wrong place to look — fetch the PR object and branch on mergeable_state:
mergeable_state | It means | The move |
|---|
dirty | main moved and now conflicts with the PR (typical cause here: a squash-merge of a sibling PR carrying overlapping history) | freeze-check (live-evidence-branch-freeze), then git fetch origin main && git rebase origin/main — already-squash-merged commits drop out on their own — then push --force-with-lease. Rebase, don't merge-commit: the ruleset requires linear history |
behind | main moved, no conflict | update branch / rebase, same freeze-check first |
blocked | a protection rule is unsatisfied: checks still running on the new head, or required_review_thread_resolution (green checks + unresolved threads) | wait for checks / resolve threads — NOT an error, do not re-kick CI |
unstable | a non-required check failed | read which; required ones may still be green |
unknown | GitHub is still computing | re-fetch in ~10 s, don't act on it |
Two traps measured in this repo: (1) rebasing flips dirty → blocked, which looks like a new
problem but just means the checks are re-running on the new head; (2) a run shown "failed" on the
old head after your push is usually cancelled (superseded), per §3.
3a-i. Stale-cache DIRTY that base-toggle clears (2026-07 sessions, #932 wave)
The table row for dirty assumes the conflict is real. The 2026-07 merge wave hit a second
shape: a PR you have proven clean — git merge-base --is-ancestor origin/main HEAD succeeds AND
git merge-tree $(git merge-base origin/main HEAD) origin/main HEAD prints zero conflict markers —
still reads mergeable: CONFLICTING / mergeStateStatus: DIRTY on GitHub for hours or days. That is
a stale server-side mergeability cache, not a conflict (the full reasoning and the admin-merge
escape hatch are in §4b's "Stale mergeability cache" paragraph). Before reaching for
gh pr merge --squash --admin, try the lighter move that forces GitHub to recompute from scratch:
(a) Base-toggle. Flip the PR base to a throwaway branch and back. A base change invalidates the
cached mergeable_state, so GitHub runs a full recompute on the restore:
BASE=$(gh pr view NNN --json baseRefName -q .baseRefName)
git push origin origin/main:refs/heads/zzz-stale-cache-recompute
gh pr edit NNN --base zzz-stale-cache-recompute
gh pr edit NNN --base "$BASE"
gh pr view NNN --json mergeable,mergeStateStatus
git push origin :refs/heads/zzz-stale-cache-recompute
Use gh pr edit --base (it PATCHes the pulls endpoint under the hood); the raw-REST equivalent is
gh api -X PATCH repos/tomyimkc/sophia-agi/pulls/NNN -f base=… — not the issues/NNN endpoint,
which shares the number but silently ignores a base field and no-ops. GitHub requires a PR's base to
be an existing branch ref (a non-existent base is rejected 422), so the throwaway target is created
before the toggle (the git push origin origin/main:refs/heads/… line above) and deleted after. An
empty commit, a real commit, and close/reopen have all been observed to fail to clear this cache;
the
base-toggle is the reliable nudge short of --admin. Re-confirm ancestry first (--is-ancestor
still succeeds) — if it now fails, a sibling has landed a real overlapping change and the DIRTY
is genuine: rebase, do not toggle over it.
3a-ii. DIRTY that starves pull_request CI — rebuild the merge ref (2026-07 sessions)
A worse corollary: when mergeable_state is stuck DIRTY, GitHub may refuse to build
refs/pull/N/merge — the ref your pull_request / pull_request_target workflows run against.
Symptom: the PR shows no check runs at all (or only the always-on schedule/push ones), the
required checks fast + ci-complete never fire, and the PR can never become mergeable on
its own — it isn't failing CI, it's being starved of CI. Do not read an empty check list as
"checks haven't reported yet"; read it as "the merge ref was never built".
The fix is to rebuild the merge ref, in this order:
- Advance the branch — push a trivial commit (
git commit --allow-empty -m "chore: retrigger CI — merge ref stuck" and push) so GitHub cuts a fresh head and, with it, a fresh refs/pull/N/merge.
- Base-toggle (§3a-i) — if advancing alone doesn't rebuild the merge ref, the stale-cache
DIRTY is the cause; the base-toggle forces the recompute that also recreates the merge ref.
- Re-read:
gh pr view NNN --json mergeable,statusCheckRollup — the required checks should
now schedule and run against the restored merge ref.
Distinguish from the benign cases before acting: a genuinely blocked PR (checks still running) and
a behind PR (main moved, no conflict) both show check runs in progress — only a stuck-DIRTY PR
shows the required checks absent. If --is-ancestor succeeds and merge-tree is clean, the
absent checks are the cache, not your code.
No gh CLI (remote/web containers): merge_blockers.py and the §5 recipes die with
FileNotFoundError: 'gh'. Do the same checks via the GitHub MCP tools (pull_request_read:
get for mergeable_state, get_review_comments for unresolved threads, get_check_runs) or
plain unauthenticated REST — this repo is public:
curl -s https://api.github.com/repos/tomyimkc/sophia-agi/pulls/NNN | jq .mergeable_state
(rate limit 60/hr/IP — poll at ≥60 s and tolerate empty responses).
4. When you commit, isolate your changes
The working tree is frequently contaminated by other advisors' uncommitted work.
git add <explicit file list> — never git add -A / git add ..
git diff --cached --stat to confirm only your files are staged.
- Confirm
git branch --show-current and git log --oneline -1 before pushing.
- Develop on the branch the user named; direct pushes to
main are blocked by protection.
- Gitignored generated paths need
git add -f — this has bitten 3× (2026-07-06).
.claude/skills/, training/worldmodel/, skills/portable/ are gitignored, yet
they hold tracked-and-committed files (skills) or files a lane must deliver (fresh
corpora). A plain git add <gitignored-path> is silently refused ("Use -f if
you really want to add them") → an empty commit → the push "succeeds" with
nothing, or a results-branch delivery step exits 1 after the expensive work
already ran (corpus run 28799005486 spent both teacher fleets, then the
git add training/worldmodel/... in its push step was refused and the branch
delivery failed — data survived only because a second artifact channel existed).
Any lane or commit touching a gitignored path uses git add -f, and a
results-branch delivery step must fail LOUD if the commit is empty
(git diff --cached --quiet && { echo '::error::nothing staged'; exit 1; }).
4a. NEVER git stash in this repo — the stash stack is shared across worktrees
The stash is per-repository, not per-worktree: every worktree that shares the
.git dir sees the SAME git stash list. Two ways this bites (both happened here):
git stash push -- <pathspec> where a path is untracked aborts with "Did you
forget to 'git add'?" and creates no stash — but a script that then runs
git stash pop pops the top of the shared stack, which belongs to another
session, dumping their WIP (and merge conflicts) into your worktree.
- Even a successful stash can be popped by a different worktree's session.
Instead of stashing to get a clean tree for a quick test: don't — you already
have an isolated worktree; run the test with your changes in place, or spin a fresh
git worktree add … origin/main for the clean baseline. If you inherit a bad pop:
git restore --source=HEAD --staged --worktree <foreign tracked files> and rm the
foreign untracked files (their data is safe in the still-kept stash), then confirm
git stash list is untouched so the owner keeps their entry.
4b. One owner per NEW shared artifact — the add/add conflict that recurs every rebase (2026-07-10, hardcase-capability-run.dispatch-plan.json)
When two PRs each create the same new file path independently, there is no merge base for
it — an add/add conflict auto-merge cannot resolve, and it re-surfaces on every rebase
until one side wins wholesale. This is exactly what happened to
agi-proof/hpo-neural/hardcase-capability-run.dispatch-plan.json: #914 (verifier-integrity
sprint) authored a 179-line plan and #925 (capability-track owner) authored a 201-line plan —
identical experimentId and schema, two different authors — so #925 conflicted on that
path and re-conflicted after every sibling merge moved main under it. The root cause is not
the merge; it is two workstreams authoring one artifact. Prevent it:
- A new shared artifact under
agi-proof/** (dispatch-plan, evidence doc, run-config) has
exactly ONE owning PR — the workstream that actually runs/owns the experiment. Declare it
before you create the file: append OWNS: <path> — <session-id> to
SESSION-COORDINATION.md (ledger-append-etiquette §1). A path already OWNED by a live
session is off-limits to your PR.
- A sibling PR that only references the run links the path — it does NOT commit its own
copy. If the verifier sprint wants to say "the hardcase run is staged", it points at the
path; it must not author a second version of the plan file. #914 should never have carried a
dispatch-plan the capability track owns.
- If two owners genuinely need distinct plans, use distinct paths (suffix run/PR/date) —
never two versions at one path.
- Resolving one that already conflicted is the OPPOSITE of the ledger/manifest rule. A plan
JSON is a single document, not an append log: take the OWNING side's file whole
(
git checkout --ours/--theirs -- <path>), then hand-fold any real content from the loser
and delete the duplicate. Do not hunk-merge two authors' fields (that yields a Frankenstein
plan with both motivation and p0ReviewedBy and a broken schema). Contrast
ledger-append-etiquette §4, where you UNION because those files are append logs.
The re-conflict-on-every-rebase tax. #925 also re-conflicted on failure-ledger.md and
evidence-manifest.json after #921 and #930 landed: every append-heavy shared file re-diverges
each time main moves. When landing N PRs that all touch the ledger/manifest, rebase-chain
them (or land back-to-back) and regenerate the manifest as the LAST step
(ledger-append-etiquette §3) instead of resolving the same tail N times. evidence-manifest.json
is generated — regenerate it (python tools/build_agi_proof_package.py), never hand-merge it.
Stale mergeability cache (distinct from a real behind/dirty). A PR you have proven clean —
git merge-base --is-ancestor origin/main HEAD succeeds AND
git merge-tree $(git merge-base origin/main HEAD) origin/main HEAD shows zero conflict markers —
can still show mergeable: CONFLICTING / mergeStateStatus: DIRTY on GitHub for hours. That is a
stale server-side cache (§3a), not a conflict; an empty commit, a real commit, and close/reopen
may all fail to clear it. Once fast + ci-complete are green and threads resolved,
gh pr merge <N> --squash --admin --delete-branch is the escape hatch (a human's call for a
claim-bearing PR — no-overclaim-merge-gate). But re-confirm ancestry first: if
--is-ancestor now fails, a sibling has since landed and the conflict is REAL — rebase, do not
admin-merge over it.
Branch protection (quick reference)
One ruleset (main-protection) on main: required checks fast + ci-complete;
required_review_thread_resolution: true (the silent blocker); required_approving_review_count: 0;
required_linear_history: true; direct pushes to main blocked — go through a PR. Repo setting
allow_auto_merge: true is on. Verify live before relying on this:
gh api repos/tomyimkc/sophia-agi/rulesets | jq '.[] | {id,name,enforcement}'
gh api repos/tomyimkc/sophia-agi/rules/branches/main \
| jq '.[] | select(.type=="required_status_checks").parameters.required_status_checks'
Why armed GitHub auto-merge stalls (diagnosed 2026-07-03)
Symptom: PRs go green on every required check yet never auto-merge; the owner merges by
hand. Happened on #400, #406, #407, and the whole Gap-2/U-series wave.
The actual root cause is NOT a missing required check. Both required checks, fast and
ci-complete, report reliably and pass — verified on the final head of #406 (all 19 check-runs
green, including fast and ci-complete). The popular hypothesis ("a required check name that
never reports on some PRs") was investigated and falsified for this repo. Two real causes:
- The arming workflow skips because no PR carries
risk:low. .github/workflows/ai-automerge.yml
enables GitHub's native auto-merge only if: contains(labels, 'risk:low'). Across the last
12 PRs (#399–#410) none was labeled risk:low, so every enable-automerge run concluded
skipped, native auto-merge was never armed, and GitHub had nothing to fire when checks went
green. autoMergeRequest was null on #407; on #406 the human armed it manually and it still
didn't auto-fire in time.
cancel-in-progress: true + a late push turns ci-complete red on the old run.
fast-ci.yml and ci.yml cancel in-progress runs on a new push; ci-complete then reports
failure (one of its needs was cancelled) on the superseded run. The newest run recovers
green, but if auto-merge was armed against the moment the previous run went red, the merge
window can be missed.
Recommended fix for the owner (NOT done here — repo settings + labels are the owner's to set):
- Apply
risk:low to low-risk PRs so ai-automerge.yml actually arms, or widen its trigger
to the labels these PRs already carry (tooling/corpus/benchmark). The label discipline is
the lever; the workflow itself is correct.
- Optional belt-and-braces: gate
ci-complete's if: always() on
github.event.check_suite/head-SHA, or set auto-merge to re-arm on synchronize, so a
cancelled sibling run can't shadow the live one.
When a "green but won't merge" happens, check the label and the enable-automerge run
conclusion first — not the check list:
gh pr view NNN --json autoMergeRequest,labels
gh run list --workflow=ai-automerge.yml -L1
Stop and ask the human
This skill governs mechanics (stale? blocked? duplicated?). If an action would change what is
claimed (a number's status, "validated", a RESULTS.md entry), that is the no-overclaim
contract — stop and ask before acting (see the ci-artifact-drift and sophia-agi skills).