--- name: land-merged-pr-with-blocked-tree-ops description: "Use when a merged PR must land in a checkout whose working tree is dirty and workspace policy blocks git checkout/restore/stash: commit-the-dirt pattern, the secret-gate false positive that blocks the landing commit, conflict resolution proven settings-vs-comments, and heartbeat (live-sync) handoff." --- Landing a merged trunk into a checkout that IS the deployment (dank/command-center `~/.omp`), when tracked files are dirty and policy forbids `checkout`/`restore`/`stash` on shared state. Re-earned 2026-10-21 landing PRs #61 + #63 into `~/.omp` (27 commits behind, two conflicts, one gate refusal). ## 1. Classify the dirt before touching it `git status --porcelain` then compare against what the merge brings in — only overlap blocks the merge: ```bash { git diff --name-only; git diff --cached --name-only; } | sort -u > /tmp/dirty.txt git diff --name-only HEAD origin/main | sort -u > /tmp/incoming.txt comm -12 /tmp/dirty.txt /tmp/incoming.txt # these paths must be clean ``` Note the first column of porcelain: `M ` = **staged**, ` M` = unstaged. A staged file shows nothing in `git diff` — look at `--cached` too, or you will "verify" a file is unchanged while it is staged. `comm` needs both inputs sorted; unsorted input prints "file is not in sorted order" and a partial answer. ## 2. Prove a dirty file content-free before resolving `cmp` it against the trunk blob, never against memory: ```bash git show origin/main:$f > /tmp/incoming cmp -s "$f" /tmp/incoming && echo "IDENTICAL to trunk" # commit/discard is content-free ``` If identical, the dirt is a sync artifact, not user work. Say so in the commit message with the `cmp` result — that is what makes a direct commit on `main` defensible for a prose-tier file. ## 3. Commit rather than discard (policy-blocked verbs) With `checkout`/`restore`/`stash` unavailable, the only non-destructive unblock is a commit. Two separate commits, and order matters: 1. Prose-tier dirt proven byte-identical to trunk — commit alone, message states `cmp` proved no prose change. 2. Machine tiers (`agent/memories/**`, `agent/managed-skills/**`, `agent/config.yml`, `agent/models.yml`, `dotfiles/`) — let the heartbeat do it: `LC_ALL=C scripts/live-sync.sh` stages allowlist only, commits, merges trunk, pushes, and **exits 0 even when it aborts the merge on conflict** (the alert goes to stderr + ntfy). `exit 0` does not mean landed — check `behind=` afterwards. Do NOT `git add -A`: the index may already hold dozens of staged machine-tier files, and `git add ` does not clear them — they ride along into your commit. ## 4. The gate refusal that blocks the landing commit The pre-commit secret gate scans the **staged** diff, so a single-file commit still trips on previously staged content. It fires on inert placeholders inside the skill docs that document the placeholder (e.g. `AKIA0000000000000EXAMPLE` — the AWS all-zero example id in `git-secret-gate-hook` / `harden-precommit-secret-gate`). If the checkout is behind, it is running the **pre-fix** hook while the fix sits unlanded in trunk: the landing is blocked by the very bug the PR corrects. Audit first, then override deliberately — never `--no-verify`: ```bash git diff --cached | grep -nE 'AKIA[A-Z0-9]{12,}|sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9]{16,}|BEGIN [A-Z ]*PRIVATE KEY|eyJ[A-Za-z0-9_-]{20,}' git diff --cached | grep -oE 'AKIA[A-Z0-9]{12,}|sk-[A-Za-z0-9]{16,}' | sort | uniq -c # what exactly, how many ALLOW_SECRETS=1 git commit -F msg # message records the audit + that the gate ran ``` Record in the message: the hits are inert placeholders, which files they sit in, that this checkout runs the pre-fix hook, and that the refusal dies with this landing. ## 5. Merge conflicts: resolve by proving settings identical ```bash git merge --no-commit --no-ff origin/main git diff --name-only --diff-filter=U ``` For each conflicted file, get all three blobs (`HEAD`, merge base, `origin/main`) and ask what the local side uniquely holds: - **Prose / doc pages**: diff ours vs trunk; if ours only lacks the incoming trim, take trunk. - **Real settings files (kdl/yaml/env)**: strip comments and diff — `diff <(grep -v '^\s*//' ours) <(grep -v '^\s*//' theirs)`. Empty output ⇒ only comments differ ⇒ taking trunk changes no deployed behaviour, and you can say that in the merge message. (2026-10-21: `desk-1-home.kdl` — modes byte-identical, trunk's comment corrected the false "no 240 Hz mode exists".) Resolve without the blocked verbs by writing the trunk blob: `git show origin/main:$f > $f && git add $f`. Then validate the live artifact (`niri validate`, the suite) before committing the merge, and push. ## 6. Verify the push actually landed `git push` output can be swallowed when wrapped in command substitution inside a batch — verify from the remote, not from the push: ```bash git fetch -q origin && printf 'HEAD=%s origin/main=%s behind=%s\n' \ "$(git rev-parse --short HEAD)" "$(git rev-parse --short origin/main)" "$(git rev-list --count HEAD..origin/main)" ``` ## 7. Close and prune - Close the issues with a comment giving merge SHA + PR link + title (numbers are per-repo; a bare `#N` is not a reference). - Prune guards, all three before `git worktree remove --force`: `ls -l /proc/*/cwd | grep -c ''` must be 0; `git -C status --porcelain | wc -l` must be 0; `git rev-list --count main..` must be 0. Check for `.AGENTTRASH/**` inside the worktree first — removal destroys the only copy. - `git branch -d` (not `-D`) once the branch is reached by main; `git push origin --delete ` is allowed on the local Gitea. - Delete the temp `-K` token config file in the same session that created it.