crewforth 3.2.0-rc.2 → 3.2.0-rc.4
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 +118 -5
- package/README.md +2 -0
- package/VERSION +1 -1
- package/adopt.sh +10 -1
- package/bin/cli.js +3 -2
- package/kit/CLAUDE.md +3 -3
- package/kit/agents/crew-commit-agent.md +6 -4
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/eval/doctor.sh +13 -2
- 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-json.awk +4 -0
- package/kit/eval/lib/statusline.sh +1 -1
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +208 -21
- package/kit/hooks/agent-outcome.sh +18 -0
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +72 -26
- package/kit/hooks/guard-bash.sh +49 -34
- package/kit/hooks/guard-write.sh +3 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/prompt-approval.sh +21 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/trace-blocklist.txt +9 -0
- 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/skills/crew-ship/SKILL.md +3 -1
- package/kit/studio/README.md +21 -10
- package/kit/studio/server/index.js +74 -22
- 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/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 +69 -7
- package/kit/studio/web/canvas.js +14 -2
- package/kit/studio/web/chat.js +32 -7
- package/kit/studio/web/graph-plan.js +3 -2
- 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 +34 -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,13 +417,58 @@ _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 '
|
|
434
|
+
_am_trim(){ # $1 -> _AMT: no white space around it, and no separator or full stop left at its end (one-line cards)
|
|
435
|
+
local v="$1" o
|
|
436
|
+
while :; do case "$v" in [$' \t']*) v="${v:1}" ;; *) break ;; esac; done
|
|
437
|
+
while :; do o="$v"
|
|
438
|
+
case "$v" in *[$' \t']) v="${v%?}" ;; *'·') v="${v%'·'}" ;; *'—') v="${v%'—'}" ;; *'–') v="${v%'–'}" ;; *[\|\;,.]) v="${v%?}" ;; esac
|
|
439
|
+
[ "$v" = "$o" ] && break
|
|
440
|
+
done
|
|
441
|
+
_AMT="$v"
|
|
442
|
+
}
|
|
443
|
+
# THE CARD ON ONE LINE. Sessions write it as `files: … · change: … · verify: …` (measured on this machine's transcripts,
|
|
444
|
+
# 2026-10-18: 17 of the 39 calls refused for their card, in 6 projects; the next call was the same card on three lines).
|
|
445
|
+
# It is read by its labels, in that order, after an optional `Card:` / `Card —`; `above: <why>` may close the line. The
|
|
446
|
+
# fields are then held to exactly the rules of the three-line card below, so nothing passes here that would not there.
|
|
447
|
+
_am_card_line(){ # $1 = the first line of the task -> 0 and CARD_* set (unchecked); 1 when it is not a one-line card
|
|
448
|
+
local l="$1" r f c v a=""
|
|
449
|
+
case "$l" in [Cc]ard*) l="${l#[Cc]ard}"; l="${l#\*\*}"
|
|
450
|
+
while :; do case "$l" in [$' \t:*']*|'—'*|'–'*|-*) case "$l" in '—'*) l="${l#'—'}" ;; '–'*) l="${l#'–'}" ;; *) l="${l:1}" ;; esac ;; *) break ;; esac; done ;;
|
|
451
|
+
esac
|
|
452
|
+
case "$l" in files:*) ;; *) return 1 ;; esac
|
|
453
|
+
r="${l#files:}"
|
|
454
|
+
case "$r" in *change:*verify:*) ;; *) return 1 ;; esac
|
|
455
|
+
f="${r%%change:*}"; r="${r#*change:}"; c="${r%%verify:*}"; v="${r#*verify:}"
|
|
456
|
+
case "$v" in *above:*) a="${v#*above:}"; v="${v%%above:*}" ;; esac
|
|
457
|
+
_am_trim "$f"; CARD_FILES="$_AMT"; _am_trim "$c"; CARD_CHANGE="$_AMT"; _am_trim "$v"; CARD_VERIFY="$_AMT"
|
|
458
|
+
[ -n "$a" ] && { _am_trim "$a"; CARD_ABOVE="${_AMT:0:200}"; }
|
|
459
|
+
return 0
|
|
460
|
+
}
|
|
417
461
|
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (CARD_ABOVE); 1 and CARD_WHY
|
|
418
|
-
local s="$1" l k v n=0 seen=""
|
|
462
|
+
local s="$1" l k v n=0 seen="" one=0
|
|
419
463
|
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""; CARD_ABOVE=""
|
|
420
464
|
s="${s//$'\r'/}"
|
|
421
|
-
|
|
465
|
+
# One line first: the first line that is not empty, read by its labels; the three-line reading otherwise.
|
|
466
|
+
l="$s"; while :; do case "$l" in [$' \t\n']*) l="${l:1}" ;; *) break ;; esac; done; l="${l%%$'\n'*}"
|
|
467
|
+
if _am_card_line "$l"; then
|
|
468
|
+
one=1; n=3
|
|
469
|
+
s="${s#*"$l"}"; s="${s#$'\n'}"
|
|
470
|
+
fi
|
|
471
|
+
while [ "$one" = 0 ] && [ "$n" -lt 3 ] && [ -n "$s" ]; do
|
|
422
472
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
423
473
|
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
424
474
|
[ -n "$l" ] || continue
|
|
@@ -438,12 +488,16 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (C
|
|
|
438
488
|
done
|
|
439
489
|
[ "$n" = 3 ] || { CARD_WHY="the task does not begin with the three lines files:, change:, verify:"; return 1; }
|
|
440
490
|
[ -n "$CARD_FILES" ] || { CARD_WHY="files: is empty (name the files or globs, or the folder)"; return 1; }
|
|
491
|
+
# A note in parentheses after the kind is the session's own comment, as `none (…)` is for verify: `architecture
|
|
492
|
+
# (planning only)` is architecture (4 of the cards refused on this machine, 2026-10-18). Only a kind on the list, and
|
|
493
|
+
# only a note that is the whole rest of the value in parentheses.
|
|
494
|
+
case "$CARD_CHANGE" in *' ('*')') v="${CARD_CHANGE%% (*}"; case "$_AM_CHANGES" in *" $v "*) CARD_CHANGE="$v" ;; esac ;; esac
|
|
441
495
|
case "$_AM_CHANGES" in *" $CARD_CHANGE "*) ;; *) CARD_WHY="change: is '${CARD_CHANGE:0:40}', not one of:$_AM_CHANGES"; return 1 ;; esac
|
|
442
496
|
[ -n "$CARD_VERIFY" ] || { CARD_WHY="verify: is empty (a command, or none)"; return 1; }
|
|
443
497
|
case "$CARD_VERIFY" in none|None|NONE|-|'none '*|'none('*|'None '*|'NONE '*) CARD_VERIFY=none ;; esac # "none (the author ran it)" is none
|
|
444
498
|
# Optional, right after the three: `above: <why>` keeps a model above the card's floor. Without it a call above
|
|
445
499
|
# 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
|
|
500
|
+
while [ -z "$CARD_ABOVE" ] && [ -n "$s" ]; do
|
|
447
501
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
448
502
|
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
449
503
|
[ -n "$l" ] || continue
|
|
@@ -557,13 +611,10 @@ _am_floor_now(){ # $1 agent, $2 change, $3 risk -> _AMF_TIER (the raised floor,
|
|
|
557
611
|
|
|
558
612
|
# ---- the gate log: a refusal of this gate is recorded like any other gate's -------------------------------------
|
|
559
613
|
_am_log(){ # $1 = the rule. Same file and line shape as guard-bash.sh's gatelog; the call's text is never recorded.
|
|
560
|
-
local gl="
|
|
614
|
+
local gl="" v=BLOCK
|
|
561
615
|
case "$1" in "note: "*) v=NOTE ;; esac # something worth knowing that refused nothing
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
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
|
|
565
|
-
gl=".claude/gate-log.tsv"
|
|
566
|
-
fi
|
|
616
|
+
declare -F crew_gate_log_write >/dev/null || return 0
|
|
617
|
+
crew_gate_log_write; gl="$_CREW_GL"; [ -n "$gl" ] || return 0
|
|
567
618
|
printf '%s\t§model\t%s\t\n' "$v" "$1" >> "$gl" 2>/dev/null || true
|
|
568
619
|
}
|
|
569
620
|
|
|
@@ -594,6 +645,7 @@ _am_log(){ # $1 = the rule. Same file and line shape as guard-bash.sh's gatelog
|
|
|
594
645
|
# 5. THE SHELL GATE AGREES, asked under a session id that is not the session's: only "nothing to say" or an
|
|
595
646
|
# explicit allow passes.
|
|
596
647
|
_AM_HERE="${BASH_SOURCE%/*}"; [ "$_AM_HERE" = "${BASH_SOURCE}" ] && _AM_HERE=.
|
|
648
|
+
[ -f "$_AM_HERE/../eval/lib/gate-log.sh" ] && . "$_AM_HERE/../eval/lib/gate-log.sh" # where the gate log goes
|
|
597
649
|
# CREW-NOT-A-RUNG: names of commands a PROJECT's verify line may begin with; nothing here is called by Crewforth.
|
|
598
650
|
_AM_RUNNERS=('npm test' 'npm t' 'pnpm test' 'yarn test' 'bun test' 'deno test'
|
|
599
651
|
'dotnet test' 'dotnet build' 'flutter test' 'flutter analyze' 'dart test' 'dart analyze' 'go test' 'go build' 'go vet'
|
|
@@ -665,19 +717,9 @@ _am_perm_hit(){ # $1 = command, $2 = allow|deny|ask -> 0 a Bash(...) rule of th
|
|
|
665
717
|
done <<< "$_AMSF"
|
|
666
718
|
return 1
|
|
667
719
|
}
|
|
668
|
-
#
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
}
|
|
720
|
+
# How a verify command is read (the `cd <folder> && <command>` split): eval/lib/verify-cmd.sh, shared with doctor.
|
|
721
|
+
[ -f "$_AM_HERE/../eval/lib/verify-cmd.sh" ] && . "$_AM_HERE/../eval/lib/verify-cmd.sh"
|
|
722
|
+
_am_verify_split(){ _AMVD=""; _AMVC="$1"; declare -F crew_verify_split >/dev/null && crew_verify_split "$1" "${_AMP:-.}"; return 0; }
|
|
681
723
|
_am_verify_ok(){ # $1 = the command, $2 = permission mode -> 0 it may run (_AMVB says by what); 1 and _AMVW says why not
|
|
682
724
|
local c="$1" pm="${2:-default}" f r rc k g
|
|
683
725
|
_AMVW=""; _AMVB=""
|
|
@@ -759,6 +801,8 @@ _am_write_check(){ # $1 = the payload, $2 = the file about to be written -> ret
|
|
|
759
801
|
if [ -f "$f" ]; then m="$(grep -m1 -o '"model":"[^"]*"' "$f" 2>/dev/null)" || m=""; _am_tier "$m"; tier="$_AMT"; fi
|
|
760
802
|
fi
|
|
761
803
|
case "$tier" in opus|fable) return 0 ;; esac
|
|
804
|
+
# A text change on sonnet may write prose there: the call was let through on that floor for exactly that.
|
|
805
|
+
if [ "$tier" = sonnet ] && _am_prose "$_AMRP" && _am_rec_get "$_AMD/agents/$aid" change && [ "$_AMV" = text ]; then return 0; fi
|
|
762
806
|
_am_log "critical path: write by an agent not on opus"
|
|
763
807
|
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
|
|
764
808
|
exit 2
|
|
@@ -832,9 +876,11 @@ if ! _am_card "$prompt"; then
|
|
|
832
876
|
fi
|
|
833
877
|
_am_state; _am_risk; _am_digest
|
|
834
878
|
# ---- the floor of THIS card: the highest of what applies, and the reason that set it ----
|
|
835
|
-
if [ "$CARD_VERIFY" = none ] && [ "$floor" -lt 2 ]; then floor=2; why="no verify command, so nothing checks the work but a reader"; fi
|
|
879
|
+
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
|
|
836
880
|
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
|
|
837
|
-
if [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE"
|
|
881
|
+
if [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" = text ] && _am_all_prose; then
|
|
882
|
+
if [ "$floor" -lt 2 ]; then floor=2; why="a text change on a critical path: $CARD_RISKWHY"; fi
|
|
883
|
+
elif [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" != test-run ] && [ "$floor" -lt 3 ]; then floor=3; why="critical work: $CARD_RISKWHY"; fi
|
|
838
884
|
# A class the user's rules or the record of outcomes hold higher. Neither can lower anything here.
|
|
839
885
|
_am_rules
|
|
840
886
|
for r in ${_AM_RULES[@]+"${_AM_RULES[@]}"}; do
|
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
|
|
@@ -2518,16 +2524,7 @@ _appr_wired(){ # 0 = prompt-approval.sh is beside this hook and a settings file
|
|
|
2518
2524
|
}
|
|
2519
2525
|
# IS THE APPROVAL QUESTION WIRED? The same reading as above, for the two halves on AskUserQuestion (guard-question.sh
|
|
2520
2526
|
# on PreToolUse, prompt-approval.sh on PostToolUse). Asked only to choose which way the refusal offers first.
|
|
2521
|
-
_q_wired(){
|
|
2522
|
-
local f t d="${CLAUDE_PROJECT_DIR:-.}"
|
|
2523
|
-
d="${d//\\//}"; d="${d%/}"
|
|
2524
|
-
for f in "${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT//\\//}/hooks/hooks.json}" "$d/.claude/settings.json" "$d/.claude/settings.local.json" "${HOME:+${HOME//\\//}/.claude/settings.json}"; do
|
|
2525
|
-
[ -n "$f" ] && [ -f "$f" ] || continue
|
|
2526
|
-
t=""; IFS= read -r -d '' t < "$f" || true
|
|
2527
|
-
case "$t" in *'"PreToolUse"'*AskUserQuestion*guard-question.sh*'"PostToolUse"'*AskUserQuestion*prompt-approval.sh*) return 0 ;; esac
|
|
2528
|
-
done
|
|
2529
|
-
return 1
|
|
2530
|
-
}
|
|
2527
|
+
_q_wired(){ declare -F _crew_q_wired >/dev/null 2>&1 && _crew_q_wired; } # defined once, in prompt-approval.sh
|
|
2531
2528
|
# MAY A PermissionRequest HOOK ANSWER THE PROMPT? One scan, defined in prompt-approval.sh (_crew_prh_scan) and read
|
|
2532
2529
|
# from there. Without that file there is no other approval to fall back to, so the answer is "no" and the gate
|
|
2533
2530
|
# asks as it always has.
|
|
@@ -2597,12 +2594,21 @@ _gate_written(){
|
|
|
2597
2594
|
{ printf 'sid=%s\nop=%s\nts=%s\n' "$_psid" "$_pw" "$_pn" > "$_pd/crew-approval-pending"; } 2>/dev/null || _pw=""
|
|
2598
2595
|
fi
|
|
2599
2596
|
fi
|
|
2597
|
+
# ONE WAY TO ASK. With the question open, the refusal used to print it and then the typed way as an equal choice,
|
|
2598
|
+
# and the session picked the typed one 7 times in 9 (a real `auto` session on rc.3, 2026-10-18): the user was told
|
|
2599
|
+
# to type a command for what one click does. The typed way is now the fallback, said as one.
|
|
2600
|
+
_QOFF=0
|
|
2600
2601
|
if [ "$PERM_MODE" = auto ] && [ "$_PRF" = 0 ] && _q_wired && declare -F _crew_q_pre >/dev/null 2>&1 && [ -f "${BASH_SOURCE%/*}/guard-question.sh" ]; then
|
|
2601
|
-
|
|
2602
|
+
_QOFF=1
|
|
2603
|
+
echo "$_CREW_Q_ASK" >&2
|
|
2602
2604
|
echo "{\"questions\":[$_CREW_Q_JSON]}" >&2
|
|
2603
|
-
echo "Their answer is recorded by Crewforth itself (the line it adds says so); then run the command it names.
|
|
2605
|
+
echo "Their answer is recorded by Crewforth itself (the line it adds says so); then run the command it names. Ask it this way only: do not also give the user a command to type." >&2
|
|
2606
|
+
fi
|
|
2607
|
+
if [ "$_QOFF" = 1 ]; then
|
|
2608
|
+
echo "ONLY IF THAT QUESTION CANNOT BE ASKED (the tool call is refused, or the user dismisses it), ask instead, in their language, in this shape and with nothing after it:" >&2
|
|
2609
|
+
else
|
|
2610
|
+
echo "ASK THE USER NOW, in their language, in this shape and with nothing after it (stage first: the approval covers what is staged at that moment):" >&2
|
|
2604
2611
|
fi
|
|
2605
|
-
echo "ASK THE USER NOW, in their language, in this shape and with nothing after it (stage first: the approval covers what is staged at that moment):" >&2
|
|
2606
2612
|
echo " 1. the commit message, whole, in ONE code block;" >&2
|
|
2607
2613
|
if [ -n "$_pw" ]; then
|
|
2608
2614
|
echo " 2. under it one sentence in their language (If you approve, send only this:), and then the command on a line of its own, exactly as it is:" >&2
|
|
@@ -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
|
|
@@ -514,6 +514,27 @@ _crew_prh_scan(){ # -> 0 when a PermissionRequest hook may be wired; _PRH names
|
|
|
514
514
|
return "$r"
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
# ---- IS THE QUESTION OPEN HERE? One answer for every hook that offers it ------------------------------------------
|
|
518
|
+
# guard-bash.sh offers it in a refusal; agent-outcome.sh offers it when crew-commit-agent is called in `auto`, so the
|
|
519
|
+
# click is on offer before any commit is tried (B-2, a real session: with the refusal as the only way to it, a session
|
|
520
|
+
# that asked for /crew-approve first never showed it). The text the user is asked stays _CREW_Q_JSON, here only.
|
|
521
|
+
_CREW_Q_HERE="${BASH_SOURCE[0]%/*}"; [ "$_CREW_Q_HERE" = "${BASH_SOURCE[0]}" ] && _CREW_Q_HERE=.
|
|
522
|
+
_crew_q_wired(){ # 0 = a settings file wires both halves on AskUserQuestion (guard-question.sh, then this file)
|
|
523
|
+
local f t d="${CLAUDE_PROJECT_DIR:-.}"
|
|
524
|
+
d="${d//\\//}"; d="${d%/}"
|
|
525
|
+
for f in "${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT//\\//}/hooks/hooks.json}" "$d/.claude/settings.json" "$d/.claude/settings.local.json" "${HOME:+${HOME//\\//}/.claude/settings.json}"; do
|
|
526
|
+
[ -n "$f" ] && [ -f "$f" ] || continue
|
|
527
|
+
t=""; IFS= read -r -d '' t < "$f" || true
|
|
528
|
+
case "$t" in *'"PreToolUse"'*AskUserQuestion*guard-question.sh*'"PostToolUse"'*AskUserQuestion*prompt-approval.sh*) return 0 ;; esac
|
|
529
|
+
done
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
_crew_q_open(){ # $1 = permission mode -> 0 when the click may be offered: auto, both halves wired and beside this file, no PermissionRequest hook
|
|
533
|
+
[ "$1" = auto ] && [ -f "$_CREW_Q_HERE/guard-question.sh" ] && _crew_q_wired && ! _crew_prh_scan
|
|
534
|
+
}
|
|
535
|
+
# What the session is told to do, the same words wherever it is offered.
|
|
536
|
+
_CREW_Q_ASK='ASK THE USER WITH ONE CLICK (stage first: the approval covers what is staged at that moment): show the commit message whole, in ONE code block, then call AskUserQuestion with exactly this input, nothing added or changed:'
|
|
537
|
+
|
|
517
538
|
# ---- the approval question's context, and its PreToolUse half -----------------------------------------------
|
|
518
539
|
# Shared by prompt-approval.sh (PostToolUse: it records the answer) and guard-question.sh (PreToolUse: it asks). The
|
|
519
540
|
# PreToolUse half is a GATE and lives in a gate's file, so an error of its own refuses the call (CREW-FAILCLOSED)
|
|
@@ -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
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
Co-Authored-By
|
|
14
14
|
#test: Co-Authored-By: An Assistant <noreply@example.com>
|
|
15
|
+
# The session-link trailer Claude Code asks the model to end a commit message with: it reached six pushed commits of a
|
|
16
|
+
# project with Crewforth installed (2026-10-18), because nothing here named it. The trailer and the address are both
|
|
17
|
+
# caught, so a message that keeps only one of them is still stopped. One line, not two: each line is one grep on every
|
|
18
|
+
# commit (a process, 62-135 ms on Git Bash).
|
|
19
|
+
Claude-Session:|claude\.ai/code/session_
|
|
20
|
+
#test: Claude-Session: https://claude.ai/code/session_0123456789abcdef
|
|
21
|
+
#test: see https://claude.ai/code/session_0123456789abcdef for the run
|
|
22
|
+
#test-clean: the claude session limit is reached
|
|
23
|
+
#test-clean: https://claude.ai/code is the web app
|
|
15
24
|
# "Generated by/with" alone matched every machine-written header in existence — a pubspec.lock, an EF Core
|
|
16
25
|
# scaffold, protoc, openapi-generator. A live --dotnet install could not make its FIRST commit because the
|
|
17
26
|
# cloned base carries a Dart lockfile saying "Generated by pub". §4.1 is about AI AUTHORSHIP, so the pattern
|
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.
|