@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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- 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
|
+
}
|