@giamme/forge 0.0.0-stage → 0.8.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 (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env bash
2
+ # Opt-in holistic final review for forge-parallel.sh. Sourced; functions only, nothing
3
+ # runs at source time, so a plan that never asks for it never spends a model call on it.
4
+ #
5
+ # Per-task QA only ever sees one slice of the work. This reviews the COMBINED result once,
6
+ # with the goal and every task's requirements in front of the reviewer, to catch what no
7
+ # slice can show: an interface one task changed and another consumes, an ordering race
8
+ # across modules, a contract regression that only appears when the pieces run together.
9
+ #
10
+ # Two entry points share one worker, final_review:
11
+ # review <plan> [--qa <spec>] [--yolo-qa] [--qa-threshold Pn] [--output summary|full]
12
+ # read-only: reviews the integration branch's combined work (base..tip) and writes
13
+ # $PLAN/final-review-<epoch>-<pid>/. Never touches the user's branch or the
14
+ # integration branch.
15
+ # integrate <plan> --approved --final-review <spec>
16
+ # after verification and before the user's branch moves, reviews parent..candidate
17
+ # (final_review_for_integrate); anything but a pass stops the integration.
18
+ #
19
+ # Exit codes (both): 0 passed (or a FAIL accepted by the --qa-threshold gate) | 2 usage
20
+ # (ambiguous QA spec, unknown option) | 3 precondition | 5 blocked (FAIL, no verdict,
21
+ # mutated during review, reviewer could not finish) | 8 the reviewer hit an infrastructure
22
+ # failure (quota, auth, network): nothing was judged.
23
+ #
24
+ # Artifacts, in the review directory (review: $PLAN/final-review-<epoch>-<pid>/, integrate:
25
+ # <integrate-dir>/final-review/): qa.input (the prompt), qa.last (the reviewer's report),
26
+ # qa.out, verdict (PASS | FAIL | UNKNOWN | INVALIDATED | ACCEPTED; absent when nothing was
27
+ # judged), qa.gate and known_issues.md (with a threshold), qa.infra (infrastructure stop),
28
+ # review.meta, context.md, notes.md, tasks.summary, combined.diff (when inlined),
29
+ # source/review.fingerprint and review-<pid>/ (the disposable checkout the reviewer ran in).
30
+ # `review` also keeps $PLAN/final-review (a link to the newest directory) and
31
+ # $PLAN/final-review.verdict (a copy of its verdict).
32
+ #
33
+ # Sourced by forge-parallel.sh, which provides die/note/field/all_ids/task_status,
34
+ # forge_pp_flag, SKILL_DIR and DISPATCH.
35
+
36
+ REVIEW_REQ_BUDGET=81920 # bytes of task requirements put in front of the reviewer
37
+ REVIEW_DIFF_LIMIT=409600 # largest combined diff placed inline in the prompt
38
+
39
+ # --- small helpers ---------------------------------------------------------------------
40
+ review_threshold() { # the plan-level blocking threshold, or nothing
41
+ [ -s "$1/qa_threshold" ] && head -1 "$1/qa_threshold" | tr -d ' \r'
42
+ return 0
43
+ }
44
+
45
+ review_yolo_flag() { # review_yolo_flag <plan> [force] -> "--yolo" or nothing
46
+ if [ -n "${2:-}" ] || [ -f "$1/yolo_qa" ] || [ -f "$1/final_yolo" ]; then echo --yolo; fi
47
+ return 0
48
+ }
49
+
50
+ # given > persisted $PLAN/final_qa > the one QA spec every task shares > ask. Only the
51
+ # first two are an explicit choice, so only a unanimous task table is trusted to fill in.
52
+ review_resolve_spec() { # review_resolve_spec <plan> [<given>] -> spec on stdout; rc 2 when ambiguous
53
+ local plan="$1" given="${2:-}" saved specs n list
54
+ if [ -n "$given" ]; then printf '%s\n' "$given"; return 0; fi
55
+ saved="$(head -1 "$plan/final_qa" 2>/dev/null | tr -d ' \r')"
56
+ if [ -n "$saved" ]; then printf '%s\n' "$saved"; return 0; fi
57
+ specs="$(awk -F'\t' '!/^#/ && NF && $6 != "" {print $6}' "$plan/tasks.tsv" | sort -u)"
58
+ n="$(printf '%s\n' "$specs" | awk 'NF' | wc -l | tr -d ' ')"
59
+ if [ "$n" = 1 ]; then printf '%s\n' "$specs"; return 0; fi
60
+ list="$(printf '%s\n' "$specs" | awk 'NF' | tr '\n' ' ')"
61
+ if [ "$n" = 0 ]; then
62
+ note "review: no QA spec found in $plan/tasks.tsv; choose one with --qa <spec>"
63
+ else
64
+ note "review: this plan's tasks use $n different QA specs: ${list% }"
65
+ note "review: choose one for the whole-run review with --qa <spec> (it is remembered in $plan/final_qa)"
66
+ fi
67
+ return 2
68
+ }
69
+
70
+ review_task_table() { # id <TAB> status <TAB> title <TAB> files, one row per task
71
+ local plan="$1" id
72
+ for id in $(all_ids "$plan/tasks.tsv"); do
73
+ printf '%s\t%s\t%s\t%s\n' "$id" "$(task_status "$plan" "$id")" \
74
+ "$(field "$plan/tasks.tsv" "$id" title)" "$(field "$plan/tasks.tsv" "$id" files)"
75
+ done
76
+ }
77
+
78
+ # Tasks that are not MERGED are not in the combined diff, so say so rather than let the
79
+ # reviewer or the user assume the whole plan was judged.
80
+ review_warn_unmerged() { # review_warn_unmerged <plan>
81
+ local plan="$1" id st list=""
82
+ for id in $(all_ids "$plan/tasks.tsv"); do
83
+ st="$(task_status "$plan" "$id")"
84
+ [ "$st" = MERGED ] || list="$list $id($st)"
85
+ done
86
+ [ -z "$list" ] || note "WARNING: not merged, so NOT part of the reviewed result:$list"
87
+ return 0
88
+ }
89
+
90
+ # The prompt is assembled in two halves (before and after the contract's head) by one
91
+ # python helper: it needs byte-accurate truncation, which is miserable in bash 3.2.
92
+ # pre goal, task table, every task's complete requirements (bounded, visibly truncated)
93
+ # notes what forge observed that no per-task reviewer was shown
94
+ review_context() { # review_context <pre|notes> <plan> <tasks.summary> <verification-dir>
95
+ python3 - "$1" "$2" "$3" "$4" "$REVIEW_REQ_BUDGET" <<'PY'
96
+ import os, re, sys
97
+
98
+ mode, plan, summary, vdir, budget = sys.argv[1:6]
99
+ BUDGET = int(budget)
100
+
101
+
102
+ def read(path):
103
+ try:
104
+ with open(path, 'rb') as fh:
105
+ return fh.read()
106
+ except OSError:
107
+ return None
108
+
109
+
110
+ def text(raw):
111
+ return raw.decode('utf-8', 'replace').strip()
112
+
113
+
114
+ rows = []
115
+ raw = read(summary)
116
+ for line in (raw or b'').decode('utf-8', 'replace').splitlines():
117
+ cells = line.split('\t')
118
+ if cells and cells[0]:
119
+ rows.append((cells + ['', '', '', ''])[:4])
120
+
121
+ out = []
122
+ if mode == 'pre':
123
+ goal = read(os.path.join(plan, 'goal.txt'))
124
+ out += ['## Overall goal', text(goal) if goal and goal.strip() else
125
+ '(this plan recorded no goal; judge the change against the task requirements below)', '']
126
+ out += ['## Tasks in this run',
127
+ 'Each task was implemented on its own branch by a separate implementer and reviewed on',
128
+ 'its own slice only. Only MERGED tasks are part of the combined diff you are reviewing.', '']
129
+ width = max([len(r[0]) for r in rows] + [2])
130
+ for tid, status, title, files in rows:
131
+ out.append(' %-*s %-10s %s [files: %s]' % (width, tid, status, title, files))
132
+ out.append('')
133
+
134
+ entries = []
135
+ for tid, status, title, files in rows:
136
+ path = os.path.join(plan, 'tasks', tid, 'prompt.md')
137
+ entries.append((tid, status, title, path, read(path)))
138
+ lengths = [len(b) for *_, b in entries if b is not None]
139
+ cap = None
140
+ if sum(lengths) > BUDGET:
141
+ # Cut only the longest requirements: find the largest per-task size that fits.
142
+ lo, hi = 0, max(lengths)
143
+ while lo < hi:
144
+ mid = (lo + hi + 1) // 2
145
+ if sum(min(n, mid) for n in lengths) <= BUDGET:
146
+ lo = mid
147
+ else:
148
+ hi = mid - 1
149
+ cap = lo
150
+ out += ['## Complete requirements of each task (as given to its implementer)', '']
151
+ for tid, status, title, path, body in entries:
152
+ tag = status if status == 'MERGED' else '%s: NOT in the diff below' % status
153
+ out.append('### Task %s: %s [%s]' % (tid, title, tag))
154
+ if body is None:
155
+ out += ['(no prompt.md was recorded for this task)', '']
156
+ continue
157
+ if cap is not None and len(body) > cap:
158
+ kept = body[:cap].decode('utf-8', 'ignore')
159
+ removed = len(body) - len(kept.encode('utf-8'))
160
+ out += [kept.rstrip(),
161
+ '[... truncated %d bytes; full text at %s ...]' % (removed, path), '']
162
+ else:
163
+ out += [text(body), '']
164
+ else:
165
+ sections = []
166
+
167
+ def capped(raw_bytes, limit=6144):
168
+ t = text(raw_bytes)
169
+ if len(t.encode('utf-8')) > limit:
170
+ t = t.encode('utf-8')[:limit].decode('utf-8', 'ignore').rstrip() + '\n[... truncated ...]'
171
+ return t
172
+
173
+ def per_task(name, title, intro, findings_only=False):
174
+ found = []
175
+ for tid, status, _t, _f in rows:
176
+ raw_bytes = read(os.path.join(plan, 'tasks', tid, name))
177
+ if raw_bytes and raw_bytes.strip():
178
+ lines = capped(raw_bytes).splitlines()
179
+ if findings_only:
180
+ # known_issues.md opens with a heading and a paragraph about the gate; the
181
+ # reviewer needs the findings, not forge's bookkeeping.
182
+ start = next((i for i, l in enumerate(lines) if re.match(r'\s*(?:[-*]|\d+[.)])\s', l)), 0)
183
+ lines = lines[start:]
184
+ found.append('Task %s:\n%s' % (tid, '\n'.join(' ' + l for l in lines)))
185
+ if found:
186
+ sections.append('%s\n%s\n\n%s' % (title, intro, '\n'.join(found)))
187
+
188
+ per_task('known_issues.md', 'Known issues already accepted for the individual tasks:',
189
+ 'These findings were recorded and tolerated; do not re-report them unless they are worse than stated.',
190
+ findings_only=True)
191
+ per_task('drift.txt', 'Scope drift (files a task changed without declaring them):',
192
+ 'Another task may own these files; check the combined effect.')
193
+ per_task('guard.txt', 'Implementer guard notes (observed by forge, not claimed by the implementers):',
194
+ 'A promise of later work means a check may never have run.')
195
+ ver = []
196
+ status = read(os.path.join(vdir, 'verification.status'))
197
+ if status and status.strip():
198
+ ver.append('status: %s' % text(status))
199
+ cmd = read(os.path.join(vdir, 'verification.command'))
200
+ if cmd and cmd.strip():
201
+ ver.append('command: %s' % text(cmd))
202
+ flaky = read(os.path.join(vdir, 'verification.flaky'))
203
+ if flaky and flaky.strip():
204
+ ver.append('FLAKY: the command failed once and passed on rerun, so the pass is not clean:')
205
+ ver += [' ' + l for l in text(flaky).splitlines() if not l.startswith(('first_log=', 'retry_log='))]
206
+ cover = read(os.path.join(vdir, 'verification.coverage.txt'))
207
+ if cover and cover.strip():
208
+ ver.append('suites the project defines that the verify command does not run:')
209
+ ver += [' ' + l for l in capped(cover).splitlines()]
210
+ if ver:
211
+ sections.append('Verification of the combined result (run by forge, not by you):\n'
212
+ + '\n'.join(' ' + l for l in ver))
213
+ out += ['## Context the per-task reviewers did not have']
214
+ if sections:
215
+ out += ['Forge recorded the following about the combined run.', '']
216
+ for s in sections:
217
+ out += [s, '']
218
+ out += ['Your working directory is a disposable checkout of the combined result, committed',
219
+ 'over its base. Do not modify it; a change invalidates this review.', '']
220
+ sys.stdout.buffer.write(('\n'.join(out) + '\n').encode('utf-8'))
221
+ PY
222
+ }
223
+
224
+ review_infra() { # review_infra <outdir>: what an infrastructure stop means, from qa.infra
225
+ local out="$1" cls after
226
+ cls="$(sed -n 's/^class=//p' "$out/qa.infra" 2>/dev/null | head -1)"
227
+ after="$(sed -n 's/^retry_after=//p' "$out/qa.infra" 2>/dev/null | head -1)"
228
+ echo
229
+ echo "final review paused: the reviewer hit an infrastructure failure (${cls:-unknown}) before judging anything."
230
+ echo " evidence: $out/qa.infra"
231
+ case "$after" in ''|*[!0-9]*) ;; *) echo " the provider suggests waiting about ${after}s" ;; esac
232
+ echo " fix the cause (quota, login, network); no verdict was recorded."
233
+ }
234
+
235
+ # Mirror the newest result into the plan dir: $PLAN/final-review (a link to its directory)
236
+ # and $PLAN/final-review.verdict. A review that never reached the reviewer leaves both as
237
+ # they were; one that ended without a verdict (an infrastructure stop) clears the old
238
+ # verdict, because an earlier PASS must not be mistaken for the latest result.
239
+ review_publish_latest() { # review_publish_latest <plan> <outdir>
240
+ local plan="$1" out="$2" rel="$2"
241
+ [ -s "$out/qa.input" ] || return 0
242
+ case "$out" in "$plan"/*) rel="${out#"$plan"/}" ;; esac
243
+ python3 - "$plan" "$rel" <<'PY' 2>/dev/null || true
244
+ import os, sys
245
+ plan, rel = sys.argv[1:3]
246
+ link = os.path.join(plan, 'final-review')
247
+ tmp = '%s.tmp%d' % (link, os.getpid())
248
+ try:
249
+ os.symlink(rel, tmp)
250
+ os.replace(tmp, link)
251
+ except OSError:
252
+ try:
253
+ os.unlink(tmp)
254
+ except OSError:
255
+ pass
256
+ PY
257
+ if [ -s "$out/verdict" ]; then cp "$out/verdict" "$plan/final-review.verdict"
258
+ else rm -f "$plan/final-review.verdict"; fi
259
+ return 0
260
+ }
261
+
262
+ # --- the shared worker -----------------------------------------------------------------
263
+ # final_review <plan> <base-rev> <tip-rev> <src> <outdir> <qa-spec> [<yolo-flag>]
264
+ # src is a repository or worktree that contains both revisions; it is also the checkout
265
+ # whose fingerprint must not change while the reviewer runs. Never uses `die`/`exit`:
266
+ # it is called from inside do_integrate.
267
+ final_review() {
268
+ local plan="$1" base="$2" tip="$3" src="$4" out="$5" spec="$6" yolo="${7:-}"
269
+ local review n rc verdict thr gate_line source_fp review_fp vdir tflag="" inline=1 diff_n
270
+ [ -n "$yolo" ] && yolo=--yolo
271
+ [ -n "$spec" ] || { note "final review: no QA spec given"; return 3; }
272
+ mkdir -p "$out" && out="$(cd "$out" && pwd)" || { note "final review: cannot create '$out'"; return 3; }
273
+ vdir="$(dirname "$out")"
274
+ [ -f "$vdir/verification.status" ] || vdir="$plan"
275
+ thr="$(review_threshold "$plan")"
276
+ rm -f "$out/verdict" "$out/qa.gate" "$out/known_issues.md" "$out/qa.infra"
277
+
278
+ git -C "$src" rev-parse --verify --quiet "$base^{commit}" >/dev/null &&
279
+ git -C "$src" rev-parse --verify --quiet "$tip^{commit}" >/dev/null ||
280
+ { note "final review: '$base' or '$tip' is not a commit in $src"; return 3; }
281
+ if [ "$(git -C "$src" rev-parse "$base^{tree}")" = "$(git -C "$src" rev-parse "$tip^{tree}")" ]; then
282
+ note "final review: the combined result is identical to its base; there is nothing to review"
283
+ return 3
284
+ fi
285
+
286
+ # Offline, and before anything is built or paid for: a spec that cannot resolve fails here.
287
+ FORGE_CAPABILITY_CACHE="${FORGE_CAPABILITY_CACHE:-$plan/capabilities}" \
288
+ /bin/bash "$DISPATCH" doctor --spec "$spec" --role qa $yolo > "$out/doctor.out" 2>&1
289
+ rc=$?
290
+ if [ "$rc" -ne 0 ]; then
291
+ cat "$out/doctor.out" >&2
292
+ note "final review: QA spec '$spec' cannot be used (see above)"
293
+ return "$rc"
294
+ fi
295
+
296
+ # `forge_review_snapshot` writes review.base beside the snapshot, so it gets a directory
297
+ # of its own under the artifact dir.
298
+ review="$out/review-$$"; n=0
299
+ while [ -e "$review" ]; do n=$((n+1)); review="$out/review-$$-$n"; done
300
+ forge_review_snapshot "$src" "$base" "$tip" "$review" > "$out/snapshot.log" 2>&1 ||
301
+ { note "final review: cannot build the review snapshot (see $out/snapshot.log)"; return 3; }
302
+ source_fp="$(forge_fingerprint "$src")" && review_fp="$(forge_fingerprint "$review")" ||
303
+ { note "final review: cannot fingerprint the checkouts"; return 3; }
304
+ printf '%s\n' "$source_fp" > "$out/source.fingerprint"
305
+ printf '%s\n' "$review_fp" > "$out/review.fingerprint"
306
+ {
307
+ echo "base=$base"; echo "tip=$tip"; echo "source=$src"; echo "spec=$spec"; echo "threshold=${thr:-none}"
308
+ } > "$out/review.meta"
309
+
310
+ review_task_table "$plan" > "$out/tasks.summary"
311
+ review_context pre "$plan" "$out/tasks.summary" "$vdir" > "$out/context.md" &&
312
+ review_context notes "$plan" "$out/tasks.summary" "$vdir" > "$out/notes.md" ||
313
+ { note "final review: cannot assemble the review context"; return 3; }
314
+
315
+ # At most the limit plus one byte is ever read, so an enormous change cannot fill the disk.
316
+ git -C "$src" diff --binary "$base" "$tip" -- . ':(exclude).forge' 2>/dev/null |
317
+ head -c $((REVIEW_DIFF_LIMIT + 1)) > "$out/combined.diff"
318
+ diff_n="$(wc -c < "$out/combined.diff" | tr -d ' ')"
319
+ if [ "$diff_n" -gt "$REVIEW_DIFF_LIMIT" ]; then inline=0; rm -f "$out/combined.diff"; fi
320
+
321
+ {
322
+ cat "$out/context.md"
323
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part head --kind final
324
+ echo
325
+ cat "$out/notes.md"
326
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part tail --kind final ${thr:+--threshold "$thr"}
327
+ echo
328
+ if [ "$inline" = 1 ]; then
329
+ echo '```diff'; cat "$out/combined.diff"; echo '```'
330
+ else
331
+ # The snapshot presents the work as ONE commit over the base, so HEAD~1..HEAD is it.
332
+ echo "The combined diff is larger than $((REVIEW_DIFF_LIMIT / 1024)) KB, so it is not inlined. Read it in your"
333
+ echo "working directory, which holds the combined result as the last commit over its base:"
334
+ echo " git diff HEAD~1 HEAD -- . ':(exclude).forge'"
335
+ echo "(and 'git diff --stat HEAD~1 HEAD' for the overview)."
336
+ fi
337
+ } > "$out/qa.input"
338
+
339
+ [ -s "$plan/timeout" ] && tflag="--timeout $(cat "$plan/timeout")"
340
+ note "final review: $spec on $(git -C "$src" diff --shortstat "$base" "$tip" -- . ':(exclude).forge' 2>/dev/null | sed 's/^ *//')"
341
+ 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
345
+ rc=$?
346
+ if [ "${FORGE_OUTPUT:-summary}" = full ]; then cat "$out/qa.out"; fi
347
+ case "$rc" in
348
+ 0) ;;
349
+ 8) note "final review: the reviewer hit an infrastructure failure; nothing was judged"
350
+ review_infra "$out"
351
+ return 8 ;;
352
+ 2|3) note "final review: the QA dispatch could not start (exit $rc); see $out/qa.out"
353
+ return 3 ;;
354
+ *) printf 'UNKNOWN\n' > "$out/verdict"
355
+ echo
356
+ if [ "$rc" = 7 ]; then
357
+ echo "final review: the reviewer hit the dispatch timeout and was stopped; nothing was concluded (blocking)."
358
+ echo " see $out/qa.out; raise it with FORGE_TIMEOUT=<seconds> or 'run --timeout S' and review again"
359
+ else
360
+ echo "final review: the QA dispatch failed (exit $rc); nothing was concluded (blocking)."
361
+ echo " see $out/qa.out"
362
+ fi
363
+ return 5 ;;
364
+ esac
365
+
366
+ if [ "$(forge_fingerprint "$src")" != "$source_fp" ] || [ "$(forge_fingerprint "$review")" != "$review_fp" ]; then
367
+ printf 'INVALIDATED\n' > "$out/verdict"
368
+ note "final review: the checkout changed while the reviewer ran; the review is invalid"
369
+ echo
370
+ echo "final review ($spec): verdict INVALIDATED (blocking) — a file was modified during the review, so it cannot be trusted."
371
+ echo " report: $out/qa.last"
372
+ return 5
373
+ fi
374
+
375
+ verdict=""
376
+ [ -s "$out/qa.last" ] && verdict="$(forge_verdict "$out/qa.last")"
377
+ verdict="${verdict:-UNKNOWN}"
378
+ if [ -n "$thr" ]; then
379
+ # Same gate as per-task QA: a FAIL made only of labelled findings below the threshold
380
+ # is accepted with known issues; anything unlabelled stays blocking.
381
+ gate_line="$(python3 "$SKILL_DIR/scripts/forge-contract.py" gate --verdict "$verdict" \
382
+ --qa-last "$out/qa.last" --threshold "$thr" --known-issues "$out/known_issues.md" 2>/dev/null)" || gate_line=""
383
+ if [ -n "$gate_line" ]; then
384
+ printf '%s\t%s\n' "$thr" "$gate_line" > "$out/qa.gate"
385
+ if [ "$(printf '%s' "$gate_line" | awk -F'\t' '{print $1}')" = accept ]; then verdict=ACCEPTED; fi
386
+ fi
387
+ fi
388
+ printf '%s\n' "$verdict" > "$out/verdict"
389
+
390
+ echo
391
+ case "$verdict" in
392
+ PASS)
393
+ echo "final review ($spec): verdict PASS — the reviewer found no confirmed defect in the combined result."
394
+ echo " report: $out/qa.last" ;;
395
+ ACCEPTED)
396
+ echo "final review ($spec): the reviewer said FAIL, but every confirmed finding is less severe than the $thr threshold — accepted with known issues."
397
+ echo " known issues: $out/known_issues.md"
398
+ echo " report: $out/qa.last" ;;
399
+ FAIL)
400
+ echo "final review ($spec): verdict FAIL (blocking) — the reviewer confirmed at least one defect in the combined result."
401
+ echo " findings: $out/qa.last" ;;
402
+ *)
403
+ echo "final review ($spec): verdict UNKNOWN (blocking) — the reviewer ended without a FORGE_VERDICT line, so nothing was concluded."
404
+ echo " read: $out/qa.last" ;;
405
+ esac
406
+ case "$verdict" in PASS|ACCEPTED) return 0 ;; esac
407
+ return 5
408
+ }
409
+
410
+ # --- integrate hook ----------------------------------------------------------------------
411
+ # Called by do_integrate after verification passed and before the user's branch can move.
412
+ final_review_for_integrate() { # <plan> <parent> <candidate_sha> <candidate> <out> <spec>
413
+ local plan="$1" parent="$2" sha="$3" candidate="$4" out="$5" spec="${6:-}" rc
414
+ [ -n "$spec" ] || { note "final review: --final-review needs a QA spec"; return 3; }
415
+ printf '%s' "$spec" > "$plan/final_qa"
416
+ note "final review of the combined candidate before it touches your branch"
417
+ final_review "$plan" "$parent" "$sha" "$candidate" "$out/final-review" "$spec" "$(review_yolo_flag "$plan")"
418
+ rc=$?
419
+ review_publish_latest "$plan" "$out/final-review"
420
+ case "$rc" in
421
+ 0) echo " the final review allows the merge; continuing." ;;
422
+ 8) echo " nothing was merged and your branch is untouched. Fix the cause, then run the same command again:"
423
+ echo " forge-parallel.sh integrate $plan --approved --final-review $spec" ;;
424
+ 5) echo
425
+ echo "integration stopped by the final review: nothing was merged and your branch is untouched."
426
+ echo " The reviewer's findings are in $out/final-review/qa.last; address them, then integrate again."
427
+ echo " The combined candidate is still at $candidate for inspection." ;;
428
+ esac
429
+ return "$rc"
430
+ }
431
+
432
+ # --- the subcommand ----------------------------------------------------------------------
433
+ do_review() {
434
+ local PLAN="${1:?review needs a plan dir}"; shift
435
+ local qa_arg="" yolo_given="" routput=summary
436
+ while [ $# -gt 0 ]; do
437
+ case "$1" in
438
+ --qa) qa_arg="${2:?--qa needs a spec}"; shift 2 ;;
439
+ --yolo-qa) yolo_given=1; shift ;;
440
+ --output) routput="${2:?--output needs summary or full}"; shift 2 ;;
441
+ --no-ripwire) export FORGE_RIPWIRE=off; shift ;;
442
+ *) if forge_pp_flag review "$PLAN" "$@"; then shift "$PP_SHIFT"; else die "review: unknown option '$1'"; fi ;;
443
+ esac
444
+ done
445
+ case "$routput" in summary|full) ;; *) die "--output must be summary or full" ;; esac
446
+ export FORGE_OUTPUT="$routput"
447
+
448
+ local T="$PLAN/tasks.tsv" REPO run_id int_br int_wt tip base spec yolo out rc id merged=0
449
+ [ -s "$T" ] || die "no tasks.tsv in '$PLAN'" 3
450
+ [ -s "$PLAN/wt_root" ] || die "this plan has never been run; there is no combined result to review" 3
451
+ REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
452
+ int_br="forge/$run_id-integration"; int_wt="$(cat "$PLAN/wt_root")/_integration"
453
+ tip="$(git -C "$REPO" rev-parse --verify --quiet "$int_br^{commit}")" ||
454
+ die "integration branch $int_br does not exist; run the plan first" 3
455
+ if [ ! -s "$PLAN/accepted.integration" ] || [ "$tip" != "$(cat "$PLAN/accepted.integration")" ]; then
456
+ die "$int_br is not at the revision this run accepted (it moved outside forge); refusing to review something forge did not merge" 3
457
+ fi
458
+ [ -e "$int_wt/.git" ] || die "the integration worktree $int_wt is missing; nothing to take a trustworthy source fingerprint from" 3
459
+ for id in $(all_ids "$T"); do [ "$(task_status "$PLAN" "$id")" = MERGED ] && merged=$((merged+1)); done
460
+ [ "$merged" -gt 0 ] || die "no task has been merged, so there is no combined result to review" 3
461
+
462
+ if [ -s "$PLAN/integration.base" ]; then
463
+ base="$(cat "$PLAN/integration.base")"
464
+ else
465
+ # Plans created before integration.base existed: where the branch forked from the user's.
466
+ base="$(git -C "$REPO" merge-base "$int_br" HEAD 2>/dev/null)"
467
+ note "review: no integration.base recorded for this plan; using the merge-base with HEAD (${base:-none})"
468
+ fi
469
+ base="$(git -C "$REPO" rev-parse --verify --quiet "${base:-none}^{commit}")" ||
470
+ die "cannot tell where $int_br started; there is no base to diff against" 3
471
+
472
+ spec="$(review_resolve_spec "$PLAN" "$qa_arg")" || return $?
473
+ printf '%s' "$spec" > "$PLAN/final_qa"
474
+ [ -z "$yolo_given" ] || : > "$PLAN/final_yolo"
475
+ yolo="$(review_yolo_flag "$PLAN" "$yolo_given")"
476
+ review_warn_unmerged "$PLAN"
477
+
478
+ out="$PLAN/final-review-$(date +%s)-$$"
479
+ note "reviewing $int_br as a whole ($merged of $(all_ids "$T" | wc -l | tr -d ' ') task(s) merged); your branch and the integration branch are not touched"
480
+ final_review "$PLAN" "$base" "$tip" "$int_wt" "$out" "$spec" "$yolo"
481
+ rc=$?
482
+ review_publish_latest "$PLAN" "$out"
483
+ echo
484
+ case "$rc" in
485
+ 0) echo "Nothing was changed. If you are satisfied, bring the result into your branch with"
486
+ echo " forge-parallel.sh integrate $PLAN --approved" ;;
487
+ 5) echo "Nothing was merged or changed. The integration branch is exactly as it was."
488
+ echo "Act on the findings with a follow-up task (the reviewer's report is the brief), or"
489
+ echo "'forge-parallel.sh retry $PLAN <task-id>' for a task that is not MERGED; then review again." ;;
490
+ 8) echo "Nothing was judged and nothing was changed. Fix the cause, then run again:"
491
+ echo " forge-parallel.sh review $PLAN" ;;
492
+ esac
493
+ return "$rc"
494
+ }