crewforth 0.0.1 → 3.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +3784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -7
  4. package/VERSION +1 -0
  5. package/adopt.sh +1670 -0
  6. package/bin/cli.js +497 -0
  7. package/kit/AGENT_TEMPLATE.md +92 -0
  8. package/kit/CLAUDE.md +228 -0
  9. package/kit/README.md +107 -0
  10. package/kit/agents/crew-backend-expert.md +95 -0
  11. package/kit/agents/crew-commit-agent.md +96 -0
  12. package/kit/agents/crew-database-expert.md +84 -0
  13. package/kit/agents/crew-devops-expert.md +82 -0
  14. package/kit/agents/crew-frontend-expert.md +85 -0
  15. package/kit/agents/crew-performance-expert.md +78 -0
  16. package/kit/agents/crew-planner.md +60 -0
  17. package/kit/agents/crew-privacy-agent.md +71 -0
  18. package/kit/agents/crew-review-agent.md +95 -0
  19. package/kit/agents/crew-security-expert.md +73 -0
  20. package/kit/agents/crew-session-manager.md +77 -0
  21. package/kit/agents/crew-test-expert.md +64 -0
  22. package/kit/eval/doctor.sh +465 -0
  23. package/kit/eval/gate-report.sh +137 -0
  24. package/kit/eval/golden-holdout.txt +53 -0
  25. package/kit/eval/golden-routing.txt +199 -0
  26. package/kit/eval/lib/crew-env.sh +14 -0
  27. package/kit/eval/lib/settings-json.awk +154 -0
  28. package/kit/eval/lib/star.sh +70 -0
  29. package/kit/eval/parser-conformance.sh +612 -0
  30. package/kit/eval/preflight.sh +186 -0
  31. package/kit/eval/routing-eval.sh +209 -0
  32. package/kit/eval/scan-skill.sh +162 -0
  33. package/kit/eval/smoke-test.sh +6693 -0
  34. package/kit/eval/update-guard.sh +81 -0
  35. package/kit/eval/utilization.sh +130 -0
  36. package/kit/hooks/board-sync.sh +106 -0
  37. package/kit/hooks/board.sh +962 -0
  38. package/kit/hooks/commit-msg +49 -0
  39. package/kit/hooks/context-usage.sh +353 -0
  40. package/kit/hooks/floor-blocklist.txt +124 -0
  41. package/kit/hooks/guard-bash.sh +1431 -0
  42. package/kit/hooks/guard-commit-scan.sh +511 -0
  43. package/kit/hooks/guard-write.sh +513 -0
  44. package/kit/hooks/pre-commit +577 -0
  45. package/kit/hooks/route-hint.sh +185 -0
  46. package/kit/hooks/secret-blocklist.txt +82 -0
  47. package/kit/hooks/session-guard.sh +149 -0
  48. package/kit/hooks/session-rehydrate.sh +41 -0
  49. package/kit/hooks/session-stats.sh +252 -0
  50. package/kit/hooks/session-update-check.sh +236 -0
  51. package/kit/hooks/skill-trust.sh +150 -0
  52. package/kit/hooks/trace-blocklist.txt +53 -0
  53. package/kit/settings.json +132 -0
  54. package/kit/skills/a11y/SKILL.md +44 -0
  55. package/kit/skills/adr/SKILL.md +74 -0
  56. package/kit/skills/api-design/SKILL.md +50 -0
  57. package/kit/skills/automode-policy/SKILL.md +91 -0
  58. package/kit/skills/automode-policy/references/policy.json +13 -0
  59. package/kit/skills/automode-policy/scripts/apply.sh +84 -0
  60. package/kit/skills/automode-policy/scripts/check.sh +102 -0
  61. package/kit/skills/backend-architecture/SKILL.md +103 -0
  62. package/kit/skills/brainstorm/SKILL.md +47 -0
  63. package/kit/skills/ci-pipeline/SKILL.md +38 -0
  64. package/kit/skills/commit-message/SKILL.md +82 -0
  65. package/kit/skills/commit-message/references/examples.md +8 -0
  66. package/kit/skills/confidence-check/SKILL.md +86 -0
  67. package/kit/skills/crew-board/SKILL.md +55 -0
  68. package/kit/skills/crew-brainstorm/SKILL.md +19 -0
  69. package/kit/skills/crew-code-review/SKILL.md +151 -0
  70. package/kit/skills/crew-code-review/references/history-search.md +19 -0
  71. package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
  72. package/kit/skills/crew-code-review/references/receiving.md +23 -0
  73. package/kit/skills/crew-doctor/SKILL.md +36 -0
  74. package/kit/skills/crew-gates/SKILL.md +25 -0
  75. package/kit/skills/crew-handoff/SKILL.md +14 -0
  76. package/kit/skills/crew-plan/SKILL.md +18 -0
  77. package/kit/skills/crew-review/SKILL.md +23 -0
  78. package/kit/skills/crew-ship/SKILL.md +19 -0
  79. package/kit/skills/crew-skill/SKILL.md +61 -0
  80. package/kit/skills/crew-studio/SKILL.md +129 -0
  81. package/kit/skills/crew-update/SKILL.md +43 -0
  82. package/kit/skills/db-migration/SKILL.md +120 -0
  83. package/kit/skills/db-migration/references/backup-restore.md +33 -0
  84. package/kit/skills/db-migration/references/raw-sql.md +30 -0
  85. package/kit/skills/db-migration/references/tool-matrix.md +35 -0
  86. package/kit/skills/dependency-audit/SKILL.md +61 -0
  87. package/kit/skills/dependency-upgrade/SKILL.md +82 -0
  88. package/kit/skills/deploy/SKILL.md +100 -0
  89. package/kit/skills/deploy/references/proxy-ssl.md +56 -0
  90. package/kit/skills/deploy/references/rollback.md +18 -0
  91. package/kit/skills/deploy/references/scripts.md +9 -0
  92. package/kit/skills/deploy/references/topology-a.md +128 -0
  93. package/kit/skills/deploy/references/topology-b.md +44 -0
  94. package/kit/skills/docs-writer/SKILL.md +53 -0
  95. package/kit/skills/docs-writer/references/coauthoring.md +34 -0
  96. package/kit/skills/eval-grader/SKILL.md +75 -0
  97. package/kit/skills/eval-grader/references/method.md +51 -0
  98. package/kit/skills/frontend/SKILL.md +56 -0
  99. package/kit/skills/frontend/references/verify-contract.md +41 -0
  100. package/kit/skills/frontend-design/SKILL.md +71 -0
  101. package/kit/skills/frontend-design/references/design-review.md +34 -0
  102. package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
  103. package/kit/skills/handoff/SKILL.md +71 -0
  104. package/kit/skills/handoff/references/cold-reader.md +51 -0
  105. package/kit/skills/i18n-integrity/SKILL.md +31 -0
  106. package/kit/skills/incident-runbook/SKILL.md +40 -0
  107. package/kit/skills/incident-runbook/references/postmortem.md +16 -0
  108. package/kit/skills/iterate/SKILL.md +40 -0
  109. package/kit/skills/mcp-builder/SKILL.md +54 -0
  110. package/kit/skills/mcp-builder/references/building.md +71 -0
  111. package/kit/skills/observability/SKILL.md +50 -0
  112. package/kit/skills/performance/SKILL.md +38 -0
  113. package/kit/skills/performance/references/bottlenecks.md +10 -0
  114. package/kit/skills/privacy-compliance/SKILL.md +75 -0
  115. package/kit/skills/red-team/SKILL.md +48 -0
  116. package/kit/skills/reflect/SKILL.md +57 -0
  117. package/kit/skills/release/SKILL.md +44 -0
  118. package/kit/skills/security-scan/SKILL.md +99 -0
  119. package/kit/skills/security-scan/references/ai-agents.md +107 -0
  120. package/kit/skills/security-scan/references/fronts.md +99 -0
  121. package/kit/skills/security-scan/references/prompting.md +23 -0
  122. package/kit/skills/security-scan/references/reporting.md +75 -0
  123. package/kit/skills/security-scan/references/verify.md +131 -0
  124. package/kit/skills/sonarqube-check/SKILL.md +108 -0
  125. package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
  126. package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
  127. package/kit/skills/spec-planning/SKILL.md +92 -0
  128. package/kit/skills/systematic-debugging/SKILL.md +57 -0
  129. package/kit/skills/systematic-debugging/references/techniques.md +44 -0
  130. package/kit/skills/teamboard/SKILL.md +88 -0
  131. package/kit/skills/teamboard/references/setup.md +37 -0
  132. package/kit/skills/testing/SKILL.md +49 -0
  133. package/kit/skills/testing/references/flaky-triage.md +51 -0
  134. package/kit/skills/threat-model/SKILL.md +57 -0
  135. package/kit/skills/threat-model/references/interview.md +51 -0
  136. package/kit/skills/threat-model/references/schema.md +42 -0
  137. package/kit/skills/token-budget/SKILL.md +51 -0
  138. package/kit/skills/trace-scan/SKILL.md +43 -0
  139. package/kit/skills/worktree/SKILL.md +40 -0
  140. package/kit/studio/README.md +266 -0
  141. package/kit/studio/ensure-node.sh +471 -0
  142. package/kit/studio/package.json +14 -0
  143. package/kit/studio/server/hooks/studio-gate.sh +169 -0
  144. package/kit/studio/server/index.js +902 -0
  145. package/kit/studio/server/lib/crew-env.js +11 -0
  146. package/kit/studio/server/lib/fleet.js +146 -0
  147. package/kit/studio/server/lib/graph.js +533 -0
  148. package/kit/studio/server/lib/instance.js +135 -0
  149. package/kit/studio/server/lib/kit-telemetry.js +182 -0
  150. package/kit/studio/server/lib/kit.js +158 -0
  151. package/kit/studio/server/lib/palette.js +118 -0
  152. package/kit/studio/server/lib/peers.js +72 -0
  153. package/kit/studio/server/lib/permissions.js +150 -0
  154. package/kit/studio/server/lib/projects.js +294 -0
  155. package/kit/studio/server/lib/roster.js +191 -0
  156. package/kit/studio/server/lib/session.js +309 -0
  157. package/kit/studio/server/lib/terminal.js +91 -0
  158. package/kit/studio/server/lib/transcript.js +100 -0
  159. package/kit/studio/web/app.js +1098 -0
  160. package/kit/studio/web/canvas.js +1179 -0
  161. package/kit/studio/web/chat.js +543 -0
  162. package/kit/studio/web/index.html +86 -0
  163. package/kit/studio/web/md.js +110 -0
  164. package/kit/studio/web/storage-migrate.js +23 -0
  165. package/kit/studio/web/style.css +1453 -0
  166. package/package.json +46 -8
  167. package/start.sh +748 -0
  168. package/bin/crewforth.js +0 -10
package/start.sh ADDED
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env bash
2
+ # Setup wizard: asks who the install is for, shows a summary and asks for confirmation; then installs the WHOLE
3
+ # kit (./.claude + ./CLAUDE.md); finally deletes kit/ and itself.
4
+ # Every install is identical — there is no frontend/backend/mobile split. Measured before it was removed: the
5
+ # widest profile pruning saved ~400 tokens of listing, while the split cost a per-profile e2e matrix, a second
6
+ # prune path in adopt.sh, and shipped a set the plugin channel never matched. Since 3.0 the backend pattern does
7
+ # not vary either: the stack is decided per project by the backend-architecture skill and recorded in CLAUDE.md.
8
+ # start.sh + kit/ must be in the SAME directory. At the project root: bash start.sh [flags]
9
+ set -euo pipefail
10
+ # The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
11
+ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
12
+ [ -f "$_crew_d/kit/eval/lib/crew-env.sh" ] && . "$_crew_d/kit/eval/lib/crew-env.sh"; unset _crew_d
13
+ HERE="$(CDPATH= cd "$(dirname "$0")" && pwd)"
14
+
15
+ # --version (or -v) is answered first, before the source-checkout and payload checks below: it reads only VERSION, so it
16
+ # must work wherever start.sh sits, including a directory those checks would refuse.
17
+ for a in "$@"; do
18
+ if [ "$a" = "--version" ] || [ "$a" = "-v" ]; then
19
+ if [ -f "$HERE/VERSION" ]; then head -1 "$HERE/VERSION" | tr -d '\r'; else echo unknown; fi
20
+ exit 0
21
+ fi
22
+ done
23
+
24
+ SRC="$HERE/kit"
25
+
26
+ if [ ! -d "$SRC" ]; then
27
+ echo "ERROR: 'kit/' folder not found."
28
+ echo "start.sh and kit/ must be in the SAME directory (both come together when you unzip)."
29
+ exit 1
30
+ fi
31
+
32
+ # Refuse to run inside a checkout of the kit's own source repository. This script ends by deleting
33
+ # kit/ and itself, which is correct when the kit has been unpacked into a project and is being
34
+ # consumed — and destroys the source when someone invokes it by absolute path from somewhere else while
35
+ # developing the kit. That is not hypothetical: it removed 122 tracked files during this kit's own
36
+ # development, recovered only because they were committed.
37
+ #
38
+ # The kit's developer instructions already said "do not run start.sh in this repo". A rule that only holds
39
+ # while someone remembers it is the exact thing this kit exists to replace with a gate, so here is the gate.
40
+ # The three markers together appear in the source repo and in no install: an installed kit has .claude/ and
41
+ # CLAUDE.md, never packaging/ next to a kit/ it has not yet consumed.
42
+ if [ -d "$HERE/packaging" ] && [ -d "$HERE/.git" ] && [ -f "$HERE/VERSION" ]; then
43
+ if [ "${CREW_ALLOW_SOURCE_INSTALL:-0}" = 1 ]; then
44
+ echo "WARNING: CREW_ALLOW_SOURCE_INSTALL=1 — installing from Crewforth's own source checkout."
45
+ echo " $SRC and this script will be deleted when the install finishes."
46
+ else
47
+ echo "ERROR: this is Crewforth's own source repository, not a project to install into."
48
+ echo " Running here would delete $SRC and this script at the end — that is what the installer does."
49
+ echo " To try the installer, copy this checkout somewhere else first:"
50
+ echo " cp -R \"$HERE\" /tmp/kit-trial && cd /tmp/kit-trial && bash start.sh"
51
+ echo " Set CREW_ALLOW_SOURCE_INSTALL=1 if you really mean to consume this checkout."
52
+ exit 1
53
+ fi
54
+ fi
55
+
56
+ # ---- CREW-I18N ------------------------------------------------------------------------------------------
57
+ # The installer speaks the user's language; the artefacts it writes do not.
58
+ #
59
+ # THE ENGLISH STRING IS THE KEY. `m 'Cancelled — nothing changed.'` looks that text up and prints the
60
+ # translation, or the text itself when there is none. Three things fall out of that and they are why it is
61
+ # written this way rather than with invented keys like `msg.cancelled`:
62
+ # * A missing translation cannot produce a blank line or a bare key — the fallback IS English, structurally.
63
+ # * The English text stays in the source, so a grep for it (the suite has two) keeps matching.
64
+ # * The call site says what it prints. `m msg.cancelled` does not.
65
+ # The cost is that editing an English string silently drops its translation and the line reverts to English.
66
+ # That is a regression, not a break, and it is catchable: every pattern in the table below must match a
67
+ # string that occurs in this file.
68
+ #
69
+ # RULES FOR THE TABLE, each one paid for:
70
+ # * No colour and no escape codes inside a message. Colour lives in the helpers (h1/sub/add/...), which
71
+ # take an already-translated string. A translator copying an ANSI sequence is a translator breaking it.
72
+ # * Interpolated values go through %s, never concatenation — Turkish word order and suffixes differ.
73
+ # * A literal percent must be written %% because the message IS the printf format. "%100 yerel" would
74
+ # otherwise eat an argument.
75
+ #
76
+ # NOT TRANSLATED, deliberately: the source-repo refusal above and --version. Both answer before the flags
77
+ # are parsed, and language selection cannot run ahead of them without putting a locale lookup in front of a
78
+ # gate whose whole job is to refuse. A gate that parses a locale before it can say no is a worse gate.
79
+ # The inherited CREW_LANG is captured before the working variable is cleared — otherwise this very line
80
+ # would destroy the environment setting it is meant to read.
81
+ CREW_LANG_ENV="${CREW_LANG:-}"
82
+ CREW_LANG=""
83
+ # `_mt` puts the result in _M with printf -v, so a call site pays no subshell: `$(m …)` is a FORK, and on Git
84
+ # Bash a fork is ~50 ms. Measured on Windows: the $(m …) form added 22 forks and ~1 s to one install; adopt.sh's
85
+ # printf -v twin removed them. `m` stays as a thin wrapper for the rare nested case that needs a value inline.
86
+ m() { _mt "$@"; printf '%s' "$_M"; }
87
+ _mt() { # $1 = English text (the key); further args fill %s; result in _M
88
+ # An empty key must still ASSIGN: bash 3.2's `printf -v _M ""` leaves _M holding the previous translation.
89
+ [ -n "${1:-}" ] || { _M=""; return 0; }
90
+ local s="$1"; shift
91
+ if [ "$CREW_LANG" = tr ]; then
92
+ case "$s" in
93
+ "Crewforth · setup wizard") s='Crewforth · kurulum' ;;
94
+ "stack-agnostic") s='her yığınla çalışır' ;;
95
+ "[1/2] Who is this install for?") s='[1/2] Kurulumu kim kullanacak?' ;;
96
+ "Decides whether your teammates get Crewforth's configuration — and what goes into .gitignore.") s="Crewforth ayarlarının ekiple paylaşılıp paylaşılmayacağını ve .gitignore'a nelerin ekleneceğini belirler." ;;
97
+ "Just me") s='Yalnızca ben' ;;
98
+ "private") s='kişisel' ;;
99
+ "The whole team") s='Tüm ekip' ;;
100
+ "shared") s='paylaşımlı' ;;
101
+ ".claude/ and CLAUDE.md stay out of git — nothing appears in your teammates' checkouts") s=".claude/ ve CLAUDE.md git'e girmez — ekip arkadaşlarınız hiçbir şey görmez" ;;
102
+ ".claude/ and CLAUDE.md are committable — everyone gets the same agents, skills and gates") s=".claude/ ve CLAUDE.md commit'lenebilir — herkes aynı ajanlarla, skill'lerle ve kapılarla çalışır" ;;
103
+ "internal working documents (docs/) stay private in BOTH answers") s='iç çalışma belgeleri (docs/) HER İKİ seçenekte de dışarıda kalır' ;;
104
+ "[2/2] Summary · see what will be installed before you confirm") s='[2/2] Özet · onaylamadan önce neyin kurulacağına bakın' ;;
105
+ "Security gates armed on every install:") s='Her kurulumda devreye giren güvenlik kapıları:' ;;
106
+ "commit/push approval gate — even in auto/bypass mode (guard-bash)") s='commit ve push için onay şartı — auto/bypass modunda bile (guard-bash)' ;;
107
+ "trace scan — a git hook blocks AI traces / vendor names") s="iz taraması — AI izlerini ve sağlayıcı adlarını bir git hook'u yakalar" ;;
108
+ "real context measurement + handoff at 75%% (Stop hook)") s="bağlam doluluğu gerçekten ölçülür, %%75'te devir önerilir (Stop hook)" ;;
109
+ "destructive command guard (rm -rf / force-push, etc.)") s='yıkıcı komut koruması (rm -rf, force-push vb.)' ;;
110
+ "Install with these settings?") s='Bu ayarlarla kurayım mı?' ;;
111
+ "Cancelled — nothing changed.") s='İptal edildi, hiçbir şey değişmedi.' ;;
112
+ "Choice") s='Seçiminiz' ;;
113
+ "empty=1") s='boş=1' ;;
114
+ "(default)") s='(varsayılan)' ;;
115
+ "Scope") s='Kapsam' ;;
116
+ "Included") s='Kurulacaklar' ;;
117
+ "Backend pattern") s='Mimari' ;;
118
+ "stack-agnostic — the stack comes from CLAUDE.md ## Stack or the repo (backend-architecture)") s="yığından bağımsız — yığın CLAUDE.md ## Stack bölümünden ya da depodan okunur (backend-architecture)" ;;
119
+ "The --dotnet flag is ignored: the .NET-specific path was removed in 3.0; installing the stack-agnostic setup.") s="Bayrak yok sayıldı (--dotnet): .NET'e özel kurulum yolu 3.0'da kaldırıldı; yığından bağımsız kurulum yapılıyor." ;;
120
+ "Will write") s='Yazılacaklar' ;;
121
+ "Install visibility:") s='Kurulum türü:' ;;
122
+ "full install") s='tam kurulum' ;;
123
+ "no effect:") s='etkisi yok:' ;;
124
+ "Installing:") s='Kuruluyor:' ;;
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." ;;
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
+ "./CLAUDE.md created — EDIT the project section.") s='./CLAUDE.md oluşturuldu — proje bölümünü sizin DOLDURMANIZ gerekiyor.' ;;
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.' ;;
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
+ "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
+ "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
+ "— 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' ;;
136
+ "(shared: .claude/ and CLAUDE.md stay committable)") s="(paylaşımlı: .claude/ ve CLAUDE.md commit'lenebilir kalır)" ;;
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
+ "no") s='hayır' ;;
139
+ "yes") s='evet' ;;
140
+ "[yes/no]") s='[evet/hayır]' ;;
141
+ "Crewforth always installs in full (all agents · all skills).") s="Crewforth her zaman eksiksiz kurulur (tüm ajanlar · tüm skill'ler)." ;;
142
+ "2 steps: who it is for -> summary & confirm.") s='2 adım: kim kullanacak -> özet ve onay.' ;;
143
+ "AGENT_TEMPLATE.md missing from the payload — /crew-skill will have nothing to read.") s='AGENT_TEMPLATE.md pakette yok — /crew-skill okuyacak bir şablon bulamayacak.' ;;
144
+ "./CLAUDE.md kept as-is (already imports the discipline) — the refresh landed in DISCIPLINE.md.") s="./CLAUDE.md'ye dokunulmadı (disiplini zaten import ediyor); güncelleme DISCIPLINE.md'ye yazıldı." ;;
145
+ "! ./CLAUDE.md carries the discipline INLINE (pre-1.1 layout) — left untouched.") s='! ./CLAUDE.md disiplini dosyanın İÇİNDE taşıyor (1.1 öncesi düzen) — dokunulmadı.' ;;
146
+ "Discipline updates will NOT reach it. To migrate: delete everything above your") s='Disiplin güncellemeleri bu dosyaya ULAŞMAZ. Geçiş için şu başlığın üstündeki her şeyi silin:' ;;
147
+ "%s heading and leave this single line in its place:") s='%s — sildiğiniz yere de yalnızca şu satırı koyun:' ;;
148
+ "./CLAUDE.md existed — prepended the discipline @import; your content is untouched.") s='./CLAUDE.md zaten vardı — en başa disiplinin @import satırı eklendi, içeriğinize dokunulmadı.' ;;
149
+ "%s eol pin(s) so shared hooks stay LF") s="%s eol kuralı eklendi; paylaşılan hook'lar LF olarak kalır" ;;
150
+ "NOTE: no git repository at this level; after %s run: %s") s='NOT: bu klasörde git deposu yok. %s yaptıktan sonra şunu çalıştırın: %s' ;;
151
+ "Panel: needs Node 18+, which is not on this machine — but that is no longer a dead end.") s='Panel: Node 18+ gerekiyor ve bu makinede yok — ama bunun da bir çözümü var.' ;;
152
+ "Crewforth fetches one for the panel: %s (asks first;") s='Crewforth, panel için Node indirebilir: %s (önce sorar;' ;;
153
+ "verified against the published checksum, into %s, nothing else touched).") s='yayımlanan checksum ile doğrular, yalnızca %s içine kurar, başka hiçbir şeye dokunmaz).' ;;
154
+ "Every gate still holds meanwhile; the panel is the only part that needs node.") s="Bu arada tüm kapılar çalışmaya devam eder; Node'a yalnızca panel ihtiyaç duyar." ;;
155
+ "ERROR: the %s sentinel line is missing from %s — refusing to guess the discipline/project split.") s='HATA: %s işaret satırı %s içinde bulunamadı — disiplinin nerede bitip proje bölümünün nerede başladığı tahmin edilmeyecek.' ;;
156
+ "Unknown parameter: %s") s='Bilinmeyen parametre: %s' ;;
157
+ ".gitignore") ;; # identifier, printed as is
158
+ # No row: the line prints in English. CREW_I18N_MISS (set by e2e case 18) collects every such key, so a
159
+ # missing translation is caught by NAME rather than guessed from which English words it happens to contain.
160
+ *) [ -n "${CREW_I18N_MISS:-}" ] && printf '%s\n' "$s" >> "$CREW_I18N_MISS" ;;
161
+ esac
162
+ fi
163
+ # shellcheck disable=SC2059
164
+ # `--` ends option parsing: without it ANY format starting with '-' ("--dotnet …", "- x") is read as an option
165
+ # and printf exits 2 — measured on bash 3.2 (macOS) and Git Bash 5.3; with it both assign normally.
166
+ printf -v _M -- "$s" "$@"
167
+ }
168
+ # ---- /CREW-I18N -----------------------------------------------------------------------------------------
169
+
170
+ usage() {
171
+ # A heredoc cannot go through m() line by line without breaking its layout, so the Turkish help is its own
172
+ # block. Flags and commands are identical in both; only the prose differs.
173
+ if [ "${CREW_LANG:-}" = tr ]; then
174
+ cat <<'USAGE_TR'
175
+ Kullanım: bash start.sh [SEÇENEKLER]
176
+ Seçenek vermezseniz kurulum sihirbazı her şeyi adım adım sorar.
177
+
178
+ Backend her zaman yığından bağımsızdır: yığın projeye göre backend-architecture skill'iyle seçilir ve
179
+ CLAUDE.md'deki ## Stack bölümüne yazılır.
180
+ --dotnet 3.0'da kaldırıldı; kabul edilir, uyarı basar ve yığından bağımsız kurulumu yapar
181
+ --generic kabul edilir, etkisi yok (artık tek kurulum biçimi bu)
182
+
183
+ Crewforth her zaman eksiksiz kurulur: tüm ajanlar ve skill'ler — backend, web ve mobil (RN/Expo).
184
+ --backend | --frontend | --mobile | --fullstack hâlâ kabul edilir ama etkisi yok (eski komutlar bozulmasın diye)
185
+ --private | --shared kurulum yalnızca sizin mi, yoksa ekiple paylaşılıp commit'lenecek mi? (varsayılan: private)
186
+ --lang tr|en kurulum dili (verilmezse sihirbaz sorar)
187
+ --yes, -y tüm sorulara evet de (gözetimsiz kurulum)
188
+ --version, -v Crewforth sürümünü yazdırıp çık
189
+ USAGE_TR
190
+ return
191
+ fi
192
+ cat <<'USAGE'
193
+ Usage: bash start.sh [OPTIONS]
194
+ If no flag is given, the script asks interactively (wizard).
195
+ The backend is always stack-agnostic: the stack is chosen per project by the backend-architecture skill and
196
+ recorded in the ## Stack section of CLAUDE.md.
197
+ --dotnet removed in 3.0; accepted, prints a warning and installs the stack-agnostic setup
198
+ --generic accepted, no effect (this is the only install shape now)
199
+
200
+ Every install ships everything: all agents, all skills — backend, web and mobile (RN/Expo) together.
201
+ --backend | --frontend | --mobile | --fullstack accepted, no effect (kept so older commands still run)
202
+ --private | --shared is the install yours alone, or committed for the team? (default: private)
203
+ --lang tr|en installer language (asked interactively when not given)
204
+ --yes, -y answer every question with yes (unattended install)
205
+ --version, -v print the Crewforth version and exit
206
+ USAGE
207
+ }
208
+
209
+ # Read one answer without hanging an unattended run — and be honest about which hang this closes.
210
+ #
211
+ # Four stdin shapes, measured (bare `read`, perl alarm as the clock, each shape calibrated first):
212
+ # a terminal with a human ....... blocks until they type <- correct, must not change
213
+ # a terminal with NOBODY ........ blocks forever <- the Claude Code pty case
214
+ # a pipe carrying data .......... returns at once
215
+ # a pipe at EOF (</dev/null) .... returns at once, answer ""
216
+ # a pipe open and empty ......... blocks forever <- closed by the timeout below
217
+ #
218
+ # The timeout applies ONLY when stdin is not a terminal. On a terminal a human may take as long as they
219
+ # like, and cutting them off would be a worse bug than the one being fixed.
220
+ #
221
+ # WHAT THIS DOES NOT FIX, stated plainly because the opposite was nearly written here: a pty IS a terminal,
222
+ # so `[ -t 0 ]` is TRUE for it and this takes the blocking branch. Measured under expect: `[ -t 0 ]` says
223
+ # yes and the read blocks with no input. An unattended run under a pty — which is what Claude Code creates —
224
+ # is therefore still answered by --yes and by nothing else. That is not a gap in the timeout; stdin carries
225
+ # no signal that distinguishes "a terminal nobody is watching" from "a terminal with a slow typist".
226
+ #
227
+ # 10 seconds rather than 5: the cost of being too short is a declined install, which is visible and
228
+ # recoverable, but a producer that legitimately takes a moment to write the answer should still win. The
229
+ # value is an integer because bash 3.2 rejects a fractional -t ("invalid timeout specification", measured).
230
+ crew_read() { # $1 = name of the variable to set
231
+ local __v="$1" __a=""
232
+ if [ -t 0 ]; then read -r __a || __a=""
233
+ else read -t 10 -r __a || __a=""
234
+ fi
235
+ eval "$__v=\$__a"
236
+ }
237
+ ask_yes() { # $1 = question; returns 0 if the user says 'yes'
238
+ local a
239
+ # --yes ALWAYS wins, and it is answered BEFORE stdin is touched at all. Claude Code runs the installer
240
+ # under a pty, so stdin IS a terminal there and a bare `read` blocks forever on input nobody will type:
241
+ # testing the terminal first would ignore a --yes that was passed precisely to avoid that. adopt.sh:50
242
+ # carries the same rule for the same reason, and this script was the one place that never learned it.
243
+ if [ "${ASSUME_YES:-0}" = 1 ]; then _mt 'yes'; printf '%s %s %s(--yes)%s\n' "$1" "$_M" "$D" "$R"; return 0; fi
244
+ # Deliberately NOT adopt.sh's `[ -t 0 ]` shape. adopt.sh declines outright when stdin is not a terminal;
245
+ # here `printf 'yes\n' | bash start.sh` is the documented CI form (see the note at the confirm prompt) and
246
+ # that shape would silently turn every piped install into a cancellation. A pipe reaching EOF already
247
+ # answers "" => no, so the unattended case stays safe without special-casing it.
248
+ _mt '[yes/no]'; printf '%s %s: ' "$1" "$_M"
249
+ crew_read a
250
+ case "$a" in [yY]|[yY][eE][sS]|[eE]|[eE][vV][eE][tT]) return 0 ;; *) return 1 ;; esac
251
+ }
252
+ # Append entries to .gitignore. Three callers had three copies of the same two bugs (start.sh's four-entry
253
+ # loop, and adopt.sh's review-pass.json line), so it lives here and adopt.sh carries the twin.
254
+ # * A file whose last line has NO trailing newline concatenates the first appended entry onto it —
255
+ # `node_modules` + `.claude/` becomes `node_modules.claude/`, ignoring neither. `touch` does not help:
256
+ # it changes the timestamp, not the last byte. So read the last byte and add the newline ourselves.
257
+ # * `grep -qxF` is an exact-literal test, so a repo that already ignores `.claude` (no trailing slash)
258
+ # gets a second, redundant line. Ask git the real question instead — `git check-ignore` answers about
259
+ # the PATH, whatever spelling the existing rule uses. It is only asked inside a repo; outside one we
260
+ # fall back to the literal test, which is all that is knowable there.
261
+ gi_add() { # $@ = entries to ensure in ./.gitignore; prints nothing, sets GI_WROTE to what it added
262
+ local e
263
+ GI_WROTE=""
264
+ [ -e .gitignore ] || : > .gitignore
265
+ for e in "$@"; do
266
+ if git rev-parse --git-dir >/dev/null 2>&1; then
267
+ git check-ignore -q --no-index "$e" 2>/dev/null && continue # --no-index: a dir holding tracked files (docs/HANDOVER.md) still counts as ignored
268
+ else
269
+ grep -qxF "$e" .gitignore 2>/dev/null && continue
270
+ fi
271
+ # last byte is not a newline (and the file is not empty) -> close the line first
272
+ if [ -s .gitignore ] && [ "$(tail -c 1 .gitignore | od -An -tx1 | tr -d ' \n')" != "0a" ]; then
273
+ printf '\n' >> .gitignore
274
+ fi
275
+ printf '%s\n' "$e" >> .gitignore
276
+ GI_WROTE="$GI_WROTE $e"
277
+ done
278
+ GI_WROTE="${GI_WROTE# }"
279
+ }
280
+ # Append eol pins to .gitattributes. ONLY in shared mode, and only for what cannot defend itself.
281
+ #
282
+ # MEASURED, not inferred — the ROADMAP called this an inference. A bare repo, a project that TRACKS .claude/,
283
+ # and a second clone with core.autocrlf=true (a git setting, so the mechanism reproduces anywhere):
284
+ # the committed blob 0 CR
285
+ # the working tree after that checkout 1345 CR in guard-bash.sh · 575 in pre-commit · 49 in commit-msg
286
+ # Reproduced on a REAL Windows machine, both installers, all three autocrlf settings, with the pin and without
287
+ # it — the numbers above are that run's, not the simulation's.
288
+ # WHO THE VICTIM IS, corrected after a real Windows measurement, because the obvious answer is wrong.
289
+ # A CRLF hook does NOT die on Git Bash: the 1345-CR copy from an unpinned clone was run against a destructive
290
+ # payload through the real invocation and answered exactly like the LF copy — rc=2, same GUARD line. The kit's
291
+ # own .gitattributes already records this ("Git Bash happens to tolerate that … but WSL does not"). The death
292
+ # shapes below are real but were measured on macOS bash, and macOS never receives CRLF from autocrlf=true in
293
+ # the first place, since that is a Windows default:
294
+ # ./hook -> env: bash\r: No such file or directory
295
+ # bash hook -> syntax error: unexpected end of file
296
+ # case … in\r -> syntax error near unexpected token `newline`
297
+ # f(){\r -> syntax error near unexpected token `{`
298
+ # So what the pin protects is a NON-MSYS bash reading that same Windows working tree — WSL is the documented
299
+ # case, and it is UNMEASURED by either of us: `wsl.exe` resolves on the Windows desk with no distro installed,
300
+ # which is the Store-python3 shape and not evidence. What IS measured is that the pin removes a difference
301
+ # nobody should have to reason about: with it the working tree matches the blob on every autocrlf setting.
302
+ #
303
+ # AND IT PROTECTS AGAINST EXACTLY ONE SETTING. Unpinned, `core.autocrlf=input` and `=false` already come back
304
+ # with 0 CR; only `true` corrupts. That one is the Git for Windows default and it arrives from the SYSTEM
305
+ # config, not the global one — so the person the pin is for is the person who changed nothing.
306
+ #
307
+ # WHY ONLY THE SCRIPTS. The data files the hooks read line by line (blocklists, profiles.conf) already strip a
308
+ # trailing CR themselves — the kit's own .gitattributes says so, and calls that strip "the real defence" for
309
+ # exactly this case, a user's project where nothing pins anything. A shell script cannot do that: it cannot
310
+ # strip its own carriage returns before bash parses it. So the pins below are the scripts plus, belt and
311
+ # braces, the data types that are cheap to include; they are deliberately NOT a blanket `.claude/**`, which
312
+ # would apply text conversion to any binary a skill might carry.
313
+ #
314
+ # WHY ONLY SHARED MODE. In the private install .claude/ is gitignored, so git never checks it out and there is
315
+ # nothing to convert. Writing repo-wide attributes for someone who did not share their config would be editing
316
+ # a file they own to fix a problem they do not have.
317
+ #
318
+ # The two lessons gi_add carries apply here too — a missing trailing newline concatenates the first entry onto
319
+ # the last line, and the real question is not "is this text in the file" but "does git already answer lf for
320
+ # this path". `git check-attr` is the equivalent of gi_add's `git check-ignore`: it answers about the PATH,
321
+ # whatever pattern spelling an existing rule uses, so a project that already pins `* text eol=lf` gets nothing.
322
+ GA_LINES='.claude/**/*.sh text eol=lf
323
+ .claude/hooks/pre-commit text eol=lf
324
+ .claude/hooks/commit-msg text eol=lf
325
+ .claude/**/*.txt text eol=lf
326
+ .claude/**/*.conf text eol=lf'
327
+ ga_add() { # no args; prints nothing, sets GA_WROTE to the number of lines added
328
+ local line
329
+ GA_WROTE=0
330
+ if git rev-parse --git-dir >/dev/null 2>&1; then
331
+ case "$(git check-attr eol -- .claude/hooks/guard-bash.sh 2>/dev/null)" in
332
+ *": lf") return 0 ;;
333
+ esac
334
+ fi
335
+ [ -e .gitattributes ] || : > .gitattributes
336
+ printf '%s\n' "$GA_LINES" | while IFS= read -r line; do
337
+ [ -n "$line" ] || continue
338
+ if grep -qxF "$line" .gitattributes 2>/dev/null; then continue; fi
339
+ if [ -s .gitattributes ] && [ "$(tail -c 1 .gitattributes | od -An -tx1 | tr -d ' \n')" != "0a" ]; then
340
+ printf '\n' >> .gitattributes
341
+ fi
342
+ printf '%s\n' "$line" >> .gitattributes
343
+ done
344
+ # The loop above runs in a subshell (it is the right-hand side of a pipe), so it cannot report back through a
345
+ # variable — the same trap this kit spent a night on. Count the result from the FILE instead.
346
+ GA_WROTE="$(printf '%s\n' "$GA_LINES" | while IFS= read -r line; do
347
+ [ -n "$line" ] && grep -qxF "$line" .gitattributes 2>/dev/null && echo x
348
+ done | wc -l | tr -d ' ')"
349
+ }
350
+ # --- CLAUDE.md split (shared contract with adopt.sh) ---
351
+ # The payload CLAUDE.md carries the kit discipline, then a one-line sentinel, then the project template.
352
+ # The discipline half is installed as .claude/DISCIPLINE.md (kit-owned, overwritten on every update) and
353
+ # the project half becomes ./CLAUDE.md (yours, written once). A single @import line joins them.
354
+ IMPORT_LINE='@.claude/DISCIPLINE.md'
355
+ # The sentinel is matched ANCHORED to the start of the line, so prose that merely mentions the token
356
+ # (in this comment, in the docs, in the discipline text itself) can never be mistaken for the split point.
357
+ # Abort loudly if it is gone: a silent miss would ship the ENTIRE template as "discipline" — exactly how the
358
+ # old '<PROJE ADI>' marker failed once the payload was translated to English.
359
+ kit_require_sentinel() { grep -qE '^<!-- KIT:DISCIPLINE-END' "$1" || { printf '%s\n' "$(m 'ERROR: the %s sentinel line is missing from %s — refusing to guess the discipline/project split.' "'<!-- KIT:DISCIPLINE-END'" "$1")"; exit 1; }; }
360
+ kit_discipline_of() { awk '/^<!-- KIT:DISCIPLINE-END/{exit} {print}' "$1"; }
361
+ kit_project_of() { awk 'f{print} /^<!-- KIT:DISCIPLINE-END/{f=1}' "$1"; }
362
+ # Anchored: the import must BE the line, not merely be mentioned in prose (the discipline text names the path).
363
+ kit_has_import() { grep -qE '^[[:space:]]*@\.claude/DISCIPLINE\.md[[:space:]]*$' "$1" 2>/dev/null; }
364
+ # A pre-1.1 install pasted the whole discipline inline into CLAUDE.md; adding the @import would load it twice.
365
+ # Both markers are required: a project that happens to write its own "Four working principles" heading is NOT a
366
+ # legacy kit install, and treating it as one would leave it without the discipline forever.
367
+ kit_claude_md_is_legacy() {
368
+ grep -q '^## Four working principles' "$1" 2>/dev/null && grep -qE '^### 4\.[45] ' "$1" 2>/dev/null
369
+ }
370
+ # --- Flag parsing (silent/CI mode) ---
371
+ # The profile flags are ACCEPTED and ignored rather than rejected: they appear in older READMEs, CI steps and
372
+ # copy-pasted commands, and erroring out there breaks a pipeline over a flag whose absence changes nothing.
373
+ # A one-line notice is printed after the colour helpers load, so the user learns the flag no longer selects
374
+ # anything instead of quietly getting a different set than the one they typed.
375
+ # Language is resolved BEFORE any other flag, because every message below it goes through m(). Four
376
+ # sources, first answer wins: --lang, then CREW_LANG, then the locale variables, then English.
377
+ #
378
+ # English is the default rather than the locale's language on purpose: that is what this installer printed
379
+ # before it could speak anything else, and a default that changes under people is not a default. The locale
380
+ # branch only ever ADDS Turkish for someone whose environment already says Turkish.
381
+ #
382
+ # Measured, and recorded here rather than treated as a defect: on stock Windows LANG, LC_ALL and
383
+ # LC_MESSAGES are ALL empty (Git Bash defaults only LC_CTYPE). So auto-detect never fires there, and a
384
+ # Turkish-speaking Windows user lands on English unless they pass --lang tr or export CREW_LANG.
385
+ #
386
+ # AN INTERACTIVE INSTALL ASKS. Detection alone was not enough: on macOS the system language can be Turkish
387
+ # while the shell exports LANG=C.UTF-8 (measured on a Turkish desk), so the locale said English and the user
388
+ # never saw a choice. So when a human is at the terminal and nothing named a language (no --lang, no
389
+ # CREW_LANG), the first thing printed is a two-line menu, and the locale only decides which entry is the
390
+ # default. It is skipped under --yes and whenever stdin is not a terminal — for the same reason the
391
+ # visibility question is: every piped caller feeds a fixed answer sequence, and one more read would shift it.
392
+ _lang_flag=""; _lang_take=0; _lang_yes=0
393
+ for a in "$@"; do
394
+ # `--lang` with no value must not swallow the next flag: `--lang --yes` is --yes with no language given.
395
+ if [ "$_lang_take" = 1 ]; then _lang_take=0; case "$a" in -*) ;; *) _lang_flag="$a"; continue ;; esac; fi
396
+ case "$a" in
397
+ --lang=*) _lang_flag="${a#--lang=}" ;;
398
+ --lang) _lang_take=1 ;;
399
+ --yes|-y) _lang_yes=1 ;;
400
+ -h|--help|-v|--version) _lang_yes=1 ;; # help and version answer at once, not after a language menu
401
+ esac
402
+ done
403
+ _loc="${LC_ALL:-}"; [ -n "$_loc" ] || _loc="${LC_MESSAGES:-}"; [ -n "$_loc" ] || _loc="${LANG:-}"
404
+ case "$_loc" in tr*|TR*) _lang_detected=tr ;; *) _lang_detected=en ;; esac
405
+ if [ -n "$_lang_flag" ]; then
406
+ CREW_LANG="$_lang_flag"
407
+ elif [ -n "${CREW_LANG_ENV:-}" ]; then
408
+ CREW_LANG="$CREW_LANG_ENV"
409
+ elif [ -t 0 ] && [ "$_lang_yes" = 0 ]; then
410
+ if [ "$_lang_detected" = tr ]; then _lang_def=2; else _lang_def=1; fi
411
+ printf '\n Language / Dil\n 1) English\n 2) Türkçe\n -> [1-2, empty/boş=%s]: ' "$_lang_def"
412
+ crew_read _lang_ans
413
+ case "${_lang_ans:-$_lang_def}" in
414
+ 2|tr|TR|t|T|[tT]ürkçe|[tT]urkce|[tT]urkish) CREW_LANG=tr ;;
415
+ *) CREW_LANG=en ;;
416
+ esac
417
+ else
418
+ CREW_LANG="$_lang_detected"; _LANG_GUESSED=1
419
+ fi
420
+ # Anything that is not a language we actually carry falls back to English rather than printing keys.
421
+ case "$CREW_LANG" in tr|en) ;; *) CREW_LANG=en ;; esac
422
+ # Exported because eval/preflight.sh runs as a child and resolves its own language from the environment: a
423
+ # choice made by --lang or the menu above stayed in this shell, and the preflight block printed English.
424
+ export CREW_LANG
425
+
426
+ STACK="generic"; DOTNET_FLAG=0; LEGACY_FLAGS=""; ASSUME_YES=0; VISIBILITY=""
427
+ for a in "$@"; do
428
+ case "$a" in
429
+ --lang) ;; # value consumed in the language pass above
430
+ --lang=*) ;;
431
+ tr|en) ;; # the value of a separated --lang
432
+ --backend|--frontend|--mobile|--fullstack) LEGACY_FLAGS="$LEGACY_FLAGS $a" ;;
433
+ --dotnet) DOTNET_FLAG=1 ;; # removed in 3.0 — warned below, never an error (old scripts keep running)
434
+ --generic) ;; # the only shape now; accepted silently
435
+ --yes|-y) ASSUME_YES=1 ;;
436
+ --private) VISIBILITY="private" ;;
437
+ --shared) VISIBILITY="shared" ;;
438
+ -h|--help) usage; exit 0 ;;
439
+ *) printf '%s\n' "$(m 'Unknown parameter: %s' "$a")"; echo; usage; exit 1 ;;
440
+ esac
441
+ done
442
+
443
+ # ===================== COLOR / STYLE HELPERS =====================
444
+ # Color is emitted ONLY on an interactive TTY + TERM!=dumb + NO_COLOR empty.
445
+ # Otherwise all codes are '' => raw \033 does NOT leak in CI/pipe/dumb (NO_COLOR is respected).
446
+ if [ -t 1 ] && [ "${TERM:-dumb}" != "dumb" ] && [ -z "${NO_COLOR:-}" ]; then
447
+ R=$'\033[0m'; B=$'\033[1m'; D=$'\033[2m'
448
+ CY=$'\033[36m'; GR=$'\033[32m'; YE=$'\033[33m'; MG=$'\033[35m'
449
+ else
450
+ R=''; B=''; D=''; CY=''; GR=''; YE=''; MG=''
451
+ fi
452
+ # printf's %-Ns pads by BYTES, so a Turkish label (İ, ç, ı are two bytes each) came out short and knocked
453
+ # its column out of line. Pad by characters instead: drop the UTF-8 continuation bytes under the C locale
454
+ # and count what is left. Builtins only — no fork per row.
455
+ padr() { # $1 = text, $2 = width; sets PADDED
456
+ local LC_ALL=C n
457
+ n="${1//[$'\200'-$'\277']/}"; n=$(( $2 - ${#n} ))
458
+ PADDED="$1"
459
+ while [ "$n" -gt 0 ]; do PADDED="$PADDED "; n=$((n-1)); done
460
+ }
461
+ # The helpers below take the ENGLISH key and translate it themselves, so no call site needs a $(m …) fork.
462
+ h1() { _mt "$@"; printf '\n%s%s%s%s\n' "$B" "$CY" "$_M" "$R"; } # section heading
463
+ sub() { _mt "$@"; printf '%s%s%s\n' "$D" "$_M" "$R"; } # dim description
464
+ opt() { # $1=no $2=label $3=is_default $4=right-badge
465
+ local mark='' lbl; [ "${3:-0}" = 1 ] && { _mt '(default)'; mark=" ${GR}${B}${_M}${R}"; }
466
+ _mt "${4:-}"; local badge="$_M"; _mt "$2"; lbl="$_M"
467
+ padr "$lbl" 24
468
+ printf ' %s%s%s)%s %s%s%s %s%s%s%s\n' "$B" "$YE" "$1" "$R" "$B" "$PADDED" "$R" "$MG" "$badge" "$R" "$mark"
469
+ }
470
+ add() { _mt "$@"; printf ' %s+%s %s\n' "$GR" "$R" "$_M"; } # INSTALLED
471
+ skip() { _mt "$@"; printf ' %s-%s %s%s%s\n' "$YE" "$R" "$D" "$_M" "$R"; } # NOT INSTALLED (tradeoff)
472
+ gate() { _mt "$@"; printf ' %s>%s %s\n' "$CY" "$R" "$_M"; } # gate to be armed
473
+ row() { _mt "$1"; padr "$_M" 15; printf ' %s%s%s %s\n' "$B" "$PADDED" "$R" "$2"; } # summary row; $1 = key
474
+ rule() { printf ' %s------------------------------------------------%s\n' "$D" "$R"; }
475
+
476
+ h1 'Crewforth · setup wizard'
477
+ sub '2 steps: who it is for -> summary & confirm.'
478
+ if [ -n "$LEGACY_FLAGS" ]; then
479
+ _mt 'no effect:'; _a="$_M"; _mt 'Crewforth always installs in full (all agents · all skills).'
480
+ printf '\n %s!%s%s %s%s %s\n' "$YE" "$R" "$B$LEGACY_FLAGS" "$_a" "$R" "$_M"
481
+ fi
482
+
483
+ # --dotnet used to pick a .NET-only install (a pattern skill, a backend agent variant, and a base project
484
+ # cloned from the network). 3.0 removed that path: every install is the same stack-agnostic kit, and the
485
+ # backend-architecture skill decides the stack per project. The flag stays ACCEPTED so an old script or README
486
+ # command keeps working — but it says what it no longer does, rather than silently meaning something else.
487
+ if [ "$DOTNET_FLAG" = 1 ]; then
488
+ # This key once started with '--' and `printf -v _M "$s"` read it as an option (rc=2, nothing installed); _mt
489
+ # now passes `--`, so a leading '-' is safe — the wording stays as it is.
490
+ _mt 'The --dotnet flag is ignored: the .NET-specific path was removed in 3.0; installing the stack-agnostic setup.'
491
+ printf '\n %s!%s %s\n' "$YE" "$R" "$_M"
492
+ fi
493
+
494
+ # ===================== STEP 1 · WHO IS THIS INSTALL FOR =====================
495
+ # ONE question about intent, not four about paths. Until now the installer wrote four .gitignore entries
496
+ # unconditionally and the summary never mentioned .gitignore at all — so "confirm the install" silently
497
+ # edited a TRACKED file, which is a change nobody agreed to. The answer decides two of the four entries;
498
+ # the summary below lists the exact lines either way.
499
+ #
500
+ # Two entries are NOT part of the question, and both are guarantees rather than preferences:
501
+ # * .private-terms.txt is the list of strings that must never be published (internal project names,
502
+ # client names, hosts). Publishing the list defeats its purpose, so it is ignored in both answers.
503
+ # * docs/ holds internal working documents — PLAN.md, SESSION_STATE.md, THREAT_MODEL.md,
504
+ # SECURITY_FINDINGS.md, DISCOVERY.md, EVAL.md. §4.3 promises they stay private and README.md says so
505
+ # too; a team that shares its kit config has not asked to publish its threat model.
506
+ #
507
+ # ASKED ONLY WHEN SOMEONE IS THERE TO ANSWER. A new prompt consumes a line of stdin, and every existing
508
+ # non-interactive caller feeds a FIXED sequence — `printf 'yes\n' | bash start.sh --generic` is the form in
509
+ # this repo's own e2e and in user scripts. Adding a read shifts that sequence by one: the visibility question
510
+ # ate the 'yes', the confirm prompt hit EOF, and the install silently CANCELLED. Measured: e2e went from
511
+ # green to rc=127 because the installed tree never existed. So a non-interactive run keeps today's behaviour
512
+ # (private) without reading anything, and --private/--shared are how a script chooses instead.
513
+ if [ -z "$VISIBILITY" ] && { [ ! -t 0 ] || [ "$ASSUME_YES" = 1 ]; }; then
514
+ VISIBILITY="private"
515
+ _mt 'Install visibility:'; _a="$_M"; _mt 'private'; _b="$_M"; _mt '(default — pass --shared to commit .claude/ and CLAUDE.md)'
516
+ printf ' %s%s%s %s %s%s%s\n' "$B" "$_a" "$R" "$_b" "$D" "$_M" "$R"
517
+ fi
518
+ if [ -z "$VISIBILITY" ]; then
519
+ h1 '[1/2] Who is this install for?'
520
+ sub "Decides whether your teammates get Crewforth's configuration — and what goes into .gitignore."
521
+ echo
522
+ opt 1 'Just me' 1 'private'
523
+ add ".claude/ and CLAUDE.md stay out of git — nothing appears in your teammates' checkouts"
524
+ echo
525
+ opt 2 'The whole team' 0 'shared'
526
+ add '.claude/ and CLAUDE.md are committable — everyone gets the same agents, skills and gates'
527
+ skip 'internal working documents (docs/) stay private in BOTH answers'
528
+ echo
529
+ _mt 'Choice'; _a="$_M"; _mt 'empty=1'
530
+ printf ' %s->%s %s %s[1-2, %s]%s: ' "$CY" "$R" "$_a" "$D" "$_M" "$R"
531
+ crew_read s # empty => default (private = today's behaviour)
532
+ case "$s" in 2) VISIBILITY="shared" ;; *) VISIBILITY="private" ;; esac
533
+ fi
534
+ # The exact lines this install will append, resolved once so the summary and the writer cannot disagree.
535
+ if [ "$VISIBILITY" = "shared" ]; then
536
+ GI_PLAN='docs/ .private-terms.txt .claude/.state/' # .state is runtime state (update cache, snapshot), never shared
537
+ else
538
+ GI_PLAN='docs/ .claude/ CLAUDE.md .private-terms.txt'
539
+ fi
540
+
541
+ # ===================== STEP 2 · SUMMARY + CONFIRM =====================
542
+ # Count the agents/skills to install LIVE FROM SOURCE (not a hardcoded constant; self-corrects if the payload changes).
543
+ count_installed() { # $1=glob -> count to install
544
+ local n=0
545
+ for p in $1; do
546
+ [ -e "$p" ] && n=$((n+1))
547
+ done
548
+ printf '%s' "$n"
549
+ }
550
+ N_AG="$(count_installed "$SRC/agents/*.md")"
551
+ N_SK="$(count_installed "$SRC/skills/*/")"
552
+
553
+ h1 '[2/2] Summary · see what will be installed before you confirm'
554
+ echo
555
+ _mt 'full install'; _a="$_M"; _mt '— backend + web + mobile (RN/Expo), every agent and skill'
556
+ 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}"
558
+ row 'Included' "$_M"
559
+ _mt 'stack-agnostic — the stack comes from CLAUDE.md ## Stack or the repo (backend-architecture)'
560
+ row 'Backend pattern' "$_M"
561
+ echo
562
+ _mt 'Security gates armed on every install:'; printf ' %s%s%s\n' "$B" "$_M" "$R"
563
+ gate 'commit/push approval gate — even in auto/bypass mode (guard-bash)'
564
+ gate 'trace scan — a git hook blocks AI traces / vendor names'
565
+ gate 'real context measurement + handoff at 75%% (Stop hook)'
566
+ gate 'destructive command guard (rm -rf / force-push, etc.)'
567
+ echo
568
+ row 'Will write' "${D}./.claude (agents·skills·commands·hooks·eval·studio·settings.json) + ./CLAUDE.md${R}"
569
+ # .gitignore is a TRACKED file in most repos, so appending to it is a change to the project — it belongs in
570
+ # the summary, named line by line, not discovered afterwards in `git diff`. Entries this repo already
571
+ # ignores are dropped at write time, so what is listed here is the upper bound, not a promise of four lines.
572
+ if [ "$VISIBILITY" = "shared" ]; then
573
+ _mt '(shared: .claude/ and CLAUDE.md stay committable)'
574
+ row ".gitignore" "${D}$(printf '%s · ' $GI_PLAN | sed 's/ · $//') ${YE}${_M}${R}"
575
+ else
576
+ _mt 'private'; _a="$_M"
577
+ row ".gitignore" "${D}$(printf '%s · ' $GI_PLAN | sed 's/ · $//') ${GR}(${_a})${R}"
578
+ fi
579
+ # What this machine is missing, BEFORE the confirm prompt — not after, when it becomes a symptom pointing
580
+ # somewhere else. Report-only and never blocking: the kit degrades rather than breaks, and that is exactly why
581
+ # a gap is otherwise invisible. See kit/eval/preflight.sh for the reasoning per tool.
582
+ [ -f "$SRC/eval/preflight.sh" ] && bash "$SRC/eval/preflight.sh"
583
+ rule
584
+ echo
585
+ # ask_yes reads from stdin => in CI `printf 'yes\n' | bash start.sh` works; 'no' on EOF (no accidental install).
586
+ _mt 'Install with these settings?'
587
+ if ! ask_yes " $_M"; then
588
+ _mt 'Cancelled — nothing changed.'; printf ' %s%s%s\n' "$YE" "$_M" "$R"
589
+ exit 0
590
+ fi
591
+ echo
592
+
593
+ # --- Kit installation (./.claude + ./CLAUDE.md) — everything, no prune ---
594
+ { _mt 'Installing:'; echo "== ${_M} ./.claude + ./CLAUDE.md =="; }
595
+ mkdir -p .claude/agents .claude/skills .claude/hooks .claude/eval .claude/studio
596
+ cp -R "$SRC/agents/." .claude/agents/
597
+ cp -R "$SRC/skills/." .claude/skills/
598
+ # The slash commands are skills since 3.0 (Claude Code merged custom commands into skills): they arrive with
599
+ # skills/ above, marked `metadata: kind: command`. There is no commands/ directory any more.
600
+ VENDOR_ARMED=0; grep -qxE $'DevArchitecture\r?' .claude/hooks/trace-blocklist.txt 2>/dev/null && VENDOR_ARMED=1 # before the copy resets it; \r? = a CRLF copy still counts
601
+ cp -R "$SRC/hooks/." .claude/hooks/ 2>/dev/null || true
602
+ cp -R "$SRC/eval/." .claude/eval/ 2>/dev/null || true
603
+ # The Studio panel — launched by /crew-studio from this project's root. One `cp -R`
604
+ # plus one `rm`, not a selective walk: on Git Bash a per-file copy of 25 files is 25
605
+ # process spawns at 62-135 ms each. test/ is dropped because its assertions read the
606
+ # The panel's own suite is NOT here to delete: it lives in packaging/studio-test/,
607
+ # outside the payload, because kit/ ships whole and 104 KB of test code
608
+ # would travel through all four channels only to be removed on arrival.
609
+ 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}"; }
611
+ [ -f "$SRC/settings.json" ] && cp "$SRC/settings.json" .claude/settings.json
612
+ [ -f "$HERE/VERSION" ] && cp "$HERE/VERSION" .claude/VERSION # make the kit version trackable in the installed project
613
+ # Glob form so every shipped hook/eval is made executable — including ones added later (guard-write.sh,
614
+ # session-rehydrate.sh, …). An explicit list silently missed new hooks and left them non-executable.
615
+ # studio's gate hook is invoked as `bash <path>` today, so the bit is not load-bearing
616
+ # yet; set it anyway, because a future direct exec would fail silently.
617
+ chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg .claude/eval/*.sh .claude/studio/server/hooks/*.sh 2>/dev/null || true
618
+
619
+ # §4.2: a vendor line that was armed before this run stays armed (the copy above resets it to a comment); nothing
620
+ # else arms it. Rationale, and the measured failure of "arm it whenever the pattern skill exists", in adopt.sh.
621
+ # The `#test:` line comes with it: smoke-test fails any active pattern that has no case.
622
+ if [ "$VENDOR_ARMED" = 1 ] && [ -f .claude/hooks/trace-blocklist.txt ] \
623
+ && grep -qx '# DevArchitecture' .claude/hooks/trace-blocklist.txt; then
624
+ awk '/^# DevArchitecture$/ { print "DevArchitecture"; print "#test: ported the handler from DevArchitecture"; next } { print }' \
625
+ .claude/hooks/trace-blocklist.txt > .claude/hooks/trace-blocklist.txt.kit-tmp \
626
+ && mv .claude/hooks/trace-blocklist.txt.kit-tmp .claude/hooks/trace-blocklist.txt
627
+ fi
628
+ # `|| true` here used to swallow a missing payload file entirely: the install reported success and
629
+ # /crew-skill opened with `Read .claude/AGENT_TEMPLATE.md` against nothing. A best-effort copy is right —
630
+ # a missing doc must not abort an otherwise good install — but it has to be AUDIBLE, or the gap is
631
+ # invisible until someone runs the command. adopt.sh copies the same file for the same reason.
632
+ cp "$SRC/AGENT_TEMPLATE.md" .claude/ 2>/dev/null || { _mt 'AGENT_TEMPLATE.md missing from the payload — /crew-skill will have nothing to read.'; printf ' %s!%s %s\n' "$YE" "$R" "$_M"; }
633
+ cp "$SRC/README.md" .claude/ 2>/dev/null || true
634
+
635
+ # Install manifest — the names the KIT ships. It is the only way to tell kit-owned from project-owned later:
636
+ # the readiness check reads it to find the project's own skills, and the trust gate reads it to spot a skill
637
+ # the kit never shipped. Generated from the PAYLOAD, not from disk: a brownfield adopt preserves a pre-existing
638
+ # project file under a kit name, and reading disk would then brand that project file "kit-owned". Erring this
639
+ # way under-reports project ownership and can never raise a false alarm on a kit file — the safe direction for
640
+ # a gate. Rewritten on every install/update, so a component the kit drops stops counting as kit-owned.
641
+ # studio/ is deliberately NOT listed. Both consumers walk components: skill-trust.sh iterates skills/*/ and
642
+ # agents/*.md, and the doctor readiness check separates kit skills from the project's. A studio/ line would be
643
+ # read by nobody, and adopt.sh's stale sweep only considers commands/, agents/ and skills/ entries anyway.
644
+ { for d in "$SRC"/skills/*/; do [ -d "$d" ] && echo "skills/$(basename "$d")"; done
645
+ for f in "$SRC"/agents/*.md; do [ -e "$f" ] && echo "agents/$(basename "$f")"; done
646
+ } > .claude/kit-manifest.txt 2>/dev/null || true
647
+
648
+ # stack= is always 'generic' since 3.0. The key is KEPT because an older updater reads it, and a file without it
649
+ # would send that updater down its stack-detection branch. No 'profile=' key — adopt.sh treats a leftover one
650
+ # from a pre-2.0 install as a migration signal, not as a shape.
651
+ { echo "# Written by the Crewforth installer. stack= is always generic since 3.0; the key is kept for older updaters."
652
+ echo "stack=$STACK"
653
+ echo "installer=start.sh"
654
+ echo "version=$( [ -f "$HERE/VERSION" ] && head -1 "$HERE/VERSION" || echo unknown )"
655
+ # A locale guess (--yes or no terminal, nothing named) is used but not recorded: the next update would reuse it
656
+ # as if somebody had chosen it. --lang, CREW_LANG and the menu are choices, and are.
657
+ [ "${_LANG_GUESSED:-0}" = 1 ] || echo "lang=${CREW_LANG:-en}"
658
+ } > .claude/kit.conf
659
+
660
+ # Discipline (kit-owned, refreshed on every update) vs project section (yours, written once), joined by @import.
661
+ kit_require_sentinel "$SRC/CLAUDE.md"
662
+ kit_discipline_of "$SRC/CLAUDE.md" > .claude/DISCIPLINE.md
663
+ { _mt '.claude/DISCIPLINE.md written — owned by Crewforth; an update overwrites it, so keep your own rules out of it.'; echo " ${_M}"; }
664
+ if [ ! -f ./CLAUDE.md ]; then
665
+ { printf '<!-- Crewforth discipline · on conflict the project rules BELOW win -->\n%s\n' "$IMPORT_LINE"
666
+ kit_project_of "$SRC/CLAUDE.md"; } > ./CLAUDE.md
667
+ { _mt './CLAUDE.md created — EDIT the project section.'; echo " ${_M}"; }
668
+ elif kit_has_import ./CLAUDE.md; then
669
+ { _mt './CLAUDE.md kept as-is (already imports the discipline) — the refresh landed in DISCIPLINE.md.'; echo " ${_M}"; }
670
+ elif kit_claude_md_is_legacy ./CLAUDE.md; then
671
+ { _mt '! ./CLAUDE.md carries the discipline INLINE (pre-1.1 layout) — left untouched.'; echo " ${_M}"; }
672
+ { _mt 'Discipline updates will NOT reach it. To migrate: delete everything above your'; echo " ${_M}"; }
673
+ { _mt '%s heading and leave this single line in its place:' "'# CLAUDE.md — <project>'"; echo " ${_M}"; }
674
+ echo " $IMPORT_LINE"
675
+ else
676
+ { printf '<!-- Crewforth discipline · on conflict the project rules BELOW win -->\n%s\n\n' "$IMPORT_LINE"; cat ./CLAUDE.md; } > ./CLAUDE.md.kit-tmp \
677
+ && mv ./CLAUDE.md.kit-tmp ./CLAUDE.md
678
+ { _mt './CLAUDE.md existed — prepended the discipline @import; your content is untouched.'; echo " ${_M}"; }
679
+ fi
680
+ # The entries were decided in step 2 and printed in the summary; gi_add drops the ones this repo already
681
+ # ignores and fixes a missing trailing newline before appending. Word-split on purpose: GI_PLAN is a
682
+ # space-separated list this script built, not user input.
683
+ # shellcheck disable=SC2086
684
+ gi_add $GI_PLAN
685
+ [ -n "$GI_WROTE" ] && printf ' %s+%s .gitignore: %s\n' "$GR" "$R" "$GI_WROTE"
686
+ # Pinned only when git will actually CHECK .claude/ OUT — and that question goes to git, not to the mode
687
+ # variable, for the same reason gi_add asks `git check-ignore` instead of grepping the file: the variable is
688
+ # what we intended, the answer is what is true. They agree in the normal case (a private install has just
689
+ # added `.claude/` to .gitignore) and they differ in the ones that matter — a repo that already ignored
690
+ # `.claude/` before this install, or a shared install inside a repo whose parent rules ignore it anyway.
691
+ # Where git will never check the directory out there is no conversion to prevent, and writing repo-wide
692
+ # attributes for that project would be editing a file its owner did not need touched.
693
+ if ! git check-ignore -q .claude 2>/dev/null; then
694
+ ga_add
695
+ [ "${GA_WROTE:-0}" != 0 ] && { _mt '%s eol pin(s) so shared hooks stay LF' "$GA_WROTE"; printf ' %s+%s .gitattributes: %s\n' "$GR" "$R" "$_M"; }
696
+ fi
697
+ # `[ -d .git ]` is a proxy for the answer, and it lies exactly where it matters: in a worktree or a submodule
698
+ # `.git` is a FILE, so the commit gate was never armed there and the installer said nothing was wrong. adopt.sh
699
+ # already names this (red-team hole #6) and start.sh was never taught it. Measured: in a worktree the installer
700
+ # printed "no git repository", core.hooksPath stayed empty, and a commit carrying a forbidden expression landed;
701
+ # arming by hand and retrying, the trace scanner rejected it. start.sh deletes itself afterwards, so there is no
702
+ # second chance from the installer.
703
+ #
704
+ # Anchored on the TOPLEVEL, not merely on being inside a work tree: a relative core.hooksPath resolves against
705
+ # the work-tree root, so arming from a subdirectory stores the value and runs no hook at all — a gate reported
706
+ # active over a dead path. `pwd -P` because git answers with the physical path, and the arming call itself is
707
+ # the probe, so a git that resolves and fails falls into the NOTE instead of a false "active".
708
+ # Asked as `--show-prefix`, not by comparing two spellings of the same path. Comparing them is what the line
709
+ # above this one used to do, and on Windows the two sides NEVER match: git answers `C:/repo/app` while
710
+ # `pwd -P` answers `/c/repo/app`, so the arming call was never reached — in a worktree AND in an ordinary
711
+ # repository. Measured on a Windows 11 desktop against this commit: `core.hooksPath` came back EMPTY in both,
712
+ # the installer exited 0, and it printed "no git repository at this level" standing inside one. That is the
713
+ # §4.1/§4.2 commit gate silently absent on every Windows install, reported as a successful one.
714
+ # `--show-prefix` is the question itself: empty at the work-tree root, `sub/dir/` below it, non-zero exit
715
+ # outside a repo — and it carries no path spelling to disagree about. Verified here at a normal root, a
716
+ # worktree root, a subdirectory and a non-repo.
717
+ if PFX="$(git rev-parse --show-prefix 2>/dev/null)" && [ -z "$PFX" ] && git config core.hooksPath .claude/hooks 2>/dev/null; then
718
+ { _mt 'trace scan: core.hooksPath -> .claude/hooks (§4.1/§4.2 commit gate active)'; echo " ${_M}"; }
719
+ else
720
+ { _mt 'NOTE: no git repository at this level; after %s run: %s' "'git init'" 'git config core.hooksPath .claude/hooks'; echo " ${_M}"; }
721
+ fi
722
+ rm -rf "$SRC"
723
+ echo
724
+ { _mt 'Done. ./.claude + ./CLAUDE.md ready (full install); kit/ deleted.'; echo "== ${_M} =="; }
725
+ { _mt 'Next: 1) fill in the CLAUDE.md project section 2) open Claude Code at the repo root'; echo "${_M}"; }
726
+ { _mt 'Note: if Claude Code is ALREADY running here, restart it — CLAUDE.md and the discipline load at session start.'; echo "${_M}"; }
727
+ { _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}"; }
728
+ # Say what is true of THIS machine, not what is true in general. The line used to
729
+ # print identically with or without node, so on a machine that cannot start the
730
+ # panel it read as a footnote rather than as the reason nothing will happen. The
731
+ # question goes to preflight so the version floor stays defined in one place, and
732
+ # it is asked of the INSTALLED copy: $SRC is deleted at line 397, a few lines
733
+ # above this, so asking there answered "no node" on every machine.
734
+ if bash .claude/eval/preflight.sh --has node 2>/dev/null; then
735
+ { _mt 'Panel: /crew-studio opens the Studio panel from this project (or: node .claude/studio/server/index.js --open).'; echo "${_M}"; }
736
+ else
737
+ { _mt 'Panel: needs Node 18+, which is not on this machine — but that is no longer a dead end.'; echo "${_M}"; }
738
+ { _mt 'Crewforth fetches one for the panel: %s (asks first;' 'bash .claude/studio/ensure-node.sh --plan'; echo " ${_M}"; }
739
+ { _mt 'verified against the published checksum, into %s, nothing else touched).' '~/.claude/studio-runtime'; echo " ${_M}"; }
740
+ { _mt 'Every gate still holds meanwhile; the panel is the only part that needs node.'; echo " ${_M}"; }
741
+ fi
742
+ # The star line, once per kit version (lib/star.sh keeps the marker in the git dir; text, URL and the
743
+ # CREW_NO_STAR / CI silence live there too). `_S` first so the blank line appears only when the line does.
744
+ if [ -f .claude/eval/lib/star.sh ]; then
745
+ _S="$(bash .claude/eval/lib/star.sh --once . 2>/dev/null || true)"
746
+ if [ -n "$_S" ]; then printf '\n%s\n' "$_S"; fi # an `&&` here was the script's LAST status: rc=1 on every update
747
+ fi
748
+ rm -f -- "$0"