crewforth 3.1.1 → 3.2.0-rc.2
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 +171 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/adopt.sh +56 -5
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +14 -13
- package/kit/agents/crew-commit-agent.md +7 -2
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +117 -6
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +98 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +163 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/scan.mjs +214 -0
- package/kit/eval/lib/crew-map/stacks.mjs +441 -0
- package/kit/eval/lib/crew-map/surface.mjs +457 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +15 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/smoke-test.sh +576 -41
- package/kit/hooks/agent-outcome.sh +26 -12
- package/kit/hooks/guard-agent-model.sh +109 -9
- package/kit/hooks/guard-bash.sh +240 -182
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +21 -0
- package/kit/hooks/prompt-approval.sh +320 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +33 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +44 -0
- package/kit/skills/crew-review/SKILL.md +2 -0
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +12 -0
- package/kit/studio/server/index.js +125 -11
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/remote.js +99 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +223 -15
- package/kit/studio/web/canvas.js +44 -120
- package/kit/studio/web/chat.js +82 -17
- package/kit/studio/web/graph-plan.js +10 -4
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +341 -0
- package/kit/studio/web/map.js +503 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/style.css +131 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +10 -0
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -109,6 +109,10 @@ note(){ echo " · $1"; } # informational; never counts as a failure
|
|
|
109
109
|
# Scope. Declared HERE rather than beside the block it first guards, because it now gates cases that run
|
|
110
110
|
# EARLIER than that block — see §6h. Reading an env var costs nothing; the note stays where the big skip is.
|
|
111
111
|
UNITS=1; [ "${CREW_SMOKE_SCOPE:-full}" = install ] && UNITS=0
|
|
112
|
+
# THE USER'S OWN CLAUDE SETTINGS ARE NOT AN INPUT OF THIS SUITE (3.2.0). Before a commit prompt the gates look for a
|
|
113
|
+
# PermissionRequest hook in the user's settings, skills, agents and plugins; on a machine that has one, every "the gate
|
|
114
|
+
# asks" case would read "the gate refuses". Cases that need a user directory set their own (HOME, CLAUDE_CONFIG_DIR).
|
|
115
|
+
_SMOKE_CCD="$(mktemp -d)"; export CLAUDE_CONFIG_DIR="$_SMOKE_CCD"
|
|
112
116
|
# Trigger-phrases requirement: a GATE in the Crewforth repo, a note in an installed project (your skills, your call).
|
|
113
117
|
need_trigger(){ if kit_owned "${2:-}"; then fail "$1"; else note "$1 (your own skill/agent; not gated in an install)"; fi; }
|
|
114
118
|
|
|
@@ -2328,7 +2332,7 @@ BUDGET_DISC=14975 # 3.1.0: 13986 → 14975 (+989): the task card (files, chan
|
|
|
2328
2332
|
# and the wrong one winning silently. The only rule in this file that is about the OTHER
|
|
2329
2333
|
# rules, so it cannot live in the README the way the compaction note does. Plus the Audit
|
|
2330
2334
|
# row naming crew-performance-expert — an agent nothing routes to is an idle component.)
|
|
2331
|
-
BUDGET_AGENTS=
|
|
2335
|
+
BUDGET_AGENTS=5746 # 3.2.0: 5596 → 5746 (+150), the measured sum: crew-docs-agent is a 14th agent (195 B, its description cut to one line first; the other 13 sum to 5551). Raised on the user's decision, not to make room. 5d.2: tightened to the measured sum (was 5800 with 204 B of slack). Before that: sum of agent frontmatter; currently 5582, measured 2026-09-23 (3.0: the backend and database
|
|
2332
2336
|
# agents' descriptions rewritten stack-agnostic, +55 B). Before that 5527, measured 2026-09-20 by reading this suite's
|
|
2333
2337
|
# own printed line rather than a hand-rolled counter (a hand-rolled one answered 5503 and
|
|
2334
2338
|
# was thrown away). Two corrections in one day: the note said 5765 against a measured 5407,
|
|
@@ -2521,20 +2525,34 @@ done
|
|
|
2521
2525
|
# PROACTIVE-CUE GATE: Claude Code auto-delegates on the description field, and only fires reliably when it carries
|
|
2522
2526
|
# an action cue ("use proactively" / "immediately after" / "use ... when"). A passive role description ("Senior X
|
|
2523
2527
|
# expert. Handlers, endpoints.") rarely auto-invokes — the specialist stays dormant and Crewforth reads as inert.
|
|
2524
|
-
#
|
|
2525
|
-
#
|
|
2526
|
-
|
|
2528
|
+
# EVERY agent carries one. Two used to be exempt as "pull-only" (a commit needs approval; session health comes from
|
|
2529
|
+
# a hook), and nothing pulled them: the main thread wrote the commit message and the handover itself, and neither
|
|
2530
|
+
# agent was ever started. The exemption is gone; what pulls a chain-step agent is pinned just below.
|
|
2527
2531
|
NO_CUE=""
|
|
2528
2532
|
for f in $(agent_quality_files); do
|
|
2529
2533
|
[ -e "$f" ] || continue
|
|
2530
2534
|
a="$(basename "$f" .md)"
|
|
2531
|
-
case "$PULL_AGENTS" in *" $a "*) continue ;; esac
|
|
2532
2535
|
awk '/^---$/{c++; next} c==1' "$f" | grep -qiE "use proactively|immediately after|use [a-z ]*when" \
|
|
2533
2536
|
|| NO_CUE="$NO_CUE $a"
|
|
2534
2537
|
done
|
|
2535
|
-
if [ -z "$NO_CUE" ]; then pass "every
|
|
2536
|
-
elif [ "$IS_KIT" = 1 ]; then fail "agent(s) with a passive description — auto-delegation will rarely fire:$NO_CUE (add 'use proactively …'
|
|
2538
|
+
if [ -z "$NO_CUE" ]; then pass "every agent carries an auto-delegation cue (proactive/immediately-after/use … when)"
|
|
2539
|
+
elif [ "$IS_KIT" = 1 ]; then fail "agent(s) with a passive description — auto-delegation will rarely fire:$NO_CUE (add 'use proactively …')"
|
|
2537
2540
|
else pass "some agents lack a proactive cue:$NO_CUE (your project's own agents, not gated)"; fi
|
|
2541
|
+
# WHO PULLS A CHAIN-STEP AGENT. An agent whose work sits at a fixed step of the chain (metadata.stage close, handoff
|
|
2542
|
+
# or understand: the reviewer, the commit message, the handover, the plan) is not reached by a prompt's words; it is
|
|
2543
|
+
# reached because a command or the discipline CALLS it there. So each one is named in the call form, `@agent-<name>`
|
|
2544
|
+
# (or `subagent_type`), in a command skill or the discipline. Producers and auditors are pulled by route-hint.sh and
|
|
2545
|
+
# the cue above; the orphan check (3b) already holds that they are routed at all. Kit agents only.
|
|
2546
|
+
_cs=0; _csb=""; _cmdfiles="$(grep -l 'kind: command' "$SKILLS"/*/SKILL.md 2>/dev/null)"
|
|
2547
|
+
for f in "$AGENTS"/*.md; do
|
|
2548
|
+
[ -f "$f" ] || continue; a="$(basename "$f" .md)"; kit_owned "agents/$a.md" || continue
|
|
2549
|
+
case "$(awk '/^---$/{c++; next} c==1 && /^[[:space:]]*stage:/{print $2; exit}' "$f")" in close|handoff|understand) ;; *) continue ;; esac
|
|
2550
|
+
_cs=$((_cs+1))
|
|
2551
|
+
grep -qE "@agent-$a([^a-z0-9-]|\$)|subagent_type[^a-z]{0,6}$a([^a-z0-9-]|\$)" $_cmdfiles "$ROUTE_DOC" 2>/dev/null || _csb="$_csb $a"
|
|
2552
|
+
done
|
|
2553
|
+
if [ "$IS_KIT" = 1 ] && [ "$_cs" -lt 4 ]; then fail "FIXTURE: $_cs chain-step agents read (close/handoff/understand) — the stage field or the count broke, not the agents"
|
|
2554
|
+
elif [ -z "$_csb" ]; then pass "every chain-step agent ($_cs) is called as @agent-<name> by a command or the discipline"
|
|
2555
|
+
else fail "chain-step agent(s) nothing calls by name — the main thread does their work instead:$_csb"; fi
|
|
2538
2556
|
|
|
2539
2557
|
|
|
2540
2558
|
# ---- gate UNIT cases: skipped under CREW_SMOKE_SCOPE=install ------------------------------------------------
|
|
@@ -2789,6 +2807,24 @@ r46rec "$R46_OID" "$R46_HEAD"
|
|
|
2789
2807
|
o="$(gj default 'git commit -m x' | r46 2>/dev/null)"
|
|
2790
2808
|
[ "$(gdec "$o")" = "ask" ] && pass "§4.6: a matching record lets the commit through to the §4.4 ask" \
|
|
2791
2809
|
|| fail "§4.6: a matching record did NOT pass the gate (out=$o)"
|
|
2810
|
+
# 2b. Who wrote the message (AJAN-1): the latest crew-review-agent / crew-commit-agent call in the transcript decides
|
|
2811
|
+
# one line in the approval. It never blocks, and it says nothing when it cannot know: a subagent's own call, or a
|
|
2812
|
+
# transcript it cannot read.
|
|
2813
|
+
_can_t="$R46/.t"; mkdir -p "$_can_t"
|
|
2814
|
+
_can_call(){ printf '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Agent","input":{"subagent_type":"%s","prompt":"x"}}]}}\n' "$1"; }
|
|
2815
|
+
{ _can_call crew-review-agent; _can_call crew-commit-agent; } > "$_can_t/both.jsonl"
|
|
2816
|
+
_can_call crew-review-agent > "$_can_t/review.jsonl"
|
|
2817
|
+
{ _can_call crew-commit-agent; _can_call crew-review-agent; } > "$_can_t/stale.jsonl"
|
|
2818
|
+
_canj(){ printf '{"cwd":"%s","permission_mode":"default","transcript_path":"%s",%s"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' "$R46" "$1" "${2:-}"; }
|
|
2819
|
+
_canb=""; _NOTE='crew-commit-agent has not run since the last review'
|
|
2820
|
+
o="$(_canj "$_can_t/both.jsonl" | r46 2>/dev/null)"; [ "$(gdec "$o")" = ask ] && [[ "$o" != *"$_NOTE"* ]] || _canb="$_canb agent-ran-but-noted"
|
|
2821
|
+
o="$(_canj "$_can_t/review.jsonl" | r46 2>/dev/null)"; [ "$(gdec "$o")" = ask ] && [[ "$o" == *"$_NOTE"* ]] || _canb="$_canb no-agent-not-noted"
|
|
2822
|
+
o="$(_canj "$_can_t/stale.jsonl" | r46 2>/dev/null)"; [[ "$o" == *"$_NOTE"* ]] || _canb="$_canb a-review-after-the-agent-not-noted"
|
|
2823
|
+
o="$(_canj "$_can_t/review.jsonl" '"agent_id":"a1",' | r46 2>/dev/null)"; [[ "$o" != *"$_NOTE"* ]] || _canb="$_canb noted-on-a-subagents-call"
|
|
2824
|
+
o="$(_canj "$_can_t/nope.jsonl" | r46 2>/dev/null)"; [ "$(gdec "$o")" = ask ] && [[ "$o" != *"$_NOTE"* ]] || _canb="$_canb noted-without-a-transcript"
|
|
2825
|
+
[ -z "$_canb" ] && pass "§4.4 approval names a commit message crew-commit-agent did not write (5 cases: ran, not run, review after it, subagent, no transcript); it never blocks" \
|
|
2826
|
+
|| fail "commit-agent note:$_canb"
|
|
2827
|
+
rm -rf "$_can_t"
|
|
2792
2828
|
# 3. Right diff, wrong base. This is the case a wall-clock TTL cannot see and the reason there is none.
|
|
2793
2829
|
r46rec "$R46_OID" "0000000000000000000000000000000000000000"
|
|
2794
2830
|
gj default 'git commit -m x' | r46 >/dev/null 2>&1; [ "$?" = 2 ] \
|
|
@@ -3476,6 +3512,27 @@ done
|
|
|
3476
3512
|
# are now cut out and the rest goes to the old rule, so nothing unproven is skipped — all pinned above.
|
|
3477
3513
|
[ -z "$_hp_bad" ] && pass "core.hooksPath: value-less reads pass, every write form stays blocked ($_hp_n of 48 shapes)" \
|
|
3478
3514
|
|| fail "core.hooksPath read/write split is wrong:$_hp_bad"
|
|
3515
|
+
# crewforth.hookschain IS JUDGED BY THE SAME READER. The key holds the path of a project hook chain that lives outside
|
|
3516
|
+
# the repository; the shim reads it on every commit. Reads pass, every write form is refused, exactly 0 or 2.
|
|
3517
|
+
_hc_bad=""; _hc_n=0
|
|
3518
|
+
for _c in 'git config crewforth.hookschain' 'git config --get crewforth.hookschain' 'git config --local crewforth.hookschain' \
|
|
3519
|
+
'git -C /r config --get crewforth.hookschain' 'git config get crewforth.hookschain' 'git config --get crewforth.hookschain | cat' \
|
|
3520
|
+
'git status; echo crewforth.hookschain' 'git config --remove-section crewforth.foo' 'git config crewforth.other x'; do
|
|
3521
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _hc_n=$((_hc_n+1)); [ "$_r" = 0 ] || _hc_bad="$_hc_bad [$_c -> $_r, want 0]"
|
|
3522
|
+
done
|
|
3523
|
+
for _c in 'git config crewforth.hookschain /tmp/x' 'git config --local crewforth.hookschain x' 'git config --unset crewforth.hookschain' \
|
|
3524
|
+
'git config --unset-all crewforth.hookschain' 'git config --add crewforth.hookschain x' 'git config --replace-all crewforth.hookschain x' \
|
|
3525
|
+
'git config set crewforth.hookschain x' 'git config unset crewforth.hookschain' 'git config crewforth.hookschain \"\"' \
|
|
3526
|
+
'git config --get crewforth.hookschain && git config crewforth.hookschain x' 'git config crewforth.HooksChain /tmp/x' \
|
|
3527
|
+
'git config crewforth.hooks\"c\"hain x' 'git config crewforth.hookschain \r' 'git -C /r config crewforth.hookschain /tmp/x' \
|
|
3528
|
+
'git config --remove-section crewforth' 'git config --rename-section crewforth x' \
|
|
3529
|
+
'git -c crewforth.hookschain=/tmp/x commit -m x' "git -c 'crewforth.hookschain=/tmp/x' commit -m x" \
|
|
3530
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=crewforth.hookschain GIT_CONFIG_VALUE_0=/tmp/x git commit -m x'; do
|
|
3531
|
+
gj auto "$_c" | bash "$HOOKS/guard-bash.sh" >/dev/null 2>&1; _r=$?; _hc_n=$((_hc_n+1)); [ "$_r" = 2 ] || _hc_bad="$_hc_bad [$_c -> $_r, want 2]"
|
|
3532
|
+
done
|
|
3533
|
+
if [ "$_hc_n" != 28 ]; then fail "FIXTURE: the crewforth.hookschain table ran $_hc_n rows, not 28"
|
|
3534
|
+
elif [ -z "$_hc_bad" ]; then pass "crewforth.hookschain: 9 reads and neighbours pass, 19 write forms are refused — a value, unset, add, set, the section removed or renamed, -c, GIT_CONFIG_ ($_hc_n rows)"
|
|
3535
|
+
else fail "crewforth.hookschain read/write split is wrong:$_hc_bad"; fi
|
|
3479
3536
|
# THE SAME SPLIT UNDER A WINDOWS PAYLOAD. A read is not proven when the payload holds an escape the reader drops
|
|
3480
3537
|
# (`\r`, `\f`, `\b`, `\v`, `\u`). That was asked of the WHOLE payload, and a Windows `cwd` is full of them once
|
|
3481
3538
|
# JSON-encoded: `C:\\repos\\app` holds a backslash and an r. So in such a directory `git config --get core.hooksPath`
|
|
@@ -4595,6 +4652,37 @@ DOC="$(mktemp -d)"
|
|
|
4595
4652
|
chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg
|
|
4596
4653
|
git config core.hooksPath .claude/hooks )
|
|
4597
4654
|
bash "$ROOT/eval/doctor.sh" "$DOC" >/dev/null 2>&1 && pass "doctor: healthy install -> exit 0" || fail "doctor flagged a healthy install"
|
|
4655
|
+
# Two editions at once, and the advisor (doctor 9c). Driven through a stub `claude` that prints a plugin list, a
|
|
4656
|
+
# config dir of its own, and the provider and advisor variables cleared, so the machine running this decides
|
|
4657
|
+
# nothing. Advisory: every case keeps the healthy verdict (exit 0).
|
|
4658
|
+
_d9="$(mktemp -d)"; mkdir -p "$_d9/bin" "$_d9/cfg"
|
|
4659
|
+
_d9stub(){ printf '#!/bin/sh\ncase "$1" in --version) echo "2.1.299 (Claude Code)" ;; plugin) cat <<J\n%s\nJ\n;; esac\n' "$1" > "$_d9/bin/claude"; chmod +x "$_d9/bin/claude"; }
|
|
4660
|
+
_d9run(){ _d9o="$( cd "$DOC" && env -u CLAUDE_CODE_DISABLE_ADVISOR_TOOL -u CLAUDE_CODE_USE_BEDROCK -u CLAUDE_CODE_USE_VERTEX \
|
|
4661
|
+
CREW_LANG=en CLAUDE_CONFIG_DIR="$_d9/cfg" PATH="$_d9/bin:$PATH" "$@" bash "$ROOT/eval/doctor.sh" 2>&1 )"; _d9rc=$?; }
|
|
4662
|
+
_d9b=""
|
|
4663
|
+
_d9stub '[{"id":"crewforth@crewforth","enabled":true},{"id":"other@market","enabled":true}]'; _d9run
|
|
4664
|
+
case "$_d9o" in *"every hook runs twice"*) ;; *) _d9b="$_d9b both-on-not-seen" ;; esac; [ "$_d9rc" = 0 ] || _d9b="$_d9b both-on-changed-the-verdict($_d9rc)"
|
|
4665
|
+
# The 2.x plugin id, still enabled at user scope beside a 3.x install, wires the same hooks: measured in a real
|
|
4666
|
+
# project (the 2.13.0 plugin was claude-starter-kit@byerlikaya).
|
|
4667
|
+
_d9stub '[{"id":"claude-starter-kit@byerlikaya","enabled":true}]'; _d9run
|
|
4668
|
+
case "$_d9o" in *"claude-starter-kit@byerlikaya plugin is enabled"*"every hook runs twice"*) ;; *) _d9b="$_d9b legacy-plugin-not-seen" ;; esac
|
|
4669
|
+
_d9stub '[{"id": "crewforth@crewforth", "enabled": false}]'; _d9run
|
|
4670
|
+
case "$_d9o" in *"one edition: the Crewforth plugin is not enabled"*) ;; *) _d9b="$_d9b plugin-off-not-one-edition" ;; esac
|
|
4671
|
+
rm -f "$_d9/bin/claude"; _d9run env PATH="$_d9/bin:/usr/bin:/bin"
|
|
4672
|
+
case "$_d9o" in *"two editions at once: not measured (no claude CLI"*) ;; *) _d9b="$_d9b no-cli-not-said" ;; esac
|
|
4673
|
+
_d9stub '[]'
|
|
4674
|
+
_d9run; case "$_d9o" in *"advisor: off (no advisorModel set"*) ;; *) _d9b="$_d9b advisor-unset" ;; esac
|
|
4675
|
+
printf '{"advisorModel":"opus"}' > "$_d9/cfg/settings.json"; _d9run
|
|
4676
|
+
case "$_d9o" in *"advisor: on, opus"*) ;; *) _d9b="$_d9b advisor-opus" ;; esac; case "$_d9o" in *"below the critical tier"*) _d9b="$_d9b opus-called-below-critical" ;; esac
|
|
4677
|
+
printf '{"advisorModel":"sonnet"}' > "$_d9/cfg/settings.json"; _d9run
|
|
4678
|
+
case "$_d9o" in *"advisor (sonnet) is below the critical tier"*) ;; *) _d9b="$_d9b sonnet-not-below" ;; esac
|
|
4679
|
+
_d9run env CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1
|
|
4680
|
+
case "$_d9o" in *"CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1 turns the advisor off"*) ;; *) _d9b="$_d9b env-off-not-seen" ;; esac
|
|
4681
|
+
_d9run env CLAUDE_CODE_USE_BEDROCK=1
|
|
4682
|
+
case "$_d9o" in *"needs the Anthropic API"*) ;; *) _d9b="$_d9b bedrock-not-seen" ;; esac; [ "$_d9rc" = 0 ] || _d9b="$_d9b bedrock-changed-the-verdict($_d9rc)"
|
|
4683
|
+
rm -rf "$_d9"
|
|
4684
|
+
[ -z "$_d9b" ] && pass "doctor 9c: both editions on is named, one is ok, no CLI is not measured; the advisor off, on, below the critical tier, env-disabled and on Bedrock each read right; none changes the verdict" \
|
|
4685
|
+
|| fail "doctor 9c:$_d9b"
|
|
4598
4686
|
# THE MANIFEST'S LINE ENDINGS MUST NOT CHANGE WHO OWNS A SKILL. Measured before the fix: the same install with
|
|
4599
4687
|
# one project skill counted 1 on an LF manifest and 2 on a CRLF one, because `grep -qxF` wants a whole line and
|
|
4600
4688
|
# `skills/handoff\r` is not `skills/handoff` — so every KIT skill read as project-owned. A Windows checkout with
|
|
@@ -4883,6 +4971,8 @@ sec "== 7u) update prompt: ASKS about a release, never installs one, never spend
|
|
|
4883
4971
|
UPD="$(mktemp -d)"; mkdir -p "$UPD/.claude/.state" "$UPD/.claude/hooks"
|
|
4884
4972
|
printf '3.0.0\n' > "$UPD/.claude/VERSION"
|
|
4885
4973
|
UH="$HOOKS/session-update-check.sh"; cp "$UH" "$UPD/.claude/hooks/"
|
|
4974
|
+
# The hook reads and orders the install's version through eval/lib/version.sh, beside it in an install.
|
|
4975
|
+
mkdir -p "$UPD/.claude/eval/lib"; cp "$ROOT/eval/lib/version.sh" "$UPD/.claude/eval/lib/"
|
|
4886
4976
|
uc(){ ( printf '{"hook_event_name":"SessionStart","source":"startup","cwd":"%s"}' "$UPD" \
|
|
4887
4977
|
| env -u CI -u CREW_NO_UPDATE_CHECK -u CREW_LANG CLAUDE_CODE_SESSION_ATTENDED=1 LANG=C LC_ALL= LC_MESSAGES= \
|
|
4888
4978
|
CLAUDE_PROJECT_DIR="$UPD" CREW_UPDATE_URL="${1:-http://10.255.255.1/blackhole}" bash "$UH" 2>/dev/null ); }
|
|
@@ -5011,10 +5101,25 @@ cp "$UH" "$UPD/uh-twin.sh"; printf 'npx --yes crewforth@latest update --here --y
|
|
|
5011
5101
|
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"
|
|
5012
5102
|
else fail "the update hook contains an installer call ($_ni line(s)) — it must only ask, or the twin was not caught"; fi
|
|
5013
5103
|
|
|
5104
|
+
# 12b) AN INSTALLED RC IS OFFERED ITS FINAL. The installer copies VERSION into the project, and an rc's says
|
|
5105
|
+
# 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
|
|
5106
|
+
# rc's user was never told the final shipped). An older final is not offered, and what the network answers is
|
|
5107
|
+
# still held to X.Y.Z: an rc off the network is no version.
|
|
5108
|
+
printf '3.2.0-rc.1\n' > "$UPD/.claude/VERSION"
|
|
5109
|
+
ucache 3.2.0
|
|
5110
|
+
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" ;;
|
|
5111
|
+
*) fail "an installed 3.2.0-rc.1 was not offered the final 3.2.0 (got: $(uc | head -c 120))" ;; esac
|
|
5112
|
+
ucache 3.1.1
|
|
5113
|
+
[ -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"
|
|
5114
|
+
ucache 3.2.0-rc.2
|
|
5115
|
+
[ -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"
|
|
5116
|
+
printf '3.0.0\n' > "$UPD/.claude/VERSION"; ustate
|
|
5117
|
+
|
|
5014
5118
|
# 13) THE PLUGIN EDITION. Its version is its own manifest, its release is the marketplace repo's copy, its state is
|
|
5015
5119
|
# user-level (a plugin serves every project), and its Update path is the plugin command — measured to work from
|
|
5016
5120
|
# inside a session, applying on restart.
|
|
5017
|
-
PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks"; cp "$UH" "$PLG/hooks/"
|
|
5121
|
+
PLG="$UPD/plugin"; mkdir -p "$PLG/.claude-plugin" "$PLG/hooks" "$PLG/eval/lib"; cp "$UH" "$PLG/hooks/"
|
|
5122
|
+
cp "$ROOT/eval/lib/version.sh" "$PLG/eval/lib/"
|
|
5018
5123
|
printf '{"name":"crewforth","version":"3.0.0"}\n' > "$PLG/.claude-plugin/plugin.json"
|
|
5019
5124
|
XDG="$UPD/xdg"; mkdir -p "$XDG/crewforth"
|
|
5020
5125
|
printf '3.1.0 %s\n' "$(date +%s)" > "$XDG/crewforth/update-check"
|
|
@@ -6610,8 +6715,10 @@ rm -rf "$_hsd"
|
|
|
6610
6715
|
# A gate's timeout is not a comfort setting: a PreToolUse hook that reaches it does not block (Claude Code's hooks
|
|
6611
6716
|
# reference: "doesn't block the tool call … don't count on a stalled hook to act as a gate"), and a field session on
|
|
6612
6717
|
# 3.0.0 showed nine of them in one day — the commands ran with no gate. Crewforth had set 60 s where Claude Code's
|
|
6613
|
-
# own default is 600, and the slowest machine measured needed 32-134 s for one guard-bash call. The
|
|
6614
|
-
# gates (the four bash gates, the no-bash gate of 12e, guard-schedule.sh
|
|
6718
|
+
# own default is 600, and the slowest machine measured needed 32-134 s for one guard-bash call. The eight PreToolUse
|
|
6719
|
+
# gates (the four bash gates, the no-bash gate of 12e, guard-schedule.sh, guard-agent-model.sh and prompt-approval.sh on
|
|
6720
|
+
# the approval question, whose `ask` is what keeps a hook from answering it) carry 600 now; every other hook keeps 60 —
|
|
6721
|
+
# when one of those times out, nothing is left unguarded.
|
|
6615
6722
|
# Read per entry, in both editions; the twin lowers one gate back to 60 and must be seen.
|
|
6616
6723
|
gate_timeouts(){ json_hooks "$1" | LC_ALL=C awk -F'\t' '$1 == "PreToolUse" { n++; t = 0
|
|
6617
6724
|
if (match($3, /"timeout"[ \t]*:[ \t]*[0-9]+/)) { t = substr($3, RSTART, RLENGTH); sub(/.*:[ \t]*/, "", t) }
|
|
@@ -6620,13 +6727,13 @@ gate_timeouts(){ json_hooks "$1" | LC_ALL=C awk -F'\t' '$1 == "PreToolUse" { n++
|
|
|
6620
6727
|
_gtd="$(mktemp -d)"
|
|
6621
6728
|
tr -d '\n' < "$ROOT/settings.json" | sed 's/guard-bash\.sh\([^}]*\)"timeout": 600/guard-bash.sh\1"timeout": 60/' > "$_gtd/twin.json"
|
|
6622
6729
|
_gtw="$(gate_timeouts "$_gtd/twin.json")"
|
|
6623
|
-
if [ "$_gtw" != "
|
|
6730
|
+
if [ "$_gtw" != "8 1" ]; then fail "gate timeout pin: the twin with guard-bash.sh back at 60 s read '$_gtw', want '8 1' — the pin sees nothing"
|
|
6624
6731
|
else for _gf in $_gpw_f; do
|
|
6625
6732
|
[ -f "$_gf" ] || continue
|
|
6626
6733
|
set -- $(gate_timeouts "$_gf")
|
|
6627
|
-
if [ "${1:-0}" !=
|
|
6628
|
-
elif [ "${2:-9}" = 0 ]; then pass "${_gf##*/}: all
|
|
6629
|
-
else fail "${_gf##*/}: $2 of
|
|
6734
|
+
if [ "${1:-0}" != 8 ]; then fail "${_gf##*/}: $1 PreToolUse gate hooks read, want 8 — the pin did not read the file as written"
|
|
6735
|
+
elif [ "${2:-9}" = 0 ]; then pass "${_gf##*/}: all 8 PreToolUse gates have a 600 s timeout (a gate that times out does not block)"
|
|
6736
|
+
else fail "${_gf##*/}: $2 of 8 PreToolUse gates time out before 600 s — a timed-out gate does not block"; fi
|
|
6630
6737
|
done; fi
|
|
6631
6738
|
rm -rf "$_gtd"
|
|
6632
6739
|
|
|
@@ -7394,6 +7501,22 @@ PCT='0 @@ git commit -m x
|
|
|
7394
7501
|
2 @@ git commit -m x
|
|
7395
7502
|
2 @@ git commit -m x;touch
|
|
7396
7503
|
2 @@ git commit -m #\x27\ntouch ../PWNED\n#\x27
|
|
7504
|
+
2 @@ git commit -m x \\
|
|
7505
|
+
2 @@ git commit -m a\\ b
|
|
7506
|
+
2 @@ git commit -m $\x27a\x27
|
|
7507
|
+
2 @@ git commit -m \"a
|
|
7508
|
+
2 @@ git commit -m \x27a
|
|
7509
|
+
2 @@ git commit -m \"a\\\\b\"
|
|
7510
|
+
2 @@ git commit -m \"a!b\"
|
|
7511
|
+
0 @@ git commit -q -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27 -m \x27p\x27
|
|
7512
|
+
0 @@ \n git commit -m \x27a\x27 \t\n
|
|
7513
|
+
0 @@ git commit -m x \\\n
|
|
7514
|
+
0 @@ git commit -m x\n 2>&1
|
|
7515
|
+
2 @@ git commit -m x\n2>&1 -a
|
|
7516
|
+
2 @@ git commit -m \"$(cat <<\x27E-F\x27\nmsg\nE-F\n)\"
|
|
7517
|
+
2 @@ git commit -m \"$(cat <<\x27EOF\x27\nmsg\nEOF\ntouch ../PWNED\nEOF\n)\"
|
|
7518
|
+
2 @@ git commit -m a*b
|
|
7519
|
+
2 @@ git commit -m x2>&1
|
|
7397
7520
|
2 @@ git commit -m \\\x27a ; touch ../PWNED ; echo b\\\x27'
|
|
7398
7521
|
_pa_say auto 'approve: commit' >/dev/null
|
|
7399
7522
|
_pan=0; _pabad=""
|
|
@@ -7401,8 +7524,8 @@ while IFS= read -r _pl; do [ -z "$_pl" ] && continue
|
|
|
7401
7524
|
_pw="${_pl%% @@ *}"; _pc="${_pl#* @@ }"; _pc="${_pc//\\x27/$_pq}"; _pan=$((_pan+1))
|
|
7402
7525
|
_pa_run auto "$_pc"; [ "$_par" = "$_pw" ] || _pabad="$_pabad [$_pc → $_par, want $_pw]"
|
|
7403
7526
|
done <<< "$PCT"
|
|
7404
|
-
if [ "$_pan" !=
|
|
7405
|
-
elif [ -z "$_pabad" ]; then pass "an approved commit is 'git commit -m …' alone in its call:
|
|
7527
|
+
if [ "$_pan" != 66 ]; then fail "FIXTURE: the approved-commit table has $_pan rows, not 66"
|
|
7528
|
+
elif [ -z "$_pabad" ]; then pass "an approved commit is 'git commit -m …' alone in its call: 10 spellings allowed (any single-quoted message, a here-document with a quoted delimiter, forty messages in one call, blank lines around the call, a continued line with nothing after it, a 2>&1 on a line of its own), 56 refused — a cd or a variable in front, a second command however it is quoted, a substitution, a redirection, -a or a path past a redirection, -n, an abbreviated --no-verify or --amend, any other option, a backslash outside quotes (the last character of the call too), a quote never closed, a \$'…' text, a here-document whose delimiter is not a plain word or whose body holds the delimiter twice, a glob in the message (66 rows)"
|
|
7406
7529
|
else fail "approved-commit table:$_pabad"; fi
|
|
7407
7530
|
|
|
7408
7531
|
# What is staged, HEAD, the clock: each one moved on its own, with the others still matching.
|
|
@@ -7772,6 +7895,113 @@ _pa_say auto 'approve: commit' >/dev/null; _pa_gate auto 'git commit -m x'; _pav
|
|
|
7772
7895
|
_pa_new; _pa_gate auto 'git commit -m x'; _pav2="$_par"
|
|
7773
7896
|
[ "$_pav1/$_pav2" = 0/2 ] && pass "the wiring check moves no verdict: a recorded approval is allowed (rc 0) and its absence refused (rc 2) with no settings file" \
|
|
7774
7897
|
|| fail "the wiring check moved a verdict: approved $_pav1 (want 0), unapproved $_pav2 (want 2)"
|
|
7898
|
+
# ---- THE APPROVAL QUESTION (3.2.0): one click in auto, recorded as the typed approval is -------------------------
|
|
7899
|
+
# guard-question.sh on PreToolUse for AskUserQuestion answers `ask` to the fixed question (a hook's `ask` beats another
|
|
7900
|
+
# hook's `allow` with filled-in answers: measured on Claude Code 2.1.294, 34 runs and by hand), PostToolUse records
|
|
7901
|
+
# (prompt-approval.sh, on PostToolUse) the user's answer when THIS call was asked. Before either, and before guard-bash.sh raises its own prompt in
|
|
7902
|
+
# default / acceptEdits, a PermissionRequest hook is looked for: such a hook answers a prompt before the person does.
|
|
7903
|
+
# Every case runs the hooks against a real repository; what is asserted is a verdict or a record.
|
|
7904
|
+
_pa_new; mkdir -p "$_PA/ccd" "$_PA/home"
|
|
7905
|
+
_PAQ_ENV="CLAUDE_PROJECT_DIR=$_paw CLAUDE_CONFIG_DIR=$_PA/ccd HOME=$_PA/home CREW_GATE_LOG=/dev/null"
|
|
7906
|
+
_paq_canon="$(bash -c '. "$1"; printf "%s" "$_CREW_Q_JSON"' _ "$HOOKS/prompt-approval.sh")"
|
|
7907
|
+
_paq_text="$(bash -c '. "$1"; printf "%s" "$_CREW_Q_TEXT"' _ "$HOOKS/prompt-approval.sh")"
|
|
7908
|
+
# $1 = mode, $2 = the questions array's content, $3 = more tool_input (",..."), $4 = tool_use_id -> rc in _par, stdout _pao
|
|
7909
|
+
_paq_pre(){ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"%s","hook_event_name":"PreToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s]%s},"tool_use_id":"%s"}' "$_patr" "$_paw" "$1" "$2" "${3:-}" "${4:-tu1}" > "$_PA/q.json"
|
|
7910
|
+
if [ -n "$JSONQ" ]; then json_ok < "$_PA/q.json" || _paqbad="$_paqbad [pre payload is not JSON]"; fi
|
|
7911
|
+
_pao="$( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV ${_PAQ_X:-} bash "$HOOKS/guard-question.sh" < "$_PA/q.json" 2>"$_PA/qerr" )"; _par=$?; }
|
|
7912
|
+
# $1 = mode, $2 = the answer, $3 = tool_use_id
|
|
7913
|
+
_paq_post(){ printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"%s","hook_event_name":"PostToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[%s],"answers":{"%s":"%s"}},"tool_response":{"questions":[%s],"answers":{"%s":"%s"}},"tool_use_id":"%s"}' "$_patr" "$_paw" "$1" "$_paq_canon" "$_paq_text" "$2" "$_paq_canon" "$_paq_text" "$2" "${3:-tu1}" > "$_PA/q.json"
|
|
7914
|
+
if [ -n "$JSONQ" ]; then json_ok < "$_PA/q.json" || _paqbad="$_paqbad [post payload is not JSON]"; fi
|
|
7915
|
+
_pao="$( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV ${_PAQ_X:-} bash "$HOOKS/prompt-approval.sh" < "$_PA/q.json" 2>"$_PA/qerr" )"; _par=$?; }
|
|
7916
|
+
_paq_gate(){ printf '{"session_id":"s","cwd":"%s","permission_mode":"%s","tool_name":"Bash","tool_input":{"command":"%s"}}' "$_paw" "$1" "$2" > "$_PA/pl.json"
|
|
7917
|
+
_pao="$( cd "$_paw" && env $_PAQ_ENV ${_PAQ_X:-} bash "$HOOKS/guard-bash.sh" < "$_PA/pl.json" 2>"$_PA/err" )"; _par=$?; _pae="$(cat "$_PA/err")"; }
|
|
7918
|
+
_paq_rec(){ [ -s "$_pa_rec" ] && _pa_op || echo none; }
|
|
7919
|
+
_paqbad=""; _paqn=0
|
|
7920
|
+
_paq_is(){ _paqn=$((_paqn+1)); [ "$2" = "$3" ] || _paqbad="$_paqbad [$1: got '$2', want '$3']"; }
|
|
7921
|
+
# the round trip, and what it opens
|
|
7922
|
+
: > "$_pa_rec"; _paq_pre auto "$_paq_canon"; _paq_is 'fixed question in auto is asked' "$_par/$(gdec "$_pao")" "0/ask"
|
|
7923
|
+
_paq_post auto 'Commit only'; _paq_is 'its answer is recorded' "$(_paq_rec)" commit
|
|
7924
|
+
_paq_gate auto 'git commit -m x'; _paq_is 'the recorded click allows the commit' "$_par" 0
|
|
7925
|
+
_pa_new; : > "$_pa_rec"; _paq_pre auto "$_paq_canon" '' tu2; _paq_post auto 'Commit + push' tu2; _paq_is 'commit + push' "$(_paq_rec)" commit+push
|
|
7926
|
+
# must-fail twins: each differs from the round trip in one thing
|
|
7927
|
+
_pa_new; : > "$_pa_rec"; _paq_post auto 'Commit only' tu3; _paq_is 'an answer no PreToolUse asked for' "$(_paq_rec)" none
|
|
7928
|
+
_paq_pre auto "$_paq_canon" '' tu4; _paq_post auto 'Commit only' tu5; _paq_is 'an answer to another call' "$(_paq_rec)" none
|
|
7929
|
+
_paq_pre auto "$_paq_canon" '' tu6; _paq_post auto 'Commit only' tu6; : > "$_pa_rec"; _paq_post auto 'Commit only' tu6
|
|
7930
|
+
_paq_is 'the same call answered twice' "$(_paq_rec)" none
|
|
7931
|
+
_paq_pre auto "$_paq_canon" '' tu7; _paq_post auto 'HOOK-CHOSE-Commit only' tu7; _paq_is 'an answer that is none of the four' "$(_paq_rec)" none
|
|
7932
|
+
_paq_pre auto "$_paq_canon" '' tu8; _paq_post auto 'No' tu8; _paq_is 'No' "$(_paq_rec)" none
|
|
7933
|
+
_paq_pre auto "$_paq_canon" '' tu9; _paq_post default 'Commit only' tu9; _paq_is 'an answer recorded outside auto' "$(_paq_rec)" none
|
|
7934
|
+
_paq_pre auto '{"question":"Crewforth approval: commit?","header":"Approval","options":[{"label":"Commit only","description":"x"}],"multiSelect":false}'
|
|
7935
|
+
_paq_is 'another wording is refused' "$_par" 2
|
|
7936
|
+
_paq_pre auto "$_paq_canon" ',"answers":{"x":"Commit only"}'; _paq_is 'a call with answers is refused' "$_par" 2
|
|
7937
|
+
_paq_pre auto "$_paq_canon,$_paq_canon"; _paq_is 'the question twice is refused' "$_par" 2
|
|
7938
|
+
_paq_pre dontAsk "$_paq_canon"; _paq_is 'dontAsk: refused' "$_par" 2
|
|
7939
|
+
_paq_pre default "$_paq_canon"; _paq_is 'default: refused' "$_par" 2
|
|
7940
|
+
_paq_pre bypassPermissions "$_paq_canon"; _paq_is 'bypassPermissions: refused' "$_par" 2
|
|
7941
|
+
_PAQ_X="CLAUDE_CODE_SESSION_ATTENDED=0"; _paq_pre auto "$_paq_canon"; unset _PAQ_X; _paq_is 'a headless session: refused' "$_par" 2
|
|
7942
|
+
_paq_pre auto '{"question":"Which colour?","header":"Colour","options":[{"label":"Red","description":"r"}],"multiSelect":false}'
|
|
7943
|
+
_paq_is 'any other question: no decision' "$_par/$_pao" "0/"
|
|
7944
|
+
if [ "$_paqn" != 18 ]; then fail "FIXTURE: the approval-question table ran $_paqn cases, not 18"
|
|
7945
|
+
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
|
+
else fail "the approval question:$_paqbad"; fi
|
|
7947
|
+
|
|
7948
|
+
# A PermissionRequest HOOK, wherever this can read one, closes the question AND the gate's own prompt. Each source is
|
|
7949
|
+
# planted alone in an otherwise clean fixture; the clean fixture itself must answer "none" (calibration).
|
|
7950
|
+
_paq_scan(){ ( cd "$_paw" && env $_PAQ_ENV ${_PAQ_X:-} bash -c '. "$1"; if _crew_prh_scan; then printf "found %s" "$_PRH"; else printf none; fi' _ "$HOOKS/prompt-approval.sh" ) 2>/dev/null; }
|
|
7951
|
+
_paqbad=""; _paqn=0
|
|
7952
|
+
_paq_clean(){ rm -rf "$_PA/ccd" "$_PA/home" "$_paw/.claude/skills" "$_paw/.claude/agents" "$_paw/.claude/settings.local.json" "$_PA/mng.json"; mkdir -p "$_PA/ccd" "$_PA/home"; }
|
|
7953
|
+
_paq_found(){ _paqn=$((_paqn+1)); case "$(_paq_scan)" in "found "*"$2"*) ;; *) _paqbad="$_paqbad [$1: $(_paq_scan)]" ;; esac; _paq_clean; }
|
|
7954
|
+
_paq_none(){ _paqn=$((_paqn+1)); [ "$(_paq_scan)" = none ] || _paqbad="$_paqbad [$1 wrongly: $(_paq_scan)]"; _paq_clean; }
|
|
7955
|
+
_PRQ='{"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"x"}]}]}}'
|
|
7956
|
+
_paq_clean; _paq_none 'a clean fixture'
|
|
7957
|
+
printf '%s' "$_PRQ" > "$_paw/.claude/settings.local.json"; _paq_found 'project settings.local.json' settings.local.json
|
|
7958
|
+
printf '%s' "$_PRQ" > "$_PA/ccd/settings.json"; _paq_found 'user settings (CLAUDE_CONFIG_DIR)' ccd/settings.json
|
|
7959
|
+
printf '%s' "$_PRQ" > "$_PA/mng.json"; _PAQ_X="CREW_MANAGED_SETTINGS=$_PA/mng.json"; _paq_found 'a managed file' mng.json; unset _PAQ_X
|
|
7960
|
+
printf '%s' '{"hooks":{"Permission\u0052equest":[]}}' > "$_paw/.claude/settings.local.json"; _paq_found 'the name spelled with a \u escape' settings.local.json
|
|
7961
|
+
mkdir -p "$_paw/.claude/skills/s"; printf -- '---\nname: s\nhooks:\n PermissionRequest:\n - hooks: []\n---\nbody\n' > "$_paw/.claude/skills/s/SKILL.md"; _paq_found 'skill front matter' SKILL.md
|
|
7962
|
+
mkdir -p "$_paw/.claude/agents"; printf -- '---\nname: a\nhooks:\n "Permission\\x52equest": []\n---\n' > "$_paw/.claude/agents/a.md"; _paq_found 'agent front matter, escaped' a.md
|
|
7963
|
+
mkdir -p "$_PA/ccd/plugins/cache/m/p/1/hooks"; printf '%s' "$_PRQ" > "$_PA/ccd/plugins/cache/m/p/1/hooks/hooks.json"; _paq_found 'an installed plugin' hooks.json
|
|
7964
|
+
mkdir -p "$_paw/.claude/skills/s"; printf -- '---\nname: s\ndescription: about hooks\n---\nA PermissionRequest hook answers a prompt.\n' > "$_paw/.claude/skills/s/SKILL.md"; _paq_none 'the name in a skill body, not its front matter'
|
|
7965
|
+
printf '%s' '{"hooks":{"PreToolUse":[]},"x":"c:\\users\\me"}' > "$_paw/.claude/settings.local.json"; _paq_none 'an escaped backslash before u'
|
|
7966
|
+
if [ "$_paqn" != 10 ]; then fail "FIXTURE: the PermissionRequest scan table ran $_paqn cases, not 10"
|
|
7967
|
+
elif [ -z "$_paqbad" ]; then pass "a PermissionRequest hook is found in project, user and managed settings, an escaped spelling, skill and agent front matter and an installed plugin (7); a clean fixture, a skill body and an escaped backslash are not (3)"
|
|
7968
|
+
else fail "the PermissionRequest scan:$_paqbad"; fi
|
|
7969
|
+
|
|
7970
|
+
# What it closes: the question, the recording of an answer, and in default the gate's prompt. What it leaves: the typed
|
|
7971
|
+
# approval, in default too. Twin: the same fixture without the hook asks (default) and offers the question (auto).
|
|
7972
|
+
_paqbad=""
|
|
7973
|
+
_pa_new; _paq_clean
|
|
7974
|
+
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"}]}]}}' > "$_paw/.claude/settings.json"
|
|
7975
|
+
_paq_gate default 'git commit -m x'; [ "$_par/$(gdec "$_pao")" = "0/ask" ] || _paqbad="$_paqbad [no hook, default: $_par/$(gdec "$_pao"), want 0/ask]"
|
|
7976
|
+
_paq_gate auto 'git commit -m x'; case "$_par/$_pae" in 2/*AskUserQuestion*"$_paq_text"*) ;; *) _paqbad="$_paqbad [no hook, auto: the refusal does not offer the question]" ;; esac
|
|
7977
|
+
printf '%s' "$_PRQ" > "$_paw/.claude/settings.local.json"
|
|
7978
|
+
_paq_gate default 'git commit -m x'; case "$_par/$_pae" in 2/*PermissionRequest*settings.local.json*/crew-approve*) ;; *) _paqbad="$_paqbad [hook, default: $_par, want 2 naming the file and /crew-approve]" ;; esac
|
|
7979
|
+
case "$_pae" in *'Shift+Tab to default/acceptEdits (this gate then asks'*) _paqbad="$_paqbad [hook, default: offers Shift+Tab]" ;; esac
|
|
7980
|
+
_paq_gate auto 'git commit -m x'; case "$_par/$_pae" in 2/*"approval question is closed"*) case "$_pae" in *AskUserQuestion*) _paqbad="$_paqbad [hook, auto: still offers the question]" ;; esac ;; *) _paqbad="$_paqbad [hook, auto: $_par, the question is not said to be closed]" ;; esac
|
|
7981
|
+
_paq_pre auto "$_paq_canon" '' tuh; [ "$_par" = 2 ] || _paqbad="$_paqbad [hook: the question was asked ($_par)]"
|
|
7982
|
+
rm -f "$_paw/.claude/settings.local.json"; _paq_pre auto "$_paq_canon" '' tuh2; printf '%s' "$_PRQ" > "$_paw/.claude/settings.local.json"
|
|
7983
|
+
: > "$_pa_rec"; _paq_post auto 'Commit only' tuh2; [ "$(_paq_rec)" = none ] || _paqbad="$_paqbad [a hook added after the question: its answer was recorded]"
|
|
7984
|
+
printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"default","hook_event_name":"UserPromptSubmit","prompt":"/crew-approve commit"}' "$_patr" "$_paw" \
|
|
7985
|
+
| ( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV bash "$HOOKS/prompt-approval.sh" ) >/dev/null 2>&1
|
|
7986
|
+
[ "$(_paq_rec)" = commit ] || _paqbad="$_paqbad [hook, default: /crew-approve commit was not recorded]"
|
|
7987
|
+
_paq_gate default 'git commit -m x'; [ "$_par" = 0 ] || _paqbad="$_paqbad [hook, default: the typed approval did not allow the commit ($_par)]"
|
|
7988
|
+
rm -f "$_paw/.claude/settings.local.json"; _pa_new
|
|
7989
|
+
printf '{"session_id":"s","transcript_path":"%s","cwd":"%s","permission_mode":"default","hook_event_name":"UserPromptSubmit","prompt":"/crew-approve commit"}' "$_patr" "$_paw" \
|
|
7990
|
+
| ( cd "$_paw" && env -u CLAUDE_CODE_SESSION_ATTENDED $_PAQ_ENV bash "$HOOKS/prompt-approval.sh" ) >/dev/null 2>&1
|
|
7991
|
+
[ "$(_paq_rec)" = none ] || _paqbad="$_paqbad [no hook, default: /crew-approve recorded although the gate asks there]"
|
|
7992
|
+
if [ -z "$_paqbad" ]; then pass "a PermissionRequest hook closes the question, the recording of its answer and the gate's own prompt in default, and leaves /crew-approve (which then allows the commit); without one, default asks, auto offers the question, and a typed approval in default records nothing (9 readings)"
|
|
7993
|
+
else fail "what a PermissionRequest hook closes:$_paqbad"; fi
|
|
7994
|
+
|
|
7995
|
+
# The record of the question is the gate's own: closed to the session like the pending note.
|
|
7996
|
+
_paqbad=""
|
|
7997
|
+
for _pqc in 'cat .claude/state/crew-approval-asked' 'printf x > .claude/state/crew-approval-asked' 'rm -f .claude/state/crew-approval-asked'; do
|
|
7998
|
+
_paq_gate auto "$_pqc"; [ "$_par" = 2 ] || _paqbad="$_paqbad [$_pqc: rc $_par]"
|
|
7999
|
+
done
|
|
8000
|
+
printf '{"cwd":"%s","permission_mode":"auto","tool_name":"Write","tool_input":{"file_path":"%s/.claude/state/crew-approval-asked","content":"x"}}' "$_paw" "$_paw" \
|
|
8001
|
+
| ( cd "$_paw" && env $_PAQ_ENV bash "$HOOKS/guard-write.sh" ) >/dev/null 2>&1; [ $? = 2 ] || _paqbad="$_paqbad [Write: not refused]"
|
|
8002
|
+
[ -z "$_paqbad" ] && pass "the question's own note (.claude/state/crew-approval-asked) is closed to the session: three shell forms and the Write tool refused" \
|
|
8003
|
+
|| fail "the question's note is open:$_paqbad"
|
|
8004
|
+
rm -f "$_paw/.claude/settings.json"; _pa_new
|
|
7775
8005
|
fi # prompt-approval.sh present
|
|
7776
8006
|
|
|
7777
8007
|
# ---- the two definitions that live in two files, and the wiring -----------------------------------------------
|
|
@@ -7800,6 +8030,22 @@ if [ "$IS_KIT" = 1 ]; then
|
|
|
7800
8030
|
elif [ -z "$_phm" ]; then pass "every script the plugin's hooks.json runs is in plugin/hooks ($_phn scripts)"
|
|
7801
8031
|
else fail "the plugin's hooks.json runs a script the plugin does not carry:$_phm (add it to the list in packaging/build-plugin.sh)"; fi
|
|
7802
8032
|
fi
|
|
8033
|
+
# ...and the approval question: guard-question.sh asks it (PreToolUse), prompt-approval.sh records its answer
|
|
8034
|
+
# (PostToolUse), each once on AskUserQuestion, in both editions.
|
|
8035
|
+
for _pf in $_pawf; do
|
|
8036
|
+
_pn="$(json_hooks "$_pf" | LC_ALL=C awk -F'\t' '$2 == "AskUserQuestion" { if ($1 == "PreToolUse" && $3 ~ /guard-question\.sh/) a++; if ($1 == "PostToolUse" && $3 ~ /prompt-approval\.sh/) b++ } END { printf "%d %d", a, b }')"
|
|
8037
|
+
[ "$_pn" = "1 1" ] && pass "${_pf##*/}: the approval question is wired on AskUserQuestion: guard-question.sh once on PreToolUse, prompt-approval.sh once on PostToolUse" \
|
|
8038
|
+
|| fail "${_pf##*/}: the approval question is not wired once each (guard-question.sh on PreToolUse, prompt-approval.sh on PostToolUse; found: $_pn)"
|
|
8039
|
+
done
|
|
8040
|
+
# The managed settings files are one list: the ones model routing reads its rules from are the ones the approval
|
|
8041
|
+
# question looks for a PermissionRequest hook in. Compared as the lines that name a managed-settings.json.
|
|
8042
|
+
_pml(){ grep -oE '"[^"]*managed-settings\.json"|CREW_MANAGED_SETTINGS' "$1" 2>/dev/null | sort -u | tr '\n' ' '; }
|
|
8043
|
+
_pm1="$(_pml "$HOOKS/prompt-approval.sh")"; _pm2="$(_pml "$HOOKS/guard-agent-model.sh")"
|
|
8044
|
+
case "$_pm1" in *managed-settings.json*managed-settings.json*managed-settings.json*CREW_MANAGED_SETTINGS*)
|
|
8045
|
+
[ "$_pm1" = "$_pm2" ] && pass "the managed settings files are one list in prompt-approval.sh and guard-agent-model.sh ($(printf '%s' "$_pm1" | tr ' ' '\n' | grep -c .) entries)" \
|
|
8046
|
+
|| fail "the managed settings lists differ: prompt-approval.sh [$_pm1] guard-agent-model.sh [$_pm2]" ;;
|
|
8047
|
+
*) fail "FIXTURE: the managed settings list could not be read from prompt-approval.sh ([$_pm1])" ;;
|
|
8048
|
+
esac
|
|
7803
8049
|
# ...and on PreToolUse for the tools that schedule a prompt, in both editions, with one matcher.
|
|
7804
8050
|
for _pf in $_pawf; do
|
|
7805
8051
|
_pn="$(json_hooks "$_pf" | LC_ALL=C awk -F'\t' '$1 == "PreToolUse" && $3 ~ /guard-schedule\.sh/ { n++; m = $2 } END { printf "%d %s", n, m }')"
|
|
@@ -7989,7 +8235,7 @@ _v2new(){ rm -rf "$_v2p" "$_v2/home"; mkdir -p "$_v2/home" "$_v2p/.claude" "$_v2
|
|
|
7989
8235
|
_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:-} "$@"; }
|
|
7990
8236
|
# $1 agent, $2 model, $3 files, $4 change, $5 verify, $6 extra fields -> _v2rc, stderr in $_v2/err
|
|
7991
8237
|
_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"
|
|
7992
|
-
( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/c.json"
|
|
8238
|
+
( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/c.json" >"$_v2/out" 2>"$_v2/err" ); _v2rc=$?; }
|
|
7993
8239
|
_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 ); }
|
|
7994
8240
|
_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 ); }
|
|
7995
8241
|
_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 ); }
|
|
@@ -8022,6 +8268,19 @@ done <<'V2'
|
|
|
8022
8268
|
2|crew-backend-expert|sonnet|prisma/schema.prisma|feature|none|
|
|
8023
8269
|
2|crew-backend-expert|sonnet|config/SECRETS_local.env|text|none|
|
|
8024
8270
|
0|crew-backend-expert|sonnet|docs/author.md, src/cryptography/hash.ts|feature|none|
|
|
8271
|
+
2|crew-backend-expert|sonnet|legal/terms-of-service.md|text|none|
|
|
8272
|
+
2|crew-backend-expert|sonnet|docs/privacy-policy.md|text|none|
|
|
8273
|
+
2|crew-backend-expert|sonnet|src/PrivacyController.cs|feature|none|
|
|
8274
|
+
2|crew-backend-expert|sonnet|src/consent/banner.ts|feature|none|
|
|
8275
|
+
2|crew-backend-expert|sonnet|content/terms/index.md|text|none|
|
|
8276
|
+
2|crew-backend-expert|sonnet|fastlane/metadata/en-US/description.txt|text|none|
|
|
8277
|
+
2|crew-backend-expert|sonnet|store/listing.md|text|none|
|
|
8278
|
+
2|crew-backend-expert|sonnet|metadata/android/en-US/full_description.txt|text|none|
|
|
8279
|
+
0|crew-backend-expert|opus|legal/terms-of-service.md|text|none|
|
|
8280
|
+
0|crew-backend-expert|sonnet|src/searchTerms.ts|feature|none|
|
|
8281
|
+
0|crew-backend-expert|sonnet|src/store/index.ts|feature|none|
|
|
8282
|
+
0|crew-backend-expert|sonnet|fastlane/metadata/en-US/images/icon.png|text|none|
|
|
8283
|
+
0|crew-backend-expert|sonnet|docs/legalese.md|text|none|
|
|
8025
8284
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|none|
|
|
8026
8285
|
2|crew-backend-expert|haiku|src/ui/a.ts|text|none|
|
|
8027
8286
|
0|crew-backend-expert|sonnet|src/ui/a.ts|text|none|
|
|
@@ -8055,9 +8314,48 @@ done <<'V2'
|
|
|
8055
8314
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|git commit -m x|
|
|
8056
8315
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|rm -rf /|
|
|
8057
8316
|
V2
|
|
8058
|
-
if [ "$_v2n" !=
|
|
8317
|
+
if [ "$_v2n" != 60 ]; then fail "FIXTURE: the card table has $_v2n rows, not 60"
|
|
8059
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) (47 rows)"
|
|
8060
8319
|
else fail "card table:$_v2b"; fi
|
|
8320
|
+
# 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
|
+
# replaced, PreToolUse updatedInput), unless the card's next line says `above: <why>`. Measured on a real project:
|
|
8322
|
+
# 73 of 186 crew tasks ran above their floor and none on haiku; measured on Claude Code 2.1.299: the rewrite holds.
|
|
8323
|
+
_v2new; _v2b=""
|
|
8324
|
+
_v2last(){ ls -t "$_v2p/.claude/state/crew-model/pending/"* 2>/dev/null | head -1; }
|
|
8325
|
+
_v2call crew-review-agent opus 'src/ui/a.ts' audit none ',"run_in_background":true,"isolation":"worktree"'
|
|
8326
|
+
_o="$(cat "$_v2/out" 2>/dev/null)"; _r="$(_v2last)"
|
|
8327
|
+
case "$_o" in *'"permissionDecision":"allow"'*'"updatedInput":'*) ;; *) _v2b="$_v2b [above the floor, no above: -> no rewrite: rc=$_v2rc ${_o:0:80}]" ;; esac
|
|
8328
|
+
_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')"
|
|
8329
|
+
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
|
|
8330
|
+
case "$_u" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [the rewrite is not to the floor: ${_u:0:120}]" ;; esac
|
|
8331
|
+
{ 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)]"
|
|
8332
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8333
|
+
_v2call crew-review-agent opus 'src/ui/a.ts' audit 'none\nabove: the audit covers the whole session layer'
|
|
8334
|
+
{ [ "$_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")]"
|
|
8335
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8336
|
+
_v2call crew-review-agent sonnet 'src/ui/a.ts' audit none
|
|
8337
|
+
{ [ "$_v2rc" = 0 ] && [ ! -s "$_v2/out" ]; } || _v2b="$_v2b [a call on its floor was rewritten: $(head -c 60 "$_v2/out")]"
|
|
8338
|
+
_v2call crew-frontend-expert sonnet 'src/ui/a.ts' text 'test -d src'
|
|
8339
|
+
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
|
|
8340
|
+
rm -f "$_v2p/.claude/state/crew-model/pending/"*
|
|
8341
|
+
# Not a crew agent: no card and no floor, only the expensive end is held. opus or fable with no `above:` line in the
|
|
8342
|
+
# task runs on sonnet; with one it stays; a call that names no model, or sonnet, is not touched.
|
|
8343
|
+
_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 ' '; }
|
|
8344
|
+
case "$(_v2o '' ',"model":"opus"')" in *'"updatedInput":'*'"model":"sonnet"'*) ;; *) _v2b="$_v2b [general-purpose on opus was not run on sonnet]" ;; esac
|
|
8345
|
+
[ -z "$(_v2o '\nabove: a design question across the codebase' ',"model":"opus"')" ] || _v2b="$_v2b [above: did not keep opus for general-purpose]"
|
|
8346
|
+
[ -z "$(_v2o '' ',"model":"sonnet"')" ] || _v2b="$_v2b [a call naming sonnet was rewritten]"
|
|
8347
|
+
# Naming no model: general-purpose takes the session's model, so it is given sonnet (CLAUDE_CODE_SUBAGENT_MODEL in
|
|
8348
|
+
# the project's settings env did not do it, measured on 2.1.299), unless the session is on haiku; an agent whose
|
|
8349
|
+
# definition names its own model is left to it; a project agent with no model line is treated like general-purpose.
|
|
8350
|
+
_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 ' '; }
|
|
8351
|
+
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"
|
|
8352
|
+
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"
|
|
8353
|
+
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
|
|
8354
|
+
[ -z "$(_v2ot general-purpose "$_v2/tr-haiku.jsonl")" ] || _v2b="$_v2b [a haiku session's general-purpose was raised to sonnet]"
|
|
8355
|
+
[ -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]"
|
|
8356
|
+
case "$(_v2ot mine "$_v2/tr-opus.jsonl")" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [a project agent with no model line was not given sonnet]" ;; esac
|
|
8357
|
+
[ -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" \
|
|
8358
|
+
|| fail "model on the floor:$_v2b"
|
|
8061
8359
|
# The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
|
|
8062
8360
|
# left the field out and was launched in the background is told so when the launch comes back.
|
|
8063
8361
|
_v2new; _v2b=""
|
|
@@ -8113,6 +8411,17 @@ _v2write "$_v2p/src/payments/p.ts" ',"agent_id":"ag-gp","agent_type":"general-pu
|
|
|
8113
8411
|
_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"
|
|
8114
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" \
|
|
8115
8413
|
|| fail "write-time check:$_v2b"
|
|
8414
|
+
# 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
|
+
# log naming the path and the rule. A write elsewhere, one outside the project, and one with routing off add none.
|
|
8416
|
+
_v2b=""; _b2l="$_v2/b2.tsv"
|
|
8417
|
+
_b2(){ : > "$_b2l"; _V2ENV="CREW_GATE_LOG=$_b2l ${2:-}" _v2write "$1"; _b2n="$(grep -c '^NOTE' "$_b2l" 2>/dev/null)"; }
|
|
8418
|
+
_b2 "$_v2p/src/payments/p.ts"; [ "$_v2wrc/$_b2n" = 0/1 ] || _v2b="$_v2b [critical: rc $_v2wrc, $_b2n notes]"
|
|
8419
|
+
grep -q "^NOTE"$'\t'"§model"$'\t'"note: the session wrote a critical path itself, not through an agent: src/payments/p.ts (payments)" "$_b2l" || _v2b="$_v2b [line: $(head -1 "$_b2l")]"
|
|
8420
|
+
_b2 "$_v2p/src/ui/a.ts"; [ "$_v2wrc/$_b2n" = 0/0 ] || _v2b="$_v2b [plain: rc $_v2wrc, $_b2n notes]"
|
|
8421
|
+
_b2 "/tmp/elsewhere/payments/p.ts"; [ "$_v2wrc/$_b2n" = 0/0 ] || _v2b="$_v2b [outside: rc $_v2wrc, $_b2n notes]"
|
|
8422
|
+
_b2 "$_v2p/src/payments/p.ts" CREW_MODEL_ROUTING=off; [ "$_v2wrc/$_b2n" = 0/0 ] || _v2b="$_v2b [routing off: rc $_v2wrc, $_b2n notes]"
|
|
8423
|
+
[ -z "$_v2b" ] && pass "the session's own write to a critical path is allowed and recorded as a NOTE with the path and the rule; a plain write, one outside the project and one with routing off are not recorded" \
|
|
8424
|
+
|| fail "the session's critical write (B2):$_v2b"
|
|
8116
8425
|
|
|
8117
8426
|
# ---- 2. the critical paths of a project with no settings file: a Flutter app and a .NET service ---------------
|
|
8118
8427
|
_v2new; _v2b=""
|
|
@@ -8170,7 +8479,7 @@ _v2call crew-backend-expert sonnet 'src/ui/tz.ts' feature none; _v2start tz1; _V
|
|
|
8170
8479
|
_tzg="$(awk -F'\t' '$2 == "tz1" { print substr($1, 1, 13) }' "$_v2p/.claude/state/model-outcomes.tsv")"; _tzn="$(date -u +%Y-%m-%dT%H)"; _tzp="$(date -u -v-1H +%Y-%m-%dT%H 2>/dev/null || date -u -d '1 hour ago' +%Y-%m-%dT%H 2>/dev/null)"
|
|
8171
8480
|
{ [ -n "$_tzg" ] && { [ "$_tzg" = "$_tzn" ] || [ "$_tzg" = "$_tzp" ]; }; } || _v2b="$_v2b [the time is not UTC: wrote $_tzg, UTC is $_tzn]"
|
|
8172
8481
|
IFS= read -r _h < "$_v2p/.claude/state/model-outcomes.tsv"
|
|
8173
|
-
[ "$_h" = "ts"$'\t'"agent_id"$'\t'"agent"$'\t'"change"$'\t'"risk"$'\t'"card"$'\t'"model"$'\t'"verify"$'\t'"fixes"$'\t'"escalated_from"$'\t'"ran_on"$'\t'"confidence" ] || _v2b="$_v2b [header: $_h]"
|
|
8482
|
+
[ "$_h" = "ts"$'\t'"agent_id"$'\t'"agent"$'\t'"change"$'\t'"risk"$'\t'"card"$'\t'"model"$'\t'"verify"$'\t'"fixes"$'\t'"escalated_from"$'\t'"ran_on"$'\t'"confidence"$'\t'"risk_why" ] || _v2b="$_v2b [header: $_h]"
|
|
8174
8483
|
[ -z "$_v2b" ] && pass "verify then escalate: red once keeps the agent running with the failing command; green after it is a pass with one fix; red twice ends the agent as a fail, recorded once; the session is told to repeat one model up; the same card is refused on the same model and allowed one up; red there too is the user's question and a third run is refused; a changed card is a new task; confidence: low is told the same way; a background launch and an agent with no record are left alone; the record's twelve columns are the ones published, and its time is UTC under a zone fourteen hours ahead" \
|
|
8175
8484
|
|| fail "verify-then-escalate:$_v2b"
|
|
8176
8485
|
|
|
@@ -8446,12 +8755,38 @@ if command -v iconv >/dev/null 2>&1; then printf '%s' "$_o" | iconv -f UTF-8 -t
|
|
|
8446
8755
|
[ "${#_o}" -le 2600 ] || _v2b="$_v2b [the reason is ${#_o} bytes]"
|
|
8447
8756
|
[ -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" \
|
|
8448
8757
|
|| fail "the stop hook's one fix:$_v2b"
|
|
8758
|
+
# A verify command in a sub-folder, `cd <folder> && <command>` (41 of 71 refusals on one project): judged as the
|
|
8759
|
+
# command and run in the folder with no chain. A folder outside the project, one that does not exist, or a second
|
|
8760
|
+
# chain after it stays refused. A card that says `none (why)` is none.
|
|
8761
|
+
_v2new; _v2b=""; mkdir -p "$_v2p/.claude/state/crew-model/agents" "$_v2p/sub"; : > "$_v2p/sub/ok.flag"
|
|
8762
|
+
printf 'verify test -f ok.flag\n' > "$_v2p/.claude/crew-model-rules"
|
|
8763
|
+
_v2plant c1 'cd sub && test -f ok.flag'; _v2stop c1 >/dev/null
|
|
8764
|
+
[ "$(_v2row c1 | cut -d/ -f2)" = pass ] || _v2b="$_v2b [cd sub && test: $(_v2row c1)]"
|
|
8765
|
+
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
|
|
8766
|
+
_v2plant "${_cc%%:*}" "${_cc#*:}"; _v2stop "${_cc%%:*}" >/dev/null
|
|
8767
|
+
[ "$(_v2row "${_cc%%:*}" | cut -d/ -f2)" = blocked ] || _v2b="$_v2b [${_cc#*:} was not refused: $(_v2row "${_cc%%:*}")]"
|
|
8768
|
+
done
|
|
8769
|
+
[ ! -e "$_v2p/sub/x" ] || _v2b="$_v2b [a chain after the folder ran]"
|
|
8770
|
+
_v2call crew-backend-expert sonnet 'src/ui/a.ts' text 'none (the author ran it)'
|
|
8771
|
+
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]"
|
|
8772
|
+
# doctor lists the commands refused for not being allowed, with the line that allows them, and not a sentence
|
|
8773
|
+
mkdir -p "$_v2p/.claude/state/crew-model/results"
|
|
8774
|
+
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
|
+
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"
|
|
8776
|
+
if [ -f "$ROOT/eval/doctor.sh" ]; then
|
|
8777
|
+
mkdir -p "$_v2p/.claude/eval"; cp -R "$ROOT/eval/." "$_v2p/.claude/eval/" 2>/dev/null
|
|
8778
|
+
_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 *'2 time(s)'*'verify node tools/build.mjs --check'*) case "$_dd" in *actionlint*) _v2b="$_v2b [doctor offered a sentence as a rule]" ;; esac ;;
|
|
8780
|
+
*) _v2b="$_v2b [doctor: ${_dd:-no line}]" ;; esac
|
|
8781
|
+
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 rules, not a sentence" \
|
|
8783
|
+
|| fail "verify in a sub-folder:$_v2b"
|
|
8449
8784
|
# Which record is whose. Two calls of one type made together are matched by order, which can be wrong; the card in
|
|
8450
8785
|
# the agent's own transcript settles it.
|
|
8451
8786
|
_v2new; _v2b=""; _v2st="$_v2p/.claude/state/crew-model"
|
|
8452
8787
|
_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"; }
|
|
8453
8788
|
_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 ); }
|
|
8454
|
-
_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'
|
|
8789
|
+
_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'
|
|
8455
8790
|
# The order the two records are taken in is made known: the haiku call's first, so each agent gets the OTHER's below.
|
|
8456
8791
|
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
|
|
8457
8792
|
_pt="$(date +%s)"; mv "$_v2st/pending/1-a" "$_v2st/pending/$_pt-a"; mv "$_v2st/pending/2-b" "$_v2st/pending/$_pt-b"
|
|
@@ -8486,11 +8821,35 @@ rm -f "$_v2st"/pending/*
|
|
|
8486
8821
|
_v2call crew-backend-expert sonnet 'src/ui/race.ts' text true
|
|
8487
8822
|
_v2start r1 & _v2start r2 & wait
|
|
8488
8823
|
_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]"
|
|
8824
|
+
ls "$_v2st/agents" 2>/dev/null | grep -qE '\.(take|claim)$' && _v2b="$_v2b a-lock-or-claim-left-behind"
|
|
8825
|
+
# A record another hook holds the lock on is not taken: the agent goes on to the next one (the lock is what keeps
|
|
8826
|
+
# two agents off one record where a rename alone did not, on Windows).
|
|
8827
|
+
rm -rf "$_v2st/agents"; rm -f "$_v2st"/pending/*
|
|
8828
|
+
_v2call crew-backend-expert sonnet 'src/ui/l1.ts' text 'test -f l1'; _pl1="$(ls "$_v2st/pending")"
|
|
8829
|
+
sleep 1; _v2call crew-backend-expert sonnet 'src/ui/l2.ts' text 'test -f l2'
|
|
8830
|
+
mkdir -p "$_v2st/agents"; : > "$_v2st/agents/$_pl1.take"
|
|
8831
|
+
_v2start k1
|
|
8832
|
+
{ 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"
|
|
8833
|
+
rm -f "$_v2st/agents/$_pl1.take" "$_v2st"/pending/*
|
|
8489
8834
|
# A record nothing claimed within five minutes is nobody's.
|
|
8490
8835
|
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"
|
|
8491
8836
|
_v2start o1; { [ ! -f "$_v2st/agents/o1" ] && [ ! -f "$_v2st/pending/1000000000-tuold" ]; } || _v2b="$_v2b a-stale-record-was-given-to-an-agent-or-kept"
|
|
8492
8837
|
[ -z "$_v2b" ] && pass "which record is whose: with two models pending for one agent type the agent is recorded on the lower one; a record is taken by rename, so of two agents starting together one gets it; an agent whose transcript carries the other card is judged by that card's record (a call nobody has taken yet is looked at first), a task given as a list of blocks is read, a transcript with no card is noted in the gate log and the record used as matched, an agent whose card no record carries is not judged, and a record older than five minutes is dropped" \
|
|
8493
8838
|
|| fail "matching a call to its agent:$_v2b"
|
|
8839
|
+
# Why the work was critical is kept with it, in the gate's own words, so a reader shows the reason and does not work
|
|
8840
|
+
# it out again: a card on a critical path carries the path and the rule that matched; normal work carries "-".
|
|
8841
|
+
_v2new; _v2b=""; _v2st="$_v2p/.claude/state/crew-model"
|
|
8842
|
+
_v2call crew-backend-expert opus 'src/payments/p.ts' feature 'test -d src' ',"run_in_background":false'
|
|
8843
|
+
[ "$_v2rc" = 0 ] || _v2b="$_v2b FIXTURE:[the critical call was refused, rc=$_v2rc: $(head -c 300 "$_v2/err")]"
|
|
8844
|
+
grep -q '^why=src/payments/p.ts is on a critical path: ' "$_v2st"/pending/* 2>/dev/null || _v2b="$_v2b [pending record: $(grep -h '^why=' "$_v2st"/pending/* 2>/dev/null)]"
|
|
8845
|
+
_v2start k1; _v2tr k1 'src/payments/p.ts' feature 'test -d src'; _v2stopt k1 >/dev/null
|
|
8846
|
+
_kw="$(awk -F'\t' '$2 == "k1" { print $13 }' "$_v2st/../model-outcomes.tsv" 2>/dev/null)"
|
|
8847
|
+
case "$_kw" in 'src/payments/p.ts is on a critical path: '?*) ;; *) _v2b="$_v2b [critical row: risk_why='$_kw']" ;; esac
|
|
8848
|
+
_v2call crew-backend-expert sonnet 'src/ui/a.ts' feature 'test -d src'; _v2start k2; _v2tr k2 'src/ui/a.ts' feature 'test -d src'; _v2stopt k2 >/dev/null
|
|
8849
|
+
_kn="$(awk -F'\t' '$2 == "k2" { print $5 "/" $13 }' "$_v2st/../model-outcomes.tsv" 2>/dev/null)"
|
|
8850
|
+
[ "$_kn" = "normal/-" ] || _v2b="$_v2b [normal row: '$_kn']"
|
|
8851
|
+
[ -z "$_v2b" ] && pass "the gate's reason for critical work reaches the record: the pending record and the outcome line carry the path and the rule that matched; normal work carries -" \
|
|
8852
|
+
|| fail "risk reason recorded:$_v2b"
|
|
8494
8853
|
# 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
|
|
8495
8854
|
# the write-time check; run_in_background named twice is refused.
|
|
8496
8855
|
_v2new; _v2b=""; _V2ENV="CREW_GATE_LOG=$_v2/gl.tsv"; : > "$_v2/gl.tsv"
|
|
@@ -8523,7 +8882,7 @@ _v2call crew-backend-expert sonnet 'src/ui/m3.ts' feature none; _v2start no1; _v
|
|
|
8523
8882
|
[ "$(_v2ran no1)" = "-" ] || _v2b="$_v2b [no model named: $(_v2ran no1)]"
|
|
8524
8883
|
_v2call crew-backend-expert sonnet 'src/ui/m4.ts' feature none; _v2start od1; _v2stop od1 >/dev/null; _v2postm od1 completed ',"resolvedModel":"a b\tc"'
|
|
8525
8884
|
[ "$(_v2ran od1)" = "-" ] || _v2b="$_v2b [a model that is not a name was written: $(_v2ran od1)]"
|
|
8526
|
-
[ "$(_v2ran fg1)/$(awk 'END { print NR }' "$_v2p/.claude/state/model-outcomes.tsv")/$(awk -F'\t' 'NF !=
|
|
8885
|
+
[ "$(_v2ran fg1)/$(awk 'END { print NR }' "$_v2p/.claude/state/model-outcomes.tsv")/$(awk -F'\t' 'NF != 13' "$_v2p/.claude/state/model-outcomes.tsv" | wc -l | tr -d ' ')" = "claude-sonnet-5-5/5/0" ] || _v2b="$_v2b [the other lines changed: $(_v2ran fg1), $(awk 'END { print NR }' "$_v2p/.claude/state/model-outcomes.tsv") lines]"
|
|
8527
8886
|
[ -d "$_v2p/.claude/state/.outcomes.lock" ] && _v2b="$_v2b the-lock-was-left-behind"
|
|
8528
8887
|
[ -z "$_v2b" ] && pass "ran_on is the model the answer to the Agent call names: written into the agent's line when the answer follows the stop (a foreground call), there already when it came at the launch (a background call), '-' when the answer names none or names something that is not a model; a decoy of the field inside the task's text is not read, the other lines and the twelve columns stay as they were, and no lock is left behind" \
|
|
8529
8888
|
|| fail "ran_on:$_v2b"
|
|
@@ -8719,6 +9078,61 @@ rm -rf "$_v2"
|
|
|
8719
9078
|
fi
|
|
8720
9079
|
# Wired where it has to be, in both editions: the four events of agent-outcome.sh, and the gate on Agent.
|
|
8721
9080
|
_pawf2="$ROOT/settings.json"; [ "$IS_KIT" = 1 ] && _pawf2="$_pawf2 $(cd "$ROOT/.." && pwd)/plugin/hooks/hooks.json"
|
|
9081
|
+
# ---- the refuter ledger: what crew-refuter concluded, written by the hook from the agent's own task and answer ----
|
|
9082
|
+
# findings.mjs --verified trusts this file, so what writes it is the gate: a line only when the TASK (the first record
|
|
9083
|
+
# of the agent's transcript) and the ANSWER (last_assistant_message) name the same finding and lens, only for a
|
|
9084
|
+
# crew-refuter (plugin-namespaced too), once per agent, whatever CREW_MODEL_ROUTING says, and never a refusal.
|
|
9085
|
+
_RL="$HOOKS/refuter-ledger.sh"
|
|
9086
|
+
if [ ! -f "$_RL" ]; then fail "hooks/refuter-ledger.sh is missing — no verdict of crew-refuter is ever recorded, so --verified can back nothing"
|
|
9087
|
+
else
|
|
9088
|
+
_rlt="$(mktemp -d)"; mkdir -p "$_rlt/p"
|
|
9089
|
+
printf '%s\n' '{"type":"user","content":"files: src/a.js\nchange: audit\nverify: none\nfinding: sec-idor-1\nlens: protection\ncite: src/a.js:3\nclaim: anyone reads any order"}' > "$_rlt/t.jsonl"
|
|
9090
|
+
_rl(){ # $1 agent id, $2 agent type, $3 answer (JSON-escaped), [$4 extra env] -> rows in the ledger after
|
|
9091
|
+
printf '{"hook_event_name":"SubagentStop","session_id":"s1","agent_id":"%s","agent_type":"%s","agent_transcript_path":"%s","last_assistant_message":"%s"}' \
|
|
9092
|
+
"$1" "$2" "$_rlt/t.jsonl" "$3" | env CLAUDE_PROJECT_DIR="$_rlt/p" ${4:-} bash "$_RL" >/dev/null 2>&1
|
|
9093
|
+
_rlrc=$?
|
|
9094
|
+
_rln=0; [ -f "$_rlt/p/.claude/state/crew-refuter.tsv" ] && _rln=$(($(wc -l < "$_rlt/p/.claude/state/crew-refuter.tsv") - 1))
|
|
9095
|
+
}
|
|
9096
|
+
_rlb=""
|
|
9097
|
+
_rl a1 crew-refuter 'checked\nFINDING: sec-idor-1\nLENS: protection\nVERDICT: FALSE_POSITIVE\nREASON: guard at src/a.js:2\nconfidence: high'
|
|
9098
|
+
[ "$_rlrc" = 0 ] && [ "$_rln" = 1 ] || _rlb="$_rlb match(rc=$_rlrc,rows=$_rln)"
|
|
9099
|
+
_rl a1 crew-refuter 'FINDING: sec-idor-1\nLENS: protection\nVERDICT: TRUE_POSITIVE'
|
|
9100
|
+
[ "$_rln" = 1 ] || _rlb="$_rlb resumed-agent-counted-twice"
|
|
9101
|
+
_rl a2 crew-refuter 'FINDING: sec-idor-1\nLENS: reachability\nVERDICT: TRUE_POSITIVE'
|
|
9102
|
+
[ "$_rln" = 1 ] || _rlb="$_rlb answer-on-another-lens-recorded"
|
|
9103
|
+
_rl a3 crew-refuter 'FINDING: sec-other\nLENS: protection\nVERDICT: TRUE_POSITIVE'
|
|
9104
|
+
[ "$_rln" = 1 ] || _rlb="$_rlb answer-on-another-finding-recorded"
|
|
9105
|
+
_rl a4 crew-refuter 'FINDING: sec-idor-1\nLENS: protection\nVERDICT: PROBABLY'
|
|
9106
|
+
[ "$_rln" = 1 ] || _rlb="$_rlb verdict-outside-the-three-recorded"
|
|
9107
|
+
_rl a5 crew-security-expert 'FINDING: sec-idor-1\nLENS: protection\nVERDICT: TRUE_POSITIVE'
|
|
9108
|
+
[ "$_rln" = 1 ] || _rlb="$_rlb another-agent-type-recorded"
|
|
9109
|
+
_rl a6 crewforth:crew-refuter 'FINDING: sec-idor-1\nLENS: protection\nVERDICT: CANNOT_VERIFY'
|
|
9110
|
+
[ "$_rln" = 2 ] || _rlb="$_rlb plugin-namespaced-type-not-recorded"
|
|
9111
|
+
_rl a7 crew-refuter 'FINDING: sec-idor-1\nLENS: protection\nVERDICT: TRUE_POSITIVE' CREW_MODEL_ROUTING=off
|
|
9112
|
+
[ "$_rln" = 3 ] || _rlb="$_rlb not-recorded-with-routing-off"
|
|
9113
|
+
_o="$(printf 'not json' | env CLAUDE_PROJECT_DIR="$_rlt/p" bash "$_RL" 2>/dev/null; echo "rc=$?")"
|
|
9114
|
+
[ "$_o" = rc=0 ] || _rlb="$_rlb unreadable-payload:$_o"
|
|
9115
|
+
_rlrow="$(sed -n 2p "$_rlt/p/.claude/state/crew-refuter.tsv" 2>/dev/null | cut -f3-6 | tr '\t' ' ')"
|
|
9116
|
+
[ "$_rlrow" = "a1 sec-idor-1 protection FALSE_POSITIVE" ] || _rlb="$_rlb row:[$_rlrow]"
|
|
9117
|
+
[ -z "$_rlb" ] && pass "refuter-ledger.sh: one line when task and answer agree (9 cases: resumed, other lens, other finding, bad verdict, other agent, plugin name, routing off, bad payload)" \
|
|
9118
|
+
|| fail "refuter-ledger.sh:$_rlb"
|
|
9119
|
+
# Its guards: the ledger's file name is refused to the shell and to the file tools; the agent's name is not.
|
|
9120
|
+
_rlg(){ printf '{"hook_event_name":"PreToolUse","tool_name":"%s","permission_mode":"default","tool_input":%s}' "$1" "$2" \
|
|
9121
|
+
| env CLAUDE_PROJECT_DIR="$_rlt/p" bash "$HOOKS/$3" >/dev/null 2>&1; echo $?; }
|
|
9122
|
+
_g1="$(_rlg Bash '{"command":"echo x >> .claude/state/crew-refuter.tsv"}' guard-bash.sh)"
|
|
9123
|
+
_g2="$(_rlg Bash '{"command":"grep -rn crew-refuter agents"}' guard-bash.sh)"
|
|
9124
|
+
_g3="$(_rlg Write "{\"file_path\":\"$_rlt/p/.claude/state/crew-refuter.tsv\",\"content\":\"x\"}" guard-write.sh)"
|
|
9125
|
+
_g4="$(_rlg Write "{\"file_path\":\"$_rlt/p/notes.md\",\"content\":\"x\"}" guard-write.sh)"
|
|
9126
|
+
[ "$_g1" = 2 ] && [ "$_g2" = 0 ] && [ "$_g3" = 2 ] && [ "$_g4" = 0 ] \
|
|
9127
|
+
&& pass "the refuter ledger is refused by name to the shell (rc 2) and the file tools (rc 2); a grep for the agent and a plain file are not (rc 0)" \
|
|
9128
|
+
|| fail "refuter ledger guards: shell write $_g1 (want 2), grep $_g2 (want 0), Write $_g3 (want 2), plain Write $_g4 (want 0)"
|
|
9129
|
+
rm -rf "$_rlt"
|
|
9130
|
+
fi
|
|
9131
|
+
# Wired in both editions: SubagentStop, matched on the agent type.
|
|
9132
|
+
_rlw=""; for _pf in $_pawf2; do
|
|
9133
|
+
json_hooks "$_pf" | LC_ALL=C awk -F'\t' '$1 == "SubagentStop" && $2 ~ /crew-refuter/ && $3 ~ /refuter-ledger\.sh/ { f = 1 } END { exit !f }' || _rlw="$_rlw ${_pf##*/}"
|
|
9134
|
+
done
|
|
9135
|
+
[ -z "$_rlw" ] && pass "refuter-ledger.sh is wired to SubagentStop, matched on crew-refuter, in every edition read" || fail "refuter-ledger.sh is not wired to SubagentStop on crew-refuter in:$_rlw"
|
|
8722
9136
|
for _pf in $_pawf2; do
|
|
8723
9137
|
_wo=""; for _ev in SubagentStart SubagentStop PostToolUse SessionStart; do
|
|
8724
9138
|
json_hooks "$_pf" | LC_ALL=C awk -F'\t' -v e="$_ev" '$1 == e && $3 ~ /agent-outcome\.sh/ { f = 1 } END { exit !f }' || _wo="$_wo $_ev"; done
|
|
@@ -8938,21 +9352,24 @@ CPT='0/ask 0/allow @@ git commit -m x
|
|
|
8938
9352
|
0 0 @@ bash -c \x27git log --grep=commit\x27
|
|
8939
9353
|
0 0 @@ grep -rn \"git commit -n\" docs/
|
|
8940
9354
|
0/ask 0/allow @@ bash build.sh && git commit -m x
|
|
8941
|
-
0/ask 0/allow @@ cat > notes.md <<\x27EOF\x27\nrun: bash -c \x27git commit -am x\x27 and it\x27s gone\nEOF\ngit commit -m x
|
|
9355
|
+
0/ask 0/allow @@ cat > notes.md <<\x27EOF\x27\nrun: bash -c \x27git commit -am x\x27 and it\x27s gone\nEOF\ngit commit -m x
|
|
9356
|
+
0/ask 0/allow @@ git commit -m x # trailing words -a'
|
|
8942
9357
|
_cf_table "$CPT"
|
|
8943
|
-
if [ "$_cfn" !=
|
|
8944
|
-
elif [ -z "$_cfbad" ]; then pass "
|
|
9358
|
+
if [ "$_cfn" != 48 ]; then fail "FIXTURE: the everyday-commit table has $_cfn rows, not 48"
|
|
9359
|
+
elif [ -z "$_cfbad" ]; then pass "48 everyday calls are judged as before (asked in default, allowed when pre-authorised): chained after git add, a message that holds flags and separators, a here-document message, -F, -S, -uno, redirections, a pipe, a comment, a cd into this same repository, GIT_AUTHOR_*, a script beside the commit"
|
|
8945
9360
|
else fail "everyday commit calls that changed verdict:$_cfbad"; fi
|
|
8946
9361
|
|
|
8947
|
-
#
|
|
8948
|
-
# (`-uno`, a key id after -S)
|
|
8949
|
-
#
|
|
9362
|
+
# Two everyday calls the OLDER scan refuses, and still does: it reads any a/o/i/p in a short token as a flag
|
|
9363
|
+
# (`-uno`, a key id after -S). The newer scan parses both correctly; they are kept refused because letting them
|
|
9364
|
+
# through is a loosening of that scan's option table, to be decided on its own. A third row stood here,
|
|
9365
|
+
# `git commit -m x # trailing words -a`: the older scan paired quotes and read on past the comment sign by itself.
|
|
9366
|
+
# It walks the one reader's text now, where a comment is left out, so that call is asked about like any commit
|
|
9367
|
+
# from the index (the row is in the table above).
|
|
8950
9368
|
CKT='2 0/allow @@ git commit -Skeyid -qm x
|
|
8951
|
-
2 0/allow @@ git commit -uno -m x
|
|
8952
|
-
2 0/allow @@ git commit -m x # trailing words -a'
|
|
9369
|
+
2 0/allow @@ git commit -uno -m x'
|
|
8953
9370
|
_cf_table "$CKT"
|
|
8954
|
-
if [ "$_cfn" !=
|
|
8955
|
-
elif [ -z "$_cfbad" ]; then pass "
|
|
9371
|
+
if [ "$_cfn" != 2 ]; then fail "FIXTURE: the kept-over-blocks table has $_cfn rows, not 2"
|
|
9372
|
+
elif [ -z "$_cfbad" ]; then pass "2 known over-blocks of the older scan are unchanged (-Skeyid, -uno)"
|
|
8956
9373
|
else fail "the older scan's verdict changed on:$_cfbad"; fi
|
|
8957
9374
|
|
|
8958
9375
|
# ---- the same commit, written for PowerShell --------------------------------------------------------------------
|
|
@@ -10312,7 +10729,9 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
10312
10729
|
# An empty answer must mean "nothing unpinned", never "git listed nothing": two files every checkout has must
|
|
10313
10730
|
# be in the listing first. Symlinks and submodules are not regular files; git leaves their i/ field empty.
|
|
10314
10731
|
SLIST="$(git -C "$SGR" ls-files -- start.sh adopt.sh VERSION LICENSE README.md bin kit plugin 2>/dev/null)"
|
|
10315
|
-
|
|
10732
|
+
# Here-strings, not a pipe: `grep -q` exits on its first match, a printf still writing gets SIGPIPE, and under
|
|
10733
|
+
# pipefail that read as "not listed" once the list grew long enough to outlast the match (Linux runner).
|
|
10734
|
+
if ! grep -qx 'start.sh' <<< "$SLIST" || ! grep -qx 'kit/CLAUDE.md' <<< "$SLIST"; then
|
|
10316
10735
|
fail "git did not list Crewforth's shipped files (start.sh and kit/CLAUDE.md are missing), so the eol pin check measured nothing"
|
|
10317
10736
|
else
|
|
10318
10737
|
UNPIN="$(git -C "$SGR" ls-files --eol -- start.sh adopt.sh VERSION LICENSE README.md bin kit plugin 2>/dev/null \
|
|
@@ -10368,6 +10787,15 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
10368
10787
|
# gate that only runs when someone remembers to run it, which is how the catalogue check went unnoticed.
|
|
10369
10788
|
[ -z "$UNRUN" ] && pass "every gate verify.sh defines is wired into ci.yml" \
|
|
10370
10789
|
|| fail "verify.sh defines gates ci.yml never runs: $UNRUN — they hold only when run by hand"
|
|
10790
|
+
# The Windows e2e runs as shards of packaging/e2e.sh's map. A shard the matrix does not list is a third of the
|
|
10791
|
+
# rehearsals that runs nowhere while every listed job is green; e2e.sh checks its own map, not the matrix. So
|
|
10792
|
+
# the matrix lists exactly 1/N … N/N, N read from e2e.sh.
|
|
10793
|
+
_e2n="$(sed -n 's/^E2E_SHARDS=\([0-9][0-9]*\)$/\1/p' "$SGR/packaging/e2e.sh" 2>/dev/null)"
|
|
10794
|
+
_e2m="$(grep -oE "shard: [0-9]+/[0-9]+" "$SGR/.github/workflows/ci.yml" | awk '{print $2}' | LC_ALL=C sort -u | tr '\n' ' ')"
|
|
10795
|
+
_e2w=""; if [ -n "$_e2n" ]; then _i=1; while [ "$_i" -le "$_e2n" ]; do _e2w="$_e2w$_i/$_e2n "; _i=$((_i+1)); done; fi
|
|
10796
|
+
if [ -z "$_e2n" ]; then fail "FIXTURE: E2E_SHARDS was not read from packaging/e2e.sh — the shard pin cannot compare"
|
|
10797
|
+
elif [ "$_e2m" = "$_e2w" ]; then pass "ci.yml runs every e2e shard the map has ($_e2w)"
|
|
10798
|
+
else fail "ci.yml's e2e matrix lists shards [$_e2m], packaging/e2e.sh has [$_e2w] — a shard it does not list runs nowhere"; fi
|
|
10371
10799
|
fi
|
|
10372
10800
|
|
|
10373
10801
|
# EVERY JOB OF EVERY WORKFLOW HAS A TIME LIMIT. A job without one runs for GitHub's default of 360 minutes: a
|
|
@@ -10437,7 +10865,7 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
10437
10865
|
# starts with a wildcard (which reaches every directory, kit/ included).
|
|
10438
10866
|
cdo_unsafe(){ while IFS= read -r _p; do case "$_p" in *.sh*|kit*|\**) printf '%s\n' "$_p" ;; esac; done; }
|
|
10439
10867
|
_cp="$(cdo_pats "$CDO" | tr '\n' ' ' | sed 's/ $//')"
|
|
10440
|
-
_want='README*.md site
|
|
10868
|
+
_want='README*.md site/* evals/README.md evals/results/* CHANGELOG.md'
|
|
10441
10869
|
[ "$_cp" = "$_want" ] && pass "the docs-only list is exactly: $_want" \
|
|
10442
10870
|
|| fail "the docs-only list changed — it reads '$_cp', pinned '$_want'. Widening it lets a change skip macOS and Windows; update this pin deliberately"
|
|
10443
10871
|
_cu="$(cdo_pats "$CDO" | cdo_unsafe)"
|
|
@@ -10454,9 +10882,11 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
10454
10882
|
[ "$(cdo evals/run.sh)" = code ] || _cv="$_cv evals/run.sh"
|
|
10455
10883
|
[ "$(cdo kit/README.md)" = code ] || _cv="$_cv kit/README.md"
|
|
10456
10884
|
[ "$(cdo .github/workflows/ci.yml)" = code ] || _cv="$_cv ci.yml"
|
|
10885
|
+
[ "$(cdo site/scripts/check.mjs site/package-lock.json)" = docs ] || _cv="$_cv site-source"
|
|
10886
|
+
[ "$(cdo site/scripts/build.sh)" = code ] || _cv="$_cv site-script"
|
|
10457
10887
|
[ "$(printf '' | bash "$CDO")" = code ] || _cv="$_cv empty"
|
|
10458
10888
|
[ "$(printf 'README.md\r\n' | bash "$CDO")" = docs ] || _cv="$_cv crlf"
|
|
10459
|
-
[ -z "$_cv" ] && pass "ci-docs-only.sh: documentation
|
|
10889
|
+
[ -z "$_cv" ] && pass "ci-docs-only.sh: documentation and the site's sources read docs; a hook, a .sh under site/, evals/run.sh, kit/README.md, ci.yml or an empty diff reads code" \
|
|
10460
10890
|
|| fail "ci-docs-only.sh gave the wrong verdict for:$_cv"
|
|
10461
10891
|
# The third answer, `text`: a pull request that changes only the text of skills, agents or commands skips the
|
|
10462
10892
|
# macOS and Windows jobs as a documentation one does, and still runs the Linux verify job. That list is pinned
|
|
@@ -10482,10 +10912,13 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
10482
10912
|
[ -z "$_cv" ] && pass "ci-docs-only.sh: skill, agent and command text reads text, with documentation too; with a hook, a skill's script, kit/CLAUDE.md or a blocklist it reads code" \
|
|
10483
10913
|
|| fail "ci-docs-only.sh gave the wrong text verdict for:$_cv"
|
|
10484
10914
|
# The Linux verify job is gated on nothing: it holds the checks a text change can break.
|
|
10485
|
-
|
|
10915
|
+
# One condition is allowed, and only this one: it skips the run a label other than full-ci starts, and says
|
|
10916
|
+
# nothing about what the pull request changed.
|
|
10917
|
+
_cyv="$(awk '/^ verify:$/{f=1;next} f && /^ [a-z-]+:$/{f=0} f' "$SGR/.github/workflows/ci.yml" | grep -E '^ (if|needs):' \
|
|
10918
|
+
| grep -vxF " if: github.event.action != 'labeled' || github.event.label.name == 'full-ci'" | grep -c .)"
|
|
10486
10919
|
_cyn="$(awk '/^ verify:$/{f=1;next} f && /^ [a-z-]+:$/{f=0} f' "$SGR/.github/workflows/ci.yml" | grep -c 'bash packaging/verify.sh')"
|
|
10487
10920
|
[ "$_cyv" = 0 ] && [ "$_cyn" -ge 1 ] \
|
|
10488
|
-
&& pass "ci.yml runs the Linux verify job on every pull request, a text-only one included (no
|
|
10921
|
+
&& pass "ci.yml runs the Linux verify job on every pull request, a text-only one included (no needs:, and no if: but the one that skips an unrelated label)" \
|
|
10489
10922
|
|| fail "ci.yml's verify job is conditional ($_cyv if:/needs: line(s)) or was not found ($_cyn verify.sh call(s)): a text-only pull request would skip the budget, pointer, routing and plugin checks"
|
|
10490
10923
|
# ...and ci.yml really uses it: both cross-platform jobs are gated on its answer, nothing else is.
|
|
10491
10924
|
_cy="$SGR/.github/workflows/ci.yml"
|
|
@@ -10604,6 +11037,63 @@ sec "== 14b) the star line (once, on a first install) and the front page it poin
|
|
|
10604
11037
|
# One file owns the line — its URL, both languages, and when it stays quiet — so these checks drive THAT file,
|
|
10605
11038
|
# with CI and CREW_NO_STAR set or cleared by each case itself: a runner exports CI=true, and a check that read it
|
|
10606
11039
|
# from the environment would pass here and assert the opposite there.
|
|
11040
|
+
# ---- the status line: what it says, and where it is set ----------------------------------------------------------
|
|
11041
|
+
# eval/lib/statusline.sh reads Claude Code's stdin and the hooks' own records. What it must never do: show a gate
|
|
11042
|
+
# line or an agent from before the session (the gate log carries no time), or replace a status line of the user's.
|
|
11043
|
+
SLS="$ROOT/eval/lib/statusline.sh"
|
|
11044
|
+
if [ -f "$SLS" ]; then
|
|
11045
|
+
_sl="$(mktemp -d)"; mkdir -p "$_sl/p/.claude/state/crew-model/agents" "$_sl/p/.claude/state/crew-model/results" "$_sl/home"
|
|
11046
|
+
( cd "$_sl/p" && git init -q . ) >/dev/null 2>&1
|
|
11047
|
+
printf 'ASK\t§4.4\tcommit\t\n' > "$_sl/p/.claude/gate-log.tsv"; : > "$_sl/p/.claude/state/crew-model/agents/before"
|
|
11048
|
+
_slj='{"session_id":"sl1","context_window":{"used_percentage":34.6},"cost":{"total_cost_usd":1.2034},"rate_limits":{"five_hour":{"used_percentage":90}},"workspace":{"project_dir":"'"$_sl/p"'"}}'
|
|
11049
|
+
sleep 1
|
|
11050
|
+
_o1="$(printf '%s' "$_slj" | (cd "$_sl" && bash "$SLS") 2>&1)"
|
|
11051
|
+
sleep 1
|
|
11052
|
+
printf 'BLOCK\t§4.2\tforce push\t\n' >> "$_sl/p/.claude/gate-log.tsv"
|
|
11053
|
+
: > "$_sl/p/.claude/state/crew-model/agents/run1"; : > "$_sl/p/.claude/state/crew-model/agents/done1"; : > "$_sl/p/.claude/state/crew-model/results/done1"
|
|
11054
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ]; then printf -v _now '%(%s)T' -1; else _now="$(date +%s)"; fi
|
|
11055
|
+
# 330 s old: 24 minutes left for the next 30 s, so a slow machine does not read 23 (measured in an install: 23).
|
|
11056
|
+
printf 'v=1\nop=commit\nts=%s\n' "$((_now - 330))" > "$_sl/p/.git/crewforth-approval"
|
|
11057
|
+
_o2="$(printf '%s' "$_slj" | (cd "$_sl" && bash "$SLS") 2>&1)"
|
|
11058
|
+
_o3="$(printf '' | bash "$SLS" 2>&1)"
|
|
11059
|
+
[ "$_o1" = "🔋 34% · ~\$1.20" ] && [ "$_o2" = "🛡 BLOCK §4.2 · ✓ commit approved 24m · ⚙ 1 crew agent · 🔋 34% · ~\$1.20" ] && [ -z "$_o3" ] \
|
|
11060
|
+
&& pass "status line: the session's own gate line, a standing approval and its running crew agents; nothing from before the session; the context fill, not a rate limit's; silent on no input" \
|
|
11061
|
+
|| fail "status line said: first='$_o1' · later='$_o2' · no input='$_o3'"
|
|
11062
|
+
# --install: only where no status line is set, and never through a file it cannot read.
|
|
11063
|
+
# The project's settings carry a hook of the user's own beside Crewforth's: both must come through whole.
|
|
11064
|
+
# Built from Crewforth's hooks alone: in an installed project settings.json already carries the status line.
|
|
11065
|
+
if [ -f "$ROOT/settings.json" ]; then
|
|
11066
|
+
printf '{"hooks":%s}\n' "$(awk -v op=get -v path=hooks -f "$ROOT/eval/lib/settings-json.awk" "$ROOT/settings.json" 2>/dev/null)" > "$_sl/base.json"
|
|
11067
|
+
awk -v op=merge -v retired= -f "$ROOT/eval/lib/settings-json.awk" "$_sl/base.json" /dev/stdin > "$_sl/p/.claude/settings.json" 2>/dev/null <<'JSON'
|
|
11068
|
+
{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"my-own-stop-hook"}]}]}}
|
|
11069
|
+
JSON
|
|
11070
|
+
fi
|
|
11071
|
+
[ -s "$_sl/p/.claude/settings.json" ] || printf '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"my-own-stop-hook"}]}]}}\n' > "$_sl/p/.claude/settings.json"
|
|
11072
|
+
_hb="$(awk -v op=get -v path=hooks -f "$ROOT/eval/lib/settings-json.awk" "$_sl/p/.claude/settings.json" 2>/dev/null | cksum)"
|
|
11073
|
+
_i(){ (cd "$_sl/p" && env CLAUDE_CONFIG_DIR="$_sl/home" bash "$SLS" --install 2>&1); }
|
|
11074
|
+
_hk0="$(awk -v op=len -v path=hooks -f "$ROOT/eval/lib/settings-json.awk" "$_sl/p/.claude/settings.json" 2>/dev/null)"
|
|
11075
|
+
_r1="$(_i)"; _c1="$(awk -v op=get -v path=statusLine.command -f "$ROOT/eval/lib/settings-json.awk" "$_sl/p/.claude/settings.json" 2>/dev/null)"
|
|
11076
|
+
_hk1="$(awk -v op=len -v path=hooks -f "$ROOT/eval/lib/settings-json.awk" "$_sl/p/.claude/settings.json" 2>/dev/null)"
|
|
11077
|
+
_ha="$(awk -v op=get -v path=hooks -f "$ROOT/eval/lib/settings-json.awk" "$_sl/p/.claude/settings.json" 2>/dev/null | cksum)"
|
|
11078
|
+
grep -q 'my-own-stop-hook' "$_sl/p/.claude/settings.json" || _ha="lost the user's own hook"
|
|
11079
|
+
_r2="$(_i)"
|
|
11080
|
+
cp "$_sl/base.json" "$_sl/p/.claude/settings.json" 2>/dev/null || printf '{"hooks":{}}\n' > "$_sl/p/.claude/settings.json"
|
|
11081
|
+
printf '{"statusLine":{"type":"command","command":"mine"}}\n' > "$_sl/home/settings.json"
|
|
11082
|
+
_sum0="$(cksum < "$_sl/p/.claude/settings.json")"; _r3="$(_i)"; _sum1="$(cksum < "$_sl/p/.claude/settings.json")"
|
|
11083
|
+
rm -f "$_sl/home/settings.json"; printf '{bad' > "$_sl/p/.claude/settings.json"; _r4="$(_i)"; _b4="$(cat "$_sl/p/.claude/settings.json")"
|
|
11084
|
+
case "$_c1" in *eval/lib/statusline.sh*) _c1=ok ;; esac
|
|
11085
|
+
[ "$_r1/$_c1/$_r2/$_r3/$_r4" = "installed/ok/kept/kept/failed" ] && [ "$_hk0" = "$_hk1" ] && [ -n "$_hk0" ] && [ "$_hb" = "$_ha" ] && [ "$_sum0" = "$_sum1" ] && [ "$_b4" = "{bad" ] \
|
|
11086
|
+
&& pass "status line --install: set where none is, every hook kept byte for byte, Crewforth's and the user's own ($_hk1 events); kept when set; a user's own left untouched; a file it cannot read left as it was" \
|
|
11087
|
+
|| fail "status line --install: $_r1/$_c1/$_r2/$_r3/$_r4 (want installed/ok/kept/kept/failed), hooks $_hk0 -> $_hk1 (content $([ "$_hb" = "$_ha" ] && echo same || echo "CHANGED: $_ha")), user's case changed the file: $([ "$_sum0" = "$_sum1" ] || echo yes), invalid left as '$_b4'"
|
|
11088
|
+
# The reader's gets: several values in one read, an absent one empty, a string with its quotes.
|
|
11089
|
+
_g="$(printf '{"a":{"b":1},"c":"x y"}' | awk -v op=gets -v 'path=a.b|nope|c' -f "$ROOT/eval/lib/settings-json.awk" - 2>&1)"
|
|
11090
|
+
[ "$_g" = "1"$'\t\t'"\"x y\"" ] && pass "settings-json.awk gets: several paths in one read, tab-separated, an absent one empty" \
|
|
11091
|
+
|| fail "settings-json.awk gets printed '$_g'"
|
|
11092
|
+
rm -rf "$_sl"
|
|
11093
|
+
else
|
|
11094
|
+
skip scope "status line checks skipped (this project has no eval/lib/statusline.sh)" 3
|
|
11095
|
+
fi
|
|
11096
|
+
|
|
10607
11097
|
STAR="$ROOT/eval/lib/star.sh"
|
|
10608
11098
|
if [ -f "$STAR" ]; then
|
|
10609
11099
|
_SURL="$(sed -n 's/^CREW_REPO_URL="\(.*\)"$/\1/p' "$STAR" | head -1)"
|
|
@@ -10643,6 +11133,26 @@ if [ -f "$STAR" ]; then
|
|
|
10643
11133
|
&& pass "--once: once per version (shown · same version silent · new version shown), marker holds the version; a silenced run writes nothing" \
|
|
10644
11134
|
|| fail "--once broken: v1='${_o1:+shown}' v1-again='${_o2:+shown}' v2='${_o3:+shown}' marker='$_mv' silenced='${_o4:+shown}' marker-after='$_mv4'"
|
|
10645
11135
|
rm -rf "$_SP"
|
|
11136
|
+
# doctor keeps its own marker: the line an installer showed for a version does not silence doctor's, and doctor
|
|
11137
|
+
# shows it once per version too. Before 3.2.0 they shared one, so doctor almost never showed it.
|
|
11138
|
+
_SD="$(mktemp -d)"; mkdir -p "$_SD/.claude"; printf '9.9.0\n' > "$_SD/.claude/VERSION"
|
|
11139
|
+
_r(){ env -u CI -u CREW_NO_STAR -u CSK_NO_STAR bash "$STAR" --once "$_SD" "$@" 2>&1 | grep -c '⭐'; }
|
|
11140
|
+
_d="$(_r)$(_r)$(_r doctor)$(_r doctor)"
|
|
11141
|
+
_dn="$(env -u CI -u CSK_NO_STAR CREW_NO_STAR=1 bash "$STAR" --once "$_SD" doctor 2>&1)"
|
|
11142
|
+
[ "$_d" = 1010 ] && [ -z "$_dn" ] && [ -f "$_SD/.claude/star-shown-doctor" ] \
|
|
11143
|
+
&& pass "--once doctor: its own marker (install shown, again silent, doctor shown, again silent); CREW_NO_STAR silences it" \
|
|
11144
|
+
|| fail "--once doctor: install/install/doctor/doctor printed $_d (want 1010), silenced='${_dn:+shown}'"
|
|
11145
|
+
rm -rf "$_SD"
|
|
11146
|
+
# The plugin edition has no .claude/VERSION: the version is read from the manifest beside star.sh (../../).
|
|
11147
|
+
_SPL="$(mktemp -d)"; mkdir -p "$_SPL/plug/eval/lib" "$_SPL/plug/.claude-plugin" "$_SPL/proj"
|
|
11148
|
+
cp "$STAR" "$ROOT/eval/lib/settings-json.awk" "$_SPL/plug/eval/lib/" 2>/dev/null
|
|
11149
|
+
[ -f "$ROOT/eval/lib/crew-env.sh" ] && cp "$ROOT/eval/lib/crew-env.sh" "$_SPL/plug/eval/lib/"
|
|
11150
|
+
printf '{\n "name": "crewforth",\n "version": "9.8.7"\n}\n' > "$_SPL/plug/.claude-plugin/plugin.json"
|
|
11151
|
+
env -u CI -u CREW_NO_STAR -u CSK_NO_STAR bash "$_SPL/plug/eval/lib/star.sh" --once "$_SPL/proj" doctor >/dev/null 2>&1
|
|
11152
|
+
_pv="$(head -1 "$_SPL/proj/.claude/star-shown-doctor" 2>/dev/null)"
|
|
11153
|
+
[ "$_pv" = 9.8.7 ] && pass "a plugin install's star marker holds the plugin manifest's version (9.8.7)" \
|
|
11154
|
+
|| fail "a plugin install's star marker holds '${_pv:-nothing}', not the manifest's 9.8.7"
|
|
11155
|
+
rm -rf "$_SPL"
|
|
10646
11156
|
if command -v git >/dev/null 2>&1; then
|
|
10647
11157
|
_SG="$(mktemp -d)"; ( cd "$_SG" && git init -q . ) >/dev/null 2>&1; mkdir -p "$_SG/.claude"; printf '9.9.0\n' > "$_SG/.claude/VERSION"
|
|
10648
11158
|
env -u CI -u CREW_NO_STAR -u CSK_NO_STAR bash "$STAR" --once "$_SG" >/dev/null 2>&1
|
|
@@ -10666,7 +11176,7 @@ if [ -f "$STAR" ]; then
|
|
|
10666
11176
|
[ -z "$_dup$_nocall" ] && pass "the repo URL lives only in lib/star.sh; $(printf '%s\n' $_callers | grep -c .) caller(s) call it" \
|
|
10667
11177
|
|| fail "star single source broken — URL copied into:${_dup:- none} · not calling star.sh:${_nocall:- none}"
|
|
10668
11178
|
else
|
|
10669
|
-
skip scope "star line checks skipped (this project has no eval/lib/star.sh)"
|
|
11179
|
+
skip scope "star line checks skipped (this project has no eval/lib/star.sh)" 9
|
|
10670
11180
|
fi
|
|
10671
11181
|
# The front page. Kit repo only: an installed project has no README of ours.
|
|
10672
11182
|
if [ "$IS_KIT" = 1 ]; then
|
|
@@ -10742,12 +11252,12 @@ adopt.sh kit/legacy-blobs.tsv 347 migration: finds and moves 2.x names (componen
|
|
|
10742
11252
|
bin/cli.js 4 migration: add accepts a typed <x>-csk and moves an add record written under the old names
|
|
10743
11253
|
evals/run.sh 4 compat: reads the 2.x trusted eval parent when the 3.0 one is absent — removed in 4.0
|
|
10744
11254
|
site/scripts/check.mjs 7 tests: the old-name pattern of the built-site gate, and its twins
|
|
10745
|
-
*/eval/doctor.sh
|
|
11255
|
+
*/eval/doctor.sh 8 migration: PROOF-5 and the variable notice name what is still on the 2.x spelling; 9c names the 2.x plugin id
|
|
10746
11256
|
*/crew-env.* 18 compat layer (bash + Node): reads CSK_* when CREW_* is unset — removed in 4.0
|
|
10747
11257
|
*/hooks/board.sh 42 compat layer: reads the 2.x board ref and settings and folds them in — removed in 4.0
|
|
10748
11258
|
*/skills/automode-policy/scripts/check.sh 4 compat layer: counts auto-mode rules still named by 2.x — removed in 4.0
|
|
10749
11259
|
*/studio/web/storage-migrate.js 4 migration: moves the panel'"'"'s saved layout to the new keys — removed in 4.0
|
|
10750
|
-
kit/eval/smoke-test.sh
|
|
11260
|
+
kit/eval/smoke-test.sh 42 tests: this gate'"'"'s own pattern, and that the 2.x names still work (doctor 9c: the 2.x plugin id; the star line'"'"'s doctor marker)
|
|
10751
11261
|
packaging/legacy-npm/* 10 the 2.x package name'"'"'s 3.0.0: a forwarder to crewforth, published once by hand
|
|
10752
11262
|
packaging/* 154 tests: the migration rehearsal on the real v2.13.0 tree, the legacy token and layout checks; the legacy blob list'"'"'s generator and its real-installer check'
|
|
10753
11263
|
# A line may name several globs, separated by spaces, when one reason covers them all; its pin is their sum. Split
|
|
@@ -10957,6 +11467,22 @@ $(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/|
|
|
|
10957
11467
|
[ -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" \
|
|
10958
11468
|
|| fail "twin: the closing-line check missed a planted 'kit/ deleted' — it reads nothing"
|
|
10959
11469
|
fi
|
|
11470
|
+
# adopt.sh --help answers before it reads anything. An unknown flag is ignored further down, so --help used to run
|
|
11471
|
+
# 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
|
|
11472
|
+
# languages, and nothing written. Twin: the same script without its --help branch must not print the usage.
|
|
11473
|
+
_ah_run(){ ( cd "$1" && CREW_LANG=en bash adopt.sh --help; echo "rc=$?"; bash adopt.sh -h --lang tr; echo "rc=$?" ) 2>&1; }
|
|
11474
|
+
mkdir -p "$_kd/ah" "$_kd/ah-twin"; git init -q "$_kd/ah" 2>/dev/null; git init -q "$_kd/ah-twin" 2>/dev/null
|
|
11475
|
+
cp "$KR/adopt.sh" "$_kd/ah/"; sed 's/-h|--help) _hh=1 ;;//' "$KR/adopt.sh" > "$_kd/ah-twin/adopt.sh"
|
|
11476
|
+
_ah="$(_ah_run "$_kd/ah")"; _aht="$(_ah_run "$_kd/ah-twin")"
|
|
11477
|
+
_ahw="$(git -C "$_kd/ah" status --porcelain 2>/dev/null | grep -vx '?? adopt.sh')"
|
|
11478
|
+
case "$_ah" in
|
|
11479
|
+
*'Usage: bash adopt.sh'*'rc=0'*'Kullanım: bash adopt.sh'*'rc=0'*)
|
|
11480
|
+
if [ -n "$_ahw" ]; then fail "adopt.sh --help wrote into the project: $(printf '%s' "$_ahw" | head -n 2 | tr '\n' ' ')"
|
|
11481
|
+
else case "$_aht" in *'Usage: bash adopt.sh'*) fail "twin: adopt.sh without its --help branch still printed the usage — the check reads nothing" ;;
|
|
11482
|
+
*) 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
|
|
11483
|
+
fi ;;
|
|
11484
|
+
*) fail "adopt.sh --help did not print its usage in both languages with rc 0: $(printf '%s' "$_ah" | head -n 3 | tr '\n' ' ')" ;;
|
|
11485
|
+
esac
|
|
10960
11486
|
rm -rf "$_kd"
|
|
10961
11487
|
else
|
|
10962
11488
|
skip scope "printed-text wording check skipped (installed project — the installers live in the source repository)"
|
|
@@ -11170,7 +11696,7 @@ _EVR="$(cd "$(dirname "$0")/../.." && pwd)/evals/run.sh"
|
|
|
11170
11696
|
_PY3OK=0
|
|
11171
11697
|
printf '' | python3 -c 'import sys,json' >/dev/null 2>&1 && _PY3OK=1
|
|
11172
11698
|
if [ -f "$_EVR" ] && [ "$_PY3OK" = 0 ]; then
|
|
11173
|
-
skip tool "evals metric not calibrated (no working python3 — eval_trace_metrics is a python heredoc)"
|
|
11699
|
+
skip tool "evals metric not calibrated (no working python3 — eval_trace_metrics is a python heredoc)" 7
|
|
11174
11700
|
elif [ -f "$_EVR" ]; then
|
|
11175
11701
|
_EVD="$(mktemp -d)"
|
|
11176
11702
|
# NOT IN A SUBSHELL, and that was a real defect in the first draft of this block: the five rows below ran
|
|
@@ -11219,6 +11745,15 @@ $_res")"
|
|
|
11219
11745
|
[ "$(_f "$_p" 19)" = 2 ] \
|
|
11220
11746
|
&& pass "evals metric: two id-less messages stay two, not one" \
|
|
11221
11747
|
|| fail "evals metric: id-less messages collapsed [$_p] — parallel_msgs would undercount"
|
|
11748
|
+
# agents_by_type (column 21): WHICH agent the main thread started. A count of calls looked healthy while two agents
|
|
11749
|
+
# were never started; this names them. A plugin's namespace is dropped, a call with no type is general-purpose, and
|
|
11750
|
+
# a subagent's own call is not the main thread's.
|
|
11751
|
+
_p="$(_mk typed "$(printf '%s' '{"type":"assistant","message":{"id":"m1","content":[{"type":"tool_use","id":"x1","name":"Agent","input":{"subagent_type":"crew-commit-agent"}},{"type":"tool_use","id":"x2","name":"Agent","input":{"subagent_type":"crewforth:crew-review-agent"}},{"type":"tool_use","id":"x3","name":"Agent","input":{}}]}}')
|
|
11752
|
+
$(_msg s1 1 n1)
|
|
11753
|
+
$_res")"
|
|
11754
|
+
{ [ "$(_f "$_p" 21)" = "crew-commit-agent:1,crew-review-agent:1,general-purpose:1" ] && [ "$(_f "$(_mk none_t "$_res")" 21)" = "-" ]; } \
|
|
11755
|
+
&& pass "evals metric: agents_by_type names the main thread's agents (namespace dropped, untyped is general-purpose, nested not counted, none is -)" \
|
|
11756
|
+
|| fail "evals metric: agents_by_type read [$(_f "$_p" 21)]"
|
|
11222
11757
|
_p="$(_mk none "$_res")"
|
|
11223
11758
|
{ [ "$(_f "$_p" 19)" = 0 ] && [ "$(_f "$_p" 20)" = 0 ]; } \
|
|
11224
11759
|
&& pass "evals metric: a stream with no Agent call reports 0, not empty" \
|
|
@@ -11387,7 +11922,7 @@ if [ -s "$SECLOG" ]; then
|
|
|
11387
11922
|
' "$SECLOG"
|
|
11388
11923
|
echo "---"
|
|
11389
11924
|
fi
|
|
11390
|
-
rm -f "$SECLOG"
|
|
11925
|
+
rm -f "$SECLOG"; rm -rf "$_SMOKE_CCD"
|
|
11391
11926
|
if [ "$SKIPN" -gt 0 ]; then
|
|
11392
11927
|
echo "SKIPPED (nothing was checked here):$SKIP_LIST"
|
|
11393
11928
|
echo "---"
|