crewforth 3.0.0 → 3.0.1-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/adopt.sh +222 -19
- package/kit/CLAUDE.md +5 -4
- package/kit/README.md +1 -1
- package/kit/agents/crew-frontend-expert.md +6 -6
- package/kit/agents/crew-session-manager.md +1 -1
- package/kit/eval/doctor.sh +77 -18
- package/kit/eval/golden-routing.txt +15 -0
- package/kit/eval/lib/git-bash.sh +108 -0
- package/kit/eval/preflight.sh +2 -2
- package/kit/eval/smoke-test.sh +761 -29
- package/kit/hooks/board-sync.sh +1 -1
- package/kit/hooks/board.sh +4 -3
- package/kit/hooks/guard-bash.sh +195 -21
- package/kit/hooks/guard-commit-scan.sh +2 -2
- package/kit/hooks/guard-powershell.sh +328 -0
- package/kit/hooks/guard-write.sh +28 -3
- package/kit/hooks/route-hint.sh +5 -1
- package/kit/hooks/session-update-check.sh +2 -2
- package/kit/hooks/skill-trust.sh +57 -14
- package/kit/legacy-blobs.tsv +208 -0
- package/kit/settings.json +21 -0
- package/kit/skills/automode-policy/SKILL.md +1 -1
- package/kit/skills/automode-policy/scripts/check.sh +2 -2
- package/kit/skills/crew-board/SKILL.md +1 -1
- package/kit/skills/crew-doctor/SKILL.md +13 -5
- package/kit/skills/crew-gates/SKILL.md +2 -1
- package/kit/skills/crew-handoff/SKILL.md +1 -1
- package/kit/skills/crew-skill/SKILL.md +2 -2
- package/kit/skills/crew-studio/SKILL.md +3 -2
- package/kit/skills/crew-update/SKILL.md +3 -3
- package/kit/skills/frontend/SKILL.md +26 -2
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +2 -2
- package/kit/skills/reflect/SKILL.md +2 -2
- package/kit/skills/teamboard/SKILL.md +1 -1
- package/kit/skills/token-budget/SKILL.md +2 -2
- package/kit/skills/trace-scan/SKILL.md +4 -3
- package/kit/studio/README.md +1 -1
- package/kit/studio/server/lib/permissions.js +3 -0
- package/package.json +5 -4
- package/start.sh +15 -8
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -705,11 +705,16 @@ else
|
|
|
705
705
|
&& pass "a decision recorded by one teammate arrives in another's clone" \
|
|
706
706
|
|| fail "the decision never reached the second clone — decisions stay as local as the ADRs they replace"
|
|
707
707
|
rm -f "$BD/ayse/.git/crew-board-seen"
|
|
708
|
-
|
|
708
|
+
# Read into a variable first: `board.sh cache | grep -q` under pipefail is a race — grep exits on its match, the
|
|
709
|
+
# writer takes SIGPIPE, and the pipeline reads false. It failed once on windows-latest (3.0.1, PR 7), where the
|
|
710
|
+
# slower process start lets the writer still be printing when grep leaves.
|
|
711
|
+
_bdc="$(cd "$BD/ayse" && bash ../board.sh cache 2>/dev/null)"
|
|
712
|
+
case "$_bdc" in *"recorded since you last looked"*) true ;; *) false ;; esac \
|
|
709
713
|
&& pass "an unread decision announces itself at session start" \
|
|
710
714
|
|| fail "an unread decision is silent at session start — it arrives after the work it should have changed"
|
|
711
715
|
( cd "$BD/ayse" && bash ../board.sh decisions ) >/dev/null 2>&1
|
|
712
|
-
(
|
|
716
|
+
_bdc="$(cd "$BD/ayse" && bash ../board.sh cache 2>/dev/null)"
|
|
717
|
+
case "$_bdc" in *"recorded since you last looked"*) true ;; *) false ;; esac \
|
|
713
718
|
&& fail "the decision keeps announcing itself after being read — a permanent alert is an ignored one" \
|
|
714
719
|
|| pass "once read, the decision stops being announced"
|
|
715
720
|
# First read must not leak a shell error: the marker file does not exist yet, and an input redirect from a
|
|
@@ -1518,6 +1523,36 @@ if [ -f "$ROOT/CLAUDE.md" ]; then
|
|
|
1518
1523
|
P_HALF="$(awk 'f{print} /^<!-- KIT:DISCIPLINE-END/{f=1}' "$ROOT/CLAUDE.md")"
|
|
1519
1524
|
case "$D_HALF" in *'<PROJECT NAME>'*) fail "discipline half swallows the project template" ;; *) pass "discipline half excludes the project template" ;; esac
|
|
1520
1525
|
case "$D_HALF" in *'Four working principles'*) pass "discipline half carries the four principles" ;; *) fail "discipline half lost the four principles" ;; esac
|
|
1526
|
+
# Where a project's own CLAUDE.md sits against §4 (field: two projects wrote a stricter §4.4 and read the discipline's
|
|
1527
|
+
# as a contradiction), and what a § number points at. Stated on the ladder, and at the top where "project wins" is.
|
|
1528
|
+
case "$D_HALF" in *'A project `CLAUDE.md` can tighten §4, never loosen it (§4.1 names the one exception). The § numbers in this file refer only to this file.'*)
|
|
1529
|
+
pass "the ladder says a project CLAUDE.md can tighten §4, never loosen it, and that § numbers are this file's" ;;
|
|
1530
|
+
*) fail "the ladder lost the project-vs-§4 sentence or the § numbers sentence" ;; esac
|
|
1531
|
+
case "$D_HALF" in *'where they win on conflict; in §4 they can only tighten, never loosen (§4.1 names the one exception).'*) pass "the opening's 'project wins' carries the §4 limit and names where its one exception is" ;;
|
|
1532
|
+
*) fail "the opening says the project wins on conflict without the §4 limit" ;; esac
|
|
1533
|
+
case "$P_HALF" in *'on conflict the rules here win; in DISCIPLINE.md §4 they can only tighten, never loosen (its §4.1 names the one exception).'*)
|
|
1534
|
+
pass "the project template's 'rules here win' carries the §4 limit" ;; *) fail "the project template says its rules win without the §4 limit" ;; esac
|
|
1535
|
+
# The same meaning where the updater and the installed README say it (decided wording: the project wins; in §4 it can
|
|
1536
|
+
# only tighten; the §4.1 trace allowlist chosen at adoption is the one exception). Each is a fixed string, so each
|
|
1537
|
+
# can revert silently — pinned here, with the twin that proves the pin reads the file.
|
|
1538
|
+
_pw_kr="$(cd "$ROOT/.." && pwd)"; _pw_f=""
|
|
1539
|
+
while IFS='|' read -r _pwf _pws; do [ -n "$_pwf" ] || continue
|
|
1540
|
+
grep -qF -- "$_pws" "$_pw_kr/$_pwf" 2>/dev/null || _pw_f="$_pw_f | $_pwf: $_pws"
|
|
1541
|
+
done <<'PWEOF'
|
|
1542
|
+
kit/README.md|which win on conflict; in DISCIPLINE.md §4 they can only tighten, never loosen (its §4.1 names the one exception).
|
|
1543
|
+
adopt.sh|"on conflict the project's rules win; in DISCIPLINE.md §4 they can only tighten, never loosen (the one exception: the §4.1 trace allowlist you choose at adoption); Crewforth fills gaps (not overridable)"
|
|
1544
|
+
adopt.sh|DISCIPLINE.md §4'te proje yalnız sıkılaştırabilir, gevşetemez (tek istisna: kurulumda sizin seçtiğiniz §4.1 iz izin listesi)
|
|
1545
|
+
adopt.sh|- On rule conflicts the PROJECT wins (axis-by-axis); in DISCIPLINE.md §4 it can only tighten, never loosen — the one exception is the §4.1 trace allowlist chosen at adoption (decision 3).
|
|
1546
|
+
adopt.sh|| 2 | Precedence | $D2 (axis-by-axis; DISCIPLINE.md §4 tighten-only, except the §4.1 trace allowlist of decision 3) |
|
|
1547
|
+
PWEOF
|
|
1548
|
+
if [ -f "$_pw_kr/adopt.sh" ]; then
|
|
1549
|
+
[ -z "$_pw_f" ] && pass "the updater (EN, TR, ADR, handover) and the installed README say the project wins, tightens §4 only, and name the §4.1 exception" \
|
|
1550
|
+
|| fail "a 'project wins' statement lost the §4 limit or its exception:$_pw_f"
|
|
1551
|
+
_pwt="$(mktemp)"; sed 's/ (its §4.1 names the one exception)\.$/./' "$ROOT/README.md" > "$_pwt"
|
|
1552
|
+
grep -qF -- 'which win on conflict; in DISCIPLINE.md §4 they can only tighten, never loosen (its §4.1 names the one exception).' "$_pwt" \
|
|
1553
|
+
&& fail "the pin twin did not change — the README pin reads nothing" || pass "the pin catches a README that drops the §4.1 exception (twin)"
|
|
1554
|
+
rm -f "$_pwt"
|
|
1555
|
+
else skip scope "the updater's precedence wording is checked in the Crewforth repository (adopt.sh is not installed)"; fi
|
|
1521
1556
|
case "$P_HALF" in *'<PROJECT NAME>'*) pass "project half carries the project template" ;; *) fail "project half lost the project template" ;; esac
|
|
1522
1557
|
case "$P_HALF" in *'Four working principles'*) fail "project half duplicates the discipline" ;; *) pass "project half does not duplicate the discipline" ;; esac
|
|
1523
1558
|
fi
|
|
@@ -1616,6 +1651,104 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
1616
1651
|
else
|
|
1617
1652
|
skip scope "Homebrew-channel check not run — not a git checkout of Crewforth's source"
|
|
1618
1653
|
fi
|
|
1654
|
+
# THE CHANNEL COUNT. Three channels ship Crewforth (the release archive, npm, the Claude Code plugin); comments and
|
|
1655
|
+
# pages still said four after Homebrew went. A number a reader takes as the total is a claim like any other. The
|
|
1656
|
+
# CHANGELOG is history. The pattern is split so this file does not match itself.
|
|
1657
|
+
_ch4_re='fo''ur (distribution )?channels|dö''rt (dağıtım )?kanal'
|
|
1658
|
+
_ch4_t="$(printf '%s\n' "all fo""ur channels carry it" "Dö""rt kanal da taşıyor" "of fo""ur distribution channels" "three channels" "four channel types of RGB" | grep -ciE "$_ch4_re")"
|
|
1659
|
+
if [ "$_ch4_t" != 3 ]; then fail "channel-count check cannot tell its twins apart (caught $_ch4_t of the 3 planted, want 3 and none of the 2 clean)"
|
|
1660
|
+
elif git -C "$KR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
1661
|
+
_ch4_hits="$(git -C "$KR" grep -niIE "$_ch4_re" -- . ':!CHANGELOG.md' ':!site/src/content/docs/changelog.md' ':!site/src/content/docs/tr/changelog.md' 2>/dev/null)"
|
|
1662
|
+
[ -z "$_ch4_hits" ] && pass "no tracked file gives the old channel count — three ship Crewforth (CHANGELOG exempt; twins 3/3 caught, 0/2 flagged)" \
|
|
1663
|
+
|| fail "a tracked file still gives the old channel count (there are three): $(printf '%s\n' "$_ch4_hits" | head -3 | tr '\n' ' ')"
|
|
1664
|
+
else skip scope "channel-count check not run — not a git checkout of Crewforth's source"; fi
|
|
1665
|
+
# THE CLIENT SIDE IS STACK-NEUTRAL (3.0.1): the install summary, the help and the install page named the scope as
|
|
1666
|
+
# "backend, web and mobile (RN/Expo)", and a user about to pick a non-RN mobile architecture read it as Crewforth
|
|
1667
|
+
# steering them to React Native (field: a new mobile project). A scope line — one that names backend AND web AND
|
|
1668
|
+
# mobile — never names a client stack, and says the stack is read from the project. RN/Expo stays an optional layer.
|
|
1669
|
+
_cs_scope(){ grep -hiE 'backend[^|]*web[^|]*mobi' "$@" 2>/dev/null | grep -vE '^[[:space:]]*#'; }
|
|
1670
|
+
_cs_f="$KR/start.sh $KR/site/content/en/install.md $KR/site/content/tr/install.md"
|
|
1671
|
+
_cs_l="$(_cs_scope $_cs_f)"; _cs_n="$(printf '%s\n' "$_cs_l" | grep -c .)"
|
|
1672
|
+
_cs_bad="$(printf '%s\n' "$_cs_l" | grep -iE 'RN/Expo|React Native|Flutter|SwiftUI|Compose')"
|
|
1673
|
+
_cs_nost="$(printf '%s\n' "$_cs_l" | grep . | grep -viE 'stack|yığın')"
|
|
1674
|
+
_cs_tw="$(printf '%s\n' "backend, web and mobile (R""N/Expo) together" | grep -ciE 'RN/Expo|React Native')"
|
|
1675
|
+
if [ "$_cs_tw" != 1 ]; then fail "client-stack scope check cannot see its twin — it reads nothing"
|
|
1676
|
+
elif [ "${_cs_n:-0}" -lt 5 ]; then fail "FIXTURE: only ${_cs_n:-0} scope line(s) found in start.sh and the install pages (want ≥5: EN/TR table, both usages, the summary, both pages)"
|
|
1677
|
+
elif [ -n "$_cs_bad$_cs_nost" ]; then fail "a scope line names a client stack or does not say the stack is read from the project:$(printf '%s\n%s\n' "$_cs_bad" "$_cs_nost" | grep . | head -n 3 | cut -c1-140 | sed 's/^/\n /')"
|
|
1678
|
+
else pass "the install summary, help and install pages name backend, web and mobile without a client stack, and say the stack is read from the project ($_cs_n scope lines)"; fi
|
|
1679
|
+
# THE SITE DEPLOY IS VERIFIED ON WHAT PAGES SERVES (3.0.1). A second deploy under the same deployment name reported
|
|
1680
|
+
# success while Pages kept the previous artefact (the 3.0.0 launch: the rc.3 changelog for 30 minutes). site.yml now
|
|
1681
|
+
# runs packaging/check-pages-served.sh after the deploy. Its four outcomes, on file:// fixtures (no network needed):
|
|
1682
|
+
# the new heading → 0 · an old page with an old heading, older than the deploy → 1 (stale) · the same content
|
|
1683
|
+
# re-deployed (right heading, old time) → 0 · nothing to fetch → 3 (not measured, never a pass).
|
|
1684
|
+
_pg="$KR/packaging/check-pages-served.sh"
|
|
1685
|
+
if [ -f "$_pg" ]; then
|
|
1686
|
+
_pgd="$(mktemp -d)"; mkdir -p "$_pgd/new/changelog" "$_pgd/old/changelog"
|
|
1687
|
+
printf '<h2>[3.9.1] — 2026-10-01</h2><h2>[3.9.0]</h2>' > "$_pgd/new/changelog/index.html"
|
|
1688
|
+
printf '<h2>[3.9.0] — 2026-09-28</h2>' > "$_pgd/old/changelog/index.html"
|
|
1689
|
+
touch -t 202001010000 "$_pgd/new/changelog/index.html" "$_pgd/old/changelog/index.html"
|
|
1690
|
+
_pgn="$(date +%s)"; _pgr=""
|
|
1691
|
+
# A file:// URL must carry the path curl can open: on Git Bash that is the native one (`pwd -W` → D:/a/…), not the
|
|
1692
|
+
# POSIX /tmp/… — measured on windows-latest, all three fixtures read as "not measured" (rc 3) with the POSIX form.
|
|
1693
|
+
_pgu="$(cd "$_pgd" && { pwd -W 2>/dev/null || pwd; })"; case "$_pgu" in /*) _pgu="file://$_pgu" ;; *) _pgu="file:///$_pgu" ;; esac
|
|
1694
|
+
for _c in "new [3.9.1] 0" "old [3.9.1] 1" "old [3.9.0] 0" "missing [3.9.1] 3"; do
|
|
1695
|
+
set -- $_c; PAGES_PATH=changelog/index.html bash "$_pg" "$_pgu/$1" "$2" "$_pgn" 2 0 >/dev/null 2>&1; _r=$?
|
|
1696
|
+
[ "$_r" = "$3" ] || _pgr="$_pgr | $1 want $2: rc $_r (expected $3)"
|
|
1697
|
+
done
|
|
1698
|
+
rm -rf "$_pgd"
|
|
1699
|
+
[ -z "$_pgr" ] && pass "the Pages check tells served (0), stale (1), re-deployed same content (0) and unreachable (3, not measured) apart" \
|
|
1700
|
+
|| fail "the Pages check misreads its fixtures:$_pgr"
|
|
1701
|
+
_sy="$KR/.github/workflows/site.yml"
|
|
1702
|
+
_dl="$(grep -n 'uses: actions/deploy-pages' "$_sy" | cut -d: -f1 | head -1)"; _vl="$(grep -n 'check-pages-served.sh' "$_sy" | cut -d: -f1 | head -1)"
|
|
1703
|
+
if [ -n "$_dl" ] && [ -n "$_vl" ] && [ "$_vl" -gt "$_dl" ] && grep -q 'bash _verify/packaging/check-pages-served.sh' "$_sy"; then
|
|
1704
|
+
pass "site.yml verifies what Pages serves after the deploy, with the check from its own commit (an old tag lacks it)"
|
|
1705
|
+
else fail "site.yml does not run the Pages check after deploy-pages, from the workflow's own checkout"; fi
|
|
1706
|
+
else fail "packaging/check-pages-served.sh is missing — site.yml has nothing to verify a deploy with"; fi
|
|
1707
|
+
# THE LISTING BUDGET, SAID AS MEASURED: a skill whose description Claude Code drops is picked LESS OFTEN on its own,
|
|
1708
|
+
# not never (the name stays listed, and a request that names it still reaches it). doctor and /crew-doctor used to
|
|
1709
|
+
# say it stops being picked / stops matching requests. Old claims 0, the measured one present in English and Turkish.
|
|
1710
|
+
_lb_old='stop being picked|stop'' matching requests|isteklerle eşleşmez'' olur|stop''[[:space:]]+being[[:space:]]+picked'
|
|
1711
|
+
_lb_f="$ROOT/eval/doctor.sh $ROOT/skills/crew-doctor/SKILL.md"
|
|
1712
|
+
_lb_o="$(cat $_lb_f 2>/dev/null | tr '\n' ' ' | grep -ciE "$_lb_old")"
|
|
1713
|
+
_lb_en="$(cat $_lb_f 2>/dev/null | tr '\n' ' ' | grep -oiE 'less likely to be picked on their own' | wc -l | tr -d ' ')"
|
|
1714
|
+
_lb_tr="$(grep -c 'kendiliğinden seçilmesi zorlaşır' "$ROOT/eval/doctor.sh" 2>/dev/null)" || true
|
|
1715
|
+
_lb_tw="$(printf '%s\n' "and those stop"" matching requests" | grep -ciE "$_lb_old")"
|
|
1716
|
+
if [ "$_lb_tw" != 1 ]; then fail "listing-budget wording check cannot see its twin — it reads nothing"
|
|
1717
|
+
elif [ "${_lb_o:-0}" = 0 ] && [ "${_lb_en:-0}" -ge 3 ] && [ "${_lb_tr:-0}" -ge 1 ]; then
|
|
1718
|
+
pass "the listing-budget warning says 'less likely to be picked on their own' (EN $_lb_en, TR $_lb_tr), never that a skill stops being picked"
|
|
1719
|
+
else fail "listing-budget wording: old claim $_lb_o (want 0), new EN $_lb_en (want ≥3: doctor key, doctor warn, /crew-doctor), TR ${_lb_tr:-0} (want ≥1)"; fi
|
|
1720
|
+
# kit/legacy-blobs.tsv is how the updater tells Crewforth's own, untouched old files from a user's: a stale or
|
|
1721
|
+
# hand-edited row would move a user's edit aside, or leave Crewforth's leftovers in place. It is generated from the
|
|
1722
|
+
# release tags, so it must equal what the generator prints now, byte for byte. The twin drops one row from the
|
|
1723
|
+
# shipped copy: a comparison that cannot see that measures nothing. No tags (a shallow clone) is a fixture skip —
|
|
1724
|
+
# red under CREW_VERIFY_STRICT, because CI checks out with its history.
|
|
1725
|
+
if [ -f "$KR/packaging/gen-legacy-blobs.sh" ] && git -C "$KR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
1726
|
+
if ! git -C "$KR" rev-parse -q --verify refs/tags/v2.13.0 >/dev/null 2>&1; then
|
|
1727
|
+
skip fixture "legacy blob list not checked — this clone has no v2.13.0 tag (shallow?), and the list is generated from tags"
|
|
1728
|
+
else
|
|
1729
|
+
# Compared as FILES with cmp: a `$( )` capture drops trailing newlines, and a list missing its last newline or
|
|
1730
|
+
# carrying extra blank lines at the end would compare equal that way (found in review).
|
|
1731
|
+
_LBT="$(mktemp -d)"
|
|
1732
|
+
bash "$KR/packaging/gen-legacy-blobs.sh" --stdout > "$_LBT/gen" 2>/dev/null; _lb_rc=$?
|
|
1733
|
+
sed '4d' "$KR/kit/legacy-blobs.tsv" > "$_LBT/twin" 2>/dev/null
|
|
1734
|
+
_lb_rows="$(grep -v '^#' "$KR/kit/legacy-blobs.tsv" 2>/dev/null | grep -c .)"
|
|
1735
|
+
_lb_comp="$(grep -v '^#' "$KR/kit/legacy-blobs.tsv" 2>/dev/null | cut -f1 | sort -u | grep -c .)"
|
|
1736
|
+
if [ "$_lb_rc" != 0 ]; then
|
|
1737
|
+
fail "packaging/gen-legacy-blobs.sh failed (rc $_lb_rc) — the legacy blob list cannot be checked"
|
|
1738
|
+
elif cmp -s "$_LBT/twin" "$_LBT/gen"; then
|
|
1739
|
+
fail "legacy blob check cannot see a missing row (twin with a data row removed compared equal) — it measures nothing"
|
|
1740
|
+
elif [ "${_lb_rows:-0}" -lt 150 ]; then
|
|
1741
|
+
fail "kit/legacy-blobs.tsv has ${_lb_rows:-0} row(s) — the shipped list is missing or truncated; regenerate it: bash packaging/gen-legacy-blobs.sh"
|
|
1742
|
+
elif cmp -s "$_LBT/gen" "$KR/kit/legacy-blobs.tsv"; then
|
|
1743
|
+
pass "kit/legacy-blobs.tsv equals what the tags generate ($_lb_rows rows, $_lb_comp components; twin with one row removed differs)"
|
|
1744
|
+
else
|
|
1745
|
+
fail "kit/legacy-blobs.tsv is stale — regenerate it: bash packaging/gen-legacy-blobs.sh (and commit the result)"
|
|
1746
|
+
fi
|
|
1747
|
+
rm -rf "$_LBT"
|
|
1748
|
+
fi
|
|
1749
|
+
else
|
|
1750
|
+
skip scope "legacy blob list not checked — not a git checkout of Crewforth's source"
|
|
1751
|
+
fi
|
|
1619
1752
|
# The npm wrapper prints its own usage, and it advertised --backend/--frontend/--mobile/--fullstack as the
|
|
1620
1753
|
# primary form for a release that no longer has profiles. A user reads `--help` before the README.
|
|
1621
1754
|
if [ -f "$KR/bin/cli.js" ]; then
|
|
@@ -2100,7 +2233,7 @@ sec "== 6f) always-on token budget =="
|
|
|
2100
2233
|
# for that cost, and a gate rather than a reminder — a verbose new description fails the suite instead of
|
|
2101
2234
|
# quietly taxing every future session. Budgets sit just above the current sizes: raising one is allowed, but
|
|
2102
2235
|
# only as a deliberate edit here.
|
|
2103
|
-
BUDGET_DISC=
|
|
2236
|
+
BUDGET_DISC=13986 # 3.0.1: 13741 → 13986 (+245): the ladder states that a project CLAUDE.md can tighten §4, never loosen it — naming §4.1 as the one exception, since adopt may loosen the trace gate — and that § numbers point into this file (two field projects read their stricter §4.4 as a contradiction), the opening's "project wins" carries the same limit, and the context-usage command names the Bash tool (a Windows session ran the scripts in PowerShell). RC-1: 13711 → 13741 (+30): "Reply in the user's language even when a skill's text is English" — a Turkish session answered /crew-review in English, because every skill body it had just read was English; the rule is stated nowhere else. evals/cases/reply-language measures it. 5R.3: 13712 → 13711 (the old-name phrases rewritten, net −1). 5d.2 prompt audit: tightened to the measured size (13719 → 13712: format-to-content style line, one reload
|
|
2104
2237
|
# answer, the orphaned background-warning line removed). Before that: 3.0 rename (suffix → crew- prefix): +23 B (23 occurrences), not content — measured 13696 → 13719.
|
|
2105
2238
|
# DISCIPLINE.md (the discipline half of CLAUDE.md); before 3.0 the ceiling was 13700, currently 13601. (2026-09-18, a second
|
|
2106
2239
|
# +100 B on top of the raise below, and the whole of it went into ONE sentence of §4.6: a commit
|
|
@@ -3279,6 +3412,97 @@ done
|
|
|
3279
3412
|
gj auto 'chmod +x build.sh' | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1 && pass "chmod +x NOT over-blocked" || fail "chmod +x wrongly blocked (gate too strict)"
|
|
3280
3413
|
# §4.5 gate-tampering (shell side) — disarming the gates is itself gated
|
|
3281
3414
|
gj auto 'git config core.hooksPath /tmp/x' | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "core.hooksPath redirect BLOCKED (§4.5)" || fail "core.hooksPath redirect PASSED (§4.5 hole)"
|
|
3415
|
+
# Reading core.hooksPath is how a person checks the gate is armed; only the write forms disarm it. A value-less
|
|
3416
|
+
# `git config [--local|--global|--system] core.hooksPath` prints the setting — it used to be refused as tampering
|
|
3417
|
+
# while `--get` passed. Both directions are pinned, and the result must be exactly 0 or 2: a 1 is the hook crashing,
|
|
3418
|
+
# and a crashed PreToolUse hook lets the command through (the first version of this rule did exactly that).
|
|
3419
|
+
_hp_bad=""; _hp_n=0
|
|
3420
|
+
for _c in 'git config core.hooksPath' 'git config --local core.hooksPath' 'git config --global core.hooksPath' \
|
|
3421
|
+
'git config --system core.hooksPath' 'git config --get core.hooksPath' 'git config --get-regexp core.hooksPath x' \
|
|
3422
|
+
'git config --file .git/config core.hooksPath' 'git -C /r config core.hooksPath' 'git config core.hookspath' \
|
|
3423
|
+
'git config core.hooksPath | cat' 'git status; echo core.hooksPath' 'git config --remove-section core.foo' \
|
|
3424
|
+
'git config get --all core.hooksPath'; do
|
|
3425
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _hp_n=$((_hp_n+1)); [ "$_r" = 0 ] || _hp_bad="$_hp_bad [$_c -> $_r, want 0]"
|
|
3426
|
+
done
|
|
3427
|
+
for _c in 'git config core.hooksPath .githooks' 'git config --local core.hooksPath x' 'git config --global core.hooksPath /tmp/x' \
|
|
3428
|
+
'git config --system core.hooksPath x' 'git config --unset core.hooksPath' 'git config --unset-all core.hooksPath' \
|
|
3429
|
+
'git config --add core.hooksPath x' 'git config --replace-all core.hooksPath x' 'git config set core.hooksPath x' \
|
|
3430
|
+
'git config unset core.hooksPath' 'git config core.hooksPath \"\"' 'git config core.hooksPath; git config core.hooksPath x' \
|
|
3431
|
+
'git config --get core.hooksPath && git config core.hooksPath x' \
|
|
3432
|
+
'git config --comment get core.hooksPath /tmp/x' 'git config --comment -l core.hooksPath /tmp/x' \
|
|
3433
|
+
'echo /tmp/x | xargs git config core.hooksPath' 'git config alias.hp \"config core.hooksPath\" && git hp /tmp/x' \
|
|
3434
|
+
"git config alias.hp '!git config core.hooksPath'" "bash -c 'git config core.hooksPath'" \
|
|
3435
|
+
'git config core.hooksPath \r' 'git config core.hooksPath \f' 'git config core.hookspath /tmp/x' \
|
|
3436
|
+
'git config \\\ncore.hooksPath /tmp/x' 'git config --remove-section core' 'git config --rename-section core x' \
|
|
3437
|
+
'git -C /r config core.hooksPath /tmp/x' 'git config core.hooksPath; git config\"\" core.hooksPath /dev/A' \
|
|
3438
|
+
'git config core.hooksPath && git config${IFS}core.hooksPath${IFS}/dev/B' \
|
|
3439
|
+
'git config core.hooksPath; git config --comment=\"a;\" core.hooksPath /dev/C' 'git config core.hooks\"P\"ath x' \
|
|
3440
|
+
'git config --remove-section core;echo' '(git config --remove-section core)' 'git -C \"a;b\" config core.hooksPath /dev/null' \
|
|
3441
|
+
'git config -l --unset core.hooksPath' 'git config --get core.hooksPath --replace-all x'; do
|
|
3442
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _hp_n=$((_hp_n+1)); [ "$_r" = 2 ] || _hp_bad="$_hp_bad [$_c -> $_r, want 2]"
|
|
3443
|
+
done
|
|
3444
|
+
# A read has to LOOK like one (the exact `git [-C d] config [flags] [read verb] core.hooksPath` shape): the review of the
|
|
3445
|
+
# first reader broke it with a read verb as another flag's value, a value from xargs or an alias, a CR / form-feed the
|
|
3446
|
+
# payload reader drops, a backslash-newline, a lower-case key, and removing the [core] section; a second round broke
|
|
3447
|
+
# the reader by making segments it could not parse get SKIPPED (`config""`, `config${IFS}`, a quoted `;`). Proven reads
|
|
3448
|
+
# are now cut out and the rest goes to the old rule, so nothing unproven is skipped — all pinned above.
|
|
3449
|
+
[ -z "$_hp_bad" ] && pass "core.hooksPath: value-less reads pass, every write form stays blocked ($_hp_n of 48 shapes)" \
|
|
3450
|
+
|| fail "core.hooksPath read/write split is wrong:$_hp_bad"
|
|
3451
|
+
# An exemption belongs to the command it sits in. The IaC, .env and credential rules exempted the WHOLE line when a
|
|
3452
|
+
# safe marker (--help, .env.example, .pub, …) appeared anywhere in it, so a harmless command chained in front of a
|
|
3453
|
+
# forbidden one carried it through: 72 such shapes were open (every operator: && ; || | ( ) $( )). Chained -> 2;
|
|
3454
|
+
# the same marker in the forbidden command's own segment keeps its exemption -> 0. Exactly 0 or 2: a 1 is a crash.
|
|
3455
|
+
_ch_bad=""; _ch_n=0
|
|
3456
|
+
for _c in 'terraform --help && terraform destroy -auto-approve' 'terraform plan --dry-run; terraform apply -auto-approve' \
|
|
3457
|
+
'pulumi preview --dry-run || pulumi up --yes' 'kubectl auth can-i delete pods | kubectl delete namespace prod' \
|
|
3458
|
+
'(helm --help); (helm uninstall app)' 'echo $(terraform --help) && terraform destroy' \
|
|
3459
|
+
'cat .env.example && cat .env' 'cat .env.sample; grep x < .env' '(cat .env.example); (cat .env)' \
|
|
3460
|
+
'(cat .env)' 'x=$(cat .env)' 'cat ~/.ssh/id_rsa.pub; cat ~/.ssh/id_rsa' 'cat config.example && cat ~/.aws/credentials' \
|
|
3461
|
+
'cat cert.template || cat server.pem' 'git config --list && git config core.hooksPath /tmp/x' \
|
|
3462
|
+
'cat .env.example $(cat .env)' 'cat .env.example `cat .env`' 'terraform --help $(terraform destroy -auto-approve)'; do
|
|
3463
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _ch_n=$((_ch_n+1)); [ "$_r" = 2 ] || _ch_bad="$_ch_bad [$_c -> $_r, want 2]"
|
|
3464
|
+
done
|
|
3465
|
+
for _c in 'terraform destroy --help' 'terraform plan -h' 'kubectl auth can-i delete pods' 'helm uninstall --help' \
|
|
3466
|
+
'cat .env.example' 'echo $(cat .env.example)' '(cat .env.example)' 'cat ~/.ssh/id_rsa.pub | ssh-keygen -lf -' \
|
|
3467
|
+
'cp .env.sample .env.local'; do
|
|
3468
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _ch_n=$((_ch_n+1)); [ "$_r" = 0 ] || _ch_bad="$_ch_bad [$_c -> $_r, want 0]"
|
|
3469
|
+
done
|
|
3470
|
+
[ -z "$_ch_bad" ] && pass "chained exemptions: a safe command in front no longer carries a forbidden one ($_ch_n of 27 shapes; same-segment exemptions still pass)" \
|
|
3471
|
+
|| fail "exemption leaks across a chain, or a legitimate exemption broke:$_ch_bad"
|
|
3472
|
+
# The read-only path costs processes, and on Git Bash a process is the price: measured on a loaded Windows 11
|
|
3473
|
+
# machine, one bare `bash` took 1.3-3.9 s and `git status` walked this hook through 14 of them (13 greps + the
|
|
3474
|
+
# stdin cat) — 37-66 s per tool call. The rules now run their regexes in the shell; what is left is the one `cat`
|
|
3475
|
+
# that reads stdin. Counted, not timed (a fork costs ~2 ms on macOS, so a regression is invisible to a clock).
|
|
3476
|
+
# The twin plants one `printf | grep` into a copy of the hook: a counter that cannot see it measures nothing.
|
|
3477
|
+
_gbfc(){ printf '%s' "$2" | bash -x "$1" >/dev/null 2>"$3"; grep -cE '^\++ (grep|sed|awk|tr|cat|head|tail|cut|sort|find|wc|mktemp|basename|dirname|git|jq|python3?|node|perl)( |$)' "$3" | tr -cd '0-9'; }
|
|
3478
|
+
_GBT="$(mktemp -d)"; _gbmax=0; _gbrow=""
|
|
3479
|
+
for _c in 'git status --short' 'git log --oneline -2 && git status --short | head; ls; cat CHANGELOG.md' 'ls -la' 'git diff --stat'; do
|
|
3480
|
+
_n="$(_gbfc "$HOOKS/guard-bash.sh" "$(gj default "$_c")" "$_GBT/t")"; _n="${_n:-0}"
|
|
3481
|
+
_gbrow="$_gbrow $_n"; [ "$_n" -gt "$_gbmax" ] && _gbmax="$_n"
|
|
3482
|
+
done
|
|
3483
|
+
sed '/^INPUT="\$(cat)"/a\
|
|
3484
|
+
printf x | grep -q x
|
|
3485
|
+
' "$HOOKS/guard-bash.sh" > "$_GBT/twin.sh"
|
|
3486
|
+
_tw="$(_gbfc "$_GBT/twin.sh" "$(gj default 'git status --short')" "$_GBT/t2")"; _tw="${_tw:-0}"
|
|
3487
|
+
if [ "$_gbmax" = 0 ]; then
|
|
3488
|
+
fail "guard-bash read-path cost: the trace recorded no external command at all — the measurement is broken, not the hook"
|
|
3489
|
+
elif [ "$_tw" -le "$_gbmax" ]; then
|
|
3490
|
+
fail "guard-bash read-path cost: the planted grep did not raise the count ($_tw) — the counter measures nothing"
|
|
3491
|
+
elif [ "$_gbmax" -le 1 ]; then
|
|
3492
|
+
pass "guard-bash read-only path: at most 1 external process per call (git status · read chain · ls · diff:$_gbrow; twin with one planted grep: $_tw)"
|
|
3493
|
+
else
|
|
3494
|
+
fail "guard-bash read-only path spawns $_gbmax processes per call (per command:$_gbrow) — budget 1; Git Bash pays up to seconds for each"
|
|
3495
|
+
fi
|
|
3496
|
+
# Coarse and SECONDARY: the first version of the in-shell matcher compiled its regex once per line per rule and a
|
|
3497
|
+
# 2,000-line heredoc took 70 s (0.2 s before). Nothing forks more in that case, so the count above cannot see it.
|
|
3498
|
+
# Measured after the fix: 0.2 s on macOS. The bound is 100x that, so a slow runner does not trip it.
|
|
3499
|
+
_hd="$(i=0; printf 'cat > f <<EOF\n'; while [ "$i" -lt 2000 ]; do printf 'const x = 1;\n'; i=$((i+1)); done; printf 'EOF\ngit status')"
|
|
3500
|
+
_hdj="$(printf '%s' "$_hd" | awk 'BEGIN{ORS=""} {gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); if (NR>1) print "\\n"; print}')"
|
|
3501
|
+
_hs=$SECONDS; printf '{"tool_name":"Bash","permission_mode":"default","tool_input":{"command":"%s"}}' "$_hdj" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _hr=$?; _hel=$((SECONDS - _hs))
|
|
3502
|
+
if [ "$_hr" != 0 ]; then fail "guard-bash on a 2,000-line heredoc answered $_hr, not 0 — the payload or the hook is broken"
|
|
3503
|
+
elif [ "$_hel" -le 20 ]; then pass "guard-bash on a 2,000-line heredoc: ${_hel}s (bound 20s; a regcomp-per-line matcher took 70s)"
|
|
3504
|
+
else fail "guard-bash took ${_hel}s on a 2,000-line heredoc (bound 20s) — something in the hook is per line again"; fi
|
|
3505
|
+
rm -rf "$_GBT"
|
|
3282
3506
|
gj auto 'rm .claude/hooks/pre-commit' | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "rm of a .claude gate file BLOCKED (§4.5)" || fail "rm of a gate file PASSED (§4.5 hole)"
|
|
3283
3507
|
# The rulebook is a gate file too — measured against 2.6.0, all three of these passed. Reading it must stay free.
|
|
3284
3508
|
gj auto "sed -i 's/x/y/' .claude/DISCIPLINE.md" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "in-place edit of DISCIPLINE.md BLOCKED (§4.5)" || fail "sed -i on the discipline document PASSED (§4.5 hole)"
|
|
@@ -3394,7 +3618,7 @@ done
|
|
|
3394
3618
|
wj Write '/p/.claude/DISCIPLINE.md ' | bash "$HOOKS/guard-write.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "a trailing space does not hide DISCIPLINE.md" || fail "trailing space defeated the DISCIPLINE.md rule"
|
|
3395
3619
|
wj Write '/p/.claude./hooks/x.sh' | bash "$HOOKS/guard-write.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "a trailing dot on a component does not hide a gate path" || fail "trailing dot defeated the hooks rule"
|
|
3396
3620
|
# THE PLUGIN EDITION ships the same gate scripts at $CLAUDE_PLUGIN_ROOT/hooks/, which is not `.claude/hooks/`:
|
|
3397
|
-
# one of Crewforth's
|
|
3621
|
+
# one of Crewforth's three channels was shipping an unguarded copy of its own gates. Matched by Crewforth's own
|
|
3398
3622
|
# filenames, so a project's unrelated `hooks/` directory keeps working.
|
|
3399
3623
|
wj Write '/Users/dev/.claude/plugins/crewforth/hooks/guard-write.sh' | bash "$HOOKS/guard-write.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "the plugin edition's own gate script is BLOCKED too" || fail "the plugin edition ships unguarded gate scripts (§4.5 hole)"
|
|
3400
3624
|
wj Write '/opt/crew/hooks/session-guard.sh' | bash "$HOOKS/guard-write.sh" >/dev/null 2>&1; [ "$?" = 2 ] && pass "a kit gate script is BLOCKED wherever it sits" || fail "a kit gate script outside .claude/ PASSED"
|
|
@@ -4062,7 +4286,7 @@ if ( cd "$BSD" && git init -q . ) >/dev/null 2>&1; then
|
|
|
4062
4286
|
printf '%s\n' $'#1 "Fix\tlogin" C:\\app\r\x01 ok\nsecond' > "$BSD/.git/crew-board-cache"
|
|
4063
4287
|
date -u +%s > "$BSD/.git/crew-board-cache.at"
|
|
4064
4288
|
o="$(printf '{}' | CLAUDE_PROJECT_DIR="$BSD" bash "$HOOKS/board-sync.sh" 2>/dev/null)"
|
|
4065
|
-
want='{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"#1 \"Fix\tlogin\" C:\\app\r\u0001 ok\nsecond\nBoard state above is a cached snapshot; bash .claude/hooks/board.sh sync (or the user can type /crew-board sync) refreshes it."}}'
|
|
4289
|
+
want='{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"#1 \"Fix\tlogin\" C:\\app\r\u0001 ok\nsecond\nBoard state above is a cached snapshot; bash .claude/hooks/board.sh sync (Bash tool, not PowerShell — or the user can type /crew-board sync) refreshes it."}}'
|
|
4066
4290
|
[ "$o" = "$want" ] && pass "board-sync escapes tab, CR, control bytes, quote and backslash exactly as jq does (no jq needed)" \
|
|
4067
4291
|
|| fail "board-sync JSON differs from jq's for a cache with a tab/CR/control byte — got: ${o:-<silence>}"
|
|
4068
4292
|
# A CRLF cache: the line-ending CR is dropped on every OS (MSYS gawk drops it on read, BSD awk does not — the
|
|
@@ -4131,6 +4355,49 @@ elif [ -f "$PHJ" ]; then
|
|
|
4131
4355
|
[ -x "$PLUGIN/hooks/$h" ] && grep -q "$h" "$PHJ" || { fail "plugin hook $h missing or not wired"; break; }
|
|
4132
4356
|
done
|
|
4133
4357
|
pass "5 Claude Code hooks shipped + wired in plugin"
|
|
4358
|
+
# The commands the model is told to run, as the plugin edition names them. The file-install path `.claude/hooks/…`
|
|
4359
|
+
# does not exist in a plugin install, so every one exited 127 there (measured: 12 lines in 8 files). build-plugin.sh
|
|
4360
|
+
# rewrites them to the plugin root, which Claude Code substitutes in skill and agent bodies. Three claims: every
|
|
4361
|
+
# rewritten path is a file this edition ships; no shipped script is still named by the file-install path; and one
|
|
4362
|
+
# rewritten command, run the way the Bash tool runs it (root substituted, NOT exported), from an empty project, exits 0.
|
|
4363
|
+
_ppn=0; _ppbad=""
|
|
4364
|
+
while IFS= read -r _pc; do _ppn=$((_ppn+1)); _pf="${_pc#\"\$\{CLAUDE_PLUGIN_ROOT\}/}"; _pf="${_pf%\"}"
|
|
4365
|
+
[ -f "$PLUGIN/$_pf" ] || _ppbad="$_ppbad $_pf"
|
|
4366
|
+
done < <(grep -rhoE '"\$\{CLAUDE_PLUGIN_ROOT\}/(hooks|skills)/[A-Za-z0-9_./-]+\.sh"' "$PLUGIN/agents" "$PLUGIN/skills" --include='*.md' 2>/dev/null | sort -u)
|
|
4367
|
+
_ppleft="$(grep -rhoE 'bash \.claude/(hooks|skills)/[A-Za-z0-9_./-]+\.sh' "$PLUGIN/agents" "$PLUGIN/skills" --include='*.md' 2>/dev/null | sort -u \
|
|
4368
|
+
| while IFS= read -r _pc; do [ -f "$PLUGIN/${_pc#bash .claude/}" ] && printf ' %s' "$_pc"; done)" || true
|
|
4369
|
+
# A shipped script still named by the file-install path is the product defect itself, so it is judged first — an
|
|
4370
|
+
# un-rewritten plugin/ has zero plugin-root commands, and calling that a FIXTURE would blame the test for the bug.
|
|
4371
|
+
# The expected number comes from kit/: every distinct `bash .claude/<x>.sh` there (off a `# full install` line) whose
|
|
4372
|
+
# <x> this edition ships. A fixed floor blamed the build for a legitimate edit that removed a command.
|
|
4373
|
+
_ppw="$(grep -rhE 'bash \.claude/' "$ROOT/agents" "$ROOT/skills" --include='*.md' 2>/dev/null | grep -v '# full install' \
|
|
4374
|
+
| grep -oE 'bash \.claude/[A-Za-z0-9_./-]+\.sh([^A-Za-z0-9_./-]|$)' | sed -E 's/[^A-Za-z0-9_./-]$//' | sort -u \
|
|
4375
|
+
| while IFS= read -r _pc; do [ -f "$PLUGIN/${_pc#bash .claude/}" ] && echo x; done | wc -l | tr -d ' ')"
|
|
4376
|
+
if [ -z "$_ppbad$_ppleft" ] && [ "$_ppn" != "$_ppw" ]; then fail "kit/ names $_ppw script(s) the plugin ships, plugin/ carries $_ppn plugin-root path(s) — run packaging/build-plugin.sh"
|
|
4377
|
+
elif [ -n "$_ppbad$_ppleft" ]; then fail "plugin edition names a script it cannot run —${_ppbad:+ missing:$_ppbad}${_ppleft:+ still the file-install path (127 there):$_ppleft}"
|
|
4378
|
+
else pass "plugin edition: all $_ppn of $_ppw script paths the model is told to run resolve under the plugin root, none left on .claude/"; fi
|
|
4379
|
+
_ppd="$(mktemp -d)"; mkdir -p "$_ppd/proj"
|
|
4380
|
+
printf '%s\n' '{"type":"assistant","isSidechain":false,"message":{"usage":{"input_tokens":1,"cache_read_input_tokens":2000,"cache_creation_input_tokens":1,"output_tokens":1}}}' > "$_ppd/t.jsonl"
|
|
4381
|
+
_ppc="$(grep -rhoE 'bash "\$\{CLAUDE_PLUGIN_ROOT\}/hooks/context-usage\.sh"' "$PLUGIN/skills" --include='*.md' 2>/dev/null | head -1)"
|
|
4382
|
+
if [ -z "$_ppc" ] && [ -n "$_ppleft" ]; then fail "no plugin-root context-usage command to run — plugin/ still names it by the file-install path (127 in a plugin install)"
|
|
4383
|
+
elif [ -z "$_ppc" ]; then fail "FIXTURE: no rewritten context-usage command in plugin/skills to run"
|
|
4384
|
+
else
|
|
4385
|
+
_ppc="${_ppc//\$\{CLAUDE_PLUGIN_ROOT\}/$PLUGIN}"
|
|
4386
|
+
_pprc="$(cd "$_ppd/proj" && env -u CLAUDE_PLUGIN_ROOT bash -c "$_ppc \"$_ppd/t.jsonl\"" >/dev/null 2>&1; echo "$?")"
|
|
4387
|
+
[ "$_pprc" = 0 ] && pass "a rewritten plugin command runs from an empty project with the root substituted (rc 0)" \
|
|
4388
|
+
|| fail "the rewritten plugin command exited $_pprc from an empty project: $_ppc"
|
|
4389
|
+
fi
|
|
4390
|
+
rm -rf "$_ppd"
|
|
4391
|
+
# board.sh names ITSELF in what it prints, and in the plugin edition that must be its absolute path. Run by a bare
|
|
4392
|
+
# name from its own directory (`cd hooks && bash board.sh`), `${BASH_SOURCE%/*}` is the name itself, the `cd` failed,
|
|
4393
|
+
# and it printed the file-install path — 127 in a plugin install (measured). A plugin-shaped copy in an empty repo.
|
|
4394
|
+
_bsd="$(mktemp -d)"; mkdir -p "$_bsd/p/hooks" "$_bsd/p/.claude-plugin"; : > "$_bsd/p/.claude-plugin/plugin.json"
|
|
4395
|
+
cp "$HOOKS/board.sh" "$_bsd/p/hooks/"; ( cd "$_bsd/p" && git init -q . ) >/dev/null 2>&1
|
|
4396
|
+
_bsb="$(cd "$_bsd/p/hooks" && bash board.sh status 2>&1)"; _bsp="$(cd "$_bsd/p" && bash hooks/board.sh status 2>&1)"
|
|
4397
|
+
_bsa="$(cd "$_bsd/p/hooks" && pwd)"
|
|
4398
|
+
case "$_bsb|$_bsp" in *"bash \"$_bsa/board.sh\""*"|"*"bash \"$_bsa/board.sh\""*) pass "board.sh names its own plugin path whether run by bare name or by path" ;;
|
|
4399
|
+
*) fail "board.sh printed a path a plugin install cannot run — bare name: ${_bsb:-<silence>} · by path: ${_bsp:-<silence>}" ;; esac
|
|
4400
|
+
rm -rf "$_bsd"
|
|
4134
4401
|
# The git hooks now DO ship, and must: guard-commit-scan.sh runs them from PreToolUse, which is the only way
|
|
4135
4402
|
# the plugin edition gets the commit CONTENT gates at all (a plugin cannot set core.hooksPath). They ship as
|
|
4136
4403
|
# data for that hook, never wired as git hooks. Shipping them WITHOUT the caller would be worse than not
|
|
@@ -4348,10 +4615,44 @@ O="$(st)"
|
|
|
4348
4615
|
case "$O" in *skills/mine*) pass "flags a component Crewforth never shipped" ;; *) fail "an unshipped skill was not flagged: $O" ;; esac
|
|
4349
4616
|
case "$O" in *skills/handoff*) fail "flagged a KIT skill — the manifest is being ignored" ;; *) pass "a kit-shipped skill is not re-litigated" ;; esac
|
|
4350
4617
|
case "$O" in *"REVIEW/DANGER"*) pass "runs the supply-chain scanner and reports its verdict" ;; *) fail "no scanner verdict on a malicious skill: $O" ;; esac
|
|
4618
|
+
# ONE COMMAND PER COMPONENT, and the user's message first (field report P4b/P5). The notice used to offer the bulk
|
|
4619
|
+
# `--trust`, which accepts every foreign component at once, including ones the user never looked at; and it told the
|
|
4620
|
+
# model to speak BEFORE anything else, which pushed a user's urgent first message behind a security question. The
|
|
4621
|
+
# properties pinned: each listed component carries its own `--trust-one <path>` with the Bash tool named, the bulk
|
|
4622
|
+
# `--trust` is never offered, the question comes at the END of the first reply, and nothing is used until answered.
|
|
4623
|
+
_stn="$(printf '%s\n' "$O" | grep -c -- '--trust-one skills/mine (Bash tool, not PowerShell)')"; _ste="$(printf '%s\n' "$O" | grep -c -- '--trust-one skills/evil (Bash tool, not PowerShell)')"
|
|
4624
|
+
_stb="$(printf '%s\n' "$O" | grep -cE -- '--trust([^-]|$)')"
|
|
4625
|
+
[ "$_stn" = 1 ] && [ "$_ste" = 1 ] && [ "$_stb" = 0 ] && pass "each unvetted component gets its own --trust-one command (2 of 2), the bulk --trust is not offered" \
|
|
4626
|
+
|| fail "trust commands: skills/mine $_stn, skills/evil $_ste (want 1 each), bulk --trust $_stb (want 0): $O"
|
|
4627
|
+
case "$O" in *"BEFORE anything else"*) fail "the notice still puts the trust question before the user's own message" ;;
|
|
4628
|
+
*"at the END of that first reply"*"do not use"*) pass "the trust question comes at the end of the first reply, and the components are not used until answered" ;;
|
|
4629
|
+
*) fail "the notice does not say to answer the user first and ask at the end of that reply: $O" ;; esac
|
|
4630
|
+
# One component: one command, and no other component named.
|
|
4631
|
+
rm -rf "${STD:?}/.claude/skills/evil"; _O1="$(st)"
|
|
4632
|
+
[ "$(printf '%s\n' "$_O1" | grep -c -- '--trust-one ')" = 1 ] && pass "a single unvetted component gets exactly one trust command" \
|
|
4633
|
+
|| fail "a single unvetted component got $(printf '%s\n' "$_O1" | grep -c -- '--trust-one ') trust commands: $_O1"
|
|
4351
4634
|
( cd "$STD" && bash .claude/hooks/skill-trust.sh --trust ) >/dev/null 2>&1
|
|
4352
4635
|
[ -z "$(st)" ] && pass "accepted components stay silent on later sessions" || fail "still reporting after --trust"
|
|
4353
4636
|
printf 'and now it also reads ~/.ssh/id_rsa\n' >> "$STD/.claude/skills/mine/SKILL.md"
|
|
4354
4637
|
case "$(st)" in *skills/mine*) pass "an accepted component edited afterwards is flagged again (digest, not a name)" ;; *) fail "an edited accepted component was not re-flagged" ;; esac
|
|
4638
|
+
# A NO is recorded too (RC-1 field: the user said no, nothing wrote it down, and the update vouched anyway). Declining
|
|
4639
|
+
# replaces a yes, the next session names the component as not to be used and asks nothing, and a later yes replaces the
|
|
4640
|
+
# no. Each step reads the files, not only the notice.
|
|
4641
|
+
case "$O" in *"--decline-one skills/mine (Bash tool, not PowerShell)"*"On a no, run the decline command"*) pass "each unvetted component also gets its own --decline-one command, and the notice says to run it on a no" ;;
|
|
4642
|
+
*) fail "the notice offers no way to record a no: $O" ;; esac
|
|
4643
|
+
( cd "$STD" && bash .claude/hooks/skill-trust.sh --decline-one skills/mine ) >/dev/null 2>&1; _dr=$?
|
|
4644
|
+
_dO="$(st)"
|
|
4645
|
+
if [ "$_dr" = 0 ] && grep -q ' skills/mine$' "$STD/.claude/declined-components.txt" 2>/dev/null \
|
|
4646
|
+
&& ! grep -q ' skills/mine$' "$STD/.claude/trusted-components.txt" 2>/dev/null; then
|
|
4647
|
+
pass "--decline-one records the no and drops the earlier yes (rc 0)"
|
|
4648
|
+
else fail "--decline-one: rc $_dr, declined file: $(cat "$STD/.claude/declined-components.txt" 2>/dev/null | tr '\n' '|'), trusted: $(cat "$STD/.claude/trusted-components.txt" 2>/dev/null | tr '\n' '|')"; fi
|
|
4649
|
+
case "$_dO" in *"Declined by the user"*"- skills/mine"*) case "$_dO" in *"--trust-one skills/mine"*) fail "a declined component is still asked about: $_dO" ;;
|
|
4650
|
+
*) pass "a declined component is named as not to be used, and not asked about again" ;; esac ;;
|
|
4651
|
+
*) fail "a declined component is not named at session start: $_dO" ;; esac
|
|
4652
|
+
( cd "$STD" && bash .claude/hooks/skill-trust.sh --trust-one skills/mine ) >/dev/null 2>&1
|
|
4653
|
+
if ! grep -q ' skills/mine$' "$STD/.claude/declined-components.txt" 2>/dev/null && grep -q ' skills/mine$' "$STD/.claude/trusted-components.txt" 2>/dev/null \
|
|
4654
|
+
&& [ -z "$(st)" ]; then pass "a later yes replaces the no: recorded once, and the session is quiet"
|
|
4655
|
+
else fail "--trust-one after --decline-one did not replace the answer"; fi
|
|
4355
4656
|
# A manifest with CRLF line endings still identifies kit components. `grep -qxF "skills/handoff"` does NOT match
|
|
4356
4657
|
# the line "skills/handoff\r", so on Windows every kit component read as unshipped and the session opened by
|
|
4357
4658
|
# declaring the entire payload unvetted — a wall of warnings about Crewforth's own files, which teaches the reader
|
|
@@ -4397,7 +4698,7 @@ uans(){ ( cd "$UPD" && bash .claude/hooks/session-update-check.sh --answer "$@"
|
|
|
4397
4698
|
ucache 3.1.0; o="$(uc)"
|
|
4398
4699
|
case "$o" in *'"Crewforth v3.1.0 is out (installed: v3.0.0). Update now?"'*'"Update" · "Later" · "Skip this version"'*) pass "a newer release becomes one three-way question naming both versions" ;;
|
|
4399
4700
|
*) fail "no three-way update question for a cached newer version (got: ${o:-<silence>})" ;; esac
|
|
4400
|
-
case "$o" in *"--answer later 3.1.0"*"--answer skip 3.1.0"*) pass "the question carries the exact --answer commands for Later and Skip" ;;
|
|
4701
|
+
case "$o" in *"--answer later 3.1.0\` (Bash tool, not PowerShell)"*"--answer skip 3.1.0\` (Bash tool, not PowerShell)"*) pass "the question carries the exact --answer commands for Later and Skip, each with the Bash tool named" ;;
|
|
4401
4702
|
*) fail "the question does not tell Claude how to record Later/Skip: $o" ;; esac
|
|
4402
4703
|
case "$o" in *AskUserQuestion*) pass "the question is asked with the question tool, not as prose" ;; *) fail "the hook does not name the question tool" ;; esac
|
|
4403
4704
|
case "$o" in *"if their first message is an error or an urgent fix, answer that first and ask this question at the end of that reply"*) pass "an urgent first message is answered first; the question moves to the end of that reply" ;;
|
|
@@ -4626,7 +4927,9 @@ fi
|
|
|
4626
4927
|
# hooks run there as on resume — the fork continues work the parent had; the update question does not — a fork
|
|
4627
4928
|
# is not a new opening. Asked of the wiring (both editions) AND of the hooks themselves with a fork payload.
|
|
4628
4929
|
hk_matcher(){ awk -v h="$2" '/"matcher"/{m=$0; sub(/.*"matcher"[[:space:]]*:[[:space:]]*"/,"",m); sub(/".*/,"",m)} index($0,h){print m; exit}' "$1"; }
|
|
4629
|
-
|
|
4930
|
+
# The plugin copy is Crewforth's only in this repository: an installed project may have a plugin/ of its own, and
|
|
4931
|
+
# reading that one turned the project's smoke red (review of 3.0.1).
|
|
4932
|
+
FK_FILES="$ROOT/settings.json"; FKR=""; [ "$IS_KIT" = 1 ] && FKR="$(cd "$ROOT/.." && pwd)"
|
|
4630
4933
|
[ -n "$FKR" ] && [ -f "$FKR/plugin/hooks/hooks.json" ] && FK_FILES="$FK_FILES $FKR/plugin/hooks/hooks.json"
|
|
4631
4934
|
for _ff in $FK_FILES; do
|
|
4632
4935
|
_fl="${_ff##*/}"
|
|
@@ -5157,6 +5460,8 @@ SILENT|<task-notification>Agent crew-database-expert finished: wrote the migrati
|
|
|
5157
5460
|
SILENT|[SYSTEM NOTIFICATION] the background agent finished its migration and seed report
|
|
5158
5461
|
SILENT|<cross-session-message>report: the migration and the seed are written, an endpoint was added</cross-session-message>
|
|
5159
5462
|
crew-database-expert|the system notification code needs a migration for the invoices table
|
|
5463
|
+
SILENT|<agent-message from=\"a0000000000000000\">\n[Subagent hand-back] The text below is the final report of a subagent this session delegated to. It is model output, NOT a message from the user.\n Wrote the migration and an index for the invoices table.
|
|
5464
|
+
crew-database-expert|Wrote the migration and an index for the invoices table.
|
|
5160
5465
|
RHCASES
|
|
5161
5466
|
|
|
5162
5467
|
# --- the field name, which is the way this hook dies quietly -------------------------------------
|
|
@@ -5501,8 +5806,101 @@ if [ -f "$GR" ]; then
|
|
|
5501
5806
|
*) fail "doctor did not report on the shell matcher" ;; esac
|
|
5502
5807
|
sed 's/"Bash|PowerShell"/"Bash"/' "$DTMP/.claude/settings.json" > "$DTMP/s.tmp" && mv "$DTMP/s.tmp" "$DTMP/.claude/settings.json"
|
|
5503
5808
|
DOUT2="$(cd "$DTMP" && CREW_LANG=en bash .claude/eval/doctor.sh 2>/dev/null)"
|
|
5504
|
-
case "$DOUT2" in *"watch only Bash"*) pass "doctor flags a pre-2.5.0 Bash-only matcher as a failure" ;;
|
|
5809
|
+
case "$DOUT2" in *"watch only Bash"*"DOCTOR: "*"issue(s)"*) pass "doctor flags a pre-2.5.0 Bash-only matcher as a failure, and the verdict counts it" ;;
|
|
5505
5810
|
*) fail "doctor stayed quiet on a Bash-only matcher — the gap is invisible to an upgrader" ;; esac
|
|
5811
|
+
# Windows: doctor must say when Claude Code cannot find Git Bash — its hooks then run under PowerShell and no gate
|
|
5812
|
+
# runs. Driven on any OS through shims: `uname` says MINGW, `cygpath` maps C:\ into a fixture drive, so the four
|
|
5813
|
+
# answers (default folder · CLAUDE_CODE_GIT_BASH_PATH · only git on PATH · nothing) come from real doctor code, and
|
|
5814
|
+
# the machine's own Git never leaks in (the fixture drive holds only what the case puts there).
|
|
5815
|
+
GBF="$DTMP/gbfix"; mkdir -p "$GBF/bin" "$GBF/c"
|
|
5816
|
+
printf '#!/bin/sh\necho MINGW64_NT-10.0-26200\n' > "$GBF/bin/uname"
|
|
5817
|
+
cat > "$GBF/bin/cygpath" <<'GBCP'
|
|
5818
|
+
#!/bin/bash
|
|
5819
|
+
# fixture cygpath: C:\x\y <-> $GBC/c/x/y; anything else is not on the fixture drive
|
|
5820
|
+
case "$1" in
|
|
5821
|
+
-u) p="$2"; case "$p" in [Cc]:\\*) p="${p:3}"; printf '%s/c/%s\n' "$GBC" "${p//\\//}" ;; *) exit 1 ;; esac ;;
|
|
5822
|
+
-w) p="$2"; case "$p" in "$GBC"/c/*) p="${p#"$GBC"/c/}"; printf 'C:\\%s\n' "${p//\//\\}" ;; *) exit 1 ;; esac ;;
|
|
5823
|
+
esac
|
|
5824
|
+
GBCP
|
|
5825
|
+
chmod +x "$GBF/bin/uname" "$GBF/bin/cygpath"
|
|
5826
|
+
gbdoc(){ ( cd "$DTMP" && env -u CLAUDE_CODE_GIT_BASH_PATH -u LOCALAPPDATA GBC="$GBF" PATH="$GBF/bin:$PATH" CREW_LANG=en "$@" bash .claude/eval/doctor.sh 2>/dev/null ); }
|
|
5827
|
+
_gb0="$(gbdoc)"
|
|
5828
|
+
case "$_gb0" in *"cannot find Git Bash"*"<Git>\\bin\\bash.exe"*) pass "doctor: no Git Bash where Claude Code looks → a failure that names the fix" ;;
|
|
5829
|
+
*) fail "doctor stayed quiet with no Git Bash on Windows — the gates are dead and nothing says so" ;; esac
|
|
5830
|
+
mkdir -p "$GBF/c/Tools/Git/cmd" "$GBF/c/Tools/Git/bin"; : > "$GBF/c/Tools/Git/bin/bash.exe"
|
|
5831
|
+
printf '#!/bin/sh\n' > "$GBF/c/Tools/Git/cmd/git"; chmod +x "$GBF/c/Tools/Git/cmd/git"
|
|
5832
|
+
_gb1="$(gbdoc env PATH="$GBF/c/Tools/Git/cmd:$GBF/bin:$PATH")"
|
|
5833
|
+
case "$_gb1" in *"only through git on PATH (C:\\Tools\\Git\\bin\\bash.exe)"*) pass "doctor: Git Bash found only through git on PATH → a warning naming it" ;;
|
|
5834
|
+
*) fail "doctor did not report a Git Bash reachable only through PATH" ;; esac
|
|
5835
|
+
_gb2="$(gbdoc env CLAUDE_CODE_GIT_BASH_PATH='C:\Tools\Git\bin\bash.exe')"
|
|
5836
|
+
case "$_gb2" in *"finds Git Bash (C:\\Tools\\Git\\bin\\bash.exe)"*) pass "doctor: CLAUDE_CODE_GIT_BASH_PATH to an existing bash → found" ;;
|
|
5837
|
+
*) fail "doctor did not honour CLAUDE_CODE_GIT_BASH_PATH" ;; esac
|
|
5838
|
+
_gb3="$(gbdoc env CLAUDE_CODE_GIT_BASH_PATH='C:\Tools\Git\bin\git.exe')"
|
|
5839
|
+
case "$_gb3" in *"CLAUDE_CODE_GIT_BASH_PATH (C:\\Tools\\Git\\bin\\git.exe) is not a bash"*"cannot find Git Bash"*) pass "doctor: a CLAUDE_CODE_GIT_BASH_PATH that is not bash is named as ignored" ;;
|
|
5840
|
+
*) fail "doctor accepted a CLAUDE_CODE_GIT_BASH_PATH that Claude Code ignores" ;; esac
|
|
5841
|
+
mkdir -p "$GBF/c/Users/u/AppData/Local/Programs/Git/bin"; : > "$GBF/c/Users/u/AppData/Local/Programs/Git/bin/bash.exe"
|
|
5842
|
+
_gbu="$(gbdoc env LOCALAPPDATA='C:\Users\u\AppData\Local')"
|
|
5843
|
+
case "$_gbu" in *"installed for this user only (C:\\Users\\u\\AppData\\Local\\Programs\\Git\\bin\\bash.exe)"*"CLAUDE_CODE_GIT_BASH_PATH to \"C:\\Users\\u\\AppData\\Local\\Programs\\Git\\bin\\bash.exe\""*)
|
|
5844
|
+
pass "doctor: a per-user Git install Claude Code does not look in → a failure naming the measured path to set" ;;
|
|
5845
|
+
*) fail "doctor did not name the per-user Git Bash for CLAUDE_CODE_GIT_BASH_PATH" ;; esac
|
|
5846
|
+
_gbx="$(gbdoc env LOCALAPPDATA='C:\Users\nobody\AppData\Local')"
|
|
5847
|
+
case "$_gbx" in *"installed for this user only"*) fail "doctor named a per-user Git Bash that does not exist — the path must be measured" ;;
|
|
5848
|
+
*"cannot find Git Bash"*) pass "doctor: no per-user Git either → names no path it did not find" ;;
|
|
5849
|
+
*) fail "doctor: an absent per-user Git gave neither answer" ;; esac
|
|
5850
|
+
mkdir -p "$GBF/c/Program Files/Git/bin"; : > "$GBF/c/Program Files/Git/bin/bash.exe"
|
|
5851
|
+
_gb4="$(gbdoc)"
|
|
5852
|
+
case "$_gb4" in *"finds Git Bash (C:\\Program Files\\Git\\bin\\bash.exe)"*) pass "doctor: Git Bash in its default folder → found" ;;
|
|
5853
|
+
*) fail "doctor did not find Git Bash in C:\\Program Files\\Git" ;; esac
|
|
5854
|
+
# The fixture has issues of its own; what counts is that the missing Git Bash adds exactly one.
|
|
5855
|
+
gbn(){ printf '%s\n' "$1" | sed -n 's/^DOCTOR: \([0-9][0-9]*\) issue.*/\1/p'; }
|
|
5856
|
+
[ "$(gbn "$_gb0")" = "$(( $(gbn "$_gb4" | grep . || echo 0) + 1 ))" ] \
|
|
5857
|
+
&& pass "doctor: the missing Git Bash is one more issue in the verdict ($(gbn "$_gb4" | grep . || echo 0) → $(gbn "$_gb0"))" \
|
|
5858
|
+
|| fail "doctor: the verdict did not count the missing Git Bash (with: '$(gbn "$_gb4")', without: '$(gbn "$_gb0")')"
|
|
5859
|
+
_gb5="$(cd "$DTMP" && env PATH="$GBF/c/Tools/Git/cmd:$PATH" CREW_LANG=en bash .claude/eval/doctor.sh 2>/dev/null)"
|
|
5860
|
+
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) skip platform "doctor off Windows says nothing about Git Bash — this runner IS Windows" ;;
|
|
5861
|
+
*) case "$_gb5" in *"Git Bash"*) fail "doctor spoke about Git Bash on a non-Windows machine" ;;
|
|
5862
|
+
*"DOCTOR: "*) pass "doctor says nothing about Git Bash off Windows" ;;
|
|
5863
|
+
*) fail "doctor printed no verdict in the off-Windows case — the silence proves nothing" ;; esac ;; esac
|
|
5864
|
+
# Third state: a Windows shell without cygpath is said to be unchecked, not passed. Only reachable where the
|
|
5865
|
+
# machine itself has no cygpath (a Windows runner has it in /usr/bin, next to everything else).
|
|
5866
|
+
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) skip platform "doctor's no-cygpath branch — this Windows runner has cygpath" ;;
|
|
5867
|
+
*) mkdir -p "$GBF/nocp"; cp "$GBF/bin/uname" "$GBF/nocp/uname"
|
|
5868
|
+
_gb6="$(cd "$DTMP" && PATH="$GBF/nocp:$PATH" CREW_LANG=en bash .claude/eval/doctor.sh 2>/dev/null)"
|
|
5869
|
+
case "$_gb6" in *"Git Bash lookup not checked (no cygpath"*) pass "doctor: Windows without cygpath → says the Git Bash lookup was not checked" ;;
|
|
5870
|
+
*) fail "doctor: Windows without cygpath did not say the lookup went unchecked" ;; esac ;; esac
|
|
5871
|
+
# ONE copy: the plugin edition's /crew-doctor runs eval/lib/git-bash.sh directly (doctor.sh does not ship there), so
|
|
5872
|
+
# the script alone must give doctor's answer and exit code, doctor must hold no second copy, and the plugin must
|
|
5873
|
+
# carry these bytes.
|
|
5874
|
+
_gbs="$DTMP/.claude/eval/lib/git-bash.sh"
|
|
5875
|
+
_gbd="$(cd "$DTMP" && env -u CLAUDE_CODE_GIT_BASH_PATH -u LOCALAPPDATA GBC="$GBF/empty" PATH="$GBF/bin:$PATH" CREW_LANG=en bash "$_gbs" 2>/dev/null; echo "rc=$?")"
|
|
5876
|
+
_gbd0="$(printf '%s\n' "$_gb0" | grep -F 'cannot find Git Bash')"
|
|
5877
|
+
case "$_gbd" in *"$_gbd0"*"rc=1") [ -n "$_gbd0" ] && pass "git-bash.sh alone gives doctor's line and exits 1 — the plugin's /crew-doctor gets the same answer" \
|
|
5878
|
+
|| fail "git-bash.sh: doctor's reference line is empty — the comparison proves nothing" ;;
|
|
5879
|
+
*) fail "git-bash.sh alone did not give doctor's line with exit 1: $(printf '%s' "$_gbd" | tr '\n' ' ')" ;; esac
|
|
5880
|
+
grep -q 'finds Git Bash' "$ROOT/eval/doctor.sh" && fail "doctor.sh holds its own copy of the Git Bash check — the plugin's would drift" \
|
|
5881
|
+
|| pass "the Git Bash check lives only in eval/lib/git-bash.sh"
|
|
5882
|
+
if [ "$IS_KIT" = 1 ]; then
|
|
5883
|
+
cmp -s "$ROOT/eval/lib/git-bash.sh" "$(cd "$ROOT/.." && pwd)/plugin/eval/lib/git-bash.sh" \
|
|
5884
|
+
&& pass "the plugin edition ships the same git-bash.sh" || fail "plugin/eval/lib/git-bash.sh is missing or differs — run packaging/build-plugin.sh"
|
|
5885
|
+
fi
|
|
5886
|
+
# Every branch, in Turkish, on a drive of its own (the one above has a default-folder Git by now, which would
|
|
5887
|
+
# answer "found" for all of them and leave the other keys untested).
|
|
5888
|
+
_gbt="$GBF/tr"; mkdir -p "$_gbt/c/Tools/Git/cmd" "$_gbt/c/Tools/Git/bin" "$_gbt/c/Users/u/AppData/Local/Programs/Git/bin"
|
|
5889
|
+
: > "$_gbt/c/Tools/Git/bin/bash.exe"; : > "$_gbt/c/Users/u/AppData/Local/Programs/Git/bin/bash.exe"
|
|
5890
|
+
printf '#!/bin/sh\n' > "$_gbt/c/Tools/Git/cmd/git"; chmod +x "$_gbt/c/Tools/Git/cmd/git"
|
|
5891
|
+
: > "$DTMP/gbmiss"; _gbtn=""
|
|
5892
|
+
for _gbe in "X=1" "CLAUDE_CODE_GIT_BASH_PATH=C:\\Tools\\Git\\bin\\bash.exe" "CLAUDE_CODE_GIT_BASH_PATH=C:\\nope\\git.exe" \
|
|
5893
|
+
"LOCALAPPDATA=C:\\Users\\u\\AppData\\Local" "PATH=$_gbt/c/Tools/Git/cmd:$GBF/bin:$PATH"; do
|
|
5894
|
+
( cd "$DTMP" && env -u CLAUDE_CODE_GIT_BASH_PATH -u LOCALAPPDATA GBC="$_gbt" PATH="$GBF/bin:$PATH" CREW_LANG=tr CREW_I18N_MISS="$DTMP/gbmiss" "$_gbe" bash "$_gbs" >/dev/null 2>&1 )
|
|
5895
|
+
_gbtn="$_gbtn$?"
|
|
5896
|
+
done
|
|
5897
|
+
[ -s "$DTMP/gbmiss" ] && fail "git-bash.sh prints English under CREW_LANG=tr: $(sort -u "$DTMP/gbmiss" | tr '\n' '|')" \
|
|
5898
|
+
|| { [ "$_gbtn" = 10113 ] && pass "git-bash.sh speaks Turkish on all five branches (no missing key; exits $_gbtn)" \
|
|
5899
|
+
|| fail "git-bash.sh Turkish sweep exited '$_gbtn', want 10113 (none · found · wrong variable · per-user · PATH only) — it did not reach every branch"; }
|
|
5900
|
+
# A bash named without .exe: MSYS says `[ -f …/bash ]` when only bash.exe exists; Claude Code does not.
|
|
5901
|
+
_gb7="$(gbdoc env CLAUDE_CODE_GIT_BASH_PATH='C:\Tools\Git\bin\bash')"
|
|
5902
|
+
case "$_gb7" in *"CLAUDE_CODE_GIT_BASH_PATH (C:\\Tools\\Git\\bin\\bash) is not a bash"*) pass "doctor: CLAUDE_CODE_GIT_BASH_PATH naming bash without .exe, where only bash.exe exists → ignored, as Claude Code does" ;;
|
|
5903
|
+
*) fail "doctor accepted a CLAUDE_CODE_GIT_BASH_PATH that exists only through the .exe suffix" ;; esac
|
|
5506
5904
|
grep -q 'command not found' "$DTMP/err" && fail "doctor.sh calls a helper before it is defined (see stderr)" \
|
|
5507
5905
|
|| pass "doctor.sh runs with no undefined-helper errors"
|
|
5508
5906
|
case "$DOUT" in *"gate activity"*) pass "doctor actually prints a gate-activity line" ;;
|
|
@@ -5688,6 +6086,309 @@ $PSOK
|
|
|
5688
6086
|
PSEOF2
|
|
5689
6087
|
[ -z "$PSFP" ] && pass "everyday PowerShell stays allowed ($PSM cases, no false positives)" \
|
|
5690
6088
|
|| fail "PowerShell false positive(s):$PSFP"
|
|
6089
|
+
|
|
6090
|
+
sec "== 12b) Crewforth's own scripts sent through PowerShell go back to the Bash tool =="
|
|
6091
|
+
# Field session: six PowerShell attempts at `bash .claude/hooks/…`, every one an error from WSL's bash, while the same
|
|
6092
|
+
# commands through the Bash tool exit 0. guard-powershell.sh stops that one call (rc 2) and names the Bash tool.
|
|
6093
|
+
# Each case is a PAIR on one line, `positive ||| negative`: the negative differs from its positive only where the rule
|
|
6094
|
+
# looks, so a JSON escape that went wrong in the fixture shows up as a positive that did not block — a mis-escaped
|
|
6095
|
+
# negative alone would pass silently, because an unreadable payload is let through. A negative must also print NOTHING.
|
|
6096
|
+
GPS="$HOOKS/guard-powershell.sh"
|
|
6097
|
+
gpsj(){ printf '{"tool_name":"%s","tool_input":{"command":"%s"},"permission_mode":"default"}' "$1" "$2"; }
|
|
6098
|
+
gpsrc(){ gpsj "$1" "$2" | bash "$GPS" >/dev/null 2>&1; echo "$?"; }
|
|
6099
|
+
GPSN=0; GPSF=""
|
|
6100
|
+
while IFS= read -r _l; do [ -z "$_l" ] && continue
|
|
6101
|
+
_pos="${_l%% ||| *}"; _neg="${_l#* ||| }"
|
|
6102
|
+
_rp="$(gpsrc PowerShell "$_pos")"; _on="$(gpsj PowerShell "$_neg" | bash "$GPS" 2>&1)"; _rn=$?
|
|
6103
|
+
[ "$_rp" = 2 ] || GPSF="$GPSF | rc $_rp (want 2): $_pos"
|
|
6104
|
+
{ [ "$_rn" = 0 ] && [ -z "$_on" ]; } || GPSF="$GPSF | rc $_rn${_on:+ + output} (want 0, silent): $_neg"
|
|
6105
|
+
GPSN=$((GPSN+1))
|
|
6106
|
+
done <<'GPSEOF'
|
|
6107
|
+
bash .claude/hooks/context-usage.sh --verbose ||| Get-Content .claude/hooks/context-usage.sh
|
|
6108
|
+
bash .claude/hooks/context-usage.sh --verbose 2>&1 | Select-Object -Last 5 ||| Select-String -Pattern bash -Path .claude/hooks/context-usage.sh
|
|
6109
|
+
& bash .claude/hooks/board.sh status ||| & mybash .claude/hooks/board.sh status
|
|
6110
|
+
& bash.exe .claude/hooks/board.sh status ||| & bashful .claude/hooks/board.sh status
|
|
6111
|
+
if (Test-Path .\\.claude\\hooks\\x.sh) { bash .claude/hooks/x.sh --verbose } ||| Test-Path .\\.claude\\hooks\\x.sh
|
|
6112
|
+
bash .claude/hooks/board.sh 2>&1 | Select-Object -First 15 ||| git config --get core.hooksPath
|
|
6113
|
+
& \"C:\\Program Files\\Git\\bin\\bash.exe\" .claude/hooks/x.sh ||| & \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build.sh
|
|
6114
|
+
bash \"C:\\Users\\John Doe\\repo\\.claude\\hooks\\board.sh\" status ||| bash \"C:\\Users\\John Doe\\repo\\scripts\\board.sh\" status
|
|
6115
|
+
bash .claude\\hooks\\x.sh ||| Get-ChildItem .claude\\hooks
|
|
6116
|
+
bash ./.claude/hooks/board.sh status ||| bash ./scripts/board.sh status
|
|
6117
|
+
bash C:\\repo\\.claude\\hooks\\board.sh ||| bash C:\\repo\\scripts\\board.sh
|
|
6118
|
+
bash .claude/eval/doctor.sh ||| rg -n bash .claude/eval/
|
|
6119
|
+
wsl bash .claude/hooks/board.sh status ||| Write-Output \"Next: bash .claude/hooks/board.sh status\"
|
|
6120
|
+
cmd /c \"bash .claude\\hooks\\board.sh status\" ||| git commit -m \"docs: bash .claude/hooks/board.sh runs in the Bash tool\"
|
|
6121
|
+
Start-Process bash -ArgumentList '.claude/hooks/x.sh' ||| Start-Process notepad -ArgumentList '.claude/hooks/x.sh'
|
|
6122
|
+
bash .claude/skills/automode-policy/scripts/check.sh ||| bash .claude/skills/automode-policy/README.md
|
|
6123
|
+
bash .claude/studio/ensure-node.sh --explain ||| Get-Content .claude/studio/ensure-node.sh
|
|
6124
|
+
GPSEOF
|
|
6125
|
+
[ "$GPSN" -ge 17 ] || fail "FIXTURE: only $GPSN case pairs were read — the heredoc broke, not the hook"
|
|
6126
|
+
[ -z "$GPSF" ] && pass "PowerShell: $GPSN shapes of Crewforth's scripts refused, their $GPSN negative twins untouched and silent" \
|
|
6127
|
+
|| fail "guard-powershell.sh:$GPSF"
|
|
6128
|
+
_gpe="$(gpsj PowerShell 'bash .claude/hooks/board.sh status' | bash "$GPS" 2>&1 >/dev/null)"
|
|
6129
|
+
case "$_gpe" in *"Bash tool"*) pass "the refusal names the Bash tool" ;; *) fail "the refusal does not name the Bash tool: ${_gpe:-<silence>}" ;; esac
|
|
6130
|
+
# Plugin edition: scripts named by absolute path under the plugin root, which the harness exports to hooks — in any of
|
|
6131
|
+
# the spellings Windows produces (native `C:\`, and Git Bash's `/c/`, which is what board.sh prints from `pwd`).
|
|
6132
|
+
_gpr='C:\Users\u\.claude\plugins\cache\crewforth\crewforth\3.0.1'; _gpf=""
|
|
6133
|
+
for _gpc in 'bash \"C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\hooks\\board.sh\" status' \
|
|
6134
|
+
'bash \"/c/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/board.sh\" sync'; do
|
|
6135
|
+
_r1="$(gpsj PowerShell "$_gpc" | CLAUDE_PLUGIN_ROOT="$_gpr" bash "$GPS" >/dev/null 2>&1; echo "$?")"
|
|
6136
|
+
_r0="$(gpsj PowerShell "$_gpc" | env -u CLAUDE_PLUGIN_ROOT bash "$GPS" >/dev/null 2>&1; echo "$?")"
|
|
6137
|
+
[ "$_r1" = 2 ] && [ "$_r0" = 0 ] || _gpf="$_gpf | rc $_r1 with the root (want 2), rc $_r0 without (want 0): $_gpc"
|
|
6138
|
+
done
|
|
6139
|
+
[ -z "$_gpf" ] && pass "plugin edition: a script under the plugin root is refused in both Windows spellings; with no plugin root the same path passes" \
|
|
6140
|
+
|| fail "plugin-root case:$_gpf"
|
|
6141
|
+
# Scope: a skill the install manifest does not list is the user's own — its scripts are not Crewforth's to redirect.
|
|
6142
|
+
_gpd="$(mktemp -d)"; mkdir -p "$_gpd/.claude/hooks"; cp "$GPS" "$_gpd/.claude/hooks/"; printf 'skills/automode-policy\n' > "$_gpd/.claude/kit-manifest.txt"
|
|
6143
|
+
_gpk="$(gpsj PowerShell 'bash .claude/skills/automode-policy/scripts/check.sh' | ( cd "$_gpd" && bash .claude/hooks/guard-powershell.sh >/dev/null 2>&1 ); echo "$?")"
|
|
6144
|
+
_gpu="$(gpsj PowerShell 'bash .claude/skills/my-own/scripts/run.sh' | ( cd "$_gpd" && bash .claude/hooks/guard-powershell.sh >/dev/null 2>&1 ); echo "$?")"
|
|
6145
|
+
rm -rf "$_gpd"
|
|
6146
|
+
[ "$_gpk" = 2 ] && [ "$_gpu" = 0 ] && pass "a listed skill's script is refused (rc 2), a skill the manifest does not list is the user's and passes (rc 0)" \
|
|
6147
|
+
|| fail "manifest scope: listed skill rc $_gpk (want 2), the user's own skill rc $_gpu (want 0)"
|
|
6148
|
+
# Never on the Bash tool: the same command there is exactly what the docs tell the model to run.
|
|
6149
|
+
[ "$(gpsrc Bash 'bash .claude/hooks/board.sh status')" = 0 ] && pass "a Bash-tool payload is never refused by guard-powershell.sh" \
|
|
6150
|
+
|| fail "guard-powershell.sh refused a Bash-tool payload — it would block the commands the docs point to"
|
|
6151
|
+
# A PowerShell command with no `bash` in it opens NO process: the hook runs on every PowerShell call, and on Git Bash
|
|
6152
|
+
# a process costs 62-135 ms. Counted from the xtrace, not timed: an external command, or a subshell (a `++` line).
|
|
6153
|
+
# The twin reads stdin the old way (`INPUT="$(cat)"`): a counter that cannot see that one fork measures nothing.
|
|
6154
|
+
_gpz(){ printf '%s' "$2" | bash -x "$1" >/dev/null 2>"$3"
|
|
6155
|
+
printf '%s' "$(( $(grep -cE '^\+ (grep|sed|awk|tr|cat|head|tail|cut|sort|find|wc|mktemp|basename|dirname|git)( |$)' "$3") + $(grep -c '^++' "$3") ))"; }
|
|
6156
|
+
_GPZ="$(mktemp -d)"; _gpzj="$(gpsj PowerShell 'Get-ChildItem -Recurse src')"
|
|
6157
|
+
_gpz0="$(_gpz "$GPS" "$_gpzj" "$_GPZ/t")"
|
|
6158
|
+
sed "s/^IFS= read -r -d '' INPUT || true\$/INPUT=\"\$(cat)\"/" "$GPS" > "$_GPZ/twin.sh"
|
|
6159
|
+
_gpz1="$(_gpz "$_GPZ/twin.sh" "$_gpzj" "$_GPZ/t2")"
|
|
6160
|
+
if ! grep -q 'read -r -d' "$_GPZ/t"; then fail "guard-powershell cost: the trace shows no stdin read — the measurement is broken, not the hook"
|
|
6161
|
+
elif [ "${_gpz1:-0}" -lt 1 ]; then fail "guard-powershell cost: the twin that reads with \$(cat) counted $_gpz1 — the counter sees nothing"
|
|
6162
|
+
elif [ "$_gpz0" = 0 ]; then pass "a PowerShell command with no 'bash' in it opens no process in guard-powershell.sh (0; the \$(cat) twin: $_gpz1)"
|
|
6163
|
+
else fail "guard-powershell.sh opens $_gpz0 process(es) on a PowerShell command with no 'bash' in it — budget 0"; fi
|
|
6164
|
+
rm -rf "$_GPZ"
|
|
6165
|
+
# Wired once, and only on PowerShell: a second wiring under Bash would cost a process on every Bash call. Every line
|
|
6166
|
+
# naming the hook is read, not the first. The plugin copy is Crewforth's only in this repository.
|
|
6167
|
+
gps_wired(){ awk -v h=guard-powershell.sh '/"matcher"/{m=$0; sub(/.*"matcher"[[:space:]]*:[[:space:]]*"/,"",m); sub(/".*/,"",m)} index($0,h){print m}' "$1"; }
|
|
6168
|
+
_gpw_f="$ROOT/settings.json"; [ "$IS_KIT" = 1 ] && _gpw_f="$_gpw_f $(cd "$ROOT/.." && pwd)/plugin/hooks/hooks.json"
|
|
6169
|
+
for _gf in $_gpw_f; do
|
|
6170
|
+
[ -f "$_gf" ] || { fail "${_gf##*/} is missing — nothing wires guard-powershell.sh"; continue; }
|
|
6171
|
+
_gm="$(gps_wired "$_gf" | tr '\n' ' ' | sed 's/ $//')"
|
|
6172
|
+
[ "$_gm" = PowerShell ] && pass "${_gf##*/}: guard-powershell.sh is wired exactly once, under the PowerShell matcher" \
|
|
6173
|
+
|| fail "${_gf##*/}: guard-powershell.sh is wired under '${_gm:-<nothing>}' — it must be exactly one PowerShell entry"
|
|
6174
|
+
done
|
|
6175
|
+
|
|
6176
|
+
# Every Crewforth hook names its shell. Left to the default, Claude Code runs a hook through PowerShell on Windows when
|
|
6177
|
+
# it does not detect Git Bash: `bash …` still starts, but the line's bash redirections become PowerShell ones, the hook
|
|
6178
|
+
# exits 0 and no gate runs (measured in the field on 3.0.1-rc.1). Paired per entry: each command object whose command
|
|
6179
|
+
# runs a Crewforth hook must carry "shell": "bash" itself. A user's own hook or a command statusLine in the same file is
|
|
6180
|
+
# theirs, not counted. Prints "<crewforth hooks> <with shell bash>".
|
|
6181
|
+
hook_shells(){ awk '{ buf = buf $0 "\n" } END {
|
|
6182
|
+
while (match(buf, /\{[^{}]*\}/)) { o = substr(buf, RSTART, RLENGTH); buf = substr(buf, RSTART + RLENGTH)
|
|
6183
|
+
if (o !~ /"type"[[:space:]]*:[[:space:]]*"command"/) continue
|
|
6184
|
+
if (!index(o, ".claude/hooks/") && !(index(o, "CLAUDE_PLUGIN_ROOT") && index(o, "/hooks/"))) continue
|
|
6185
|
+
c++; if (o ~ /"shell"[[:space:]]*:[[:space:]]*"bash"/) b++ }
|
|
6186
|
+
printf "%d %d", c, b }' "$1"; }
|
|
6187
|
+
_hsd="$(mktemp -d)"
|
|
6188
|
+
printf '%s\n' '{"statusLine":{"type":"command","command":"my-status"},"hooks":{"Stop":[{"hooks":[' \
|
|
6189
|
+
'{"type":"command","shell":"bash","command":"bash .claude/hooks/a.sh"},{"type":"command","command":"bash .claude/hooks/b.sh"},' \
|
|
6190
|
+
'{"type":"command","command":"my-own-hook"}]}]}}' > "$_hsd/t.json"
|
|
6191
|
+
if [ "$(hook_shells "$_hsd/t.json")" != "2 1" ]; then fail "hook shell pin: the counter read '$(hook_shells "$_hsd/t.json")' on a fixture with 2 Crewforth hooks (1 with shell) beside a user hook and a statusLine — want '2 1'; the measurement is broken, not the settings"
|
|
6192
|
+
else for _gf in $_gpw_f; do
|
|
6193
|
+
[ -f "$_gf" ] || continue
|
|
6194
|
+
set -- $(hook_shells "$_gf")
|
|
6195
|
+
if [ "${1:-0}" -lt 1 ]; then fail "${_gf##*/}: no Crewforth command hook counted — the pin read nothing"
|
|
6196
|
+
elif [ "$1" = "$2" ]; then pass "${_gf##*/}: all $1 Crewforth command hooks run under \"shell\": \"bash\""
|
|
6197
|
+
else fail "${_gf##*/}: $2 of $1 Crewforth command hooks say \"shell\": \"bash\" — the rest run under PowerShell when Claude Code finds no Git Bash, and fail open"; fi
|
|
6198
|
+
done; fi
|
|
6199
|
+
rm -rf "$_hsd"
|
|
6200
|
+
|
|
6201
|
+
sec "== 12c) every command the model is told to run says: the Bash tool, not PowerShell =="
|
|
6202
|
+
# SCOPE, one rule (decided): Crewforth's own scripts — every `.sh` the payload ships, read from the payload itself, so a
|
|
6203
|
+
# script a later version adds anywhere is covered without editing this list. WHERE: every text the model reads — the
|
|
6204
|
+
# discipline, AGENT_TEMPLATE, agents, skills (markdown), and what the scripts and installers print (shell).
|
|
6205
|
+
# Markdown unit: the paragraph, ended by a blank line, a frontmatter fence, or the start of a list item; a code fence
|
|
6206
|
+
# belongs to the paragraph right above it. Shell unit: the line — a command quoted as TEXT (at its own `$( )` level;
|
|
6207
|
+
# executed or piped is code, not text), or the line that expands a variable holding one (`X='bash …'`) — the command
|
|
6208
|
+
# is often stored in one place and printed in another. A file that defines pass() and fail() is a test harness: its
|
|
6209
|
+
# strings are fixtures for other code, not output, and it is not scanned.
|
|
6210
|
+
# Checked in the Crewforth repository, where these texts are written: an install carries the same bytes plus the
|
|
6211
|
+
# user's own files, which the rule does not cover.
|
|
6212
|
+
if [ "$IS_KIT" != 1 ]; then
|
|
6213
|
+
skip scope "Bash-tool inventory runs in the Crewforth repository — an installed project also holds the user's own skills and scripts"
|
|
6214
|
+
else
|
|
6215
|
+
BT_PH='Bash tool, not PowerShell'
|
|
6216
|
+
BT_SET="$(cd "$ROOT" && find . -name '*.sh' -type f | sed 's#^\./##' | LC_ALL=C sort)"
|
|
6217
|
+
BT_CMD='bash[[:space:]]+("?\$\{?(CLAUDE_PROJECT_DIR|CLAUDE_PLUGIN_ROOT)\}?"?/|\./)?\.claude/[A-Za-z0-9_./-]+\.sh'
|
|
6218
|
+
BT_PLG='bash[[:space:]]+"?\$\{CLAUDE_PLUGIN_ROOT\}/[A-Za-z0-9_./-]+\.sh'
|
|
6219
|
+
bt_md(){ # $1 = markdown -> offending "file:line: text"; stderr = number of command lines seen
|
|
6220
|
+
BT_SET="$BT_SET" BT_CMD="$BT_CMD" BT_PLG="$BT_PLG" BT_PH="$BT_PH" awk -v f="$1" '
|
|
6221
|
+
BEGIN { ns = split(ENVIRON["BT_SET"], a, "\n"); for (i = 1; i <= ns; i++) S[a[i]] = 1 }
|
|
6222
|
+
function cmdline(s, r, m, hit) { hit = 0
|
|
6223
|
+
while (match(s, ENVIRON["BT_CMD"]) || match(s, ENVIRON["BT_PLG"])) {
|
|
6224
|
+
m = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
|
6225
|
+
r = m; sub(/^.*(\.claude|CLAUDE_PLUGIN_ROOT\})\//, "", r); if (r in S) hit = 1 }
|
|
6226
|
+
return hit }
|
|
6227
|
+
function flush() { if (hit != "" && index(blk, ENVIRON["BT_PH"]) == 0) printf "%s", hit; blk = ""; hit = "" }
|
|
6228
|
+
FNR == 1 && /^---[[:space:]]*$/ { fm = 1; next }
|
|
6229
|
+
fm { if (/^---[[:space:]]*$/) fm = 0; next }
|
|
6230
|
+
/^[[:space:]]*$/ && !fence { pend = 1; next }
|
|
6231
|
+
{ isf = ($0 ~ /^[[:space:]]*```/)
|
|
6232
|
+
item = !fence && ($0 ~ /^[[:space:]]{0,3}([0-9]+\.|[-*])[[:space:]]/)
|
|
6233
|
+
if ((pend && !(isf && !fence)) || item) flush(); pend = 0
|
|
6234
|
+
if (isf) fence = !fence
|
|
6235
|
+
blk = blk "\n" $0
|
|
6236
|
+
if (cmdline($0)) { hit = hit f ":" FNR ": " $0 "\n"; n++ } }
|
|
6237
|
+
END { flush(); print n + 0 > "/dev/stderr" }' "$1"; }
|
|
6238
|
+
bt_sh(){ # $1 = shell file -> offending "file:line: text"; stderr = number of lines checked
|
|
6239
|
+
BT_SET="$BT_SET" BT_CMD="$BT_CMD" BT_PH="$BT_PH" awk -v f="$1" '
|
|
6240
|
+
BEGIN { ns = split(ENVIRON["BT_SET"], a, "\n"); for (i = 1; i <= ns; i++) S[a[i]] = 1 }
|
|
6241
|
+
# quoted(p): is position p of line L inside a quote at its own $( ) level?
|
|
6242
|
+
function quoted(L, p, i, c, d, sq, dq, ss, ds) { d = 0; sq = 0; dq = 0
|
|
6243
|
+
for (i = 1; i < p; i++) { c = substr(L, i, 1)
|
|
6244
|
+
if (sq) { if (c == "\047") sq = 0; continue }
|
|
6245
|
+
if (c == "\\") { i++; continue }
|
|
6246
|
+
if (c == "$" && substr(L, i + 1, 1) == "(") { ss[++d] = sq; ds[d] = dq; sq = 0; dq = 0; i++; continue }
|
|
6247
|
+
if (c == ")" && !dq && d > 0) { sq = ss[d]; dq = ds[d]; d--; continue }
|
|
6248
|
+
if (c == "\"") dq = !dq; else if (c == "\047" && !dq) sq = 1 }
|
|
6249
|
+
return sq || dq }
|
|
6250
|
+
/^[[:space:]]*#/ { next }
|
|
6251
|
+
{ L = $0; s = L; off = 0; text = 0; carrier = ""
|
|
6252
|
+
while (match(s, ENVIRON["BT_CMD"])) {
|
|
6253
|
+
m = substr(s, RSTART, RLENGTH); p = off + RSTART; s = substr(s, RSTART + RLENGTH); off = p + RLENGTH - 1
|
|
6254
|
+
r = m; sub(/^.*\.claude\//, "", r); if (!(r in S)) continue
|
|
6255
|
+
if (!quoted(L, p)) continue
|
|
6256
|
+
pre = substr(L, 1, p - 1)
|
|
6257
|
+
if (match(pre, /[A-Za-z_][A-Za-z0-9_]*=["\047]$/)) { v = substr(pre, RSTART, RLENGTH - 2); C[v] = 1; carrier = v }
|
|
6258
|
+
else text = 1 }
|
|
6259
|
+
if (text) { n++; if (index(L, ENVIRON["BT_PH"]) == 0) print f ":" FNR ": " L }
|
|
6260
|
+
else if (carrier == "") { for (v in C) if (index(L, "$" v) || index(L, "${" v "}")) { n++; if (index(L, ENVIRON["BT_PH"]) == 0) print f ":" FNR ": " L; break } } }
|
|
6261
|
+
END { print n + 0 > "/dev/stderr" }' "$1"; }
|
|
6262
|
+
BT_MISS=""; _btn=0; _btf=0; _bte=0; _BTT="$(mktemp)"
|
|
6263
|
+
{ for _bf in "$ROOT/CLAUDE.md" "$ROOT/AGENT_TEMPLATE.md" "$ROOT"/agents/*.md; do [ -f "$_bf" ] && printf '%s\n' "$_bf"; done
|
|
6264
|
+
find "$ROOT/skills" -name '*.md' -type f 2>/dev/null | LC_ALL=C sort; } > "$_BTT"
|
|
6265
|
+
while IFS= read -r _bf; do _btf=$((_btf+1))
|
|
6266
|
+
_o="$(bt_md "$_bf" 2>"$_BTT.n")"; _btn=$((_btn + $(cat "$_BTT.n")))
|
|
6267
|
+
[ -n "$_o" ] && BT_MISS="$BT_MISS
|
|
6268
|
+
$_o"
|
|
6269
|
+
done < "$_BTT"
|
|
6270
|
+
# The independent count the markdown number must equal: a plain grep of the same files for the same command shape.
|
|
6271
|
+
_bte="$(while IFS= read -r _bf; do grep -hoE "$BT_CMD|$BT_PLG" "$_bf" 2>/dev/null; done < "$_BTT" \
|
|
6272
|
+
| sed -E 's#^.*(\.claude|CLAUDE_PLUGIN_ROOT\})/##' | while IFS= read -r _r; do printf '%s\n' "$BT_SET" | grep -qxF "$_r" && echo x; done | wc -l | tr -d ' ')"
|
|
6273
|
+
_bts=0; _btsf=0; _btr="$(cd "$ROOT/.." && pwd)"
|
|
6274
|
+
for _bf in $(cd "$ROOT" && find . -name '*.sh' -type f | LC_ALL=C sort | sed "s#^\./#$ROOT/#") "$_btr/adopt.sh" "$_btr/start.sh"; do
|
|
6275
|
+
[ -f "$_bf" ] || continue
|
|
6276
|
+
grep -qE '^[[:space:]]*pass\(\)[[:space:]]*\{' "$_bf" && grep -qE '^[[:space:]]*fail\(\)[[:space:]]*\{' "$_bf" && continue # a test harness
|
|
6277
|
+
_btsf=$((_btsf+1)); _o="$(bt_sh "$_bf" 2>"$_BTT.n")"; _bts=$((_bts + $(cat "$_BTT.n")))
|
|
6278
|
+
[ -n "$_o" ] && BT_MISS="$BT_MISS
|
|
6279
|
+
$_o"
|
|
6280
|
+
done
|
|
6281
|
+
rm -f "$_BTT" "$_BTT.n"
|
|
6282
|
+
if [ "$_btf" -lt 50 ] || [ "$_btn" -lt 15 ] || [ "$_bts" -lt 8 ]; then
|
|
6283
|
+
fail "FIXTURE: the Bash-tool inventory read $_btf markdown file(s), $_btn markdown and $_bts shell command line(s) — the scan broke, not the text"
|
|
6284
|
+
elif [ "$_btn" != "$_bte" ]; then
|
|
6285
|
+
fail "the markdown scan saw $_btn command line(s), a plain grep of the same files finds $_bte — the scan is skipping some"
|
|
6286
|
+
elif [ -n "$BT_MISS" ]; then
|
|
6287
|
+
fail "a command the model is told to run does not say '$BT_PH' in its paragraph (markdown) or on its line (shell):$(printf '%s\n' "$BT_MISS" | sed '/^$/d' | head -n 8 | sed 's/^/\n /')"
|
|
6288
|
+
else
|
|
6289
|
+
pass "every one of Crewforth's $(printf '%s\n' "$BT_SET" | grep -c .) shipped scripts, wherever the model is told to run it, says '$BT_PH': $_btn of $_bte markdown line(s) in $_btf files, $_bts printed line(s) in $_btsf script(s)"
|
|
6290
|
+
fi
|
|
6291
|
+
# Must-fail twins, on copies: the phrase removed from one markdown paragraph, one list item beside a kept one, one
|
|
6292
|
+
# printed hook line, and a variable-carried command printed without it.
|
|
6293
|
+
_btt="$(mktemp -d)"
|
|
6294
|
+
sed 's/ (Bash tool, not PowerShell)//' "$ROOT/skills/crew-doctor/SKILL.md" > "$_btt/a.md"
|
|
6295
|
+
sed 's/ (Bash tool, not PowerShell) and show/ and show/' "$ROOT/skills/crew-update/SKILL.md" > "$_btt/b.md"
|
|
6296
|
+
sed 's/\(--[a-z]*-one %s\) (Bash tool, not PowerShell)/\1/g' "$HOOKS/skill-trust.sh" > "$_btt/c.sh"
|
|
6297
|
+
sed 's/ (Bash tool, not PowerShell — or the user can type \/crew-board sync)/ (or the user can type \/crew-board sync)/' "$HOOKS/guard-write.sh" > "$_btt/d.sh"
|
|
6298
|
+
_bt1="$(bt_md "$_btt/a.md" 2>/dev/null)"; _bt2="$(bt_md "$_btt/b.md" 2>/dev/null)"
|
|
6299
|
+
_bt3="$(bt_sh "$_btt/c.sh" 2>/dev/null)"; _bt4="$(bt_sh "$_btt/d.sh" 2>/dev/null)"
|
|
6300
|
+
[ -n "$_bt1" ] && [ -n "$_bt2" ] && [ -n "$_bt3" ] && [ -n "$_bt4" ] \
|
|
6301
|
+
&& pass "the inventory catches a removed phrase in a paragraph, in one list item of several, on a printed line, and where a stored command is printed (4 twins)" \
|
|
6302
|
+
|| fail "the inventory missed a removed phrase — paragraph:${_bt1:+ caught} list item:${_bt2:+ caught} printed:${_bt3:+ caught} stored:${_bt4:+ caught}"
|
|
6303
|
+
rm -rf "$_btt"
|
|
6304
|
+
fi
|
|
6305
|
+
sec "== 12d) the plugin's own gate files are guarded like the project's =="
|
|
6306
|
+
# The plugin edition keeps its gate scripts and its hook wiring under the plugin root, not under .claude/, and the
|
|
6307
|
+
# project-path rules never matched there: `rm <plugin>/hooks/guard-bash.sh` returned rc 0 while
|
|
6308
|
+
# `rm .claude/hooks/guard-bash.sh` returned rc 2 (measured, 3.0.1 review). Same rule now: writing, deleting or renaming
|
|
6309
|
+
# anything under <plugin>/hooks/ or <plugin>/.claude-plugin/ is refused — and under the same two folders of every
|
|
6310
|
+
# other version of this plugin in Claude Code's cache, since an older copy is one path away. Reading stays free.
|
|
6311
|
+
# Pairs, `positive ||| negative`: @R@ is this version's root, @O@ another cached version. The negative reads the same
|
|
6312
|
+
# file, so a fixture that broke shows up as a positive that passed.
|
|
6313
|
+
PGR='/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1'; PGO='/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.0'
|
|
6314
|
+
PGRW='C:\Users\u\.claude\plugins\cache\crewforth\crewforth\3.0.1'
|
|
6315
|
+
pgrc(){ # $1 = hook, $2 = plugin root, $3 = payload -> rc
|
|
6316
|
+
printf '%s' "$3" | CLAUDE_PLUGIN_ROOT="$2" bash "$HOOKS/$1" >/dev/null 2>&1; echo "$?"; }
|
|
6317
|
+
pgpair(){ # $1 = hook, $2 = root, $3 = payload builder, $4 = pos, $5 = neg -> appends to PGF, counts PGN
|
|
6318
|
+
local p n
|
|
6319
|
+
# An empty payload is the fixture breaking (a helper missing in this scope), never the hook: say so, do not grade.
|
|
6320
|
+
[ -n "$($3 "$4" 2>/dev/null)" ] || { PGF="$PGF | FIXTURE: $3 built no payload"; PGN=$((PGN+1)); return 0; }
|
|
6321
|
+
p="$(pgrc "$1" "$2" "$($3 "$4")")"; n="$(pgrc "$1" "$2" "$($3 "$5")")"
|
|
6322
|
+
[ "$p" = 2 ] || PGF="$PGF | $1 rc $p (want 2): $4"; [ "$n" = 0 ] || PGF="$PGF | $1 rc $n (want 0): $5"; PGN=$((PGN+1)); }
|
|
6323
|
+
# Own builders: this section also runs in an installed project's smoke (install scope), where the unit section that
|
|
6324
|
+
# defines the shared ones is skipped — `wj` there was "command not found" and every Write pair read as a hook failure.
|
|
6325
|
+
pgbash(){ printf '{"tool_name":"Bash","permission_mode":"default","tool_input":{"command":"%s"}}' "$1"; }
|
|
6326
|
+
pgps(){ printf '{"tool_name":"PowerShell","tool_input":{"command":"%s"},"permission_mode":"default"}' "$1"; }
|
|
6327
|
+
pgwr(){ printf '{"tool_name":"Write","tool_input":{"file_path":"%s"}}' "$1"; }
|
|
6328
|
+
PGN=0; PGF=""
|
|
6329
|
+
while IFS= read -r _l; do [ -z "$_l" ] && continue
|
|
6330
|
+
_l="${_l//@R@/$PGR}"; _l="${_l//@O@/$PGO}"
|
|
6331
|
+
pgpair guard-bash.sh "$PGR" pgbash "${_l%% ||| *}" "${_l#* ||| }"
|
|
6332
|
+
done <<'PGEOF'
|
|
6333
|
+
rm @R@/hooks/guard-bash.sh ||| cat @R@/hooks/guard-bash.sh
|
|
6334
|
+
echo x > \"@R@/hooks/guard-bash.sh\" ||| grep -n block \"@R@/hooks/guard-bash.sh\"
|
|
6335
|
+
mv @R@/hooks/guard-write.sh /tmp/gw ||| ls -la @R@/hooks
|
|
6336
|
+
cp /tmp/x @R@/hooks/hooks.json ||| wc -l @R@/hooks/hooks.json
|
|
6337
|
+
sed -i.bak s/2/0/ @R@/hooks/guard-bash.sh ||| sed -n 1p @R@/hooks/guard-bash.sh
|
|
6338
|
+
rm @R@/.claude-plugin/plugin.json ||| cat @R@/.claude-plugin/plugin.json
|
|
6339
|
+
rm @O@/hooks/guard-bash.sh ||| cat @O@/hooks/guard-bash.sh
|
|
6340
|
+
truncate -s 0 @O@/hooks/context-usage.sh ||| head -3 @O@/hooks/context-usage.sh
|
|
6341
|
+
ln -s @R@ cfg ||| ln -s @R@/skills/teamboard skill-link
|
|
6342
|
+
rm \"@R@/hooks/board.sh\" ||| bash \"@R@/hooks/board.sh\" status
|
|
6343
|
+
rm ~/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh ||| cat ~/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh
|
|
6344
|
+
echo 'exit 0' > \"$HOME/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh\" ||| cat \"$HOME/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh\"
|
|
6345
|
+
rm /Users/*/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh ||| ls /Users/*/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks
|
|
6346
|
+
rm /Users/u//.claude/plugins/cache/crewforth//./crewforth/3.0.1/hooks/guard-bash.sh ||| cat /Users/u//.claude/plugins/cache/crewforth//./crewforth/3.0.1/hooks/guard-bash.sh
|
|
6347
|
+
rm @R@/../3.0.1/hooks/guard-bash.sh ||| cat @R@/../3.0.1/hooks/guard-bash.sh
|
|
6348
|
+
rm /Users/u/.claude/plugins/cache/crewforth/crewforth/\"3.0.1\"/hooks/guard-bash.sh ||| cat /Users/u/.claude/plugins/cache/crewforth/crewforth/\"3.0.1\"/hooks/guard-bash.sh
|
|
6349
|
+
cd ~/.claude/plugins/cache && rm crewforth/crewforth/3.0.1/hooks/guard-bash.sh ||| cd ~/.claude/plugins/cache && cat crewforth/crewforth/3.0.1/hooks/guard-bash.sh
|
|
6350
|
+
ln -s ~/.claude/plugins/cache/crewforth c ||| ln -s ~/.claude/plugins/cache/crewforth/crewforth/3.0.1/skills c
|
|
6351
|
+
echo x > p/crewforth/crewforth/3.0.1/hooks/guard-bash.sh ||| cat p/crewforth/crewforth/3.0.1/hooks/guard-bash.sh
|
|
6352
|
+
echo 'exit 0' > @R@/eval/lib/crew-env.sh ||| cat @R@/eval/lib/crew-env.sh
|
|
6353
|
+
echo 'exit 0' > .claude/eval/lib/crew-env.sh ||| cat .claude/eval/lib/crew-env.sh
|
|
6354
|
+
rm /Users/b/Projects/crewforth/crewforth/3.0.1/hooks/x.sh ||| rm /Users/b/Projects/crewforth/plugin/hooks/x.sh
|
|
6355
|
+
PGEOF
|
|
6356
|
+
# Windows spellings: the root as the harness exports it (native), the command as PowerShell and Git Bash write it.
|
|
6357
|
+
while IFS= read -r _l; do [ -z "$_l" ] && continue
|
|
6358
|
+
pgpair guard-bash.sh "$PGRW" pgps "${_l%% ||| *}" "${_l#* ||| }"
|
|
6359
|
+
done <<'PGEOF2'
|
|
6360
|
+
Remove-Item C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\hooks\\guard-bash.sh ||| Get-Content C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\hooks\\guard-bash.sh
|
|
6361
|
+
Rename-Item C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.0\\hooks\\hooks.json x.json ||| Test-Path C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.0\\hooks\\hooks.json
|
|
6362
|
+
Remove-Item $env:USERPROFILE\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\hooks\\guard-bash.sh ||| Get-Content $env:USERPROFILE\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\hooks\\guard-bash.sh
|
|
6363
|
+
cmd /c mklink /J cfg C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1 ||| cmd /c mklink /J cfg C:\\Users\\u\\.claude\\plugins\\cache\\crewforth\\crewforth\\3.0.1\\skills
|
|
6364
|
+
PGEOF2
|
|
6365
|
+
pgpair guard-bash.sh "$PGRW" pgbash 'rm /c/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh' 'cat /c/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1/hooks/guard-bash.sh'
|
|
6366
|
+
pgpair guard-bash.sh "$PGRW" pgbash 'ln -s C:/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1 cfg' 'ln -s C:/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1/skills cfg'
|
|
6367
|
+
pgpair guard-bash.sh "$PGRW" pgbash 'ln -s /c/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1 cfg' 'ln -s /c/Users/u/.claude/plugins/cache/crewforth/crewforth/3.0.1/skills cfg'
|
|
6368
|
+
# A root outside the cache (a --plugin-dir checkout) is matched as a whole path: at a token start, after a drive or
|
|
6369
|
+
# Git Bash's /c — not inside a longer path, and its dots are literal.
|
|
6370
|
+
pgpair guard-bash.sh '/opt/cr.w/plugin' pgbash 'rm /opt/cr.w/plugin/hooks/a' 'rm ~/w/opt/cr.w/plugin/hooks/a'
|
|
6371
|
+
pgpair guard-bash.sh '/opt/cr.w/plugin' pgbash 'rm /c/opt/cr.w/plugin/hooks/a' 'rm /opt/crXw/plugin/hooks/a'
|
|
6372
|
+
# The Write/Edit side: every file under the two folders, not only the gate scripts it knew by name.
|
|
6373
|
+
while IFS= read -r _l; do [ -z "$_l" ] && continue
|
|
6374
|
+
_l="${_l//@R@/$PGR}"; _l="${_l//@O@/$PGO}"
|
|
6375
|
+
pgpair guard-write.sh "$PGR" pgwr "${_l%% ||| *}" "${_l#* ||| }"
|
|
6376
|
+
done <<'PGEOF3'
|
|
6377
|
+
@R@/hooks/hooks.json ||| @R@/skills/teamboard/notes.md
|
|
6378
|
+
@R@/.claude-plugin/plugin.json ||| @R@/README.md
|
|
6379
|
+
@O@/hooks/context-usage.sh ||| /Users/u/project/hooks/context-usage.sh
|
|
6380
|
+
@R@/hooks/trace-blocklist.txt ||| /Users/u/project/.claude-plugin-notes/plugin.json
|
|
6381
|
+
@R@/eval/lib/crew-env.sh ||| @R@/eval/lib/skill-listing.awk
|
|
6382
|
+
/p/.claude/eval/lib/crew-env.sh ||| /p/.claude/eval/lib/settings-json.awk
|
|
6383
|
+
PGEOF3
|
|
6384
|
+
[ "$PGN" -ge 37 ] || fail "FIXTURE: only $PGN plugin-gate pair(s) were read — the heredocs broke, not the hooks"
|
|
6385
|
+
# Reading and running stay free, and another plugin in the same cache is not Crewforth's to guard.
|
|
6386
|
+
_pgok=""
|
|
6387
|
+
for _c in "rm /Users/u/.claude/plugins/cache/acme/tool/1.0/hooks/x.sh" "diff $PGO/hooks/guard-bash.sh $PGR/hooks/guard-bash.sh"; do
|
|
6388
|
+
[ "$(pgrc guard-bash.sh "$PGR" "$(pgbash "$_c")")" = 0 ] || _pgok="$_pgok | $_c"
|
|
6389
|
+
done
|
|
6390
|
+
[ -z "$PGF$_pgok" ] && pass "plugin gate files: $PGN writes, deletes, renames and links refused (this version, another cached version, ~/\$HOME/glob/..//. spellings, Windows spellings, a root outside the cache, the sourced crew-env.sh, Write tool); each negative twin — reading, running board.sh, a source tree, another plugin — stays free" \
|
|
6391
|
+
|| fail "plugin gate files:$PGF${_pgok:+ | wrongly refused:$_pgok}"
|
|
5691
6392
|
sec "== 13) pre-commit cost — the gate people route around is the one that is slow =="
|
|
5692
6393
|
# Measured on a 373-file merge: the old file loop spawned ~7 processes per file (three `printf | grep` pairs and
|
|
5693
6394
|
# a `git cat-file`), 2,643 in total. At the 62-135 ms a Git Bash process was measured to cost on a Windows 11
|
|
@@ -6060,29 +6761,47 @@ fi
|
|
|
6060
6761
|
# The front page. Kit repo only: an installed project has no README of ours.
|
|
6061
6762
|
if [ "$IS_KIT" = 1 ]; then
|
|
6062
6763
|
KR="$(cd "$ROOT/.." && pwd)"
|
|
6063
|
-
# THE FRONT PAGE'S ORDER, pinned (3.0 rewrite, Barış's decision): the definition sentence, then the
|
|
6064
|
-
#
|
|
6065
|
-
#
|
|
6066
|
-
#
|
|
6067
|
-
|
|
6068
|
-
|
|
6764
|
+
# THE FRONT PAGE'S ORDER, pinned (3.0 rewrite, Barış's decision): the definition sentence, then the overview
|
|
6765
|
+
# (the video, and under it the one line linking crewforth.com/#overview), then the quick start with
|
|
6766
|
+
# `npx crewforth init` — the overview link only once. The pin is the #overview link, not the video URL, so
|
|
6767
|
+
# swapping the video does not touch this check.
|
|
6768
|
+
# The npm page has no overview; its definition still comes before the install line. Order is read by line number,
|
|
6769
|
+
# so a page that has all three pieces in the wrong order is red, not green.
|
|
6770
|
+
FP_OV="https://crewforth.com/#overview"
|
|
6771
|
+
fp_order(){ # $1 file $2 definition sentence $3 1 = the overview must sit between the two -> problems, ;-separated
|
|
6772
|
+
awk -v d="$2" -v g="$3" -v o="$FP_OV" '
|
|
6069
6773
|
!dl && index($0,d) {dl=NR}
|
|
6070
|
-
index($0,
|
|
6774
|
+
index($0,o) {if(!gl) gl=NR; gn++}
|
|
6071
6775
|
!ql && index($0,"npx crewforth init") {ql=NR}
|
|
6072
6776
|
END { if(!dl) print "no definition sentence"; if(!ql) print "no npx crewforth init"
|
|
6073
6777
|
if(dl && ql && ql<dl) print "quick start above the definition"
|
|
6074
|
-
if(g==1) { if(!gl) print "no
|
|
6778
|
+
if(g==1) { if(!gl) print "no " o " link"; else if(gn>1) print o " linked " gn " times"
|
|
6075
6779
|
else if(dl && ql && (gl<dl || gl>ql)) print "overview not between the definition and the quick start" } }' "$1" 2>/dev/null | tr '\n' ';'; }
|
|
6076
6780
|
FP_EN="Crewforth is your engineering crew for Claude Code."; FP_TR="Crewforth, Claude Code için mühendislik ekibinizdir."
|
|
6077
6781
|
_fp=""
|
|
6078
6782
|
_o="$(fp_order "$KR/README.md" "$FP_EN" 1)"; [ -z "$_o" ] || _fp="$_fp README.md($_o)"
|
|
6079
6783
|
_o="$(fp_order "$KR/README.tr.md" "$FP_TR" 1)"; [ -z "$_o" ] || _fp="$_fp README.tr.md($_o)"
|
|
6080
6784
|
_o="$(fp_order "$KR/README.npm.md" "$FP_EN" 0)"; [ -z "$_o" ] || _fp="$_fp README.npm.md($_o)"
|
|
6081
|
-
# Must-fail twin: the same README with the
|
|
6082
|
-
_fpt="$(mktemp)"; grep -
|
|
6785
|
+
# Must-fail twin: the same README with the overview link moved to the end must be read as out of order.
|
|
6786
|
+
_fpt="$(mktemp)"; grep -vF "$FP_OV" "$KR/README.md" > "$_fpt"; grep -F "$FP_OV" "$KR/README.md" >> "$_fpt"
|
|
6083
6787
|
if [ -n "$_fp" ]; then fail "front page order is wrong:$_fp"
|
|
6084
6788
|
elif [ -z "$(fp_order "$_fpt" "$FP_EN" 1)" ]; then fail "the front-page check passed a README whose overview was moved below the quick start — it reads nothing"
|
|
6085
6789
|
else pass "front page: definition → overview → npx crewforth init on both GitHub READMEs (overview once), definition before install on npm; an overview moved below is caught"; fi
|
|
6790
|
+
# Two strings that must never reach a README: VIDEO_URL, the placeholder the video link is written over, so a
|
|
6791
|
+
# placeholder cannot be merged by accident; and overview-poster.jpg, the image the video replaced — the file
|
|
6792
|
+
# stays in assets/, so nothing else would notice it coming back. Each has a must-fail twin: the real README
|
|
6793
|
+
# with the string planted must be caught, or the check is reading nothing.
|
|
6794
|
+
fp_forbid(){ # $1 fixed string, rest files -> " name(count)" for every file that holds it
|
|
6795
|
+
local n="$1" f c o=""; shift
|
|
6796
|
+
for f in "$@"; do c="$(grep -cF -- "$n" "$f" 2>/dev/null)"; [ "${c:-0}" -gt 0 ] && o="$o ${f##*/}($c)"; done
|
|
6797
|
+
printf '%s' "$o"; }
|
|
6798
|
+
for _fs in VIDEO_URL overview-poster.jpg; do
|
|
6799
|
+
_hit="$(fp_forbid "$_fs" "$KR/README.md" "$KR/README.tr.md" "$KR/README.npm.md")"
|
|
6800
|
+
grep -vF -- "$_fs" "$KR/README.md" > "$_fpt"; printf '%s\n' "$_fs" >> "$_fpt"
|
|
6801
|
+
if [ -n "$_hit" ]; then fail "$_fs is in a README:$_hit — it must not be merged"
|
|
6802
|
+
elif [ -z "$(fp_forbid "$_fs" "$_fpt")" ]; then fail "the $_fs check passed a README with $_fs planted in it — it reads nothing"
|
|
6803
|
+
else pass "$_fs is in none of the 3 READMEs; one planted is caught"; fi
|
|
6804
|
+
done
|
|
6086
6805
|
rm -f "$_fpt"
|
|
6087
6806
|
# Every assets/ file a README points at exists — src=, srcset= and the npm README's absolute raw URL alike.
|
|
6088
6807
|
# The count is printed: "no broken image" means nothing unless it says how many references it looked at.
|
|
@@ -6094,7 +6813,7 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
6094
6813
|
elif [ -z "$_miss" ]; then pass "every README and site-page asset reference resolves ($_nref of $_nref distinct files exist)"
|
|
6095
6814
|
else fail "README points at missing assets:$_miss"; fi
|
|
6096
6815
|
else
|
|
6097
|
-
skip scope "front-page checks skipped (installed project — the READMEs live in the Crewforth repo)"
|
|
6816
|
+
skip scope "front-page checks skipped (installed project — the READMEs live in the Crewforth repo)" 4
|
|
6098
6817
|
fi
|
|
6099
6818
|
|
|
6100
6819
|
sec "== 14c) the 3.0 rename left no old name behind — outside history and the code that reads the old names =="
|
|
@@ -6109,7 +6828,7 @@ if [ -n "$SGR" ] && [ -d "$SGR/packaging" ] && [ -f "$SGR/VERSION" ] && [ -d "$S
|
|
|
6109
6828
|
RN_ALLOW='CHANGELOG.md 196 history: every entry before 3.0 keeps the name it shipped under
|
|
6110
6829
|
README*.md site/content/*/install.md 4 migration: the 2.x plugin note names the plugin to uninstall — one line in each README and install page, EN + TR
|
|
6111
6830
|
evals/results/* 6 history: recorded eval runs stay byte-for-byte
|
|
6112
|
-
adopt.sh
|
|
6831
|
+
adopt.sh kit/legacy-blobs.tsv 347 migration: finds and moves 2.x names (components, CLAUDE.md, board, auto-mode rules, variables), and the generated list of the 1.x/2.x files it may move
|
|
6113
6832
|
bin/cli.js 4 migration: add accepts a typed <x>-csk and moves an add record written under the old names
|
|
6114
6833
|
evals/run.sh 4 compat: reads the 2.x trusted eval parent when the 3.0 one is absent — removed in 4.0
|
|
6115
6834
|
site/scripts/check.mjs 7 tests: the old-name pattern of the built-site gate, and its twins
|
|
@@ -6120,7 +6839,7 @@ site/scripts/check.mjs 7 tests: the old-name pattern of the built-site gate, and
|
|
|
6120
6839
|
*/studio/web/storage-migrate.js 4 migration: moves the panel'"'"'s saved layout to the new keys — removed in 4.0
|
|
6121
6840
|
kit/eval/smoke-test.sh 36 tests: this gate'"'"'s own pattern, and that the 2.x names still work
|
|
6122
6841
|
packaging/legacy-npm/* 10 the 2.x package name'"'"'s 3.0.0: a forwarder to crewforth, published once by hand
|
|
6123
|
-
packaging/*
|
|
6842
|
+
packaging/* 154 tests: the migration rehearsal on the real v2.13.0 tree, the legacy token and layout checks; the legacy blob list'"'"'s generator and its real-installer check'
|
|
6124
6843
|
# A line may name several globs, separated by spaces, when one reason covers them all; its pin is their sum. Split
|
|
6125
6844
|
# with `read -a`, never a bare `for g in $globs`, which would expand each pattern against the working directory.
|
|
6126
6845
|
_rn_match(){ # $1 = a path -> the allow-list line (its glob field) that covers it, or nothing
|
|
@@ -6224,6 +6943,10 @@ $(printf '%s\n' "$_nh" | head -n 5 | sed 's/^/ /')"
|
|
|
6224
6943
|
echo x >> "$_kd/n"
|
|
6225
6944
|
case "$_l" in
|
|
6226
6945
|
https://crewforth.com|https://crewforth.com/|https://crewforth.com/tr|https://crewforth.com/tr/) ;;
|
|
6946
|
+
# The home page with an anchor: no content page to look for, so the anchor itself must be on it.
|
|
6947
|
+
https://crewforth.com/\#*|https://crewforth.com/tr/\#*)
|
|
6948
|
+
grep -qF "id=\"${_l##*#}\"" "$KR/site/src/components/Home.astro" 2>/dev/null \
|
|
6949
|
+
|| printf '%s: %s has no id="%s" on the home page\n' "${_f#$KR/}" "$_l" "${_l##*#}" ;;
|
|
6227
6950
|
https://crewforth.com/tr/*) _pg="${_l#https://crewforth.com/tr/}"; [ -f "$KR/site/content/tr/${_pg%%[#?]*}.md" ] || printf '%s: %s has no site/content/tr page\n' "${_f#$KR/}" "$_l" ;;
|
|
6228
6951
|
https://crewforth.com/*) _pg="${_l#https://crewforth.com/}"; [ -f "$KR/site/content/en/${_pg%%[#?]*}.md" ] || printf '%s: %s has no site/content/en page\n' "${_f#$KR/}" "$_l" ;;
|
|
6229
6952
|
http://*|https://*|mailto:*|\#*) ;;
|
|
@@ -6231,15 +6954,15 @@ $(printf '%s\n' "$_nh" | head -n 5 | sed 's/^/ /')"
|
|
|
6231
6954
|
esac
|
|
6232
6955
|
done
|
|
6233
6956
|
done; }
|
|
6234
|
-
printf '[a](missing.md) [b](https://crewforth.com/nope) [c](https://crewforth.com/tr/gates)\n' > "$_kd/link-bad.md"
|
|
6957
|
+
printf '[a](missing.md) [b](https://crewforth.com/nope) [c](https://crewforth.com/tr/gates) [d](https://crewforth.com/#nope) [e](https://crewforth.com/#overview)\n' > "$_kd/link-bad.md"
|
|
6235
6958
|
mkdir -p "$_kd/site/content/tr"
|
|
6236
6959
|
# shellcheck disable=SC2086
|
|
6237
6960
|
_lh="$(link_hits $FP_PAGES)"; _ln="$(grep -c . "$_kd/n")"
|
|
6238
6961
|
if [ "$_ln" -lt 20 ]; then fail "the link check looked at only $_ln link(s) across the pages — the extractor is broken, not the pages"
|
|
6239
6962
|
elif [ -n "$_lh" ]; then fail "a front page links to something that is not there:
|
|
6240
6963
|
$(printf '%s\n' "$_lh" | head -n 5 | sed 's/^/ /')"
|
|
6241
|
-
elif [ "$(link_hits "$_kd/link-bad.md" | grep -c .)" !=
|
|
6242
|
-
else pass "every relative link
|
|
6964
|
+
elif [ "$(link_hits "$_kd/link-bad.md" | grep -c .)" != 3 ]; then fail "the link check did not catch exactly the three planted breaks (a missing file, a crewforth.com page with no source, a home-page anchor that is not there) — it reads nothing, or it flagged a real page or anchor"
|
|
6965
|
+
else pass "every relative link, crewforth.com page and home-page anchor on the READMEs and site pages resolves ($_ln links); three planted breaks are caught, a real site page and a real anchor are not"; fi
|
|
6243
6966
|
rm -rf "$_kd"
|
|
6244
6967
|
else
|
|
6245
6968
|
skip scope "front-page name, number and link checks skipped (installed project — the READMEs live in the source repository)" 3
|
|
@@ -6287,10 +7010,17 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
6287
7010
|
_ka="$(kw_lines $KW_A)"; _kb="$(kw_printed "$KR/start.sh" "$KR/adopt.sh" "$KR/bin/cli.js")"
|
|
6288
7011
|
# C: the real output. start.sh refuses to run from its own checkout, so its help is read from a copy.
|
|
6289
7012
|
_kd="$(mktemp -d)"; cp "$KR/start.sh" "$KR/VERSION" "$_kd/" 2>/dev/null
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
7013
|
+
# Each source is captured and COUNTED on its own. They used to share one file and one threshold (30 lines), and
|
|
7014
|
+
# `start.sh --help` in a copy without kit/ printed only its 2-line "kit/ not found" error — so the check passed on
|
|
7015
|
+
# the cli.js lines where node exists, and failed as a FIXTURE on a machine without node (3.0.1, PR 10): the help
|
|
7016
|
+
# text itself was read nowhere. Now start.sh answers --help without kit/, and each part has its own floor.
|
|
7017
|
+
( cd "$_kd" && CREW_LANG=en bash start.sh --help; CREW_LANG=tr bash start.sh --help ) > "$_kd/start.txt" 2>&1; _ksr=$?
|
|
7018
|
+
{ CREW_LANG=en bash "$ROOT/eval/preflight.sh"; CREW_LANG=tr bash "$ROOT/eval/preflight.sh"; } > "$_kd/pre.txt" 2>&1
|
|
7019
|
+
_kcl=""; if command -v node >/dev/null 2>&1; then node "$KR/bin/cli.js" --help > "$_kd/cli.txt" 2>&1; _kcl="$(grep -c . "$_kd/cli.txt")"
|
|
7020
|
+
else : > "$_kd/cli.txt"; skip tool "node bin/cli.js --help not read — no node on this machine (start.sh --help and preflight still are)"; fi
|
|
7021
|
+
cat "$_kd/start.txt" "$_kd/pre.txt" "$_kd/cli.txt" > "$_kd/out.txt"
|
|
6293
7022
|
_kc="$(kw_lines "$_kd/out.txt" | sed "s|^$_kd/||")"; _kn="$(grep -c . "$_kd/out.txt")"
|
|
7023
|
+
_ksn="$(grep -c . "$_kd/start.txt")"; _kpn="$(grep -c . "$_kd/pre.txt")"
|
|
6294
7024
|
# Twins. Must fail: a start.sh whose table says "full kit" again, and output that says "installing Crewforth.".
|
|
6295
7025
|
# Must pass: output that only names paths — .claude/kit.conf, the folder in the archive's own error, kit-manifest.txt,
|
|
6296
7026
|
# drizzle-kit, KIT_X. ("kit/ deleted" used to sit here as a path; it was the last line of every install, and the
|
|
@@ -6300,11 +7030,13 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
6300
7030
|
printf 'wrote .claude/kit.conf\nERROR: kit/ not found\nsee .claude/kit-manifest.txt and drizzle-kit status\nKIT_X=1\n' > "$_kd/good.txt"
|
|
6301
7031
|
if [ -n "$_ka$_kb$_kc" ]; then fail "a printed line still says kit — say Crewforth:
|
|
6302
7032
|
$(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/||; s|^| |")"
|
|
6303
|
-
elif [ "$
|
|
7033
|
+
elif [ "$_ksr" != 0 ] || [ "$_ksn" -lt 20 ]; then fail "start.sh --help, run where kit/ is absent, exited $_ksr with $_ksn line(s) (EN+TR, want rc 0 and ≥20): $(head -n 2 "$_kd/start.txt" | tr '\n' ' ')"
|
|
7034
|
+
elif [ "$_kpn" -lt 6 ]; then fail "FIXTURE: preflight printed only $_kpn line(s) in EN+TR — the run broke, not the wording"
|
|
7035
|
+
elif [ -n "$_kcl" ] && [ "$_kcl" -lt 10 ]; then fail "FIXTURE: cli.js --help printed only $_kcl line(s) — the run broke, not the wording"
|
|
6304
7036
|
elif [ -z "$(kw_printed "$_kd/mut-start.sh")" ]; then fail "the printed-text check missed a start.sh table entry reverted to 'full kit' — it reads nothing"
|
|
6305
7037
|
elif [ "$(kw_lines "$_kd/bad.txt" | grep -c .)" != 2 ]; then fail "Crewforth-word matcher missed a planted old-name word (English or Turkish) in the output"
|
|
6306
7038
|
elif [ -n "$(kw_lines "$_kd/good.txt")" ]; then fail "Crewforth-word matcher flagged a path or identifier: $(kw_lines "$_kd/good.txt" | head -n 2 | tr '\n' ' ')"
|
|
6307
|
-
else pass "no printed line says kit: terminal-only scripts, the installers' tables/echo/usage and cli --help, plus $_kn lines of real help
|
|
7039
|
+
else pass "no printed line says kit: terminal-only scripts, the installers' tables/echo/usage and cli --help, plus $_kn lines of real output (start.sh --help $_ksn, preflight $_kpn, cli.js ${_kcl:-not read}; EN+TR); a reverted table entry and planted words are caught, paths are not"; fi
|
|
6308
7040
|
# The closing line of an install is the one line every user reads. It may name no kit at all, not even as a path:
|
|
6309
7041
|
# the key and its Turkish value are both read, and a planted "kit/ deleted" in a copy must be caught.
|
|
6310
7042
|
_dn(){ grep -hE '"Done\.|'"'"'Done\.' "$@" 2>/dev/null | grep -iE '(^|[^a-z_.-])kit([^a-z_-]|$)'; }
|