crewforth 0.0.1 → 3.0.0-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 +3805 -0
- package/LICENSE +21 -0
- package/README.md +37 -7
- package/VERSION +1 -0
- package/adopt.sh +1739 -0
- package/bin/cli.js +497 -0
- package/kit/AGENT_TEMPLATE.md +92 -0
- package/kit/CLAUDE.md +228 -0
- package/kit/README.md +107 -0
- package/kit/agents/crew-backend-expert.md +95 -0
- package/kit/agents/crew-commit-agent.md +96 -0
- package/kit/agents/crew-database-expert.md +84 -0
- package/kit/agents/crew-devops-expert.md +82 -0
- package/kit/agents/crew-frontend-expert.md +85 -0
- package/kit/agents/crew-performance-expert.md +78 -0
- package/kit/agents/crew-planner.md +60 -0
- package/kit/agents/crew-privacy-agent.md +71 -0
- package/kit/agents/crew-review-agent.md +95 -0
- package/kit/agents/crew-security-expert.md +73 -0
- package/kit/agents/crew-session-manager.md +77 -0
- package/kit/agents/crew-test-expert.md +64 -0
- package/kit/eval/doctor.sh +639 -0
- package/kit/eval/gate-report.sh +137 -0
- package/kit/eval/golden-holdout.txt +53 -0
- package/kit/eval/golden-routing.txt +199 -0
- package/kit/eval/lib/crew-env.sh +14 -0
- package/kit/eval/lib/settings-json.awk +154 -0
- package/kit/eval/lib/skill-listing.awk +43 -0
- package/kit/eval/lib/star.sh +70 -0
- package/kit/eval/parser-conformance.sh +612 -0
- package/kit/eval/preflight.sh +186 -0
- package/kit/eval/routing-eval.sh +209 -0
- package/kit/eval/scan-skill.sh +162 -0
- package/kit/eval/smoke-test.sh +6736 -0
- package/kit/eval/update-guard.sh +81 -0
- package/kit/eval/utilization.sh +130 -0
- package/kit/hooks/board-sync.sh +106 -0
- package/kit/hooks/board.sh +962 -0
- package/kit/hooks/commit-msg +49 -0
- package/kit/hooks/context-usage.sh +353 -0
- package/kit/hooks/floor-blocklist.txt +124 -0
- package/kit/hooks/guard-bash.sh +1433 -0
- package/kit/hooks/guard-commit-scan.sh +511 -0
- package/kit/hooks/guard-write.sh +513 -0
- package/kit/hooks/pre-commit +577 -0
- package/kit/hooks/route-hint.sh +185 -0
- package/kit/hooks/secret-blocklist.txt +82 -0
- package/kit/hooks/session-guard.sh +149 -0
- package/kit/hooks/session-rehydrate.sh +41 -0
- package/kit/hooks/session-stats.sh +252 -0
- package/kit/hooks/session-update-check.sh +236 -0
- package/kit/hooks/skill-trust.sh +163 -0
- package/kit/hooks/trace-blocklist.txt +53 -0
- package/kit/settings.json +132 -0
- package/kit/skills/a11y/SKILL.md +44 -0
- package/kit/skills/adr/SKILL.md +74 -0
- package/kit/skills/api-design/SKILL.md +50 -0
- package/kit/skills/automode-policy/SKILL.md +91 -0
- package/kit/skills/automode-policy/references/policy.json +13 -0
- package/kit/skills/automode-policy/scripts/apply.sh +84 -0
- package/kit/skills/automode-policy/scripts/check.sh +102 -0
- package/kit/skills/backend-architecture/SKILL.md +103 -0
- package/kit/skills/brainstorm/SKILL.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +38 -0
- package/kit/skills/commit-message/SKILL.md +82 -0
- package/kit/skills/commit-message/references/examples.md +8 -0
- package/kit/skills/confidence-check/SKILL.md +86 -0
- package/kit/skills/crew-board/SKILL.md +55 -0
- package/kit/skills/crew-brainstorm/SKILL.md +19 -0
- package/kit/skills/crew-code-review/SKILL.md +151 -0
- package/kit/skills/crew-code-review/references/history-search.md +19 -0
- package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
- package/kit/skills/crew-code-review/references/receiving.md +23 -0
- package/kit/skills/crew-doctor/SKILL.md +46 -0
- package/kit/skills/crew-gates/SKILL.md +25 -0
- package/kit/skills/crew-handoff/SKILL.md +14 -0
- package/kit/skills/crew-plan/SKILL.md +18 -0
- package/kit/skills/crew-review/SKILL.md +26 -0
- package/kit/skills/crew-ship/SKILL.md +19 -0
- package/kit/skills/crew-skill/SKILL.md +62 -0
- package/kit/skills/crew-studio/SKILL.md +129 -0
- package/kit/skills/crew-update/SKILL.md +43 -0
- package/kit/skills/db-migration/SKILL.md +120 -0
- package/kit/skills/db-migration/references/backup-restore.md +33 -0
- package/kit/skills/db-migration/references/raw-sql.md +30 -0
- package/kit/skills/db-migration/references/tool-matrix.md +35 -0
- package/kit/skills/dependency-audit/SKILL.md +61 -0
- package/kit/skills/dependency-upgrade/SKILL.md +82 -0
- package/kit/skills/deploy/SKILL.md +100 -0
- package/kit/skills/deploy/references/proxy-ssl.md +56 -0
- package/kit/skills/deploy/references/rollback.md +18 -0
- package/kit/skills/deploy/references/scripts.md +9 -0
- package/kit/skills/deploy/references/topology-a.md +128 -0
- package/kit/skills/deploy/references/topology-b.md +44 -0
- package/kit/skills/docs-writer/SKILL.md +53 -0
- package/kit/skills/docs-writer/references/coauthoring.md +34 -0
- package/kit/skills/eval-grader/SKILL.md +75 -0
- package/kit/skills/eval-grader/references/method.md +51 -0
- package/kit/skills/frontend/SKILL.md +56 -0
- package/kit/skills/frontend/references/verify-contract.md +41 -0
- package/kit/skills/frontend-design/SKILL.md +71 -0
- package/kit/skills/frontend-design/references/design-review.md +34 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
- package/kit/skills/handoff/SKILL.md +71 -0
- package/kit/skills/handoff/references/cold-reader.md +51 -0
- package/kit/skills/i18n-integrity/SKILL.md +31 -0
- package/kit/skills/incident-runbook/SKILL.md +40 -0
- package/kit/skills/incident-runbook/references/postmortem.md +16 -0
- package/kit/skills/iterate/SKILL.md +40 -0
- package/kit/skills/mcp-builder/SKILL.md +54 -0
- package/kit/skills/mcp-builder/references/building.md +71 -0
- package/kit/skills/observability/SKILL.md +50 -0
- package/kit/skills/performance/SKILL.md +38 -0
- package/kit/skills/performance/references/bottlenecks.md +10 -0
- package/kit/skills/privacy-compliance/SKILL.md +75 -0
- package/kit/skills/red-team/SKILL.md +48 -0
- package/kit/skills/reflect/SKILL.md +57 -0
- package/kit/skills/release/SKILL.md +44 -0
- package/kit/skills/security-scan/SKILL.md +99 -0
- package/kit/skills/security-scan/references/ai-agents.md +107 -0
- package/kit/skills/security-scan/references/fronts.md +99 -0
- package/kit/skills/security-scan/references/prompting.md +23 -0
- package/kit/skills/security-scan/references/reporting.md +75 -0
- package/kit/skills/security-scan/references/verify.md +131 -0
- package/kit/skills/sonarqube-check/SKILL.md +108 -0
- package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
- package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
- package/kit/skills/spec-planning/SKILL.md +92 -0
- package/kit/skills/systematic-debugging/SKILL.md +57 -0
- package/kit/skills/systematic-debugging/references/techniques.md +44 -0
- package/kit/skills/teamboard/SKILL.md +88 -0
- package/kit/skills/teamboard/references/setup.md +37 -0
- package/kit/skills/testing/SKILL.md +49 -0
- package/kit/skills/testing/references/flaky-triage.md +51 -0
- package/kit/skills/threat-model/SKILL.md +57 -0
- package/kit/skills/threat-model/references/interview.md +51 -0
- package/kit/skills/threat-model/references/schema.md +42 -0
- package/kit/skills/token-budget/SKILL.md +51 -0
- package/kit/skills/trace-scan/SKILL.md +43 -0
- package/kit/skills/worktree/SKILL.md +40 -0
- package/kit/studio/README.md +266 -0
- package/kit/studio/ensure-node.sh +471 -0
- package/kit/studio/package.json +14 -0
- package/kit/studio/server/hooks/studio-gate.sh +169 -0
- package/kit/studio/server/index.js +902 -0
- package/kit/studio/server/lib/crew-env.js +11 -0
- package/kit/studio/server/lib/fleet.js +146 -0
- package/kit/studio/server/lib/graph.js +533 -0
- package/kit/studio/server/lib/instance.js +135 -0
- package/kit/studio/server/lib/kit-telemetry.js +182 -0
- package/kit/studio/server/lib/kit.js +158 -0
- package/kit/studio/server/lib/palette.js +118 -0
- package/kit/studio/server/lib/peers.js +72 -0
- package/kit/studio/server/lib/permissions.js +150 -0
- package/kit/studio/server/lib/projects.js +294 -0
- package/kit/studio/server/lib/roster.js +191 -0
- package/kit/studio/server/lib/session.js +309 -0
- package/kit/studio/server/lib/terminal.js +91 -0
- package/kit/studio/server/lib/transcript.js +100 -0
- package/kit/studio/web/app.js +1098 -0
- package/kit/studio/web/canvas.js +1179 -0
- package/kit/studio/web/chat.js +543 -0
- package/kit/studio/web/index.html +86 -0
- package/kit/studio/web/md.js +110 -0
- package/kit/studio/web/storage-migrate.js +23 -0
- package/kit/studio/web/style.css +1453 -0
- package/package.json +46 -8
- package/start.sh +764 -0
- package/bin/crewforth.js +0 -10
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Preflight — name the tools this machine is missing BEFORE they turn into a surprise mid-session.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists. Crewforth is written to degrade rather than break: sha256sum -> shasum -> cksum for digests,
|
|
5
|
+
# and so on. (It no longer needs jq or python at all — every JSON read and write is one bash/awk path on every
|
|
6
|
+
# OS — so neither is reported here: a row for a tool nothing uses would send people to install it.) That is the right design, and it is also
|
|
7
|
+
# why a missing tool never announces itself — the fallback runs, something is quietly worse, and the user finds
|
|
8
|
+
# out later from a symptom that points somewhere else. A Windows Git Bash install with no jq and no python is
|
|
9
|
+
# the normal case, not the exotic one, and it was where every surprise in this project came from.
|
|
10
|
+
#
|
|
11
|
+
# So: report, do not install. Nothing here writes to the machine or blocks a run. The user asked for exactly
|
|
12
|
+
# that boundary — a scaffolding tool that silently installs software on someone's workstation is a worse problem
|
|
13
|
+
# than the one it solves, and on a managed corporate machine it simply fails in a new way.
|
|
14
|
+
#
|
|
15
|
+
# Called from start.sh and adopt.sh (before the install summary) and from doctor.sh (so it stays re-runnable).
|
|
16
|
+
# bash preflight.sh # human-readable report, always exit 0
|
|
17
|
+
# bash preflight.sh --quiet # print only what is missing; exit 1 if any REQUIRED tool is absent
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
20
|
+
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
21
|
+
[ -f "$_crew_d/lib/crew-env.sh" ] && . "$_crew_d/lib/crew-env.sh"; unset _crew_d
|
|
22
|
+
|
|
23
|
+
QUIET=0
|
|
24
|
+
case "${1:-}" in --quiet|-q) QUIET=1 ;; esac
|
|
25
|
+
# A machine-readable question, so a caller can branch on one tool without parsing
|
|
26
|
+
# the display output or restating the rule. `--has node` answers with an exit
|
|
27
|
+
# code and prints nothing. The rule it answers with is works(), the same one the
|
|
28
|
+
# report uses — the version floor and the does-it-actually-run probe live in one
|
|
29
|
+
# place, and a second copy could drift from it silently.
|
|
30
|
+
HAS=""
|
|
31
|
+
case "${1:-}" in --has) HAS="${2:-}" ;; esac
|
|
32
|
+
|
|
33
|
+
B=""; D=""; R=""; YE=""; GR=""
|
|
34
|
+
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then B=$'\033[1m'; D=$'\033[2m'; R=$'\033[0m'; YE=$'\033[33m'; GR=$'\033[32m'; fi
|
|
35
|
+
|
|
36
|
+
MISSING_REQ=""; MISSING_OPT=""
|
|
37
|
+
|
|
38
|
+
have(){ command -v "$1" >/dev/null 2>&1; }
|
|
39
|
+
# `have` answers "is it on PATH", which is the wrong question for an interpreter on Windows. Windows puts
|
|
40
|
+
# %LOCALAPPDATA%\Microsoft\WindowsApps\python3 on PATH BY DEFAULT: the Microsoft Store redirector stub, which
|
|
41
|
+
# passes `command -v`, writes "Python was not found" to stderr and exits 49. Measured on a stock Windows 11
|
|
42
|
+
# desktop, this preflight printed "✓ jq or python → python3 · Everything Crewforth wants is here" on a machine
|
|
43
|
+
# with no Python at all — and the settings merge that line is about cannot run there. So for the interpreters,
|
|
44
|
+
# ask whether they RUN. Everything else is a plain binary where presence is the whole question.
|
|
45
|
+
#
|
|
46
|
+
# The probe always passes arguments. An ARGLESS run of that stub is the one that opens the Microsoft Store,
|
|
47
|
+
# which is why doctor.sh warns against calling it unconditionally; `-c …` makes it print and exit instead.
|
|
48
|
+
works(){
|
|
49
|
+
have "$1" || return 1
|
|
50
|
+
case "$1" in
|
|
51
|
+
# The panel needs 18+, and a name that resolves is not an interpreter that runs: the Windows Store ships a
|
|
52
|
+
# python3 that satisfies `command -v` and then exits 49. So run node, read the major it reports, and hold
|
|
53
|
+
# the floor. A node that cannot execute fails here rather than at first use.
|
|
54
|
+
node) v="$("$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || return 1
|
|
55
|
+
case "$v" in ''|*[!0-9]*) return 1 ;; esac
|
|
56
|
+
[ "$v" -ge 18 ] ;;
|
|
57
|
+
*) return 0 ;;
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
# printf's %-Ns pads by bytes, and a translated label ("sha256 aracı") is longer in bytes than in characters,
|
|
61
|
+
# so its column came out one short. Count characters: drop UTF-8 continuation bytes under C, pad the rest.
|
|
62
|
+
padr() { # $1 = text, $2 = width; sets PADDED
|
|
63
|
+
local LC_ALL=C n
|
|
64
|
+
n="${1//[$'\200'-$'\277']/}"; n=$(( $2 - ${#n} ))
|
|
65
|
+
PADDED="$1"
|
|
66
|
+
while [ "$n" -gt 0 ]; do PADDED="$PADDED "; n=$((n-1)); done
|
|
67
|
+
}
|
|
68
|
+
# any_of "label" "why it matters" "fix hint" cmd...
|
|
69
|
+
any_of(){
|
|
70
|
+
# label, why and fix are ENGLISH keys, translated here so no call site forks. A tool name passes through.
|
|
71
|
+
_mt "$1"; label="$_M"; _mt "$2"; why="$_M"; _mt "$3"; fix="$_M"; shift 3
|
|
72
|
+
found=""
|
|
73
|
+
for c in "$@"; do works "$c" && { found="$c"; break; }; done
|
|
74
|
+
if [ -n "$found" ]; then
|
|
75
|
+
[ "$QUIET" = 1 ] || { padr "$label" 22; printf ' %s✓%s %s %s%s%s\n' "$GR" "$R" "$PADDED" "$D" "$found" "$R"; }
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
padr "$label" 22
|
|
79
|
+
printf ' %s✗%s %s %s\n' "$YE" "$R" "$PADDED" "$why"
|
|
80
|
+
_mt 'fix: '; printf ' %s%s%s%s\n' "$D" "$_M" "$fix" "$R"
|
|
81
|
+
return 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if [ -n "$HAS" ]; then
|
|
85
|
+
works "$HAS" && exit 0 || exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# ---- CREW-I18N ------------------------------------------------------------------------------------------
|
|
89
|
+
# This script prints during the install, so it speaks the installer's language. Same contract as start.sh:
|
|
90
|
+
# the English string is the key, a missing translation prints English, and TOOL NAMES ARE NEVER TRANSLATED —
|
|
91
|
+
# `bash`, `git`, `jq`, `node` are identifiers, not words. Only the prose around them is.
|
|
92
|
+
case "${CREW_LANG:-}" in tr|en) ;; *)
|
|
93
|
+
_loc="${LC_ALL:-}"; [ -n "$_loc" ] || _loc="${LC_MESSAGES:-}"; [ -n "$_loc" ] || _loc="${LANG:-}"
|
|
94
|
+
case "$_loc" in tr*|TR*) CREW_LANG=tr ;; *) CREW_LANG=en ;; esac ;;
|
|
95
|
+
esac
|
|
96
|
+
# `_mt` writes into _M with printf -v: a `$(m …)` call site is a fork, ~50 ms each on Git Bash.
|
|
97
|
+
m() { _mt "$@"; printf '%s' "$_M"; }
|
|
98
|
+
_mt() {
|
|
99
|
+
# An empty key must still ASSIGN: bash 3.2's `printf -v _M ""` leaves _M holding the previous translation.
|
|
100
|
+
[ -n "${1:-}" ] || { _M=""; return 0; }
|
|
101
|
+
local s="$1"; shift
|
|
102
|
+
if [ "$CREW_LANG" = tr ]; then
|
|
103
|
+
case "$s" in
|
|
104
|
+
"Preflight — what this machine has") s='Ön kontrol — bu makinede neler var' ;;
|
|
105
|
+
"Missing REQUIRED:") s='Eksik ZORUNLU araçlar:' ;;
|
|
106
|
+
"— install these first.") s='— önce bunları kurun.' ;;
|
|
107
|
+
"All required tools present.") s='Zorunlu araçların hepsi kurulu.' ;;
|
|
108
|
+
"Optional gaps above are safe but worth closing.") s='Yukarıdaki isteğe bağlı eksikler sorun çıkarmaz ama gidermeye değer.' ;;
|
|
109
|
+
"Everything Crewforth needs is here.") s="Crewforth'un ihtiyaç duyduğu her şey kurulu." ;;
|
|
110
|
+
"is the panel only — every gate still holds without it.") s='yalnızca panel için gerekli — o olmadan da tüm kapılar çalışır.' ;;
|
|
111
|
+
"Or let Crewforth get one: %s") s='İsterseniz Crewforth sizin için indirebilir: %s' ;;
|
|
112
|
+
"fix: ") s='çözüm: ' ;;
|
|
113
|
+
"node 18+") s='node 18+' ;;
|
|
114
|
+
"sha256 tool") s='sha256 aracı' ;;
|
|
115
|
+
"all of Crewforth is bash") s="Crewforth'un tamamı bash ile yazıldı" ;;
|
|
116
|
+
"Windows: install Git for Windows (git-scm.com) and run Claude Code from Git Bash") s="Windows: Git for Windows'u kurun (git-scm.com) ve Claude Code'u Git Bash'ten çalıştırın" ;;
|
|
117
|
+
"context measurement, routing, doctor") s='bağlam ölçümü, yönlendirme ve doctor için' ;;
|
|
118
|
+
"Windows: ships with Git Bash · macOS: preinstalled · Linux: apt install gawk") s='Windows: Git Bash ile gelir · macOS: hazır gelir · Linux: apt install gawk' ;;
|
|
119
|
+
"the commit-time trace/secret gates are git hooks") s="commit anındaki iz ve gizli bilgi kapıları git hook'u olarak çalışır" ;;
|
|
120
|
+
"the Crewforth Studio panel (/crew-studio); the gates themselves are bash and do not need it") s='Crewforth Studio paneli (/crew-studio) için; kapılar bash ile çalışır, Node gerektirmez' ;;
|
|
121
|
+
"the skill-trust gate falls back to cksum (catches accidental edits, not crafted ones)") s="skill güven kapısı cksum'a düşer (kazara değişiklikleri yakalar, kasıtlı olanları yakalayamaz)" ;;
|
|
122
|
+
"Windows/Linux: coreutils (sha256sum) · macOS: shasum is preinstalled") s='Windows/Linux: coreutils (sha256sum) · macOS: shasum hazır gelir' ;;
|
|
123
|
+
"bash") ;; # identifier, printed as is
|
|
124
|
+
"awk") ;; # identifier, printed as is
|
|
125
|
+
"git") ;; # identifier, printed as is
|
|
126
|
+
"nodejs.org · Windows: winget install OpenJS.NodeJS.LTS · macOS: brew install node · Linux: apt install nodejs") ;; # identifier, printed as is
|
|
127
|
+
"git-scm.com · macOS: xcode-select --install · Linux: apt install git") ;; # identifier, printed as is
|
|
128
|
+
# No row: the line prints in English. CREW_I18N_MISS (set by e2e case 18) collects every such key, so a
|
|
129
|
+
# missing translation is caught by NAME rather than guessed from which English words it happens to contain.
|
|
130
|
+
*) [ -n "${CREW_I18N_MISS:-}" ] && printf '%s\n' "$s" >> "$CREW_I18N_MISS" ;;
|
|
131
|
+
esac
|
|
132
|
+
fi
|
|
133
|
+
# shellcheck disable=SC2059
|
|
134
|
+
printf -v _M "$s" "$@"
|
|
135
|
+
}
|
|
136
|
+
# ---- /CREW-I18N -----------------------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
[ "$QUIET" = 1 ] || { _mt 'Preflight — what this machine has'; printf '\n %s%s%s\n' "$B" "$_M" "$R"; }
|
|
139
|
+
|
|
140
|
+
# --- REQUIRED: without these Crewforth does not work at all -------------------------------------------------
|
|
141
|
+
# Hooks are wired in SHELL form on purpose, and `bash` is resolved by the shell Claude Code already runs them
|
|
142
|
+
# in — not off the Windows PATH. That distinction is not academic. On a Windows box checked during this work,
|
|
143
|
+
# `where bash` answered `C:\Windows\System32\bash.exe`, which is not Git Bash at all: it is the WSL launcher,
|
|
144
|
+
# living in a filesystem namespace where `C:\Repos\app` does not exist (`/mnt/c/Repos/app` does). A wiring that
|
|
145
|
+
# spawns `bash` by PATH name would have run THAT, or failed outright where WSL is not installed — every gate
|
|
146
|
+
# gone, with an error pointing nowhere near the cause. Hence shell form, and hence this note.
|
|
147
|
+
any_of "bash" 'all of Crewforth is bash' \
|
|
148
|
+
'Windows: install Git for Windows (git-scm.com) and run Claude Code from Git Bash' bash || MISSING_REQ="$MISSING_REQ bash"
|
|
149
|
+
any_of "awk" 'context measurement, routing, doctor' \
|
|
150
|
+
'Windows: ships with Git Bash · macOS: preinstalled · Linux: apt install gawk' awk gawk mawk || MISSING_REQ="$MISSING_REQ awk"
|
|
151
|
+
any_of "git" 'the commit-time trace/secret gates are git hooks' \
|
|
152
|
+
"git-scm.com · macOS: xcode-select --install · Linux: apt install git" git || MISSING_REQ="$MISSING_REQ git"
|
|
153
|
+
# Node is required for the panel and for nothing else: every gate in Crewforth is bash, and they all hold on a
|
|
154
|
+
# machine that has never seen node. It sits in REQUIRED anyway, deliberately, because the panel now installs
|
|
155
|
+
# into every project, and a component that silently does not start on some machines is worse than one that
|
|
156
|
+
# says what it needs. The reason string names which half is affected, so the REQUIRED heading stays true.
|
|
157
|
+
any_of 'node 18+' 'the Crewforth Studio panel (/crew-studio); the gates themselves are bash and do not need it' \
|
|
158
|
+
"nodejs.org · Windows: winget install OpenJS.NodeJS.LTS · macOS: brew install node · Linux: apt install nodejs" \
|
|
159
|
+
node || MISSING_REQ="$MISSING_REQ node"
|
|
160
|
+
|
|
161
|
+
# --- OPTIONAL: Crewforth falls back, but the fallback is worse in a way worth knowing about ------------------
|
|
162
|
+
any_of 'sha256 tool' 'the skill-trust gate falls back to cksum (catches accidental edits, not crafted ones)' \
|
|
163
|
+
'Windows/Linux: coreutils (sha256sum) · macOS: shasum is preinstalled' sha256sum shasum || MISSING_OPT="$MISSING_OPT sha256"
|
|
164
|
+
|
|
165
|
+
if [ -n "$MISSING_REQ" ]; then
|
|
166
|
+
_mt 'Missing REQUIRED:'; _a="$_M"; _mt '— install these first.'
|
|
167
|
+
printf '\n %s%s%s%s %s\n' "$YE$B" "$_a" "$R" "$MISSING_REQ" "$_M"
|
|
168
|
+
case "$MISSING_REQ" in
|
|
169
|
+
# Which half is gone. "Crewforth will not work" is false when only node is missing: every gate is bash and
|
|
170
|
+
# still holds; what is lost is the panel.
|
|
171
|
+
*node*) _mt 'is the panel only — every gate still holds without it.'; printf ' %snode%s %s\n' "$B" "$R" "$_M"
|
|
172
|
+
# And it is not a dead end: Crewforth fetches a runtime for the panel itself, into one
|
|
173
|
+
# directory under $HOME, verified against the published checksum. It asks first.
|
|
174
|
+
printf ' %s\n' "$(m 'Or let Crewforth get one: %s' "${B}bash .claude/studio/ensure-node.sh --plan${R}")" ;;
|
|
175
|
+
esac
|
|
176
|
+
elif [ -n "$MISSING_OPT" ]; then
|
|
177
|
+
[ "$QUIET" = 1 ] || { _mt 'All required tools present.'; _a="$_M"; _mt 'Optional gaps above are safe but worth closing.'
|
|
178
|
+
printf '\n %s%s%s %s\n' "$GR" "$_a" "$R" "$_M"; }
|
|
179
|
+
else
|
|
180
|
+
[ "$QUIET" = 1 ] || { _mt 'Everything Crewforth needs is here.'; printf '\n %s%s%s\n' "$GR" "$_M" "$R"; }
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Report-only by design: a missing OPTIONAL tool never fails. `--quiet` exits non-zero only for REQUIRED gaps,
|
|
184
|
+
# so a caller can branch on it without having to parse this output.
|
|
185
|
+
[ "$QUIET" = 1 ] && [ -n "$MISSING_REQ" ] && exit 1
|
|
186
|
+
exit 0
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Behavioral eval: does a golden prompt route to the expected target via a trigger (deterministic)?
|
|
3
|
+
# Does NOT run Claude Code; it statically proxies the routing correctness of the trigger design:
|
|
4
|
+
# 1) Golden routing — each example prompt must contain (as a substring) one trigger of its expected target.
|
|
5
|
+
# 2) Agent collision — two DIFFERENT agents must not share the same trigger phrase (routing ambiguity).
|
|
6
|
+
# Turkish diacritics are normalized on both sides (guvenlik == güvenlik), so it is also robust for
|
|
7
|
+
# users who type without diacritics. In a pruned install, a line whose target is not installed is SKIPPED.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
ROOT="$(cd "$HERE/.." && pwd)"
|
|
11
|
+
AGENTS="$ROOT/agents"; SKILLS="$ROOT/skills"
|
|
12
|
+
GOLD="$HERE/golden-routing.txt"
|
|
13
|
+
FAIL=0; SKIP=0
|
|
14
|
+
pass(){ echo " ✅ $1"; }
|
|
15
|
+
fail(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
|
|
16
|
+
skip(){ echo " ⏭ $1"; SKIP=$((SKIP+1)); }
|
|
17
|
+
|
|
18
|
+
# Turkish diacritics -> ascii, lowercase, then every run of non-alphanumerics becomes ONE space and the whole
|
|
19
|
+
# string is space-padded. That padding is what makes the match word-bounded: a bare substring test routed "the
|
|
20
|
+
# build fails on CI" to the frontend expert, because `build` contains `ui` and `UI` is one of its triggers.
|
|
21
|
+
# Every short trigger has that failure mode (ui · api · e2e), and it points the wrong way — a CI failure sent to
|
|
22
|
+
# the frontend agent is worse than no routing at all, because it looks like Crewforth worked.
|
|
23
|
+
norm() {
|
|
24
|
+
printf '%s' "$1" | sed \
|
|
25
|
+
-e 's/Ç/c/g' -e 's/ç/c/g' -e 's/Ğ/g/g' -e 's/ğ/g/g' -e 's/İ/i/g' -e 's/ı/i/g' \
|
|
26
|
+
-e 's/Ö/o/g' -e 's/ö/o/g' -e 's/Ş/s/g' -e 's/ş/s/g' -e 's/Ü/u/g' -e 's/ü/u/g' \
|
|
27
|
+
| tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]\{1,\}/ /g' -e 's/^/ /' -e 's/$/ /'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
triggers_of() { # $1 = target name; prints its trigger phrases line by line (exit 1 if none)
|
|
31
|
+
local n="$1" f=""
|
|
32
|
+
if [ -f "$AGENTS/$n.md" ]; then f="$AGENTS/$n.md"
|
|
33
|
+
elif [ -f "$SKILLS/$n/SKILL.md" ]; then f="$SKILLS/$n/SKILL.md"
|
|
34
|
+
else return 1; fi
|
|
35
|
+
grep -i "Trigger phrases:" "$f" | head -1 | grep -oE '"[^"]+"' | sed 's/"//g'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# The working set and the held-out set are read by the SAME matcher on purpose: if the holdout were scored more
|
|
39
|
+
# leniently, its failures would stop meaning anything. What differs is only where the prompts came from.
|
|
40
|
+
GOLD_SETS="$GOLD"
|
|
41
|
+
[ -f "$(dirname "$GOLD")/golden-holdout.txt" ] && GOLD_SETS="$GOLD $(dirname "$GOLD")/golden-holdout.txt"
|
|
42
|
+
echo "== 1) Golden routing (prompt -> expected target) =="
|
|
43
|
+
[ -f "$GOLD" ] || { fail "golden-routing.txt missing"; }
|
|
44
|
+
while IFS='|' read -r prompt expected; do
|
|
45
|
+
case "$prompt" in ''|\#*) continue ;; esac
|
|
46
|
+
# `[:space:]` includes CR, and that is the ONLY thing standing between this suite and a CRLF golden file.
|
|
47
|
+
# The Crewforth repo pins `*.txt text eol=lf` in .gitattributes, but these files are also INSTALLED into a user's
|
|
48
|
+
# project, where nothing pins them and Git for Windows sets core.autocrlf=true by default — so a Windows user
|
|
49
|
+
# who commits .claude/ and re-clones gets CRLF here. Measured on Windows with the golden files converted to
|
|
50
|
+
# CRLF byte for byte: 151 passes, 0 failures, identical to the LF run. Without this strip `expected` carries a
|
|
51
|
+
# trailing CR, matches no installed component, and every row fails as "target not installed".
|
|
52
|
+
expected="$(printf '%s' "$expected" | tr -d '[:space:]')"
|
|
53
|
+
[ -n "$expected" ] || continue
|
|
54
|
+
neg=0; case "$expected" in '!'*) neg=1; expected="${expected#!}" ;; esac # !target = must NOT route here
|
|
55
|
+
# Pre-2.0 this skipped when the target was pruned by profile. Profiles are gone: every install carries every
|
|
56
|
+
# agent and skill, so an absent target is a missing component or a stale golden row — both real failures.
|
|
57
|
+
# (Until 3.0 the .NET pattern skill was the one exception; 3.0 ships one install shape, so there is none.)
|
|
58
|
+
if ! trs="$(triggers_of "$expected")"; then
|
|
59
|
+
fail "\"$prompt\" -> $expected: target not installed — every install ships every component"
|
|
60
|
+
continue
|
|
61
|
+
fi
|
|
62
|
+
np="$(norm "$prompt")"
|
|
63
|
+
hit=0
|
|
64
|
+
while IFS= read -r ph; do
|
|
65
|
+
[ -n "$ph" ] || continue
|
|
66
|
+
nph="$(norm "$ph")"
|
|
67
|
+
case "$np" in *"$nph"*) hit=1; break ;; esac
|
|
68
|
+
done <<EOF
|
|
69
|
+
$trs
|
|
70
|
+
EOF
|
|
71
|
+
if [ "$neg" = 1 ]; then
|
|
72
|
+
if [ "$hit" = 0 ]; then pass "\"$prompt\" -/-> $expected (correctly not matched)"
|
|
73
|
+
else fail "\"$prompt\" -/-> $expected (over-broad trigger — matched a prompt it should not route)"; fi
|
|
74
|
+
else
|
|
75
|
+
if [ "$hit" = 1 ]; then pass "\"$prompt\" -> $expected"
|
|
76
|
+
else fail "\"$prompt\" -> $expected (no trigger matched — routing gap)"; fi
|
|
77
|
+
fi
|
|
78
|
+
done < <(cat $GOLD_SETS)
|
|
79
|
+
|
|
80
|
+
echo "== 1b) Routing COVERAGE — every installed component has at least one positive case =="
|
|
81
|
+
# The golden set proves that the cases in it route. It never proved that every component HAS a case, and an audit
|
|
82
|
+
# found 12 skills and one agent with none — including security-scan, code-review, testing and spec-planning. A
|
|
83
|
+
# component with no case is a component whose reachability is nobody's job to check, which is exactly how
|
|
84
|
+
# crew-frontend-expert stayed unreachable for a whole class of request while every gate reported green. Adding a
|
|
85
|
+
# component now means adding the sentence that must reach it.
|
|
86
|
+
MISSING_CASE=""
|
|
87
|
+
for f in "$AGENTS"/*.md "$SKILLS"/*/SKILL.md; do
|
|
88
|
+
[ -e "$f" ] || continue
|
|
89
|
+
case "$f" in */SKILL.md) n="$(basename "$(dirname "$f")")" ;; *) n="$(basename "$f" .md)" ;; esac
|
|
90
|
+
# A slash command (a skill marked `metadata: kind: command` since 3.0) is typed as /name, never routed to.
|
|
91
|
+
case "$f" in */SKILL.md) grep -q '^ kind: command' "$f" && continue ;; esac
|
|
92
|
+
grep -qE "^[^#]*\|$n\$" $GOLD_SETS || MISSING_CASE="$MISSING_CASE $n"
|
|
93
|
+
done
|
|
94
|
+
[ -z "$MISSING_CASE" ] && pass "every installed agent/skill has a positive routing case" \
|
|
95
|
+
|| fail "no positive golden case for:$MISSING_CASE — add the sentence a user would type to reach it"
|
|
96
|
+
|
|
97
|
+
echo "== 1c) Routing WINNER — what the real hook names, not only whether a trigger is present =="
|
|
98
|
+
# Section 1 asks whether the expected target's trigger APPEARS in the prompt. That never asked whether the target
|
|
99
|
+
# WINS: a prompt can carry its owner's trigger and still be routed somewhere else, because a louder word from a
|
|
100
|
+
# rival scored higher. Measured, with every golden prompt run through the real route-hint.sh: two working-set
|
|
101
|
+
# prompts routed to the wrong owner while section 1 reported both green. "the app feels laggy after the last
|
|
102
|
+
# release" went to `release`; "is this endpoint fast enough on the hot path" went to the backend agent.
|
|
103
|
+
#
|
|
104
|
+
# So this section feeds every golden case through the REAL hook — not a second matcher, which would be the same
|
|
105
|
+
# rule written twice and free to drift from the one that actually routes. It counts a positive as a hit when the
|
|
106
|
+
# hook names the expected target, or names the agent whose body applies that expected skill: by design the hook
|
|
107
|
+
# prefers an agent over a skill, and the agent carries the skill with it (the same notion §3b uses to call a
|
|
108
|
+
# skill routed). A negative (`!target`) must never be what the hook names.
|
|
109
|
+
#
|
|
110
|
+
# KNOWN MISSES ARE A RATCHET, NOT A TOLERANCE. They are listed below by exact prompt. A wrong route that is not
|
|
111
|
+
# on the list fails the suite; a listed one that starts routing correctly asks for its line to be removed, so the
|
|
112
|
+
# number can only get better. The scorer itself is deliberately untouched: whether a single loud keyword should
|
|
113
|
+
# be allowed to win is an open decision recorded in the roadmap, and this is the measurement that decision waits on.
|
|
114
|
+
#
|
|
115
|
+
# Rank the target among all rivals, not just check that it could match — scored by Crewforth's own scorer, the one
|
|
116
|
+
# the hook runs, rather than a TF-IDF approximation.
|
|
117
|
+
RH="$ROOT/hooks/route-hint.sh"
|
|
118
|
+
KNOWN_MISSES='the app feels laggy after the last release
|
|
119
|
+
is this endpoint fast enough on the hot path'
|
|
120
|
+
if [ -f "$RH" ]; then
|
|
121
|
+
# CLAUDE_PLUGIN_ROOT points the hook at $ROOT/agents and $ROOT/skills directly, which is the same layout in
|
|
122
|
+
# Crewforth's own repo (kit/) and in an installed project (.claude/). No copy, no second tree to drift.
|
|
123
|
+
rh_names(){ printf '{"hook_event_name":"UserPromptSubmit","prompt":"%s"}' "$1" \
|
|
124
|
+
| CLAUDE_PROJECT_DIR=/nonexistent CLAUDE_PLUGIN_ROOT="$ROOT" bash "$RH" 2>/dev/null \
|
|
125
|
+
| sed -n -e 's/.*Use the \([a-z][a-z0-9-]*\) subagent.*/\1/p' -e 's/.*Use the .\([a-z][a-z0-9-]*\). skill.*/\1/p'; }
|
|
126
|
+
# Calibrate before trusting a single verdict: a prompt the hook is known to route must come back named. An
|
|
127
|
+
# extractor that silently returns nothing makes EVERY positive read as "silent" — measured, the first version
|
|
128
|
+
# of this section did exactly that, with a `\|` that BSD sed does not support, and reported 0 of 82.
|
|
129
|
+
if [ "$(rh_names 'add an endpoint that returns unpaid invoices')" != "crew-backend-expert" ]; then
|
|
130
|
+
fail "winner check cannot read the hook's answer (calibration prompt came back unnamed) — the measurement is broken, not the routing"
|
|
131
|
+
else
|
|
132
|
+
WIN_HIT=0; WIN_N=0; NEG_N=0; NEW_MISS=""; FIXED_MISS=""; NEG_BAD=""
|
|
133
|
+
while IFS='|' read -r prompt expected; do
|
|
134
|
+
case "$prompt" in ''|\#*) continue ;; esac
|
|
135
|
+
expected="$(printf '%s' "$expected" | tr -d '[:space:]')"
|
|
136
|
+
[ -n "$expected" ] || continue
|
|
137
|
+
got="$(rh_names "$prompt")"
|
|
138
|
+
case "$expected" in
|
|
139
|
+
'!'*)
|
|
140
|
+
NEG_N=$((NEG_N+1))
|
|
141
|
+
[ "$got" = "${expected#!}" ] && NEG_BAD="$NEG_BAD
|
|
142
|
+
↳ named '$got' for: $prompt" ;;
|
|
143
|
+
*)
|
|
144
|
+
WIN_N=$((WIN_N+1))
|
|
145
|
+
ok=0
|
|
146
|
+
if [ -n "$got" ] && [ "$got" = "$expected" ]; then ok=1
|
|
147
|
+
elif [ -n "$got" ] && [ -f "$AGENTS/$got.md" ] && [ -f "$SKILLS/$expected/SKILL.md" ] \
|
|
148
|
+
&& grep -qE "\`$expected\`|\*\*$expected\*\*" "$AGENTS/$got.md"; then ok=1
|
|
149
|
+
fi
|
|
150
|
+
known=0; printf '%s\n' "$KNOWN_MISSES" | grep -qxF -- "$prompt" && known=1
|
|
151
|
+
if [ "$ok" = 1 ]; then
|
|
152
|
+
WIN_HIT=$((WIN_HIT+1))
|
|
153
|
+
[ "$known" = 1 ] && FIXED_MISS="$FIXED_MISS
|
|
154
|
+
↳ now routes correctly, remove it from KNOWN_MISSES: $prompt"
|
|
155
|
+
elif [ "$known" = 0 ]; then
|
|
156
|
+
NEW_MISS="$NEW_MISS
|
|
157
|
+
↳ expected $expected, hook named '${got:-nothing}': $prompt"
|
|
158
|
+
fi ;;
|
|
159
|
+
esac
|
|
160
|
+
done <<EOF_GOLD
|
|
161
|
+
$(cat $GOLD_SETS)
|
|
162
|
+
EOF_GOLD
|
|
163
|
+
[ -z "$NEW_MISS" ] && pass "winner: $WIN_HIT of $WIN_N positive prompts routed to their owner by the real hook (known misses: $(printf '%s\n' "$KNOWN_MISSES" | grep -c .))" \
|
|
164
|
+
|| { fail "winner: a prompt is routed to the wrong owner and is not a known miss"; printf '%s\n' "$NEW_MISS"; }
|
|
165
|
+
[ -z "$FIXED_MISS" ] || { fail "winner: a known miss now routes correctly — tighten the ratchet"; printf '%s\n' "$FIXED_MISS"; }
|
|
166
|
+
# A known miss that is not a golden prompt is never evaluated, so it would sit on the list forever and inflate
|
|
167
|
+
# the count shown above. Measured: a stray line reported "known misses: 3" in a green run. A ratchet entry
|
|
168
|
+
# nothing checks is the same defect as a blocklist pattern nothing matches.
|
|
169
|
+
STALE=""
|
|
170
|
+
while IFS= read -r km; do
|
|
171
|
+
[ -n "$km" ] || continue
|
|
172
|
+
cat $GOLD_SETS | cut -d'|' -f1 | grep -qxF -- "$km" || STALE="$STALE
|
|
173
|
+
↳ not a golden prompt: $km"
|
|
174
|
+
done <<EOF_KM
|
|
175
|
+
$KNOWN_MISSES
|
|
176
|
+
EOF_KM
|
|
177
|
+
[ -z "$STALE" ] || { fail "winner: a KNOWN_MISSES line is not in any golden set, so nothing ever checks it"; printf '%s\n' "$STALE"; }
|
|
178
|
+
[ -z "$NEG_BAD" ] && pass "winner: none of the $NEG_N negative cases is named by the real hook" \
|
|
179
|
+
|| { fail "winner: the real hook named a target a negative case forbids"; printf '%s\n' "$NEG_BAD"; }
|
|
180
|
+
fi
|
|
181
|
+
else
|
|
182
|
+
skip "winner check skipped: $RH not present in this layout"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo "== 2) Agent-agent trigger collision =="
|
|
186
|
+
# NOTE: Only AGENT-AGENT collisions matter (routing ambiguity lives here). An agent sharing a trigger
|
|
187
|
+
# with the skill it OWNS (crew-backend-expert<->backend-architecture, crew-security-expert<->security-scan,
|
|
188
|
+
# crew-devops-expert<->incident-runbook ...) is EXPECTED: the skill is the agent's internal "how"
|
|
189
|
+
# source, not a separate dispatch — the router picks the agent, the agent reads the skill inside a
|
|
190
|
+
# single subagent. So an agent<->its-own-skill overlap is intentional and is NOT a FAIL here.
|
|
191
|
+
# Collect each agent's unique (normalized) triggers; anything appearing in 2+ agents = collision.
|
|
192
|
+
dupe="$(
|
|
193
|
+
for f in "$AGENTS"/*.md; do
|
|
194
|
+
grep -i "Trigger phrases:" "$f" | head -1 | grep -oE '"[^"]+"' | sed 's/"//g' | while IFS= read -r p; do
|
|
195
|
+
[ -n "$p" ] && norm "$p"
|
|
196
|
+
done | sort -u
|
|
197
|
+
done | sort | uniq -d
|
|
198
|
+
)"
|
|
199
|
+
if [ -n "$dupe" ]; then
|
|
200
|
+
while IFS= read -r d; do [ -n "$d" ] && fail "same trigger in multiple agents: \"$d\""; done <<EOF
|
|
201
|
+
$dupe
|
|
202
|
+
EOF
|
|
203
|
+
else
|
|
204
|
+
pass "agent triggers unique (no collision)"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo "---"
|
|
208
|
+
if [ "$FAIL" -eq 0 ]; then echo "ROUTING-EVAL: PASSED ✅ (skipped: $SKIP)"; exit 0
|
|
209
|
+
else echo "ROUTING-EVAL: $FAIL errors ❌ (skipped: $SKIP)"; exit 1; fi
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install-time skill/agent security scanner — zero-dep bash + regex. Scans a directory or file (meant for a
|
|
3
|
+
# project's EXISTING third-party .claude skills/agents that adopt.sh will coexist with) for supply-chain red flags
|
|
4
|
+
# and scores each file: score = 100 − CRIT×20 − HIGH×10 − MED×3 − LOW×1 (floored at 0).
|
|
5
|
+
# SAFE ≥ 90 AND no CRIT/HIGH · REVIEW 70–89, or any HIGH · DANGER < 70, or any CRIT
|
|
6
|
+
#
|
|
7
|
+
# ADVISORY + HEURISTIC. It substring-matches, so a *security-education* skill (a red-team guide, an exfil example)
|
|
8
|
+
# can legitimately score low — REVIEW the flagged file, don't trust the number blindly. Its job is to SURFACE
|
|
9
|
+
# curl|bash, prompt-injection directives, credential/exfil patterns, not to prove intent.
|
|
10
|
+
#
|
|
11
|
+
# Exit: 0 if every scanned file is SAFE, 1 if any is REVIEW/DANGER, 3 if NOTHING WAS SCANNED.
|
|
12
|
+
# 3 exists because 0 was answering a question nobody asked. skill-trust.sh gates on this exit code and prints
|
|
13
|
+
# "scanner: SAFE" when it is 0 — so a path the scanner never looked at (an empty directory, a vanished target,
|
|
14
|
+
# a folder with no manifest) was reported to the user as clean. "I found nothing wrong" and "I did not look"
|
|
15
|
+
# are different answers, and only one of them belongs on a security surface.
|
|
16
|
+
# Usage: bash scan-skill.sh [path] (default: .claude)
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
TARGET="${1:-.claude}"
|
|
19
|
+
|
|
20
|
+
# --- pattern groups (ERE) ------------------------------------------------------------------------------------
|
|
21
|
+
# CRIT: download-and-exec, known exfil/collaborator hosts, rm -rf of home/root.
|
|
22
|
+
CRIT='(curl|wget|fetch)[^|]*\|[[:space:]]*(sudo[[:space:]]+)?(bash|sh|zsh|python[0-9.]*|node|perl|ruby)|(bash|sh)[[:space:]]+<\(|(webhook\.site|requestbin|pipedream\.net|ngrok\.io|burpcollaborator|oastify|interactsh|dnslog\.|\.oast\.)|rm[[:space:]]+-[A-Za-z]*r[A-Za-z]*[[:space:]]+(~|/|\$HOME)([[:space:]]|$|/)'
|
|
23
|
+
# HIGH: prompt-injection directives; a credential file READ/exfil (a reader verb + the path — a bare `~/.ssh/id_rsa`
|
|
24
|
+
# config value is NOT flagged); base64-decode piped to a shell; and a RUNTIME INSTRUCTION FETCH.
|
|
25
|
+
#
|
|
26
|
+
# That last one is not a missing regex, it is the assumption the trust model rests on. skill-trust.sh accepts a
|
|
27
|
+
# component by DIGEST, and a digest answers "have these bytes changed?" — which is the wrong question for a file
|
|
28
|
+
# whose bytes say "fetch your real instructions from this URL". The digest never moves while the behaviour is
|
|
29
|
+
# rewritten by whoever controls the endpoint, and the component stays on the accepted list forever. So fetching
|
|
30
|
+
# an instruction-shaped file (.md/.txt/.yml/.json) and a WebFetch tied to instructions/prompts are HIGH, while
|
|
31
|
+
# an ordinary outbound URL stays LOW: measured against Crewforth's own payload, this flags 0 of 59 files, and the
|
|
32
|
+
# two places Crewforth itself names WebFetch (a `tools:` line and a `Requires-tool` marker) are untouched.
|
|
33
|
+
HIGH='ignore[[:space:]]+(all[[:space:]]+)?(the[[:space:]]+)?(previous|prior|above)[[:space:]]+(instruction|prompt)|disregard[[:space:]]+(the[[:space:]]+|all[[:space:]]+)?(previous|above|prior)|ignore[[:space:]]+your[[:space:]]+(system[[:space:]]+)?(prompt|instruction)|(cat|less|more|tail|head|base64|xxd|od|strings|curl|wget|scp|cp|rsync)[^|]*(~/\.ssh|id_rsa|/etc/(passwd|shadow)|\.aws/credentials|\.netrc|\.git-credentials)|(~/\.ssh|id_rsa|/etc/(passwd|shadow)|\.aws/credentials|\.netrc|\.git-credentials)[^|]{0,80}(curl|wget|scp|nc |netcat|base64|exfiltrat)|base64[[:space:]]+-[A-Za-z]*d[^|]*\||(curl|wget)[[:space:]][^|]*https?://[^[:space:]]*\.(md|txt|ya?ml|json)|(WebFetch|web_fetch)[^|]{0,120}(instruction|prompt|then follow|steps to follow)'
|
|
34
|
+
# MED: named cloud/CI secret env vars, process.env secret access, chmod 777, code eval/exec.
|
|
35
|
+
MED='(GITHUB_TOKEN|AWS_SECRET|AWS_ACCESS_KEY|NPM_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|SLACK_TOKEN)|process\.env\.[A-Za-z_]*(TOKEN|SECRET|KEY|PASSWORD)|chmod[[:space:]]+(-R[[:space:]]+)?0?777|[^a-zA-Z](eval|exec)[[:space:]]*\('
|
|
36
|
+
# LOW: an outbound URL fetch or a raw socket — common in benign deploy/example prose, hence low weight.
|
|
37
|
+
LOW='(^|[^a-zA-Z])(curl|wget)[[:space:]]+[^|]*https?://|/dev/tcp/|[^a-zA-Z]nc[[:space:]]+-[A-Za-z]*e'
|
|
38
|
+
|
|
39
|
+
FAIL=0; N=0
|
|
40
|
+
# The score ranks; the SEVERITY decides. A single HIGH costs 10 points and lands on exactly 90 — the SAFE line —
|
|
41
|
+
# so one credential-exfil line or one prompt-injection directive used to pass as safe on arithmetic alone. That
|
|
42
|
+
# is the wrong way round: the score exists to order a review queue, not to let a high-severity hit through it.
|
|
43
|
+
# So severity floors the verdict — any CRIT is DANGER, any HIGH is never SAFE — and the number still ranks what
|
|
44
|
+
# is left. (Measured: Crewforth's own 59 payload files carry no CRIT or HIGH at all, lowest score 99.)
|
|
45
|
+
verdict(){ # $1 = score, $2 = crit count, $3 = high count
|
|
46
|
+
if [ "${2:-0}" -gt 0 ]; then echo DANGER; return; fi
|
|
47
|
+
if [ "${3:-0}" -gt 0 ]; then [ "$1" -ge 70 ] && echo REVIEW || echo DANGER; return; fi
|
|
48
|
+
if [ "$1" -ge 90 ]; then echo SAFE; elif [ "$1" -ge 70 ]; then echo REVIEW; else echo DANGER; fi; }
|
|
49
|
+
|
|
50
|
+
# One grep PER SEVERITY over ALL files, not four greps per file.
|
|
51
|
+
#
|
|
52
|
+
# Measured on a user's Windows machine: scanning 64 files took 8m07s — user 12.6s, sys 2m46s. The regex work is
|
|
53
|
+
# not the cost; process creation is. Git Bash has no real fork, so every spawn is a CreateProcess plus MSYS
|
|
54
|
+
# emulation plus an antivirus scan of the binary, and 64 files × 4 greps = 256 of them. adopt.sh calls this on
|
|
55
|
+
# every refresh, which is where most of a "the update is hung" report actually went.
|
|
56
|
+
#
|
|
57
|
+
# grep accepts many files at once and `-cH` prints `path:count` for each, including zeros — so the same regex
|
|
58
|
+
# engine, the same `-i` semantics and the same per-file line counts come back in 4 processes instead of 256.
|
|
59
|
+
# Deliberately NOT rewritten in awk: these patterns carry intervals and alternations whose behaviour would have
|
|
60
|
+
# to be re-proved against a different engine, and the win here is process count, not matching speed.
|
|
61
|
+
counts_of(){ # $1 = pattern, $2 = output file. `path:count` per input file, in the order given.
|
|
62
|
+
grep -cHiE "$1" -- "${FILES[@]}" > "$2" 2>/dev/null
|
|
63
|
+
return 0 # grep exits 1 when nothing matched anywhere; that is a normal result, not an error
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Sets the named array AND the global RC_N. Deliberately not "$(read_counts …)": command substitution runs in a
|
|
67
|
+
# SUBSHELL, so the array assignments would be discarded and every file would score a spotless 100. That is exactly
|
|
68
|
+
# what the first version did — and the output diff against the old implementation is what caught it.
|
|
69
|
+
read_counts(){ # $1 = grep -cH output file, $2 = array name
|
|
70
|
+
local line i=0
|
|
71
|
+
while IFS= read -r line; do
|
|
72
|
+
# split from the RIGHT: a path may contain ':' (a Windows drive letter), a count never does
|
|
73
|
+
eval "$2[$i]=\"\${line##*:}\""
|
|
74
|
+
i=$((i+1))
|
|
75
|
+
done < "$1"
|
|
76
|
+
RC_N=$i
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
emit(){ # prints one line per file, in FILES order, from the four count arrays
|
|
80
|
+
local f c h m l score v i=0
|
|
81
|
+
while [ "$i" -lt "${#FILES[@]}" ]; do
|
|
82
|
+
f="${FILES[$i]}"
|
|
83
|
+
c="${CNT_C[$i]:-0}"; h="${CNT_H[$i]:-0}"; m="${CNT_M[$i]:-0}"; l="${CNT_L[$i]:-0}"
|
|
84
|
+
i=$((i+1))
|
|
85
|
+
score=$((100 - c*20 - h*10 - m*3 - l*1)); [ "$score" -lt 0 ] && score=0
|
|
86
|
+
v="$(verdict "$score" "$c" "$h")"
|
|
87
|
+
N=$((N+1))
|
|
88
|
+
case "$v" in
|
|
89
|
+
SAFE) printf ' ✅ %-3s %s\n' "$score" "$f" ;;
|
|
90
|
+
REVIEW) printf ' ⚠️ %-3s %s (crit:%s high:%s med:%s low:%s) — REVIEW\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
91
|
+
DANGER) printf ' ❌ %-3s %s (crit:%s high:%s med:%s low:%s) — DANGER\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# A single file keeps the old path: grep prints a bare count with one argument, so the batch parser would have to
|
|
97
|
+
# special-case it anyway, and one file means one process either way.
|
|
98
|
+
scan_file(){
|
|
99
|
+
local f="$1"
|
|
100
|
+
local c h m l
|
|
101
|
+
# grep -c prints the count (0 on no match) and exits 1 on no match; that non-zero exit is fine here (no set -e),
|
|
102
|
+
# and a `|| echo 0` would DOUBLE the printed 0 and break the arithmetic — so don't add one.
|
|
103
|
+
c=$(grep -icE "$CRIT" "$f" 2>/dev/null); h=$(grep -icE "$HIGH" "$f" 2>/dev/null)
|
|
104
|
+
m=$(grep -icE "$MED" "$f" 2>/dev/null); l=$(grep -icE "$LOW" "$f" 2>/dev/null)
|
|
105
|
+
c=${c:-0}; h=${h:-0}; m=${m:-0}; l=${l:-0}
|
|
106
|
+
local score=$((100 - c*20 - h*10 - m*3 - l*1)); [ "$score" -lt 0 ] && score=0
|
|
107
|
+
local v; v="$(verdict "$score" "$c" "$h")"
|
|
108
|
+
N=$((N+1))
|
|
109
|
+
case "$v" in
|
|
110
|
+
SAFE) printf ' ✅ %-3s %s\n' "$score" "$f" ;;
|
|
111
|
+
REVIEW) printf ' ⚠️ %-3s %s (crit:%s high:%s med:%s low:%s) — REVIEW\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
112
|
+
DANGER) printf ' ❌ %-3s %s (crit:%s high:%s med:%s low:%s) — DANGER\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
echo "== skill/agent security scan: $TARGET =="
|
|
117
|
+
if [ -f "$TARGET" ]; then
|
|
118
|
+
scan_file "$TARGET"
|
|
119
|
+
elif [ -d "$TARGET" ]; then
|
|
120
|
+
# Skill/agent definition files only (markdown). Skip Crewforth's own crew- agents (trusted, not third-party).
|
|
121
|
+
FILES=()
|
|
122
|
+
while IFS= read -r f; do
|
|
123
|
+
case "$f" in agents/crew-*.md|*/agents/crew-*.md) continue ;; esac # kit agents only — a skill dir named crew-* is still scanned
|
|
124
|
+
FILES+=("$f")
|
|
125
|
+
done < <(find "$TARGET" \( -name 'SKILL.md' -o -name '*.md' \) -path '*/skills/*' -o -path '*/agents/*' -name '*.md' 2>/dev/null | sort -u)
|
|
126
|
+
# A skill directory with no SKILL.md is invisible to the scan above — the loop only ever sees files, so a
|
|
127
|
+
# folder carrying scripts and references but no manifest was scanned file by file (or not at all) and never
|
|
128
|
+
# named as odd. It is also the shape a component takes when it is trying not to look like a component.
|
|
129
|
+
# Globs, not `find`: `-depth 2` means "process contents first" to GNU find and takes no argument, so the
|
|
130
|
+
# BSD spelling that works on macOS is a syntax error on Linux and in Git Bash — i.e. on two of the three
|
|
131
|
+
# platforms Crewforth ships to. Globbing is also fork-free, which this scanner cares about (see the cost note).
|
|
132
|
+
for d in "$TARGET"/skills/*/ "$TARGET"/*/skills/*/; do
|
|
133
|
+
[ -d "$d" ] || continue # an unmatched glob expands to itself
|
|
134
|
+
[ -f "${d}SKILL.md" ] && continue
|
|
135
|
+
printf ' ⚠️ --- %s (no SKILL.md) — REVIEW: a skill directory without its manifest\n' "${d%/}"
|
|
136
|
+
FAIL=1; N=$((N+1))
|
|
137
|
+
done
|
|
138
|
+
if [ "${#FILES[@]}" -le 1 ]; then
|
|
139
|
+
[ "${#FILES[@]}" -eq 1 ] && scan_file "${FILES[0]}"
|
|
140
|
+
else
|
|
141
|
+
ST="$(mktemp -d)"
|
|
142
|
+
counts_of "$CRIT" "$ST/c"; counts_of "$HIGH" "$ST/h"
|
|
143
|
+
counts_of "$MED" "$ST/m"; counts_of "$LOW" "$ST/l"
|
|
144
|
+
read_counts "$ST/c" CNT_C; NC=$RC_N; read_counts "$ST/h" CNT_H; NH=$RC_N
|
|
145
|
+
read_counts "$ST/m" CNT_M; NM=$RC_N; read_counts "$ST/l" CNT_L; NL=$RC_N
|
|
146
|
+
rm -rf "$ST"
|
|
147
|
+
# If grep skipped a file (unreadable, vanished mid-run) the rows no longer line up with FILES, and a silently
|
|
148
|
+
# misaligned count would score the wrong file. Fall back to the per-file path rather than report a guess.
|
|
149
|
+
if [ "$NC" = "${#FILES[@]}" ] && [ "$NH" = "$NC" ] && [ "$NM" = "$NC" ] && [ "$NL" = "$NC" ]; then
|
|
150
|
+
emit
|
|
151
|
+
else
|
|
152
|
+
for f in "${FILES[@]}"; do scan_file "$f"; done
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
else
|
|
156
|
+
echo " (nothing to scan at '$TARGET')"; exit 3
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
echo "---"
|
|
160
|
+
if [ "$N" -eq 0 ]; then echo "scan: nothing scanned"; exit 3; fi
|
|
161
|
+
if [ "$FAIL" -eq 0 ]; then echo "SCAN: all $N file(s) SAFE ✅"; exit 0
|
|
162
|
+
else echo "SCAN: review the flagged file(s) above ⚠️ (heuristic — security-education skills can score low by design)"; exit 1; fi
|