crewforth 3.2.0 → 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 CHANGED
@@ -5,6 +5,32 @@ 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
+ ## [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
+
8
34
  ## [3.2.0] — 2026-10-21
9
35
 
10
36
  ### Added
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.0
1
+ 3.2.1
@@ -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
@@ -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
@@ -8568,7 +8590,25 @@ case "$(_v2ot general-purpose "$_v2/tr-opus.jsonl")" in *'"updatedInput":'*'"mod
8568
8590
  [ -z "$(_v2ot general-purpose "$_v2/tr-haiku.jsonl")" ] || _v2b="$_v2b [a haiku session's general-purpose was raised to sonnet]"
8569
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]"
8570
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
8571
- [ -z "$_v2b" ] && pass "above the floor runs on the floor: opus for a sonnet card is rewritten to sonnet with every other field kept and recorded as lowered; 'above: <why>' keeps opus; a call on its floor is untouched; checkable text work goes to haiku; an agent outside the crew asking for opus, or inheriting an opus session's model, runs on sonnet unless its task says why; a haiku session and an agent with its own model are left alone" \
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)" \
8572
8612
  || fail "model on the floor:$_v2b"
8573
8613
  # The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
8574
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"
@@ -1,7 +1,8 @@
1
- # Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.1.1 (below VERSION 3.2.0) — do not edit by hand.
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.1.1
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.21.0
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 the model gate does not look at: one whose type is not crew-* (hooks/guard-agent-model.sh
69
- * leaves it alone), so there is no card to read and no line in the record. Null for a crew agent or an unknown type.
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 the gate chose nothing for it and Crewforth kept no record of it. It ran on the model its call asked for, or else the session\u2019s.';
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,6 +1,6 @@
1
1
  {
2
2
  "name": "crewforth",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
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"