@zenera/cli 1.1.23 → 1.1.24
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/README.md +35 -2
- package/dist/ask.d.ts +51 -0
- package/dist/ask.js +143 -0
- package/dist/batch.d.ts +77 -0
- package/dist/batch.js +338 -0
- package/dist/command.d.ts +6 -0
- package/dist/commands/inspect.d.ts +11 -0
- package/dist/commands/inspect.js +388 -89
- package/dist/commands/list.js +128 -19
- package/dist/commands/memory.js +342 -12
- package/dist/commands/models.js +14 -2
- package/dist/commands/run.js +139 -34
- package/dist/commands/sandbox.js +72 -1
- package/dist/engine.d.ts +19 -0
- package/dist/engine.js +60 -8
- package/dist/liveness.js +6 -3
- package/dist/main.js +5 -2
- package/dist/podman.d.ts +16 -0
- package/dist/podman.js +115 -2
- package/dist/progress.d.ts +33 -0
- package/dist/progress.js +402 -0
- package/dist/request.d.ts +45 -0
- package/dist/request.js +300 -0
- package/dist/sandbox.js +10 -0
- package/dist/session.d.ts +26 -1
- package/dist/session.js +42 -3
- package/dist/term.d.ts +3 -0
- package/dist/term.js +17 -1
- package/dist/trace.d.ts +84 -0
- package/dist/trace.js +686 -0
- package/dist/validate.js +25 -3
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +11 -6
- package/templates/editor/.github/prompts/finetune.prompt.md +251 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -3
- package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +262 -21
- package/templates/editor/.github/skills/zen-cli/references/memory.md +89 -19
- package/templates/editor/.github/skills/zen-cli/references/models.md +1 -1
- package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
- package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1000 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +432 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +199 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +396 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +74 -29
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +19 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
- package/templates/editor/.vscode/agents.schema.json +1 -1
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +13 -0
- package/templates/project/agents/fork-instructions.md +27 -0
- package/templates/project/agents/memory-instructions.md +80 -7
- package/templates/project/gitignore +3 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Read back a fine-tuning batch: what each item did, and where its trajectory is.
|
|
4
|
+
#
|
|
5
|
+
# collect.sh the index: verdict, agent, stop reason, cost
|
|
6
|
+
# collect.sh graphs [id...] every trajectory graph, concatenated
|
|
7
|
+
# collect.sh paths [id...] id <TAB> run directory, for xargs and zen inspect
|
|
8
|
+
# collect.sh failures only the items that did not finish, with errors
|
|
9
|
+
# collect.sh oom items whose commands were killed; is this round gradeable
|
|
10
|
+
# collect.sh compare the per-sample table findings.md opens with
|
|
11
|
+
#
|
|
12
|
+
# -d <dir> the batch directory; default the newest .finetune/rounds/*/batch
|
|
13
|
+
# -p <dir> the batch to compare against, for `compare`; default none
|
|
14
|
+
# -s <file> the dataset, for rubrics; default .finetune/dataset.json
|
|
15
|
+
#
|
|
16
|
+
# `index`, `graphs` and `compare` each open by saying whether the round was
|
|
17
|
+
# COLD, WARM or memory-OFF, because the same trajectory means opposite things
|
|
18
|
+
# either way and a round written up without that line compares to nothing.
|
|
19
|
+
#
|
|
20
|
+
# Run `oom` first, and do not expect `failures` to have caught it: an item whose
|
|
21
|
+
# command was killed usually recovers, answers anyway and is recorded `ok`. A
|
|
22
|
+
# round can read "16 items, 16 ok, 0 failed" with six of them OOM-killed inside.
|
|
23
|
+
# A round that contains an exit 137 ran out of memory, and nothing it did is
|
|
24
|
+
# evidence about the prompt — grading it sends the next round after a defect
|
|
25
|
+
# that does not exist. See the zen-sandbox-capacity skill.
|
|
26
|
+
#
|
|
27
|
+
# `graphs` is the one that saves the most: it is every item's graph.mmd with its
|
|
28
|
+
# id, verdict and rubric written above it, which is one read instead of N
|
|
29
|
+
# invocations of `zen inspect graph`. Grade from that, then descend into the
|
|
30
|
+
# runs that need it with `zen inspect node --dir "$(collect.sh paths | ...)"`.
|
|
31
|
+
#
|
|
32
|
+
# Run it from anywhere; it finds the project root from its own location, which
|
|
33
|
+
# `zen init` and `zen open` fix. See .github/skills/zen-finetune/SKILL.md.
|
|
34
|
+
|
|
35
|
+
set -eu
|
|
36
|
+
|
|
37
|
+
# Taken before the cd, or a relative $0 stops naming this file.
|
|
38
|
+
SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
|
39
|
+
cd "$(dirname "$0")/../../../.."
|
|
40
|
+
|
|
41
|
+
MODE=""
|
|
42
|
+
BATCH=""
|
|
43
|
+
PREV=""
|
|
44
|
+
DATASET=.finetune/dataset.json
|
|
45
|
+
WANTED=""
|
|
46
|
+
|
|
47
|
+
usage() {
|
|
48
|
+
sed -n '3,25p' "$SELF" | sed 's/^# \{0,1\}//'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
while [ $# -gt 0 ]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
-d) BATCH="${2:?-d needs a directory}"; shift 2 ;;
|
|
54
|
+
-p) PREV="${2:?-p needs a directory}"; shift 2 ;;
|
|
55
|
+
-s) DATASET="${2:?-s needs a file}"; shift 2 ;;
|
|
56
|
+
-h | --help) usage; exit 0 ;;
|
|
57
|
+
index | graphs | paths | failures | oom | compare)
|
|
58
|
+
if [ -n "$MODE" ]; then
|
|
59
|
+
echo "collect.sh: one mode at a time" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
MODE="$1"
|
|
63
|
+
shift
|
|
64
|
+
;;
|
|
65
|
+
-*)
|
|
66
|
+
echo "collect.sh: unknown flag $1" >&2
|
|
67
|
+
usage >&2
|
|
68
|
+
exit 2
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
WANTED="$WANTED $1"
|
|
72
|
+
shift
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
MODE="${MODE:-index}"
|
|
78
|
+
|
|
79
|
+
if ! command -v jq > /dev/null 2>&1; then
|
|
80
|
+
echo "collect.sh: jq is required" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Without -d, the most recently written batch. Rounds are read far more often
|
|
85
|
+
# than they are named, and the one just run is almost always the one meant.
|
|
86
|
+
if [ -z "$BATCH" ]; then
|
|
87
|
+
for dir in .finetune/rounds/*/batch; do
|
|
88
|
+
if [ -f "$dir/batch.json" ]; then BATCH="$dir"; fi
|
|
89
|
+
done
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [ -z "$BATCH" ] || [ ! -f "$BATCH/batch.json" ]; then
|
|
93
|
+
echo "collect.sh: no batch.json${BATCH:+ in $BATCH}" >&2
|
|
94
|
+
echo " name one with -d, or run a batch first" >&2
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# `id ok` per line, in the order the batch file listed them, filtered to the
|
|
99
|
+
# ids asked for if any were. Everything below walks this.
|
|
100
|
+
roster() {
|
|
101
|
+
jq -r '.batch_results[] | "\(.id) \(.ok)"' "$BATCH/batch.json" | while read -r id ok; do
|
|
102
|
+
if [ -z "$WANTED" ]; then
|
|
103
|
+
echo "$id $ok"
|
|
104
|
+
else
|
|
105
|
+
for want in $WANTED; do
|
|
106
|
+
if [ "$want" = "$id" ]; then echo "$id $ok"; fi
|
|
107
|
+
done
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# One field out of an item's envelope, empty if the item failed or never wrote.
|
|
113
|
+
field() {
|
|
114
|
+
if [ -f "$BATCH/$1/output.json" ]; then
|
|
115
|
+
jq -r "$2 // empty" "$BATCH/$1/output.json" 2> /dev/null || true
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Everything one item cost, as "tokens secs nodes llm tools forks", from any
|
|
120
|
+
# batch directory rather than the current one — `compare` reads two.
|
|
121
|
+
#
|
|
122
|
+
# The counts come from the graph's own header row, which already states them:
|
|
123
|
+
# %% nodes 103 · 33 llm · 30 tool calls · 1 forks
|
|
124
|
+
# Counting nodes out of state.json instead over-counts tool calls about
|
|
125
|
+
# threefold, because nested structures repeat the same call.
|
|
126
|
+
metrics() {
|
|
127
|
+
out="$1/$2/output.json"
|
|
128
|
+
if [ ! -f "$out" ]; then
|
|
129
|
+
echo "0 0 0 0 0 0"
|
|
130
|
+
return
|
|
131
|
+
fi
|
|
132
|
+
tok="$(jq -r '((.usage.inputTokens // 0) + (.usage.outputTokens // 0))' "$out" 2> /dev/null || echo 0)"
|
|
133
|
+
sec="$(jq -r '(((.durationMs // 0) / 1000) | floor)' "$out" 2> /dev/null || echo 0)"
|
|
134
|
+
graph="$(jq -r '.run.graph // empty' "$out" 2> /dev/null || true)"
|
|
135
|
+
hdr=""
|
|
136
|
+
if [ -n "$graph" ] && [ -f "$graph" ]; then
|
|
137
|
+
hdr="$(sed -n 's/^%% nodes *//p' "$graph" | head -1 || true)"
|
|
138
|
+
fi
|
|
139
|
+
if [ -z "$hdr" ]; then
|
|
140
|
+
echo "$tok $sec 0 0 0 0"
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
echo "$tok $sec $(echo "$hdr" | awk -F' · ' '{printf "%d %d %d %d", $1+0, $2+0, $3+0, $4+0}')"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# 1.09M, 812k, 94 — these are read by eye and compared by eye.
|
|
147
|
+
hum() {
|
|
148
|
+
awk -v n="$1" 'BEGIN {
|
|
149
|
+
if (n + 0 >= 1000000) printf "%.2fM\n", n / 1000000
|
|
150
|
+
else if (n + 0 >= 1000) printf "%dk\n", n / 1000
|
|
151
|
+
else printf "%d\n", n
|
|
152
|
+
}'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# "33 → 30 (−3)" against a previous round, "33" without one. Tokens and seconds
|
|
156
|
+
# read as a percentage because they move continuously; calls and forks read as
|
|
157
|
+
# an absolute difference because two of them is two, not eleven per cent.
|
|
158
|
+
cell() {
|
|
159
|
+
prev="$1"
|
|
160
|
+
now="$2"
|
|
161
|
+
mode="$3"
|
|
162
|
+
case "$mode" in
|
|
163
|
+
tok) shown_prev="$(hum "$prev")"; shown_now="$(hum "$now")" ;;
|
|
164
|
+
sec) shown_prev="${prev}s"; shown_now="${now}s" ;;
|
|
165
|
+
*) shown_prev="$prev"; shown_now="$now" ;;
|
|
166
|
+
esac
|
|
167
|
+
if [ -z "$PREV" ]; then
|
|
168
|
+
echo "$shown_now"
|
|
169
|
+
return
|
|
170
|
+
fi
|
|
171
|
+
awk -v p="$prev" -v n="$now" -v sp="$shown_prev" -v sn="$shown_now" -v mode="$mode" 'BEGIN {
|
|
172
|
+
d = n - p
|
|
173
|
+
a = (d < 0) ? -d : d
|
|
174
|
+
sign = (d >= 0) ? "+" : "−"
|
|
175
|
+
if (a == 0) printf "%s (=)\n", sn
|
|
176
|
+
else if (mode == "num") printf "%s → %s (%s%d)\n", sp, sn, sign, a
|
|
177
|
+
else if (p > 0) printf "%s → %s (%s%.0f%%)\n", sp, sn, sign, a * 100.0 / p
|
|
178
|
+
else printf "%s → %s\n", sp, sn
|
|
179
|
+
}'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# cold / warm / off, for a batch directory.
|
|
183
|
+
#
|
|
184
|
+
# batch.json records the mode and the source but not whether that source was a
|
|
185
|
+
# real graph when the batch ran — and `--memory .finetune/empty`, which is how a
|
|
186
|
+
# cold round is made, records as `copied` exactly like a warm one. The graph
|
|
187
|
+
# file is the difference: an empty-graph round never creates one at the source.
|
|
188
|
+
memkind() {
|
|
189
|
+
mode="$(jq -r '.batch.memory.mode // "none"' "$1/batch.json" 2> /dev/null || echo none)"
|
|
190
|
+
src="$(jq -r '.batch.memory.source // empty' "$1/batch.json" 2> /dev/null || true)"
|
|
191
|
+
case "$mode" in
|
|
192
|
+
none) echo off ;;
|
|
193
|
+
read-only) echo warm ;;
|
|
194
|
+
copied)
|
|
195
|
+
if [ -n "$src" ] && [ -f "$src/graph.json" ]; then echo warm; else echo cold; fi
|
|
196
|
+
;;
|
|
197
|
+
*) echo "$mode" ;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# The line every report opens with, so that no round is ever graded or compared
|
|
202
|
+
# without it being plain whether memory was in play.
|
|
203
|
+
memline() {
|
|
204
|
+
mode="$(jq -r '.batch.memory.mode // "none"' "$1/batch.json" 2> /dev/null || echo none)"
|
|
205
|
+
src="$(jq -r '.batch.memory.source // empty' "$1/batch.json" 2> /dev/null || true)"
|
|
206
|
+
case "$(memkind "$1")" in
|
|
207
|
+
off) echo "OFF — no memory was given to this batch, and none was written" ;;
|
|
208
|
+
cold) echo "COLD — every item started from an empty graph and wrote its own (${src:-none})" ;;
|
|
209
|
+
warm)
|
|
210
|
+
if [ "$mode" = read-only ]; then
|
|
211
|
+
echo "WARM — one shared graph, read-only, every item read it and none wrote ($src)"
|
|
212
|
+
else
|
|
213
|
+
echo "WARM — each item got its own copy of an existing graph ($src)"
|
|
214
|
+
fi
|
|
215
|
+
;;
|
|
216
|
+
*) echo "$mode${src:+ ($src)}" ;;
|
|
217
|
+
esac
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# The rubric from the dataset, joined on the id. Absent dataset, absent rubric,
|
|
221
|
+
# absent sample: all the same thing here, and none of them is an error.
|
|
222
|
+
rubric() {
|
|
223
|
+
if [ -f "$DATASET" ]; then
|
|
224
|
+
jq -r --arg id "$1" '
|
|
225
|
+
.samples[] | select(.id == $id) | (.rubric // [])[]
|
|
226
|
+
| " - " + .' "$DATASET" 2> /dev/null || true
|
|
227
|
+
fi
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case "$MODE" in
|
|
231
|
+
index)
|
|
232
|
+
jq -r '.batch | "batch \(.items) items, \(.ok) ok, \(.failed) failed"
|
|
233
|
+
+ " · \((.durationMs / 1000) | floor)s"' "$BATCH/batch.json"
|
|
234
|
+
echo "dir $BATCH"
|
|
235
|
+
echo "memory $(memline "$BATCH")"
|
|
236
|
+
echo
|
|
237
|
+
printf '%-24s %-5s %-12s %-12s %7s %5s %6s\n' \
|
|
238
|
+
ID OK AGENT STOP TOKENS SEC RUBRIC
|
|
239
|
+
roster | while read -r id ok; do
|
|
240
|
+
agent="$(field "$id" .agent)"
|
|
241
|
+
stop="$(field "$id" .stopReason)"
|
|
242
|
+
tokens="$(field "$id" '(.usage.inputTokens + .usage.outputTokens)')"
|
|
243
|
+
secs="$(field "$id" '((.durationMs / 1000) | floor)')"
|
|
244
|
+
lines="$(rubric "$id" | grep -c . || true)"
|
|
245
|
+
if [ "$lines" -eq 0 ]; then lines="-"; fi
|
|
246
|
+
printf '%-24s %-5s %-12s %-12s %7s %5s %6s\n' \
|
|
247
|
+
"$id" "$ok" "${agent:--}" "${stop:--}" "${tokens:--}" "${secs:--}" "$lines"
|
|
248
|
+
done
|
|
249
|
+
echo
|
|
250
|
+
echo "next: collect.sh -d $BATCH graphs | less"
|
|
251
|
+
;;
|
|
252
|
+
|
|
253
|
+
graphs)
|
|
254
|
+
# Before the first graph, because a warm trajectory graded as a cold one
|
|
255
|
+
# reads as an agent that knew things it was never told.
|
|
256
|
+
echo "== memory: $(memline "$BATCH")"
|
|
257
|
+
roster | while read -r id ok; do
|
|
258
|
+
graph="$(field "$id" .run.graph)"
|
|
259
|
+
echo "================================================================"
|
|
260
|
+
echo "== $id ok=$ok"
|
|
261
|
+
lines="$(rubric "$id")"
|
|
262
|
+
if [ -n "$lines" ]; then
|
|
263
|
+
echo "== rubric:"
|
|
264
|
+
echo "$lines" | sed 's/^/== /'
|
|
265
|
+
fi
|
|
266
|
+
echo "================================================================"
|
|
267
|
+
if [ -n "$graph" ] && [ -f "$graph" ]; then
|
|
268
|
+
cat "$graph"
|
|
269
|
+
elif [ -f "$BATCH/$id/output.json" ]; then
|
|
270
|
+
jq -r '.error.message // "no graph was written"' "$BATCH/$id/output.json"
|
|
271
|
+
else
|
|
272
|
+
echo "no output.json"
|
|
273
|
+
fi
|
|
274
|
+
echo
|
|
275
|
+
done
|
|
276
|
+
;;
|
|
277
|
+
|
|
278
|
+
paths)
|
|
279
|
+
roster | while read -r id ok; do
|
|
280
|
+
dir="$(field "$id" .run.dir)"
|
|
281
|
+
printf '%s\t%s\n' "$id" "${dir:--}"
|
|
282
|
+
done
|
|
283
|
+
;;
|
|
284
|
+
|
|
285
|
+
failures)
|
|
286
|
+
# The count comes from batch.json rather than the loop below: a `while`
|
|
287
|
+
# on the right of a pipe is a subshell, and a variable set in one is
|
|
288
|
+
# gone by the time anything could read it.
|
|
289
|
+
if [ "$(jq -r '[.batch_results[] | select(.ok | not)] | length' "$BATCH/batch.json")" -eq 0 ]; then
|
|
290
|
+
echo "nothing failed"
|
|
291
|
+
exit 0
|
|
292
|
+
fi
|
|
293
|
+
roster | while read -r id ok; do
|
|
294
|
+
if [ "$ok" != "true" ]; then
|
|
295
|
+
echo "-- $id"
|
|
296
|
+
if [ -f "$BATCH/$id/output.json" ]; then
|
|
297
|
+
jq -r '" " + (.error.message // "unknown")
|
|
298
|
+
+ (if .error.hint then "\n hint: " + .error.hint else "" end)' \
|
|
299
|
+
"$BATCH/$id/output.json"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
done
|
|
303
|
+
;;
|
|
304
|
+
|
|
305
|
+
compare)
|
|
306
|
+
# roster is redirected to a file rather than piped: a `while` on the
|
|
307
|
+
# right of a pipe is a subshell, and the totals set in one are gone by
|
|
308
|
+
# the time the total row could print them.
|
|
309
|
+
tmp="$(mktemp)"
|
|
310
|
+
trap 'rm -f "$tmp"' EXIT
|
|
311
|
+
roster > "$tmp"
|
|
312
|
+
|
|
313
|
+
if [ -n "$PREV" ] && [ ! -f "$PREV/batch.json" ]; then
|
|
314
|
+
echo "collect.sh: no batch.json in $PREV" >&2
|
|
315
|
+
exit 2
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Part of the table, not a footnote: the same numbers mean opposite
|
|
319
|
+
# things depending on this line, and a pasted table loses anything
|
|
320
|
+
# printed after it.
|
|
321
|
+
echo "memory: $(memline "$BATCH")"
|
|
322
|
+
if [ -n "$PREV" ]; then
|
|
323
|
+
echo "prev memory: $(memline "$PREV")"
|
|
324
|
+
fi
|
|
325
|
+
echo
|
|
326
|
+
echo "| sample | verdict | tokens | llm calls | tool calls | forks | time |"
|
|
327
|
+
echo "| --- | --- | --- | --- | --- | --- | --- |"
|
|
328
|
+
|
|
329
|
+
ptok=0; psec=0; pllm=0; ptool=0; pfork=0
|
|
330
|
+
ntok=0; nsec=0; nllm=0; ntool=0; nfork=0
|
|
331
|
+
while read -r id ok; do
|
|
332
|
+
read -r a_tok a_sec a_nodes a_llm a_tool a_fork <<< "$(metrics "$BATCH" "$id")"
|
|
333
|
+
if [ -n "$PREV" ]; then
|
|
334
|
+
read -r b_tok b_sec b_nodes b_llm b_tool b_fork <<< "$(metrics "$PREV" "$id")"
|
|
335
|
+
else
|
|
336
|
+
b_tok=0; b_sec=0; b_llm=0; b_tool=0; b_fork=0
|
|
337
|
+
fi
|
|
338
|
+
ntok=$((ntok + a_tok)); nsec=$((nsec + a_sec))
|
|
339
|
+
nllm=$((nllm + a_llm)); ntool=$((ntool + a_tool)); nfork=$((nfork + a_fork))
|
|
340
|
+
ptok=$((ptok + b_tok)); psec=$((psec + b_sec))
|
|
341
|
+
pllm=$((pllm + b_llm)); ptool=$((ptool + b_tool)); pfork=$((pfork + b_fork))
|
|
342
|
+
# The verdict column is left for the grader: it is the one thing
|
|
343
|
+
# here no script can read off a trajectory.
|
|
344
|
+
verdict="?"
|
|
345
|
+
if [ "$ok" != "true" ]; then verdict="**did not finish**"; fi
|
|
346
|
+
printf '| %s | %s | %s | %s | %s | %s | %s |\n' \
|
|
347
|
+
"$id" \
|
|
348
|
+
"$verdict" \
|
|
349
|
+
"$(cell "$b_tok" "$a_tok" tok)" \
|
|
350
|
+
"$(cell "$b_llm" "$a_llm" num)" \
|
|
351
|
+
"$(cell "$b_tool" "$a_tool" num)" \
|
|
352
|
+
"$(cell "$b_fork" "$a_fork" num)" \
|
|
353
|
+
"$(cell "$b_sec" "$a_sec" sec)"
|
|
354
|
+
done < "$tmp"
|
|
355
|
+
|
|
356
|
+
printf '| **total** | %s | **%s** | **%s** | **%s** | **%s** | **%s** |\n' \
|
|
357
|
+
"—" \
|
|
358
|
+
"$(cell "$ptok" "$ntok" tok)" \
|
|
359
|
+
"$(cell "$pllm" "$nllm" num)" \
|
|
360
|
+
"$(cell "$ptool" "$ntool" num)" \
|
|
361
|
+
"$(cell "$pfork" "$nfork" num)" \
|
|
362
|
+
"$(cell "$psec" "$nsec" sec)"
|
|
363
|
+
|
|
364
|
+
echo
|
|
365
|
+
echo "this $BATCH"
|
|
366
|
+
if [ -n "$PREV" ]; then echo "prev $PREV"; fi
|
|
367
|
+
echo "note: wall time is concurrent — the totals are machine load, not a sum anybody waited."
|
|
368
|
+
if [ -n "$PREV" ] && [ "$(memkind "$BATCH")" != "$(memkind "$PREV")" ]; then
|
|
369
|
+
echo
|
|
370
|
+
echo "note: these two rounds did not run on the same memory ($(memkind "$PREV") → $(memkind "$BATCH"))."
|
|
371
|
+
echo " Cold against warm is the phase-8 measurement and reads as one; cold against"
|
|
372
|
+
echo " cold is a prose diff. Say which this is in the verdict block, or the numbers"
|
|
373
|
+
echo " will be read as the effect of an instruction that did nothing."
|
|
374
|
+
fi
|
|
375
|
+
;;
|
|
376
|
+
|
|
377
|
+
oom)
|
|
378
|
+
# The graph is where an exit code survives: every tool call is a node,
|
|
379
|
+
# and a killed `run_command` reads `= exit code 137`. Do not grep the
|
|
380
|
+
# batch directory instead — an item's workspace is full of data files
|
|
381
|
+
# with 137 in them, and every one is a false positive.
|
|
382
|
+
#
|
|
383
|
+
# Collected into a variable rather than counted in the loop: a `while`
|
|
384
|
+
# on the right of a pipe is a subshell, and a count set in one is gone
|
|
385
|
+
# before the verdict could read it.
|
|
386
|
+
scan="$(
|
|
387
|
+
roster | while read -r id ok; do
|
|
388
|
+
graph="$(field "$id" .run.graph)"
|
|
389
|
+
if [ -z "$graph" ] || [ ! -f "$graph" ]; then continue; fi
|
|
390
|
+
killed="$(grep -c 'exit code 137' "$graph" 2> /dev/null || true)"
|
|
391
|
+
timedout="$(grep -c 'exit code 124' "$graph" 2> /dev/null || true)"
|
|
392
|
+
if [ "${killed:-0}" -gt 0 ] || [ "${timedout:-0}" -gt 0 ]; then
|
|
393
|
+
printf '%s %s %s\n' "$id" "${killed:-0}" "${timedout:-0}"
|
|
394
|
+
fi
|
|
395
|
+
done
|
|
396
|
+
)"
|
|
397
|
+
|
|
398
|
+
if [ -z "$scan" ]; then
|
|
399
|
+
echo "no killed commands in this round — it is gradeable"
|
|
400
|
+
exit 0
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
printf '%-32s %8s %8s\n' ITEM 'OOM 137' 'TIME 124'
|
|
404
|
+
echo "$scan" | while read -r id killed timedout; do
|
|
405
|
+
printf '%-32s %8s %8s\n' "$id" "$killed" "$timedout"
|
|
406
|
+
done
|
|
407
|
+
echo
|
|
408
|
+
|
|
409
|
+
items_killed="$(echo "$scan" | awk '$2 > 0' | grep -c . || true)"
|
|
410
|
+
total_killed="$(echo "$scan" | awk '{ s += $2 } END { print s + 0 }')"
|
|
411
|
+
total_timedout="$(echo "$scan" | awk '{ s += $3 } END { print s + 0 }')"
|
|
412
|
+
|
|
413
|
+
if [ "${items_killed:-0}" -gt 0 ]; then
|
|
414
|
+
echo "$total_killed command(s) across $items_killed item(s) were OOM-killed."
|
|
415
|
+
echo
|
|
416
|
+
echo "THIS ROUND IS VOID, NOT GRADED."
|
|
417
|
+
echo " Nothing here is evidence about the prompt. Do not grade it, do not"
|
|
418
|
+
echo " change instructions on it, and do not compare its tokens to another"
|
|
419
|
+
echo " round. Size the machine, then run it again:"
|
|
420
|
+
echo
|
|
421
|
+
echo " .github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh --concurrency 8"
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
echo "$total_timedout command(s) hit the timeout; none was OOM-killed."
|
|
426
|
+
echo " Under memory pressure 124 is 137 wearing a different number, and raising"
|
|
427
|
+
echo " the timeout converts one into the other rather than fixing either. Check"
|
|
428
|
+
echo " the machine before concluding the index is slow:"
|
|
429
|
+
echo
|
|
430
|
+
echo " .github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh --concurrency 8"
|
|
431
|
+
;;
|
|
432
|
+
esac
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Choose the next fine-tuning batch out of .finetune/dataset.json.
|
|
4
|
+
#
|
|
5
|
+
# sample.sh every sample, in stratified order
|
|
6
|
+
# sample.sh -n 16 sixteen, spread evenly over the strata
|
|
7
|
+
# sample.sh -n 16 --class planning sixteen out of one class
|
|
8
|
+
# sample.sh --rubric-only -o cases.json only the graded ones, to a file
|
|
9
|
+
#
|
|
10
|
+
# A stratum is one class x one complexity. The sample is taken one item at a
|
|
11
|
+
# time from each stratum in turn, so a budget that cannot afford the dataset
|
|
12
|
+
# still buys a cross-section of it rather than the front of the file. Inside a
|
|
13
|
+
# stratum, samples carrying a rubric come first: a graded run says why it
|
|
14
|
+
# failed, an ungraded one only says what it did.
|
|
15
|
+
#
|
|
16
|
+
# Deterministic. The same --seed over the same dataset picks the same batch, so
|
|
17
|
+
# a re-run after a prompt change is a comparison and not a new experiment.
|
|
18
|
+
#
|
|
19
|
+
# What it writes is a `zen run batch --input` file: { "batch": [ {id, input} ] }.
|
|
20
|
+
# Relative media paths inside `input` resolve against the file they are written
|
|
21
|
+
# in, so -o somewhere other than beside the dataset needs absolute paths in it.
|
|
22
|
+
#
|
|
23
|
+
# Run it from anywhere; it finds the project root from its own location, which
|
|
24
|
+
# `zen init` and `zen open` fix. See .github/skills/zen-finetune/SKILL.md.
|
|
25
|
+
|
|
26
|
+
set -eu
|
|
27
|
+
cd "$(dirname "$0")/../../../.."
|
|
28
|
+
|
|
29
|
+
DATASET=.finetune/dataset.json
|
|
30
|
+
LIMIT=0
|
|
31
|
+
CLASS=""
|
|
32
|
+
COMPLEXITY=""
|
|
33
|
+
RUBRIC_ONLY=0
|
|
34
|
+
SEED=1
|
|
35
|
+
OUT=""
|
|
36
|
+
|
|
37
|
+
usage() {
|
|
38
|
+
sed -n '3,10p' "$0" | sed 's/^# \{0,1\}//'
|
|
39
|
+
echo
|
|
40
|
+
echo " -d <file> dataset; default $DATASET"
|
|
41
|
+
echo " -n <N> budget; default every sample"
|
|
42
|
+
echo " -o <file> where to write; default stdout"
|
|
43
|
+
echo " --class <name> one class only"
|
|
44
|
+
echo " --complexity <lvl> one complexity only"
|
|
45
|
+
echo " --rubric-only drop samples with no rubric"
|
|
46
|
+
echo " --seed <n> shuffle seed; default $SEED"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
while [ $# -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
-d) DATASET="${2:?-d needs a file}"; shift 2 ;;
|
|
52
|
+
-n) LIMIT="${2:?-n needs a number}"; shift 2 ;;
|
|
53
|
+
-o) OUT="${2:?-o needs a file}"; shift 2 ;;
|
|
54
|
+
--class) CLASS="${2:?--class needs a name}"; shift 2 ;;
|
|
55
|
+
--complexity) COMPLEXITY="${2:?--complexity needs a level}"; shift 2 ;;
|
|
56
|
+
--rubric-only) RUBRIC_ONLY=1; shift ;;
|
|
57
|
+
--seed) SEED="${2:?--seed needs a number}"; shift 2 ;;
|
|
58
|
+
-h | --help) usage; exit 0 ;;
|
|
59
|
+
*)
|
|
60
|
+
echo "sample.sh: unknown argument $1" >&2
|
|
61
|
+
usage >&2
|
|
62
|
+
exit 2
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
if ! command -v jq > /dev/null 2>&1; then
|
|
68
|
+
echo "sample.sh: jq is required" >&2
|
|
69
|
+
exit 2
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ ! -f "$DATASET" ]; then
|
|
73
|
+
echo "sample.sh: no $DATASET" >&2
|
|
74
|
+
echo " build it first: see .github/skills/zen-finetune/SKILL.md, phase 1" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
case "$LIMIT$SEED" in
|
|
79
|
+
*[!0-9]*)
|
|
80
|
+
echo "sample.sh: -n and --seed take whole numbers" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# The dataset has to be sound before it is worth sampling. An id names a
|
|
87
|
+
# directory under the batch dir, and `zen run batch` refuses a bad one item by
|
|
88
|
+
# item, halfway through a long run. Refuse it here instead, all at once.
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
if ! jq -e '.samples | type == "array" and length > 0' "$DATASET" > /dev/null 2>&1; then
|
|
92
|
+
echo "sample.sh: $DATASET has no non-empty \"samples\" array" >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
BAD="$(jq -r '
|
|
97
|
+
.samples[]
|
|
98
|
+
| select((.id | type) != "string"
|
|
99
|
+
or (.id | test("^[A-Za-z0-9_.-]+$") | not)
|
|
100
|
+
or .id == "." or .id == "..")
|
|
101
|
+
| (.id | tostring)' "$DATASET")"
|
|
102
|
+
if [ -n "$BAD" ]; then
|
|
103
|
+
echo "sample.sh: these ids cannot name a directory:" >&2
|
|
104
|
+
echo "$BAD" | sed 's/^/ /' >&2
|
|
105
|
+
echo " letters, digits, dot, dash and underscore only" >&2
|
|
106
|
+
exit 2
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
DUPES="$(jq -r '[.samples[].id] | group_by(.) | map(select(length > 1) | .[0]) | .[]' "$DATASET")"
|
|
110
|
+
if [ -n "$DUPES" ]; then
|
|
111
|
+
echo "sample.sh: duplicate ids in $DATASET:" >&2
|
|
112
|
+
echo "$DUPES" | sed 's/^/ /' >&2
|
|
113
|
+
exit 2
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
MISSING="$(jq -r '.samples[] | select(.input == null) | .id' "$DATASET")"
|
|
117
|
+
if [ -n "$MISSING" ]; then
|
|
118
|
+
echo "sample.sh: these samples have no \"input\":" >&2
|
|
119
|
+
echo "$MISSING" | sed 's/^/ /' >&2
|
|
120
|
+
exit 2
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# The sample
|
|
125
|
+
#
|
|
126
|
+
# Lehmer's generator gives the shuffle: s = s * 48271 mod 2^31-1, which stays
|
|
127
|
+
# exact in a double and so gives the same order on every machine jq runs on.
|
|
128
|
+
# Strata are interleaved rather than concatenated, which is what makes a short
|
|
129
|
+
# budget a cross-section; rubric-bearing samples sort to the front of each.
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
PROGRAM='
|
|
133
|
+
def lehmer($s): ($s * 48271) % 2147483647;
|
|
134
|
+
|
|
135
|
+
(($seed | tonumber) % 2147483646 + 1) as $s0
|
|
136
|
+
| [ .samples[]
|
|
137
|
+
| select($class == "" or (.class // "") == $class)
|
|
138
|
+
| select($complexity == "" or (.complexity // "") == $complexity)
|
|
139
|
+
| select($rubric == "0" or ((.rubric // []) | length) > 0) ] as $in
|
|
140
|
+
| [ foreach range(0; ($in | length)) as $i ($s0; lehmer(.); .) ] as $r
|
|
141
|
+
| [ range(0; ($in | length))
|
|
142
|
+
| . as $i
|
|
143
|
+
| { s: $in[$i],
|
|
144
|
+
k: $r[$i],
|
|
145
|
+
st: ((($in[$i].class // "unclassified") + " / "
|
|
146
|
+
+ ($in[$i].complexity // "unrated"))),
|
|
147
|
+
rb: (if (($in[$i].rubric // []) | length) > 0 then 0 else 1 end) } ]
|
|
148
|
+
| group_by(.st)
|
|
149
|
+
| map(sort_by(.rb, .k))
|
|
150
|
+
| . as $g
|
|
151
|
+
| ((map(length) | max) // 0) as $m
|
|
152
|
+
| [ range(0; $m) as $i | $g[] | select(length > $i) | .[$i] ]
|
|
153
|
+
| (if ($n | tonumber) > 0 then .[0:($n | tonumber)] else . end)
|
|
154
|
+
| .[].s
|
|
155
|
+
'
|
|
156
|
+
|
|
157
|
+
SELECTED="$(jq -c \
|
|
158
|
+
--arg class "$CLASS" \
|
|
159
|
+
--arg complexity "$COMPLEXITY" \
|
|
160
|
+
--arg rubric "$RUBRIC_ONLY" \
|
|
161
|
+
--arg seed "$SEED" \
|
|
162
|
+
--arg n "$LIMIT" \
|
|
163
|
+
"$PROGRAM" "$DATASET")"
|
|
164
|
+
|
|
165
|
+
COUNT="$(printf '%s' "$SELECTED" | grep -c . || true)"
|
|
166
|
+
if [ "$COUNT" -eq 0 ]; then
|
|
167
|
+
echo "sample.sh: nothing matched" >&2
|
|
168
|
+
if [ -n "$CLASS" ] || [ -n "$COMPLEXITY" ] || [ "$RUBRIC_ONLY" -eq 1 ]; then
|
|
169
|
+
echo " the filters may be narrower than the dataset; what is in it:" >&2
|
|
170
|
+
jq -r '.samples[] | " " + (.class // "unclassified") + " / "
|
|
171
|
+
+ (.complexity // "unrated")' "$DATASET" | sort | uniq -c >&2
|
|
172
|
+
fi
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# The batch file itself. Only `id` and `input`: a rubric is not part of a run
|
|
177
|
+
# request, and the dataset stays the one place it is written down. The id is
|
|
178
|
+
# the join back to it, and the name of the item's directory in the batch.
|
|
179
|
+
CASES="$(printf '%s\n' "$SELECTED" | jq -s '{ batch: map({ id, input }) }')"
|
|
180
|
+
|
|
181
|
+
if [ -n "$OUT" ]; then
|
|
182
|
+
mkdir -p "$(dirname "$OUT")"
|
|
183
|
+
printf '%s\n' "$CASES" > "$OUT"
|
|
184
|
+
else
|
|
185
|
+
printf '%s\n' "$CASES"
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# What was taken, and from where, on stderr so it survives a redirect of the
|
|
189
|
+
# cases file. A round that looks lopsided here will grade lopsided too.
|
|
190
|
+
{
|
|
191
|
+
echo
|
|
192
|
+
echo "sample.sh: $COUNT of $(jq '.samples | length' "$DATASET") samples${OUT:+ -> $OUT}"
|
|
193
|
+
printf '%s\n' "$SELECTED" \
|
|
194
|
+
| jq -r '" " + (.class // "unclassified") + " / " + (.complexity // "unrated")
|
|
195
|
+
+ (if ((.rubric // []) | length) > 0 then " [rubric]" else "" end)' \
|
|
196
|
+
| sort | uniq -c
|
|
197
|
+
GRADED="$(printf '%s\n' "$SELECTED" | jq -r 'select(((.rubric // []) | length) > 0) | .id' | grep -c . || true)"
|
|
198
|
+
echo " $GRADED with a rubric, seed $SEED"
|
|
199
|
+
} >&2
|