crewforth 3.2.0-rc.5 → 3.2.0-rc.7

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
@@ -220,6 +220,18 @@ versioning follows [SemVer](https://semver.org/).
220
220
 
221
221
  ### Fixed
222
222
 
223
+ - **A push approval covers a tag on the approved commit.** `git push <remote> <tag>` had no way to be approved: the
224
+ click and `/crew-approve push` both record a push of the branch, and the tag push of a release was refused after
225
+ either, so the session handed the user the command to run. It now passes when the tag points at the commit the
226
+ approval covers (after a commit + push, the new one) and no branch has its name; a tag on another commit, a full
227
+ ref, a refspec and `--tags` are still refused.
228
+ - **`change: review` is read as `audit`.** The model table of the working rules names review with audit, and
229
+ `crew-review-agent` cards that said `change: review` were refused. It now takes the audit floor, on a critical path
230
+ too; a kind that is not on the list is still refused.
231
+ - **`above:` is read anywhere among a task card's three lines.** A card with `above: <why>` between `change:` and
232
+ `verify:` was refused ("line 3 of the task is not one of files:, change:, verify:") and the call was sent again
233
+ with the line moved down. It is now read in any position and keeps the model as it does after the three; the three
234
+ are still all required.
223
235
  - **A click on the approval question is recorded whatever order its keys come back in.** Claude Code hands the
224
236
  answered question back either as it was asked or with its keys sorted, and the answer side matched it byte for
225
237
  byte: in real `auto` sessions 30 of 30 answers in the asked order were recorded and 7 of 7 sorted ones refused
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.0-rc.5
1
+ 3.2.0-rc.7
@@ -7689,6 +7689,25 @@ _pa_say auto 'approve: push' >/dev/null; ( cd "$_paw" && git checkout -q -b othe
7689
7689
  _pa_run auto 'git push origin feat/x'; [ "$_par" = 2 ] || _pabad="$_pabad other-branch-checked-out:rc=$_par"
7690
7690
  [ -z "$_pabad" ] && pass "a push approval is for the commit HEAD was on and the branch that was checked out: one more commit, or another branch, and it is refused" \
7691
7691
  || fail "push binding:$_pabad"
7692
+ # A tag on the approved commit is part of that push (a release: `/crew-approve push`, then `git push origin v2.0.0`
7693
+ # was refused on a real project). A tag elsewhere, a name that is also a branch, or a full ref is not.
7694
+ _pa_new; _pabad=""; ( cd "$_paw" && git commit -qm second && git tag old HEAD^ && git tag v1 && git tag -a v1a -m v1a && git tag both && git branch both ) >/dev/null 2>&1
7695
+ _pa_say auto 'approve: commit' >/dev/null
7696
+ _pa_run auto 'git push origin v1'; [ "$_par" = 2 ] || _pabad="$_pabad [a commit approval pushed a tag: rc=$_par]"
7697
+ _pa_say auto 'approve: push' >/dev/null
7698
+ for _pc in 'git push origin v1' 'git push origin v1a' 'git push -q origin v1'; do
7699
+ _pa_run auto "$_pc"; [ "$_par" = 0 ] || _pabad="$_pabad [$_pc → $_par $(head -c 120 "$_PA/err")]"
7700
+ done
7701
+ _pa_run auto 'git push origin old'; { [ "$_par" = 2 ] && grep -q 'the tag old is on' "$_PA/err"; } || _pabad="$_pabad [a tag on another commit → $_par]"
7702
+ for _pc in 'git push origin both' 'git push origin refs/tags/v1' 'git push origin nosuch' 'git push origin v1:v2' 'git push other v1'; do
7703
+ _pa_run auto "$_pc"; [ "$_par" = 2 ] || _pabad="$_pabad [$_pc → $_par]"
7704
+ done
7705
+ _pa_new; ( cd "$_paw" && git tag before ) >/dev/null 2>&1; _pa_say auto 'approve: commit+push' >/dev/null
7706
+ _pa_run auto 'git commit -m x' >/dev/null; ( cd "$_paw" && git commit -qm approved && git tag after ) >/dev/null 2>&1
7707
+ _pa_run auto 'git push origin after'; [ "$_par" = 0 ] || _pabad="$_pabad [commit+push: a tag on the new commit → $_par $(head -c 120 "$_PA/err")]"
7708
+ _pa_run auto 'git push origin before'; { [ "$_par" = 2 ] && grep -q 'the tag before is on' "$_PA/err"; } || _pabad="$_pabad [commit+push: a tag on the old commit → $_par]"
7709
+ [ -z "$_pabad" ] && pass "a push approval covers a tag on the approved commit (lightweight or annotated; after commit+push, on the new commit): a tag on another commit, a name that is also a branch, a full ref, a refspec, another remote or a commit-only approval is refused" \
7710
+ || fail "tag push:$_pabad"
7692
7711
 
7693
7712
  # ---- commit+push: the push is for the commit the approved diff became -----------------------------------------
7694
7713
  _pa_new; _pabad=""
@@ -8598,6 +8617,26 @@ for _pr in 'files: a · change: archi (planning only) · verify: none' 'files: a
8598
8617
  done
8599
8618
  [ -z "$_v2b" ] && pass "a card on one line (· ; | separators, a Card: lead, above: on the line) is read by its labels and held to the three-line rules: a critical path still needs opus; a kind with a note in parentheses is the kind, on one line or three; a bad change, labels out of order, a missing verify or prose first are refused" \
8600
8619
  || fail "the one-line card:$_v2b"
8620
+ # `above:` among the three lines, not only after them (2026-10-19: 2 of 19 line refusals had it between change: and
8621
+ # verify:; the retry moved it down and passed). It keeps the model the same way; the three are still all required.
8622
+ _v2b=""
8623
+ for _pr in 'files: src/ui/a.ts\nchange: feature\nabove: the parser is subtle\nverify: true' 'above: the parser is subtle\nfiles: src/ui/a.ts\nchange: feature\nverify: true'; do
8624
+ _v2one "$_pr" opus; { [ "$_rc" = 0 ] && [ ! -s "$_v2/oneout" ]; } || _v2b="$_v2b [above: among the three did not keep opus: $_pr → $_rc $(head -c 100 "$_v2/oneout")]"
8625
+ done
8626
+ _v2one 'files: src/ui/c.ts\nchange: feature\nverify: true' opus
8627
+ grep -q '"updatedInput":' "$_v2/oneout" || _v2b="$_v2b [without above: opus was not lowered (rc $_rc)]"
8628
+ for _pr in 'files: src/ui/a.ts\nchange: feature\nabove: x\nDo it.' 'files: src/ui/a.ts\nabove: x\nabove: y\nverify: true'; do
8629
+ _v2one "$_pr" sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [passed: $_pr → $_rc]"
8630
+ done
8631
+ # `change: review` is an audit (the discipline's table lists it with them): same floor, a near miss still refused.
8632
+ _v2one 'files: src/ui/a.ts\nchange: review\nverify: none' haiku; { [ "$_rc" = 2 ] && grep -q 'runs on sonnet' "$_v2/err"; } || _v2b="$_v2b [review on haiku was not held to sonnet as audit (rc $_rc)]"
8633
+ for _pr in 'files: src/ui/a.ts\nchange: review\nverify: none' 'files: src/ui/a.ts · change: review (the staged diff) · verify: none'; do
8634
+ _v2one "$_pr" sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [refused: $_pr → $_rc $(head -c 100 "$_v2/err")]"
8635
+ done
8636
+ _v2one 'files: src/payments/p.ts\nchange: review\nverify: none' sonnet; { [ "$_rc" = 2 ] && grep -q 'runs on opus' "$_v2/err"; } || _v2b="$_v2b [review on a critical path was not held to opus (rc $_rc)]"
8637
+ _v2one 'files: src/ui/a.ts\nchange: reviews\nverify: none' sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [change: reviews passed (rc $_rc)]"
8638
+ [ -z "$_v2b" ] && pass "above: may stand anywhere among the three card lines and keeps the model; without it the model is lowered; above: does not stand in for files:, change: or verify:; change: review is an audit, held to the same floors, and a near miss is still refused" \
8639
+ || fail "above: in the card:$_v2b"
8601
8640
 
8602
8641
  # ---- at write time: the card can leave a file out --------------------------------------------------------------
8603
8642
  _v2new; _v2b=""
@@ -459,7 +459,7 @@ _am_card_line(){ # $1 = the first line of the task -> 0 and CARD_* set (uncheck
459
459
  return 0
460
460
  }
461
461
  _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (CARD_ABOVE); 1 and CARD_WHY
462
- local s="$1" l k v n=0 seen="" one=0
462
+ local s="$1" l k v n=0 ln=0 seen="" one=0
463
463
  CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""; CARD_ABOVE=""
464
464
  s="${s//$'\r'/}"
465
465
  # One line first: the first line that is not empty, read by its labels; the three-line reading otherwise.
@@ -472,17 +472,20 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (C
472
472
  l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
473
473
  while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
474
474
  [ -n "$l" ] || continue
475
- n=$((n+1))
476
- case "$l" in *:*) ;; *) CARD_WHY="line $n of the task is not one of files:, change:, verify:"; return 1 ;; esac
475
+ ln=$((ln+1))
476
+ case "$l" in *:*) ;; *) CARD_WHY="line $ln of the task is not one of files:, change:, verify:"; return 1 ;; esac
477
477
  k="${l%%:*}"; v="${l#*:}"
478
478
  while :; do case "$v" in [$' \t']*) v="${v:1}" ;; *) break ;; esac; done
479
479
  while :; do case "$v" in *[$' \t']) v="${v%?}" ;; *) break ;; esac; done
480
+ # `above:` may stand anywhere among the three (2 of 19 line refusals, 2026-10-19: between change: and verify:).
480
481
  case "$k" in
482
+ above) [ -n "$CARD_ABOVE" ] || CARD_ABOVE="${v:0:200}"; continue ;;
481
483
  files) CARD_FILES="$v" ;;
482
484
  change) CARD_CHANGE="$v" ;;
483
485
  verify) CARD_VERIFY="$v" ;;
484
- *) CARD_WHY="line $n of the task is not one of files:, change:, verify:"; return 1 ;;
486
+ *) CARD_WHY="line $ln of the task is not one of files:, change:, verify:"; return 1 ;;
485
487
  esac
488
+ n=$((n+1))
486
489
  case "$seen" in *" $k "*) CARD_WHY="the card names $k twice"; return 1 ;; esac
487
490
  seen="$seen $k "
488
491
  done
@@ -491,6 +494,9 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (C
491
494
  # A note in parentheses after the kind is the session's own comment, as `none (…)` is for verify: `architecture
492
495
  # (planning only)` is architecture (4 of the cards refused on this machine, 2026-10-18). Only a kind on the list, and
493
496
  # only a note that is the whole rest of the value in parentheses.
497
+ # `review` is an audit: the discipline's model table names it with them, and 4 crew-review-agent cards were refused
498
+ # for it (2026-10-19).
499
+ case "$CARD_CHANGE" in review|'review ('*')') CARD_CHANGE=audit ;; esac
494
500
  case "$CARD_CHANGE" in *' ('*')') v="${CARD_CHANGE%% (*}"; case "$_AM_CHANGES" in *" $v "*) CARD_CHANGE="$v" ;; esac ;; esac
495
501
  case "$_AM_CHANGES" in *" $CARD_CHANGE "*) ;; *) CARD_WHY="change: is '${CARD_CHANGE:0:40}', not one of:$_AM_CHANGES"; return 1 ;; esac
496
502
  [ -n "$CARD_VERIFY" ] || { CARD_WHY="verify: is empty (a command, or none)"; return 1; }
@@ -2437,7 +2437,7 @@ _p44_scan(){ # $1 = the collapsed `git push …` -> 0 when it is `git push [-u]
2437
2437
  return 0
2438
2438
  }
2439
2439
  _approval_ok(){ # 0 = the user's recorded approval covers THIS call. Otherwise _APW says why not, for the message.
2440
- local k v op="" tr="" h="" b="" r="" u="" sid="" ts="" now age dir="${_CWD:-.}" hc=1 hp=1 cur tool
2440
+ local k v op="" tr="" h="" b="" r="" u="" sid="" ts="" now age dir="${_CWD:-.}" hc=1 hp=1 cur tool tag _h2
2441
2441
  _APW="no approval from the user is on record"
2442
2442
  git_has "$CMD_SEEN" 'commit' && hc=0; git_has "$CMD_SEEN" 'push' && hp=0
2443
2443
  if [ "$hc" = 0 ] && [ "$hp" = 0 ]; then
@@ -2478,7 +2478,17 @@ _approval_ok(){ # 0 = the user's recorded approval covers THIS call. Otherwise
2478
2478
  _p44_scan "$_A44_S" || { _APW="the push is not in the one form an approval covers, git push <remote> <branch> ($_P44_WHY)"; return 1; }
2479
2479
  [ -n "$b" ] && [ -n "$r" ] && [ -n "$u" ] || { _APW="the approval record could not be read"; return 1; }
2480
2480
  if [ "$_P44_REMOTE" != "$r" ]; then _APW="the user approved a push to $r, not to $_P44_REMOTE"; return 1; fi
2481
- case "$_P44_REF" in "$b"|HEAD) ;; *) _APW="the user approved a push of $b, not of $_P44_REF"; return 1 ;; esac
2481
+ # A TAG ON THE APPROVED COMMIT is part of that push (a release is tagged on what was approved): `git push <remote>
2482
+ # <tag>` passes when <tag> is a tag and not also a branch, and it points at the commit the approval covers. A tag
2483
+ # on any other commit, --tags, and a refspec are refused as before.
2484
+ tag=""
2485
+ case "$_P44_REF" in
2486
+ "$b"|HEAD) ;;
2487
+ *[!A-Za-z0-9._/-]*|-*|*..*|*/) _APW="the user approved a push of $b, not of $_P44_REF"; return 1 ;;
2488
+ *) if git -C "$dir" show-ref --verify --quiet "refs/tags/$_P44_REF" 2>/dev/null \
2489
+ && ! git -C "$dir" show-ref --verify --quiet "refs/heads/$_P44_REF" 2>/dev/null; then tag="$_P44_REF"
2490
+ else _APW="the user approved a push of $b, not of $_P44_REF"; return 1; fi ;;
2491
+ esac
2482
2492
  cur="$(git -C "$dir" symbolic-ref --short -q HEAD 2>/dev/null || true)"
2483
2493
  [ "$cur" = "$b" ] || { _APW="the user approved a push of $b, and the branch checked out now is ${cur:-<detached>}"; return 1; }
2484
2494
  # Where the remote pushes TO, with pushurl and pushInsteadOf applied: the name alone can be pointed elsewhere.
@@ -2491,8 +2501,16 @@ _approval_ok(){ # 0 = the user's recorded approval covers THIS call. Otherwise
2491
2501
  cur="$(git -C "$dir" rev-parse --verify --quiet HEAD 2>/dev/null || echo NONE)"
2492
2502
  if [ "$op" = push ]; then
2493
2503
  [ "$cur" = "$h" ] || { _APW="HEAD moved after the user approved the push (approved ${h:0:7}, now ${cur:0:7})"; return 1; }
2504
+ if [ -n "$tag" ]; then
2505
+ cur="$(git -C "$dir" rev-parse --verify --quiet "refs/tags/$tag^{commit}" 2>/dev/null || echo NONE)"
2506
+ [ "$cur" = "$h" ] || { _APW="the tag $tag is on ${cur:0:7}, not on the commit the user approved for push (${h:0:7})"; return 1; }
2507
+ fi
2494
2508
  return 0
2495
2509
  fi
2510
+ if [ -n "$tag" ]; then
2511
+ _h2="$cur"; cur="$(git -C "$dir" rev-parse --verify --quiet "refs/tags/$tag^{commit}" 2>/dev/null || echo NONE)"
2512
+ [ "$cur" = "$_h2" ] || { _APW="the tag $tag is on ${cur:0:7}, not on the commit the user approved for push (${_h2:0:7})"; return 1; }
2513
+ fi
2496
2514
  # commit+push: HEAD has to be the commit the approved index became.
2497
2515
  cur="$(git -C "$dir" rev-parse --verify --quiet 'HEAD^' 2>/dev/null || echo NONE)"
2498
2516
  if [ "$h" = NONE ] || [ "$cur" != "$h" ]; then
@@ -707,9 +707,9 @@ _pa_record(){
707
707
  commit) WHAT="commit of what is staged now (tree $_d) on HEAD $_h"
708
708
  HOW="Run git commit -m with the message, alone in its call: no other option than -q, -s or -v, no paths, no cd, no pipe, nothing chained. Single-quote the message, or read it from a here-document with a quoted delimiter. The review record of §4.6 is still required." ;;
709
709
  push) WHAT="push of $BR at $_h to $RM"
710
- HOW="Run exactly: git push $RM $BR (alone in its call: the remote and the branch written out, no cd, no pipe, nothing chained)." ;;
710
+ HOW="Run exactly: git push $RM $BR (alone in its call: the remote and the branch written out, no cd, no pipe, nothing chained). A tag on that commit goes the same way: git push $RM <tag>." ;;
711
711
  commit+push) WHAT="commit of what is staged now (tree $_d) on HEAD $_h, then push of that commit on $BR to $RM"
712
- HOW="Run git commit -m with the message, alone in its call (no other option than -q, -s or -v, no paths, no cd, no pipe, nothing chained; single-quote the message; §4.6 still applies), then in a separate call exactly: git push $RM $BR" ;;
712
+ HOW="Run git commit -m with the message, alone in its call (no other option than -q, -s or -v, no paths, no cd, no pipe, nothing chained; single-quote the message; §4.6 still applies), then in a separate call exactly: git push $RM $BR (a tag on the new commit: git push $RM <tag>)" ;;
713
713
  esac
714
714
  [ -n "$_u" ] && _u=" ($RM pushes to $_u)"
715
715
  _bw=""; [ "$BARE" = 1 ] && _bw="approved: $OP (what this session was refused). "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewforth",
3
- "version": "3.2.0-rc.5",
3
+ "version": "3.2.0-rc.7",
4
4
  "description": "Crewforth — your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
5
5
  "bin": {
6
6
  "crewforth": "bin/cli.js"