Skip to content

feat(0.23.0): gitignore-aware + non-destructive bootstrap (closes #67)#68

Merged
broomva merged 1 commit into
mainfrom
feat/bootstrap-gitignore-aware-safe-hooks
May 29, 2026
Merged

feat(0.23.0): gitignore-aware + non-destructive bootstrap (closes #67)#68
broomva merged 1 commit into
mainfrom
feat/bootstrap-gitignore-aware-safe-hooks

Conversation

@broomva
Copy link
Copy Markdown
Owner

@broomva broomva commented May 29, 2026

Closes #67.

Problem (found dogfooding v0.22.0 on real repos)

bstack bootstrap is built for fresh workspaces. On existing repos (stimulus, broomva.tech) it did two unsafe things + lacked one capability:

  1. Clobbered a tracked .githooks/pre-commit — replaced broomva.tech's committed multimedia-asset validator with the bstack L3 hook, even though core.hooksPath.githooks (so the bstack hook never fired). Destructive.
  2. No gitignore-awareness — scaffolded files into gitignored paths, and would silently lose the loop definitions if a repo broadly ignores .control/.
  3. No public-repo awareness — no signal that scaffolded governance is about to become public.

Fixes

install-l3-stability.sh — never clobber a tracked pre-commit

  • Git-tracked .githooks/pre-commit + no --forceskip + warn (the committed hook is authoritative).
  • --force → preserves the current hook as .pre-commit.local first, then installs (the documented recovery path now actually exists in both the untracked and forced cases).
  • Untracked hook → preserved as .pre-commit.local as before.

bootstrap.sh Phase 2.6 — gitignore reconciliation + public-repo advisory

  • Auto-adds machine-local telemetry (.control/audit/*.jsonl) to .gitignore (idempotent).
  • Warns (never auto-un-ignores) when committable substrate (arcs.yaml, rcs-parameters.toml, policy.yaml) is gitignored — a coverage gap.
  • Public-repo advisory via gh repo view --json visibility (graceful if absent): surfaces that scaffolded governance will be public so secrets get reviewed before pushing.

Verification (P11)

  • tests/gitignore-aware-bootstrap.test.sh7/7 (incl. the --force-preserves-sidecar + idempotency cases added after review).
  • canary 14/14; shellcheck clean; Phase 2.6 skips gracefully on non-git workspaces.

Cross-Review (P20)

Strata B fresh-context review: initial 7/10 PASS. It caught a real should-fix — the original --force comment lied (it overwrote a tracked hook with no sidecar, covered by no test). Both fixed in this PR: --force now creates the sidecar, and two regression assertions (--force + idempotency) were added.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Version bumped to v0.23.0
    • Enhanced bootstrap now respects tracked git hooks and safely preserves them during installation
    • Added automatic .gitignore management for machine-local telemetry files
    • Public repository advisory now displayed when scaffolded governance content may be publicly visible
  • Tests

    • New comprehensive test suite validating gitignore-aware bootstrap safety

Review Change Stack

…ootstrap (#67)

Closes the safety gaps found dogfooding v0.22.0 on existing repos (stimulus,
broomva.tech). Bootstrap was built for fresh workspaces; on repos with their own
hooks/CI/gitignore it clobbered a tracked pre-commit and lacked gitignore awareness.

Dep-Chain (P14):
- upstream: install-l3-stability.sh pre-commit logic; bootstrap.sh Phase 2;
  arcs.v1 committable substrate; gh CLI (graceful-optional).
- downstream: onboard.sh + repair.sh (call the same installer — inherit the fix);
  every workspace that runs `bstack bootstrap` on an existing repo.

Fixed:
- install-l3-stability.sh: never clobber a git-TRACKED .githooks/pre-commit
  (skip+warn; --force still moves aside). Untracked hooks still preserved as .local.

Added:
- bootstrap.sh Phase 2.6: gitignore reconciliation (auto-ignore .control/audit/*.jsonl;
  warn when committable substrate is ignored) + public-repo advisory (gh visibility).
- tests/gitignore-aware-bootstrap.test.sh (5/5).

Verified: new test 5/5; canary 14/14; shellcheck clean. VERSION 0.22.0→0.23.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR increments the version to 0.23.0 and addresses issue #67 by implementing two interconnected safety improvements: preventing install-l3-stability.sh from overwriting tracked .githooks/pre-commit files while safely preserving untracked hooks, and adding Phase 2.6 to bootstrap.sh to reconcile .gitignore with committable vs. machine-local control-loop files and surface public-repo visibility.

Changes

Bootstrap hook and gitignore safety

Layer / File(s) Summary
Pre-commit hook installation safety
scripts/install-l3-stability.sh
Detects existing L3 rate-gate hooks and skips reinstallation; checks git-tracked status and skips overwriting committed hooks when --force=0; preserves untracked hooks as .githooks/pre-commit.local before installing the L3 hook template.
Bootstrap Phase 2.6: gitignore reconciliation & public-repo advisory
scripts/bootstrap.sh
Adds Phase 2.6 to idempotently ensure machine-local telemetry globs (.control/audit/*.jsonl) are in .gitignore, warns when committable substrate files (.control/arcs.yaml, .control/policy.yaml, etc.) are gitignored, and emits a conditional public-repo advisory when detected via gh CLI.
Gitignore-aware bootstrap and hook safety tests
tests/gitignore-aware-bootstrap.test.sh
Test harness covering tracked vs. untracked .githooks/pre-commit preservation (with and without --force), idempotent .gitignore audit-log entries, and warnings for ignored committable substrates.
Release v0.23.0 metadata
VERSION, CHANGELOG.md
Version bumped from 0.22.0 to 0.23.0; changelog documents tracked hook preservation, Phase 2.6 gitignore behavior, public-repo detection, and test coverage additions.

🎯 2 (Simple) | ⏱️ ~12 minutes

🐰 A bootstrap that's wise and thoughtful and kind,
Won't clobber the hooks that the team left behind—
With gitignore checks and a warning so clear,
Now public repos know when the world might see here! 🌿✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: gitignore-aware bootstrap and non-destructive pre-commit hook installation, directly reflecting the core updates across all modified files.
Linked Issues check ✅ Passed All primary coding requirements from #67 are met: tracked hook preservation in install-l3-stability.sh, gitignore reconciliation in bootstrap.sh Phase 2.6, public-repo detection, and comprehensive test coverage via gitignore-aware-bootstrap.test.sh.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #67: VERSION bump for release coordination, CHANGELOG documentation, and implementation of the three core fixes without unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bootstrap-gitignore-aware-safe-hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/bootstrap.sh`:
- Around line 168-171: The LOCAL_GLOBS array currently contains only
".control/audit/*.jsonl" and should also include ".claude/settings.json" so
user-specific Claude settings are treated as machine-local; update the
LOCAL_GLOBS definition (the LOCAL_GLOBS variable) to add the
".claude/settings.json" entry alongside ".control/audit/*.jsonl" so the
bootstrap flow ignores that file as intended.
- Around line 201-203: The gh check is running in the caller's cwd instead of
the intended workspace, so change the gh invocation to run from the workspace
directory (use the existing WORKSPACE_DIR/BROOMVA_WORKSPACE variable) — e.g. run
gh repo view inside a subshell that cds into "$WORKSPACE_DIR" (or use an
equivalent cd && gh pattern) so the gh repo view --json visibility --jq
.visibility call resolves the correct repository.

In `@scripts/install-l3-stability.sh`:
- Around line 110-113: The current move of "$PRE_COMMIT" to
"$WORKSPACE/.githooks/pre-commit.local" (inside the DRY_RUN check) will clobber
any existing .pre-commit.local; update the block that runs when DRY_RUN=0 to
detect an existing target and avoid overwrite — e.g., if
"$WORKSPACE/.githooks/pre-commit.local" exists, create a unique backup name
(append a timestamp or incrementing suffix) or fail with a clear message, then
mv "$PRE_COMMIT" to that unique path and chmod +x the new file; reference the
variables/commands PRE_COMMIT, WORKSPACE, .githooks/pre-commit.local, DRY_RUN,
mv and chmod when making the change.
- Around line 87-91: The current skip branch checks only for the "L3 rate gate"
marker and prevents overwriting even when the script was invoked with --force;
update the conditional that guards the skip so it respects the force flag (e.g.,
a parsed variable like FORCE or FORCE_FLAG). Concretely, modify the if in the
pre-commit check (the block referencing PRE_COMMIT, "L3 rate gate", SKIPPED) to
also require that the force flag is not set (for example: && [ "$FORCE" != "1" ]
or similar) before treating the hook as idempotent; this preserves the skip
behavior normally but allows --force to overwrite existing L3 hook content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34937def-891c-4fbc-883b-0b05add5e84f

📥 Commits

Reviewing files that changed from the base of the PR and between ca46568 and 3967ea9.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • VERSION
  • scripts/bootstrap.sh
  • scripts/install-l3-stability.sh
  • tests/gitignore-aware-bootstrap.test.sh

Comment thread scripts/bootstrap.sh
Comment on lines +168 to +171
# Machine-local globs that should always be ignored (absolute paths / runtime telemetry).
LOCAL_GLOBS=(".control/audit/*.jsonl")
# Committable substrate the loop needs — warn if a repo ignores these.
COMMITTABLE_FILES=(".control/arcs.yaml" ".control/rcs-parameters.toml" ".control/policy.yaml")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Also reconcile .claude/settings.json as machine-local.

Phase 2.6 only adds .control/audit/*.jsonl, but the machine-local set for this flow also includes .claude/settings.json. Without adding it here, bootstrap still leaves a user-specific settings file committable in a fresh repo.

Suggested fix
-LOCAL_GLOBS=(".control/audit/*.jsonl")
+LOCAL_GLOBS=(".claude/settings.json" ".control/audit/*.jsonl")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Machine-local globs that should always be ignored (absolute paths / runtime telemetry).
LOCAL_GLOBS=(".control/audit/*.jsonl")
# Committable substrate the loop needs — warn if a repo ignores these.
COMMITTABLE_FILES=(".control/arcs.yaml" ".control/rcs-parameters.toml" ".control/policy.yaml")
# Machine-local globs that should always be ignored (absolute paths / runtime telemetry).
LOCAL_GLOBS=(".claude/settings.json" ".control/audit/*.jsonl")
# Committable substrate the loop needs — warn if a repo ignores these.
COMMITTABLE_FILES=(".control/arcs.yaml" ".control/rcs-parameters.toml" ".control/policy.yaml")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bootstrap.sh` around lines 168 - 171, The LOCAL_GLOBS array currently
contains only ".control/audit/*.jsonl" and should also include
".claude/settings.json" so user-specific Claude settings are treated as
machine-local; update the LOCAL_GLOBS definition (the LOCAL_GLOBS variable) to
add the ".claude/settings.json" entry alongside ".control/audit/*.jsonl" so the
bootstrap flow ignores that file as intended.

Comment thread scripts/bootstrap.sh
Comment on lines +201 to +203
if command -v gh >/dev/null 2>&1; then
_vis=$(gh repo view --json visibility --jq .visibility 2>/dev/null || true)
if [ "$_vis" = "PUBLIC" ]; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate and print the relevant section of scripts/bootstrap.sh (around lines 180-230)
if [ ! -f scripts/bootstrap.sh ]; then
  echo "Missing scripts/bootstrap.sh"
  exit 1
fi

echo "== scripts/bootstrap.sh (lines 160-240) =="
nl -ba scripts/bootstrap.sh | sed -n '160,240p'

# 2) Print where WORKSPACE_DIR is defined and how it's used (if present)
echo
echo "== WORKSPACE_DIR occurrences =="
rg -n "WORKSPACE_DIR" scripts/bootstrap.sh || true

# 3) Detect any prior cd into workspace (before the gh call)
echo
echo "== cd commands before the gh call section =="
# Print lines up to ~230 and look for cd
nl -ba scripts/bootstrap.sh | sed -n '1,240p' | rg -n "^[[:space:]]*[0-9]+[[:space:]]+cd[[:space:]]" || true

Repository: broomva/bstack

Length of output: 163


🌐 Web query:

gh repo view no repository argument targets current directory behavior

💡 Result:

When you run the command gh repo view without any arguments, the GitHub CLI automatically detects and displays the repository associated with your current working directory [1]. The command uses your local Git configuration to identify the remote repository corresponding to your current directory and retrieves its information—such as the owner, repository name, description, and README—from GitHub [1]. If the current directory is not part of a Git repository, the command will be unable to identify the target and will return an error [2]. Key details regarding this behavior: - Scope: It defaults to the repository linked to the current directory [1]. - Default Branch: By default, gh repo view displays information from the repository's default branch [3]. - No Arguments: Providing no arguments is the standard way to view the repository associated with your current project context, avoiding the need to manually type the owner/repo path [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show scripts/bootstrap.sh around lines 190-215 (avoid nl) =="
if [ ! -f scripts/bootstrap.sh ]; then
  echo "Missing scripts/bootstrap.sh"
  exit 1
fi

# Print with line numbers via awk
awk 'NR>=190 && NR<=215 { printf "%5d  %s\n", NR, $0 }' scripts/bootstrap.sh

echo
echo "== Find gh repo view usages =="
rg -n "gh repo view" scripts/bootstrap.sh || true

echo
echo "== Find WORKSPACE_DIR definition/usage =="
rg -n "WORKSPACE_DIR" scripts/bootstrap.sh || true

echo
echo "== List cd commands before/near the gh repo view call =="
# Print first 250 lines with grep for "cd " lines
awk 'NR<=250 {print NR ":" $0}' scripts/bootstrap.sh | rg -n "^[0-9]+:.*\bcd\b" || true

Repository: broomva/bstack

Length of output: 2878


Resolve visibility from $WORKSPACE_DIR, not the caller’s cwd.

gh repo view with no repository argument uses the current working directory; since this script’s WORKSPACE_DIR can be overridden via BROOMVA_WORKSPACE but the advisory doesn’t cd into it (unlike other git -C "$WORKSPACE_DIR" calls), the PUBLIC/private advisory can come from the wrong repo. (cli.github.com)

Suggested fix
-        _vis=$(gh repo view --json visibility --jq .visibility 2>/dev/null || true)
+        _vis=$(cd "$WORKSPACE_DIR" && gh repo view --json visibility --jq .visibility 2>/dev/null || true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if command -v gh >/dev/null 2>&1; then
_vis=$(gh repo view --json visibility --jq .visibility 2>/dev/null || true)
if [ "$_vis" = "PUBLIC" ]; then
if command -v gh >/dev/null 2>&1; then
_vis=$(cd "$WORKSPACE_DIR" && gh repo view --json visibility --jq .visibility 2>/dev/null || true)
if [ "$_vis" = "PUBLIC" ]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bootstrap.sh` around lines 201 - 203, The gh check is running in the
caller's cwd instead of the intended workspace, so change the gh invocation to
run from the workspace directory (use the existing
WORKSPACE_DIR/BROOMVA_WORKSPACE variable) — e.g. run gh repo view inside a
subshell that cds into "$WORKSPACE_DIR" (or use an equivalent cd && gh pattern)
so the gh repo view --json visibility --jq .visibility call resolves the correct
repository.

Comment on lines +87 to +91
if [ -f "$PRE_COMMIT" ] && grep -q "L3 rate gate" "$PRE_COMMIT" 2>/dev/null; then
# Already our hook — idempotent no-op (even under --force; reinstalling
# identical content is pointless).
echo " [skip] .githooks/pre-commit (already bstack L3 hook)"
SKIPPED=$((SKIPPED + 1))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor --force when the existing file is already the L3 hook.

This branch skips solely on the marker string, so a workspace that already has the L3 hook can never receive template updates via --force. That breaks the script’s overwrite contract and leaves existing installs stuck on older hook content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install-l3-stability.sh` around lines 87 - 91, The current skip
branch checks only for the "L3 rate gate" marker and prevents overwriting even
when the script was invoked with --force; update the conditional that guards the
skip so it respects the force flag (e.g., a parsed variable like FORCE or
FORCE_FLAG). Concretely, modify the if in the pre-commit check (the block
referencing PRE_COMMIT, "L3 rate gate", SKIPPED) to also require that the force
flag is not set (for example: && [ "$FORCE" != "1" ] or similar) before treating
the hook as idempotent; this preserves the skip behavior normally but allows
--force to overwrite existing L3 hook content.

Comment on lines +110 to +113
echo " [info] existing .githooks/pre-commit found — preserving as .pre-commit.local"
if [ "$DRY_RUN" = "0" ]; then
mv "$PRE_COMMIT" "$WORKSPACE/.githooks/pre-commit.local"
chmod +x "$WORKSPACE/.githooks/pre-commit.local"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't overwrite an existing .pre-commit.local sidecar.

mv will clobber any existing .githooks/pre-commit.local, so a later forced install can destroy the previously preserved hook. Please guard this case or write to a unique backup name instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install-l3-stability.sh` around lines 110 - 113, The current move of
"$PRE_COMMIT" to "$WORKSPACE/.githooks/pre-commit.local" (inside the DRY_RUN
check) will clobber any existing .pre-commit.local; update the block that runs
when DRY_RUN=0 to detect an existing target and avoid overwrite — e.g., if
"$WORKSPACE/.githooks/pre-commit.local" exists, create a unique backup name
(append a timestamp or incrementing suffix) or fail with a clear message, then
mv "$PRE_COMMIT" to that unique path and chmod +x the new file; reference the
variables/commands PRE_COMMIT, WORKSPACE, .githooks/pre-commit.local, DRY_RUN,
mv and chmod when making the change.

@broomva broomva merged commit ec27482 into main May 29, 2026
6 checks passed
@broomva broomva deleted the feat/bootstrap-gitignore-aware-safe-hooks branch May 29, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bootstrap: gitignore-awareness + public-repo detection + don't clobber tracked pre-commit

1 participant