crewforth 3.2.0-rc.1 → 3.2.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -3
- package/README.md +2 -0
- package/VERSION +1 -1
- package/adopt.sh +55 -6
- package/bin/cli.js +3 -2
- package/kit/CLAUDE.md +6 -6
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/eval/doctor.sh +37 -3
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/lib/crew-map/narrative.mjs +42 -8
- package/kit/eval/lib/crew-map/project.mjs +3 -2
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +56 -4
- package/kit/eval/lib/crew-map/stacks.mjs +114 -8
- package/kit/eval/lib/crew-map/surface.mjs +62 -1
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +14 -4
- package/kit/eval/lib/statusline.sh +4 -19
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +251 -21
- package/kit/hooks/agent-outcome.sh +20 -9
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +114 -15
- package/kit/hooks/guard-bash.sh +36 -21
- package/kit/hooks/guard-write.sh +3 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/settings.json +12 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-map/SKILL.md +16 -5
- package/kit/skills/crew-review/SKILL.md +2 -1
- package/kit/studio/README.md +21 -10
- package/kit/studio/server/index.js +85 -22
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +104 -18
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/web/app.js +79 -11
- package/kit/studio/web/canvas.js +14 -2
- package/kit/studio/web/chat.js +114 -24
- package/kit/studio/web/graph-plan.js +3 -2
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +72 -3
- package/kit/studio/web/map.js +103 -10
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +40 -0
- package/kit/studio/web/tokens.css +6 -0
- package/package.json +1 -1
- package/start.sh +14 -1
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`
|
|
@@ -4492,6 +4532,41 @@ else fail "session-rehydrate git-hooks states read '$_got' (want '--UO---': no h
|
|
|
4492
4532
|
rm -rf "$RHG"
|
|
4493
4533
|
_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
4534
|
[ "$_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"
|
|
4535
|
+
# V3: just before a compaction, precompact-snapshot.sh writes the state of the work from the files and the gates;
|
|
4536
|
+
# after it, session-rehydrate gives that state back, to the same session only. The snapshot never blocks the
|
|
4537
|
+
# compaction (PreCompact exit 2 would): rc 0 and silent in every case, a broken one included.
|
|
4538
|
+
PCS="$HOOKS/precompact-snapshot.sh"
|
|
4539
|
+
if [ ! -x "$PCS" ]; then fail "precompact-snapshot.sh missing/not executable"
|
|
4540
|
+
else
|
|
4541
|
+
PCD="$(mktemp -d)"; PCD="$(cd -P "$PCD" && pwd)"; _pco="$(mktemp)"; _pcr="$(mktemp)" # outside the repository: a file there is uncommitted
|
|
4542
|
+
( 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
|
|
4543
|
+
mkdir -p "$PCD/.claude" "$PCD/.crewforth" "$PCD/docs"
|
|
4544
|
+
printf 'x\n' > "$PCD/plain.txt"; printf 'x\n' > "$PCD/odd\"na me.txt"; printf 'x\n' > "$PCD/ıİşğü.txt"
|
|
4545
|
+
printf '{\n "findings": [\n {\n "rule": "a"\n },\n {\n "rule": "b"\n }\n ]\n}\n' > "$PCD/.crewforth/review.json"
|
|
4546
|
+
printf 'BLOCK\t§4.2\tvendor name\t\n' > "$PCD/.git/crewforth-gate-log.tsv"
|
|
4547
|
+
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=$?
|
|
4548
|
+
_snap="$PCD/.git/crewforth-session-snapshot.json"; _sb=""
|
|
4549
|
+
[ -f "$_snap" ] && IFS= read -r _sb < "$_snap"
|
|
4550
|
+
_rs(){ printf '{"session_id":"%s","source":"%s","cwd":"%s"}' "$1" "$2" "$PCD" | CLAUDE_PROJECT_DIR="$PCD" bash "$HOOKS/session-rehydrate.sh" 2>/dev/null; }
|
|
4551
|
+
_same="$(_rs s-1 compact)"; _other="$(_rs s-2 compact)"; _start="$(_rs s-1 startup)"
|
|
4552
|
+
printf '%s' "$_same" > "$_pcr"
|
|
4553
|
+
_b=""
|
|
4554
|
+
[ "$_pcrc" = 0 ] && [ ! -s "$_pco" ] || _b="$_b [rc $_pcrc, printed: $(head -c 120 "$_pco")]"
|
|
4555
|
+
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}]"
|
|
4556
|
+
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
|
|
4557
|
+
case "$_same" in *'Before this context was compacted'*'review 2'*) ;; *) _b="$_b [not given back after compaction: ${_same:0:120}]" ;; esac
|
|
4558
|
+
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]"
|
|
4559
|
+
case "$_other$_start" in *'Before this context was compacted'*) _b="$_b [given to another session or on a plain start]" ;; esac
|
|
4560
|
+
# Fail-open: no repository, a cwd that is not there, no JSON at all, and the hook alone without eval/lib.
|
|
4561
|
+
_nr="$(mktemp -d)"; _alone="$(mktemp -d)"; cp "$PCS" "$_alone/precompact-snapshot.sh"
|
|
4562
|
+
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 $?]"
|
|
4563
|
+
printf '{"session_id":"s-9","cwd":"/nonexistent/x"}' | CLAUDE_PROJECT_DIR= bash "$PCS" >/dev/null 2>&1 || _b="$_b [missing cwd rc $?]"
|
|
4564
|
+
printf 'not json' | CLAUDE_PROJECT_DIR="$_nr" bash "$PCS" >/dev/null 2>&1 || _b="$_b [garbage rc $?]"
|
|
4565
|
+
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 $?]"
|
|
4566
|
+
[ -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" \
|
|
4567
|
+
|| fail "precompact snapshot:$_b"
|
|
4568
|
+
rm -rf "$PCD" "$_nr" "$_alone" "$_pco" "$_pcr"
|
|
4569
|
+
fi
|
|
4495
4570
|
# Asked of the entry that carries session-rehydrate, not of the file: skill-trust's entry has the same matcher.
|
|
4496
4571
|
_rw(){ _n="$(awk -v op=len -v path=hooks.SessionStart -f "$ROOT/eval/lib/settings-json.awk" "$1" 2>/dev/null)"; _i=0
|
|
4497
4572
|
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 +5046,8 @@ sec "== 7u) update prompt: ASKS about a release, never installs one, never spend
|
|
|
4971
5046
|
UPD="$(mktemp -d)"; mkdir -p "$UPD/.claude/.state" "$UPD/.claude/hooks"
|
|
4972
5047
|
printf '3.0.0\n' > "$UPD/.claude/VERSION"
|
|
4973
5048
|
UH="$HOOKS/session-update-check.sh"; cp "$UH" "$UPD/.claude/hooks/"
|
|
5049
|
+
# The hook reads and orders the install's version through eval/lib/version.sh, beside it in an install.
|
|
5050
|
+
mkdir -p "$UPD/.claude/eval/lib"; cp "$ROOT/eval/lib/version.sh" "$UPD/.claude/eval/lib/"
|
|
4974
5051
|
uc(){ ( printf '{"hook_event_name":"SessionStart","source":"startup","cwd":"%s"}' "$UPD" \
|
|
4975
5052
|
| env -u CI -u CREW_NO_UPDATE_CHECK -u CREW_LANG CLAUDE_CODE_SESSION_ATTENDED=1 LANG=C LC_ALL= LC_MESSAGES= \
|
|
4976
5053
|
CLAUDE_PROJECT_DIR="$UPD" CREW_UPDATE_URL="${1:-http://10.255.255.1/blackhole}" bash "$UH" 2>/dev/null ); }
|
|
@@ -5099,10 +5176,25 @@ cp "$UH" "$UPD/uh-twin.sh"; printf 'npx --yes crewforth@latest update --here --y
|
|
|
5099
5176
|
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
5177
|
else fail "the update hook contains an installer call ($_ni line(s)) — it must only ask, or the twin was not caught"; fi
|
|
5101
5178
|
|
|
5179
|
+
# 12b) AN INSTALLED RC IS OFFERED ITS FINAL. The installer copies VERSION into the project, and an rc's says
|
|
5180
|
+
# 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
|
|
5181
|
+
# rc's user was never told the final shipped). An older final is not offered, and what the network answers is
|
|
5182
|
+
# still held to X.Y.Z: an rc off the network is no version.
|
|
5183
|
+
printf '3.2.0-rc.1\n' > "$UPD/.claude/VERSION"
|
|
5184
|
+
ucache 3.2.0
|
|
5185
|
+
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" ;;
|
|
5186
|
+
*) fail "an installed 3.2.0-rc.1 was not offered the final 3.2.0 (got: $(uc | head -c 120))" ;; esac
|
|
5187
|
+
ucache 3.1.1
|
|
5188
|
+
[ -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"
|
|
5189
|
+
ucache 3.2.0-rc.2
|
|
5190
|
+
[ -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"
|
|
5191
|
+
printf '3.0.0\n' > "$UPD/.claude/VERSION"; ustate
|
|
5192
|
+
|
|
5102
5193
|
# 13) THE PLUGIN EDITION. Its version is its own manifest, its release is the marketplace repo's copy, its state is
|
|
5103
5194
|
# user-level (a plugin serves every project), and its Update path is the plugin command — measured to work from
|
|
5104
5195
|
# inside a session, applying on restart.
|
|
5105
|
-
PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks"; cp "$UH" "$PLG/hooks/"
|
|
5196
|
+
PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks" "$PLG/eval/lib"; cp "$UH" "$PLG/hooks/"
|
|
5197
|
+
cp "$ROOT/eval/lib/version.sh" "$PLG/eval/lib/"
|
|
5106
5198
|
printf '{"name":"crewforth","version":"3.0.0"}\n' > "$PLG/.claude-plugin/plugin.json"
|
|
5107
5199
|
XDG="$UPD/xdg"; mkdir -p "$XDG/crewforth"
|
|
5108
5200
|
printf '3.1.0 %s\n' "$(date +%s)" > "$XDG/crewforth/update-check"
|
|
@@ -6363,6 +6455,14 @@ GNI="$(mktemp -d)"; ( cd "$GNI" && git init -q . && mkdir -p .claude )
|
|
|
6363
6455
|
( cd "$GNI" && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
|
|
6364
6456
|
[ -e "$GNI/.claude/gate-log.tsv" ] && fail "gate log wrote into a repo where the path is not gitignored" \
|
|
6365
6457
|
|| pass "gate log declines a path git would track"
|
|
6458
|
+
# ...and since 3.2.0 writes inside the repository's .git instead, which is never committed: a project that tracks
|
|
6459
|
+
# .claude/ had no record of a refusal at all (seen on a real project: no 🛡, no note of a lowered model).
|
|
6460
|
+
# The readers find it there (eval/lib/gate-log.sh, the one rule), and `git status` stays clean.
|
|
6461
|
+
_gni_read="$( cd "$GNI" && . "$ROOT/eval/lib/gate-log.sh" && crew_gate_log_read && printf '%s' "$_CREW_GL" )"
|
|
6462
|
+
_gni_st="$( cd "$GNI" && git status --porcelain --untracked-files=all 2>/dev/null | grep -c gate-log )"
|
|
6463
|
+
{ [ -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 ]; } \
|
|
6464
|
+
&& pass "where git would track it, the gate log goes inside .git: written, found by the readers, and git status stays clean" \
|
|
6465
|
+
|| 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
6466
|
( cd "$GNI" && echo '.claude/' > .gitignore && gjson 'git reset --hard' | bash "$ROOT/hooks/guard-bash.sh" >/dev/null 2>&1 )
|
|
6367
6467
|
[ -s "$GNI/.claude/gate-log.tsv" ] && pass "gate log writes once the path is gitignored" \
|
|
6368
6468
|
|| fail "gate log stayed silent even though the path is ignored"
|
|
@@ -8218,7 +8318,7 @@ _v2new(){ rm -rf "$_v2p" "$_v2/home"; mkdir -p "$_v2/home" "$_v2p/.claude" "$_v2
|
|
|
8218
8318
|
_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
8319
|
# $1 agent, $2 model, $3 files, $4 change, $5 verify, $6 extra fields -> _v2rc, stderr in $_v2/err
|
|
8220
8320
|
_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"
|
|
8321
|
+
( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/c.json" >"$_v2/out" 2>"$_v2/err" ); _v2rc=$?; }
|
|
8222
8322
|
_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
8323
|
_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
8324
|
_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 +8351,26 @@ done <<'V2'
|
|
|
8251
8351
|
2|crew-backend-expert|sonnet|prisma/schema.prisma|feature|none|
|
|
8252
8352
|
2|crew-backend-expert|sonnet|config/SECRETS_local.env|text|none|
|
|
8253
8353
|
0|crew-backend-expert|sonnet|docs/author.md, src/cryptography/hash.ts|feature|none|
|
|
8254
|
-
|
|
8255
|
-
|
|
8354
|
+
0|crew-backend-expert|sonnet|legal/terms-of-service.md|text|none|
|
|
8355
|
+
0|crew-backend-expert|sonnet|docs/privacy-policy.md|text|none|
|
|
8256
8356
|
2|crew-backend-expert|sonnet|src/PrivacyController.cs|feature|none|
|
|
8257
8357
|
2|crew-backend-expert|sonnet|src/consent/banner.ts|feature|none|
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8358
|
+
0|crew-backend-expert|sonnet|content/terms/index.md|text|none|
|
|
8359
|
+
0|crew-backend-expert|sonnet|fastlane/metadata/en-US/description.txt|text|none|
|
|
8360
|
+
0|crew-backend-expert|sonnet|store/listing.md|text|none|
|
|
8361
|
+
0|crew-backend-expert|sonnet|metadata/android/en-US/full_description.txt|text|none|
|
|
8262
8362
|
0|crew-backend-expert|opus|legal/terms-of-service.md|text|none|
|
|
8363
|
+
2|crew-backend-expert|haiku|legal/terms-of-service.md|text|none|
|
|
8364
|
+
2|crew-backend-expert|sonnet|legal/terms-of-service.md, src/legal/consent.ts|text|none|
|
|
8365
|
+
2|crew-backend-expert|sonnet|legal/terms-of-service.md|feature|none|
|
|
8263
8366
|
0|crew-backend-expert|sonnet|src/searchTerms.ts|feature|none|
|
|
8264
8367
|
0|crew-backend-expert|sonnet|src/store/index.ts|feature|none|
|
|
8265
8368
|
0|crew-backend-expert|sonnet|fastlane/metadata/en-US/images/icon.png|text|none|
|
|
8266
8369
|
0|crew-backend-expert|sonnet|docs/legalese.md|text|none|
|
|
8267
8370
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|none|
|
|
8268
|
-
|
|
8371
|
+
0|crew-backend-expert|haiku|src/ui/a.ts|text|none|
|
|
8372
|
+
2|crew-backend-expert|haiku|src/ui/a.ts|fix-known|none|
|
|
8373
|
+
0|crew-commit-agent|haiku|src/ui/a.ts|fix-known|none|
|
|
8269
8374
|
0|crew-backend-expert|sonnet|src/ui/a.ts|text|none|
|
|
8270
8375
|
0|crew-backend-expert|haiku|src/ui/a.ts|text|true|
|
|
8271
8376
|
0|crew-test-expert|haiku|src/ui/**|test-run|true|
|
|
@@ -8297,9 +8402,48 @@ done <<'V2'
|
|
|
8297
8402
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|git commit -m x|
|
|
8298
8403
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|rm -rf /|
|
|
8299
8404
|
V2
|
|
8300
|
-
if [ "$_v2n" !=
|
|
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) (
|
|
8405
|
+
if [ "$_v2n" != 65 ]; then fail "FIXTURE: the card table has $_v2n rows, not 65"
|
|
8406
|
+
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
8407
|
else fail "card table:$_v2b"; fi
|
|
8408
|
+
# The card's floor is also the default: a call above it runs ON it (the gate hands back the same input with `model`
|
|
8409
|
+
# replaced, PreToolUse updatedInput), unless the card's next line says `above: <why>`. Measured on a real project:
|
|
8410
|
+
# 73 of 186 crew tasks ran above their floor and none on haiku; measured on Claude Code 2.1.299: the rewrite holds.
|
|
8411
|
+
_v2new; _v2b=""
|
|
8412
|
+
_v2last(){ ls -t "$_v2p/.claude/state/crew-model/pending/"* 2>/dev/null | head -1; }
|
|
8413
|
+
_v2call crew-review-agent opus 'src/ui/a.ts' audit none ',"run_in_background":true,"isolation":"worktree"'
|
|
8414
|
+
_o="$(cat "$_v2/out" 2>/dev/null)"; _r="$(_v2last)"
|
|
8415
|
+
case "$_o" in *'"permissionDecision":"allow"'*'"updatedInput":'*) ;; *) _v2b="$_v2b [above the floor, no above: -> no rewrite: rc=$_v2rc ${_o:0:80}]" ;; esac
|
|
8416
|
+
_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')"
|
|
8417
|
+
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
|
|
8418
|
+
case "$_u" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [the rewrite is not to the floor: ${_u:0:120}]" ;; esac
|
|
8419
|
+
{ 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)]"
|
|
8420
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8421
|
+
_v2call crew-review-agent opus 'src/ui/a.ts' audit 'none\nabove: the audit covers the whole session layer'
|
|
8422
|
+
{ [ "$_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")]"
|
|
8423
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8424
|
+
_v2call crew-review-agent sonnet 'src/ui/a.ts' audit none
|
|
8425
|
+
{ [ "$_v2rc" = 0 ] && [ ! -s "$_v2/out" ]; } || _v2b="$_v2b [a call on its floor was rewritten: $(head -c 60 "$_v2/out")]"
|
|
8426
|
+
_v2call crew-frontend-expert sonnet 'src/ui/a.ts' text 'test -d src'
|
|
8427
|
+
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
|
|
8428
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8429
|
+
# Not a crew agent: no card and no floor, only the expensive end is held. opus or fable with no `above:` line in the
|
|
8430
|
+
# task runs on sonnet; with one it stays; a call that names no model, or sonnet, is not touched.
|
|
8431
|
+
_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 ' '; }
|
|
8432
|
+
case "$(_v2o '' ',"model":"opus"')" in *'"updatedInput":'*'"model":"sonnet"'*) ;; *) _v2b="$_v2b [general-purpose on opus was not run on sonnet]" ;; esac
|
|
8433
|
+
[ -z "$(_v2o '\nabove: a design question across the codebase' ',"model":"opus"')" ] || _v2b="$_v2b [above: did not keep opus for general-purpose]"
|
|
8434
|
+
[ -z "$(_v2o '' ',"model":"sonnet"')" ] || _v2b="$_v2b [a call naming sonnet was rewritten]"
|
|
8435
|
+
# Naming no model: general-purpose takes the session's model, so it is given sonnet (CLAUDE_CODE_SUBAGENT_MODEL in
|
|
8436
|
+
# the project's settings env did not do it, measured on 2.1.299), unless the session is on haiku; an agent whose
|
|
8437
|
+
# definition names its own model is left to it; a project agent with no model line is treated like general-purpose.
|
|
8438
|
+
_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 ' '; }
|
|
8439
|
+
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"
|
|
8440
|
+
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"
|
|
8441
|
+
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
|
|
8442
|
+
[ -z "$(_v2ot general-purpose "$_v2/tr-haiku.jsonl")" ] || _v2b="$_v2b [a haiku session's general-purpose was raised to sonnet]"
|
|
8443
|
+
[ -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]"
|
|
8444
|
+
case "$(_v2ot mine "$_v2/tr-opus.jsonl")" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [a project agent with no model line was not given sonnet]" ;; esac
|
|
8445
|
+
[ -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" \
|
|
8446
|
+
|| fail "model on the floor:$_v2b"
|
|
8303
8447
|
# The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
|
|
8304
8448
|
# left the field out and was launched in the background is told so when the launch comes back.
|
|
8305
8449
|
_v2new; _v2b=""
|
|
@@ -8345,15 +8489,20 @@ done
|
|
|
8345
8489
|
_v2new; _v2b=""
|
|
8346
8490
|
_v2call crew-backend-expert sonnet 'src/ui/a.ts' refactor true; _v2start ag-son
|
|
8347
8491
|
_v2call crew-backend-expert opus 'src/payments/p.ts' feature none; _v2start ag-op
|
|
8492
|
+
_v2call crew-docs-agent sonnet 'legal/terms.md' text none; _v2start ag-txt crew-docs-agent
|
|
8348
8493
|
_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
8494
|
_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
8495
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-op","agent_type":"crew-backend-expert"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b opus-agent-refused:$_v2wrc"
|
|
8351
8496
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-unknown","agent_type":"general-purpose"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b unknown-model-wrote-payments:$_v2wrc"
|
|
8352
8497
|
_v2write "$_v2p/src/payments/p.ts"; [ "$_v2wrc" = 0 ] || _v2b="$_v2b the-session-refused:$_v2wrc"
|
|
8498
|
+
# A text change on sonnet writes prose on a critical path; not code there, and not prose for any other change.
|
|
8499
|
+
_v2write "$_v2p/legal/terms.md" ',"agent_id":"ag-txt","agent_type":"crew-docs-agent"'; [ "$_v2wrc" = 0 ] || _v2b="$_v2b sonnet-text-refused-legal-prose:$_v2wrc"
|
|
8500
|
+
_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"
|
|
8501
|
+
_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
8502
|
mkdir -p "$_v2/t/subagents"; printf '{"type":"assistant","message":{"model":"claude-opus-5-5"}}\n' > "$_v2/t/subagents/agent-ag-gp.jsonl"
|
|
8354
8503
|
_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
8504
|
_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" \
|
|
8505
|
+
[ -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
8506
|
|| fail "write-time check:$_v2b"
|
|
8358
8507
|
# 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
8508
|
# log naming the path and the rule. A write elsewhere, one outside the project, and one with routing off add none.
|
|
@@ -8387,6 +8536,32 @@ _v2call crew-backend-expert sonnet 'docs/security.md' text none; [ "$_v2rc" = 0
|
|
|
8387
8536
|
_v2call crew-backend-expert sonnet 'Api/Services/AuthorService.cs' feature none; [ "$_v2rc" = 0 ] || _v2b="$_v2b AuthorService-read-as-auth"
|
|
8388
8537
|
[ -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
8538
|
|| fail "critical paths:$_v2b"
|
|
8539
|
+
# V5: an organisation preset (eval/lib/preset.sh) is a block at the top of crew-model-rules. The project's own lines
|
|
8540
|
+
# stay below it and win where the file's rule lets them; applying again changes nothing; an invalid one writes nothing.
|
|
8541
|
+
_v2new; _v2b=""; PSH="$ROOT/eval/lib/preset.sh"; _psr="$_v2p/.claude/crew-model-rules"; _psx="$(mktemp -d)"
|
|
8542
|
+
printf '# ours\n- infra\n' > "$_psr"
|
|
8543
|
+
( cd "$_v2p" && bash "$PSH" --apply strict ) >/dev/null 2>&1 || _v2b="$_v2b [strict did not apply]"
|
|
8544
|
+
{ IFS= read -r _l1 < "$_psr"; case "$_l1" in '# ---- preset: strict '*) ;; *) false ;; esac; } || _v2b="$_v2b [first line: ${_l1:-none}]"
|
|
8545
|
+
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]"
|
|
8546
|
+
[ "$(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]"
|
|
8547
|
+
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]"
|
|
8548
|
+
[ "$( cd "$_v2p" && bash "$PSH" --show )" = strict ] || _v2b="$_v2b [--show]"
|
|
8549
|
+
# The gate follows it: review on sonnet is refused under strict, and the project's `- infra` undoes the preset's `+ infra`.
|
|
8550
|
+
_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]"
|
|
8551
|
+
_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]"
|
|
8552
|
+
_v2call crew-backend-expert sonnet 'deploy/run.sh' feature 'test -d src'; [ "$_v2rc" = 2 ] || _v2b="$_v2b [deploy is not critical under strict: rc $_v2rc]"
|
|
8553
|
+
( 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]"
|
|
8554
|
+
# Invalid presets: each refused with rc 1, the file untouched.
|
|
8555
|
+
cp "$_psr" "$_psx/b"; _bad=0
|
|
8556
|
+
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
|
|
8557
|
+
printf '%s' "$_bj" > "$_psx/bad.json"; ( cd "$_v2p" && bash "$PSH" --apply "$_psx/bad.json" ) >/dev/null 2>&1; _rc=$?
|
|
8558
|
+
{ [ "$_rc" = 1 ] && cmp -s "$_psr" "$_psx/b"; } || { _bad=$((_bad+1)); _v2b="$_v2b [accepted or wrote: $_bj rc $_rc]"; }
|
|
8559
|
+
done
|
|
8560
|
+
( cd "$_v2p" && bash "$PSH" --apply no-such-preset ) >/dev/null 2>&1 && _v2b="$_v2b [a preset that is not there applied]"
|
|
8561
|
+
_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]"
|
|
8562
|
+
[ -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" \
|
|
8563
|
+
|| fail "presets:$_v2b"
|
|
8564
|
+
rm -rf "$_psx"
|
|
8390
8565
|
|
|
8391
8566
|
# ---- 3. verify, one fix inside the agent, then one model up, then the user -----------------------------------
|
|
8392
8567
|
_v2new; _v2b=""
|
|
@@ -8699,12 +8874,41 @@ if command -v iconv >/dev/null 2>&1; then printf '%s' "$_o" | iconv -f UTF-8 -t
|
|
|
8699
8874
|
[ "${#_o}" -le 2600 ] || _v2b="$_v2b [the reason is ${#_o} bytes]"
|
|
8700
8875
|
[ -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
8876
|
|| fail "the stop hook's one fix:$_v2b"
|
|
8877
|
+
# A verify command in a sub-folder, `cd <folder> && <command>` (41 of 71 refusals on one project): judged as the
|
|
8878
|
+
# command and run in the folder with no chain. A folder outside the project, one that does not exist, or a second
|
|
8879
|
+
# chain after it stays refused. A card that says `none (why)` is none.
|
|
8880
|
+
_v2new; _v2b=""; mkdir -p "$_v2p/.claude/state/crew-model/agents" "$_v2p/sub"; : > "$_v2p/sub/ok.flag"
|
|
8881
|
+
printf 'verify test -f ok.flag\n' > "$_v2p/.claude/crew-model-rules"
|
|
8882
|
+
_v2plant c1 'cd sub && test -f ok.flag'; _v2stop c1 >/dev/null
|
|
8883
|
+
[ "$(_v2row c1 | cut -d/ -f2)" = pass ] || _v2b="$_v2b [cd sub && test: $(_v2row c1)]"
|
|
8884
|
+
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
|
|
8885
|
+
_v2plant "${_cc%%:*}" "${_cc#*:}"; _v2stop "${_cc%%:*}" >/dev/null
|
|
8886
|
+
[ "$(_v2row "${_cc%%:*}" | cut -d/ -f2)" = blocked ] || _v2b="$_v2b [${_cc#*:} was not refused: $(_v2row "${_cc%%:*}")]"
|
|
8887
|
+
done
|
|
8888
|
+
[ ! -e "$_v2p/sub/x" ] || _v2b="$_v2b [a chain after the folder ran]"
|
|
8889
|
+
_v2call crew-backend-expert sonnet 'src/ui/a.ts' text 'none (the author ran it)'
|
|
8890
|
+
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]"
|
|
8891
|
+
# doctor lists the commands refused for not being allowed, with the line that allows them, and not a sentence
|
|
8892
|
+
mkdir -p "$_v2p/.claude/state/crew-model/results"
|
|
8893
|
+
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"
|
|
8894
|
+
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"
|
|
8895
|
+
# The other way of saying "nothing allows it" (default mode), for a command in a sub-folder: counted, and offered as
|
|
8896
|
+
# the rule that matches it, the command without its `cd` (measured on Windows: the line with the cd in it did not).
|
|
8897
|
+
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"
|
|
8898
|
+
if [ -f "$ROOT/eval/doctor.sh" ]; then
|
|
8899
|
+
mkdir -p "$_v2p/.claude/eval"; cp -R "$ROOT/eval/." "$_v2p/.claude/eval/" 2>/dev/null
|
|
8900
|
+
_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' )"
|
|
8901
|
+
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 ;;
|
|
8902
|
+
*) _v2b="$_v2b [doctor: ${_dd:-no line}]" ;; esac
|
|
8903
|
+
fi
|
|
8904
|
+
[ -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" \
|
|
8905
|
+
|| fail "verify in a sub-folder:$_v2b"
|
|
8702
8906
|
# Which record is whose. Two calls of one type made together are matched by order, which can be wrong; the card in
|
|
8703
8907
|
# the agent's own transcript settles it.
|
|
8704
8908
|
_v2new; _v2b=""; _v2st="$_v2p/.claude/state/crew-model"
|
|
8705
8909
|
_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
8910
|
_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'
|
|
8911
|
+
_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
8912
|
# 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
8913
|
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
8914
|
_pt="$(date +%s)"; mv "$_v2st/pending/1-a" "$_v2st/pending/$_pt-a"; mv "$_v2st/pending/2-b" "$_v2st/pending/$_pt-b"
|
|
@@ -8739,6 +8943,16 @@ rm -f "$_v2st"/pending/*
|
|
|
8739
8943
|
_v2call crew-backend-expert sonnet 'src/ui/race.ts' text true
|
|
8740
8944
|
_v2start r1 & _v2start r2 & wait
|
|
8741
8945
|
_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]"
|
|
8946
|
+
ls "$_v2st/agents" 2>/dev/null | grep -qE '\.(take|claim)$' && _v2b="$_v2b a-lock-or-claim-left-behind"
|
|
8947
|
+
# A record another hook holds the lock on is not taken: the agent goes on to the next one (the lock is what keeps
|
|
8948
|
+
# two agents off one record where a rename alone did not, on Windows).
|
|
8949
|
+
rm -rf "$_v2st/agents"; rm -f "$_v2st"/pending/*
|
|
8950
|
+
_v2call crew-backend-expert sonnet 'src/ui/l1.ts' text 'test -f l1'; _pl1="$(ls "$_v2st/pending")"
|
|
8951
|
+
sleep 1; _v2call crew-backend-expert sonnet 'src/ui/l2.ts' text 'test -f l2'
|
|
8952
|
+
mkdir -p "$_v2st/agents"; : > "$_v2st/agents/$_pl1.take"
|
|
8953
|
+
_v2start k1
|
|
8954
|
+
{ 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"
|
|
8955
|
+
rm -f "$_v2st/agents/$_pl1.take" "$_v2st"/pending/*
|
|
8742
8956
|
# A record nothing claimed within five minutes is nobody's.
|
|
8743
8957
|
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
8958
|
_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 +8975,7 @@ _kn="$(awk -F'\t' '$2 == "k2" { print $5 "/" $13 }' "$_v2st/../model-outcomes.ts
|
|
|
8761
8975
|
# 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
8976
|
# the write-time check; run_in_background named twice is refused.
|
|
8763
8977
|
_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'
|
|
8978
|
+
_v2call crew-database-expert sonnet 'src/payments/p.ts' feature none; _v2call crew-backend-expert haiku 'src/ui/a.ts' fix-known none
|
|
8765
8979
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"nobody-known","agent_type":"x"'
|
|
8766
8980
|
_v2write "$_v2p/src/payments/p.ts" ',"agent_id":"","agent_type":"x"'; [ "$_v2wrc" = 2 ] || _v2b="$_v2b an-empty-agent-id-opened-the-check:$_v2wrc"
|
|
8767
8981
|
_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 +9119,11 @@ _v2call crew-backend-expert sonnet 'src/payments/p.ts' feature none; [ "$_v2rc"
|
|
|
8905
9119
|
# What a lowering cannot reach: the floors the card and the agent carry. Two raised floors are planted and lowered;
|
|
8906
9120
|
# the model below still fails where another rule holds it, and passes where only the lowered floor did.
|
|
8907
9121
|
_v2new; _v2b=""; mkdir -p "$_v2p/.claude/state"
|
|
8908
|
-
printf 'crew-backend-expert\
|
|
8909
|
-
_o="$(_v2say '/crew-loosen crew-backend-expert
|
|
9122
|
+
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"
|
|
9123
|
+
_o="$(_v2say '/crew-loosen crew-backend-expert fix-known normal')$(_v2say '/crew-loosen crew-database-expert feature normal')"
|
|
8910
9124
|
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'
|
|
8912
|
-
_v2call crew-backend-expert haiku 'src/ui/f1.ts'
|
|
9125
|
+
_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"
|
|
9126
|
+
_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
9127
|
_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
9128
|
_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
9129
|
_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 +11369,7 @@ sec "== 14c) the 3.0 rename left no old name behind — outside history and the
|
|
|
11155
11369
|
if [ -n "$SGR" ] && [ -d "$SGR/packaging" ] && [ -f "$SGR/VERSION" ] && [ -d "$SGR/kit" ] && [ -f "$SGR/packaging/build-plugin.sh" ]; then
|
|
11156
11370
|
RN_ALLOW='CHANGELOG.md 196 history: every entry before 3.0 keeps the name it shipped under
|
|
11157
11371
|
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/*
|
|
11372
|
+
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
11373
|
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
11374
|
bin/cli.js 4 migration: add accepts a typed <x>-csk and moves an add record written under the old names
|
|
11161
11375
|
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 +11589,22 @@ $(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/|
|
|
|
11375
11589
|
[ -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
11590
|
|| fail "twin: the closing-line check missed a planted 'kit/ deleted' — it reads nothing"
|
|
11377
11591
|
fi
|
|
11592
|
+
# adopt.sh --help answers before it reads anything. An unknown flag is ignored further down, so --help used to run
|
|
11593
|
+
# 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
|
|
11594
|
+
# languages, and nothing written. Twin: the same script without its --help branch must not print the usage.
|
|
11595
|
+
_ah_run(){ ( cd "$1" && CREW_LANG=en bash adopt.sh --help; echo "rc=$?"; bash adopt.sh -h --lang tr; echo "rc=$?" ) 2>&1; }
|
|
11596
|
+
mkdir -p "$_kd/ah" "$_kd/ah-twin"; git init -q "$_kd/ah" 2>/dev/null; git init -q "$_kd/ah-twin" 2>/dev/null
|
|
11597
|
+
cp "$KR/adopt.sh" "$_kd/ah/"; sed 's/-h|--help) _hh=1 ;;//' "$KR/adopt.sh" > "$_kd/ah-twin/adopt.sh"
|
|
11598
|
+
_ah="$(_ah_run "$_kd/ah")"; _aht="$(_ah_run "$_kd/ah-twin")"
|
|
11599
|
+
_ahw="$(git -C "$_kd/ah" status --porcelain 2>/dev/null | grep -vx '?? adopt.sh')"
|
|
11600
|
+
case "$_ah" in
|
|
11601
|
+
*'Usage: bash adopt.sh'*'rc=0'*'Kullanım: bash adopt.sh'*'rc=0'*)
|
|
11602
|
+
if [ -n "$_ahw" ]; then fail "adopt.sh --help wrote into the project: $(printf '%s' "$_ahw" | head -n 2 | tr '\n' ' ')"
|
|
11603
|
+
else case "$_aht" in *'Usage: bash adopt.sh'*) fail "twin: adopt.sh without its --help branch still printed the usage — the check reads nothing" ;;
|
|
11604
|
+
*) 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
|
|
11605
|
+
fi ;;
|
|
11606
|
+
*) fail "adopt.sh --help did not print its usage in both languages with rc 0: $(printf '%s' "$_ah" | head -n 3 | tr '\n' ' ')" ;;
|
|
11607
|
+
esac
|
|
11378
11608
|
rm -rf "$_kd"
|
|
11379
11609
|
else
|
|
11380
11610
|
skip scope "printed-text wording check skipped (installed project — the installers live in the source repository)"
|
|
@@ -204,27 +204,36 @@ case "$EV" in
|
|
|
204
204
|
low=9; cands=""; if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOWS '%(%s)T' -1; else NOWS="$(date +%s)"; fi
|
|
205
205
|
for f in "$_AMD/pending"/*; do
|
|
206
206
|
[ -f "$f" ] || continue
|
|
207
|
-
t="${f##*/}"; t="${t%%-*}"; case "$t" in ''|*[!0-9]*) ;; *) if [ $((NOWS - t)) -gt 300 ]; then rm -f "$f" 2>/dev/null; continue; fi ;; esac
|
|
207
|
+
t="${f##*/}"; t="${t%%-*}"; case "$t" in ''|*[!0-9]*) ;; *) if [ $((NOWS - t)) -gt 300 ]; then rm -f "$f" "$_AMD/agents/${f##*/}.take" 2>/dev/null; continue; fi ;; esac
|
|
208
208
|
_am_rec_get "$f" agent || continue; [ "$_AMV" = "$AT" ] || continue
|
|
209
209
|
cands="$cands$f"$'\n'
|
|
210
210
|
_am_rec_get "$f" tier && { _am_rank "$_AMV"; [ "$_AMR" -lt "$low" ] && low="$_AMR"; }
|
|
211
211
|
done
|
|
212
212
|
[ -n "$cands" ] || exit 0
|
|
213
213
|
mkdir -p "$_AMD/agents" 2>/dev/null || exit 0
|
|
214
|
-
# TAKEN BY RENAME. Two agents of one type start at the same moment, each in its own hook
|
|
215
|
-
# the same oldest record.
|
|
216
|
-
#
|
|
217
|
-
|
|
214
|
+
# TAKEN UNDER A LOCK, THEN BY RENAME. Two agents of one type start at the same moment, each in its own hook
|
|
215
|
+
# process, and both see the same oldest record. (Reading it and deleting it afterwards gave both the same record.)
|
|
216
|
+
# A rename alone was the lock on POSIX, and was not on Windows: two hook processes both reported the same rename
|
|
217
|
+
# as done (smoke, windows-latest, "2 got it", twice). Why is not measured; a rename there goes through an open
|
|
218
|
+
# handle, which two processes can both hold. So each record is first locked by creating <record>.take with
|
|
219
|
+
# O_EXCL (bash's noclobber, a builtin: no process); only the one that made the lock renames, the others go on to
|
|
220
|
+
# the next record.
|
|
221
|
+
claim="$_AMD/agents/$AID.claim"; got=0; take=""
|
|
222
|
+
set -C
|
|
218
223
|
while IFS= read -r f; do
|
|
219
224
|
[ -n "$f" ] || continue
|
|
225
|
+
take="$_AMD/agents/${f##*/}.take"
|
|
226
|
+
{ : > "$take"; } 2>/dev/null || { take=""; continue; }
|
|
220
227
|
if mv "$f" "$claim" 2>/dev/null; then got=1; break; fi
|
|
228
|
+
rm -f "$take" 2>/dev/null; take="" # locked, but the record had gone (dropped as stale): let it be
|
|
221
229
|
done <<< "$cands"
|
|
230
|
+
set +C
|
|
222
231
|
[ "$got" = 1 ] || exit 0
|
|
223
232
|
_am_rec_get "$claim" tier; _am_rank "$_AMV"
|
|
224
233
|
if [ "$low" -lt "$_AMR" ]; then _am_name "$low"
|
|
225
234
|
{ grep -v '^tier=' "$claim"; printf 'asked=%s\ntier=%s\nambiguous=1\n' "$_AMV" "$_AMN"; } > "$_AMD/agents/$AID" 2>/dev/null
|
|
226
235
|
else { cat "$claim"; printf 'asked=%s\n' "$_AMV"; } > "$_AMD/agents/$AID" 2>/dev/null; fi
|
|
227
|
-
rm -f "$claim" 2>/dev/null
|
|
236
|
+
rm -f "$claim" "$take" 2>/dev/null
|
|
228
237
|
exit 0 ;;
|
|
229
238
|
|
|
230
239
|
SubagentStop)
|
|
@@ -290,7 +299,9 @@ case "$EV" in
|
|
|
290
299
|
RES=none; WHYNOT=""
|
|
291
300
|
if [ -n "$VCMD" ] && [ "$VCMD" != none ]; then
|
|
292
301
|
_json_slice "$INPUT" permission_mode >/dev/null; PM="$_JS"
|
|
293
|
-
|
|
302
|
+
# `cd <folder> && <command>` is judged as the command and run in the folder (guard-agent-model.sh).
|
|
303
|
+
_am_verify_split "$VCMD"; VDIR="$_AMVD"; VRUN="$_AMVC"
|
|
304
|
+
if ! _am_verify_ok "$VRUN" "$PM"; then
|
|
294
305
|
# Judged again here, at the moment it would run: the gate judged it when the agent was called, and this
|
|
295
306
|
# is the place the command actually starts. Not run; nothing is concluded about the work.
|
|
296
307
|
RES=blocked; WHYNOT="$_AMVW"; VRC=0
|
|
@@ -305,11 +316,11 @@ case "$EV" in
|
|
|
305
316
|
# control, which gives a background job its own group in bash (macOS ships no setsid). On Windows the tree
|
|
306
317
|
# is ended by its Windows process id.
|
|
307
318
|
if [ -z "${CREW_VERIFY_NO_SETSID:-}" ] && command -v setsid >/dev/null 2>&1; then
|
|
308
|
-
( cd "$_AMP" 2>/dev/null && exec setsid bash -c "$
|
|
319
|
+
( cd "$_AMP/$VDIR" 2>/dev/null && exec setsid bash -c "$VRUN" ) > "$VF" 2>&1 </dev/null &
|
|
309
320
|
VPID=$!
|
|
310
321
|
else
|
|
311
322
|
set -m
|
|
312
|
-
( cd "$_AMP" 2>/dev/null && exec bash -c "$
|
|
323
|
+
( cd "$_AMP/$VDIR" 2>/dev/null && exec bash -c "$VRUN" ) > "$VF" 2>&1 </dev/null &
|
|
313
324
|
VPID=$!
|
|
314
325
|
set +m
|
|
315
326
|
fi
|
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
|