crewforth 3.2.0 → 3.3.0

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.
@@ -262,9 +262,12 @@ PCT="$((PCT_T / 10)).$((PCT_T % 10))"
262
262
  # ago: the expression above rounds. A comment that describes a tool the code no longer calls is worse than
263
263
  # no comment, because the next reader trusts it instead of the line.
264
264
  PCTI="${PCT%%.*}"; case "$PCTI" in ''|*[!0-9]*) PCTI=0 ;; esac
265
- if [ "$PCTI" -lt 50 ]; then LEVEL="continue"
266
- elif [ "$PCTI" -lt 75 ]; then LEVEL="medium"
267
- else LEVEL="handoff+clear"
265
+ # The form the discipline's status line uses, word for word: `→ <low|medium|high> fill · Recommendation: <…>`.
266
+ # It used to print the recommendation alone for two bands and the level for the third (`continue`, `medium`,
267
+ # `handoff+clear`), which left the model to work the level word out of the thresholds itself.
268
+ if [ "$PCTI" -lt 50 ]; then LEVEL="low fill · Recommendation: continue"
269
+ elif [ "$PCTI" -lt 75 ]; then LEVEL="medium fill · Recommendation: continue"
270
+ else LEVEL="high fill · Recommendation: handoff+clear"
268
271
  fi
269
272
  # The '%<number>' shape is a contract: session-guard.sh reads the percentage back out of this line.
270
273
  if [ "$VERBOSE" = 1 ]; then
@@ -9,7 +9,8 @@
9
9
  # change: text | feature | fix-known | fix-unknown | refactor | migration | security | architecture |
10
10
  # test-run | test-write | audit
11
11
  # verify: the command that says the work is right, or `none`
12
- # and this gate reads the card. What it holds, all of it by refusing and never by rewriting a call:
12
+ # and this gate reads the card. What it holds, by refusing a call below its floor (and running one above it ON the
13
+ # floor, unless the card says `above: <why>`):
13
14
  # * the call names a model, and carries a card;
14
15
  # * RISK CLASS. `critical` when the change is migration, security, architecture or fix-unknown, or a file is on a
15
16
  # critical path (auth, payments, billing, migrations, security, crypto, secrets, *.sql, schema.prisma;
@@ -20,6 +21,8 @@
20
21
  # that wants opus there says so in .claude/crew-model-rules (floor * text critical opus).
21
22
  # * the referee is as strong as the risk: test-write and audit on sonnet or above (opus when critical, by the rule
22
23
  # above); review, privacy, planner and database agents on sonnet or above; the security agent on opus.
24
+ # * feature, fix-known and refactor run on sonnet or above, the table's sonnet row (not the commit agent, whose
25
+ # work is the message).
23
26
  # * work with no verify command runs on sonnet or above: haiku goes only to work a command can check. Not for
24
27
  # change: text, nor for the commit agent: a reader is all prose ever has, and a commit message is checked by
25
28
  # the commit-msg hook (seen on a real project: a commit message proposed on sonnet for that reason alone).
@@ -359,7 +362,8 @@ _am_tier(){ # $1 = a model as a call or a transcript names it -> _AMT haiku|son
359
362
  # of every file of the project (the scan in agent-outcome.sh). A path is critical when one of its parts is one of
360
363
  # these WORDS, standing as a word: after a separator or at a capital (`auth/`, `user_auth.py`, `AuthController.cs`,
361
364
  # `userAuth.ts`, `AUTH_KEYS`) and not running on in lower case (`author.md`, `cryptography/` are not). And by name:
362
- # a .sql file, schema.prisma. The path is read from the project's root down, so the name of the folder the project
365
+ # a .sql file, schema.prisma. And the migration folders whose names carry no such word: alembic/versions/,
366
+ # Rails' db/migrate/. The path is read from the project's root down, so the name of the folder the project
363
367
  # sits in decides nothing. .claude/crew-model-rules, which only the user writes, adds (`+ glob`) and removes
364
368
  # (`- glob`); the last line that matches wins.
365
369
  # Legal and personal-data texts are critical too (crew-docs-agent's work): legal, privacy, consent in every form, and
@@ -404,6 +408,7 @@ _am_crit_path(){ # $1 = a path or a glob -> 0 critical (_AMW says by what), 1 n
404
408
  _am_rel "$1"; p="$_AMRP"; _AMW=""
405
409
  if [[ "$p" =~ $_AM_RE_L ]] || [[ "$p" =~ $_AM_RE_C ]] || [[ "$p" =~ $_AM_RE_U ]]; then hit=0; _AMW="${BASH_REMATCH[2]}"
406
410
  else case "$p" in *.sql|*.SQL) hit=0; _AMW="a .sql file" ;; schema.prisma|*/schema.prisma) hit=0; _AMW="schema.prisma" ;;
411
+ alembic/versions/*|*/alembic/versions/*|db/migrate/*|*/db/migrate/*) hit=0; _AMW="migrations" ;;
407
412
  store/*|*/store/*|store-listing/*|*/store-listing/*|fastlane/metadata/*|*/fastlane/metadata/*|metadata/android/*|*/metadata/android/*|metadata/ios/*|*/metadata/ios/*)
408
413
  case "$p" in *.txt|*.md|*.xml|*.yml|*.yaml) hit=0; _AMW="a store listing text" ;; esac ;;
409
414
  esac; fi
@@ -533,6 +538,9 @@ _am_digest(){ # after _am_card -> CARD_ID: 12 hex of what the card says, so "th
533
538
  # A call to an agent that is not Crewforth's (general-purpose, a project's own): no card and no floor, so only the
534
539
  # expensive end is held, the same rewrite as a crew call above its floor.
535
540
  # * It names opus or fable, and its task has no `above: <why>` line: it runs on sonnet.
541
+ # * Either way, a task that opens with a card naming critical work (the same reading as a crew call: change:
542
+ # fix-unknown, security, migration, architecture, or a file on a critical path) is not lowered: a crew agent
543
+ # would have had an opus floor for it, so the way round the crew must not be the cheaper one. Nothing is raised.
536
544
  # * It names no model, and the agent takes the session's (general-purpose, or a project or user agent whose
537
545
  # definition has no model of its own): it runs on sonnet, unless the session itself is on haiku, read from the
538
546
  # end of its transcript. An agent whose definition names a model (Explore, claude-code-guide) is left to it:
@@ -572,6 +580,11 @@ _am_outside(){ # $1 = the tool_input text, $2 = the agent's name
572
580
  _json_slice "$1" model >/dev/null; _json_unescape "$_JS" >/dev/null; _am_tier "$_JU"; m="$_AMT"
573
581
  case "$m" in opus|fable) ;; *) return 0 ;; esac
574
582
  fi
583
+ if _am_card "$pr"; then
584
+ [ -z "$CARD_ABOVE" ] || return 0
585
+ _am_risk
586
+ if [ "$CARD_RISK" = critical ]; then _am_log "note: $name (not a crew agent) kept $m: its card names critical work ($CARD_RISKWHY)"; return 0; fi
587
+ fi
575
588
  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
576
589
  case "$ui" in '{'*'}') ;; *) return 0 ;; esac
577
590
  _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"
@@ -814,6 +827,26 @@ _am_write_check(){ # $1 = the payload, $2 = the file about to be written -> ret
814
827
  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
815
828
  exit 2
816
829
  }
830
+ # THE FLOOR OF A CLASS, before any card, record or rule: the agent's own, then the card's change. One definition, for
831
+ # the gate and for /crew-loosen (prompt-approval.sh), which must not report a lowering below it: a raise on record for
832
+ # a class whose own floor is the same model is not lowered by anything (field review, 3.2.1).
833
+ _am_agent_floor(){ # $1 = agent -> floor (rank) and why. The agent's own floor, whatever the card says.
834
+ floor=0; why=""
835
+ case "$1" in
836
+ crew-security-expert) floor=3; why="the security audit" ;;
837
+ crew-privacy-agent|crew-review-agent|crew-planner|crew-database-expert) floor=2; why="this agent's floor" ;;
838
+ esac
839
+ return 0
840
+ }
841
+ _am_change_floor(){ # $1 = change, $2 = agent -> raises floor and why
842
+ case "$1" in test-write|audit) if [ "$floor" -lt 2 ]; then floor=2; why="change: $1, the referee of the work"; fi ;; esac
843
+ # The table's sonnet row: feature, fix-known and refactor run on sonnet with a verify command too. They had no floor
844
+ # here, so with the rule that a call runs ON its floor, a call that asked for sonnet or opus by the table ran on haiku
845
+ # (field report, 3.2.1: a fix-known on a PII file, asked for opus, ran 106 turns on haiku and came back
846
+ # `confidence: low`). haiku stays the floor of text, test-run and the commit agent, whose work is the message.
847
+ [ "$2" != crew-commit-agent ] && case "$1" in feature|fix-known|refactor) if [ "$floor" -lt 2 ]; then floor=2; why="change: $1, the table's sonnet row"; fi ;; esac
848
+ return 0
849
+ }
817
850
  [ "$_AM_LIB" = 1 ] && return 0
818
851
  # ---- CREW-FAILCLOSED ---------------------------------------------------------------------------------------
819
852
  # A gate that stops on an error of its own must refuse the call, not let it through. Claude Code blocks a tool call
@@ -855,11 +888,7 @@ _json_keycount "$ti" model
855
888
  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
856
889
  _json_slice "$ti" model >/dev/null; _json_unescape "$_JS" >/dev/null; m="$_JU"
857
890
  if [ -z "$m" ]; then rank=0; else _am_tier "$m"; if [ -n "$_AMT" ]; then m="$_AMT"; _am_rank "$m"; rank="$_AMR"; else rank=-1; fi; fi
858
- # The agent's own floor, whatever the card says.
859
- case "$st" in
860
- crew-security-expert) floor=3; why="the security audit" ;;
861
- crew-privacy-agent|crew-review-agent|crew-planner|crew-database-expert) floor=2; why="this agent's floor" ;;
862
- esac
891
+ _am_agent_floor "$st"
863
892
  _am_name "$floor"; fname="$_AMN"
864
893
  if [ "$rank" = 0 ]; then
865
894
  echo "GUARD (agent model): a call to $st has to name its model. Choose haiku, sonnet or opus by the table in CLAUDE.md (risk decides, not size) and repeat the same call with model set.${fname:+ $st runs on $fname or above.}" >&2
@@ -884,7 +913,7 @@ fi
884
913
  _am_state; _am_risk; _am_digest
885
914
  # ---- the floor of THIS card: the highest of what applies, and the reason that set it ----
886
915
  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
887
- 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
916
+ _am_change_floor "$CARD_CHANGE" "$st"
888
917
  if [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" = text ] && _am_all_prose; then
889
918
  if [ "$floor" -lt 2 ]; then floor=2; why="a text change on a critical path: $CARD_RISKWHY"; fi
890
919
  elif [ "$CARD_RISK" = critical ] && [ "$CARD_CHANGE" != test-run ] && [ "$floor" -lt 3 ]; then floor=3; why="critical work: $CARD_RISKWHY"; fi
@@ -1996,7 +1996,10 @@ _mr_named(){ local nc=0 rc=1; shopt -q nocasematch && nc=1; shopt -s nocasematch
1996
1996
  [ "$unglob" = 1 ] && set +f
1997
1997
  fi
1998
1998
  [ "$nc" = 0 ] && shopt -u nocasematch; return "$rc"; }
1999
- _mr_named && block "a model routing record named in a command (the user and Crewforth's hooks write it)" "4.5" tamper
1999
+ # Reading one is open through the Read tool, which no gate here matches: the rule is about a command, which cannot be
2000
+ # told reading from writing by its text. Said in the refusal, since a session that did not know it told the user it
2001
+ # could not even show them the file (field report, 3.2.1).
2002
+ _mr_named && block "a model routing record named in a command (the user and Crewforth's hooks write it; to show it to the user, read it with the Read tool)" "4.5" tamper
2000
2003
  # The refuter ledger (hooks/refuter-ledger.sh) by its file name: the agent's name alone is not refused, so a command
2001
2004
  # that greps for crew-refuter still runs. The same honest scope as the rule above.
2002
2005
  _rf_named(){ local nc=0 rc=1; shopt -q nocasematch && nc=1; shopt -s nocasematch
@@ -2436,6 +2439,29 @@ _p44_scan(){ # $1 = the collapsed `git push …` -> 0 when it is `git push [-u]
2436
2439
  [ "$npos" = 2 ] || { _P44_WHY="the remote and the branch are not both written out"; return 1; }
2437
2440
  return 0
2438
2441
  }
2442
+ # A PUSH NO APPROVAL CAN COVER: one that deletes or mirrors a remote ref (`--delete`, `-d`, `--mirror`, `--prune`,
2443
+ # `--all`, `:<ref>`). An approval records one form, `git push <remote> <branch>`, and the form was checked only after
2444
+ # the record was read, so with no record the refusal offered the approval question and /crew-approve for it; in the
2445
+ # field (3.2.1) the user approved twice and typed the command once, and each time it was refused again. Asked before
2446
+ # the record, so the refusal can say at once that this one is the user's to run.
2447
+ _p44_never(){ # 0 = this call is a push no approval covers; _P44N names the token that makes it so
2448
+ # Read from the text with its quotes taken out (CMD_UQ), after the word push: `git -C . push origin :x` and
2449
+ # `git push origin "":old` are deleting pushes too, and a reading that gave up on a form it could not collapse let
2450
+ # them fall through to the approval offer (found in review).
2451
+ local tok unglob=0 after=0
2452
+ _P44N=""
2453
+ git_has "$CMD_SEEN" 'push' || return 1
2454
+ git_has "$CMD_SEEN" 'commit' && return 1
2455
+ case "$-" in *f*) ;; *) unglob=1; set -f ;; esac
2456
+ set -- ${CMD_UQ//[;&|]/ ; }
2457
+ [ "$unglob" = 1 ] && set +f
2458
+ for tok in "$@"; do
2459
+ case "$tok" in \;) after=0; continue ;; push) after=1; continue ;; esac
2460
+ [ "$after" = 1 ] || continue
2461
+ case "$tok" in -d|--delete|--mirror|--prune|--all|:*|+:*) _P44N="$tok"; return 0 ;; esac
2462
+ done
2463
+ return 1
2464
+ }
2439
2465
  _approval_ok(){ # 0 = the user's recorded approval covers THIS call. Otherwise _APW says why not, for the message.
2440
2466
  local k v op="" tr="" h="" b="" r="" u="" sid="" ts="" now age dir="${_CWD:-.}" hc=1 hp=1 cur tool tag _h2
2441
2467
  _APW="no approval from the user is on record"
@@ -2565,6 +2591,13 @@ allow_approved(){
2565
2591
  _gate_written(){
2566
2592
  # No prompt reaches a person here, but their own message does: see _approval_ok above.
2567
2593
  _approval_ok && allow_approved
2594
+ if _p44_never; then
2595
+ gatelog BLOCK 4.4 "a push form no approval covers"
2596
+ echo "GUARD (§4.4): this push ($_P44N) deletes or mirrors a remote ref, and no approval given in a session covers it: an approval records one form, git push <remote> <branch>." >&2
2597
+ echo "Do not ask the user to approve it in this session, by question or by a typed command: they would approve and this command would be refused again." >&2
2598
+ echo "Tell the user, in their language, that this one is theirs to run: in their own terminal, or on GitHub (a merged pull request's branch has a Delete branch button there)." >&2
2599
+ exit 2
2600
+ fi
2568
2601
  [ -n "${_PRF:-}" ] || { _PRF=0; _gb_prh && _PRF=1; } # the default branch has already asked
2569
2602
  gatelog BLOCK 4.4 "commit/push with no matching approval from the user"
2570
2603
  case "$PERM_MODE" in
@@ -2765,13 +2798,14 @@ _c47_scan(){ # $1 = command. Sets: _C47_N (commits read) _C47_WT _C47_NV _C47_A
2765
2798
  _C47_VARS=" "
2766
2799
  _C47_SHRE='(^|[^A-Za-z0-9_-])git[[:space:]]+(-[^[:space:]]+[[:space:]]+)*commit([[:space:]]|$)'
2767
2800
  _C47_N=0; _C47_WT=""; _C47_NV=""; _C47_AM=""; _C47_UNK=""; _C47_EXP=""; _C47_ENV=""; _C47_SH=""; _C47_CFG=""; _C47_UNR=""; _C47_SUB=""; _C47_ARGS=""; _C47_CD=0; _C47_CDT=""
2801
+ _C47_MK=""; _mk_sub=""
2768
2802
  _c47_read "$1"; m="$_C47_M"
2769
2803
  case "$-" in *f*) ;; *) unglob=1; set -f ;; esac
2770
2804
  set -- $_C47_T
2771
2805
  [ "$unglob" = 1 ] && set +f
2772
2806
  while [ $# -gt 0 ]; do
2773
2807
  raw="$1"; shift
2774
- [ "$raw" = ";" ] && { ph=cmd; wrapped=0; vc=0; continue; }
2808
+ [ "$raw" = ";" ] && { [ "$ph" = mk ] && _c47_mk_close; ph=cmd; wrapped=0; vc=0; continue; }
2775
2809
  _WAT=0
2776
2810
  case "$raw" in "$m"*|*"$m"*) _c47_w "$raw" ;; *) _W="$raw"; _WQ=0; case "$raw" in *[\$\*\?\[\{]*) _WX=1 ;; *) _WX=0 ;; esac ;; esac
2777
2811
  tok="$_W"
@@ -2878,8 +2912,23 @@ _c47_scan(){ # $1 = command. Sets: _C47_N (commits read) _C47_WT _C47_NV _C47_A
2878
2912
  _C47_SUB="${tok:0:40}"; ph=skip ;;
2879
2913
  commit) ph=commit; _C47_N=$((_C47_N+1))
2880
2914
  if [ "$rcd" != 0 ] && [ "$_C47_CD" = 0 ]; then _C47_CD=$rcd; _C47_CDT="$rcdt"; fi ;;
2915
+ merge|cherry-pick|revert|am|commit-tree|pull) ph=mk; _mk_sub="$tok"; _mk_nc=0; _mk_sq=0; _mk_ffo=0; _mk_ffs=0; _mk_rb=0; _mk_ctl=0 ;;
2881
2916
  *) ph=skip ;;
2882
2917
  esac ;;
2918
+ mk) # §4.4: a git command that makes a commit (see _c47_mk_close). The value of an option that takes one is
2919
+ # skipped, so `-m --squash` is a message and not a flag; of two opposite flags git takes the last, and so
2920
+ # does this (`--no-commit --commit` commits).
2921
+ case "$tok" in
2922
+ -m|-F|-s|-X|-o|-j|--message|--file|--strategy|--strategy-option|--into-name|--cleanup|--mainline|--server-option|--depth|--deepen|--shallow-since|--shallow-exclude|--upload-pack|--jobs|--whitespace|--directory|--exclude|--include|--resolvemsg)
2923
+ case "${1:-}" in ''|\;) ;; *) shift ;; esac ;;
2924
+ --no-commit) _mk_nc=1 ;; --commit) _mk_nc=0 ;;
2925
+ -n) case "$_mk_sub" in cherry-pick|revert) _mk_nc=1 ;; esac ;;
2926
+ --squash) _mk_sq=1 ;; --no-squash) _mk_sq=0 ;;
2927
+ --ff-only) _mk_ffo=1; _mk_ffs=1 ;; --ff|--no-ff) _mk_ffo=0; _mk_ffs=1 ;;
2928
+ -r|--rebase|--rebase=*) _mk_rb=1 ;; --no-rebase) _mk_rb=0 ;;
2929
+ --abort|--quit|--skip|--show-current-patch|--show-current-patch=*) _mk_ctl=1 ;;
2930
+ --continue) _mk_ctl=0 ;;
2931
+ esac ;;
2883
2932
  commit)
2884
2933
  case "$raw" in
2885
2934
  '<<H') continue ;; # a here-document; arguments may follow it
@@ -2928,9 +2977,29 @@ _c47_scan(){ # $1 = command. Sets: _C47_N (commits read) _C47_WT _C47_NV _C47_A
2928
2977
  esac ;;
2929
2978
  esac
2930
2979
  done
2980
+ [ "$ph" = mk ] && _c47_mk_close
2931
2981
  [ "$envf" = 1 ] && [ "$_C47_N" != 0 ] && _C47_ENV="a variable that changes what git reads"
2932
2982
  return 0
2933
2983
  }
2984
+ # The end of one `git merge|cherry-pick|revert|am|commit-tree|pull`: does it make a commit? The forms that make none
2985
+ # stay free. pull.ff=only frees a plain pull only when the command line overrides neither it nor pull.rebase.
2986
+ _c47_mk_close(){
2987
+ local free=0
2988
+ case "$_mk_sub" in
2989
+ merge) { [ "$_mk_ctl" = 1 ] || [ "$_mk_nc" = 1 ] || [ "$_mk_sq" = 1 ] || [ "$_mk_ffo" = 1 ]; } && free=1 ;;
2990
+ cherry-pick|revert) { [ "$_mk_ctl" = 1 ] || [ "$_mk_nc" = 1 ]; } && free=1 ;;
2991
+ am) [ "$_mk_ctl" = 1 ] && free=1 ;;
2992
+ pull)
2993
+ if [ "$_mk_rb" = 0 ]; then
2994
+ { [ "$_mk_nc" = 1 ] || [ "$_mk_sq" = 1 ] || [ "$_mk_ffo" = 1 ]; } && free=1
2995
+ if [ "$free" = 0 ] && [ "$_mk_ffs" = 0 ] && [ "$(git -C "${_CWD:-.}" config --get pull.ff 2>/dev/null)" = only ]; then
2996
+ case "$(git -C "${_CWD:-.}" config --get pull.rebase 2>/dev/null)" in ''|false) free=1 ;; esac
2997
+ fi
2998
+ fi ;;
2999
+ esac
3000
+ [ "$free" = 1 ] || _C47_MK="${_C47_MK:-git $_mk_sub}"
3001
+ _mk_sub=""
3002
+ }
2934
3003
 
2935
3004
  # The §4.5 half of what that reading finds is judged HERE, before the pre-authorised branch below: with CLAUDE_GIT_OK
2936
3005
  # nobody is asked, so a commit that skips its hooks or rewrites the last commit must not reach that allow. (§4.6 is
@@ -3101,6 +3170,48 @@ if git_has "$CMD_SEEN" 'add|commit|push|checkout|switch'; then
3101
3170
  1|yes|true|on|YES|TRUE|ON) allow_preauthorised ;; # pre-authorised session (headless/CI)
3102
3171
  esac
3103
3172
  fi
3173
+ # §4.4 — A COMMIT MADE BY ANOTHER COMMAND. `git merge`, `cherry-pick`, `revert`, `am`, `commit-tree` and `pull` make a
3174
+ # commit as surely as `git commit`, and none of them reached this gate: measured in auto mode, each ran with rc 0, and
3175
+ # in the field (3.2.1) a merge commit landed with nobody asked. The forms that make none stay free (_c47_mk_close). The
3176
+ # approval a session records is for a staged tree under `git commit`, which these do not commit, so where no prompt
3177
+ # reaches a person the call is refused and the user runs it; in default and acceptEdits the user is asked.
3178
+ # READ BY THE COMMIT GATE'S OWN READER (_c47_scan), not by a second one: the first version split the text itself, and
3179
+ # review found `'git' merge`, `(git merge …)`, `$(git merge …)`, `git -C "a b" merge` and `git merge -m --squash` all
3180
+ # passing. The reader pairs quotes as the shell does and skips an option's value. Run only when the text, with quotes
3181
+ # and backslashes taken out, holds git and one of the words: builtins up to there, so an ordinary call pays nothing.
3182
+ _mk_try=0
3183
+ # A quote or a backslash can split the word git itself (`g\it merge`, `"g"it merge`: found in review), so a call that
3184
+ # holds one is taken out of its quoting before either word is looked for. Builtins only.
3185
+ case "$CMD_SEEN" in *[Gg][Ii][Tt]*|*[\\\"\']*)
3186
+ _gsub "$CMD_SEEN" '\\'$'\n' '' bs; _unquoted "$_GS"; _t="$_GS"
3187
+ case "$_t" in *[Gg][Ii][Tt]*) ;; *) _t="" ;; esac
3188
+ case "$_t" in *[Mm][Ee][Rr][Gg][Ee]*|*[Cc][Hh][Ee][Rr][Rr][Yy]-[Pp][Ii][Cc][Kk]*|*[Rr][Ee][Vv][Ee][Rr][Tt]*|*[Cc][Oo][Mm][Mm][Ii][Tt]-[Tt][Rr][Ee][Ee]*|*[Pp][Uu][Ll][Ll]*|*[[:space:]][Aa][Mm][[:space:]]*|*[[:space:]][Aa][Mm]|*[Aa][Ll][Ii][Aa][Ss].*) _mk_try=1 ;; esac ;;
3189
+ esac
3190
+ if [ "$_mk_try" = 1 ]; then
3191
+ # The reader resets what the commit rules read from it; this call holds the same text, so they are put back as read.
3192
+ _mk_s1="${_C47_N:-0}" _mk_s2="${_C47_WT:-}" _mk_s3="${_C47_CD:-0}" _mk_s4="${_C47_CDT:-}" _mk_s5="${_C47_CFG:-}"
3193
+ _c47_scan "$CMD"
3194
+ _MKC="$_C47_MK"
3195
+ # An alias given for this one call can stand for any of them: `git -c alias.mg=merge mg` (found in review).
3196
+ case "$_C47_CFG" in *[Aa][Ll][Ii][Aa][Ss].*) [ -z "$_MKC" ] && _MKC="a git alias given in the command ($_C47_CFG)" ;; esac
3197
+ _C47_N="$_mk_s1"; _C47_WT="$_mk_s2"; _C47_CD="$_mk_s3"; _C47_CDT="$_mk_s4"; _C47_CFG="$_mk_s5"
3198
+ if [ -n "$_MKC" ]; then
3199
+ case "${CLAUDE_GIT_OK:-}" in 1|yes|true|on|YES|TRUE|ON) allow_preauthorised ;; esac
3200
+ _mkp=0; case "$PERM_MODE" in default|acceptEdits) _gb_prh || _mkp=1 ;; esac
3201
+ if [ "$_mkp" = 1 ]; then
3202
+ SHORT="$CMD"; [ "${#SHORT}" -gt 300 ] && { _utf8_cut "$SHORT" 300; SHORT="$_UC…"; }
3203
+ ask_user "§4.4 commit approval gate. Claude wants to run a command that makes a commit ($_MKC):
3204
+
3205
+ $SHORT
3206
+
3207
+ Approve only if you want that commit made. Approving lets Claude run the command itself."
3208
+ fi
3209
+ gatelog BLOCK 4.4 "a commit made by another git command with no approval"
3210
+ echo "GUARD (§4.4): '$_MKC' makes a commit, and in '${PERM_MODE:-unknown}' no prompt reaches a person. The approval a session records is for a staged tree under git commit, and does not cover a commit this command makes." >&2
3211
+ echo "Tell the user, in their language, that this one is theirs to run: in their own terminal, or after Shift+Tab to default mode, where this gate asks them. A form that makes no commit needs no approval (merge --ff-only, pull --ff-only, --no-commit, --squash)." >&2
3212
+ exit 2
3213
+ fi
3214
+ fi
3104
3215
  if git_has "$CMD_SEEN" 'commit|push'; then
3105
3216
  # §4.6 — A COMMIT NEEDS A CLEAN REVIEW OF THIS DIFF.
3106
3217
  # crew-review-agent records what it cleared in .claude/review-pass.json; this reads it back and compares two
@@ -3274,6 +3385,10 @@ if git_has "$CMD_SEEN" 'commit|push'; then
3274
3385
  echo "GUARD (§4.6): this call changes directory before it commits, and the commit would not run in the repository" >&2
3275
3386
  echo "this session is in — or the target cannot be read (a variable, 'cd -', more than one). The review record" >&2
3276
3387
  echo "describes what is staged HERE. Run the commit from this directory, or run it yourself in your terminal." >&2
3388
+ # The way through, said (field report, 3.2.1: a session in the root tried `cd <worktree> && git commit` and stopped
3389
+ # there). The gate stays closed to the cd; the session moves into the worktree instead.
3390
+ echo "For another worktree: move the session into it first (EnterWorktree, or start the session there), have the" >&2
3391
+ echo "review run there, and commit from it; or the user commits there in their own terminal." >&2
3277
3392
  exit 2
3278
3393
  fi
3279
3394
  fi
@@ -3297,6 +3412,16 @@ if git_has "$CMD_SEEN" 'commit|push'; then
3297
3412
  # git queries move too, not just the path: a staged diff read in the wrong worktree is the same defect
3298
3413
  # wearing different clothes. `_CWD` is "" when the payload carried none, and `-C .` is then a no-op.
3299
3414
  _RPD="${_CWD:-.}"
3415
+ # ...and at the TOP of that work tree: a cwd that moved into a sub-folder (a subagent's run leaves the Bash tool
3416
+ # in packages/api) looked for packages/api/.claude/review-pass.json and refused a commit whose record sat at the
3417
+ # root (field report, 3.2.1). The top is the nearest folder above that holds `.git` (a directory, or a linked
3418
+ # worktree's file), walked with builtins: a `git rev-parse` here cost the refusal path two processes over the
3419
+ # gate's ceiling (smoke's cost table). Only when the record is not where the cwd is.
3420
+ if [ ! -f "$_RPD/.claude/review-pass.json" ]; then
3421
+ _RPT="$_RPD"
3422
+ while [ ! -e "$_RPT/.git" ]; do case "$_RPT" in */*) _RPT="${_RPT%/*}" ;; *) _RPT=""; break ;; esac; done
3423
+ [ -n "$_RPT" ] && _RPD="$_RPT" # found or not, the record belongs there, and the refusal names that path
3424
+ fi
3300
3425
  RP="$_RPD/.claude/review-pass.json"
3301
3426
  if [ ! -f "$RP" ]; then
3302
3427
  gatelog BLOCK 4.6 "no review-pass record"
@@ -81,7 +81,10 @@ while IFS= read -r sv; do
81
81
  [ -n "$sv" ] || continue
82
82
  _json_unescape "$sv" >/dev/null; _crew_appr_op "$_JU"
83
83
  if [ -n "$OP" ]; then
84
- if [ "$OP" = loosen ]; then
84
+ if [ "$OP" = allowverify ]; then
85
+ echo "GUARD (§4.4): this call schedules a prompt that allows a verify command. Only the user can type /crew-allow-verify; a prompt a tool schedules would grant the hook a command in their place, so the call is refused." >&2
86
+ echo "Schedule the work without it. If a verify command should run, say which: the user can type /crew-allow-verify <command> themselves." >&2
87
+ elif [ "$OP" = loosen ]; then
85
88
  echo "GUARD (§4.4): this call schedules a prompt that lowers a model floor. Only the user can type /crew-loosen; a prompt a tool schedules would lower the floor in their place, so the call is refused." >&2
86
89
  echo "Schedule the work without it. If a floor looks too high, say so: the user can type /crew-loosen $LO_AGENT $LO_CHANGE $LO_RISK themselves." >&2
87
90
  else
@@ -471,6 +471,27 @@ fi
471
471
  # escape : .private-allowlist.txt, a term per full line, for the rare case a term is legitimately public.
472
472
  PRIV_F="$(mktemp)"; PTERM_F="$(mktemp)"
473
473
  PTF=""; [ -n "$GITROOT" ] && [ -f "$GITROOT/.private-terms.txt" ] && PTF="$GITROOT/.private-terms.txt"
474
+ # A linked worktree has no copy of a gitignored file, so its own root never holds the terms: committing from one
475
+ # skipped them (measured: the same commit rc=1 in the checkout, rc=0 in its worktree). Read the checkout's list
476
+ # then. Only this list falls back — it tightens; an allowlist missing there only makes the gate stricter.
477
+ # Builtins only: `.git` is a file in a worktree ("gitdir: <dir>"), and <dir>/commondir names the shared .git.
478
+ if [ -z "$PTF" ] && [ -n "$GITROOT" ] && [ -f "$GITROOT/.git" ]; then
479
+ _gd=""; IFS= read -r _gd < "$GITROOT/.git" || true; _gd="${_gd#gitdir: }"; _gd="${_gd%$'\r'}"
480
+ case "$_gd" in /*|[A-Za-z]:*) ;; *) _gd="$GITROOT/$_gd" ;; esac
481
+ _cd=""; [ -f "$_gd/commondir" ] && { IFS= read -r _cd < "$_gd/commondir" || true; _cd="${_cd%$'\r'}"; }
482
+ if [ -n "$_cd" ]; then
483
+ case "$_cd" in /*|[A-Za-z]:*) ;; *) _cd="$_gd/$_cd" ;; esac
484
+ # Only a shared dir named .git sits in its checkout; a bare or separate git dir's parent is not the checkout
485
+ # (found in review: it read whatever list happened to sit beside the git dir).
486
+ # A relative commondir reads <gitdir>/../..: drop each trailing "/<name>/.." before asking for the name.
487
+ _cd="${_cd%/}"; _up=0
488
+ while :; do case "$_cd" in */..) _cd="${_cd%/..}"; _up=$((_up+1)) ;; */.) _cd="${_cd%/.}" ;; *) break ;; esac; done
489
+ while [ "$_up" -gt 0 ]; do _cd="${_cd%/*}"; _up=$((_up-1)); done
490
+ case "$_cd" in */.git) [ -f "${_cd%/.git}/.private-terms.txt" ] && PTF="${_cd%/.git}/.private-terms.txt" ;; esac
491
+ unset _up
492
+ fi
493
+ unset _gd _cd
494
+ fi
474
495
  PAL=""; [ -n "$GITROOT" ] && [ -f "$GITROOT/.private-allowlist.txt" ] && PAL="$GITROOT/.private-allowlist.txt"
475
496
  {
476
497
  # A home path shorter than this cannot be distinguishing (`/root`, `/`), and matching it would flag everything.
@@ -376,15 +376,24 @@ _crew_appr_path(){ # $1 = a directory -> _AP: the record's path in that worktre
376
376
 
377
377
  # ---- WHICH TEXT IS AN APPROVAL: one function, asked here of the user's prompt and by guard-schedule.sh of a
378
378
  # prompt a tool is about to schedule ---------------------------------------------------------------------------
379
- _crew_appr_op(){ # $1 = a text, decoded -> OP: commit | push | commit+push | pending | loosen, or "" when the text is none
379
+ _crew_appr_op(){ # $1 = a text, decoded -> OP: commit | push | commit+push | pending | loosen | allowverify, or "" when the text is none
380
380
  # `loosen` is the other thing only the user's own message does: `/crew-loosen <agent> <change> <risk>` lowers a
381
381
  # floor that model routing raised (3.1.0). It is matched HERE so that everything that refuses to hand a session
382
382
  # an approval (a scheduled prompt, a `claude -p` call) refuses to hand it this as well: one question, one answer.
383
383
  local P="$1" _k _v _vr LC_ALL=C
384
- OP=""; LO_AGENT=""; LO_CHANGE=""; LO_RISK=""
384
+ OP=""; LO_AGENT=""; LO_CHANGE=""; LO_RISK=""; AV_CMD=""
385
385
  P="${P//$'\r'/}"
386
386
  while :; do case "$P" in [$' \t\n']*) P="${P:1}" ;; *) break ;; esac; done
387
387
  while :; do case "$P" in *[$' \t\n']) P="${P%?}" ;; *) break ;; esac; done
388
+ # `/crew-allow-verify [<command>]` (3.2.2): the user allows a verify command by name, the third thing only their own
389
+ # message does. The message is at most 120 bytes before it reaches this function (the limit every reader of an
390
+ # approval shares, guard-schedule.sh's included); a longer one is answered before that limit, not here.
391
+ _k="${P%%[$' \t']*}"
392
+ case "$_k" in /*) case "${_k##*[/:]}" in crew-allow-verify)
393
+ case "$P" in *$'\n'*) return 0 ;; esac
394
+ AV_CMD="${P:${#_k}}"; while :; do case "$AV_CMD" in [$' \t']*) AV_CMD="${AV_CMD:1}" ;; *) break ;; esac; done
395
+ OP=allowverify; return 0 ;; esac ;;
396
+ esac
388
397
  [ "${#P}" -le 90 ] || return 0
389
398
  _vr=""
390
399
  case "$P" in
@@ -664,7 +673,13 @@ _pa_record(){
664
673
  case "$TREE" in *[!0-9a-f]*|'') no "git could not write the tree of what is staged (an unresolved merge?)." ;; esac
665
674
  if [ "$HEAD_NOW" = NONE ]; then _ht=4b825dc642cb6eb9a060e54bf8d69288fbee4904 # the empty tree
666
675
  else _ht="$(git -C "$CWD" rev-parse --verify --quiet 'HEAD^{tree}' 2>/dev/null)"; fi
667
- [ "$TREE" != "$_ht" ] || no "nothing is staged, so there is nothing to approve. Stage the change with git add, show the commit message, then ask again." ;;
676
+ # A commit+push with nothing staged is most often a push alone, answered with the option people pick first (field
677
+ # report, 3.2.1: twice in a row). It is not turned into a push approval, which the user did not give; the line
678
+ # names the one they can.
679
+ if [ "$TREE" = "$_ht" ]; then
680
+ [ "$OP" = commit+push ] && no "nothing is staged, so there is no commit to approve. For a push of the branch as it is, the answer is Push only (or the user can type /crew-approve push); for a commit, stage it with git add, show the commit message, then ask again."
681
+ no "nothing is staged, so there is nothing to approve. Stage the change with git add, show the commit message, then ask again."
682
+ fi ;;
668
683
  esac
669
684
  case "$OP" in *push)
670
685
  [ "$OP" = push ] && [ "$HEAD_NOW" = NONE ] && no "there is no commit to push yet."
@@ -817,6 +832,15 @@ _crew_q_dir "$CWD"; [ -s "$TRIED" ] && { : > "$TRIED"; } 2>/dev/null
817
832
 
818
833
  # ---- is this message an approval? ------------------------------------------------------------------------
819
834
  [ "$_n_prompt" = 1 ] || exit 0
835
+ # A /crew-allow-verify longer than that is answered, not dropped in silence (found in review): it writes nothing, and
836
+ # the limit stays the one guard-schedule.sh reads a scheduled prompt by, so a long one cannot be scheduled past it.
837
+ if [ "${#RAW}" -gt 120 ]; then
838
+ _lk="${RAW%%[ \\]*}"
839
+ case "$_lk" in /*) case "${_lk##*[/:]}" in crew-allow-verify)
840
+ EVN=UserPromptSubmit
841
+ say "Crewforth: verify command NOT allowed - the message is longer than this command reads (120 characters). Write the line into .claude/crew-model-rules by hand, as verify <command>." "The user sent the command only the user can type, /crew-allow-verify, with a command longer than it reads, and Crewforth added no rule. Tell the user to write that line into .claude/crew-model-rules by hand, as verify <command>." ;; esac ;;
842
+ esac
843
+ fi
820
844
  [ "${#RAW}" -le 120 ] || exit 0 # an approval is a few words; a pasted page is not decoded to find that out
821
845
  _json_unescape "$RAW" >/dev/null
822
846
  _crew_appr_op "$_JU"
@@ -848,6 +872,14 @@ if [ "$OP" = loosen ]; then
848
872
  [ "$_AMF_LOOSE" = 0 ] || nol "that floor was already lowered once ($_AMF_TIER to $_AMF_TO, $_AMF_LTS). It is lowered once for each time it is raised."
849
873
  _am_rank "$_AMF_TIER"; [ "$_AMR" -ge 2 ] || nol "the floor on record is $_AMF_TIER, and there is no model below it."
850
874
  _am_name $((_AMR-1)); LTO="$_AMN"
875
+ # Not below the class's own floor (the agent's, then the card's change): a lowering there would be written, read
876
+ # back as in force, and refused by the gate all the same (found in review: feature, raised to sonnet by the record
877
+ # of the haiku runs before its own floor was sonnet, was "lowered to haiku" and still refused on haiku).
878
+ if declare -F _am_change_floor >/dev/null 2>&1; then
879
+ floor=0; why=""; _am_agent_floor "$LO_AGENT"; _am_change_floor "$LO_CHANGE" "$LO_AGENT"
880
+ _am_rank "$LTO"
881
+ if [ "$_AMR" -lt "$floor" ]; then _am_name "$floor"; nol "the class's own floor is $_AMN ($why), which nothing lowers, so a lowering of the raise on record to $LTO would not be in force."; fi
882
+ fi
851
883
  LTS="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)"; case "$LTS" in [0-9][0-9][0-9][0-9]-*Z) ;; *) nol "the clock could not be read." ;; esac
852
884
  LF="$_AMS/crew-model-loosened.tsv"
853
885
  mkdir -p "$_AMS" 2>/dev/null || nol "the state folder could not be made."
@@ -862,6 +894,51 @@ if [ "$OP" = loosen ]; then
862
894
  "The user lowered the calibration floor of $LO_AGENT / $LO_CHANGE / $LO_RISK from $LFROM to $LTO with their own command. The card's own rules still hold (critical work on opus, no verify command on sonnet or above, the agent's floor). If the class fails its first try too often again, the floor rises again."
863
895
  fi
864
896
 
897
+ # ---- /crew-allow-verify: a verify command the hook may run, allowed by the user and by nobody else --------------
898
+ # .claude/crew-model-rules is closed to the session (guard-write.sh, guard-bash.sh), so doctor's advice "add its line"
899
+ # left the user to find and edit the file by hand (field report, 3.2.1). With nothing after it the command lists the
900
+ # verify commands refused for want of a rule (the reading doctor uses, eval/lib/verify-cmd.sh); with a command after
901
+ # it, that command gets its `verify <command>` line. Only a plain one: one command in letters, digits and . _ / = : , @
902
+ # + % - and spaces, so it can neither chain another (the gate would refuse a chain anyway) nor need escaping here.
903
+ if [ "$OP" = allowverify ]; then
904
+ noa(){ say "Crewforth: verify command NOT allowed - $1" "The user sent the command only the user can type /crew-allow-verify, and Crewforth added no rule: $1 Tell the user exactly that."; }
905
+ case "${CLAUDE_CODE_SESSION_ATTENDED-}" in 0) noa "this session has nobody in front of it (it was started with -p, or by a program)." ;; esac
906
+ _json_slice "$INPUT" transcript_path >/dev/null; _json_unescape "$_JS" >/dev/null; TP="${_JU//\\//}"
907
+ { [ -n "$TP" ] && [ -s "$TP" ]; } || noa "this is the first message of this session, or the session keeps no transcript."
908
+ _lh="${BASH_SOURCE%/*}"; [ "$_lh" = "${BASH_SOURCE}" ] && _lh=.
909
+ [ -f "$_lh/../eval/lib/verify-cmd.sh" ] || noa "Crewforth's verify reading is not installed beside this hook (eval/lib/verify-cmd.sh)."
910
+ . "$_lh/../eval/lib/verify-cmd.sh"
911
+ _avr="${CLAUDE_PROJECT_DIR:-$CWD}"; _avr="${_avr//\\//}"; _avr="${_avr%/}"
912
+ _avok(){ case "$1" in ''|*[!A-Za-z0-9\ ._/=:,@+%-]*|-*) return 1 ;; esac; return 0; }
913
+ if [ -z "$AV_CMD" ]; then
914
+ crew_verify_blocked "$_avr"
915
+ _avl=""; _avn=0; _avx=0; _avk='/crew-allow-verify'; _avwho='each is the whole message that allows it, and only the user can type it'
916
+ while IFS= read -r _avc; do [ -n "$_avc" ] || continue
917
+ if _avok "$_avc"; then _avn=$((_avn+1)); _avl="$_avl $_avk $_avc ·"; else _avx=$((_avx+1)); fi
918
+ done <<< "$_VB_CMDS"
919
+ _avl="${_avl% ·}"
920
+ [ "$_avn$_avx" = 00 ] && say "Crewforth: no verify command on record was refused for want of a rule." "The user sent the command only the user can type, /crew-allow-verify, with nothing after it, and no verify command on record was refused for want of a rule: tell them so, in one line."
921
+ _avm=""; [ "$_avx" -gt 0 ] && _avm=" $_avx more hold a character this command does not write; those lines go into .claude/crew-model-rules by hand, as verify <command>."
922
+ say "Crewforth: verify commands refused for want of a rule:${_avl:- none in a form this command writes}.$_avm" "The user sent the command only the user can type, /crew-allow-verify, with nothing after it. Show them, in their language, the lines in Crewforth's message, one per line, as they stand: $_avwho. Do not type one for them."
923
+ fi
924
+ case "$AV_CMD" in *[\;\&\|\<\>\`]*|*'$('*) noa "this is more than one command (a ; & | < > or a substitution): the hook runs a verify command alone, so no rule makes it run a chain. Give the card a single command, or a script that holds the chain." ;; esac
925
+ _avok "$AV_CMD" || noa "'${AV_CMD//[!A-Za-z0-9 ._\/=:,@+%-]/?}' is not one plain command (letters, digits, . _ / = : , @ + % - and spaces only, not starting with -). Write such a line into .claude/crew-model-rules by hand, as verify <command>."
926
+ case " $AV_CMD " in *" git commit "*|*" git push "*) noa "a git commit or push is never a verify command: the hook does not commit or push." ;; esac
927
+ crew_verify_open_ended "$AV_CMD" && noa "a rule '$AV_CMD' would let the hook run any command that begins with it (an interpreter, a shell, a wrapper or a package runner, with nothing after it that names the file or the task). Name the script or the test: bash tools/check.sh, npm test, pytest tests/."
928
+ _avf="$_avr/.claude/crew-model-rules"
929
+ { [ -L "$_avr/.claude" ] || [ -L "$_avf" ]; } && noa "the rules file, or the .claude folder, is a symbolic link: Crewforth writes the rules only into the project's own file."
930
+ if [ -f "$_avf" ]; then
931
+ while IFS= read -r _avl || [ -n "$_avl" ]; do [ "${_avl%$'\r'}" = "verify $AV_CMD" ] && say "Crewforth: verify $AV_CMD is already allowed." "The user sent the command only the user can type, /crew-allow-verify $AV_CMD, and .claude/crew-model-rules already holds that line: tell them so, in one line."; done < "$_avf"
932
+ fi
933
+ mkdir -p "$_avr/.claude" 2>/dev/null || noa "the .claude folder could not be made."
934
+ if [ -s "$_avf" ] && [ "$(tail -c 1 "$_avf" | od -An -tx1 | tr -d ' \n')" != 0a ]; then printf '\n' >> "$_avf" || noa "the rules file could not be written."; fi
935
+ { printf 'verify %s\n' "$AV_CMD" >> "$_avf"; } 2>/dev/null || noa "the rules file could not be written."
936
+ # Said only when it is TRUE: the line is read back from the file.
937
+ _avh=0; while IFS= read -r _avl || [ -n "$_avl" ]; do [ "${_avl%$'\r'}" = "verify $AV_CMD" ] && _avh=1; done < "$_avf"
938
+ [ "$_avh" = 1 ] || noa "the line was written but is not read back from .claude/crew-model-rules."
939
+ say "Crewforth: verify command allowed - the hook may now run: $AV_CMD (the line is in .claude/crew-model-rules)." "The user allowed the verify command $AV_CMD with their own command; Crewforth added the line verify $AV_CMD to .claude/crew-model-rules. From the next agent call, the hook runs it when a card names it, and judges the result. Tell the user that in one line. A deny rule of the settings still stops it, and it is still run alone, with no chain."
940
+ fi
941
+
865
942
  # ---- `/crew-approve` with nothing after it: what this session was last refused ---------------------------------
866
943
  # guard-bash.sh, when it refuses a commit or a push in auto / dontAsk, writes down what it refused
867
944
  # (.claude/state/crew-approval-pending: the session, commit | push | commit+push, the second). The bare command
@@ -133,7 +133,24 @@ function norm(s) {
133
133
  sub(/^ +/,"",s); sub(/ +$/,"",s)
134
134
  return s
135
135
  }
136
- BEGIN { np = " " norm(ENVIRON["CREW_PROMPT"]) " "; bestA=0; bestS=0; nameA=""; nameS="" }
136
+ # A request to READ is not work for an owner. "read the notes at the end of the README" named crew-docs-agent, an
137
+ # agent that writes docs, because the scorer saw `README` and never the verb (field report, 3.2.1). Such a prompt
138
+ # is left to the main thread, and only when the read verb is the REQUEST: the first word of an English prompt
139
+ # (after please / can you / could you), the last word of a Turkish one (before mısın / musun / lütfen), since
140
+ # Turkish puts the verb last. Anywhere else it is a bug report or part of a larger task — "the button does not
141
+ # show the spinner" routed to the frontend expert before this, and must still (found in review). A second clause
142
+ # (and, then, ve, sonra) or one action word anywhere and the hint is scored as before. `show` and `göster` are not
143
+ # on the list: "show the avatar in the navbar" asks for UI work, and routed to the frontend expert (found in review).
144
+ function hasw(re) { return np ~ ("(^| )(" re ")( |$)") }
145
+ function readonly( w, n, f, l) {
146
+ n = split(np, w, " "); if (n == 0) return 0
147
+ f = 1; while (f < n && w[f] ~ /^(please|pls|can|could|would|you)$/) f++
148
+ l = n; while (l > 1 && w[l] ~ /^(misin|musun|misiniz|musunuz|lutfen|bakalim)$/) l--
149
+ if (w[f] !~ /^(read|explain|summari[sz]e|describe)$/ &&
150
+ w[l] !~ /^(oku|okur|okuyun|okuyabilir|anlat|anlatir|anlatin|acikla|aciklar|aciklayin|ozetle|ozetler|ozetleyin)$/) return 0
151
+ return !hasw("and|then|ve|sonra|(fix|add|updat|writ|wrote|chang|implement|creat|remov|delet|refactor|build|buil|mak|renam|edit|migrat|mov|deploy|ship|instal|upgrad|review|audit|check|test|debug|investigat|optimi[sz]|improv|translat|draft|generat|setup|set|harden|resolv|run|merg|publish|configur|propos|analy[sz])[a-z]?[a-z]?[a-z]?[a-z]?|why|bug|bugs|crash[a-z]*|fail[a-z]*|error[a-z]*|broken|duzelt[a-z]*|ekle[a-z]*|guncelle[a-z]*|yaz[a-z]*|degistir[a-z]*|olustur[a-z]*|sil[a-z]*|kaldir[a-z]*|yap[a-z]*|kur[a-z]*|tasi[a-z]*|incele[a-z]*|denetle[a-z]*|cevir[a-z]*|duzenle[a-z]*|iyilestir[a-z]*|yayinla[a-z]*|cikar[a-z]*|hizlandir[a-z]*|uygula[a-z]*|coz[a-z]*|gelistir[a-z]*|sadelestir[a-z]*|analiz[a-z]*|neden|hata[a-z]*|cokuyor|bozuk[a-z]*")
152
+ }
153
+ BEGIN { np = " " norm(ENVIRON["CREW_PROMPT"]) " "; bestA=0; bestS=0; nameA=""; nameS=""; if (readonly()) exit }
137
154
  seen[FILENAME] { next } # one Trigger-phrases line per component, the first
138
155
  {
139
156
  if (tolower($0) !~ /trigger phrases:/) next
@@ -62,8 +62,12 @@ HOOKMSG=""
62
62
  if [ -f "$ROOT/.claude/hooks/pre-commit" ] && git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
63
63
  _hp="$(git -C "$ROOT" config --get core.hooksPath 2>/dev/null || true)"; _hp="${_hp%$'\r'}"; _hp="${_hp//\\//}"
64
64
  case "${_hp%/}" in
65
- .claude/hooks|*/.claude/hooks|.claude/git-shim|*/.claude/git-shim) ;;
66
- '') HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath is not set, so a commit made outside Claude Code is not scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: git config core.hooksPath .claude/hooks, or the Crewforth update when the repository has hooks of its own. /crew-doctor shows the state." ;;
65
+ .claude/hooks|.claude/git-shim) ;;
66
+ # Absolute, as the installers now write it (a relative one runs no hook in a linked worktree): it has to be THERE.
67
+ # A repository that was moved keeps the old path, and git then runs no hook at all, with nothing said.
68
+ */.claude/hooks|*/.claude/git-shim)
69
+ [ -d "$_hp" ] || HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath points at a directory that does not exist (the repository was moved?), so no commit is scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: the Crewforth update (npx crewforth update) writes the path again. /crew-doctor shows the state." ;;
70
+ '') HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath is not set, so a commit made outside Claude Code is not scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: the Crewforth update (npx crewforth update), which arms it and keeps any hooks the repository has of its own. /crew-doctor shows the state." ;;
67
71
  *) HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath points at another hook directory, so a commit made outside Claude Code is not scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: the Crewforth update keeps that directory's hooks and runs Crewforth's before them. /crew-doctor shows the state." ;;
68
72
  esac
69
73
  fi
@@ -1,7 +1,8 @@
1
- # Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.1.1 (below VERSION 3.2.0) — do not edit by hand.
1
+ # Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.2.1 (below VERSION 3.3.0) — do not edit by hand.
2
2
  # The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
3
3
  # install_path blob_sha1 first_tag last_tag
4
4
  .claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
5
+ .claude/AGENT_TEMPLATE.md 2c8223d811245e3627a3011bb64fb9e4b0bf3b94 v3.2.0 v3.2.1
5
6
  .claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
6
7
  .claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
7
8
  .claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.1
@@ -29,6 +30,7 @@
29
30
  .claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.1
30
31
  .claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
31
32
  .claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
33
+ .claude/DISCIPLINE.md cf881a5229d85850194a98b8d0d0438a8eaaa9d6 v3.2.0 v3.2.1
32
34
  .claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
33
35
  .claude/DISCIPLINE.md dc1c41baa3a8b7d637214bc78553a78d94d14ac9 v1.6.2 v1.6.3
34
36
  .claude/DISCIPLINE.md e42aefc9c1ffaeace8e5712d92df780959212280 v2.0.0 v2.1.0
@@ -43,4 +45,4 @@
43
45
  .claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
44
46
  .claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
45
47
  .claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
46
- .claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.1.1
48
+ .claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.2.1
@@ -63,6 +63,11 @@ git add <specific/file>
63
63
  ```
64
64
  Each commit focused on a single topic; "feat + fix + refactor" doesn't belong in one commit.
65
65
 
66
+ **The lines that describe a change belong to its commit**: its CHANGELOG entry, its README line, its docs. They are
67
+ part of the same logical change, not a second one, so stage them with it. A separate "docs: changelog" commit after
68
+ the code is a second commit, and so a second approval (§4.4): the approval covers what is staged and HEAD at that
69
+ moment, and a commit added later is not covered by it.
70
+
66
71
  ## Good / bad
67
72
 
68
73
  Worked good-vs-bad commit examples: **`references/examples.md`**.