crewforth 3.0.0-rc.1 → 3.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -125,6 +125,27 @@ One name everywhere: the package, the components, the variables, the payload. **
125
125
  the project's own system first. Installed text no longer calls Crewforth a kit: the skills' adaptation note is
126
126
  "Crewforth adaptation", and the discipline quotes the update warning the hook actually prints.
127
127
 
128
+ ### Fixed — found by the release rehearsal
129
+
130
+ - **Counts:** a fresh install says "12 agents · 39 skills · 10 commands", as the README does, and an update from 2.x
131
+ counts skills and commands (it used to print "51 skills" and "+75" — directories and files). The closing line no
132
+ longer names the installer's folder.
133
+ - **`/crew-doctor` speaks your language:** it answers in the language the project was installed in; commands,
134
+ paths and settings keys stay as written. The skill-listing check counts the way Claude Code counts, reads
135
+ `skillListingBudgetFraction` from your settings, and on a 200,000-token model tells you the one line to add and
136
+ what it costs. In the plugin edition, `/crew-doctor` reports the same thing.
137
+ - **Updating from 2.x:**
138
+ - a repository with no commit yet gets a one-line branch name and accept/discard commands that work there;
139
+ - a repository that already ignores `.claude` is recommended to keep it local;
140
+ - in a private install the handover and the ADR are written but not staged;
141
+ - the `.NET` pattern skill the update keeps is not flagged as unvetted in the next session;
142
+ - the 2.x template sentences in `CLAUDE.md` are brought up to date, and your own lines are left alone.
143
+ - **The 2.x package name** says which package it forwards to.
144
+ - **Replies stay in your language** even when a skill's text is English.
145
+ - **`/crew-review`** runs the closing reviewer whenever the audits leave no critical or high finding; medium and low
146
+ findings go into its prompt and the report.
147
+ - **Release asset:** a release candidate's archive carries its own version in the file name.
148
+
128
149
  ### Added — "an update is out, update now?"
129
150
 
130
151
  - **A newer release is a question, not a footnote.** At session start Claude asks once — **Update · Later · Skip
package/adopt.sh CHANGED
@@ -110,6 +110,7 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
110
110
  "3.0 rename: %s → %s") s='3.0 ad değişikliği: %s → %s' ;;
111
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
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 ref-sweep: the 2.x template wording in %s now says Crewforth") s="3.0 referans taraması: %s içindeki 2.x şablon ifadeleri artık Crewforth diyor" ;;
113
114
  "3.0 commands are skills: %s → %s") s="3.0'da komutlar skill oldu: %s → %s" ;;
114
115
  "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
116
  "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" ;;
@@ -157,6 +158,10 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
157
158
  "share") s='paylaş' ;;
158
159
  ".claude/CLAUDE.md is tracked — keep sharing with the team") s=".claude/CLAUDE.md git'te izleniyor — ekiple paylaşmaya devam" ;;
159
160
  "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' ;;
161
+ "hide") s='gizle' ;;
162
+ ".claude is already gitignored in this repo — Crewforth stays local (pick share and un-ignore it to share)") s="bu depoda .claude zaten gitignore'da — Crewforth yerelde kalır (paylaşmak için share seçip ignore kuralını kaldırın)" ;;
163
+ "#4 hide -> this repo already ignores .claude — nothing to add; Crewforth stays local") s="#4 hide -> bu depo .claude'u zaten yok sayıyor — eklenecek bir şey yok; Crewforth yerelde kalır" ;;
164
+ "docs/ is gitignored here (a private install): %s and %s are written but not staged — they stay on this machine") s="docs/ burada gitignore'da (kişisel kurulum): %s ve %s yazıldı ama stage edilmedi — bu makinede kalır" ;;
160
165
  "5 Git hooks") s="5 Git hook'ları" ;;
161
166
  "install directly") s='doğrudan kur' ;;
162
167
  "no existing hook system") s='hook sistemi yok' ;;
@@ -610,6 +615,12 @@ if [ "$IS_GIT" = 1 ]; then
610
615
  { [ "$HAS_CLAUDE" = 1 ] && [ -n "$(git ls-files .claude 2>/dev/null | head -1)" ]; } && TRACKED=1
611
616
  fi
612
617
  if [ "$TRACKED" = 1 ]; then _v='YES — shared with the team'; else _v='no/untracked'; fi
618
+ # Already ignored? Then "share" is not a real option without the user undoing their own rule, and recommending it
619
+ # ended with "#4 share -> … NOT shared" (RC-1 rehearsal). Asked of git, like the stage step below asks it.
620
+ # The question names a path INSIDE .claude: a fresh adopt has no .claude yet, and a `.claude/` rule only matches a
621
+ # directory git can see — asked about the bare name it answered "not ignored" (measured). An inner path's leading
622
+ # components are directories to git, so both `.claude/` and `.claude` rules answer.
623
+ IGN=0; [ "$IS_GIT" = 1 ] && [ "$TRACKED" != 1 ] && git check-ignore -q .claude/settings.json 2>/dev/null && IGN=1
613
624
  rowm '.claude/CLAUDE.md in git' "$_v"
614
625
 
615
626
  # Supply-chain scan (advisory, read-only): the project's OWN (non-crew) skills/agents may have been pulled from an
@@ -652,6 +663,8 @@ else
652
663
  fi
653
664
  if [ "$TRACKED" = 1 ]; then
654
665
  propm '4 Share/hide' 'share' '.claude/CLAUDE.md is tracked — keep sharing with the team'
666
+ elif [ "$IGN" = 1 ]; then
667
+ propm '4 Share/hide' 'hide' '.claude is already gitignored in this repo — Crewforth stays local (pick share and un-ignore it to share)'
655
668
  else
656
669
  propm '4 Share/hide' 'share' 'untracked; Crewforth files are shared by default — pick hide to keep them local'
657
670
  fi
@@ -673,7 +686,7 @@ fi
673
686
  DEC1="$([ "$N_PAGENTS" != 0 ] && echo keep || echo none)"
674
687
  DEC2="project" # precedence is FIXED to project-wins (not overridable — reflected in the @import comment)
675
688
  DEC3="$([ "$COAUTHOR" = 1 ] && echo loosen || echo keep)"
676
- DEC4="$([ "$TRACKED" = 1 ] && echo share || echo kit-default)"
689
+ DEC4="$([ "$TRACKED" = 1 ] && echo share || { [ "$IGN" = 1 ] && echo hide || echo kit-default; })"
677
690
  DEC6="baseline"
678
691
  DEC7="$([ "$OFFREPO" = 1 ] && echo transfer || echo local)"
679
692
  # normalize display defaults to a real, offered token
@@ -764,7 +777,11 @@ fi
764
777
  # A new branch isolates a big first change so the main line stays clean until you review. But on a routine UPDATE
765
778
  # of a project whose .claude/ is gitignored, a forced new branch is empty and pointless — the refresh lands on disk
766
779
  # 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)"
780
+ # symbolic-ref, not `rev-parse --abbrev-ref HEAD || echo main`: on a repository with no commit yet rev-parse PRINTS
781
+ # "HEAD" and then fails, so the fallback was appended and the prompt named the branch "HEAD<newline>main" (RC-1).
782
+ # symbolic-ref names an unborn branch; a detached HEAD falls through to rev-parse and reads "HEAD" as before.
783
+ BASE="$(git symbolic-ref --short -q HEAD 2>/dev/null)" || BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
784
+ [ -n "$BASE" ] || BASE=main
768
785
  DEC_BR="$BRANCH_MODE"
769
786
  if [ -z "$DEC_BR" ]; then
770
787
  if [ "$KIT_PRESENT" != 1 ]; then DEC_BR=new # first adopt: isolate the change, keep the main line clean
@@ -785,6 +802,10 @@ fi
785
802
 
786
803
  # BR_HANDOVER_LINE / GEN_WHERE / ADR_BR_STATUS go into HANDOVER.md and the ADR, so they stay English (artefacts).
787
804
  # ONBRANCH/ACCEPT/DISCARD_LINE are only ever printed to the terminal, so they are translated here.
805
+ # A repository with no commit yet has no HEAD to reset to and no branch to delete, so the discard command differs:
806
+ # un-stage with `git rm --cached` (the files stay on disk, nothing was committed) and, on a new branch, point HEAD
807
+ # back at the original name. Measured on an unborn repository: both leave 0 staged files and HEAD on the base name.
808
+ UNBORN=0; git rev-parse --verify -q HEAD >/dev/null 2>&1 || UNBORN=1
788
809
  if [ "$DEC_BR" = here ]; then
789
810
  BR="$BASE" # $BR is referenced downstream; on 'here' it IS the current branch
790
811
  say 'applying on the current branch: %s (no separate branch; staged, HEAD untouched until you commit)' "${B}$BASE${R}"
@@ -793,7 +814,8 @@ if [ "$DEC_BR" = here ]; then
793
814
  ADR_BR_STATUS="accepted (applied on current branch: $BASE — staged, not committed)"
794
815
  _mt 'You are on your current branch %s with everything STAGED but NOT committed.' "$BASE"; ONBRANCH_LINE="$_M"
795
816
  _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"
817
+ if [ "$UNBORN" = 1 ]; then _mt 'discard: %s (un-stages everything; nothing was committed)' 'git rm -r -q --cached .'
818
+ else _mt 'discard: %s (un-stages everything; nothing was committed)' 'git reset --hard HEAD'; fi; DISCARD_LINE="$_M"
797
819
  else
798
820
  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
821
  TS="$(date +%Y%m%d-%H%M%S)"; BR="kit-adopt-$TS"
@@ -806,8 +828,12 @@ else
806
828
  GEN_WHERE="branch $BR"
807
829
  ADR_BR_STATUS="accepted (handover branch: $BR)"
808
830
  _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"
831
+ # With no commit yet there is no $BASE to merge into (checkout fails: "pathspec did not match"); the first commit
832
+ # simply becomes the base branch by renaming this one.
833
+ if [ "$UNBORN" = 1 ]; then _mt 'accept: %s then: %s' "git commit -m 'adopt Crewforth'" "git branch -m $BASE"
834
+ else _mt 'accept: %s then: %s' "git commit -m 'adopt Crewforth'" "git checkout $BASE && git merge $BR"; fi; ACCEPT_LINE="$_M"
835
+ if [ "$UNBORN" = 1 ]; then _mt 'discard: %s' "git rm -r -q --cached . && git symbolic-ref HEAD refs/heads/$BASE"
836
+ else _mt 'discard: %s' "git reset --hard $BASE && git checkout $BASE && git branch -D $BR"; fi; DISCARD_LINE="$_M"
811
837
  fi
812
838
 
813
839
  mkdir -p .claude
@@ -959,17 +985,37 @@ fi
959
985
  [ "$COLLIDE_MODE" = keepmine ] && for b in $COLLIDE; do EXCL_A="$EXCL_A crew-$b.md"; done
960
986
  # kit-owned trees: FORCE-refresh on a re-adopt (KIT_PRESENT) so kit updates land; never-overwrite on a fresh adopt
961
987
  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
988
+ # The summary counts skills and commands the way the README does — one per skill, a `kind: command` skill as a
989
+ # command, an `experimental: true` one as neither. copy_noclobber counts FILES (a skill with references/ is several),
990
+ # which is how an update from 2.13.0 once reported "skills +75" for 39 skills. A skill counts as added when the
991
+ # copy put it there or refreshed it; one that was excluded, or that a fresh adopt found already present and left
992
+ # alone, counts as skipped. The "already present" answer is taken before the copy, with builtins only.
993
+ _pre=" "; for kd in .claude/skills/*/; do [ -d "$kd" ] && { kn="${kd%/}"; _pre="$_pre${kn##*/} "; }; done
994
+ copy_noclobber "$SRC/skills" .claude/skills "$KIT_PRESENT" "$EXCL_S"
995
+ S_ADD=0; S_SKIP=0; C_ADD=0; C_SKIP=0
996
+ for kd in "$SRC"/skills/*/; do
997
+ [ -f "$kd/SKILL.md" ] || continue; kn="${kd%/}"; kn="${kn##*/}"; _k=0; _e=0; _d=0
998
+ while IFS= read -r _l || [ -n "$_l" ]; do
999
+ _l="${_l%$'\r'}"
1000
+ case "$_l" in ---) _d=$((_d+1)); [ "$_d" -ge 2 ] && break ;; " kind: command") _k=1 ;; " experimental: true") _e=1 ;; esac
1001
+ done < "$kd/SKILL.md"
1002
+ [ "$_e" = 1 ] && continue
1003
+ _got=1
1004
+ case " $EXCL_S " in *" $kn "*) _got=0 ;; esac
1005
+ [ "$_got" = 1 ] && [ "$KIT_PRESENT" != 1 ] && case "$_pre" in *" $kn "*) _got=0 ;; esac
1006
+ if [ "$_k" = 1 ]; then [ "$_got" = 1 ] && C_ADD=$((C_ADD+1)) || C_SKIP=$((C_SKIP+1))
1007
+ else [ "$_got" = 1 ] && S_ADD=$((S_ADD+1)) || S_SKIP=$((S_SKIP+1)); fi
1008
+ done
968
1009
  # Read BEFORE the hooks tree is refreshed: whether §4.2's vendor line is armed right now (see the re-arm below).
969
1010
  # `\r?`: a blocklist that reached this checkout CRLF (autocrlf=true, a Windows editor) still counts as armed —
970
1011
  # a plain -x match read it as disarmed and the refresh switched §4.2 off without a word (measured in review).
971
1012
  VENDOR_ARMED=0; grep -qxE $'DevArchitecture\r?' .claude/hooks/trace-blocklist.txt 2>/dev/null && VENDOR_ARMED=1
972
1013
  copy_noclobber "$SRC/hooks" .claude/hooks "$KIT_PRESENT"; H_ADD=$ret_add; H_SKIP=$ret_skip
1014
+ # The 3.0 migration made cqrs-aop-module a project skill; it is the one Crewforth shipped until now, so the updater
1015
+ # vouches for it rather than having the next session open by asking whether to trust it. Only that component, and
1016
+ # only after the refreshed hook (which knows --trust-one) is in place.
1017
+ [ "$LEGACY_DOTNET" = 1 ] && [ -f .claude/skills/cqrs-aop-module/SKILL.md ] \
1018
+ && bash .claude/hooks/skill-trust.sh --trust-one skills/cqrs-aop-module >/dev/null 2>&1 || true
973
1019
  copy_noclobber "$SRC/eval" .claude/eval "$KIT_PRESENT"; E_ADD=$ret_add; E_SKIP=$ret_skip
974
1020
  # The Studio panel. This is the line that answers "I updated and `/crew-studio` says the panel
975
1021
  # is missing": with KIT_PRESENT=1 it is a force-refresh, so a project that already has the kit
@@ -1096,6 +1142,20 @@ if [ "$KIT_PRESENT" = 1 ] && [ -f CLAUDE.md ] && grep -q -e '-csk' CLAUDE.md $(g
1096
1142
  rm -f "$f.kit-before"
1097
1143
  done
1098
1144
  set --
1145
+ # The 2.x template's own sentences say "kit" (the RC-1 rehearsal found three in an updated CLAUDE.md). Each is
1146
+ # matched as that template wrote it — the whole header line, and two sentences from their first word — so text the
1147
+ # user wrote is never touched; 3.0's template says Crewforth in exactly these places.
1148
+ if grep -qE '^<!-- kit discipline · on conflict the project rules BELOW win -->$|^kit-owned and identical in every project|^kit-owned: an update overwrites it' CLAUDE.md 2>/dev/null; then
1149
+ cp CLAUDE.md CLAUDE.md.kit-before
1150
+ if sed $_SB -E -e 's/^<!-- kit discipline · on conflict the project rules BELOW win -->$/<!-- Crewforth discipline · on conflict the project rules BELOW win -->/' \
1151
+ -e 's/^kit-owned (and identical in every project, so it cannot know either\.)/Crewforth-owned \1/' \
1152
+ -e 's/^kit-owned(: an update overwrites it, so put )/Crewforth-owned\1/' CLAUDE.md > CLAUDE.md.kit-sweep; then
1153
+ cat CLAUDE.md.kit-sweep > CLAUDE.md
1154
+ fi
1155
+ rm -f CLAUDE.md.kit-sweep
1156
+ cmp -s CLAUDE.md CLAUDE.md.kit-before || say '3.0 ref-sweep: the 2.x template wording in %s now says Crewforth' CLAUDE.md
1157
+ rm -f CLAUDE.md.kit-before
1158
+ fi
1099
1159
  fi
1100
1160
  # §4.2: an armed vendor line STAYS armed, and nothing else arms it. The blocklist ships `# DevArchitecture`
1101
1161
  # commented; before 3.0 only `start.sh --dotnet` uncommented it, and the force-refresh above resets it to a
@@ -1445,7 +1505,9 @@ else say '#3 keep -> full trace scan'; fi
1445
1505
  # 'hide' becomes a post-merge follow-up in HANDOVER. (Gitignoring .claude BEFORE the commit would drop it from the
1446
1506
  # review diff and leave it untracked after a rollback -> 'project untouched' would be a lie.)
1447
1507
  HIDE_NOTE=""
1448
- if [ "$DEC4" = hide ]; then
1508
+ if [ "$DEC4" = hide ] && [ "$IGN" = 1 ]; then
1509
+ say '#4 hide -> this repo already ignores .claude — nothing to add; Crewforth stays local'
1510
+ elif [ "$DEC4" = hide ]; then
1449
1511
  # docs/ belongs in this command for the same reason it belongs in .gitignore: the opt-out has to cover
1450
1512
  # the working documents too, or "keep the kit local" leaves the plans, handovers and threat models behind
1451
1513
  # in the shared repository. The two files the adoption force-added are named explicitly, because they are
@@ -1627,7 +1689,14 @@ fi
1627
1689
  # and they are the only things under docs/ that the adoption itself put there. Everything the skills write
1628
1690
  # later stays private, which is what the guarantee was always about.
1629
1691
  git add .claude CLAUDE.md >/dev/null 2>&1
1630
- git add -f docs/HANDOVER.md "$ADR1" >/dev/null 2>&1
1692
+ # ...except in a private install. When docs/ is gitignored the owner has said internal documents stay off the
1693
+ # repository, and Crewforth's own review flagged the force-added pair as a §4.3 leak (RC-1 rehearsal). They are
1694
+ # written all the same and their place is said out loud; a shared install stages them as before.
1695
+ if git check-ignore -q docs/HANDOVER.md 2>/dev/null; then
1696
+ say 'docs/ is gitignored here (a private install): %s and %s are written but not staged — they stay on this machine' docs/HANDOVER.md "$ADR1"
1697
+ else
1698
+ git add -f docs/HANDOVER.md "$ADR1" >/dev/null 2>&1
1699
+ fi
1631
1700
  [ -e .gitignore ] && git add .gitignore >/dev/null 2>&1
1632
1701
  [ -e .trace-allowlist.txt ] && git add .trace-allowlist.txt >/dev/null 2>&1
1633
1702
  # NO auto-commit: the change set stays STAGED-but-uncommitted on branch $BR, so every added/changed file shows up
package/kit/CLAUDE.md CHANGED
@@ -22,7 +22,7 @@ lines for a greeting or a quick question. At every
22
22
  decision point ask with the **`AskUserQuestion`** tool (selectable single/multi-select) — never prose to type back,
23
23
  never skip asking. Correct wrong information gently
24
24
  but clearly. When there is a decision, give a clear recommendation and close with the single next step that
25
- matters most.
25
+ matters most. Reply in the user's language even when a skill's text is English; names stay as written.
26
26
 
27
27
  ## No deferral
28
28
  Nothing is left for later ("we'll do it in v2" is not acceptable). At a blocker: **STOP → inform → present options →