crewforth 3.2.0-rc.1 → 3.2.0-rc.3
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 +133 -3
- package/README.md +2 -0
- package/VERSION +1 -1
- package/adopt.sh +55 -6
- package/bin/cli.js +3 -2
- package/kit/CLAUDE.md +6 -6
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/eval/doctor.sh +37 -3
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/lib/crew-map/narrative.mjs +42 -8
- package/kit/eval/lib/crew-map/project.mjs +3 -2
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +56 -4
- package/kit/eval/lib/crew-map/stacks.mjs +114 -8
- package/kit/eval/lib/crew-map/surface.mjs +62 -1
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +14 -4
- package/kit/eval/lib/statusline.sh +4 -19
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +251 -21
- package/kit/hooks/agent-outcome.sh +20 -9
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +114 -15
- package/kit/hooks/guard-bash.sh +36 -21
- package/kit/hooks/guard-write.sh +3 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/settings.json +12 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-map/SKILL.md +16 -5
- package/kit/skills/crew-review/SKILL.md +2 -1
- package/kit/studio/README.md +21 -10
- package/kit/studio/server/index.js +85 -22
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +104 -18
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/web/app.js +79 -11
- package/kit/studio/web/canvas.js +14 -2
- package/kit/studio/web/chat.js +114 -24
- package/kit/studio/web/graph-plan.js +3 -2
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +72 -3
- package/kit/studio/web/map.js +103 -10
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +40 -0
- package/kit/studio/web/tokens.css +6 -0
- package/package.json +1 -1
- package/start.sh +14 -1
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
# * RISK CLASS. `critical` when the change is migration, security, architecture or fix-unknown, or a file is on a
|
|
15
15
|
# critical path (auth, payments, billing, migrations, security, crypto, secrets, *.sql, schema.prisma;
|
|
16
16
|
# .claude/crew-model-rules adds and removes paths). Otherwise `normal`.
|
|
17
|
-
# * critical work runs on opus, whatever the agent.
|
|
17
|
+
# * critical work runs on opus, whatever the agent. Two exceptions: test-run (running the tests is objective), and
|
|
18
|
+
# a text change whose every file is prose (.md .txt .rst .adoc), which runs on sonnet or above: a reworded
|
|
19
|
+
# paragraph under legal/ is read by a person, and opus for it is the cost this gate exists to stop. A project
|
|
20
|
+
# that wants opus there says so in .claude/crew-model-rules (floor * text critical opus).
|
|
18
21
|
# * the referee is as strong as the risk: test-write and audit on sonnet or above (opus when critical, by the rule
|
|
19
22
|
# above); review, privacy, planner and database agents on sonnet or above; the security agent on opus.
|
|
20
|
-
# * work with no verify command runs on sonnet or above: haiku goes only to work a command can check.
|
|
23
|
+
# * work with no verify command runs on sonnet or above: haiku goes only to work a command can check. Not for
|
|
24
|
+
# change: text, nor for the commit agent: a reader is all prose ever has, and a commit message is checked by
|
|
25
|
+
# the commit-msg hook (seen on a real project: a commit message proposed on sonnet for that reason alone).
|
|
21
26
|
# * a class that failed too often is held one model up (crew-model-floors.auto, written by hooks/agent-outcome.sh),
|
|
22
27
|
# until the user lowers it again with /crew-loosen (one model, once for each raise).
|
|
23
28
|
# * a card whose verify failed is not run again on the same or a lower model, and not a third time at all.
|
|
@@ -412,11 +417,23 @@ _am_crit_path(){ # $1 = a path or a glob -> 0 critical (_AMW says by what), 1 n
|
|
|
412
417
|
return "$hit"
|
|
413
418
|
}
|
|
414
419
|
|
|
420
|
+
_am_prose(){ # $1 = a path or a glob -> 0 when it is prose a person reads (the exception for a text change)
|
|
421
|
+
case "$1" in *.md|*.MD|*.markdown|*.txt|*.TXT|*.rst|*.adoc) return 0 ;; esac
|
|
422
|
+
return 1
|
|
423
|
+
}
|
|
424
|
+
_am_all_prose(){ # after _am_card -> 0 when every file of the card is prose
|
|
425
|
+
local f r=0 unglob=0
|
|
426
|
+
case "$-" in *f*) ;; *) unglob=1; set -f ;; esac
|
|
427
|
+
for f in ${CARD_FILES//,/ }; do _am_prose "$f" || { r=1; break; }; done
|
|
428
|
+
[ "$unglob" = 1 ] && set +f
|
|
429
|
+
return "$r"
|
|
430
|
+
}
|
|
431
|
+
|
|
415
432
|
# ---- THE CARD ---------------------------------------------------------------------------------------------------
|
|
416
433
|
_AM_CHANGES=' text feature fix-known fix-unknown refactor migration security architecture test-run test-write audit '
|
|
417
|
-
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY; 1 and CARD_WHY
|
|
434
|
+
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (CARD_ABOVE); 1 and CARD_WHY
|
|
418
435
|
local s="$1" l k v n=0 seen=""
|
|
419
|
-
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""
|
|
436
|
+
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""; CARD_ABOVE=""
|
|
420
437
|
s="${s//$'\r'/}"
|
|
421
438
|
while [ "$n" -lt 3 ] && [ -n "$s" ]; do
|
|
422
439
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
@@ -440,7 +457,16 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY; 1
|
|
|
440
457
|
[ -n "$CARD_FILES" ] || { CARD_WHY="files: is empty (name the files or globs, or the folder)"; return 1; }
|
|
441
458
|
case "$_AM_CHANGES" in *" $CARD_CHANGE "*) ;; *) CARD_WHY="change: is '${CARD_CHANGE:0:40}', not one of:$_AM_CHANGES"; return 1 ;; esac
|
|
442
459
|
[ -n "$CARD_VERIFY" ] || { CARD_WHY="verify: is empty (a command, or none)"; return 1; }
|
|
443
|
-
case "$CARD_VERIFY" in none|None|NONE
|
|
460
|
+
case "$CARD_VERIFY" in none|None|NONE|-|'none '*|'none('*|'None '*|'NONE '*) CARD_VERIFY=none ;; esac # "none (the author ran it)" is none
|
|
461
|
+
# Optional, right after the three: `above: <why>` keeps a model above the card's floor. Without it a call above
|
|
462
|
+
# the floor is run on the floor (the cost of a model nobody needed is what this gate exists to stop).
|
|
463
|
+
while [ -n "$s" ]; do
|
|
464
|
+
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
465
|
+
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
466
|
+
[ -n "$l" ] || continue
|
|
467
|
+
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
|
|
468
|
+
break
|
|
469
|
+
done
|
|
444
470
|
return 0
|
|
445
471
|
}
|
|
446
472
|
_am_risk(){ # after _am_card -> CARD_RISK critical|normal, CARD_RISKWHY
|
|
@@ -460,6 +486,53 @@ _am_digest(){ # after _am_card -> CARD_ID: 12 hex of what the card says, so "th
|
|
|
460
486
|
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
|
|
461
487
|
CARD_ID="${t:0:12}"
|
|
462
488
|
}
|
|
489
|
+
# A call to an agent that is not Crewforth's (general-purpose, a project's own): no card and no floor, so only the
|
|
490
|
+
# expensive end is held, the same rewrite as a crew call above its floor.
|
|
491
|
+
# * It names opus or fable, and its task has no `above: <why>` line: it runs on sonnet.
|
|
492
|
+
# * It names no model, and the agent takes the session's (general-purpose, or a project or user agent whose
|
|
493
|
+
# definition has no model of its own): it runs on sonnet, unless the session itself is on haiku, read from the
|
|
494
|
+
# end of its transcript. An agent whose definition names a model (Explore, claude-code-guide) is left to it:
|
|
495
|
+
# a model in the call would override a cheaper one there.
|
|
496
|
+
# Measured on 2.1.299: a general-purpose call that named no model ran on the session's model; CLAUDE_CODE_SUBAGENT_MODEL
|
|
497
|
+
# in the project's settings `env` did not change that (in the shell's environment it did), so the hook sets it.
|
|
498
|
+
_am_outside(){ # $1 = the tool_input text, $2 = the agent's name
|
|
499
|
+
local m pr to=sonnet ui name="$2" f d l ln inherit=0 tp last
|
|
500
|
+
case "$name" in ''|*[!A-Za-z0-9._-]*) name="an agent" ;; esac
|
|
501
|
+
_json_keycount "$1" prompt; [ "$_KC" = 1 ] || return 0
|
|
502
|
+
_json_slice "$1" prompt >/dev/null; pr="${_JS:0:6000}"; _json_unescape "$pr" >/dev/null; pr=$'\n'"${_JU//$'\r'/}"
|
|
503
|
+
case "$pr" in *$'\n'above:*|*$'\n'[$' \t']above:*) return 0 ;; esac
|
|
504
|
+
_json_keycount "$1" model
|
|
505
|
+
if [ "$_KC" = 0 ]; then
|
|
506
|
+
case "$name" in
|
|
507
|
+
general-purpose) inherit=1 ;;
|
|
508
|
+
'an agent') ;;
|
|
509
|
+
*) for d in "${CLAUDE_PROJECT_DIR:-.}/.claude/agents" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/agents"; do
|
|
510
|
+
f="$d/$name.md"; [ -f "$f" ] || continue
|
|
511
|
+
inherit=1; ln=0
|
|
512
|
+
while IFS= read -r l || [ -n "$l" ]; do
|
|
513
|
+
ln=$((ln+1)); [ "$ln" -le 40 ] || break
|
|
514
|
+
case "$l" in ---*) [ "$ln" -gt 1 ] && break ;; model:*) case "${l#model:}" in *inherit*|'') ;; *) inherit=0 ;; esac ;; esac
|
|
515
|
+
done < "$f"
|
|
516
|
+
break
|
|
517
|
+
done ;;
|
|
518
|
+
esac
|
|
519
|
+
[ "$inherit" = 1 ] || return 0
|
|
520
|
+
_json_slice "$INPUT" transcript_path >/dev/null; _json_unescape "$_JS" >/dev/null; tp="$_JU"
|
|
521
|
+
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
|
522
|
+
last="$(tail -c 65536 "$tp" 2>/dev/null)"; last="${last##*\"model\":\"}"; last="${last%%\"*}"
|
|
523
|
+
case "$last" in *haiku*) return 0 ;; esac
|
|
524
|
+
fi
|
|
525
|
+
m="the session's model"
|
|
526
|
+
else
|
|
527
|
+
[ "$_KC" = 1 ] || return 0
|
|
528
|
+
_json_slice "$1" model >/dev/null; _json_unescape "$_JS" >/dev/null; _am_tier "$_JU"; m="$_AMT"
|
|
529
|
+
case "$m" in opus|fable) ;; *) return 0 ;; esac
|
|
530
|
+
fi
|
|
531
|
+
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
|
|
532
|
+
case "$ui" in '{'*'}') ;; *) return 0 ;; esac
|
|
533
|
+
_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"
|
|
534
|
+
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"
|
|
535
|
+
}
|
|
463
536
|
_am_rec_get(){ # $1 = a record file, $2 = key -> _AMV (lines are key=value)
|
|
464
537
|
local l; _AMV=""
|
|
465
538
|
[ -f "$1" ] || return 1
|
|
@@ -501,13 +574,10 @@ _am_floor_now(){ # $1 agent, $2 change, $3 risk -> _AMF_TIER (the raised floor,
|
|
|
501
574
|
|
|
502
575
|
# ---- the gate log: a refusal of this gate is recorded like any other gate's -------------------------------------
|
|
503
576
|
_am_log(){ # $1 = the rule. Same file and line shape as guard-bash.sh's gatelog; the call's text is never recorded.
|
|
504
|
-
local gl="
|
|
577
|
+
local gl="" v=BLOCK
|
|
505
578
|
case "$1" in "note: "*) v=NOTE ;; esac # something worth knowing that refused nothing
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git check-ignore -q ".claude/gate-log.tsv" 2>/dev/null || return 0; fi
|
|
509
|
-
gl=".claude/gate-log.tsv"
|
|
510
|
-
fi
|
|
579
|
+
declare -F crew_gate_log_write >/dev/null || return 0
|
|
580
|
+
crew_gate_log_write; gl="$_CREW_GL"; [ -n "$gl" ] || return 0
|
|
511
581
|
printf '%s\t§model\t%s\t\n' "$v" "$1" >> "$gl" 2>/dev/null || true
|
|
512
582
|
}
|
|
513
583
|
|
|
@@ -538,6 +608,7 @@ _am_log(){ # $1 = the rule. Same file and line shape as guard-bash.sh's gatelog
|
|
|
538
608
|
# 5. THE SHELL GATE AGREES, asked under a session id that is not the session's: only "nothing to say" or an
|
|
539
609
|
# explicit allow passes.
|
|
540
610
|
_AM_HERE="${BASH_SOURCE%/*}"; [ "$_AM_HERE" = "${BASH_SOURCE}" ] && _AM_HERE=.
|
|
611
|
+
[ -f "$_AM_HERE/../eval/lib/gate-log.sh" ] && . "$_AM_HERE/../eval/lib/gate-log.sh" # where the gate log goes
|
|
541
612
|
# CREW-NOT-A-RUNG: names of commands a PROJECT's verify line may begin with; nothing here is called by Crewforth.
|
|
542
613
|
_AM_RUNNERS=('npm test' 'npm t' 'pnpm test' 'yarn test' 'bun test' 'deno test'
|
|
543
614
|
'dotnet test' 'dotnet build' 'flutter test' 'flutter analyze' 'dart test' 'dart analyze' 'go test' 'go build' 'go vet'
|
|
@@ -609,6 +680,9 @@ _am_perm_hit(){ # $1 = command, $2 = allow|deny|ask -> 0 a Bash(...) rule of th
|
|
|
609
680
|
done <<< "$_AMSF"
|
|
610
681
|
return 1
|
|
611
682
|
}
|
|
683
|
+
# How a verify command is read (the `cd <folder> && <command>` split): eval/lib/verify-cmd.sh, shared with doctor.
|
|
684
|
+
[ -f "$_AM_HERE/../eval/lib/verify-cmd.sh" ] && . "$_AM_HERE/../eval/lib/verify-cmd.sh"
|
|
685
|
+
_am_verify_split(){ _AMVD=""; _AMVC="$1"; declare -F crew_verify_split >/dev/null && crew_verify_split "$1" "${_AMP:-.}"; return 0; }
|
|
612
686
|
_am_verify_ok(){ # $1 = the command, $2 = permission mode -> 0 it may run (_AMVB says by what); 1 and _AMVW says why not
|
|
613
687
|
local c="$1" pm="${2:-default}" f r rc k g
|
|
614
688
|
_AMVW=""; _AMVB=""
|
|
@@ -690,6 +764,8 @@ _am_write_check(){ # $1 = the payload, $2 = the file about to be written -> ret
|
|
|
690
764
|
if [ -f "$f" ]; then m="$(grep -m1 -o '"model":"[^"]*"' "$f" 2>/dev/null)" || m=""; _am_tier "$m"; tier="$_AMT"; fi
|
|
691
765
|
fi
|
|
692
766
|
case "$tier" in opus|fable) return 0 ;; esac
|
|
767
|
+
# A text change on sonnet may write prose there: the call was let through on that floor for exactly that.
|
|
768
|
+
if [ "$tier" = sonnet ] && _am_prose "$_AMRP" && _am_rec_get "$_AMD/agents/$aid" change && [ "$_AMV" = text ]; then return 0; fi
|
|
693
769
|
_am_log "critical path: write by an agent not on opus"
|
|
694
770
|
echo "GUARD (agent model): $_AMRP is on a critical path ($_AMW) and this agent runs on ${tier:-a model that could not be read}; only an agent on opus writes there. Do not reach the file another way. Stop, and end your report with the two lines: escalate: $_AMRP / confidence: low" >&2
|
|
695
771
|
exit 2
|
|
@@ -729,7 +805,7 @@ _json_keycount "$ti" subagent_type
|
|
|
729
805
|
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
|
|
730
806
|
_json_slice "$ti" subagent_type >/dev/null; _json_unescape "$_JS" >/dev/null; st="$_JU"
|
|
731
807
|
st="${st##*:}" # a plugin names its agents <plugin>:crew-…
|
|
732
|
-
case "$st" in crew-*) ;; *) exit 0 ;; esac
|
|
808
|
+
case "$st" in crew-*) ;; *) _am_outside "$ti" "$st"; exit 0 ;; esac
|
|
733
809
|
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
|
|
734
810
|
_json_keycount "$ti" model
|
|
735
811
|
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
|
|
@@ -763,9 +839,11 @@ if ! _am_card "$prompt"; then
|
|
|
763
839
|
fi
|
|
764
840
|
_am_state; _am_risk; _am_digest
|
|
765
841
|
# ---- the floor of THIS card: the highest of what applies, and the reason that set it ----
|
|
766
|
-
if [ "$CARD_VERIFY" = none ] && [ "$floor" -lt 2 ]; then floor=2; why="no verify command, so nothing checks the work but a reader"; fi
|
|
842
|
+
if [ "$CARD_VERIFY" = none ] && [ "$CARD_CHANGE" != text ] && [ "$st" != crew-commit-agent ] && [ "$floor" -lt 2 ]; then floor=2; why="no verify command, so nothing checks the work but a reader"; fi
|
|
767
843
|
case "$CARD_CHANGE" in test-write|audit) if [ "$floor" -lt 2 ]; then floor=2; why="change: $CARD_CHANGE, the referee of the work"; fi ;; esac
|
|
768
|
-
if [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE"
|
|
844
|
+
if [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" = text ] && _am_all_prose; then
|
|
845
|
+
if [ "$floor" -lt 2 ]; then floor=2; why="a text change on a critical path: $CARD_RISKWHY"; fi
|
|
846
|
+
elif [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" != test-run ] && [ "$floor" -lt 3 ]; then floor=3; why="critical work: $CARD_RISKWHY"; fi
|
|
769
847
|
# A class the user's rules or the record of outcomes hold higher. Neither can lower anything here.
|
|
770
848
|
_am_rules
|
|
771
849
|
for r in ${_AM_RULES[@]+"${_AM_RULES[@]}"}; do
|
|
@@ -802,6 +880,23 @@ if [ "$rank" -lt "$floor" ]; then
|
|
|
802
880
|
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
|
|
803
881
|
_am_log "below the floor of the card"; exit 2
|
|
804
882
|
fi
|
|
883
|
+
# ---- above the floor: run on the floor, unless the card says why not ----
|
|
884
|
+
# The floor is what the work needs; a model above it is cost nobody asked for (measured on a real project: 73 of 186
|
|
885
|
+
# crew tasks ran above their floor, 0 on haiku). Unless the card carries `above: <why>`, the call is let through ON
|
|
886
|
+
# THE FLOOR: the hook hands Claude Code the same input with `model` replaced (PreToolUse updatedInput; measured on
|
|
887
|
+
# 2.1.299: a call asking for opus ran on haiku alone). The rewrite is one awk over the payload, on this path only.
|
|
888
|
+
lowered=""
|
|
889
|
+
low=$floor; [ "$low" -lt 1 ] && low=1
|
|
890
|
+
if [ "$rank" -gt "$low" ] && [ -z "$CARD_ABOVE" ]; then
|
|
891
|
+
_am_name "$low"; to="$_AMN"
|
|
892
|
+
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=""
|
|
893
|
+
case "$ui" in '{'*'}') ;; *) ui="" ;; esac
|
|
894
|
+
if [ -n "$ui" ]; then
|
|
895
|
+
lowered="$m"
|
|
896
|
+
_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"
|
|
897
|
+
m="$to"
|
|
898
|
+
fi
|
|
899
|
+
fi
|
|
805
900
|
# ---- where it runs, and what its verify command is ----
|
|
806
901
|
_json_keycount "$ti" run_in_background
|
|
807
902
|
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
|
|
@@ -828,7 +923,11 @@ if mkdir -p "$_AMD/pending" 2>/dev/null; then
|
|
|
828
923
|
# Why the work counts as critical, as the gate decided it, so a reader shows the reason and does not work it out
|
|
829
924
|
# again: one line, no tab (it ends up in a column of model-outcomes.tsv).
|
|
830
925
|
rw="${CARD_RISKWHY:--}"; rw="${rw//$'\t'/ }"; rw="${rw//$'\r'/ }"; rw="${rw//$'\n'/ }"
|
|
831
|
-
{ printf 'agent=%s\ntier=%s\nchange=%s\nrisk=%s\ncard=%s\nbg=%s\nesc=%s\nverify=%s\nfiles=%s\nwhy=%s\n' "$st" "$m" "$CARD_CHANGE" "$CARD_RISK" "$CARD_ID" "$bg" "${p:--}" "$CARD_VERIFY" "$CARD_FILES" "$rw" > "$_AMD/pending/$t-$tid"; } 2>/dev/null || true
|
|
926
|
+
{ 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
|
|
927
|
+
fi
|
|
928
|
+
# The call, on the floor: the same input with `model` replaced, and allowed. Nothing else is printed on this path.
|
|
929
|
+
if [ -n "$lowered" ]; then
|
|
930
|
+
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"
|
|
832
931
|
fi
|
|
833
932
|
exit 0
|
|
834
933
|
}
|
package/kit/hooks/guard-bash.sh
CHANGED
|
@@ -148,7 +148,14 @@ _join_cmd(){ # CMD -> CMD with its continued lines joined
|
|
|
148
148
|
# ---- /CREW-JOIN
|
|
149
149
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
150
150
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
151
|
-
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
151
|
+
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
152
|
+
# Where the gate log goes: one rule for every gate and reader (eval/lib/gate-log.sh).
|
|
153
|
+
[ -f "$_crew_d/../eval/lib/gate-log.sh" ] && . "$_crew_d/../eval/lib/gate-log.sh"
|
|
154
|
+
# What a stopped or asked command would have touched (eval/lib/preview.sh), read only when one is stopped or asked.
|
|
155
|
+
_CREW_PV_LIB="$_crew_d/../eval/lib/preview.sh"; unset _crew_d
|
|
156
|
+
# The preview of a command. ALWAYS called as "$(_pv_line …)": the command substitution is the subshell that keeps an
|
|
157
|
+
# exit or a failure in the library from ending this hook or changing its decision (smoke plants both).
|
|
158
|
+
_pv_line(){ [ -f "$_CREW_PV_LIB" ] || return 0; . "$_CREW_PV_LIB" 2>/dev/null && crew_preview "$1" "$2" 2>/dev/null && printf '%s' "${_CREW_PV:-}"; return 0; }
|
|
152
159
|
# Read with the builtin, as guard-powershell.sh does: `INPUT="$(cat)"` is a subshell plus a process on EVERY call
|
|
153
160
|
# of this hook. `read` returns 1 at end of input; the text is read regardless.
|
|
154
161
|
IFS= read -r -d '' INPUT || true
|
|
@@ -168,20 +175,17 @@ IFS= read -r -d '' INPUT || true
|
|
|
168
175
|
# token, and `/crew-gates` never prints it — the report is rule names and counts. Recording it by default would
|
|
169
176
|
# buy nothing and add a place for a secret to sit. `CREW_GATE_LOG_CMD=1` puts it back for debugging a false
|
|
170
177
|
# positive, which is the only thing it is good for.
|
|
171
|
-
# Where the
|
|
172
|
-
#
|
|
173
|
-
# the
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
+
# Where the log goes. An explicit CREW_GATE_LOG is the operator's call and is used as given. .claude/gate-log.tsv
|
|
179
|
+
# only where writing there cannot surprise anyone: outside a git repo, or where git ignores it (a kit install
|
|
180
|
+
# ignores .claude/; the plugin edition drops into repos the installer never touched, and this repo proved the
|
|
181
|
+
# failure: the suite left a gate-log.tsv in `git status` waiting to be committed). Elsewhere it goes inside the
|
|
182
|
+
# repository's .git, which is never committed. git is asked only when a decision is logged (a block, an approval
|
|
183
|
+
# prompt or a CLAUDE_GIT_OK allow); ordinary commands never reach it.
|
|
184
|
+
# Since 3.2.0 a repository that tracks .claude/ gets the log inside its .git (eval/lib/gate-log.sh), where it was
|
|
185
|
+
# not written at all before.
|
|
178
186
|
_gatelog_path(){
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
182
|
-
git check-ignore -q ".claude/gate-log.tsv" 2>/dev/null || return 0
|
|
183
|
-
fi
|
|
184
|
-
printf '%s' ".claude/gate-log.tsv"
|
|
187
|
+
declare -F crew_gate_log_write >/dev/null || return 0
|
|
188
|
+
crew_gate_log_write; printf '%s' "$_CREW_GL"
|
|
185
189
|
}
|
|
186
190
|
gatelog(){ # $1 = verdict (BLOCK|ASK|ALLOW) $2 = section $3 = rule
|
|
187
191
|
# Resolve the path ONCE per hook run and remember it HERE, not inside _gatelog_path: that function is invoked
|
|
@@ -720,6 +724,8 @@ block(){
|
|
|
720
724
|
gatelog BLOCK "$2" "$1"
|
|
721
725
|
echo "GUARD (§$2): '$1' stopped AT THE TOOL LEVEL." >&2
|
|
722
726
|
[ -z "${_BLOCK_NOTE:-}" ] || echo "$_BLOCK_NOTE" >&2
|
|
727
|
+
# What it would have touched, when the rule names a kind (_PV_KIND): read only, after the decision is made.
|
|
728
|
+
if [ -n "${_PV_KIND:-}" ]; then _pvo="$(_pv_line "$_PV_KIND" "${_PV_CMD:-$CMD}")"; [ -n "$_pvo" ] && echo "$_pvo" >&2; fi
|
|
723
729
|
case "${3:-}" in
|
|
724
730
|
loss) echo "Nothing here is undone by retrying — but the refusal is not the end of the task. Do the same job the reversible way: name the paths instead of sweeping, and look first (git clean -n, ls) so you act on what you can see. Only if the broad form is genuinely required does the user run it in their own terminal." >&2 ;;
|
|
725
731
|
history) echo "This rewrites or discards work that is already committed. Ask the person who shares the branch; a new commit usually reaches the same end without rewriting." >&2 ;;
|
|
@@ -865,15 +871,15 @@ _push_forces(){ # $1 = one captured `git push …` span -> 0 when a force flag
|
|
|
865
871
|
_git_d45(){ # $1 = command text; block() exits on the first rule it breaks
|
|
866
872
|
local CMD="$1" HAS_GIT=0 _PUSHSEG _seg # has() reads CMD: this one
|
|
867
873
|
case "$CMD" in *[Gg][Ii][Tt]*) HAS_GIT=1 ;; esac
|
|
868
|
-
{ git_has "$CMD" 'reset' && has '--hard'; } && block "git reset --hard" "4.5" history
|
|
874
|
+
{ git_has "$CMD" 'reset' && has '--hard'; } && { _PV_KIND=reset; _PV_CMD="$CMD"; block "git reset --hard" "4.5" history; }
|
|
869
875
|
if [ "$HAS_GIT" = 1 ] && git_has "$CMD" 'push'; then
|
|
870
876
|
_grep_out "$CMD" -oE 'git[[:space:]]+([^;&|]*[[:space:]])?push([^;&|]*)'; _PUSHSEG="$_GO"
|
|
871
877
|
while IFS= read -r _seg; do
|
|
872
878
|
[ -n "$_seg" ] || continue
|
|
873
|
-
_push_forces "$_seg" && { block "git push --force" "4.5" history; break; }
|
|
879
|
+
_push_forces "$_seg" && { _PV_KIND=push; _PV_CMD="$_seg"; block "git push --force" "4.5" history; break; }
|
|
874
880
|
done <<< "$_PUSHSEG"
|
|
875
881
|
fi
|
|
876
|
-
{ git_has "$CMD" 'clean' && has '-[A-Za-z]*f'; } && block "git clean -f" "4.5" loss
|
|
882
|
+
{ git_has "$CMD" 'clean' && has '-[A-Za-z]*f'; } && { _PV_KIND=clean; _PV_CMD="$CMD"; block "git clean -f" "4.5" loss; }
|
|
877
883
|
return 0
|
|
878
884
|
}
|
|
879
885
|
# ---- the quoted argument of a command that does not run it ---------------------------------------------------
|
|
@@ -1109,7 +1115,7 @@ case "$CMD" in *branch*) _HAS_BRANCH=1 ;; *) _HAS_BRANCH=0 ;; esac
|
|
|
1109
1115
|
# short cluster, so `rm -Rf /`, `rm -fR /`, `rm -f -r /` and `rm --recursive --force /` all walked past while
|
|
1110
1116
|
# `rm -rf /` was blocked. Same class as the chmod hole found in evals/permission-pressure: one spelling gated,
|
|
1111
1117
|
# another reaching the identical state. Case, flag order and the long form are all the same command.
|
|
1112
|
-
case "$CMD" in *[Rr][Mm]*) : ;; *) false ;; esac && _grep "$CMD" -qE 'rm +(-[A-Za-z]* +|--[a-z-]+ +)*(-[A-Za-z]*[rR][A-Za-z]*|--recursive)( +(-[A-Za-z]+|--[a-z-]+))* +.*(/|\*|~)' && block "destructive rm -rf" "4.5" loss
|
|
1118
|
+
case "$CMD" in *[Rr][Mm]*) : ;; *) false ;; esac && _grep "$CMD" -qE 'rm +(-[A-Za-z]* +|--[a-z-]+ +)*(-[A-Za-z]*[rR][A-Za-z]*|--recursive)( +(-[A-Za-z]+|--[a-z-]+))* +.*(/|\*|~)' && { _PV_KIND=rm; _PV_CMD="$CMD"; block "destructive rm -rf" "4.5" loss; }
|
|
1113
1119
|
# ADDED (3.1.0): `.` and `..` as a target carry none of those three characters and are the widest targets there are —
|
|
1114
1120
|
# `rm -rf .` at a project root takes the whole tree, `..` its parent (both passed). Narrower than the test above on
|
|
1115
1121
|
# purpose, so that it adds no noise: `rm` must stand at a command position, `sudo` or not (`git rm -r --cached .`, `npm rm`,
|
|
@@ -1121,7 +1127,7 @@ case "$CMD" in *[Rr][Mm]*)
|
|
|
1121
1127
|
_rmc="$CMD"
|
|
1122
1128
|
# a backslash-newline continues the line (`rm -rf \⏎.`); folded only when one is there and the command is short
|
|
1123
1129
|
case "$CMD" in *\\"$_RMNL"*) [ "${#CMD}" -le 4096 ] && _rmc="${CMD//\\$_RMNL/ }" ;; esac
|
|
1124
|
-
[[ $_rmc =~ $_RMDOT ]] && block "destructive rm -rf" "4.5" loss ;; esac
|
|
1130
|
+
[[ $_rmc =~ $_RMDOT ]] && { _PV_KIND=rm; _PV_CMD="$_rmc"; block "destructive rm -rf" "4.5" loss; } ;; esac
|
|
1125
1131
|
# A whole-tree `git checkout -- .` / `git restore .` destroys every uncommitted change with no reflog and no
|
|
1126
1132
|
# undo — the same loss as `reset --hard`, which has been gated since the beginning, by a command that was not.
|
|
1127
1133
|
# Not hypothetical: a verification subagent ran exactly this over uncommitted work in this repo and took the
|
|
@@ -3342,17 +3348,26 @@ if git_has "$CMD_SEEN" 'commit|push'; then
|
|
|
3342
3348
|
BRANCH_WARN=""
|
|
3343
3349
|
# symbolic-ref names the branch even before its first commit, where rev-parse answers "HEAD" and the very
|
|
3344
3350
|
# first commit onto main went by without this line. A detached HEAD reads empty: not the default branch.
|
|
3345
|
-
|
|
3351
|
+
# Read from HEAD with builtins (the gate log's crew_git_dir finds the git dir, a linked worktree's too): the
|
|
3352
|
+
# preview below pays one subshell to keep this hook's decision out of its reach, and this pays it back.
|
|
3353
|
+
_hb=""; if declare -F crew_git_dir >/dev/null 2>&1 && crew_git_dir . && [ -f "$_CREW_GD/HEAD" ]; then
|
|
3354
|
+
IFS= read -r _hb < "$_CREW_GD/HEAD"; _hb="${_hb%$'\r'}"; case "$_hb" in "ref: refs/heads/"*) _hb="${_hb#ref: refs/heads/}" ;; *) _hb="" ;; esac
|
|
3355
|
+
else _hb="$(git symbolic-ref --short -q HEAD 2>/dev/null)"; fi
|
|
3356
|
+
case "$_hb" in
|
|
3346
3357
|
main|master) BRANCH_WARN="⚠️ This commits DIRECTLY to the default branch. Prefer a feature branch unless you meant to.
|
|
3347
3358
|
|
|
3348
3359
|
" ;;
|
|
3349
3360
|
esac
|
|
3350
3361
|
_commit_agent_note
|
|
3362
|
+
# What it takes or sends: the staged change for a commit, the commits for a push (read only).
|
|
3363
|
+
_PVA=""; case "$CMD" in *push*) _PVA="$(_pv_line push-ask "$CMD")" ;; *) _PVA="$(_pv_line commit "$CMD")" ;; esac
|
|
3351
3364
|
ask_user "§4.4 commit/push approval gate. Claude wants to run:
|
|
3352
3365
|
|
|
3353
3366
|
$SHORT
|
|
3354
3367
|
|
|
3355
|
-
${
|
|
3368
|
+
${_PVA:+$_PVA
|
|
3369
|
+
|
|
3370
|
+
}${BRANCH_WARN}${_CAN}Approve only if the commit message above was shown to you and you agree with it. Approving lets Claude run the command itself."
|
|
3356
3371
|
;;
|
|
3357
3372
|
auto|dontAsk)
|
|
3358
3373
|
_commit_agent_note; [ -n "$_CAN" ] && printf '%s' "$_CAN" >&2
|
package/kit/hooks/guard-write.sh
CHANGED
|
@@ -72,7 +72,8 @@ trap '_crew_rc=$?; trap - EXIT; case "$_crew_rc" in 0|2) exit "$_crew_rc" ;; esa
|
|
|
72
72
|
_gate_main(){
|
|
73
73
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
74
74
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
75
|
-
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
75
|
+
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
76
|
+
[ -f "$_crew_d/../eval/lib/gate-log.sh" ] && . "$_crew_d/../eval/lib/gate-log.sh"; unset _crew_d # where the gate log goes
|
|
76
77
|
# Read with the builtin, as guard-powershell.sh does: `INPUT="$(cat)"` is a subshell plus a process on EVERY call
|
|
77
78
|
# of this hook. `read` returns 1 at end of input; the text is read regardless.
|
|
78
79
|
IFS= read -r -d '' INPUT || true
|
|
@@ -365,12 +366,7 @@ block(){ # $1 = rule name for the log (must keep the `gate-file edit` prefix
|
|
|
365
366
|
# and with the same rule about the payload: the path is NOT recorded unless CREW_GATE_LOG_CMD=1, because
|
|
366
367
|
# /crew-gates reports rule names and counts and never the argument. Logged after the verdict; it cannot
|
|
367
368
|
# change it. CREW_GATE_LOG overrides the path; point it at /dev/null to turn recording off.
|
|
368
|
-
_GL="$
|
|
369
|
-
if [ -z "$_GL" ] && [ -d ".claude" ]; then
|
|
370
|
-
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git check-ignore -q ".claude/gate-log.tsv" 2>/dev/null; then
|
|
371
|
-
_GL=".claude/gate-log.tsv"
|
|
372
|
-
fi
|
|
373
|
-
fi
|
|
369
|
+
_GL=""; declare -F crew_gate_log_write >/dev/null && { crew_gate_log_write; _GL="$_CREW_GL"; }
|
|
374
370
|
if [ -n "$_GL" ]; then
|
|
375
371
|
if [ "${CREW_GATE_LOG_CMD:-0}" = 1 ]; then
|
|
376
372
|
printf 'BLOCK\t§4.5\t%s\t%s\n' "$1" \
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreCompact hook — just before the context is compacted, write down the state of the work in a few lines, so the
|
|
3
|
+
# session that comes back after the compaction is told it (session-rehydrate.sh reads it on SessionStart, source
|
|
4
|
+
# compact). A compaction summary is the model's own retelling; this is what the files and the gates say.
|
|
5
|
+
#
|
|
6
|
+
# What it writes, at most ~2 KB: the branch, the uncommitted files (five named, the rest counted), the last refusal in
|
|
7
|
+
# the gate log, how many findings each document under .crewforth/ holds, and whether a handover
|
|
8
|
+
# (docs/SESSION_STATE.md) is there. With the session id and the trigger, so a snapshot is only read back into the
|
|
9
|
+
# session that wrote it.
|
|
10
|
+
#
|
|
11
|
+
# Where: inside the repository's git directory (crewforth-session-snapshot.json), so it is never committed even in a
|
|
12
|
+
# project that tracks .claude/; outside a repository, .claude/state/session-snapshot.json.
|
|
13
|
+
#
|
|
14
|
+
# NEVER BLOCKS. PreCompact exit 2 blocks the compaction (docs: hooks, PreCompact), and blocking an automatic one at
|
|
15
|
+
# the context limit fails the request that needed it. Every path here ends in exit 0, and nothing is printed: a
|
|
16
|
+
# PreCompact hook's output does not reach the summary.
|
|
17
|
+
set -u
|
|
18
|
+
IN="$(cat 2>/dev/null || true)"
|
|
19
|
+
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
20
|
+
LIB="$_here/../eval/lib"
|
|
21
|
+
|
|
22
|
+
ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
23
|
+
[ -n "$ROOT" ] || ROOT="$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
|
|
24
|
+
[ -n "$ROOT" ] || ROOT="$PWD"
|
|
25
|
+
ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
|
|
26
|
+
cd "$ROOT" 2>/dev/null || exit 0
|
|
27
|
+
|
|
28
|
+
field(){ local v="${IN#*\"$1\"}"; [ "$v" = "$IN" ] && return 1; v="${v#*:}"; v="${v#"${v%%[![:space:]]*}"}"; case "$v" in \"*) v="${v#\"}"; printf '%s' "${v%%\"*}" ;; *) printf '%s' "${v%%[,\}]*}" ;; esac; }
|
|
29
|
+
SID="$(field session_id)" || SID=""
|
|
30
|
+
TRIG="$(field trigger)" || TRIG=""
|
|
31
|
+
case "$SID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac # a snapshot no session can claim is not written
|
|
32
|
+
|
|
33
|
+
# ---- what the files and the gates say ----
|
|
34
|
+
LINES=()
|
|
35
|
+
GD=""
|
|
36
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
37
|
+
GD="$(git rev-parse --absolute-git-dir 2>/dev/null)" || GD=""
|
|
38
|
+
BR="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || BR=""
|
|
39
|
+
[ -n "$BR" ] && LINES+=("branch $BR")
|
|
40
|
+
# quotePath off: a name in Turkish (or any non-ASCII) is given as written, not as octal escapes (seen on Windows).
|
|
41
|
+
CH="$(git -c core.quotePath=false status --porcelain 2>/dev/null)" || CH=""
|
|
42
|
+
if [ -n "$CH" ]; then
|
|
43
|
+
n=0; names=""
|
|
44
|
+
while IFS= read -r l; do n=$((n+1)); [ "$n" -le 5 ] && names="${names:+$names, }${l:3}"; done <<< "$CH"
|
|
45
|
+
LINES+=("$n uncommitted file(s): $names$([ "$n" -gt 5 ] && printf ', and %d more' $((n-5)))")
|
|
46
|
+
else LINES+=("nothing uncommitted"); fi
|
|
47
|
+
fi
|
|
48
|
+
if [ -f "$LIB/gate-log.sh" ]; then
|
|
49
|
+
. "$LIB/gate-log.sh"; crew_gate_log_read
|
|
50
|
+
if [ -n "${_CREW_GL:-}" ] && [ -f "$_CREW_GL" ]; then
|
|
51
|
+
last=""; while IFS= read -r l; do case "$l" in BLOCK$'\t'*) last="$l" ;; esac; done < "$_CREW_GL"
|
|
52
|
+
if [ -n "$last" ]; then IFS=$'\t' read -r _ rule why _ <<< "$last"; LINES+=("the last refusal in the gate log: ${rule} ${why:0:120}"); fi
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
fnd=""
|
|
56
|
+
for f in .crewforth/*.json; do
|
|
57
|
+
[ -f "$f" ] || continue
|
|
58
|
+
c=0; while IFS= read -r l; do case "$l" in *'"rule": '*) c=$((c+1)) ;; esac; done < "$f"
|
|
59
|
+
[ "$c" -gt 0 ] && { b="${f##*/}"; fnd="${fnd:+$fnd, }${b%.json} $c"; }
|
|
60
|
+
done
|
|
61
|
+
[ -n "$fnd" ] && LINES+=("findings on record: $fnd")
|
|
62
|
+
[ -s docs/SESSION_STATE.md ] && LINES+=("a handover is at docs/SESSION_STATE.md")
|
|
63
|
+
[ "${#LINES[@]}" -gt 0 ] || exit 0
|
|
64
|
+
|
|
65
|
+
SUM=""; for l in "${LINES[@]}"; do SUM="${SUM:+$SUM; }$l"; done
|
|
66
|
+
SUM="${SUM:0:1800}"
|
|
67
|
+
|
|
68
|
+
# ---- where it goes ----
|
|
69
|
+
if [ -n "$GD" ] && [ -d "$GD" ]; then OUT="$GD/crewforth-session-snapshot.json"
|
|
70
|
+
else mkdir -p .claude/state 2>/dev/null || exit 0; OUT=".claude/state/session-snapshot.json"; fi
|
|
71
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOW '%(%Y-%m-%dT%H:%M:%SZ)T' -1; else NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; fi
|
|
72
|
+
ESC="$(printf '%s\n' "$SUM" | LC_ALL=C awk -f "$LIB/json-string.awk" 2>/dev/null)" || exit 0
|
|
73
|
+
printf '{"session_id":"%s","trigger":"%s","at":"%s","summary":"%s"}\n' "$SID" "${TRIG//[!a-z]/}" "$NOW" "$ESC" > "$OUT.$$" 2>/dev/null && mv -f "$OUT.$$" "$OUT" 2>/dev/null
|
|
74
|
+
rm -f "$OUT.$$" 2>/dev/null
|
|
75
|
+
exit 0
|
|
@@ -68,11 +68,34 @@ if [ -f "$ROOT/.claude/hooks/pre-commit" ] && git -C "$ROOT" rev-parse --git-dir
|
|
|
68
68
|
esac
|
|
69
69
|
fi
|
|
70
70
|
[ -n "$HOOKMSG" ] && MSG="${MSG:+$MSG }$HOOKMSG"
|
|
71
|
-
|
|
71
|
+
|
|
72
|
+
# THE STATE BEFORE THE COMPACTION. precompact-snapshot.sh wrote it just before this context was compacted: the
|
|
73
|
+
# branch, the uncommitted files, the last gate refusal, the findings on record. Read back only after a compaction and
|
|
74
|
+
# only into the session that wrote it (same session id); its summary is already JSON-escaped, so it is put into the
|
|
75
|
+
# output as it is, and the rest of MSG stays fixed text that needs no escaping and no extra process.
|
|
76
|
+
SNAP=""
|
|
77
|
+
_rh="${BASH_SOURCE%[/\\]*}"; [ "$_rh" = "${BASH_SOURCE}" ] && _rh=.
|
|
78
|
+
if [ "$SRC" = compact ] && [ -f "$_rh/../eval/lib/gate-log.sh" ]; then
|
|
79
|
+
. "$_rh/../eval/lib/gate-log.sh"
|
|
80
|
+
_sf=""
|
|
81
|
+
if crew_git_dir "$ROOT" && [ -f "$_CREW_GD/crewforth-session-snapshot.json" ]; then _sf="$_CREW_GD/crewforth-session-snapshot.json"
|
|
82
|
+
elif [ -f "$ROOT/.claude/state/session-snapshot.json" ]; then _sf="$ROOT/.claude/state/session-snapshot.json"; fi
|
|
83
|
+
_sid(){ _SID=""; local v="${1#*\"session_id\"}"; [ "$v" = "$1" ] && return 0; v="${v#*\"}"; _SID="${v%%\"*}"; }
|
|
84
|
+
if [ -n "$_sf" ] && IFS= read -r _sj < "$_sf" 2>/dev/null; then
|
|
85
|
+
_sid "$IN"; _mine="$_SID"; _sid "$_sj"
|
|
86
|
+
case "$_sj" in *'"summary":"'*)
|
|
87
|
+
if [ -n "$_mine" ] && [ "$_mine" = "$_SID" ]; then
|
|
88
|
+
_sum="${_sj#*\"summary\":\"}"; _sum="${_sum%\"\}*}"
|
|
89
|
+
SNAP="Before this context was compacted, Crewforth wrote down the state of the work from the files and the gates: $_sum. It is a record of that moment; check the files before relying on it."
|
|
90
|
+
fi ;;
|
|
91
|
+
esac
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
[ -n "$MSG$SNAP" ] || exit 0 # nothing to offer -> stay silent
|
|
72
95
|
|
|
73
96
|
# hookSpecificOutput.additionalContext is the documented channel that injects text into the model's context.
|
|
74
97
|
# ONE PATH, no jq: MSG is built from fixed constants with no quote, backslash, control character or newline, so it needs no
|
|
75
98
|
# escaping and every machine emits the same bytes. If you ever make MSG dynamic, escape it the way board-sync.sh
|
|
76
99
|
# does (jq-identical, measured) — a raw quote or tab here makes JSON the CLI silently drops.
|
|
77
|
-
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$MSG"
|
|
100
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s%s%s"}}\n' "$MSG" "${MSG:+${SNAP:+ }}" "$SNAP"
|
|
78
101
|
exit 0
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
set -uo pipefail
|
|
45
45
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
46
46
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
47
|
-
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
47
|
+
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
48
|
+
# How an install's own version is read and ordered (an rc before its final): one definition, eval/lib/version.sh.
|
|
49
|
+
[ -f "$_crew_d/../eval/lib/version.sh" ] && . "$_crew_d/../eval/lib/version.sh"; unset _crew_d
|
|
48
50
|
|
|
49
51
|
URL="${CREW_UPDATE_URL:-https://registry.npmjs.org/-/package/crewforth/dist-tags}"
|
|
50
52
|
# plugin-stable, not main: the marketplace installs the plugin from that branch, and only an approved release
|
|
@@ -139,7 +141,10 @@ PJSON="$PR_ROOT/.claude-plugin/plugin.json"
|
|
|
139
141
|
# plugin's copy steps aside whenever a project install is present, instead of asking the project's question too.
|
|
140
142
|
if [ "$IS_PLUGIN" = 1 ] && [ -f "$CL/VERSION" ]; then exit 0; fi
|
|
141
143
|
if [ -f "$CL/VERSION" ]; then
|
|
142
|
-
|
|
144
|
+
# The install's own file, so an rc (X.Y.Z-rc.N) is a version here; what npm answers is still held to X.Y.Z.
|
|
145
|
+
read -r _cv < "$CL/VERSION" 2>/dev/null || _cv=""
|
|
146
|
+
declare -F crew_local_version >/dev/null || exit 0
|
|
147
|
+
CUR="$(crew_local_version "$_cv")" || exit 0
|
|
143
148
|
STATE="$CL/.state" # repo-local: the install lives in the repo
|
|
144
149
|
FEED="${CREW_UPDATE_URL:-$URL}"; KEY=latest; EDITION=project
|
|
145
150
|
ANSWER='bash .claude/hooks/session-update-check.sh --answer'
|
|
@@ -186,12 +191,8 @@ fi
|
|
|
186
191
|
[ -n "$LATEST" ] || exit 0
|
|
187
192
|
LATEST="$(sane_version "$LATEST")" || exit 0
|
|
188
193
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
awk -v a="$LATEST" -v b="$CUR" 'BEGIN{
|
|
192
|
-
split(a,x,"."); split(b,y,".");
|
|
193
|
-
for(i=1;i<=3;i++){ if(x[i]+0 > y[i]+0) exit 0; if(x[i]+0 < y[i]+0) exit 1 }
|
|
194
|
-
exit 1 }' || exit 0
|
|
194
|
+
# One awk, eval/lib/version.sh's order: 3.2.0 is newer than an installed 3.2.0-rc.1, so the rc's user is asked.
|
|
195
|
+
awk -v a="$LATEST" -v b="$CUR" "${CREW_VCMP_AWK:-}"' BEGIN { exit !(vcmp(a, b) > 0) }' 2>/dev/null || exit 0
|
|
195
196
|
|
|
196
197
|
# Skipped for good? Asked (or answered "later") within the last day?
|
|
197
198
|
if [ -f "$STATE/update-skip" ]; then read -r _sk < "$STATE/update-skip" 2>/dev/null || _sk=""; [ "${_sk//[!0-9.]/}" = "$LATEST" ] && exit 0; fi
|
package/kit/settings.json
CHANGED
|
@@ -216,6 +216,18 @@
|
|
|
216
216
|
]
|
|
217
217
|
}
|
|
218
218
|
],
|
|
219
|
+
"PreCompact": [
|
|
220
|
+
{
|
|
221
|
+
"hooks": [
|
|
222
|
+
{
|
|
223
|
+
"type": "command",
|
|
224
|
+
"shell": "bash",
|
|
225
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/precompact-snapshot.sh",
|
|
226
|
+
"timeout": 30
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
],
|
|
219
231
|
"SessionStart": [
|
|
220
232
|
{
|
|
221
233
|
"matcher": "startup|resume|clear|compact|fork",
|
|
@@ -17,6 +17,21 @@ Trigger phrases: "code-review", "review the code", "review the PR", "review my c
|
|
|
17
17
|
A review runs in four steps — **group, plan, review, fact-check** — and reports few findings that are right rather
|
|
18
18
|
than many that might be. Every step below says what it produces.
|
|
19
19
|
|
|
20
|
+
## 0. The pre-pass first, and the findings in one shape
|
|
21
|
+
Before reading the diff, run the deterministic pre-pass (`eval/lib/review-prepass/` in either edition):
|
|
22
|
+
`bash <dir>/prepass.sh` for the staged change, `--base <ref>` for a branch. It writes
|
|
23
|
+
`.crewforth/review-prepass.json`: secret patterns, a debug call or TODO left in code, shellcheck on shell files, a
|
|
24
|
+
lock file that moved without its manifest, a very large change. **Carry those over, do not re-derive them**; your
|
|
25
|
+
time goes to what a pattern cannot see. rc 3 (no Node) or a `notMeasured` line means that part was NOT checked:
|
|
26
|
+
say so in the review, never read it as clean.
|
|
27
|
+
|
|
28
|
+
Write your own findings in the same shape (`crewforth.findings/1`: `rule` `review-<category>`, `severity`,
|
|
29
|
+
`confidence`, `evidence` with `file`, `line` and a `text` quote, `detail` = what to do instead) and store them with
|
|
30
|
+
`bash <dir>/../findings/findings.sh write review <file>`. A `critical` or `high` finding first goes to
|
|
31
|
+
**crew-refuter** (claim + `file:line`, one call per lens, in parallel) and the document is written with
|
|
32
|
+
`--verified`, which then refuses any `medium`-or-higher finding without its verifiers. This is model discipline:
|
|
33
|
+
no hook runs the pre-pass or reads `review.json`.
|
|
34
|
+
|
|
20
35
|
## 1. Scope
|
|
21
36
|
- **Review the lines this change adds or edits.** Deleted code is context for understanding them, not a target.
|
|
22
37
|
- Code that is correct, or that the change did not touch, gets no comment.
|
|
@@ -142,6 +157,7 @@ five questions that decide *answer in the thread* vs *edit the code*: **`referen
|
|
|
142
157
|
item leaves with a disposition from the triage table above — a reasoned refusal is an answer, silence is not.
|
|
143
158
|
|
|
144
159
|
## DoD (this skill's contribution)
|
|
160
|
+
- The pre-pass ran (or its rc 3 / `notMeasured` is reported as unchecked), and the findings are in `.crewforth/review.json`.
|
|
145
161
|
- The plan (what changed, the risks) came before the findings; every file in each group was read.
|
|
146
162
|
- Every finding has a severity, a category and a reason, in the finding format above.
|
|
147
163
|
- Scope creep and hidden complexity are flagged.
|