@giamme/forge 0.11.0 → 0.12.0
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 +47 -0
- package/SKILL.md +4 -2
- package/VERSION +1 -1
- package/docs/artifacts.md +46 -12
- package/docs/commands.md +48 -9
- package/docs/decomposed-runs.md +43 -2
- package/docs/forge-ui.md +27 -6
- package/docs/reviews.md +31 -6
- package/package.json +1 -1
- package/references/dataset.md +3 -1
- package/references/decompose.md +159 -17
- package/references/harnesses.md +13 -1
- package/references/ui.md +14 -4
- package/scripts/forge-control.py +268 -25
- package/scripts/forge-dispatch.sh +80 -3
- package/scripts/forge-guard.py +156 -15
- package/scripts/forge-issues.py +1 -1
- package/scripts/forge-lib-plans.sh +9 -3
- package/scripts/forge-lib-review.sh +22 -3
- package/scripts/forge-lib-task.sh +51 -4
- package/scripts/forge-lib-verify.sh +137 -6
- package/scripts/forge-parallel-options.sh +59 -0
- package/scripts/forge-parallel.sh +212 -28
- package/scripts/forge-ripwire.py +1 -1
- package/scripts/forge-route.py +157 -5
- package/scripts/forge-schedule.py +687 -50
- package/scripts/forge-ui.py +8 -4
- package/scripts/forge-verify.py +878 -0
- package/scripts/forge_dataset/forecast.py +29 -4
- package/scripts/forge_dataset/harvest.py +10 -4
- package/scripts/forge_verify_reports.py +225 -0
- package/ui/README.md +19 -4
- package/ui/docs/api.md +139 -25
- package/ui/forge_ui/control.py +26 -7
- package/ui/forge_ui/export.py +20 -7
- package/ui/forge_ui/reader.py +307 -11
- package/ui/forge_ui/server.py +16 -0
- package/ui/forge_ui/static/app.js +2 -1
- package/ui/forge_ui/static/controls.js +217 -33
- package/ui/forge_ui/static/findingsview.js +37 -12
- package/ui/forge_ui/static/gantt.js +14 -9
- package/ui/forge_ui/static/index.html +1 -0
- package/ui/forge_ui/static/panel.js +2 -1
- package/ui/forge_ui/static/runmodel.js +92 -11
- package/ui/forge_ui/static/scene3d.js +9 -5
- package/ui/forge_ui/static/settings.js +48 -12
- package/ui/forge_ui/static/style.css +60 -0
- package/ui/forge_ui/static/verifyview.js +324 -0
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
# forge-parallel.sh split <plan-dir> <new-plan> --tasks <id,id> [--dry-run]
|
|
19
19
|
# forge-parallel.sh combine <new-plan> <plan-a> <plan-b> [<plan>...] [--verify <command>] [--dry-run]
|
|
20
20
|
# forge-parallel.sh _task <plan-dir> <task-id> (internal; coordinator target)
|
|
21
|
+
# forge-parallel.sh _qa_only <plan-dir> <task-id> (internal; exit 0 when the next entry only re-runs QA)
|
|
22
|
+
# forge-parallel.sh _waves <plan-dir> (internal; rewrite waves.tsv after a live `forge control add`)
|
|
21
23
|
#
|
|
22
24
|
# Routing flags (plan):
|
|
23
25
|
# --dwarf <spec> fallback for any difficulty tier not named below
|
|
@@ -430,8 +432,9 @@ EOF
|
|
|
430
432
|
# parallelism: two dwarves editing one file produce a conflict no reviewer can
|
|
431
433
|
# untangle, so they get separate waves no matter how the goal was decomposed.
|
|
432
434
|
compute_waves() {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
# Written to temp files and renamed only on success: a cycle leaves the previous files.
|
|
436
|
+
local PLAN="$1" T="$1/tasks.tsv" W="$1/.waves.tsv.$$" D="$1/.deferrals.txt.$$"
|
|
437
|
+
: > "$W"; : > "$D"
|
|
435
438
|
local remaining done_ids wave n id deps files wave_files ok d progressed
|
|
436
439
|
remaining="$(all_ids "$T" | tr '\n' ' ')"
|
|
437
440
|
done_ids=" "; wave=0
|
|
@@ -450,7 +453,7 @@ compute_waves() {
|
|
|
450
453
|
if [ "$ok" = 1 ] && lists_overlap "$files" "$wave_files"; then
|
|
451
454
|
ok=0
|
|
452
455
|
printf 'wave %s: %s deferred (file overlap with a task already in this wave)\n' \
|
|
453
|
-
"$wave" "$id" >> "$
|
|
456
|
+
"$wave" "$id" >> "$D"
|
|
454
457
|
fi
|
|
455
458
|
if [ "$ok" = 1 ]; then
|
|
456
459
|
printf '%s\t%s\n' "$wave" "$id" >> "$W"
|
|
@@ -460,10 +463,11 @@ compute_waves() {
|
|
|
460
463
|
next_remaining="$next_remaining $id"
|
|
461
464
|
fi
|
|
462
465
|
done
|
|
463
|
-
[ "$progressed" = 1 ] || die "dependency cycle or unsatisfiable deps among:$next_remaining"
|
|
466
|
+
[ "$progressed" = 1 ] || { rm -f "$W" "$D"; die "dependency cycle or unsatisfiable deps among:$next_remaining"; }
|
|
464
467
|
for id in $(awk -F'\t' -v w="$wave" '$1==w {print $2}' "$W"); do done_ids="$done_ids$id "; done
|
|
465
468
|
remaining="$next_remaining"
|
|
466
469
|
done
|
|
470
|
+
mv -f "$D" "$PLAN/deferrals.txt" && mv -f "$W" "$PLAN/waves.tsv"
|
|
467
471
|
}
|
|
468
472
|
|
|
469
473
|
render_table() {
|
|
@@ -1171,7 +1175,7 @@ do_retry() {
|
|
|
1171
1175
|
--dwarf) NEWDWARF="${2:?--dwarf needs a spec}"; shift 2 ;;
|
|
1172
1176
|
--yolo-dwarf) touch "$PLAN/yolo_dwarf"; shift ;;
|
|
1173
1177
|
--yolo-qa) touch "$PLAN/yolo_qa"; shift ;;
|
|
1174
|
-
--verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; shift 2 ;;
|
|
1178
|
+
--verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; rm -rf "$PLAN/verify.d"; PP_LANES_FRESH=0; shift 2 ;;
|
|
1175
1179
|
--setup) printf '%s' "${2:?--setup needs a command}" > "$PLAN/setup_cmd"; shift 2 ;;
|
|
1176
1180
|
*)
|
|
1177
1181
|
# `case` is not a loop, so `continue` here belongs to the enclosing while.
|
|
@@ -1210,6 +1214,9 @@ do_retry() {
|
|
|
1210
1214
|
fi
|
|
1211
1215
|
[ "${FORGE_RIPWIRE:-}" != off ] || touch "$PLAN/no_ripwire"
|
|
1212
1216
|
dependencies_ready "$PLAN" "$id" || { write_results "$PLAN"; return 5; }
|
|
1217
|
+
local vrepo vcheckout; vrepo="$(cat "$PLAN/repo" 2>/dev/null)"; vcheckout="$(cat "$PLAN/wt_root")/_integration"
|
|
1218
|
+
[ -d "$vcheckout" ] || vcheckout="$vrepo"
|
|
1219
|
+
verify_lanes_check "$PLAN" "$vrepo" "$vcheckout"
|
|
1213
1220
|
[ ! -f "$PLAN/no_ripwire" ] || export FORGE_RIPWIRE=off
|
|
1214
1221
|
preflight_plan "$PLAN" || return $?
|
|
1215
1222
|
/bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" || true
|
|
@@ -1286,14 +1293,104 @@ verify_run_note() { # verify_run_note <out> <kind> <rc> <before> <after> <log> <
|
|
|
1286
1293
|
"$(( $(date +%s) - $7 ))" "verification.run-$n.log" >> "$out/verification.runs.tsv"
|
|
1287
1294
|
}
|
|
1288
1295
|
|
|
1289
|
-
|
|
1296
|
+
# --- stoppable verification --------------------------------------------------
|
|
1297
|
+
# `run` sets FORGE_RUN_STOPPABLE for the verification it supervises. Then the verify command
|
|
1298
|
+
# runs as its own process group, and a TERM to the verification subshell stops that whole
|
|
1299
|
+
# group: TERM at once, KILL after a grace period. `retry` and `integrate` leave it unset and
|
|
1300
|
+
# keep the plain foreground command.
|
|
1301
|
+
verify_group_stop() { # TERM the verify command's group; KILL it after FORGE_VERIFY_GRACE seconds
|
|
1302
|
+
[ -n "$FORGE_VERIFY_GROUP" ] && [ -z "$FORGE_VERIFY_TIMER" ] || return 0
|
|
1303
|
+
local group="$FORGE_VERIFY_GROUP" termed=1
|
|
1304
|
+
# Until the guard's setsid there is no group: the TERM goes to the guard itself, which dies
|
|
1305
|
+
# of it before it starts the command. A group that forms all the same gets its TERM from
|
|
1306
|
+
# the timer.
|
|
1307
|
+
kill -TERM -- "-$group" 2>/dev/null || { termed=0; kill -TERM "$group" 2>/dev/null; }
|
|
1308
|
+
# The timer polls in short naps rather than sleeping the whole grace in one: it ends by
|
|
1309
|
+
# itself within a nap of the group's last member, and a long sleep left behind would hold
|
|
1310
|
+
# the plan lock it inherited.
|
|
1311
|
+
( trap - TERM INT
|
|
1312
|
+
ticks=$((FORGE_VERIFY_GRACE * 10))
|
|
1313
|
+
while kill -0 -- "-$group" 2>/dev/null || { [ "$termed" = 0 ] && kill -0 "$group" 2>/dev/null; }; do
|
|
1314
|
+
if [ "$termed" = 0 ] && kill -TERM -- "-$group" 2>/dev/null; then termed=1; fi
|
|
1315
|
+
if [ "$ticks" -le 0 ]; then
|
|
1316
|
+
if [ "$termed" = 1 ]; then kill -KILL -- "-$group" 2>/dev/null; else kill -KILL "$group" 2>/dev/null; fi
|
|
1317
|
+
break
|
|
1318
|
+
fi
|
|
1319
|
+
sleep 0.1
|
|
1320
|
+
ticks=$((ticks - 1))
|
|
1321
|
+
done ) &
|
|
1322
|
+
FORGE_VERIFY_TIMER=$!
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
verify_signal() { # trap handler of a stoppable verification
|
|
1326
|
+
FORGE_VERIFY_STOP=1
|
|
1327
|
+
verify_group_stop
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
verify_exec() { # verify_exec <checkout> <command> <log> [grace seconds, default 5] -> the command's exit code, 130 when stopped
|
|
1331
|
+
local wt="$1" cmd="$2" log="$3" rc=0 group deadline
|
|
1332
|
+
if [ "${FORGE_RUN_STOPPABLE:-0}" != 1 ]; then
|
|
1333
|
+
(cd "$wt" && /bin/bash -c "$cmd") > "$log" 2>&1
|
|
1334
|
+
return $?
|
|
1335
|
+
fi
|
|
1336
|
+
FORGE_VERIFY_GRACE="${4:-5}"
|
|
1337
|
+
[ "$FORGE_VERIFY_STOP" = 1 ] && return 130
|
|
1338
|
+
# A background job ignores SIGINT and SIGQUIT, and the command would inherit that; the
|
|
1339
|
+
# wrapper restores both before the guard puts the command in a session of its own.
|
|
1340
|
+
(cd "$wt" && exec python3 -c 'import os, signal, sys
|
|
1341
|
+
for name in (signal.SIGINT, signal.SIGQUIT):
|
|
1342
|
+
signal.signal(name, signal.SIG_DFL)
|
|
1343
|
+
os.execv(sys.executable, [sys.executable] + sys.argv[1:])' \
|
|
1344
|
+
"$SKILL_DIR/scripts/forge-guard.py" session -- /bin/bash -c "$cmd") > "$log" 2>&1 <&0 &
|
|
1345
|
+
group=$!
|
|
1346
|
+
FORGE_VERIFY_GROUP="$group"
|
|
1347
|
+
[ "$FORGE_VERIFY_STOP" = 1 ] && verify_group_stop
|
|
1348
|
+
# `wait` returns at once when a trapped signal arrives, so wait again until the job is gone.
|
|
1349
|
+
wait "$group" || rc=$?
|
|
1350
|
+
while kill -0 "$group" 2>/dev/null; do rc=0; wait "$group" || rc=$?; done
|
|
1351
|
+
if [ "$FORGE_VERIFY_STOP" = 1 ]; then
|
|
1352
|
+
# Members that outlived the leader hold the plan lock; the KILL timer ends them.
|
|
1353
|
+
deadline=$((SECONDS + FORGE_VERIFY_GRACE + 5))
|
|
1354
|
+
while kill -0 -- "-$group" 2>/dev/null && [ "$SECONDS" -lt "$deadline" ]; do sleep 0.1; done
|
|
1355
|
+
fi
|
|
1356
|
+
FORGE_VERIFY_GROUP=""
|
|
1357
|
+
if [ -n "$FORGE_VERIFY_TIMER" ]; then
|
|
1358
|
+
wait "$FORGE_VERIFY_TIMER" 2>/dev/null
|
|
1359
|
+
FORGE_VERIFY_TIMER=""
|
|
1360
|
+
fi
|
|
1361
|
+
[ "$FORGE_VERIFY_STOP" = 1 ] && return 130
|
|
1362
|
+
return "$rc"
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
verify_mark_interrupted() { # verify_mark_interrupted <out>
|
|
1366
|
+
echo INTERRUPTED > "$1/verification.status"
|
|
1367
|
+
echo 130 > "$1/verification.exit"
|
|
1368
|
+
note "verification interrupted; the verify command was stopped and is not rerun"
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
# True, once the verification is marked INTERRUPTED, when a stop has arrived. Checked before
|
|
1372
|
+
# each run of the command (a stop that came first runs nothing and records no run) and after.
|
|
1373
|
+
verify_stopped() { # verify_stopped <out>
|
|
1374
|
+
[ "$FORGE_VERIFY_STOP" = 1 ] || return 1
|
|
1375
|
+
verify_mark_interrupted "$1"
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
verify_result() ( verify_result_body "$@" ) # plan, checkout, artifact directory
|
|
1379
|
+
|
|
1380
|
+
# The body is a function of its own so `run` can start it as `( verify_result_body ... ) &`:
|
|
1381
|
+
# that one subshell is the background job, the home of the signal handler above and of
|
|
1382
|
+
# forge_metric_begin's EXIT trap.
|
|
1383
|
+
verify_result_body() { # plan, checkout, artifact directory
|
|
1290
1384
|
local plan="$1" wt="$2" out="$3" cmd="" before after rc=0 repo jev_loaded=0 vretries
|
|
1385
|
+
FORGE_VERIFY_STOP=0 FORGE_VERIFY_GROUP="" FORGE_VERIFY_TIMER="" FORGE_VERIFY_GRACE=5
|
|
1386
|
+
[ "${FORGE_RUN_STOPPABLE:-0}" = 1 ] && trap verify_signal TERM INT
|
|
1291
1387
|
mkdir -p "$out"
|
|
1292
1388
|
forge_metric_begin "$out" verification
|
|
1293
1389
|
forge_metric_phase verification
|
|
1294
1390
|
repo="$(cat "$plan/repo")"
|
|
1295
1391
|
rm -f "$out/verification.flaky" "$out/verification.retry.log" "$out/verification.coverage.txt" \
|
|
1296
|
-
"$out/verification.runs.tsv" "$out"/verification.run-*.log "$out/verification.commit"
|
|
1392
|
+
"$out/verification.runs.tsv" "$out"/verification.run-*.log "$out/verification.commit" \
|
|
1393
|
+
"$out/verification.lanes.json" "$out"/verification.lane.*.log
|
|
1297
1394
|
# The commit being verified, named before any early return, so every outcome (FAIL and
|
|
1298
1395
|
# UNVERIFIED included) can be joined to the code it judged.
|
|
1299
1396
|
git -C "$wt" rev-parse HEAD > "$out/verification.commit" 2>/dev/null || rm -f "$out/verification.commit"
|
|
@@ -1309,12 +1406,20 @@ verify_result() ( # plan, checkout, artifact directory
|
|
|
1309
1406
|
if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
|
|
1310
1407
|
source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
|
|
1311
1408
|
fi
|
|
1312
|
-
|
|
1313
|
-
|
|
1409
|
+
local vsource; vsource="$(verify_source_for "$plan" "$repo")"
|
|
1410
|
+
case "$vsource" in command:*) cmd="$(cat "${vsource#*$'\t'}")" ;; esac
|
|
1314
1411
|
if [ "$(forge_tree "$wt")" != "$(git -C "$wt" rev-parse HEAD^{tree})" ]; then
|
|
1315
1412
|
echo FAIL > "$out/verification.status"
|
|
1316
1413
|
note "verification checkout has source changes outside its commit"; return 1
|
|
1317
1414
|
fi
|
|
1415
|
+
case "$vsource" in
|
|
1416
|
+
lanes:*)
|
|
1417
|
+
if [ "$jev_loaded" = 1 ] && forge_jev_active tests; then
|
|
1418
|
+
note "jev: flake triage applies to a single verify command; lanes rerun their own failures"
|
|
1419
|
+
fi
|
|
1420
|
+
verify_lanes_run "$plan" "$wt" "$out" "${vsource#*$'\t'}" "$vretries"
|
|
1421
|
+
return $? ;;
|
|
1422
|
+
esac
|
|
1318
1423
|
# Only when nothing is configured: ask Jev to name a command that actually exists
|
|
1319
1424
|
# in the repo. Its exit code, not its stdout text, decides whether a judgment was
|
|
1320
1425
|
# produced — any non-zero exit (no key, disabled, nothing suitable, a traceback)
|
|
@@ -1363,10 +1468,12 @@ with open(out + "/verification.jev.json", "w") as fh:
|
|
|
1363
1468
|
printf '%s\n' "$cmd" > "$out/verification.command"
|
|
1364
1469
|
before="$(forge_fingerprint "$wt")" || return 1
|
|
1365
1470
|
local vt0; vt0="$(date +%s)"
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1471
|
+
verify_stopped "$out" && return 130
|
|
1472
|
+
verify_exec "$wt" "$cmd" "$out/verification.log" || rc=$?
|
|
1473
|
+
[ "$FORGE_VERIFY_STOP" = 1 ] || echo "$rc" > "$out/verification.exit"
|
|
1474
|
+
after="$(forge_fingerprint "$wt")" || { verify_stopped "$out" && return 130; return 1; }
|
|
1369
1475
|
verify_run_note "$out" initial "$rc" "$before" "$after" verification.log "$vt0"
|
|
1476
|
+
verify_stopped "$out" && return 130
|
|
1370
1477
|
echo "$before" > "$out/verification.fingerprint"
|
|
1371
1478
|
if [ "$rc" != 0 ] || [ "$before" != "$after" ]; then
|
|
1372
1479
|
local flaked=0 det_retried=0 n_retry=0 rc3 b3 a3
|
|
@@ -1379,9 +1486,11 @@ with open(out + "/verification.jev.json", "w") as fh:
|
|
|
1379
1486
|
note "verification failed (exit $rc); rerunning to tell a flaky lane from a regression ($n_retry/$vretries)"
|
|
1380
1487
|
b3="$(forge_fingerprint "$wt")" || return 1
|
|
1381
1488
|
vt0="$(date +%s)"
|
|
1382
|
-
|
|
1383
|
-
|
|
1489
|
+
verify_stopped "$out" && return 130
|
|
1490
|
+
verify_exec "$wt" "$cmd" "$out/verification.retry.log" || rc3=$?
|
|
1491
|
+
a3="$(forge_fingerprint "$wt")" || { verify_stopped "$out" && return 130; return 1; }
|
|
1384
1492
|
verify_run_note "$out" rerun "$rc3" "$b3" "$a3" verification.retry.log "$vt0"
|
|
1493
|
+
verify_stopped "$out" && return 130
|
|
1385
1494
|
if [ "$rc3" = 0 ] && [ "$b3" = "$a3" ]; then
|
|
1386
1495
|
flaked=1
|
|
1387
1496
|
{
|
|
@@ -1446,10 +1555,12 @@ print(v if v is not None else "0.90")
|
|
|
1446
1555
|
note "jev: known_flake (confidence $confidence >= $flake_act), corroborated by a recorded trap — retrying once"
|
|
1447
1556
|
before2="$(forge_fingerprint "$wt")" || return 1
|
|
1448
1557
|
vt0="$(date +%s)"
|
|
1449
|
-
|
|
1558
|
+
verify_stopped "$out" && return 130
|
|
1559
|
+
verify_exec "$wt" "$cmd" "$out/verification.retry.log" || rc2=$?
|
|
1450
1560
|
rc2="${rc2:-0}"
|
|
1451
|
-
after2="$(forge_fingerprint "$wt")" || return 1
|
|
1561
|
+
after2="$(forge_fingerprint "$wt")" || { verify_stopped "$out" && return 130; return 1; }
|
|
1452
1562
|
verify_run_note "$out" jev_retry "$rc2" "$before2" "$after2" verification.retry.log "$vt0"
|
|
1563
|
+
verify_stopped "$out" && return 130
|
|
1453
1564
|
if [ "$rc2" = 0 ] && [ "$before2" = "$after2" ]; then
|
|
1454
1565
|
flaked=1
|
|
1455
1566
|
note "jev: retry passed — treating as a corroborated flake, not a regression"
|
|
@@ -1490,7 +1601,7 @@ with open(path, "w") as fh:
|
|
|
1490
1601
|
return 1
|
|
1491
1602
|
fi
|
|
1492
1603
|
echo PASS > "$out/verification.status"
|
|
1493
|
-
|
|
1604
|
+
}
|
|
1494
1605
|
|
|
1495
1606
|
write_results() { # write_results <plan>
|
|
1496
1607
|
local PLAN="$1" T="$1/tasks.tsv" run_id id
|
|
@@ -1511,7 +1622,29 @@ forecast_report() {
|
|
|
1511
1622
|
}
|
|
1512
1623
|
|
|
1513
1624
|
# --- run ----------------------------------------------------------------------
|
|
1514
|
-
|
|
1625
|
+
# Waits for a background job and, when a trapped signal ends the wait early, waits again
|
|
1626
|
+
# until the job is gone; its exit code is left in run_rc.
|
|
1627
|
+
run_wait() { # run_wait <pid>
|
|
1628
|
+
run_rc=0
|
|
1629
|
+
wait "$1" || run_rc=$?
|
|
1630
|
+
while kill -0 "$1" 2>/dev/null; do run_rc=0; wait "$1" || run_rc=$?; done
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
# The handler of do_run's subshell: a signal reaches the job in progress (the scheduler,
|
|
1634
|
+
# then the verification) as TERM. Before any job exists there is nothing to clean up.
|
|
1635
|
+
run_signal() {
|
|
1636
|
+
run_stop=1
|
|
1637
|
+
if [ -n "$run_child" ]; then kill -TERM "$run_child" 2>/dev/null
|
|
1638
|
+
elif [ "$run_phase" = pre ]; then exit 130; fi
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
# Not a ( ) function: main runs it as `( do_run ... ) &`, one subshell that is both the
|
|
1642
|
+
# background job and the home of its signal handler. A function called with `&` would be a
|
|
1643
|
+
# second subshell level, and an async function does not run its EXIT trap.
|
|
1644
|
+
do_run() {
|
|
1645
|
+
local run_child="" run_stop=0 run_phase=pre run_rc=0 interrupted=0 vstopped=0
|
|
1646
|
+
FORGE_RUN_STOPPABLE=1
|
|
1647
|
+
trap run_signal TERM INT
|
|
1515
1648
|
local PLAN="${1:?run needs a plan dir}"; shift
|
|
1516
1649
|
local fractal_pipeline_args=("$@")
|
|
1517
1650
|
local MAXP=3 DRY=0 jev_rc=0 FORECAST=1
|
|
@@ -1527,7 +1660,7 @@ do_run() (
|
|
|
1527
1660
|
--max-parallel) MAXP="${2:?}"; shift 2 ;;
|
|
1528
1661
|
--yolo-dwarf) touch "$PLAN/yolo_dwarf"; shift ;;
|
|
1529
1662
|
--yolo-qa) touch "$PLAN/yolo_qa"; shift ;;
|
|
1530
|
-
--verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; shift 2 ;;
|
|
1663
|
+
--verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; rm -rf "$PLAN/verify.d"; PP_LANES_FRESH=0; shift 2 ;;
|
|
1531
1664
|
--setup) printf '%s' "${2:?--setup needs a command}" > "$PLAN/setup_cmd"; shift 2 ;;
|
|
1532
1665
|
--dry-run) DRY=1; shift ;;
|
|
1533
1666
|
*)
|
|
@@ -1623,22 +1756,46 @@ do_run() (
|
|
|
1623
1756
|
# Also before any model is paid for: what this plan is likely to cost, and which tasks
|
|
1624
1757
|
# history says may bounce. Advisory, budgeted and silent without history; it never stops a run.
|
|
1625
1758
|
[ "$FORECAST" = 1 ] && forecast_report "$PLAN"
|
|
1759
|
+
# Invalid verification lanes stop the run here, before any model is paid for.
|
|
1760
|
+
verify_lanes_check "$PLAN" "$REPO" "$int_wt"
|
|
1626
1761
|
# Say now, before any model is paid for, which suites the verify command will not run.
|
|
1627
1762
|
verify_coverage_report "$REPO" "$(verify_command_for "$PLAN" "$REPO")" "$PLAN/verification.coverage.txt"
|
|
1628
1763
|
local sched_rc=0 paused=0 sched_args=()
|
|
1629
1764
|
[ "$PP_RETRY_FAILED" -gt 0 ] && sched_args+=(--retry-failed "$PP_RETRY_FAILED")
|
|
1630
1765
|
[ "$PP_INFRA_RETRIES" -gt 0 ] && sched_args+=(--infra-retries "$PP_INFRA_RETRIES")
|
|
1631
1766
|
forge_metric_phase dispatch
|
|
1632
|
-
|
|
1767
|
+
# In the background so a signal to this shell ends the wait; the scheduler gets it as TERM,
|
|
1768
|
+
# stops its tasks and returns 130.
|
|
1769
|
+
run_phase=scheduler
|
|
1770
|
+
python3 "$SKILL_DIR/scripts/forge-schedule.py" "$SELF" "$PLAN" "$MAXP" ${sched_args[@]+"${sched_args[@]}"} <&0 &
|
|
1771
|
+
run_child=$!
|
|
1772
|
+
[ "$run_stop" = 1 ] && kill -TERM "$run_child" 2>/dev/null
|
|
1773
|
+
run_wait "$run_child"; sched_rc=$run_rc
|
|
1774
|
+
run_child=""
|
|
1633
1775
|
[ "$sched_rc" != 0 ] && failed=1
|
|
1634
1776
|
[ "$sched_rc" = 8 ] && paused=1
|
|
1635
1777
|
|
|
1636
1778
|
forge_metric_phase verification
|
|
1779
|
+
# Counted after that phase call: a signal that arrived while it ran is handled only once it
|
|
1780
|
+
# returned, and must still keep verification from starting.
|
|
1781
|
+
[ "$sched_rc" = 130 ] || [ "$run_stop" = 1 ] && interrupted=1
|
|
1637
1782
|
# An infrastructure pause leaves the run unfinished, so there is nothing yet worth
|
|
1638
|
-
# verifying; the next `run` resumes and verifies at its end.
|
|
1639
|
-
|
|
1640
|
-
|
|
1783
|
+
# verifying; the next `run` resumes and verifies at its end. An interrupted run has
|
|
1784
|
+
# merged only part of its work: a verification of that is not worth its minutes, and it
|
|
1785
|
+
# would run again as the rerun if it were killed.
|
|
1786
|
+
if [ "$paused" = 0 ] && [ "$interrupted" = 0 ]; then
|
|
1787
|
+
run_phase=verification
|
|
1788
|
+
( verify_result_body "$PLAN" "$int_wt" "$PLAN" ) <&0 &
|
|
1789
|
+
run_child=$!
|
|
1790
|
+
[ "$run_stop" = 1 ] && kill -TERM "$run_child" 2>/dev/null
|
|
1791
|
+
run_wait "$run_child"
|
|
1792
|
+
[ "$run_rc" != 0 ] && failed=1
|
|
1793
|
+
[ "$run_rc" = 130 ] && vstopped=1
|
|
1794
|
+
run_child=""
|
|
1795
|
+
# A stop that killed the subshell before its handler was set leaves no record: it counts too.
|
|
1796
|
+
[ "$vstopped" = 1 ] || [ "$run_stop" = 1 ] && interrupted=1
|
|
1641
1797
|
fi
|
|
1798
|
+
run_phase=results
|
|
1642
1799
|
write_results "$PLAN"
|
|
1643
1800
|
# Only prune worktrees for work that is safely merged, and only after the record
|
|
1644
1801
|
# of it has been written and read back. A failed task keeps its worktree and
|
|
@@ -1670,15 +1827,27 @@ do_run() (
|
|
|
1670
1827
|
done
|
|
1671
1828
|
[ "$drift" = 1 ] && echo
|
|
1672
1829
|
|
|
1830
|
+
# From this run's own flags, never from a verification.status an earlier run left.
|
|
1831
|
+
if [ "$interrupted" = 1 ]; then
|
|
1832
|
+
if [ "$vstopped" = 1 ]; then echo "interrupted during verification, which was stopped and not rerun."
|
|
1833
|
+
else echo "interrupted; verification was skipped."; fi
|
|
1834
|
+
fi
|
|
1835
|
+
local stopped_ids; stopped_ids="$(awk -F'\t' '$2=="INTERRUPTED" {print $1}' "$PLAN/results.tsv")"
|
|
1836
|
+
if [ -n "$stopped_ids" ]; then
|
|
1837
|
+
echo "A later run does not dispatch an interrupted task; continue each one with:"
|
|
1838
|
+
for id in $stopped_ids; do echo " forge-parallel.sh retry $PLAN $id"; done
|
|
1839
|
+
fi
|
|
1840
|
+
[ "$interrupted" = 1 ] || [ -n "$stopped_ids" ] && echo
|
|
1673
1841
|
# The retry hint is for tasks that actually failed; a run that only failed verification,
|
|
1674
|
-
# or only paused on infrastructure, has nothing to retry task by task.
|
|
1675
|
-
|
|
1842
|
+
# or only paused on infrastructure, has nothing to retry task by task. An interrupted task
|
|
1843
|
+
# has its own hint above.
|
|
1844
|
+
if [ "$(awk -F'\t' '$2!="MERGED" && $2!="INFRA" && $2!="PENDING" && $2!="BLOCKED" && $2!="INTERRUPTED" {n++} END {print n+0}' "$PLAN/results.tsv")" -gt 0 ]; then
|
|
1676
1845
|
echo "retry a failed task with its reviewer's findings:"
|
|
1677
1846
|
echo " forge-parallel.sh retry $PLAN <task-id> [--dwarf <spec>]"
|
|
1678
1847
|
echo "or let a run retry them itself: forge-parallel.sh run $PLAN --retry-failed 1"
|
|
1679
1848
|
echo
|
|
1680
1849
|
fi
|
|
1681
|
-
if [ "$failed" = 1 ] && [ "$paused" = 0 ] && [ "$(cat "$PLAN/verification.status" 2>/dev/null)" = FAIL ]; then
|
|
1850
|
+
if [ "$failed" = 1 ] && [ "$paused" = 0 ] && [ "$interrupted" = 0 ] && [ "$(cat "$PLAN/verification.status" 2>/dev/null)" = FAIL ]; then
|
|
1682
1851
|
echo "combined verification failed: see $PLAN/verification.log"
|
|
1683
1852
|
echo
|
|
1684
1853
|
fi
|
|
@@ -1695,10 +1864,11 @@ do_run() (
|
|
|
1695
1864
|
else
|
|
1696
1865
|
echo "Your branch and working tree were not touched."
|
|
1697
1866
|
fi
|
|
1867
|
+
[ "$interrupted" = 1 ] && return 130
|
|
1698
1868
|
[ "$paused" = 1 ] && return 8
|
|
1699
1869
|
[ "$failed" = 1 ] && return 5
|
|
1700
1870
|
return 0
|
|
1701
|
-
|
|
1871
|
+
}
|
|
1702
1872
|
|
|
1703
1873
|
# --- integrate ----------------------------------------------------------------
|
|
1704
1874
|
do_integrate() {
|
|
@@ -1789,7 +1959,19 @@ esac
|
|
|
1789
1959
|
need_fn() { declare -F "$1" >/dev/null || die "'$CMD' is not available: $1 is missing" 3; }
|
|
1790
1960
|
case "$CMD" in
|
|
1791
1961
|
plan) do_plan "$@"; rc=$?; forge_update_notice; exit "$rc" ;;
|
|
1792
|
-
run)
|
|
1962
|
+
run)
|
|
1963
|
+
# The signal handler lives here, at the pid the user started: do_run is a subshell, and
|
|
1964
|
+
# bash runs a trap only between commands, so it is started in the background and waited
|
|
1965
|
+
# for. A signal goes to it as TERM; do_run stops the scheduler or the verification.
|
|
1966
|
+
run_signalled=0 run_job=""
|
|
1967
|
+
trap 'run_signalled=1; [ -n "$run_job" ] && kill -TERM "$run_job" 2>/dev/null' TERM INT
|
|
1968
|
+
( do_run "$@" ) <&0 &
|
|
1969
|
+
run_job=$!
|
|
1970
|
+
[ "$run_signalled" = 1 ] && kill -TERM "$run_job" 2>/dev/null
|
|
1971
|
+
run_wait "$run_job"; rc=$run_rc
|
|
1972
|
+
trap - TERM INT
|
|
1973
|
+
[ "$run_signalled" = 1 ] && [ "$rc" -gt 128 ] && rc=130
|
|
1974
|
+
forge_update_notice; exit "$rc" ;;
|
|
1793
1975
|
retry) do_retry "$@" ;;
|
|
1794
1976
|
integrate) do_integrate "$@" ;;
|
|
1795
1977
|
accept) need_fn do_accept; do_accept "$@" ;;
|
|
@@ -1799,5 +1981,7 @@ case "$CMD" in
|
|
|
1799
1981
|
_task) do_task "$@" ;;
|
|
1800
1982
|
_merge) merge_task "$@" ;;
|
|
1801
1983
|
_prepare_retry) retry_for_scheduler "$@" ;;
|
|
1984
|
+
_qa_only) qa_only_next "$@" ;;
|
|
1985
|
+
_waves) compute_waves "$@" ;;
|
|
1802
1986
|
*) die "unknown subcommand '$CMD'" ;;
|
|
1803
1987
|
esac
|
package/scripts/forge-ripwire.py
CHANGED
|
@@ -52,7 +52,7 @@ def bounded(command, cwd, deadline, limit=16384):
|
|
|
52
52
|
# Also kill descendants that closed their pipes before the parent exited.
|
|
53
53
|
try:
|
|
54
54
|
os.killpg(process.pid, signal.SIGKILL)
|
|
55
|
-
except ProcessLookupError:
|
|
55
|
+
except (ProcessLookupError, PermissionError): # macOS: EPERM for a group of zombies only
|
|
56
56
|
pass
|
|
57
57
|
process.wait()
|
|
58
58
|
streams.close()
|
package/scripts/forge-route.py
CHANGED
|
@@ -20,9 +20,15 @@ bias (inverse-propensity weighting needs the probability each choice had).
|
|
|
20
20
|
The retry policy: after <plan>/escalate_after (default 3; 0 = off) failed attempts on
|
|
21
21
|
one spec, the next attempt goes to the next tier up of the user's pools instead of
|
|
22
22
|
repeating the same dispatch. Prints that spec when one is due. When none exists, writes
|
|
23
|
-
tasks/<id>/escalation.stop and split.suggested and exits 3.
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
tasks/<id>/escalation.stop and split.suggested and exits 3. When <plan>/escalate_effort
|
|
24
|
+
contains `on` and no higher tier has another model, the same alias and harness is tried
|
|
25
|
+
once more at the next effort its pairing accepts (weakest above the current one; a spec
|
|
26
|
+
that names no effort counts as medium, a dwarf dispatch's default) before that stop.
|
|
27
|
+
The streak then counts per effort, so the new effort starts again. At the pairing's
|
|
28
|
+
highest effort the task still stops, and split.suggested says that effort was tried too.
|
|
29
|
+
With the file absent the streak and the stop are exactly as before. `--check` (run after
|
|
30
|
+
every attempt) also records a due escalation in escalation.next, and clears the markers
|
|
31
|
+
once the streak no longer holds. A pending tasks/<id>/escalation.pin (a human's one-shot
|
|
26
32
|
choice, applied by prepare_retry) is what `--check` reports as the next model instead
|
|
27
33
|
(escalation.next = its spec), whatever escalate_after says, unless the next retry is a
|
|
28
34
|
QA-only resume or the plan is Fractal (pins never apply there). `--qa-only-next` is
|
|
@@ -60,6 +66,7 @@ import os
|
|
|
60
66
|
import random
|
|
61
67
|
import secrets
|
|
62
68
|
import stat
|
|
69
|
+
import subprocess
|
|
63
70
|
import sys
|
|
64
71
|
import tempfile
|
|
65
72
|
import time
|
|
@@ -71,7 +78,12 @@ REGISTRY = SCRIPTS.parent / 'registry.tsv'
|
|
|
71
78
|
FAILED = ('FAIL', 'TIMEOUT') # a judgment against the spec that ran
|
|
72
79
|
NEUTRAL = ('INFRA', 'UNKNOWN', 'ERROR', 'RUNNING', 'INTERRUPTED') # say nothing about it
|
|
73
80
|
DEFAULT_ESCALATE_AFTER = 3
|
|
81
|
+
DEFAULT_EFFORT = 'medium' # forge-dispatch.sh dwarf default
|
|
74
82
|
LOCK_WAIT = 5.0 # seconds to wait for P/control.lock
|
|
83
|
+
# forge-dispatch.sh rank_of. Only for "which accepted effort is above this one"; the set of
|
|
84
|
+
# efforts a pairing accepts comes from `forge-dispatch.sh efforts`, not from this table.
|
|
85
|
+
# ultra and ultracode share a rank: each is its own harness's top, under different names.
|
|
86
|
+
EFFORT_RANK = {'low': 0, 'medium': 1, 'high': 2, 'xhigh': 3, 'max': 4, 'ultra': 5, 'ultracode': 5}
|
|
75
87
|
|
|
76
88
|
|
|
77
89
|
def rng_for(task: str):
|
|
@@ -209,6 +221,64 @@ def stronger(spec: str, pools: dict, registry: dict):
|
|
|
209
221
|
return None, 'it is already in the highest tier of this plan\'s pools (%s)' % TIERS[rank]
|
|
210
222
|
|
|
211
223
|
|
|
224
|
+
# --- capacity fallback ---------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def pair_of(spec: str, registry: dict):
|
|
227
|
+
"""(harness, model id) a spec runs on, as forge-dispatch.sh's lookup resolves it; None when it cannot."""
|
|
228
|
+
parts = str(spec).split(':')
|
|
229
|
+
alias, harness = parts[0], ':'.join(parts[2:])
|
|
230
|
+
rows = registry.get(alias)
|
|
231
|
+
if rows:
|
|
232
|
+
if not harness:
|
|
233
|
+
return next(iter(rows.items()))
|
|
234
|
+
return harness, rows.get(harness, alias)
|
|
235
|
+
return (harness, alias) if harness else None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_excluded(text: str) -> set:
|
|
239
|
+
"""`harness/model` items, comma-separated; `harness/*` excludes every model on that harness."""
|
|
240
|
+
out = set()
|
|
241
|
+
for item in (text or '').split(','):
|
|
242
|
+
harness, slash, model = item.strip().partition('/')
|
|
243
|
+
if harness and slash and model:
|
|
244
|
+
out.add((harness, model))
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def fallback_pick(current: str, pools: dict, registry: dict, excluded: set):
|
|
249
|
+
"""The next member of the current dwarf's own tier pool, in pool order and wrapping around,
|
|
250
|
+
that runs another model on a pair nobody paused; None when there is none."""
|
|
251
|
+
rank = rank_of(current, pools, registry)
|
|
252
|
+
if rank is None:
|
|
253
|
+
return None
|
|
254
|
+
pool = pools[TIERS[rank]]
|
|
255
|
+
own = canonical(current, registry)
|
|
256
|
+
keys = [canonical(s, registry) for s in pool]
|
|
257
|
+
start = keys.index(own) if own in keys else -1
|
|
258
|
+
for step in range(1, len(pool) + 1):
|
|
259
|
+
candidate = pool[(start + step) % len(pool)]
|
|
260
|
+
pair = pair_of(candidate, registry)
|
|
261
|
+
if canonical(candidate, registry) == own or pair is None:
|
|
262
|
+
continue
|
|
263
|
+
if pair in excluded or (pair[0], '*') in excluded:
|
|
264
|
+
continue
|
|
265
|
+
return candidate
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def fallback(args) -> int:
|
|
270
|
+
plan = Path(os.path.realpath(args.plan))
|
|
271
|
+
current = _dwarf_of(plan, args.task)
|
|
272
|
+
if not current or current in ('-', 'UNASSIGNED'):
|
|
273
|
+
return 1
|
|
274
|
+
pick = fallback_pick(current, load_pools(plan / 'fractal-routing.json'), registry_models(),
|
|
275
|
+
parse_excluded(args.exclude))
|
|
276
|
+
if not pick:
|
|
277
|
+
return 1
|
|
278
|
+
print(pick)
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
212
282
|
# --- exploration -------------------------------------------------------------------
|
|
213
283
|
|
|
214
284
|
def explore(args) -> str:
|
|
@@ -250,20 +320,87 @@ def _dwarf_of(plan: Path, task: str) -> str:
|
|
|
250
320
|
return ''
|
|
251
321
|
|
|
252
322
|
|
|
253
|
-
def
|
|
323
|
+
def _spec_parts(spec: str):
|
|
324
|
+
"""(alias, effort, harness) of alias[:effort[:harness]]. Effort and harness are '' when unnamed."""
|
|
325
|
+
alias, sep, rest = str(spec).partition(':')
|
|
326
|
+
if not sep:
|
|
327
|
+
return alias, '', ''
|
|
328
|
+
effort, sep, harness = rest.partition(':')
|
|
329
|
+
return alias, effort, harness if sep else ''
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _effort_of(spec: str) -> str:
|
|
333
|
+
"""The effort a spec names, or medium when it names none (what a dwarf dispatch runs)."""
|
|
334
|
+
_alias, effort, _harness = _spec_parts(spec)
|
|
335
|
+
return effort or DEFAULT_EFFORT
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _effort_step(plan: Path) -> bool:
|
|
339
|
+
try:
|
|
340
|
+
return (plan / 'escalate_effort').read_text().strip() == 'on'
|
|
341
|
+
except OSError:
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _accepted_efforts(spec: str) -> list:
|
|
346
|
+
"""Efforts `spec`'s pairing accepts, weakest first. Empty when dispatch cannot say."""
|
|
347
|
+
try:
|
|
348
|
+
done = subprocess.run(
|
|
349
|
+
['bash', str(SCRIPTS / 'forge-dispatch.sh'), 'efforts', '--spec', spec],
|
|
350
|
+
capture_output=True, text=True, timeout=30)
|
|
351
|
+
except (OSError, subprocess.SubprocessError):
|
|
352
|
+
return []
|
|
353
|
+
if done.returncode != 0:
|
|
354
|
+
return []
|
|
355
|
+
return [line.strip() for line in done.stdout.splitlines() if line.strip()]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _effective_effort(requested: str, accepted: list):
|
|
359
|
+
"""The effort a dwarf dispatch runs when `requested` is asked of a pairing accepting
|
|
360
|
+
`accepted`: as asked when accepted, else the strongest accepted at or below it, else the
|
|
361
|
+
weakest accepted (forge-dispatch.sh's clamp)."""
|
|
362
|
+
if requested in accepted:
|
|
363
|
+
return requested
|
|
364
|
+
want = EFFORT_RANK.get(requested)
|
|
365
|
+
below = [e for e in accepted if want is not None and EFFORT_RANK.get(e, 99) <= want]
|
|
366
|
+
return below[-1] if below else (accepted[0] if accepted else None)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _next_effort_spec(spec: str):
|
|
370
|
+
"""The same alias and harness at the next accepted effort above the one a dispatch of
|
|
371
|
+
`spec` actually runs, or None."""
|
|
372
|
+
alias, _effort, harness = _spec_parts(spec)
|
|
373
|
+
accepted = _accepted_efforts(spec)
|
|
374
|
+
rank = EFFORT_RANK.get(_effective_effort(_effort_of(spec), accepted) or '')
|
|
375
|
+
if not alias or rank is None:
|
|
376
|
+
return None
|
|
377
|
+
for effort in accepted:
|
|
378
|
+
other = EFFORT_RANK.get(effort)
|
|
379
|
+
if other is not None and other > rank:
|
|
380
|
+
return '%s:%s:%s' % (alias, effort, harness) if harness else '%s:%s' % (alias, effort)
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def failed_streak(rows: list, current: str, registry: dict, by_effort: bool = False) -> int:
|
|
254
385
|
"""Distinct trailing attempt numbers that failed on `current`'s model.
|
|
255
386
|
|
|
256
387
|
rows: (attempt, status, spec-or-None) oldest first. A row without a spec predates the
|
|
257
388
|
column and is taken as the current spec. Infra stops, missing verdicts and forge errors
|
|
258
389
|
judge nothing and are skipped; a pass, a merge or another model ends the streak.
|
|
390
|
+
With by_effort, another effort of the same model ends it too (a bump starts a new
|
|
391
|
+
streak). A spec that names no effort counts as medium. With by_effort off the count
|
|
392
|
+
is by model only, as before.
|
|
259
393
|
"""
|
|
260
394
|
model = canonical(current, registry)
|
|
395
|
+
effort = _effort_of(current) if by_effort else None
|
|
261
396
|
seen = set()
|
|
262
397
|
for attempt, status, spec in reversed(rows):
|
|
263
398
|
if status in NEUTRAL:
|
|
264
399
|
continue
|
|
265
400
|
if spec and canonical(spec, registry) != model:
|
|
266
401
|
break
|
|
402
|
+
if effort is not None and _effort_of(spec or current) != effort:
|
|
403
|
+
break
|
|
267
404
|
if status not in FAILED:
|
|
268
405
|
break
|
|
269
406
|
seen.add(attempt)
|
|
@@ -439,13 +576,19 @@ def _decide(plan: Path, tdir: Path, args, after: int):
|
|
|
439
576
|
if not current or current in ('-', 'UNASSIGNED'):
|
|
440
577
|
return (), (), '', 0
|
|
441
578
|
registry = registry_models()
|
|
442
|
-
|
|
579
|
+
step = _effort_step(plan)
|
|
580
|
+
streak = failed_streak(attempt_rows(tdir), current, registry, by_effort=step)
|
|
443
581
|
if streak < after:
|
|
444
582
|
return MARKERS, (), '', 0
|
|
445
583
|
target, why = stronger(current, load_pools(plan / 'fractal-routing.json'), registry)
|
|
584
|
+
# Only once no higher tier has a model. A tier-up is still the stronger move.
|
|
585
|
+
if target is None and step:
|
|
586
|
+
target = _next_effort_spec(current)
|
|
446
587
|
if target:
|
|
447
588
|
writes = (('escalation.next', target + '\n'),) if args.check else ()
|
|
448
589
|
return ('escalation.stop', 'split.suggested'), writes, target, 0
|
|
590
|
+
if step:
|
|
591
|
+
why = '%s. The highest effort this pairing accepts was tried as well' % why
|
|
449
592
|
split = ('%s failed %d attempts on %s, and %s. Another attempt on the same model rarely passes, '
|
|
450
593
|
'so split the task into smaller ones, or retry it with an explicit --dwarf <spec>.\n'
|
|
451
594
|
% (args.task, streak, current, why))
|
|
@@ -536,6 +679,10 @@ def main(argv=None) -> int:
|
|
|
536
679
|
p.add_argument('--check', action='store_true')
|
|
537
680
|
p.add_argument('--qa-only-next', choices=('yes', 'no'), default=None,
|
|
538
681
|
help='whether the next entry only re-runs QA, as the caller measured it')
|
|
682
|
+
p = sub.add_parser('fallback', help='the same-tier model a capacity stop moves to')
|
|
683
|
+
p.add_argument('--plan', required=True)
|
|
684
|
+
p.add_argument('--task', required=True)
|
|
685
|
+
p.add_argument('--exclude', default='', help='harness/model pairs to skip, comma-separated (harness/* for all)')
|
|
539
686
|
p = sub.add_parser('record', help=argparse.SUPPRESS)
|
|
540
687
|
p.add_argument('--plan', required=True)
|
|
541
688
|
p.add_argument('--task', required=True)
|
|
@@ -551,6 +698,11 @@ def main(argv=None) -> int:
|
|
|
551
698
|
return 0
|
|
552
699
|
if args.command == 'record':
|
|
553
700
|
return record_escalation(args)
|
|
701
|
+
if args.command == 'fallback':
|
|
702
|
+
try:
|
|
703
|
+
return fallback(args)
|
|
704
|
+
except Exception:
|
|
705
|
+
return 1
|
|
554
706
|
try:
|
|
555
707
|
return escalation(args)
|
|
556
708
|
except Exception:
|