crewforth 3.2.0-rc.2 → 3.2.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -5
- package/README.md +2 -0
- package/VERSION +1 -1
- package/adopt.sh +10 -1
- package/bin/cli.js +3 -2
- package/kit/CLAUDE.md +3 -3
- package/kit/agents/crew-commit-agent.md +6 -4
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/eval/doctor.sh +13 -2
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/lib/crew-map/narrative.mjs +42 -8
- package/kit/eval/lib/crew-map/project.mjs +3 -2
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +56 -4
- package/kit/eval/lib/crew-map/stacks.mjs +114 -8
- package/kit/eval/lib/crew-map/surface.mjs +62 -1
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-json.awk +4 -0
- package/kit/eval/lib/statusline.sh +1 -1
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +208 -21
- package/kit/hooks/agent-outcome.sh +18 -0
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +72 -26
- package/kit/hooks/guard-bash.sh +49 -34
- package/kit/hooks/guard-write.sh +3 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/prompt-approval.sh +21 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/trace-blocklist.txt +9 -0
- package/kit/settings.json +12 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-map/SKILL.md +16 -5
- package/kit/skills/crew-review/SKILL.md +2 -1
- package/kit/skills/crew-ship/SKILL.md +3 -1
- package/kit/studio/README.md +21 -10
- package/kit/studio/server/index.js +74 -22
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +104 -18
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/web/app.js +69 -7
- package/kit/studio/web/canvas.js +14 -2
- package/kit/studio/web/chat.js +32 -7
- package/kit/studio/web/graph-plan.js +3 -2
- package/kit/studio/web/map-plan.js +72 -3
- package/kit/studio/web/map.js +103 -10
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +34 -0
- package/kit/studio/web/tokens.css +6 -0
- package/package.json +1 -1
- package/start.sh +14 -1
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -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 @@
|
|
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)"
|
|
@@ -6380,6 +6457,14 @@ GNI="$(mktemp -d)"; ( cd "$GNI" && git init -q . && mkdir -p .claude )
|
|
|
6380
6457
|
( cd "$GNI" && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
|
|
6381
6458
|
[ -e "$GNI/.claude/gate-log.tsv" ] && fail "gate log wrote into a repo where the path is not gitignored" \
|
|
6382
6459
|
|| pass "gate log declines a path git would track"
|
|
6460
|
+
# ...and since 3.2.0 writes inside the repository's .git instead, which is never committed: a project that tracks
|
|
6461
|
+
# .claude/ had no record of a refusal at all (seen on a real project: no 🛡, no note of a lowered model).
|
|
6462
|
+
# The readers find it there (eval/lib/gate-log.sh, the one rule), and `git status` stays clean.
|
|
6463
|
+
_gni_read="$( cd "$GNI" && . "$ROOT/eval/lib/gate-log.sh" && crew_gate_log_read && printf '%s' "$_CREW_GL" )"
|
|
6464
|
+
_gni_st="$( cd "$GNI" && git status --porcelain --untracked-files=all 2>/dev/null | grep -c gate-log )"
|
|
6465
|
+
{ [ -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 ]; } \
|
|
6466
|
+
&& pass "where git would track it, the gate log goes inside .git: written, found by the readers, and git status stays clean" \
|
|
6467
|
+
|| 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}'"
|
|
6383
6468
|
( cd "$GNI" && echo '.claude/' > .gitignore && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
|
|
6384
6469
|
[ -s "$GNI/.claude/gate-log.tsv" ] && pass "gate log writes once the path is gitignored" \
|
|
6385
6470
|
|| fail "gate log stayed silent even though the path is ignored"
|
|
@@ -7944,6 +8029,48 @@ _paq_pre auto '{"question":"Which colour?","header":"Colour","options":[{"label"
|
|
|
7944
8029
|
if [ "$_paqn" != 18 ]; then fail "FIXTURE: the approval-question table ran $_paqn cases, not 18"
|
|
7945
8030
|
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)"
|
|
7946
8031
|
else fail "the approval question:$_paqbad"; fi
|
|
8032
|
+
# ...and the way to it. B-2 (a real `auto` session, 2026-10-17): /crew-ship asked for /crew-approve before any commit
|
|
8033
|
+
# was refused, so the refusal that prints the question never came and the click was never offered; telling the agent
|
|
8034
|
+
# to try the commit first did not help either (§4.4 says not to, and it did not). agent-outcome.sh now hands the
|
|
8035
|
+
# question over when crew-commit-agent is called; the text pinned here only says to ask it.
|
|
8036
|
+
_cqa="$AGENTS/crew-commit-agent.md"; _cqs="$SKILLS/crew-ship/SKILL.md"
|
|
8037
|
+
if [ ! -f "$_cqa" ] || [ ! -f "$_cqs" ]; then skip scope "the way to the approval question: crew-commit-agent or crew-ship is not installed here"
|
|
8038
|
+
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
|
|
8039
|
+
pass "in auto the commit agent and /crew-ship ask the one-click question Crewforth hands over, before asking for /crew-approve"
|
|
8040
|
+
else fail "the commit agent or /crew-ship asks for /crew-approve before the one-click question Crewforth hands over (B-2)"; fi
|
|
8041
|
+
# The hand-over itself, run: agent-outcome.sh on PostToolUse of an Agent call. In `auto`, for crew-commit-agent, with
|
|
8042
|
+
# both halves of the question wired and no PermissionRequest hook, it adds the fixed question (_CREW_Q_JSON, read from
|
|
8043
|
+
# prompt-approval.sh) to the session's context; in any other mode, for any other agent, or with such a hook, nothing.
|
|
8044
|
+
_aq="$(mktemp -d)"; _aq="$(cd -P "$_aq" && pwd)"; mkdir -p "$_aq/p/.claude/state" "$_aq/ccd" "$_aq/home"
|
|
8045
|
+
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"
|
|
8046
|
+
_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"
|
|
8047
|
+
( 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 ); }
|
|
8048
|
+
_aqbad=""; _aqo="$(_aq_run auto crew-commit-agent)"
|
|
8049
|
+
case "$_aqo" in *"$_paq_text"*) ;; *) _aqbad="$_aqbad [auto, crew-commit-agent: the question was not handed over]" ;; esac
|
|
8050
|
+
if [ -n "$JSONQ" ] && [ -n "$_aqo" ]; then printf '%s' "$_aqo" | json_ok || _aqbad="$_aqbad [its output is not JSON]"; fi
|
|
8051
|
+
for _aqc in default:crew-commit-agent dontAsk:crew-commit-agent acceptEdits:crew-commit-agent auto:crew-review-agent; do
|
|
8052
|
+
[ -z "$(_aq_run "${_aqc%%:*}" "${_aqc#*:}")" ] || _aqbad="$_aqbad [${_aqc%%:*}, ${_aqc#*:}: handed the question over]"
|
|
8053
|
+
done
|
|
8054
|
+
printf '%s' '{"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"x"}]}]}}' > "$_aq/ccd/settings.json"
|
|
8055
|
+
[ -z "$(_aq_run auto crew-commit-agent)" ] || _aqbad="$_aqbad [a PermissionRequest hook is wired: the question was still handed over]"
|
|
8056
|
+
rm -rf "$_aq"
|
|
8057
|
+
[ -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" \
|
|
8058
|
+
|| fail "the one-click question hand-over:$_aqbad"
|
|
8059
|
+
# ONE WAY TO ASK in the refusal (2026-10-18, a real `auto` session on rc.3: the refusal offered the question and the
|
|
8060
|
+
# typed command as equals, and the session told the user to type 7 times in 9). With the question open the typed way is
|
|
8061
|
+
# printed only as the fallback for a question that cannot be asked; with it closed (dontAsk), only the typed way.
|
|
8062
|
+
_pa_new
|
|
8063
|
+
( 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 )
|
|
8064
|
+
_oabad=""
|
|
8065
|
+
_paq_gate auto 'git commit -m x'
|
|
8066
|
+
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
|
|
8067
|
+
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
|
|
8068
|
+
_paq_gate dontAsk 'git commit -m x'
|
|
8069
|
+
case "$_pae" in *'ASK THE USER NOW'*'/crew-approve'*) ;; *) _oabad="$_oabad [dontAsk: the typed way is not asked for]" ;; esac
|
|
8070
|
+
case "$_pae" in *'"questions":['*|*'ONLY IF THAT QUESTION'*) _oabad="$_oabad [dontAsk: the question is offered]" ;; esac
|
|
8071
|
+
_pa_new
|
|
8072
|
+
[ -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" \
|
|
8073
|
+
|| fail "one way to ask:$_oabad"
|
|
7947
8074
|
|
|
7948
8075
|
# A PermissionRequest HOOK, wherever this can read one, closes the question AND the gate's own prompt. Each source is
|
|
7949
8076
|
# planted alone in an otherwise clean fixture; the clean fixture itself must answer "none" (calibration).
|
|
@@ -8268,21 +8395,26 @@ done <<'V2'
|
|
|
8268
8395
|
2|crew-backend-expert|sonnet|prisma/schema.prisma|feature|none|
|
|
8269
8396
|
2|crew-backend-expert|sonnet|config/SECRETS_local.env|text|none|
|
|
8270
8397
|
0|crew-backend-expert|sonnet|docs/author.md, src/cryptography/hash.ts|feature|none|
|
|
8271
|
-
|
|
8272
|
-
|
|
8398
|
+
0|crew-backend-expert|sonnet|legal/terms-of-service.md|text|none|
|
|
8399
|
+
0|crew-backend-expert|sonnet|docs/privacy-policy.md|text|none|
|
|
8273
8400
|
2|crew-backend-expert|sonnet|src/PrivacyController.cs|feature|none|
|
|
8274
8401
|
2|crew-backend-expert|sonnet|src/consent/banner.ts|feature|none|
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8402
|
+
0|crew-backend-expert|sonnet|content/terms/index.md|text|none|
|
|
8403
|
+
0|crew-backend-expert|sonnet|fastlane/metadata/en-US/description.txt|text|none|
|
|
8404
|
+
0|crew-backend-expert|sonnet|store/listing.md|text|none|
|
|
8405
|
+
0|crew-backend-expert|sonnet|metadata/android/en-US/full_description.txt|text|none|
|
|
8279
8406
|
0|crew-backend-expert|opus|legal/terms-of-service.md|text|none|
|
|
8407
|
+
2|crew-backend-expert|haiku|legal/terms-of-service.md|text|none|
|
|
8408
|
+
2|crew-backend-expert|sonnet|legal/terms-of-service.md, src/legal/consent.ts|text|none|
|
|
8409
|
+
2|crew-backend-expert|sonnet|legal/terms-of-service.md|feature|none|
|
|
8280
8410
|
0|crew-backend-expert|sonnet|src/searchTerms.ts|feature|none|
|
|
8281
8411
|
0|crew-backend-expert|sonnet|src/store/index.ts|feature|none|
|
|
8282
8412
|
0|crew-backend-expert|sonnet|fastlane/metadata/en-US/images/icon.png|text|none|
|
|
8283
8413
|
0|crew-backend-expert|sonnet|docs/legalese.md|text|none|
|
|
8284
8414
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|none|
|
|
8285
|
-
|
|
8415
|
+
0|crew-backend-expert|haiku|src/ui/a.ts|text|none|
|
|
8416
|
+
2|crew-backend-expert|haiku|src/ui/a.ts|fix-known|none|
|
|
8417
|
+
0|crew-commit-agent|haiku|src/ui/a.ts|fix-known|none|
|
|
8286
8418
|
0|crew-backend-expert|sonnet|src/ui/a.ts|text|none|
|
|
8287
8419
|
0|crew-backend-expert|haiku|src/ui/a.ts|text|true|
|
|
8288
8420
|
0|crew-test-expert|haiku|src/ui/**|test-run|true|
|
|
@@ -8314,8 +8446,8 @@ done <<'V2'
|
|
|
8314
8446
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|git commit -m x|
|
|
8315
8447
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|rm -rf /|
|
|
8316
8448
|
V2
|
|
8317
|
-
if [ "$_v2n" !=
|
|
8318
|
-
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) (
|
|
8449
|
+
if [ "$_v2n" != 65 ]; then fail "FIXTURE: the card table has $_v2n rows, not 65"
|
|
8450
|
+
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)"
|
|
8319
8451
|
else fail "card table:$_v2b"; fi
|
|
8320
8452
|
# The card's floor is also the default: a call above it runs ON it (the gate hands back the same input with `model`
|
|
8321
8453
|
# replaced, PreToolUse updatedInput), unless the card's next line says `above: <why>`. Measured on a real project:
|
|
@@ -8396,20 +8528,46 @@ for _pr in 'Do the work.' 'Please do this:\nfiles: a\nchange: feature\nverify: t
|
|
|
8396
8528
|
done
|
|
8397
8529
|
[ -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" \
|
|
8398
8530
|
|| fail "a task without a proper card passed:$_v2b"
|
|
8531
|
+
# The card on one line (2026-10-18: 17 of 39 card refusals on this machine were `files: … · change: … · verify: …`). It is
|
|
8532
|
+
# read by its labels and held to the same rules: what passes on three lines passes on one, and what fails there fails.
|
|
8533
|
+
_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"
|
|
8534
|
+
( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/one.json" >"$_v2/oneout" 2>"$_v2/err" ); _rc=$?; }
|
|
8535
|
+
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' \
|
|
8536
|
+
'Card: files: src/ui/a.ts, src/ui/b.ts · change: feature · verify: true.' 'Card — files: src/ui/a.ts · change: text · verify: none'; do
|
|
8537
|
+
_v2one "$_pr" sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [refused: $_pr → $_rc $(head -c 120 "$_v2/err")]"
|
|
8538
|
+
done
|
|
8539
|
+
_v2one 'files: src/payments/p.ts · change: feature · verify: true' sonnet
|
|
8540
|
+
{ [ "$_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)]"
|
|
8541
|
+
_v2one 'files: src/ui/a.ts · change: feature · verify: true · above: the parser is subtle' opus
|
|
8542
|
+
{ [ "$_rc" = 0 ] && ! grep -q 'runs on sonnet' "$_v2/oneout"; } || _v2b="$_v2b [above: on the same line was not read (rc $_rc)]"
|
|
8543
|
+
_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]"
|
|
8544
|
+
_v2one 'files: src/ui/a.ts · change: architecture (planning only) · verify: none' sonnet
|
|
8545
|
+
{ [ "$_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)]"
|
|
8546
|
+
_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]"
|
|
8547
|
+
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'; do
|
|
8548
|
+
_v2one "$_pr" sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [passed: $_pr → $_rc]"
|
|
8549
|
+
done
|
|
8550
|
+
[ -z "$_v2b" ] && pass "a card on one line (· ; | separators, a Card: 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" \
|
|
8551
|
+
|| fail "the one-line card:$_v2b"
|
|
8399
8552
|
|
|
8400
8553
|
# ---- at write time: the card can leave a file out --------------------------------------------------------------
|
|
8401
8554
|
_v2new; _v2b=""
|
|
8402
8555
|
_v2call crew-backend-expert sonnet 'src/ui/a.ts' refactor true; _v2start ag-son
|
|
8403
8556
|
_v2call crew-backend-expert opus 'src/payments/p.ts' feature none; _v2start ag-op
|
|
8557
|
+
_v2call crew-docs-agent sonnet 'legal/terms.md' text none; _v2start ag-txt crew-docs-agent
|
|
8404
8558
|
_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"
|
|
8405
8559
|
_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"
|
|
8406
8560
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-op","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b opus-agent-refused:$_v2wrc"
|
|
8407
8561
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-unknown","agent_type":"general-purpose"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b unknown-model-wrote-payments:$_v2wrc"
|
|
8408
8562
|
_v2write "$_v2p/src/payments/p.ts"; [ "$_v2wrc" = 0 ] || _v2b="$_v2b the-session-refused:$_v2wrc"
|
|
8563
|
+
# A text change on sonnet writes prose on a critical path; not code there, and not prose for any other change.
|
|
8564
|
+
_v2write "$_v2p/legal/terms.md" ',"agent_id":"ag-txt","agent_type":"crew-docs-agent"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b sonnet-text-refused-legal-prose:$_v2wrc"
|
|
8565
|
+
_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"
|
|
8566
|
+
_v2write "$_v2p/legal/terms.md" ',"agent_id":"ag-son","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b sonnet-refactor-wrote-legal-prose:$_v2wrc"
|
|
8409
8567
|
mkdir -p "$_v2/t/subagents"; printf '{"type":"assistant","message":{"model":"claude-opus-5-5"}}\n' > "$_v2/t/subagents/agent-ag-gp.jsonl"
|
|
8410
8568
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-gp","agent_type":"general-purpose"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b opus-by-its-transcript-refused:$_v2wrc"
|
|
8411
8569
|
_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"
|
|
8412
|
-
[ -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" \
|
|
8570
|
+
[ -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" \
|
|
8413
8571
|
|| fail "write-time check:$_v2b"
|
|
8414
8572
|
# B2 (K7): the session's own write to a critical path is not refused, and it is recorded: one NOTE line in the gate
|
|
8415
8573
|
# log naming the path and the rule. A write elsewhere, one outside the project, and one with routing off add none.
|
|
@@ -8443,6 +8601,32 @@ _v2call crew-backend-expert sonnet 'docs/security.md' text none; [ "$_v2rc" = 0
|
|
|
8443
8601
|
_v2call crew-backend-expert sonnet 'Api/Services/AuthorService.cs' feature none; [ "$_v2rc" = 0 ] || _v2b="$_v2b AuthorService-read-as-auth"
|
|
8444
8602
|
[ -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" \
|
|
8445
8603
|
|| fail "critical paths:$_v2b"
|
|
8604
|
+
# V5: an organisation preset (eval/lib/preset.sh) is a block at the top of crew-model-rules. The project's own lines
|
|
8605
|
+
# stay below it and win where the file's rule lets them; applying again changes nothing; an invalid one writes nothing.
|
|
8606
|
+
_v2new; _v2b=""; PSH="$ROOT/eval/lib/preset.sh"; _psr="$_v2p/.claude/crew-model-rules"; _psx="$(mktemp -d)"
|
|
8607
|
+
printf '# ours\n- infra\n' > "$_psr"
|
|
8608
|
+
( cd "$_v2p" && bash "$PSH" --apply strict ) >/dev/null 2>&1 || _v2b="$_v2b [strict did not apply]"
|
|
8609
|
+
{ IFS= read -r _l1 < "$_psr"; case "$_l1" in '# ---- preset: strict '*) ;; *) false ;; esac; } || _v2b="$_v2b [first line: ${_l1:-none}]"
|
|
8610
|
+
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]"
|
|
8611
|
+
[ "$(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]"
|
|
8612
|
+
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]"
|
|
8613
|
+
[ "$( cd "$_v2p" && bash "$PSH" --show )" = strict ] || _v2b="$_v2b [--show]"
|
|
8614
|
+
# The gate follows it: review on sonnet is refused under strict, and the project's `- infra` undoes the preset's `+ infra`.
|
|
8615
|
+
_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]"
|
|
8616
|
+
_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]"
|
|
8617
|
+
_v2call crew-backend-expert sonnet 'deploy/run.sh' feature 'test -d src'; [ "$_v2rc" = 2 ] || _v2b="$_v2b [deploy is not critical under strict: rc $_v2rc]"
|
|
8618
|
+
( 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]"
|
|
8619
|
+
# Invalid presets: each refused with rc 1, the file untouched.
|
|
8620
|
+
cp "$_psr" "$_psx/b"; _bad=0
|
|
8621
|
+
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
|
|
8622
|
+
printf '%s' "$_bj" > "$_psx/bad.json"; ( cd "$_v2p" && bash "$PSH" --apply "$_psx/bad.json" ) >/dev/null 2>&1; _rc=$?
|
|
8623
|
+
{ [ "$_rc" = 1 ] && cmp -s "$_psr" "$_psx/b"; } || { _bad=$((_bad+1)); _v2b="$_v2b [accepted or wrote: $_bj rc $_rc]"; }
|
|
8624
|
+
done
|
|
8625
|
+
( cd "$_v2p" && bash "$PSH" --apply no-such-preset ) >/dev/null 2>&1 && _v2b="$_v2b [a preset that is not there applied]"
|
|
8626
|
+
_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]"
|
|
8627
|
+
[ -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" \
|
|
8628
|
+
|| fail "presets:$_v2b"
|
|
8629
|
+
rm -rf "$_psx"
|
|
8446
8630
|
|
|
8447
8631
|
# ---- 3. verify, one fix inside the agent, then one model up, then the user -----------------------------------
|
|
8448
8632
|
_v2new; _v2b=""
|
|
@@ -8773,13 +8957,16 @@ grep -q '^verify=none$' "$(ls -t "$_v2p/.claude/state/crew-model/pending/"* 2>/d
|
|
|
8773
8957
|
mkdir -p "$_v2p/.claude/state/crew-model/results"
|
|
8774
8958
|
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"
|
|
8775
8959
|
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"
|
|
8960
|
+
# The other way of saying "nothing allows it" (default mode), for a command in a sub-folder: counted, and offered as
|
|
8961
|
+
# the rule that matches it, the command without its `cd` (measured on Windows: the line with the cd in it did not).
|
|
8962
|
+
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"
|
|
8776
8963
|
if [ -f "$ROOT/eval/doctor.sh" ]; then
|
|
8777
8964
|
mkdir -p "$_v2p/.claude/eval"; cp -R "$ROOT/eval/." "$_v2p/.claude/eval/" 2>/dev/null
|
|
8778
8965
|
_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' )"
|
|
8779
|
-
case "$_dd" in *'
|
|
8966
|
+
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 ;;
|
|
8780
8967
|
*) _v2b="$_v2b [doctor: ${_dd:-no line}]" ;; esac
|
|
8781
8968
|
fi
|
|
8782
|
-
[ -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 as
|
|
8969
|
+
[ -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" \
|
|
8783
8970
|
|| fail "verify in a sub-folder:$_v2b"
|
|
8784
8971
|
# Which record is whose. Two calls of one type made together are matched by order, which can be wrong; the card in
|
|
8785
8972
|
# the agent's own transcript settles it.
|
|
@@ -8853,7 +9040,7 @@ _kn="$(awk -F'\t' '$2 == "k2" { print $5 "/" $13 }' "$_v2st/../model-outcomes.ts
|
|
|
8853
9040
|
# 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
|
|
8854
9041
|
# the write-time check; run_in_background named twice is refused.
|
|
8855
9042
|
_v2new; _v2b=""; _V2ENV="CREW_GATE_LOG=$_v2/gl.tsv"; : > "$_v2/gl.tsv"
|
|
8856
|
-
_v2call crew-database-expert sonnet 'src/payments/p.ts' feature none; _v2call crew-backend-expert haiku 'src/ui/a.ts'
|
|
9043
|
+
_v2call crew-database-expert sonnet 'src/payments/p.ts' feature none; _v2call crew-backend-expert haiku 'src/ui/a.ts' fix-known none
|
|
8857
9044
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"nobody-known","agent_type":"x"'
|
|
8858
9045
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"","agent_type":"x"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b an-empty-agent-id-opened-the-check:$_v2wrc"
|
|
8859
9046
|
_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"
|
|
@@ -8997,11 +9184,11 @@ _v2call crew-backend-expert sonnet 'src/payments/p.ts' feature none; [ "$_v2rc"
|
|
|
8997
9184
|
# What a lowering cannot reach: the floors the card and the agent carry. Two raised floors are planted and lowered;
|
|
8998
9185
|
# the model below still fails where another rule holds it, and passes where only the lowered floor did.
|
|
8999
9186
|
_v2new; _v2b=""; mkdir -p "$_v2p/.claude/state"
|
|
9000
|
-
printf 'crew-backend-expert\
|
|
9001
|
-
_o="$(_v2say '/crew-loosen crew-backend-expert
|
|
9187
|
+
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"
|
|
9188
|
+
_o="$(_v2say '/crew-loosen crew-backend-expert fix-known normal')$(_v2say '/crew-loosen crew-database-expert feature normal')"
|
|
9002
9189
|
case "$_o" in *'from sonnet to haiku'*'from opus to sonnet'*) ;; *) _v2b="$_v2b FIXTURE:the-two-floors-were-not-lowered" ;; esac
|
|
9003
|
-
_v2call crew-backend-expert haiku 'src/ui/f1.ts'
|
|
9004
|
-
_v2call crew-backend-expert haiku 'src/ui/f1.ts'
|
|
9190
|
+
_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"
|
|
9191
|
+
_v2call crew-backend-expert haiku 'src/ui/f1.ts' fix-known none; [ "$_v2rc" = 2 ] || _v2b="$_v2b verify-none-went-to-haiku-after-a-lowering"
|
|
9005
9192
|
_v2call crew-database-expert sonnet 'src/ui/f2.ts' feature true; [ "$_v2rc" = 0 ] || _v2b="$_v2b the-lowered-class-is-still-refused-on-sonnet:$_v2rc"
|
|
9006
9193
|
_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"
|
|
9007
9194
|
_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"
|
|
@@ -11247,7 +11434,7 @@ sec "== 14c) the 3.0 rename left no old name behind — outside history and the
|
|
|
11247
11434
|
if [ -n "$SGR" ] && [ -d "$SGR/packaging" ] && [ -f "$SGR/VERSION" ] && [ -d "$SGR/kit" ] && [ -f "$SGR/packaging/build-plugin.sh" ]; then
|
|
11248
11435
|
RN_ALLOW='CHANGELOG.md 196 history: every entry before 3.0 keeps the name it shipped under
|
|
11249
11436
|
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
|
|
11250
|
-
evals/results/*
|
|
11437
|
+
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
|
|
11251
11438
|
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
|
|
11252
11439
|
bin/cli.js 4 migration: add accepts a typed <x>-csk and moves an add record written under the old names
|
|
11253
11440
|
evals/run.sh 4 compat: reads the 2.x trusted eval parent when the 3.0 one is absent — removed in 4.0
|
|
@@ -404,6 +404,24 @@ case "$EV" in
|
|
|
404
404
|
fi
|
|
405
405
|
fi
|
|
406
406
|
fi
|
|
407
|
+
# THE ONE-CLICK APPROVAL, OFFERED WHEN THE COMMIT MESSAGE IS ASKED FOR (B-2). In `auto` the click used to reach the
|
|
408
|
+
# user only inside a refused commit, and a session that asked for /crew-approve first never tried one (§4.4 says
|
|
409
|
+
# not to), so it was never offered. A call to crew-commit-agent is when the message is on its way: the session is
|
|
410
|
+
# told then, in foreground and background alike (a background launch answers here at once). Whether the question
|
|
411
|
+
# is open, and its words, come from prompt-approval.sh, as the refusal's do. Its verify is `none` (change: text),
|
|
412
|
+
# so no outcome below is lost by answering here; the model it ran on is already kept above.
|
|
413
|
+
_json_slice "$INPUT" subagent_type >/dev/null
|
|
414
|
+
if [ "$_JS" = crew-commit-agent ] && [ -f "$_ao_here/prompt-approval.sh" ]; then
|
|
415
|
+
_json_slice "$INPUT" permission_mode >/dev/null; _AOPM="$_JS"
|
|
416
|
+
if [ "$_AOPM" = auto ]; then
|
|
417
|
+
declare -F _crew_q_open >/dev/null 2>&1 || . "$_ao_here/prompt-approval.sh"
|
|
418
|
+
if declare -F _crew_q_open >/dev/null 2>&1 && _crew_q_open auto; then
|
|
419
|
+
_ao_json "Crewforth: when you show the user the commit message this agent proposes, ask with one click before anything else. $_CREW_Q_ASK {\"questions\":[$_CREW_Q_JSON]} Their answer is recorded by Crewforth itself (the line it adds says so); then run the commit it names. If that question is refused, the user can type /crew-approve instead, as the refusal says."
|
|
420
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_AOJ"
|
|
421
|
+
exit 0
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
407
425
|
R="$_AMD/results/$AID"
|
|
408
426
|
if [ ! -f "$R" ]; then
|
|
409
427
|
# No result yet: the agent was launched in the background (measured, 2.1.294 and 2.1.296: an Agent call that
|
package/kit/hooks/board-sync.sh
CHANGED
|
@@ -79,28 +79,7 @@ MSG="$(cat "$CACHE")
|
|
|
79
79
|
Board state above is a cached snapshot; $_BS (Bash tool, not PowerShell — or the user can type /crew-board sync) refreshes it."
|
|
80
80
|
|
|
81
81
|
# ONE PATH, no jq. The JSON is built here on every machine, so a Mac with jq and a Windows box without it emit the
|
|
82
|
-
# same bytes
|
|
83
|
-
|
|
84
|
-
# the quote, the backslash and the newline, so a tab in an item title produced JSON the CLI cannot parse
|
|
85
|
-
# (measured: jq rc=5) and the board silently vanished from the session — on exactly the no-jq machine this
|
|
86
|
-
# branch existed for. The trailing newline on the printf closes the last record, so a value ending in a newline
|
|
87
|
-
# keeps it. One awk, no subshell per character. A CR that ENDS a line is dropped on purpose: it is a CRLF line
|
|
88
|
-
# ending, not content, and MSYS gawk already drops it on read (text mode) while BSD awk keeps it — measured on
|
|
89
|
-
# Windows. Stripping it here makes every OS emit the same bytes; a CR inside a line is still escaped as \r.
|
|
90
|
-
ESC="$(printf '%s\n' "$MSG" | LC_ALL=C awk 'BEGIN { ORS = ""
|
|
91
|
-
for (i = 1; i < 32; i++) ctl[i] = sprintf("%c", i)
|
|
92
|
-
nm[8] = "\\b"; nm[9] = "\\t"; nm[12] = "\\f"; nm[13] = "\\r" }
|
|
93
|
-
NR > 1 { print "\\n" }
|
|
94
|
-
{ s = $0; sub(/\r$/, "", s)
|
|
95
|
-
gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s)
|
|
96
|
-
for (i = 1; i < 32; i++) {
|
|
97
|
-
if (i == 10 || !index(s, ctl[i])) continue
|
|
98
|
-
r = (i in nm) ? nm[i] : sprintf("\\u%04x", i)
|
|
99
|
-
out = ""; while ((p = index(s, ctl[i])) > 0) { out = out substr(s, 1, p - 1) r; s = substr(s, p + 1) }
|
|
100
|
-
s = out s
|
|
101
|
-
}
|
|
102
|
-
d = sprintf("%c", 127)
|
|
103
|
-
if (index(s, d)) { out = ""; while ((p = index(s, d)) > 0) { out = out substr(s, 1, p - 1) "\\u007f"; s = substr(s, p + 1) }; s = out s }
|
|
104
|
-
print s }')"
|
|
82
|
+
# same bytes, through Crewforth's one escaper (eval/lib/json-string.awk: jq-identical, measured; why, in its header).
|
|
83
|
+
ESC="$(printf '%s\n' "$MSG" | LC_ALL=C awk -f "$HERE/../eval/lib/json-string.awk")"
|
|
105
84
|
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$ESC"
|
|
106
85
|
exit 0
|