@giamme/forge 0.9.0 → 0.11.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/SKILL.md +22 -3
  3. package/VERSION +1 -1
  4. package/docs/artifacts.md +28 -2
  5. package/docs/commands.md +55 -0
  6. package/docs/dataset.md +22 -0
  7. package/docs/decomposed-runs.md +54 -2
  8. package/docs/forge-ui.md +48 -7
  9. package/docs/memory.md +4 -2
  10. package/docs/reviews.md +87 -2
  11. package/forge +31 -1
  12. package/package.json +1 -1
  13. package/references/dataset.md +103 -4
  14. package/references/decompose.md +41 -5
  15. package/references/harnesses.md +43 -1
  16. package/references/ui.md +31 -6
  17. package/scripts/forge-config.py +350 -0
  18. package/scripts/forge-control.py +649 -0
  19. package/scripts/forge-dispatch.sh +235 -121
  20. package/scripts/forge-issues.py +1134 -0
  21. package/scripts/forge-lib-plans.sh +6 -2
  22. package/scripts/forge-lib-review.sh +158 -3
  23. package/scripts/forge-lib-task.sh +358 -41
  24. package/scripts/forge-parallel-options.sh +3 -1
  25. package/scripts/forge-parallel.sh +99 -13
  26. package/scripts/forge-prompt.py +61 -4
  27. package/scripts/forge-route.py +245 -28
  28. package/scripts/forge-runs-rm.py +840 -0
  29. package/scripts/forge-runs.py +19 -1
  30. package/scripts/forge-runtime.py +47 -1
  31. package/scripts/forge-schedule.py +227 -11
  32. package/scripts/forge-ui.py +85 -19
  33. package/scripts/forge_dataset/__init__.py +84 -8
  34. package/scripts/forge_dataset/capture.py +5 -2
  35. package/scripts/forge_dataset/cli.py +44 -1
  36. package/scripts/forge_dataset/export.py +36 -3
  37. package/scripts/forge_dataset/forecast.py +355 -0
  38. package/scripts/forge_dataset/harvest.py +45 -2
  39. package/scripts/forge_dataset/importer.py +431 -17
  40. package/scripts/forge_dataset/reviewers.py +145 -0
  41. package/scripts/forge_dataset/routing.py +1 -0
  42. package/scripts/forge_jev/__init__.py +7 -3
  43. package/scripts/forge_registry.py +183 -26
  44. package/ui/README.md +41 -7
  45. package/ui/docs/api.md +259 -21
  46. package/ui/forge_ui/__main__.py +10 -2
  47. package/ui/forge_ui/control.py +587 -0
  48. package/ui/forge_ui/export.py +17 -3
  49. package/ui/forge_ui/reader.py +210 -12
  50. package/ui/forge_ui/server.py +125 -4
  51. package/ui/forge_ui/static/app.js +104 -18
  52. package/ui/forge_ui/static/cloud3d.js +4 -1
  53. package/ui/forge_ui/static/cloudmodel.js +1 -1
  54. package/ui/forge_ui/static/cloudui.js +13 -4
  55. package/ui/forge_ui/static/controls.js +619 -0
  56. package/ui/forge_ui/static/findingsview.js +438 -0
  57. package/ui/forge_ui/static/graph.js +6 -5
  58. package/ui/forge_ui/static/index.html +7 -1
  59. package/ui/forge_ui/static/leaderboard.js +18 -0
  60. package/ui/forge_ui/static/modelstats.js +19 -1
  61. package/ui/forge_ui/static/panel.js +76 -14
  62. package/ui/forge_ui/static/rundelete.js +388 -0
  63. package/ui/forge_ui/static/runmodel.js +128 -13
  64. package/ui/forge_ui/static/scene3d.js +32 -15
  65. package/ui/forge_ui/static/settings.js +399 -0
  66. package/ui/forge_ui/static/style.css +184 -5
  67. package/ui/forge_ui/static/uikit.js +161 -0
@@ -130,7 +130,7 @@ plans_make_integration() { # plans_make_integration <repo> <branch> <commit> <wt
130
130
 
131
131
  # Per-plan option files carried to a derived plan. The cost-bearing choices a user made
132
132
  # for this plan apply to the plan made from it; `planner` does not (nothing plans again).
133
- PLANS_OPTION_FILES="no_ripwire no_memory yolo_dwarf yolo_qa qa_threshold timeout verify_retries verify_cmd setup_cmd final_qa shadow_qa shadow_rate explore escalate_after fractal-routing.json fractal-selection.json jev-selection.json"
133
+ PLANS_OPTION_FILES="no_ripwire no_memory yolo_dwarf yolo_qa reviewer_session qa_threshold timeout verify_retries verify_cmd setup_cmd final_qa shadow_qa shadow_rate explore escalate_after fractal-routing.json fractal-selection.json jev-selection.json"
134
134
 
135
135
  # --- accept ---------------------------------------------------------------------
136
136
  # A reviewer's FAIL is the QA gate; overriding it is the user's call and never forge's or
@@ -505,7 +505,7 @@ Retry or accept them (or split them off into their own plan) first." 3
505
505
  [ "$ok" = 1 ] || { plans_undo; trap - INT TERM; die "could not write the combined plan" 3; }
506
506
 
507
507
  # Options: a plan-wide choice carries over when every source made it identically; the
508
- # two permission flags only when every source granted them.
508
+ # permission flags and the reviewer-session experiment only when every source chose them.
509
509
  local goals="" g
510
510
  for p in "${SRC[@]}"; do
511
511
  if [ -s "$p/goal.txt" ]; then g="$(tr '\n' ' ' < "$p/goal.txt" | sed 's/ *$//')"; else g="$(basename "$p")"; fi
@@ -520,6 +520,10 @@ Retry or accept them (or split them off into their own plan) first." 3
520
520
  all=1; for p in "${SRC[@]}"; do [ -f "$p/$f" ] || all=0; done
521
521
  [ "$all" = 1 ] && : > "$NEW/$f"
522
522
  done
523
+ # Same gate as the permission flags. The file holds `on`, which is what `plan` writes
524
+ # and what the dataset records; readers only test that the file exists.
525
+ all=1; for p in "${SRC[@]}"; do [ -f "$p/reviewer_session" ] || all=0; done
526
+ [ "$all" = 1 ] && printf '%s\n' on > "$NEW/reviewer_session"
523
527
  for f in qa_threshold timeout verify_retries; do
524
528
  same=1
525
529
  for p in "${SRC[@]}"; do
@@ -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
- /bin/bash "$DISPATCH" qa "$spec" --repo "$review" --run-dir "$out" \
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) ;;