@giamme/forge 0.10.0 → 0.11.1
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 +43 -0
- package/SKILL.md +4 -3
- package/VERSION +1 -1
- package/docs/artifacts.md +13 -1
- package/docs/commands.md +11 -5
- package/docs/dataset.md +13 -0
- package/docs/decomposed-runs.md +13 -3
- package/docs/forge-ui.md +8 -3
- package/docs/memory.md +4 -2
- package/docs/reviews.md +73 -2
- package/forge +16 -1
- package/package.json +1 -1
- package/references/dataset.md +86 -4
- package/references/decompose.md +24 -6
- package/references/harnesses.md +43 -1
- package/references/ui.md +4 -1
- package/scripts/forge-config.py +5 -2
- package/scripts/forge-control.py +4 -0
- package/scripts/forge-dispatch.sh +235 -121
- package/scripts/forge-issues.py +345 -17
- package/scripts/forge-lib-plans.sh +6 -2
- package/scripts/forge-lib-review.sh +158 -3
- package/scripts/forge-lib-task.sh +132 -14
- package/scripts/forge-parallel.sh +96 -11
- package/scripts/forge-prompt.py +61 -4
- package/scripts/forge-route.py +72 -12
- package/scripts/forge-runtime.py +47 -1
- package/scripts/forge-ui.py +8 -4
- package/scripts/forge_dataset/__init__.py +7 -2
- package/scripts/forge_dataset/cli.py +44 -1
- package/scripts/forge_dataset/export.py +36 -3
- package/scripts/forge_dataset/forecast.py +355 -0
- package/scripts/forge_dataset/harvest.py +23 -2
- package/scripts/forge_dataset/importer.py +20 -8
- package/scripts/forge_dataset/reviewers.py +145 -0
- package/scripts/forge_dataset/routing.py +1 -0
- package/scripts/forge_jev/__init__.py +7 -3
- package/scripts/forge_registry.py +15 -10
- package/ui/README.md +5 -4
- package/ui/docs/api.md +88 -23
- package/ui/forge_ui/control.py +26 -5
- package/ui/forge_ui/reader.py +184 -11
- package/ui/forge_ui/static/app.js +24 -10
- package/ui/forge_ui/static/cloud3d.js +4 -1
- package/ui/forge_ui/static/cloudmodel.js +1 -1
- package/ui/forge_ui/static/cloudui.js +13 -4
- package/ui/forge_ui/static/findingsview.js +65 -14
- package/ui/forge_ui/static/graph.js +6 -5
- package/ui/forge_ui/static/leaderboard.js +18 -0
- package/ui/forge_ui/static/modelstats.js +19 -1
- package/ui/forge_ui/static/panel.js +52 -10
- package/ui/forge_ui/static/runmodel.js +128 -13
- package/ui/forge_ui/static/scene3d.js +32 -15
- package/ui/forge_ui/static/settings.js +48 -12
- package/ui/forge_ui/static/style.css +7 -3
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
# forge_pp_flag, SKILL_DIR and DISPATCH.
|
|
35
35
|
|
|
36
36
|
REVIEW_REQ_BUDGET=81920 # bytes of task requirements put in front of the reviewer
|
|
37
|
+
REVIEW_SINCE_LIMIT=204800 # largest "changes since the previous review" placed inline
|
|
37
38
|
REVIEW_DIFF_LIMIT=409600 # largest combined diff placed inline in the prompt
|
|
38
39
|
|
|
39
40
|
# --- small helpers ---------------------------------------------------------------------
|
|
@@ -259,6 +260,80 @@ PY
|
|
|
259
260
|
return 0
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
# --- the previous final review -----------------------------------------------------------
|
|
264
|
+
# review_previous <plan> <current-out>: the newest earlier review dir of this plan that
|
|
265
|
+
# reached a verdict (PASS, FAIL or ACCEPTED) and left a report; prints its absolute path, or
|
|
266
|
+
# nothing. Candidates are final-review-*/ and integrate-*/final-review/, ordered by the epoch
|
|
267
|
+
# in the directory name. `final-review-*` never matches the published `final-review` link,
|
|
268
|
+
# and paths are resolved so a link and its target are one candidate, not two.
|
|
269
|
+
review_previous() {
|
|
270
|
+
python3 - "$1" "$2" <<'PY' 2>/dev/null || true
|
|
271
|
+
import glob, os, re, sys
|
|
272
|
+
|
|
273
|
+
plan, cur = os.path.realpath(sys.argv[1]), os.path.realpath(sys.argv[2])
|
|
274
|
+
found = {}
|
|
275
|
+
for pat, rx in (('final-review-*', r'^final-review-(\d+)-(\d+)$'),
|
|
276
|
+
('integrate-*/final-review', r'^integrate-(\d+)-(\d+)$')):
|
|
277
|
+
for path in glob.glob(os.path.join(glob.escape(plan), pat)):
|
|
278
|
+
real = os.path.realpath(path)
|
|
279
|
+
owner = os.path.basename(path if pat[0] == 'f' else os.path.dirname(path))
|
|
280
|
+
m = re.match(rx, owner)
|
|
281
|
+
if not m or real == cur or not os.path.isdir(real):
|
|
282
|
+
continue
|
|
283
|
+
try:
|
|
284
|
+
with open(os.path.join(real, 'verdict'), encoding='utf-8', errors='replace') as fh:
|
|
285
|
+
verdict = fh.read().strip()
|
|
286
|
+
if verdict not in ('PASS', 'FAIL', 'ACCEPTED') or os.path.getsize(os.path.join(real, 'qa.last')) == 0:
|
|
287
|
+
continue
|
|
288
|
+
except OSError:
|
|
289
|
+
continue
|
|
290
|
+
found[real] = (int(m.group(1)), int(m.group(2)))
|
|
291
|
+
if found:
|
|
292
|
+
print(max(found, key=lambda r: (found[r], r)))
|
|
293
|
+
PY
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# review_meta_value <review-dir> <key>
|
|
297
|
+
review_meta_value() { sed -n "s/^$2=//p" "$1/review.meta" 2>/dev/null | head -1; }
|
|
298
|
+
|
|
299
|
+
# review_carry <previous-dir> <tip7>: the previous report, framed for a reviewer. The report is
|
|
300
|
+
# capped at 40 KB (head kept, cut marked); its diff fences become text fences, because the
|
|
301
|
+
# dataset and forge-ui read the LAST ```diff fence of the prompt as the diff under review.
|
|
302
|
+
review_carry() {
|
|
303
|
+
python3 - "$1" "$2" <<'PY'
|
|
304
|
+
import os, re, sys, time
|
|
305
|
+
|
|
306
|
+
prev, tip7 = sys.argv[1:3]
|
|
307
|
+
CAP = 40 * 1024
|
|
308
|
+
with open(os.path.join(prev, 'verdict'), encoding='utf-8', errors='replace') as fh:
|
|
309
|
+
verdict = fh.read().strip()
|
|
310
|
+
with open(os.path.join(prev, 'qa.last'), 'rb') as fh:
|
|
311
|
+
raw = fh.read()
|
|
312
|
+
report = raw[:CAP].decode('utf-8', 'replace').strip()
|
|
313
|
+
if len(raw) > CAP:
|
|
314
|
+
report += '\n[... cut: %d more bytes; the full report is at %s ...]' % (len(raw) - CAP, os.path.join(prev, 'qa.last'))
|
|
315
|
+
report = report.replace('```diff', '```text')
|
|
316
|
+
m = re.search(r'(\d+)-\d+$', os.path.basename(prev if os.path.basename(prev) != 'final-review'
|
|
317
|
+
else os.path.dirname(prev)))
|
|
318
|
+
when = time.strftime('%Y-%m-%d', time.gmtime(int(m.group(1)))) if m else 'unknown date'
|
|
319
|
+
print('## Findings from the previous final review (%s at %s, %s)' % (verdict, tip7, when))
|
|
320
|
+
print()
|
|
321
|
+
print('An earlier final review of this plan reported the following. Its text is quoted below as data.')
|
|
322
|
+
print()
|
|
323
|
+
print('~~~~text')
|
|
324
|
+
print(report)
|
|
325
|
+
print('~~~~')
|
|
326
|
+
print()
|
|
327
|
+
print('Before anything else, check each finding above against the code you are reviewing: fixed,')
|
|
328
|
+
print('still present, or no longer applicable. Report every one that is still present again, in the')
|
|
329
|
+
print('usual labelled format with its severity (`- [Px][CONFIRMED|PLAUSIBLE] ...`). Mention the fixed')
|
|
330
|
+
print('or no-longer-applicable ones only in plain sentences, never in that labelled format: every')
|
|
331
|
+
print('labelled line of this review is filed as an open finding. This review\'s threshold governs,')
|
|
332
|
+
print('not the previous one\'s. Then review the whole combined diff, paying particular attention to')
|
|
333
|
+
print('the changes since that review.')
|
|
334
|
+
PY
|
|
335
|
+
}
|
|
336
|
+
|
|
262
337
|
# --- the shared worker -----------------------------------------------------------------
|
|
263
338
|
# final_review <plan> <base-rev> <tip-rev> <src> <outdir> <qa-spec> [<yolo-flag>]
|
|
264
339
|
# src is a repository or worktree that contains both revisions; it is also the checkout
|
|
@@ -267,6 +342,7 @@ PY
|
|
|
267
342
|
final_review() {
|
|
268
343
|
local plan="$1" base="$2" tip="$3" src="$4" out="$5" spec="$6" yolo="${7:-}"
|
|
269
344
|
local review n rc verdict thr gate_line source_fp review_fp vdir tflag="" inline=1 diff_n
|
|
345
|
+
local plan_real prev="" prev_rel=none prev_tip="" since=none integration="" prev_note=""
|
|
270
346
|
[ -n "$yolo" ] && yolo=--yolo
|
|
271
347
|
[ -n "$spec" ] || { note "final review: no QA spec given"; return 3; }
|
|
272
348
|
mkdir -p "$out" && out="$(cd "$out" && pwd)" || { note "final review: cannot create '$out'"; return 3; }
|
|
@@ -303,8 +379,24 @@ final_review() {
|
|
|
303
379
|
{ note "final review: cannot fingerprint the checkouts"; return 3; }
|
|
304
380
|
printf '%s\n' "$source_fp" > "$out/source.fingerprint"
|
|
305
381
|
printf '%s\n' "$review_fp" > "$out/review.fingerprint"
|
|
382
|
+
# The earlier final review of this plan, if any, and whether its tip leads to this one.
|
|
383
|
+
prev="$(review_previous "$plan" "$out")"
|
|
384
|
+
if [ -n "$prev" ]; then
|
|
385
|
+
prev_rel="$prev"; plan_real="$(cd "$plan" && pwd -P)"
|
|
386
|
+
case "$prev" in "$plan_real"/*) prev_rel="${prev#"$plan_real"/}" ;; esac
|
|
387
|
+
prev_tip="$(review_meta_value "$prev" tip)"
|
|
388
|
+
if [ -z "$prev_tip" ] || ! git -C "$src" rev-parse --verify --quiet "$prev_tip^{commit}" >/dev/null; then
|
|
389
|
+
prev_note="the previous review's tip (${prev_tip:-not recorded}) is not a commit in this checkout, so the range since it cannot be shown."
|
|
390
|
+
elif git -C "$src" merge-base --is-ancestor "$prev_tip" "$tip" 2>/dev/null; then
|
|
391
|
+
prev_tip="$(git -C "$src" rev-parse "$prev_tip^{commit}")"; since="$prev_tip"
|
|
392
|
+
else
|
|
393
|
+
prev_note="the previous review's tip ($(printf '%.7s' "$prev_tip")) is not an ancestor of this one (it judged a different line of history, for example an integrate candidate that included your branch), so the range since it cannot be shown."
|
|
394
|
+
fi
|
|
395
|
+
fi
|
|
396
|
+
[ -s "$plan/accepted.integration" ] && integration="$(head -1 "$plan/accepted.integration" | tr -d ' \r')"
|
|
306
397
|
{
|
|
307
398
|
echo "base=$base"; echo "tip=$tip"; echo "source=$src"; echo "spec=$spec"; echo "threshold=${thr:-none}"
|
|
399
|
+
echo "integration=${integration:-none}"; echo "previous=$prev_rel"; echo "since=$since"
|
|
308
400
|
} > "$out/review.meta"
|
|
309
401
|
|
|
310
402
|
review_task_table "$plan" > "$out/tasks.summary"
|
|
@@ -318,11 +410,44 @@ final_review() {
|
|
|
318
410
|
diff_n="$(wc -c < "$out/combined.diff" | tr -d ' ')"
|
|
319
411
|
if [ "$diff_n" -gt "$REVIEW_DIFF_LIMIT" ]; then inline=0; rm -f "$out/combined.diff"; fi
|
|
320
412
|
|
|
413
|
+
# What the earlier review said, and the change since it. Nothing here may open a ```diff
|
|
414
|
+
# fence: when the combined diff is not inlined, the prompt must hold none.
|
|
415
|
+
rm -f "$out/previous.md"
|
|
416
|
+
if [ -n "$prev" ]; then
|
|
417
|
+
{
|
|
418
|
+
review_carry "$prev" "$(printf '%.7s' "${prev_tip:-unknown}")"
|
|
419
|
+
echo
|
|
420
|
+
if [ -n "$prev_note" ]; then
|
|
421
|
+
echo "Note: $prev_note"
|
|
422
|
+
elif [ "$since" = "$tip" ]; then
|
|
423
|
+
echo "### Changes since that review ($(printf '%.7s' "$since")..$(printf '%.7s' "$tip"))"
|
|
424
|
+
echo "There are no changes since that review: it judged this same commit."
|
|
425
|
+
else
|
|
426
|
+
echo "### Changes since that review ($(printf '%.7s' "$since")..$(printf '%.7s' "$tip"))"
|
|
427
|
+
git -C "$src" diff --binary "$since" "$tip" -- . ':(exclude).forge' 2>/dev/null |
|
|
428
|
+
head -c $((REVIEW_SINCE_LIMIT + 1)) > "$out/since.diff"
|
|
429
|
+
if [ "$inline" = 1 ] && [ "$(wc -c < "$out/since.diff" | tr -d ' ')" -le "$REVIEW_SINCE_LIMIT" ] &&
|
|
430
|
+
! grep -q '```diff' "$out/since.diff"; then
|
|
431
|
+
echo '```patch'; cat "$out/since.diff"; echo '```'
|
|
432
|
+
else
|
|
433
|
+
echo "Too large (or quoting a diff fence) to inline here; the files changed since that review:"
|
|
434
|
+
echo '```text'
|
|
435
|
+
git -C "$src" diff --stat "$since" "$tip" -- . ':(exclude).forge' 2>/dev/null
|
|
436
|
+
echo '```'
|
|
437
|
+
echo "(The previous tip is not in your checkout; use the combined diff to look at those files.)"
|
|
438
|
+
fi
|
|
439
|
+
rm -f "$out/since.diff"
|
|
440
|
+
fi
|
|
441
|
+
echo
|
|
442
|
+
} > "$out/previous.md"
|
|
443
|
+
fi
|
|
444
|
+
|
|
321
445
|
{
|
|
322
446
|
cat "$out/context.md"
|
|
323
447
|
python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part head --kind final
|
|
324
448
|
echo
|
|
325
449
|
cat "$out/notes.md"
|
|
450
|
+
[ -s "$out/previous.md" ] && cat "$out/previous.md"
|
|
326
451
|
python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part tail --kind final ${thr:+--threshold "$thr"}
|
|
327
452
|
echo
|
|
328
453
|
if [ "$inline" = 1 ]; then
|
|
@@ -338,11 +463,41 @@ final_review() {
|
|
|
338
463
|
|
|
339
464
|
[ -s "$plan/timeout" ] && tflag="--timeout $(cat "$plan/timeout")"
|
|
340
465
|
note "final review: $spec on $(git -C "$src" diff --shortstat "$base" "$tip" -- . ':(exclude).forge' 2>/dev/null | sed 's/^ *//')"
|
|
466
|
+
# Opt-in (plan --reviewer-session): continue the previous final review's session when it ran
|
|
467
|
+
# as this one does, by the same rule as per-task QA. Anything else is a fresh dispatch.
|
|
468
|
+
local rs_mode=fresh rs_reason="" rs_id="" rs_cont=()
|
|
469
|
+
if [ -f "$plan/reviewer_session" ]; then
|
|
470
|
+
if [ -n "${FORGE_FRACTAL_RUN:-}" ] || forge_plan_is_fractal "$plan"; then
|
|
471
|
+
rs_reason=fractal
|
|
472
|
+
elif [ -z "$prev" ]; then
|
|
473
|
+
rs_reason=first-round
|
|
474
|
+
else
|
|
475
|
+
IFS=$'\t' read -r rs_mode rs_reason rs_id < <(review_session_decide \
|
|
476
|
+
"$prev/qa.session" "$prev/qa.resolved" "$out/doctor.out")
|
|
477
|
+
[ "$rs_mode" = continued ] && rs_cont=(--continue-session "$rs_id")
|
|
478
|
+
fi
|
|
479
|
+
fi
|
|
480
|
+
local qa_cmd=(/bin/bash "$DISPATCH" qa "$spec" --repo "$review" --run-dir "$out"
|
|
481
|
+
--review-base "$(cat "$out/review.base")" --ripwire-query-file "$out/context.md"
|
|
482
|
+
--prompt-file "$out/qa.input" ${yolo:+"$yolo"} $tflag --output "${FORGE_OUTPUT:-summary}")
|
|
341
483
|
FORGE_CAPABILITY_CACHE="${FORGE_CAPABILITY_CACHE:-$plan/capabilities}" \
|
|
342
|
-
|
|
343
|
-
--review-base "$(cat "$out/review.base")" --ripwire-query-file "$out/context.md" \
|
|
344
|
-
--prompt-file "$out/qa.input" $yolo $tflag --output "${FORGE_OUTPUT:-summary}" > "$out/qa.out" 2>&1
|
|
484
|
+
"${qa_cmd[@]}" ${rs_cont[@]+"${rs_cont[@]}"} > "$out/qa.out" 2>&1
|
|
345
485
|
rc=$?
|
|
486
|
+
if [ "$rc" = 9 ] && [ "$rs_mode" = continued ]; then
|
|
487
|
+
# Not continued (unsupported, or it failed): the same input again, fresh. The record of why
|
|
488
|
+
# is the first dispatch's, which the second one clears.
|
|
489
|
+
if [ "$(sed -n 's/^reason=//p' "$out/qa.continue" 2>/dev/null | head -1)" = unsupported ]; then
|
|
490
|
+
rs_reason=continue-unsupported
|
|
491
|
+
else
|
|
492
|
+
rs_reason=continue-failed
|
|
493
|
+
fi
|
|
494
|
+
rs_mode=fresh
|
|
495
|
+
mv "$out/qa.out" "$out/qa.continue.out" 2>/dev/null || true
|
|
496
|
+
FORGE_CAPABILITY_CACHE="${FORGE_CAPABILITY_CACHE:-$plan/capabilities}" \
|
|
497
|
+
"${qa_cmd[@]}" > "$out/qa.out" 2>&1
|
|
498
|
+
rc=$?
|
|
499
|
+
fi
|
|
500
|
+
[ ! -f "$plan/reviewer_session" ] || { echo "session=$rs_mode"; echo "session_reason=$rs_reason"; } >> "$out/review.meta"
|
|
346
501
|
if [ "${FORGE_OUTPUT:-summary}" = full ]; then cat "$out/qa.out"; fi
|
|
347
502
|
case "$rc" in
|
|
348
503
|
0) ;;
|
|
@@ -40,17 +40,24 @@ infra_stop() { # infra_stop <tdir> <id> <role> <attempt>
|
|
|
40
40
|
note "$id: $role dispatch hit an infrastructure failure (${cls:-unknown}) — no attempt spent; re-run to resume"
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
# reviewed
|
|
45
|
-
|
|
43
|
+
# True when the worktree is still exactly what was reviewed. Reads and deletes nothing:
|
|
44
|
+
# the reviewed commit, the fingerprint and changes.diff are present, and the worktree's
|
|
45
|
+
# HEAD and fingerprint equal them.
|
|
46
|
+
reviewed_state_matches() { # reviewed_state_matches <tdir> <wt>
|
|
46
47
|
local tdir="$1" wt="$2"
|
|
47
|
-
[ "$(cat "$tdir/resume" 2>/dev/null)" = qa ] || return 1
|
|
48
48
|
[ -s "$tdir/reviewed.commit" ] && [ -s "$tdir/source.fingerprint" ] && [ -s "$tdir/changes.diff" ] || return 1
|
|
49
49
|
[ -e "$wt/.git" ] || return 1
|
|
50
50
|
[ "$(git -C "$wt" rev-parse HEAD 2>/dev/null)" = "$(cat "$tdir/reviewed.commit")" ] || return 1
|
|
51
51
|
[ "$(forge_fingerprint "$wt")" = "$(cat "$tdir/source.fingerprint")" ]
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# A QA-only resume is trusted only while the worktree is still exactly what was
|
|
55
|
+
# reviewed. Anything else clears the marker and the full path runs.
|
|
56
|
+
resume_valid() { # resume_valid <tdir> <wt>
|
|
57
|
+
[ "$(cat "$1/resume" 2>/dev/null)" = qa ] || return 1
|
|
58
|
+
reviewed_state_matches "$1" "$2"
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
# The next entry's kind, decided before it starts with the test do_task_body applies
|
|
55
62
|
# (resume_valid): true when it only re-runs QA. A marker the worktree no longer matches is
|
|
56
63
|
# cleared here, so whoever prepares the entry and do_task_body agree that it is a full one.
|
|
@@ -128,10 +135,37 @@ settle_pin() {
|
|
|
128
135
|
# True when the task's next retry only re-runs QA: a `resume` marker (a QA-stage infrastructure
|
|
129
136
|
# stop), or a verdict-less review of committed work, which prepare_retry turns into the marker
|
|
130
137
|
# but has not yet when the run summary is printed. Such a retry leaves a pin in place.
|
|
138
|
+
# Both are believed only while the worktree is still what was reviewed — the comparison
|
|
139
|
+
# resume_valid uses, and the marker prepare_retry is about to write gets the same test.
|
|
140
|
+
# This never deletes the marker; resume_check owns that.
|
|
131
141
|
qa_only_next() { # qa_only_next <plan> <id>
|
|
132
|
-
local tdir="$1/tasks/$2"
|
|
133
|
-
|
|
134
|
-
[
|
|
142
|
+
local tdir="$1/tasks/$2" root="" wt=""
|
|
143
|
+
root="$(cat "$1/wt_root" 2>/dev/null)"
|
|
144
|
+
[ -n "$root" ] && wt="$root/$2"
|
|
145
|
+
if [ "$(cat "$tdir/resume" 2>/dev/null)" = qa ]; then
|
|
146
|
+
[ -n "$wt" ] && reviewed_state_matches "$tdir" "$wt"
|
|
147
|
+
return
|
|
148
|
+
fi
|
|
149
|
+
if [ "$(task_status "$1" "$2")" = UNKNOWN ] && [ -s "$tdir/reviewed.commit" ]; then
|
|
150
|
+
[ -n "$wt" ] && reviewed_state_matches "$tdir" "$wt"
|
|
151
|
+
return
|
|
152
|
+
fi
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
qa_only_next_flag() { # qa_only_next_flag <plan> <id> -> yes | no, for `escalation --check`
|
|
157
|
+
if qa_only_next "$1" "$2"; then printf 'yes\n'; else printf 'no\n'; fi
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# tasks/<id>/escalations.tsv, probed and appended the way forge-control writes a plan file
|
|
161
|
+
# (forge-route.py record loads that code): every component below the plan is a real
|
|
162
|
+
# directory, and the file is absent or a regular file. A symlink at any level is a refusal.
|
|
163
|
+
escalation_record_ok() { # escalation_record_ok <plan> <id>
|
|
164
|
+
python3 "$SKILL_DIR/scripts/forge-route.py" record --plan "$1" --task "$2" --probe
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
escalation_record_line() { # escalation_record_line <plan> <id> <line>
|
|
168
|
+
python3 "$SKILL_DIR/scripts/forge-route.py" record --plan "$1" --task "$2" --line "$3"
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
# Writes the task's implementer column in tasks.tsv: a temp file, then a rename.
|
|
@@ -142,6 +176,80 @@ set_dwarf() { # set_dwarf <plan> <id> <spec>
|
|
|
142
176
|
}
|
|
143
177
|
|
|
144
178
|
# --- retry preparation ----------------------------------------------------------
|
|
179
|
+
# Carries the last review's report forward as the findings of the next attempt, and records
|
|
180
|
+
# which commit that report judged (what reviewed.commit holds now, since qa.last is that
|
|
181
|
+
# review's). The two are written together: the commit first, so a report never sits beside a
|
|
182
|
+
# commit that belongs to an older one. Repeating the call writes the same bytes.
|
|
183
|
+
carry_findings() { # carry_findings <tdir>
|
|
184
|
+
local tdir="$1"
|
|
185
|
+
if [ -s "$tdir/reviewed.commit" ]; then
|
|
186
|
+
cp "$tdir/reviewed.commit" "$tdir/.retry_findings.commit.tmp" \
|
|
187
|
+
&& mv "$tdir/.retry_findings.commit.tmp" "$tdir/retry_findings.commit"
|
|
188
|
+
else
|
|
189
|
+
rm -f "$tdir/retry_findings.commit"
|
|
190
|
+
fi
|
|
191
|
+
# The reviewer session that wrote these findings, and what it ran as, kept beside them for a
|
|
192
|
+
# plan with --reviewer-session. Removed first, so an older session never sits next to newer
|
|
193
|
+
# findings; a review that left no session leaves none here.
|
|
194
|
+
rm -f "$tdir/retry_findings.session" "$tdir/retry_findings.resolved"
|
|
195
|
+
cp "$tdir/qa.last" "$tdir/retry_findings.md"
|
|
196
|
+
[ ! -f "$tdir/qa.session" ] || cp "$tdir/qa.session" "$tdir/retry_findings.session" 2>/dev/null || true
|
|
197
|
+
[ ! -f "$tdir/qa.resolved" ] || cp "$tdir/qa.resolved" "$tdir/retry_findings.resolved" 2>/dev/null || true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# --- reviewer-session continuation (plan --reviewer-session, an opt-in experiment) ------------
|
|
201
|
+
# review_session_decide <session-file> <resolved-file> <resolution-file>: whether this review
|
|
202
|
+
# round continues the earlier reviewer's session. Prints `mode<TAB>reason<TAB>session`; the
|
|
203
|
+
# session is the id to continue, and empty for every fresh answer.
|
|
204
|
+
# session-file the id the earlier review ran under (retry_findings.session / qa.session)
|
|
205
|
+
# resolved-file that review's resolution (retry_findings.resolved / qa.resolved)
|
|
206
|
+
# resolution-file this dispatch's resolution (doctor output): harness=, model=, effort=
|
|
207
|
+
# The caller has already handled the cases that need no files (first round, Fractal). Anything
|
|
208
|
+
# that cannot be checked is fresh; nothing here aborts.
|
|
209
|
+
review_session_decide() {
|
|
210
|
+
local sf="$1" rf="$2" cf="$3" id="" k a b h
|
|
211
|
+
[ -f "$sf" ] && id="$(head -1 "$sf" 2>/dev/null | tr -d '\r')"
|
|
212
|
+
if [ -z "$id" ]; then printf 'fresh\tno-session\t\n'; return 0; fi
|
|
213
|
+
case "$id" in *[!A-Za-z0-9-]*) printf 'fresh\tinvalid-session\t\n'; return 0 ;; esac
|
|
214
|
+
if [ "${#id}" -lt 8 ] || [ "${#id}" -gt 128 ]; then printf 'fresh\tinvalid-session\t\n'; return 0; fi
|
|
215
|
+
h="$(sed -n 's/^harness=//p' "$cf" 2>/dev/null | head -1)"
|
|
216
|
+
if [ -z "$h" ]; then printf 'fresh\tharness-unsupported\t\n'; return 0; fi
|
|
217
|
+
for k in harness model effort; do
|
|
218
|
+
a="$(sed -n "s/^$k=//p" "$rf" 2>/dev/null | head -1)"
|
|
219
|
+
b="$(sed -n "s/^$k=//p" "$cf" 2>/dev/null | head -1)"
|
|
220
|
+
if [ -z "$a" ] || [ "$a" != "$b" ]; then printf 'fresh\tspec-changed\t\n'; return 0; fi
|
|
221
|
+
done
|
|
222
|
+
case "$h" in
|
|
223
|
+
claude|openclaude|codex) printf 'continued\tsame-spec\t%s\n' "$id" ;;
|
|
224
|
+
*) printf 'fresh\tharness-unsupported\t\n' ;;
|
|
225
|
+
esac
|
|
226
|
+
return 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# The diff between the attempt the carried findings judged and the one under review now, as
|
|
230
|
+
# files a later gate can reuse (tasks/<id>/changes.since.{diff,range,stat}). All three go
|
|
231
|
+
# when there is no valid range: no findings, no recorded commit, a commit that is unknown or
|
|
232
|
+
# no ancestor of the reviewed one, or nothing changed.
|
|
233
|
+
attempt_delta() { # attempt_delta <tdir> <worktree> <reviewed-commit>
|
|
234
|
+
local tdir="$1" wt="$2" reviewed="$3" from="" to=""
|
|
235
|
+
rm -f "$tdir/.changes.since.diff" "$tdir/.changes.since.stat"
|
|
236
|
+
if [ -s "$tdir/retry_findings.md" ] && [ -s "$tdir/retry_findings.commit" ] \
|
|
237
|
+
&& from="$(git -C "$wt" rev-parse --verify -q "$(cat "$tdir/retry_findings.commit")^{commit}")" \
|
|
238
|
+
&& to="$(git -C "$wt" rev-parse --verify -q "$reviewed^{commit}")" \
|
|
239
|
+
&& git -C "$wt" merge-base --is-ancestor "$from" "$to" \
|
|
240
|
+
&& git -C "$wt" diff --binary "$from" "$to" -- . ':(exclude).forge' > "$tdir/.changes.since.diff" 2>/dev/null \
|
|
241
|
+
&& [ -s "$tdir/.changes.since.diff" ] \
|
|
242
|
+
&& git -C "$wt" diff --stat "$from" "$to" -- . ':(exclude).forge' > "$tdir/.changes.since.stat" 2>/dev/null; then
|
|
243
|
+
printf '%s %s\n' "$from" "$to" > "$tdir/changes.since.range"
|
|
244
|
+
mv "$tdir/.changes.since.diff" "$tdir/changes.since.diff"
|
|
245
|
+
mv "$tdir/.changes.since.stat" "$tdir/changes.since.stat"
|
|
246
|
+
return 0
|
|
247
|
+
fi
|
|
248
|
+
rm -f "$tdir/.changes.since.diff" "$tdir/.changes.since.stat" \
|
|
249
|
+
"$tdir/changes.since.diff" "$tdir/changes.since.range" "$tdir/changes.since.stat"
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
145
253
|
# Shared by `retry` (a human asked) and the scheduler's automatic retries. The findings
|
|
146
254
|
# are the whole point of retrying rather than re-running: the dwarf is told what was
|
|
147
255
|
# wrong with its own previous attempt.
|
|
@@ -163,14 +271,15 @@ prepare_retry() { # prepare_retry <plan> <id> [new-dwarf]
|
|
|
163
271
|
# all"), and copying that over would throw away the actual findings the previous
|
|
164
272
|
# reviewer gave — the one thing a retry exists to carry forward.
|
|
165
273
|
elif [ "$verdict" = PASS ] || [ "$verdict" = FAIL ]; then
|
|
166
|
-
|
|
274
|
+
carry_findings "$tdir"
|
|
167
275
|
elif grep -q 'FORGE_VERDICT' "$tdir/qa.last" 2>/dev/null; then
|
|
168
|
-
|
|
276
|
+
carry_findings "$tdir"
|
|
169
277
|
elif [ -s "$tdir/retry_findings.md" ]; then
|
|
170
278
|
note "$id: no new review since the last retry — reusing the previous findings"
|
|
171
279
|
else
|
|
172
280
|
note "$id: no previous QA findings to pass on (last status was $st)"
|
|
173
281
|
: > "$tdir/retry_findings.md"
|
|
282
|
+
rm -f "$tdir/retry_findings.commit" "$tdir/retry_findings.session" "$tdir/retry_findings.resolved"
|
|
174
283
|
fi
|
|
175
284
|
# A QA-only resume (an unreviewed verdict, or a QA-stage infrastructure stop) re-asks the
|
|
176
285
|
# reviewer about work that is already committed; the dwarf does not run, so it is no moment
|
|
@@ -197,6 +306,10 @@ prepare_retry() { # prepare_retry <plan> <id> [new-dwarf]
|
|
|
197
306
|
note "$id: the escalation pin waits for the next full retry (this one only re-runs QA)"
|
|
198
307
|
elif [ "$has_pin" = 1 ] && forge_plan_is_fractal "$PLAN"; then
|
|
199
308
|
note "$id: an escalation pin is set, but Fractal plans never apply pins — leaving it in place"
|
|
309
|
+
elif [ "$has_pin" = 1 ] && ! escalation_record_ok "$PLAN" "$id"; then
|
|
310
|
+
# Before the claim: a refused record leaves the pin unclaimed and tasks.tsv as it was.
|
|
311
|
+
note "$id: could not record the escalation in escalations.tsv; nothing was retried"
|
|
312
|
+
return 1
|
|
200
313
|
elif [ "$has_pin" = 1 ] && pinned="$(claim_pin "$tdir")"; then
|
|
201
314
|
esc_source=pin
|
|
202
315
|
newdwarf="$pinned"
|
|
@@ -225,10 +338,10 @@ prepare_retry() { # prepare_retry <plan> <id> [new-dwarf]
|
|
|
225
338
|
# Puts the next implementer in place: tasks.tsv, the escalations.tsv record and the dataset
|
|
226
339
|
# event. Returns 1, with tasks.tsv and a claimed pin given back, when nothing was changed.
|
|
227
340
|
write_escalation() { # write_escalation <plan> <id> <from> <to> <retry|pin|policy>
|
|
228
|
-
local PLAN="$1" id="$2" cur="$3" newdwarf="$4" esc_source="$5" tdir="$1/tasks/$2"
|
|
341
|
+
local PLAN="$1" id="$2" cur="$3" newdwarf="$4" esc_source="$5" tdir="$1/tasks/$2" line
|
|
229
342
|
# A pin is held to its record: whether the record can be written is learned before
|
|
230
343
|
# anything is changed, so a failure there leaves tasks.tsv and the pin as they were.
|
|
231
|
-
if [ "$esc_source" = pin ] && !
|
|
344
|
+
if [ "$esc_source" = pin ] && ! escalation_record_ok "$PLAN" "$id"; then
|
|
232
345
|
note "$id: could not record the escalation in escalations.tsv; nothing was retried"
|
|
233
346
|
settle_pin "$tdir" restore
|
|
234
347
|
return 1
|
|
@@ -247,8 +360,9 @@ write_escalation() { # write_escalation <plan> <id> <from> <to> <retry|pin|polic
|
|
|
247
360
|
# The rewrite loses which model failed first, and an escalation is exactly the record
|
|
248
361
|
# of "this tier was too weak for this task". The last column says who decided: forge's
|
|
249
362
|
# policy, a human's `retry --dwarf`, or a pin.
|
|
250
|
-
|
|
251
|
-
"$cur" "$newdwarf" "$esc_source"
|
|
363
|
+
line="$(printf '%s\t%s\t%s\t%s\t%s' "$(date +%s)" "$(cat "$tdir/attempt" 2>/dev/null || echo 0)" \
|
|
364
|
+
"$cur" "$newdwarf" "$esc_source")"
|
|
365
|
+
if ! escalation_record_line "$PLAN" "$id" "$line" && [ "$esc_source" = pin ]; then
|
|
252
366
|
# The rewrite is undone with the record, so the pin given back is still a real
|
|
253
367
|
# escalation next time, not a same-spec one that hides the model it replaced.
|
|
254
368
|
if [ "$newdwarf" = "$cur" ] || set_dwarf "$PLAN" "$id" "$cur"; then
|
|
@@ -282,6 +396,9 @@ prepare_resume() { # prepare_resume <plan> <id>
|
|
|
282
396
|
note "$id: the escalation pin waits for the next full retry (this resume only re-runs QA)"
|
|
283
397
|
elif forge_plan_is_fractal "$PLAN"; then
|
|
284
398
|
note "$id: an escalation pin is set, but Fractal plans never apply pins — leaving it in place"
|
|
399
|
+
elif ! escalation_record_ok "$PLAN" "$id"; then
|
|
400
|
+
note "$id: could not record the escalation in escalations.tsv; nothing was retried"
|
|
401
|
+
return 1
|
|
285
402
|
elif pinned="$(claim_pin "$tdir")"; then
|
|
286
403
|
note "$id: applying the pinned escalation to $pinned"
|
|
287
404
|
write_escalation "$PLAN" "$id" "$(field "$PLAN/tasks.tsv" "$id" dwarf)" "$pinned" pin || return 1
|
|
@@ -466,7 +583,8 @@ report_escalations() { # report_escalations <plan>
|
|
|
466
583
|
continue
|
|
467
584
|
fi
|
|
468
585
|
if [ -s "$plan/tasks/$id/escalation.next" ] && [ "$(task_status "$plan" "$id")" != MERGED ]; then
|
|
469
|
-
python3 "$SKILL_DIR/scripts/forge-route.py" escalation --plan "$plan" --task "$id" --check
|
|
586
|
+
python3 "$SKILL_DIR/scripts/forge-route.py" escalation --plan "$plan" --task "$id" --check \
|
|
587
|
+
--qa-only-next "$(qa_only_next_flag "$plan" "$id")" >/dev/null 2>&1 || true
|
|
470
588
|
fi
|
|
471
589
|
if [ -s "$plan/tasks/$id/split.suggested" ]; then
|
|
472
590
|
[ "$n" = 0 ] && echo "$head"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# forge-parallel.sh run <plan-dir> [--max-parallel N] [--yolo-dwarf] [--yolo-qa]
|
|
10
10
|
# [--verify <command>] [--setup <command>] [--dry-run]
|
|
11
11
|
# [--timeout S] [--retry-failed N] [--infra-retries M]
|
|
12
|
-
# [--qa-threshold P0|P1|P2|P3] [--verify-retries N]
|
|
12
|
+
# [--qa-threshold P0|P1|P2|P3] [--verify-retries N] [--no-forecast]
|
|
13
13
|
# forge-parallel.sh retry <plan-dir> <task-id> [--dwarf <spec>] [--setup <command>]
|
|
14
14
|
# [--timeout S] [--qa-threshold Pn] [--verify-retries N]
|
|
15
15
|
# forge-parallel.sh integrate <plan-dir> --approved [--verify-retries N] [--final-review <qa-spec>]
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
# by a listed reviewer of another model family, recorded and
|
|
31
31
|
# never read (measures reviewer agreement). `none` turns it off
|
|
32
32
|
# --shadow-rate R that share, 0..1 (default 0.1)
|
|
33
|
+
# --reviewer-session opt-in experiment: a later review round continues the reviewer's
|
|
34
|
+
# earlier session (claude, openclaude, codex) when the QA spec is
|
|
35
|
+
# unchanged, and starts fresh whenever it cannot; each round is
|
|
36
|
+
# logged in tasks/<id>/review_sessions.tsv and tagged in the
|
|
37
|
+
# dataset. `--no-reviewer-session` turns it off (the default)
|
|
33
38
|
# --explore E on by default at 0.2 (0 turns it off, and is kept): a row left
|
|
34
39
|
# to forge gets, with probability E/2, its neighbouring model
|
|
35
40
|
# (one tier away in the given pools) instead of its pool's turn;
|
|
@@ -129,7 +134,7 @@ pool_pick() { # pool_pick <comma-list> <index>
|
|
|
129
134
|
do_plan() {
|
|
130
135
|
local PLAN="${1:?plan needs a plan dir}"; shift
|
|
131
136
|
local REPO="" D_ANY="" D_HI="" D_MD="" D_LO="" Q_ANY="" Q_HI="" Q_MD="" Q_LO=""
|
|
132
|
-
local PLANNER="" NOMEM=0 jev_rc=0 DLEVEL="" SHADOW_QA="" SHADOW_RATE="" EXPLORE="" FAIL_FIRST=""
|
|
137
|
+
local PLANNER="" NOMEM=0 jev_rc=0 DLEVEL="" SHADOW_QA="" SHADOW_RATE="" EXPLORE="" FAIL_FIRST="" REVIEWER_SESSION=""
|
|
133
138
|
while [ $# -gt 0 ]; do
|
|
134
139
|
case "$1" in
|
|
135
140
|
--repo) REPO="${2:?}"; shift 2 ;;
|
|
@@ -157,6 +162,8 @@ do_plan() {
|
|
|
157
162
|
--ui|--no-ui) forge_ui_flag "$1"; shift ;;
|
|
158
163
|
--fail-first) FAIL_FIRST=on; shift ;;
|
|
159
164
|
--no-fail-first) FAIL_FIRST=off; shift ;;
|
|
165
|
+
--reviewer-session) REVIEWER_SESSION=on; shift ;;
|
|
166
|
+
--no-reviewer-session) REVIEWER_SESSION=off; shift ;;
|
|
160
167
|
*)
|
|
161
168
|
# `case` is not a loop, so `continue` here belongs to the enclosing while.
|
|
162
169
|
if [ "$JEV_OPTS" = 1 ]; then
|
|
@@ -192,6 +199,9 @@ do_plan() {
|
|
|
192
199
|
|| printf '%s\n' "$SHADOW_RATE" > "$PLAN/shadow_rate"
|
|
193
200
|
# Fail-first evidence (opt-in, advisory) is part of the plan too: every task's review gets it.
|
|
194
201
|
case "$FAIL_FIRST" in on) : > "$PLAN/fail_first" ;; off) rm -f "$PLAN/fail_first" ;; esac
|
|
202
|
+
# Reviewer-session continuation (opt-in experiment): the file holds `on`. Readers only
|
|
203
|
+
# test that it exists; the dataset records its content.
|
|
204
|
+
case "$REVIEWER_SESSION" in on) printf '%s\n' on > "$PLAN/reviewer_session" ;; off) rm -f "$PLAN/reviewer_session" ;; esac
|
|
195
205
|
# Exploration is decided here and only here, so the table and preflight see what will run.
|
|
196
206
|
# On by default: choosing models from measured pass rates needs some randomized picks, and
|
|
197
207
|
# history had almost none (9 tasks ever ran on more than one spec). The flag wins, then
|
|
@@ -480,6 +490,9 @@ render_table() {
|
|
|
480
490
|
echo "fail-first: on, but $(cat "$PLAN/repo")/.forge/verify-subset (a test command with {files}) is missing, so nothing will run"
|
|
481
491
|
fi
|
|
482
492
|
fi
|
|
493
|
+
if [ -f "$PLAN/reviewer_session" ]; then
|
|
494
|
+
echo "reviewer-session: a later review round continues the reviewer's earlier session when its QA spec is unchanged (experiment; falls back to a fresh session)"
|
|
495
|
+
fi
|
|
483
496
|
echo
|
|
484
497
|
local w id dws starred=0
|
|
485
498
|
for w in $(awk -F'\t' '{print $1}' "$W" | sort -un); do
|
|
@@ -868,8 +881,11 @@ do_task_body() (
|
|
|
868
881
|
echo "$review_fp" > "$tdir/review.fingerprint"
|
|
869
882
|
|
|
870
883
|
forge_metric_phase preparation
|
|
871
|
-
local jev_loaded_qa=0 subset_cmd="" qa_thr
|
|
884
|
+
local jev_loaded_qa=0 subset_cmd="" qa_thr rs_on=0
|
|
872
885
|
qa_thr="$(qa_threshold_for "$PLAN" "$id")"
|
|
886
|
+
# Read once per round, like the threshold: continuing the earlier reviewer's session is an
|
|
887
|
+
# opt-in experiment (plan --reviewer-session), and a round never changes its mind midway.
|
|
888
|
+
[ -f "$PLAN/reviewer_session" ] && rs_on=1
|
|
873
889
|
|
|
874
890
|
# A SEPARATE file from .forge/verify, and opt-in. It must contain {files}, because
|
|
875
891
|
# only the project knows how its runner takes a file list -- `pytest -q {files}` works,
|
|
@@ -895,11 +911,29 @@ do_task_body() (
|
|
|
895
911
|
# on the review snapshot, like the subset run above; the reviewer gets the evidence.
|
|
896
912
|
rm -f "$tdir/failfirst.json" "$tdir/failfirst.txt"
|
|
897
913
|
[ ! -f "$PLAN/fail_first" ] || forge_fail_first "$review" "$tdir" "$REPO"
|
|
914
|
+
# What changed since the review whose findings this reviewer is handed. Fractal keeps
|
|
915
|
+
# today's prompt unless it retries, as changes.prev.diff does. dwarf.last is the reply of
|
|
916
|
+
# the attempt under review: a dwarf run just rewrote it, or $resume (the validated QA-only
|
|
917
|
+
# path, never the raw marker) guarantees the reviewed commit is the one it describes.
|
|
918
|
+
local prompt_extra=()
|
|
919
|
+
if [ -z "${FORGE_FRACTAL_RUN:-}" ] || [ "${FORGE_FRACTAL_RETRY:-}" = 1 ]; then
|
|
920
|
+
prompt_extra+=(--role qa)
|
|
921
|
+
attempt_delta "$tdir" "$wt" "$reviewed"
|
|
922
|
+
if [ -s "$tdir/changes.since.diff" ]; then
|
|
923
|
+
prompt_extra+=(--retry-diff "$tdir/changes.since.diff" --retry-stat "$tdir/changes.since.stat"
|
|
924
|
+
--retry-range "$(awk '{ print substr($1, 1, 7) ".." substr($2, 1, 7) }' "$tdir/changes.since.range")")
|
|
925
|
+
fi
|
|
926
|
+
if [ -s "$tdir/retry_findings.md" ] && [ -s "$tdir/dwarf.last" ]; then
|
|
927
|
+
prompt_extra+=(--reply "$tdir/dwarf.last")
|
|
928
|
+
fi
|
|
929
|
+
else
|
|
930
|
+
rm -f "$tdir/changes.since.diff" "$tdir/changes.since.range" "$tdir/changes.since.stat"
|
|
931
|
+
fi
|
|
898
932
|
# QA uses the dispatch-time ownership and baseline, never later merge state.
|
|
899
933
|
/bin/bash "$MEMORY" inject "$REPO" qa --task "$tdir/prompt.md" \
|
|
900
934
|
--run-dir "$tdir" > "$tdir/qa.memory"
|
|
901
935
|
{
|
|
902
|
-
python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$tdir/prompt.md" --capsule "$tdir/baseline.capsule" --approach "$tdir/approach.md" --retry "$tdir/retry_findings.md" --memory "$tdir/dwarf.memory" --memory "$tdir/qa.memory"
|
|
936
|
+
python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$tdir/prompt.md" --capsule "$tdir/baseline.capsule" --approach "$tdir/approach.md" --retry "$tdir/retry_findings.md" ${prompt_extra[@]+"${prompt_extra[@]}"} --memory "$tdir/dwarf.memory" --memory "$tdir/qa.memory"
|
|
903
937
|
python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part head --kind task
|
|
904
938
|
echo
|
|
905
939
|
if [ -s "$tdir/failfirst.txt" ]; then cat "$tdir/failfirst.txt"; echo; fi
|
|
@@ -915,9 +949,10 @@ do_task_body() (
|
|
|
915
949
|
echo
|
|
916
950
|
fi
|
|
917
951
|
if [ -s "$tdir/guard.txt" ]; then
|
|
918
|
-
# Facts forge observed about how the implementer worked.
|
|
919
|
-
# implementer's own report
|
|
920
|
-
#
|
|
952
|
+
# Facts forge observed about how the implementer worked. On a first attempt the reviewer
|
|
953
|
+
# never sees the implementer's own report (a retry's reviewer gets the reply, to check
|
|
954
|
+
# against the code), so this is how an unfinished measurement or a stray process
|
|
955
|
+
# reaches the review.
|
|
921
956
|
echo "Implementer guard notes (observed by forge, not claimed by the implementer):"
|
|
922
957
|
sed 's/^/ - /' "$tdir/guard.txt"
|
|
923
958
|
echo "A promise of later work means a check may never have run; judge the diff on its own merits."
|
|
@@ -949,9 +984,45 @@ do_task_body() (
|
|
|
949
984
|
shadow_pid=$!
|
|
950
985
|
fi
|
|
951
986
|
forge_metric_phase dispatch
|
|
952
|
-
|
|
953
|
-
|
|
987
|
+
# Whether this round continues the reviewer's session. Every answer but "continued" is a
|
|
988
|
+
# fresh dispatch, and nothing here can abort the round: a doctor that fails means fresh.
|
|
989
|
+
local rs_mode=fresh rs_reason="" rs_id="" rs_row=1 rs_cont=()
|
|
990
|
+
if [ "$rs_on" = 1 ]; then
|
|
991
|
+
if [ -n "${FORGE_FRACTAL_RUN:-}" ] || forge_plan_is_fractal "$PLAN"; then
|
|
992
|
+
rs_reason=fractal
|
|
993
|
+
elif [ ! -s "$tdir/retry_findings.md" ]; then
|
|
994
|
+
rs_reason=first-round
|
|
995
|
+
else
|
|
996
|
+
FORGE_CAPABILITY_CACHE="${FORGE_CAPABILITY_CACHE:-$PLAN/capabilities}" \
|
|
997
|
+
/bin/bash "$DISPATCH" doctor --spec "$qa" --role qa $yq > "$tdir/qa.doctor" 2>&1 || : > "$tdir/qa.doctor"
|
|
998
|
+
IFS=$'\t' read -r rs_mode rs_reason rs_id < <(review_session_decide \
|
|
999
|
+
"$tdir/retry_findings.session" "$tdir/retry_findings.resolved" "$tdir/qa.doctor")
|
|
1000
|
+
[ "$rs_mode" = continued ] && rs_cont=(--continue-session "$rs_id")
|
|
1001
|
+
fi
|
|
1002
|
+
fi
|
|
1003
|
+
local qa_cmd=(/bin/bash "$DISPATCH" qa "$qa" --repo "$review" --run-dir "$tdir" \
|
|
1004
|
+
--review-base "$(cat "$tdir/review.base")" --ripwire-query-file "$tdir/prompt.md" --prompt-file "$tdir/qa.input" ${yq:+"$yq"} --output "${FORGE_OUTPUT:-summary}")
|
|
1005
|
+
"${qa_cmd[@]}" ${rs_cont[@]+"${rs_cont[@]}"} >"$tdir/qa.out" 2>&1
|
|
954
1006
|
rc=$?
|
|
1007
|
+
if [ "$rc" = 9 ] && [ "$rs_mode" = continued ]; then
|
|
1008
|
+
# The session was not continued (the harness cannot, or it failed). Same input, fresh: the
|
|
1009
|
+
# fresh dispatch decides the task. Why comes from the first dispatch's record, which the
|
|
1010
|
+
# second one clears.
|
|
1011
|
+
if [ "$(sed -n 's/^reason=//p' "$tdir/qa.continue" 2>/dev/null | head -1)" = unsupported ]; then
|
|
1012
|
+
rs_reason=continue-unsupported
|
|
1013
|
+
else
|
|
1014
|
+
rs_reason=continue-failed
|
|
1015
|
+
fi
|
|
1016
|
+
rs_mode=fresh
|
|
1017
|
+
mv "$tdir/qa.out" "$tdir/qa.continue.out" 2>/dev/null || true
|
|
1018
|
+
"${qa_cmd[@]}" >"$tdir/qa.out" 2>&1
|
|
1019
|
+
rc=$?
|
|
1020
|
+
fi
|
|
1021
|
+
if [ "$rs_on" = 1 ]; then
|
|
1022
|
+
# One row per round, describing the dispatch whose result counts.
|
|
1023
|
+
[ ! -f "$tdir/review_sessions.tsv" ] || rs_row=$(( $(wc -l < "$tdir/review_sessions.tsv") + 1 ))
|
|
1024
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$rs_row" "$attempt" "$rs_mode" "$rs_reason" "$rs_id" >> "$tdir/review_sessions.tsv"
|
|
1025
|
+
fi
|
|
955
1026
|
if [ -n "$shadow_pid" ]; then
|
|
956
1027
|
# On every path below, before any status is written: the shadow learns the primary is
|
|
957
1028
|
# done (which bounds how much longer it may take), and no shadow outlives its task, so
|
|
@@ -1036,7 +1107,8 @@ do_task() {
|
|
|
1036
1107
|
record_attempt "$1" "$2"
|
|
1037
1108
|
# The retry policy's state (escalation.next or escalation.stop), decided now rather than
|
|
1038
1109
|
# at the next retry so a spent retry budget still reports it and the scheduler sees it.
|
|
1039
|
-
python3 "$SKILL_DIR/scripts/forge-route.py" escalation --plan "$1" --task "$2" --check
|
|
1110
|
+
python3 "$SKILL_DIR/scripts/forge-route.py" escalation --plan "$1" --task "$2" --check \
|
|
1111
|
+
--qa-only-next "$(qa_only_next_flag "$1" "$2")" >/dev/null 2>&1 || true
|
|
1040
1112
|
return $rc
|
|
1041
1113
|
}
|
|
1042
1114
|
|
|
@@ -1429,13 +1501,23 @@ write_results() { # write_results <plan>
|
|
|
1429
1501
|
done
|
|
1430
1502
|
}
|
|
1431
1503
|
|
|
1504
|
+
# forecast_report PLAN: `forge dataset forecast --run`, as note lines. Whatever it does
|
|
1505
|
+
# (no dataset, a broken store, a slow read), the run goes on and its exit code is its own.
|
|
1506
|
+
forecast_report() {
|
|
1507
|
+
local line
|
|
1508
|
+
python3 "$SKILL_DIR/scripts/forge-dataset.py" forecast "$1" --run 2>/dev/null \
|
|
1509
|
+
| while IFS= read -r line; do note "$line"; done
|
|
1510
|
+
return 0
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1432
1513
|
# --- run ----------------------------------------------------------------------
|
|
1433
1514
|
do_run() (
|
|
1434
1515
|
local PLAN="${1:?run needs a plan dir}"; shift
|
|
1435
1516
|
local fractal_pipeline_args=("$@")
|
|
1436
|
-
local MAXP=3 DRY=0 jev_rc=0
|
|
1517
|
+
local MAXP=3 DRY=0 jev_rc=0 FORECAST=1
|
|
1437
1518
|
while [ $# -gt 0 ]; do
|
|
1438
1519
|
case "$1" in
|
|
1520
|
+
--no-forecast) FORECAST=0; shift ;;
|
|
1439
1521
|
--fractal) forge_fractal_flag on || exit $?; shift ;;
|
|
1440
1522
|
--fractal-auto-decompose) forge_fractal_flag on || exit $?; FRACTAL_ARGS+=("$1"); shift ;;
|
|
1441
1523
|
--no-fractal) forge_fractal_flag off || exit $?; shift ;;
|
|
@@ -1538,6 +1620,9 @@ do_run() (
|
|
|
1538
1620
|
fi
|
|
1539
1621
|
git -C "$int_wt" rev-parse HEAD > "$PLAN/accepted.integration"
|
|
1540
1622
|
local id failed=0
|
|
1623
|
+
# Also before any model is paid for: what this plan is likely to cost, and which tasks
|
|
1624
|
+
# history says may bounce. Advisory, budgeted and silent without history; it never stops a run.
|
|
1625
|
+
[ "$FORECAST" = 1 ] && forecast_report "$PLAN"
|
|
1541
1626
|
# Say now, before any model is paid for, which suites the verify command will not run.
|
|
1542
1627
|
verify_coverage_report "$REPO" "$(verify_command_for "$PLAN" "$REPO")" "$PLAN/verification.coverage.txt"
|
|
1543
1628
|
local sched_rc=0 paused=0 sched_args=()
|