You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
upsert_gitignore_baseline in scripts/lib/gitignore-baseline.sh (shipped in #798) prepends the marker-wrapped L1 secrets baseline and keeps the target's existing content below the END marker as "L2". That is correct for a greenfield .gitignore, but the fleet dry-run for #800 proved it ships a correctness regression on repos that already contain secret patterns.
Verified failures (fleet dry-run, 2026-07-19)
Reproduced with git check-ignore on the upsert output of live repo .gitignores:
markets and google-app-scripts: after upsert, public.pem becomes IGNORED. Their ad-hoc .gitignore has a broad *.pem with no !public.pem; sitting below END as L2, it re-hides the file the L1 block re-allows via !public.pem. The baseline explicitly warns: "a later ignore silently re-hides a previously-negated file."
TalkTerm / bmad-bgreat-suite / ContentTwin: carry the old unmarkered baseline, so upsert duplicates the whole 398-line block (TalkTerm 434 → 831 lines; .env, *.pem twice).
This blocks the #800 fleet back-fill — applying as-is would gitignore legitimate public keys and produce 800-line files.
Required behavior
Baseline negations must win. When the target's L2 content contains a broad ignore that would re-hide a baseline-negated path (!public.pem, !.env.example, !*.crt, !ca.crt, !*.pub, and the other ! re-allows in the block), neutralize it — strip the conflicting L2 line, or re-emit the baseline negations after L2 so they evaluate last.
Migrate, don't duplicate. Detect an existing unmarkered baseline in the target and fold it into the single marker-wrapped block instead of prepending a second full copy. Genuine ecosystem/OS L2 entries are preserved.
Acceptance / required tests (bats)
markets-like input (*.pem, no !public.pem) → after upsert, in a temp git init dir, git check-ignore public.pem is NOT ignored (exit 1).
google-app-scripts-like input → same.
TalkTerm-like input (full unmarkered baseline) → exactly one BEGIN petry-projects marker; no duplicated .env / *.pem; genuine L2 preserved.
Make upsert_gitignore_baseline neutralize L2 content that conflicts with the L1 block by de-duplicating L2 lines against the block — a single mechanism that both lets baseline negations win (strips re-hiding broad patterns like a bare *.pem) and folds an existing unmarkered baseline into the one marker-wrapped block instead of duplicating it.
Root cause
L2 (everything below END) is preserved verbatim and evaluated after the block. A bare *.pem in L2 re-hides public.pem that the block re-allows via !public.pem; a full unmarkered old baseline in L2 gets duplicated on upsert.
Implementation Checklist
scripts/lib/gitignore-baseline.sh: add _gib_neutralize_l2 helper that strips any L2 line (CR-stripped, trailing-ws-trimmed) that exactly matches a non-blank line of the block, and squeezes the resulting blank-line runs.
Apply the helper to L2 in the marker-less insert path (block on top, existing → L2).
Apply the helper to the post/L2 region in the replace-in-place path.
Skip emitting L2 entirely when it neutralizes to empty (keeps brand-new-style output; preserves idempotency).
Tests to Write (test/scripts/lib/gitignore-baseline.bats)
markets-like L2 (*.pem, no !public.pem) → in a temp git init dir, git check-ignore public.pem exits 1 (NOT ignored).
google-app-scripts-like L2 → same.
TalkTerm-like L2 (full unmarkered baseline copy) → exactly one BEGIN marker, .env/*.pem appear once, genuine ecosystem L2 (node_modules/, *.log) preserved.
Negation-win + migration still idempotent on second upsert.
./node_modules/.bin/bats test/scripts/lib/gitignore-baseline.bats (full gate: also test/scripts/sync-gitignore-baseline/ and tests/test_seed_repo_template_gitignore.bats), plus bash .dev-lead/scripts/dev-lead-lint.sh.
Added _gib_neutralize_l2 helper to scripts/lib/gitignore-baseline.sh
Wired it into the marker-less INSERT path and the marker-present REPLACE-in-place path
Skip emitting L2 when it neutralizes to empty (preserves brand-new-style output + idempotency)
Wrote the required bats: markets / google-app-scripts negation-win, TalkTerm migration, idempotency
Full gate suite + bootstrap/seed suites + shellcheck + dev-lead-lint all green
Approach
L2 (everything below END) is evaluated by git after the L1 block, so a bare *.pem
kept in L2 re-hid the public.pem the block re-allows, and an old unmarkered baseline
pasted into L2 got duplicated on upsert. Fix: _gib_neutralize_l2 drops any L2 line
that exactly matches a pattern line of the block (comments/blank lines are ignored —
the block's bare # separators must not clobber a repo's own comments). This single
mechanism makes baseline negations win (the re-hiding duplicate is removed; the block
still ignores real secrets and its !public.pem is no longer overridden) and folds a
pasted unmarkered baseline into the one marker block. Genuine ecosystem/OS L2 is preserved;
blank runs left behind are squeezed. Idempotent.
Test Results
1..23
ok 1 gib_extract_baseline_block pulls the BEGIN..END block inclusive
ok 2 gib_extract_baseline_block fails when no markers are present
ok 3 gib_extract_baseline_block works on the real repo /.gitignore (anchors present)
ok 4 absent: inserts the block on top, existing content becomes L2 below END
ok 5 absent + empty existing: output is exactly the block
ok 6 present: replaces a stale block between markers, leaving L2 intact
ok 7 idempotent: upserting an already-upserted file is a no-op
ok 8 negations survive verbatim and stay immediately after their broad pattern
ok 9 replace does not drop a negation the stale block already carried
ok 10 upsert fails on a file with only BEGIN marker (half-open state)
ok 11 upsert fails on a file with only END marker (half-open state)
ok 12 #809 markets-like: broad *.pem in L2 does not re-hide baseline-negated public.pem
ok 13 #809 google-app-scripts-like: broad *.pem in L2 does not re-hide public.pem
ok 14 #809 TalkTerm-like: unmarkered baseline in L2 is folded into one block, not duplicated
ok 15 #809 negation-win + migration is idempotent
ok 16 dry-run plans a PR to INSERT the baseline into a repo missing .gitignore
ok 17 dry-run skips a repo whose .gitignore already carries the current baseline
ok 18 dry-run plans a PR for a marker-less .gitignore (INSERT, L2 preserved)
ok 19 errors when the canonical baseline is unreadable
ok 20 template .gitignore leads with the marker-wrapped L1 block
ok 21 template .gitignore passes the STORY3 anchor check (.env, *.pem, *.key present)
ok 22 template .gitignore keeps its L2 ecosystem entries below the END marker
ok 23 template .gitignore does not re-ignore a baseline-negated path (no bare .env.* in L2)
Files Changed
scripts/lib/gitignore-baseline.sh: added _gib_neutralize_l2; neutralize L2 in both upsert paths; updated behavior docs.
Only pattern lines are de-duplicated, never comments — this both avoids the block's bare # separators clobbering a repo's own comment lines and keeps the Story: SEED + DISTRIBUTE — baseline into repo-template, bootstrap, and deploy sync #798 "already carries the baseline → skip" sync path (which relies on exact idempotency) working. Comment residue from a migrated old baseline is left in place: harmless (comments don't affect git) and the functional duplication the issue flagged is fully removed.
Neutralization is applied in the marker-present REPLACE path too (not just the two failing cases), so requirement 1 holds regardless of marker state.
No git operations performed — CI handles commit/PR.
Activity
don-petry commentedon Jul 19, 2026
Dev-Lead Implementation Plan
Issue: #809 — upsert_gitignore_baseline must preserve baseline negations + migrate unmarkered baselines (blocks #800 apply)
Scope
Make
upsert_gitignore_baselineneutralize L2 content that conflicts with the L1 block by de-duplicating L2 lines against the block — a single mechanism that both lets baseline negations win (strips re-hiding broad patterns like a bare*.pem) and folds an existing unmarkered baseline into the one marker-wrapped block instead of duplicating it.Root cause
L2 (everything below END) is preserved verbatim and evaluated after the block. A bare
*.pemin L2 re-hidespublic.pemthat the block re-allows via!public.pem; a full unmarkered old baseline in L2 gets duplicated on upsert.Implementation Checklist
scripts/lib/gitignore-baseline.sh: add_gib_neutralize_l2helper that strips any L2 line (CR-stripped, trailing-ws-trimmed) that exactly matches a non-blank line of the block, and squeezes the resulting blank-line runs.Tests to Write (
test/scripts/lib/gitignore-baseline.bats)markets-like L2 (*.pem, no!public.pem) → in a tempgit initdir,git check-ignore public.pemexits 1 (NOT ignored).google-app-scripts-like L2 → same.TalkTerm-like L2 (full unmarkered baseline copy) → exactly one BEGIN marker,.env/*.pemappear once, genuine ecosystem L2 (node_modules/,*.log) preserved.Test Command
./node_modules/.bin/bats test/scripts/lib/gitignore-baseline.bats(full gate: alsotest/scripts/sync-gitignore-baseline/andtests/test_seed_repo_template_gitignore.bats), plusbash .dev-lead/scripts/dev-lead-lint.sh.don-petry commentedon Jul 19, 2026
Dev-Lead: Implementation Complete
Plan Execution
_gib_neutralize_l2helper toscripts/lib/gitignore-baseline.shApproach
L2 (everything below END) is evaluated by git after the L1 block, so a bare
*.pemkept in L2 re-hid the
public.pemthe block re-allows, and an old unmarkered baselinepasted into L2 got duplicated on upsert. Fix:
_gib_neutralize_l2drops any L2 linethat exactly matches a pattern line of the block (comments/blank lines are ignored —
the block's bare
#separators must not clobber a repo's own comments). This singlemechanism makes baseline negations win (the re-hiding duplicate is removed; the block
still ignores real secrets and its
!public.pemis no longer overridden) and folds apasted unmarkered baseline into the one marker block. Genuine ecosystem/OS L2 is preserved;
blank runs left behind are squeezed. Idempotent.
Test Results
Files Changed
scripts/lib/gitignore-baseline.sh: added_gib_neutralize_l2; neutralize L2 in both upsert paths; updated behavior docs.test/scripts/lib/gitignore-baseline.bats: added upsert_gitignore_baseline must preserve baseline negations + migrate unmarkered baselines (blocks #800 apply) #809 tests (markets/google-app-scripts negation-win viagit check-ignore, TalkTerm migration, negation-win+migration idempotency) with a_gib_check_ignoredhelper.Notes
#separators clobbering a repo's own comment lines and keeps the Story: SEED + DISTRIBUTE — baseline into repo-template, bootstrap, and deploy sync #798 "already carries the baseline → skip" sync path (which relies on exact idempotency) working. Comment residue from a migrated old baseline is left in place: harmless (comments don't affect git) and the functional duplication the issue flagged is fully removed.test: feed the post-#809 upsert steady state to the already-current f…
feat: implement issue #809 — upsert_gitignore_baseline must preserve …