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 +12 -0
- package/VERSION +1 -1
- package/kit/eval/smoke-test.sh +39 -0
- package/kit/hooks/guard-agent-model.sh +10 -4
- package/kit/hooks/guard-bash.sh +20 -2
- package/kit/hooks/prompt-approval.sh +2 -2
- package/package.json +1 -1
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.
|
|
1
|
+
3.2.0-rc.7
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -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
|
-
|
|
476
|
-
case "$l" in *:*) ;; *) CARD_WHY="line $
|
|
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 $
|
|
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; }
|
package/kit/hooks/guard-bash.sh
CHANGED
|
@@ -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
|
-
|
|
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