crewforth 3.1.0-rc.5 โ 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 +76 -12
- package/README.md +3 -2
- package/kit/AGENT_TEMPLATE.md +41 -21
- package/kit/CLAUDE.md +9 -7
- package/kit/agents/crew-backend-expert.md +4 -1
- package/kit/agents/crew-commit-agent.md +8 -3
- package/kit/agents/crew-database-expert.md +4 -1
- package/kit/agents/crew-devops-expert.md +4 -1
- package/kit/agents/crew-frontend-expert.md +4 -1
- package/kit/agents/crew-performance-expert.md +4 -1
- package/kit/agents/crew-planner.md +10 -1
- package/kit/agents/crew-privacy-agent.md +4 -1
- package/kit/agents/crew-review-agent.md +4 -1
- package/kit/agents/crew-security-expert.md +4 -1
- package/kit/agents/crew-session-manager.md +4 -1
- package/kit/agents/crew-test-expert.md +4 -1
- package/kit/eval/smoke-test.sh +860 -27
- package/kit/hooks/agent-outcome.sh +437 -0
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-agent-model.sh +494 -60
- package/kit/hooks/guard-bash.sh +69 -8
- package/kit/hooks/guard-schedule.sh +8 -2
- package/kit/hooks/guard-write.sh +31 -0
- package/kit/hooks/prompt-approval.sh +94 -7
- package/kit/settings.json +43 -0
- package/kit/skills/crew-approve/SKILL.md +4 -1
- package/kit/skills/crew-loosen/SKILL.md +28 -0
- package/kit/studio/server/hooks/studio-gate.sh +90 -4
- package/kit/studio/server/hooks/studio-host.mjs +92 -0
- package/kit/studio/server/index.js +85 -6
- package/kit/studio/server/lib/agent-origin.js +44 -0
- package/kit/studio/server/lib/graph.js +45 -7
- package/kit/studio/server/lib/model-outcomes.js +287 -0
- package/kit/studio/server/lib/palette.js +5 -18
- package/kit/studio/server/lib/permissions.js +260 -17
- package/kit/studio/server/lib/session.js +41 -1
- package/kit/studio/server/lib/usage.js +8 -1
- package/kit/studio/web/app.js +203 -32
- package/kit/studio/web/approvals.js +53 -2
- package/kit/studio/web/ask.js +137 -0
- package/kit/studio/web/canvas.js +91 -42
- package/kit/studio/web/chat.js +11 -1
- package/kit/studio/web/convo.js +9 -0
- package/kit/studio/web/dock.js +30 -9
- package/kit/studio/web/graph-plan.js +29 -9
- package/kit/studio/web/icons/builtin.svg +1 -1
- package/kit/studio/web/index.html +11 -4
- package/kit/studio/web/keys.js +29 -0
- package/kit/studio/web/list-plan.js +8 -3
- package/kit/studio/web/list.js +27 -5
- package/kit/studio/web/models-plan.js +167 -0
- package/kit/studio/web/models.js +127 -0
- package/kit/studio/web/names.js +30 -0
- package/kit/studio/web/style.css +85 -9
- package/kit/studio/web/timeline-plan.js +8 -0
- package/kit/studio/web/timeline.js +26 -7
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agent-outcome.sh - what became of a crew agent's work: verified, fixed once, recorded, and told to the session.
|
|
3
|
+
#
|
|
4
|
+
# WHY. Whether a model was enough for a piece of work cannot be known before the work; it can be measured after it.
|
|
5
|
+
# The card a crew agent is called with (hooks/guard-agent-model.sh) names a verify command. This hook runs it when
|
|
6
|
+
# the agent stops, so a cheap model that was not enough costs one more run and not broken code, and the record of
|
|
7
|
+
# what happened is what the next choice is made from.
|
|
8
|
+
#
|
|
9
|
+
# FOUR EVENTS, one script (the wiring is in settings.json / hooks.json):
|
|
10
|
+
# SubagentStart the call's record, written by the gate when it allowed the call, is matched to the agent
|
|
11
|
+
# that has just started: from here on the agent's id names its model, card and class.
|
|
12
|
+
# SubagentStop the verify command is run in the project.
|
|
13
|
+
# green -> pass.
|
|
14
|
+
# red, the first time -> the agent is kept running, once, with the failing command and
|
|
15
|
+
# the end of its output as its next instruction.
|
|
16
|
+
# red, the second time -> the agent ends; the result is fail, and the card is marked so
|
|
17
|
+
# the gate refuses it on the same or a lower model.
|
|
18
|
+
# not a command a hook may run (it chains, or the shell gate would refuse or ask)
|
|
19
|
+
# -> blocked: not run, nothing concluded.
|
|
20
|
+
# not finished in CREW_VERIFY_TIMEOUT seconds (540)
|
|
21
|
+
# -> timeout: stopped, nothing concluded.
|
|
22
|
+
# Then one line is added to .claude/state/model-outcomes.tsv, and the class is looked at:
|
|
23
|
+
# where the first try failed too often, its floor goes one model up (crew-model-floors.auto).
|
|
24
|
+
# Only ever up: lowering a floor is the user's decision and nothing here does it.
|
|
25
|
+
# PostToolUse (Agent) a foreground call has come back. If its verify failed, or its report closed with
|
|
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
|
+
# 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.
|
|
30
|
+
# SessionStart, --scan the project's critical paths are listed in .claude/state/crew-critical-paths.auto, when the
|
|
31
|
+
# list of files has changed. A generated file: the gate does not read it, it reads the paths.
|
|
32
|
+
#
|
|
33
|
+
# Measured on Claude Code 2.1.294 before this was written: an agent's Edit carries its agent_id, and it is the id
|
|
34
|
+
# SubagentStart was given and the one PostToolUse(Agent) returns as agentId; a SubagentStop that answers "block"
|
|
35
|
+
# keeps the agent working and the agent acts on the reason; PostToolUse's additionalContext reaches the session.
|
|
36
|
+
#
|
|
37
|
+
# THIS IS NOT A GATE. It cannot undo what an agent wrote, and when it fails it fails OPEN: a stop hook that refused
|
|
38
|
+
# on its own error would keep an agent running for ever. What stops broken work is the repeat one model up and the
|
|
39
|
+
# review before a commit. CREW_MODEL_ROUTING=off: nothing here does anything.
|
|
40
|
+
#
|
|
41
|
+
# THE VERIFY COMMAND comes from the card, which the session wrote, and would run here where no PreToolUse gate sees
|
|
42
|
+
# it. So it is one command with nothing chained to it, and it is handed to hooks/guard-bash.sh first, as the Bash
|
|
43
|
+
# call it would be: what that gate refuses, or would ask the user about, is not run (_am_verify_ok, one function,
|
|
44
|
+
# asked by the gate when the agent is called and here again when the command would start).
|
|
45
|
+
set -uo pipefail
|
|
46
|
+
export LC_ALL=C
|
|
47
|
+
[ "${CREW_MODEL_ROUTING:-}" = off ] && exit 0
|
|
48
|
+
_ao_here="${BASH_SOURCE%/*}"; [ "$_ao_here" = "${BASH_SOURCE}" ] && _ao_here=.
|
|
49
|
+
[ -f "$_ao_here/guard-agent-model.sh" ] || exit 0
|
|
50
|
+
. "$_ao_here/guard-agent-model.sh"
|
|
51
|
+
declare -F _am_state >/dev/null 2>&1 || exit 0
|
|
52
|
+
_am_state
|
|
53
|
+
|
|
54
|
+
# UTC, from `date -u` on every bash. The builtin `printf '%(โฆ)T'` prints LOCAL time: with a `Z` after it the record
|
|
55
|
+
# was three hours off on a machine at +0300 (measured on Windows, bash 5.3; macOS's bash 3.2 took the `date` branch
|
|
56
|
+
# and CI runs in UTC, so neither showed it). One process, on a hook that is not on any hot path.
|
|
57
|
+
_ao_now(){ _AON="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)"; case "$_AON" in [0-9][0-9][0-9][0-9]-*Z) ;; *) _AON=- ;; esac; }
|
|
58
|
+
# One writer at a time on the record: a line is added when an agent stops and a line is rewritten when its model
|
|
59
|
+
# arrives, and two hooks can run at once. The lock is a folder (made, or not) with the second it was taken inside.
|
|
60
|
+
# held now waited for, three seconds at most, then gone ahead without it: this hook does not hang an
|
|
61
|
+
# agent over a file, and the holder keeps its lock.
|
|
62
|
+
# older than 30 s nobody's: a hook that died holding it left it, and every write after it paid the three
|
|
63
|
+
# seconds. Removed and taken again.
|
|
64
|
+
# no stamp its holder died between making it and stamping it: cleared after the wait.
|
|
65
|
+
_ao_lock(){ local i=0 at now
|
|
66
|
+
_AOL="$_AMS/.outcomes.lock"
|
|
67
|
+
while [ "$i" -lt 6 ]; do
|
|
68
|
+
if mkdir "$_AOL" 2>/dev/null; then date +%s > "$_AOL/at" 2>/dev/null; return 0; fi
|
|
69
|
+
now="$(date +%s 2>/dev/null)"; at=""; [ -f "$_AOL/at" ] && IFS= read -r at < "$_AOL/at"
|
|
70
|
+
case "$at" in ''|*[!0-9]*) at="" ;; esac; case "$now" in ''|*[!0-9]*) now=0 ;; esac
|
|
71
|
+
if [ -n "$at" ] && [ $((now - at)) -gt 30 ]; then rm -rf "$_AOL" 2>/dev/null; i=$((i+1)); continue; fi
|
|
72
|
+
if [ "$i" -ge 3 ]; then
|
|
73
|
+
[ -z "$at" ] && { rm -rf "$_AOL" 2>/dev/null; i=$((i+1)); continue; }
|
|
74
|
+
break
|
|
75
|
+
fi
|
|
76
|
+
i=$((i+1)); sleep 1
|
|
77
|
+
done
|
|
78
|
+
_AOL=""; return 0; }
|
|
79
|
+
_ao_unlock(){ [ -n "${_AOL:-}" ] && rm -rf "$_AOL" 2>/dev/null; _AOL=""; return 0; }
|
|
80
|
+
_ao_json(){ # $1 = text -> _AOJ, safe inside a JSON string
|
|
81
|
+
local s="$1"
|
|
82
|
+
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//$'\n'/\\n}"; s="${s//$'\t'/ }"; s="${s//$'\r'/}"
|
|
83
|
+
_AOJ="$(printf '%s' "$s" | tr -d '\000-\010\013-\037\177')"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_ao_cut(){ # $1 = text, $2 = bytes -> _AOC: at most that many bytes, never ending inside a UTF-8 character
|
|
87
|
+
# In bytes, by tools that count bytes whatever the shell makes of the text, and with the arithmetic done here.
|
|
88
|
+
# Two earlier versions each passed on one macOS and failed on another: a shell substring counts bytes or
|
|
89
|
+
# characters depending on how the shell was started, and `iconv -c` does not drop a character cut at the end of
|
|
90
|
+
# its input on every macOS. So: `head -c` cuts; `od` shows the last four bytes; a character begun and not
|
|
91
|
+
# finished there (a lead byte with too few continuation bytes after it) is cut off as well.
|
|
92
|
+
local n="$2" hx b k=0 need=0 drop=0 total
|
|
93
|
+
total="$(printf '%s' "$1" | wc -c | tr -d ' ')"; case "$total" in ''|*[!0-9]*) total=0 ;; esac
|
|
94
|
+
if [ "$total" -le "$n" ]; then _AOC="$1"; return 0; fi
|
|
95
|
+
hx="$(printf '%s' "$1" | head -c "$n" | tail -c 4 | od -An -tx1 | tr -d ' \n')"
|
|
96
|
+
while [ -n "$hx" ]; do
|
|
97
|
+
b="${hx: -2}"; hx="${hx%??}"
|
|
98
|
+
case "$b" in
|
|
99
|
+
[89ab][0-9a-f]) k=$((k+1)); [ "$k" -ge 4 ] && { drop=$k; break; } ;; # a continuation byte
|
|
100
|
+
[cd][0-9a-f]) need=1; [ "$k" -lt "$need" ] && drop=$((k+1)); break ;;
|
|
101
|
+
e[0-9a-f]) need=2; [ "$k" -lt "$need" ] && drop=$((k+1)); break ;;
|
|
102
|
+
f[0-9a-f]) need=3; [ "$k" -lt "$need" ] && drop=$((k+1)); break ;;
|
|
103
|
+
*) break ;; # ASCII: nothing is open
|
|
104
|
+
esac
|
|
105
|
+
done
|
|
106
|
+
_AOC="$(printf '%s' "$1" | head -c "$((n - drop))")"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ---- the list of critical paths (SessionStart, or `--scan` from the installers) ----------------------------------
|
|
110
|
+
_ao_scan(){
|
|
111
|
+
local out="$_AMS/crew-critical-paths.auto" stamp="$_AMS/crew-critical-paths.stamp" list sum old="" f rest seg pre n=0
|
|
112
|
+
[ -d "$_AMP/.claude" ] || return 0
|
|
113
|
+
if git -C "$_AMP" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
114
|
+
list="$(git -C "$_AMP" ls-files -co --exclude-standard 2>/dev/null)" || return 0
|
|
115
|
+
else
|
|
116
|
+
list="$(cd "$_AMP" 2>/dev/null && find . \( -name .git -o -name node_modules -o -name .claude \) -prune -o -type f -print 2>/dev/null | sed 's|^\./||')" || return 0
|
|
117
|
+
fi
|
|
118
|
+
sum="$(printf '%s\n' "$list" | cat - "$_AMP/.claude/crew-model-rules" 2>/dev/null | cksum)"; sum="${sum%% *}"
|
|
119
|
+
[ -f "$stamp" ] && IFS= read -r old < "$stamp"
|
|
120
|
+
[ "$1" != force ] && [ "$old" = "$sum" ] && [ -f "$out" ] && return 0
|
|
121
|
+
mkdir -p "$_AMS" 2>/dev/null || return 0
|
|
122
|
+
{
|
|
123
|
+
echo "# Generated by Crewforth (hooks/agent-outcome.sh). Do not edit: it is written again when the files change."
|
|
124
|
+
echo "# The paths of this project that are critical for model routing: work that touches one runs on opus."
|
|
125
|
+
echo "# To add or remove a path, write .claude/crew-model-rules ('+ <glob>' adds, '- <glob>' removes)."
|
|
126
|
+
while IFS= read -r f; do
|
|
127
|
+
[ -n "$f" ] || continue
|
|
128
|
+
case "$f" in .claude/*|.git/*) continue ;; esac # Crewforth's own files and git's are not the project's work
|
|
129
|
+
n=$((n+1)); [ "$n" -le 20000 ] || { echo "# stopped after 20000 files: the rest was not read"; break; }
|
|
130
|
+
_am_crit_path "$f" || continue
|
|
131
|
+
# The shortest leading part of the path that is critical by itself: a folder is listed once, not per file.
|
|
132
|
+
pre=""; rest="$f"
|
|
133
|
+
while :; do
|
|
134
|
+
seg="${rest%%/*}"; pre="${pre:+$pre/}$seg"
|
|
135
|
+
[ "$seg" = "$rest" ] && break
|
|
136
|
+
rest="${rest#*/}"
|
|
137
|
+
if _am_crit_path "$pre"; then pre="$pre/"; break; fi
|
|
138
|
+
done
|
|
139
|
+
printf '%s\n' "$pre"
|
|
140
|
+
done <<< "$list" | sort -u
|
|
141
|
+
} > "$out.tmp" 2>/dev/null && mv "$out.tmp" "$out" 2>/dev/null && printf '%s\n' "$sum" > "$stamp" 2>/dev/null
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
case "${1:-}" in --scan) _ao_scan force; exit 0 ;; esac
|
|
145
|
+
|
|
146
|
+
IFS= read -r -d '' INPUT || true
|
|
147
|
+
_json_slice "$INPUT" hook_event_name >/dev/null; EV="$_JS"
|
|
148
|
+
|
|
149
|
+
# ---- the class's floor, after a result (tightening only) --------------------------------------------------------
|
|
150
|
+
# A class is agent x change x risk. Where at least CREW_MODEL_CAL_N (5) of its calls on one model had a verify
|
|
151
|
+
# command, and more than CREW_MODEL_CAL_PCT (20) percent of them did not pass on the first try, the class is held
|
|
152
|
+
# one model up. Both numbers are a starting point that has not been measured.
|
|
153
|
+
_ao_calibrate(){ # $1 agent, $2 change, $3 risk
|
|
154
|
+
local tsv="$_AMS/model-outcomes.tsv" fl="$_AMS/crew-model-floors.auto" want a b c t rest keep="" since=""
|
|
155
|
+
[ -f "$tsv" ] || return 0
|
|
156
|
+
# After the user lowered this class's floor its outcomes are counted from that moment: the failures that raised
|
|
157
|
+
# it are not counted a second time, and new ones raise it again.
|
|
158
|
+
_am_floor_now "$1" "$2" "$3"
|
|
159
|
+
[ "$_AMF_LOOSE" = 1 ] && since="$_AMF_LTS"
|
|
160
|
+
want="$(awk -F'\t' -v A="$1" -v C="$2" -v R="$3" -v S="$since" -v N="${CREW_MODEL_CAL_N:-5}" -v P="${CREW_MODEL_CAL_PCT:-20}" '
|
|
161
|
+
NR > 1 && $3 == A && $4 == C && $5 == R && ($8 == "pass" || $8 == "fail") && (S == "" || $1 > S) { n[$7]++; if ($8 == "fail" || $9 + 0 > 0) bad[$7]++ }
|
|
162
|
+
END { r["haiku"] = 1; r["sonnet"] = 2; r["opus"] = 3; nm[2] = "sonnet"; nm[3] = "opus"; best = 0
|
|
163
|
+
for (m in n) if (r[m] > 0 && r[m] < 3 && n[m] >= N && bad[m] * 100 > P * n[m] && r[m] + 1 > best) { best = r[m] + 1; bn = n[m]; bb = bad[m] + 0 }
|
|
164
|
+
if (best) printf "%s\t%d\t%d", nm[best], bn, bb }' "$tsv" 2>/dev/null)" || return 0
|
|
165
|
+
[ -n "$want" ] || return 0
|
|
166
|
+
# Higher than the floor that HOLDS now (the lowered one, where the user lowered it): then it is raised, with a
|
|
167
|
+
# new time, which is what puts a lowering behind it.
|
|
168
|
+
_am_rank "$_AMF_EFF"; a="$_AMR"; _am_rank "${want%%$'\t'*}"
|
|
169
|
+
[ "$_AMR" -gt "$a" ] || return 0
|
|
170
|
+
if [ -f "$fl" ]; then
|
|
171
|
+
while IFS=$'\t' read -r a b c t rest || [ -n "$a" ]; do
|
|
172
|
+
case "$a" in ''|'#'*) continue ;; esac
|
|
173
|
+
[ "$a" = "$1" ] && [ "$b" = "$2" ] && [ "$c" = "$3" ] && continue
|
|
174
|
+
keep="$keep$a"$'\t'"$b"$'\t'"$c"$'\t'"$t"$'\t'"$rest"$'\n'
|
|
175
|
+
done < "$fl"
|
|
176
|
+
fi
|
|
177
|
+
_ao_now
|
|
178
|
+
# A raise that follows a lowering is stamped LATER than it, so the lowering is behind it: within the same second
|
|
179
|
+
# the two could not be told apart.
|
|
180
|
+
if [ "$_AMF_LOOSE" = 1 ] && [[ ! "$_AON" > "$_AMF_LTS" ]]; then sleep 1; _ao_now; fi
|
|
181
|
+
{ echo "# Generated by Crewforth (hooks/agent-outcome.sh) from model-outcomes.tsv. A class held above its usual model"
|
|
182
|
+
echo "# because its first try failed too often one model down. agent, change, risk, model, calls, not-first-try, when."
|
|
183
|
+
echo "# Nothing lowers a line here but the user: the user can type /crew-loosen <agent> <change> <risk> (one model, once for each raise)."
|
|
184
|
+
printf '%s' "$keep"
|
|
185
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$want" "$_AON"; } > "$fl.tmp" 2>/dev/null && mv "$fl.tmp" "$fl" 2>/dev/null
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "$EV" in
|
|
190
|
+
SessionStart) _ao_scan auto; exit 0 ;;
|
|
191
|
+
|
|
192
|
+
SubagentStart)
|
|
193
|
+
_json_slice "$INPUT" agent_id >/dev/null; AID="$_JS"
|
|
194
|
+
_json_slice "$INPUT" agent_type >/dev/null; AT="${_JS##*:}"
|
|
195
|
+
case "$AID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
196
|
+
case "$AT" in crew-*) ;; *) exit 0 ;; esac
|
|
197
|
+
[ -d "$_AMD/pending" ] || exit 0
|
|
198
|
+
# The oldest call of this agent type that no agent has taken yet. Two calls of one type made together can start
|
|
199
|
+
# in either order; where they ask for different models the agent is recorded on the LOWER one, so the check at
|
|
200
|
+
# write time errs towards refusing.
|
|
201
|
+
# A call the gate allowed and nothing started (another hook refused it, the user did) leaves its record behind.
|
|
202
|
+
# An agent starts within moments of its call, so a record older than five minutes is nobody's and is dropped.
|
|
203
|
+
low=9; cands=""; if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOWS '%(%s)T' -1; else NOWS="$(date +%s)"; fi
|
|
204
|
+
for f in "$_AMD/pending"/*; do
|
|
205
|
+
[ -f "$f" ] || continue
|
|
206
|
+
t="${f##*/}"; t="${t%%-*}"; case "$t" in ''|*[!0-9]*) ;; *) if [ $((NOWS - t)) -gt 300 ]; then rm -f "$f" 2>/dev/null; continue; fi ;; esac
|
|
207
|
+
_am_rec_get "$f" agent || continue; [ "$_AMV" = "$AT" ] || continue
|
|
208
|
+
cands="$cands$f"$'\n'
|
|
209
|
+
_am_rec_get "$f" tier && { _am_rank "$_AMV"; [ "$_AMR" -lt "$low" ] && low="$_AMR"; }
|
|
210
|
+
done
|
|
211
|
+
[ -n "$cands" ] || exit 0
|
|
212
|
+
mkdir -p "$_AMD/agents" 2>/dev/null || exit 0
|
|
213
|
+
# TAKEN BY RENAME. Two agents of one type start at the same moment, each in its own hook process, and both see
|
|
214
|
+
# the same oldest record. A rename either happens or fails, so exactly one of them gets it; the other goes on
|
|
215
|
+
# to the next record. (Reading it and deleting it afterwards gave both agents the same record.)
|
|
216
|
+
claim="$_AMD/agents/$AID.claim"; got=0
|
|
217
|
+
while IFS= read -r f; do
|
|
218
|
+
[ -n "$f" ] || continue
|
|
219
|
+
if mv "$f" "$claim" 2>/dev/null; then got=1; break; fi
|
|
220
|
+
done <<< "$cands"
|
|
221
|
+
[ "$got" = 1 ] || exit 0
|
|
222
|
+
_am_rec_get "$claim" tier; _am_rank "$_AMV"
|
|
223
|
+
if [ "$low" -lt "$_AMR" ]; then _am_name "$low"
|
|
224
|
+
{ grep -v '^tier=' "$claim"; printf 'asked=%s\ntier=%s\nambiguous=1\n' "$_AMV" "$_AMN"; } > "$_AMD/agents/$AID" 2>/dev/null
|
|
225
|
+
else { cat "$claim"; printf 'asked=%s\n' "$_AMV"; } > "$_AMD/agents/$AID" 2>/dev/null; fi
|
|
226
|
+
rm -f "$claim" 2>/dev/null
|
|
227
|
+
exit 0 ;;
|
|
228
|
+
|
|
229
|
+
SubagentStop)
|
|
230
|
+
_json_slice "$INPUT" agent_id >/dev/null; AID="$_JS"
|
|
231
|
+
case "$AID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
232
|
+
REC="$_AMD/agents/$AID"
|
|
233
|
+
[ -f "$REC" ] || exit 0
|
|
234
|
+
[ -f "$_AMD/results/$AID" ] && exit 0 # an agent that is resumed stops again: its work was judged once
|
|
235
|
+
# WHOSE RECORD IS THIS? The record was matched to the agent by type and order, which two calls made together
|
|
236
|
+
# can get wrong. The card itself is in the agent's own transcript: the task it was given opens with it. So the
|
|
237
|
+
# card is read from there, and the record that is used is the one whose card it is: this agent's, or, where the
|
|
238
|
+
# two were swapped, the one filed under the other agent (left in place for that agent to find its own the same
|
|
239
|
+
# way). No transcript to read: the record is used as it was matched. A card no record carries: nothing is
|
|
240
|
+
# judged, because the command that would run is not one the gate saw for this agent.
|
|
241
|
+
_json_slice "$INPUT" agent_transcript_path >/dev/null; _json_unescape "$_JS" >/dev/null; ATP="${_JU//\\//}"
|
|
242
|
+
if [ -f "$ATP" ]; then
|
|
243
|
+
L1=""; IFS= read -r L1 < "$ATP" || true
|
|
244
|
+
# The task is the first record's content: a string (measured, Claude Code 2.1.294), or, where it is a list of
|
|
245
|
+
# blocks, the first block's text.
|
|
246
|
+
_json_slice "$L1" content >/dev/null; TXT="$_JS"
|
|
247
|
+
[ -n "$TXT" ] || { _json_slice "$L1" text >/dev/null; TXT="$_JS"; }
|
|
248
|
+
TXT="${TXT:0:6000}"; CARDOK=0
|
|
249
|
+
if [ -n "$TXT" ]; then _json_unescape "$TXT" >/dev/null; _am_card "$_JU" && CARDOK=1; fi
|
|
250
|
+
if [ "$CARDOK" = 0 ]; then
|
|
251
|
+
# Nothing to check the record against. Said in the gate log, and the record is used as it was matched.
|
|
252
|
+
_am_log "note: the card could not be read from the agent's transcript; its record was used as matched"
|
|
253
|
+
else
|
|
254
|
+
_am_digest
|
|
255
|
+
_am_rec_get "$REC" card
|
|
256
|
+
if [ "$_AMV" != "$CARD_ID" ]; then
|
|
257
|
+
_am_rec_get "$REC" agent; MYT="$_AMV"; FOUND=""
|
|
258
|
+
# First a call nobody has taken yet, then the newest record that has not judged anyone. (A record is
|
|
259
|
+
# used up by the agent it judges, which is not always the agent it is filed under: that is the swap.)
|
|
260
|
+
for f in "$_AMD/pending"/*; do
|
|
261
|
+
[ -f "$f" ] || continue
|
|
262
|
+
_am_rec_get "$f" card && [ "$_AMV" = "$CARD_ID" ] || continue
|
|
263
|
+
_am_rec_get "$f" agent && [ "$_AMV" = "$MYT" ] || continue
|
|
264
|
+
FOUND="$f"; break
|
|
265
|
+
done
|
|
266
|
+
if [ -z "$FOUND" ]; then
|
|
267
|
+
while IFS= read -r f; do
|
|
268
|
+
[ -n "$f" ] && [ -f "$f" ] || continue; case "$f" in *.claim|*.out|*.used) continue ;; esac
|
|
269
|
+
[ -f "$f.used" ] && continue # that record has already judged an agent
|
|
270
|
+
_am_rec_get "$f" card && [ "$_AMV" = "$CARD_ID" ] || continue
|
|
271
|
+
_am_rec_get "$f" agent && [ "$_AMV" = "$MYT" ] || continue
|
|
272
|
+
FOUND="$f"; break
|
|
273
|
+
done <<< "$(ls -t "$_AMD/agents"/* 2>/dev/null)"
|
|
274
|
+
fi
|
|
275
|
+
if [ -z "$FOUND" ]; then _am_log "note: no record carries the card in the agent's transcript; the agent was not judged"; exit 0; fi
|
|
276
|
+
REC="$FOUND"
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
279
|
+
fi
|
|
280
|
+
_am_rec_get "$REC" verify; VCMD="$_AMV"
|
|
281
|
+
_am_rec_get "$REC" agent; AG="$_AMV"; _am_rec_get "$REC" change; CH="$_AMV"; _am_rec_get "$REC" risk; RK="$_AMV"
|
|
282
|
+
_am_rec_get "$REC" card; CID="$_AMV"; _am_rec_get "$REC" esc; ESC="${_AMV:--}"
|
|
283
|
+
_am_rec_get "$REC" asked || _am_rec_get "$REC" tier; ASK="$_AMV"
|
|
284
|
+
mkdir -p "$_AMD/results" "$_AMD/tries" "$_AMD/fails" 2>/dev/null || exit 0
|
|
285
|
+
TRIES=0; [ -f "$_AMD/tries/$AID" ] && IFS= read -r TRIES < "$_AMD/tries/$AID"; case "$TRIES" in ''|*[!0-9]*) TRIES=0 ;; esac
|
|
286
|
+
SHA=0; case "$INPUT" in *'"stop_hook_active":true'*|*'"stop_hook_active": true'*) SHA=1 ;; esac
|
|
287
|
+
RES=none; WHYNOT=""
|
|
288
|
+
if [ -n "$VCMD" ] && [ "$VCMD" != none ]; then
|
|
289
|
+
_json_slice "$INPUT" permission_mode >/dev/null; PM="$_JS"
|
|
290
|
+
if ! _am_verify_ok "$VCMD" "$PM"; then
|
|
291
|
+
# Judged again here, at the moment it would run: the gate judged it when the agent was called, and this
|
|
292
|
+
# is the place the command actually starts. Not run; nothing is concluded about the work.
|
|
293
|
+
RES=blocked; WHYNOT="$_AMVW"; VRC=0
|
|
294
|
+
else
|
|
295
|
+
# A time limit of its own, below the hook's: a hook killed at its timeout records nothing, and a verify
|
|
296
|
+
# that hangs must not read as a pass or as a fail.
|
|
297
|
+
# The limit stays under the hook's own 600 s whatever is asked for.
|
|
298
|
+
LIM="${CREW_VERIFY_TIMEOUT:-540}"; case "$LIM" in ''|*[!0-9]*) LIM=540 ;; esac; [ "$LIM" -le 560 ] || LIM=560
|
|
299
|
+
VF="$_AMD/tries/$AID.out"
|
|
300
|
+
# IN A PROCESS GROUP OF ITS OWN, so that stopping it stops what it started: a test runner's workers, a
|
|
301
|
+
# server a test left behind. `setsid` where there is one (CREW_VERIFY_NO_SETSID=1 skips it); otherwise job
|
|
302
|
+
# control, which gives a background job its own group in bash (macOS ships no setsid). On Windows the tree
|
|
303
|
+
# is ended by its Windows process id.
|
|
304
|
+
if [ -z "${CREW_VERIFY_NO_SETSID:-}" ] && command -v setsid >/dev/null 2>&1; then
|
|
305
|
+
( cd "$_AMP" 2>/dev/null && exec setsid bash -c "$VCMD" ) > "$VF" 2>&1 </dev/null &
|
|
306
|
+
VPID=$!
|
|
307
|
+
else
|
|
308
|
+
set -m
|
|
309
|
+
( cd "$_AMP" 2>/dev/null && exec bash -c "$VCMD" ) > "$VF" 2>&1 </dev/null &
|
|
310
|
+
VPID=$!
|
|
311
|
+
set +m
|
|
312
|
+
fi
|
|
313
|
+
WAITED=0; VRC=""
|
|
314
|
+
while kill -0 "$VPID" 2>/dev/null; do
|
|
315
|
+
if [ "$WAITED" -ge "$LIM" ]; then
|
|
316
|
+
case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*)
|
|
317
|
+
WP=""; [ -r "/proc/$VPID/winpid" ] && IFS= read -r WP < "/proc/$VPID/winpid"
|
|
318
|
+
case "$WP" in ''|*[!0-9]*) ;; *) taskkill //T //F //PID "$WP" >/dev/null 2>&1 ;; esac ;;
|
|
319
|
+
esac
|
|
320
|
+
kill -TERM -- "-$VPID" 2>/dev/null; kill -TERM "$VPID" 2>/dev/null
|
|
321
|
+
W2=0; while kill -0 "$VPID" 2>/dev/null && [ "$W2" -lt 3 ]; do sleep 1; W2=$((W2+1)); done
|
|
322
|
+
kill -KILL -- "-$VPID" 2>/dev/null; kill -KILL "$VPID" 2>/dev/null
|
|
323
|
+
VRC=timeout; break
|
|
324
|
+
fi
|
|
325
|
+
sleep 1; WAITED=$((WAITED+1))
|
|
326
|
+
done
|
|
327
|
+
if [ "$VRC" = timeout ]; then wait "$VPID" 2>/dev/null; RES=timeout; WHYNOT="it did not finish in ${LIM}s"; VRC=0
|
|
328
|
+
else wait "$VPID" 2>/dev/null; VRC=$?; fi
|
|
329
|
+
VOUT="$(tail -n 15 "$VF" 2>/dev/null)"; rm -f "$VF" 2>/dev/null
|
|
330
|
+
fi
|
|
331
|
+
if [ "$RES" = blocked ] || [ "$RES" = timeout ]; then :
|
|
332
|
+
elif [ "$VRC" = 0 ]; then RES=pass
|
|
333
|
+
elif [ "$TRIES" = 0 ] && [ "$SHA" = 0 ] && { printf '1\n' > "$_AMD/tries/$AID"; } 2>/dev/null; then
|
|
334
|
+
# Asked ONCE, and only where that can be counted: Claude Code says when a stop hook has already kept this
|
|
335
|
+
# agent running (stop_hook_active), and the counter has to be on disk before the answer goes out. Either
|
|
336
|
+
# one missing, the agent is not kept: a stop hook that blocks without counting blocks for ever.
|
|
337
|
+
TAILV="$(printf '%s\n' "$VOUT" | tail -n 15)"; _ao_cut "$TAILV" 1500; TAILV="$_AOC"
|
|
338
|
+
_ao_json "The verify command of your task failed (exit $VRC): $VCMD"$'\n'"The end of its output:"$'\n'"$TAILV"$'\n'"Fix the work so that the command passes, then finish. This is asked once: if it fails again your report is taken as it is."
|
|
339
|
+
printf '{"decision":"block","reason":"%s"}\n' "$_AOJ"
|
|
340
|
+
exit 0
|
|
341
|
+
else RES=fail; fi
|
|
342
|
+
fi
|
|
343
|
+
# The report's last line, and the model the agent's own transcript names.
|
|
344
|
+
_json_slice "$INPUT" last_assistant_message >/dev/null; LAST="$_JS"; [ "${#LAST}" -gt 400 ] && LAST="${LAST:${#LAST}-400}"
|
|
345
|
+
_json_unescape "$LAST" >/dev/null; LAST="$_JU"
|
|
346
|
+
while :; do case "$LAST" in *[$' \t\n\r']) LAST="${LAST%?}" ;; *) break ;; esac; done
|
|
347
|
+
ESCL=0; case $'\n'"$LAST" in *$'\n'"escalate:"*) ESCL=1 ;; esac # a write to a critical path was refused
|
|
348
|
+
LAST="${LAST##*$'\n'}"
|
|
349
|
+
case "$LAST" in "confidence: high") CONF=high ;; "confidence: low") CONF=low ;; *) CONF=- ;; esac
|
|
350
|
+
# The model the agent ran on. Claude Code names it in the answer to the Agent call (resolvedModel), which the
|
|
351
|
+
# PostToolUse branch below keeps under the agent's id: for a background call that answer comes when the agent is
|
|
352
|
+
# launched, so it is here already; for a foreground call it comes after this, and that branch then writes it
|
|
353
|
+
# into the line this one adds. Failing that, the first model the agent's own transcript names; failing that, `-`.
|
|
354
|
+
RAN=-
|
|
355
|
+
if [ -s "$_AMD/ran/$AID" ]; then IFS= read -r RAN < "$_AMD/ran/$AID" || true
|
|
356
|
+
elif [ -f "$ATP" ]; then RAN="$(grep -m1 -o '"model":"[^"]*"' "$ATP" 2>/dev/null)" || RAN=""; RAN="${RAN#\"model\":\"}"; RAN="${RAN%\"}"; fi
|
|
357
|
+
case "$RAN" in ''|*[!A-Za-z0-9._:\[\]-]*) RAN=- ;; esac
|
|
358
|
+
[ "$RES" = fail ] && printf '%s\n' "$ASK" >> "$_AMD/fails/$CID" 2>/dev/null
|
|
359
|
+
printf 'verify=%s\nfixes=%s\nconfidence=%s\ntier=%s\nesc=%s\nescalate=%s\nwhynot=%s\ncmd=%s\n' "$RES" "$TRIES" "$CONF" "$ASK" "$ESC" "$ESCL" "$WHYNOT" "$VCMD" > "$_AMD/results/$AID" 2>/dev/null
|
|
360
|
+
case "$REC" in "$_AMD/pending/"*) rm -f "$REC" 2>/dev/null ;; *) : > "$REC.used" 2>/dev/null ;; esac
|
|
361
|
+
TSV="$_AMS/model-outcomes.tsv"
|
|
362
|
+
[ -s "$TSV" ] || printf 'ts\tagent_id\tagent\tchange\trisk\tcard\tmodel\tverify\tfixes\tescalated_from\tran_on\tconfidence\n' > "$TSV" 2>/dev/null
|
|
363
|
+
_ao_now
|
|
364
|
+
_ao_lock
|
|
365
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_AON" "$AID" "$AG" "$CH" "$RK" "$CID" "$ASK" "$RES" "$TRIES" "$ESC" "$RAN" "$CONF" >> "$TSV" 2>/dev/null
|
|
366
|
+
_ao_unlock
|
|
367
|
+
case "$RES" in pass|fail) _ao_calibrate "$AG" "$CH" "$RK" ;; esac
|
|
368
|
+
exit 0 ;;
|
|
369
|
+
|
|
370
|
+
PostToolUse)
|
|
371
|
+
_json_slice "$INPUT" tool_name >/dev/null; case "$_JS" in Agent|Task) ;; *) exit 0 ;; esac
|
|
372
|
+
_json_slice "$INPUT" agentId >/dev/null; AID="$_JS"
|
|
373
|
+
case "$AID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
374
|
+
# The model the agent really ran on, as Claude Code resolved it (measured, 2.1.294: tool_response.resolvedModel,
|
|
375
|
+
# in the answer of a finished foreground call and of a background launch alike). Kept under the agent's id for
|
|
376
|
+
# the stop hook, and written into the agent's line where that line is there already.
|
|
377
|
+
_json_keycount "$INPUT" resolvedModel
|
|
378
|
+
if [ "$_KC" = 1 ]; then
|
|
379
|
+
_json_slice "$INPUT" resolvedModel >/dev/null; RM="$_JS"
|
|
380
|
+
case "$RM" in ''|*[!A-Za-z0-9._:\[\]-]*) RM="" ;; esac
|
|
381
|
+
if [ -n "$RM" ] && [ "${#RM}" -le 80 ] && mkdir -p "$_AMD/ran" 2>/dev/null; then
|
|
382
|
+
printf '%s\n' "$RM" > "$_AMD/ran/$AID" 2>/dev/null
|
|
383
|
+
TSV="$_AMS/model-outcomes.tsv"
|
|
384
|
+
if [ -f "$TSV" ] && grep -q "$(printf '\t')$AID$(printf '\t')" "$TSV" 2>/dev/null; then
|
|
385
|
+
_ao_lock
|
|
386
|
+
awk -F'\t' -v OFS='\t' -v A="$AID" -v M="$RM" 'NR > 1 && $2 == A && NF >= 11 { $11 = M } { print }' "$TSV" > "$TSV.tmp" 2>/dev/null \
|
|
387
|
+
&& [ "$(wc -l < "$TSV.tmp")" = "$(wc -l < "$TSV")" ] && mv "$TSV.tmp" "$TSV" 2>/dev/null
|
|
388
|
+
rm -f "$TSV.tmp" 2>/dev/null
|
|
389
|
+
_ao_unlock
|
|
390
|
+
fi
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
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
|
|
414
|
+
_am_rec_get "$R" verify; RES="$_AMV"; _am_rec_get "$R" confidence; CONF="$_AMV"; _am_rec_get "$R" tier; TI="$_AMV"
|
|
415
|
+
_am_rec_get "$R" esc; ESC="$_AMV"; _am_rec_get "$R" cmd; VCMD="$_AMV"
|
|
416
|
+
case "$RES" in fail|blocked|timeout) ;; *) [ "$CONF" = low ] || exit 0 ;; esac
|
|
417
|
+
if [ "$RES" = blocked ] || [ "$RES" = timeout ]; then
|
|
418
|
+
# Nothing is known about the work, so nothing is escalated: the session runs the command where a gate sees it.
|
|
419
|
+
_am_rec_get "$R" whynot; WN="$_AMV"
|
|
420
|
+
_ao_json "Crewforth: the agent's verify command was NOT run by the hook ($VCMD): $WN. Nothing is concluded about the work. Run the verify command yourself with the Bash tool and judge the result; do not repeat the task on another model for this."
|
|
421
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_AOJ"
|
|
422
|
+
exit 0
|
|
423
|
+
fi
|
|
424
|
+
_am_rank "$TI"; _am_name $((_AMR+1)); UP="$_AMN"
|
|
425
|
+
_am_rec_get "$R" escalate; ESCL="$_AMV"
|
|
426
|
+
if [ "$RES" = fail ]; then MSG="verify failed after one in-agent fix ($VCMD)."; TAILM="The work the agent left is in the tree and has NOT passed its verify command."
|
|
427
|
+
else MSG="the agent closed its report with confidence: low."; TAILM="What the agent left is in the tree; read its report for what it could not do."; fi
|
|
428
|
+
# A write refused on a critical path is not helped by one model up: that file takes opus.
|
|
429
|
+
if [ "$ESCL" = 1 ] && [ "$TI" != opus ] && [ "$TI" != fable ]; then UP=opus; MSG="$MSG It was refused a write to a critical path, which only an agent on opus may make; name that file in the card."; fi
|
|
430
|
+
if [ "$ESC" != - ] && [ -n "$ESC" ]; then MSG="$MSG This was already the repeat one model up (first run: $ESC). Do not run it a third time: ask the user what to do (AskUserQuestion), with what failed and on which models."
|
|
431
|
+
elif [ -z "$UP" ] || [ "$UP" = fable ]; then MSG="$MSG It ran on $TI and there is no model above it to repeat on: ask the user what to do (AskUserQuestion)."
|
|
432
|
+
else MSG="$MSG Re-run this task once with $UP: the same card, the same agent, model $UP. If that also fails, ask the user."; fi
|
|
433
|
+
_ao_json "Crewforth: $MSG $TAILM"
|
|
434
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}\n' "$_AOJ"
|
|
435
|
+
exit 0 ;;
|
|
436
|
+
esac
|
|
437
|
+
exit 0
|
|
@@ -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."
|