crewforth 3.2.0-rc.9 → 3.2.1
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 +37 -1
- package/VERSION +1 -1
- package/adopt.sh +8 -4
- package/kit/eval/doctor.sh +27 -0
- package/kit/eval/lib/node-advisory.tsv +9 -0
- package/kit/eval/smoke-test.sh +55 -1
- package/kit/hooks/guard-agent-model.sh +8 -0
- package/kit/hooks/guard-commit-scan.sh +10 -2
- package/kit/owned-blobs.tsv +4 -2
- package/kit/studio/ensure-node.sh +1 -1
- package/kit/studio/server/index.js +3 -0
- package/kit/studio/server/lib/node-advisory.js +46 -0
- package/kit/studio/web/models-plan.js +3 -3
- package/package.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,33 @@ Crewforth was named Claude Starter Kit until 3.0.0.
|
|
|
5
5
|
Notable changes to this project are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/),
|
|
6
6
|
versioning follows [SemVer](https://semver.org/).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [3.2.1] — 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Doctor and Studio say when Node is missing a published security fix.** Node's October 2026 security releases fix,
|
|
13
|
+
among others, a Brotli decoder write that a crafted HTTP response can reach: Studio's `fetch` decodes a `br`
|
|
14
|
+
response even though it does not ask for one (measured), so a Studio with `--peer` over plain HTTP on a network
|
|
15
|
+
someone else can answer on was exposed on an unpatched Node. `/crew-doctor` and Studio's start now read one table
|
|
16
|
+
of fixed releases (22.23.4, 24.22.1, 26.12.1) and warn below them, or on a line that gets no fixes (20.x); neither
|
|
17
|
+
refuses anything. `ensure-node.sh`'s fallback, used only when nodejs.org cannot be asked for the current LTS, moves
|
|
18
|
+
from 24.21.0 to 24.22.1.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Studio says what the model gate does to an agent that is not crew-\*.** Its inspector said the gate chose nothing
|
|
23
|
+
for such an agent and that it ran on the model its call asked for, while the same panel showed it had asked for opus
|
|
24
|
+
and run on sonnet. The gate does hold the expensive end there: a call that asks for opus or fable, or names no model
|
|
25
|
+
and would take the session's, runs on sonnet unless its task carries an `above: <why>` line. The inspector and the
|
|
26
|
+
Studio page now say that.
|
|
27
|
+
- **Critical work sent to an agent that is not crew-\* is no longer moved to a cheaper model.** Outside the crew the
|
|
28
|
+
model gate holds the expensive end: opus or fable, or a session model the call would inherit, runs on sonnet unless
|
|
29
|
+
the task carries `above: <why>`. It did that even when the task opened with a card naming critical work (`change:
|
|
30
|
+
fix-unknown`, `security`, `migration`, `architecture`, or a file on a critical path), which a crew agent would run on
|
|
31
|
+
opus at least, so going round the crew was the cheaper way. Such a call now keeps the model it asked for; nothing is
|
|
32
|
+
raised. An `above:` at the end of a one-line card is now read there too, and Studio's inspector and page say so.
|
|
33
|
+
|
|
34
|
+
## [3.2.0] — 2026-10-21
|
|
9
35
|
|
|
10
36
|
### Added
|
|
11
37
|
|
|
@@ -233,6 +259,16 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
233
259
|
- **`change: review` is read as `audit`.** The model table of the working rules names review with audit, and
|
|
234
260
|
`crew-review-agent` cards that said `change: review` were refused. It now takes the audit floor, on a critical path
|
|
235
261
|
too; a kind that is not on the list is still refused.
|
|
262
|
+
- **An install or an update never loosens the trace gate by itself.** The installer suggested exempting
|
|
263
|
+
the co-author trailer whenever the last 80 commits carried one, and with no terminal to ask in it took its own
|
|
264
|
+
suggestion: an update wrote the line into `.trace-allowlist.txt` and staged it, undoing a removal the team had
|
|
265
|
+
committed. The suggestion is now always to keep the gate, and the exemption is written only when you type
|
|
266
|
+
`loosen` in the interactive review.
|
|
267
|
+
- **A commit refusal names a trace in the message even when the staged content also fails.** The content gate
|
|
268
|
+
scanned the `-m` message only when the staged change was clean, so a refusal for a silenced lint check said nothing about
|
|
269
|
+
the session-link trailer in the same message, and the session showed the user the traced message again. Both are
|
|
270
|
+
named in one refusal now, and a trace in the message adds that a harness reminder to end a message with a
|
|
271
|
+
trailer does not override §4.1. The git `commit-msg` hook still stops the trailer at the commit itself.
|
|
236
272
|
- **A one-line task card may open with `Kart`.** Sessions that write in Turkish lead the card with `Kart —` as
|
|
237
273
|
others write `Card —`, and the call was refused as having no card. The Turkish word is read like the English one;
|
|
238
274
|
the fields after it are held to the same rules.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.2.
|
|
1
|
+
3.2.1
|
package/adopt.sh
CHANGED
|
@@ -199,8 +199,7 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
|
|
|
199
199
|
"project wins (fixed)") s='proje önde (sabit)' ;;
|
|
200
200
|
"on conflict the project's rules win; in DISCIPLINE.md §4 they can only tighten, never loosen (the one exception: the §4.1 trace allowlist you choose at adoption); Crewforth fills gaps (not overridable)") s="çakışmada projenin kuralı geçerli; DISCIPLINE.md §4'te proje yalnız sıkılaştırabilir, gevşetemez (tek istisna: kurulumda sizin seçtiğiniz §4.1 iz izin listesi); Crewforth boşlukları doldurur (değiştirilemez)" ;;
|
|
201
201
|
"3 Trace gate") s='3 İz kapısı' ;;
|
|
202
|
-
"loosen
|
|
203
|
-
"co-author/sign-off present in git log — may be a convention") s='git geçmişinde co-author/sign-off var — ekibin alışkanlığı olabilir' ;;
|
|
202
|
+
"co-author/sign-off present in git log — type loosen in the review to exempt it; never assumed") s='git geçmişinde co-author/sign-off var — muaf tutmak için incelemede loosen yazın; kendiliğinden yapılmaz' ;;
|
|
204
203
|
"keep") s='koru' ;;
|
|
205
204
|
"no co-author/sign-off convention seen") s='co-author/sign-off alışkanlığı görülmedi' ;;
|
|
206
205
|
"4 Share/hide") s='4 Paylaş/gizle' ;;
|
|
@@ -789,7 +788,7 @@ else
|
|
|
789
788
|
fi
|
|
790
789
|
propm '2 Precedence' 'project wins (fixed)' "on conflict the project's rules win; in DISCIPLINE.md §4 they can only tighten, never loosen (the one exception: the §4.1 trace allowlist you choose at adoption); Crewforth fills gaps (not overridable)"
|
|
791
790
|
if [ "$COAUTHOR" = 1 ]; then
|
|
792
|
-
propm '3 Trace gate' '
|
|
791
|
+
propm '3 Trace gate' 'keep' 'co-author/sign-off present in git log — type loosen in the review to exempt it; never assumed'
|
|
793
792
|
else
|
|
794
793
|
propm '3 Trace gate' 'keep' 'no co-author/sign-off convention seen'
|
|
795
794
|
fi
|
|
@@ -817,7 +816,12 @@ fi
|
|
|
817
816
|
# ============ COMPILE DECISIONS + OVERRIDE (Stage B) ============
|
|
818
817
|
DEC1="$([ "$N_PAGENTS" != 0 ] && echo keep || echo none)"
|
|
819
818
|
DEC2="project" # precedence is FIXED to project-wins (not overridable — reflected in the @import comment)
|
|
820
|
-
|
|
819
|
+
# The trace gate is loosened only when a person types `loosen` in the review below. Never a default: old co-author
|
|
820
|
+
# lines in the history are not a decision, and an install that cannot ask (no terminal) or an update has no person
|
|
821
|
+
# to make one. Measured on a real project, 2026-10-21: a non-interactive update found five old trailers in the last
|
|
822
|
+
# 80 commits, wrote the co-author trailer into .trace-allowlist.txt and staged it, undoing a removal the team had
|
|
823
|
+
# committed.
|
|
824
|
+
DEC3=keep
|
|
821
825
|
DEC4="$([ "$TRACKED" = 1 ] && echo share || { [ "$IGN" = 1 ] && echo hide || echo kit-default; })"
|
|
822
826
|
DEC6="baseline"
|
|
823
827
|
DEC7="$([ "$OFFREPO" = 1 ] && echo transfer || echo local)"
|
package/kit/eval/doctor.sh
CHANGED
|
@@ -154,6 +154,9 @@ _mt() {
|
|
|
154
154
|
'CLAUDE.md does not import .claude/DISCIPLINE.md — the discipline is on disk but never loaded') s="CLAUDE.md .claude/DISCIPLINE.md'yi içe aktarmıyor — disiplin diskte duruyor ama hiç yüklenmiyor" ;;
|
|
155
155
|
'add this as its own line at the top of CLAUDE.md: @.claude/DISCIPLINE.md') s="CLAUDE.md'nin en üstüne şunu ayrı bir satır olarak ekleyin: @.claude/DISCIPLINE.md" ;;
|
|
156
156
|
' · panel needs Node 18+ — .claude/studio/ensure-node.sh --plan fetches one') s=' · panel Node 18+ ister — .claude/studio/ensure-node.sh --plan bir tane indirir' ;;
|
|
157
|
+
'Node %s is on a line that no longer gets security fixes (%s fixes %s) — update Node') s="Node %s artık güvenlik düzeltmesi almayan bir hatta (%s şunları düzeltiyor: %s) — Node'u güncelleyin" ;;
|
|
158
|
+
'Node %s is below %s, the release of its line with the fixes of %s — update Node') s="Node %s, %s altında; o hattın %s düzeltmelerini taşıyan sürümü bu — Node'u güncelleyin" ;;
|
|
159
|
+
'Node %s carries the security fixes on record') s='Node %s kayıttaki güvenlik düzeltmelerini taşıyor' ;;
|
|
157
160
|
'%s is set — its 3.0 name is CREW_%s (the old name works until 4.0)') s="%s ayarlı — 3.0'daki adı CREW_%s (eski ad 4.0'a kadar çalışır)" ;;
|
|
158
161
|
'%s is set but no longer read — set CREW_%s instead') s='%s ayarlı ama artık okunmuyor — yerine CREW_%s ayarlayın' ;;
|
|
159
162
|
'DOCTOR: healthy ✅%s') s='DOCTOR: sağlıklı ✅%s' ;;
|
|
@@ -661,6 +664,30 @@ for _v in $(compgen -e); do
|
|
|
661
664
|
esac ;;
|
|
662
665
|
esac
|
|
663
666
|
done
|
|
667
|
+
# A Node missing a published security fix (eval/lib/node-advisory.tsv, the table the Studio server reads too).
|
|
668
|
+
# Warned, never counted: the gates are bash, Node runs the panel and the map, and the user updates it.
|
|
669
|
+
_na="$(dirname "$0")/lib/node-advisory.tsv"; [ -f "$_na" ] || _na=".claude/eval/lib/node-advisory.tsv"
|
|
670
|
+
if [ -f "$_na" ] && _nv="$(node -p 'process.versions.node' 2>/dev/null)"; then
|
|
671
|
+
_nu=""; _nfix=""; _nall=""; _ntop=0; _nmaj="${_nv%%.*}"
|
|
672
|
+
while IFS=$'\t' read -r _k _f || [ -n "$_k" ]; do
|
|
673
|
+
_f="${_f%$'\r'}"
|
|
674
|
+
case "$_k" in advisory) _nu="$_f" ;; *[!0-9]*|'') ;;
|
|
675
|
+
*) _nall="${_nall:+$_nall, }$_f"; [ "$_k" -gt "$_ntop" ] && _ntop="$_k"; [ "$_k" = "$_nmaj" ] && _nfix="$_f" ;; esac
|
|
676
|
+
done < "$_na"
|
|
677
|
+
case "$_nv" in
|
|
678
|
+
[0-9]*.[0-9]*.[0-9]*)
|
|
679
|
+
if [ "$_ntop" -gt 0 ] && [ "$_nmaj" -le "$_ntop" ]; then
|
|
680
|
+
if [ -z "$_nfix" ]; then
|
|
681
|
+
warn "Node %s is on a line that no longer gets security fixes (%s fixes %s) — update Node" "$_nv" "$_nu" "$_nall"
|
|
682
|
+
else
|
|
683
|
+
IFS=. read -r _a1 _a2 _a3 <<< "$_nv"; IFS=. read -r _b1 _b2 _b3 <<< "$_nfix"; _a3="${_a3%%[!0-9]*}"
|
|
684
|
+
if [ "$_a2" -lt "$_b2" ] || { [ "$_a2" = "$_b2" ] && [ "${_a3:-0}" -lt "$_b3" ]; }; then
|
|
685
|
+
warn "Node %s is below %s, the release of its line with the fixes of %s — update Node" "$_nv" "$_nfix" "$_nu"
|
|
686
|
+
else ok "Node %s carries the security fixes on record" "$_nv"; fi
|
|
687
|
+
fi
|
|
688
|
+
fi ;;
|
|
689
|
+
esac
|
|
690
|
+
fi
|
|
664
691
|
if [ "$FAIL" -eq 0 ]; then _mt "DOCTOR: healthy ✅%s" "$PANEL_NOTE"; echo "$_M"
|
|
665
692
|
# Healthy verdict: the star line, once per kit version under doctor's own marker (the installers keep theirs).
|
|
666
693
|
# /crew-update runs this with CREW_NO_STAR=1, so an update shows it once, not twice. Text/URL/silence: lib/star.sh.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Node release lines and the first release of each that carries the fixes of Node's latest security advisory.
|
|
2
|
+
# Read by eval/doctor.sh and the Studio server (studio/server/lib/node-advisory.js); one table, two readers.
|
|
3
|
+
# A Node below its line's release, or on a line older than the newest listed that is not listed (it gets no fixes),
|
|
4
|
+
# is warned about, never refused: the gates are bash, Node runs the panel and the map, and the user updates it.
|
|
5
|
+
# A line newer than every listed one is not judged. Update this file when Node publishes a new advisory.
|
|
6
|
+
advisory https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
|
|
7
|
+
22 22.23.4
|
|
8
|
+
24 24.22.1
|
|
9
|
+
26 26.12.1
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -4760,6 +4760,28 @@ case "$_d9o" in *"needs the Anthropic API"*) ;; *) _d9b="$_d9b bedrock-not-seen"
|
|
|
4760
4760
|
rm -rf "$_d9"
|
|
4761
4761
|
[ -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" \
|
|
4762
4762
|
|| fail "doctor 9c:$_d9b"
|
|
4763
|
+
# A Node missing a published security fix is warned about, never counted (eval/lib/node-advisory.tsv, read by the
|
|
4764
|
+
# Studio server too). Driven through a stub `node` per version; the minor is compared as a number (22.9 < 22.23),
|
|
4765
|
+
# a line older than the table's newest that it does not list gets no fixes, a newer line is not judged, and a node
|
|
4766
|
+
# that cannot run says nothing. No case changes the verdict.
|
|
4767
|
+
_dn="$(mktemp -d)"; _dnb=""; _dnn=0
|
|
4768
|
+
_dnrun(){ printf '#!/bin/sh\ncase "$*" in *split*) echo "%s" | cut -d. -f1 ;; *) echo "%s" ;; esac\n' "$1" "$1" > "$_dn/node"; chmod +x "$_dn/node"
|
|
4769
|
+
_dno="$( cd "$DOC" && CREW_LANG="${2:-en}" PATH="$_dn:$PATH" bash "$ROOT/eval/doctor.sh" 2>&1 )"; _dnrc=$?; _dnn=$((_dnn+1))
|
|
4770
|
+
[ "$_dnrc" = 0 ] || _dnb="$_dnb [$1 changed the verdict: rc $_dnrc]"; }
|
|
4771
|
+
_dnrun 24.21.0; case "$_dno" in *"Node 24.21.0 is below 24.22.1, the release of its line with the fixes of https://nodejs.org/"*) ;; *) _dnb="$_dnb [24.21.0 not warned]" ;; esac
|
|
4772
|
+
_dnrun 22.9.10; case "$_dno" in *"Node 22.9.10 is below 22.23.4"*) ;; *) _dnb="$_dnb [22.9.10 not warned: minor compared as text?]" ;; esac
|
|
4773
|
+
_dnrun 26.12.0; case "$_dno" in *"Node 26.12.0 is below 26.12.1"*) ;; *) _dnb="$_dnb [26.12.0 not warned]" ;; esac
|
|
4774
|
+
_dnrun 20.19.0; case "$_dno" in *"Node 20.19.0 is on a line that no longer gets security fixes"*"22.23.4, 24.22.1, 26.12.1"*) ;; *) _dnb="$_dnb [20.x not warned]" ;; esac
|
|
4775
|
+
for _v in 24.22.1 24.23.0 26.12.1; do _dnrun "$_v"; case "$_dno" in *"Node $_v carries the security fixes on record"*) ;; *) _dnb="$_dnb [$_v not ok]" ;; esac
|
|
4776
|
+
case "$_dno" in *"is below"*|*"no longer gets"*) _dnb="$_dnb [$_v warned]" ;; esac; done
|
|
4777
|
+
_dnrun 28.0.0; case "$_dno" in *"Node 28.0.0"*) _dnb="$_dnb [a newer line was judged]" ;; esac
|
|
4778
|
+
_dnrun 24.21.0 tr; case "$_dno" in *"Node 24.21.0, 24.22.1 altında"*) ;; *) _dnb="$_dnb [no Turkish line]" ;; esac
|
|
4779
|
+
printf '#!/bin/sh\nexit 127\n' > "$_dn/node"; _dno="$( cd "$DOC" && CREW_LANG=en PATH="$_dn:$PATH" bash "$ROOT/eval/doctor.sh" 2>&1 )"; _dnn=$((_dnn+1))
|
|
4780
|
+
case "$_dno" in *"Node "*"security fixes"*|*"is below"*) _dnb="$_dnb [a node that cannot run was judged]" ;; esac
|
|
4781
|
+
rm -rf "$_dn"
|
|
4782
|
+
if [ "$_dnn" != 10 ]; then fail "FIXTURE: the Node advisory block ran $_dnn doctor runs, not 10"
|
|
4783
|
+
elif [ -z "$_dnb" ]; then pass "doctor warns about a Node missing a published security fix: below its line's release (24.21.0, 26.12.0, 22.9.10 with the minor compared as a number), on a line that gets none (20.x); says ok on or above it; judges no newer line and no node that cannot run; Turkish too; never changes the verdict (10 runs)"
|
|
4784
|
+
else fail "doctor Node advisory:$_dnb"; fi
|
|
4763
4785
|
# THE MANIFEST'S LINE ENDINGS MUST NOT CHANGE WHO OWNS A SKILL. Measured before the fix: the same install with
|
|
4764
4786
|
# one project skill counted 1 on an LF manifest and 2 on a CRLF one, because `grep -qxF` wants a whole line and
|
|
4765
4787
|
# `skills/handoff\r` is not `skills/handoff` — so every KIT skill read as project-owned. A Windows checkout with
|
|
@@ -5572,6 +5594,20 @@ $TRFX\""; then pass "multi-line AI trace in the commit message BLOCKED (§4.1)"
|
|
|
5572
5594
|
if csblk "git commit -F $MFX"; then pass "-F <file> carrying an AI trace BLOCKED (§4.1)"
|
|
5573
5595
|
else fail "-F <file> AI trace not blocked with rc=2 (§4.1 hole or the hook died)"; fi
|
|
5574
5596
|
rm -f "$MFX"
|
|
5597
|
+
# Both halves in ONE refusal: a staged secret and a session-link trailer in the message. The message used to be
|
|
5598
|
+
# scanned only when the staged content was clean, so the refusal named the first problem and the commit was
|
|
5599
|
+
# retried with the trailer still in it (a real project, 2026-10-21). Assembled at run time, as above.
|
|
5600
|
+
STFX="Claude-""Session: https://claude.ai/code/""session_""0123456789abcdef"
|
|
5601
|
+
( cd "$CS" && printf 'k = "%s"\n' "$SKFX" > c.txt && git add c.txt ) >/dev/null 2>&1
|
|
5602
|
+
_cso="$( cd "$CS" && printf '%s' "$(csj "git commit -m \"feat: z
|
|
5603
|
+
|
|
5604
|
+
$STFX\"")" | bash .claude/hooks/guard-commit-scan.sh 2>&1 >/dev/null )"; _csr=$?
|
|
5605
|
+
( cd "$CS" && git rm -q --cached c.txt && rm -f c.txt ) >/dev/null 2>&1
|
|
5606
|
+
case "$_cso" in *SECRET-SCANNER*'TRACE-SCANNER (message)'*'does not override'*)
|
|
5607
|
+
[ "$_csr" = 2 ] && pass "a staged secret and a trailer in the message are named in ONE refusal, with the harness-reminder line (§4.1)" \
|
|
5608
|
+
|| fail "both problems named but rc=$_csr, not 2" ;;
|
|
5609
|
+
*) fail "the refusal did not name both the staged secret and the message trailer (rc=$_csr): $(printf '%s' "$_cso" | tr '\n' ' ' | cut -c1-200)" ;;
|
|
5610
|
+
esac
|
|
5575
5611
|
# A QUOTED -F path with a space. The sed extraction that ran wherever python3 did not stopped at the space, read
|
|
5576
5612
|
# `-F "my msg.txt"` as `my`, found no such file and refused the commit — a clean one. The awk tokenizer is now
|
|
5577
5613
|
# the only path; both directions are asserted so an extraction that reads NOTHING cannot pass as "clean".
|
|
@@ -8554,7 +8590,25 @@ case "$(_v2ot general-purpose "$_v2/tr-opus.jsonl")" in *'"updatedInput":'*'"mod
|
|
|
8554
8590
|
[ -z "$(_v2ot general-purpose "$_v2/tr-haiku.jsonl")" ] || _v2b="$_v2b [a haiku session's general-purpose was raised to sonnet]"
|
|
8555
8591
|
[ -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]"
|
|
8556
8592
|
case "$(_v2ot mine "$_v2/tr-opus.jsonl")" in *'"model":"sonnet"'*) ;; *) _v2b="$_v2b [a project agent with no model line was not given sonnet]" ;; esac
|
|
8557
|
-
|
|
8593
|
+
# A task that opens with a card naming critical work is not lowered outside the crew either: a crew agent would have
|
|
8594
|
+
# had an opus floor for it. Seen on a real session: a one-line card with change: fix-unknown to general-purpose on opus
|
|
8595
|
+
# ran on sonnet. Nothing is raised, and a normal card, or a card that is not one, is lowered as before.
|
|
8596
|
+
_v2oc(){ printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"PreToolUse","tool_name":"Agent","transcript_path":"%s","tool_input":{"description":"d","prompt":"%s\\n\\nFind X.","subagent_type":"general-purpose"%s},"tool_use_id":"tu%s"}' "$_v2/tr-opus.jsonl" "$1" "${2:-}" "$RANDOM" | ( cd "$_v2p" && _v2e bash "$_V2G" 2>/dev/null ) | tr -d ' '; }
|
|
8597
|
+
_v2kept=0
|
|
8598
|
+
for _c in 'files: none in repo (scratch scripts only)\nchange: fix-unknown\nverify: none' \
|
|
8599
|
+
'Card — files: none in repo (scratch scripts only) · change: fix-unknown · verify: none' \
|
|
8600
|
+
'files: db/migrations/001.sql\nchange: feature\nverify: none' \
|
|
8601
|
+
'files: src/payments/p.ts\nchange: feature\nverify: none' \
|
|
8602
|
+
'files: src/ui/a.ts · change: feature · verify: none · above: a design question'; do
|
|
8603
|
+
[ -z "$(_v2oc "$_c" ',"model":"opus"')" ] && _v2kept=$((_v2kept+1)) || _v2b="$_v2b [kept on opus: ${_c:0:50}]"
|
|
8604
|
+
done
|
|
8605
|
+
[ -z "$(_v2oc 'files: x\nchange: security\nverify: none')" ] && _v2kept=$((_v2kept+1)) || _v2b="$_v2b [critical card naming no model was lowered]"
|
|
8606
|
+
[ -z "$(_v2oc 'files: x\nchange: security\nverify: none' ',"model":"sonnet"')" ] && _v2kept=$((_v2kept+1)) || _v2b="$_v2b [critical card on sonnet was raised]"
|
|
8607
|
+
for _c in 'files: src/ui/a.ts\nchange: feature\nverify: none' 'files: src/ui/a.ts\nchange: fix-unknown' 'change: fix-unknown is what this is'; do
|
|
8608
|
+
case "$(_v2oc "$_c" ',"model":"opus"')" in *'"updatedInput":'*'"model":"sonnet"'*) _v2kept=$((_v2kept+1)) ;; *) _v2b="$_v2b [not lowered: ${_c:0:50}]" ;; esac
|
|
8609
|
+
done
|
|
8610
|
+
[ "$_v2kept" = 10 ] || _v2b="$_v2b [critical-card cases: $_v2kept of 10]"
|
|
8611
|
+
[ -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; a task whose card names critical work (fix-unknown, a migration, a payments file, on one line or three, or naming no model) is not lowered and not raised, while a normal card, a broken card and an inline above: behave as before (10 cases)" \
|
|
8558
8612
|
|| fail "model on the floor:$_v2b"
|
|
8559
8613
|
# The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
|
|
8560
8614
|
# left the field out and was launched in the background is told so when the launch comes back.
|
|
@@ -533,6 +533,9 @@ _am_digest(){ # after _am_card -> CARD_ID: 12 hex of what the card says, so "th
|
|
|
533
533
|
# A call to an agent that is not Crewforth's (general-purpose, a project's own): no card and no floor, so only the
|
|
534
534
|
# expensive end is held, the same rewrite as a crew call above its floor.
|
|
535
535
|
# * It names opus or fable, and its task has no `above: <why>` line: it runs on sonnet.
|
|
536
|
+
# * Either way, a task that opens with a card naming critical work (the same reading as a crew call: change:
|
|
537
|
+
# fix-unknown, security, migration, architecture, or a file on a critical path) is not lowered: a crew agent
|
|
538
|
+
# would have had an opus floor for it, so the way round the crew must not be the cheaper one. Nothing is raised.
|
|
536
539
|
# * It names no model, and the agent takes the session's (general-purpose, or a project or user agent whose
|
|
537
540
|
# definition has no model of its own): it runs on sonnet, unless the session itself is on haiku, read from the
|
|
538
541
|
# end of its transcript. An agent whose definition names a model (Explore, claude-code-guide) is left to it:
|
|
@@ -572,6 +575,11 @@ _am_outside(){ # $1 = the tool_input text, $2 = the agent's name
|
|
|
572
575
|
_json_slice "$1" model >/dev/null; _json_unescape "$_JS" >/dev/null; _am_tier "$_JU"; m="$_AMT"
|
|
573
576
|
case "$m" in opus|fable) ;; *) return 0 ;; esac
|
|
574
577
|
fi
|
|
578
|
+
if _am_card "$pr"; then
|
|
579
|
+
[ -z "$CARD_ABOVE" ] || return 0
|
|
580
|
+
_am_risk
|
|
581
|
+
if [ "$CARD_RISK" = critical ]; then _am_log "note: $name (not a crew agent) kept $m: its card names critical work ($CARD_RISKWHY)"; return 0; fi
|
|
582
|
+
fi
|
|
575
583
|
ui="$(printf '%s' "$INPUT" | awk -v op=get -v path=tool_input -v setk=model -v setv="\"$to\"" -f "$_AM_HERE/../eval/lib/settings-json.awk" - 2>/dev/null)" || return 0
|
|
576
584
|
case "$ui" in '{'*'}') ;; *) return 0 ;; esac
|
|
577
585
|
_am_log "note: $name (not a crew agent) asked for $m, run on $to; add 'above: <why>' to its task to keep a model above it"
|
|
@@ -588,8 +588,11 @@ fi
|
|
|
588
588
|
|
|
589
589
|
# The message carries its own trace risk (a co-author trailer lives there, not in the diff), and commit-msg is
|
|
590
590
|
# what scans it. Reuse it on the -m value when there is one; an editor-composed message is not visible here and
|
|
591
|
-
# stays the git hook's job.
|
|
592
|
-
|
|
591
|
+
# stays the git hook's job. It is scanned even when the staged content already failed: a refusal that names one
|
|
592
|
+
# problem gets the commit retried with the other still in it (measured on a real project, 2026-10-21: a silenced
|
|
593
|
+
# lint check was named, the session-link trailer in the same message was not, and the message was shown to the
|
|
594
|
+
# user again).
|
|
595
|
+
if [ -x "$DIR/commit-msg" ]; then
|
|
593
596
|
# The message is tokenized the way a shell does (crew_opt_values), which matters because a real commit message
|
|
594
597
|
# is MULTI-LINE: a line-oriented `sed` extraction found the subject and stopped, so a co-author trailer on line
|
|
595
598
|
# 3 — the single most likely §4.1 violation, and the one the bare arm of the eval actually produced — went
|
|
@@ -640,6 +643,7 @@ $(bash "$DIR/commit-msg" "$MF" 2>&1)" || FAILED=1
|
|
|
640
643
|
| sed -n 's/.*[[:space:]]--\{0,1\}[Ff]\(ile\)\{0,1\}[[:space:]=]\{1,\}\([^[:space:];&|]\{1,\}\).*/\2/p' | head -1)"
|
|
641
644
|
MFILE="${MFILE%\"}"; MFILE="${MFILE#\"}"; MFILE="${MFILE%\'}"; MFILE="${MFILE#\'}"
|
|
642
645
|
if _grep "$CMD" -qE '(^|[[:space:]])(-F|--file)([[:space:]=]|$)' && { [ -z "$MFILE" ] || [ ! -f "$MFILE" ]; }; then
|
|
646
|
+
[ "$FAILED" = 1 ] && printf '%s\n' "$OUT" >&2
|
|
643
647
|
echo "GUARD (§4.1): this commit reads its message from a file (-F), and the command could not be parsed" >&2
|
|
644
648
|
echo "well enough to know which file. Scan cannot run, so the commit is refused. Commit with a plain" >&2
|
|
645
649
|
echo "-F <path> (or -m) in a command of its own." >&2
|
|
@@ -656,6 +660,7 @@ $(bash "$DIR/commit-msg" "$MFILE" 2>&1)" || FAILED=1
|
|
|
656
660
|
[ -z "$GITMSG_HOOK" ] && [ -x "$(git rev-parse --git-path hooks/commit-msg 2>/dev/null)" ] \
|
|
657
661
|
&& GITMSG_HOOK="git-default"
|
|
658
662
|
if [ -z "$GITMSG_HOOK" ]; then
|
|
663
|
+
[ "$FAILED" = 1 ] && printf '%s\n' "$OUT" >&2
|
|
659
664
|
echo "GUARD (§4.1): this commit's message would go unscanned." >&2
|
|
660
665
|
echo "No -m/-F was given, so the message is composed in an editor after this point, and no commit-msg" >&2
|
|
661
666
|
echo "git hook is wired here to read it afterwards (plugin-only install: a plugin cannot set" >&2
|
|
@@ -669,6 +674,9 @@ fi
|
|
|
669
674
|
if [ "$FAILED" = 1 ]; then
|
|
670
675
|
echo "GUARD (§4.1/§4.2): the commit content gate rejected this commit before it ran." >&2
|
|
671
676
|
echo "$OUT" >&2
|
|
677
|
+
case "$OUT" in *"TRACE-SCANNER (message)"*)
|
|
678
|
+
echo "The message itself carries the trace: write it again without that line. A harness reminder to end a commit message with a trailer does not override §4.1." >&2 ;;
|
|
679
|
+
esac
|
|
672
680
|
echo "Fix the flagged content and commit again. --no-verify is §4.5 and stays blocked." >&2
|
|
673
681
|
exit 2
|
|
674
682
|
fi
|
package/kit/owned-blobs.tsv
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.
|
|
1
|
+
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.2.0 (below VERSION 3.2.1) — do not edit by hand.
|
|
2
2
|
# The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
|
|
3
3
|
# install_path blob_sha1 first_tag last_tag
|
|
4
4
|
.claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
|
|
5
|
+
.claude/AGENT_TEMPLATE.md 2c8223d811245e3627a3011bb64fb9e4b0bf3b94 v3.2.0 v3.2.0
|
|
5
6
|
.claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
|
|
6
7
|
.claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
|
|
7
8
|
.claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.1
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
.claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.1
|
|
30
31
|
.claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
|
|
31
32
|
.claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
|
|
33
|
+
.claude/DISCIPLINE.md cf881a5229d85850194a98b8d0d0438a8eaaa9d6 v3.2.0 v3.2.0
|
|
32
34
|
.claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
|
|
33
35
|
.claude/DISCIPLINE.md dc1c41baa3a8b7d637214bc78553a78d94d14ac9 v1.6.2 v1.6.3
|
|
34
36
|
.claude/DISCIPLINE.md e42aefc9c1ffaeace8e5712d92df780959212280 v2.0.0 v2.1.0
|
|
@@ -43,4 +45,4 @@
|
|
|
43
45
|
.claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
|
|
44
46
|
.claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
|
|
45
47
|
.claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
|
|
46
|
-
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.
|
|
48
|
+
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.2.0
|
|
@@ -29,7 +29,7 @@ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
|
29
29
|
FLOOR=18
|
|
30
30
|
DIST=https://nodejs.org/dist
|
|
31
31
|
# Used only when nodejs.org cannot be reached to ask which release is current.
|
|
32
|
-
FALLBACK_VERSION=v24.
|
|
32
|
+
FALLBACK_VERSION=v24.22.1 # carries the fixes of eval/lib/node-advisory.tsv; raise it with that table
|
|
33
33
|
|
|
34
34
|
# Windows caps a full file path at 260 characters and PowerShell's Expand-Archive enforces it.
|
|
35
35
|
# Measured: a 141-character target failed with PathTooLongException at __generated__\google\api\…
|
|
@@ -43,6 +43,7 @@ import { addViewer, listViewers, viewerByToken, revokeViewer, noteRequest, noteR
|
|
|
43
43
|
import { redact, transcriptOnly } from './lib/redact.js';
|
|
44
44
|
import { lanListener } from './lib/lan.js';
|
|
45
45
|
import { randomUUID } from 'node:crypto';
|
|
46
|
+
import { nodeAdvisoryNote } from './lib/node-advisory.js';
|
|
46
47
|
import { spawn } from 'node:child_process';
|
|
47
48
|
|
|
48
49
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1104,6 +1105,8 @@ async function main() {
|
|
|
1104
1105
|
process.stdout.write(TOKEN_GENERATED
|
|
1105
1106
|
? ' (loopback only; token generated for this run)\n'
|
|
1106
1107
|
: ' (loopback only; token from CREW_STUDIO_TOKEN)\n');
|
|
1108
|
+
const nodeNote = nodeAdvisoryNote();
|
|
1109
|
+
if (nodeNote) process.stdout.write(` warning: ${nodeNote}\n`);
|
|
1107
1110
|
|
|
1108
1111
|
// Recorded only after listen() succeeds, so the file never claims a port this process did not get. Failure
|
|
1109
1112
|
// to write is not fatal: the panel works, the next session simply cannot find it, which is where we started.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Is the Node running this panel missing a published security fix? The table is eval/lib/node-advisory.tsv, the
|
|
2
|
+
// same file eval/doctor.sh reads, so the two cannot disagree. A warning only: the panel still starts.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const TABLE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'eval', 'lib', 'node-advisory.tsv');
|
|
8
|
+
|
|
9
|
+
/** The table's text as { url, lines: Map(major -> 'x.y.z') }; null when it holds no release line. */
|
|
10
|
+
export function parseAdvisory(text) {
|
|
11
|
+
let url = '';
|
|
12
|
+
const lines = new Map();
|
|
13
|
+
for (const raw of String(text ?? '').split(/\r?\n/)) {
|
|
14
|
+
if (!raw || raw.startsWith('#')) continue;
|
|
15
|
+
const [k, v] = raw.split('\t');
|
|
16
|
+
if (k === 'advisory') url = v ?? '';
|
|
17
|
+
else if (/^\d+$/.test(k) && /^\d+\.\d+\.\d+$/.test(v ?? '')) lines.set(Number(k), v);
|
|
18
|
+
}
|
|
19
|
+
return lines.size ? { url, lines } : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parts = (v) => String(v).replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
23
|
+
const below = (a, b) => { const x = parts(a), y = parts(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i]; return false; };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The sentence to print for `version` (process.versions.node), or null when there is nothing to say: it carries
|
|
27
|
+
* its line's fixes, its line is newer than the table, or the table could not be read.
|
|
28
|
+
*/
|
|
29
|
+
export function judgeNode(version, advisory) {
|
|
30
|
+
if (!advisory || !/^\d+\.\d+\.\d+/.test(String(version ?? ''))) return null;
|
|
31
|
+
const major = parts(version)[0];
|
|
32
|
+
const newest = Math.max(...advisory.lines.keys());
|
|
33
|
+
if (major > newest) return null;
|
|
34
|
+
const fixed = advisory.lines.get(major);
|
|
35
|
+
if (fixed && !below(version, fixed)) return null;
|
|
36
|
+
return fixed
|
|
37
|
+
? `Node ${version} is below ${fixed}, the release of its line with the fixes of ${advisory.url} — update Node`
|
|
38
|
+
: `Node ${version} is on a line that no longer gets security fixes (${advisory.url} fixes ${[...advisory.lines.values()].join(', ')}) — update Node`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** judgeNode for the running process, reading the table from disk; null when the table is not there. */
|
|
42
|
+
export function nodeAdvisoryNote(version = process.versions.node) {
|
|
43
|
+
let text;
|
|
44
|
+
try { text = fs.readFileSync(TABLE, 'utf8'); } catch { return null; }
|
|
45
|
+
return judgeNode(version, parseAdvisory(text));
|
|
46
|
+
}
|
|
@@ -65,12 +65,12 @@ export function workSteps(row) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* What is said about an agent
|
|
69
|
-
*
|
|
68
|
+
* What is said about an agent that is not crew-*: it has no floor and no line in the record, but the model gate still
|
|
69
|
+
* holds its expensive end, except for a card naming critical work (_am_outside in hooks/guard-agent-model.sh). Null for a crew agent or an unknown type.
|
|
70
70
|
*/
|
|
71
71
|
export function outsideRouting(agentType) {
|
|
72
72
|
if (typeof agentType !== 'string' || !agentType || agentType.startsWith('crew-')) return null;
|
|
73
|
-
return 'Outside model routing: not a crew agent, so
|
|
73
|
+
return 'Outside model routing: not a crew agent, so it has no floor and Crewforth keeps no outcome line for it. The gate only holds the expensive end: a call that asks for opus or fable, or names no model and would take the session\u2019s, is held at sonnet unless its task carries an above: <why> line or opens with a card naming critical work. An agent whose own definition names a model keeps it.';
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewforth",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"description": "Crewforth
|
|
3
|
+
"version": "3.2.1",
|
|
4
|
+
"description": "Crewforth \u2014 your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"crewforth": "bin/cli.js"
|
|
7
7
|
},
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"kit/",
|
|
16
16
|
"VERSION",
|
|
17
17
|
"LICENSE",
|
|
18
|
-
"README.md",
|
|
19
|
-
"CHANGELOG.md"
|
|
18
|
+
"README.md", "CHANGELOG.md"
|
|
20
19
|
],
|
|
21
20
|
"engines": {
|
|
22
21
|
"node": ">=20"
|
|
@@ -43,7 +42,7 @@
|
|
|
43
42
|
"code-review",
|
|
44
43
|
"git-hooks"
|
|
45
44
|
],
|
|
46
|
-
"author": "
|
|
45
|
+
"author": "Bar\u0131\u015f Yerlikaya",
|
|
47
46
|
"homepage": "https://crewforth.com/",
|
|
48
47
|
"repository": {
|
|
49
48
|
"type": "git",
|