-
-
Notifications
You must be signed in to change notification settings - Fork 0
policy: add a language-policy drift gate (self-tested) #661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f6accdf
c5912a1
0966e4e
a4d9ea1
42a3a8a
f87c2ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||
| # Language-policy drift gate. | ||||||||||||||||
| # | ||||||||||||||||
| # WHY THIS EXISTS. The estate's language policy is duplicated into ~372 per-repo | ||||||||||||||||
| # `.claude/CLAUDE.md` files across 131 repos. On 2026-08-26 a census found 868 of them | ||||||||||||||||
| # still listed **Bun as BANNED** with Deno as its replacement - the exact inverse of the | ||||||||||||||||
| # standing ruling - and nothing had ever detected it. Correcting `standards` fixes one copy; | ||||||||||||||||
| # agents read the local one. | ||||||||||||||||
| # | ||||||||||||||||
| # WHY ASSERTIONS, NOT A DIFF. The copies are legitimately not identical: repos carry their | ||||||||||||||||
| # own exemption tables, architecture notes and carve-outs. A byte-for-byte generator would | ||||||||||||||||
| # be permanently red. So this gate asserts the INVARIANTS the policy must satisfy, whatever | ||||||||||||||||
| # the surrounding wording. | ||||||||||||||||
| # | ||||||||||||||||
| # Exit 0 = compliant. Exit 1 = drift. Every failure prints file:line. | ||||||||||||||||
| set -uo pipefail | ||||||||||||||||
| status=0 | ||||||||||||||||
| files=$(git ls-files '*CLAUDE.md' 2>/dev/null | grep -v node_modules) | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK Iterating over file paths with a word-splitting |
||||||||||||||||
| [ -z "$files" ] && { echo "no CLAUDE.md tracked - nothing to check"; exit 0; } | ||||||||||||||||
|
Check failure on line 19 in tools/policy/check-language-policy.sh
|
||||||||||||||||
|
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Fail closed when file discovery fails. Line [18] suppresses 🧰 Tools🪛 GitHub Check: SonarCloud Code Analysis[failure] 19-19: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich. 🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| fail(){ printf ' \033[31mFAIL\033[0m %s\n %s\n' "$1" "$2"; status=1; } | ||||||||||||||||
|
Check warning on line 21 in tools/policy/check-language-policy.sh
|
||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Do not expand an unset failure message.
Also applies to: 52-60 🧰 Tools🪛 GitHub Check: SonarCloud Code Analysis[warning] 21-21: Assign this positional parameter to a local variable. [warning] 21-21: Assign this positional parameter to a local variable. [warning] 21-21: Add an explicit return statement at the end of the function. 🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| for f in $files; do | ||||||||||||||||
| echo "checking $f" | ||||||||||||||||
|
|
||||||||||||||||
| # --- must NOT appear ------------------------------------------------------- | ||||||||||||||||
| # 1. Bun banned. This is the inversion that went undetected across 868 files. | ||||||||||||||||
| if grep -nF -- '| Bun | Deno |' "$f" >/dev/null; then | ||||||||||||||||
| fail "$f:$(grep -nF -- '| Bun | Deno |' "$f" | head -1 | cut -d: -f1)" \ | ||||||||||||||||
| 'Bun is listed as BANNED with Deno as replacement - inverted. Bun is tier 1.' | ||||||||||||||||
|
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Apply historical-text filtering to every negative check. Only the dependency checks use Also applies to: 46-64 🤖 Prompt for AI Agents
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Normalize table rows before applying policy matchers. The exact row and cell patterns at these checks are formatting-sensitive, so padded or double-pipe rows, bold cells, and alternate Markdown forms can either evade detection or reject valid copies. Normalize leading pipes, cell whitespace, and emphasis before checking the Bun/Deno inversion, blank 📍 Affects 1 file
🤖 Prompt for AI Agents |
||||||||||||||||
| fi | ||||||||||||||||
| # 2. The rule that told repos not to declare dependencies at all. hyperpolymath/ubicity | ||||||||||||||||
| # a phrase inside a blockquote or quotation marks is HISTORY, not policy | ||||||||||||||||
| live(){ grep -vE '^[[:space:]]*>' "$1" | grep -vE '"[^"]*'"$2"'[^"]*"|\u201c[^\u201d]*'"$2"'[^\u201d]*\u201d'; } | ||||||||||||||||
|
Check warning on line 34 in tools/policy/check-language-policy.sh
|
||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
value='“No package.json for runtime deps”'
if printf '%s\n' "$value" |
grep -vE '"[^"]*No package.json for runtime deps[^"]*"|\u201c[^\u201d]*No package.json for runtime deps[^\u201d]*\u201d' |
grep -qF 'No package.json for runtime deps'
then
echo "smart-quoted text was not filtered"
exit 1
fiRepository: hyperpolymath/standards Length of output: 260 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- applicable conventions ---'
find /tmp/coderabbit-repo-knowledge/hyperpolymath-standards-aec7736b -mindepth 2 -maxdepth 2 -type f -name '*.md' -print
for f in /tmp/coderabbit-repo-knowledge/hyperpolymath-standards-aec7736b/*/*.md; do
case "$f" in
*/standards/*.md|*/learnings/*.md) cat "$f" ;;
esac
done
printf '%s\n' '--- target script ---'
cat -n tools/policy/check-language-policy.sh | sed -n '28,46p'
printf '%s\n' '--- grep implementation/version ---'
grep --version | sed -n '1,2p'
printf '%s\n' '--- matcher result with a literal curly-quoted line ---'
value='“No package.json for runtime deps”'
printf '%s\n' "$value" |
grep -vE '"[^"]*No package.json for runtime deps[^"]*"|\u201c[^\u201d]*No package.json for runtime deps[^\u201d]*\u201d' |
cat -nRepository: hyperpolymath/standards Length of output: 4934 Use a grep-compatible representation for curly quotes. The 🧰 Tools🪛 GitHub Check: SonarCloud Code Analysis[warning] 34-34: Assign this positional parameter to a local variable. [warning] 34-34: Assign this positional parameter to a local variable. [warning] 34-34: Add an explicit return statement at the end of the function. [warning] 34-34: Assign this positional parameter to a local variable. 🤖 Prompt for AI Agents |
||||||||||||||||
| # imported zod and glob, shipped no manifest, and could not build under ANY toolchain. | ||||||||||||||||
| if live "$f" 'No package.json for runtime deps' | grep -qF 'No package.json for runtime deps'; then | ||||||||||||||||
| fail "$f:$(grep -nF 'No package.json for runtime deps' "$f" | head -1 | cut -d: -f1)" \ | ||||||||||||||||
| 'Forbids declaring dependencies. Bun is npm-compatible; a manifest is REQUIRED.' | ||||||||||||||||
| fi | ||||||||||||||||
| if live "$f" 'deno.json imports' | grep -qF 'deno.json imports'; then | ||||||||||||||||
| fail "$f:$(grep -nF 'deno.json imports' "$f" | head -1 | cut -d: -f1)" \ | ||||||||||||||||
|
Comment on lines
+36
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
{
printf '%s\n' 'No package.json for runtime deps'
for i in $(seq 1 200000); do
printf 'padding %s\n' "$i"
done
} >"$tmp"
live() { cat "$1"; }
if live "$tmp" | grep -qF 'No package.json for runtime deps'; then
echo "matched"
else
echo "missed"
exit 1
fiRepository: hyperpolymath/standards Length of output: 206 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/hyperpolymath-standards-aec7736b -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- policy script ---'
cat -n tools/policy/check-language-policy.sh
printf '%s\n' '--- targeted live references ---'
rg -n -C 4 '(^|[[:space:]])live[[:space:]]*\(|grep -qF|No package.json for runtime deps' tools/policy/check-language-policy.sh
printf '%s\n' '--- pipefail probe with a large stream ---'
python3 - <<'PY'
import os
import signal
import subprocess
import tempfile
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
f.write("No package.json for runtime deps\n")
for i in range(2_000_000):
f.write(f"padding {i}\n")
path = f.name
script = r'''
set -o pipefail
live() { cat "$1"; }
live "$1" | grep -qF 'No package.json for runtime deps'
status=$?
printf 'pipeline_status=%s PIPESTATUS=%s,%s\n' "$status" "${PIPESTATUS[0]}" "${PIPESTATUS[1]}"
exit "$status"
'''
p = subprocess.run(["bash", "-c", script, "bash", path],
text=True, capture_output=True)
print(p.stdout, end="")
print(f"process_returncode={p.returncode}")
os.unlink(path)
PYRepository: hyperpolymath/standards Length of output: 9627 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- applicable repository standards and learnings ---'
for f in /tmp/coderabbit-repo-knowledge/hyperpolymath-standards-aec7736b/standards/*.md \
/tmp/coderabbit-repo-knowledge/hyperpolymath-standards-aec7736b/learnings/*.md; do
[ -f "$f" ] || continue
case "$f" in
*policy*|*shell*|*script*|*review*|*learn*) printf '%s\n' "### $f"; cat "$f";;
esac
doneRepository: hyperpolymath/standards Length of output: 215 Consume the complete stream before matching. When 🤖 Prompt for AI Agents |
||||||||||||||||
| 'Directs dependency declaration into deno.json. Use package.json + bun.lock.' | ||||||||||||||||
| fi | ||||||||||||||||
| # 3. No tool description may advertise TypeScript. Owner ruling 2026-08-27: | ||||||||||||||||
| # "no typescript ... that should not exist at all." | ||||||||||||||||
| if grep -nE 'Executes .\.ts. directly|JS/TS runtime' "$f" >/dev/null; then | ||||||||||||||||
| fail "$f:$(grep -nE 'Executes .\.ts. directly|JS/TS runtime' "$f" | head -1 | cut -d: -f1)" \ | ||||||||||||||||
| 'Advertises TypeScript execution. TypeScript is banned; do not describe tools as TS runtimes.' | ||||||||||||||||
| fi | ||||||||||||||||
| # 4. Blanking scars. A bulk purge substituted a token with an EMPTY STRING, which also | ||||||||||||||||
| # produced `rm -rf /lib` in wordpress-tools (the lethal shape is <token>/path -> /path). | ||||||||||||||||
| if awk -F'|' 'NF==4 && $2 ~ /^[[:space:]]*$/{exit 0} END{exit 1}' "$f"; then | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK The |
||||||||||||||||
| fail "$f" 'Policy table row with an EMPTY first cell - blanking scar from a bulk substitution.' | ||||||||||||||||
|
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Fix the blanking-scar exit status. The Proposed fix- if awk -F'|' 'NF==4 && $2 ~ /^[[:space:]]*$/{exit 0} END{exit 1}' "$f"; then
+ if awk -F'|' '
+ /^[[:space:]]*\|/ && NF >= 4 && $2 ~ /^[[:space:]]*$/ { found=1 }
+ END { exit !found }
+ ' "$f"; then📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| fi | ||||||||||||||||
| if grep -nF '| **** |' "$f" >/dev/null; then | ||||||||||||||||
| fail "$f:$(grep -nF '| **** |' "$f" | head -1 | cut -d: -f1)" \ | ||||||||||||||||
| 'Empty bold cell (****) - the language name was blanked out.' | ||||||||||||||||
| fi | ||||||||||||||||
| if grep -nE '\*\*No new +files\*\*|Only where +cannot' "$f" >/dev/null; then | ||||||||||||||||
| fail "$f" 'Enforcement rule with a blanked language name.' | ||||||||||||||||
| fi | ||||||||||||||||
| # 5. A rule may not ban the language it mandates. | ||||||||||||||||
| if grep -nE '^\| AffineScript \| AffineScript \|' "$f" >/dev/null; then | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK This regex is too rigid and fails to account for the double-pipe ( |
||||||||||||||||
| fail "$f" 'BANNED table maps AffineScript to itself - it bans the mandated language.' | ||||||||||||||||
| fi | ||||||||||||||||
|
|
||||||||||||||||
| # --- must appear, if the file carries a language-policy table --------------- | ||||||||||||||||
| if grep -qE '^### (ALLOWED|BANNED)' "$f"; then | ||||||||||||||||
| { grep -qE '^\|[[:space:]]*\*\*Bun\*\*[[:space:]]*\|' "$f" || grep -qiE '^[-*][[:space:]]+\*{0,2}Bun\*{0,2}\b' "$f"; } || \ | ||||||||||||||||
| fail "$f" 'No Bun row in ALLOWED. Bun is the tier-1 JS runtime and package manager.' | ||||||||||||||||
| { grep -qE '^\|[[:space:]]*\*{0,2}Deno\*{0,2}[[:space:]]*\|[[:space:]]*\*{0,2}Bun\*{0,2}[[:space:]]*\|' "$f" || grep -qiE '^[-*][[:space:]]+Deno[[:space:]]*\(use Bun\)' "$f"; } || \ | ||||||||||||||||
| fail "$f" 'Deno is not listed in BANNED with Bun as its replacement (ruling 2026-08-26).' | ||||||||||||||||
| fi | ||||||||||||||||
| done | ||||||||||||||||
|
|
||||||||||||||||
| if [ $status -eq 0 ]; then echo "language policy OK"; else | ||||||||||||||||
|
Check failure on line 76 in tools/policy/check-language-policy.sh
|
||||||||||||||||
| echo | ||||||||||||||||
| echo "Language-policy drift detected. Canonical source: hyperpolymath/standards .claude/CLAUDE.md" | ||||||||||||||||
| echo "Fix the local copy; do not weaken this gate." | ||||||||||||||||
| fi | ||||||||||||||||
| exit $status | ||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||
| # Fail if any GitHub Actions workflow does not parse. | ||||||||||||||||||
| # | ||||||||||||||||||
| # WHY THIS EXISTS. Measured across the estate on 2026-08-27: **481 workflow files in | ||||||||||||||||||
| # 134 repos do not parse at all**. A workflow that cannot be loaded produces NO check | ||||||||||||||||||
| # run, so it is invisible to `?status=failure` sweeps and to `gh pr checks` — the gate | ||||||||||||||||||
| # simply never runs, and its absence looks exactly like success. | ||||||||||||||||||
| # | ||||||||||||||||||
| # One was root-caused to a literal BACKSPACE byte (0x08) committed inside a regex. | ||||||||||||||||||
| # The other 480 are structural YAML: 245 "mapping values are not allowed in this | ||||||||||||||||||
| # context", 137 "could not find expected ':'", 73 block-mapping errors, 6 unterminated | ||||||||||||||||||
| # quotes. | ||||||||||||||||||
| # | ||||||||||||||||||
| # Exit 0 = every workflow parses. Exit 1 = at least one does not. | ||||||||||||||||||
| set -uo pipefail | ||||||||||||||||||
|
|
||||||||||||||||||
| parser="" | ||||||||||||||||||
| if command -v yq >/dev/null 2>&1; then parser=yq | ||||||||||||||||||
| elif command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then parser=python | ||||||||||||||||||
| elif command -v ruby >/dev/null 2>&1; then parser=ruby | ||||||||||||||||||
| else | ||||||||||||||||||
| echo "::warning::no YAML parser available (yq, python3+pyyaml, or ruby) — cannot verify workflows" | ||||||||||||||||||
| exit 0 | ||||||||||||||||||
| fi | ||||||||||||||||||
|
Comment on lines
+21
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Return a failure when no parser is available. If none of the supported parsers is installed, Line [22] emits a warning and Line [23] returns status 0. The gate then reports success without reading any workflow, so invalid YAML can pass undetected. Emit an error and return status 1, or require a parser in the calling workflow. Proposed fix else
- echo "::warning::no YAML parser available (yq, python3+pyyaml, or ruby) — cannot verify workflows"
- exit 0
+ echo "::error::no YAML parser available (yq, python3+pyyaml, or ruby) — cannot verify workflows"
+ exit 1
fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| parse_ok() { | ||||||||||||||||||
|
Check warning on line 26 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| case "$parser" in | ||||||||||||||||||
|
Check failure on line 27 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| yq) yq '.' "$1" >/dev/null 2>&1 ;; | ||||||||||||||||||
|
Check warning on line 28 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| python) python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$1" >/dev/null 2>&1 ;; | ||||||||||||||||||
|
Check warning on line 29 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| ruby) ruby -ryaml -e 'YAML.safe_load(File.read(ARGV[0]), aliases: true)' "$1" >/dev/null 2>&1 ;; | ||||||||||||||||||
|
Check warning on line 30 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| esac | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| status=0; checked=0 | ||||||||||||||||||
| while IFS= read -r f; do | ||||||||||||||||||
| [ -f "$f" ] || continue | ||||||||||||||||||
|
Check failure on line 36 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| checked=$((checked + 1)) | ||||||||||||||||||
| if ! parse_ok "$f"; then | ||||||||||||||||||
| status=1 | ||||||||||||||||||
| printf '::error file=%s::workflow does not parse — it produces NO check run, so this gate never executes\n' "$f" | ||||||||||||||||||
| case "$parser" in | ||||||||||||||||||
|
Check failure on line 41 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| yq) yq '.' "$f" 2>&1 | head -2 | sed 's/^/ /' ;; | ||||||||||||||||||
| python) python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$f" 2>&1 | tail -2 | sed 's/^/ /' ;; | ||||||||||||||||||
| ruby) ruby -ryaml -e 'YAML.safe_load(File.read(ARGV[0]), aliases: true)' "$f" 2>&1 | head -2 | sed 's/^/ /' ;; | ||||||||||||||||||
| esac | ||||||||||||||||||
| # control characters are a common, easily-missed cause | ||||||||||||||||||
| if grep -qP '[\x00-\x08\x0B\x0C\x0E-\x1F]' "$f" 2>/dev/null; then | ||||||||||||||||||
| echo " ⚠ contains CONTROL CHARACTERS — YAML forbids them; see empty-linter" | ||||||||||||||||||
| grep -nP '[\x00-\x08\x0B\x0C\x0E-\x1F]' "$f" | head -3 | cat -v | sed 's/^/ /' | ||||||||||||||||||
| fi | ||||||||||||||||||
| fi | ||||||||||||||||||
| done < <(git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' '**/.github/workflows/*.yml' '**/.github/workflows/*.yaml' 2>/dev/null | sort -u) | ||||||||||||||||||
|
|
||||||||||||||||||
| if [ "$checked" -eq 0 ]; then echo "no workflows tracked — nothing to check"; exit 0; fi | ||||||||||||||||||
|
Check failure on line 54 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| if [ "$status" -eq 0 ]; then echo "✅ all $checked workflow(s) parse"; else | ||||||||||||||||||
|
Check failure on line 55 in tools/policy/check-workflows-parse.sh
|
||||||||||||||||||
| echo | ||||||||||||||||||
| echo "A workflow that does not parse produces no check run. Its gate has never run," | ||||||||||||||||||
| echo "and its silence is indistinguishable from success. Fix the YAML; do not delete" | ||||||||||||||||||
| echo "the check." | ||||||||||||||||||
| fi | ||||||||||||||||||
| exit $status | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: hyperpolymath/standards
Length of output: 15862
🏁 Script executed:
Repository: hyperpolymath/standards
Length of output: 40362
Remove Deno from the language-policy job.
The
language-policyjob installs Deno and executesdeno run ...check-ts-allowlist.deno.js, although.claude/CLAUDE.mdbans Deno. Port the checker to Bun, or document and encode a narrow CI exception before merging.🤖 Prompt for AI Agents