crewforth 3.2.0-rc.1 → 3.2.0-rc.10

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +190 -5
  2. package/README.md +2 -0
  3. package/VERSION +1 -1
  4. package/adopt.sh +55 -6
  5. package/bin/cli.js +3 -2
  6. package/kit/CLAUDE.md +6 -6
  7. package/kit/agents/crew-commit-agent.md +6 -4
  8. package/kit/agents/crew-review-agent.md +2 -0
  9. package/kit/eval/doctor.sh +37 -3
  10. package/kit/eval/gate-report.sh +2 -0
  11. package/kit/eval/lib/crew-map/narrative.mjs +42 -8
  12. package/kit/eval/lib/crew-map/project.mjs +3 -2
  13. package/kit/eval/lib/crew-map/rules/tech.json +35 -0
  14. package/kit/eval/lib/crew-map/scan.mjs +56 -4
  15. package/kit/eval/lib/crew-map/stacks.mjs +114 -8
  16. package/kit/eval/lib/crew-map/surface.mjs +81 -3
  17. package/kit/eval/lib/gate-log.sh +52 -0
  18. package/kit/eval/lib/json-string.awk +27 -0
  19. package/kit/eval/lib/preset.sh +109 -0
  20. package/kit/eval/lib/presets/default.json +7 -0
  21. package/kit/eval/lib/presets/solo.json +7 -0
  22. package/kit/eval/lib/presets/strict.json +11 -0
  23. package/kit/eval/lib/preview.sh +150 -0
  24. package/kit/eval/lib/review-bench/bench.mjs +273 -0
  25. package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
  26. package/kit/eval/lib/review-prepass/prepass.sh +23 -0
  27. package/kit/eval/lib/settings-default.sh +36 -0
  28. package/kit/eval/lib/settings-json.awk +14 -4
  29. package/kit/eval/lib/statusline.sh +4 -19
  30. package/kit/eval/lib/verify-cmd.sh +17 -0
  31. package/kit/eval/lib/version.sh +41 -0
  32. package/kit/eval/review-bench.sh +26 -0
  33. package/kit/eval/smoke-test.sh +424 -26
  34. package/kit/hooks/agent-outcome.sh +38 -9
  35. package/kit/hooks/board-sync.sh +2 -23
  36. package/kit/hooks/guard-agent-model.sh +163 -20
  37. package/kit/hooks/guard-bash.sh +69 -36
  38. package/kit/hooks/guard-commit-scan.sh +10 -2
  39. package/kit/hooks/guard-write.sh +3 -7
  40. package/kit/hooks/precompact-snapshot.sh +75 -0
  41. package/kit/hooks/prompt-approval.sh +74 -5
  42. package/kit/hooks/session-rehydrate.sh +25 -2
  43. package/kit/hooks/session-update-check.sh +9 -8
  44. package/kit/hooks/trace-blocklist.txt +9 -0
  45. package/kit/settings.json +18 -0
  46. package/kit/skills/crew-code-review/SKILL.md +16 -0
  47. package/kit/skills/crew-map/SKILL.md +16 -5
  48. package/kit/skills/crew-review/SKILL.md +2 -1
  49. package/kit/skills/crew-ship/SKILL.md +3 -1
  50. package/kit/studio/README.md +21 -10
  51. package/kit/studio/server/index.js +85 -22
  52. package/kit/studio/server/lib/code-stamp.js +35 -0
  53. package/kit/studio/server/lib/graph.js +9 -1
  54. package/kit/studio/server/lib/kit-telemetry.js +30 -5
  55. package/kit/studio/server/lib/lan.js +87 -0
  56. package/kit/studio/server/lib/owned-stream.js +56 -0
  57. package/kit/studio/server/lib/projects.js +11 -4
  58. package/kit/studio/server/lib/redact.js +60 -0
  59. package/kit/studio/server/lib/remote.js +104 -18
  60. package/kit/studio/server/lib/session.js +1 -1
  61. package/kit/studio/server/lib/terminal.js +51 -13
  62. package/kit/studio/server/lib/transcript.js +32 -0
  63. package/kit/studio/web/app.js +79 -11
  64. package/kit/studio/web/canvas.js +14 -2
  65. package/kit/studio/web/chat.js +114 -24
  66. package/kit/studio/web/graph-plan.js +3 -2
  67. package/kit/studio/web/liveness.js +14 -0
  68. package/kit/studio/web/map-plan.js +72 -3
  69. package/kit/studio/web/map.js +103 -10
  70. package/kit/studio/web/qr.js +200 -0
  71. package/kit/studio/web/style.css +40 -0
  72. package/kit/studio/web/tokens.css +6 -0
  73. package/package.json +1 -1
  74. package/start.sh +14 -1
@@ -1674,8 +1674,8 @@ if [ "$IS_KIT" = 1 ]; then
1674
1674
  _ch4_t="$(printf '%s\n' "all fo""ur channels carry it" "Dö""rt kanal da taşıyor" "of fo""ur distribution channels" "three channels" "four channel types of RGB" | grep -ciE "$_ch4_re")"
1675
1675
  if [ "$_ch4_t" != 3 ]; then fail "channel-count check cannot tell its twins apart (caught $_ch4_t of the 3 planted, want 3 and none of the 2 clean)"
1676
1676
  elif git -C "$KR" rev-parse --git-dir >/dev/null 2>&1; then
1677
- _ch4_hits="$(git -C "$KR" grep -niIE "$_ch4_re" -- . ':!CHANGELOG.md' ':!site/src/content/docs/changelog.md' ':!site/src/content/docs/tr/changelog.md' 2>/dev/null)"
1678
- [ -z "$_ch4_hits" ] && pass "no tracked file gives the old channel count — three ship Crewforth (CHANGELOG exempt; twins 3/3 caught, 0/2 flagged)" \
1677
+ _ch4_hits="$(git -C "$KR" grep -niIE "$_ch4_re" -- . ':!CHANGELOG.md' ':!site/src/content/docs/changelog.md' ':!site/src/content/docs/tr/changelog.md' ':!packaging/review-bench/cases' 2>/dev/null)"
1678
+ [ -z "$_ch4_hits" ] && pass "no tracked file gives the old channel count — three ship Crewforth (CHANGELOG and the review bench's past-version cases exempt; twins 3/3 caught, 0/2 flagged)" \
1679
1679
  || fail "a tracked file still gives the old channel count (there are three): $(printf '%s\n' "$_ch4_hits" | head -3 | tr '\n' ' ')"
1680
1680
  else skip scope "channel-count check not run — not a git checkout of Crewforth's source"; fi
1681
1681
  # THE CLIENT SIDE IS STACK-NEUTRAL (3.0.1): the install summary, the help and the install page named the scope as
@@ -3407,6 +3407,46 @@ pass|rm -f file.txt
3407
3407
  pass|rm file.txt
3408
3408
  pass|rmdir empty/
3409
3409
  RMCASES
3410
+ # V4: a stopped or asked command says what it would have touched (eval/lib/preview.sh): the changes a reset discards,
3411
+ # the paths a clean deletes, the files under an rm target inside the project, what a commit takes and a push sends.
3412
+ # Read only, and after the decision: a preview library that fails or ends its shell leaves the decision as it was.
3413
+ PVR="$(mktemp -d)"; PVR="$(cd -P "$PVR" && pwd)"; PVO="$(mktemp)"; PVE="$(mktemp)"; PVX="$(mktemp -d)"
3414
+ ( cd "$PVR" && git init -q . && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m base \
3415
+ && printf '1\n' > a.txt && printf '2\n' > b.txt && git add . && git -c user.email=a@b -c user.name=a commit -q -m two \
3416
+ && printf 'x\n' >> a.txt && printf 'y\n' >> b.txt && mkdir -p out/sub && : > out/sub/1 && : > out/2 && : > junk.log ) >/dev/null 2>&1
3417
+ _pvrun(){ printf '{"session_id":"s","permission_mode":"default","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$PVR" \
3418
+ | ( cd "$PVR" && CLAUDE_PROJECT_DIR="$PVR" CREW_GATE_LOG=/dev/null bash "${2:-$HOOKS}/guard-bash.sh" ) > "$PVO" 2> "$PVE"; _pvrc=$?; }
3419
+ _pvb=""
3420
+ _pvrun "git reset --hard"; { [ "$_pvrc" = 2 ] && grep -q 'Preview: it would discard 2 uncommitted change' "$PVE"; } || _pvb="$_pvb [reset: rc $_pvrc · $(grep Preview "$PVE")]"
3421
+ _pvrun "git reset --hard HEAD~1"; grep -q 'and drop 1 commit(s) from the branch' "$PVE" || _pvb="$_pvb [reset HEAD~1: $(grep Preview "$PVE")]"
3422
+ _pvrun "git clean -fd"; { [ "$_pvrc" = 2 ] && grep -q 'lists 2 path(s) it would delete' "$PVE"; } || _pvb="$_pvb [clean: $(grep Preview "$PVE")]"
3423
+ _pvrun "rm -rf out/"; { [ "$_pvrc" = 2 ] && grep -q 'Preview: out/: 2 file(s)' "$PVE"; } || _pvb="$_pvb [rm inside: $(grep Preview "$PVE")]"
3424
+ _pvrun "rm -rf $PVX/"; { [ "$_pvrc" = 2 ] && grep -q 'outside the project, not counted' "$PVE"; } || _pvb="$_pvb [rm outside: $(grep Preview "$PVE")]"
3425
+ # The asked ones: a commit (past §4.6, with the review record) and a push to a remote of its own.
3426
+ ( cd "$PVR" && git add a.txt && mkdir -p .claude && D=$(git diff --cached --no-ext-diff --no-textconv | git hash-object --stdin) && H=$(git rev-parse HEAD) \
3427
+ && printf '{"diff_oid":"%s","head":"%s","ts":"%s"}\n' "$D" "$H" "2026-01-01T00:00:00Z" > .claude/review-pass.json ) >/dev/null 2>&1
3428
+ _pvrun "git commit -m wip"; { [ "$_pvrc" = 0 ] && grep -q 'Preview: 1 file changed, 1 insertion' "$PVO"; } || _pvb="$_pvb [commit ask: rc $_pvrc · $(grep -o 'Preview[^\\]*' "$PVO")]"
3429
+ ( cd "$PVR" && git init -q --bare "$PVX/r.git" && git remote add origin "$PVX/r.git" && git push -q -u origin HEAD:main && git branch -q -u origin/main \
3430
+ && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m "one ahead" ) >/dev/null 2>&1
3431
+ _pvrun "git push"; { [ "$_pvrc" = 0 ] && grep -q 'Preview: it sends 1 commit(s) to origin/main' "$PVO"; } || _pvb="$_pvb [push ask: rc $_pvrc · $(grep -o 'Preview[^\\]*' "$PVO")]"
3432
+ # A push that names its branch sends to that one: an existing one is counted, an unknown one would be created.
3433
+ _pvrun "git push origin main"; grep -q 'Preview: it sends 1 commit(s) to origin/main' "$PVO" || _pvb="$_pvb [named push: $(grep -o 'Preview[^\\]*' "$PVO")]"
3434
+ _pvrun "git push origin feat/new"; grep -q 'Preview: origin/feat/new is not known here; the push would create it' "$PVO" || _pvb="$_pvb [push to a new branch: $(grep -o 'Preview[^\\]*' "$PVO")]"
3435
+ # A push of another local branch counts that branch's commits, not the current one's (current: 1 ahead; side: 3).
3436
+ ( cd "$PVR" && _cur="$(git rev-parse --abbrev-ref HEAD)" && git checkout -q -b side && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m s1 \
3437
+ && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m s2 && git checkout -q "$_cur" ) >/dev/null 2>&1
3438
+ _pvrun "git push origin side:main"; grep -q 'Preview: it sends 3 commit(s) to origin/main' "$PVO" || _pvb="$_pvb [push of another branch: $(grep -o 'Preview[^\\]*' "$PVO")]"
3439
+ # A pattern is said, not expanded by the hook's own shell into the files it matches.
3440
+ _pvrun "rm -rf out/*"; { [ "$_pvrc" = 2 ] && grep -q 'Preview: out/\*: a pattern, not counted' "$PVE"; } || _pvb="$_pvb [rm pattern: rc $_pvrc · $(grep Preview "$PVE")]"
3441
+ # A broken preview library: one that exits, one that does not parse. The decision stays rc 2 and nothing is said.
3442
+ _pvh="$(mktemp -d)"; cp -R "$HOOKS" "$_pvh/hooks"; mkdir -p "$_pvh/eval"; cp -R "$ROOT/eval/lib" "$_pvh/eval/lib"
3443
+ printf 'crew_preview(){ exit 0; }\n' > "$_pvh/eval/lib/preview.sh"; _pvrun "git reset --hard" "$_pvh/hooks"
3444
+ { [ "$_pvrc" = 2 ] && ! grep -q Preview "$PVE"; } || _pvb="$_pvb [a preview that exits changed the decision: rc $_pvrc]"
3445
+ printf 'crew_preview(){ ( \n' > "$_pvh/eval/lib/preview.sh"; _pvrun "rm -rf out/" "$_pvh/hooks"
3446
+ [ "$_pvrc" = 2 ] || _pvb="$_pvb [a preview that does not parse changed the decision: rc $_pvrc]"
3447
+ [ -z "$_pvb" ] && pass "a stopped command says what it would have touched: reset (2 changes, and 1 commit for HEAD~1), clean (2 paths, dry run), rm (2 files inside the project; a target outside it not counted); an asked commit and push say what they take and send (a push that names a new branch says it would create it, one of another local branch counts that branch's commits); a pattern is said, not expanded; a preview library that exits or does not parse leaves the decision at rc 2" \
3448
+ || fail "risk preview:$_pvb"
3449
+ rm -rf "$PVR" "$PVX" "$_pvh"; rm -f "$PVO" "$PVE"
3410
3450
  # §4.5 whole-tree revert. `reset --hard` was gated from the start; `git checkout -- .` does the same damage and
3411
3451
  # was not — a verification subagent used it over uncommitted work in this repo and the tree went with it. The
3412
3452
  # single-file form stays allowed (everyday, recoverable), and the two ways around a naive matcher — `git -C`
@@ -3625,6 +3665,8 @@ if [ "$_gc1" != 5 ] || [ "$_gc2" != 7 ]; then
3625
3665
  else
3626
3666
  gcj(){ printf '{"tool_name":"%s","permission_mode":"default","tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$2" "$_gcp"; }
3627
3667
  gwj(){ printf '{"tool_name":"Write","permission_mode":"default","tool_input":{"file_path":"%s/%s","content":"x"},"cwd":"%s"}' "$_gcp" "$1" "$_gcp"; }
3668
+ # commit-msg 15 -> 16 (2026-10-18): one more line in trace-blocklist.txt, the session-link trailer that reached
3669
+ # pushed commits; each line is one grep on every commit.
3628
3670
  GCT='guard-bash @@ 0 @@ 0 @@ Bash @@ git status --short
3629
3671
  guard-bash @@ 0 @@ 0 @@ Bash @@ ls -la
3630
3672
  guard-bash @@ 0 @@ 0 @@ Bash @@ git log --oneline -2 && git status --short | head; ls; cat CHANGELOG.md
@@ -3646,7 +3688,7 @@ guard-write @@ 0 @@ 0 @@ Write @@ .env
3646
3688
  guard-write @@ 2 @@ 0 @@ Write @@ .claude/hooks/guard-bash.sh
3647
3689
  guard-powershell @@ 0 @@ 0 @@ PowerShell @@ Get-ChildItem -Recurse src
3648
3690
  pre-commit @@ 0 @@ 22 @@ Script @@ -
3649
- commit-msg @@ 0 @@ 15 @@ Script @@ msg'
3691
+ commit-msg @@ 0 @@ 16 @@ Script @@ msg'
3650
3692
  _gcn=0; _gcbad=""; _gcrow=""
3651
3693
  while IFS= read -r _gl; do [ -z "$_gl" ] && continue
3652
3694
  _gh="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gr="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gb="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gt="${_gl%% @@ *}"; _gc="${_gl#* @@ }"
@@ -4492,6 +4534,41 @@ else fail "session-rehydrate git-hooks states read '$_got' (want '--UO---': no h
4492
4534
  rm -rf "$RHG"
4493
4535
  _rj=0; for _o in "$_o1" "$_o2" "$_o4" "$_o6"; do printf '%s' "$_o" > "$RHD/o.json"; awk -v op=validate -f "$ROOT/eval/lib/settings-json.awk" "$RHD/o.json" 2>/dev/null || _rj=$((_rj+1)); done
4494
4536
  [ "$_rj" = 0 ] && pass "each language/handover output is valid JSON (Crewforth's reader, 4 of 4)" || fail "$_rj session-rehydrate output(s) are not valid JSON"
4537
+ # V3: just before a compaction, precompact-snapshot.sh writes the state of the work from the files and the gates;
4538
+ # after it, session-rehydrate gives that state back, to the same session only. The snapshot never blocks the
4539
+ # compaction (PreCompact exit 2 would): rc 0 and silent in every case, a broken one included.
4540
+ PCS="$HOOKS/precompact-snapshot.sh"
4541
+ if [ ! -x "$PCS" ]; then fail "precompact-snapshot.sh missing/not executable"
4542
+ else
4543
+ PCD="$(mktemp -d)"; PCD="$(cd -P "$PCD" && pwd)"; _pco="$(mktemp)"; _pcr="$(mktemp)" # outside the repository: a file there is uncommitted
4544
+ ( cd "$PCD" && git init -q . && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m base ) >/dev/null 2>&1
4545
+ mkdir -p "$PCD/.claude" "$PCD/.crewforth" "$PCD/docs"
4546
+ printf 'x\n' > "$PCD/plain.txt"; printf 'x\n' > "$PCD/odd\"na me.txt"; printf 'x\n' > "$PCD/ıİşğü.txt"
4547
+ printf '{\n "findings": [\n {\n "rule": "a"\n },\n {\n "rule": "b"\n }\n ]\n}\n' > "$PCD/.crewforth/review.json"
4548
+ printf 'BLOCK\t§4.2\tvendor name\t\n' > "$PCD/.git/crewforth-gate-log.tsv"
4549
+ printf '{"session_id":"s-1","cwd":"%s","hook_event_name":"PreCompact","trigger":"auto","custom_instructions":null}' "$PCD" | CLAUDE_PROJECT_DIR="$PCD" bash "$PCS" > "$_pco" 2>&1; _pcrc=$?
4550
+ _snap="$PCD/.git/crewforth-session-snapshot.json"; _sb=""
4551
+ [ -f "$_snap" ] && IFS= read -r _sb < "$_snap"
4552
+ _rs(){ printf '{"session_id":"%s","source":"%s","cwd":"%s"}' "$1" "$2" "$PCD" | CLAUDE_PROJECT_DIR="$PCD" bash "$HOOKS/session-rehydrate.sh" 2>/dev/null; }
4553
+ _same="$(_rs s-1 compact)"; _other="$(_rs s-2 compact)"; _start="$(_rs s-1 startup)"
4554
+ printf '%s' "$_same" > "$_pcr"
4555
+ _b=""
4556
+ [ "$_pcrc" = 0 ] && [ ! -s "$_pco" ] || _b="$_b [rc $_pcrc, printed: $(head -c 120 "$_pco")]"
4557
+ awk -v op=validate -f "$ROOT/eval/lib/settings-json.awk" "$_snap" 2>/dev/null || _b="$_b [the snapshot is not valid JSON: ${_sb:0:160}]"
4558
+ case "$_sb" in *'"session_id":"s-1"'*'"trigger":"auto"'*'branch '*'4 uncommitted file(s)'*'ıİşğü.txt'*'§4.2 vendor name'*'review 2'*) ;; *) _b="$_b [snapshot: ${_sb:0:200}]" ;; esac
4559
+ case "$_same" in *'Before this context was compacted'*'review 2'*) ;; *) _b="$_b [not given back after compaction: ${_same:0:120}]" ;; esac
4560
+ awk -v op=validate -f "$ROOT/eval/lib/settings-json.awk" "$_pcr" 2>/dev/null || _b="$_b [the SessionStart output with an odd file name is not valid JSON]"
4561
+ case "$_other$_start" in *'Before this context was compacted'*) _b="$_b [given to another session or on a plain start]" ;; esac
4562
+ # Fail-open: no repository, a cwd that is not there, no JSON at all, and the hook alone without eval/lib.
4563
+ _nr="$(mktemp -d)"; _alone="$(mktemp -d)"; cp "$PCS" "$_alone/precompact-snapshot.sh"
4564
+ printf '{"session_id":"s-9","cwd":"%s","trigger":"manual"}' "$_nr" | CLAUDE_PROJECT_DIR="$_nr" bash "$PCS" >/dev/null 2>&1 || _b="$_b [non-repo rc $?]"
4565
+ printf '{"session_id":"s-9","cwd":"/nonexistent/x"}' | CLAUDE_PROJECT_DIR= bash "$PCS" >/dev/null 2>&1 || _b="$_b [missing cwd rc $?]"
4566
+ printf 'not json' | CLAUDE_PROJECT_DIR="$_nr" bash "$PCS" >/dev/null 2>&1 || _b="$_b [garbage rc $?]"
4567
+ printf '{"session_id":"s-9","cwd":"%s"}' "$PCD" | CLAUDE_PROJECT_DIR="$PCD" bash "$_alone/precompact-snapshot.sh" >/dev/null 2>&1 || _b="$_b [no eval/lib rc $?]"
4568
+ [ -z "$_b" ] && pass "PreCompact writes the state of the work (branch, uncommitted files, the last gate refusal, the findings on record) as valid JSON, even for a file name with a quote and a tab, and names a Turkish file name as written; SessionStart gives it back after a compaction to the same session only, never to another or on a plain start; the hook is rc 0 and silent outside a repository, with no cwd, with no JSON and without eval/lib" \
4569
+ || fail "precompact snapshot:$_b"
4570
+ rm -rf "$PCD" "$_nr" "$_alone" "$_pco" "$_pcr"
4571
+ fi
4495
4572
  # Asked of the entry that carries session-rehydrate, not of the file: skill-trust's entry has the same matcher.
4496
4573
  _rw(){ _n="$(awk -v op=len -v path=hooks.SessionStart -f "$ROOT/eval/lib/settings-json.awk" "$1" 2>/dev/null)"; _i=0
4497
4574
  while [ "$_i" -lt "${_n:-0}" ]; do _e="$(awk -v op=get -v path="hooks.SessionStart.$_i" -f "$ROOT/eval/lib/settings-json.awk" "$1" 2>/dev/null)"
@@ -4971,6 +5048,8 @@ sec "== 7u) update prompt: ASKS about a release, never installs one, never spend
4971
5048
  UPD="$(mktemp -d)"; mkdir -p "$UPD/.claude/.state" "$UPD/.claude/hooks"
4972
5049
  printf '3.0.0\n' > "$UPD/.claude/VERSION"
4973
5050
  UH="$HOOKS/session-update-check.sh"; cp "$UH" "$UPD/.claude/hooks/"
5051
+ # The hook reads and orders the install's version through eval/lib/version.sh, beside it in an install.
5052
+ mkdir -p "$UPD/.claude/eval/lib"; cp "$ROOT/eval/lib/version.sh" "$UPD/.claude/eval/lib/"
4974
5053
  uc(){ ( printf '{"hook_event_name":"SessionStart","source":"startup","cwd":"%s"}' "$UPD" \
4975
5054
  | env -u CI -u CREW_NO_UPDATE_CHECK -u CREW_LANG CLAUDE_CODE_SESSION_ATTENDED=1 LANG=C LC_ALL= LC_MESSAGES= \
4976
5055
  CLAUDE_PROJECT_DIR="$UPD" CREW_UPDATE_URL="${1:-http://10.255.255.1/blackhole}" bash "$UH" 2>/dev/null ); }
@@ -5099,10 +5178,25 @@ cp "$UH" "$UPD/uh-twin.sh"; printf 'npx --yes crewforth@latest update --here --y
5099
5178
  if [ "$_ni" = 0 ] && [ "$(_noinst "$UPD/uh-twin.sh")" -ge 1 ]; then pass "the update hook runs no installer (npx, npm, adopt.sh, start.sh): 0 lines; the twin with one is caught"
5100
5179
  else fail "the update hook contains an installer call ($_ni line(s)) — it must only ask, or the twin was not caught"; fi
5101
5180
 
5181
+ # 12b) AN INSTALLED RC IS OFFERED ITS FINAL. The installer copies VERSION into the project, and an rc's says
5182
+ # X.Y.Z-rc.N: 3.2.0 published over an installed 3.2.0-rc.1 is newer (it used to read as the same 3.2.0, and the
5183
+ # rc's user was never told the final shipped). An older final is not offered, and what the network answers is
5184
+ # still held to X.Y.Z: an rc off the network is no version.
5185
+ printf '3.2.0-rc.1\n' > "$UPD/.claude/VERSION"
5186
+ ucache 3.2.0
5187
+ case "$(uc)" in *'"Crewforth v3.2.0 is out (installed: v3.2.0-rc.1). Update now?"'*) pass "an installed 3.2.0-rc.1 is offered the final 3.2.0" ;;
5188
+ *) fail "an installed 3.2.0-rc.1 was not offered the final 3.2.0 (got: $(uc | head -c 120))" ;; esac
5189
+ ucache 3.1.1
5190
+ [ -z "$(uc)" ] && pass "an installed rc is not offered an older final (3.1.1 under 3.2.0-rc.1)" || fail "3.1.1 was offered over an installed 3.2.0-rc.1"
5191
+ ucache 3.2.0-rc.2
5192
+ [ -z "$(uc)" ] && pass "an rc answered off the network is no version: nothing asked" || fail "a pre-release from the network was put in a question"
5193
+ printf '3.0.0\n' > "$UPD/.claude/VERSION"; ustate
5194
+
5102
5195
  # 13) THE PLUGIN EDITION. Its version is its own manifest, its release is the marketplace repo's copy, its state is
5103
5196
  # user-level (a plugin serves every project), and its Update path is the plugin command — measured to work from
5104
5197
  # inside a session, applying on restart.
5105
- PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks"; cp "$UH" "$PLG/hooks/"
5198
+ PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks" "$PLG/eval/lib"; cp "$UH" "$PLG/hooks/"
5199
+ cp "$ROOT/eval/lib/version.sh" "$PLG/eval/lib/"
5106
5200
  printf '{"name":"crewforth","version":"3.0.0"}\n' > "$PLG/.claude-plugin/plugin.json"
5107
5201
  XDG="$UPD/xdg"; mkdir -p "$XDG/crewforth"
5108
5202
  printf '3.1.0 %s\n' "$(date +%s)" > "$XDG/crewforth/update-check"
@@ -5478,6 +5572,20 @@ $TRFX\""; then pass "multi-line AI trace in the commit message BLOCKED (§4.1)"
5478
5572
  if csblk "git commit -F $MFX"; then pass "-F <file> carrying an AI trace BLOCKED (§4.1)"
5479
5573
  else fail "-F <file> AI trace not blocked with rc=2 (§4.1 hole or the hook died)"; fi
5480
5574
  rm -f "$MFX"
5575
+ # Both halves in ONE refusal: a staged secret and a session-link trailer in the message. The message used to be
5576
+ # scanned only when the staged content was clean, so the refusal named the first problem and the commit was
5577
+ # retried with the trailer still in it (a real project, 2026-10-21). Assembled at run time, as above.
5578
+ STFX="Claude-""Session: https://claude.ai/code/""session_""0123456789abcdef"
5579
+ ( cd "$CS" && printf 'k = "%s"\n' "$SKFX" > c.txt && git add c.txt ) >/dev/null 2>&1
5580
+ _cso="$( cd "$CS" && printf '%s' "$(csj "git commit -m \"feat: z
5581
+
5582
+ $STFX\"")" | bash .claude/hooks/guard-commit-scan.sh 2>&1 >/dev/null )"; _csr=$?
5583
+ ( cd "$CS" && git rm -q --cached c.txt && rm -f c.txt ) >/dev/null 2>&1
5584
+ case "$_cso" in *SECRET-SCANNER*'TRACE-SCANNER (message)'*'does not override'*)
5585
+ [ "$_csr" = 2 ] && pass "a staged secret and a trailer in the message are named in ONE refusal, with the harness-reminder line (§4.1)" \
5586
+ || fail "both problems named but rc=$_csr, not 2" ;;
5587
+ *) fail "the refusal did not name both the staged secret and the message trailer (rc=$_csr): $(printf '%s' "$_cso" | tr '\n' ' ' | cut -c1-200)" ;;
5588
+ esac
5481
5589
  # A QUOTED -F path with a space. The sed extraction that ran wherever python3 did not stopped at the space, read
5482
5590
  # `-F "my msg.txt"` as `my`, found no such file and refused the commit — a clean one. The awk tokenizer is now
5483
5591
  # the only path; both directions are asserted so an extraction that reads NOTHING cannot pass as "clean".
@@ -6363,6 +6471,14 @@ GNI="$(mktemp -d)"; ( cd "$GNI" && git init -q . && mkdir -p .claude )
6363
6471
  ( cd "$GNI" && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
6364
6472
  [ -e "$GNI/.claude/gate-log.tsv" ] && fail "gate log wrote into a repo where the path is not gitignored" \
6365
6473
  || pass "gate log declines a path git would track"
6474
+ # ...and since 3.2.0 writes inside the repository's .git instead, which is never committed: a project that tracks
6475
+ # .claude/ had no record of a refusal at all (seen on a real project: no 🛡, no note of a lowered model).
6476
+ # The readers find it there (eval/lib/gate-log.sh, the one rule), and `git status` stays clean.
6477
+ _gni_read="$( cd "$GNI" && . "$ROOT/eval/lib/gate-log.sh" && crew_gate_log_read && printf '%s' "$_CREW_GL" )"
6478
+ _gni_st="$( cd "$GNI" && git status --porcelain --untracked-files=all 2>/dev/null | grep -c gate-log )"
6479
+ { [ -s "$GNI/.git/crewforth-gate-log.tsv" ] && grep -q '^BLOCK' "$GNI/.git/crewforth-gate-log.tsv" && [ "$_gni_st" = 0 ] && [ "$_gni_read" = ./.git/crewforth-gate-log.tsv ]; } \
6480
+ && pass "where git would track it, the gate log goes inside .git: written, found by the readers, and git status stays clean" \
6481
+ || fail "gate log inside .git: file $([ -s "$GNI/.git/crewforth-gate-log.tsv" ] && echo written || echo MISSING), git status shows it ${_gni_st}x, reader found '${_gni_read}'"
6366
6482
  ( cd "$GNI" && echo '.claude/' > .gitignore && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
6367
6483
  [ -s "$GNI/.claude/gate-log.tsv" ] && pass "gate log writes once the path is gitignored" \
6368
6484
  || fail "gate log stayed silent even though the path is ignored"
@@ -7587,6 +7703,25 @@ _pa_say auto 'approve: push' >/dev/null; ( cd "$_paw" && git checkout -q -b othe
7587
7703
  _pa_run auto 'git push origin feat/x'; [ "$_par" = 2 ] || _pabad="$_pabad other-branch-checked-out:rc=$_par"
7588
7704
  [ -z "$_pabad" ] && pass "a push approval is for the commit HEAD was on and the branch that was checked out: one more commit, or another branch, and it is refused" \
7589
7705
  || fail "push binding:$_pabad"
7706
+ # A tag on the approved commit is part of that push (a release: `/crew-approve push`, then `git push origin v2.0.0`
7707
+ # was refused on a real project). A tag elsewhere, a name that is also a branch, or a full ref is not.
7708
+ _pa_new; _pabad=""; ( cd "$_paw" && git commit -qm second && git tag old HEAD^ && git tag v1 && git tag -a v1a -m v1a && git tag both && git branch both ) >/dev/null 2>&1
7709
+ _pa_say auto 'approve: commit' >/dev/null
7710
+ _pa_run auto 'git push origin v1'; [ "$_par" = 2 ] || _pabad="$_pabad [a commit approval pushed a tag: rc=$_par]"
7711
+ _pa_say auto 'approve: push' >/dev/null
7712
+ for _pc in 'git push origin v1' 'git push origin v1a' 'git push -q origin v1'; do
7713
+ _pa_run auto "$_pc"; [ "$_par" = 0 ] || _pabad="$_pabad [$_pc → $_par $(head -c 120 "$_PA/err")]"
7714
+ done
7715
+ _pa_run auto 'git push origin old'; { [ "$_par" = 2 ] && grep -q 'the tag old is on' "$_PA/err"; } || _pabad="$_pabad [a tag on another commit → $_par]"
7716
+ for _pc in 'git push origin both' 'git push origin refs/tags/v1' 'git push origin nosuch' 'git push origin v1:v2' 'git push other v1'; do
7717
+ _pa_run auto "$_pc"; [ "$_par" = 2 ] || _pabad="$_pabad [$_pc → $_par]"
7718
+ done
7719
+ _pa_new; ( cd "$_paw" && git tag before ) >/dev/null 2>&1; _pa_say auto 'approve: commit+push' >/dev/null
7720
+ _pa_run auto 'git commit -m x' >/dev/null; ( cd "$_paw" && git commit -qm approved && git tag after ) >/dev/null 2>&1
7721
+ _pa_run auto 'git push origin after'; [ "$_par" = 0 ] || _pabad="$_pabad [commit+push: a tag on the new commit → $_par $(head -c 120 "$_PA/err")]"
7722
+ _pa_run auto 'git push origin before'; { [ "$_par" = 2 ] && grep -q 'the tag before is on' "$_PA/err"; } || _pabad="$_pabad [commit+push: a tag on the old commit → $_par]"
7723
+ [ -z "$_pabad" ] && pass "a push approval covers a tag on the approved commit (lightweight or annotated; after commit+push, on the new commit): a tag on another commit, a name that is also a branch, a full ref, a refspec, another remote or a commit-only approval is refused" \
7724
+ || fail "tag push:$_pabad"
7590
7725
 
7591
7726
  # ---- commit+push: the push is for the commit the approved diff became -----------------------------------------
7592
7727
  _pa_new; _pabad=""
@@ -7892,8 +8027,8 @@ _paq_text="$(bash -c '. "$1"; printf "%s" "$_CREW_Q_TEXT"' _ "$HOOKS/prompt-appr
7892
8027
  _paq_pre(){ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"%s","hook_event_name":"PreToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s]%s},"tool_use_id":"%s"}' "$_patr" "$_paw" "$1" "$2" "${3:-}" "${4:-tu1}" > "$_PA/q.json"
7893
8028
  if [ -n "$JSONQ" ]; then json_ok < "$_PA/q.json" || _paqbad="$_paqbad [pre payload is not JSON]"; fi
7894
8029
  _pao="$( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV ${_PAQ_X:-} bash "$HOOKS/guard-question.sh" < "$_PA/q.json" 2>"$_PA/qerr" )"; _par=$?; }
7895
- # $1 = mode, $2 = the answer, $3 = tool_use_id
7896
- _paq_post(){ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"%s","hook_event_name":"PostToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s],"answers":{"%s":"%s"}},"tool_response":{"questions":[%s],"answers":{"%s":"%s"}},"tool_use_id":"%s"}' "$_patr" "$_paw" "$1" "$_paq_canon" "$_paq_text" "$2" "$_paq_canon" "$_paq_text" "$2" "${3:-tu1}" > "$_PA/q.json"
8030
+ # $1 = mode, $2 = the answer, $3 = tool_use_id, $4 = the questions array's content as Claude Code hands it back (default: as asked)
8031
+ _paq_post(){ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"%s","hook_event_name":"PostToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s],"answers":{"%s":"%s"}},"tool_response":{"questions":[%s],"answers":{"%s":"%s"}},"tool_use_id":"%s"}' "$_patr" "$_paw" "$1" "${4:-$_paq_canon}" "$_paq_text" "$2" "${4:-$_paq_canon}" "$_paq_text" "$2" "${3:-tu1}" > "$_PA/q.json"
7897
8032
  if [ -n "$JSONQ" ]; then json_ok < "$_PA/q.json" || _paqbad="$_paqbad [post payload is not JSON]"; fi
7898
8033
  _pao="$( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV ${_PAQ_X:-} bash "$HOOKS/prompt-approval.sh" < "$_PA/q.json" 2>"$_PA/qerr" )"; _par=$?; }
7899
8034
  _paq_gate(){ printf '{"session_id":"s","cwd":"%s","permission_mode":"%s","tool_name":"Bash","tool_input":{"command":"%s"}}' "$_paw" "$1" "$2" > "$_PA/pl.json"
@@ -7906,6 +8041,11 @@ _paq_is(){ _paqn=$((_paqn+1)); [ "$2" = "$3" ] || _paqbad="$_paqbad [$1: got '$2
7906
8041
  _paq_post auto 'Commit only'; _paq_is 'its answer is recorded' "$(_paq_rec)" commit
7907
8042
  _paq_gate auto 'git commit -m x'; _paq_is 'the recorded click allows the commit' "$_par" 0
7908
8043
  _pa_new; : > "$_pa_rec"; _paq_pre auto "$_paq_canon" '' tu2; _paq_post auto 'Commit + push' tu2; _paq_is 'commit + push' "$(_paq_rec)" commit+push
8044
+ # Claude Code may hand the answered question back with its keys sorted and multiSelect gone: 7 of 7 such answers were
8045
+ # refused on a byte match (real `auto` sessions, 2026-10-18). This is that shape, as it was handed back.
8046
+ _paq_sorted="{\"header\":\"Approval\",\"options\":[{\"description\":\"Commit exactly what is staged now. No push.\",\"label\":\"Commit only\"},{\"description\":\"Commit what is staged now, then push that commit on this branch.\",\"label\":\"Commit + push\"},{\"description\":\"Push this branch as it is now. No new commit.\",\"label\":\"Push only\"},{\"description\":\"Neither commit nor push.\",\"label\":\"No\"}],\"question\":\"$_paq_text\"}"
8047
+ _pa_new; : > "$_pa_rec"; _paq_pre auto "$_paq_canon" '' tu10; _paq_post auto 'Commit + push' tu10 "$_paq_sorted"
8048
+ _paq_is 'the answered question handed back with its keys sorted' "$(_paq_rec)" commit+push
7909
8049
  # must-fail twins: each differs from the round trip in one thing
7910
8050
  _pa_new; : > "$_pa_rec"; _paq_post auto 'Commit only' tu3; _paq_is 'an answer no PreToolUse asked for' "$(_paq_rec)" none
7911
8051
  _paq_pre auto "$_paq_canon" '' tu4; _paq_post auto 'Commit only' tu5; _paq_is 'an answer to another call' "$(_paq_rec)" none
@@ -7914,6 +8054,8 @@ _paq_pre auto "$_paq_canon" '' tu6; _paq_post auto 'Commit only' tu6; : > "$_pa_
7914
8054
  _paq_pre auto "$_paq_canon" '' tu7; _paq_post auto 'HOOK-CHOSE-Commit only' tu7; _paq_is 'an answer that is none of the four' "$(_paq_rec)" none
7915
8055
  _paq_pre auto "$_paq_canon" '' tu8; _paq_post auto 'No' tu8; _paq_is 'No' "$(_paq_rec)" none
7916
8056
  _paq_pre auto "$_paq_canon" '' tu9; _paq_post default 'Commit only' tu9; _paq_is 'an answer recorded outside auto' "$(_paq_rec)" none
8057
+ _paq_pre auto "$_paq_canon" '' tu11; _paq_post auto 'Commit only' tu11 "${_paq_sorted/No new commit./Then force-push.}"
8058
+ _paq_is 'sorted, with one text of the question changed' "$(_paq_rec)" none
7917
8059
  _paq_pre auto '{"question":"Crewforth approval: commit?","header":"Approval","options":[{"label":"Commit only","description":"x"}],"multiSelect":false}'
7918
8060
  _paq_is 'another wording is refused' "$_par" 2
7919
8061
  _paq_pre auto "$_paq_canon" ',"answers":{"x":"Commit only"}'; _paq_is 'a call with answers is refused' "$_par" 2
@@ -7924,9 +8066,93 @@ _paq_pre bypassPermissions "$_paq_canon"; _paq_is 'bypassPermiss
7924
8066
  _PAQ_X="CLAUDE_CODE_SESSION_ATTENDED=0"; _paq_pre auto "$_paq_canon"; unset _PAQ_X; _paq_is 'a headless session: refused' "$_par" 2
7925
8067
  _paq_pre auto '{"question":"Which colour?","header":"Colour","options":[{"label":"Red","description":"r"}],"multiSelect":false}'
7926
8068
  _paq_is 'any other question: no decision' "$_par/$_pao" "0/"
7927
- if [ "$_paqn" != 18 ]; then fail "FIXTURE: the approval-question table ran $_paqn cases, not 18"
7928
- elif [ -z "$_paqbad" ]; then pass "the approval question: asked (ask) only as the fixed text, in auto, attended, with no answers; its answer recorded only for the call that was asked, once, and only as one of the four; the record allows the commit (18 cases, 13 must-fail)"
8069
+ if [ "$_paqn" != 20 ]; then fail "FIXTURE: the approval-question table ran $_paqn cases, not 20"
8070
+ elif [ -z "$_paqbad" ]; then pass "the approval question: asked (ask) only as the fixed text, in auto, attended, with no answers; its answer recorded only for the call that was asked, once, and only as one of the four, in whatever key order Claude Code hands it back; the record allows the commit (20 cases, 14 must-fail)"
7929
8071
  else fail "the approval question:$_paqbad"; fi
8072
+ # ...and the way to it. B-2 (a real `auto` session, 2026-10-17): /crew-ship asked for /crew-approve before any commit
8073
+ # was refused, so the refusal that prints the question never came and the click was never offered; telling the agent
8074
+ # to try the commit first did not help either (§4.4 says not to, and it did not). agent-outcome.sh now hands the
8075
+ # question over when crew-commit-agent is called; the text pinned here only says to ask it.
8076
+ _cqa="$AGENTS/crew-commit-agent.md"; _cqs="$SKILLS/crew-ship/SKILL.md"
8077
+ if [ ! -f "$_cqa" ] || [ ! -f "$_cqs" ]; then skip scope "the way to the approval question: crew-commit-agent or crew-ship is not installed here"
8078
+ elif grep -q 'question (below): ask it, with the message, instead of' "$_cqa" && grep -q 'one-click approval question when crew-commit-agent is called' "$_cqs"; then
8079
+ pass "in auto the commit agent and /crew-ship ask the one-click question Crewforth hands over, before asking for /crew-approve"
8080
+ else fail "the commit agent or /crew-ship asks for /crew-approve before the one-click question Crewforth hands over (B-2)"; fi
8081
+ # The hand-over itself, run: agent-outcome.sh on PostToolUse of an Agent call. In `auto`, for crew-commit-agent, with
8082
+ # both halves of the question wired and no PermissionRequest hook, it adds the fixed question (_CREW_Q_JSON, read from
8083
+ # prompt-approval.sh) to the session's context; in any other mode, for any other agent, or with such a hook, nothing.
8084
+ _aq="$(mktemp -d)"; _aq="$(cd -P "$_aq" && pwd)"; mkdir -p "$_aq/p/.claude/state" "$_aq/ccd" "$_aq/home"
8085
+ printf '%s' '{"hooks":{"PreToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/guard-question.sh"}]}],"PostToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}]}}' > "$_aq/p/.claude/settings.json"
8086
+ _aq_run(){ printf '{"hook_event_name":"PostToolUse","tool_name":"Agent","permission_mode":"%s","session_id":"s","tool_use_id":"toolu_aq1","cwd":"%s","tool_input":{"subagent_type":"%s","description":"d","prompt":"p"},"tool_response":{"status":"async_launched","agentId":"aq123abc"}}' "$1" "$_aq/p" "$2" > "$_aq/pl.json"
8087
+ ( cd "$_aq/p" && env -u CREW_MODEL_ROUTING CLAUDE_PROJECT_DIR="$_aq/p" CLAUDE_CONFIG_DIR="$_aq/ccd" HOME="$_aq/home" CREW_GATE_LOG=/dev/null bash "$HOOKS/agent-outcome.sh" < "$_aq/pl.json" 2>/dev/null ); }
8088
+ _aqbad=""; _aqo="$(_aq_run auto crew-commit-agent)"
8089
+ case "$_aqo" in *"$_paq_text"*) ;; *) _aqbad="$_aqbad [auto, crew-commit-agent: the question was not handed over]" ;; esac
8090
+ if [ -n "$JSONQ" ] && [ -n "$_aqo" ]; then printf '%s' "$_aqo" | json_ok || _aqbad="$_aqbad [its output is not JSON]"; fi
8091
+ for _aqc in default:crew-commit-agent dontAsk:crew-commit-agent acceptEdits:crew-commit-agent auto:crew-review-agent; do
8092
+ [ -z "$(_aq_run "${_aqc%%:*}" "${_aqc#*:}")" ] || _aqbad="$_aqbad [${_aqc%%:*}, ${_aqc#*:}: handed the question over]"
8093
+ done
8094
+ printf '%s' '{"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"x"}]}]}}' > "$_aq/ccd/settings.json"
8095
+ [ -z "$(_aq_run auto crew-commit-agent)" ] || _aqbad="$_aqbad [a PermissionRequest hook is wired: the question was still handed over]"
8096
+ rm -rf "$_aq"
8097
+ [ -z "$_aqbad" ] && pass "in auto, calling crew-commit-agent hands the session the fixed one-click question; not in default, acceptEdits or dontAsk, not for another agent, not with a PermissionRequest hook" \
8098
+ || fail "the one-click question hand-over:$_aqbad"
8099
+ # THE CLICK BEFORE THE TYPED WAY, at the turn's end (2026-10-18: 9 of 18 typed asks in two real `auto` sessions on rc.4
8100
+ # came in a turn where no commit was tried, so neither the refusal nor the commit agent's hand-over offered the click).
8101
+ # prompt-approval.sh on Stop: the reply names /crew-approve, auto, the question open, not asked since the user's last
8102
+ # message -> the session is told once to ask the click. Every other case is a twin that differs in one thing.
8103
+ _sq="$(mktemp -d)"; _sq="$(cd -P "$_sq" && pwd)"; mkdir -p "$_sq/p/.claude/state" "$_sq/ccd" "$_sq/home"
8104
+ printf '%s' '{"hooks":{"PreToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/guard-question.sh"}]}],"PostToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}]}}' > "$_sq/p/.claude/settings.json"
8105
+ _SQ_ENV="CLAUDE_PROJECT_DIR=$_sq/p CLAUDE_CONFIG_DIR=$_sq/ccd HOME=$_sq/home CREW_GATE_LOG=/dev/null"
8106
+ # $1 = mode, $2 = the reply, $3 = stop_hook_active, $4 = session id
8107
+ _sq_stop(){ printf '{"session_id":"%s","transcript_path":"%s/t.jsonl","cwd":"%s","permission_mode":"%s","hook_event_name":"Stop","stop_hook_active":%s,"last_assistant_message":"%s"}' "${4:-s1}" "$_sq" "$_sq/p" "$1" "${3:-false}" "$2" > "$_sq/pl.json"
8108
+ ( cd "$_sq/p" && env $_SQ_ENV bash "$HOOKS/prompt-approval.sh" < "$_sq/pl.json" 2>/dev/null ); }
8109
+ _sq_ask='Review is clean. If you approve, send only this:\n/crew-approve commit+push'
8110
+ _sqbad=""; _sqn=0
8111
+ _sq_says(){ _sqn=$((_sqn+1)); case "$2" in *'ASK THE USER WITH ONE CLICK'*"$_paq_text"*) ;; *) _sqbad="$_sqbad [$1: the click was not asked for]" ;; esac
8112
+ if [ -n "$JSONQ" ]; then printf '%s' "$2" | json_ok || _sqbad="$_sqbad [$1: the output is not JSON]"; fi; }
8113
+ _sq_quiet(){ _sqn=$((_sqn+1)); [ -z "$2" ] || _sqbad="$_sqbad [$1: said something: ${2:0:80}]"; }
8114
+ _sq_says 'auto, a typed ask, not asked this turn' "$(_sq_stop auto "$_sq_ask")"
8115
+ _sq_quiet 'stop_hook_active' "$(_sq_stop auto "$_sq_ask" true)"
8116
+ _sq_quiet 'dontAsk' "$(_sq_stop dontAsk "$_sq_ask")"
8117
+ _sq_quiet 'default' "$(_sq_stop default "$_sq_ask")"
8118
+ _sq_quiet 'a reply that names no /crew-approve' "$(_sq_stop auto 'Done: the tests pass.')"
8119
+ # asked this turn: the PreToolUse half writes it down, even when it refuses the question
8120
+ printf '{"session_id":"s1","cwd":"%s","permission_mode":"dontAsk","hook_event_name":"PreToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s]},"tool_use_id":"tq1"}' "$_sq/p" "$_paq_canon" > "$_sq/pre.json"
8121
+ ( cd "$_sq/p" && env $_SQ_ENV bash "$HOOKS/guard-question.sh" < "$_sq/pre.json" >/dev/null 2>&1 ); _sqr=$?
8122
+ [ "$_sqr" = 2 ] || _sqbad="$_sqbad [FIXTURE: the dontAsk question was not refused (rc $_sqr), so 'refused but asked' is not what runs]"
8123
+ _sq_quiet 'asked this turn (and refused)' "$(_sq_stop auto "$_sq_ask")"
8124
+ _sq_says 'asked in another session' "$(_sq_stop auto "$_sq_ask" false s2)"
8125
+ # the user's next message starts a new turn
8126
+ printf '{"session_id":"s1","cwd":"%s","permission_mode":"auto","hook_event_name":"UserPromptSubmit","prompt":"next"}' "$_sq/p" > "$_sq/ups.json"
8127
+ ( cd "$_sq/p" && env $_SQ_ENV bash "$HOOKS/prompt-approval.sh" < "$_sq/ups.json" >/dev/null 2>&1 )
8128
+ _sq_says 'the user wrote since it was asked' "$(_sq_stop auto "$_sq_ask")"
8129
+ printf '%s' '{"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"x"}]}]}}' > "$_sq/ccd/settings.json"
8130
+ _sq_quiet 'a PermissionRequest hook is wired' "$(_sq_stop auto "$_sq_ask")"
8131
+ rm -rf "$_sq"
8132
+ # A Stop is not the user's next message: an approval recorded by a click survives the turn's end, even when the reply
8133
+ # quotes the tool's name (which sends the payload past the AskUserQuestion test).
8134
+ _pa_new; : > "$_pa_rec"; _paq_pre auto "$_paq_canon" '' ts1; _paq_post auto 'Commit only' ts1
8135
+ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"auto","hook_event_name":"Stop","stop_hook_active":false,"last_assistant_message":"I asked with \\"AskUserQuestion\\"; run /crew-approve if not."}' "$_patr" "$_paw" > "$_PA/st.json"
8136
+ ( cd "$_paw" && env $_PAQ_ENV bash "$HOOKS/prompt-approval.sh" < "$_PA/st.json" >/dev/null 2>&1 )
8137
+ _sqn=$((_sqn+1)); [ "$(_paq_rec)" = commit ] || _sqbad="$_sqbad [a Stop ended the recorded approval: '$(_paq_rec)']"
8138
+ if [ "$_sqn" != 10 ]; then fail "FIXTURE: the Stop-half table ran $_sqn cases, not 10"
8139
+ elif [ -z "$_sqbad" ]; then pass "at the turn's end a reply asking for /crew-approve, in auto with the question open and not asked since the user's last message, is sent back once to ask the click; not after a Stop continuation, outside auto, without the command, once asked (even refused), or with a PermissionRequest hook; a Stop leaves a recorded approval alone (10 cases, 6 must-stay-quiet)"
8140
+ else fail "the Stop half of the approval question:$_sqbad"; fi
8141
+ # ONE WAY TO ASK in the refusal (2026-10-18, a real `auto` session on rc.3: the refusal offered the question and the
8142
+ # typed command as equals, and the session told the user to type 7 times in 9). With the question open the typed way is
8143
+ # printed only as the fallback for a question that cannot be asked; with it closed (dontAsk), only the typed way.
8144
+ _pa_new
8145
+ ( cd "$_paw" && printf '%s' '{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}],"PreToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/guard-question.sh"}]}],"PostToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}]}}' > .claude/settings.json )
8146
+ _oabad=""
8147
+ _paq_gate auto 'git commit -m x'
8148
+ case "$_pae" in *'"questions":['*'do not also give the user a command to type.'*'ONLY IF THAT QUESTION CANNOT BE ASKED'*) ;; *) _oabad="$_oabad [auto, question open: not the click first and the typed way as its fallback]" ;; esac
8149
+ case "$_pae" in *'ASK THE USER NOW'*|*'the way below works as well'*) _oabad="$_oabad [auto, question open: the typed way is still offered as an equal]" ;; esac
8150
+ _paq_gate dontAsk 'git commit -m x'
8151
+ case "$_pae" in *'ASK THE USER NOW'*'/crew-approve'*) ;; *) _oabad="$_oabad [dontAsk: the typed way is not asked for]" ;; esac
8152
+ case "$_pae" in *'"questions":['*|*'ONLY IF THAT QUESTION'*) _oabad="$_oabad [dontAsk: the question is offered]" ;; esac
8153
+ _pa_new
8154
+ [ -z "$_oabad" ] && pass "a refused commit in auto offers the click and only falls back to the typed /crew-approve when the question cannot be asked; in dontAsk only the typed way" \
8155
+ || fail "one way to ask:$_oabad"
7930
8156
 
7931
8157
  # A PermissionRequest HOOK, wherever this can read one, closes the question AND the gate's own prompt. Each source is
7932
8158
  # planted alone in an otherwise clean fixture; the clean fixture itself must answer "none" (calibration).
@@ -8218,7 +8444,7 @@ _v2new(){ rm -rf "$_v2p" "$_v2/home"; mkdir -p "$_v2/home" "$_v2p/.claude" "$_v2
8218
8444
  _v2e(){ env -u CREW_MODEL_ROUTING -u CREW_ALLOW_FABLE -u CREW_MODEL_CAL_N -u CREW_MODEL_CAL_PCT -u CREW_VERIFY_NO_SETSID -u CREW_VERIFY_TIMEOUT -u CLAUDE_CONFIG_DIR -u CREW_MANAGED_SETTINGS HOME="$_v2/home" CLAUDE_PROJECT_DIR="$_v2p" CREW_GATE_LOG=/dev/null ${_V2ENV:-} "$@"; }
8219
8445
  # $1 agent, $2 model, $3 files, $4 change, $5 verify, $6 extra fields -> _v2rc, stderr in $_v2/err
8220
8446
  _v2call(){ printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"description":"d","prompt":"files: %s\\nchange: %s\\nverify: %s\\n\\nDo the work.","subagent_type":"%s","model":"%s"%s},"tool_use_id":"tu%s"}' "$3" "$4" "$5" "$1" "$2" "${6:-}" "$RANDOM$RANDOM" > "$_v2/c.json"
8221
- ( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/c.json" >/dev/null 2>"$_v2/err" ); _v2rc=$?; }
8447
+ ( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/c.json" >"$_v2/out" 2>"$_v2/err" ); _v2rc=$?; }
8222
8448
  _v2start(){ printf '{"session_id":"s","hook_event_name":"SubagentStart","agent_id":"%s","agent_type":"%s"}' "$1" "${2:-crew-backend-expert}" | ( cd "$_v2p" && _v2e bash "$_V2O" >/dev/null 2>&1 ); }
8223
8449
  _v2stop(){ printf '{"session_id":"s","permission_mode":"%s","hook_event_name":"SubagentStop","agent_id":"%s","agent_type":"x","stop_hook_active":false,"agent_transcript_path":"%s/none.jsonl","last_assistant_message":"did it\\n%s"}' "${_V2PM:-auto}" "$1" "$_v2" "${2:-confidence: high}" | ( cd "$_v2p" && _v2e bash "$_V2O" 2>/dev/null ); }
8224
8450
  _v2post(){ printf '{"session_id":"s","hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"p"},"tool_response":{"status":"completed","agentId":"%s","content":"x"}}' "$1" | ( cd "$_v2p" && _v2e bash "$_V2O" 2>/dev/null ); }
@@ -8251,21 +8477,26 @@ done <<'V2'
8251
8477
  2|crew-backend-expert|sonnet|prisma/schema.prisma|feature|none|
8252
8478
  2|crew-backend-expert|sonnet|config/SECRETS_local.env|text|none|
8253
8479
  0|crew-backend-expert|sonnet|docs/author.md, src/cryptography/hash.ts|feature|none|
8254
- 2|crew-backend-expert|sonnet|legal/terms-of-service.md|text|none|
8255
- 2|crew-backend-expert|sonnet|docs/privacy-policy.md|text|none|
8480
+ 0|crew-backend-expert|sonnet|legal/terms-of-service.md|text|none|
8481
+ 0|crew-backend-expert|sonnet|docs/privacy-policy.md|text|none|
8256
8482
  2|crew-backend-expert|sonnet|src/PrivacyController.cs|feature|none|
8257
8483
  2|crew-backend-expert|sonnet|src/consent/banner.ts|feature|none|
8258
- 2|crew-backend-expert|sonnet|content/terms/index.md|text|none|
8259
- 2|crew-backend-expert|sonnet|fastlane/metadata/en-US/description.txt|text|none|
8260
- 2|crew-backend-expert|sonnet|store/listing.md|text|none|
8261
- 2|crew-backend-expert|sonnet|metadata/android/en-US/full_description.txt|text|none|
8484
+ 0|crew-backend-expert|sonnet|content/terms/index.md|text|none|
8485
+ 0|crew-backend-expert|sonnet|fastlane/metadata/en-US/description.txt|text|none|
8486
+ 0|crew-backend-expert|sonnet|store/listing.md|text|none|
8487
+ 0|crew-backend-expert|sonnet|metadata/android/en-US/full_description.txt|text|none|
8262
8488
  0|crew-backend-expert|opus|legal/terms-of-service.md|text|none|
8489
+ 2|crew-backend-expert|haiku|legal/terms-of-service.md|text|none|
8490
+ 2|crew-backend-expert|sonnet|legal/terms-of-service.md, src/legal/consent.ts|text|none|
8491
+ 2|crew-backend-expert|sonnet|legal/terms-of-service.md|feature|none|
8263
8492
  0|crew-backend-expert|sonnet|src/searchTerms.ts|feature|none|
8264
8493
  0|crew-backend-expert|sonnet|src/store/index.ts|feature|none|
8265
8494
  0|crew-backend-expert|sonnet|fastlane/metadata/en-US/images/icon.png|text|none|
8266
8495
  0|crew-backend-expert|sonnet|docs/legalese.md|text|none|
8267
8496
  0|crew-backend-expert|sonnet|src/ui/a.ts|feature|none|
8268
- 2|crew-backend-expert|haiku|src/ui/a.ts|text|none|
8497
+ 0|crew-backend-expert|haiku|src/ui/a.ts|text|none|
8498
+ 2|crew-backend-expert|haiku|src/ui/a.ts|fix-known|none|
8499
+ 0|crew-commit-agent|haiku|src/ui/a.ts|fix-known|none|
8269
8500
  0|crew-backend-expert|sonnet|src/ui/a.ts|text|none|
8270
8501
  0|crew-backend-expert|haiku|src/ui/a.ts|text|true|
8271
8502
  0|crew-test-expert|haiku|src/ui/**|test-run|true|
@@ -8297,9 +8528,48 @@ done <<'V2'
8297
8528
  0|crew-backend-expert|sonnet|src/ui/a.ts|feature|git commit -m x|
8298
8529
  0|crew-backend-expert|sonnet|src/ui/a.ts|feature|rm -rf /|
8299
8530
  V2
8300
- if [ "$_v2n" != 60 ]; then fail "FIXTURE: the card table has $_v2n rows, not 60"
8301
- elif [ -z "$_v2b" ]; then pass "the card decides the model: critical work (payments/, migrations, a .sql file, AuthController, userAuth, schema.prisma, and the four critical kinds of change) is refused below opus and passes on opus; verify none is refused on haiku; test-run passes on haiku, also on a critical path; test-write, audit and review are held to sonnet in normal work and to opus in critical work; critical work with a verify command is refused when the call says run_in_background true, and passes with false and with the field left out (a session whose Agent tool has no such field cannot send it); a card that is not one is refused; a verify command never refuses the call, whatever it is (the hook decides whether to run it) (47 rows)"
8531
+ if [ "$_v2n" != 65 ]; then fail "FIXTURE: the card table has $_v2n rows, not 65"
8532
+ elif [ -z "$_v2b" ]; then pass "the card decides the model: critical work (payments/, migrations, a .sql file, AuthController, userAuth, schema.prisma, and the four critical kinds of change) is refused below opus and passes on opus; a text change whose every file is prose (legal, privacy, terms, a store listing) passes on sonnet and is refused on haiku, and one card file that is not prose holds it to opus; verify none is refused on haiku, except for a text change and the commit agent; test-run passes on haiku, also on a critical path; test-write, audit and review are held to sonnet in normal work and to opus in critical work; critical work with a verify command is refused when the call says run_in_background true, and passes with false and with the field left out (a session whose Agent tool has no such field cannot send it); a card that is not one is refused; a verify command never refuses the call, whatever it is (the hook decides whether to run it) ($_v2n rows)"
8302
8533
  else fail "card table:$_v2b"; fi
8534
+ # The card's floor is also the default: a call above it runs ON it (the gate hands back the same input with `model`
8535
+ # replaced, PreToolUse updatedInput), unless the card's next line says `above: <why>`. Measured on a real project:
8536
+ # 73 of 186 crew tasks ran above their floor and none on haiku; measured on Claude Code 2.1.299: the rewrite holds.
8537
+ _v2new; _v2b=""
8538
+ _v2last(){ ls -t "$_v2p/.claude/state/crew-model/pending/"* 2>/dev/null | head -1; }
8539
+ _v2call crew-review-agent opus 'src/ui/a.ts' audit none ',"run_in_background":true,"isolation":"worktree"'
8540
+ _o="$(cat "$_v2/out" 2>/dev/null)"; _r="$(_v2last)"
8541
+ case "$_o" in *'"permissionDecision":"allow"'*'"updatedInput":'*) ;; *) _v2b="$_v2b [above the floor, no above: -> no rewrite: rc=$_v2rc ${_o:0:80}]" ;; esac
8542
+ _u="$(printf '%s' "$_o" | awk -v op=get -v path=hookSpecificOutput.updatedInput -f "$ROOT/eval/lib/settings-json.awk" - 2>/dev/null | tr -d ' \n')"
8543
+ case "$_u" in *'"model":"sonnet"'*'"run_in_background":true'*'"isolation":"worktree"'*|*'"run_in_background":true'*'"isolation":"worktree"'*) ;; *) _v2b="$_v2b [the rewrite lost a field: ${_u:0:120}]" ;; esac
8544
+ case "$_u" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [the rewrite is not to the floor: ${_u:0:120}]" ;; esac
8545
+ { grep -q '^tier=sonnet$' "$_r" && grep -q '^lowered=opus$' "$_r"; } 2>/dev/null || _v2b="$_v2b [record: $(tr '\n' ' ' < "$_r" 2>/dev/null | cut -c1-80)]"
8546
+ rm -f "$_v2p/.claude/state/crew-model/pending/"*
8547
+ _v2call crew-review-agent opus 'src/ui/a.ts' audit 'none\nabove: the audit covers the whole session layer'
8548
+ { [ "$_v2rc" = 0 ] && [ ! -s "$_v2/out" ] && grep -q '^tier=opus$' "$(_v2last)"; } 2>/dev/null || _v2b="$_v2b [above: did not keep opus: rc=$_v2rc $(head -c 60 "$_v2/out")]"
8549
+ rm -f "$_v2p/.claude/state/crew-model/pending/"*
8550
+ _v2call crew-review-agent sonnet 'src/ui/a.ts' audit none
8551
+ { [ "$_v2rc" = 0 ] && [ ! -s "$_v2/out" ]; } || _v2b="$_v2b [a call on its floor was rewritten: $(head -c 60 "$_v2/out")]"
8552
+ _v2call crew-frontend-expert sonnet 'src/ui/a.ts' text 'test -d src'
8553
+ case "$(tr -d ' ' < "$_v2/out")" in *'"model":"haiku"'*) ;; *) _v2b="$_v2b [work a command checks did not go to haiku: $(head -c 80 "$_v2/out")]" ;; esac
8554
+ rm -f "$_v2p/.claude/state/crew-model/pending/"*
8555
+ # Not a crew agent: no card and no floor, only the expensive end is held. opus or fable with no `above:` line in the
8556
+ # task runs on sonnet; with one it stays; a call that names no model, or sonnet, is not touched.
8557
+ _v2o(){ printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"description":"d","prompt":"Find where X is defined.%s","subagent_type":"general-purpose"%s},"tool_use_id":"tu%s"}' "$1" "$2" "$RANDOM" | ( cd "$_v2p" && _v2e bash "$_V2G" 2>/dev/null ) | tr -d ' '; }
8558
+ case "$(_v2o '' ',"model":"opus"')" in *'"updatedInput":'*'"model":"sonnet"'*) ;; *) _v2b="$_v2b [general-purpose on opus was not run on sonnet]" ;; esac
8559
+ [ -z "$(_v2o '\nabove: a design question across the codebase' ',"model":"opus"')" ] || _v2b="$_v2b [above: did not keep opus for general-purpose]"
8560
+ [ -z "$(_v2o '' ',"model":"sonnet"')" ] || _v2b="$_v2b [a call naming sonnet was rewritten]"
8561
+ # Naming no model: general-purpose takes the session's model, so it is given sonnet (CLAUDE_CODE_SUBAGENT_MODEL in
8562
+ # the project's settings env did not do it, measured on 2.1.299), unless the session is on haiku; an agent whose
8563
+ # definition names its own model is left to it; a project agent with no model line is treated like general-purpose.
8564
+ _v2ot(){ printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"PreToolUse","tool_name":"Agent","transcript_path":"%s","tool_input":{"description":"d","prompt":"Find X.","subagent_type":"%s"},"tool_use_id":"tu%s"}' "$2" "$1" "$RANDOM" | ( cd "$_v2p" && _v2e bash "$_V2G" 2>/dev/null ) | tr -d ' '; }
8565
+ printf '{"type":"assistant","message":{"model":"claude-opus-5-5"}}\n' > "$_v2/tr-opus.jsonl"; printf '{"type":"assistant","message":{"model":"claude-haiku-5-5"}}\n' > "$_v2/tr-haiku.jsonl"
8566
+ mkdir -p "$_v2p/.claude/agents"; printf -- '---\nname: mine\ndescription: x\n---\nDo it.\n' > "$_v2p/.claude/agents/mine.md"; printf -- '---\nname: cheap\nmodel: haiku\n---\nDo it.\n' > "$_v2p/.claude/agents/cheap.md"
8567
+ case "$(_v2ot general-purpose "$_v2/tr-opus.jsonl")" in *'"updatedInput":'*'"model":"sonnet"'*) ;; *) _v2b="$_v2b [general-purpose naming no model under opus was not given sonnet]" ;; esac
8568
+ [ -z "$(_v2ot general-purpose "$_v2/tr-haiku.jsonl")" ] || _v2b="$_v2b [a haiku session's general-purpose was raised to sonnet]"
8569
+ [ -z "$(_v2ot Explore "$_v2/tr-opus.jsonl")$(_v2ot cheap "$_v2/tr-opus.jsonl")" ] || _v2b="$_v2b [an agent with a model of its own was given one]"
8570
+ case "$(_v2ot mine "$_v2/tr-opus.jsonl")" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [a project agent with no model line was not given sonnet]" ;; esac
8571
+ [ -z "$_v2b" ] && pass "above the floor runs on the floor: opus for a sonnet card is rewritten to sonnet with every other field kept and recorded as lowered; 'above: <why>' keeps opus; a call on its floor is untouched; checkable text work goes to haiku; an agent outside the crew asking for opus, or inheriting an opus session's model, runs on sonnet unless its task says why; a haiku session and an agent with its own model are left alone" \
8572
+ || fail "model on the floor:$_v2b"
8303
8573
  # The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
8304
8574
  # left the field out and was launched in the background is told so when the launch comes back.
8305
8575
  _v2new; _v2b=""
@@ -8340,20 +8610,67 @@ for _pr in 'Do the work.' 'Please do this:\nfiles: a\nchange: feature\nverify: t
8340
8610
  done
8341
8611
  [ -z "$_v2b" ] && pass "a crew call with no card, a card that is not at the top, a card with a line missing or one named twice is refused, with the three lines to write" \
8342
8612
  || fail "a task without a proper card passed:$_v2b"
8613
+ # The card on one line (2026-10-18: 17 of 39 card refusals on this machine were `files: … · change: … · verify: …`). It is
8614
+ # read by its labels and held to the same rules: what passes on three lines passes on one, and what fails there fails.
8615
+ _v2b=""; _v2one(){ printf '{"hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"prompt":"%s\\n\\nDo it.","subagent_type":"crew-backend-expert","model":"%s"}}' "$1" "$2" > "$_v2/one.json"
8616
+ ( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/one.json" >"$_v2/oneout" 2>"$_v2/err" ); _rc=$?; }
8617
+ for _pr in 'files: src/ui/a.ts · change: feature · verify: true' 'files: src/ui/a.ts; change: feature; verify: true' 'files: src/ui/a.ts | change: feature | verify: true' \
8618
+ 'Card: files: src/ui/a.ts, src/ui/b.ts · change: feature · verify: true.' 'Card — files: src/ui/a.ts · change: text · verify: none' \
8619
+ 'Kart — files: src/ui/a.ts · change: text · verify: none' 'Kart: files: src/ui/a.ts · change: feature · verify: true'; do
8620
+ _v2one "$_pr" sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [refused: $_pr → $_rc $(head -c 120 "$_v2/err")]"
8621
+ done
8622
+ _v2one 'files: src/payments/p.ts · change: feature · verify: true' sonnet
8623
+ { [ "$_rc" = 2 ] && ! grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [a one-line card on a critical path was not read as one (rc $_rc)]"
8624
+ _v2one 'files: src/ui/a.ts · change: feature · verify: true · above: the parser is subtle' opus
8625
+ { [ "$_rc" = 0 ] && ! grep -q 'runs on sonnet' "$_v2/oneout"; } || _v2b="$_v2b [above: on the same line was not read (rc $_rc)]"
8626
+ _v2one 'files: src/ui/a.ts · change: feature (a small one) · verify: true' sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [a kind with a note in parentheses, one line → $_rc]"
8627
+ _v2one 'files: src/ui/a.ts · change: architecture (planning only) · verify: none' sonnet
8628
+ { [ "$_rc" = 2 ] && grep -q 'runs on opus' "$_v2/err"; } || _v2b="$_v2b [architecture (planning only) was not read as architecture, which runs on opus (rc $_rc)]"
8629
+ _v2one 'files: src/ui/a.ts\nchange: refactor (no behaviour change)\nverify: true' sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [a kind with a note in parentheses, three lines → $_rc]"
8630
+ for _pr in 'files: a · change: archi (planning only) · verify: none' 'files: a · change: feature extra · verify: none' 'files: a · verify: true · change: text' 'files: a · change: feature' 'Do this. files: a · change: feature · verify: true' 'Kartal files: a · change: feature · verify: true'; do
8631
+ _v2one "$_pr" sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [passed: $_pr → $_rc]"
8632
+ done
8633
+ [ -z "$_v2b" ] && pass "a card on one line (· ; | separators, a Card: or Kart: lead, above: on the line) is read by its labels and held to the three-line rules: a critical path still needs opus; a kind with a note in parentheses is the kind, on one line or three; a bad change, labels out of order, a missing verify or prose first are refused" \
8634
+ || fail "the one-line card:$_v2b"
8635
+ # `above:` among the three lines, not only after them (2026-10-19: 2 of 19 line refusals had it between change: and
8636
+ # verify:; the retry moved it down and passed). It keeps the model the same way; the three are still all required.
8637
+ _v2b=""
8638
+ for _pr in 'files: src/ui/a.ts\nchange: feature\nabove: the parser is subtle\nverify: true' 'above: the parser is subtle\nfiles: src/ui/a.ts\nchange: feature\nverify: true'; do
8639
+ _v2one "$_pr" opus; { [ "$_rc" = 0 ] && [ ! -s "$_v2/oneout" ]; } || _v2b="$_v2b [above: among the three did not keep opus: $_pr → $_rc $(head -c 100 "$_v2/oneout")]"
8640
+ done
8641
+ _v2one 'files: src/ui/c.ts\nchange: feature\nverify: true' opus
8642
+ grep -q '"updatedInput":' "$_v2/oneout" || _v2b="$_v2b [without above: opus was not lowered (rc $_rc)]"
8643
+ for _pr in 'files: src/ui/a.ts\nchange: feature\nabove: x\nDo it.' 'files: src/ui/a.ts\nabove: x\nabove: y\nverify: true'; do
8644
+ _v2one "$_pr" sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [passed: $_pr → $_rc]"
8645
+ done
8646
+ # `change: review` is an audit (the discipline's table lists it with them): same floor, a near miss still refused.
8647
+ _v2one 'files: src/ui/a.ts\nchange: review\nverify: none' haiku; { [ "$_rc" = 2 ] && grep -q 'runs on sonnet' "$_v2/err"; } || _v2b="$_v2b [review on haiku was not held to sonnet as audit (rc $_rc)]"
8648
+ for _pr in 'files: src/ui/a.ts\nchange: review\nverify: none' 'files: src/ui/a.ts · change: review (the staged diff) · verify: none'; do
8649
+ _v2one "$_pr" sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [refused: $_pr → $_rc $(head -c 100 "$_v2/err")]"
8650
+ done
8651
+ _v2one 'files: src/payments/p.ts\nchange: review\nverify: none' sonnet; { [ "$_rc" = 2 ] && grep -q 'runs on opus' "$_v2/err"; } || _v2b="$_v2b [review on a critical path was not held to opus (rc $_rc)]"
8652
+ _v2one 'files: src/ui/a.ts\nchange: reviews\nverify: none' sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [change: reviews passed (rc $_rc)]"
8653
+ [ -z "$_v2b" ] && pass "above: may stand anywhere among the three card lines and keeps the model; without it the model is lowered; above: does not stand in for files:, change: or verify:; change: review is an audit, held to the same floors, and a near miss is still refused" \
8654
+ || fail "above: in the card:$_v2b"
8343
8655
 
8344
8656
  # ---- at write time: the card can leave a file out --------------------------------------------------------------
8345
8657
  _v2new; _v2b=""
8346
8658
  _v2call crew-backend-expert sonnet 'src/ui/a.ts' refactor true; _v2start ag-son
8347
8659
  _v2call crew-backend-expert opus 'src/payments/p.ts' feature none; _v2start ag-op
8660
+ _v2call crew-docs-agent sonnet 'legal/terms.md' text none; _v2start ag-txt crew-docs-agent
8348
8661
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-son","agent_type":"crew-backend-expert"'; { [ "$_v2wrc" = 2 ] && grep -q 'escalate: src/payments/p.ts' "$_v2/werr" && grep -q '^GUARD (agent model): ' "$_v2/werr"; } || _v2b="$_v2b sonnet-agent-wrote-payments:$_v2wrc"
8349
8662
  _v2write "$_v2p/src/ui/a.ts" ',"agent_id":"ag-son","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b sonnet-agent-refused-on-ui:$_v2wrc"
8350
8663
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-op","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b opus-agent-refused:$_v2wrc"
8351
8664
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-unknown","agent_type":"general-purpose"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b unknown-model-wrote-payments:$_v2wrc"
8352
8665
  _v2write "$_v2p/src/payments/p.ts"; [ "$_v2wrc" = 0 ] || _v2b="$_v2b the-session-refused:$_v2wrc"
8666
+ # A text change on sonnet writes prose on a critical path; not code there, and not prose for any other change.
8667
+ _v2write "$_v2p/legal/terms.md" ',"agent_id":"ag-txt","agent_type":"crew-docs-agent"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b sonnet-text-refused-legal-prose:$_v2wrc"
8668
+ _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-txt","agent_type":"crew-docs-agent"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b sonnet-text-wrote-payments-code:$_v2wrc"
8669
+ _v2write "$_v2p/legal/terms.md" ',"agent_id":"ag-son","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b sonnet-refactor-wrote-legal-prose:$_v2wrc"
8353
8670
  mkdir -p "$_v2/t/subagents"; printf '{"type":"assistant","message":{"model":"claude-opus-5-5"}}\n' > "$_v2/t/subagents/agent-ag-gp.jsonl"
8354
8671
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-gp","agent_type":"general-purpose"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b opus-by-its-transcript-refused:$_v2wrc"
8355
8672
  _V2ENV="CREW_MODEL_ROUTING=off" _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-son","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b routing-off-refused:$_v2wrc"
8356
- [ -z "$_v2b" ] && pass "an agent on sonnet is refused a write to a critical path its card did not name, and told to report escalate; the same agent writes elsewhere; an agent on opus (by its call, or by its own transcript) writes there; an agent whose model cannot be read does not; the session's own write is not asked; routing off asks nothing" \
8673
+ [ -z "$_v2b" ] && pass "an agent on sonnet is refused a write to a critical path its card did not name, and told to report escalate; the same agent writes elsewhere; an agent on opus (by its call, or by its own transcript) writes there; an agent whose model cannot be read does not; the session's own write is not asked; routing off asks nothing; an agent on sonnet with a text card writes prose there (legal/terms.md) and not code, and one with any other card writes neither" \
8357
8674
  || fail "write-time check:$_v2b"
8358
8675
  # B2 (K7): the session's own write to a critical path is not refused, and it is recorded: one NOTE line in the gate
8359
8676
  # log naming the path and the rule. A write elsewhere, one outside the project, and one with routing off add none.
@@ -8387,6 +8704,32 @@ _v2call crew-backend-expert sonnet 'docs/security.md' text none; [ "$_v2rc" = 0
8387
8704
  _v2call crew-backend-expert sonnet 'Api/Services/AuthorService.cs' feature none; [ "$_v2rc" = 0 ] || _v2b="$_v2b AuthorService-read-as-auth"
8388
8705
  [ -z "$_v2b" ] && pass "with no settings file the scan finds the critical paths of a Flutter app and a .NET service (a folder once, not per file; AuthorService and author.md are not auth); crew-model-rules adds and removes paths, at SessionStart too, and the gate follows it" \
8389
8706
  || fail "critical paths:$_v2b"
8707
+ # V5: an organisation preset (eval/lib/preset.sh) is a block at the top of crew-model-rules. The project's own lines
8708
+ # stay below it and win where the file's rule lets them; applying again changes nothing; an invalid one writes nothing.
8709
+ _v2new; _v2b=""; PSH="$ROOT/eval/lib/preset.sh"; _psr="$_v2p/.claude/crew-model-rules"; _psx="$(mktemp -d)"
8710
+ printf '# ours\n- infra\n' > "$_psr"
8711
+ ( cd "$_v2p" && bash "$PSH" --apply strict ) >/dev/null 2>&1 || _v2b="$_v2b [strict did not apply]"
8712
+ { IFS= read -r _l1 < "$_psr"; case "$_l1" in '# ---- preset: strict '*) ;; *) false ;; esac; } || _v2b="$_v2b [first line: ${_l1:-none}]"
8713
+ grep -qx '+ deploy' "$_psr" && grep -qx 'floor crew-review-agent \* \* opus' "$_psr" && grep -qx -- '- infra' "$_psr" && grep -qx '# ours' "$_psr" || _v2b="$_v2b [block or own lines missing]"
8714
+ [ "$(grep -n -- '^- infra$' "$_psr" | cut -d: -f1)" -gt "$(grep -n '^+ infra$' "$_psr" | cut -d: -f1)" ] || _v2b="$_v2b [the project's own line is not below the block]"
8715
+ cp "$_psr" "$_psx/a"; ( cd "$_v2p" && bash "$PSH" --apply strict ) >/dev/null 2>&1; cmp -s "$_psr" "$_psx/a" || _v2b="$_v2b [a second apply changed the file]"
8716
+ [ "$( cd "$_v2p" && bash "$PSH" --show )" = strict ] || _v2b="$_v2b [--show]"
8717
+ # The gate follows it: review on sonnet is refused under strict, and the project's `- infra` undoes the preset's `+ infra`.
8718
+ _v2call crew-review-agent sonnet 'src/ui/a.ts' audit none; { [ "$_v2rc" = 2 ] && grep -q 'floor crew-review-agent \* \* opus' "$_v2/err"; } || _v2b="$_v2b [review on sonnet under strict: rc $_v2rc]"
8719
+ _v2call crew-backend-expert sonnet 'infra/main.tf' feature 'test -d src'; [ "$_v2rc" = 0 ] || _v2b="$_v2b [infra stayed critical though the project removed it: rc $_v2rc]"
8720
+ _v2call crew-backend-expert sonnet 'deploy/run.sh' feature 'test -d src'; [ "$_v2rc" = 2 ] || _v2b="$_v2b [deploy is not critical under strict: rc $_v2rc]"
8721
+ ( cd "$_v2p" && bash "$PSH" --apply solo ) >/dev/null 2>&1; ! grep -q '^+ deploy' "$_psr" && grep -qx 'verify npm test' "$_psr" && grep -qx -- '- infra' "$_psr" || _v2b="$_v2b [switching to solo left the old block or lost the own line]"
8722
+ # Invalid presets: each refused with rc 1, the file untouched.
8723
+ cp "$_psr" "$_psx/b"; _bad=0
8724
+ for _bj in '{"name":"x","oops":1}' '{"name":"x","floors":[["a","b","c","gpt"]]}' '{"name":"x","verify":["npm test; rm -rf /"]}' '{"name":"X Y"}' 'not json'; do
8725
+ printf '%s' "$_bj" > "$_psx/bad.json"; ( cd "$_v2p" && bash "$PSH" --apply "$_psx/bad.json" ) >/dev/null 2>&1; _rc=$?
8726
+ { [ "$_rc" = 1 ] && cmp -s "$_psr" "$_psx/b"; } || { _bad=$((_bad+1)); _v2b="$_v2b [accepted or wrote: $_bj rc $_rc]"; }
8727
+ done
8728
+ ( cd "$_v2p" && bash "$PSH" --apply no-such-preset ) >/dev/null 2>&1 && _v2b="$_v2b [a preset that is not there applied]"
8729
+ _dl="$(cd "$_v2p" && bash "$ROOT/eval/doctor.sh" . 2>/dev/null | grep -c 'organisation preset: solo')"; [ "$_dl" = 1 ] || _v2b="$_v2b [doctor did not name the preset: $_dl]"
8730
+ [ -z "$_v2b" ] && pass "an organisation preset is a block at the top of crew-model-rules: the project's own lines stay below it and win (its '- infra' undoes the preset's), the gate follows it (review held to opus under strict), applying again changes no byte, switching replaces the block, five invalid presets and a missing one write nothing, and doctor names the one in force" \
8731
+ || fail "presets:$_v2b"
8732
+ rm -rf "$_psx"
8390
8733
 
8391
8734
  # ---- 3. verify, one fix inside the agent, then one model up, then the user -----------------------------------
8392
8735
  _v2new; _v2b=""
@@ -8699,12 +9042,41 @@ if command -v iconv >/dev/null 2>&1; then printf '%s' "$_o" | iconv -f UTF-8 -t
8699
9042
  [ "${#_o}" -le 2600 ] || _v2b="$_v2b [the reason is ${#_o} bytes]"
8700
9043
  [ -z "$_v2b" ] && pass "the one fix is asked once and only where it can be counted: a red verify under stop_hook_active ends as fail with no block, and so does one whose counter cannot be written (both recorded as fail with no fix); the output handed to the agent is cut at 1500 bytes without splitting a character" \
8701
9044
  || fail "the stop hook's one fix:$_v2b"
9045
+ # A verify command in a sub-folder, `cd <folder> && <command>` (41 of 71 refusals on one project): judged as the
9046
+ # command and run in the folder with no chain. A folder outside the project, one that does not exist, or a second
9047
+ # chain after it stays refused. A card that says `none (why)` is none.
9048
+ _v2new; _v2b=""; mkdir -p "$_v2p/.claude/state/crew-model/agents" "$_v2p/sub"; : > "$_v2p/sub/ok.flag"
9049
+ printf 'verify test -f ok.flag\n' > "$_v2p/.claude/crew-model-rules"
9050
+ _v2plant c1 'cd sub && test -f ok.flag'; _v2stop c1 >/dev/null
9051
+ [ "$(_v2row c1 | cut -d/ -f2)" = pass ] || _v2b="$_v2b [cd sub && test: $(_v2row c1)]"
9052
+ for _cc in 'c2:cd ../sub && test -f ok.flag' 'c3:cd nowhere && test -f ok.flag' 'c4:cd sub && test -f ok.flag && touch x' 'c5:cd /tmp && test -f ok.flag'; do
9053
+ _v2plant "${_cc%%:*}" "${_cc#*:}"; _v2stop "${_cc%%:*}" >/dev/null
9054
+ [ "$(_v2row "${_cc%%:*}" | cut -d/ -f2)" = blocked ] || _v2b="$_v2b [${_cc#*:} was not refused: $(_v2row "${_cc%%:*}")]"
9055
+ done
9056
+ [ ! -e "$_v2p/sub/x" ] || _v2b="$_v2b [a chain after the folder ran]"
9057
+ _v2call crew-backend-expert sonnet 'src/ui/a.ts' text 'none (the author ran it)'
9058
+ grep -q '^verify=none$' "$(ls -t "$_v2p/.claude/state/crew-model/pending/"* 2>/dev/null | head -1)" 2>/dev/null || _v2b="$_v2b [none (why) was not none]"
9059
+ # doctor lists the commands refused for not being allowed, with the line that allows them, and not a sentence
9060
+ mkdir -p "$_v2p/.claude/state/crew-model/results"
9061
+ printf 'verify=blocked\nwhynot=it is not a test or build command on this hook'"'"'s list, and no rule names it\ncmd=node tools/build.mjs --check\n' > "$_v2p/.claude/state/crew-model/results/r1"
9062
+ printf 'verify=blocked\nwhynot=it is not a test or build command on this hook'"'"'s list, and no rule names it\ncmd=actionlint (if installed)\n' > "$_v2p/.claude/state/crew-model/results/r2"
9063
+ # The other way of saying "nothing allows it" (default mode), for a command in a sub-folder: counted, and offered as
9064
+ # the rule that matches it, the command without its `cd` (measured on Windows: the line with the cd in it did not).
9065
+ printf "verify=blocked\nwhynot=in 'default' mode Claude Code does not run a command the user has not allowed, so neither does a hook\ncmd=cd sub && ./check.sh\n" > "$_v2p/.claude/state/crew-model/results/r3"
9066
+ if [ -f "$ROOT/eval/doctor.sh" ]; then
9067
+ mkdir -p "$_v2p/.claude/eval"; cp -R "$ROOT/eval/." "$_v2p/.claude/eval/" 2>/dev/null
9068
+ _dd="$( cd "$_v2p" && env -u CLAUDE_CODE_SUBAGENT_MODEL CREW_NO_STAR=1 CREW_LANG=en CLAUDE_CONFIG_DIR="$_v2/home" bash .claude/eval/doctor.sh 2>/dev/null | grep 'verify commands never run' )"
9069
+ case "$_dd" in *'3 time(s)'*'verify node tools/build.mjs --check'*'verify ./check.sh'*) case "$_dd" in *actionlint*|*'verify cd '*) _v2b="$_v2b [doctor offered a sentence, or a rule with its cd, as a rule: ${_dd}]" ;; esac ;;
9070
+ *) _v2b="$_v2b [doctor: ${_dd:-no line}]" ;; esac
9071
+ fi
9072
+ [ -z "$_v2b" ] && pass "a verify command in a sub-folder runs there: 'cd sub && <command>' passes, judged as the command alone; a folder outside the project or missing, an absolute one, and a chain after it stay refused; 'none (why)' is none; doctor offers the refused commands, in either mode, as the rule that matches them (no cd prefix), not a sentence" \
9073
+ || fail "verify in a sub-folder:$_v2b"
8702
9074
  # Which record is whose. Two calls of one type made together are matched by order, which can be wrong; the card in
8703
9075
  # the agent's own transcript settles it.
8704
9076
  _v2new; _v2b=""; _v2st="$_v2p/.claude/state/crew-model"
8705
9077
  _v2tr(){ printf '{"type":"user","message":{"role":"user","content":"files: %s\\nchange: %s\\nverify: %s\\n\\nDo the work."}}\n' "$2" "$3" "$4" > "$_v2/tr-$1.jsonl"; }
8706
9078
  _v2stopt(){ printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"SubagentStop","agent_id":"%s","agent_type":"x","stop_hook_active":false,"agent_transcript_path":"%s/tr-%s.jsonl","last_assistant_message":"did it\\nconfidence: high"}' "$1" "$_v2" "$1" | ( cd "$_v2p" && _v2e bash "$_V2O" 2>/dev/null ); }
8707
- _v2call crew-backend-expert haiku 'src/ui/one.ts' text 'test -f one.ok'; _v2call crew-backend-expert opus 'src/ui/two.ts' feature 'test -f two.ok'
9079
+ _v2call crew-backend-expert haiku 'src/ui/one.ts' text 'test -f one.ok'; _v2call crew-backend-expert opus 'src/ui/two.ts' feature 'test -f two.ok\nabove: two models pending for one agent type'
8708
9080
  # The order the two records are taken in is made known: the haiku call's first, so each agent gets the OTHER's below.
8709
9081
  for _pf in "$_v2st/pending"/*; do if grep -q 'one.ok' "$_pf"; then mv "$_pf" "$_v2st/pending/1-a"; else mv "$_pf" "$_v2st/pending/2-b"; fi; done
8710
9082
  _pt="$(date +%s)"; mv "$_v2st/pending/1-a" "$_v2st/pending/$_pt-a"; mv "$_v2st/pending/2-b" "$_v2st/pending/$_pt-b"
@@ -8739,6 +9111,16 @@ rm -f "$_v2st"/pending/*
8739
9111
  _v2call crew-backend-expert sonnet 'src/ui/race.ts' text true
8740
9112
  _v2start r1 & _v2start r2 & wait
8741
9113
  _rn=0; [ -f "$_v2st/agents/r1" ] && _rn=$((_rn+1)); [ -f "$_v2st/agents/r2" ] && _rn=$((_rn+1)); [ "$_rn" = 1 ] || _v2b="$_v2b [one record, two agents: $_rn got it]"
9114
+ ls "$_v2st/agents" 2>/dev/null | grep -qE '\.(take|claim)$' && _v2b="$_v2b a-lock-or-claim-left-behind"
9115
+ # A record another hook holds the lock on is not taken: the agent goes on to the next one (the lock is what keeps
9116
+ # two agents off one record where a rename alone did not, on Windows).
9117
+ rm -rf "$_v2st/agents"; rm -f "$_v2st"/pending/*
9118
+ _v2call crew-backend-expert sonnet 'src/ui/l1.ts' text 'test -f l1'; _pl1="$(ls "$_v2st/pending")"
9119
+ sleep 1; _v2call crew-backend-expert sonnet 'src/ui/l2.ts' text 'test -f l2'
9120
+ mkdir -p "$_v2st/agents"; : > "$_v2st/agents/$_pl1.take"
9121
+ _v2start k1
9122
+ { grep -q '^verify=test -f l2' "$_v2st/agents/k1" 2>/dev/null && [ -f "$_v2st/pending/$_pl1" ]; } || _v2b="$_v2b a-locked-record-was-taken-or-the-next-one-was-not"
9123
+ rm -f "$_v2st/agents/$_pl1.take" "$_v2st"/pending/*
8742
9124
  # A record nothing claimed within five minutes is nobody's.
8743
9125
  mkdir -p "$_v2st/pending"; printf 'agent=crew-backend-expert\ntier=opus\nchange=text\nrisk=normal\ncard=old\nbg=unset\nesc=-\nverify=none\n' > "$_v2st/pending/1000000000-tuold"
8744
9126
  _v2start o1; { [ ! -f "$_v2st/agents/o1" ] && [ ! -f "$_v2st/pending/1000000000-tuold" ]; } || _v2b="$_v2b a-stale-record-was-given-to-an-agent-or-kept"
@@ -8761,7 +9143,7 @@ _kn="$(awk -F'\t' '$2 == "k2" { print $5 "/" $13 }' "$_v2st/../model-outcomes.ts
8761
9143
  # The gate's and the write-time check's refusals are in the gate log; an agent id that cannot be read does not open
8762
9144
  # the write-time check; run_in_background named twice is refused.
8763
9145
  _v2new; _v2b=""; _V2ENV="CREW_GATE_LOG=$_v2/gl.tsv"; : > "$_v2/gl.tsv"
8764
- _v2call crew-database-expert sonnet 'src/payments/p.ts' feature none; _v2call crew-backend-expert haiku 'src/ui/a.ts' text none
9146
+ _v2call crew-database-expert sonnet 'src/payments/p.ts' feature none; _v2call crew-backend-expert haiku 'src/ui/a.ts' fix-known none
8765
9147
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"nobody-known","agent_type":"x"'
8766
9148
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"","agent_type":"x"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b an-empty-agent-id-opened-the-check:$_v2wrc"
8767
9149
  _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"a b/../c","agent_type":"x"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b an-unreadable-agent-id-opened-the-check:$_v2wrc"
@@ -8905,11 +9287,11 @@ _v2call crew-backend-expert sonnet 'src/payments/p.ts' feature none; [ "$_v2rc"
8905
9287
  # What a lowering cannot reach: the floors the card and the agent carry. Two raised floors are planted and lowered;
8906
9288
  # the model below still fails where another rule holds it, and passes where only the lowered floor did.
8907
9289
  _v2new; _v2b=""; mkdir -p "$_v2p/.claude/state"
8908
- printf 'crew-backend-expert\ttext\tnormal\tsonnet\t6\t3\t2026-01-01T00:00:00Z\ncrew-database-expert\tfeature\tnormal\topus\t6\t3\t2026-01-01T00:00:00Z\n' > "$_v2fl"
8909
- _o="$(_v2say '/crew-loosen crew-backend-expert text normal')$(_v2say '/crew-loosen crew-database-expert feature normal')"
9290
+ printf 'crew-backend-expert\tfix-known\tnormal\tsonnet\t6\t3\t2026-01-01T00:00:00Z\ncrew-database-expert\tfeature\tnormal\topus\t6\t3\t2026-01-01T00:00:00Z\n' > "$_v2fl"
9291
+ _o="$(_v2say '/crew-loosen crew-backend-expert fix-known normal')$(_v2say '/crew-loosen crew-database-expert feature normal')"
8910
9292
  case "$_o" in *'from sonnet to haiku'*'from opus to sonnet'*) ;; *) _v2b="$_v2b FIXTURE:the-two-floors-were-not-lowered" ;; esac
8911
- _v2call crew-backend-expert haiku 'src/ui/f1.ts' text true; [ "$_v2rc" = 0 ] || _v2b="$_v2b the-lowered-class-is-still-refused-on-haiku:$_v2rc"
8912
- _v2call crew-backend-expert haiku 'src/ui/f1.ts' text none; [ "$_v2rc" = 2 ] || _v2b="$_v2b verify-none-went-to-haiku-after-a-lowering"
9293
+ _v2call crew-backend-expert haiku 'src/ui/f1.ts' fix-known true; [ "$_v2rc" = 0 ] || _v2b="$_v2b the-lowered-class-is-still-refused-on-haiku:$_v2rc"
9294
+ _v2call crew-backend-expert haiku 'src/ui/f1.ts' fix-known none; [ "$_v2rc" = 2 ] || _v2b="$_v2b verify-none-went-to-haiku-after-a-lowering"
8913
9295
  _v2call crew-database-expert sonnet 'src/ui/f2.ts' feature true; [ "$_v2rc" = 0 ] || _v2b="$_v2b the-lowered-class-is-still-refused-on-sonnet:$_v2rc"
8914
9296
  _v2call crew-database-expert haiku 'src/ui/f2.ts' feature true; { [ "$_v2rc" = 2 ] && grep -q 'runs on sonnet or above' "$_v2/err"; } || _v2b="$_v2b an-agents-own-floor-was-lowered"
8915
9297
  _v2call crew-database-expert sonnet 'db/migrations/1.sql' feature true ',"run_in_background":false'; [ "$_v2rc" = 2 ] || _v2b="$_v2b critical-work-went-below-opus-after-a-lowering"
@@ -11155,7 +11537,7 @@ sec "== 14c) the 3.0 rename left no old name behind — outside history and the
11155
11537
  if [ -n "$SGR" ] && [ -d "$SGR/packaging" ] && [ -f "$SGR/VERSION" ] && [ -d "$SGR/kit" ] && [ -f "$SGR/packaging/build-plugin.sh" ]; then
11156
11538
  RN_ALLOW='CHANGELOG.md 196 history: every entry before 3.0 keeps the name it shipped under
11157
11539
  README*.md site/content/*/install.md 4 migration: the 2.x plugin note names the plugin to uninstall — one line in each README and install page, EN + TR
11158
- evals/results/* 6 history: recorded eval runs stay byte-for-byte
11540
+ evals/results/* packaging/review-bench/cases/* packaging/review-bench/results/* packaging/advisor-bench/results/* 729 history: recorded eval runs, and the review bench'"'"'s cases (past versions of Crewforth'"'"'s own files) and results, and the advisor bench'"'"'s results on them, stay byte-for-byte
11159
11541
  adopt.sh kit/legacy-blobs.tsv 347 migration: finds and moves 2.x names (components, CLAUDE.md, board, auto-mode rules, variables), and the generated list of the 1.x/2.x files it may move
11160
11542
  bin/cli.js 4 migration: add accepts a typed <x>-csk and moves an add record written under the old names
11161
11543
  evals/run.sh 4 compat: reads the 2.x trusted eval parent when the 3.0 one is absent — removed in 4.0
@@ -11375,6 +11757,22 @@ $(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/|
11375
11757
  [ -n "$(_dn "$_kd/done-twin.sh")" ] && pass "the installers' closing lines name no kit (EN key and TR value); a planted 'kit/ deleted' is caught" \
11376
11758
  || fail "twin: the closing-line check missed a planted 'kit/ deleted' — it reads nothing"
11377
11759
  fi
11760
+ # adopt.sh --help answers before it reads anything. An unknown flag is ignored further down, so --help used to run
11761
+ # the detection (seen on Windows in the 3.2.0-rc.1 trial). Read in a git repo holding only adopt.sh: no kit/, both
11762
+ # languages, and nothing written. Twin: the same script without its --help branch must not print the usage.
11763
+ _ah_run(){ ( cd "$1" && CREW_LANG=en bash adopt.sh --help; echo "rc=$?"; bash adopt.sh -h --lang tr; echo "rc=$?" ) 2>&1; }
11764
+ mkdir -p "$_kd/ah" "$_kd/ah-twin"; git init -q "$_kd/ah" 2>/dev/null; git init -q "$_kd/ah-twin" 2>/dev/null
11765
+ cp "$KR/adopt.sh" "$_kd/ah/"; sed 's/-h|--help) _hh=1 ;;//' "$KR/adopt.sh" > "$_kd/ah-twin/adopt.sh"
11766
+ _ah="$(_ah_run "$_kd/ah")"; _aht="$(_ah_run "$_kd/ah-twin")"
11767
+ _ahw="$(git -C "$_kd/ah" status --porcelain 2>/dev/null | grep -vx '?? adopt.sh')"
11768
+ case "$_ah" in
11769
+ *'Usage: bash adopt.sh'*'rc=0'*'Kullanım: bash adopt.sh'*'rc=0'*)
11770
+ if [ -n "$_ahw" ]; then fail "adopt.sh --help wrote into the project: $(printf '%s' "$_ahw" | head -n 2 | tr '\n' ' ')"
11771
+ else case "$_aht" in *'Usage: bash adopt.sh'*) fail "twin: adopt.sh without its --help branch still printed the usage — the check reads nothing" ;;
11772
+ *) pass "adopt.sh --help prints its usage in English and Turkish, rc 0, with no kit/ and nothing written (a twin without it does not)" ;; esac
11773
+ fi ;;
11774
+ *) fail "adopt.sh --help did not print its usage in both languages with rc 0: $(printf '%s' "$_ah" | head -n 3 | tr '\n' ' ')" ;;
11775
+ esac
11378
11776
  rm -rf "$_kd"
11379
11777
  else
11380
11778
  skip scope "printed-text wording check skipped (installed project — the installers live in the source repository)"