crewforth 3.1.0-rc.7 → 3.1.0

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
@@ -5,7 +5,7 @@ Crewforth was named Claude Starter Kit until 3.0.0.
5
5
  Notable changes to this project are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/),
6
6
  versioning follows [SemVer](https://semver.org/).
7
7
 
8
- ## [Unreleased] — 3.1.0
8
+ ## [3.1.0] — 2026-10-12
9
9
 
10
10
  ### Security
11
11
 
@@ -343,6 +343,12 @@ versioning follows [SemVer](https://semver.org/).
343
343
 
344
344
  ### Fixed
345
345
 
346
+ - **On some Windows machines every commit was stopped: "PRIVATE-PATH SCANNER: … could not run".** The look for a
347
+ machine-private string used `grep -iF`, and Git Bash's grep 3.0 aborts for it (exit 134): in the C locale with
348
+ Git for Windows 2.53, under a Turkish locale with 2.55 (measured on one machine of each). The commit hook sets
349
+ the C locale, so on the first kind every look aborted, and the hook rightly stopped the commit. A private term
350
+ is now escaped and matched with `-iE`, which aborted in none of the 15 pairs of `LANG` and `LC_ALL` measured on
351
+ either machine. A term is still a literal: `.`, `\`, `[`, `(`, `+`, `|` and the rest match only themselves.
346
352
  - **Critical work with a verify command was refused every time where the Agent tool has no `run_in_background`.**
347
353
  The gate asked for the field to be `false`. Where background tasks are off the tool has no such field (measured on
348
354
  Claude Code 2.1.296: the call arrives with description, prompt, subagent_type and model, and runs in the
@@ -10102,8 +10102,10 @@ printf 'acme-internal-host\n' > "$_TH/r/.private-terms.txt"
10102
10102
  _thbad=""
10103
10103
  _th_hook "$_TH/r" pre-commit ""; { [ "$_thr" = 1 ] && grep -q 'PRIVATE-PATH SCANNER: a staged line contains' "$_TH/out"; } \
10104
10104
  || _thbad="$_thbad [FIXTURE: with a working grep the private string is not reported: rc=$_thr]"
10105
- _th_hook "$_TH/r" pre-commit "" "$_TH/bin"; { [ "$_thr" = 1 ] && grep -q 'could not run' "$_TH/out"; } \
10106
- || _thbad="$_thbad [grep aborting for -iF: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
10105
+ # A grep that aborts for -iF changes nothing: the look for a private string does not use it (measured on two
10106
+ # Windows machines: Git Bash's grep 3.0 aborts for -iF in the C locale on one and under tr_TR on the other).
10107
+ _th_hook "$_TH/r" pre-commit "" "$_TH/bin"; { [ "$_thr" = 1 ] && grep -q 'PRIVATE-PATH SCANNER: a staged line contains' "$_TH/out" && ! grep -q 'could not run' "$_TH/out"; } \
10108
+ || _thbad="$_thbad [grep aborting for -iF: rc=$_thr, want 1 and the private string reported — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
10107
10109
  # the same for a scan by pattern: an abort is not "this pattern does not compile"
10108
10110
  mkdir -p "$_TH/bin2"
10109
10111
  printf '#!/bin/sh\ncase " $* " in *" -qiE "*) kill -ABRT $$ ;; esac\nexec "%s" "$@"\n' "$_thg" > "$_TH/bin2/grep"; chmod +x "$_TH/bin2/grep"
@@ -10112,6 +10114,41 @@ _th_hook "$_TH/e" pre-commit "" "$_TH/bin2"; { [ "$_thr" = 1 ] && grep -q 'TRACE
10112
10114
  || _thbad="$_thbad [pre-commit, grep aborting for -iE: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
10113
10115
  _th_hook "$_TH/e" commit-msg "" "$_TH/bin2" msg.txt; { [ "$_thr" = 1 ] && grep -q 'could not run' "$_TH/out"; } \
10114
10116
  || _thbad="$_thbad [commit-msg, grep aborting for -iE: rc=$_thr, want 1 and 'could not run' — $(sed -n 1p "$_TH/out" | cut -c1-120)]"
10117
+ # ...and the look for a private string, which is a scan by pattern now: an abort stops the commit and says so
10118
+ _th_hook "$_TH/r" pre-commit "" "$_TH/bin2"; { [ "$_thr" = 1 ] && grep -q 'PRIVATE-PATH SCANNER: the look for a machine-private string could not run' "$_TH/out"; } \
10119
+ || _thbad="$_thbad [the private-string look, grep aborting for -iE: rc=$_thr, want 1 and 'could not run' — $(grep -m1 PRIVATE "$_TH/out" | cut -c1-120)]"
10120
+ # A term is a literal, whatever it holds: each is found as written and in another case, and none of its characters
10121
+ # works as a pattern (the line on the right is what it would match if one did).
10122
+ _thl=0
10123
+ while IFS= read -r _tl; do [ -z "$_tl" ] && continue
10124
+ _tt="${_tl%% @@ *}"; _tr="${_tl#* @@ }"; _th1="${_tr%% @@ *}"; _th0="${_tr#* @@ }"; _thl=$((_thl+1))
10125
+ _th_repo "$_TH/l"; printf '%s\n' "$_tt" > "$_TH/l/.private-terms.txt"
10126
+ ( cd "$_TH/l" && printf 'x %s y\n' "$_th1" > n.md && git add n.md ) >/dev/null 2>&1
10127
+ _th_hook "$_TH/l" pre-commit ""; { [ "$_thr" = 1 ] && grep -q 'a staged line contains' "$_TH/out"; } || _thbad="$_thbad [the term $_tt is not found in: $_th1 (rc=$_thr)]"
10128
+ ( cd "$_TH/l" && printf 'x %s y\n' "$_th0" > n.md && git add n.md ) >/dev/null 2>&1
10129
+ _th_hook "$_TH/l" pre-commit ""; [ "$_thr" = 0 ] || _thbad="$_thbad [the term $_tt is read as a pattern: it matched $_th0 (rc=$_thr, $(sed -n 1p "$_TH/out" | cut -c1-80))]"
10130
+ done <<'TL'
10131
+ acme.internal @@ ACME.Internal @@ acmeXinternal
10132
+ C:\Users\ada.l @@ c:\users\ADA.L @@ C:Usersada.l
10133
+ build[7] @@ BUILD[7] @@ build7
10134
+ a+b(c) @@ A+B(C) @@ aab(c)
10135
+ host|other @@ HOST|OTHER @@ host
10136
+ cost$ ^x* @@ COST$ ^X* @@ cost ^
10137
+ q{2}? @@ Q{2}? @@ qq
10138
+ TL
10139
+ [ "$_thl" = 7 ] || _thbad="$_thbad [FIXTURE: the literal-term table has $_thl rows, not 7]"
10140
+ # No hook looks for anything with -iF: a code line of a gate that calls grep with both flags is the defect itself.
10141
+ _thf="$(for _f in "$HOOKS"/*.sh "$HOOKS/pre-commit" "$HOOKS/commit-msg"; do grep -nE '^[^#]*(grep[[:space:]][^|;#]*-[A-Za-z]*(i[A-Za-z]*F|F[A-Za-z]*i)|_crew_(scan|any)[^#]*[[:space:]](iF|Fi)([[:space:]]|$))' "$_f" 2>/dev/null | sed "s#^#${_f##*/}:#"; done)"
10142
+ [ -z "$_thf" ] || _thbad="$_thbad [a hook calls grep with -i and -F together: $(printf '%s' "$_thf" | head -2 | cut -c1-140)]"
10143
+ # ...and the look itself names the C locale on the call (3 calls: _crew_scan in both git hooks, _crew_any): under
10144
+ # tr_TR `grep -iE` answers "no match" for i against I without aborting, so a look left to the session's locale
10145
+ # would miss a term in capitals, silently.
10146
+ _thc="$(grep -cE '^[[:space:]]*LC_ALL=C grep -q' "$HOOKS/pre-commit" "$HOOKS/commit-msg" | tr '\n' ' ')"
10147
+ _thu="$(grep -nE '^[[:space:]]*grep -q("\$1"|\$3) -- ' "$HOOKS/pre-commit" "$HOOKS/commit-msg" | head -2)"
10148
+ case "$_thc" in *"pre-commit:2 "*"commit-msg:1 "*) ;; *) _thbad="$_thbad [the first look does not name the C locale on the call: $_thc]" ;; esac
10149
+ [ -z "$_thu" ] || _thbad="$_thbad [a first look runs in whatever locale is exported: $(printf '%s' "$_thu" | cut -c1-120)]"
10150
+ printf 'x() { grep -qiF -- "$a" "$b"; }\n_crew_scan iF "$t" "$f"\n# grep -iF in a comment\n' > "$_TH/pin.sh"
10151
+ [ "$(grep -cE '^[^#]*(grep[[:space:]][^|;#]*-[A-Za-z]*(i[A-Za-z]*F|F[A-Za-z]*i)|_crew_(scan|any)[^#]*[[:space:]](iF|Fi)([[:space:]]|$))' "$_TH/pin.sh")" = 2 ] || _thbad="$_thbad [FIXTURE: the -iF pin does not see the two lines it is for]"
10115
10152
  # and for the diff itself: the grep that picks the added lines aborting must not leave an empty file to scan
10116
10153
  mkdir -p "$_TH/bin3"
10117
10154
  printf '#!/bin/sh\n[ "$1" = -E ] && [ "$2" = "^\\+" ] && kill -ABRT $$\nexec "%s" "$@"\n' "$_thg" > "$_TH/bin3/grep"; chmod +x "$_TH/bin3/grep"
@@ -10124,7 +10161,7 @@ _th_hook "$_TH/b" pre-commit ""; { [ "$_thr" = 0 ] && grep -q 'is not a valid re
10124
10161
  || _thbad="$_thbad [pre-commit with a pattern that does not compile: rc=$_thr, want 0 and the warning]"
10125
10162
  _th_hook "$_TH/b" commit-msg "" "" msg.txt; { [ "$_thr" = 0 ] && grep -q 'is not a valid regular expression' "$_TH/out"; } \
10126
10163
  || _thbad="$_thbad [commit-msg with a pattern that does not compile: rc=$_thr, want 0 and the warning]"
10127
- [ -z "$_thbad" ] && pass "a scan that could not run stops the commit: with grep aborting for -iF (what Git Bash's grep does under a Turkish locale) or for -iE, or while the added lines are collected, pre-commit and commit-msg exit 1 and say so; a pattern that does not compile still only warns, in both" \
10164
+ [ -z "$_thbad" ] && pass "a scan that could not run stops the commit: with grep aborting for -iE (the scan by pattern and the look for a private string), or while the added lines are collected, pre-commit and commit-msg exit 1 and say so; a pattern that does not compile still only warns, in both; a grep that aborts for -iF changes nothing, since no hook calls it (pinned, with a twin the pin sees); the first look names the C locale on the call, in both git hooks; a private term is a literal: 7 terms with . \\ [ ] + ( ) | \$ ^ * { } ? are found as written, in another case, and match nothing else" \
10128
10165
  || fail "a scan that could not run:$_thbad"
10129
10166
 
10130
10167
  # The command shown in the approval prompt is cut to 300 bytes a line and goes into JSON. `cut -c` did it: GNU cut
@@ -10207,7 +10244,7 @@ else
10207
10244
  ( cd "$_TH/n" && printf 'see ŞİRKET-GİZLİ\n' > n.md && git add n.md ) >/dev/null 2>&1
10208
10245
  _th_hook "$_TH/n" pre-commit "$_th"; _thna="$_thr"
10209
10246
  # required where this platform's grep folds such a letter under the locale at all (GNU grep does, BSD grep does not)
10210
- if ( printf 'ŞİRKET\n' | LC_ALL=$_th grep -qiF 'şirket' ) >/dev/null 2>&1; then
10247
+ if ( printf 'ŞİRKET\n' | LC_ALL=$_th grep -qiE 'şirket' ) >/dev/null 2>&1; then
10211
10248
  [ "$_thna" = 1 ] || _thbad="$_thbad [pre-commit, a private string of non-ASCII letters in another case: rc=$_thna, want 1 (this grep folds them under $_th)]"
10212
10249
  fi
10213
10250
  [ -z "$_thbad" ] && pass "under $_th the git hooks stop what they stop under C: a vendor name in another case in an added line and in the message, a staged key that holds an I, a private string in another case; an ordinary line and message pass (a private string of non-ASCII letters in another case: exit $_thna, 1 = caught by the second look)" \
@@ -36,7 +36,9 @@ export LC_ALL=C
36
36
  # the first look finds nothing and the match ignores case: in the C locale such a letter has no other case.
37
37
  _crew_scan(){ # $1 = grep's flags after -q (iE, E, iF) · $2 = pattern · $3 = file
38
38
  local r=0
39
- grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
39
+ # The first look is in the C locale, SAID ON THE CALL and not left to the export at the top of the file: under
40
+ # tr_TR `grep -iE` does not abort, it answers "no match" for i against I (measured in Git Bash), which is silent.
41
+ LC_ALL=C grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
40
42
  if [ "$r" = 1 ] && [ -n "$_CREW_LOCALE" ]; then
41
43
  case "$1" in *i*) case "$2" in *[!$'\t'\ -~]*) r=0; LC_ALL="$_CREW_LOCALE" grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$? ;; esac ;; esac
42
44
  fi
@@ -37,7 +37,9 @@ export LC_ALL=C
37
37
  # the first look finds nothing and the match ignores case: in the C locale such a letter has no other case.
38
38
  _crew_scan(){ # $1 = grep's flags after -q (iE, E, iF) · $2 = pattern · $3 = file
39
39
  local r=0
40
- grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
40
+ # The first look is in the C locale, SAID ON THE CALL and not left to the export at the top of the file: under
41
+ # tr_TR `grep -iE` does not abort, it answers "no match" for i against I (measured in Git Bash), which is silent.
42
+ LC_ALL=C grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$?
41
43
  if [ "$r" = 1 ] && [ -n "$_CREW_LOCALE" ]; then
42
44
  case "$1" in *i*) case "$2" in *[!$'\t'\ -~]*) r=0; LC_ALL="$_CREW_LOCALE" grep -q"$1" -- "$2" "$3" 2>/dev/null || r=$? ;; esac ;; esac
43
45
  fi
@@ -352,18 +354,32 @@ done < <({ _floor_diff --cached; [ "${CREW_SCAN_UNSTAGED:-0}" = 1 ] && _floor_di
352
354
  # escapes to top level): 8 mismatches, in BOTH directions — three clean messages newly rejected and two real
353
355
  # secrets newly passed. Also checked structurally: no pattern in either shipped blocklist carries a bare
354
356
  # top-level `|` (every one is inside a group) and none uses a back-reference, so joining cannot reshape them.
355
- _crew_any(){ # $1 = pattern list $2 = corpus $3 = grep flags $4 = 1 for newline-joined fixed strings
357
+ # A LITERAL TERM IS LOOKED FOR AS A PATTERN, NEVER WITH `grep -iF`. Measured on two Windows machines, both GNU grep
358
+ # 3.0 in Git Bash, and they fail in opposite places:
359
+ # Git for Windows 2.53 (msys bash 5.2) grep -iF aborts (exit 134) when the locale in effect is C or POSIX
360
+ # Git for Windows 2.55 (cygwin bash 5.3) grep -iF aborts (exit 134) when the locale in effect is tr_TR
361
+ # This file sets the C locale (above), so on the first of them every look for a private string aborted and every
362
+ # commit was stopped; with no locale set, the second one aborted for a Turkish user. No locale is safe for both.
363
+ # On both, -iE and -i did not abort in any of 15 pairs of LANG and LC_ALL. So the term is escaped and matched with
364
+ # -iE: _crew_lit puts a backslash before each character that means something in a pattern.
365
+ _crew_lit(){ # $1 = a literal string -> _CL, the pattern that matches exactly it
366
+ local s="$1" c; _CL=""
367
+ while [ -n "$s" ]; do
368
+ c="${s:0:1}"; s="${s:1}"
369
+ case "$c" in '\'|.|'['|']'|'^'|'$'|'*'|+|'?'|'('|')'|'{'|'}'|'|') _CL="$_CL\\$c" ;; *) _CL="$_CL$c" ;; esac
370
+ done
371
+ }
372
+ _crew_any(){ # $1 = pattern list $2 = corpus $3 = grep flags $4 = 1 when the list holds literal terms, not patterns
356
373
  local pat alt="" sep="|" na=0 r=0
357
- [ "$4" = 1 ] && sep="
358
- "
359
374
  while IFS= read -r pat || [ -n "$pat" ]; do
360
375
  pat="${pat%$'\r'}"
361
376
  case "$pat" in ''|\#*) continue;; esac
362
- alt="${alt:+$alt$sep}$pat"
363
377
  case "$pat" in *[!$'\t'\ -~]*) na=1 ;; esac
378
+ if [ "$4" = 1 ]; then _crew_lit "$pat"; pat="$_CL"; fi
379
+ alt="${alt:+$alt$sep}$pat"
364
380
  done < "$1"
365
381
  [ -n "$alt" ] || return 1
366
- grep -q$3 -- "$alt" "$2" 2>/dev/null || r=$?
382
+ LC_ALL=C grep -q$3 -- "$alt" "$2" 2>/dev/null || r=$?
367
383
  # Nothing found, but a pattern holds a letter outside ASCII and the match ignores case: the per-pattern loop looks
368
384
  # again under the session's own locale (_crew_scan), so send the caller there.
369
385
  if [ "$r" = 1 ] && [ "$na" = 1 ] && [ -n "$_CREW_LOCALE" ]; then case "$3" in *i*) return 4 ;; esac; fi
@@ -472,21 +488,21 @@ PAL=""; [ -n "$GITROOT" ] && [ -f "$GITROOT/.private-allowlist.txt" ] && PAL="$
472
488
  fi
473
489
  [ -n "$PTF" ] && sed -e 's/[[:space:]]*$//' -e '/^#/d' -e '/^$/d' "$PTF"
474
490
  } > "$PTERM_F" 2>/dev/null || true
475
- if _crew_any "$PTERM_F" "$SECRET_F" iF 1; then _dec=0; else _dec=$?; fi
491
+ if _crew_any "$PTERM_F" "$SECRET_F" iE 1; then _dec=0; else _dec=$?; fi
476
492
  if [ -s "$PTERM_F" ] && [ -s "$SECRET_F" ] && [ "$_dec" != 1 ]; then
477
493
  while IFS= read -r term || [ -n "$term" ]; do
478
494
  term="${term%$'\r'}"
479
495
  [ -n "$term" ] || continue
480
496
  if [ -n "$PAL" ] && grep -qxF -- "$term" "$PAL"; then continue; fi
481
- _gr=0; _crew_scan iF "$term" "$SECRET_F" || _gr=$?
497
+ _crew_lit "$term"; _gr=0; _crew_scan iE "$_CL" "$SECRET_F" || _gr=$?
482
498
  if [ "$_gr" = 0 ]; then
483
499
  # The term is echoed back deliberately: unlike a secret, the author has to see WHICH private string to
484
500
  # replace, and it is already sitting in their own working tree.
485
501
  echo "PRIVATE-PATH SCANNER: a staged line contains a machine-private string -> '$term' (§4.3)"
486
502
  HIT=1
487
503
  elif [ "$_gr" -ge 2 ]; then
488
- # This loop used to read every exit status but 0 as "no match": under a Turkish locale in Git Bash grep -iF
489
- # aborted (134) for each term and the commit went through with both private strings in it (measured).
504
+ # This loop used to read every exit status but 0 as "no match": where grep aborted (134) for each term the
505
+ # commit went through with both private strings in it (measured).
490
506
  echo "PRIVATE-PATH SCANNER: the look for a machine-private string could not run, so the staged lines are not known to be clean and the commit is stopped (§4.3)."
491
507
  HIT=1
492
508
  fi
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "crewforth",
3
- "version": "3.1.0-rc.7",
4
- "description": "Crewforth — your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
3
+ "version": "3.1.0",
4
+ "description": "Crewforth \u2014 your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
5
5
  "bin": {
6
6
  "crewforth": "bin/cli.js"
7
7
  },
@@ -15,8 +15,7 @@
15
15
  "kit/",
16
16
  "VERSION",
17
17
  "LICENSE",
18
- "README.md",
19
- "CHANGELOG.md"
18
+ "README.md", "CHANGELOG.md"
20
19
  ],
21
20
  "engines": {
22
21
  "node": ">=20"
@@ -43,7 +42,8 @@
43
42
  "code-review",
44
43
  "git-hooks"
45
44
  ],
46
- "author": "Barış Yerlikaya",
45
+ "author": "Bar\u0131\u015f Yerlikaya",
46
+ "homepage": "https://crewforth.com/",
47
47
  "repository": {
48
48
  "type": "git",
49
49
  "url": "git+https://github.com/Crewforth/crewforth.git"