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 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.** The refusal now also offers a fixed question, asked with
88
- AskUserQuestion: Commit only, Commit + push, Push only, No. A new gate, `guard-question.sh`, answers `ask` on it,
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.3
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` and `dontAsk`
98
- the prompt reaches nobody: stage, then ask in this shape and with nothing after it — the message in ONE code block,
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
- In `auto` the refusal may also offer the approval question: show the message, then call AskUserQuestion with
106
- exactly the input the refusal prints (the fixed question and its four answers, no answers of your own). The
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.
@@ -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 @@ 15 @@ Script @@ msg'
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
- while [ "$n" -lt 3 ] && [ -n "$s" ]; do
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
@@ -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
- echo "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:" >&2
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. If they would rather type, the way below works as well." >&2
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewforth",
3
- "version": "3.2.0-rc.3",
3
+ "version": "3.2.0-rc.4",
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"