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.
@@ -18,22 +18,164 @@ cd "$ROOT" 2>/dev/null || { echo "doctor: cannot enter '$ROOT'"; exit 2; }
18
18
  exec </dev/null
19
19
 
20
20
  FAIL=0
21
- ok(){ echo " ✅ $1"; }
22
- bad(){ echo " ❌ $1"; echo " ↳ fix: $2"; FAIL=$((FAIL+1)); }
23
- warn(){ echo " ⚠️ $1"; }
21
+ # Every reporter takes a FORMAT and its arguments, and the format is the key of the message table below: a value
22
+ # (a path, a count, a setting's name) goes in as an argument, so the sentence around it can be translated whole.
23
+ ok(){ _mt "$@"; echo " ✅ $_M"; }
24
+ # bad MESSAGE FIX [arguments for MESSAGE]. The fix is a key of its own and takes no arguments; a fix that ends in
25
+ # something untranslatable (a command, a list of names) puts it in BAD_TAIL, which is printed after it once.
26
+ bad(){ local _m="$1" _x="$2"; shift 2; _mt "$_m" "$@"; echo " ❌ $_M"; _mt "fix: "; local _f="$_M"; _mt "$_x"
27
+ echo " ↳ $_f$_M${BAD_TAIL:-}"; BAD_TAIL=""; FAIL=$((FAIL+1)); }
28
+ warn(){ _mt "$@"; echo " ⚠️ $_M"; }
24
29
  # `skip` lives up here with the other reporters, not down in the readiness block where it used to be
25
30
  # defined: the health checks call it too, and a helper defined after its first caller is a silent
26
31
  # no-op — the line never prints and the run shows `skip: command not found` on stderr, where nobody
27
32
  # looks. Found by RUNNING doctor against a fixture install; grepping for the call sites said wired.
28
- skip(){ echo " · $1"; }
33
+ skip(){ _mt "$@"; echo " · $_M"; }
29
34
 
30
- echo "== Crewforth — install doctor =="
35
+ # ---- CREW-I18N ------------------------------------------------------------------------------------------
36
+ # The doctor speaks the language the project was installed in: CREW_LANG when it is set, otherwise the `lang=` that
37
+ # start.sh / adopt.sh recorded in .claude/kit.conf, otherwise the locale. Same contract as the installers: the
38
+ # English string is the key, a missing translation prints English, and names are never translated — commands,
39
+ # paths, file names, settings keys and hook events are identifiers, only the prose around them is.
40
+ case "${CREW_LANG:-}" in tr|en) ;; *)
41
+ CREW_LANG=""
42
+ if [ -f .claude/kit.conf ]; then
43
+ while IFS= read -r _l || [ -n "$_l" ]; do case "$_l" in lang=*) CREW_LANG="${_l#lang=}"; CREW_LANG="${CREW_LANG%$'\r'}" ;; esac; done < .claude/kit.conf
44
+ fi
45
+ case "$CREW_LANG" in tr|en) ;; *)
46
+ _loc="${LC_ALL:-}"; [ -n "$_loc" ] || _loc="${LC_MESSAGES:-}"; [ -n "$_loc" ] || _loc="${LANG:-}"
47
+ case "$_loc" in tr*|TR*) CREW_LANG=tr ;; *) CREW_LANG=en ;; esac ;;
48
+ esac ;;
49
+ esac
50
+ export CREW_LANG # preflight.sh, which the doctor runs below, reads it and speaks the same language
51
+ _mt() {
52
+ # An empty key must still ASSIGN: bash 3.2's `printf -v _M ""` leaves _M holding the previous translation.
53
+ [ -n "${1:-}" ] || { _M=""; return 0; }
54
+ local s="$1"; shift
55
+ if [ "$CREW_LANG" = tr ]; then
56
+ case "$s" in
57
+ '== Crewforth — install doctor ==') s='== Crewforth — kurulum denetimi ==' ;;
58
+ 'fix: ') s='çözüm: ' ;;
59
+ "no .claude/ in '%s' — is Crewforth installed here?") s="'%s' içinde .claude/ yok — Crewforth burada kurulu mu?" ;;
60
+ 'VERSION present (%s)') s='VERSION var (%s)' ;;
61
+ 'VERSION missing') s='VERSION yok' ;;
62
+ 'reinstall or update Crewforth (npx crewforth update)') s="Crewforth'u yeniden kurun ya da güncelleyin (npx crewforth update)" ;;
63
+ 'Crewforth v%s installed, v%s published — update with /crew-update') s='Crewforth v%s kurulu, v%s yayında — /crew-update ile güncelleyin' ;;
64
+ 'required git hooks present (pre-commit, commit-msg)') s="gerekli git hook'ları yerinde (pre-commit, commit-msg)" ;;
65
+ 'MISSING git hook(s):%s — the commit trace/secret scan is absent') s="EKSİK git hook'u:%s — commit iz ve gizli bilgi taraması yok" ;;
66
+ 'reinstall or update Crewforth') s="Crewforth'u yeniden kurun ya da güncelleyin" ;;
67
+ 'all hooks are executable') s="tüm hook'lar çalıştırılabilir" ;;
68
+ 'not executable:%s') s='çalıştırılabilir değil:%s' ;;
69
+ 'guard-bash.sh did NOT block a force-push — the §4.5 gate is neutered/disarmed') s="guard-bash.sh force-push'u ENGELLEMEDİ — §4.5 kapısı etkisiz" ;;
70
+ 'restore guard-bash.sh from Crewforth (npx crewforth update)') s="guard-bash.sh'yi Crewforth'tan geri yükleyin (npx crewforth update)" ;;
71
+ 'guard-bash.sh blocks a force-push (gate live, not neutered)') s="guard-bash.sh force-push'u engelliyor (kapı canlı, etkisiz değil)" ;;
72
+ 'guard-bash.sh enforces the §4.6 review gate (gate live, not neutered)') s='guard-bash.sh §4.6 review kapısını uyguluyor (kapı canlı, etkisiz değil)' ;;
73
+ 'guard-bash.sh did NOT enforce §4.6 — a commit can land with no review of its diff') s="guard-bash.sh §4.6'yı UYGULAMADI — diff'i review edilmemiş bir commit geçebilir" ;;
74
+ 'restore guard-bash.sh from Crewforth (and check crew-review-agent still writes .claude/review-pass.json)') s="guard-bash.sh'yi Crewforth'tan geri yükleyin (ve crew-review-agent'ın .claude/review-pass.json'u hâlâ yazdığını kontrol edin)" ;;
75
+ 'core.hooksPath -> %s (commit-time gates active)') s='core.hooksPath -> %s (commit anındaki kapılar devrede)' ;;
76
+ 'core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE') s='core.hooksPath ayarlı değil — commit iz, gizli bilgi ve şişkinlik kapıları DEVRE DIŞI' ;;
77
+ "core.hooksPath -> %s (not Crewforth's hooks)") s="core.hooksPath -> %s (Crewforth'un hook'ları değil)" ;;
78
+ 'not a git repo — commit gates need: git init && git config core.hooksPath .claude/hooks') s='git deposu değil — commit kapıları için: git init && git config core.hooksPath .claude/hooks' ;;
79
+ "Crewforth's JSON reader is missing (%s) — settings.json cannot be checked") s="Crewforth'un JSON okuyucusu yok (%s) — settings.json denetlenemiyor" ;;
80
+ 'update Crewforth') s="Crewforth'u güncelleyin" ;;
81
+ 'settings.json is valid JSON') s='settings.json geçerli JSON' ;;
82
+ 'settings.json wires PreToolUse / UserPromptSubmit / Stop (non-empty)') s='settings.json PreToolUse / UserPromptSubmit / Stop olaylarını bağlıyor (boş değil)' ;;
83
+ "settings.json hook events empty or missing:%s — those gates won't fire") s="settings.json'da boş ya da eksik hook olayları:%s — bu kapılar çalışmaz" ;;
84
+ 'restore settings.json from Crewforth (npx crewforth update)') s="settings.json'u Crewforth'tan geri yükleyin (npx crewforth update)" ;;
85
+ 'SessionStart not wired — session rehydration after /compact or /clear is inactive (update Crewforth)') s="SessionStart bağlı değil — /compact ya da /clear sonrasında oturum toparlanmıyor (Crewforth'u güncelleyin)" ;;
86
+ 'SessionStart wired (session rehydration active)') s='SessionStart bağlı (oturum toparlama devrede)' ;;
87
+ 'settings.json is invalid JSON') s='settings.json geçersiz JSON' ;;
88
+ "fix the syntax by hand — restoring Crewforth's file would drop any hooks you added") s="sözdizimini elle düzeltin — Crewforth'un dosyasını geri yüklemek eklediğiniz hook'ları siler" ;;
89
+ 'settings.json wires hooks through the %s placeholder — on Windows its separators are stripped before bash runs, so NO hook launches and every gate is silently absent') s="settings.json hook'ları %s yer tutucusuyla bağlıyor — Windows'ta ayırıcılar bash çalışmadan silinir, bu yüzden HİÇBİR hook başlamaz ve bütün kapılar sessizce yok olur" ;;
90
+ 'update Crewforth (npx crewforth adopt) — hook commands become:') s="Crewforth'u güncelleyin (npx crewforth adopt) — hook komutları şu olur:" ;;
91
+ 'hook wiring carries no path placeholder (nothing for Windows to mangle)') s="hook bağlantısında yol yer tutucusu yok (Windows'un bozacağı bir şey yok)" ;;
92
+ 'settings.json missing — the tool-level gates (commit approval, guards, context) are INACTIVE') s='settings.json yok — araç düzeyindeki kapılar (commit onayı, korumalar, bağlam) DEVRE DIŞI' ;;
93
+ 'reinstall Crewforth') s="Crewforth'u yeniden kurun" ;;
94
+ "Claude Code's default") s="Claude Code'un varsayılanı" ;;
95
+ ' (estimated: only 1M and 200k windows were measured)') s=' (tahmini: yalnız 1M ve 200k pencereler ölçüldü)' ;;
96
+ 'skill listing: %s skill(s) use when_to_use or a folded description — a shape the count was not measured on') s='skill listesi: %s skill when_to_use ya da katlanmış açıklama kullanıyor — sayımın ölçülmediği bir biçim' ;;
97
+ 'skill listing %s chars for %s skills fits the %s-char budget (fraction %s from %s, %s-token window%s)') s="skill listesi %s karakter, %s skill; %s karakterlik bütçeye sığıyor (kesir %s, kaynak %s; %s token'lık pencere%s)" ;;
98
+ 'skill listing %s chars for %s skills EXCEEDS the %s-char budget (fraction %s from %s, %s-token window%s)') s="skill listesi %s karakter, %s skill; %s karakterlik bütçeyi AŞIYOR (kesir %s, kaynak %s; %s token'lık pencere%s)" ;;
99
+ ' — Claude Code drops the descriptions of the least-used skills, and those stop matching requests.') s=" — Claude Code en az kullanılan skill'lerin açıklamalarını düşürür; o skill'ler isteklerle eşleşmez olur." ;;
100
+ ' Fixes: raise %s in settings, or set rarely-used skills to %s in %s.') s=" Çözüm: ayarlarda %s değerini yükseltin ya da nadir kullanılan skill'leri %s olarak %s içine yazın." ;;
101
+ ' Which skills? bash .claude/eval/utilization.sh — it reports the ones nothing in this project reached.') s=" Hangileri? bash .claude/eval/utilization.sh — bu projede hiçbir şeyin ulaşmadığı skill'leri listeler." ;;
102
+ "on a 200,000-token model the whole listing would be ~%s chars (Crewforth %s + your personal skills %s + Claude Code's own ~%s, measured) against %s at fraction %s — the least-used skills there lose their descriptions.") s="200 000 token'lık bir modelde listenin tamamı ~%s karakter olur (Crewforth %s + kişisel skill'leriniz %s + Claude Code'un kendi ~%s, ölçüldü); bütçe ise %s karakter (kesir %s). Orada en az kullanılan skill'ler açıklamalarını kaybeder." ;;
103
+ ' If you use such a model, one line in %s/settings.json fixes it: %s') s=' Böyle bir model kullanıyorsanız %s/settings.json dosyasına tek satır yeter: %s' ;;
104
+ ' What it costs: the listing is sent every turn. At 0.04 it stays whole — ~%s chars, about %s tokens,') s=" Bedeli: liste her turda gönderilir. 0.04'te bütün kalır — ~%s karakter, yaklaşık %s token," ;;
105
+ ' %s%% of a 200k window — instead of at most %s chars. (4 characters per token is how Claude Code sizes') s=' 200k pencerede %%%s yer tutar — en fazla %s karakter yerine. (Claude Code bu bütçeyi 200k pencerede token başına' ;;
106
+ ' this budget on a 200k window: 0.01 of 200,000 tokens is 8,000 characters.)') s=" 4 karakter sayarak hesaplar: 200 000 token'ın 0.01'i 8 000 karakterdir.)" ;;
107
+ "delegation check could not read:%s — not valid JSON (or Crewforth's reader is missing).") s="delegasyon denetimi okuyamadı:%s — geçerli JSON değil (ya da Crewforth'un okuyucusu yok)." ;;
108
+ ' open it and check that Agent/Task is not under permissions.deny. If it is, no subagent can ever run.') s=" dosyayı açıp Agent/Task'ın permissions.deny altında olmadığını kontrol edin. Oradaysa hiçbir alt ajan çalışamaz." ;;
109
+ 'the Agent tool is DENIED in:%s — no subagent can ever run, so every agent on disk is dead weight') s='Agent aracı şurada REDDEDİLMİŞ:%s — hiçbir alt ajan çalışamaz, diskteki her ajan ölü yük' ;;
110
+ 'remove the Agent/Task entry from permissions.deny, or accept that this project runs main-thread-only') s='Agent/Task girdisini permissions.deny içinden çıkarın ya da bu projenin yalnız ana oturumda çalışacağını kabul edin' ;;
111
+ 'delegation is enabled (the Agent tool is not denied)') s='delegasyon açık (Agent aracı reddedilmemiş)' ;;
112
+ 'CLAUDE.md (or a doc it references) names auto-delegated agent(s) that no installed agent matches — delegation to them silently fails') s='CLAUDE.md (ya da başvurduğu bir belge) kurulu hiçbir ajanla eşleşmeyen, otomatik devredilen ajan(lar) anıyor — onlara devretmek sessizce başarısız olur' ;;
113
+ 'rename each bare reference to its `crew-` id:') s='her çıplak anmayı `crew-` kimliğiyle değiştirin:' ;;
114
+ 'CLAUDE.md (or a referenced doc) names pull-only agent(s) by their old bare id — invoked explicitly, so delegation still works; rename for consistency:%s') s='CLAUDE.md (ya da başvurduğu bir belge) yalnız açıkça çağrılan ajan(lar)ı eski çıplak kimliğiyle anıyor — açıkça çağrıldıkları için devretme yine çalışır; tutarlılık için yeniden adlandırın:%s' ;;
115
+ 'agent references resolve to installed agents (CLAUDE.md + referenced docs)') s='ajan anmaları kurulu ajanlara çözülüyor (CLAUDE.md ve başvurduğu belgeler)' ;;
116
+ 'CLAUDE.md missing — .claude/DISCIPLINE.md is never loaded (routing / DoD / session rules absent)') s='CLAUDE.md yok — .claude/DISCIPLINE.md hiç yüklenmiyor (yönlendirme / DoD / oturum kuralları yok)' ;;
117
+ 'create CLAUDE.md with this as its own line: @.claude/DISCIPLINE.md') s="CLAUDE.md'yi oluşturun ve şunu ayrı bir satır olarak yazın: @.claude/DISCIPLINE.md" ;;
118
+ 'CLAUDE.md imports .claude/DISCIPLINE.md (the discipline reaches the model)') s="CLAUDE.md .claude/DISCIPLINE.md'yi içe aktarıyor (disiplin modele ulaşıyor)" ;;
119
+ "CLAUDE.md carries the discipline INLINE (pre-1.1 layout) — it loads, but Crewforth updates never reach it; migrate to the '@.claude/DISCIPLINE.md' import line") s="CLAUDE.md disiplini SATIR İÇİNDE taşıyor (1.1 öncesi düzen) — yükleniyor ama Crewforth güncellemeleri ona ulaşmıyor; '@.claude/DISCIPLINE.md' içe aktarma satırına geçin" ;;
120
+ '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" ;;
121
+ '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" ;;
122
+ ' · 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' ;;
123
+ '%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)" ;;
124
+ '%s is set but no longer read — set CREW_%s instead') s='%s ayarlı ama artık okunmuyor — yerine CREW_%s ayarlayın' ;;
125
+ 'DOCTOR: healthy ✅%s') s='DOCTOR: sağlıklı ✅%s' ;;
126
+ 'DOCTOR: %s issue(s) ❌ — apply the fixes above%s') s='DOCTOR: %s sorun ❌ — yukarıdaki çözümleri uygulayın%s' ;;
127
+ 'shell gates watch both Bash and PowerShell') s="kabuk kapıları hem Bash'i hem PowerShell'i izliyor" ;;
128
+ 'shell gates watch only Bash — PowerShell commands bypass every §4.5 rule') s="kabuk kapıları yalnız Bash'i izliyor — PowerShell komutları her §4.5 kuralını atlıyor" ;;
129
+ 'update Crewforth (npx crewforth update), or set the PreToolUse matcher to') s="Crewforth'u güncelleyin (npx crewforth update) ya da PreToolUse matcher'ını şuna ayarlayın:" ;;
130
+ 'auto-mode classifier config: built-ins intact, Crewforth rules present (config, not a gate)') s='auto-mode sınıflandırıcı ayarı: yerleşik kurallar sağlam, Crewforth kuralları var (ayar, kapı değil)' ;;
131
+ 'auto-mode classifier BUILT-INS DROPPED — an autoMode array lacks %s') s='auto-mode sınıflandırıcının YERLEŞİK KURALLARI DÜŞMÜŞ — bir autoMode dizisinde %s yok' ;;
132
+ 'restore it in ~/.claude/settings.json; see .claude/skills/automode-policy/SKILL.md') s='~/.claude/settings.json içinde geri koyun; bkz. .claude/skills/automode-policy/SKILL.md' ;;
133
+ 'auto-mode classifier config: Crewforth rules absent (measured not to enforce — see the skill)') s="auto-mode sınıflandırıcı ayarı: Crewforth kuralları yok (uygulanmadıkları ölçüldü — skill'e bakın)" ;;
134
+ 'auto-mode classifier config: not checked (no claude CLI, or auto mode unavailable here)') s='auto-mode sınıflandırıcı ayarı: denetlenmedi (claude CLI yok ya da burada auto mode kullanılamıyor)' ;;
135
+ 'auto-mode policy check skipped (install predates the automode-policy skill; run the updater)') s="auto-mode politika denetimi atlandı (kurulum automode-policy skill'inden eski; güncelleyiciyi çalıştırın)" ;;
136
+ 'gate activity: no gate has fired in this project yet — %s rules wired, recording on') s='kapı etkinliği: bu projede henüz hiçbir kapı tetiklenmedi — %s kural bağlı, kayıt açık' ;;
137
+ 'gate activity: %s decision(s) recorded (see /crew-gates)') s='kapı etkinliği: %s karar kaydedildi (bkz. /crew-gates)' ;;
138
+ 'gate activity recorded (see /crew-gates)') s='kapı etkinliği kaydediliyor (bkz. /crew-gates)' ;;
139
+ 'gate activity NOT MEASURED — nowhere to record (see /crew-gates)') s='kapı etkinliği ÖLÇÜLMEDİ — kaydedilecek yer yok (bkz. /crew-gates)' ;;
140
+ 'gate activity unreadable (see /crew-gates)') s='kapı etkinliği okunamadı (bkz. /crew-gates)' ;;
141
+ 'Readiness (advisory — does not affect the verdict above):') s='Hazırlık (tavsiye niteliğinde — yukarıdaki kararı etkilemez):' ;;
142
+ 'CLAUDE.md project section is still the template (placeholders left in)') s='CLAUDE.md proje bölümü hâlâ şablon (yer tutucular duruyor)' ;;
143
+ 'fill in Project / Stack / Project skills — agents read the stack from there') s='Project / Stack / Project skills bölümlerini doldurun — ajanlar yığını oradan okur' ;;
144
+ 'CLAUDE.md project section is filled in') s='CLAUDE.md proje bölümü doldurulmuş' ;;
145
+ "%s project-specific skill(s) alongside Crewforth's") s="Crewforth'unkilerin yanında %s projeye özel skill" ;;
146
+ "no project-specific skill — only Crewforth's generic ones are installed") s="projeye özel skill yok — yalnız Crewforth'un genel skill'leri kurulu" ;;
147
+ "put the domain 'how's in .claude/skills/ (format: .claude/AGENT_TEMPLATE.md)") s="alanınıza özgü 'nasıl'ları .claude/skills/ altına koyun (biçim: .claude/AGENT_TEMPLATE.md)" ;;
148
+ 'project-skill signal skipped (no .claude/kit-manifest.txt — install predates it; run the updater)') s="proje skill'i sinyali atlandı (.claude/kit-manifest.txt yok — kurulum ondan eski; güncelleyiciyi çalıştırın)" ;;
149
+ 'devcontainer present (agentic execution is sandboxed)') s='devcontainer var (ajan komutları yalıtılmış ortamda çalışır)' ;;
150
+ 'no .devcontainer/devcontainer.json — agent commands run directly against your machine') s='.devcontainer/devcontainer.json yok — ajan komutları doğrudan makinenizde çalışır' ;;
151
+ 'add a devcontainer, or keep approval-mode gates on for anything destructive (§4.5)') s='bir devcontainer ekleyin ya da yıkıcı her şey için onay kapılarını açık tutun (§4.5)' ;;
152
+ 'MCP servers configured (project tools/data reach the model)') s='MCP sunucuları ayarlı (projenin araçları ve verisi modele ulaşır)' ;;
153
+ 'no MCP server configured — the model has no project-specific tool access') s='MCP sunucusu ayarlı değil — modelin projeye özel araç erişimi yok' ;;
154
+ 'add .mcp.json when a tool/data source would help (the mcp-builder skill covers writing one)') s="bir araç ya da veri kaynağı işe yarayacaksa .mcp.json ekleyin (nasıl yazılacağını mcp-builder skill'i anlatır)" ;;
155
+ 'CLAUDE.md is current (%s commit(s) of drift since it was last touched)') s="CLAUDE.md güncel (son dokunulduğundan beri %s commit'lik kayma)" ;;
156
+ 'CLAUDE.md is stale — %s commits changed the project since it was last touched (limit %s)') s='CLAUDE.md bayat — son dokunulduğundan beri %s commit projeyi değiştirdi (sınır %s)' ;;
157
+ 're-read it against the code and update Stack / Project skills (the claude-md-improver flow)') s='kodla karşılaştırarak yeniden okuyun ve Stack / Project skills bölümlerini güncelleyin (claude-md-improver akışı)' ;;
158
+ 'freshness signal skipped (cannot read CLAUDE.md mtime on this platform)') s="güncellik sinyali atlandı (bu platformda CLAUDE.md'nin değişme zamanı okunamıyor)" ;;
159
+ ' → readiness %s/%s') s=' → hazırlık %s/%s' ;;
160
+ 'npx crewforth adopt') ;; # identifier, printed as is
161
+ 'chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg') ;; # identifier, printed as is
162
+ 'git config core.hooksPath .claude/hooks') ;; # identifier, printed as is
163
+ # No row: the line prints in English. CREW_I18N_MISS collects every such key, so smoke catches a missing
164
+ # translation by NAME.
165
+ *) [ -n "${CREW_I18N_MISS:-}" ] && printf '%s\n' "$s" >> "$CREW_I18N_MISS" ;;
166
+ esac
167
+ fi
168
+ # shellcheck disable=SC2059
169
+ printf -v _M "$s" "$@"
170
+ }
171
+ # ---- /CREW-I18N -----------------------------------------------------------------------------------------
172
+ _mt "== Crewforth — install doctor =="; echo "$_M"
31
173
 
32
174
  # 0) Is Crewforth even here?
33
- [ -d .claude ] || { echo " ❌ no .claude/ in '$PWD' — is Crewforth installed here?"; echo " ↳ fix: npx crewforth adopt"; exit 1; }
175
+ [ -d .claude ] || { bad "no .claude/ in '%s' — is Crewforth installed here?" "npx crewforth adopt" "$PWD"; exit 1; }
34
176
 
35
177
  # 1) VERSION (marks a full install; also what /crew-update compares)
36
- if [ -f .claude/VERSION ]; then ok "VERSION present ($(head -1 .claude/VERSION | tr -cd '0-9A-Za-z.-'))"
178
+ if [ -f .claude/VERSION ]; then ok "VERSION present (%s)" "$(head -1 .claude/VERSION | tr -cd '0-9A-Za-z.-')"
37
179
  else bad "VERSION missing" "reinstall or update Crewforth (npx crewforth update)"; fi
38
180
 
39
181
  # 1b) Is that version the current one? Read-only, from the cache session-update-check.sh maintains — doctor makes
@@ -45,7 +187,7 @@ if [ -f .claude/VERSION ] && [ -f .claude/.state/update-check ]; then
45
187
  DCUR="$(head -1 .claude/VERSION 2>/dev/null | tr -cd '0-9A-Za-z.-')"
46
188
  if [ -n "$DLATEST" ] && awk -v a="$DLATEST" -v b="$DCUR" 'BEGIN{split(a,x,".");split(b,y,".");
47
189
  for(i=1;i<=3;i++){if(x[i]+0>y[i]+0)exit 0; if(x[i]+0<y[i]+0)exit 1} exit 1}'; then
48
- warn "Crewforth v$DCUR installed, v$DLATEST published — update with /crew-update"
190
+ warn "Crewforth v%s installed, v%s published — update with /crew-update" "$DCUR" "$DLATEST"
49
191
  fi
50
192
  fi
51
193
 
@@ -57,9 +199,9 @@ for h in .claude/hooks/pre-commit .claude/hooks/commit-msg; do
57
199
  if [ ! -e "$h" ]; then GONE="$GONE $(basename "$h")"; elif [ ! -x "$h" ]; then NX="$NX $(basename "$h")"; fi
58
200
  done
59
201
  [ -z "$GONE" ] && ok "required git hooks present (pre-commit, commit-msg)" \
60
- || bad "MISSING git hook(s):$GONE — the commit trace/secret scan is absent" "reinstall or update Crewforth"
202
+ || bad "MISSING git hook(s):%s — the commit trace/secret scan is absent" "reinstall or update Crewforth" "$GONE"
61
203
  [ -z "$NX" ] && ok "all hooks are executable" \
62
- || bad "not executable:$NX" "chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg"
204
+ || bad "not executable:%s" "chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg" "$NX"
63
205
 
64
206
  # 2b) Behaviour probe — a hook that is present + executable can still be NEUTERED (its body replaced with `exit 0`).
65
207
  # Drive guard-bash with a command it MUST block; if it does not exit 2, the §4.5 gate is disarmed.
@@ -92,9 +234,9 @@ fi
92
234
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
93
235
  HP="$(git config --get core.hooksPath 2>/dev/null || true)"
94
236
  case "$HP" in
95
- */.claude/hooks|.claude/hooks) ok "core.hooksPath -> $HP (commit-time gates active)" ;;
237
+ */.claude/hooks|.claude/hooks) ok "core.hooksPath -> %s (commit-time gates active)" "$HP" ;;
96
238
  "") bad "core.hooksPath is unset — commit trace/secret/bloat gates are INACTIVE" "git config core.hooksPath .claude/hooks" ;;
97
- *) bad "core.hooksPath -> $HP (not Crewforth's hooks)" "git config core.hooksPath .claude/hooks" ;;
239
+ *) bad "core.hooksPath -> %s (not Crewforth's hooks)" "git config core.hooksPath .claude/hooks" "$HP" ;;
98
240
  esac
99
241
  else
100
242
  warn "not a git repo — commit gates need: git init && git config core.hooksPath .claude/hooks"
@@ -110,7 +252,7 @@ if [ -f "$S" ]; then
110
252
  # parse: validity, then each event's array length.
111
253
  SJ=.claude/eval/lib/settings-json.awk
112
254
  if [ ! -f "$SJ" ]; then
113
- bad "Crewforth's JSON reader is missing ($SJ) — settings.json cannot be checked" "update Crewforth"
255
+ bad "Crewforth's JSON reader is missing (%s) — settings.json cannot be checked" "update Crewforth" "$SJ"
114
256
  elif awk -v op=validate -f "$SJ" "$S" 2>/dev/null; then
115
257
  ok "settings.json is valid JSON"
116
258
  EMPTY=""
@@ -119,7 +261,7 @@ if [ -f "$S" ]; then
119
261
  case "$n" in ''|0) EMPTY="$EMPTY $ev" ;; esac
120
262
  done
121
263
  [ -z "$EMPTY" ] && ok "settings.json wires PreToolUse / UserPromptSubmit / Stop (non-empty)" \
122
- || bad "settings.json hook events empty or missing:$EMPTY — those gates won't fire" "restore settings.json from Crewforth (npx crewforth update)"
264
+ || bad "settings.json hook events empty or missing:%s — those gates won't fire" "restore settings.json from Crewforth (npx crewforth update)" "$EMPTY"
123
265
  sn="$(awk -v op=len -v path=hooks.SessionStart -f "$SJ" "$S" 2>/dev/null)"
124
266
  case "$sn" in ''|0) warn "SessionStart not wired — session rehydration after /compact or /clear is inactive (update Crewforth)" ;; *) ok "SessionStart wired (session rehydration active)" ;; esac
125
267
  else bad "settings.json is invalid JSON" "fix the syntax by hand — restoring Crewforth's file would drop any hooks you added"; fi
@@ -134,8 +276,9 @@ if [ -f "$S" ]; then
134
276
  # Reported on every platform, not only Windows: a repo is shared across machines, and the wiring is wrong on
135
277
  # all of them the moment one teammate is on Windows.
136
278
  if grep -q '\${CLAUDE_PROJECT_DIR' "$S" 2>/dev/null; then
137
- bad "settings.json wires hooks through the \${CLAUDE_PROJECT_DIR} placeholder — on Windows its separators are stripped before bash runs, so NO hook launches and every gate is silently absent" \
138
- "update Crewforth (npx crewforth adopt) — hook commands become: cd \"\$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/<name>.sh"
279
+ BAD_TAIL=' cd "$CLAUDE_PROJECT_DIR" 2>/dev/null; bash .claude/hooks/<name>.sh'
280
+ bad "settings.json wires hooks through the %s placeholder — on Windows its separators are stripped before bash runs, so NO hook launches and every gate is silently absent" \
281
+ "update Crewforth (npx crewforth adopt) — hook commands become:" '${CLAUDE_PROJECT_DIR}'
139
282
  else
140
283
  ok "hook wiring carries no path placeholder (nothing for Windows to mangle)"
141
284
  fi
@@ -143,34 +286,57 @@ else
143
286
  bad "settings.json missing — the tool-level gates (commit approval, guards, context) are INACTIVE" "reinstall Crewforth"
144
287
  fi
145
288
 
146
- # 4a) Skill listing budget. Claude Code loads a listing of every skill's name + description into context each
147
- # session; the budget is 1% of the context window, and over it descriptions get truncated or dropped
148
- # "which can strip the keywords Claude needs to match your request" (skills docs). The failure is invisible:
149
- # the skills are all still installed and still listed BY NAME, they just stop matching. Reported, never
150
- # enforced — a project is free to ship many skills, it just needs to know the trade and the two documented
151
- # ways out (raise skillListingBudgetFraction, or set low-priority skills to "name-only" in skillOverrides).
289
+ # 4a) Skill listing budget. Claude Code puts a listing of every model-invocable skill's name and description into
290
+ # context each turn and caps it at skillListingBudgetFraction of the context window (default 0.01); over the
291
+ # cap it drops the descriptions of the least-used skills, which stops them matching requests. The count is
292
+ # eval/lib/skill-listing.awk — the method smoke-test.sh gates with, measured against Claude Code's own counter
293
+ # (see that file). The budget is the fraction as Claude Code resolves it (settings.local.json, then
294
+ # settings.json, then the user's settings, then 0.01) times the window's characters, and those were MEASURED
295
+ # from the "Skill listing over budget: … > B budget" debug warning: a 1,000,000-token window gives 3 characters
296
+ # per token of budget (0.001 -> 3000, 0.005 -> 15000), a 200,000-token window 4 (0.001 -> 800, 0.01 -> 8000).
297
+ # Another window is not measured; it is estimated at 3 and said to be an estimate. Reported, never enforced.
152
298
  if [ -d .claude/skills ]; then
153
- # ONE awk over every SKILL.md, not two per skill: 41 skills made this 82 spawns, which on Git Bash is several
154
- # seconds on its own. Counts what `awk c==1 | awk '/^(name|description):/,0' | wc -c` counted — the frontmatter
155
- # from its first name:/description: line on, in bytes. LC_ALL=C because wc -c counts bytes and gawk's length()
156
- # counts characters under a UTF-8 locale: measured on Git Bash (gawk 5.4, LC_ALL=en_US.UTF-8), dropping it read
157
- # the shipped set 38 short — its em dashes alone move the number. macOS awk counts bytes either way.
299
+ SLA=.claude/eval/lib/skill-listing.awk
158
300
  SKF=""; for f in .claude/skills/*/SKILL.md; do [ -e "$f" ] && SKF=1 && break; done
159
- LISTING=0
160
- [ -n "$SKF" ] && LISTING=$(LC_ALL=C awk '
161
- FNR==1 { c=0; on=0 }
162
- /^---$/ { c++; next }
163
- c==1 { if (!on && /^(name|description):/) on=1; if (on) n += length($0) + 1 }
164
- END { print n+0 }' .claude/skills/*/SKILL.md)
165
- CW="${CONTEXT_WINDOW:-1000000}"; BUDGET=$((CW/100))
166
- if [ "$LISTING" -le "$BUDGET" ]; then
167
- ok "skill listing fits the budget (${LISTING} <= ${BUDGET} chars at 1% of a ${CW}-token window)"
168
- else
169
- warn "skill listing ${LISTING} chars EXCEEDS the ~${BUDGET} budget for a ${CW}-token window — Claude Code will"
170
- warn " truncate or drop descriptions, and a skill whose description is gone stops matching requests."
171
- warn " Fixes: raise \"skillListingBudgetFraction\" in settings, or set rarely-used skills to \"name-only\""
172
- warn " in \"skillOverrides\". Re-check with a smaller CONTEXT_WINDOW=200000 to see your real model's budget."
173
- warn " Which skills? bash .claude/eval/utilization.sh — it reports the ones nothing in this project reached."
301
+ if [ -n "$SKF" ] && [ -f "$SLA" ]; then
302
+ read -r LISTING NLISTED UNMEAS <<EOF_SLA
303
+ $(LC_ALL=C awk -f "$SLA" .claude/skills/*/SKILL.md)
304
+ EOF_SLA
305
+ FRAC=""; _mt "Claude Code's default"; FROM="$_M"
306
+ for sf in .claude/settings.local.json .claude/settings.json "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"; do
307
+ [ -f "$sf" ] || continue
308
+ v="$(awk -v op=get -v path=skillListingBudgetFraction -f "${SJ:-.claude/eval/lib/settings-json.awk}" "$sf" 2>/dev/null)" || v=""
309
+ case "$v" in ''|*[!0-9.]*) ;; *) FRAC="$v"; FROM="$sf"; break ;; esac
310
+ done
311
+ [ -n "$FRAC" ] || FRAC=0.01
312
+ CW="${CONTEXT_WINDOW:-1000000}"
313
+ case "$CW" in 1000000) CPT=3; EST="" ;; 200000) CPT=4; EST="" ;; *) CPT=3; _mt " (estimated: only 1M and 200k windows were measured)"; EST="$_M" ;; esac
314
+ BUDGET=$(awk -v f="$FRAC" -v w="$CW" -v c="$CPT" 'BEGIN { printf "%d", f * w * c }')
315
+ B200=$(awk -v f="$FRAC" 'BEGIN { printf "%d", f * 200000 * 4 }')
316
+ [ "${UNMEAS:-0}" = 0 ] || warn "skill listing: %s skill(s) use when_to_use or a folded description — a shape the count was not measured on" "$UNMEAS"
317
+ if [ "$LISTING" -le "$BUDGET" ]; then
318
+ ok "skill listing %s chars for %s skills fits the %s-char budget (fraction %s from %s, %s-token window%s)" "$LISTING" "$NLISTED" "$BUDGET" "$FRAC" "$FROM" "$CW" "$EST"
319
+ else
320
+ warn "skill listing %s chars for %s skills EXCEEDS the %s-char budget (fraction %s from %s, %s-token window%s)" "$LISTING" "$NLISTED" "$BUDGET" "$FRAC" "$FROM" "$CW" "$EST"
321
+ warn " — Claude Code drops the descriptions of the least-used skills, and those stop matching requests."
322
+ warn " Fixes: raise %s in settings, or set rarely-used skills to %s in %s." '"skillListingBudgetFraction"' '"name-only"' '"skillOverrides"'
323
+ warn " Which skills? bash .claude/eval/utilization.sh — it reports the ones nothing in this project reached."
324
+ fi
325
+ # The whole listing, not only Crewforth's share: the user's personal skills and Claude Code's own bundled skills
326
+ # sit in the same budget. The bundled ones were measured once (14 skills, 5898 chars, Claude Code v2.1.282, an
327
+ # isolated config) — another version may differ, so the figure is labelled as measured, not read. Skills from
328
+ # other plugins are not counted: the doctor cannot see them.
329
+ PERS=0; PD="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills"
330
+ for f in "$PD"/*/SKILL.md; do [ -e "$f" ] && { PERS=$(LC_ALL=C awk -f "$SLA" "$PD"/*/SKILL.md | cut -d' ' -f1); break; }; done
331
+ BUILTIN=5898; TOTAL=$((LISTING + BUILTIN + 1)); [ "$PERS" -gt 0 ] && TOTAL=$((TOTAL + PERS + 1)) # one joining character per group
332
+ if [ "$TOTAL" -gt "$B200" ]; then
333
+ warn "on a 200,000-token model the whole listing would be ~%s chars (Crewforth %s + your personal skills %s + Claude Code's own ~%s, measured) against %s at fraction %s — the least-used skills there lose their descriptions." "$TOTAL" "$LISTING" "$PERS" "$BUILTIN" "$B200" "$FRAC"
334
+ warn " If you use such a model, one line in %s/settings.json fixes it: %s" "${CLAUDE_CONFIG_DIR:-~/.claude}" '"skillListingBudgetFraction": 0.04'
335
+ TK=$((TOTAL / 4)); PCT=$(awk -v t="$TK" 'BEGIN { printf "%.1f", t / 2000 }'); [ "$CREW_LANG" = tr ] && PCT="${PCT/./,}"
336
+ warn " What it costs: the listing is sent every turn. At 0.04 it stays whole — ~%s chars, about %s tokens," "$TOTAL" "$TK"
337
+ warn " %s%% of a 200k window — instead of at most %s chars. (4 characters per token is how Claude Code sizes" "$PCT" "$B200"
338
+ warn " this budget on a 200k window: 0.01 of 200,000 tokens is 8,000 characters.)"
339
+ fi
174
340
  fi
175
341
  fi
176
342
 
@@ -193,7 +359,7 @@ for f in .claude/settings.json .claude/settings.local.json "$HOME/.claude/settin
193
359
  | grep -qE '^(Agent|Task)([^A-Za-z0-9_]|$)' && DENYSRC="$DENYSRC $f"
194
360
  done
195
361
  if [ "${NOPY:-0}" = 1 ]; then
196
- warn "delegation check could not read:${MAYBEDENY} — not valid JSON (or Crewforth's reader is missing)."
362
+ warn "delegation check could not read:%s — not valid JSON (or Crewforth's reader is missing)." "$MAYBEDENY"
197
363
  warn " open it and check that Agent/Task is not under permissions.deny. If it is, no subagent can ever run."
198
364
  fi
199
365
  # `A && B && ok … || bad …` cannot express three outcomes. With no usable python3 the && chain is false, so the
@@ -201,8 +367,8 @@ fi
201
367
  # and a verdict of ❌ on a healthy install. It stayed invisible while `command -v python3` was the test, because
202
368
  # on Windows the Store stub answered yes and NOPY was never set. Three states, three branches.
203
369
  if [ -n "$DENYSRC" ]; then
204
- bad "the Agent tool is DENIED in:$DENYSRC — no subagent can ever run, so every agent on disk is dead weight" \
205
- "remove the Agent/Task entry from permissions.deny, or accept that this project runs main-thread-only"
370
+ bad "the Agent tool is DENIED in:%s — no subagent can ever run, so every agent on disk is dead weight" \
371
+ "remove the Agent/Task entry from permissions.deny, or accept that this project runs main-thread-only" "$DENYSRC"
206
372
  elif [ "${NOPY:-0}" != 1 ]; then
207
373
  ok "delegation is enabled (the Agent tool is not denied)"
208
374
  fi
@@ -271,10 +437,11 @@ $(awk '
271
437
  }' $SCAN)
272
438
  EOF
273
439
  if [ -n "$STALE" ]; then
440
+ BAD_TAIL="$STALE"
274
441
  bad "CLAUDE.md (or a doc it references) names auto-delegated agent(s) that no installed agent matches — delegation to them silently fails" \
275
- "rename each bare reference to its \`crew-\` id:$STALE"
442
+ 'rename each bare reference to its `crew-` id:'
276
443
  fi
277
- [ -n "$STALE_PULL" ] && warn "CLAUDE.md (or a referenced doc) names pull-only agent(s) by their old bare id — invoked explicitly, so delegation still works; rename for consistency:$STALE_PULL"
444
+ [ -n "$STALE_PULL" ] && warn "CLAUDE.md (or a referenced doc) names pull-only agent(s) by their old bare id — invoked explicitly, so delegation still works; rename for consistency:%s" "$STALE_PULL"
278
445
  [ -z "$STALE$STALE_PULL" ] && ok "agent references resolve to installed agents (CLAUDE.md + referenced docs)"
279
446
  fi
280
447
 
@@ -310,22 +477,22 @@ PREFLIGHT=".claude/eval/preflight.sh"
310
477
  [ -f "$PREFLIGHT" ] || PREFLIGHT="$(dirname "$0")/preflight.sh"
311
478
  PANEL_NOTE=""
312
479
  if [ -d .claude/studio ] && ! bash "$PREFLIGHT" --has node 2>/dev/null; then
313
- PANEL_NOTE=" · panel needs Node 18+ — .claude/studio/ensure-node.sh --plan fetches one"
480
+ _mt " · panel needs Node 18+ — .claude/studio/ensure-node.sh --plan fetches one"; PANEL_NOTE="$_M"
314
481
  fi
315
482
  # A 2.x variable name still works until 4.0 (lib/crew-env.sh reads it); name its 3.0 spelling so the user can switch.
316
483
  for _v in $(compgen -e); do
317
484
  case "$_v" in CSK_CORRECT_STACK) ;; CSK_*)
318
485
  case "${_crew_legacy:-}" in
319
- *" ${_v#CSK_} "*) warn "$_v is set — its 3.0 name is CREW_${_v#CSK_} (the old name works until 4.0)" ;;
320
- *) warn "$_v is set but no longer read — set CREW_${_v#CSK_} instead" ;;
486
+ *" ${_v#CSK_} "*) warn "%s is set — its 3.0 name is CREW_%s (the old name works until 4.0)" "$_v" "${_v#CSK_}" ;;
487
+ *) warn "%s is set but no longer read — set CREW_%s instead" "$_v" "${_v#CSK_}" ;;
321
488
  esac ;;
322
489
  esac
323
490
  done
324
- if [ "$FAIL" -eq 0 ]; then echo "DOCTOR: healthy ✅$PANEL_NOTE"
491
+ if [ "$FAIL" -eq 0 ]; then _mt "DOCTOR: healthy ✅%s" "$PANEL_NOTE"; echo "$_M"
325
492
  # Healthy verdict: the star line, once per kit version — the marker is shared with the installers, so the
326
493
  # doctor run that /crew-update makes right after an update stays quiet. Text/URL/silence: lib/star.sh.
327
494
  [ -f "$(dirname "$0")/lib/star.sh" ] && bash "$(dirname "$0")/lib/star.sh" --once .
328
- else echo "DOCTOR: $FAIL issue(s) ❌ — apply the fixes above$PANEL_NOTE"; fi
495
+ else _mt "DOCTOR: %s issue(s) ❌ — apply the fixes above%s" "$FAIL" "$PANEL_NOTE"; echo "$_M"; fi
329
496
 
330
497
  # 8b) The shell matcher. Claude Code's hooks reference is explicit: inspect shell commands with
331
498
  # `Bash|PowerShell`, because wherever the PowerShell tool is enabled it IS the shell — and it is on by
@@ -336,8 +503,9 @@ if [ -f .claude/settings.json ]; then
336
503
  if grep -q '"matcher"[[:space:]]*:[[:space:]]*"[^"]*PowerShell' .claude/settings.json; then
337
504
  ok "shell gates watch both Bash and PowerShell"
338
505
  elif grep -q '"matcher"[[:space:]]*:[[:space:]]*"Bash"' .claude/settings.json; then
506
+ BAD_TAIL=' "Bash|PowerShell"'
339
507
  bad "shell gates watch only Bash — PowerShell commands bypass every §4.5 rule" \
340
- "update Crewforth (npx crewforth update), or set the PreToolUse matcher to \"Bash|PowerShell\""
508
+ "update Crewforth (npx crewforth update), or set the PreToolUse matcher to"
341
509
  fi
342
510
  fi
343
511
  # 9) The auto-mode classifier. Since 2026-08-14 auto mode is the default permission mode on Pro/Max/Team, so a
@@ -349,8 +517,8 @@ if [ -x .claude/skills/automode-policy/scripts/check.sh ] || [ -f .claude/skills
349
517
  AMOUT="$(bash .claude/skills/automode-policy/scripts/check.sh 2>&1)"; AMRC=$?
350
518
  case "$AMRC" in
351
519
  0) ok "auto-mode classifier config: built-ins intact, Crewforth rules present (config, not a gate)" ;;
352
- 2) bad "auto-mode classifier BUILT-INS DROPPED — an autoMode array lacks \"\$defaults\"" \
353
- "restore it in ~/.claude/settings.json; see .claude/skills/automode-policy/SKILL.md" ;;
520
+ 2) bad "auto-mode classifier BUILT-INS DROPPED — an autoMode array lacks %s" \
521
+ "restore it in ~/.claude/settings.json; see .claude/skills/automode-policy/SKILL.md" '"$defaults"' ;;
354
522
  3) skip "auto-mode classifier config: Crewforth rules absent (measured not to enforce — see the skill)" ;;
355
523
  # Same vocabulary as the other three branches on purpose. It used to read "auto-mode policy check
356
524
  # skipped", and the suite's assertion — written on a machine that HAS the claude CLI — never saw this
@@ -364,11 +532,17 @@ fi
364
532
  # 10) Gate activity. The suite proves the gates CAN fire; this reports whether anything actually tripped them.
365
533
  # Recording is on by default (rule names only, never the command), so "no log" here means no gate has
366
534
  # fired yet — a measured zero, not a gap. Never a failure either way.
535
+ # Read through --json and worded here, so the line speaks the doctor's language (gate-report itself is English
536
+ # and prints its own sentence; quoting it here used to leave half of that sentence dangling on this line).
367
537
  if [ -f .claude/eval/gate-report.sh ]; then
368
- GOUT="$(bash .claude/eval/gate-report.sh 2>/dev/null)"; GRC=$?
538
+ GOUT="$(bash .claude/eval/gate-report.sh --json 2>/dev/null)"; GRC=$?
369
539
  case "$GRC" in
370
- 0) GL="$(printf '%s' "$GOUT" | grep -E 'decision\(s\)|no gate has fired' | head -1 | sed 's/^ *//')"
371
- [ -n "$GL" ] && ok "gate activity: $GL" || ok "gate activity recorded (see /crew-gates)" ;;
540
+ 0) GRU="${GOUT#*\"rules\":}"; GRU="${GRU%%[,\}]*}"; GDE="${GOUT#*\"decisions\":}"; GDE="${GDE%%[,\}]*}"
541
+ case "$GRU$GDE" in
542
+ *[!0-9]*|'') ok "gate activity recorded (see /crew-gates)" ;;
543
+ *) if [ "$GDE" = 0 ]; then ok "gate activity: no gate has fired in this project yet — %s rules wired, recording on" "$GRU"
544
+ else ok "gate activity: %s decision(s) recorded (see /crew-gates)" "$GDE"; fi ;;
545
+ esac ;;
372
546
  3) skip "gate activity NOT MEASURED — nowhere to record (see /crew-gates)" ;;
373
547
  *) skip "gate activity unreadable (see /crew-gates)" ;;
374
548
  esac
@@ -387,10 +561,10 @@ echo
387
561
  # sitting beside this script for the case where doctor is run straight out of the Crewforth source.
388
562
  [ -f "$PREFLIGHT" ] && bash "$PREFLIGHT"
389
563
 
390
- echo "Readiness (advisory — does not affect the verdict above):"
564
+ _mt "Readiness (advisory — does not affect the verdict above):"; echo "$_M"
391
565
  RDY=0; RTOT=0
392
- rdy(){ RTOT=$((RTOT+1)); RDY=$((RDY+1)); echo " ✅ $1"; }
393
- gap(){ RTOT=$((RTOT+1)); echo " ➖ $1"; echo " ↳ $2"; }
566
+ rdy(){ RTOT=$((RTOT+1)); RDY=$((RDY+1)); _mt "$@"; echo " ✅ $_M"; }
567
+ gap(){ local _m="$1" _x="$2"; shift 2; RTOT=$((RTOT+1)); _mt "$_m" "$@"; echo " ➖ $_M"; _mt "$_x"; echo " ↳ $_M"; }
394
568
 
395
569
  # R1) Is the CLAUDE.md project section filled in, or still the shipped template? An unfilled section means every
396
570
  # agent works stack-blind — it is the single most common way a correct install still underperforms.
@@ -424,7 +598,7 @@ if [ -f "$MAN" ]; then
424
598
  s="${d%/}"; s="${s##*/}"
425
599
  case "$NL$MANTXT$NL" in *"${NL}skills/$s$NL"*) ;; *) OWN=$((OWN+1)) ;; esac
426
600
  done
427
- [ "$OWN" -gt 0 ] && rdy "$OWN project-specific skill(s) alongside Crewforth's" \
601
+ [ "$OWN" -gt 0 ] && rdy "%s project-specific skill(s) alongside Crewforth's" "$OWN" \
428
602
  || gap "no project-specific skill — only Crewforth's generic ones are installed" \
429
603
  "put the domain 'how's in .claude/skills/ (format: .claude/AGENT_TEMPLATE.md)"
430
604
  else
@@ -455,11 +629,11 @@ if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ -f CLAUDE.md ]; then
455
629
  if [ -n "$MT" ]; then
456
630
  CH="$(git rev-list --count HEAD --since="@$MT" -- . ':(exclude).claude' 2>/dev/null | tr -cd '0-9')"
457
631
  CH="${CH:-0}"
458
- [ "$CH" -le "$MAXC" ] && rdy "CLAUDE.md is current ($CH commit(s) of drift since it was last touched)" \
459
- || gap "CLAUDE.md is stale — $CH commits changed the project since it was last touched (limit $MAXC)" \
460
- "re-read it against the code and update Stack / Project skills (the claude-md-improver flow)"
632
+ [ "$CH" -le "$MAXC" ] && rdy "CLAUDE.md is current (%s commit(s) of drift since it was last touched)" "$CH" \
633
+ || gap "CLAUDE.md is stale — %s commits changed the project since it was last touched (limit %s)" \
634
+ "re-read it against the code and update Stack / Project skills (the claude-md-improver flow)" "$CH" "$MAXC"
461
635
  else skip "freshness signal skipped (cannot read CLAUDE.md mtime on this platform)"; fi
462
636
  fi
463
637
 
464
- [ "$RTOT" -gt 0 ] && echo " → readiness $RDY/$RTOT"
638
+ [ "$RTOT" -gt 0 ] && { _mt " → readiness %s/%s" "$RDY" "$RTOT"; echo "$_M"; }
465
639
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,43 @@
1
+ # The size of the skill listing Claude Code puts into context, counted the way Claude Code counts it — the one
2
+ # method doctor.sh and smoke-test.sh share, so there is one number and not two.
3
+ #
4
+ # LC_ALL=C awk -f skill-listing.awk .claude/skills/*/SKILL.md -> "<chars> <skills> <unmeasured>"
5
+ # LC_ALL=C awk -v per=1 -f skill-listing.awk … -> one "<name>\t<chars>" row per skill, then the line above
6
+ #
7
+ # Measured against Claude Code's own counter (the "Skill listing over budget: N skills, C chars" debug warning, read
8
+ # with an isolated config and bundled skills off, so only these files are listed):
9
+ # - a skill costs len(name) + len(description) + 4; skills are joined by one more character each
10
+ # fixtures: name "aa" + "bbb" -> 9 · two skills (aa/bbb, cc/dddd) -> 20 · "aa" + 40 chars -> 46
11
+ # - a block description (`description: |`) counts its lines with nothing between them: the line breaks cost 0
12
+ # 46 shipped skills one by one: every multi-line description read (lines - 1) under "joined by a space"
13
+ # - a skill with `disable-model-invocation: true` is not listed at all
14
+ # - characters, not bytes: an em dash is one (UTF-8 continuation bytes are not counted)
15
+ # the 46 skills a fresh install lists: 9063 — the same figure Claude Code printed for them together.
16
+ # `when_to_use` and a folded (`>`) description were not measured; they are reported as "unmeasured" so a caller can
17
+ # refuse to trust the total rather than guess.
18
+ function chars(s, t) { t = s; gsub(/[\200-\277]/, "", t); return length(t) }
19
+ function flush() {
20
+ if (open && !dmi && name != "") {
21
+ n++; c = chars(name) + dlen + 4; total += c
22
+ if (per) printf "%s\t%d\n", name, c
23
+ }
24
+ open = 0
25
+ }
26
+ FNR == 1 { flush(); open = 1; fm = 0; name = ""; dlen = 0; blk = 0; dmi = 0 }
27
+ fm >= 2 { next }
28
+ /^---[[:space:]]*$/ { fm++; blk = 0; next }
29
+ fm != 1 { next }
30
+ blk && /^[[:space:]]+[^[:space:]]/ { l = $0; sub(/^[[:space:]]+/, "", l); sub(/[[:space:]]+$/, "", l); dlen += chars(l); next }
31
+ blk && /^[[:space:]]*$/ { next }
32
+ { blk = 0 }
33
+ /^name:/ { v = $0; sub(/^name:[[:space:]]*/, "", v); sub(/[[:space:]]+$/, "", v); name = v; next }
34
+ /^description:[[:space:]]*\|[-+]?[[:space:]]*$/ { blk = 1; next }
35
+ /^description:[[:space:]]*>/ { blk = 1; unmeasured++; next }
36
+ /^description:/ {
37
+ v = $0; sub(/^description:[[:space:]]*/, "", v); sub(/[[:space:]]+$/, "", v)
38
+ if (v ~ /^".*"$/ || v ~ /^'.*'$/) v = substr(v, 2, length(v) - 2)
39
+ dlen = chars(v); next
40
+ }
41
+ /^disable-model-invocation:[[:space:]]*true[[:space:]]*$/ { dmi = 1; next }
42
+ /^when_to_use:/ { unmeasured++; next }
43
+ END { flush(); printf "%d %d %d\n", total + (n > 0 ? n - 1 : 0), n, unmeasured + 0 }