crewforth 3.2.0-rc.3 → 3.2.0-rc.4
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 +18 -2
- package/VERSION +1 -1
- package/kit/agents/crew-commit-agent.md +6 -4
- package/kit/eval/smoke-test.sh +66 -1
- package/kit/hooks/agent-outcome.sh +18 -0
- package/kit/hooks/guard-agent-model.sh +40 -3
- package/kit/hooks/guard-bash.sh +13 -13
- package/kit/hooks/prompt-approval.sh +21 -0
- package/kit/hooks/trace-blocklist.txt +9 -0
- package/kit/skills/crew-ship/SKILL.md +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -84,8 +84,10 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
84
84
|
own findings to `.crewforth/review.json`, blockers through the refuter. What it could not check (no shellcheck,
|
|
85
85
|
a file edited after staging, no Node) is said, never read as clean.
|
|
86
86
|
|
|
87
|
-
- **In `auto`, approve a commit or a push with one click.**
|
|
88
|
-
|
|
87
|
+
- **In `auto`, approve a commit or a push with one click.** A fixed question, asked with AskUserQuestion: Commit
|
|
88
|
+
only, Commit + push, Push only, No. It is handed to the session when `crew-commit-agent` is called, so `/crew-ship`
|
|
89
|
+
asks it with the message, and a refused commit offers it too (measured in real sessions: offered by the refusal
|
|
90
|
+
alone, it never came up, since the session asks before trying a commit). A new gate, `guard-question.sh`, answers `ask` on it,
|
|
89
91
|
so a hook that fills in the answer cannot answer it for you (measured in 34 runs and by hand in the terminal), and
|
|
90
92
|
`prompt-approval.sh` records your click exactly as it records `/crew-approve`: tied to what is staged, `HEAD`, the
|
|
91
93
|
session, for 30 minutes.
|
|
@@ -151,6 +153,16 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
151
153
|
|
|
152
154
|
### Changed
|
|
153
155
|
|
|
156
|
+
- **A refused commit asks one way.** In `auto`, with the approval question open, the refusal offered the click and
|
|
157
|
+
the typed `/crew-approve` side by side, and a session picked the typed one 7 times in 9. The click is now the way
|
|
158
|
+
to ask, and the typed command is only the fallback for a question that cannot be asked.
|
|
159
|
+
|
|
160
|
+
- **A task card on one line is read.** `files: … · change: … · verify: …` (with `;` or `|` too, after an optional
|
|
161
|
+
`Card:`, and `above:` on the same line) is read by its labels and held to the rules of the three-line card, and a
|
|
162
|
+
kind with a note in parentheses (`architecture (planning only)`) is that kind, on either form. Measured on one
|
|
163
|
+
machine's sessions: 37 calls had been refused for their card, every one followed by a second call; 17 of them now
|
|
164
|
+
pass on the first.
|
|
165
|
+
|
|
154
166
|
- **A model is chosen to cost what the work needs.** Measured on a real project: 186 crew tasks, 0 on haiku, 73 (39%)
|
|
155
167
|
above their card's floor, and no verify command ever passing, so the cheaper path never opened. Now:
|
|
156
168
|
- the card's model is the default: a call above it runs on it (the gate rewrites the call's model), unless the
|
|
@@ -183,6 +195,10 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
183
195
|
|
|
184
196
|
### Security
|
|
185
197
|
|
|
198
|
+
- **The session-link trailer is a trace.** Claude Code asks the model to end a commit message with a line linking the
|
|
199
|
+
session on claude.ai; the trace scan had no pattern for it, and six commits of a project with Crewforth installed
|
|
200
|
+
carried it to a remote. The trailer and its address are both caught now, in the message and in staged files.
|
|
201
|
+
|
|
186
202
|
- **A `PermissionRequest` hook could approve a commit with nobody at the keyboard.** Such a hook answers a
|
|
187
203
|
permission prompt before the person does, and the answer cannot be told from a click: measured, it approved the
|
|
188
204
|
Bash prompt `guard-bash.sh` raises for a commit in `default`, and it answered the new question too. Before it
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.2.0-rc.
|
|
1
|
+
3.2.0-rc.4
|
|
@@ -94,16 +94,18 @@ write is refused because the file is on a critical path, do not reach it another
|
|
|
94
94
|
proceed" is not approval (§4.4).
|
|
95
95
|
The tool-level gate `guard-bash.sh` intercepts commit/push in **every** permission mode: in normal modes it raises an
|
|
96
96
|
approval prompt only the user can answer — so present the message FIRST, then run the commit yourself and let the user
|
|
97
|
-
approve it at the prompt. Never hand the user a command to paste into their own terminal. In `auto
|
|
98
|
-
|
|
97
|
+
approve it at the prompt. Never hand the user a command to paste into their own terminal. In `auto`, when this agent
|
|
98
|
+
is called Crewforth hands the session the one-click approval question (below): ask it, with the message, instead of
|
|
99
|
+
asking for `/crew-approve`. In `dontAsk`, and in `auto` when Crewforth gives no question or says it is closed, the
|
|
100
|
+
prompt reaches nobody: ask in this shape and with nothing after it — the message in ONE code block,
|
|
99
101
|
and under it one sentence in the user's language ("If you approve, send only this:") followed by the command
|
|
100
102
|
on a line of its own: `/crew-approve`. With nothing after it, it approves what the gate just refused in this
|
|
101
103
|
session (a commit, a push, or both when both were refused); spelled out it is `/crew-approve commit`,
|
|
102
104
|
`/crew-approve push` or `/crew-approve commit+push`. The user approves by sending
|
|
103
105
|
that command alone; a sentence that says yes is not an approval, and you cannot run `/crew-approve` for them. It covers what is staged on that HEAD. If the gate says the approval path is closed in this session (the
|
|
104
106
|
recording hook is not wired), say so and offer Shift+Tab or their own terminal instead.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
The approval question: call AskUserQuestion with exactly the input Crewforth prints (the fixed question and its four
|
|
108
|
+
answers, no answers of your own; never write the question yourself). The
|
|
107
109
|
user's click is recorded the same way; read the line Crewforth adds and run what it names. Where a
|
|
108
110
|
`PermissionRequest` hook may be wired the gate closes the question, and in `default` / `acceptEdits` its own
|
|
109
111
|
prompt too, because such a hook can answer either before the user does: then `/crew-approve` is the way.
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -3665,6 +3665,8 @@ if [ "$_gc1" != 5 ] || [ "$_gc2" != 7 ]; then
|
|
|
3665
3665
|
else
|
|
3666
3666
|
gcj(){ printf '{"tool_name":"%s","permission_mode":"default","tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$2" "$_gcp"; }
|
|
3667
3667
|
gwj(){ printf '{"tool_name":"Write","permission_mode":"default","tool_input":{"file_path":"%s/%s","content":"x"},"cwd":"%s"}' "$_gcp" "$1" "$_gcp"; }
|
|
3668
|
+
# commit-msg 15 -> 16 (2026-10-18): one more line in trace-blocklist.txt, the session-link trailer that reached
|
|
3669
|
+
# pushed commits; each line is one grep on every commit.
|
|
3668
3670
|
GCT='guard-bash @@ 0 @@ 0 @@ Bash @@ git status --short
|
|
3669
3671
|
guard-bash @@ 0 @@ 0 @@ Bash @@ ls -la
|
|
3670
3672
|
guard-bash @@ 0 @@ 0 @@ Bash @@ git log --oneline -2 && git status --short | head; ls; cat CHANGELOG.md
|
|
@@ -3686,7 +3688,7 @@ guard-write @@ 0 @@ 0 @@ Write @@ .env
|
|
|
3686
3688
|
guard-write @@ 2 @@ 0 @@ Write @@ .claude/hooks/guard-bash.sh
|
|
3687
3689
|
guard-powershell @@ 0 @@ 0 @@ PowerShell @@ Get-ChildItem -Recurse src
|
|
3688
3690
|
pre-commit @@ 0 @@ 22 @@ Script @@ -
|
|
3689
|
-
commit-msg @@ 0 @@
|
|
3691
|
+
commit-msg @@ 0 @@ 16 @@ Script @@ msg'
|
|
3690
3692
|
_gcn=0; _gcbad=""; _gcrow=""
|
|
3691
3693
|
while IFS= read -r _gl; do [ -z "$_gl" ] && continue
|
|
3692
3694
|
_gh="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gr="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gb="${_gl%% @@ *}"; _gl="${_gl#* @@ }"; _gt="${_gl%% @@ *}"; _gc="${_gl#* @@ }"
|
|
@@ -8027,6 +8029,48 @@ _paq_pre auto '{"question":"Which colour?","header":"Colour","options":[{"label"
|
|
|
8027
8029
|
if [ "$_paqn" != 18 ]; then fail "FIXTURE: the approval-question table ran $_paqn cases, not 18"
|
|
8028
8030
|
elif [ -z "$_paqbad" ]; then pass "the approval question: asked (ask) only as the fixed text, in auto, attended, with no answers; its answer recorded only for the call that was asked, once, and only as one of the four; the record allows the commit (18 cases, 13 must-fail)"
|
|
8029
8031
|
else fail "the approval question:$_paqbad"; fi
|
|
8032
|
+
# ...and the way to it. B-2 (a real `auto` session, 2026-10-17): /crew-ship asked for /crew-approve before any commit
|
|
8033
|
+
# was refused, so the refusal that prints the question never came and the click was never offered; telling the agent
|
|
8034
|
+
# to try the commit first did not help either (§4.4 says not to, and it did not). agent-outcome.sh now hands the
|
|
8035
|
+
# question over when crew-commit-agent is called; the text pinned here only says to ask it.
|
|
8036
|
+
_cqa="$AGENTS/crew-commit-agent.md"; _cqs="$SKILLS/crew-ship/SKILL.md"
|
|
8037
|
+
if [ ! -f "$_cqa" ] || [ ! -f "$_cqs" ]; then skip scope "the way to the approval question: crew-commit-agent or crew-ship is not installed here"
|
|
8038
|
+
elif grep -q 'question (below): ask it, with the message, instead of' "$_cqa" && grep -q 'one-click approval question when crew-commit-agent is called' "$_cqs"; then
|
|
8039
|
+
pass "in auto the commit agent and /crew-ship ask the one-click question Crewforth hands over, before asking for /crew-approve"
|
|
8040
|
+
else fail "the commit agent or /crew-ship asks for /crew-approve before the one-click question Crewforth hands over (B-2)"; fi
|
|
8041
|
+
# The hand-over itself, run: agent-outcome.sh on PostToolUse of an Agent call. In `auto`, for crew-commit-agent, with
|
|
8042
|
+
# both halves of the question wired and no PermissionRequest hook, it adds the fixed question (_CREW_Q_JSON, read from
|
|
8043
|
+
# prompt-approval.sh) to the session's context; in any other mode, for any other agent, or with such a hook, nothing.
|
|
8044
|
+
_aq="$(mktemp -d)"; _aq="$(cd -P "$_aq" && pwd)"; mkdir -p "$_aq/p/.claude/state" "$_aq/ccd" "$_aq/home"
|
|
8045
|
+
printf '%s' '{"hooks":{"PreToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/guard-question.sh"}]}],"PostToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}]}}' > "$_aq/p/.claude/settings.json"
|
|
8046
|
+
_aq_run(){ printf '{"hook_event_name":"PostToolUse","tool_name":"Agent","permission_mode":"%s","session_id":"s","tool_use_id":"toolu_aq1","cwd":"%s","tool_input":{"subagent_type":"%s","description":"d","prompt":"p"},"tool_response":{"status":"async_launched","agentId":"aq123abc"}}' "$1" "$_aq/p" "$2" > "$_aq/pl.json"
|
|
8047
|
+
( cd "$_aq/p" && env -u CREW_MODEL_ROUTING CLAUDE_PROJECT_DIR="$_aq/p" CLAUDE_CONFIG_DIR="$_aq/ccd" HOME="$_aq/home" CREW_GATE_LOG=/dev/null bash "$HOOKS/agent-outcome.sh" < "$_aq/pl.json" 2>/dev/null ); }
|
|
8048
|
+
_aqbad=""; _aqo="$(_aq_run auto crew-commit-agent)"
|
|
8049
|
+
case "$_aqo" in *"$_paq_text"*) ;; *) _aqbad="$_aqbad [auto, crew-commit-agent: the question was not handed over]" ;; esac
|
|
8050
|
+
if [ -n "$JSONQ" ] && [ -n "$_aqo" ]; then printf '%s' "$_aqo" | json_ok || _aqbad="$_aqbad [its output is not JSON]"; fi
|
|
8051
|
+
for _aqc in default:crew-commit-agent dontAsk:crew-commit-agent acceptEdits:crew-commit-agent auto:crew-review-agent; do
|
|
8052
|
+
[ -z "$(_aq_run "${_aqc%%:*}" "${_aqc#*:}")" ] || _aqbad="$_aqbad [${_aqc%%:*}, ${_aqc#*:}: handed the question over]"
|
|
8053
|
+
done
|
|
8054
|
+
printf '%s' '{"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"x"}]}]}}' > "$_aq/ccd/settings.json"
|
|
8055
|
+
[ -z "$(_aq_run auto crew-commit-agent)" ] || _aqbad="$_aqbad [a PermissionRequest hook is wired: the question was still handed over]"
|
|
8056
|
+
rm -rf "$_aq"
|
|
8057
|
+
[ -z "$_aqbad" ] && pass "in auto, calling crew-commit-agent hands the session the fixed one-click question; not in default, acceptEdits or dontAsk, not for another agent, not with a PermissionRequest hook" \
|
|
8058
|
+
|| fail "the one-click question hand-over:$_aqbad"
|
|
8059
|
+
# ONE WAY TO ASK in the refusal (2026-10-18, a real `auto` session on rc.3: the refusal offered the question and the
|
|
8060
|
+
# typed command as equals, and the session told the user to type 7 times in 9). With the question open the typed way is
|
|
8061
|
+
# printed only as the fallback for a question that cannot be asked; with it closed (dontAsk), only the typed way.
|
|
8062
|
+
_pa_new
|
|
8063
|
+
( cd "$_paw" && printf '%s' '{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}],"PreToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/guard-question.sh"}]}],"PostToolUse":[{"matcher":"AskUserQuestion","hooks":[{"type":"command","command":"bash .claude/hooks/prompt-approval.sh"}]}]}}' > .claude/settings.json )
|
|
8064
|
+
_oabad=""
|
|
8065
|
+
_paq_gate auto 'git commit -m x'
|
|
8066
|
+
case "$_pae" in *'"questions":['*'do not also give the user a command to type.'*'ONLY IF THAT QUESTION CANNOT BE ASKED'*) ;; *) _oabad="$_oabad [auto, question open: not the click first and the typed way as its fallback]" ;; esac
|
|
8067
|
+
case "$_pae" in *'ASK THE USER NOW'*|*'the way below works as well'*) _oabad="$_oabad [auto, question open: the typed way is still offered as an equal]" ;; esac
|
|
8068
|
+
_paq_gate dontAsk 'git commit -m x'
|
|
8069
|
+
case "$_pae" in *'ASK THE USER NOW'*'/crew-approve'*) ;; *) _oabad="$_oabad [dontAsk: the typed way is not asked for]" ;; esac
|
|
8070
|
+
case "$_pae" in *'"questions":['*|*'ONLY IF THAT QUESTION'*) _oabad="$_oabad [dontAsk: the question is offered]" ;; esac
|
|
8071
|
+
_pa_new
|
|
8072
|
+
[ -z "$_oabad" ] && pass "a refused commit in auto offers the click and only falls back to the typed /crew-approve when the question cannot be asked; in dontAsk only the typed way" \
|
|
8073
|
+
|| fail "one way to ask:$_oabad"
|
|
8030
8074
|
|
|
8031
8075
|
# A PermissionRequest HOOK, wherever this can read one, closes the question AND the gate's own prompt. Each source is
|
|
8032
8076
|
# planted alone in an otherwise clean fixture; the clean fixture itself must answer "none" (calibration).
|
|
@@ -8484,6 +8528,27 @@ for _pr in 'Do the work.' 'Please do this:\nfiles: a\nchange: feature\nverify: t
|
|
|
8484
8528
|
done
|
|
8485
8529
|
[ -z "$_v2b" ] && pass "a crew call with no card, a card that is not at the top, a card with a line missing or one named twice is refused, with the three lines to write" \
|
|
8486
8530
|
|| fail "a task without a proper card passed:$_v2b"
|
|
8531
|
+
# The card on one line (2026-10-18: 17 of 39 card refusals on this machine were `files: … · change: … · verify: …`). It is
|
|
8532
|
+
# read by its labels and held to the same rules: what passes on three lines passes on one, and what fails there fails.
|
|
8533
|
+
_v2b=""; _v2one(){ printf '{"hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"prompt":"%s\\n\\nDo it.","subagent_type":"crew-backend-expert","model":"%s"}}' "$1" "$2" > "$_v2/one.json"
|
|
8534
|
+
( cd "$_v2p" && _v2e bash "$_V2G" < "$_v2/one.json" >"$_v2/oneout" 2>"$_v2/err" ); _rc=$?; }
|
|
8535
|
+
for _pr in 'files: src/ui/a.ts · change: feature · verify: true' 'files: src/ui/a.ts; change: feature; verify: true' 'files: src/ui/a.ts | change: feature | verify: true' \
|
|
8536
|
+
'Card: files: src/ui/a.ts, src/ui/b.ts · change: feature · verify: true.' 'Card — files: src/ui/a.ts · change: text · verify: none'; do
|
|
8537
|
+
_v2one "$_pr" sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [refused: $_pr → $_rc $(head -c 120 "$_v2/err")]"
|
|
8538
|
+
done
|
|
8539
|
+
_v2one 'files: src/payments/p.ts · change: feature · verify: true' sonnet
|
|
8540
|
+
{ [ "$_rc" = 2 ] && ! grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [a one-line card on a critical path was not read as one (rc $_rc)]"
|
|
8541
|
+
_v2one 'files: src/ui/a.ts · change: feature · verify: true · above: the parser is subtle' opus
|
|
8542
|
+
{ [ "$_rc" = 0 ] && ! grep -q 'runs on sonnet' "$_v2/oneout"; } || _v2b="$_v2b [above: on the same line was not read (rc $_rc)]"
|
|
8543
|
+
_v2one 'files: src/ui/a.ts · change: feature (a small one) · verify: true' sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [a kind with a note in parentheses, one line → $_rc]"
|
|
8544
|
+
_v2one 'files: src/ui/a.ts · change: architecture (planning only) · verify: none' sonnet
|
|
8545
|
+
{ [ "$_rc" = 2 ] && grep -q 'runs on opus' "$_v2/err"; } || _v2b="$_v2b [architecture (planning only) was not read as architecture, which runs on opus (rc $_rc)]"
|
|
8546
|
+
_v2one 'files: src/ui/a.ts\nchange: refactor (no behaviour change)\nverify: true' sonnet; [ "$_rc" = 0 ] || _v2b="$_v2b [a kind with a note in parentheses, three lines → $_rc]"
|
|
8547
|
+
for _pr in 'files: a · change: archi (planning only) · verify: none' 'files: a · change: feature extra · verify: none' 'files: a · verify: true · change: text' 'files: a · change: feature' 'Do this. files: a · change: feature · verify: true'; do
|
|
8548
|
+
_v2one "$_pr" sonnet; { [ "$_rc" = 2 ] && grep -q 'opens with its task card' "$_v2/err"; } || _v2b="$_v2b [passed: $_pr → $_rc]"
|
|
8549
|
+
done
|
|
8550
|
+
[ -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" \
|
|
8551
|
+
|| fail "the one-line card:$_v2b"
|
|
8487
8552
|
|
|
8488
8553
|
# ---- at write time: the card can leave a file out --------------------------------------------------------------
|
|
8489
8554
|
_v2new; _v2b=""
|
|
@@ -404,6 +404,24 @@ case "$EV" in
|
|
|
404
404
|
fi
|
|
405
405
|
fi
|
|
406
406
|
fi
|
|
407
|
+
# THE ONE-CLICK APPROVAL, OFFERED WHEN THE COMMIT MESSAGE IS ASKED FOR (B-2). In `auto` the click used to reach the
|
|
408
|
+
# user only inside a refused commit, and a session that asked for /crew-approve first never tried one (§4.4 says
|
|
409
|
+
# not to), so it was never offered. A call to crew-commit-agent is when the message is on its way: the session is
|
|
410
|
+
# told then, in foreground and background alike (a background launch answers here at once). Whether the question
|
|
411
|
+
# is open, and its words, come from prompt-approval.sh, as the refusal's do. Its verify is `none` (change: text),
|
|
412
|
+
# so no outcome below is lost by answering here; the model it ran on is already kept above.
|
|
413
|
+
_json_slice "$INPUT" subagent_type >/dev/null
|
|
414
|
+
if [ "$_JS" = crew-commit-agent ] && [ -f "$_ao_here/prompt-approval.sh" ]; then
|
|
415
|
+
_json_slice "$INPUT" permission_mode >/dev/null; _AOPM="$_JS"
|
|
416
|
+
if [ "$_AOPM" = auto ]; then
|
|
417
|
+
declare -F _crew_q_open >/dev/null 2>&1 || . "$_ao_here/prompt-approval.sh"
|
|
418
|
+
if declare -F _crew_q_open >/dev/null 2>&1 && _crew_q_open auto; then
|
|
419
|
+
_ao_json "Crewforth: when you show the user the commit message this agent proposes, ask with one click before anything else. $_CREW_Q_ASK {\"questions\":[$_CREW_Q_JSON]} Their answer is recorded by Crewforth itself (the line it adds says so); then run the commit it names. If that question is refused, the user can type /crew-approve instead, as the refusal says."
|
|
420
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_AOJ"
|
|
421
|
+
exit 0
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
407
425
|
R="$_AMD/results/$AID"
|
|
408
426
|
if [ ! -f "$R" ]; then
|
|
409
427
|
# No result yet: the agent was launched in the background (measured, 2.1.294 and 2.1.296: an Agent call that
|
|
@@ -431,11 +431,44 @@ _am_all_prose(){ # after _am_card -> 0 when every file of the card is prose
|
|
|
431
431
|
|
|
432
432
|
# ---- THE CARD ---------------------------------------------------------------------------------------------------
|
|
433
433
|
_AM_CHANGES=' text feature fix-known fix-unknown refactor migration security architecture test-run test-write audit '
|
|
434
|
+
_am_trim(){ # $1 -> _AMT: no white space around it, and no separator or full stop left at its end (one-line cards)
|
|
435
|
+
local v="$1" o
|
|
436
|
+
while :; do case "$v" in [$' \t']*) v="${v:1}" ;; *) break ;; esac; done
|
|
437
|
+
while :; do o="$v"
|
|
438
|
+
case "$v" in *[$' \t']) v="${v%?}" ;; *'·') v="${v%'·'}" ;; *'—') v="${v%'—'}" ;; *'–') v="${v%'–'}" ;; *[\|\;,.]) v="${v%?}" ;; esac
|
|
439
|
+
[ "$v" = "$o" ] && break
|
|
440
|
+
done
|
|
441
|
+
_AMT="$v"
|
|
442
|
+
}
|
|
443
|
+
# THE CARD ON ONE LINE. Sessions write it as `files: … · change: … · verify: …` (measured on this machine's transcripts,
|
|
444
|
+
# 2026-10-18: 17 of the 39 calls refused for their card, in 6 projects; the next call was the same card on three lines).
|
|
445
|
+
# It is read by its labels, in that order, after an optional `Card:` / `Card —`; `above: <why>` may close the line. The
|
|
446
|
+
# fields are then held to exactly the rules of the three-line card below, so nothing passes here that would not there.
|
|
447
|
+
_am_card_line(){ # $1 = the first line of the task -> 0 and CARD_* set (unchecked); 1 when it is not a one-line card
|
|
448
|
+
local l="$1" r f c v a=""
|
|
449
|
+
case "$l" in [Cc]ard*) l="${l#[Cc]ard}"; l="${l#\*\*}"
|
|
450
|
+
while :; do case "$l" in [$' \t:*']*|'—'*|'–'*|-*) case "$l" in '—'*) l="${l#'—'}" ;; '–'*) l="${l#'–'}" ;; *) l="${l:1}" ;; esac ;; *) break ;; esac; done ;;
|
|
451
|
+
esac
|
|
452
|
+
case "$l" in files:*) ;; *) return 1 ;; esac
|
|
453
|
+
r="${l#files:}"
|
|
454
|
+
case "$r" in *change:*verify:*) ;; *) return 1 ;; esac
|
|
455
|
+
f="${r%%change:*}"; r="${r#*change:}"; c="${r%%verify:*}"; v="${r#*verify:}"
|
|
456
|
+
case "$v" in *above:*) a="${v#*above:}"; v="${v%%above:*}" ;; esac
|
|
457
|
+
_am_trim "$f"; CARD_FILES="$_AMT"; _am_trim "$c"; CARD_CHANGE="$_AMT"; _am_trim "$v"; CARD_VERIFY="$_AMT"
|
|
458
|
+
[ -n "$a" ] && { _am_trim "$a"; CARD_ABOVE="${_AMT:0:200}"; }
|
|
459
|
+
return 0
|
|
460
|
+
}
|
|
434
461
|
_am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (CARD_ABOVE); 1 and CARD_WHY
|
|
435
|
-
local s="$1" l k v n=0 seen=""
|
|
462
|
+
local s="$1" l k v n=0 seen="" one=0
|
|
436
463
|
CARD_FILES=""; CARD_CHANGE=""; CARD_VERIFY=""; CARD_WHY=""; CARD_ABOVE=""
|
|
437
464
|
s="${s//$'\r'/}"
|
|
438
|
-
|
|
465
|
+
# One line first: the first line that is not empty, read by its labels; the three-line reading otherwise.
|
|
466
|
+
l="$s"; while :; do case "$l" in [$' \t\n']*) l="${l:1}" ;; *) break ;; esac; done; l="${l%%$'\n'*}"
|
|
467
|
+
if _am_card_line "$l"; then
|
|
468
|
+
one=1; n=3
|
|
469
|
+
s="${s#*"$l"}"; s="${s#$'\n'}"
|
|
470
|
+
fi
|
|
471
|
+
while [ "$one" = 0 ] && [ "$n" -lt 3 ] && [ -n "$s" ]; do
|
|
439
472
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
440
473
|
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
441
474
|
[ -n "$l" ] || continue
|
|
@@ -455,12 +488,16 @@ _am_card(){ # $1 = a task text -> 0 and CARD_FILES, CARD_CHANGE, CARD_VERIFY (C
|
|
|
455
488
|
done
|
|
456
489
|
[ "$n" = 3 ] || { CARD_WHY="the task does not begin with the three lines files:, change:, verify:"; return 1; }
|
|
457
490
|
[ -n "$CARD_FILES" ] || { CARD_WHY="files: is empty (name the files or globs, or the folder)"; return 1; }
|
|
491
|
+
# A note in parentheses after the kind is the session's own comment, as `none (…)` is for verify: `architecture
|
|
492
|
+
# (planning only)` is architecture (4 of the cards refused on this machine, 2026-10-18). Only a kind on the list, and
|
|
493
|
+
# only a note that is the whole rest of the value in parentheses.
|
|
494
|
+
case "$CARD_CHANGE" in *' ('*')') v="${CARD_CHANGE%% (*}"; case "$_AM_CHANGES" in *" $v "*) CARD_CHANGE="$v" ;; esac ;; esac
|
|
458
495
|
case "$_AM_CHANGES" in *" $CARD_CHANGE "*) ;; *) CARD_WHY="change: is '${CARD_CHANGE:0:40}', not one of:$_AM_CHANGES"; return 1 ;; esac
|
|
459
496
|
[ -n "$CARD_VERIFY" ] || { CARD_WHY="verify: is empty (a command, or none)"; return 1; }
|
|
460
497
|
case "$CARD_VERIFY" in none|None|NONE|-|'none '*|'none('*|'None '*|'NONE '*) CARD_VERIFY=none ;; esac # "none (the author ran it)" is none
|
|
461
498
|
# Optional, right after the three: `above: <why>` keeps a model above the card's floor. Without it a call above
|
|
462
499
|
# the floor is run on the floor (the cost of a model nobody needed is what this gate exists to stop).
|
|
463
|
-
while [ -n "$s" ]; do
|
|
500
|
+
while [ -z "$CARD_ABOVE" ] && [ -n "$s" ]; do
|
|
464
501
|
l="${s%%$'\n'*}"; if [ "$l" = "$s" ]; then s=""; else s="${s#*$'\n'}"; fi
|
|
465
502
|
while :; do case "$l" in [$' \t']*) l="${l:1}" ;; *) break ;; esac; done
|
|
466
503
|
[ -n "$l" ] || continue
|
package/kit/hooks/guard-bash.sh
CHANGED
|
@@ -2524,16 +2524,7 @@ _appr_wired(){ # 0 = prompt-approval.sh is beside this hook and a settings file
|
|
|
2524
2524
|
}
|
|
2525
2525
|
# IS THE APPROVAL QUESTION WIRED? The same reading as above, for the two halves on AskUserQuestion (guard-question.sh
|
|
2526
2526
|
# on PreToolUse, prompt-approval.sh on PostToolUse). Asked only to choose which way the refusal offers first.
|
|
2527
|
-
_q_wired(){
|
|
2528
|
-
local f t d="${CLAUDE_PROJECT_DIR:-.}"
|
|
2529
|
-
d="${d//\\//}"; d="${d%/}"
|
|
2530
|
-
for f in "${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT//\\//}/hooks/hooks.json}" "$d/.claude/settings.json" "$d/.claude/settings.local.json" "${HOME:+${HOME//\\//}/.claude/settings.json}"; do
|
|
2531
|
-
[ -n "$f" ] && [ -f "$f" ] || continue
|
|
2532
|
-
t=""; IFS= read -r -d '' t < "$f" || true
|
|
2533
|
-
case "$t" in *'"PreToolUse"'*AskUserQuestion*guard-question.sh*'"PostToolUse"'*AskUserQuestion*prompt-approval.sh*) return 0 ;; esac
|
|
2534
|
-
done
|
|
2535
|
-
return 1
|
|
2536
|
-
}
|
|
2527
|
+
_q_wired(){ declare -F _crew_q_wired >/dev/null 2>&1 && _crew_q_wired; } # defined once, in prompt-approval.sh
|
|
2537
2528
|
# MAY A PermissionRequest HOOK ANSWER THE PROMPT? One scan, defined in prompt-approval.sh (_crew_prh_scan) and read
|
|
2538
2529
|
# from there. Without that file there is no other approval to fall back to, so the answer is "no" and the gate
|
|
2539
2530
|
# asks as it always has.
|
|
@@ -2603,12 +2594,21 @@ _gate_written(){
|
|
|
2603
2594
|
{ printf 'sid=%s\nop=%s\nts=%s\n' "$_psid" "$_pw" "$_pn" > "$_pd/crew-approval-pending"; } 2>/dev/null || _pw=""
|
|
2604
2595
|
fi
|
|
2605
2596
|
fi
|
|
2597
|
+
# ONE WAY TO ASK. With the question open, the refusal used to print it and then the typed way as an equal choice,
|
|
2598
|
+
# and the session picked the typed one 7 times in 9 (a real `auto` session on rc.3, 2026-10-18): the user was told
|
|
2599
|
+
# to type a command for what one click does. The typed way is now the fallback, said as one.
|
|
2600
|
+
_QOFF=0
|
|
2606
2601
|
if [ "$PERM_MODE" = auto ] && [ "$_PRF" = 0 ] && _q_wired && declare -F _crew_q_pre >/dev/null 2>&1 && [ -f "${BASH_SOURCE%/*}/guard-question.sh" ]; then
|
|
2607
|
-
|
|
2602
|
+
_QOFF=1
|
|
2603
|
+
echo "$_CREW_Q_ASK" >&2
|
|
2608
2604
|
echo "{\"questions\":[$_CREW_Q_JSON]}" >&2
|
|
2609
|
-
echo "Their answer is recorded by Crewforth itself (the line it adds says so); then run the command it names.
|
|
2605
|
+
echo "Their answer is recorded by Crewforth itself (the line it adds says so); then run the command it names. Ask it this way only: do not also give the user a command to type." >&2
|
|
2606
|
+
fi
|
|
2607
|
+
if [ "$_QOFF" = 1 ]; then
|
|
2608
|
+
echo "ONLY IF THAT QUESTION CANNOT BE ASKED (the tool call is refused, or the user dismisses it), ask instead, in their language, in this shape and with nothing after it:" >&2
|
|
2609
|
+
else
|
|
2610
|
+
echo "ASK THE USER NOW, in their language, in this shape and with nothing after it (stage first: the approval covers what is staged at that moment):" >&2
|
|
2610
2611
|
fi
|
|
2611
|
-
echo "ASK THE USER NOW, in their language, in this shape and with nothing after it (stage first: the approval covers what is staged at that moment):" >&2
|
|
2612
2612
|
echo " 1. the commit message, whole, in ONE code block;" >&2
|
|
2613
2613
|
if [ -n "$_pw" ]; then
|
|
2614
2614
|
echo " 2. under it one sentence in their language (If you approve, send only this:), and then the command on a line of its own, exactly as it is:" >&2
|
|
@@ -514,6 +514,27 @@ _crew_prh_scan(){ # -> 0 when a PermissionRequest hook may be wired; _PRH names
|
|
|
514
514
|
return "$r"
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
# ---- IS THE QUESTION OPEN HERE? One answer for every hook that offers it ------------------------------------------
|
|
518
|
+
# guard-bash.sh offers it in a refusal; agent-outcome.sh offers it when crew-commit-agent is called in `auto`, so the
|
|
519
|
+
# click is on offer before any commit is tried (B-2, a real session: with the refusal as the only way to it, a session
|
|
520
|
+
# that asked for /crew-approve first never showed it). The text the user is asked stays _CREW_Q_JSON, here only.
|
|
521
|
+
_CREW_Q_HERE="${BASH_SOURCE[0]%/*}"; [ "$_CREW_Q_HERE" = "${BASH_SOURCE[0]}" ] && _CREW_Q_HERE=.
|
|
522
|
+
_crew_q_wired(){ # 0 = a settings file wires both halves on AskUserQuestion (guard-question.sh, then this file)
|
|
523
|
+
local f t d="${CLAUDE_PROJECT_DIR:-.}"
|
|
524
|
+
d="${d//\\//}"; d="${d%/}"
|
|
525
|
+
for f in "${CLAUDE_PLUGIN_ROOT:+${CLAUDE_PLUGIN_ROOT//\\//}/hooks/hooks.json}" "$d/.claude/settings.json" "$d/.claude/settings.local.json" "${HOME:+${HOME//\\//}/.claude/settings.json}"; do
|
|
526
|
+
[ -n "$f" ] && [ -f "$f" ] || continue
|
|
527
|
+
t=""; IFS= read -r -d '' t < "$f" || true
|
|
528
|
+
case "$t" in *'"PreToolUse"'*AskUserQuestion*guard-question.sh*'"PostToolUse"'*AskUserQuestion*prompt-approval.sh*) return 0 ;; esac
|
|
529
|
+
done
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
_crew_q_open(){ # $1 = permission mode -> 0 when the click may be offered: auto, both halves wired and beside this file, no PermissionRequest hook
|
|
533
|
+
[ "$1" = auto ] && [ -f "$_CREW_Q_HERE/guard-question.sh" ] && _crew_q_wired && ! _crew_prh_scan
|
|
534
|
+
}
|
|
535
|
+
# What the session is told to do, the same words wherever it is offered.
|
|
536
|
+
_CREW_Q_ASK='ASK THE USER WITH ONE CLICK (stage first: the approval covers what is staged at that moment): show the commit message whole, in ONE code block, then call AskUserQuestion with exactly this input, nothing added or changed:'
|
|
537
|
+
|
|
517
538
|
# ---- the approval question's context, and its PreToolUse half -----------------------------------------------
|
|
518
539
|
# Shared by prompt-approval.sh (PostToolUse: it records the answer) and guard-question.sh (PreToolUse: it asks). The
|
|
519
540
|
# PreToolUse half is a GATE and lives in a gate's file, so an error of its own refuses the call (CREW-FAILCLOSED)
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
Co-Authored-By
|
|
14
14
|
#test: Co-Authored-By: An Assistant <noreply@example.com>
|
|
15
|
+
# The session-link trailer Claude Code asks the model to end a commit message with: it reached six pushed commits of a
|
|
16
|
+
# project with Crewforth installed (2026-10-18), because nothing here named it. The trailer and the address are both
|
|
17
|
+
# caught, so a message that keeps only one of them is still stopped. One line, not two: each line is one grep on every
|
|
18
|
+
# commit (a process, 62-135 ms on Git Bash).
|
|
19
|
+
Claude-Session:|claude\.ai/code/session_
|
|
20
|
+
#test: Claude-Session: https://claude.ai/code/session_0123456789abcdef
|
|
21
|
+
#test: see https://claude.ai/code/session_0123456789abcdef for the run
|
|
22
|
+
#test-clean: the claude session limit is reached
|
|
23
|
+
#test-clean: https://claude.ai/code is the web app
|
|
15
24
|
# "Generated by/with" alone matched every machine-written header in existence — a pubspec.lock, an EF Core
|
|
16
25
|
# scaffold, protoc, openapi-generator. A live --dotnet install could not make its FIRST commit because the
|
|
17
26
|
# cloned base carries a Dart lockfile saying "Generated by pub". §4.1 is about AI AUTHORSHIP, so the pattern
|
|
@@ -9,7 +9,9 @@ Closure flow — in order:
|
|
|
9
9
|
1. **DoD gate:** was `/simplify` applied · are tests green (@agent-crew-test-expert) · (if SonarQube is used) `sonarqube-check` 0/0/0/0.
|
|
10
10
|
If any one is red, **STOP**, report what is missing; no deferral.
|
|
11
11
|
2. If clean, @agent-crew-commit-agent (commit-message) **proposes** an atomic Conventional Commit from the staged diff.
|
|
12
|
-
3. Commit runs ONLY if the user says "commit" (§4.4). "Done" is not approval.
|
|
12
|
+
3. Commit runs ONLY if the user says "commit" (§4.4). "Done" is not approval. In `auto`, Crewforth hands the session the
|
|
13
|
+
one-click approval question when crew-commit-agent is called: ask it with the message; the typed `/crew-approve` is
|
|
14
|
+
for when no question is offered.
|
|
13
15
|
4. **Close the board item too** (`teamboard`), when one is held. Closing work is one movement: ask what shipped,
|
|
14
16
|
run `board.sh done <id> "<note>"`, name the items it unblocked, and offer the next one as options. Do not
|
|
15
17
|
leave this to the user to remember — a claim nobody closes reads to the whole team as still in progress, and
|
package/package.json
CHANGED