crewforth 3.1.1 → 3.2.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 +171 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/adopt.sh +56 -5
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +14 -13
- package/kit/agents/crew-commit-agent.md +7 -2
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +117 -6
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +98 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +163 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/scan.mjs +214 -0
- package/kit/eval/lib/crew-map/stacks.mjs +441 -0
- package/kit/eval/lib/crew-map/surface.mjs +457 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +15 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/smoke-test.sh +576 -41
- package/kit/hooks/agent-outcome.sh +26 -12
- package/kit/hooks/guard-agent-model.sh +109 -9
- package/kit/hooks/guard-bash.sh +240 -182
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +21 -0
- package/kit/hooks/prompt-approval.sh +320 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +33 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +44 -0
- package/kit/skills/crew-review/SKILL.md +2 -0
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +12 -0
- package/kit/studio/server/index.js +125 -11
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/remote.js +99 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +223 -15
- package/kit/studio/web/canvas.js +44 -120
- package/kit/studio/web/chat.js +82 -17
- package/kit/studio/web/graph-plan.js +10 -4
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +341 -0
- package/kit/studio/web/map.js +503 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/style.css +131 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +10 -0
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
# -> blocked: not run, nothing concluded.
|
|
20
20
|
# not finished in CREW_VERIFY_TIMEOUT seconds (540)
|
|
21
21
|
# -> timeout: stopped, nothing concluded.
|
|
22
|
-
# Then one line is added to .claude/state/model-outcomes.tsv,
|
|
22
|
+
# Then one line is added to .claude/state/model-outcomes.tsv, its last column the gate's reason
|
|
23
|
+
# when the work is critical (risk_why, "-" when it is not), and the class is looked at:
|
|
23
24
|
# where the first try failed too often, its floor goes one model up (crew-model-floors.auto).
|
|
24
25
|
# Only ever up: lowering a floor is the user's decision and nothing here does it.
|
|
25
26
|
# PostToolUse (Agent) a foreground call has come back. If its verify failed, or its report closed with
|
|
@@ -203,27 +204,36 @@ case "$EV" in
|
|
|
203
204
|
low=9; cands=""; if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOWS '%(%s)T' -1; else NOWS="$(date +%s)"; fi
|
|
204
205
|
for f in "$_AMD/pending"/*; do
|
|
205
206
|
[ -f "$f" ] || continue
|
|
206
|
-
t="${f##*/}"; t="${t%%-*}"; case "$t" in ''|*[!0-9]*) ;; *) if [ $((NOWS - t)) -gt 300 ]; then rm -f "$f" 2>/dev/null; continue; fi ;; esac
|
|
207
|
+
t="${f##*/}"; t="${t%%-*}"; case "$t" in ''|*[!0-9]*) ;; *) if [ $((NOWS - t)) -gt 300 ]; then rm -f "$f" "$_AMD/agents/${f##*/}.take" 2>/dev/null; continue; fi ;; esac
|
|
207
208
|
_am_rec_get "$f" agent || continue; [ "$_AMV" = "$AT" ] || continue
|
|
208
209
|
cands="$cands$f"$'\n'
|
|
209
210
|
_am_rec_get "$f" tier && { _am_rank "$_AMV"; [ "$_AMR" -lt "$low" ] && low="$_AMR"; }
|
|
210
211
|
done
|
|
211
212
|
[ -n "$cands" ] || exit 0
|
|
212
213
|
mkdir -p "$_AMD/agents" 2>/dev/null || exit 0
|
|
213
|
-
# TAKEN BY RENAME. Two agents of one type start at the same moment, each in its own hook
|
|
214
|
-
# the same oldest record.
|
|
215
|
-
#
|
|
216
|
-
|
|
214
|
+
# TAKEN UNDER A LOCK, THEN BY RENAME. Two agents of one type start at the same moment, each in its own hook
|
|
215
|
+
# process, and both see the same oldest record. (Reading it and deleting it afterwards gave both the same record.)
|
|
216
|
+
# A rename alone was the lock on POSIX, and was not on Windows: two hook processes both reported the same rename
|
|
217
|
+
# as done (smoke, windows-latest, "2 got it", twice). Why is not measured; a rename there goes through an open
|
|
218
|
+
# handle, which two processes can both hold. So each record is first locked by creating <record>.take with
|
|
219
|
+
# O_EXCL (bash's noclobber, a builtin: no process); only the one that made the lock renames, the others go on to
|
|
220
|
+
# the next record.
|
|
221
|
+
claim="$_AMD/agents/$AID.claim"; got=0; take=""
|
|
222
|
+
set -C
|
|
217
223
|
while IFS= read -r f; do
|
|
218
224
|
[ -n "$f" ] || continue
|
|
225
|
+
take="$_AMD/agents/${f##*/}.take"
|
|
226
|
+
{ : > "$take"; } 2>/dev/null || { take=""; continue; }
|
|
219
227
|
if mv "$f" "$claim" 2>/dev/null; then got=1; break; fi
|
|
228
|
+
rm -f "$take" 2>/dev/null; take="" # locked, but the record had gone (dropped as stale): let it be
|
|
220
229
|
done <<< "$cands"
|
|
230
|
+
set +C
|
|
221
231
|
[ "$got" = 1 ] || exit 0
|
|
222
232
|
_am_rec_get "$claim" tier; _am_rank "$_AMV"
|
|
223
233
|
if [ "$low" -lt "$_AMR" ]; then _am_name "$low"
|
|
224
234
|
{ grep -v '^tier=' "$claim"; printf 'asked=%s\ntier=%s\nambiguous=1\n' "$_AMV" "$_AMN"; } > "$_AMD/agents/$AID" 2>/dev/null
|
|
225
235
|
else { cat "$claim"; printf 'asked=%s\n' "$_AMV"; } > "$_AMD/agents/$AID" 2>/dev/null; fi
|
|
226
|
-
rm -f "$claim" 2>/dev/null
|
|
236
|
+
rm -f "$claim" "$take" 2>/dev/null
|
|
227
237
|
exit 0 ;;
|
|
228
238
|
|
|
229
239
|
SubagentStop)
|
|
@@ -280,6 +290,8 @@ case "$EV" in
|
|
|
280
290
|
_am_rec_get "$REC" verify; VCMD="$_AMV"
|
|
281
291
|
_am_rec_get "$REC" agent; AG="$_AMV"; _am_rec_get "$REC" change; CH="$_AMV"; _am_rec_get "$REC" risk; RK="$_AMV"
|
|
282
292
|
_am_rec_get "$REC" card; CID="$_AMV"; _am_rec_get "$REC" esc; ESC="${_AMV:--}"
|
|
293
|
+
# A record written before the gate kept its reason has none: that is "-", not a reason made up here.
|
|
294
|
+
_am_rec_get "$REC" why; RW="${_AMV:--}"; RW="${RW//$'\t'/ }"
|
|
283
295
|
_am_rec_get "$REC" asked || _am_rec_get "$REC" tier; ASK="$_AMV"
|
|
284
296
|
mkdir -p "$_AMD/results" "$_AMD/tries" "$_AMD/fails" 2>/dev/null || exit 0
|
|
285
297
|
TRIES=0; [ -f "$_AMD/tries/$AID" ] && IFS= read -r TRIES < "$_AMD/tries/$AID"; case "$TRIES" in ''|*[!0-9]*) TRIES=0 ;; esac
|
|
@@ -287,7 +299,9 @@ case "$EV" in
|
|
|
287
299
|
RES=none; WHYNOT=""
|
|
288
300
|
if [ -n "$VCMD" ] && [ "$VCMD" != none ]; then
|
|
289
301
|
_json_slice "$INPUT" permission_mode >/dev/null; PM="$_JS"
|
|
290
|
-
|
|
302
|
+
# `cd <folder> && <command>` is judged as the command and run in the folder (guard-agent-model.sh).
|
|
303
|
+
_am_verify_split "$VCMD"; VDIR="$_AMVD"; VRUN="$_AMVC"
|
|
304
|
+
if ! _am_verify_ok "$VRUN" "$PM"; then
|
|
291
305
|
# Judged again here, at the moment it would run: the gate judged it when the agent was called, and this
|
|
292
306
|
# is the place the command actually starts. Not run; nothing is concluded about the work.
|
|
293
307
|
RES=blocked; WHYNOT="$_AMVW"; VRC=0
|
|
@@ -302,11 +316,11 @@ case "$EV" in
|
|
|
302
316
|
# control, which gives a background job its own group in bash (macOS ships no setsid). On Windows the tree
|
|
303
317
|
# is ended by its Windows process id.
|
|
304
318
|
if [ -z "${CREW_VERIFY_NO_SETSID:-}" ] && command -v setsid >/dev/null 2>&1; then
|
|
305
|
-
( cd "$_AMP" 2>/dev/null && exec setsid bash -c "$
|
|
319
|
+
( cd "$_AMP/$VDIR" 2>/dev/null && exec setsid bash -c "$VRUN" ) > "$VF" 2>&1 </dev/null &
|
|
306
320
|
VPID=$!
|
|
307
321
|
else
|
|
308
322
|
set -m
|
|
309
|
-
( cd "$_AMP" 2>/dev/null && exec bash -c "$
|
|
323
|
+
( cd "$_AMP/$VDIR" 2>/dev/null && exec bash -c "$VRUN" ) > "$VF" 2>&1 </dev/null &
|
|
310
324
|
VPID=$!
|
|
311
325
|
set +m
|
|
312
326
|
fi
|
|
@@ -359,10 +373,10 @@ case "$EV" in
|
|
|
359
373
|
printf 'verify=%s\nfixes=%s\nconfidence=%s\ntier=%s\nesc=%s\nescalate=%s\nwhynot=%s\ncmd=%s\n' "$RES" "$TRIES" "$CONF" "$ASK" "$ESC" "$ESCL" "$WHYNOT" "$VCMD" > "$_AMD/results/$AID" 2>/dev/null
|
|
360
374
|
case "$REC" in "$_AMD/pending/"*) rm -f "$REC" 2>/dev/null ;; *) : > "$REC.used" 2>/dev/null ;; esac
|
|
361
375
|
TSV="$_AMS/model-outcomes.tsv"
|
|
362
|
-
[ -s "$TSV" ] || printf 'ts\tagent_id\tagent\tchange\trisk\tcard\tmodel\tverify\tfixes\tescalated_from\tran_on\tconfidence\n' > "$TSV" 2>/dev/null
|
|
376
|
+
[ -s "$TSV" ] || printf 'ts\tagent_id\tagent\tchange\trisk\tcard\tmodel\tverify\tfixes\tescalated_from\tran_on\tconfidence\trisk_why\n' > "$TSV" 2>/dev/null
|
|
363
377
|
_ao_now
|
|
364
378
|
_ao_lock
|
|
365
|
-
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_AON" "$AID" "$AG" "$CH" "$RK" "$CID" "$ASK" "$RES" "$TRIES" "$ESC" "$RAN" "$CONF" >> "$TSV" 2>/dev/null
|
|
379
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_AON" "$AID" "$AG" "$CH" "$RK" "$CID" "$ASK" "$RES" "$TRIES" "$ESC" "$RAN" "$CONF" "$RW" >> "$TSV" 2>/dev/null
|
|
366
380
|
_ao_unlock
|
|
367
381
|
case "$RES" in pass|fail) _ao_calibrate "$AG" "$CH" "$RK" ;; esac
|
|
368
382
|
exit 0 ;;
|
|
@@ -357,9 +357,13 @@ _am_tier(){ # $1 = a model as a call or a transcript names it -> _AMT haiku|son
|
|
|
357
357
|
# a .sql file, schema.prisma. The path is read from the project's root down, so the name of the folder the project
|
|
358
358
|
# sits in decides nothing. .claude/crew-model-rules, which only the user writes, adds (`+ glob`) and removes
|
|
359
359
|
# (`- glob`); the last line that matches wins.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
# Legal and personal-data texts are critical too (crew-docs-agent's work): legal, privacy, consent in every form, and
|
|
361
|
+
# terms only in lower case — `terms/`, `terms-of-service.md` — because the camelCase `searchTerms` is a search box.
|
|
362
|
+
# Store listings are critical by place and only as text: under store/, store-listing/, fastlane/metadata/ or
|
|
363
|
+
# metadata/android|ios/, a .txt .md .xml .yml .yaml is; code there (a Redux `store/index.ts`) and images are not.
|
|
364
|
+
_AM_W_L='auth|authn|authz|authentication|authorization|oauth|payments?|billing|migrations?|security|crypto|secrets?|legal|privacy|terms|consent'
|
|
365
|
+
_AM_W_C='Auth|Authn|Authz|Authentication|Authorization|OAuth|Payments?|Billing|Migrations?|Security|Crypto|Secrets?|Legal|Privacy|Consent'
|
|
366
|
+
_AM_W_U='AUTH|AUTHN|AUTHZ|OAUTH|PAYMENTS?|BILLING|MIGRATIONS?|SECURITY|CRYPTO|SECRETS?|LEGAL|PRIVACY|TERMS|CONSENT'
|
|
363
367
|
_AM_RE_L="(^|[^A-Za-z])($_AM_W_L)([^a-z]|\$)"
|
|
364
368
|
_AM_RE_C="(^|[^A-Z])($_AM_W_C)([^a-z]|\$)"
|
|
365
369
|
_AM_RE_U="(^|[^A-Za-z])($_AM_W_U)([^A-Za-z]|\$)"
|
|
@@ -394,7 +398,10 @@ _am_crit_path(){ # $1 = a path or a glob -> 0 critical (_AMW says by what), 1 n
|
|
|
394
398
|
local p r k g hit=1
|
|
395
399
|
_am_rel "$1"; p="$_AMRP"; _AMW=""
|
|
396
400
|
if [[ "$p" =~ $_AM_RE_L ]] || [[ "$p" =~ $_AM_RE_C ]] || [[ "$p" =~ $_AM_RE_U ]]; then hit=0; _AMW="${BASH_REMATCH[2]}"
|
|
397
|
-
else case "$p" in *.sql|*.SQL) hit=0; _AMW="a .sql file" ;; schema.prisma|*/schema.prisma) hit=0; _AMW="schema.prisma" ;;
|
|
401
|
+
else case "$p" in *.sql|*.SQL) hit=0; _AMW="a .sql file" ;; schema.prisma|*/schema.prisma) hit=0; _AMW="schema.prisma" ;;
|
|
402
|
+
store/*|*/store/*|store-listing/*|*/store-listing/*|fastlane/metadata/*|*/fastlane/metadata/*|metadata/android/*|*/metadata/android/*|metadata/ios/*|*/metadata/ios/*)
|
|
403
|
+
case "$p" in *.txt|*.md|*.xml|*.yml|*.yaml) hit=0; _AMW="a store listing text" ;; esac ;;
|
|
404
|
+
esac; fi
|
|
398
405
|
_am_rules
|
|
399
406
|
for r in ${_AM_RULES[@]+"${_AM_RULES[@]}"}; do
|
|
400
407
|
k="${r%%$'\t'*}"; g="${r#*$'\t'}"
|
|
@@ -407,9 +414,9 @@ _am_crit_path(){ # $1 = a path or a glob -> 0 critical (_AMW says by what), 1 n
|
|
|
407
414
|
|
|
408
415
|
# ---- THE CARD ---------------------------------------------------------------------------------------------------
|
|
409
416
|
_AM_CHANGES=' text feature fix-known fix-unknown refactor migration security architecture test-run test-write audit '
|
|
410
|
-
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY; 1 and CARD_WHY
|
|
417
|
+
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (CARD_ABOVE); 1 and CARD_WHY
|
|
411
418
|
local s="$1" l k v n=0 seen=""
|
|
412
|
-
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""
|
|
419
|
+
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""; CARD_ABOVE=""
|
|
413
420
|
s="${s//$'\r'/}"
|
|
414
421
|
while [ "$n" -lt 3 ] && [ -n "$s" ]; do
|
|
415
422
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
@@ -433,7 +440,16 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY; 1
|
|
|
433
440
|
[ -n "$CARD_FILES" ] || { CARD_WHY="files: is empty (name the files or globs, or the folder)"; return 1; }
|
|
434
441
|
case "$_AM_CHANGES" in *" $CARD_CHANGE "*) ;; *) CARD_WHY="change: is '${CARD_CHANGE:0:40}', not one of:$_AM_CHANGES"; return 1 ;; esac
|
|
435
442
|
[ -n "$CARD_VERIFY" ] || { CARD_WHY="verify: is empty (a command, or none)"; return 1; }
|
|
436
|
-
case "$CARD_VERIFY" in none|None|NONE
|
|
443
|
+
case "$CARD_VERIFY" in none|None|NONE|-|'none '*|'none('*|'None '*|'NONE '*) CARD_VERIFY=none ;; esac # "none (the author ran it)" is none
|
|
444
|
+
# Optional, right after the three: `above: <why>` keeps a model above the card's floor. Without it a call above
|
|
445
|
+
# the floor is run on the floor (the cost of a model nobody needed is what this gate exists to stop).
|
|
446
|
+
while [ -n "$s" ]; do
|
|
447
|
+
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
448
|
+
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
449
|
+
[ -n "$l" ] || continue
|
|
450
|
+
case "$l" in above:*) v="${l#above:}"; while :; do case "$v" in [$' \t']*) v="${v:1}" ;; *) break ;; esac; done; CARD_ABOVE="${v:0:200}" ;; esac
|
|
451
|
+
break
|
|
452
|
+
done
|
|
437
453
|
return 0
|
|
438
454
|
}
|
|
439
455
|
_am_risk(){ # after _am_card -> CARD_RISK critical|normal, CARD_RISKWHY
|
|
@@ -453,6 +469,53 @@ _am_digest(){ # after _am_card -> CARD_ID: 12 hex of what the card says, so "th
|
|
|
453
469
|
case "$t" in ''|*[!0-9a-f]*) t="$(printf '%s\n%s\n%s\n' "$CARD_FILES" "$CARD_CHANGE" "$CARD_VERIFY" | cksum 2>/dev/null)"; t="${t%% *}"; printf -v t '%012x' "${t:-0}" 2>/dev/null || t=000000000000 ;; esac
|
|
454
470
|
CARD_ID="${t:0:12}"
|
|
455
471
|
}
|
|
472
|
+
# A call to an agent that is not Crewforth's (general-purpose, a project's own): no card and no floor, so only the
|
|
473
|
+
# expensive end is held, the same rewrite as a crew call above its floor.
|
|
474
|
+
# * It names opus or fable, and its task has no `above: <why>` line: it runs on sonnet.
|
|
475
|
+
# * It names no model, and the agent takes the session's (general-purpose, or a project or user agent whose
|
|
476
|
+
# definition has no model of its own): it runs on sonnet, unless the session itself is on haiku, read from the
|
|
477
|
+
# end of its transcript. An agent whose definition names a model (Explore, claude-code-guide) is left to it:
|
|
478
|
+
# a model in the call would override a cheaper one there.
|
|
479
|
+
# Measured on 2.1.299: a general-purpose call that named no model ran on the session's model; CLAUDE_CODE_SUBAGENT_MODEL
|
|
480
|
+
# in the project's settings `env` did not change that (in the shell's environment it did), so the hook sets it.
|
|
481
|
+
_am_outside(){ # $1 = the tool_input text, $2 = the agent's name
|
|
482
|
+
local m pr to=sonnet ui name="$2" f d l ln inherit=0 tp last
|
|
483
|
+
case "$name" in ''|*[!A-Za-z0-9._-]*) name="an agent" ;; esac
|
|
484
|
+
_json_keycount "$1" prompt; [ "$_KC" = 1 ] || return 0
|
|
485
|
+
_json_slice "$1" prompt >/dev/null; pr="${_JS:0:6000}"; _json_unescape "$pr" >/dev/null; pr=$'\n'"${_JU//$'\r'/}"
|
|
486
|
+
case "$pr" in *$'\n'above:*|*$'\n'[$' \t']above:*) return 0 ;; esac
|
|
487
|
+
_json_keycount "$1" model
|
|
488
|
+
if [ "$_KC" = 0 ]; then
|
|
489
|
+
case "$name" in
|
|
490
|
+
general-purpose) inherit=1 ;;
|
|
491
|
+
'an agent') ;;
|
|
492
|
+
*) for d in "${CLAUDE_PROJECT_DIR:-.}/.claude/agents" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/agents"; do
|
|
493
|
+
f="$d/$name.md"; [ -f "$f" ] || continue
|
|
494
|
+
inherit=1; ln=0
|
|
495
|
+
while IFS= read -r l || [ -n "$l" ]; do
|
|
496
|
+
ln=$((ln+1)); [ "$ln" -le 40 ] || break
|
|
497
|
+
case "$l" in ---*) [ "$ln" -gt 1 ] && break ;; model:*) case "${l#model:}" in *inherit*|'') ;; *) inherit=0 ;; esac ;; esac
|
|
498
|
+
done < "$f"
|
|
499
|
+
break
|
|
500
|
+
done ;;
|
|
501
|
+
esac
|
|
502
|
+
[ "$inherit" = 1 ] || return 0
|
|
503
|
+
_json_slice "$INPUT" transcript_path >/dev/null; _json_unescape "$_JS" >/dev/null; tp="$_JU"
|
|
504
|
+
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
|
505
|
+
last="$(tail -c 65536 "$tp" 2>/dev/null)"; last="${last##*\"model\":\"}"; last="${last%%\"*}"
|
|
506
|
+
case "$last" in *haiku*) return 0 ;; esac
|
|
507
|
+
fi
|
|
508
|
+
m="the session's model"
|
|
509
|
+
else
|
|
510
|
+
[ "$_KC" = 1 ] || return 0
|
|
511
|
+
_json_slice "$1" model >/dev/null; _json_unescape "$_JS" >/dev/null; _am_tier "$_JU"; m="$_AMT"
|
|
512
|
+
case "$m" in opus|fable) ;; *) return 0 ;; esac
|
|
513
|
+
fi
|
|
514
|
+
ui="$(printf '%s' "$INPUT" | awk -v op=get -v path=tool_input -v setk=model -v setv="\"$to\"" -f "$_AM_HERE/../eval/lib/settings-json.awk" - 2>/dev/null)" || return 0
|
|
515
|
+
case "$ui" in '{'*'}') ;; *) return 0 ;; esac
|
|
516
|
+
_am_log "note: $name (not a crew agent) asked for $m, run on $to; add 'above: <why>' to its task to keep a model above it"
|
|
517
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"Crewforth: %s asked for %s and runs on %s (a line above: <why> in its task keeps a model above it)","updatedInput":%s}}\n' "$name" "$m" "$to" "$ui"
|
|
518
|
+
}
|
|
456
519
|
_am_rec_get(){ # $1 = a record file, $2 = key -> _AMV (lines are key=value)
|
|
457
520
|
local l; _AMV=""
|
|
458
521
|
[ -f "$1" ] || return 1
|
|
@@ -602,6 +665,19 @@ _am_perm_hit(){ # $1 = command, $2 = allow|deny|ask -> 0 a Bash(...) rule of th
|
|
|
602
665
|
done <<< "$_AMSF"
|
|
603
666
|
return 1
|
|
604
667
|
}
|
|
668
|
+
# A verify command in a sub-folder: `cd apps/mobile && flutter test`, the usual shape in a repository of several
|
|
669
|
+
# projects (measured on one: 41 of 71 refused verify commands had it). Split into the folder and the one command,
|
|
670
|
+
# so the command is judged alone and run in that folder with no chain. Only a relative folder inside the project,
|
|
671
|
+
# with no .. step, that exists; anything else is left whole, and is refused as a chain as before.
|
|
672
|
+
_am_verify_split(){ # $1 = the command -> _AMVD the folder ("" = the project root), _AMVC the command
|
|
673
|
+
local c="$1" d r
|
|
674
|
+
_AMVD=""; _AMVC="$c"
|
|
675
|
+
case "$c" in 'cd '*' && '*) ;; *) return 0 ;; esac
|
|
676
|
+
d="${c#cd }"; d="${d%% && *}"; r="${c#cd "$d" && }"
|
|
677
|
+
case "$d" in ''|/*|~*|*[!A-Za-z0-9._/-]*|..|../*|*/..|*/../*) return 0 ;; esac
|
|
678
|
+
[ -n "$r" ] && [ -d "${_AMP:-.}/$d" ] || return 0
|
|
679
|
+
_AMVD="$d"; _AMVC="$r"
|
|
680
|
+
}
|
|
605
681
|
_am_verify_ok(){ # $1 = the command, $2 = permission mode -> 0 it may run (_AMVB says by what); 1 and _AMVW says why not
|
|
606
682
|
local c="$1" pm="${2:-default}" f r rc k g
|
|
607
683
|
_AMVW=""; _AMVB=""
|
|
@@ -722,7 +798,7 @@ _json_keycount "$ti" subagent_type
|
|
|
722
798
|
if [ "$_KC" -gt 1 ]; then echo "GUARD (agent model): this call names subagent_type more than once, so which agent it starts cannot be read and the call is refused. Send one." >&2; _am_log "an ambiguous call"; exit 2; fi
|
|
723
799
|
_json_slice "$ti" subagent_type >/dev/null; _json_unescape "$_JS" >/dev/null; st="$_JU"
|
|
724
800
|
st="${st##*:}" # a plugin names its agents <plugin>:crew-…
|
|
725
|
-
case "$st" in crew-*) ;; *) exit 0 ;; esac
|
|
801
|
+
case "$st" in crew-*) ;; *) _am_outside "$ti" "$st"; exit 0 ;; esac
|
|
726
802
|
case "$st" in *[!A-Za-z0-9_-]*) echo "GUARD (agent model): the agent's name holds a character this gate does not read, so the call is refused." >&2; _am_log "an agent name it cannot read"; exit 2 ;; esac
|
|
727
803
|
_json_keycount "$ti" model
|
|
728
804
|
if [ "$_KC" -gt 1 ]; then echo "GUARD (agent model): this call names model more than once, so which model it asks for cannot be read and the call is refused. Send one." >&2; _am_log "an ambiguous call"; exit 2; fi
|
|
@@ -795,6 +871,23 @@ if [ "$rank" -lt "$floor" ]; then
|
|
|
795
871
|
echo "GUARD (agent model): $st runs on $fname or above for this card ($why), and this call asks for $m. Repeat the same call with model $fname." >&2
|
|
796
872
|
_am_log "below the floor of the card"; exit 2
|
|
797
873
|
fi
|
|
874
|
+
# ---- above the floor: run on the floor, unless the card says why not ----
|
|
875
|
+
# The floor is what the work needs; a model above it is cost nobody asked for (measured on a real project: 73 of 186
|
|
876
|
+
# crew tasks ran above their floor, 0 on haiku). Unless the card carries `above: <why>`, the call is let through ON
|
|
877
|
+
# THE FLOOR: the hook hands Claude Code the same input with `model` replaced (PreToolUse updatedInput; measured on
|
|
878
|
+
# 2.1.299: a call asking for opus ran on haiku alone). The rewrite is one awk over the payload, on this path only.
|
|
879
|
+
lowered=""
|
|
880
|
+
low=$floor; [ "$low" -lt 1 ] && low=1
|
|
881
|
+
if [ "$rank" -gt "$low" ] && [ -z "$CARD_ABOVE" ]; then
|
|
882
|
+
_am_name "$low"; to="$_AMN"
|
|
883
|
+
ui="$(printf '%s' "$INPUT" | awk -v op=get -v path=tool_input -v setk=model -v setv="\"$to\"" -f "$_AM_HERE/../eval/lib/settings-json.awk" - 2>/dev/null)" || ui=""
|
|
884
|
+
case "$ui" in '{'*'}') ;; *) ui="" ;; esac
|
|
885
|
+
if [ -n "$ui" ]; then
|
|
886
|
+
lowered="$m"
|
|
887
|
+
_am_log "note: $st asked for $m, run on $to, the floor of this card (${why:-no floor above haiku}); add 'above: <why>' to the card to keep $m"
|
|
888
|
+
m="$to"
|
|
889
|
+
fi
|
|
890
|
+
fi
|
|
798
891
|
# ---- where it runs, and what its verify command is ----
|
|
799
892
|
_json_keycount "$ti" run_in_background
|
|
800
893
|
if [ "$_KC" -gt 1 ]; then echo "GUARD (agent model): this call names run_in_background more than once, so where it runs cannot be read and the call is refused. Send one." >&2; _am_log "an ambiguous call"; exit 2; fi
|
|
@@ -818,7 +911,14 @@ fi
|
|
|
818
911
|
_json_slice "$INPUT" tool_use_id >/dev/null; tid="$_JS"; case "$tid" in ''|*[!A-Za-z0-9._-]*) tid="x$RANDOM" ;; esac
|
|
819
912
|
if mkdir -p "$_AMD/pending" 2>/dev/null; then
|
|
820
913
|
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v t '%(%s)T' -1; else t="$(date +%s)"; fi
|
|
821
|
-
|
|
914
|
+
# Why the work counts as critical, as the gate decided it, so a reader shows the reason and does not work it out
|
|
915
|
+
# again: one line, no tab (it ends up in a column of model-outcomes.tsv).
|
|
916
|
+
rw="${CARD_RISKWHY:--}"; rw="${rw//$'\t'/ }"; rw="${rw//$'\r'/ }"; rw="${rw//$'\n'/ }"
|
|
917
|
+
{ printf 'agent=%s\ntier=%s\nchange=%s\nrisk=%s\ncard=%s\nbg=%s\nesc=%s\nverify=%s\nfiles=%s\nlowered=%s\nwhy=%s\n' "$st" "$m" "$CARD_CHANGE" "$CARD_RISK" "$CARD_ID" "$bg" "${p:--}" "$CARD_VERIFY" "$CARD_FILES" "${lowered:--}" "$rw" > "$_AMD/pending/$t-$tid"; } 2>/dev/null || true
|
|
918
|
+
fi
|
|
919
|
+
# The call, on the floor: the same input with `model` replaced, and allowed. Nothing else is printed on this path.
|
|
920
|
+
if [ -n "$lowered" ]; then
|
|
921
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"Crewforth: %s asked for %s and runs on %s, the floor of its card (add above: <why> to the card to keep %s)","updatedInput":%s}}\n' "$st" "$lowered" "$m" "$lowered" "$ui"
|
|
822
922
|
fi
|
|
823
923
|
exit 0
|
|
824
924
|
}
|