83 lines
5.7 KiB
Markdown
83 lines
5.7 KiB
Markdown
---
|
|
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 <one file>` 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 '<wt-name>'` must be 0; `git -C <wt> status --porcelain | wc -l` must be 0; `git rev-list --count main..<branch>` 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 <branch>` is allowed on the local Gitea.
|
|
- Delete the temp `-K` token config file in the same session that created it.
|