@@ -14,17 +14,41 @@ set -euo pipefail
1414#
1515# --dry-run Print what would be removed without actually removing anything
1616#
17+ # What this script protects:
18+ # develop is private and messy; main is the public branch that ships
19+ # outside the door. This script is the automated safety net that keeps
20+ # private material off main. It never modifies develop — it only runs
21+ # on main (or a detached HEAD for testing) to ensure the main tree is
22+ # stripped before a merge commit lands.
23+ #
1724# Safe merge flow (develop → main):
1825# 1. Commit/push all work on develop
1926# 2. git checkout main
20- # 3. git merge develop --no-commit
21- # 4. ./scripts/strip_for_main.sh
22- # 5. git add -A
27+ # 3. git merge develop --no-commit (may conflict on modify/delete)
28+ # 4. ./scripts/strip_for_main.sh (resolves strip-target conflicts
29+ # by deleting + staging together;
30+ # see "Mid-merge mode" below)
31+ # 5. git add -A (stage any remaining clean merges)
2332# 6. git commit -m "merge: develop → main (stripped dev files)"
2433#
2534# Safety checks:
26- # - Refuses to run on the develop branch (must be on main or detached)
27- # - Refuses to run with uncommitted changes unless --dry-run
35+ # - ABSOLUTE: refuses to run on the develop branch. No exceptions.
36+ # develop is the source of truth for dev artifacts; stripping there
37+ # would destroy work. See guard below.
38+ # - Outside a merge: refuses to run with uncommitted changes. Mixing
39+ # strips with unrelated edits in one commit would destroy the audit
40+ # trail of what-got-stripped-when.
41+ # - Mid-merge (.git/MERGE_HEAD exists): the dirty-tree check is skipped
42+ # because a --no-commit merge always leaves a dirty tree. The
43+ # "refuses on develop" check is still enforced — we can never be
44+ # mid-merge ON develop (develop is the source, not the target).
45+ #
46+ # Mid-merge mode:
47+ # When .git/MERGE_HEAD exists, strip_path uses `git rm -rf` so modify/
48+ # delete conflicts (a file deleted on main but modified on develop) are
49+ # resolved by keeping the deletion AND staging the resolution in one
50+ # step. Without this, the script would delete the file but git would
51+ # still see it as conflicted, blocking the commit.
2852#
2953# What it removes:
3054# - Internal tracking: MEMORY.md, MEMORY_ARCHIVE.md, FLOW.md, TECH-DEBT.md,
@@ -55,40 +79,89 @@ if [[ "${1:-}" == "--dry-run" ]]; then
5579else
5680 echo " === strip_for_main.sh ==="
5781fi
82+
83+ # Detect mid-merge state. MERGE_HEAD exists only between `git merge --no-commit`
84+ # (or a conflict pause) and the eventual merge commit. When this is set we
85+ # know the dirty working tree is expected from the merge, not from random edits.
86+ GIT_DIR=$( git rev-parse --git-dir 2> /dev/null || echo " .git" )
87+ IN_MERGE=0
88+ if [[ -f " $GIT_DIR /MERGE_HEAD" ]]; then
89+ IN_MERGE=1
90+ echo " (mid-merge mode: MERGE_HEAD present)"
91+ fi
5892echo " "
5993
60- # Safety: refuse real runs on develop (dry-run is fine — it doesn't touch files)
94+ # Safety: ABSOLUTE refusal to run on develop. This guard exists no matter
95+ # what mode or flag is passed, and no matter whether a merge is in progress.
96+ # develop is the source of dev artifacts; stripping there would destroy
97+ # work. A mid-merge on develop is also impossible in the documented flow
98+ # (merges go FROM develop INTO main, not the other way), so reaching this
99+ # condition means something has gone badly wrong and we must abort.
61100BRANCH=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo " unknown" )
62101if [[ $DRY_RUN -eq 0 && " $BRANCH " == " develop" ]]; then
63102 echo " ERROR: refusing to run on 'develop' branch."
64- echo " This script strips dev files — run it only on 'main' (or a detached HEAD for a "
65- echo " temporary test). On develop you'd lose your tracked dev files."
103+ echo " This script strips dev files — run it only on 'main' (or a detached HEAD"
104+ echo " for a temporary test). On develop you'd lose your tracked dev files."
66105 echo " "
67106 echo " Safe flow: checkout main, merge develop --no-commit, then run this script."
68107 echo " To preview from develop: ./scripts/strip_for_main.sh --dry-run"
69108 exit 1
70109fi
71110
72- # Safety: refuse real runs with uncommitted changes
73- if [[ $DRY_RUN -eq 0 ]] && ! git diff --quiet HEAD -- 2> /dev/null; then
74- echo " ERROR: uncommitted changes detected. Commit or stash first."
75- echo " (Running the strip with dirty state would mix strips with other edits.)"
111+ # Safety: refuse dirty tree UNLESS we're in the middle of a merge. A merge
112+ # always leaves a dirty tree (staged adds, conflicted modifies), and the
113+ # documented flow runs the strip right after `git merge --no-commit`. Outside
114+ # a merge, a dirty tree means unrelated edits — stripping would mix them in.
115+ if [[ $DRY_RUN -eq 0 && $IN_MERGE -eq 0 ]] && ! git diff --quiet HEAD -- 2> /dev/null; then
116+ echo " ERROR: uncommitted changes detected (and we are NOT mid-merge)."
117+ echo " Commit or stash first. Running the strip with a dirty state would"
118+ echo " mix strips with unrelated edits in one commit, destroying the audit"
119+ echo " trail of what-got-stripped-when."
76120 echo " "
77121 echo " Override: pass --dry-run to preview without stripping."
78122 exit 1
79123fi
80124
81125STRIPPED=0
82126
127+ # strip_path removes $1 from the working tree. Behavior:
128+ # - dry run : only prints what would be removed
129+ # - mid-merge : uses `git rm -rf` so modify/delete conflicts are
130+ # resolved (deleted AND staged as resolved) in one
131+ # step. This is what makes the merge flow work.
132+ # - normal run : uses `rm -rf` (classic behavior — works on tracked
133+ # and untracked paths alike; any resulting deletions
134+ # get staged by the caller's `git add -A`).
83135strip_path () {
84136 local path=" $1 "
85- if [[ -e " $path " ]]; then
86- if [[ $DRY_RUN -eq 1 ]]; then
137+ if [[ ! -e " $path " ]] && [[ $IN_MERGE -eq 0 ]]; then
138+ return 0
139+ fi
140+ if [[ $DRY_RUN -eq 1 ]]; then
141+ if [[ -e " $path " ]]; then
87142 echo " would remove: $path "
88- else
89- rm -rf " $path "
90- echo " removed: $path "
143+ STRIPPED=$(( STRIPPED + 1 ))
144+ fi
145+ return 0
146+ fi
147+ if [[ $IN_MERGE -eq 1 ]]; then
148+ # --ignore-unmatch: quiet no-op if path doesn't exist (covers the case
149+ # where the path was already deleted on main and develop also deleted
150+ # it, leaving nothing to strip).
151+ if git rm -rf --ignore-unmatch " $path " > /dev/null 2>&1 ; then
152+ if [[ -e " $path " ]]; then
153+ # git rm failed to touch it (e.g. untracked in merge state) — fall
154+ # back to plain rm and stage later via git add -A.
155+ rm -rf " $path "
156+ echo " removed (untracked): $path "
157+ else
158+ echo " removed (git rm): $path "
159+ fi
160+ STRIPPED=$(( STRIPPED + 1 ))
91161 fi
162+ else
163+ rm -rf " $path "
164+ echo " removed: $path "
92165 STRIPPED=$(( STRIPPED + 1 ))
93166 fi
94167}
@@ -110,6 +183,7 @@ strip_path ".claude"
110183strip_path " .agents"
111184strip_path " audit"
112185strip_path " adr"
186+ strip_path " .vscode" # editor-specific settings (Snyk IDE prefs, etc.)
113187
114188# Internal reports and utilities
115189strip_path " generate_pdf.py"
0 commit comments