crewforth 3.1.0-rc.6 → 3.1.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 +37 -1
- package/README.md +2 -1
- package/kit/agents/crew-commit-agent.md +4 -2
- package/kit/eval/smoke-test.sh +121 -14
- package/kit/hooks/agent-outcome.sh +24 -3
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-agent-model.sh +11 -4
- package/kit/hooks/guard-bash.sh +43 -7
- package/kit/hooks/guard-schedule.sh +2 -1
- package/kit/hooks/prompt-approval.sh +40 -3
- package/kit/skills/crew-approve/SKILL.md +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -122,7 +122,11 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
122
122
|
- `guard-agent-model.sh` holds the rules at the tool level and never rewrites a call; `agent-outcome.sh` runs the
|
|
123
123
|
verify command and keeps the record. Which kind of change a task is, is the caller's judgement.
|
|
124
124
|
`CREW_MODEL_ROUTING=off` turns all of it off; `fable` needs `CREW_ALLOW_FABLE=1`.
|
|
125
|
-
- **Known limits.**
|
|
125
|
+
- **Known limits.** Model routing applies to `crew-*` agents only: Explore, general-purpose and a project's own
|
|
126
|
+
agents run on the session's model and are not in the record (a kit agent for documents is planned for 3.2.0).
|
|
127
|
+
The gate holds a floor, not a ceiling: nothing stops ordinary work from being given to Opus. Not measured on
|
|
128
|
+
Windows: Studio under a path that holds a space, and stopping a session from the panel; both were measured on
|
|
129
|
+
macOS. A test runner runs code the agent wrote (test files, `package.json` scripts, `conftest.py`, a
|
|
126
130
|
`Makefile`): the list limits the command, not what the tests do. A failed card stays failed until the card
|
|
127
131
|
changes. The shell rule that protects the routing files goes by their names. The list of critical paths is
|
|
128
132
|
written at the first session, not at install; the gate reads the patterns, not the list. Managed policy is
|
|
@@ -184,6 +188,17 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
184
188
|
agent that reported before it was called, with the words `after Planner reported`; that wire is inferred from the
|
|
185
189
|
order of events and is not a call from one agent to another, and the panel says so. Read from the transcript
|
|
186
190
|
alone. Agents a workflow started are not called by the session, so they carry no place.
|
|
191
|
+
- **Studio: the Models view.** A fourth tab (key `m`), read from the record of outcomes: for each kind of task
|
|
192
|
+
(agent, change, risk) the model, the tasks, how often the first try passed, how often it was repeated one model
|
|
193
|
+
up, and an estimated cost. A class with no verified call shows `—`, not 0%. A class the record holds one model up
|
|
194
|
+
is offered for lowering once its latest ten verified calls all passed first time; the offer is one button, **Copy
|
|
195
|
+
command**, for `/crew-loosen …`. Studio writes nothing. A floor you lowered reads **lowered**, with what raised
|
|
196
|
+
it and when you lowered it. The inspector shows a task's card (files, change, verify) and its steps: each call
|
|
197
|
+
of the card, what it ran on, what its verify command said.
|
|
198
|
+
- **Studio: a session started in the panel can ask its question and have its plan approved, in the dock.** Such a
|
|
199
|
+
session had neither tool. It has both now, and the question or the plan waits in the strip where an approval
|
|
200
|
+
does; approving a plan can carry the mode the session goes on in.
|
|
201
|
+
- **Studio: Reset layout**, and **Report**: an agent's hand-back is read as its report.
|
|
187
202
|
- **Studio: which model did which work.** Every agent card carries a badge with the model it ran on, and a third
|
|
188
203
|
line with its duration, new tokens and estimated cost. Two facts are kept apart: the model the call **asked** for
|
|
189
204
|
and the model the agent **ran on**; when they differ the badge carries `≠`. The inspector has a **Model** section.
|
|
@@ -201,6 +216,11 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
201
216
|
|
|
202
217
|
### Changed
|
|
203
218
|
|
|
219
|
+
- **Studio: every agent that is not Crewforth's is a Claude Code agent.** A type the panel did not know carried a
|
|
220
|
+
`?`; there is no list to go stale now. Its tooltip says where it is defined (the project, the user's folder, a
|
|
221
|
+
plugin, or Claude Code itself). **Density: Auto** chooses what fits.
|
|
222
|
+
- **Studio: Expand all and Fold all reach every group**, the cards that stand for several agents of one type and
|
|
223
|
+
the groups inside an opened group included; what you open or fold by hand afterwards stays as you left it.
|
|
204
224
|
- **Studio: agents are named by their role.** `crew-backend-expert` reads Backend, `general-purpose` General Purpose,
|
|
205
225
|
in the graph, the Timeline, the List, the dock, the inspector and the conversation; the declared type is under the
|
|
206
226
|
name in the inspector and on hover. A task cut short on a card is said in full on hover.
|
|
@@ -214,6 +234,12 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
214
234
|
- **Studio: a group of agents opens beside itself.** Several agents of one type are one card; in a window of 1024 px
|
|
215
235
|
and up, clicking it puts its agents in the next column as ordinary cards, and opening another group folds the
|
|
216
236
|
first. Below that width it opens downward as before.
|
|
237
|
+
- **`/crew-approve`, with nothing after it, approves what was just refused.** In a trial the session said "send
|
|
238
|
+
`/crew-approve commit+push`" and stood still until someone noticed. The gate now writes down what it refused
|
|
239
|
+
(the session, commit, push or both, the moment), and the bare command approves that: the same session's, within
|
|
240
|
+
30 minutes, once. The refusal puts the command on a line of its own. What is approved is the gate's note of the
|
|
241
|
+
command it refused, never the model's choice: the note is closed to the session, and the bare command is refused
|
|
242
|
+
when scheduled or handed to another session, like the spelled-out forms, which still work.
|
|
217
243
|
- **The approval of 3.1.0's release candidates was a text, `approve: commit` (`onay: commit`); it is the command
|
|
218
244
|
`/crew-approve commit` now.** The same in every language, and recorded exactly as the text was. Only a command you
|
|
219
245
|
type counts: measured on Claude Code 2.1.284, a typed command reaches the recording hook as typed, a skill the
|
|
@@ -317,6 +343,16 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
317
343
|
|
|
318
344
|
### Fixed
|
|
319
345
|
|
|
346
|
+
- **Critical work with a verify command was refused every time where the Agent tool has no `run_in_background`.**
|
|
347
|
+
The gate asked for the field to be `false`. Where background tasks are off the tool has no such field (measured on
|
|
348
|
+
Claude Code 2.1.296: the call arrives with description, prompt, subagent_type and model, and runs in the
|
|
349
|
+
foreground), so the model could not send it and each call was refused, four times in a row in one session. Now
|
|
350
|
+
only a call that says `run_in_background: true` is refused; one that leaves the field out passes. Where the tool
|
|
351
|
+
does have the field, a call that leaves it out starts in the background: the session is then told, when the
|
|
352
|
+
launch comes back, to run the verify command itself once the agent has finished.
|
|
353
|
+
- **Studio: no spool is left behind.** A server that stopped or was killed left its sessions' settings in the temp
|
|
354
|
+
directory, and they piled up; it removes them as it stops and sweeps when it starts. Two calls in plan mode (a
|
|
355
|
+
tool lookup, the plan file) no longer come to the dock.
|
|
320
356
|
- **Studio: a card's text stays in the card.** `N in background` could push the session card's line past its edge;
|
|
321
357
|
it has a line of its own, and every text part of a card is clipped inside it, whole on hover.
|
|
322
358
|
- **Studio: the time moves in a live session.** The summary read the span of the session's own transcript, which
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center"><img src="https://raw.githubusercontent.com/Crewforth/crewforth/main/assets/logo.svg" alt="Crewforth" width="420"></p>
|
|
2
2
|
|
|
3
|
-
**Crewforth is your engineering crew for Claude Code.** It adds subagents, skills, commands and hooks to Claude Code: 12 specialist agents that each own a domain, 40 skills that hold the method,
|
|
3
|
+
**Crewforth is your engineering crew for Claude Code.** It adds subagents, skills, commands and hooks to Claude Code: 12 specialist agents that each own a domain, 40 skills that hold the method, 12 commands you start with `/crew-…`, and hooks that enforce the rules that matter.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx crewforth init # set up a new project
|
|
@@ -31,6 +31,7 @@ Then open Claude Code and run `/crew-doctor` to confirm the setup. Requires bash
|
|
|
31
31
|
- **12 specialist agents**, from `crew-planner` to `crew-commit-agent`, each owning one domain.
|
|
32
32
|
- **40 skills** holding the method: testing, migrations, API contracts, observability, accessibility, deployment.
|
|
33
33
|
- **12 commands**, each started with its `/crew-…` name: `/crew-plan`, `/crew-review`, `/crew-ship`, `/crew-handoff`, `/crew-brainstorm`, `/crew-update`, `/crew-doctor`, `/crew-gates`, `/crew-skill`, `/crew-studio`, `/crew-approve`, `/crew-loosen`.
|
|
34
|
+
- In `auto` mode a commit or a push waits for you: type `/crew-approve` and what was just refused is approved, tied to what is staged.
|
|
34
35
|
- **Safe adoption.** `adopt` lands everything on a branch, staged and uncommitted; `main` is never touched.
|
|
35
36
|
|
|
36
37
|
## Updating
|
|
@@ -96,8 +96,10 @@ write is refused because the file is on a critical path, do not reach it another
|
|
|
96
96
|
approval prompt only the user can answer — so present the message FIRST, then run the commit yourself and let the user
|
|
97
97
|
approve it at the prompt. Never hand the user a command to paste into their own terminal. In `auto` and `dontAsk`
|
|
98
98
|
the prompt reaches nobody: stage, then ask in this shape and with nothing after it — the message in ONE code block,
|
|
99
|
-
and under it one
|
|
100
|
-
|
|
99
|
+
and under it one sentence in the user's language ("If you approve, send only this:") followed by the command
|
|
100
|
+
on a line of its own: `/crew-approve`. With nothing after it, it approves what the gate just refused in this
|
|
101
|
+
session (a commit, a push, or both when both were refused); spelled out it is `/crew-approve commit`,
|
|
102
|
+
`/crew-approve push` or `/crew-approve commit+push`. The user approves by sending
|
|
101
103
|
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
|
|
102
104
|
recording hook is not wired), say so and offer Shift+Tab or their own terminal instead.
|
|
103
105
|
Then run `git commit -m '…'` alone in its call (single-quoted, or from a here-document with a quoted delimiter),
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -6951,7 +6951,7 @@ _pa_op(){ sed -n 's/^op=//p' "$_pa_rec" 2>/dev/null; }
|
|
|
6951
6951
|
if [ "$UNITS" != 1 ]; then
|
|
6952
6952
|
# The cases below feed payloads to the two hooks; like the other gate unit cases they belong to the source run.
|
|
6953
6953
|
# An installed project keeps the two pins after them: the shared definitions and the wiring.
|
|
6954
|
-
skip scope "approval by the user's own message: the unit cases run in the source checkout (scope=install)"
|
|
6954
|
+
skip scope "approval by the user's own message: the unit cases run in the source checkout (scope=install)" 49
|
|
6955
6955
|
elif [ ! -f "$HOOKS/prompt-approval.sh" ]; then
|
|
6956
6956
|
fail "hooks/prompt-approval.sh is missing — in auto and dontAsk nothing can turn the user's own yes into an approval"
|
|
6957
6957
|
else
|
|
@@ -7180,13 +7180,87 @@ done <<'PC'
|
|
|
7180
7180
|
0 @@ claude --version
|
|
7181
7181
|
0 @@ echo \"/crew-approve commit\"
|
|
7182
7182
|
0 @@ grep -rn \"approve: commit\" kit | head
|
|
7183
|
-
|
|
7183
|
+
1 @@ claude -p /crew-approve commit
|
|
7184
7184
|
PC
|
|
7185
7185
|
if [ "$_pcn" != 18 ]; then fail "FIXTURE: the claude-call table has $_pcn rows, not 18"
|
|
7186
|
-
elif [ -z "$_pcbad" ]; then pass "a command that hands a Claude Code session an approval is refused in auto and in default (
|
|
7186
|
+
elif [ -z "$_pcbad" ]; then pass "a command that hands a Claude Code session an approval is refused in auto and in default (13 forms: quoted, escaped, an option's value, a variable, a pipe, npx / bunx / node, interactive, and the unquoted one whose prompt is the bare command); an ordinary claude -p and a command without claude are not touched (5)"
|
|
7187
7187
|
else fail "claude-call table:$_pcbad"; fi
|
|
7188
7188
|
_pa_new
|
|
7189
7189
|
|
|
7190
|
+
# `/crew-approve` WITH NOTHING AFTER IT (3.1.0). In the trial the session said "send /crew-approve commit+push" and
|
|
7191
|
+
# stood still until the user noticed. Now the gate writes down what it refused (.claude/state/crew-approval-pending:
|
|
7192
|
+
# the session, the operation, the second), and the bare command approves that: the same session's, not older than
|
|
7193
|
+
# 30 minutes, once. What is approved is the gate's record of the command it refused, never the message's and never
|
|
7194
|
+
# the model's choice; every proof a spelled-out approval needs is still asked.
|
|
7195
|
+
_pa_pend="$_paw/.claude/state/crew-approval-pending"
|
|
7196
|
+
_pa_pop(){ sed -n 's/^op=//p' "$_pa_pend" 2>/dev/null; }
|
|
7197
|
+
_pbb=""
|
|
7198
|
+
# One refused commit; one refused push; a commit and then a push refused: each approved by the bare command.
|
|
7199
|
+
_pa_new; _pa_run auto 'git commit -m x'; [ "$_par/$(_pa_pop)" = "2/commit" ] || _pbb="$_pbb [a refused commit left: $_par/$(_pa_pop)]"
|
|
7200
|
+
_po="$(_pa_say auto '/crew-approve')"; case "$_po" in *'approved: commit (what this session was refused)'*'approval recorded'*) ;; *) _pbb="$_pbb [bare, commit: ${_po:0:110}]" ;; esac
|
|
7201
|
+
[ "$(_pa_op)" = commit ] || _pbb="$_pbb [the record after a bare approval of a commit: $(_pa_op)]"
|
|
7202
|
+
_pa_run auto 'git commit -m x'; { [ "$_par" = 0 ] && [ "$(gdec "$_pao")" = allow ]; } || _pbb="$_pbb the-approved-commit-is-still-refused:$_par"
|
|
7203
|
+
[ -z "$(_pa_pop)" ] || _pbb="$_pbb what-was-waiting-is-still-there-after-it-was-approved"
|
|
7204
|
+
_pa_new; ( cd "$_paw" && git commit -qm second ) >/dev/null 2>&1
|
|
7205
|
+
_pa_run auto 'git push origin feat/x'; [ "$_par/$(_pa_pop)" = "2/push" ] || _pbb="$_pbb [a refused push left: $_par/$(_pa_pop)]"
|
|
7206
|
+
_pa_say auto '/crew-approve' >/dev/null; [ "$(_pa_op)" = push ] || _pbb="$_pbb [the record after a bare approval of a push: $(_pa_op)]"
|
|
7207
|
+
_pa_run auto 'git push origin feat/x'; [ "$_par" = 0 ] || _pbb="$_pbb the-approved-push-is-still-refused:$_par"
|
|
7208
|
+
_pa_new; _pa_run auto 'git commit -m x'; _pa_run auto 'git push origin feat/x'; [ "$(_pa_pop)" = "commit+push" ] || _pbb="$_pbb [a commit then a push refused left: $(_pa_pop)]"
|
|
7209
|
+
_po="$(_pa_say dontAsk '/crewforth:crew-approve')"; case "$_po" in *'approved: commit+push'*) ;; *) _pbb="$_pbb [bare in the plugin's spelling: ${_po:0:90}]" ;; esac
|
|
7210
|
+
[ "$(_pa_op)" = "commit+push" ] || _pbb="$_pbb [the record after a bare approval of both: $(_pa_op)]"
|
|
7211
|
+
# Used once: a second bare command has nothing to approve.
|
|
7212
|
+
_po="$(_pa_say auto '/crew-approve')"; { [ -z "$(_pa_op)" ] && case "$_po" in *'approval NOT recorded'*'nothing is waiting'*) true ;; *) false ;; esac; } || _pbb="$_pbb [a second bare approval: op $(_pa_op), ${_po:0:80}]"
|
|
7213
|
+
# Nothing refused at all; refused in another session; refused more than 30 minutes ago.
|
|
7214
|
+
_pa_new; rm -f "$_pa_pend"; _po="$(_pa_say auto '/crew-approve')"; { [ ! -s "$_pa_rec" ] && case "$_po" in *'nothing is waiting'*) true ;; *) false ;; esac; } || _pbb="$_pbb approved-with-nothing-waiting"
|
|
7215
|
+
_pa_run auto 'git commit -m x' Bash other-session; _po="$(_pa_say auto '/crew-approve')"; { [ ! -s "$_pa_rec" ] && case "$_po" in *'refused in another session'*) true ;; *) false ;; esac; } || _pbb="$_pbb another-sessions-refusal-was-approved"
|
|
7216
|
+
[ "$(_pa_pop)" = commit ] || _pbb="$_pbb the-other-sessions-refusal-was-used-up"
|
|
7217
|
+
printf 'sid=s\nop=commit+push\nts=%s\n' "$(( $(date +%s) - 1900 ))" > "$_pa_pend"; _po="$(_pa_say auto '/crew-approve')"; { [ ! -s "$_pa_rec" ] && case "$_po" in *'more than 30 minutes old'*) true ;; *) false ;; esac; } || _pbb="$_pbb a-refusal-31-minutes-old-was-approved"
|
|
7218
|
+
printf 'sid=s\nop=rm -rf\nts=%s\n' "$(date +%s)" > "$_pa_pend"; _pa_say auto '/crew-approve' >/dev/null; [ ! -s "$_pa_rec" ] || _pbb="$_pbb an-operation-that-is-not-one-was-approved"
|
|
7219
|
+
# The proofs a spelled-out approval needs are asked of the bare one: the mode, an attended session, a transcript.
|
|
7220
|
+
_pa_new; _pa_run auto 'git commit -m x'
|
|
7221
|
+
for _pm in default acceptEdits plan bypassPermissions; do _pa_say "$_pm" '/crew-approve' >/dev/null; [ -s "$_pa_rec" ] && _pbb="$_pbb bare-recorded-in-$_pm"; done
|
|
7222
|
+
[ "$(_pa_pop)" = commit ] || _pbb="$_pbb a-mode-that-records-nothing-used-up-what-was-waiting"
|
|
7223
|
+
_PA_ENV="CLAUDE_CODE_SESSION_ATTENDED=0" _pa_say auto '/crew-approve' >/dev/null; [ -s "$_pa_rec" ] && _pbb="$_pbb bare-recorded-in-a-headless-session"
|
|
7224
|
+
_pa_run auto 'git commit -m x'; _PA_TR="$_PA/never-written.jsonl" _pa_say auto '/crew-approve' >/dev/null; [ -s "$_pa_rec" ] && _pbb="$_pbb bare-recorded-as-a-sessions-first-message"
|
|
7225
|
+
for _pp in '/crew-approve now' 'please /crew-approve' '/crew-approved' '<task-notification>/crew-approve' 'Another Claude session sent a message:\n/crew-approve' '/crew-approve\n/crew-approve'; do
|
|
7226
|
+
_pa_new; _pa_run auto 'git commit -m x'; _pa_say auto "$_pp" >/dev/null; [ -s "$_pa_rec" ] && _pbb="$_pbb [recorded for: $_pp]"; done
|
|
7227
|
+
[ -z "$_pbb" ] && pass "/crew-approve with nothing after it approves what the gate last refused in this session: a commit, a push, and both when both were refused (also in the plugin's spelling), once; with nothing refused, a refusal of another session, one 31 minutes old or an operation that is not one, nothing is recorded and the reason is said; it records in auto and dontAsk only, not in a headless session, not as a session's first message, and not for six messages that are not the command alone" \
|
|
7228
|
+
|| fail "bare /crew-approve:$_pbb"
|
|
7229
|
+
# The session cannot choose what the user's approval covers, nor have the bare command sent for it.
|
|
7230
|
+
_pbb=""; _pa_new; _pa_run auto 'git commit -m x'
|
|
7231
|
+
for _tk in 'Write|file_path' 'Edit|file_path' 'MultiEdit|file_path' 'NotebookEdit|notebook_path'; do
|
|
7232
|
+
printf '{"session_id":"s","cwd":"%s","permission_mode":"acceptEdits","hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"%s":"%s","content":"sid=s","old_string":"commit","new_string":"commit+push","edits":[],"new_source":"x"}}' "$_paw" "${_tk%|*}" "${_tk#*|}" "$_pa_pend" \
|
|
7233
|
+
| ( cd "$_paw" && CREW_NO_BOARD=1 CREW_GATE_LOG=/dev/null bash "$HOOKS/guard-write.sh" >/dev/null 2>&1 ); [ "$?" = 2 ] || _pbb="$_pbb [${_tk%|*} may write the pending record]"
|
|
7234
|
+
done
|
|
7235
|
+
while IFS= read -r _pl; do [ -z "$_pl" ] && continue
|
|
7236
|
+
_pw="${_pl%% @@ *}"; _pc="${_pl#* @@ }"
|
|
7237
|
+
_pa_run auto "$_pc"; [ "$_par" = "$_pw" ] || _pbb="$_pbb [$_pc → $_par, want $_pw]"
|
|
7238
|
+
done <<'PB'
|
|
7239
|
+
2 @@ printf 'sid=s\\nop=commit+push\\nts=1\\n' > .claude/state/crew-approval-pending
|
|
7240
|
+
2 @@ echo op=push >> .claude/state/crew-approval-pending
|
|
7241
|
+
2 @@ tee .claude/state/crew-approval-pending
|
|
7242
|
+
2 @@ cp /tmp/p .claude/state/crew-approval-pending
|
|
7243
|
+
2 @@ mv /tmp/p .claude/state/crew-approval-pending
|
|
7244
|
+
2 @@ sed -i s/commit/commit+push/ .claude/state/crew-approval-pending
|
|
7245
|
+
2 @@ cp /tmp/p .claude/state/crew-approval-p*
|
|
7246
|
+
2 @@ rm .claude/state/crew-approval-pending
|
|
7247
|
+
2 @@ cat .claude/state/crew-approval-pending
|
|
7248
|
+
2 @@ claude -p /crew-approve
|
|
7249
|
+
2 @@ claude -p \"/crew-approve\"
|
|
7250
|
+
2 @@ echo /crew-approve | claude -p
|
|
7251
|
+
2 @@ npx @anthropic-ai/claude-code -p /crewforth:crew-approve
|
|
7252
|
+
0 @@ grep -rn /crew-approve .claude/hooks
|
|
7253
|
+
0 @@ echo /crew-approve
|
|
7254
|
+
0 @@ claude -p \"explain what /crew-approve does\"
|
|
7255
|
+
PB
|
|
7256
|
+
[ "$(_pa_pop)" = commit ] || _pbb="$_pbb [what was waiting changed: $(_pa_pop)]"
|
|
7257
|
+
for _pt in CronCreate ScheduleWakeup; do
|
|
7258
|
+
printf '{"session_id":"s","permission_mode":"auto","hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"cron":"* * * * *","delaySeconds":60,"prompt":"/crew-approve"}}' "$_pt" | bash "$HOOKS/guard-schedule.sh" >/dev/null 2>"$_PA/ps.err"
|
|
7259
|
+
{ [ "$?" = 2 ] && grep -q 'the user can type /crew-approve when they agree' "$_PA/ps.err"; } || _pbb="$_pbb [a scheduled bare /crew-approve passed $_pt]"
|
|
7260
|
+
done
|
|
7261
|
+
[ -z "$_pbb" ] && pass "the session cannot choose or send the bare approval: Write, Edit, MultiEdit and NotebookEdit refuse the pending record; 9 shell commands that write, copy, move, remove or name it are refused; a claude call handed the bare command (4 forms) and a scheduled one (2 tools) are refused; a grep for the word, an echo of it and a prompt that mentions it are not" \
|
|
7262
|
+
|| fail "bare /crew-approve cannot be arranged by the session:$_pbb"
|
|
7263
|
+
|
|
7190
7264
|
# ---- which modes ----------------------------------------------------------------------------------------------
|
|
7191
7265
|
_pabad=""
|
|
7192
7266
|
for _pm in auto dontAsk; do : > "$_pa_rec"; _pa_say "$_pm" 'approve: commit' >/dev/null; [ "$(_pa_op)" = commit ] || _pabad="$_pabad $_pm:no-record"; done
|
|
@@ -7674,11 +7748,12 @@ _parow=""
|
|
|
7674
7748
|
_pa_want(){ # $1 = label, $2 = text that must be there, $3 = text that must not
|
|
7675
7749
|
case "$_par/$_pae" in 2/*"$2"*) case "$_pae" in *"$3"*) _parow="$_parow [$1: says '$3']" ;; esac ;; *) _parow="$_parow [$1: rc $_par, no '$2']" ;; esac; }
|
|
7676
7750
|
_pa_wire "$_PAW_ON"
|
|
7677
|
-
_pa_gate auto 'git commit -m x'; _pa_want 'wired, commit' '
|
|
7678
|
-
_pa_want 'wired, commit, one block' 'in ONE code block' '
|
|
7679
|
-
_pa_gate dontAsk 'git push origin feat/x'; _pa_want 'wired, push' '
|
|
7680
|
-
|
|
7681
|
-
|
|
7751
|
+
_pa_gate auto 'git commit -m x'; _pa_want 'wired, commit' 'approves what was just refused here: commit.' 'APPROVAL PATH IS CLOSED'
|
|
7752
|
+
_pa_want 'wired, commit, one block' 'in ONE code block' 'refused here: push'
|
|
7753
|
+
_pa_gate dontAsk 'git push origin feat/x'; _pa_want 'wired, push after the commit' 'approves what was just refused here: commit+push.' 'APPROVAL PATH IS CLOSED'
|
|
7754
|
+
grep -qx '/crew-approve' "$_PA/err" || _parow="$_parow [the command is not on a line of its own]"
|
|
7755
|
+
_pa_gate auto 'git commit -m x'; _pa_want 'wired, no text form' 'exactly as it is' 'approve: '
|
|
7756
|
+
_pa_want 'wired, no Turkish alias' 'exactly as it is' 'onay:'
|
|
7682
7757
|
rm -f "$_paw/.claude/settings.json"
|
|
7683
7758
|
_pa_gate auto 'git commit -m x'; _pa_want 'no settings' 'APPROVAL PATH IS CLOSED' 'send only this'
|
|
7684
7759
|
_pa_want 'no settings, the ways' 'Shift+Tab' 'ONE code block'
|
|
@@ -7687,7 +7762,7 @@ _pa_gate auto 'git push origin feat/x'; _pa_want 'named before the event only'
|
|
|
7687
7762
|
_pa_wire '{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"bash .claude/hooks/guard-bash.sh"}]}]}}'
|
|
7688
7763
|
_pa_gate auto 'git commit -m x'; _pa_want 'other events only' 'APPROVAL PATH IS CLOSED' 'send only this'
|
|
7689
7764
|
rm -f "$_paw/.claude/settings.json"; _pa_wire "$_PAW_ON"; mv "$_paw/.claude/settings.json" "$_paw/.claude/settings.local.json"
|
|
7690
|
-
_pa_gate auto 'git commit -m x'; _pa_want 'wired in settings.local.json' '
|
|
7765
|
+
_pa_gate auto 'git commit -m x'; _pa_want 'wired in settings.local.json' 'approves what was just refused here' 'APPROVAL PATH IS CLOSED'
|
|
7691
7766
|
rm -f "$_paw/.claude/settings.local.json"
|
|
7692
7767
|
if [ -z "$_parow" ]; then pass "the auto-mode refusal asks in one shape when the approval hook is wired, and says the path is closed when it is not (10 readings, each refused; the text form 'approve:' / 'onay:' is named in none)"
|
|
7693
7768
|
else fail "the auto-mode refusal does not say what it should:$_parow"; fi
|
|
@@ -7902,9 +7977,11 @@ sec "== 12h3) model routing v2: the card, the risk class, verify-then-escalate,
|
|
|
7902
7977
|
# three events it is wired to. Measured on Claude Code 2.1.294 before any of it was written: an agent's Edit carries
|
|
7903
7978
|
# agent_id; it is the id SubagentStart was given and the agentId PostToolUse(Agent) returns; a SubagentStop that
|
|
7904
7979
|
# answers "block" keeps the agent working; PostToolUse's additionalContext reaches the session; an Agent call that
|
|
7905
|
-
# leaves run_in_background out started in the background.
|
|
7980
|
+
# leaves run_in_background out started in the background. Measured on 2.1.296 as well: with background tasks off
|
|
7981
|
+
# (CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1) the Agent tool has no run_in_background at all, the call arrives with
|
|
7982
|
+
# description, prompt and subagent_type, and it runs in the foreground (status completed).
|
|
7906
7983
|
_V2G="$HOOKS/guard-agent-model.sh"; _V2O="$HOOKS/agent-outcome.sh"
|
|
7907
|
-
if [ "$UNITS" != 1 ]; then skip scope "model routing v2: the unit cases run in the source checkout (scope=install)"
|
|
7984
|
+
if [ "$UNITS" != 1 ]; then skip scope "model routing v2: the unit cases run in the source checkout (scope=install)" 27
|
|
7908
7985
|
elif [ ! -f "$_V2O" ] || [ ! -f "$_V2G" ]; then fail "hooks/agent-outcome.sh or guard-agent-model.sh is missing — nothing verifies an agent's work or holds the model to the risk"
|
|
7909
7986
|
else
|
|
7910
7987
|
_v2="$(mktemp -d)"; _v2="$(cd -P "$_v2" && pwd)"; _v2p="$_v2/p"
|
|
@@ -7965,8 +8042,10 @@ done <<'V2'
|
|
|
7965
8042
|
0|crew-review-agent|opus|src/payments/**|audit|none|
|
|
7966
8043
|
2|crew-security-expert|sonnet|src/ui/**|audit|none|
|
|
7967
8044
|
0|crew-security-expert|opus|src/ui/**|audit|none|
|
|
7968
|
-
|
|
8045
|
+
0|crew-backend-expert|opus|src/payments/p.ts|feature|true|
|
|
8046
|
+
0|crew-backend-expert|opus|src/payments/p.ts|feature|true|fg
|
|
7969
8047
|
2|crew-backend-expert|opus|src/payments/p.ts|feature|true|bg
|
|
8048
|
+
0|crew-backend-expert|opus|src/a.ts|security|true|
|
|
7970
8049
|
0|crew-backend-expert|opus|src/payments/p.ts|feature|none|bg
|
|
7971
8050
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|true|bg
|
|
7972
8051
|
2|crew-backend-expert|sonnet|src/ui/a.ts|tidy|true|
|
|
@@ -7976,9 +8055,37 @@ done <<'V2'
|
|
|
7976
8055
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|git commit -m x|
|
|
7977
8056
|
0|crew-backend-expert|sonnet|src/ui/a.ts|feature|rm -rf /|
|
|
7978
8057
|
V2
|
|
7979
|
-
if [ "$_v2n" !=
|
|
7980
|
-
elif [ -z "$_v2b" ]; then pass "the card decides the model: critical work (payments/, migrations, a .sql file, AuthController, userAuth, schema.prisma, and the four critical kinds of change) is refused below opus and passes on opus; verify none is refused on haiku; test-run passes on haiku, also on a critical path; test-write, audit and review are held to sonnet in normal work and to opus in critical work; critical work with a verify command is refused
|
|
8058
|
+
if [ "$_v2n" != 47 ]; then fail "FIXTURE: the card table has $_v2n rows, not 47"
|
|
8059
|
+
elif [ -z "$_v2b" ]; then pass "the card decides the model: critical work (payments/, migrations, a .sql file, AuthController, userAuth, schema.prisma, and the four critical kinds of change) is refused below opus and passes on opus; verify none is refused on haiku; test-run passes on haiku, also on a critical path; test-write, audit and review are held to sonnet in normal work and to opus in critical work; critical work with a verify command is refused when the call says run_in_background true, and passes with false and with the field left out (a session whose Agent tool has no such field cannot send it); a card that is not one is refused; a verify command never refuses the call, whatever it is (the hook decides whether to run it) (47 rows)"
|
|
7981
8060
|
else fail "card table:$_v2b"; fi
|
|
8061
|
+
# The refusal names the way forward that exists (the field was sent, so it can be sent as false), and a call that
|
|
8062
|
+
# left the field out and was launched in the background is told so when the launch comes back.
|
|
8063
|
+
_v2new; _v2b=""
|
|
8064
|
+
_v2call crew-backend-expert opus 'src/payments/p.ts' feature true ',"run_in_background":true'
|
|
8065
|
+
{ [ "$_v2rc" = 2 ] && grep -q 'is not sent to the background' "$_v2/err" && grep -q 'run_in_background set to false' "$_v2/err"; } || _v2b="$_v2b [true: rc $_v2rc, $(cut -c1-80 "$_v2/err")]"
|
|
8066
|
+
grep -q 'left out' "$_v2/err" && _v2b="$_v2b [the refusal still tells the session what happens when the field is left out]"
|
|
8067
|
+
_v2call crew-backend-expert opus 'src/payments/p.ts' feature true ',"run_in_background": true'; [ "$_v2rc" = 2 ] || _v2b="$_v2b [true after a space: rc $_v2rc]"
|
|
8068
|
+
_v2call crew-backend-expert opus 'src/payments/p.ts' feature true ',"run_in_background":"true"'; [ "$_v2rc" = 0 ] || _v2b="$_v2b [a string, which is not the boolean: rc $_v2rc]"
|
|
8069
|
+
# $1 agent id, $2 status -> what the hook prints
|
|
8070
|
+
# $3 = the call's tool_use_id (default: one no record is named after)
|
|
8071
|
+
_v2launch(){ printf '{"session_id":"s","hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"p"},"tool_response":{"status":"%s","agentId":"%s","isAsync":true},"tool_use_id":"%s"}' "$2" "$1" "${3:-tu-none}" | ( cd "$_v2p" && _v2e bash "$_V2O" 2>/dev/null ); }
|
|
8072
|
+
# The order a real session has (measured on Claude Code 2.1.296): the launch is answered BEFORE SubagentStart, so the
|
|
8073
|
+
# record is still the call's own, named after its tool_use_id.
|
|
8074
|
+
_v2new; _v2call crew-backend-expert opus 'src/payments/p.ts' feature true
|
|
8075
|
+
_tid="$(sed -n 's/.*"tool_use_id":"\([^"]*\)".*/\1/p' "$_v2/c.json")"
|
|
8076
|
+
_o="$(_v2launch la0 async_launched "$_tid")"; case "$_o" in *'launched in the background'*'(true)'*) ;; *) _v2b="$_v2b [a launch answered before the agent started is not told: ${_o:0:80}]" ;; esac
|
|
8077
|
+
_o="$(_v2launch la0 async_launched tu-of-another-call)"; [ -z "$_o" ] || _v2b="$_v2b [told from a record of another call]"
|
|
8078
|
+
_v2start la0; _v2stop la0 >/dev/null; [ -n "$(_v2row la0)" ] || _v2b="$_v2b [the launch notice used up the call's record: no line for the agent]"
|
|
8079
|
+
# ...and the other order: the agent has its record already.
|
|
8080
|
+
_v2new; _v2call crew-backend-expert opus 'src/payments/p.ts' feature true; _v2start la1
|
|
8081
|
+
_o="$(_v2launch la1 async_launched)"; case "$_o" in *'"additionalContext":"Crewforth: this critical work was launched in the background'*'run the verify command yourself with the Bash tool (true)'*) ;; *) _v2b="$_v2b [a background launch of critical work is not told: ${_o:0:80}]" ;; esac
|
|
8082
|
+
printf '%s' "$_o" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{JSON.parse(s)})' 2>/dev/null || { command -v node >/dev/null 2>&1 && _v2b="$_v2b [what it prints is not JSON]"; }
|
|
8083
|
+
_o="$(_v2launch la1 completed)"; [ -z "$_o" ] || _v2b="$_v2b [a call that is not a launch is told it was one]"
|
|
8084
|
+
_v2new; _v2call crew-backend-expert opus 'src/payments/p.ts' feature none; _v2start la2; _o="$(_v2launch la2 async_launched)"; [ -z "$_o" ] || _v2b="$_v2b [told for a card with no verify command]"
|
|
8085
|
+
_v2new; _v2call crew-backend-expert sonnet 'src/ui/a.ts' feature true; _v2start la3; _o="$(_v2launch la3 async_launched)"; [ -z "$_o" ] || _v2b="$_v2b [told for normal work]"
|
|
8086
|
+
_o="$(_v2launch nobody async_launched)"; [ -z "$_o" ] || _v2b="$_v2b [told for an agent with no record]"
|
|
8087
|
+
[ -z "$_v2b" ] && pass "run_in_background on critical work with a verify command: true is refused with the way forward (set it to false), in both spellings of the JSON; a string is not the boolean; a call that left the field out and came back as a background launch is told to run the verify command itself when the agent ends, whether the launch is answered before the agent starts (what Claude Code 2.1.296 does) or after, and the agent still gets its line; nothing is said for a finished call, a card with no verify command, normal work or an agent with no record" \
|
|
8088
|
+
|| fail "run_in_background, critical work:$_v2b"
|
|
7982
8089
|
# "In normal work an author on Opus does not pull the review up": the one deliberate change of an instruction.
|
|
7983
8090
|
_v2call crew-backend-expert opus 'src/ui/a.ts' feature true; _r1=$_v2rc; _v2call crew-review-agent sonnet 'src/ui/a.ts' audit none; _r2=$_v2rc
|
|
7984
8091
|
[ "$_r1/$_r2" = 0/0 ] && pass "normal work written on opus is reviewed on sonnet: the gate allows both calls (the instruction 'audit ≥ author' is gone; no gate held it)" \
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
# Only ever up: lowering a floor is the user's decision and nothing here does it.
|
|
25
25
|
# PostToolUse (Agent) a foreground call has come back. If its verify failed, or its report closed with
|
|
26
26
|
# confidence: low, the session is told to repeat the task once, one model up, and to ask
|
|
27
|
-
# the user if that fails too. A background call comes back before its agent has finished
|
|
28
|
-
#
|
|
27
|
+
# the user if that fails too. A background call comes back before its agent has finished:
|
|
28
|
+
# for critical work with a verify command the session is told to run that command itself
|
|
29
|
+
# when the agent ends; the record and the gate still hold.
|
|
29
30
|
# SessionStart, --scan the project's critical paths are listed in .claude/state/crew-critical-paths.auto, when the
|
|
30
31
|
# list of files has changed. A generated file: the gate does not read it, it reads the paths.
|
|
31
32
|
#
|
|
@@ -389,7 +390,27 @@ case "$EV" in
|
|
|
389
390
|
fi
|
|
390
391
|
fi
|
|
391
392
|
fi
|
|
392
|
-
R="$_AMD/results/$AID"
|
|
393
|
+
R="$_AMD/results/$AID"
|
|
394
|
+
if [ ! -f "$R" ]; then
|
|
395
|
+
# No result yet: the agent was launched in the background (measured, 2.1.294 and 2.1.296: an Agent call that
|
|
396
|
+
# leaves run_in_background out, where the tool has the field, answers status async_launched). For critical
|
|
397
|
+
# work with a verify command the session is told now, since this hook will not be asked again when it ends.
|
|
398
|
+
# Measured on 2.1.296: the answer to a launch comes BEFORE SubagentStart, so the call's record is still where the
|
|
399
|
+
# gate left it, named after the call (tool_use_id); after SubagentStart it is under the agent's id.
|
|
400
|
+
A="$_AMD/agents/$AID"
|
|
401
|
+
if [ ! -f "$A" ]; then
|
|
402
|
+
_json_slice "$INPUT" tool_use_id >/dev/null; TID="$_JS"; A=""
|
|
403
|
+
case "$TID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
404
|
+
for f in "$_AMD/pending"/*-"$TID"; do [ -f "$f" ] && A="$f"; done
|
|
405
|
+
[ -n "$A" ] || exit 0
|
|
406
|
+
fi
|
|
407
|
+
case "$INPUT" in *'"status":"async_launched"'*|*'"status": "async_launched"'*) ;; *) exit 0 ;; esac
|
|
408
|
+
_am_rec_get "$A" risk; [ "$_AMV" = critical ] || exit 0
|
|
409
|
+
_am_rec_get "$A" verify; VCMD="$_AMV"; case "$VCMD" in ''|none) exit 0 ;; esac
|
|
410
|
+
_ao_json "Crewforth: this critical work was launched in the background, so whether its verify command passed will not be told to you here. When the agent has finished, run the verify command yourself with the Bash tool ($VCMD) and judge the result before you build on the work or report it done."
|
|
411
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_AOJ"
|
|
412
|
+
exit 0
|
|
413
|
+
fi
|
|
393
414
|
_am_rec_get "$R" verify; RES="$_AMV"; _am_rec_get "$R" confidence; CONF="$_AMV"; _am_rec_get "$R" tier; TI="$_AMV"
|
|
394
415
|
_am_rec_get "$R" esc; ESC="$_AMV"; _am_rec_get "$R" cmd; VCMD="$_AMV"
|
|
395
416
|
case "$RES" in fail|blocked|timeout) ;; *) [ "$CONF" = low ] || exit 0 ;; esac
|
|
@@ -103,7 +103,7 @@ if [ -n "${IN:-}" ] && [ -n "${PSID:-}" ]; then
|
|
|
103
103
|
# In auto and dontAsk the user's own message is the approval (hooks/prompt-approval.sh); plan and
|
|
104
104
|
# bypassPermissions have no such route.
|
|
105
105
|
case "$PM" in
|
|
106
|
-
auto|dontAsk) PMHOW="stage the change, show the message in ONE code block and under it one line in the user's language, 'If you approve, send only this:
|
|
106
|
+
auto|dontAsk) PMHOW="stage the change, show the message in ONE code block and under it one line in the user's language, 'If you approve, send only this:' and then, on a line of its own, the command the gate's refusal names: /crew-approve (it approves what the gate just refused; spelled out it is /crew-approve commit, push or commit+push; the command stays as it is in every language, and only the user can type it); the user approves by sending that command alone. It covers what is staged at that moment, for 30 minutes, until their next message; never write it for them" ;;
|
|
107
107
|
*) PMHOW="a real yes still comes first, then the user switches mode (or exports CLAUDE_GIT_OK=1 in headless/CI)" ;;
|
|
108
108
|
esac
|
|
109
109
|
echo "🔒 Permission mode: $PM. git commit/push fail closed in this mode (§4.4). Only when a commit or push comes up: say so before you ask for approval; $PMHOW. Otherwise do not mention it."
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
# * a class that failed too often is held one model up (crew-model-floors.auto, written by hooks/agent-outcome.sh),
|
|
22
22
|
# until the user lowers it again with /crew-loosen (one model, once for each raise).
|
|
23
23
|
# * a card whose verify failed is not run again on the same or a lower model, and not a third time at all.
|
|
24
|
-
# * critical work with a verify command
|
|
24
|
+
# * critical work with a verify command is not sent to the background (run_in_background true), where its
|
|
25
|
+
# result does not reach the session. A call that leaves the field out passes: not every Agent tool has it.
|
|
25
26
|
# THE WORDING IS READ BY ANOTHER PROGRAM (the Studio panel): a refusal is one line that begins
|
|
26
27
|
# `GUARD (agent model):`, and a floor is said as ` runs on <model> or above`. The suite pins both.
|
|
27
28
|
#
|
|
@@ -799,9 +800,15 @@ _json_keycount "$ti" run_in_background
|
|
|
799
800
|
if [ "$_KC" -gt 1 ]; then echo "GUARD (agent model): this call names run_in_background more than once, so where it runs cannot be read and the call is refused. Send one." >&2; _am_log "an ambiguous call"; exit 2; fi
|
|
800
801
|
_json_find "$ti" '"run_in_background"'; bg=unset
|
|
801
802
|
if [ "$_KC" = 1 ] && [ "$_JF" -ge 0 ]; then f="${ti:_JF+19:12}"; f="${f//[$' \t\n\r']/}"; case "$f" in :false*) bg=false ;; :true*) bg=true ;; esac; fi
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
803
|
+
# Measured on Claude Code 2.1.294 and 2.1.296. Where the Agent tool has run_in_background, a call that leaves it out
|
|
804
|
+
# starts in the background. Where background tasks are off the tool has NO such parameter: the model cannot send it
|
|
805
|
+
# and the call runs in the foreground. The payload of the two is the same, so a call that leaves the field out is
|
|
806
|
+
# let through. (Asking for `false` refused every critical call with a verify command in a session whose tool has
|
|
807
|
+
# no such field, each time.) Only a call that SAYS background is refused; agent-outcome.sh tells the session when a
|
|
808
|
+
# call that left the field out turned out to be launched in the background.
|
|
809
|
+
if [ "$CARD_RISK" = critical ] && [ "$CARD_VERIFY" != none ] && [ "$bg" = true ]; then
|
|
810
|
+
echo "GUARD (agent model): critical work with a verify command is not sent to the background, where its result does not come back to you: repeat the same call with run_in_background set to false." >&2
|
|
811
|
+
_am_log "critical work sent to the background"; exit 2
|
|
805
812
|
fi
|
|
806
813
|
# The verify command is NOT judged here. Whether the hook runs it is decided when it would start (_am_verify_ok, in
|
|
807
814
|
# hooks/agent-outcome.sh): a command it will not run is recorded as blocked and the session is told to run it
|
package/kit/hooks/guard-bash.sh
CHANGED
|
@@ -1823,7 +1823,7 @@ _appr_named(){ # sets _APN: 1 = the record is named, 2 = the hook is fed, 3 = a
|
|
|
1823
1823
|
t="$CMD_UQ"; _APN=0
|
|
1824
1824
|
shopt -q nocasematch && nc=1; shopt -s nocasematch
|
|
1825
1825
|
case "$t" in
|
|
1826
|
-
*crewforth-appr*) _APN=1 ;;
|
|
1826
|
+
*crewforth-appr*|*crew-approval-pending*) _APN=1 ;;
|
|
1827
1827
|
esac
|
|
1828
1828
|
[ "$nc" = 0 ] && shopt -u nocasematch
|
|
1829
1829
|
# FED means the hook is what the input goes INTO: it stands after a pipe, or shares its command with an input
|
|
@@ -1857,9 +1857,13 @@ _appr_named
|
|
|
1857
1857
|
_appr_word(){ # $1 = a word of the command -> 0 when it, or what follows its first `=`, is an approval
|
|
1858
1858
|
local w="$1"
|
|
1859
1859
|
[ "${#w}" -le 160 ] || return 1
|
|
1860
|
-
_crew_appr_op "$w"; [ -
|
|
1861
|
-
|
|
1862
|
-
|
|
1860
|
+
_crew_appr_op "$w"; [ -z "$OP" ] && case "$w" in *=*) _crew_appr_op "${w#*=}" ;; esac
|
|
1861
|
+
[ -n "$OP" ] || return 1
|
|
1862
|
+
# `/crew-approve` with nothing after it is a single word, and a single word turns up where nothing is started:
|
|
1863
|
+
# `grep -rn /crew-approve .claude/hooks`. It counts only in a command one of whose words IS claude (also
|
|
1864
|
+
# claude.exe, a path that ends in it, the package's name): then it is a prompt, or about to be piped into one.
|
|
1865
|
+
if [ "$OP" = pending ] && [ "$_AWC" != 1 ]; then OP=""; return 1; fi
|
|
1866
|
+
return 0
|
|
1863
1867
|
}
|
|
1864
1868
|
_appr_in_claude_call(){ # 0 = a word of this command is an approval (OP names it)
|
|
1865
1869
|
# The command AS IT WAS SENT: the prompt of a plain `claude -p "…"` has been taken out of CMD by now (it is text for
|
|
@@ -1871,7 +1875,10 @@ _appr_in_claude_call(){ # 0 = a word of this command is an approval (OP names i
|
|
|
1871
1875
|
[ -f "$here/prompt-approval.sh" ] || return 1 # no recording hook: no approval can be recorded at all
|
|
1872
1876
|
declare -F _crew_appr_op >/dev/null 2>&1 || . "$here/prompt-approval.sh"
|
|
1873
1877
|
declare -F _crew_appr_op >/dev/null 2>&1 || return 1
|
|
1874
|
-
OP=""
|
|
1878
|
+
OP=""; _AWC=0
|
|
1879
|
+
case " ${CMD_UQ//[;|&()]/ } " in
|
|
1880
|
+
*[[:space:]]claude[[:space:]]*|*[[:space:]]claude.exe[[:space:]]*|*[[:space:]]claude.cmd[[:space:]]*|*/claude[[:space:]]*|*/claude.exe[[:space:]]*|*claude-code*) _AWC=1 ;;
|
|
1881
|
+
esac
|
|
1875
1882
|
while :; do
|
|
1876
1883
|
pre="${s%%[\"\'\\$' \t\n;|&()<>']*}"
|
|
1877
1884
|
[ -n "$pre" ] && { w="$w$pre"; inw=1; }
|
|
@@ -3254,10 +3261,39 @@ ${BRANCH_WARN}Approve only if the commit message above was shown to you and you
|
|
|
3254
3261
|
exit 2
|
|
3255
3262
|
fi
|
|
3256
3263
|
_aps=push; git_has "$CMD_SEEN" 'commit' && _aps=commit
|
|
3264
|
+
# WHAT WAS REFUSED IS WRITTEN DOWN, so that the user can approve it with `/crew-approve` and nothing after it:
|
|
3265
|
+
# the session, the operation and the second, in .claude/state/crew-approval-pending. The operation is what
|
|
3266
|
+
# THIS gate saw in the command it refused; a commit refused and then a push refused in the same session
|
|
3267
|
+
# within 30 minutes are one thing waiting, commit+push. The file is closed to the session (guard-write.sh,
|
|
3268
|
+
# and the name rule above): a session that could write it would choose what the user's approval covers.
|
|
3269
|
+
_pw=""
|
|
3270
|
+
if ! { git_has "$CMD_SEEN" 'commit' && git_has "$CMD_SEEN" 'push'; }; then
|
|
3271
|
+
_pd="${CLAUDE_PROJECT_DIR:-${_CWD:-.}}"; _pd="${_pd//\\//}"; _pd="${_pd%/}/.claude/state"
|
|
3272
|
+
_json_slice "$INPUT" session_id >/dev/null; _psid="$_JS"
|
|
3273
|
+
case "$_psid" in ''|*[!A-Za-z0-9._-]*) _psid="" ;; esac
|
|
3274
|
+
if [ -n "$_psid" ] && mkdir -p "$_pd" 2>/dev/null; then
|
|
3275
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v _pn '%(%s)T' -1; else _pn="$(date +%s)"; fi
|
|
3276
|
+
_po=""; _ps=""; _pt=""
|
|
3277
|
+
if [ -f "$_pd/crew-approval-pending" ]; then
|
|
3278
|
+
while IFS= read -r _pl || [ -n "$_pl" ]; do case "$_pl" in sid=*) _ps="${_pl#sid=}" ;; op=*) _po="${_pl#op=}" ;; ts=*) _pt="${_pl#ts=}" ;; esac; done < "$_pd/crew-approval-pending"
|
|
3279
|
+
fi
|
|
3280
|
+
_pw="$_aps"
|
|
3281
|
+
case "$_pt" in ''|*[!0-9]*) _pt=0 ;; esac
|
|
3282
|
+
if [ "$_ps" = "$_psid" ] && [ $((_pn - _pt)) -ge 0 ] && [ $((_pn - _pt)) -le 1800 ]; then
|
|
3283
|
+
case "$_po:$_aps" in commit:push|push:commit|commit+push:*) _pw="commit+push" ;; esac
|
|
3284
|
+
fi
|
|
3285
|
+
{ printf 'sid=%s\nop=%s\nts=%s\n' "$_psid" "$_pw" "$_pn" > "$_pd/crew-approval-pending"; } 2>/dev/null || _pw=""
|
|
3286
|
+
fi
|
|
3287
|
+
fi
|
|
3257
3288
|
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
|
|
3258
3289
|
echo " 1. the commit message, whole, in ONE code block;" >&2
|
|
3259
|
-
|
|
3260
|
-
|
|
3290
|
+
if [ -n "$_pw" ]; then
|
|
3291
|
+
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
|
|
3292
|
+
echo "/crew-approve"$'\n'" That line is the whole message, and only the user can type it. It approves what was just refused here: $_pw. (Spelled out, the user can type /crew-approve commit, push or commit+push instead.)" >&2
|
|
3293
|
+
else
|
|
3294
|
+
echo " 2. under it ONE line, the sentence in their language and the command as it is (only the user can type it): If you approve, send only this: /crew-approve $_aps" >&2
|
|
3295
|
+
echo " Name the one the user can type for this call: /crew-approve commit, /crew-approve push, or /crew-approve commit+push when a push follows the commit." >&2
|
|
3296
|
+
fi
|
|
3261
3297
|
echo "Their message has to be that command alone, typed by them: a sentence such as 'go ahead and commit' is not an approval, and you cannot run the command for them. It is tied to the staged tree and HEAD, lasts 30 minutes and ends at their next message." >&2
|
|
3262
3298
|
echo "Then run the command ALONE in its call (no cd, no pipe, nothing chained): git commit -m '…', or git push <remote> <branch>." >&2
|
|
3263
3299
|
echo "The other ways: Shift+Tab to default/acceptEdits (this gate then asks them directly), or the command in their own terminal." >&2
|
|
@@ -86,7 +86,8 @@ while IFS= read -r sv; do
|
|
|
86
86
|
echo "Schedule the work without it. If a floor looks too high, say so: the user can type /crew-loosen $LO_AGENT $LO_CHANGE $LO_RISK themselves." >&2
|
|
87
87
|
else
|
|
88
88
|
echo "GUARD (§4.4): this call schedules a prompt that is an approval for a commit or a push ($OP). An approval is what the user types; a prompt a tool schedules would approve in their place, so the call is refused." >&2
|
|
89
|
-
|
|
89
|
+
_so=" $OP"; [ "$OP" = pending ] && _so=""
|
|
90
|
+
echo "Schedule the work without it. For the approval itself, show the commit message and ask: the user can type /crew-approve$_so when they agree." >&2
|
|
90
91
|
fi
|
|
91
92
|
exit 2
|
|
92
93
|
fi
|
|
@@ -358,7 +358,7 @@ _crew_appr_path(){ # $1 = a directory -> _AP: the record's path in that worktre
|
|
|
358
358
|
|
|
359
359
|
# ---- WHICH TEXT IS AN APPROVAL: one function, asked here of the user's prompt and by guard-schedule.sh of a
|
|
360
360
|
# prompt a tool is about to schedule ---------------------------------------------------------------------------
|
|
361
|
-
_crew_appr_op(){ # $1 = a text, decoded -> OP: commit | push | commit+push | loosen, or "" when the text is
|
|
361
|
+
_crew_appr_op(){ # $1 = a text, decoded -> OP: commit | push | commit+push | pending | loosen, or "" when the text is none
|
|
362
362
|
# `loosen` is the other thing only the user's own message does: `/crew-loosen <agent> <change> <risk>` lowers a
|
|
363
363
|
# floor that model routing raised (3.1.0). It is matched HERE so that everything that refuses to hand a session
|
|
364
364
|
# an approval (a scheduled prompt, a `claude -p` call) refuses to hand it this as well: one question, one answer.
|
|
@@ -374,7 +374,11 @@ _crew_appr_op(){ # $1 = a text, decoded -> OP: commit | push | commit+push | lo
|
|
|
374
374
|
# THE COMMAND: `/crew-approve <what>`, or `/<plugin>:crew-approve <what>` as a plugin names its commands. Measured
|
|
375
375
|
# on Claude Code 2.1.284: a command the user types reaches UserPromptSubmit with `prompt` as typed, and a skill
|
|
376
376
|
# the model starts with the Skill tool raises PreToolUse and PostToolUse only, never that event.
|
|
377
|
-
|
|
377
|
+
# With NOTHING after it, `/crew-approve` approves what this session was last refused (3.1.0): OP is `pending`
|
|
378
|
+
# here, and the hook's own flow looks up what that is. It is an approval like the others to everything that
|
|
379
|
+
# asks this function, so a scheduled or handed-over `/crew-approve` is refused the same way.
|
|
380
|
+
/*) _k="${P%%[$' \t']*}"
|
|
381
|
+
if [ "$_k" = "$P" ]; then _k="${_k#/}"; [ "${_k##*:}" = crew-approve ] && OP=pending; return 0; fi
|
|
378
382
|
_v="${P:${#_k}}"; _vr="$_v"; _k="${_k#/}"; _k="@${_k##*:}" ;; # `@`: a command, so `crew-approve: commit` as text is not one
|
|
379
383
|
# The text form of 3.1.0's release candidates, kept: `approve: <what>` and `onay: <what>`.
|
|
380
384
|
*:*) _k="${P%%:*}"; _v="${P#*:}" ;;
|
|
@@ -483,6 +487,35 @@ if [ "$OP" = loosen ]; then
|
|
|
483
487
|
"The user lowered the calibration floor of $LO_AGENT / $LO_CHANGE / $LO_RISK from $LFROM to $LTO with their own command. The card's own rules still hold (critical work on opus, no verify command on sonnet or above, the agent's floor). If the class fails its first try too often again, the floor rises again."
|
|
484
488
|
fi
|
|
485
489
|
|
|
490
|
+
# ---- `/crew-approve` with nothing after it: what this session was last refused ---------------------------------
|
|
491
|
+
# guard-bash.sh, when it refuses a commit or a push in auto / dontAsk, writes down what it refused
|
|
492
|
+
# (.claude/state/crew-approval-pending: the session, commit | push | commit+push, the second). The bare command
|
|
493
|
+
# approves THAT and nothing else: the same session's, not older than 30 minutes. What is approved is therefore
|
|
494
|
+
# never read from the message and never from the model: the gate wrote it, from the command it refused. From here
|
|
495
|
+
# on the flow is the one a spelled-out approval takes, with every proof it asks for (the mode, a session that is
|
|
496
|
+
# attended, a transcript on disk) and the same record at the end.
|
|
497
|
+
BARE=0
|
|
498
|
+
if [ "$OP" = pending ]; then
|
|
499
|
+
BARE=1
|
|
500
|
+
_json_slice "$INPUT" permission_mode >/dev/null
|
|
501
|
+
case "$_JS" in auto|dontAsk) ;; *) exit 0 ;; esac # elsewhere the gate asks the user itself; nothing waits here
|
|
502
|
+
_json_slice "$INPUT" session_id >/dev/null; _bsid="$_JS"
|
|
503
|
+
_bp="${CLAUDE_PROJECT_DIR:-$CWD}"; _bp="${_bp//\\//}"; _bp="${_bp%/}/.claude/state/crew-approval-pending"
|
|
504
|
+
_bs=""; _bo=""; _bt=""
|
|
505
|
+
if [ -f "$_bp" ]; then
|
|
506
|
+
while IFS= read -r _bl || [ -n "$_bl" ]; do
|
|
507
|
+
case "$_bl" in sid=*) _bs="${_bl#sid=}" ;; op=*) _bo="${_bl#op=}" ;; ts=*) _bt="${_bl#ts=}" ;; esac
|
|
508
|
+
done < "$_bp"
|
|
509
|
+
fi
|
|
510
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v _bn '%(%s)T' -1; else _bn="$(date +%s)"; fi
|
|
511
|
+
OP="a commit or a push"
|
|
512
|
+
case "$_bo" in commit|push|commit+push) ;; *) no "nothing is waiting for approval in this session: no commit or push was refused. Have the commit message shown and the commit tried first; or name it: the user can type /crew-approve commit, push or commit+push." ;; esac
|
|
513
|
+
{ [ -n "$_bsid" ] && [ "$_bs" = "$_bsid" ]; } || no "what is waiting for approval was refused in another session, not this one. Nothing is approved; have it tried here first."
|
|
514
|
+
case "$_bt$_bn" in *[!0-9]*|'') no "the time of what is waiting could not be read." ;; esac
|
|
515
|
+
[ $((_bn - _bt)) -ge 0 ] && [ $((_bn - _bt)) -le 1800 ] || no "what was waiting for approval ($_bo) is more than 30 minutes old. Nothing is approved; have it tried again first."
|
|
516
|
+
OP="$_bo" # used up further down, once the approval is on record
|
|
517
|
+
fi
|
|
518
|
+
|
|
486
519
|
_json_slice "$INPUT" permission_mode >/dev/null; PM="$_JS"
|
|
487
520
|
case "$PM" in
|
|
488
521
|
auto|dontAsk) ;;
|
|
@@ -559,6 +592,9 @@ case "$TS" in ''|*[!0-9]*) no "the clock could not be read." ;; esac
|
|
|
559
592
|
|
|
560
593
|
{ printf 'v=1\nop=%s\ntree=%s\nhead=%s\nbranch=%s\nremote=%s\nurl=%s\nsid=%s\nts=%s\n' "$OP" "$TREE" "$HEAD_NOW" "$BR" "$RM" "$URL" "$SID" "$TS" > "$REC"; } 2>/dev/null \
|
|
561
594
|
|| no "the record could not be written in the git directory."
|
|
595
|
+
# What was waiting has been approved: it is not approved a second time. Only here, after every proof and the record
|
|
596
|
+
# itself: a bare command that recorded nothing (a headless session, a first message) leaves it for the user.
|
|
597
|
+
[ "$BARE" = 1 ] && { : > "$_bp"; } 2>/dev/null
|
|
562
598
|
|
|
563
599
|
_h="${HEAD_NOW:0:7}"; _d="${TREE:0:7}"
|
|
564
600
|
# THE ADDRESS IS SHOWN TO THE USER, not only compared: a rewrite set BEFORE the approval (`url.<x>.insteadOf`) is
|
|
@@ -577,5 +613,6 @@ case "$OP" in
|
|
|
577
613
|
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" ;;
|
|
578
614
|
esac
|
|
579
615
|
[ -n "$_u" ] && _u=" ($RM pushes to $_u)"
|
|
580
|
-
|
|
616
|
+
_bw=""; [ "$BARE" = 1 ] && _bw="approved: $OP (what this session was refused). "
|
|
617
|
+
say "Crewforth: ${_bw}approval recorded - $WHAT$_u. Valid for 30 minutes; your next message ends it." \
|
|
581
618
|
"The user approved this in their own message and Crewforth recorded it: $WHAT. $HOW The approval covers what was staged and where HEAD was at that moment, in this session: if either changes it no longer applies and the user approves again. It ends in 30 minutes or at the user's next message. Force-push and the rest of §4.5 are not opened by it."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crew-approve
|
|
3
3
|
description: Approve the commit or push Claude just showed you (auto and dontAsk modes).
|
|
4
|
-
argument-hint: "[commit | push | commit+push]"
|
|
4
|
+
argument-hint: "[commit | push | commit+push] (nothing: what was just refused)"
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
metadata:
|
|
7
7
|
kind: command
|
|
@@ -19,6 +19,9 @@ Read the line Crewforth added to this turn and do what it says:
|
|
|
19
19
|
`git commit -m '…'` with the message you showed (single-quoted, or from a here-document with a quoted delimiter);
|
|
20
20
|
a push is `git push <remote> <branch>`. The review record of §4.6 is still required.
|
|
21
21
|
- **"approval NOT recorded"**: tell the user the reason it gives, and run nothing.
|
|
22
|
+
- **With no argument** the command approves what the gate last refused in this session (the line says which:
|
|
23
|
+
"approved: commit+push"); when nothing was refused, or it was more than 30 minutes ago, nothing is recorded and
|
|
24
|
+
the line says so.
|
|
22
25
|
- **No such line**: the session is in a mode where the gate asks the user itself (`default`, `acceptEdits`), or
|
|
23
26
|
the argument was not `commit`, `push` or `commit+push`. Say which, and in the first case run the command and let
|
|
24
27
|
the gate's own prompt ask.
|
package/package.json
CHANGED