From 79a73baa9b72e9331f8279aa516e662208bd693c Mon Sep 17 00:00:00 2001 From: "Carlos D. Escobar-Valbuena" Date: Fri, 5 Jun 2026 10:38:01 -0500 Subject: [PATCH 1/3] feat(0.25.0): doctor advisory + repair backfill for Development Philosophy section (BRO-1409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the gap from 0.24.0: idempotent-never-overwrite means newly-templated *content* never reaches existing workspaces, so the Development Philosophy section only shipped to new installs. - doctor.sh §4b: informational advisory when AGENTS.md lacks the section. Not a GAP, never fails --strict (mirrors §12 Pillars / §13 dogfood). - repair.sh backfill_philosophy_section(): extracts the section verbatim from the template (files only, no shell interpolation of content) and inserts it before the target's '## Bstack Core Automation Primitives' anchor in both AGENTS.md + CLAUDE.md. Runs before the compliance early-exit (like the hook merge). Idempotent + insert-only; skips with a warning if anchor absent. - tests/philosophy-backfill.test.sh: 13 assertions (backfill, position, content integrity, idempotency, --dry-run, missing-anchor, doctor advisory). - new-workspace-flow.md documents §4b + backfill. Primitive count unchanged (20). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++ VERSION | 2 +- references/new-workspace-flow.md | 4 +- scripts/doctor.sh | 16 +++ scripts/repair.sh | 64 ++++++++++ tests/philosophy-backfill.test.sh | 196 ++++++++++++++++++++++++++++++ 6 files changed, 299 insertions(+), 2 deletions(-) create mode 100755 tests/philosophy-backfill.test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 25bf215..b610689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.25.0 — 2026-06-05 + +### feat: doctor advisory + repair backfill for the Development Philosophy section (BRO-1409) + +Follow-up to 0.24.0 (BRO-1406). The scaffold's idempotent-never-overwrite policy means newly-templated *content* never reaches existing workspaces — so the Development Philosophy section shipped only to new installs. This closes that gap for the section: `bstack doctor` surfaces it and `bstack repair` backfills it. + +### Changed + +- **`scripts/doctor.sh`** — new §4b advisory: if `AGENTS.md` lacks `## Development Philosophy`, print an `[info]` nudge to run `bstack repair`. Informational only — a pre-0.24.0 workspace legitimately lacks it; it is **not** a GAP and does **not** fail `--strict` (mirrors the §12 Pillars / §13 dogfood convention). +- **`scripts/repair.sh`** — new `backfill_philosophy_section()`: extracts the section verbatim from the template (heading → `## Bstack Core Automation Primitives` anchor, exclusive) via files only (no shell interpolation of content), and inserts it before the target's own anchor line in both `AGENTS.md` and `CLAUDE.md`. Runs before the "fully bstack-compliant" early-exit (same pattern as the hook merge), since the advisory is not a GAP. Idempotent (skips if present) and non-destructive (insert-only; skips with a warning if the anchor is absent). Honors `--dry-run` / `--apply-all`. +- **`tests/philosophy-backfill.test.sh`** — new: backfill + position + content-integrity + idempotency + `--dry-run` + missing-anchor + doctor-advisory (13 assertions). +- **`references/new-workspace-flow.md`** — documents §4b + the repair backfill. + +### Notes + +- Primitive count unchanged (**20**). Governance-substrate tooling, not a P-row. +- Backfill targets *content* gaps the never-overwrite scaffold skips — a general pattern (the hook merge does the same for `.claude/settings.json`). +- `VERSION` 0.24.0 → 0.25.0. + ## 0.24.0 — 2026-06-05 ### feat: scaffold a Development Philosophy section into AGENTS.md/CLAUDE.md on install (BRO-1406) diff --git a/VERSION b/VERSION index 2094a10..d21d277 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.24.0 +0.25.0 diff --git a/references/new-workspace-flow.md b/references/new-workspace-flow.md index 923c18c..5a5d0a1 100644 --- a/references/new-workspace-flow.md +++ b/references/new-workspace-flow.md @@ -36,7 +36,7 @@ Both paths wire the RCS control loop. `bootstrap.sh` scaffolds governance files ## What `bstack doctor` reports -§1–§13 v0.13.0 substrate checks · §14 RCS λ compute + drift · §15 G0/G1/G2 wiring · §16 L0 tool-call audit summary · §17 L1 reflex compliance · §18 L2 promotion throttle · §19 multi-layer composite health (`L0=stable L1=stable L2=stable L3=stable` form) · §20 federation registry · §21 closure-contract arcs · §22 composite-ω drift trend · **§23 control-loop closure verdict** — the single "is the loop wired + connected + running?" answer (substrate-absent / wired-but-idle / wired+running+closing). New workspaces show §16–§18 + §23 as informational ("no audit log yet" / "wired but idle") until first events fire; For CI lanes that must fail on an idle loop, run `BSTACK_LOOP_STRICT=1 doctor.sh --strict` — `BSTACK_LOOP_STRICT=1` records the gap but only `--strict` changes the exit code, so **both** are required. +§1–§13 v0.13.0 substrate checks · **§4b Development Philosophy advisory** (informational since 0.25.0 — flags an AGENTS.md that predates the 0.24.0 templated section; backfill with `bstack repair`; never a GAP, never fails `--strict`) · §14 RCS λ compute + drift · §15 G0/G1/G2 wiring · §16 L0 tool-call audit summary · §17 L1 reflex compliance · §18 L2 promotion throttle · §19 multi-layer composite health (`L0=stable L1=stable L2=stable L3=stable` form) · §20 federation registry · §21 closure-contract arcs · §22 composite-ω drift trend · **§23 control-loop closure verdict** — the single "is the loop wired + connected + running?" answer (substrate-absent / wired-but-idle / wired+running+closing). New workspaces show §16–§18 + §23 as informational ("no audit log yet" / "wired but idle") until first events fire; For CI lanes that must fail on an idle loop, run `BSTACK_LOOP_STRICT=1 doctor.sh --strict` — `BSTACK_LOOP_STRICT=1` records the gap but only `--strict` changes the exit code, so **both** are required. ## Common gotchas @@ -50,6 +50,8 @@ Both paths wire the RCS control loop. `bootstrap.sh` scaffolds governance files `bstack onboard --force` redoes the wizard. `bstack repair` detects missing pieces (G0/G1/G2 hooks, audit dir, parameters.toml) and re-runs the relevant installer. Both are idempotent — existing files are preserved unless `--force` is passed; settings.json merges are structurally idempotent via `_bstack_primitive` markers. +`bstack repair` also **backfills newly-templated *content*** into existing governance files where the scaffold's never-overwrite policy would otherwise skip it. Since 0.25.0 it inserts the `## Development Philosophy` section (templated in 0.24.0) into a pre-existing `AGENTS.md`/`CLAUDE.md` — runs before the "fully bstack-compliant" early-exit (like the hook merge), is insert-only + idempotent, and skips with a warning if the `## Bstack Core Automation Primitives` anchor is absent (never guesses a location). + ## See also - `references/primitives.md` §P11 — Empirical Feedback Loop discipline diff --git a/scripts/doctor.sh b/scripts/doctor.sh index d4e6bec..a4caac2 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -202,6 +202,22 @@ if [ -f "$AGENTS" ]; then done fi +# ── 4b. AGENTS.md Development Philosophy section (advisory, backfillable) ──── +# Templated since bstack 0.24.0. A workspace bootstrapped before then +# legitimately lacks it — this is NOT a contract violation (the P1-P20 contract +# is unchanged), so it is reported as an informational advisory only: never a +# GAP, never fails --strict (mirrors §12 Pillars / §13 dogfood convention). +# `bstack repair` backfills it from the template. +section "4b. AGENTS.md Development Philosophy (advisory)" +if [ -f "$AGENTS" ]; then + if grep -qE "^## Development Philosophy" "$AGENTS"; then + ok "AGENTS.md has Development Philosophy section" + else + echo " [info] AGENTS.md has no Development Philosophy section (templated since 0.24.0)" + echo " → backfill: bstack repair (informational; not a gap, does not fail --strict)" + fi +fi + # ── 5. policy.yaml required blocks ────────────────────────────────────────── section "5. .control/policy.yaml blocks" POL="$WORKSPACE/.control/policy.yaml" diff --git a/scripts/repair.sh b/scripts/repair.sh index b2f5f02..9822051 100755 --- a/scripts/repair.sh +++ b/scripts/repair.sh @@ -113,6 +113,63 @@ PYEOF fi } +# ── Development Philosophy backfill (helper) ─────────────────────────────── +# Inserts the `## Development Philosophy` section (templated since bstack 0.24.0) +# into an existing AGENTS.md / CLAUDE.md that predates it. The scaffold is +# idempotent-never-overwrite, so existing workspaces never receive newly- +# templated *content* — only freshly-created files. This closes that gap for +# this one section. +# +# Idempotent + non-destructive: skips if the section is already present; skips +# with a warning if the insertion anchor (`## Bstack Core Automation +# Primitives`) is absent (never guesses a location). Extracts the section +# verbatim from the template (heading → Primitives anchor, exclusive) via files +# only — no shell interpolation of the content — so backticks/quotes/pipes in +# the section survive intact. +backfill_philosophy_section() { + local target="$1" # AGENTS.md | CLAUDE.md + local template="$2" # AGENTS.md.template | CLAUDE.md.template + local tgt="$WORKSPACE_DIR/$target" + local tpl="$TEMPLATES_DIR/$template" + local anchor="## Bstack Core Automation Primitives" + [ -f "$tgt" ] || return # nothing to backfill into + [ -f "$tpl" ] || { echo " [skip] $target philosophy — template missing: $template"; return; } + grep -qE "^## Development Philosophy" "$tgt" && return # already present (idempotent) + if ! grep -qF "$anchor" "$tgt"; then + echo " [skip] $target philosophy — anchor '$anchor' not found (insert manually)" + return + fi + if [ "$DRY_RUN" = "1" ]; then + echo " [dry-run] would backfill Development Philosophy into $target" + return + fi + if ! confirm "Backfill Development Philosophy section into $target?"; then + echo " [skip] $target philosophy (declined)" + return + fi + local secfile tmp + secfile="$(mktemp)" + # Section block = template lines from the heading up to (excluding) the anchor. + awk '/^## Development Philosophy$/{f=1} /^## Bstack Core Automation Primitives$/{f=0} f' "$tpl" > "$secfile" + if [ ! -s "$secfile" ]; then + echo " [skip] $target philosophy — could not extract section from $template" + rm -f "$secfile" + return + fi + tmp="$(mktemp)" + # Insert the section immediately before the first anchor line in the target. + awk -v anchor="$anchor" -v secfile="$secfile" ' + $0 == anchor && !done { + while ((getline line < secfile) > 0) print line + close(secfile) + done = 1 + } + { print } + ' "$tgt" > "$tmp" && mv "$tmp" "$tgt" + rm -f "$secfile" + echo " [fix] backfilled Development Philosophy into $target" +} + # ── Hook re-wire (helper) ────────────────────────────────────────────────── # Idempotently merges every hook in assets/templates/settings.json.snippet # into $WORKSPACE_DIR/.claude/settings.json. Existing entries are never @@ -223,6 +280,13 @@ if [ "$DRY_RUN" = "1" ] || confirm "Merge missing hooks from settings.json.snipp merge_hooks_into_settings fi +# Backfill templated-since-0.24.0 governance content that even a *compliant* +# (pre-0.24.0) workspace can lack — run BEFORE the compliance early-exit, like +# the hook merge above, because the Development Philosophy advisory is not a +# GAP (so doctor still reports "fully bstack-compliant" without it). +backfill_philosophy_section "AGENTS.md" "AGENTS.md.template" +backfill_philosophy_section "CLAUDE.md" "CLAUDE.md.template" + if echo "$GAPS_OUTPUT" | grep -q "fully bstack-compliant"; then echo " ✓ no other gaps — workspace already bstack-compliant" exit 0 diff --git a/tests/philosophy-backfill.test.sh b/tests/philosophy-backfill.test.sh new file mode 100755 index 0000000..a77513e --- /dev/null +++ b/tests/philosophy-backfill.test.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# tests/philosophy-backfill.test.sh — fixture-based tests for the Development +# Philosophy backfill in scripts/repair.sh and the advisory in scripts/doctor.sh. +# +# Verifies: +# 1. A pre-0.24.0 AGENTS.md/CLAUDE.md (no section) gets the section backfilled, +# positioned BEFORE the `## Bstack Core Automation Primitives` anchor. +# 2. Re-running is idempotent (exactly one section; no duplicate). +# 3. --dry-run reports the backfill but writes nothing. +# 4. Missing anchor → skip with a warning, file unchanged (never guesses). +# 5. doctor surfaces the advisory when the section is absent, and reports ok +# when present — and the advisory is NOT a GAP / does NOT fail --strict. +# +# Run from repo root: bash tests/philosophy-backfill.test.sh +set -uo pipefail + +BSTACK_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPAIR_SH="$BSTACK_REPO/scripts/repair.sh" +DOCTOR_SH="$BSTACK_REPO/scripts/doctor.sh" + +PASS=0 +FAIL=0 +FAILED_TESTS=() + +assert_pass() { PASS=$((PASS + 1)); echo " ✓ $1"; } +assert_fail() { + FAIL=$((FAIL + 1)); FAILED_TESTS+=("$1") + echo " ✗ $1"; [ -n "${2:-}" ] && echo " ${2}" +} + +# Count `## Development Philosophy` headings in a file. +# grep -c prints "0" and exits 1 on zero matches; `|| true` swallows the exit +# without appending a second "0" (the naive `|| echo 0` double-prints). +phil_count() { grep -cE "^## Development Philosophy" "$1" 2>/dev/null || true; } + +# A workspace with AGENTS.md + CLAUDE.md that have the Primitives anchor but +# NO Development Philosophy section (simulates a pre-0.24.0 bootstrap). No +# .git / .control → doctor early-exits, so repair doesn't trigger the heavier +# RCS-install fix paths; the backfill (which runs before the early-exit) still +# executes. That keeps this test fast and focused. +fresh_pre024_workspace() { + local ws; ws="$(mktemp -d)" + cat > "$ws/AGENTS.md" <<'EOF' +# demo — Agent Guidelines + +## Self-Meta Definition + +This file IS the control harness. + +## Bstack Core Automation Primitives + +(primitive table here) +EOF + cat > "$ws/CLAUDE.md" <<'EOF' +# demo — bstack-governed workspace + +## Identity + +Governed by bstack. + +## Bstack Core Automation Primitives + +(primitive table here) +EOF + echo "$ws" +} + +echo "=== philosophy-backfill.test.sh ===" + +# ── Test 1 + 2: backfill + idempotency ────────────────────────────────────── +echo "" +echo "Test 1+2: backfill into pre-0.24.0 workspace, then idempotency" +WS="$(fresh_pre024_workspace)" + +[ "$(phil_count "$WS/AGENTS.md")" -eq 0 ] && assert_pass "precondition: AGENTS.md has no section" \ + || assert_fail "precondition: AGENTS.md should start with no section" + +BROOMVA_WORKSPACE="$WS" bash "$REPAIR_SH" --apply-all >/dev/null 2>&1 + +if [ "$(phil_count "$WS/AGENTS.md")" -eq 1 ]; then + assert_pass "AGENTS.md section backfilled (exactly one)" +else + assert_fail "AGENTS.md section not backfilled (count=$(phil_count "$WS/AGENTS.md"))" +fi +if [ "$(phil_count "$WS/CLAUDE.md")" -eq 1 ]; then + assert_pass "CLAUDE.md section backfilled (exactly one)" +else + assert_fail "CLAUDE.md section not backfilled (count=$(phil_count "$WS/CLAUDE.md"))" +fi + +# Position: section must come BEFORE the Primitives anchor in AGENTS.md. +phil_line="$(grep -nE "^## Development Philosophy" "$WS/AGENTS.md" | head -1 | cut -d: -f1)" +anchor_line="$(grep -nF "## Bstack Core Automation Primitives" "$WS/AGENTS.md" | head -1 | cut -d: -f1)" +if [ -n "$phil_line" ] && [ -n "$anchor_line" ] && [ "$phil_line" -lt "$anchor_line" ]; then + assert_pass "section positioned before Primitives anchor (line $phil_line < $anchor_line)" +else + assert_fail "section not positioned before anchor (phil=$phil_line anchor=$anchor_line)" +fi + +# Key content row survived extraction intact (backticks/quotes/pipes). +if grep -qF 'No verifiable "done"' "$WS/AGENTS.md"; then + assert_pass "section content extracted verbatim (quotes intact)" +else + assert_fail "section content lost in extraction" +fi + +# Idempotency: second run must not duplicate. +BROOMVA_WORKSPACE="$WS" bash "$REPAIR_SH" --apply-all >/dev/null 2>&1 +if [ "$(phil_count "$WS/AGENTS.md")" -eq 1 ] && [ "$(phil_count "$WS/CLAUDE.md")" -eq 1 ]; then + assert_pass "idempotent: re-run did not duplicate the section" +else + assert_fail "idempotency broken (AGENTS=$(phil_count "$WS/AGENTS.md") CLAUDE=$(phil_count "$WS/CLAUDE.md"))" +fi +rm -rf "$WS" + +# ── Test 3: --dry-run writes nothing ──────────────────────────────────────── +echo "" +echo "Test 3: --dry-run reports but does not write" +WS="$(fresh_pre024_workspace)" +DRY_OUT="$(BROOMVA_WORKSPACE="$WS" bash "$REPAIR_SH" --dry-run 2>&1)" +if echo "$DRY_OUT" | grep -q "would backfill Development Philosophy into AGENTS.md"; then + assert_pass "--dry-run reports the backfill" +else + assert_fail "--dry-run did not report the backfill" +fi +if [ "$(phil_count "$WS/AGENTS.md")" -eq 0 ]; then + assert_pass "--dry-run wrote nothing" +else + assert_fail "--dry-run modified the file (count=$(phil_count "$WS/AGENTS.md"))" +fi +rm -rf "$WS" + +# ── Test 4: missing anchor → skip, file unchanged ─────────────────────────── +echo "" +echo "Test 4: missing Primitives anchor → skip with warning" +WS="$(mktemp -d)" +printf '# demo\n\nNo anchor here.\n' > "$WS/AGENTS.md" +printf '# demo\n\nNo anchor here.\n' > "$WS/CLAUDE.md" +SKIP_OUT="$(BROOMVA_WORKSPACE="$WS" bash "$REPAIR_SH" --apply-all 2>&1)" +if echo "$SKIP_OUT" | grep -q "anchor '## Bstack Core Automation Primitives' not found"; then + assert_pass "missing anchor reported as skip" +else + assert_fail "missing-anchor skip not reported" +fi +if [ "$(phil_count "$WS/AGENTS.md")" -eq 0 ]; then + assert_pass "missing-anchor file left unchanged" +else + assert_fail "section inserted despite missing anchor" +fi +rm -rf "$WS" + +# ── Test 5: doctor advisory present/absent + --strict safety ───────────────── +echo "" +echo "Test 5: doctor advisory + --strict does not fail on missing section" +WS="$(mktemp -d)"; mkdir -p "$WS/.control" +: > "$WS/.control/policy.yaml" +cp "$BSTACK_REPO/assets/templates/CLAUDE.md.template" "$WS/CLAUDE.md" +# AGENTS.md WITHOUT the section (strip it from the template between the heading +# and the Primitives anchor). +awk '/^## Development Philosophy$/{skip=1} /^## Bstack Core Automation Primitives$/{skip=0} !skip' \ + "$BSTACK_REPO/assets/templates/AGENTS.md.template" > "$WS/AGENTS.md" + +DOC_OUT="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" +if echo "$DOC_OUT" | grep -q "no Development Philosophy section"; then + assert_pass "doctor surfaces the advisory when section absent" +else + assert_fail "doctor did not surface the advisory" +fi + +# --strict must still exit 0 for *this* (advisory is not a GAP). Other gaps may +# exist in this minimal workspace, so we assert specifically that the advisory +# itself is phrased as informational (not a [gap] line). +if echo "$DOC_OUT" | grep -q "\[gap\].*Development Philosophy"; then + assert_fail "advisory was emitted as a GAP (should be informational)" +else + assert_pass "advisory is informational, not a GAP" +fi + +# With the section present, doctor reports ok. +cp "$BSTACK_REPO/assets/templates/AGENTS.md.template" "$WS/AGENTS.md" +DOC_OUT2="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" +if echo "$DOC_OUT2" | grep -q "AGENTS.md has Development Philosophy section"; then + assert_pass "doctor reports ok when section present" +else + assert_fail "doctor did not report ok with section present" +fi +rm -rf "$WS" + +# ── Summary ───────────────────────────────────────────────────────────────── +echo "" +echo "=== results: $PASS passed, $FAIL failed ===" +if [ "$FAIL" -gt 0 ]; then + printf ' - %s\n' "${FAILED_TESTS[@]}" + exit 1 +fi +exit 0 From ce61cfee92d8022d59af53805114e75db1a30abe Mon Sep 17 00:00:00 2001 From: "Carlos D. Escobar-Valbuena" Date: Fri, 5 Jun 2026 10:45:40 -0500 Subject: [PATCH 2/3] =?UTF-8?q?fix(P20=20round=201):=20harden=20backfill?= =?UTF-8?q?=20=E2=80=94=20template-anchor=20guard,=20CRLF-tolerant=20match?= =?UTF-8?q?,=20verify-after-insert,=20pipefail=20SIGPIPE=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-review (Codex, Strata A) BLOCKed 6/10. All four findings real: - extractor now guards the TEMPLATE anchor too (else awk runs heading→EOF). - probe/inserter predicate unified + CRLF/trailing-space tolerant; verify the section actually landed before reporting [fix] (no silent false success). - mktemp/awk/mv now status-checked + temp files cleaned on every failure path. - test now actually runs 'doctor --strict' and asserts the advisory changes neither the GAP total nor the strict exit code (differential with/without). - root-cause of the AGENTS-only failure during hardening: 'tr | grep -q' under 'set -o pipefail' fails spuriously (grep -q SIGPIPEs tr on large files); grep the file directly — [[:space:]]*$ already absorbs trailing CR. Test: 17/17 (adds CRLF + differential-strict cases). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/repair.sh | 53 +++++++++++++++++++----- tests/philosophy-backfill.test.sh | 67 +++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/scripts/repair.sh b/scripts/repair.sh index 9822051..af3e1fd 100755 --- a/scripts/repair.sh +++ b/scripts/repair.sh @@ -132,10 +132,23 @@ backfill_philosophy_section() { local tgt="$WORKSPACE_DIR/$target" local tpl="$TEMPLATES_DIR/$template" local anchor="## Bstack Core Automation Primitives" + # Anchor regex tolerant of trailing whitespace + CRLF (a realistic line-ending + # variant must NOT become a silent no-op). The CR is stripped before matching. + local anchor_re='^## Bstack Core Automation Primitives[[:space:]]*$' [ -f "$tgt" ] || return # nothing to backfill into [ -f "$tpl" ] || { echo " [skip] $target philosophy — template missing: $template"; return; } grep -qE "^## Development Philosophy" "$tgt" && return # already present (idempotent) - if ! grep -qF "$anchor" "$tgt"; then + # BOTH source and destination must carry the anchor: the template needs it to + # *bound* the extracted section (else awk runs heading→EOF and over-copies); + # the target needs it to *locate* the insertion point. grep the files directly + # — `[[:space:]]*$` absorbs trailing spaces AND a trailing CR, so this is + # CRLF-tolerant without a `tr | grep -q` pipe (which under `set -o pipefail` + # fails spuriously: grep -q closes the pipe early on a match → SIGPIPE on tr). + if ! grep -qE "$anchor_re" "$tpl"; then + echo " [skip] $target philosophy — template lacks anchor; cannot bound section" + return + fi + if ! grep -qE "$anchor_re" "$tgt"; then echo " [skip] $target philosophy — anchor '$anchor' not found (insert manually)" return fi @@ -148,25 +161,47 @@ backfill_philosophy_section() { return fi local secfile tmp - secfile="$(mktemp)" + secfile="$(mktemp)" || { echo " [skip] $target philosophy — mktemp failed"; return; } # Section block = template lines from the heading up to (excluding) the anchor. - awk '/^## Development Philosophy$/{f=1} /^## Bstack Core Automation Primitives$/{f=0} f' "$tpl" > "$secfile" - if [ ! -s "$secfile" ]; then + # CR is stripped first so the awk delimiters match on a CRLF template too. + tr -d '\r' < "$tpl" | awk ' + /^## Development Philosophy$/ { f=1 } + /^## Bstack Core Automation Primitives[ \t]*$/ { f=0 } + f { print } + ' > "$secfile" + if [ ! -s "$secfile" ] || ! grep -qE "^## Development Philosophy" "$secfile"; then echo " [skip] $target philosophy — could not extract section from $template" rm -f "$secfile" return fi - tmp="$(mktemp)" - # Insert the section immediately before the first anchor line in the target. - awk -v anchor="$anchor" -v secfile="$secfile" ' - $0 == anchor && !done { + tmp="$(mktemp)" || { echo " [skip] $target philosophy — mktemp failed"; rm -f "$secfile"; return; } + # Insert the section immediately before the first anchor line (CR/space-tolerant). + if ! awk -v secfile="$secfile" ' + function isanchor(s) { sub(/\r$/, "", s); sub(/[ \t]+$/, "", s); return (s == "## Bstack Core Automation Primitives") } + isanchor($0) && !done { while ((getline line < secfile) > 0) print line close(secfile) done = 1 } { print } - ' "$tgt" > "$tmp" && mv "$tmp" "$tgt" + ' "$tgt" > "$tmp"; then + echo " [skip] $target philosophy — insertion failed (awk)" + rm -f "$secfile" "$tmp" + return + fi rm -f "$secfile" + # Verify the section actually landed BEFORE committing the write. Guards the + # probe-vs-inserter mismatch and any awk no-op — never report a false [fix]. + if ! grep -qE "^## Development Philosophy" "$tmp"; then + echo " [skip] $target philosophy — anchor not matched during insert (no change)" + rm -f "$tmp" + return + fi + if ! mv "$tmp" "$tgt"; then + echo " [skip] $target philosophy — could not write $target" + rm -f "$tmp" + return + fi echo " [fix] backfilled Development Philosophy into $target" } diff --git a/tests/philosophy-backfill.test.sh b/tests/philosophy-backfill.test.sh index a77513e..bde21ee 100755 --- a/tests/philosophy-backfill.test.sh +++ b/tests/philosophy-backfill.test.sh @@ -149,41 +149,76 @@ else fi rm -rf "$WS" -# ── Test 5: doctor advisory present/absent + --strict safety ───────────────── +# ── Test 5: doctor advisory present/absent + advisory contributes 0 gaps ───── echo "" -echo "Test 5: doctor advisory + --strict does not fail on missing section" +echo "Test 5: doctor advisory is informational (does not change the GAP total / --strict)" +gap_lines() { grep -c "^ \[gap\]" 2>/dev/null || true; } # count [gap] lines from stdin WS="$(mktemp -d)"; mkdir -p "$WS/.control" -: > "$WS/.control/policy.yaml" +cp "$BSTACK_REPO/assets/templates/policy.yaml.template" "$WS/.control/policy.yaml" cp "$BSTACK_REPO/assets/templates/CLAUDE.md.template" "$WS/CLAUDE.md" -# AGENTS.md WITHOUT the section (strip it from the template between the heading -# and the Primitives anchor). + +# (a) WITHOUT the section: strip it from the template (heading → anchor, exclusive). awk '/^## Development Philosophy$/{skip=1} /^## Bstack Core Automation Primitives$/{skip=0} !skip' \ "$BSTACK_REPO/assets/templates/AGENTS.md.template" > "$WS/AGENTS.md" +DOC_WITHOUT="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" +BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" --strict >/dev/null 2>&1; STRICT_WITHOUT=$? +GAPS_WITHOUT="$(echo "$DOC_WITHOUT" | gap_lines)" -DOC_OUT="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" -if echo "$DOC_OUT" | grep -q "no Development Philosophy section"; then +if echo "$DOC_WITHOUT" | grep -q "no Development Philosophy section"; then assert_pass "doctor surfaces the advisory when section absent" else assert_fail "doctor did not surface the advisory" fi - -# --strict must still exit 0 for *this* (advisory is not a GAP). Other gaps may -# exist in this minimal workspace, so we assert specifically that the advisory -# itself is phrased as informational (not a [gap] line). -if echo "$DOC_OUT" | grep -q "\[gap\].*Development Philosophy"; then +if echo "$DOC_WITHOUT" | grep -q "\[gap\].*Development Philosophy"; then assert_fail "advisory was emitted as a GAP (should be informational)" else - assert_pass "advisory is informational, not a GAP" + assert_pass "advisory is informational, not a [gap] line" fi -# With the section present, doctor reports ok. +# (b) WITH the section: identical workspace except the section is present. cp "$BSTACK_REPO/assets/templates/AGENTS.md.template" "$WS/AGENTS.md" -DOC_OUT2="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" -if echo "$DOC_OUT2" | grep -q "AGENTS.md has Development Philosophy section"; then +DOC_WITH="$(BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" 2>&1)" +BROOMVA_WORKSPACE="$WS" bash "$DOCTOR_SH" --strict >/dev/null 2>&1; STRICT_WITH=$? +GAPS_WITH="$(echo "$DOC_WITH" | gap_lines)" + +if echo "$DOC_WITH" | grep -q "AGENTS.md has Development Philosophy section"; then assert_pass "doctor reports ok when section present" else assert_fail "doctor did not report ok with section present" fi +# The contract: presence/absence of the section changes neither the GAP total +# nor the --strict exit code. This actually runs --strict and asserts on it. +if [ "$GAPS_WITHOUT" = "$GAPS_WITH" ]; then + assert_pass "GAP total identical with/without section ($GAPS_WITH) — advisory adds 0 gaps" +else + assert_fail "advisory changed the GAP total (without=$GAPS_WITHOUT with=$GAPS_WITH)" +fi +if [ "$STRICT_WITHOUT" = "$STRICT_WITH" ]; then + assert_pass "--strict exit code identical with/without section (=$STRICT_WITH)" +else + assert_fail "--strict exit changed (without=$STRICT_WITHOUT with=$STRICT_WITH)" +fi +rm -rf "$WS" + +# ── Test 6: CRLF / trailing-space anchor must not be a silent no-op ────────── +echo "" +echo "Test 6: anchor with CRLF + trailing space is matched (no false [fix])" +WS="$(mktemp -d)" +# AGENTS.md whose Primitives anchor carries trailing spaces + CRLF line endings. +printf '# demo\r\n\r\n## Self-Meta\r\n\r\n## Bstack Core Automation Primitives \r\n\r\n(table)\r\n' > "$WS/AGENTS.md" +printf '# demo\n\n## Identity\n\n## Bstack Core Automation Primitives\n\n(table)\n' > "$WS/CLAUDE.md" +BF_OUT="$(BROOMVA_WORKSPACE="$WS" bash "$REPAIR_SH" --apply-all 2>&1)" +if [ "$(phil_count "$WS/AGENTS.md")" -eq 1 ]; then + assert_pass "CRLF/trailing-space anchor matched — section inserted" +else + assert_fail "CRLF/trailing-space anchor NOT matched (count=$(phil_count "$WS/AGENTS.md"))" +fi +# And it must not have claimed a fix it didn't make on AGENTS.md. +if echo "$BF_OUT" | grep -q "backfilled Development Philosophy into AGENTS.md"; then + assert_pass "reported [fix] only because it really inserted" +else + assert_fail "did not report the AGENTS.md fix" +fi rm -rf "$WS" # ── Summary ───────────────────────────────────────────────────────────────── From bca34804091e088e0c98b130da2547701d12e953 Mon Sep 17 00:00:00 2001 From: "Carlos D. Escobar-Valbuena" Date: Fri, 5 Jun 2026 10:50:02 -0500 Subject: [PATCH 3/3] fix(P20 round 2): check extraction-pipeline exit status (pipefail-safe) Codex round-2 residual (finding 3): the tr|awk extraction was unchecked under set -o pipefail. Now wrapped in 'if ! ...; then skip+cleanup'. SIGPIPE-safe (awk drains all of tr); content checks still catch partial/empty writes. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/repair.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/repair.sh b/scripts/repair.sh index af3e1fd..914c224 100755 --- a/scripts/repair.sh +++ b/scripts/repair.sh @@ -164,11 +164,18 @@ backfill_philosophy_section() { secfile="$(mktemp)" || { echo " [skip] $target philosophy — mktemp failed"; return; } # Section block = template lines from the heading up to (excluding) the anchor. # CR is stripped first so the awk delimiters match on a CRLF template too. - tr -d '\r' < "$tpl" | awk ' + # The pipeline's exit status is checked (under `set -o pipefail` a tr/awk/write + # failure surfaces here); it is SIGPIPE-safe because awk drains all of tr's + # output (no early close). The content checks below catch a partial/empty write. + if ! tr -d '\r' < "$tpl" | awk ' /^## Development Philosophy$/ { f=1 } /^## Bstack Core Automation Primitives[ \t]*$/ { f=0 } f { print } - ' > "$secfile" + ' > "$secfile"; then + echo " [skip] $target philosophy — extraction pipeline failed" + rm -f "$secfile" + return + fi if [ ! -s "$secfile" ] || ! grep -qE "^## Development Philosophy" "$secfile"; then echo " [skip] $target philosophy — could not extract section from $template" rm -f "$secfile"