crewforth 3.0.0-rc.1 → 3.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1575,6 +1575,25 @@ if [ "$IS_KIT" = 1 ]; then
1575
1575
  grep -q 'N_AG="$(count_installed' "$KR/start.sh" 2>/dev/null \
1576
1576
  && pass "start.sh derives its summary counts from the payload" \
1577
1577
  || fail "start.sh no longer derives its summary counts from the payload"
1578
+ # ...and the skills/commands it prints are the README's, not the directory count: the RC-1 rehearsal printed
1579
+ # "51 skills" beside a README that says 39 skills and 10 commands. count_showcase is lifted out of start.sh and
1580
+ # driven on a three-entry fixture (a skill, a command, an experimental one) and on the real payload, whose answer
1581
+ # must equal the counts the README gates above use.
1582
+ _csf="$(sed -n '/^count_showcase() {/,/^}/p' "$KR/start.sh")"
1583
+ if [ -z "$_csf" ]; then fail "start.sh has no count_showcase — the install prints a directory count again"
1584
+ else
1585
+ _csd="$(mktemp -d)"; mkdir -p "$_csd/a" "$_csd/b" "$_csd/c"
1586
+ printf -- '---\nname: a\ndescription: x\n---\n' > "$_csd/a/SKILL.md"
1587
+ printf -- '---\nname: b\ndescription: x\nmetadata:\n kind: command\n---\n' > "$_csd/b/SKILL.md"
1588
+ printf -- '---\nname: c\ndescription: x\nmetadata:\n kind: command\n experimental: true\n---\n' > "$_csd/c/SKILL.md"
1589
+ _csx="$(bash -c "$_csf"'; count_showcase "$1"; echo "$N_SKL $N_CMD"' _ "$_csd")"
1590
+ _cshown="$(bash -c "$_csf"'; count_showcase "$1"; echo "$N_SKL $N_CMD"' _ "$SKILLS")"
1591
+ rm -rf "$_csd"
1592
+ if [ "$_csx" != "1 1" ]; then fail "count_showcase miscounts its fixture: '$_csx', want '1 1' (the experimental entry counts as neither)"
1593
+ elif _nsk=0; for _sf in "$SKILLS"/*/SKILL.md; do _sn="${_sf%/SKILL.md}"; _sn="${_sn##*/}"; is_cmd "$_sn" || is_exp "$_sn" || _nsk=$((_nsk+1)); done
1594
+ [ "$_cshown" != "$_nsk $NCMD_SHOWN" ]; then fail "start.sh would print '$_cshown' skills/commands; the README gates count $_nsk and $NCMD_SHOWN"
1595
+ else pass "start.sh prints the README's counts ($_cshown skills/commands; fixture 1 1 with the experimental one left out)"; fi
1596
+ fi
1578
1597
  # Crewforth installs through npx, the Claude Code plugin or the release archive; the Homebrew channel was
1579
1598
  # removed. A line that tells a reader to `brew install` Crewforth, or names the old tap, points at a formula
1580
1599
  # nobody maintains. `brew install node` and other tools stay: those install something else. The CHANGELOG is
@@ -2081,7 +2100,7 @@ sec "== 6f) always-on token budget =="
2081
2100
  # for that cost, and a gate rather than a reminder — a verbose new description fails the suite instead of
2082
2101
  # quietly taxing every future session. Budgets sit just above the current sizes: raising one is allowed, but
2083
2102
  # only as a deliberate edit here.
2084
- BUDGET_DISC=13711 # 5R.3: 13712 → 13711 (the old-name phrases rewritten, net −1). 5d.2 prompt audit: tightened to the measured size (13719 → 13712: format-to-content style line, one reload
2103
+ BUDGET_DISC=13741 # RC-1: 13711 → 13741 (+30): "Reply in the user's language even when a skill's text is English" — a Turkish session answered /crew-review in English, because every skill body it had just read was English; the rule is stated nowhere else. evals/cases/reply-language measures it. 5R.3: 13712 → 13711 (the old-name phrases rewritten, net −1). 5d.2 prompt audit: tightened to the measured size (13719 → 13712: format-to-content style line, one reload
2085
2104
  # answer, the orphaned background-warning line removed). Before that: 3.0 rename (suffix → crew- prefix): +23 B (23 occurrences), not content — measured 13696 → 13719.
2086
2105
  # DISCIPLINE.md (the discipline half of CLAUDE.md); before 3.0 the ceiling was 13700, currently 13601. (2026-09-18, a second
2087
2106
  # +100 B on top of the raise below, and the whole of it went into ONE sentence of §4.6: a commit
@@ -2178,53 +2197,17 @@ BUDGET_AGENTS=5596 # 5d.2: tightened to the measured sum (was 5800 with 204 B
2178
2197
  # +crew-performance-expert (~426B) — security, privacy and tests each had an independent
2179
2198
  # reviewer and performance was the one quality axis where the author audited their own
2180
2199
  # work. Bought at ~110 tokens per session; the alternative was leaving that gap open.)
2181
- BUDGET_SKILLS=10249 # 5S: 10255 → 10249 (crew-doctor: "the install"). 5R.4: 10259 → 10255 (crew-update: "a newer version"). 5d.2: tightened to the measured size (10265 → 10259: two descriptions lost a stale word). Before that:
2182
- # 3.0: commands merged into skills — 661 B previously in the listing but uncounted, not new content:
2183
- # the six model-invocable commands (doctor 136 · handoff 91 · plan 151 · review 82 · ship 78 ·
2184
- # update 123) now live in skills/, where this sum sees them. The five user-only ones
2185
- # (disable-model-invocation: true) are not in the listing and are not counted. The other skills
2186
- # measured 9604 before and after — the ceiling moved by exactly the 661, not a byte for anything new.
2187
- # Before that: 9604 — 3.0 rename (suffix → crew- prefix): +7 B (7 occurrences), not content — 9597 → 9604.
2188
- # Before 3.0: 9600; sum of skill frontmatter; currently 9597 — **3 bytes of headroom**, measured 2026-09-23 from
2189
- # this suite's own line. 3.0 swapped cqrs-aop-module (-202 B) for backend-architecture (+211 B) and
2190
- # the ceiling was NOT raised: the new description was cut until it fit. Before that 9588, measured
2191
- # 2026-09-20 (the note said 9521 and was 67 B stale). Read that margin before editing any
2192
- # description: one added clause trips this gate, and that is the ratchet working, not a bug.
2193
- # Raising the ceiling needs the same thing every bump here needed — a written reason for what
2194
- # the bytes buy. (2.6.x: +843 B — ten descriptions gained a
2195
- # "Use when …" sentence. The field's job is to say WHEN to reach for the skill; a description
2196
- # that only says what its author knows is matched by nothing, and inside Crewforth that was
2197
- # invisible because route-hint.sh and the trigger map do the routing. Outside the harness —
2198
- # a skill copied into another project, another client, a bare session — the routing is gone
2199
- # and the description is all there is. STATED HONESTLY: this bump does NOT fix the small-window
2200
- # case. The listing budget is 1% of the context window, so a 200k model allows ~2,000 B and
2201
- # Crewforth is far past that with or without these ten sentences. What changed is that the
2202
- # remedy is now targetable: `eval/utilization.sh` reports which skills nothing in a project
2203
- # actually reached, which is the list `skillOverrides: name-only` needs and never had. The
2204
- # next skill that wants room takes it from a description, not from another bump.)
2205
- # Was 8700 / 8678. (2.5.0: +307 B for `automode-policy` — about
2206
- # half of it the allowed-tools grant that lets the skill run its own verifier without a
2207
- # prompt, not prose. The listing is what every session pays for; the next skill that wants
2208
- # room takes it from a description, not from another bump.) Was 8380 / 8371 — nine bytes
2209
- # of headroom, so the ceiling was already the binding constraint, not this skill. (2.3.0: +187 B for `teamboard`. It is the only
2210
- # skill whose absence from the LISTING is silently unsafe rather than merely unhelpful: a
2211
- # skill that fails to match usually means the model does the work itself, but this one
2212
- # failing to match means two people do the SAME work, on separate machines, discovering it
2213
- # at merge time. Trimmed to one sentence and the words people actually type — claim, item,
2214
- # team board.) RATCHETED DOWN in 1.11.0 from 12,350: the
2215
- # `Trigger phrases:` lines moved out of every skill's `description` into the body. This is not
2216
- # cosmetic. Claude Code loads a LISTING of skill names+descriptions every session and the
2217
- # budget is 1% of the context window; over it, descriptions are truncated or dropped outright,
2218
- # "which can strip the keywords Claude needs to match your request" (official skills docs). The
2219
- # Crewforth's listing was 11,372 chars against a 10,000 budget on a 1M window — overflowing on every
2220
- # model, and 5.7x over on a 200k one. Now 7,208. A kit whose own skills push its descriptions
2221
- # out of the listing is a kit that stops matching, which is exactly the symptom users report.
2222
- # (1.8.0: +confidence-check (~359B), Crewforth's
2223
- # only gate that fires BEFORE implementation — every other one reviews code that already
2224
- # exists, and none catch correct code that should never have been written; and
2225
- # +dependency-upgrade (~444B), split from dependency-audit because one reports and the
2226
- # other rewrites lockfiles: different risk, different DoD, and an audit you can run on any
2227
- # branch stops being safe the moment it can also apply things)
2200
+ BUDGET_SKILL_LISTING=9063 # the skill listing as Claude Code counts it (eval/lib/skill-listing.awk — the count doctor.sh
2201
+ # reports too), in CHARACTERS: 46 model-invocable skills, 9063, the figure Claude Code's own
2202
+ # "Skill listing over budget" warning printed for a fresh install. It replaced a byte sum of
2203
+ # the frontmatter that no tool used (10249), so doctor and this gate now read one number.
2204
+ # The ceiling is "does not grow", not a target. 8,000 was weighed and rejected: that is the
2205
+ # whole listing's budget on a 200k model at the default fraction, and Claude Code's own
2206
+ # skills take ~5,900 of it (v2.1.282), so no Crewforth-only figure could make the whole list
2207
+ # fit. The full install raises the fraction to 0.04; the plugin cannot, so doctor and
2208
+ # /crew-doctor tell a plugin user on a 200k model the one line to add and what it costs.
2209
+ # Shorter descriptions wait for an eval that measures whether Claude still picks the skill
2210
+ # from them — trimming blind trades a number for matching nobody measured.
2228
2211
  # A SKILL.md's `metadata:` block is Crewforth's own catalogue data (`kind: command`); Claude Code does not act on it
2229
2212
  # and it never enters the listing, so it is not counted. Nothing else in a frontmatter is skipped.
2230
2213
  fm_bytes(){ awk '/^---$/{c++; next} c==1 { if ($0 ~ /^metadata:/) { m=1; next } if (m && $0 ~ /^[ \t]/) next; m=0; print }' "$1" 2>/dev/null | wc -c | tr -d ' '; }
@@ -2242,15 +2225,33 @@ elif [ -f "$ROOT/DISCIPLINE.md" ]; then
2242
2225
  DB="$(wc -c < "$ROOT/DISCIPLINE.md" | tr -d ' ')"; DBCR="$(tr -dc '\r' < "$ROOT/DISCIPLINE.md" | wc -c | tr -d ' ')"
2243
2226
  else DB=0; DBCR=0; fi
2244
2227
  AB=0; for f in "$AGENTS"/*.md; do [ -e "$f" ] && AB=$((AB + $(fm_bytes "$f"))); done
2245
- # THE RULE: a skill counts unless its frontmatter says `disable-model-invocation: true`. Per the skills reference,
2246
- # with that flag "the description is not in context" — only the user can invoke it, so it costs the listing nothing.
2247
- # Every other skill's description IS in the listing every session, and that is what this budget is for. (Before
2248
- # 3.0 the six model-invocable commands sat in the same listing from commands/, and this sum never saw them.)
2249
- SB=0; for f in "$SKILLS"/*/SKILL.md; do
2250
- [ -e "$f" ] || continue
2251
- grep -q '^disable-model-invocation:[[:space:]]*true' "$f" && continue
2252
- SB=$((SB + $(fm_bytes "$f")))
2253
- done
2228
+ # The skill listing is counted by eval/lib/skill-listing.awk, the file doctor.sh uses: a skill with
2229
+ # `disable-model-invocation: true` is not listed, every other one costs its name and description as Claude Code counts
2230
+ # them. The counter is pinned first on the fixtures Claude Code itself was measured on, so a drift in the awk (or in an
2231
+ # awk: BSD, mawk and Git Bash gawk all run this) reads as a broken meter, not as a budget.
2232
+ SLA="$ROOT/eval/lib/skill-listing.awk"; [ -f "$SLA" ] || SLA="$SGR/kit/eval/lib/skill-listing.awk"
2233
+ if [ ! -f "$SLA" ]; then fail "eval/lib/skill-listing.awk is missing — the skill listing cannot be counted"; SL=0
2234
+ else
2235
+ _slf="$(mktemp -d)"; mkdir -p "$_slf/a" "$_slf/b" "$_slf/c" "$_slf/d"
2236
+ printf -- '---\nname: aa\ndescription: bbb\n---\n' > "$_slf/a/SKILL.md"
2237
+ printf -- '---\nname: cc\ndescription: dddd\n---\n' > "$_slf/b/SKILL.md"
2238
+ printf -- '---\nname: ee\ndescription: |\n one\n two\n---\n' > "$_slf/c/SKILL.md"
2239
+ printf -- '---\nname: ff\ndescription: gg\ndisable-model-invocation: true\n---\n' > "$_slf/d/SKILL.md"
2240
+ _c1="$(LC_ALL=C awk -f "$SLA" "$_slf/a/SKILL.md")"; _c2="$(LC_ALL=C awk -f "$SLA" "$_slf/a/SKILL.md" "$_slf/b/SKILL.md")"
2241
+ _c3="$(LC_ALL=C awk -f "$SLA" "$_slf/c/SKILL.md")"; _c4="$(LC_ALL=C awk -f "$SLA" "$_slf/a/SKILL.md" "$_slf/d/SKILL.md")"
2242
+ _c5="$(printf -- '---\nname: hh\ndescription: a\342\200\224b\n---\n' > "$_slf/e.md"; LC_ALL=C awk -f "$SLA" "$_slf/e.md")"
2243
+ rm -rf "$_slf"
2244
+ # 9 and 20 are Claude Code's own readings; ee = 2 + len("onetwo") + 4 (a line break costs nothing, as measured on
2245
+ # 46 shipped skills); ff is not listed; an em dash is one character (hh = 2 + 3 + 4).
2246
+ if [ "$_c1" = "9 1 0" ] && [ "$_c2" = "20 2 0" ] && [ "$_c3" = "12 1 0" ] && [ "$_c4" = "9 1 0" ] && [ "$_c5" = "9 1 0" ]; then
2247
+ pass "skill-listing.awk reproduces Claude Code's counter on its fixtures (9 · 20 · block lines · unlisted · em dash)"
2248
+ else fail "skill-listing.awk drifted from Claude Code's counter: got '$_c1' '$_c2' '$_c3' '$_c4' '$_c5', want '9 1 0' '20 2 0' '12 1 0' '9 1 0' '9 1 0'"; fi
2249
+ read -r SL SLN SLU <<EOF_SL
2250
+ $(LC_ALL=C awk -f "$SLA" "$SKILLS"/*/SKILL.md)
2251
+ EOF_SL
2252
+ [ "${SLU:-0}" = 0 ] && pass "no skill uses a listing shape the count was not measured on (when_to_use, folded description)" \
2253
+ || fail "$SLU skill(s) use when_to_use or a folded description — measure that shape against Claude Code before trusting the listing count"
2254
+ fi
2254
2255
  # The budget GATES Crewforth's payload (kit repo, IS_KIT). In an INSTALLED project the user's own agents/skills —
2255
2256
  # including the ones adopt imports from a taken-over agent — legitimately add to the always-on cost (their choice),
2256
2257
  # so there we REPORT the numbers instead of failing the suite.
@@ -2263,20 +2264,21 @@ done
2263
2264
  #
2264
2265
  # $4 is the number of carriage returns in the same text the budget was measured on, so the two numbers describe
2265
2266
  # the same bytes. A real overrun still says "over budget"; only a CRLF one is renamed.
2266
- bud(){ # $1 name $2 measured $3 budget $4 (optional) carriage returns in the measured text
2267
- if [ "$2" -le "$3" ]; then pass "$1 within budget ($2 ≤ $3 bytes)"
2267
+ bud(){ # $1 name $2 measured $3 budget $4 (optional) carriage returns in the measured text $5 (optional) unit
2268
+ local u="${5:-bytes}"
2269
+ if [ "$2" -le "$3" ]; then pass "$1 within budget ($2 ≤ $3 $u)"
2268
2270
  elif [ "$IS_KIT" = 1 ]; then
2269
2271
  local cr="${4:-0}"
2270
2272
  if [ "$cr" -gt 0 ] && [ $(( $2 - cr )) -le "$3" ]; then
2271
2273
  fail "$1 over budget ONLY because this checkout is CRLF: $2 > $3 bytes, and $cr of those bytes are carriage returns ($(( $2 - cr )) with LF endings, which is within budget). Re-check out the file rather than editing the budget."
2272
2274
  else
2273
- fail "$1 over budget: $2 > $3 bytes"
2275
+ fail "$1 over budget: $2 > $3 $u"
2274
2276
  fi
2275
- else pass "$1 $2 bytes (over Crewforth's $3 baseline — your project's own additions, not gated in an install)"; fi; }
2277
+ else pass "$1 $2 $u (over Crewforth's $3 baseline — your project's own additions, not gated in an install)"; fi; }
2276
2278
  bud "discipline" "$DB" "$BUDGET_DISC" "$DBCR"
2277
2279
  bud "agent descriptions" "$AB" "$BUDGET_AGENTS"
2278
- bud "skill descriptions" "$SB" "$BUDGET_SKILLS"
2279
- echo " always-on total: $((DB+AB+SB)) bytes (budget $((BUDGET_DISC+BUDGET_AGENTS+BUDGET_SKILLS)))"
2280
+ bud "skill listing" "$SL" "$BUDGET_SKILL_LISTING" 0 chars
2281
+ echo " always-on: discipline + agents $((DB+AB)) bytes (budget $((BUDGET_DISC+BUDGET_AGENTS))) · skill listing $SL chars for ${SLN:-0} skills (budget $BUDGET_SKILL_LISTING)"
2280
2282
  # The diagnosis is pinned, not only written, on CRLF copies of this very file: the carriage-return count must cover
2281
2283
  # exactly the measured lines, a copy whose text is exactly at the budget with LF endings must be named as CRLF with its
2282
2284
  # figures, and one a byte past the budget must still read "over budget". All three failed while the count came from
@@ -4026,6 +4028,30 @@ case "$o" in *'"additionalContext"'*SESSION_STATE*) pass "handover present -> in
4026
4028
  *) fail "session-rehydrate did not inject a pointer when SESSION_STATE.md exists" ;; esac
4027
4029
  if [ -n "$JSONQ" ]; then printf '%s' "$o" | json_ok && pass "rehydrate output is valid JSON ($JSONQ)" || fail "rehydrate output is not valid JSON";
4028
4030
  else skip tool "the rehydrate output JSON-validity check (no working jq)"; fi
4031
+ # The install language (RC-2 rehearsal: a bare /crew-doctor in a Turkish install came back in English). The hook
4032
+ # now runs on startup too and tells the model the install language; the handover is still offered only on a
4033
+ # boundary. Six states, each read by what the hook actually printed: a Turkish install gets the language line on
4034
+ # startup and on a boundary; an English install gets none; startup never offers the handover, a boundary does.
4035
+ # JSON validity is asked of Crewforth's own reader, so it holds on a machine without jq.
4036
+ _rh(){ printf '{"hook_event_name":"SessionStart","cwd":"%s","source":"%s"}' "$RHD" "$1" | CLAUDE_PROJECT_DIR= bash "$HOOKS/session-rehydrate.sh" 2>/dev/null; }
4037
+ _rk(){ case "$1" in *'handover'*) printf 'H' ;; esac; case "$1" in *'installed in Turkish'*) printf 'L' ;; esac; }
4038
+ mkdir -p "$RHD/.claude"
4039
+ printf 'lang=tr\n' > "$RHD/.claude/kit.conf"; _o1="$(_rh startup)"; _o2="$(_rh compact)"
4040
+ printf 'lang=en\n' > "$RHD/.claude/kit.conf"; _o3="$(_rh startup)"; _o4="$(_rh compact)"
4041
+ rm -f "$RHD/docs/SESSION_STATE.md"; _o5="$(_rh startup)"
4042
+ printf 'lang=tr\n' > "$RHD/.claude/kit.conf"; _o6="$(_rh clear)"
4043
+ _got="$(_rk "$_o1")/$(_rk "$_o2")/$(_rk "$_o3")/$(_rk "$_o4")/$(_rk "$_o5")/$(_rk "$_o6")"
4044
+ [ "$_got" = "L/HL//H//L" ] && pass "SessionStart names a Turkish install's language on startup and on a boundary; English adds nothing; startup never offers the handover (6 states)" \
4045
+ || fail "session-rehydrate language/handover states read '$_got', want 'L/HL//H//L' (tr startup · tr compact+handover · en startup · en compact+handover · en startup no handover · tr clear no handover)"
4046
+ _rj=0; for _o in "$_o1" "$_o2" "$_o4" "$_o6"; do printf '%s' "$_o" > "$RHD/o.json"; awk -v op=validate -f "$ROOT/eval/lib/settings-json.awk" "$RHD/o.json" 2>/dev/null || _rj=$((_rj+1)); done
4047
+ [ "$_rj" = 0 ] && pass "each language/handover output is valid JSON (Crewforth's reader, 4 of 4)" || fail "$_rj session-rehydrate output(s) are not valid JSON"
4048
+ # Asked of the entry that carries session-rehydrate, not of the file: skill-trust's entry has the same matcher.
4049
+ _rw(){ _n="$(awk -v op=len -v path=hooks.SessionStart -f "$ROOT/eval/lib/settings-json.awk" "$1" 2>/dev/null)"; _i=0
4050
+ while [ "$_i" -lt "${_n:-0}" ]; do _e="$(awk -v op=get -v path="hooks.SessionStart.$_i" -f "$ROOT/eval/lib/settings-json.awk" "$1" 2>/dev/null)"
4051
+ case "$_e" in *session-rehydrate*) case "$_e" in *'"matcher"'*startup*) return 0 ;; esac ;; esac; _i=$((_i+1)); done; return 1; }
4052
+ sed 's/"startup|resume|clear|compact|fork"/"compact|clear|resume|fork"/' "$ROOT/settings.json" > "$RHD/s-twin.json"
4053
+ if _rw "$ROOT/settings.json" && ! _rw "$RHD/s-twin.json"; then pass "settings.json runs session-rehydrate on startup too; a copy wired as before (no startup) is caught"
4054
+ else fail "session-rehydrate is not wired on startup (or the check cannot tell) — a Turkish install's first session would not be told its language"; fi
4029
4055
  rm -rf "$RHD"
4030
4056
  # board-sync builds its JSON with one awk escaper on every machine. The no-jq escaper it replaced handled only the
4031
4057
  # quote, the backslash and the newline, so a TAB in an item title produced JSON the CLI cannot parse and the board
@@ -4148,7 +4174,7 @@ mkdir -p "$DCR/.claude/skills/only-mine" "$DCR/.claude/hooks"
4148
4174
  cp -R "$SKILLS/handoff" "$DCR/.claude/skills/" 2>/dev/null
4149
4175
  printf '# x\n' > "$DCR/.claude/skills/only-mine/SKILL.md"
4150
4176
  cp "$HOOKS"/*.sh "$DCR/.claude/hooks/" 2>/dev/null; chmod +x "$DCR/.claude/hooks/"*.sh 2>/dev/null
4151
- _own(){ ( cd "$DCR" && bash "$ROOT/eval/doctor.sh" 2>&1 | grep -oE '[0-9]+ project-specific skill' | head -1 | cut -d' ' -f1 ); }
4177
+ _own(){ ( cd "$DCR" && CREW_LANG=en bash "$ROOT/eval/doctor.sh" 2>&1 | grep -oE '[0-9]+ project-specific skill' | head -1 | cut -d' ' -f1 ); }
4152
4178
  printf 'skills/handoff\n' > "$DCR/.claude/kit-manifest.txt"; _lf="$(_own)"
4153
4179
  printf 'skills/handoff\r\n' > "$DCR/.claude/kit-manifest.txt"; _crlf="$(_own)"
4154
4180
  [ -n "$_lf" ] && [ "$_lf" = "$_crlf" ] \
@@ -4191,7 +4217,7 @@ bash "$ROOT/eval/doctor.sh" "$DOC" >/dev/null 2>&1 \
4191
4217
  && pass "doctor: CLAUDE.md with the @import -> exit 0" \
4192
4218
  || fail "doctor flagged a CLAUDE.md that DOES import the discipline"
4193
4219
  # Readiness is ADVISORY: a bare project trips every readiness signal, and the verdict must stay exit 0.
4194
- DOUT="$(bash "$ROOT/eval/doctor.sh" "$DOC" 2>&1)"; DRC=$?
4220
+ DOUT="$(CREW_LANG=en bash "$ROOT/eval/doctor.sh" "$DOC" 2>&1)"; DRC=$?
4195
4221
  [ "$DRC" -eq 0 ] && pass "doctor: readiness gaps do NOT change the verdict (advisory)" || fail "readiness gaps changed doctor's exit code — it must stay a statement about the install"
4196
4222
  case "$DOUT" in *"Readiness (advisory"*) pass "doctor prints the readiness block" ;; *) fail "doctor readiness block missing" ;; esac
4197
4223
  case "$DOUT" in *"➖"*) pass "readiness flags gaps on a bare project (devcontainer/MCP/manifest absent)" ;; *) fail "readiness found no gap on a bare project — the signals are not firing" ;; esac
@@ -5468,13 +5494,13 @@ if [ -f "$GR" ]; then
5468
5494
  DTMP="$(mktemp -d)"; mkdir -p "$DTMP/.claude"
5469
5495
  for d in eval hooks skills agents; do [ -d "$ROOT/$d" ] && cp -R "$ROOT/$d" "$DTMP/.claude/$d"; done
5470
5496
  cp "$ROOT/settings.json" "$DTMP/.claude/settings.json" 2>/dev/null
5471
- DOUT="$(cd "$DTMP" && bash .claude/eval/doctor.sh 2>"$DTMP/err")"
5497
+ DOUT="$(cd "$DTMP" && CREW_LANG=en bash .claude/eval/doctor.sh 2>"$DTMP/err")"
5472
5498
  # An install from before 2.5.0 keeps the old `Bash`-only matcher, and nothing in the session looks wrong
5473
5499
  # while every PowerShell command walks past §4.5. doctor has to SAY so, so this drives the downgrade.
5474
5500
  case "$DOUT" in *"watch both Bash and PowerShell"*) pass "doctor confirms the shell matcher covers PowerShell" ;;
5475
5501
  *) fail "doctor did not report on the shell matcher" ;; esac
5476
5502
  sed 's/"Bash|PowerShell"/"Bash"/' "$DTMP/.claude/settings.json" > "$DTMP/s.tmp" && mv "$DTMP/s.tmp" "$DTMP/.claude/settings.json"
5477
- DOUT2="$(cd "$DTMP" && bash .claude/eval/doctor.sh 2>/dev/null)"
5503
+ DOUT2="$(cd "$DTMP" && CREW_LANG=en bash .claude/eval/doctor.sh 2>/dev/null)"
5478
5504
  case "$DOUT2" in *"watch only Bash"*) pass "doctor flags a pre-2.5.0 Bash-only matcher as a failure" ;;
5479
5505
  *) fail "doctor stayed quiet on a Bash-only matcher — the gap is invisible to an upgrader" ;; esac
5480
5506
  grep -q 'command not found' "$DTMP/err" && fail "doctor.sh calls a helper before it is defined (see stderr)" \
@@ -5487,9 +5513,38 @@ if [ -f "$GR" ]; then
5487
5513
  # that has it, so it only ever exercised one of doctor's four branches; CI has no CLI, took the fourth, and
5488
5514
  # failed on a wording difference. Running it both ways is what makes the assertion about doctor rather than
5489
5515
  # about the machine the suite happens to run on.
5490
- DOUT3="$(cd "$DTMP" && PATH=/usr/bin:/bin bash .claude/eval/doctor.sh 2>/dev/null)"
5516
+ DOUT3="$(cd "$DTMP" && CREW_LANG=en PATH=/usr/bin:/bin bash .claude/eval/doctor.sh 2>/dev/null)"
5491
5517
  case "$DOUT3" in *"auto-mode classifier"*) pass "doctor reports the auto-mode line with no claude CLI present" ;;
5492
5518
  *) fail "doctor went silent on auto-mode when the claude CLI is absent" ;; esac
5519
+ # (f) doctor speaks the install's language. A Turkish install ran a Turkish installer and then got an English
5520
+ # doctor (RC-1 rehearsal); the language now comes from the `lang=` start.sh/adopt.sh record in kit.conf. Four
5521
+ # claims, driven on this fixture: the record picks the language with no variable set; no line the doctor
5522
+ # printed fell back to English (CREW_I18N_MISS names each key that had no row); every identifier the English
5523
+ # run printed — a path, a command, a settings key — is still there, untranslated, in the Turkish run; and an
5524
+ # English record gets English. The must-fail twin plants a line with no row in a copy of the doctor and
5525
+ # needs the miss list to name it, so an empty list means "all translated", not "the collector is dead".
5526
+ _dl(){ printf 'lang=%s\n' "$1" > "$DTMP/.claude/kit.conf"; }
5527
+ _dl tr; : > "$DTMP/miss"
5528
+ DTR="$(cd "$DTMP" && env -u CREW_LANG CREW_I18N_MISS="$DTMP/miss" bash .claude/eval/doctor.sh 2>/dev/null)"
5529
+ _dl en; DEN="$(cd "$DTMP" && env -u CREW_LANG bash .claude/eval/doctor.sh 2>/dev/null)"
5530
+ case "$DTR" in *"kurulum denetimi"*) pass "doctor speaks Turkish when the install recorded lang=tr (no variable set)" ;;
5531
+ *) fail "doctor ignored lang=tr in kit.conf — a Turkish install gets an English doctor" ;; esac
5532
+ case "$DEN" in *"install doctor"*) pass "doctor speaks English when the install recorded lang=en" ;;
5533
+ *) fail "doctor did not speak English for lang=en" ;; esac
5534
+ [ ! -s "$DTMP/miss" ] && pass "every line the Turkish doctor printed has a translation (miss list empty)" \
5535
+ || fail "the Turkish doctor fell back to English for: $(tr '\n' '|' < "$DTMP/miss")"
5536
+ _idm=""; _idn=0
5537
+ for _id in $(printf '%s\n' "$DEN" | grep -oE '\.claude/[A-Za-z0-9_./*-]+|settings\.json|core\.hooksPath|/crew-[a-z-]+|npx crewforth [a-z]+|skillListingBudgetFraction|PreToolUse|UserPromptSubmit|SessionStart' | sort -u); do
5538
+ _idn=$((_idn + 1)); case "$DTR" in *"$_id"*) ;; *) _idm="$_idm $_id" ;; esac
5539
+ done
5540
+ if [ "$_idn" -lt 5 ]; then fail "identifier check read only $_idn identifier(s) from the English doctor — the extractor is broken, not the doctor"
5541
+ elif [ -z "$_idm" ]; then pass "the Turkish doctor keeps all $_idn identifiers of the English one untranslated (paths, commands, keys)"
5542
+ else fail "the Turkish doctor lost or translated identifier(s):$_idm"; fi
5543
+ sed 's/^_mt "== Crewforth — install doctor =="; echo "$_M"$/&; ok "planted line with no translation"/' "$DTMP/.claude/eval/doctor.sh" > "$DTMP/.claude/eval/doctor-twin.sh"
5544
+ _dl tr; : > "$DTMP/miss2"
5545
+ ( cd "$DTMP" && env -u CREW_LANG CREW_I18N_MISS="$DTMP/miss2" bash .claude/eval/doctor-twin.sh >/dev/null 2>&1 )
5546
+ grep -qx 'planted line with no translation' "$DTMP/miss2" && pass "twin: a planted untranslated line is named by the miss list" \
5547
+ || fail "twin: the miss list did not name a planted untranslated line — the empty list above proves nothing"
5493
5548
  rm -rf "$DTMP" "$GTMP"
5494
5549
  else
5495
5550
  fail "eval/gate-report.sh missing from the payload"
@@ -6237,10 +6292,12 @@ if [ "$IS_KIT" = 1 ]; then
6237
6292
  command -v node >/dev/null 2>&1 && node "$KR/bin/cli.js" --help; } > "$_kd/out.txt" 2>&1
6238
6293
  _kc="$(kw_lines "$_kd/out.txt" | sed "s|^$_kd/||")"; _kn="$(grep -c . "$_kd/out.txt")"
6239
6294
  # Twins. Must fail: a start.sh whose table says "full kit" again, and output that says "installing Crewforth.".
6240
- # Must pass: output that only names paths — .claude/kit.conf, kit/ deleted, kit-manifest.txt, drizzle-kit, KIT_X.
6295
+ # Must pass: output that only names paths — .claude/kit.conf, the folder in the archive's own error, kit-manifest.txt,
6296
+ # drizzle-kit, KIT_X. ("kit/ deleted" used to sit here as a path; it was the last line of every install, and the
6297
+ # RC-1 rehearsal read it as the old word. The closing lines are held to it separately below.)
6241
6298
  sed 's/"full install") s=/"full kit") s=/' "$KR/start.sh" > "$_kd/mut-start.sh"
6242
6299
  _ok=kit; printf 'Done: installing the %s.\nKurulum: %sin dosyaları hazır\n' "$_ok" "$_ok" > "$_kd/bad.txt"
6243
- printf 'wrote .claude/kit.conf\nkit/ deleted\nsee .claude/kit-manifest.txt and drizzle-kit status\nKIT_X=1\n' > "$_kd/good.txt"
6300
+ printf 'wrote .claude/kit.conf\nERROR: kit/ not found\nsee .claude/kit-manifest.txt and drizzle-kit status\nKIT_X=1\n' > "$_kd/good.txt"
6244
6301
  if [ -n "$_ka$_kb$_kc" ]; then fail "a printed line still says kit — say Crewforth:
6245
6302
  $(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/||; s|^| |")"
6246
6303
  elif [ "$_kn" -lt 30 ]; then fail "FIXTURE: the captured help and preflight output is only $_kn line(s) — the run broke, not the wording"
@@ -6248,6 +6305,16 @@ $(printf '%s\n%s\n%s\n' "$_ka" "$_kb" "$_kc" | grep . | head -n 6 | sed "s|$KR/|
6248
6305
  elif [ "$(kw_lines "$_kd/bad.txt" | grep -c .)" != 2 ]; then fail "Crewforth-word matcher missed a planted old-name word (English or Turkish) in the output"
6249
6306
  elif [ -n "$(kw_lines "$_kd/good.txt")" ]; then fail "Crewforth-word matcher flagged a path or identifier: $(kw_lines "$_kd/good.txt" | head -n 2 | tr '\n' ' ')"
6250
6307
  else pass "no printed line says kit: terminal-only scripts, the installers' tables/echo/usage and cli --help, plus $_kn lines of real help and preflight output (EN+TR); a reverted table entry and planted words are caught, paths are not"; fi
6308
+ # The closing line of an install is the one line every user reads. It may name no kit at all, not even as a path:
6309
+ # the key and its Turkish value are both read, and a planted "kit/ deleted" in a copy must be caught.
6310
+ _dn(){ grep -hE '"Done\.|'"'"'Done\.' "$@" 2>/dev/null | grep -iE '(^|[^a-z_.-])kit([^a-z_-]|$)'; }
6311
+ if [ ! -f "$SGR/start.sh" ]; then :
6312
+ elif [ -n "$(_dn "$SGR/start.sh" "$SGR/adopt.sh")" ]; then fail "an installer's closing line still names kit: $(_dn "$SGR/start.sh" "$SGR/adopt.sh" | head -n 1 | cut -c1-120)"
6313
+ else
6314
+ sed 's/the installer files are removed\./kit\/ deleted./' "$SGR/start.sh" > "$_kd/done-twin.sh"
6315
+ [ -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" \
6316
+ || fail "twin: the closing-line check missed a planted 'kit/ deleted' — it reads nothing"
6317
+ fi
6251
6318
  rm -rf "$_kd"
6252
6319
  else
6253
6320
  skip scope "printed-text wording check skipped (installed project — the installers live in the source repository)"
@@ -1390,7 +1390,9 @@ if git_has "$CMD" 'commit|push'; then
1390
1390
  # §4.4 branch guard: committing straight onto main/master is not blocked (a fresh project legitimately
1391
1391
  # lives on main), but it is surfaced in the approval prompt so the user can send it to a branch instead.
1392
1392
  BRANCH_WARN=""
1393
- case "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" in
1393
+ # symbolic-ref names the branch even before its first commit, where rev-parse answers "HEAD" and the very
1394
+ # first commit onto main went by without this line. A detached HEAD reads empty: not the default branch.
1395
+ case "$(git symbolic-ref --short -q HEAD 2>/dev/null)" in
1394
1396
  main|master) BRANCH_WARN="⚠️ This commits DIRECTLY to the default branch. Prefer a feature branch unless you meant to.
1395
1397
 
1396
1398
  " ;;
@@ -8,8 +8,14 @@
8
8
  # - SessionStart is the right event: its stdout / hookSpecificOutput.additionalContext is injected into the
9
9
  # model's context (docs: "Re-inject context after compaction"). PreCompact is NOT used — it can only block
10
10
  # compaction, not add instructions to the summary, and blocking is not what we want.
11
- # - Matched on compact|clear|resume, NOT startup: those three are explicit "continue where I left off" boundaries.
12
- # startup fires on every unrelated session open and would keep surfacing a stale handover — that would nag.
11
+ # - The HANDOVER is offered on compact|clear|resume|fork only, NOT on startup: those are explicit "continue where I
12
+ # left off" boundaries, while startup fires on every unrelated session open and would keep surfacing a stale
13
+ # handover — that would nag. The hook itself also runs on startup, for the language line below; it reads the
14
+ # payload's `source` to tell the two apart (no `source` -> treated as a boundary, the pre-3.0 payload shape).
15
+ # - THE LANGUAGE LINE. A project installed in Turkish (`lang=tr` in .claude/kit.conf) is told so at every session
16
+ # start: reply in Turkish unless the user writes in another language. The RC-2 rehearsal typed a bare
17
+ # `/crew-doctor` in a Turkish install: the doctor printed Turkish and the summary came back in English, because a
18
+ # bare command has no word to take the language from. An English install adds nothing (English is the default).
13
19
  # - CLAUDE.md itself reloads on /compact and /clear on its own, so this hook does NOT re-inject the discipline —
14
20
  # only the session-specific state the reload can't recover.
15
21
  # - Fails OPEN and SILENT: no handover file, no output. It never blocks the session (always exits 0).
@@ -28,13 +34,26 @@ fi
28
34
  # Undo the JSON escaping first (`\\`), then fold any lone separator. Both are no-ops on a POSIX path.
29
35
  ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
30
36
 
31
- STATE="$ROOT/docs/SESSION_STATE.md"
32
- [ -s "$STATE" ] || exit 0 # no (non-empty) handover → nothing to rehydrate, stay silent
37
+ SRC=""
38
+ case "$IN" in *'"source"'*) SRC="${IN#*\"source\"}"; SRC="${SRC#*\"}"; SRC="${SRC%%\"*}" ;; esac
39
+
40
+ LANGMSG=""
41
+ if [ -f "$ROOT/.claude/kit.conf" ]; then
42
+ while IFS= read -r _l || [ -n "$_l" ]; do
43
+ case "${_l%$'\r'}" in lang=tr) LANGMSG="This project was installed in Turkish (lang=tr in .claude/kit.conf). Reply in Turkish unless the user writes in another language, including after a bare command such as /crew-doctor. Commands, paths, file names and identifiers stay as written." ;; esac
44
+ done < "$ROOT/.claude/kit.conf"
45
+ fi
33
46
 
47
+ MSG=""
48
+ STATE="$ROOT/docs/SESSION_STATE.md"
49
+ if [ "$SRC" != startup ] && [ -s "$STATE" ]; then
34
50
  MSG="A session handover from before this context boundary exists at docs/SESSION_STATE.md. If the user's request continues that work, read it first — it holds the in-progress task state, open decisions, and the intended next step — and do not restart from scratch. If the request is about something else, do not bring it up."
51
+ fi
52
+ [ -n "$LANGMSG" ] && MSG="${MSG:+$MSG }$LANGMSG"
53
+ [ -n "$MSG" ] || exit 0 # no handover to offer and nothing to say about language -> stay silent
35
54
 
36
55
  # hookSpecificOutput.additionalContext is the documented channel that injects text into the model's context.
37
- # ONE PATH, no jq: MSG is a fixed constant with no quote, backslash, control character or newline, so it needs no
56
+ # ONE PATH, no jq: MSG is built from fixed constants with no quote, backslash, control character or newline, so it needs no
38
57
  # escaping and every machine emits the same bytes. If you ever make MSG dynamic, escape it the way board-sync.sh
39
58
  # does (jq-identical, measured) — a raw quote or tab here makes JSON the CLI silently drops.
40
59
  printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$MSG"
@@ -21,11 +21,15 @@
21
21
  # never fail to start because of this.
22
22
  set -uo pipefail
23
23
  HERE="$(cd "$(dirname "$0")" && pwd)"
24
- MODE=""
25
- case "${1:-}" in --trust) MODE=trust ;; esac
24
+ MODE=""; ONE=""
25
+ # --trust-one <skills/name | agents/name.md>: record ONE component as accepted, and nothing else. The updater uses it
26
+ # for the skill it turned into a project skill itself (cqrs-aop-module, which Crewforth shipped until 3.0): flagging
27
+ # the updater's own move as unvetted was the first thing an updated session said (RC-1 rehearsal). --trust would
28
+ # have accepted every other foreign component too, including ones the user never looked at.
29
+ case "${1:-}" in --trust) MODE=trust ;; --trust-one) MODE=one; ONE="${2:-}" ;; esac
26
30
 
27
31
  IN=""
28
- [ ! -t 0 ] && [ "$MODE" != trust ] && IN="$(cat 2>/dev/null || true)"
32
+ [ ! -t 0 ] && [ -z "$MODE" ] && IN="$(cat 2>/dev/null || true)"
29
33
  ROOT="${CLAUDE_PROJECT_DIR:-}"
30
34
  [ -n "$ROOT" ] || ROOT="$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
31
35
  [ -n "$ROOT" ] || ROOT="$PWD"
@@ -58,6 +62,15 @@ digest(){
58
62
  else cksum "$1" 2>/dev/null | tr -s ' ' | cut -d' ' -f1,2 | tr ' ' '-'; fi
59
63
  }
60
64
 
65
+ if [ "$MODE" = one ]; then
66
+ case "$ONE" in skills/*/*|agents/*/*) exit 1 ;; skills/?*) p="$CL/$ONE/SKILL.md" ;; agents/?*.md) p="$CL/$ONE" ;; *) exit 1 ;; esac
67
+ [ -f "$p" ] || exit 1
68
+ dg="$(digest "$p")"; [ -n "$dg" ] || exit 1
69
+ [ -f "$TRUST" ] || printf '# Components reviewed and accepted by the user. Regenerate with: bash skill-trust.sh --trust\n' > "$TRUST" 2>/dev/null || exit 1
70
+ grep -qxF "$dg $ONE" "$TRUST" 2>/dev/null || printf '%s %s\n' "$dg" "$ONE" >> "$TRUST"
71
+ exit 0
72
+ fi
73
+
61
74
  # The component list: a skill is its SKILL.md, an agent is its file. Only those Crewforth does NOT ship —
62
75
  # without a manifest we cannot tell kit-owned from project-owned, so we stay silent rather than guess.
63
76
  [ -f "$MAN" ] || exit 0
package/kit/settings.json CHANGED
@@ -93,7 +93,7 @@
93
93
  ],
94
94
  "SessionStart": [
95
95
  {
96
- "matcher": "compact|clear|resume|fork",
96
+ "matcher": "startup|resume|clear|compact|fork",
97
97
  "hooks": [
98
98
  {
99
99
  "type": "command",
@@ -27,10 +27,20 @@ Verify Crewforth is actually *active* in this project (not just present on disk)
27
27
  it prints once per Crewforth version, and it is meant for the user, not for you.
28
28
 
29
29
  If `.claude/eval/doctor.sh` doesn't exist, this is not a full (start.sh / adopt.sh) install — Crewforth is likely
30
- running as a **plugin**, whose hooks are managed by Claude Code itself; there's nothing for the doctor to check.
30
+ running as a **plugin**, whose hooks are managed by Claude Code itself; there's nothing for the doctor to check,
31
+ with one exception: the skill listing, which a plugin cannot size for itself (its settings cannot set
32
+ `skillListingBudgetFraction`). Count it the way the full install's doctor does:
33
+ `LC_ALL=C awk -f "${CLAUDE_PLUGIN_ROOT}/eval/lib/skill-listing.awk" "${CLAUDE_PLUGIN_ROOT}"/skills/*/SKILL.md`
34
+ (prints characters, then skills). At the default fraction a 200,000-token model has 8,000 characters for the whole
35
+ listing, and Claude Code's own skills take about 5,900 of them (measured on v2.1.282). If Crewforth's number plus
36
+ 5,900 is over 8,000, tell the user plainly: on such a model the least-used skills lose their descriptions and stop
37
+ being picked; one line in `~/.claude/settings.json` fixes it — `"skillListingBudgetFraction": 0.04`; the cost is
38
+ that the listing is then sent whole every turn, about (that total ÷ 4) tokens, (tokens ÷ 2,000)% of a 200k window.
39
+ It is their call — show the line and the cost, do not edit their settings. On a 1,000,000-token model the default
40
+ already fits.
31
41
 
32
42
  **The eval scripts are installer-only, by decision.** `eval/` — `doctor.sh`, `smoke-test.sh`, `routing-eval.sh`,
33
43
  `scan-skill.sh`, `utilization.sh` — ships with `start.sh` and `adopt.sh` and NOT with the plugin edition. The eval
34
44
  scripts are developer instruments: they inspect an installation from outside it, and the plugin edition has no
35
- installation to inspect. Say this plainly when someone asks why `/crew-doctor` reports nothing on a plugin install,
45
+ installation to inspect. Say this plainly when someone asks why `/crew-doctor` reports only the listing on a plugin install,
36
46
  rather than treating it as a defect. The Studio panel ships in both editions (`/crew-studio`).
@@ -14,10 +14,13 @@ Run the change set through the review trio (read-only). **The audits go out in p
14
14
  - @agent-crew-privacy-agent (`privacy-compliance`) — **when the diff touches personal data**: legal basis,
15
15
  minimisation, retention, transfer.
16
16
  2. (if SonarQube is in use) **sonarqube-check** — 0/0/0/0 gate (language-agnostic).
17
- 3. **Last, and only once 1-2 are clean:** @agent-crew-review-agent (code-review) — "does it improve overall code
18
- health"; severity-ranked comments. It is the closing reviewer in every writing agent's Coordination ("at
19
- closure, report findings to crew-review-agent"), so it reads a diff the audits have already cleared — not the
20
- other way round.
17
+ 3. **Last, once 1-2 leave no blocker:** @agent-crew-review-agent (crew-code-review) — "does it improve overall
18
+ code health": the plan (what changed, the risks), then findings with a severity and a category. "Clean" means
19
+ what crew-code-review means by it: **no critical or high finding open**. A medium or low audit finding does not
20
+ hold the reviewer back; hand it over in the reviewer's prompt and keep it in the report. A critical or high one
21
+ stops here: report it, leave the fix to its owner, and run the reviewer on the fixed diff. It is the closing
22
+ reviewer in every writing agent's Coordination ("at closure, report findings to crew-review-agent"), so it
23
+ reads a diff the audits have already cleared of blockers — not the other way round.
21
24
 
22
25
  Each agent returns a **short summary** to the main thread; raw output goes to `docs/` if needed. Do NOT modify code;
23
26
  collect findings in severity order, and leave the fix to the relevant expert.
@@ -43,7 +43,8 @@ Skipping one of these is how a component ships half-installed:
43
43
  `node packaging/gen-diagrams.mjs` writes `assets/`, and smoke fails while any of them is stale.
44
44
  4. **Plugin edition** *(Crewforth repository only)*. `bash packaging/build-plugin.sh` — in the same commit, or the
45
45
  release stops at the sync gate.
46
- 5. **Budget.** A new skill moves `BUDGET_SKILLS`; raise it in the same commit with the justification comment the
46
+ 5. **Budget.** A new skill moves `BUDGET_SKILL_LISTING` (the skill listing in characters, counted like Claude Code
47
+ counts it; `/crew-doctor` reports the same number). Raise it in the same commit with the justification comment the
47
48
  file's convention requires. Never raise it to make a red gate green without saying why.
48
49
 
49
50
  ## 4. Prove it — run all four, in this order
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewforth",
3
- "version": "3.0.0-rc.1",
3
+ "version": "3.0.0-rc.3",
4
4
  "description": "Crewforth — your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
5
5
  "bin": {
6
6
  "crewforth": "bin/cli.js"
package/start.sh CHANGED
@@ -123,16 +123,16 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
123
123
  "no effect:") s='etkisi yok:' ;;
124
124
  "Installing:") s='Kuruluyor:' ;;
125
125
  "Tip: open Claude Code and run /crew-doctor — it checks the install is wired (hooks executable, core.hooksPath set, discipline imported) and scores the project's readiness. CLAUDE.md loads the discipline every session.") s="İpucu: Claude Code'u açıp /crew-doctor çalıştırın — kurulumun eksiksiz bağlandığını denetler (hook'lar çalıştırılabilir mi, core.hooksPath ayarlı mı, disiplin import edilmiş mi) ve projenin ne kadar hazır olduğunu puanlar. Disiplin, CLAUDE.md sayesinde her oturumda yüklenir." ;;
126
- "%s agents, %s skills installed.") s="%s ajan ve %s skill kuruldu." ;;
126
+ "%s agents, %s skills and %s commands installed.") s="%s ajan, %s skill ve %s komut kuruldu." ;;
127
127
  ".claude/DISCIPLINE.md written — owned by Crewforth; an update overwrites it, so keep your own rules out of it.") s=".claude/DISCIPLINE.md yazıldı. Bu dosya Crewforth'a ait ve her güncellemede yeniden yazılır; kendi kurallarınızı buraya eklemeyin." ;;
128
128
  "./CLAUDE.md created — EDIT the project section.") s='./CLAUDE.md oluşturuldu — proje bölümünü sizin DOLDURMANIZ gerekiyor.' ;;
129
129
  "trace scan: core.hooksPath -> .claude/hooks (§4.1/§4.2 commit gate active)") s='iz taraması: core.hooksPath -> .claude/hooks (§4.1/§4.2 commit kapısı açık)' ;;
130
- "Done. ./.claude + ./CLAUDE.md ready (full install); kit/ deleted.") s='Tamamlandı. ./.claude ve ./CLAUDE.md hazır (tam kurulum); kit/ silindi.' ;;
130
+ "Done. ./.claude + ./CLAUDE.md ready (full install); the installer files are removed.") s='Tamamlandı. ./.claude ve ./CLAUDE.md hazır (tam kurulum); kurulum dosyaları silindi.' ;;
131
131
  "Next: 1) fill in the CLAUDE.md project section 2) open Claude Code at the repo root") s="Sıradaki adımlar: 1) CLAUDE.md'deki proje bölümünü doldurun 2) Claude Code'u deponun kökünde açın" ;;
132
132
  "Note: if Claude Code is ALREADY running here, restart it — CLAUDE.md and the discipline load at session start.") s='Not: Claude Code bu klasörde ZATEN açıksa yeniden başlatın — CLAUDE.md ve disiplin oturum açılırken yüklenir.' ;;
133
133
  "Panel: /crew-studio opens the Studio panel from this project (or: node .claude/studio/server/index.js --open).") s='Panel: /crew-studio komutu Studio panelini bu projeden açar (alternatif: node .claude/studio/server/index.js --open).' ;;
134
134
  "— backend + web + mobile (RN/Expo), every agent and skill") s="— backend, web ve mobil (RN/Expo); tüm ajanlar ve skill'ler" ;;
135
- "%s agents · %s skills will be installed") s='%s ajan · %s skill' ;;
135
+ "%s agents · %s skills · %s commands will be installed") s='%s ajan · %s skill · %s komut' ;;
136
136
  "(shared: .claude/ and CLAUDE.md stay committable)") s="(paylaşımlı: .claude/ ve CLAUDE.md commit'lenebilir kalır)" ;;
137
137
  "(default — pass --shared to commit .claude/ and CLAUDE.md)") s="(varsayılan — .claude/ ve CLAUDE.md'yi commit'lemek için --shared verin)" ;;
138
138
  "no") s='hayır' ;;
@@ -548,13 +548,28 @@ count_installed() { # $1=glob -> count to install
548
548
  printf '%s' "$n"
549
549
  }
550
550
  N_AG="$(count_installed "$SRC/agents/*.md")"
551
- N_SK="$(count_installed "$SRC/skills/*/")"
551
+ # Skills and commands the way the README and crewforth.com count them, so the install does not print a third number:
552
+ # a skill whose metadata says `kind: command` is a command, one marked `experimental: true` is neither (it is
553
+ # installed and works when typed, but nothing advertises it). Read from the frontmatter with builtins only.
554
+ count_showcase() { # $1 = a skills directory -> N_SKL N_CMD
555
+ N_SKL=0; N_CMD=0; local f l k e d
556
+ for f in "$1"/*/SKILL.md; do
557
+ [ -f "$f" ] || continue; k=0; e=0; d=0
558
+ while IFS= read -r l || [ -n "$l" ]; do
559
+ l="${l%$'\r'}"
560
+ case "$l" in ---) d=$((d+1)); [ "$d" -ge 2 ] && break ;; " kind: command") k=1 ;; " experimental: true") e=1 ;; esac
561
+ done < "$f"
562
+ [ "$e" = 1 ] && continue
563
+ if [ "$k" = 1 ]; then N_CMD=$((N_CMD+1)); else N_SKL=$((N_SKL+1)); fi
564
+ done
565
+ }
566
+ count_showcase "$SRC/skills"
552
567
 
553
568
  h1 '[2/2] Summary · see what will be installed before you confirm'
554
569
  echo
555
570
  _mt 'full install'; _a="$_M"; _mt '— backend + web + mobile (RN/Expo), every agent and skill'
556
571
  row 'Scope' "${B}${_a}${D} ${_M}${R}"
557
- _mt '%s agents · %s skills will be installed' "${MG}${B}${N_AG}${R}" "${MG}${B}${N_SK}${R}"
572
+ _mt '%s agents · %s skills · %s commands will be installed' "${MG}${B}${N_AG}${R}" "${MG}${B}${N_SKL}${R}" "${MG}${B}${N_CMD}${R}"
558
573
  row 'Included' "$_M"
559
574
  _mt 'stack-agnostic — the stack comes from CLAUDE.md ## Stack or the repo (backend-architecture)'
560
575
  row 'Backend pattern' "$_M"
@@ -607,7 +622,8 @@ cp -R "$SRC/eval/." .claude/eval/ 2>/dev/null || true
607
622
  # outside the payload, because kit/ ships whole and 104 KB of test code
608
623
  # would travel through all four channels only to be removed on arrival.
609
624
  cp -R "$SRC/studio/." .claude/studio/ 2>/dev/null || true
610
- { _mt "%s agents, %s skills installed." "$(ls .claude/agents/*.md 2>/dev/null | wc -l | tr -d ' ')" "$(ls -d .claude/skills/*/ 2>/dev/null | wc -l | tr -d ' ')"; echo " ${_M}"; }
625
+ count_showcase .claude/skills
626
+ { _mt "%s agents, %s skills and %s commands installed." "$(ls .claude/agents/*.md 2>/dev/null | wc -l | tr -d ' ')" "$N_SKL" "$N_CMD"; echo " ${_M}"; }
611
627
  [ -f "$SRC/settings.json" ] && cp "$SRC/settings.json" .claude/settings.json
612
628
  [ -f "$HERE/VERSION" ] && cp "$HERE/VERSION" .claude/VERSION # make the kit version trackable in the installed project
613
629
  # Glob form so every shipped hook/eval is made executable — including ones added later (guard-write.sh,
@@ -721,7 +737,7 @@ else
721
737
  fi
722
738
  rm -rf "$SRC"
723
739
  echo
724
- { _mt 'Done. ./.claude + ./CLAUDE.md ready (full install); kit/ deleted.'; echo "== ${_M} =="; }
740
+ { _mt 'Done. ./.claude + ./CLAUDE.md ready (full install); the installer files are removed.'; echo "== ${_M} =="; }
725
741
  { _mt 'Next: 1) fill in the CLAUDE.md project section 2) open Claude Code at the repo root'; echo "${_M}"; }
726
742
  { _mt 'Note: if Claude Code is ALREADY running here, restart it — CLAUDE.md and the discipline load at session start.'; echo "${_M}"; }
727
743
  { _mt "Tip: open Claude Code and run /crew-doctor — it checks the install is wired (hooks executable, core.hooksPath set, discipline imported) and scores the project's readiness. CLAUDE.md loads the discipline every session."; echo "${_M}"; }