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/adopt.sh ADDED
@@ -0,0 +1,1670 @@
1
+ #!/usr/bin/env bash
2
+ # kit adopt — hands the kit OVER (handover) to an EXISTING project, brownfield-safe.
3
+ # Later becomes the `kit adopt` subcommand. Handover philosophy: don't break the project · don't lose decisions made ·
4
+ # don't leave the kit passive (100% hybrid). Kit agents are namespaced with crew- -> no clash with project agents.
5
+ #
6
+ # >>> STAGE 1: detection + smart suggestion only. CHANGES NOTHING (read-only). <<<
7
+ # Later stages: open git branch -> mutation (settings merge · DISCIPLINE.md · coexist) -> install proof
8
+ # -> HANDOVER.md / ADR. The SUGGESTION produced here for each decision is applied in the next stage via review/override.
9
+ #
10
+ # Usage: at the target project root (same directory as kit/): bash adopt.sh
11
+ set -uo pipefail
12
+ # The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
13
+ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
14
+ [ -f "$_crew_d/kit/eval/lib/crew-env.sh" ] && . "$_crew_d/kit/eval/lib/crew-env.sh"; unset _crew_d
15
+ HERE="$(CDPATH= cd "$(dirname "$0")" && pwd)"
16
+
17
+ # --version (or -v) is answered first, before the payload check below: it reads only VERSION, so it works
18
+ # wherever adopt.sh sits. `npx … adopt --version` and `update --version` reach it through bin/cli.js.
19
+ for _a in "$@"; do
20
+ if [ "$_a" = "--version" ] || [ "$_a" = "-v" ]; then
21
+ if [ -f "$HERE/VERSION" ]; then head -1 "$HERE/VERSION" | tr -d '\r'; else echo unknown; fi
22
+ exit 0
23
+ fi
24
+ done
25
+
26
+ SRC="$HERE/kit"
27
+ [ -d "$SRC" ] || { echo "ERROR: kit/ not found (must be in the same directory as adopt.sh)."; exit 1; }
28
+
29
+ # --- flags (B5 — where a refresh lands) --- --here: the current branch · --new-branch: a fresh review branch.
30
+ # Left empty, Stage 2 picks a smart default (first adopt -> new; update + untracked .claude -> here; update +
31
+ # tracked -> ask). Unknown flags are ignored here (start.sh owns --backend/--frontend/… ; adopt auto-detects shape).
32
+ BRANCH_MODE=""; ASSUME_YES=0
33
+ _lang_flag=""; _lang_take=0
34
+ for _a in "$@"; do
35
+ # `--lang` with no value must not swallow the next flag: `--lang --yes` is --yes with no language given.
36
+ if [ "$_lang_take" = 1 ]; then _lang_take=0; case "$_a" in -*) ;; *) _lang_flag="$_a"; continue ;; esac; fi
37
+ case "$_a" in
38
+ --here) BRANCH_MODE=here ;;
39
+ --new-branch) BRANCH_MODE=new ;;
40
+ --yes|-y) ASSUME_YES=1 ;; # assume "yes" at every gate — for agent-driven / CI updates (no TTY to prompt)
41
+ --lang=*) _lang_flag="${_a#--lang=}" ;;
42
+ --lang) _lang_take=1 ;;
43
+ esac; done
44
+
45
+ # ---- CREW-I18N (the twin of start.sh's; see the long note there for why the English string is the key) ----
46
+ # Short version, because the reasoning belongs in one place: `m 'text'` prints the translation of that text
47
+ # or the text itself. A missing translation therefore cannot print a blank line or a bare key — the fallback
48
+ # IS English. Colour never enters a message (the helpers above add it), interpolation goes through %s, and a
49
+ # literal percent must be written %% because the message is the printf format.
50
+ #
51
+ # Language: --lang, then an inherited CREW_LANG, then — on an interactive run without --yes — a one-line menu,
52
+ # then LC_ALL/LC_MESSAGES/LANG, then English. An interactive run ASKS because detection alone never offered
53
+ # Turkish where it should have: measured, macOS can run with a Turkish system language while the shell exports
54
+ # LANG=C.UTF-8. The locale still picks the menu's default. A piped, CI or --yes run never sees the menu (a
55
+ # `read` there would block or eat the caller's input) and keeps plain detection. On stock Windows all three
56
+ # locale variables are empty (measured), so a non-interactive Windows run needs the flag — documented
57
+ # behaviour, not a defect. English stays the fallback because it is what this script printed before it could
58
+ # speak anything else.
59
+ _loc="${LC_ALL:-}"; [ -n "$_loc" ] || _loc="${LC_MESSAGES:-}"; [ -n "$_loc" ] || _loc="${LANG:-}"
60
+ case "$_loc" in tr*|TR*) _lang_det=tr; _lang_def=2 ;; *) _lang_det=en; _lang_def=1 ;; esac
61
+ _LANG_CHOSEN=1 # 0 = a locale guess: used, never written down
62
+ if [ -n "$_lang_flag" ]; then
63
+ CREW_LANG="$_lang_flag"
64
+ elif [ -n "${CREW_LANG:-}" ]; then
65
+ :
66
+ elif _lang_rec="$(sed -n 's/^lang=//p' .claude/kit.conf 2>/dev/null | head -1 | tr -d '\r')" && [ -n "$_lang_rec" ]; then
67
+ CREW_LANG="$_lang_rec" # an update keeps the language the install chose (the session-start prompt speaks it too)
68
+ elif [ -t 0 ] && [ "$ASSUME_YES" != 1 ]; then
69
+ printf '\n Language / Dil\n 1) English\n 2) Türkçe\n -> [1-2, empty/boş=%s]: ' "$_lang_def"
70
+ read -r _lang_ans || _lang_ans=""
71
+ [ -n "$_lang_ans" ] || _lang_ans="$_lang_def"
72
+ case "$_lang_ans" in 2|tr|TR|t|T) CREW_LANG=tr ;; *) CREW_LANG=en ;; esac
73
+ else
74
+ CREW_LANG="$_lang_det"; _LANG_CHOSEN=0
75
+ fi
76
+ case "$CREW_LANG" in tr|en) ;; *) CREW_LANG=en ;; esac
77
+ # Exported: child scripts (eval/preflight.sh) resolve their own language from the environment, and an
78
+ # unexported choice from --lang or the menu made them print English.
79
+ export CREW_LANG
80
+ # _mt sets _M instead of printing, so a call site can translate WITHOUT a `$( )` subshell. Each subshell is a
81
+ # fork, and on Git Bash a fork costs 62-135 ms; wrapping ~100 lines in `$(m …)` would have added seconds to
82
+ # every update. The helpers below (say/h1m/subm/warnm/rowm/rowv/propm, ask_yes) all go through _mt.
83
+ m() { _mt "$@"; printf '%s' "$_M"; }
84
+ _mt() { # $1 = English text (the key); further args fill %s; result in _M
85
+ # An empty key must still ASSIGN: bash 3.2's `printf -v _M ""` leaves _M holding the previous translation.
86
+ [ -n "${1:-}" ] || { _M=""; return 0; }
87
+ local s="$1"; shift
88
+ if [ "$CREW_LANG" = tr ]; then
89
+ case "$s" in
90
+ "crewforth adopt · Stage 1 — DETECTION (read-only; nothing changes)") s='crewforth adopt · Aşama 1 — TESPİT (salt okunur, hiçbir şey değişmez)' ;;
91
+ "Reads the existing project, produces a smart suggestion for the 7 handover decisions. Approval + mutation in the next stage.") s='Projeyi okur ve devralma için 7 karara akıllı bir öneri çıkarır. Onay ve değişiklikler sonraki aşamada.' ;;
92
+ "[1] Environment") s='[1] Ortam' ;;
93
+ "no git — 'git init' required") s="git yok — önce 'git init' gerekli" ;;
94
+ "worktree/submodule (.git file)") s='worktree/submodule (.git bir dosya)' ;;
95
+ "normal repo") s='normal depo' ;;
96
+ "git hook system") s='git hook sistemi' ;;
97
+ "none") s='yok' ;;
98
+ "Crewforth (already armed)") s='Crewforth (zaten devrede)' ;;
99
+ "pre-commit framework") s='pre-commit çatısı' ;;
100
+ "unknown") s='bilinmiyor' ;;
101
+ "stack hint") s='yığın ipucu' ;;
102
+ "[2] Existing agentic setup (accumulated work to inherit)") s='[2] Projedeki agentic kurulum (korunacak birikim)' ;;
103
+ "present — %s project agents · %s project skills") s="var — %s proje ajanı · %s proje skill'i" ;;
104
+ "present") s='var' ;;
105
+ "already adopted%s — this run REFRESHES Crewforth files, project untouched") s='Crewforth zaten kurulu%s — bu çalıştırma yalnız Crewforth dosyalarını YENİLER, projeye dokunmaz' ;;
106
+ "Crewforth status") s='Crewforth durumu' ;;
107
+ "(no kit.conf — read back from the installed files)") s='(kit.conf yok — kurulu dosyalardan çıkarıldı)' ;;
108
+ "inferred pattern") s='çıkarılan desen' ;;
109
+ "recorded pattern") s='kayıtlı desen' ;;
110
+ "3.0 rename: %s → %s") s='3.0 ad değişikliği: %s → %s' ;;
111
+ "3.0 rename: both the old and the new name exist for:%s — nothing moved; keep one") s='3.0 ad değişikliği: eski ve yeni ad ikisi de var:%s — hiçbir şey taşınmadı; birini tutun' ;;
112
+ "3.0 ref-sweep: old 2.x names → crew- names in %s") s='3.0 referans taraması: %s içindeki eski 2.x adları crew- adlarına çevrildi' ;;
113
+ "3.0 commands are skills: %s → %s") s="3.0'da komutlar skill oldu: %s → %s" ;;
114
+ "3.0 commands are skills: a skill of that name already exists for:%s — nothing moved; keep one") s="3.0'da komutlar skill oldu: bu adla bir skill zaten var:%s — hiçbir şey taşınmadı; birini tutun" ;;
115
+ "3.0 commands are skills: an older copy of an already-moved command is left in place:%s — remove it") s="3.0'da komutlar skill oldu: taşınmış bir komutun eski kopyası yerinde bırakıldı:%s — silin" ;;
116
+ "3.0 commands are skills: symlinked command file(s) left as they are:%s — the skill of that name now answers /name") s="3.0'da komutlar skill oldu: symlink olan komut dosyaları olduğu gibi bırakıldı:%s — /ad artık aynı adlı skill'e gider" ;;
117
+ "3.0 commands are skills: .claude/commands is a symlink (shared?) — nothing was moved out of it; the skills of those names now answer /name") s="3.0'da komutlar skill oldu: .claude/commands bir symlink (paylaşılan?) — içinden hiçbir şey taşınmadı; /ad artık aynı adlı skill'lere gider" ;;
118
+ "your own command(s) keep their name — the Crewforth skill of the same name was not installed:%s") s="kendi komutlarınız adını korur — aynı adlı Crewforth skill'i kurulmadı:%s" ;;
119
+ "3.0 board: moved to the crew names in this clone:%s") s='3.0 pano: bu klonda crew adlarına taşındı:%s' ;;
120
+ "3.0 board: the remote's 2.x board ref is not deleted — while it exists, 3.x writes both, so 2.x teammates still see your claims; ask the team to update, then delete the old ref") s="3.0 pano: uzaktaki 2.x pano ref'i silinmedi — o durdukça 3.x ikisine birden yazar, 2.x kullanan ekip arkadaşları sahiplenmelerinizi görmeye devam eder; ekipten de güncellemesini isteyin, sonra eski ref'i silin" ;;
121
+ "3.0 auto-mode: the Crewforth rules in %s are renamed CSK … → Crewforth … (backup: %s)") s='3.0 auto-mode: %s içindeki Crewforth kuralları CSK … → Crewforth … olarak yeniden adlandırıldı (yedek: %s)' ;;
122
+ "%s is set — its 3.0 name is %s (the old name works until 4.0)") s="%s ayarlı — 3.0'daki adı %s (eski ad 4.0'a kadar çalışır)" ;;
123
+ "%s is set but no longer read — set %s instead") s='%s ayarlı ama artık okunmuyor — yerine %s ayarlayın' ;;
124
+ "stack=%s %s") s='stack=%s %s' ;;
125
+ "stack=%s · via %s") s='stack=%s · kuran: %s' ;;
126
+ "stack=dotnet — 3.0 records generic; the pattern skill stays as a project skill") s="stack=dotnet — 3.0 generic kaydeder; desen skill'i proje skill'i olarak kalır" ;;
127
+ "cqrs-aop-module is now a project skill (Crewforth no longer ships it); backend-expert applies it as your project's pattern.") s="cqrs-aop-module artık bir proje skill'i (Crewforth onu artık taşımıyor); backend-expert onu projenizin deseni olarak uygular." ;;
128
+ "CSK_CORRECT_STACK has no effect since 3.0 — there is one backend shape; the stack lives in CLAUDE.md ## Stack.") s="CSK_CORRECT_STACK 3.0'dan beri etkisiz — tek bir backend biçimi var; yığın CLAUDE.md ## Stack bölümünde durur." ;;
129
+ "§4.2: DevArchitecture stays armed in the trace blocklist (it was armed before this update)") s="§4.2: DevArchitecture iz engel listesinde devrede kalıyor (güncellemeden önce de devredeydi)" ;;
130
+ "— profile pruning was removed in 2.0; this refresh completes the install") s="— profil budama 2.0'da kalktı; bu güncelleme eksikleri tamamlar" ;;
131
+ "pre-2.0 profile") s='2.0 öncesi profil' ;;
132
+ "YES — shared with the team") s='EVET — ekiple paylaşılıyor' ;;
133
+ "no/untracked") s='hayır / izlenmiyor' ;;
134
+ ".claude/CLAUDE.md in git") s=".claude/CLAUDE.md git'te mi" ;;
135
+ "supply-chain scan flagged existing project skills/agents (advisory — review before trusting them):") s='tedarik zinciri taraması projedeki bazı skill/ajanları işaretledi (yalnız uyarı — güvenmeden önce inceleyin):' ;;
136
+ "full report after install: %s (heuristic; a security skill can score low by design)") s="kurulumdan sonra tam rapor: %s (sezgisel; güvenlik skill'leri doğası gereği düşük puan alabilir)" ;;
137
+ "supply-chain scan") s='tedarik zinciri taraması' ;;
138
+ "existing project skills/agents look clean (no red flags)") s='projedeki skill/ajanlar temiz görünüyor (şüpheli bir şey yok)' ;;
139
+ "[3] 7 handover decisions — SMART SUGGESTION") s='[3] Devralma için 7 karar — AKILLI ÖNERİ' ;;
140
+ "format: decision -> SUGGESTED -> rationale (you can review and override all of them in the next stage)") s='biçim: karar -> ÖNERİ -> gerekçe (hepsini sonraki aşamada inceleyip değiştirebilirsiniz)' ;;
141
+ "1 Role overlap") s='1 Rol çakışması' ;;
142
+ "Crewforth takes over") s='Crewforth üstlenir' ;;
143
+ "%s project agent(s) cover the SAME job as a Crewforth agent (%s) — routing is ambiguous; Crewforth wins, yours preserved") s='%s proje ajanı bir Crewforth ajanıyla AYNI işi yapıyor (%s) — hangisine gideceği belirsiz; Crewforth öne geçer, sizinkiler saklanır' ;;
144
+ "1 Role clash") s='1 Rol çakışması' ;;
145
+ "keep (coexist)") s='koru (yan yana)' ;;
146
+ "%s project agents, none overlap a Crewforth role; thanks to the crew- prefix they live side by side") s='%s proje ajanı var, hiçbiri Crewforth rolleriyle çakışmıyor; crew- öneki sayesinde yan yana çalışırlar' ;;
147
+ "no custom agents found in the project") s='projede özel ajan yok' ;;
148
+ "2 Precedence") s='2 Öncelik' ;;
149
+ "project wins (fixed)") s='proje önde (sabit)' ;;
150
+ "on conflict the project's rules always win; Crewforth fills gaps (not overridable)") s='çakışmada her zaman projenin kuralı geçerli; Crewforth yalnız boşlukları doldurur (değiştirilemez)' ;;
151
+ "3 Trace gate") s='3 İz kapısı' ;;
152
+ "loosen (.trace-allowlist)") s='gevşet (.trace-allowlist)' ;;
153
+ "co-author/sign-off present in git log — may be a convention") s='git geçmişinde co-author/sign-off var — ekibin alışkanlığı olabilir' ;;
154
+ "keep") s='koru' ;;
155
+ "no co-author/sign-off convention seen") s='co-author/sign-off alışkanlığı görülmedi' ;;
156
+ "4 Share/hide") s='4 Paylaş/gizle' ;;
157
+ "share") s='paylaş' ;;
158
+ ".claude/CLAUDE.md is tracked — keep sharing with the team") s=".claude/CLAUDE.md git'te izleniyor — ekiple paylaşmaya devam" ;;
159
+ "untracked; Crewforth files are shared by default — pick hide to keep them local") s='izlenmiyor; Crewforth dosyaları varsayılan olarak paylaşılır — yerelde tutmak için hide seçin' ;;
160
+ "5 Git hooks") s="5 Git hook'ları" ;;
161
+ "install directly") s='doğrudan kur' ;;
162
+ "no existing hook system") s='hook sistemi yok' ;;
163
+ "SHIM (bridge)") s='SHIM (köprü)' ;;
164
+ "existing %s present — let both run") s='%s zaten var — ikisi birlikte çalışsın' ;;
165
+ "baseline+regression") s='taban + gerileme' ;;
166
+ "existing code debt unknown; absolute 0/0/0/0 is risky") s='mevcut kod borcu bilinmiyor; mutlak 0/0/0/0 riskli' ;;
167
+ "7 Off-repo: no local .claude/CLAUDE.md — decisions may live in chat/on the web; there is context I CANNOT SEE.") s='7 Depo dışı: yerelde .claude/CLAUDE.md yok — kararlar sohbette ya da webde kalmış olabilir; GÖREMEDİĞİM bir bağlam var.' ;;
168
+ " -> suggestion") s=' -> öneri' ;;
169
+ "you transfer") s='siz aktarın' ;;
170
+ "in the mutation stage 'paste if any' is asked; goes into HANDOVER.md") s="uygulama aşamasında 'varsa yapıştırın' diye sorulur; HANDOVER.md'ye yazılır" ;;
171
+ "7 Off-repo") s='7 Depo dışı' ;;
172
+ "local + ask") s='yerel + sor' ;;
173
+ "some decisions are in files; still may be in-chat (asked during the stage)") s='kararların bir kısmı dosyalarda; sohbette kalanlar olabilir (aşama sırasında sorulur)' ;;
174
+ "Review the decisions") s='Kararları gözden geçirin' ;;
175
+ "[%s/%s] (current: %s, ENTER=keep): ") s='[%s/%s] (şu an: %s, ENTER=aynen kalsın): ' ;;
176
+ 'type "%s" or "%s" (or ENTER to keep "%s")') s='"%s" ya da "%s" yazın ("%s" kalsın diye yalnız ENTER)' ;;
177
+ "Accept all smart suggestions?") s='Tüm akıllı öneriler kabul edilsin mi?' ;;
178
+ "All smart suggestions accepted.") s='Tüm akıllı öneriler kabul edildi.' ;;
179
+ "Reviewing each decision. ENTER keeps the current value. (#1 overlap and #2/#5 are handled separately below.)") s='Kararlar tek tek soruluyor. ENTER mevcut değeri korur. (#1 çakışma ve #2/#5 aşağıda ayrıca ele alınıyor.)' ;;
180
+ "#3 Trace gate") s='#3 İz kapısı' ;;
181
+ "#4 Share/hide") s='#4 Paylaş/gizle' ;;
182
+ "#7 Off-repo") s='#7 Depo dışı' ;;
183
+ "Final: #3=%s #4=%s #6=%s #7=%s (#2 project-wins, #5 SHIM — fixed)") s='Sonuç: #3=%s #4=%s #6=%s #7=%s (#2 proje önde, #5 SHIM — sabit)' ;;
184
+ "(non-interactive: smart defaults accepted)") s='(etkileşimsiz çalışma: akıllı varsayılanlar kabul edildi)' ;;
185
+ "Role overlap — project & Crewforth both cover: %s") s='Rol çakışması — proje ve Crewforth aynı işi yapıyor: %s' ;;
186
+ "Two agents for one job = the router picks one, usually your older agent — so Crewforth's would sit idle.") s="Aynı iş için iki ajan olunca yönlendirici birini seçer, çoğu zaman sizin eski ajanınızı — Crewforth'unki boşta kalır." ;;
187
+ "Crewforth's crew- agents win; each old agent's domain is imported to a draft skill (skills/<name>-local), original backed up") s="Crewforth'un crew- ajanları öne geçer; eski ajanın alan bilgisi taslak bir skill'e (skills/<name>-local) taşınır, orijinali yedeklenir" ;;
188
+ "your agents win; Crewforth's overlapping crew- agents are not installed") s="sizin ajanlarınız öne geçer; Crewforth'un çakışan crew- ajanları kurulmaz" ;;
189
+ "keep both (routing stays ambiguous; only documented in HANDOVER)") s="ikisi de kalır (yönlendirme belirsiz kalır; yalnız HANDOVER'a not düşülür)" ;;
190
+ "owner") s='sahip' ;;
191
+ "type takeover, keepmine or coexist") s='takeover, keepmine ya da coexist yazın' ;;
192
+ "overlap -> %s") s='çakışma -> %s' ;;
193
+ "Stage 2 — apply Crewforth (coexist)") s="Aşama 2 — Crewforth'u uygula (projeyle yan yana)" ;;
194
+ "no git repo — cannot apply safely. First: %s (then run again).") s='git deposu yok — güvenle uygulanamaz. Önce şunu çalıştırın: %s (sonra tekrar deneyin).' ;;
195
+ "Apply on a NEW review branch? (no = apply on the current branch '%s')") s="YENİ bir inceleme dalında mı uygulansın? (hayır = mevcut '%s' dalında)" ;;
196
+ "the current branch '%s'") s="mevcut '%s' dalı" ;;
197
+ "a new review branch (off '%s')") s="'%s' üzerinden açılan yeni bir inceleme dalı" ;;
198
+ "Apply Crewforth onto %s now? (mutation; staged-not-committed, reversible with git)") s='Crewforth şimdi uygulansın mı? Hedef: %s (dosyalar değişir; stage edilir, commit edilmez, git ile geri alınabilir)' ;;
199
+ "Stopped") s='Durduruldu' ;;
200
+ "Stayed at Stage 1 — NOTHING CHANGED (read-only).") s="Aşama 1'de kalındı — HİÇBİR ŞEY DEĞİŞMEDİ (salt okunur)." ;;
201
+ "applying on the current branch: %s (no separate branch; staged, HEAD untouched until you commit)") s='mevcut dala uygulanıyor: %s (ayrı dal yok; değişiklikler stage edilir, siz commit edene kadar HEAD yerinde kalır)' ;;
202
+ "You are on your current branch %s with everything STAGED but NOT committed.") s='Mevcut %s dalındasınız; her şey STAGE edildi ama commit EDİLMEDİ.' ;;
203
+ "accept: %s") s='kabul: %s' ;;
204
+ "discard: %s (un-stages everything; nothing was committed)") s="vazgeç: %s (stage'i boşaltır; zaten hiçbir şey commit edilmedi)" ;;
205
+ "HEAD is a prior adopt branch (%s) — the review diff will be vs it, not your main line. Consider %s first.") s="HEAD önceki bir adopt dalında (%s) — inceleme diff'i ana dalınıza değil bu dala göre çıkar. Önce %s çalıştırmayı düşünün." ;;
206
+ "ERROR: could not open branch '%s'.") s="HATA: '%s' dalı açılamadı." ;;
207
+ "handover branch: %s (%s stays clean)") s='inceleme dalı: %s (%s temiz kalır)' ;;
208
+ "You are on branch %s with everything STAGED but NOT committed.") s='%s dalındasınız; her şey STAGE edildi ama commit EDİLMEDİ.' ;;
209
+ "accept: %s then: %s") s='kabul: %s ardından: %s' ;;
210
+ "discard: %s") s='vazgeç: %s' ;;
211
+ ".NET pattern skill renamed: devarch-module -> cqrs-aop-module (content kept)") s=".NET desen skill'inin adı değişti: devarch-module -> cqrs-aop-module (içerik korundu)" ;;
212
+ "⚠️ both devarch-module and cqrs-aop-module are present — nothing moved; remove the old one when ready") s='⚠️ devarch-module ve cqrs-aop-module ikisi birden var — hiçbir şey taşınmadı; hazır olduğunuzda eskisini silin' ;;
213
+ "AGENT_TEMPLATE.md written (owned by Crewforth; refreshed on every update)") s="AGENT_TEMPLATE.md yazıldı (Crewforth'un dosyası; her güncellemede yenilenir)" ;;
214
+ "pre-2.0 install (profile=%s): profile pruning was removed — completing the install") s='2.0 öncesi kurulum (profile=%s): profil budama kalktı — eksikler tamamlanıyor' ;;
215
+ "pre-2.0 install (profile=%s): nothing was missing — the full set was already present") s='2.0 öncesi kurulum (profile=%s): eksik yok — tam set zaten kuruluydu' ;;
216
+ "overlap: %s -> skill '%s' already present (kept); original re-backed up") s="çakışma: %s -> '%s' skill'i zaten var (korundu); orijinal yeniden yedeklendi" ;;
217
+ "overlap: %s -> imported to skill '%s' (draft); Crewforth's %s owns routing") s="çakışma: %s -> '%s' skill'ine taşındı (taslak); yönlendirme artık Crewforth'un %s ajanında" ;;
218
+ "ref-sweep: %s → %s in %s") s='ref-sweep: %s → %s (%s)' ;;
219
+ "Reference sweep: rewrote taken-over agent names to their crew- id across CLAUDE.md + referenced docs") s='Referans taraması: devralınan ajan adları CLAUDE.md ve bağlı belgelerde crew- adlarına çevrildi' ;;
220
+ "ref-sweep: no stale references in CLAUDE.md's chain") s='ref-sweep: CLAUDE.md ve bağlı belgelerde eski ad kalmamış' ;;
221
+ "⚠️ installed by an older Crewforth version and no longer shipped:%s") s='⚠️ eski bir Crewforth sürümünden kalan, artık dağıtılmayan dosyalar:%s' ;;
222
+ "The name is the invocation: a leftover COMMAND still lists in the / picker (/review twice), and a") s='Burada adın kendisi çağrıdır: artakalan bir KOMUT / menüsünde hâlâ görünür (/review iki kez), artakalan' ;;
223
+ "leftover SKILL still matches prompts, so it competes with whatever replaced it.") s='bir SKILL de istemlerle eşleşmeye devam eder ve yerine gelenle yarışır.' ;;
224
+ "Nothing is deleted for you — one of these may be a file you customised. To drop them all:") s='Hiçbiri sizin yerinize silinmez — aralarında özelleştirdiğiniz bir dosya olabilir. Hepsini kaldırmak için:' ;;
225
+ "Coexist summary") s='Kurulum özeti' ;;
226
+ "%s · %s skipped") s='%s · %s atlandı' ;;
227
+ "+%s added") s='+%s eklendi' ;;
228
+ "Crewforth agents (crew-)") s='Crewforth ajanları (crew-)' ;;
229
+ "skills") s="skill'ler" ;;
230
+ "commands") s='komutlar' ;;
231
+ "hooks") s="hook'lar" ;;
232
+ "%s (needs Node 18+)") s='%s (Node 18+ gerekir)' ;;
233
+ "%s (%s imported to skills/<name>-local drafts; originals backed up in superseded/)") s='%s (%s tanesi skills/<name>-local taslaklarına taşındı; orijinaller superseded/ altında)' ;;
234
+ "%s — the rest UNTOUCHED") s='%s — geri kalanına DOKUNULMADI' ;;
235
+ "project agents") s='proje ajanları' ;;
236
+ "overlap") s='çakışma' ;;
237
+ "keepmine — your agents own: %s (Crewforth's crew- for these NOT installed)") s="keepmine — bu roller sizin ajanlarınızda: %s (Crewforth'un karşılık gelen crew- ajanları KURULMADI)" ;;
238
+ "overlap: %s — BOTH kept; routing between your agent and Crewforth's crew- stays ambiguous") s="çakışma: %s — İKİSİ de kaldı; sizin ajanınızla Crewforth'un crew- ajanı arasında seçim belirsiz" ;;
239
+ "conflicting files (the project's was PRESERVED, Crewforth's skipped):") s="çakışan dosyalar (projeninki KORUNDU, Crewforth'unki atlandı):" ;;
240
+ "Stage 3 — activate the Crewforth discipline (without touching the project CLAUDE.md) + settings merge") s="Aşama 3 — Crewforth disiplinini etkinleştir (proje CLAUDE.md'sine dokunmadan) + ayarları birleştir" ;;
241
+ "DISCIPLINE.md written (Crewforth discipline only; the project template stays out of it)") s='DISCIPLINE.md yazıldı (yalnız Crewforth disiplini; proje şablonu içinde yok)' ;;
242
+ "CLAUDE.md: @import already present (idempotent)") s='CLAUDE.md: @import zaten var (tekrar eklenmedi)' ;;
243
+ "CLAUDE.md carries the discipline INLINE (pre-1.1 layout) — discipline updates cannot reach it.") s='CLAUDE.md disiplini DOSYANIN İÇİNDE taşıyor (1.1 öncesi düzen) — disiplin güncellemeleri ona ulaşamaz.' ;;
244
+ "the inline block is lines 1-%s; your project section starts at line %s") s='gömülü blok 1-%s. satırlar; proje bölümünüz %s. satırda başlıyor' ;;
245
+ " Replace that inline block with the single @import line? (a backup is written; this branch is reviewable)") s=' Bu gömülü blok tek bir @import satırıyla değiştirilsin mi? (yedek alınır; değişiklik bu dalda incelenebilir)' ;;
246
+ "CLAUDE.md migrated -> @import + your project section (backup: %s)") s='CLAUDE.md taşındı -> @import + proje bölümünüz (yedek: %s)' ;;
247
+ "Skipped. Discipline updates will NOT reach this project until you migrate.") s='Atlandı. Taşıyana kadar disiplin güncellemeleri bu projeye ULAŞMAZ.' ;;
248
+ "Project heading not found — migrate by hand: delete everything above it, leave only: %s") s='Proje başlığı bulunamadı — elle taşıyın: başlığın üstündeki her şeyi silin, yalnız şunu bırakın: %s' ;;
249
+ "CLAUDE.md: single-line @import prepended (project content untouched)") s='CLAUDE.md: başına tek satırlık @import eklendi (proje içeriğine dokunulmadı)' ;;
250
+ "CLAUDE.md was missing -> @import + project template created") s='CLAUDE.md yoktu -> @import ve proje şablonuyla oluşturuldu' ;;
251
+ "custom hooks and every other permission PRESERVED") s="özel hook'lar ve diğer tüm izinler KORUNDU" ;;
252
+ "custom hooks/permissions PRESERVED") s="özel hook'lar ve izinler KORUNDU" ;;
253
+ "settings.json: was missing in the project -> Crewforth's was installed") s="settings.json: projede yoktu -> Crewforth'unki kuruldu" ;;
254
+ "settings.json: hook-aware MERGE (Crewforth hooks refreshed - %s)") s="settings.json: hook'ları gözeten BİRLEŞTİRME (Crewforth hook'ları yenilendi - %s)" ;;
255
+ "settings.json: %s") s='settings.json: %s' ;;
256
+ "merge failed -> project setting PRESERVED (not overwritten)") s='birleştirme başarısız -> proje ayarı KORUNDU (üzerine yazılmadı)' ;;
257
+ "existing file is INVALID JSON -> merge ABORT (no silent overwrite). Fix it by hand first.") s='mevcut dosya GEÇERSİZ JSON -> birleştirme İPTAL (üzerine sessizce yazılmaz). Önce elle düzeltin.' ;;
258
+ "settings.json: retired §4.4 ask rule(s) REMOVED (%s) — guard-bash.sh now asks for these itself; an ask rule would override its CLAUDE_GIT_OK allow. Re-add one only if your project wants that trade.") s='settings.json: emekliye ayrılan §4.4 ask kuralları KALDIRILDI (%s) — bunları artık guard-bash.sh kendisi soruyor; bir ask kuralı onun CLAUDE_GIT_OK iznini ezerdi. Bu bedeli bilerek istiyorsanız geri ekleyin.' ;;
259
+ "Stage 4 — arm the git gates (SHIM via husky) + PROOF") s='Aşama 4 — git kapılarını devreye al (husky üzerinden SHIM) + KANIT' ;;
260
+ "core.hooksPath -> .claude/hooks (no existing hook chain)") s='core.hooksPath -> .claude/hooks (başka hook zinciri yok)' ;;
261
+ "SHIM installed -> core.hooksPath=.claude/git-shim (Crewforth + %s run together)") s='SHIM kuruldu -> core.hooksPath=.claude/git-shim (Crewforth ve %s birlikte çalışır)' ;;
262
+ "worktree/submodule: core.hooksPath may also affect the main checkout (git design).") s="worktree/submodule: core.hooksPath ana checkout'u da etkileyebilir (git böyle tasarlanmış)." ;;
263
+ "Stage 4b — PROOF") s='Aşama 4b — KANIT' ;;
264
+ "~ PROOF-1: skipped — could not stage the probe file (nothing was measured)") s='~ KANIT-1: atlandı — deneme dosyası stage edilemedi (hiçbir şey ölçülmedi)' ;;
265
+ "PROOF-1 FAILED: the trace scan LET THROUGH the AI trace") s='KANIT-1 BAŞARISIZ: iz taraması AI izini GEÇİRDİ' ;;
266
+ "OK · PROOF-1: staged AI trace BLOCKED by the trace scan") s='OK · KANIT-1: stage edilen AI izini iz taraması ENGELLEDİ' ;;
267
+ "~ PROOF-1: hook blocked (%s)") s='~ KANIT-1: hook engelledi (%s)' ;;
268
+ "PROOF-2 FAILED: guard-bash LET THROUGH the keyless commit") s="KANIT-2 BAŞARISIZ: guard-bash anahtarsız commit'i GEÇİRDİ" ;;
269
+ "OK · PROOF-2: guard-bash BLOCKED the keyless 'git commit' (holds in auto/bypass too)") s="OK · KANIT-2: guard-bash anahtarsız 'git commit'i ENGELLEDİ (auto/bypass modunda da geçerli)" ;;
270
+ "OK · PROOF-3: %s Crewforth agents (crew-) installed + discoverable") s='OK · KANIT-3: %s Crewforth ajanı (crew-) kurulu ve bulunabiliyor' ;;
271
+ "PROOF-3: no Crewforth agent") s='KANIT-3: Crewforth ajanı yok' ;;
272
+ "OK · PROOF-4: DISCIPLINE.md loaded + @import-ed from CLAUDE.md") s="OK · KANIT-4: DISCIPLINE.md yerinde ve CLAUDE.md'den @import ediliyor" ;;
273
+ "PROOF-4: discipline not linked") s='KANIT-4: disiplin bağlanmamış' ;;
274
+ "%s line(s): %s") s='%s, satır: %s' ;;
275
+ "PROOF-5: CLAUDE.md (or a doc it references) names auto-delegated agent(s) by an old bare id — rename each to its crew- id, else delegation to them silently fails:%s") s='KANIT-5: CLAUDE.md (ya da bağlı bir belge) otomatik devredilen ajanları eski, eksiz adıyla anıyor — her birini crew- adıyla değiştirin, yoksa onlara devretme sessizce başarısız olur:%s' ;;
276
+ "PROOF-5: CLAUDE.md (or a referenced doc) names pull-only agent(s) by an old bare id (still work; rename for consistency):%s") s='KANIT-5: CLAUDE.md (ya da bağlı bir belge) elle çağrılan ajanları eski, eksiz adıyla anıyor (yine çalışır; tutarlılık için yeniden adlandırın):%s' ;;
277
+ "PROOF: Crewforth 100%% ACTIVE — gates armed, agents + discipline loaded") s='KANIT: Crewforth %%100 ETKİN — kapılar devrede, ajanlar ve disiplin yüklü' ;;
278
+ "PROOF: some gates could not be verified (see above)") s='KANIT: bazı kapılar doğrulanamadı (yukarıya bakın)' ;;
279
+ "Stage B — apply the decisions") s='Aşama B — kararları uygula' ;;
280
+ "#3 loosen -> .trace-allowlist.txt (co-author trailer exempt)") s='#3 loosen -> .trace-allowlist.txt (co-author satırı taramadan muaf)' ;;
281
+ "#3 keep -> full trace scan") s='#3 keep -> tam iz taraması' ;;
282
+ "#4 hide -> recorded; .claude stays TRACKED on the branch (rollback-safe). Post-merge steps in HANDOVER.") s="#4 hide -> kaydedildi; .claude bu dalda İZLENMEYE devam eder (geri almak güvenli). Merge sonrası adımlar HANDOVER'da." ;;
283
+ "#4 share -> .claude/CLAUDE.md is tracked; nothing added to .gitignore, so it stays shared") s="#4 share -> .claude/CLAUDE.md izleniyor; .gitignore'a bir şey eklenmedi, paylaşılmaya devam eder" ;;
284
+ "#4 share -> nothing added to .gitignore, but this repo ALREADY ignores .claude — the stage step skips it, so it is NOT shared") s="#4 share -> .gitignore'a bir şey eklenmedi, ama bu depo .claude'u ZATEN yok sayıyor — stage adımı onu atlar, yani PAYLAŞILMAZ" ;;
285
+ "#4 share -> nothing added to .gitignore; the stage step below adds .claude — commit it to share it") s="#4 share -> .gitignore'a bir şey eklenmedi; aşağıdaki stage adımı .claude'u ekler — paylaşmak için commit edin" ;;
286
+ "#7 off-repo decisions — paste them here") s='#7 depo dışındaki kararlar — buraya yapıştırın' ;;
287
+ "Write the decisions made in chat/on the web but NOT in the repo. When done, an EMPTY line (Enter).") s='Sohbette ya da webde alınıp depoya YAZILMAMIŞ kararları girin. Bitirmek için BOŞ bir satır (Enter).' ;;
288
+ "#7 -> %s lines will go into HANDOVER") s="#7 -> %s satır HANDOVER'a yazılacak" ;;
289
+ "#7 -> empty") s='#7 -> boş' ;;
290
+ "Stage 5 — HANDOVER.md + ADR (handover persists; decisions are not lost)") s='Aşama 5 — HANDOVER.md + ADR (devralma kayda geçer; kararlar kaybolmaz)' ;;
291
+ "%s written") s='%s yazıldı' ;;
292
+ "%s written (persistent handover decision)") s='%s yazıldı (devralmanın kalıcı karar kaydı)' ;;
293
+ "%s already exists — untouched (never-overwrite)") s='%s zaten var — dokunulmadı (üzerine asla yazılmaz)' ;;
294
+ "+ .gitattributes: eol pins so the shared hooks stay LF on a Windows checkout") s="+ .gitattributes: paylaşılan hook'lar Windows checkout'unda LF kalsın diye eol sabitlendi" ;;
295
+ "Review in your editor — nothing committed yet") s='Editörünüzde inceleyin — henüz hiçbir şey commit edilmedi' ;;
296
+ "staged") s='stage edilen' ;;
297
+ "see it: open the Source Control / Changes panel (every added + changed file is listed) · or: %s") s='görmek için: Source Control / Changes panelini açın (eklenen ve değişen her dosya orada) · ya da: %s' ;;
298
+ "panel: %s opens it from this project (or: %s)") s='panel: bu projede %s ile açılır (ya da: %s)' ;;
299
+ "panel: needs Node 18+, absent here — Crewforth can fetch one: %s") s='panel: Node 18+ gerekiyor ve bu makinede yok — Crewforth indirebilir: %s' ;;
300
+ "(it asks first, verifies the checksum, and touches nothing outside %s)") s='(önce sorar, sağlama toplamını doğrular ve %s dışında hiçbir şeye dokunmaz)' ;;
301
+ "If Claude Code is running in this project, run /clear (or quit and relaunch it) — a new session loads the") s='Claude Code bu projede açıksa /clear çalıştırın (ya da kapatıp yeniden açın) — yeni oturum güncel' ;;
302
+ "updated CLAUDE.md and discipline; a session opened before this run keeps the old rules until then.") s='CLAUDE.md ve disiplini yükler; bu çalıştırmadan önce açılmış oturum o zamana kadar eski kuralları uygular.' ;;
303
+ "yes") s='evet' ;;
304
+ "no") s='hayır' ;;
305
+ "[yes/no]") s='[evet/hayır]' ;;
306
+ "(non-interactive — pass --yes to apply)") s='(etkileşimsiz — uygulamak için --yes ekleyin)' ;;
307
+ "ERROR: the %s sentinel line is missing from %s — refusing to guess the discipline/project split.") s='HATA: %s işaret satırı yok (dosya: %s) — disiplin ile proje bölümünün sınırı tahmin edilmeyecek.' ;;
308
+ "studio (panel)") ;; # identifier, printed as is
309
+ "settings.json") ;; # identifier, printed as is
310
+ "eval") ;; # identifier, printed as is
311
+ "Node/JS") ;; # identifier, printed as is
312
+ "Go") ;; # identifier, printed as is
313
+ "Python") ;; # identifier, printed as is
314
+ ".NET") ;; # identifier, printed as is
315
+ "CLAUDE.md") ;; # identifier, printed as is
316
+ "6 Brownfield DoD") ;; # identifier, printed as is
317
+ ".claude/") ;; # identifier, printed as is
318
+ "git") ;; # identifier, printed as is
319
+ "husky (.husky/)") ;; # identifier, printed as is
320
+ "lefthook") ;; # identifier, printed as is
321
+ # No row: the line prints in English. CREW_I18N_MISS (set by e2e case 18) collects every such key, so a
322
+ # missing translation is caught by NAME rather than guessed from which English words it happens to contain.
323
+ *) [ -n "${CREW_I18N_MISS:-}" ] && printf '%s\n' "$s" >> "$CREW_I18N_MISS" ;;
324
+ esac
325
+ fi
326
+ # shellcheck disable=SC2059
327
+ # `--` ends option parsing: without it ANY format starting with '-' ("--dotnet …", "- x") is read as an option
328
+ # and printf exits 2 — measured on bash 3.2 (macOS) and Git Bash 5.3; with it both assign normally.
329
+ printf -v _M -- "$s" "$@"
330
+ }
331
+ # ---- /CREW-I18N -----------------------------------------------------------------------------------------
332
+
333
+ # --- color: only on an interactive TTY (same guard as start.sh) ---
334
+ if [ -t 1 ] && [ "${TERM:-dumb}" != "dumb" ] && [ -z "${NO_COLOR:-}" ]; then
335
+ R=$'\033[0m'; B=$'\033[1m'; D=$'\033[2m'; CY=$'\033[36m'; GR=$'\033[32m'; YE=$'\033[33m'; MG=$'\033[35m'
336
+ else R=''; B=''; D=''; CY=''; GR=''; YE=''; MG=''; fi
337
+ h1() { printf '\n%s%s%s%s\n' "$B" "$CY" "$1" "$R"; }
338
+ sub() { printf '%s%s%s\n' "$D" "$1" "$R"; }
339
+ # printf's %-Ns pads by BYTES, so a Turkish label (İ, ç, ı are two bytes each) came out short and knocked its
340
+ # column out of line. Pad by characters: drop UTF-8 continuation bytes under C and count the rest. No fork.
341
+ padr() { # $1 = text, $2 = width; sets PADDED
342
+ local LC_ALL=C n
343
+ n="${1//[$'\200'-$'\277']/}"; n=$(( $2 - ${#n} ))
344
+ PADDED="$1"
345
+ while [ "$n" -gt 0 ]; do PADDED="$PADDED "; n=$((n-1)); done
346
+ }
347
+ row() { padr "$1" 20; printf ' %s%s%s %s\n' "$B" "$PADDED" "$R" "$2"; }
348
+ warn(){ printf ' %s!%s %s%s%s\n' "$YE" "$R" "$YE" "$1" "$R"; }
349
+ # smart suggestion line: number+decision · SUGGESTED(green) · rationale(dim)
350
+ prop(){ local _p1; padr "$1" 18; _p1="$PADDED"; padr "$2" 24
351
+ printf ' %s%s%s %s%s%s %s%s%s\n' "$B" "$_p1" "$R" "$GR" "$PADDED" "$R" "$D" "$3" "$R"; }
352
+ # Translating twins of the helpers above: the FIRST argument is the English key, the rest fill its %s.
353
+ # rowv translates only the label (value printed verbatim); rowm translates both; propm all three.
354
+ say() { _mt "$@"; printf ' %s\n' "$_M"; }
355
+ h1m() { _mt "$@"; h1 "$_M"; }
356
+ subm() { _mt "$@"; sub "$_M"; }
357
+ warnm(){ _mt "$@"; warn "$_M"; }
358
+ rowv() { local v="$2"; _mt "$1"; row "$_M" "$v"; }
359
+ rowm() { local l; _mt "$1"; l="$_M"; shift; _mt "$@"; row "$l" "$_M"; }
360
+ propm(){ local a b; _mt "$1"; a="$_M"; _mt "$2"; b="$_M"; shift 2; _mt "$@"; prop "$a" "$b" "$_M"; }
361
+ # --yes ALWAYS wins — check it BEFORE the TTY test. A `read` on a TTY blocks on human input, so if we tested
362
+ # `-t 0` first, an agent/CI run that DID pass --yes but happens to inherit a TTY (Claude Code on Windows runs
363
+ # under a pty) would hang at the prompt, ignoring --yes. Only when --yes is absent do we prompt (TTY) or decline
364
+ # cleanly (no TTY — a bare `read` would otherwise block forever on an open-but-empty stdin).
365
+ # $1 = the English prompt (translated here), further args fill its %s.
366
+ ask_yes(){ local a q w
367
+ _mt "$@"; q="$_M"
368
+ if [ "${ASSUME_YES:-0}" = 1 ]; then _mt 'yes'; printf '%s %s %s(--yes)%s\n' "$q" "$_M" "$D" "$R"; a=yes
369
+ elif [ -t 0 ]; then _mt '[yes/no]'; printf '%s %s: ' "$q" "$_M"; read -r a || a=""
370
+ else _mt 'no'; w="$_M"; _mt '(non-interactive — pass --yes to apply)'; printf '%s %s %s%s%s\n' "$q" "$w" "$D" "$_M" "$R"; a=no; fi
371
+ case "$a" in
372
+ [yY]|[yY][eE][sS]|[eE]|[eE][vV][eE][tT]) return 0;;
373
+ [hH]|hayır|Hayır|hayir|Hayir) return 1;; # explicit Turkish "no" (anything unrecognised is "no" too)
374
+ *) return 1;;
375
+ esac; }
376
+ # Twin of start.sh's gi_add — the same two defects were present in both scripts, and twice in this one.
377
+ # * A .gitignore whose last line has NO trailing newline concatenates the first appended entry onto it:
378
+ # `node_modules` + `docs/` becomes `node_modulesdocs/`, which ignores neither. Reproduced on the old
379
+ # shape before this was written. `touch` does not help — it changes the timestamp, not the last byte.
380
+ # * `grep -qxF` is an exact-literal test, so a repo that already ignores `.claude` (no trailing slash)
381
+ # collected a second, redundant line. `git check-ignore` asks about the PATH rather than the spelling,
382
+ # which is the technique this script already uses at the #4 share branch and never applied to its own
383
+ # writes. Outside a repository there is nothing to ask, so the literal test stays as the fallback.
384
+ gi_add() { # $@ = entries to ensure in ./.gitignore; sets GI_WROTE to what it actually added
385
+ local e
386
+ GI_WROTE=""
387
+ [ -e .gitignore ] || : > .gitignore
388
+ for e in "$@"; do
389
+ if git rev-parse --git-dir >/dev/null 2>&1; then
390
+ 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
391
+ else
392
+ grep -qxF "$e" .gitignore 2>/dev/null && continue
393
+ fi
394
+ if [ -s .gitignore ] && [ "$(tail -c 1 .gitignore | od -An -tx1 | tr -d ' \n')" != "0a" ]; then
395
+ printf '\n' >> .gitignore
396
+ fi
397
+ printf '%s\n' "$e" >> .gitignore
398
+ GI_WROTE="$GI_WROTE $e"
399
+ done
400
+ GI_WROTE="${GI_WROTE# }"
401
+ }
402
+ # never-overwrite copy: does NOT overwrite an EXISTING target file (project file is preserved), skips+counts.
403
+ # Result globals: ret_add / ret_skip; conflicts are added to SKIP_LIST. Do NOT call in a subshell (globals are lost).
404
+ SKIP_LIST=""
405
+ # $4 = space-separated names to SKIP entirely, matched against the first path component of each source file
406
+ # ('crew-frontend-expert.md' for agents/, 'a11y' for skills/). We skip rather than copy-then-delete: a project
407
+ # may own a directory of the same name, and a refresh must never remove the project's own files.
408
+ # Per-FILE process spawns are what make this hurt on Windows: Git Bash pays 62-135 ms per process where Linux pays
409
+ # ~1.7ms, so `dirname`+`mkdir`+`cp` for each of ~100 payload files is minutes, not milliseconds. Measured on a
410
+ # user's machine: a refresh took 6m43s wall with 66s of it in the kernel — the fork signature, not file I/O.
411
+ # - the refresh case (force=1, nothing excluded) is ONE `cp -R`, not one `cp` per file;
412
+ # - the selective case keeps per-file decisions but drops `dirname` for parameter expansion, and only calls
413
+ # `mkdir` when the directory actually changes (find walks a directory at a time, so that is nearly always).
414
+ copy_noclobber(){ local src="$1" dst="$2" force="${3:-0}" exclraw="${4:-}" excl=" ${4:-} " rel top f d lastd=""; ret_add=0; ret_skip=0; [ -d "$src" ] || return; mkdir -p "$dst"
415
+ if [ "$force" = 1 ] && [ -z "$exclraw" ]; then
416
+ ret_add="$(find "$src" -type f 2>/dev/null | wc -l | tr -d ' ')"
417
+ cp -R "$src/." "$dst/" 2>/dev/null
418
+ return
419
+ fi
420
+ while IFS= read -r f; do rel="${f#"$src"/}"; top="${rel%%/*}"
421
+ case "$excl" in *" $top "*) continue ;; esac
422
+ if [ -e "$dst/$rel" ] && [ "$force" != 1 ]; then ret_skip=$((ret_skip+1)); SKIP_LIST="$SKIP_LIST $dst/$rel"
423
+ else
424
+ case "$rel" in */*) d="$dst/${rel%/*}" ;; *) d="$dst" ;; esac
425
+ [ "$d" = "$lastd" ] || { mkdir -p "$d"; lastd="$d"; }
426
+ cp "$f" "$dst/$rel"; ret_add=$((ret_add+1))
427
+ fi
428
+ done < <(find "$src" -type f 2>/dev/null); }
429
+
430
+ # --- CLAUDE.md split (shared contract with start.sh; keep the two in lockstep) ---
431
+ IMPORT_LINE='@.claude/DISCIPLINE.md'
432
+ # Sentinel matched ANCHORED to line start, so prose that merely names the token is never mistaken for the split
433
+ # point. Abort loudly if it is gone: a silent miss ships the ENTIRE template as "discipline" — which is exactly
434
+ # what the previous '<PROJE ADI>' marker did once the payload was translated to English.
435
+ kit_require_sentinel() { grep -qE '^<!-- KIT:DISCIPLINE-END' "$1" || { _mt "ERROR: the %s sentinel line is missing from %s — refusing to guess the discipline/project split." "'<!-- KIT:DISCIPLINE-END'" "$1"; printf '%s\n' "$_M"; exit 1; }; }
436
+ kit_discipline_of() { awk '/^<!-- KIT:DISCIPLINE-END/{exit} {print}' "$1"; }
437
+ kit_project_of() { awk 'f{print} /^<!-- KIT:DISCIPLINE-END/{f=1}' "$1"; }
438
+ # Anchored: the import must BE the line, not merely be mentioned in prose (the discipline text names the path).
439
+ kit_has_import() { grep -qE '^[[:space:]]*@\.claude/DISCIPLINE\.md[[:space:]]*$' "$1" 2>/dev/null; }
440
+ # A pre-1.1 install pasted the whole discipline inline into CLAUDE.md; adding the @import would load it twice.
441
+ # Both markers are required: a project that happens to write its own "Four working principles" heading is NOT a
442
+ # legacy kit install, and treating it as one would leave it without the discipline forever.
443
+ kit_claude_md_is_legacy() {
444
+ grep -q '^## Four working principles' "$1" 2>/dev/null && grep -qE '^### 4\.[45] ' "$1" 2>/dev/null
445
+ }
446
+ # Where does the project section of a legacy CLAUDE.md begin? Line 1 is the discipline's own '# CLAUDE.md — Working
447
+ # rules' heading, so look from line 2 on. Fallback: the first '# ' heading after the §4.5 block (covers a renamed
448
+ # heading). Capture the output — `awk … && return` would return on awk's exit status even when it printed nothing.
449
+ kit_legacy_boundary() {
450
+ local n
451
+ n="$(awk 'NR>1 && /^# CLAUDE\.md/{print NR; exit}' "$1" 2>/dev/null)"
452
+ [ -n "$n" ] || n="$(awk '/^### 4\.5 /{f=1;next} f && /^# /{print NR; exit}' "$1" 2>/dev/null)"
453
+ printf '%s' "$n"
454
+ }
455
+ # A project installed before kit.conf existed carries its backend pattern only in what is on disk. Read it
456
+ # back, so the 3.0 migration below can tell a former .NET install (which keeps its pattern skill) from one that
457
+ # never had it.
458
+ kit_infer_shape() {
459
+ # A pre-3.0 .NET install is marked by the .NET pattern skill being on disk; the generic stack pruned it.
460
+ # BOTH NAMES. The skill was `devarch-module` from v1.0.0 until the rename to `cqrs-aop-module`, and this
461
+ # function exists for exactly the installs that predate kit.conf — which are the installs that carry the OLD
462
+ # name. Reading only the new one would miss every such project, and the migration notice with it.
463
+ if [ -d .claude/skills/cqrs-aop-module ] || [ -d .claude/skills/devarch-module ]; then KIT_STACK=dotnet; else KIT_STACK=generic; fi
464
+ KIT_INSTALLER="${KIT_INSTALLER:-pre-kit.conf}"
465
+ INFERRED=1
466
+ }
467
+ # tr -d '\r' on both readers: a CRLF file (Windows checkout, or kit.conf reopened in Notepad) would otherwise
468
+ # glue '\r' to every value — 'generic\r' matches no branch, and the refresh would pick the wrong pattern.
469
+ kit_conf_get() { [ -f .claude/kit.conf ] && sed -n "s/^$1=//p" .claude/kit.conf | head -1 | tr -d '\r'; }
470
+
471
+ # Turn a project agent into a DRAFT project skill (prints the SKILL.md to stdout). On takeover the kit's crew-
472
+ # agent owns routing (who/when); this carries the OLD agent's domain (its "how") into an active skill the kit
473
+ # agent can apply, so nothing is lost from the working setup. It keeps the agent's description (domain keywords)
474
+ # and body, guarantees a Trigger-phrases line for discovery, and marks it as a carried-over draft to refine.
475
+ kit_agent_to_skill() { # $1 = agent .md file, $2 = base name
476
+ local f="$1" b="$2" desc trig body
477
+ desc="$(awk '/^---[ \t]*$/{c++; next} c==1 && /^description:/{sub(/^description:[ \t]*\|?[ \t]*/,""); if($0!="")print; exit}' "$f")"
478
+ [ -n "$desc" ] || desc="Project-specific $b knowledge carried over when Crewforth took over the role."
479
+ trig="$(awk '/[Tt]rigger phrases:/{sub(/.*[Tt]rigger phrases:[ \t]*/,""); print; exit}' "$f")"
480
+ [ -n "$trig" ] || trig="\"$b\""
481
+ body="$(awk 'c>=2{print} /^---[ \t]*$/{c++}' "$f")"
482
+ printf '%s\n' '---' "name: ${b}-local"
483
+ printf 'description: |\n %s\n' "$desc"
484
+ printf ' Carried over from the project'"'"'s own %s agent when crew-%s took over the role — trim to the domain worth keeping.\n' "$b" "$b"
485
+ printf ' Trigger phrases: %s\n---\n\n' "$trig"
486
+ printf '# %s — project knowledge (carried over on Crewforth adoption)\n\n' "$b"
487
+ printf '> Draft generated from your original `%s` agent; Crewforth'"'"'s `crew-%s` applies this skill. Refine it to the domain "how" worth keeping.\n\n' "$b" "$b"
488
+ printf '%s\n' "$body"
489
+ }
490
+
491
+ h1m 'crewforth adopt · Stage 1 — DETECTION (read-only; nothing changes)'
492
+ subm 'Reads the existing project, produces a smart suggestion for the 7 handover decisions. Approval + mutation in the next stage.'
493
+
494
+ # ========================= [1] ENVIRONMENT =========================
495
+ h1m '[1] Environment'
496
+ # git context — in a worktree/submodule .git is a FILE (do NOT use [ -d .git ]; red-team hole #6)
497
+ IS_GIT=0; GITTOP=""; GITKIND="no git — 'git init' required"
498
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
499
+ IS_GIT=1; GITTOP="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
500
+ if [ -f "$GITTOP/.git" ]; then GITKIND="worktree/submodule (.git file)"; else GITKIND="normal repo"; fi
501
+ fi
502
+ rowm 'git' "$GITKIND" # every GITKIND value is a literal with its own table row
503
+
504
+ # existing hook system (decision #5 — single-hooksPath clash with husky/lefthook)
505
+ HOOKSYS="none"
506
+ CURHP="$(git config --get core.hooksPath 2>/dev/null || true)"
507
+ case "$CURHP" in
508
+ "") : ;;
509
+ .claude/hooks|.claude/git-shim) HOOKSYS="Crewforth (already armed)"; CURHP="" ;; # kit's OWN path — not a foreign chain (re-adopt must not shim itself)
510
+ *) HOOKSYS="core.hooksPath=$CURHP" ;;
511
+ esac
512
+ [ -d .husky ] && HOOKSYS="husky (.husky/)"
513
+ { [ -f lefthook.yml ] || [ -f .lefthook.yml ]; } && HOOKSYS="lefthook"
514
+ [ -f .pre-commit-config.yaml ] && HOOKSYS="pre-commit framework"
515
+ # A core.hooksPath value is DATA (a Windows one carries backslashes, which a printf format would expand: `\t`, `\n`),
516
+ # so it goes in as a plain value; only the fixed names are table keys.
517
+ case "$HOOKSYS" in core.hooksPath=*) rowv 'git hook system' "$HOOKSYS" ;; *) rowm 'git hook system' "$HOOKSYS" ;; esac
518
+
519
+ # stack hint (context only — it selects nothing since 3.0; the backend-architecture skill resolves the stack).
520
+ # Look PAST the root: a .NET solution commonly lives in ./backend, ./src, ./server, and a root-only
521
+ # `ls ./*.sln` reported "unknown" for exactly those layouts. Search a few levels deep, skipping build/vendor dirs.
522
+ STACK="unknown"
523
+ # PRUNE, don't filter. The old form walked bin/obj/node_modules in full and threw the results away afterwards —
524
+ # on a real .NET repo that is tens of thousands of directory entries, and on Windows every one of them is a
525
+ # Defender-scanned syscall. Pruning stops the descent instead of discarding its output.
526
+ PRUNE_DIRS=' ( -name bin -o -name obj -o -name node_modules -o -name .git -o -name .vs -o -name packages ) -prune -o '
527
+ # shellcheck disable=SC2086
528
+ DOTNET_HIT="$(find . -maxdepth 3 $PRUNE_DIRS \( -name '*.sln*' -o -name '*.csproj' \) -print 2>/dev/null | head -1)"
529
+ if [ -n "$DOTNET_HIT" ]; then STACK=".NET"
530
+ elif [ -f package.json ]; then STACK="Node/JS"
531
+ elif [ -f go.mod ]; then STACK="Go"
532
+ elif [ -f pyproject.toml ] || [ -f requirements.txt ]; then STACK="Python"; fi
533
+ _mt "$STACK"; rowv 'stack hint' "$_M"
534
+
535
+ # ================= [2] EXISTING AGENTIC SETUP =================
536
+ h1m '[2] Existing agentic setup (accumulated work to inherit)'
537
+ HAS_CLAUDE=0; [ -d .claude ] && HAS_CLAUDE=1
538
+ # count only the PROJECT's own agents/skills — exclude the kit's crew- agents and kit skills left by a prior adopt
539
+ # A 2.x install still has the kit's components under their old <x>-csk names (moved further down); they are the
540
+ # kit's, not the project's, so they are left out of both counts. The names come from the payload, like the move.
541
+ KIT_OLD=" "; for kf in "$SRC"/agents/crew-*.md "$SRC"/skills/crew-*/; do kf="${kf%/}"; kf="${kf##*/}"; kf="${kf%.md}"; KIT_OLD="$KIT_OLD${kf#crew-}-csk "; done
542
+ N_PAGENTS=0
543
+ if [ -d .claude/agents ]; then
544
+ while IFS= read -r f; do f="${f##*/}"; f="${f%.md}"; case "$KIT_OLD" in *" $f "*) ;; *) N_PAGENTS=$((N_PAGENTS+1)) ;; esac
545
+ done < <(find .claude/agents -name '*.md' ! -name 'crew-*.md' 2>/dev/null)
546
+ fi
547
+ N_PSKILLS=0
548
+ if [ -d .claude/skills ]; then
549
+ # `basename $(dirname …)` per skill is two spawns × every installed skill, for a number printed once. Parameter
550
+ # expansion does the same slicing with none.
551
+ while IFS= read -r f; do d="${f%/SKILL.md}"; d="${d##*/}"
552
+ [ -d "$SRC/skills/$d" ] || case "$KIT_OLD" in *" $d "*) ;; *) N_PSKILLS=$((N_PSKILLS+1)) ;; esac
553
+ done < <(find .claude/skills -name 'SKILL.md' 2>/dev/null)
554
+ fi
555
+ # Same-domain agent overlap: a PROJECT agent whose base name matches a kit crew- agent (e.g. backend-expert vs
556
+ # crew-backend-expert). The two describe the same job, so the router has to pick between them — plain coexist
557
+ # leaves that ambiguous and the project's older agent tends to win, which defeats installing the kit. Collect
558
+ # the overlaps so the handover can RESOLVE them, not merely note them.
559
+ COLLIDE=""
560
+ if [ -d .claude/agents ]; then
561
+ for kf in "$SRC"/agents/crew-*.md; do b="${kf##*/}"; b="${b%.md}"; b="${b#crew-}"; [ -f ".claude/agents/$b.md" ] && COLLIDE="$COLLIDE $b"; done
562
+ fi
563
+ COLLIDE="${COLLIDE# }"; N_COLLIDE=0; [ -n "$COLLIDE" ] && N_COLLIDE="$(printf '%s\n' $COLLIDE | wc -l | tr -d ' ')"
564
+ HAS_MD=0; [ -f CLAUDE.md ] && HAS_MD=1
565
+ HAS_SETTINGS=0; [ -f .claude/settings.json ] && HAS_SETTINGS=1
566
+ # already-adopted fingerprint: did a PRIOR adopt/kit install run here? -> REFRESH semantics, not a fresh handover
567
+ KIT_PRESENT=0; KIT_VER=""
568
+ { [ -f .claude/DISCIPLINE.md ] || [ -d .claude/git-shim ] || ls .claude/agents/crew-*.md >/dev/null 2>&1 || [ -f .claude/VERSION ]; } && KIT_PRESENT=1
569
+ # A 2.x install, by the kit's OWN old names only: a project agent that merely ends in -csk (my-helper-csk.md) must
570
+ # not make a never-installed project look installed — that turns on the force-refresh and overwrites its files.
571
+ if [ "$KIT_PRESENT" = 0 ]; then for n in $KIT_OLD; do [ -f ".claude/agents/$n.md" ] && { KIT_PRESENT=1; break; }; done; fi
572
+ [ -f .claude/VERSION ] && KIT_VER="$(head -1 .claude/VERSION 2>/dev/null)"
573
+ # Backend pattern of the existing install. Since 3.0 there is ONE shape (generic) and this only answers a
574
+ # migration question: was this a pre-3.0 .NET install? If so its pattern skill stays, as the project's own.
575
+ # LEGACY_PROFILE is a pre-2.0 leftover of the same kind: it selects nothing, it only drives a notice.
576
+ LEGACY_PROFILE="$(kit_conf_get profile)"; KIT_STACK="$(kit_conf_get stack)"; KIT_INSTALLER="$(kit_conf_get installer)"
577
+ # No kit.conf means the project predates it (v1.0.x). Recover the pattern from the files themselves.
578
+ INFERRED=0
579
+ { [ "$KIT_PRESENT" = 1 ] && [ -z "$KIT_STACK" ]; } && kit_infer_shape
580
+ # LEGACY_DOTNET: recorded dotnet, or no record at all while a pattern skill sits on disk. Either way the kit
581
+ # writes stack=generic from here on and leaves the skill where it is (see the 3.0 migration further down).
582
+ # Only for a project the KIT was on: a fresh adopt of a repo that ships its own pattern skill is not a migration,
583
+ # and telling it "the kit no longer ships" a skill it never got from the kit would be false.
584
+ LEGACY_DOTNET=0
585
+ if [ "$KIT_PRESENT" = 1 ] && [ "$KIT_STACK" = dotnet ]; then LEGACY_DOTNET=1; fi
586
+ if [ "$HAS_CLAUDE" = 1 ]; then rowm '.claude/' 'present — %s project agents · %s project skills' "$N_PAGENTS" "$N_PSKILLS"
587
+ else rowm '.claude/' 'none'; fi
588
+ if [ "$HAS_MD" = 1 ]; then _v=present; else _v=none; fi; rowm 'CLAUDE.md' "$_v"
589
+ if [ "$HAS_SETTINGS" = 1 ]; then _v=present; else _v=none; fi; rowm 'settings.json' "$_v"
590
+ [ "$KIT_PRESENT" = 1 ] && { _mt 'already adopted%s — this run REFRESHES Crewforth files, project untouched' "${KIT_VER:+ (v$KIT_VER)}"
591
+ rowv 'Crewforth status' "${YE}$_M${R}"; }
592
+ if [ "$LEGACY_DOTNET" = 1 ]; then
593
+ if [ "$INFERRED" = 1 ]; then _k='inferred pattern'; else _k='recorded pattern'; fi
594
+ rowm "$_k" 'stack=dotnet — 3.0 records generic; the pattern skill stays as a project skill'
595
+ elif [ -n "$KIT_STACK" ]; then
596
+ if [ "$INFERRED" = 1 ]; then
597
+ _mt '(no kit.conf — read back from the installed files)'
598
+ rowm 'inferred pattern' 'stack=%s %s' "$KIT_STACK" "${YE}$_M${R}"
599
+ else
600
+ rowm 'recorded pattern' 'stack=%s · via %s' "$KIT_STACK" "${KIT_INSTALLER:-?}"
601
+ fi
602
+ fi
603
+ [ -n "$LEGACY_PROFILE" ] && { _mt '— profile pruning was removed in 2.0; this refresh completes the install'
604
+ rowv 'pre-2.0 profile' "${YE}profile=${LEGACY_PROFILE}${R} ${D}$_M${R}"; }
605
+
606
+ # tracked in git? (decision #4 — share/hide)
607
+ TRACKED=0
608
+ if [ "$IS_GIT" = 1 ]; then
609
+ git ls-files --error-unmatch CLAUDE.md >/dev/null 2>&1 && TRACKED=1
610
+ { [ "$HAS_CLAUDE" = 1 ] && [ -n "$(git ls-files .claude 2>/dev/null | head -1)" ]; } && TRACKED=1
611
+ fi
612
+ if [ "$TRACKED" = 1 ]; then _v='YES — shared with the team'; else _v='no/untracked'; fi
613
+ rowm '.claude/CLAUDE.md in git' "$_v"
614
+
615
+ # Supply-chain scan (advisory, read-only): the project's OWN (non-crew) skills/agents may have been pulled from an
616
+ # untrusted source. Scan them for red flags (curl|bash, prompt-injection directives, credential exfil) before the
617
+ # kit starts coexisting with them. Heuristic; it NEVER blocks — it surfaces, the user judges.
618
+ if { [ "$N_PAGENTS" != 0 ] || [ "$N_PSKILLS" != 0 ]; } && [ -f "$SRC/eval/scan-skill.sh" ]; then
619
+ SCANOUT="$(bash "$SRC/eval/scan-skill.sh" .claude 2>/dev/null)"
620
+ if printf '%s' "$SCANOUT" | grep -qE 'DANGER|REVIEW'; then
621
+ warnm 'supply-chain scan flagged existing project skills/agents (advisory — review before trusting them):'
622
+ printf '%s\n' "$SCANOUT" | grep -E 'DANGER|REVIEW' | sed 's/^/ /'
623
+ _mt 'full report after install: %s (heuristic; a security skill can score low by design)' 'bash .claude/eval/scan-skill.sh .claude'
624
+ sub " $_M"
625
+ else
626
+ rowm 'supply-chain scan' 'existing project skills/agents look clean (no red flags)'
627
+ fi
628
+ fi
629
+
630
+ # co-author/sign-off convention (decision #3)
631
+ COAUTHOR=0
632
+ [ "$IS_GIT" = 1 ] && git log -80 --format='%b' 2>/dev/null | grep -qiE 'Co-Authored[-]By|Signed-off-by' && COAUTHOR=1 # [-] : not a contiguous literal in source (trace hook)
633
+
634
+ # off-repo hint (decision #7 — decisions may live in chat/on the web)
635
+ OFFREPO=0; { [ "$HAS_CLAUDE" = 0 ] && [ "$HAS_MD" = 0 ]; } && OFFREPO=1
636
+
637
+ # ===================== [3] SMART SUGGESTION ======================
638
+ h1m '[3] 7 handover decisions — SMART SUGGESTION'
639
+ subm 'format: decision -> SUGGESTED -> rationale (you can review and override all of them in the next stage)'
640
+ if [ "$N_COLLIDE" != 0 ]; then
641
+ propm '1 Role overlap' 'Crewforth takes over' '%s project agent(s) cover the SAME job as a Crewforth agent (%s) — routing is ambiguous; Crewforth wins, yours preserved' "$N_COLLIDE" "$COLLIDE"
642
+ elif [ "$N_PAGENTS" != 0 ]; then
643
+ propm '1 Role clash' 'keep (coexist)' '%s project agents, none overlap a Crewforth role; thanks to the crew- prefix they live side by side' "$N_PAGENTS"
644
+ else
645
+ propm '1 Role clash' 'none' 'no custom agents found in the project'
646
+ fi
647
+ propm '2 Precedence' 'project wins (fixed)' "on conflict the project's rules always win; Crewforth fills gaps (not overridable)"
648
+ if [ "$COAUTHOR" = 1 ]; then
649
+ propm '3 Trace gate' 'loosen (.trace-allowlist)' 'co-author/sign-off present in git log — may be a convention'
650
+ else
651
+ propm '3 Trace gate' 'keep' 'no co-author/sign-off convention seen'
652
+ fi
653
+ if [ "$TRACKED" = 1 ]; then
654
+ propm '4 Share/hide' 'share' '.claude/CLAUDE.md is tracked — keep sharing with the team'
655
+ else
656
+ propm '4 Share/hide' 'share' 'untracked; Crewforth files are shared by default — pick hide to keep them local'
657
+ fi
658
+ if [ "$HOOKSYS" = "none" ]; then
659
+ propm '5 Git hooks' 'install directly' 'no existing hook system'
660
+ else
661
+ case "$HOOKSYS" in core.hooksPath=*) _M="$HOOKSYS" ;; *) _mt "$HOOKSYS" ;; esac # a path is data, a name is a key
662
+ propm '5 Git hooks' 'SHIM (bridge)' 'existing %s present — let both run' "$_M"
663
+ fi
664
+ propm '6 Brownfield DoD' 'baseline+regression' 'existing code debt unknown; absolute 0/0/0/0 is risky'
665
+ if [ "$OFFREPO" = 1 ]; then
666
+ warnm '7 Off-repo: no local .claude/CLAUDE.md — decisions may live in chat/on the web; there is context I CANNOT SEE.'
667
+ propm ' -> suggestion' 'you transfer' "in the mutation stage 'paste if any' is asked; goes into HANDOVER.md"
668
+ else
669
+ propm '7 Off-repo' 'local + ask' 'some decisions are in files; still may be in-chat (asked during the stage)'
670
+ fi
671
+
672
+ # ============ COMPILE DECISIONS + OVERRIDE (Stage B) ============
673
+ DEC1="$([ "$N_PAGENTS" != 0 ] && echo keep || echo none)"
674
+ DEC2="project" # precedence is FIXED to project-wins (not overridable — reflected in the @import comment)
675
+ DEC3="$([ "$COAUTHOR" = 1 ] && echo loosen || echo keep)"
676
+ DEC4="$([ "$TRACKED" = 1 ] && echo share || echo kit-default)"
677
+ DEC6="baseline"
678
+ DEC7="$([ "$OFFREPO" = 1 ] && echo transfer || echo local)"
679
+ # normalize display defaults to a real, offered token
680
+ [ "$DEC1" = none ] && DEC1=keep
681
+ [ "$DEC4" = kit-default ] && DEC4=share
682
+ if [ -t 0 ]; then
683
+ h1m 'Review the decisions'
684
+ # ask_dec: echoes the chosen value to STDOUT; ALL prompts/errors go to STDERR so $(...) captures only the value.
685
+ # $1 is the English label (translated here); the typed tokens $2/$3 stay English — they are what the user types.
686
+ ask_dec(){ local label a="$2" b="$3" cur="$4" v fa fb; fa="${a:0:1}"; fb="${b:0:1}"; _mt "$1"; label="$_M"
687
+ while :; do
688
+ _mt '[%s/%s] (current: %s, ENTER=keep): ' "$a" "$b" "${B}$cur${R}"
689
+ printf ' %s%s%s %s' "$B" "$label" "$R" "$_M" >&2
690
+ read -r v || v=""
691
+ case "$v" in
692
+ "") echo "$cur"; return ;;
693
+ "$a"|"$fa") echo "$a"; return ;;
694
+ "$b"|"$fb") echo "$b"; return ;;
695
+ *) _mt 'type "%s" or "%s" (or ENTER to keep "%s")' "$a" "$b" "$cur"
696
+ printf ' %s! %s%s\n' "$YE" "$_M" "$R" >&2 ;;
697
+ esac
698
+ done; }
699
+ if ask_yes 'Accept all smart suggestions?'; then
700
+ subm 'All smart suggestions accepted.'
701
+ else
702
+ subm 'Reviewing each decision. ENTER keeps the current value. (#1 overlap and #2/#5 are handled separately below.)'
703
+ DEC3="$(ask_dec '#3 Trace gate' loosen keep "$DEC3")"
704
+ DEC4="$(ask_dec '#4 Share/hide' share hide "$DEC4")"
705
+ DEC6="$(ask_dec '#6 Brownfield DoD' baseline absolute "$DEC6")"
706
+ DEC7="$(ask_dec '#7 Off-repo' transfer skip "$DEC7")"
707
+ fi
708
+ say 'Final: #3=%s #4=%s #6=%s #7=%s (#2 project-wins, #5 SHIM — fixed)' "$DEC3" "$DEC4" "$DEC6" "$DEC7"
709
+ else
710
+ subm '(non-interactive: smart defaults accepted)'
711
+ fi
712
+
713
+ # --- Backend stack: one shape since 3.0 ----------------------------------------------------------------------
714
+ # The fresh-adopt .NET question and the stale-generic correction are gone with the .NET install path: there is
715
+ # nothing left to choose here. The stack is resolved per project by the backend-architecture skill and recorded
716
+ # in CLAUDE.md ## Stack. CSK_CORRECT_STACK is read only to say it no longer does anything — an automation that
717
+ # still sets it deserves to hear that, not a silent no-op.
718
+ KIT_STACK=generic
719
+ [ "${CSK_CORRECT_STACK:-0}" = 1 ] && say 'CSK_CORRECT_STACK has no effect since 3.0 — there is one backend shape; the stack lives in CLAUDE.md ## Stack.'
720
+
721
+ # --- Role overlap (#1): resolve same-domain agent collisions ---------------------------------------------
722
+ # When a project agent and a kit crew- agent cover the same job, "coexist" leaves routing ambiguous. Offer to
723
+ # resolve it. takeover = kit wins (your agent preserved, moved out of the routing pool); keepmine = your agent
724
+ # wins (the kit's overlapping crew- is not installed); coexist = keep both (documented). Non-interactive -> takeover
725
+ # (you ran adopt to get the kit's agents). The chosen mode is APPLIED on the handover branch in Stage 2.
726
+ COLLIDE_MODE=coexist
727
+ if [ "$N_COLLIDE" != 0 ]; then
728
+ h1m 'Role overlap — project & Crewforth both cover: %s' "$COLLIDE"
729
+ subm "Two agents for one job = the router picks one, usually your older agent — so Crewforth's would sit idle."
730
+ # the first word of each line is the token the user types, so it stays English and outside the message
731
+ _mt "Crewforth's crew- agents win; each old agent's domain is imported to a draft skill (skills/<name>-local), original backed up"; sub " takeover $_M"
732
+ _mt "your agents win; Crewforth's overlapping crew- agents are not installed"; sub " keepmine $_M"
733
+ _mt 'keep both (routing stays ambiguous; only documented in HANDOVER)'; sub " coexist $_M"
734
+ COLLIDE_MODE=takeover
735
+ if [ -t 0 ] && [ "${ASSUME_YES:-0}" != 1 ]; then # --yes keeps the documented non-interactive default (takeover)
736
+ while :; do
737
+ _mt 'owner'; printf ' %s%s%s [takeover/keepmine/coexist] (ENTER=takeover): ' "$B" "$_M" "$R"
738
+ read -r _v || _v=""
739
+ case "$_v" in ""|t|takeover) COLLIDE_MODE=takeover; break ;; k|keepmine) COLLIDE_MODE=keepmine; break ;; c|coexist) COLLIDE_MODE=coexist; break ;;
740
+ *) _mt 'type takeover, keepmine or coexist'; printf ' %s! %s%s\n' "$YE" "$_M" "$R" >&2 ;; esac
741
+ done
742
+ fi
743
+ say 'overlap -> %s' "$COLLIDE_MODE"
744
+ fi
745
+ # #1 display/HANDOVER value reflects what actually happens.
746
+ case "$COLLIDE_MODE" in
747
+ takeover) DEC1=takeover ;; keepmine) DEC1=keepmine ;;
748
+ *) DEC1="$([ "$N_PAGENTS" != 0 ] && echo keep || echo none)" ;;
749
+ esac
750
+
751
+ # A non-interactive REFRESH of an existing kit install is low-risk — it rewrites only kit-owned files and the change
752
+ # is staged/reversible — so it applies by default; this is what lets /crew-update self-heal without any flag. A first
753
+ # adopt (KIT_PRESENT=0, a larger brownfield change) still requires an explicit --yes when there is no TTY to ask.
754
+ if [ ! -t 0 ] && [ "$KIT_PRESENT" = 1 ]; then ASSUME_YES=1; fi
755
+
756
+ # ================= [STAGE 2] HANDOVER BRANCH + COEXIST =================
757
+ h1m 'Stage 2 — apply Crewforth (coexist)'
758
+ if [ "$IS_GIT" != 1 ]; then
759
+ warnm 'no git repo — cannot apply safely. First: %s (then run again).' 'git init && git add -A && git commit -m init'
760
+ exit 0
761
+ fi
762
+
763
+ # --- Branch decision (B5): where does this land? A flag wins; otherwise a smart default. ---
764
+ # A new branch isolates a big first change so the main line stays clean until you review. But on a routine UPDATE
765
+ # of a project whose .claude/ is gitignored, a forced new branch is empty and pointless — the refresh lands on disk
766
+ # with no tracked diff to review, so a branch is pure noise on top of your working branch.
767
+ BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
768
+ DEC_BR="$BRANCH_MODE"
769
+ if [ -z "$DEC_BR" ]; then
770
+ if [ "$KIT_PRESENT" != 1 ]; then DEC_BR=new # first adopt: isolate the change, keep the main line clean
771
+ elif [ "$TRACKED" != 1 ]; then DEC_BR=here # update + untracked .claude: no tracked diff -> a branch is noise
772
+ elif [ -t 0 ]; then # update + tracked: a real diff exists -> prefer new, but ask
773
+ ask_yes "Apply on a NEW review branch? (no = apply on the current branch '%s')" "$BASE" && DEC_BR=new || DEC_BR=here
774
+ else DEC_BR=new; fi # non-interactive + tracked: the safe default is a new branch
775
+ fi
776
+
777
+ if [ "$DEC_BR" = here ]; then _mt "the current branch '%s'" "$BASE"; else _mt "a new review branch (off '%s')" "$BASE"; fi
778
+ WHERE="$_M"
779
+ # Missing tools named before the mutation prompt, not after, while going ahead is still a choice. The settings
780
+ # merge is not among them: it is awk and runs the same everywhere. Report-only; never blocks.
781
+ [ -f "$SRC/eval/preflight.sh" ] && bash "$SRC/eval/preflight.sh"
782
+ if ! ask_yes 'Apply Crewforth onto %s now? (mutation; staged-not-committed, reversible with git)' "$WHERE"; then
783
+ h1m 'Stopped'; subm 'Stayed at Stage 1 — NOTHING CHANGED (read-only).'; exit 0
784
+ fi
785
+
786
+ # BR_HANDOVER_LINE / GEN_WHERE / ADR_BR_STATUS go into HANDOVER.md and the ADR, so they stay English (artefacts).
787
+ # ONBRANCH/ACCEPT/DISCARD_LINE are only ever printed to the terminal, so they are translated here.
788
+ if [ "$DEC_BR" = here ]; then
789
+ BR="$BASE" # $BR is referenced downstream; on 'here' it IS the current branch
790
+ say 'applying on the current branch: %s (no separate branch; staged, HEAD untouched until you commit)' "${B}$BASE${R}"
791
+ BR_HANDOVER_LINE="Applied on the current branch: $BASE (the change set is STAGED-not-committed — review in 'git status' / your editor, then commit; HEAD untouched until you do)."
792
+ GEN_WHERE="current branch $BASE"
793
+ ADR_BR_STATUS="accepted (applied on current branch: $BASE — staged, not committed)"
794
+ _mt 'You are on your current branch %s with everything STAGED but NOT committed.' "$BASE"; ONBRANCH_LINE="$_M"
795
+ _mt 'accept: %s' "git commit -m 'adopt Crewforth'"; ACCEPT_LINE="$_M"
796
+ _mt 'discard: %s (un-stages everything; nothing was committed)' 'git reset --hard HEAD'; DISCARD_LINE="$_M"
797
+ else
798
+ case "$BASE" in kit-adopt-*) warnm 'HEAD is a prior adopt branch (%s) — the review diff will be vs it, not your main line. Consider %s first.' "$BASE" "'git checkout <main>'" ;; esac
799
+ TS="$(date +%Y%m%d-%H%M%S)"; BR="kit-adopt-$TS"
800
+ # Second-resolution timestamp: two adopts within one second would collide and the second checkout would fail.
801
+ # Bump a counter until the name is free (also covers a re-run after a discarded attempt left the branch behind).
802
+ n=2; while git rev-parse --verify -q "refs/heads/$BR" >/dev/null 2>&1; do BR="kit-adopt-$TS-$n"; n=$((n+1)); done
803
+ git checkout -b "$BR" >/dev/null 2>&1 || { _mt "ERROR: could not open branch '%s'." "$BR"; printf '%s\n' "$_M"; exit 1; }
804
+ say 'handover branch: %s (%s stays clean)' "${B}$BR${R}" "$BASE"
805
+ BR_HANDOVER_LINE="Handover branch: $BR ($BASE untouched; the change set is STAGED-not-committed — review in your editor / 'git status', then commit)."
806
+ GEN_WHERE="branch $BR"
807
+ ADR_BR_STATUS="accepted (handover branch: $BR)"
808
+ _mt 'You are on branch %s with everything STAGED but NOT committed.' "$BR"; ONBRANCH_LINE="$_M"
809
+ _mt 'accept: %s then: %s' "git commit -m 'adopt Crewforth'" "git checkout $BASE && git merge $BR"; ACCEPT_LINE="$_M"
810
+ _mt 'discard: %s' "git reset --hard $BASE && git checkout $BASE && git branch -D $BR"; DISCARD_LINE="$_M"
811
+ fi
812
+
813
+ mkdir -p .claude
814
+ # Every adopt installs the full payload; the only thing that varies is the .NET pattern skill below.
815
+ EXCL_A=""; EXCL_S=""; EXCL_C=""
816
+ # Pre-2.0 migration: that install pruned by profile, so components are MISSING and this refresh restores them.
817
+ # The list is derived from a before/after disk diff rather than from a profile→pruned map, because the map
818
+ # (profiles.conf) no longer exists — and a diff also reports components added by the version bump itself,
819
+ # which is the honest thing to show: it names what actually appeared, not what a table predicts.
820
+ MIGRATE_MISSING=""
821
+ if [ -n "$LEGACY_PROFILE" ]; then
822
+ for f in "$SRC"/agents/*.md; do [ -e "$f" ] && [ ! -e ".claude/agents/$(basename "$f")" ] && MIGRATE_MISSING="$MIGRATE_MISSING agents/$(basename "$f")"; done
823
+ for d in "$SRC"/skills/*/; do [ -d "$d" ] && [ ! -d ".claude/skills/$(basename "$d")" ] && MIGRATE_MISSING="$MIGRATE_MISSING skills/$(basename "$d")"; done
824
+ fi
825
+ # THE 3.0 MIGRATION. The kit no longer ships cqrs-aop-module, and a project that had it keeps it: from now on it is
826
+ # the PROJECT's own pattern skill, which crew-backend-expert applies ahead of backend-architecture. Nothing here
827
+ # deletes it, the copy below cannot touch it (the payload no longer carries the name), and the stale sweep
828
+ # further down is told to leave it out, because that sweep's advice is an `rm -r` line.
829
+ #
830
+ # THE RENAME MIGRATION is kept inside it: devarch-module -> cqrs-aop-module. The kit renamed its own skill, so an
831
+ # install from before the rename carries the old name. Three measured reasons it is not left to the stale sweep:
832
+ # 1. that sweep REPORTS and never deletes, so until the user acts the project would carry two pattern skills
833
+ # competing for every prompt;
834
+ # 2. kit-manifest.txt arrived in v1.8.0 and this skill shipped from v1.0.0, so an install not updated since
835
+ # before 1.8.0 has no manifest and the sweep is BLIND to it;
836
+ # 3. kit_infer_shape reads the legacy stack from this directory's name.
837
+ # Nothing is deleted: the directory is renamed and its content travels with it, so a customised copy survives.
838
+ # If BOTH names are already present nothing is moved — the user decides which one is theirs.
839
+ # Only on a project the kit was installed on: a repo adopted for the first time may own a `devarch-module` skill of
840
+ # its own, and renaming that is not a migration of anything the kit shipped.
841
+ if [ "$KIT_PRESENT" = 1 ] && [ -d .claude/skills/devarch-module ]; then
842
+ if [ ! -e .claude/skills/cqrs-aop-module ]; then
843
+ mv .claude/skills/devarch-module .claude/skills/cqrs-aop-module 2>/dev/null \
844
+ && say '.NET pattern skill renamed: devarch-module -> cqrs-aop-module (content kept)'
845
+ else
846
+ say '⚠️ both devarch-module and cqrs-aop-module are present — nothing moved; remove the old one when ready'
847
+ fi
848
+ fi
849
+ [ "$LEGACY_DOTNET" = 1 ] && [ -d .claude/skills/cqrs-aop-module ] \
850
+ && say "cqrs-aop-module is now a project skill (Crewforth no longer ships it); backend-expert applies it as your project's pattern."
851
+ # THE 3.0 NAME MIGRATION: <x>-csk -> crew-<x>, for the KIT'S OWN components only (agents, commands, the code-review
852
+ # skill), and only on a project the kit was installed on. Same principle as the devarch-module rename above: move,
853
+ # never delete. The names come from the payload, so a project file that merely ends in -csk (my-helper-csk.md) is
854
+ # never considered. If both names exist nothing moves — the user decides which one is theirs. The force-refresh just
855
+ # below then brings every moved file's content up to 3.0.
856
+ LEGACY_MOVED=0; LEGACY_BOTH=""
857
+ if [ "$KIT_PRESENT" = 1 ]; then
858
+ for kf in "$SRC"/agents/crew-*.md "$SRC"/skills/crew-*/; do # skills/ includes the commands since 3.0
859
+ [ -e "$kf" ] || continue
860
+ kf="${kf%/}"; kd="${kf%/*}"; kd="${kd##*/}"; kn="${kf##*/}"; kx=""
861
+ case "$kn" in *.md) kn="${kn%.md}"; kx=".md" ;; esac
862
+ old=".claude/$kd/${kn#crew-}-csk$kx"; new=".claude/$kd/$kn$kx"
863
+ [ -e "$old" ] || continue
864
+ if [ -e "$new" ]; then LEGACY_BOTH="$LEGACY_BOTH $old"
865
+ # 2.x never shipped a crew- name, so that file is the user's: the force-refresh below must not replace it.
866
+ case "$kd" in agents) EXCL_A="$EXCL_A $kn$kx" ;; skills) EXCL_S="$EXCL_S $kn" ;; commands) EXCL_C="$EXCL_C $kn$kx" ;; esac
867
+ else mv "$old" "$new" 2>/dev/null && { LEGACY_MOVED=$((LEGACY_MOVED+1)); say '3.0 rename: %s → %s' "${old#.claude/}" "${new#.claude/}"; }
868
+ fi
869
+ done
870
+ [ -n "$LEGACY_BOTH" ] && warnm '3.0 rename: both the old and the new name exist for:%s — nothing moved; keep one' "$LEGACY_BOTH"
871
+ fi
872
+ # THE 3.0 COMMANDS -> SKILLS MOVE. Claude Code merged custom commands into skills (`.claude/commands/` is "the
873
+ # older format"), and a skill and a command of the same name are the same `/name` — the skill wins. So the kit's
874
+ # own commands move to `.claude/skills/crew-<x>/SKILL.md`: `/crew-review` stays `/crew-review`. A 2.x `<x>-csk.md`
875
+ # goes straight there, with no stop at `commands/crew-<x>.md`. Which skills were commands is read from the payload
876
+ # (`metadata: kind: command`), so a command of the user's own in .claude/commands/ is never considered; and when a
877
+ # `.claude/skills/crew-<x>/` already exists nothing moves — the user is told and decides, and the refresh leaves
878
+ # that directory alone. Moved, never deleted; the force-refresh below then brings each moved file up to 3.0.
879
+ CMD_SKILLS="$(grep -l '^ kind: command' "$SRC"/skills/crew-*/SKILL.md 2>/dev/null)" # one process for all of them
880
+ NCMD=0; CMD_BOTH=""; CMD_DUP=""; CMD_LINK=""; CMD_MINE=""
881
+ # A .claude/commands that is itself a symlink (a directory shared by several projects) is left alone whole: moving
882
+ # files OUT of it would empty it for every other project that points at it.
883
+ CMD_DIR_SHARED=0; [ -L .claude/commands ] && CMD_DIR_SHARED=1
884
+ for kf in $CMD_SKILLS; do
885
+ NCMD=$((NCMD+1))
886
+ kn="${kf%/SKILL.md}"; kn="${kn##*/}"; kb="${kn#crew-}"
887
+ if [ "$KIT_PRESENT" != 1 ]; then
888
+ # FRESH adopt: a commands/crew-<x>.md here is the PROJECT's own (the kit was never installed). The kit's skill
889
+ # of that name would silently win over it, so the kit's copy is not installed and the user is told.
890
+ [ -f ".claude/commands/$kn.md" ] && { EXCL_S="$EXCL_S $kn"; CMD_MINE="$CMD_MINE $kn"; }
891
+ continue
892
+ fi
893
+ [ "$CMD_DIR_SHARED" = 1 ] && continue
894
+ MOVED_NOW=0
895
+ for old in ".claude/commands/$kn.md" ".claude/commands/$kb-csk.md"; do
896
+ [ -f "$old" ] || [ -L "$old" ] || continue
897
+ if [ -L "$old" ]; then CMD_LINK="$CMD_LINK ${old#.claude/}"; continue; fi # a moved relative link would dangle
898
+ if [ "$MOVED_NOW" = 1 ]; then CMD_DUP="$CMD_DUP ${old#.claude/}"; continue; fi # the other kit copy of the same command
899
+ if [ -e ".claude/skills/$kn" ]; then CMD_BOTH="$CMD_BOTH ${old#.claude/}"; EXCL_S="$EXCL_S $kn"
900
+ else mkdir -p ".claude/skills/$kn" && mv "$old" ".claude/skills/$kn/SKILL.md" \
901
+ && { MOVED_NOW=1; say '3.0 commands are skills: %s → %s' "${old#.claude/}" "skills/$kn/SKILL.md"; }
902
+ fi
903
+ done
904
+ done
905
+ [ -n "$CMD_BOTH" ] && warnm '3.0 commands are skills: a skill of that name already exists for:%s — nothing moved; keep one' "$CMD_BOTH"
906
+ [ -n "$CMD_DUP" ] && warnm '3.0 commands are skills: an older copy of an already-moved command is left in place:%s — remove it' "$CMD_DUP"
907
+ [ -n "$CMD_LINK" ] && warnm '3.0 commands are skills: symlinked command file(s) left as they are:%s — the skill of that name now answers /name' "$CMD_LINK"
908
+ [ "$CMD_DIR_SHARED" = 1 ] && [ "$KIT_PRESENT" = 1 ] && warnm '3.0 commands are skills: .claude/commands is a symlink (shared?) — nothing was moved out of it; the skills of those names now answer /name'
909
+ [ -n "$CMD_MINE" ] && warnm 'your own command(s) keep their name — the Crewforth skill of the same name was not installed:%s' "$CMD_MINE"
910
+ [ "$CMD_DIR_SHARED" = 1 ] || rmdir .claude/commands 2>/dev/null || true # only when nothing of the user's is left in it
911
+ # THE 3.0 BOARD NAMES, in THIS clone only: the board ref, its local settings, its caches, and a `csk-board` remote
912
+ # move to the crew names. The remote's 2.x ref is never deleted: while it exists board.sh writes both refs in one
913
+ # atomic push, so a 2.x teammate still sees every claim — and the user is told the team should update too.
914
+ if [ "$KIT_PRESENT" = 1 ] && _BGD="$(git rev-parse --git-common-dir 2>/dev/null)" && [ -n "$_BGD" ]; then
915
+ _BMV=""; _BHEAD="$(git symbolic-ref -q HEAD 2>/dev/null || true)"
916
+ for _bp in "refs/csk/board refs/crew/board" "refs/heads/csk-board refs/heads/crew-board"; do
917
+ _bo="${_bp% *}"; _bn="${_bp#* }"
918
+ _bs="$(git rev-parse -q --verify "$_bo" 2>/dev/null)" || continue
919
+ [ "$_BHEAD" = "$_bo" ] && continue # never delete the checked-out branch
920
+ git rev-parse -q --verify "$_bn" >/dev/null 2>&1 && continue # both: board.sh folds the old one in
921
+ git update-ref "$_bn" "$_bs" && git update-ref -d "$_bo" "$_bs" && _BMV="$_BMV $_bn"
922
+ done
923
+ # A board remote made by `init --remote <url>` is ADDED under the crew name, never renamed: a global
924
+ # csk.boardRemote=csk-board (outside this clone, not moved here) must keep finding its remote. And a remote that
925
+ # is already called crew-board is someone else's — then the setting keeps pointing at csk-board.
926
+ _BRM=0
927
+ if _bu="$(git remote get-url csk-board 2>/dev/null)"; then
928
+ if ! git remote get-url crew-board >/dev/null 2>&1; then git remote add crew-board "$_bu" 2>/dev/null && { _BRM=1; _BMV="$_BMV remote:crew-board"; }
929
+ elif [ "$(git remote get-url crew-board 2>/dev/null)" = "$_bu" ]; then _BRM=1; fi
930
+ fi
931
+ for _bk in board boardRef boardRemote; do
932
+ _bv="$(git config --local --get "csk.$_bk" 2>/dev/null)" || continue
933
+ case "$_bv" in refs/csk/board) _bv=refs/crew/board ;; refs/heads/csk-board) _bv=refs/heads/crew-board ;; csk-board) [ "$_BRM" = 1 ] && _bv=crew-board ;; esac
934
+ git config --local --get "crew.$_bk" >/dev/null 2>&1 || git config --local "crew.$_bk" "$_bv"
935
+ git config --local --unset "csk.$_bk" 2>/dev/null; _BMV="$_BMV crew.$_bk"
936
+ done
937
+ for _bf in "$_BGD"/csk-board-*; do
938
+ [ -e "$_bf" ] || continue
939
+ _bt="$_BGD/crew-board-${_bf##*/csk-board-}"
940
+ if [ -e "$_bt" ]; then rm -f "$_bf"; else mv "$_bf" "$_bt"; fi
941
+ done
942
+ if [ -n "$_BMV" ]; then
943
+ say '3.0 board: moved to the crew names in this clone:%s' "$_BMV"
944
+ warnm "3.0 board: the remote's 2.x board ref is not deleted — while it exists, 3.x writes both, so 2.x teammates still see your claims; ask the team to update, then delete the old ref"
945
+ fi
946
+ fi
947
+ # THE 3.0 AUTO-MODE RULE NAMES: a policy applied by 2.x named its rules "CSK …" in the USER's settings. They become
948
+ # "Crewforth …" here, after a backup, and only those three rule names change — every other byte is left as it was.
949
+ _AMS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
950
+ if [ "$KIT_PRESENT" = 1 ] && [ -f "$_AMS" ] && grep -qE '"CSK (Uncommitted Work Destruction|Gate Tampering|Internal Docs Publication):' "$_AMS" 2>/dev/null; then
951
+ _AMB="$_AMS.crew-bak-$(date +%Y%m%d-%H%M%S)"; _SB0=; sed -b q </dev/null >/dev/null 2>&1 && _SB0=-b
952
+ if cp "$_AMS" "$_AMB" && sed $_SB0 -E 's/"CSK (Uncommitted Work Destruction|Gate Tampering|Internal Docs Publication):/"Crewforth \1:/g' "$_AMB" > "$_AMS.crew-new" \
953
+ && [ -s "$_AMS.crew-new" ] && cat "$_AMS.crew-new" > "$_AMS"; then
954
+ say '3.0 auto-mode: the Crewforth rules in %s are renamed CSK … → Crewforth … (backup: %s)' "$_AMS" "$_AMB"
955
+ fi
956
+ rm -f "$_AMS.crew-new"
957
+ fi
958
+ # #1 keepmine: your overlapping agents own those roles, so the kit's matching crew- agents are NOT installed.
959
+ [ "$COLLIDE_MODE" = keepmine ] && for b in $COLLIDE; do EXCL_A="$EXCL_A crew-$b.md"; done
960
+ # kit-owned trees: FORCE-refresh on a re-adopt (KIT_PRESENT) so kit updates land; never-overwrite on a fresh adopt
961
+ copy_noclobber "$SRC/agents" .claude/agents "$KIT_PRESENT" "$EXCL_A"; A_ADD=$ret_add; A_SKIP=$ret_skip
962
+ copy_noclobber "$SRC/skills" .claude/skills "$KIT_PRESENT" "$EXCL_S"; S_ADD=$ret_add; S_SKIP=$ret_skip
963
+ # The commands arrived with skills/ (one SKILL.md each), so the summary counts them apart, as the user knows them.
964
+ # Counted from what is on disk: a command skill counts as delivered when the installed SKILL.md is the payload's.
965
+ C_ADD=0; for kf in $CMD_SKILLS; do kn="${kf%/SKILL.md}"; kn="${kn##*/}"; cmp -s "$kf" ".claude/skills/$kn/SKILL.md" && C_ADD=$((C_ADD+1)); done
966
+ C_SKIP=$((NCMD - C_ADD)); S_ADD=$((S_ADD - C_ADD)); S_SKIP=$((S_SKIP - C_SKIP))
967
+ [ "$S_ADD" -ge 0 ] || S_ADD=0; [ "$S_SKIP" -ge 0 ] || S_SKIP=0
968
+ # Read BEFORE the hooks tree is refreshed: whether §4.2's vendor line is armed right now (see the re-arm below).
969
+ # `\r?`: a blocklist that reached this checkout CRLF (autocrlf=true, a Windows editor) still counts as armed —
970
+ # a plain -x match read it as disarmed and the refresh switched §4.2 off without a word (measured in review).
971
+ VENDOR_ARMED=0; grep -qxE $'DevArchitecture\r?' .claude/hooks/trace-blocklist.txt 2>/dev/null && VENDOR_ARMED=1
972
+ copy_noclobber "$SRC/hooks" .claude/hooks "$KIT_PRESENT"; H_ADD=$ret_add; H_SKIP=$ret_skip
973
+ copy_noclobber "$SRC/eval" .claude/eval "$KIT_PRESENT"; E_ADD=$ret_add; E_SKIP=$ret_skip
974
+ # The Studio panel. This is the line that answers "I updated and `/crew-studio` says the panel
975
+ # is missing": with KIT_PRESENT=1 it is a force-refresh, so a project that already has the kit
976
+ # gets the panel on its next update.
977
+ # NOT passed the $4 exclusion argument even though `test` would match it correctly — a non-empty
978
+ # exclraw disables the force=1 fast path above and drops into the per-file loop, i.e. 25 process
979
+ # spawns on Git Bash instead of one `cp -R` plus one `rm`. That fast path exists because a
980
+ # per-file refresh was measured at 6m43s. On a FRESH adopt (KIT_PRESENT=0) copy_noclobber never
981
+ # overwrites, so a project that somehow owns .claude/studio keeps its own files.
982
+ copy_noclobber "$SRC/studio" .claude/studio "$KIT_PRESENT"; T_ADD=$ret_add; T_SKIP=$ret_skip
983
+ # test/ reads the REPO (root package.json, the payload beside it); from .claude/studio those
984
+ # resolve to a tree that has neither, so shipping it would be a gate red in every project. The
985
+ # installed diagnostic is `node .claude/studio/server/index.js --selftest`. The suite itself
986
+ # lives in packaging/studio-test/, outside the payload, so nothing has to be deleted here and
987
+ # the count is simply what landed.
988
+ chmod +x .claude/studio/server/hooks/*.sh 2>/dev/null || true
989
+ # AGENT_TEMPLATE.md — a kit-owned flat file, so it is written on every run rather than never-overwritten.
990
+ # `/crew-skill` opens with `Read .claude/AGENT_TEMPLATE.md`, and until now only start.sh copied it
991
+ # (start.sh:486). That left the command pointing at a file that does not exist on an adopted install, and
992
+ # `update` is an alias of this script (bin/cli.js:48), so a copy placed by start.sh was never refreshed
993
+ # either — it went stale from the day it landed and nothing ever noticed, because §3b iterates skills and
994
+ # agents and this is neither. Overwriting is right for the same reason DISCIPLINE.md is overwritten: the
995
+ # file states the kit's own contract, a project does not author it, and a stale contract is worse than none.
996
+ cp "$SRC/AGENT_TEMPLATE.md" .claude/ 2>/dev/null && say 'AGENT_TEMPLATE.md written (owned by Crewforth; refreshed on every update)'
997
+ # Report the migration by what LANDED, not by what was missing: a component the payload lists can still be kept
998
+ # out (EXCL_S), and must not be announced as restored when it was.
999
+ if [ -n "$MIGRATE_MISSING" ]; then
1000
+ MIGRATED=""
1001
+ for c in $MIGRATE_MISSING; do [ -e ".claude/$c" ] && MIGRATED="$MIGRATED $c"; done
1002
+ if [ -n "$MIGRATED" ]; then
1003
+ warnm 'pre-2.0 install (profile=%s): profile pruning was removed — completing the install' "$LEGACY_PROFILE"
1004
+ for c in $MIGRATED; do echo " ${GR}+${R} $c"; done
1005
+ else
1006
+ say 'pre-2.0 install (profile=%s): nothing was missing — the full set was already present' "$LEGACY_PROFILE"
1007
+ fi
1008
+ fi
1009
+ # #1 takeover: the kit's crew- owns the role, and the OLD agent's domain is IMPORTED into an active project skill
1010
+ # (skills/<base>-local) that the kit agent applies — so nothing is lost from the working setup. The agent is then
1011
+ # removed from the routing pool (Claude Code discovers .claude/agents/*.md, not subdirs) so routing is no longer
1012
+ # ambiguous, and the raw original is kept under .claude/superseded/agents/ as a backup. The skill is a DRAFT to refine.
1013
+ N_TAKEN=0
1014
+ if [ "$COLLIDE_MODE" = takeover ] && [ -n "$COLLIDE" ]; then
1015
+ mkdir -p .claude/superseded/agents
1016
+ for b in $COLLIDE; do
1017
+ af=".claude/agents/$b.md"; [ -f "$af" ] || continue
1018
+ if [ -e ".claude/skills/$b-local/SKILL.md" ]; then # idempotent: a prior takeover already imported it
1019
+ say "overlap: %s -> skill '%s' already present (kept); original re-backed up" "$b" "$b-local"
1020
+ else
1021
+ mkdir -p ".claude/skills/$b-local"
1022
+ kit_agent_to_skill "$af" "$b" > ".claude/skills/$b-local/SKILL.md"
1023
+ say "overlap: %s -> imported to skill '%s' (draft); Crewforth's %s owns routing" "$b" "$b-local" "crew-$b"
1024
+ fi
1025
+ cp "$af" ".claude/superseded/agents/$b.md"; rm -f "$af"
1026
+ N_TAKEN=$((N_TAKEN+1))
1027
+ done
1028
+ fi
1029
+ # Both reference sweeps below rewrite user files with sed. MSYS sed (Git Bash) drops the CR of every CRLF line it
1030
+ # reads, so a CRLF CLAUDE.md came back LF-only in full — not just the rewritten line (measured on Windows: 36 CRs
1031
+ # → 0; the 2.13 re-adopt kept all 36). GNU sed's -b reads the bytes as they are; BSD sed has no -b and keeps CRs
1032
+ # anyway, so the flag is used only where it exists.
1033
+ _SB=; sed -b q </dev/null >/dev/null 2>&1 && _SB=-b
1034
+ # #1b takeover reference sweep — the rename above orphaned every project reference to the taken-over agents
1035
+ # ($COLLIDE): "→ backend-expert" in CLAUDE.md, the "detail: docs/AGENTS.md" orchestration doc, etc. This is the ONE
1036
+ # moment the kit knows the exact old→new map, so it COMPLETES the migration instead of leaving the user to chase
1037
+ # dangling names. It rewrites each bare old name to its crew- id across CLAUDE.md's reference chain (its @imports +
1038
+ # docs/…md paths), boundary-safe: `backend-expert` → `crew-backend-expert`, but `crew-backend-expert`/`-local` and
1039
+ # `backend-expertise` are left intact (no double-suffix). Unreferenced design/audit docs and code comments are NOT
1040
+ # touched (precise, no false positives). The edit lands on the adopt review branch — visible in the diff, revertible.
1041
+ if [ "$N_TAKEN" -gt 0 ] && [ -f CLAUDE.md ]; then
1042
+ SWEEP="CLAUDE.md"
1043
+ for r in $(grep -oE '@?[A-Za-z0-9_./-]+\.md' CLAUDE.md 2>/dev/null | sed 's/^@//' | sort -u); do
1044
+ [ -f "$r" ] && [ "$r" != "CLAUDE.md" ] && SWEEP="$SWEEP $r"
1045
+ done
1046
+ SWEPT=0
1047
+ for b in $COLLIDE; do
1048
+ for f in $SWEEP; do
1049
+ i=0
1050
+ while grep -qE "(^|[^A-Za-z-])$b([^A-Za-z-]|$)" "$f" 2>/dev/null && [ "$i" -lt 5 ]; do
1051
+ sed $_SB -E "s/(^|[^A-Za-z-])$b([^A-Za-z-]|\$)/\1crew-$b\2/g" "$f" > "$f.kit-sweep" && mv "$f.kit-sweep" "$f"
1052
+ i=$((i+1)); SWEPT=$((SWEPT+1))
1053
+ done
1054
+ [ "$i" -gt 0 ] && say 'ref-sweep: %s → %s in %s' "$b" "crew-$b" "$f"
1055
+ done
1056
+ done
1057
+ [ "$SWEPT" -gt 0 ] && h1m 'Reference sweep: rewrote taken-over agent names to their crew- id across CLAUDE.md + referenced docs' \
1058
+ || say "ref-sweep: no stale references in CLAUDE.md's chain"
1059
+ fi
1060
+ # The 3.0 names in CLAUDE.md's reference chain (itself, its @imports, the docs/*.md it names): each OLD kit name —
1061
+ # backend-expert-csk, /review-csk, @agent-planner-csk — becomes its crew- form. Only kit names, from the payload;
1062
+ # the boundary is the takeover sweep's (not glued to a longer name, `@agent-` allowed), so the rest of the file is
1063
+ # left as it was. Runs only when a file mentions -csk at all.
1064
+ if [ "$KIT_PRESENT" = 1 ] && [ -f CLAUDE.md ] && grep -q -e '-csk' CLAUDE.md $(grep -oE '@?[A-Za-z0-9_./-]+\.md' CLAUDE.md 2>/dev/null | sed 's/^@//' | sort -u) 2>/dev/null; then
1065
+ LSWEEP="CLAUDE.md"; PROJ_REAL="$(pwd -P)"
1066
+ for r in $(grep -oE '@?[A-Za-z0-9_./-]+\.md' CLAUDE.md 2>/dev/null | sed 's/^@//' | sort -u); do
1067
+ r="${r#./}"
1068
+ case "$r" in .claude/*) continue ;; esac # kit-owned (DISCIPLINE.md is rewritten on every update anyway)
1069
+ # Inside the project only: an update does not edit files outside it — not by `..`, not by an absolute path, and
1070
+ # not through a symlinked directory (docs -> ../shared), which is why the directory is resolved, not the spelling.
1071
+ case "/$r/" in */../*) continue ;; esac
1072
+ case "$r" in /*) continue ;; esac
1073
+ [ -f "$r" ] && [ "$r" != "CLAUDE.md" ] || continue
1074
+ case "$r" in */*) rd="$(cd "${r%/*}" 2>/dev/null && pwd -P)" || continue
1075
+ case "$rd/" in "$PROJ_REAL"/*) ;; *) continue ;; esac ;; esac
1076
+ LSWEEP="$LSWEEP $r"
1077
+ done
1078
+ set --
1079
+ for kf in "$SRC"/agents/crew-*.md "$SRC"/skills/crew-*/; do # skills/ includes the commands since 3.0
1080
+ [ -e "$kf" ] || continue
1081
+ kn="${kf%/}"; kn="${kn##*/}"; kn="${kn%.md}"; kb="${kn#crew-}"
1082
+ set -- "$@" -e "s/(^|[^A-Za-z0-9_-]|@agent-)$kb-csk([^A-Za-z0-9_-]|\$)/\1$kn\2/g"
1083
+ done
1084
+ for f in $LSWEEP; do
1085
+ grep -q -e '-csk' "$f" 2>/dev/null || continue
1086
+ # A symlink is followed only to a target inside the project (CLAUDE.md → AGENTS.md is common); writing with
1087
+ # `cat >` rather than `mv` goes THROUGH the link and keeps the file's mode, instead of replacing it.
1088
+ if [ -L "$f" ]; then t="$(readlink "$f")"; case "$t" in /*|*..*) continue ;; esac; fi
1089
+ i=0; cp "$f" "$f.kit-before"
1090
+ while [ "$i" -lt 5 ]; do
1091
+ sed $_SB -E "$@" "$f" > "$f.kit-sweep" || { rm -f "$f.kit-sweep"; break; } # a failed sed changes nothing
1092
+ cmp -s "$f" "$f.kit-sweep" && { rm -f "$f.kit-sweep"; break; }
1093
+ cat "$f.kit-sweep" > "$f"; rm -f "$f.kit-sweep"; i=$((i+1))
1094
+ done
1095
+ cmp -s "$f" "$f.kit-before" || say '3.0 ref-sweep: old 2.x names → crew- names in %s' "$f"
1096
+ rm -f "$f.kit-before"
1097
+ done
1098
+ set --
1099
+ fi
1100
+ # §4.2: an armed vendor line STAYS armed, and nothing else arms it. The blocklist ships `# DevArchitecture`
1101
+ # commented; before 3.0 only `start.sh --dotnet` uncommented it, and the force-refresh above resets it to a
1102
+ # comment — so a 2.13 --dotnet project would silently lose the protection on update. The rule is "keep what was
1103
+ # there", NOT "arm it whenever the pattern skill exists": the updater never armed it before 3.0, so a DevArchitecture
1104
+ # project installed through adopt.sh still carries the name in its own namespaces, and arming it there made every
1105
+ # commit that touches that code fail (measured: rc=1, "forbidden expression … 'DevArchitecture'"). The `#test:`
1106
+ # line travels with it: smoke-test fails any active pattern that has no case.
1107
+ if [ "$VENDOR_ARMED" = 1 ] && [ -f .claude/hooks/trace-blocklist.txt ] \
1108
+ && grep -qx '# DevArchitecture' .claude/hooks/trace-blocklist.txt; then
1109
+ awk '/^# DevArchitecture$/ { print "DevArchitecture"; print "#test: ported the handler from DevArchitecture"; next } { print }' \
1110
+ .claude/hooks/trace-blocklist.txt > .claude/hooks/trace-blocklist.txt.kit-tmp \
1111
+ && mv .claude/hooks/trace-blocklist.txt.kit-tmp .claude/hooks/trace-blocklist.txt \
1112
+ && say '§4.2: DevArchitecture stays armed in the trace blocklist (it was armed before this update)'
1113
+ fi
1114
+ chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg 2>/dev/null || true
1115
+ [ -f "$HERE/VERSION" ] && cp "$HERE/VERSION" .claude/VERSION 2>/dev/null || true # first-class marker so a future adopt detects a REFRESH
1116
+ # Stale kit files: names the kit USED to ship and no longer does. `copy_noclobber` only ever adds, so a component
1117
+ # removed or renamed upstream lives on in the project forever — and that is not cosmetic, because for all three
1118
+ # kinds the NAME IS THE INVOCATION. A command's filename is what the / picker lists: after 1.11.0 renamed the
1119
+ # commands, an un-pruned `review.md` sits beside `crew-review.md` and both show up. A SKILL's directory name is
1120
+ # what the router scores — `route-hint.sh` ranks `.claude/skills/*/SKILL.md` by the trigger phrases inside, so a
1121
+ # renamed skill left on disk competes with its own replacement for every prompt. Measured on this machine: a
1122
+ # 2.6.0 install upgraded in place kept `skills/vps-deploy` beside the new `skills/deploy`, both with live
1123
+ # triggers, and the installer said nothing — it had counted the leftover as one of the PROJECT's own skills.
1124
+ # That silence is the bug; skills were simply outside the scan below.
1125
+ #
1126
+ # They are REPORTED, never deleted: this installer deliberately preserves a pre-existing project file that
1127
+ # happens to sit under a kit name, so a name in the old manifest is not proof the file is ours. Deleting on that
1128
+ # assumption would destroy the user's own work; naming it costs them one command.
1129
+ # .claude/studio is deliberately outside this sweep, and outside kit-manifest.txt with it. The sweep works
1130
+ # on components whose NAME is the invocation — a leftover command lists in the / picker, a leftover skill
1131
+ # competes for prompts — and the panel is neither: nothing routes to a file under studio/, so a stale copy
1132
+ # is dead weight rather than a competing answer. The cost of accepting it is stated rather than hidden: if
1133
+ # the panel is ever renamed or dropped, an old copy stays in every project until someone deletes it.
1134
+ if [ -f .claude/kit-manifest.txt ]; then
1135
+ STALE=""
1136
+ while IFS= read -r entry; do
1137
+ case "$entry" in commands/*|agents/*|skills/*) ;; *) continue ;; esac
1138
+ [ -e "$SRC/${entry#*/}" ] && continue # still shipped under the same folder? keep
1139
+ [ -e "$SRC/$entry" ] && continue
1140
+ # The 3.0 migration keeps the former .NET pattern skill as the project's own; this sweep's advice is `rm -r`.
1141
+ [ "$entry" = skills/cqrs-aop-module ] && continue
1142
+ [ -e ".claude/$entry" ] && STALE="$STALE $entry"
1143
+ done < .claude/kit-manifest.txt
1144
+ if [ -n "$STALE" ]; then
1145
+ echo
1146
+ say '⚠️ installed by an older Crewforth version and no longer shipped:%s' "$STALE"
1147
+ _mt 'The name is the invocation: a leftover COMMAND still lists in the / picker (/review twice), and a'; printf ' %s\n' "$_M"
1148
+ _mt 'leftover SKILL still matches prompts, so it competes with whatever replaced it.'; printf ' %s\n' "$_M"
1149
+ _mt 'Nothing is deleted for you — one of these may be a file you customised. To drop them all:'; printf ' %s\n' "$_M"
1150
+ printf ' rm -r'; for e in $STALE; do printf ' .claude/%s' "$e"; done; echo
1151
+ fi
1152
+ fi
1153
+
1154
+ # Install manifest — the names the KIT ships (see start.sh for the full rationale). Generated from the PAYLOAD,
1155
+ # never from disk: this installer deliberately PRESERVES a pre-existing project file under a kit name, and a
1156
+ # disk-read manifest would brand that project file "kit-owned". Consumers: the doctor readiness check and the
1157
+ # skill trust gate.
1158
+ { for d in "$SRC"/skills/*/; do [ -d "$d" ] && echo "skills/$(basename "$d")"; done
1159
+ for f in "$SRC"/agents/*.md; do [ -e "$f" ] && echo "agents/$(basename "$f")"; done
1160
+ } > .claude/kit-manifest.txt 2>/dev/null || true
1161
+
1162
+ # stack= is always generic since 3.0; the key is kept for older updaters. Rewritten WITHOUT the pre-2.0
1163
+ # 'profile=' key: dropping it is what retires the migration notice, so a second refresh stays quiet — and
1164
+ # writing generic over a former 'dotnet' retires the 3.0 notice the same way.
1165
+ { echo "# Written by the Crewforth installer. stack= is always generic since 3.0; the key is kept for older updaters."
1166
+ echo "stack=generic"
1167
+ echo "installer=${KIT_INSTALLER:-adopt.sh}"
1168
+ echo "version=$( [ -f "$HERE/VERSION" ] && head -1 "$HERE/VERSION" || echo unknown )"
1169
+ # Only a language somebody chose is recorded (--lang, CREW_LANG, the menu, or the one already recorded). A
1170
+ # --yes run with nothing named guesses from the locale, and a guess written down would pin an install that was
1171
+ # set up in Turkish to English from its first unattended update on.
1172
+ [ "$_LANG_CHOSEN" = 1 ] && echo "lang=$CREW_LANG"
1173
+ } > .claude/kit.conf
1174
+ # What changed, for /crew-update to report: the package's own CHANGELOG sections newer than the version this
1175
+ # project had, and no newer than the one being installed — read from the package on disk, never from the network.
1176
+ # A fresh adopt has no "before", so it gets no file (and a stale one from an earlier update is removed).
1177
+ rm -f .claude/.state/whats-new.md 2>/dev/null
1178
+ _kv="$(printf '%s' "$KIT_VER" | tr -d '\r')"
1179
+ case "$_kv" in *[!0-9.]*|'') _kv="" ;; [0-9]*.[0-9]*.[0-9]*) ;; *) _kv="" ;; esac # an odd old version compares as 0.0.0 — every section would look new
1180
+ if [ "$KIT_PRESENT" = 1 ] && [ -n "$_kv" ] && [ -f "$HERE/CHANGELOG.md" ] && [ -f "$HERE/VERSION" ]; then
1181
+ mkdir -p .claude/.state 2>/dev/null
1182
+ awk -v from="$_kv" -v to="$(head -1 "$HERE/VERSION" | tr -d '\r')" '
1183
+ function cmp(a,b, x,y,i){ split(a,x,"."); split(b,y,"."); for(i=1;i<=3;i++){ if(x[i]+0>y[i]+0) return 1; if(x[i]+0<y[i]+0) return -1 } return 0 }
1184
+ /^## \[/ { v=$0; sub(/^## \[/,"",v); w=v; sub(/\].*/,"",v)
1185
+ if (v=="Unreleased") { v=w; sub(/^[^—]*— */,"",v); sub(/[^0-9.].*/,"",v) }
1186
+ keep = (v ~ /^[0-9]+\.[0-9]+\.[0-9]+$/ && cmp(v,from)>0 && cmp(v,to)<=0) }
1187
+ keep' "$HERE/CHANGELOG.md" | tr -d '\r' > .claude/.state/whats-new.md 2>/dev/null
1188
+ [ -s .claude/.state/whats-new.md ] || rm -f .claude/.state/whats-new.md
1189
+ fi
1190
+
1191
+ h1m 'Coexist summary'
1192
+ # _cnt: "<added>" plus " · N skipped" when something was skipped; result in _v (no subshell, see _mt)
1193
+ _cnt(){ _v="$1"; if [ "${2:-0}" != 0 ]; then _mt '%s · %s skipped' "$_v" "$2"; _v="$_M"; fi; }
1194
+ _mt '+%s added' "$A_ADD"; _cnt "$_M" "$A_SKIP"; rowv 'Crewforth agents (crew-)' "$_v"
1195
+ _cnt "+$S_ADD" "$S_SKIP"; rowv 'skills' "$_v"
1196
+ _cnt "+$C_ADD" "$C_SKIP"; rowv 'commands' "$_v"
1197
+ _cnt "+$H_ADD" "$H_SKIP"; rowv 'hooks' "$_v"
1198
+ rowv 'eval' "+$E_ADD"
1199
+ # The component table is where someone scans for what they got, and it was
1200
+ # advertising a command that refuses on a machine without node — a row promising
1201
+ # a capability it had not checked. Qualified from the same preflight query the
1202
+ # closing line uses, so the version rule keeps one home.
1203
+ PANEL_CMD="/crew-studio"
1204
+ bash "$SRC/eval/preflight.sh" --has node 2>/dev/null || { _mt '%s (needs Node 18+)' /crew-studio; PANEL_CMD="$_M"; }
1205
+ _cnt "+$T_ADD" "${T_SKIP:-0}"; rowv 'studio (panel)' "$_v ${D}— $PANEL_CMD${R}"
1206
+ _v="$N_PAGENTS"
1207
+ [ "${N_TAKEN:-0}" != 0 ] && { _mt '%s (%s imported to skills/<name>-local drafts; originals backed up in superseded/)' "$_v" "$N_TAKEN"; _v="$_M"; }
1208
+ _mt '%s — the rest UNTOUCHED' "$_v"; rowv 'project agents' "$_M"
1209
+ case "$COLLIDE_MODE" in
1210
+ keepmine) [ "$N_COLLIDE" != 0 ] && rowm 'overlap' "keepmine — your agents own: %s (Crewforth's crew- for these NOT installed)" "$COLLIDE" ;;
1211
+ coexist) [ "$N_COLLIDE" != 0 ] && warnm "overlap: %s — BOTH kept; routing between your agent and Crewforth's crew- stays ambiguous" "$COLLIDE" ;;
1212
+ esac
1213
+ [ -n "$SKIP_LIST" ] && { warnm "conflicting files (the project's was PRESERVED, Crewforth's skipped):"; for s in $SKIP_LIST; do printf ' %s- %s%s\n' "$D" "$s" "$R"; done; }
1214
+
1215
+ # ============ [STAGE 3] DISCIPLINE ACTIVE + SETTINGS MERGE ============
1216
+ h1m "Stage 3 — activate the Crewforth discipline (without touching the project CLAUDE.md) + settings merge"
1217
+
1218
+ # 3a) DISCIPLINE.md: install the discipline half of the payload CLAUDE.md as a separate, FLAT file —
1219
+ # everything above the sentinel line. Contains NO @import (leaf) -> no 4-hop trap.
1220
+ if [ -f "$SRC/CLAUDE.md" ]; then
1221
+ kit_require_sentinel "$SRC/CLAUDE.md"
1222
+ kit_discipline_of "$SRC/CLAUDE.md" > .claude/DISCIPLINE.md
1223
+ say 'DISCIPLINE.md written (Crewforth discipline only; the project template stays out of it)'
1224
+ fi
1225
+
1226
+ # 3b) single-line @import into the project CLAUDE.md (if present DON'T touch content, only prepend; if absent create).
1227
+ if [ -f CLAUDE.md ]; then
1228
+ if kit_has_import CLAUDE.md; then say 'CLAUDE.md: @import already present (idempotent)'
1229
+ elif kit_claude_md_is_legacy CLAUDE.md; then
1230
+ # Pre-1.1: the whole discipline sits inline. Blindly prepending the @import would load it TWICE, and leaving
1231
+ # it alone means discipline updates never reach this project. Offer the exact swap, with a backup.
1232
+ warnm 'CLAUDE.md carries the discipline INLINE (pre-1.1 layout) — discipline updates cannot reach it.'
1233
+ BND="$(kit_legacy_boundary CLAUDE.md | head -1)"
1234
+ if [ -n "$BND" ] && [ "$BND" -gt 1 ] 2>/dev/null \
1235
+ && head -n "$((BND-1))" CLAUDE.md | grep -q '^## Four working principles' \
1236
+ && head -n "$((BND-1))" CLAUDE.md | grep -q '^### 4\.5 '; then
1237
+ _mt 'the inline block is lines 1-%s; your project section starts at line %s' "$((BND-1))" "$BND"
1238
+ printf ' %s%s%s\n' "$D" "$_M" "$R"
1239
+ if ask_yes ' Replace that inline block with the single @import line? (a backup is written; this branch is reviewable)'; then
1240
+ BK=".claude/CLAUDE.md.pre-kit-$TS"
1241
+ cp CLAUDE.md "$BK"
1242
+ { printf '<!-- Crewforth discipline · on conflict the project rules BELOW win -->\n%s\n\n' "$IMPORT_LINE"
1243
+ tail -n +"$BND" CLAUDE.md; } > CLAUDE.md.kit-tmp && mv CLAUDE.md.kit-tmp CLAUDE.md
1244
+ say 'CLAUDE.md migrated -> @import + your project section (backup: %s)' "$BK"
1245
+ else
1246
+ _mt 'Skipped. Discipline updates will NOT reach this project until you migrate.'
1247
+ printf ' %s%s%s\n' "$D" "$_M" "$R"
1248
+ fi
1249
+ else
1250
+ _mt 'Project heading not found — migrate by hand: delete everything above it, leave only: %s' "$IMPORT_LINE"
1251
+ printf ' %s%s%s\n' "$D" "$_M" "$R"
1252
+ fi
1253
+ else
1254
+ { printf '<!-- Crewforth discipline · on conflict the project rules BELOW win -->\n%s\n\n' "$IMPORT_LINE"; cat CLAUDE.md; } > CLAUDE.md.kit-tmp && mv CLAUDE.md.kit-tmp CLAUDE.md
1255
+ say 'CLAUDE.md: single-line @import prepended (project content untouched)'
1256
+ fi
1257
+ else
1258
+ printf '%s\n\n# CLAUDE.md — <PROJECT NAME>\n\n## Project\n<One sentence: what it does, for whom.>\n' "$IMPORT_LINE" > CLAUDE.md
1259
+ say 'CLAUDE.md was missing -> @import + project template created'
1260
+ fi
1261
+
1262
+ # 3c) settings.json HOOK-AWARE merge: the kit OWNS its hooks (any command referencing .claude/hooks/), so on update
1263
+ # kit hook entries are REFRESHED (new events + current timeouts land; stale kit entries drop) while the project's OWN
1264
+ # custom hooks and permissions are PRESERVED. Non-hook keys deep-merge (project scalar wins, arrays concat+dedup).
1265
+ # Blind concat would leave a stale duplicate (e.g. an old timeout-10 context-usage hook that then times out).
1266
+ # RETIRED permission entries are the same problem for rules: concat+dedup never REMOVES, so a rule the kit stopped
1267
+ # shipping stays in every project that installed it. The four §4.4 `ask` rules are retired because a matching
1268
+ # ask rule prompts even when a hook returns "allow" (Claude Code permissions doc), which made the hook's
1269
+ # CLAUDE_GIT_OK pre-authorisation dead: a headless session was refused `git add` and never committed. The hook
1270
+ # now asks for all four itself. These exact strings are the kit's own; a project that wants them can re-add them.
1271
+ # ONE path on every OS: the merge is kit/eval/lib/settings-json.awk (POSIX awk; its header holds the
1272
+ # reader contract and the merge semantics). jq and python3 are absent on a stock Windows Git-Bash, where python3
1273
+ # is often the Microsoft Store stub (exit 49); the old per-tool arms REPLACED the file there and lost the project's
1274
+ # own rules. Input that is not a JSON object is refused and left untouched; the output is re-read before use.
1275
+ KSET="$SRC/settings.json"; PSET=".claude/settings.json"
1276
+ SET_AWK="$SRC/eval/lib/settings-json.awk"
1277
+ # Which retired rules the project carries NOW, read before the merge so the removal can be announced by name.
1278
+ # A string match cannot tell the kit's copy from one the project wrote itself, so the removal is never silent
1279
+ # and the merge line below does not claim "permissions PRESERVED" when some were not.
1280
+ SET_NOTE=""; RET_HIT=""; [ -f "$PSET" ] && for _r in 'git add' 'git commit' 'git push' 'git checkout -b'; do
1281
+ grep -qF "\"Bash($_r:*)\"" "$PSET" && RET_HIT="$RET_HIT${RET_HIT:+|}$_r"; done
1282
+ if [ -n "$RET_HIT" ]; then _mt 'custom hooks and every other permission PRESERVED'; else _mt 'custom hooks/permissions PRESERVED'; fi
1283
+ KEPT="$_M" # terminal-only (HANDOVER.md words its own line), so translated
1284
+ SET_FRESH=0
1285
+ if [ ! -f "$PSET" ]; then
1286
+ SET_FRESH=1
1287
+ [ -f "$KSET" ] && { cp "$KSET" "$PSET"; say "settings.json: was missing in the project -> Crewforth's was installed"; }
1288
+ else
1289
+ awk -v op=merge -v retired='Bash(git add:*)|Bash(git commit:*)|Bash(git push:*)|Bash(git checkout -b:*)' -f "$SET_AWK" "$KSET" "$PSET" > "$PSET.tmp" 2>/dev/null
1290
+ case "$?" in
1291
+ 0) if [ -s "$PSET.tmp" ] && awk -v op=validate -f "$SET_AWK" "$PSET.tmp" 2>/dev/null; then
1292
+ # Written THROUGH the existing file, not swapped in with mv: a symlinked settings.json (a team's shared
1293
+ # file) keeps its link and gets the merge, and a 0600 mode survives.
1294
+ cat "$PSET.tmp" > "$PSET"; say 'settings.json: hook-aware MERGE (Crewforth hooks refreshed - %s)' "$KEPT"
1295
+ else SET_NOTE="merge failed -> project setting PRESERVED (not overwritten)"; fi ;;
1296
+ 10) SET_NOTE="existing file is INVALID JSON -> merge ABORT (no silent overwrite). Fix it by hand first." ;;
1297
+ *) SET_NOTE="merge failed -> project setting PRESERVED (not overwritten)" ;;
1298
+ esac
1299
+ # SET_NOTE stays English: HANDOVER.md records it verbatim. The terminal gets it translated — each value above
1300
+ # is its own table key.
1301
+ rm -f "$PSET.tmp"; [ -n "$SET_NOTE" ] && { _mt "$SET_NOTE"; warnm 'settings.json: %s' "$_M"; }
1302
+ fi
1303
+ # Announce only what is actually gone: a refused or failed merge leaves the file untouched.
1304
+ RET_GONE=""; _IFS="$IFS"; IFS='|'; for _r in $RET_HIT; do
1305
+ grep -qF "\"Bash($_r:*)\"" "$PSET" 2>/dev/null || RET_GONE="$RET_GONE${RET_GONE:+, }$_r"; done; IFS="$_IFS"
1306
+ [ -n "$RET_GONE" ] && say 'settings.json: retired §4.4 ask rule(s) REMOVED (%s) — guard-bash.sh now asks for these itself; an ask rule would override its CLAUDE_GIT_OK allow. Re-add one only if your project wants that trade.' "$RET_GONE"
1307
+ # A refused or failed merge leaves the file as it was, so HANDOVER must not claim a merge that did not run.
1308
+ if [ -n "$SET_NOTE" ]; then HAND_SET="NOT merged — ${SET_NOTE%.}"
1309
+ elif [ "$SET_FRESH" = 1 ]; then HAND_SET="Crewforth's settings.json installed (the project had none, so nothing was merged)"
1310
+ else HAND_SET="hook-aware merge (Crewforth hooks REFRESHED to current — new events + timeouts land; your own custom hooks/permissions PRESERVED${RET_GONE:+, except the retired §4.4 ask rule(s) REMOVED: $RET_GONE — guard-bash.sh asks for these itself})"; fi
1311
+
1312
+ # ============ [STAGE 4] GIT-HOOK ARMING (SHIM) + PROOF ============
1313
+ h1m 'Stage 4 — arm the git gates (SHIM via husky) + PROOF'
1314
+
1315
+ # 4a) location of the existing hook chain (the shim calls this too)
1316
+ ORIG_HOOKS=""
1317
+ if [ -n "$CURHP" ]; then ORIG_HOOKS="$CURHP"
1318
+ elif [ -d .husky ]; then ORIG_HOOKS=".husky"
1319
+ elif [ -x .git/hooks/pre-commit ] || [ -x .git/hooks/commit-msg ]; then ORIG_HOOKS=".git/hooks"; fi
1320
+ # never shim the kit onto its OWN hooks (re-adopt) — the shim would exec itself and recurse on every commit
1321
+ case "$ORIG_HOOKS" in .claude/hooks|.claude/git-shim) ORIG_HOOKS="" ;; esac
1322
+
1323
+ if [ -z "$ORIG_HOOKS" ]; then
1324
+ git config core.hooksPath .claude/hooks
1325
+ say 'core.hooksPath -> .claude/hooks (no existing hook chain)'
1326
+ else
1327
+ mkdir -p .claude/git-shim
1328
+ for hk in pre-commit commit-msg; do
1329
+ cat > ".claude/git-shim/$hk" <<SHIM
1330
+ #!/usr/bin/env bash
1331
+ # kit git-shim: runs the kit hook + the existing project chain IN ORDER (if one fails git stops).
1332
+ set -e
1333
+ H="\$(basename "\$0")"
1334
+ ROOT="\$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
1335
+ K="\$ROOT/.claude/hooks/\$H"; [ -x "\$K" ] && "\$K" "\$@"
1336
+ P="\$ROOT/$ORIG_HOOKS/\$H"
1337
+ if [ -x "\$P" ]; then "\$P" "\$@"
1338
+ elif [ -f "\$P" ]; then bash "\$P" "\$@"; fi
1339
+ exit 0
1340
+ SHIM
1341
+ chmod +x ".claude/git-shim/$hk"
1342
+ done
1343
+ git config core.hooksPath .claude/git-shim
1344
+ say 'SHIM installed -> core.hooksPath=.claude/git-shim (Crewforth + %s run together)' "$ORIG_HOOKS"
1345
+ fi
1346
+ [ "$GITKIND" = "worktree/submodule (.git file)" ] && warnm 'worktree/submodule: core.hooksPath may also affect the main checkout (git design).'
1347
+
1348
+ # 4b) PROOF — is the kit actually working? (not a claim)
1349
+ h1m 'Stage 4b — PROOF'
1350
+ PROOF_OK=1; HP="$(git config --get core.hooksPath 2>/dev/null || echo .claude/hooks)"
1351
+ # 1) trace-scan git hook: a staged AI trace MUST be BLOCKED (NO real commit; run the hook directly)
1352
+ # move any allowlist aside so PROOF measures the SCANNER itself, not the project's own exemptions (else a loosened repo fails the proof)
1353
+ [ -f .trace-allowlist.txt ] && mv .trace-allowlist.txt .trace-allowlist.txt.proofbak 2>/dev/null
1354
+ printf 'Co-Authored%s: Test <x@y.z>\n' '-By' > .kit-proof.txt # not contiguous in source (so the trace hook doesn't block itself); full at runtime
1355
+ # `-f`, and the staging is CHECKED. Without both, a project whose .gitignore happens to cover this probe file
1356
+ # (`*.txt`, `.kit-*`, anything broad) stages nothing, the scanner reads an empty diff, the hook exits 0 — and
1357
+ # the proof then accuses a perfectly working gate of letting an AI trace through. Reproduced here: identical
1358
+ # repos, the only difference a .gitignore line for the probe, and PROOF-1 flipped from BLOCKED to FAILED.
1359
+ # A proof that can fail for a reason unrelated to what it proves is worse than no proof: it sends the reader
1360
+ # hunting a gate that is fine. (The allowlist is already handled above by moving it aside.)
1361
+ if ! git add -f .kit-proof.txt >/dev/null 2>&1 || [ -z "$(git diff --cached --name-only -- .kit-proof.txt 2>/dev/null)" ]; then
1362
+ say '~ PROOF-1: skipped — could not stage the probe file (nothing was measured)'
1363
+ elif bash "$HP/pre-commit" >/tmp/kitproof.$$ 2>&1; then
1364
+ warnm 'PROOF-1 FAILED: the trace scan LET THROUGH the AI trace'; PROOF_OK=0
1365
+ elif grep -qiE 'TRACE-SCANNER|Commit stopped|forbidden' /tmp/kitproof.$$; then
1366
+ say 'OK · PROOF-1: staged AI trace BLOCKED by the trace scan'
1367
+ else say '~ PROOF-1: hook blocked (%s)' "$(head -1 /tmp/kitproof.$$ 2>/dev/null)"; fi
1368
+ git reset -q .kit-proof.txt 2>/dev/null; rm -f .kit-proof.txt /tmp/kitproof.$$
1369
+ [ -f .trace-allowlist.txt.proofbak ] && mv .trace-allowlist.txt.proofbak .trace-allowlist.txt 2>/dev/null
1370
+ # 2) guard-bash git-approval gate: keyless 'git commit' -> block
1371
+ if printf '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' | bash .claude/hooks/guard-bash.sh >/dev/null 2>&1; then
1372
+ warnm 'PROOF-2 FAILED: guard-bash LET THROUGH the keyless commit'; PROOF_OK=0
1373
+ else say "OK · PROOF-2: guard-bash BLOCKED the keyless 'git commit' (holds in auto/bypass too)"; fi
1374
+ # 3) can the kit agents + discipline be loaded
1375
+ NCCK="$(ls .claude/agents/crew-*.md 2>/dev/null | wc -l | tr -d ' ')"
1376
+ if [ "${NCCK:-0}" -ge 1 ]; then say 'OK · PROOF-3: %s Crewforth agents (crew-) installed + discoverable' "$NCCK"; else warnm 'PROOF-3: no Crewforth agent'; PROOF_OK=0; fi
1377
+ if [ -s .claude/DISCIPLINE.md ] && grep -qF '@.claude/DISCIPLINE.md' CLAUDE.md; then say 'OK · PROOF-4: DISCIPLINE.md loaded + @import-ed from CLAUDE.md'; else warnm 'PROOF-4: discipline not linked'; PROOF_OK=0; fi
1378
+ # PROOF-5: a takeover renamed the project's agents to `crew-`, but CLAUDE.md — or an orchestration doc it points to
1379
+ # (e.g. "detail: docs/AGENTS.md") — may still name the OLD bare agent, which now matches no installed agent, so
1380
+ # delegation to it silently fails. We follow CLAUDE.md's reference chain (its @imports + docs/…md paths) so the
1381
+ # pointed-to docs are checked too; the migration cannot safely rewrite hand-authored prose, so we TELL, precisely.
1382
+ if [ -f CLAUDE.md ] && ls .claude/agents/crew-*.md >/dev/null 2>&1; then
1383
+ PULL_AGENTS=" crew-commit-agent crew-session-manager " # invoked explicitly, not auto-delegated
1384
+ SCAN="CLAUDE.md"
1385
+ for r in $(grep -oE '@?[A-Za-z0-9_./-]+\.md' CLAUDE.md 2>/dev/null | sed 's/^@//' | sort -u); do
1386
+ [ -f "$r" ] && [ "$r" != "CLAUDE.md" ] && SCAN="$SCAN $r"
1387
+ done
1388
+ # This was agent × document with `sed|head|tr` per agent and `grep|cut|tr|sed` per PAIR — 12 agents × 7 docs ×
1389
+ # 4 spawns is ~340 processes to discover, almost always, that nothing is stale. doctor.sh carried the identical
1390
+ # loop and was converted to two awk passes in 2.0.1; adopt.sh kept it, which is why an update still crawled on
1391
+ # Git Bash. Same conversion, same output (agent order, then file order, comma-joined line numbers).
1392
+ STALE=""; STALE_PULL=""
1393
+ CREW_AGENT_BASES="$(awk '
1394
+ FNR==1 { files[++nf]=FILENAME }
1395
+ !got[FILENAME] && /^name:[[:space:]]*/ {
1396
+ n=$0; sub(/^name:[[:space:]]*/,"",n); gsub(/[^a-zA-Z0-9-]/,"",n)
1397
+ if (n != "") { nm[FILENAME]=n; got[FILENAME]=1 }
1398
+ }
1399
+ END {
1400
+ for (i=1;i<=nf;i++) {
1401
+ f=files[i]; n=(f in nm) ? nm[f] : ""
1402
+ if (n=="") { n=f; sub(/\.md$/,"",n); sub(/.*\//,"",n) }
1403
+ if (n ~ /^crew-/) { b=n; sub(/^crew-/,"",b); print b "\t" n; print b "-csk\t" n }
1404
+ }
1405
+ }' .claude/agents/crew-*.md 2>/dev/null)"
1406
+ export CREW_AGENT_BASES
1407
+ while IFS="$(printf '\t')" read -r base aname f lines; do
1408
+ [ -n "$base" ] || continue
1409
+ _mt '%s line(s): %s' "$f" "$lines"
1410
+ entry="
1411
+ ↳ \"$base\" → \"$aname\" ($_M)"
1412
+ case "$PULL_AGENTS" in *" $aname "*) STALE_PULL="$STALE_PULL$entry" ;; *) STALE="$STALE$entry" ;; esac
1413
+ done <<EOF
1414
+ $(awk '
1415
+ BEGIN {
1416
+ n = split(ENVIRON["CREW_AGENT_BASES"], rows, "\n"); k=0
1417
+ for (i=1;i<=n;i++) { if (rows[i]=="") continue; split(rows[i], a, "\t"); k++; base[k]=a[1]; full[k]=a[2] }
1418
+ nb=k
1419
+ }
1420
+ FNR==1 { order[++nf]=FILENAME }
1421
+ {
1422
+ for (i=1;i<=nb;i++)
1423
+ if ($0 ~ ("(^|[^a-zA-Z0-9_-]|@agent-)" base[i] "([^a-zA-Z0-9_-]|\$)"))
1424
+ hit[i, FILENAME] = (hit[i, FILENAME]=="" ? FNR : hit[i, FILENAME] "," FNR)
1425
+ }
1426
+ END {
1427
+ for (i=1;i<=nb;i++)
1428
+ for (j=1;j<=nf;j++)
1429
+ if ((i, order[j]) in hit) print base[i] "\t" full[i] "\t" order[j] "\t" hit[i, order[j]]
1430
+ }' $SCAN 2>/dev/null)
1431
+ EOF
1432
+ [ -n "$STALE" ] && warnm 'PROOF-5: CLAUDE.md (or a doc it references) names auto-delegated agent(s) by an old bare id — rename each to its crew- id, else delegation to them silently fails:%s' "$STALE"
1433
+ [ -n "$STALE_PULL" ] && warnm 'PROOF-5: CLAUDE.md (or a referenced doc) names pull-only agent(s) by an old bare id (still work; rename for consistency):%s' "$STALE_PULL"
1434
+ fi
1435
+ [ "$PROOF_OK" = 1 ] && h1m 'PROOF: Crewforth 100%% ACTIVE — gates armed, agents + discipline loaded' || warnm 'PROOF: some gates could not be verified (see above)'
1436
+
1437
+ # ============ [STAGE B] APPLY THE DECISIONS ============
1438
+ h1m 'Stage B — apply the decisions'
1439
+ # #3 loosen trace gate -> repo-root .trace-allowlist.txt (co-author/sign-off exempt from the trace scan)
1440
+ if [ "$DEC3" = loosen ]; then
1441
+ { [ -f .trace-allowlist.txt ] && cat .trace-allowlist.txt; printf 'Co-Authored%s\n' '-By'; } | sort -u > .trace-allowlist.txt.t && mv .trace-allowlist.txt.t .trace-allowlist.txt
1442
+ say '#3 loosen -> .trace-allowlist.txt (co-author trailer exempt)'
1443
+ else say '#3 keep -> full trace scan'; fi
1444
+ # #4 share/hide — the payload is ALWAYS committed to the review branch (so the diff is real + rollback stays clean);
1445
+ # 'hide' becomes a post-merge follow-up in HANDOVER. (Gitignoring .claude BEFORE the commit would drop it from the
1446
+ # review diff and leave it untracked after a rollback -> 'project untouched' would be a lie.)
1447
+ HIDE_NOTE=""
1448
+ if [ "$DEC4" = hide ]; then
1449
+ # docs/ belongs in this command for the same reason it belongs in .gitignore: the opt-out has to cover
1450
+ # the working documents too, or "keep the kit local" leaves the plans, handovers and threat models behind
1451
+ # in the shared repository. The two files the adoption force-added are named explicitly, because they are
1452
+ # tracked despite the ignore rule and `git rm --cached docs` alone would not reach them.
1453
+ HIDE_NOTE="Keep Crewforth local after merging: git rm -r --cached .claude CLAUDE.md docs && printf '.claude/\nCLAUDE.md\ndocs/\n' >> .gitignore && git commit -m 'crewforth: keep local'"
1454
+ say '#4 hide -> recorded; .claude stays TRACKED on the branch (rollback-safe). Post-merge steps in HANDOVER.'
1455
+ else
1456
+ # `share` is a NO-OP by design: the installer never stages a user's files, it only declines to add a
1457
+ # .gitignore entry. Saying "tracked + shared with the team" therefore reported an outcome it had neither
1458
+ # produced nor checked — and it was wrong in the repo that noticed, where the project's own .gitignore
1459
+ # already covers .claude/ and `git ls-files .claude` returns nothing. Report the state that IS true, and
1460
+ # say plainly which part is left to the user.
1461
+ if [ "$TRACKED" = 1 ]; then
1462
+ say '#4 share -> .claude/CLAUDE.md is tracked; nothing added to .gitignore, so it stays shared'
1463
+ elif git check-ignore -q .claude 2>/dev/null; then
1464
+ say '#4 share -> nothing added to .gitignore, but this repo ALREADY ignores .claude — the stage step skips it, so it is NOT shared'
1465
+ else
1466
+ say '#4 share -> nothing added to .gitignore; the stage step below adds .claude — commit it to share it'
1467
+ fi
1468
+ fi
1469
+ # #1 merge: document (NO automatic risky merge — red-team; merging is a human-approved follow-up)
1470
+ case "$DEC1" in
1471
+ takeover) MERGE_NOTE="takeover: Crewforth's crew- agents own the overlapping roles ($COLLIDE); each old agent's domain was imported to a draft skill (skills/<name>-local) the Crewforth agent applies, and the original backed up under superseded/agents/ — refine the drafts" ;;
1472
+ keepmine) MERGE_NOTE="keepmine: your agents own the overlapping roles ($COLLIDE); Crewforth's matching crew- agents were not installed" ;;
1473
+ *) MERGE_NOTE="keep: project + Crewforth agents side by side (no overlaps, or overlaps left to coexist)" ;;
1474
+ esac
1475
+ # #7 off-repo transfer: paste from the user (interactive; skipped on non-TTY and under --yes)
1476
+ OFFREPO_TEXT=""
1477
+ if [ "$DEC7" = transfer ] && [ -t 0 ] && [ "${ASSUME_YES:-0}" != 1 ]; then
1478
+ h1m '#7 off-repo decisions — paste them here'
1479
+ subm 'Write the decisions made in chat/on the web but NOT in the repo. When done, an EMPTY line (Enter).'
1480
+ while IFS= read -r line; do [ -z "$line" ] && break; OFFREPO_TEXT="$OFFREPO_TEXT
1481
+ - $line"; done
1482
+ [ -n "$OFFREPO_TEXT" ] && say '#7 -> %s lines will go into HANDOVER' "$(printf '%s' "$OFFREPO_TEXT" | grep -c .)" || say '#7 -> empty'
1483
+ fi
1484
+
1485
+ # ============ [STAGE 5] HANDOVER.md + ADR (decisions persist) ============
1486
+ h1m 'Stage 5 — HANDOVER.md + ADR (handover persists; decisions are not lost)'
1487
+ mkdir -p docs docs/adr
1488
+ DATE_H="$(date +%Y-%m-%d)"
1489
+ # compute the decision values first (avoid inner-quote/command-sub tangle in the heredoc)
1490
+ case "$DEC1" in takeover) D1='takeover (Crewforth crew- owns overlaps; your agents imported to <name>-local skills, originals in superseded/)';; keepmine) D1='keepmine (your agents own overlaps)';; none) D1='none';; *) D1='keep (coexist)';; esac
1491
+ D2='project wins' # precedence is fixed (DEC2 not overridable) — no false 'kit wins' record
1492
+ D3="$([ "$DEC3" = loosen ] && echo 'loosen (.trace-allowlist written)' || echo 'keep (full)')"
1493
+ D4="$([ "$DEC4" = hide ] && echo 'hide (gitignore)' || echo 'keep sharing')"
1494
+ D5="$([ -n "$ORIG_HOOKS" ] && echo "SHIM ($ORIG_HOOKS)" || echo 'direct')"
1495
+ D6="$([ "$DEC6" = absolute ] && echo 'absolute 0/0/0/0' || echo 'baseline+regression')"
1496
+ case "$DEC7" in transfer) D7='transferred (below)';; skip) D7='knowingly missing';; *) D7='local + ask';; esac
1497
+ HOOKDESC="$([ -n "$ORIG_HOOKS" ] && echo "SHIM (Crewforth + $ORIG_HOOKS together)" || echo '.claude/hooks direct')"
1498
+ if [ -n "${OFFREPO_TEXT:-}" ]; then OFFSEC="$OFFREPO_TEXT"
1499
+ elif [ "$OFFREPO" = 1 ]; then OFFSEC="> WARNING: no local .claude/CLAUDE.md -> decisions may also be in chat/on the web; the tool COULD NOT SEE them.
1500
+ <!-- Write off-repo decisions here; move the important ones under docs/adr/. -->"
1501
+ else OFFSEC="<!-- Write potentially in-chat decisions here; move the important ones under docs/adr/. -->"; fi
1502
+
1503
+ # 5a) HANDOVER.md — mechanical fact (verifiable) + human section (NO LLM signature)
1504
+ cat > docs/HANDOVER.md <<HAND
1505
+ # Handover Note (HANDOVER) — $DATE_H
1506
+
1507
+ > This document records what the tool did MECHANICALLY (verifiable) + marks the HUMAN
1508
+ > sections you need to fill in. The tool does NOT SIGN off anything as "done".
1509
+
1510
+ ## What was handed over (mechanical)
1511
+ - Crewforth agents: $NCCK (crew- namespace; no clash with project agents).
1512
+ - Project agents: $N_PAGENTS — UNTOUCHED, in place + active (recursive discovery).
1513
+ - Discipline: .claude/DISCIPLINE.md + @import into the project CLAUDE.md (content untouched).
1514
+ - settings.json: $HAND_SET.
1515
+ - Git gates: $HOOKDESC.
1516
+ - Overlapping roles: $MERGE_NOTE.
1517
+ - $BR_HANDOVER_LINE
1518
+
1519
+ ## Decisions made (smart suggestion; review/override in Stage B)
1520
+ | # | Decision | Value |
1521
+ |---|---|---|
1522
+ | 1 | Role clash | $D1 |
1523
+ | 2 | Precedence | $D2 (axis-by-axis) |
1524
+ | 3 | Trace gate | $D3 |
1525
+ | 4 | Share/hide | $D4 |
1526
+ | 5 | Git hook | $D5 |
1527
+ | 6 | Brownfield DoD | $D6 |
1528
+ | 7 | Off-repo | $D7 |
1529
+
1530
+ ## CONFIRM (the tool cannot verify — you check)
1531
+ - [ ] Are the inherited project rules/agents UP TO DATE? (stale rule = regression)
1532
+ - [ ] Overlapping roles (project + Crewforth same job): which one to use / merge?
1533
+ - [ ] Has the staged change set been reviewed (editor's Changes panel / git status) before committing?
1534
+ ${HIDE_NOTE:+- [ ] HIDE chosen — after merge run: $HIDE_NOTE}
1535
+
1536
+ ## Off-repo / in-chat decisions
1537
+ $OFFSEC
1538
+
1539
+ ---
1540
+ Generated: crewforth adopt · $DATE_H · $GEN_WHERE (apart from this line there is NO tool SIGNATURE)
1541
+ HAND
1542
+ say '%s written' docs/HANDOVER.md
1543
+
1544
+ # 5b) ADR-0001 — the handover itself is a persistent decision (never-overwrite)
1545
+ # 3.0 renamed the file with the product. A project adopted before that already holds its ADR-0001 under the old
1546
+ # name, and a second one next to it would say the same decision twice: the old file, when present, IS the record,
1547
+ # and it is left exactly as it is. Either name existing means nothing is written.
1548
+ ADR1="docs/adr/0001-crewforth-adoption.md"
1549
+ [ -e "docs/adr/0001-agentic-kit-adoption.md" ] && ADR1="docs/adr/0001-agentic-kit-adoption.md"
1550
+ if [ ! -e "$ADR1" ]; then
1551
+ cat > "$ADR1" <<ADR
1552
+ # ADR-0001: Crewforth was handed over to this project
1553
+
1554
+ - Date: $DATE_H
1555
+ - Status: $ADR_BR_STATUS
1556
+
1557
+ ## Context
1558
+ The existing project was equipped for agentic work with Crewforth, handed over the way one team hands a project to another.
1559
+ Goal: don't break the project, don't lose decisions already made, and don't leave Crewforth passive (hybrid).
1560
+
1561
+ ## Decision
1562
+ - Crewforth's agents were installed under the crew- namespace; the project's agents are preserved side by side, untouched.
1563
+ - Crewforth's discipline is active via .claude/DISCIPLINE.md + @import; the project CLAUDE.md is untouched.
1564
+ - On rule conflicts the PROJECT wins (axis-by-axis).
1565
+ - Git gates: $HOOKDESC.
1566
+ - Every change is on a reviewable git branch; rollback = git.
1567
+
1568
+ ## Consequence
1569
+ From now on decisions are written as ADRs under docs/adr/, NOT in chat (persistence).
1570
+ Inherited stale rules are subject to "confirm"; not authoritative until verified with code.
1571
+ ADR
1572
+ say '%s written (persistent handover decision)' "$ADR1"
1573
+ else
1574
+ say '%s already exists — untouched (never-overwrite)' "$ADR1"
1575
+ fi
1576
+
1577
+ # The §4.6 review record is runtime state, not configuration. `start.sh` gitignores `.claude/` wholesale so it
1578
+ # is covered there, but an adoption may deliberately TRACK that directory (#4 share) — and then this one file
1579
+ # would turn up in every `git status` as a change nobody made on purpose. One narrow line, either way.
1580
+ #
1581
+ # `docs/` closes a privacy hole rather than expressing a preference. README.md and the adr, teamboard and
1582
+ # handoff skills all state that docs/ is gitignored in an install, and §4.3 promises internal working
1583
+ # documents stay private — but only start.sh ever wrote that entry, so an adoption published every one of
1584
+ # them. Measured across the payload: PLAN.md is named in 7 components, SESSION_STATE.md in 6,
1585
+ # THREAT_MODEL.md in 6, plus SECURITY_FINDINGS.md, DISCOVERY.md and EVAL.md. A repository receiving this
1586
+ # adoption was receiving its own threat model and security findings along with it.
1587
+ # `.claude/.state/` for the same reason as the review record: it is this machine's runtime state (the update
1588
+ # check's cache and answers, the pre-update snapshot) and a tracked .claude/ would otherwise put it in git status.
1589
+ gi_add '.claude/review-pass.json' 'docs/' '.claude/.state/'
1590
+ # TWIN OF start.sh's ga_add, and this path needs it MORE: adopt.sh does not gitignore `.claude/` at all — it
1591
+ # only ignores review-pass.json and docs/ — so an adopted project TRACKS the kit's configuration by default.
1592
+ # That is the shared case, which is the one where a Windows teammate's `core.autocrlf=true` rewrites every
1593
+ # installed hook to CRLF on checkout. Measured on a bare-repo round trip: the committed blob carries 0 CR and
1594
+ # the working tree comes back with 1345 in guard-bash.sh, 575 in pre-commit and 49 in commit-msg — measured on
1595
+ # a real Windows machine through this path, with 149 files tracked and `.claude/` not gitignored. Only
1596
+ # `autocrlf=true` corrupts (`input` and `false` come back clean unpinned), and that is the Git for Windows
1597
+ # system-level default, so this protects the person who changed nothing.
1598
+ # The victim is NOT Git Bash: a CRLF hook still runs there, measured against a destructive payload with the
1599
+ # identical verdict. It is a non-MSYS bash on the same tree — WSL, which the kit's own .gitattributes names
1600
+ # and which neither of us could measure. The data files the hooks read strip a trailing CR themselves; a
1601
+ # script cannot strip its own, which is why the pin covers the scripts rather than trusting a strip.
1602
+ # Asked of git rather than assumed, like the line above: if this repo ignores `.claude/` after all, nothing to do.
1603
+ if ! git check-ignore -q .claude 2>/dev/null; then
1604
+ GA_LINES='.claude/**/*.sh text eol=lf
1605
+ .claude/hooks/pre-commit text eol=lf
1606
+ .claude/hooks/commit-msg text eol=lf
1607
+ .claude/**/*.txt text eol=lf
1608
+ .claude/**/*.conf text eol=lf'
1609
+ if ! git check-attr eol -- .claude/hooks/guard-bash.sh 2>/dev/null | grep -q ': lf$'; then
1610
+ [ -e .gitattributes ] || : > .gitattributes
1611
+ printf '%s\n' "$GA_LINES" | while IFS= read -r _gal; do
1612
+ [ -n "$_gal" ] || continue # set -e is on from line 898: no `cmd && continue` here
1613
+ if grep -qxF "$_gal" .gitattributes 2>/dev/null; then continue; fi
1614
+ if [ -s .gitattributes ] && [ "$(tail -c 1 .gitattributes | od -An -tx1 | tr -d ' \n')" != "0a" ]; then
1615
+ printf '\n' >> .gitattributes
1616
+ fi
1617
+ printf '%s\n' "$_gal" >> .gitattributes
1618
+ done
1619
+ say '+ .gitattributes: eol pins so the shared hooks stay LF on a Windows checkout'
1620
+ fi
1621
+ fi
1622
+
1623
+ # Ignoring docs/ and then `git add docs` would stage NOTHING, and that would take the adoption's own record
1624
+ # out of the review diff — the exact failure recorded in CHANGELOG 2.5.0, where gitignoring before the
1625
+ # branch commit dropped the payload from the diff and left "the project is untouched" untrue after a
1626
+ # rollback. So the two files THIS SCRIPT authored are force-added: they are the evidence a reviewer reads,
1627
+ # and they are the only things under docs/ that the adoption itself put there. Everything the skills write
1628
+ # later stays private, which is what the guarantee was always about.
1629
+ git add .claude CLAUDE.md >/dev/null 2>&1
1630
+ git add -f docs/HANDOVER.md "$ADR1" >/dev/null 2>&1
1631
+ [ -e .gitignore ] && git add .gitignore >/dev/null 2>&1
1632
+ [ -e .trace-allowlist.txt ] && git add .trace-allowlist.txt >/dev/null 2>&1
1633
+ # NO auto-commit: the change set stays STAGED-but-uncommitted on branch $BR, so every added/changed file shows up
1634
+ # in your editor's Source Control / Changes panel for review. HEAD is untouched until you commit yourself.
1635
+
1636
+ h1m 'Review in your editor — nothing committed yet'
1637
+ rowv 'staged' "$(git diff --cached --stat 2>/dev/null | tail -1 || echo '(none)')" # git's own stat line
1638
+ sub "$ONBRANCH_LINE"
1639
+ subm 'see it: open the Source Control / Changes panel (every added + changed file is listed) · or: %s' 'git status'
1640
+ # The refresh is how an existing project gains the panel, and until now its closing
1641
+ # summary named neither the panel nor node — so a machine without node finished
1642
+ # quietly and the user found out later, at first use. That is the report this
1643
+ # whole round came from.
1644
+ if bash "$SRC/eval/preflight.sh" --has node 2>/dev/null; then
1645
+ subm 'panel: %s opens it from this project (or: %s)' /crew-studio 'node .claude/studio/server/index.js --open'
1646
+ else
1647
+ subm 'panel: needs Node 18+, absent here — Crewforth can fetch one: %s' 'bash .claude/studio/ensure-node.sh --plan'
1648
+ _mt '(it asks first, verifies the checksum, and touches nothing outside %s)' '~/.claude/studio-runtime'; sub " $_M"
1649
+ fi
1650
+ sub "$ACCEPT_LINE"
1651
+ sub "$DISCARD_LINE"
1652
+ warnm 'If Claude Code is running in this project, run /clear (or quit and relaunch it) — a new session loads the'
1653
+ _mt 'updated CLAUDE.md and discipline; a session opened before this run keeps the old rules until then.'
1654
+ printf ' %s%s%s\n' "$D" "$_M" "$R"
1655
+ # A 2.x variable name still works until 4.0 (eval/lib/crew-env.sh reads it); say its 3.0 name once, here, so the
1656
+ # user can switch. compgen is a builtin — the list of set names costs no process.
1657
+ for _v in $(compgen -e); do
1658
+ case "$_v" in CSK_CORRECT_STACK) ;; CSK_*)
1659
+ case "${_crew_legacy:-}" in
1660
+ *" ${_v#CSK_} "*) warnm '%s is set — its 3.0 name is %s (the old name works until 4.0)' "$_v" "CREW_${_v#CSK_}" ;;
1661
+ *) warnm '%s is set but no longer read — set %s instead' "$_v" "CREW_${_v#CSK_}" ;;
1662
+ esac ;;
1663
+ esac
1664
+ done
1665
+ # The star line, once per kit version: a first adopt, or the first update to a new version. lib/star.sh keeps
1666
+ # the marker (shared with doctor.sh) and owns the text, URL and the CREW_NO_STAR / CI silence.
1667
+ if [ -f .claude/eval/lib/star.sh ]; then
1668
+ _S="$(bash .claude/eval/lib/star.sh --once . 2>/dev/null || true)"
1669
+ if [ -n "$_S" ]; then printf '\n%s\n' "$_S"; fi # an `&&` here was the script's LAST status: rc=1 on every update
1670
+ fi