@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
package/scripts/forge-issues.py
CHANGED
|
@@ -6,6 +6,13 @@ A run can end with findings nobody will act on: CONFIRMED findings `--qa-thresho
|
|
|
6
6
|
`--include-plausible`), and whatever a whole-run review left open. This drafts one issue per
|
|
7
7
|
finding (or per task/run with `--group`) and creates them only with `--apply`.
|
|
8
8
|
|
|
9
|
+
A whole-run review whose integration branch has moved is marked outdated: its findings stay
|
|
10
|
+
listed (a later merge may have fixed only some of them) and the issue names the tasks that
|
|
11
|
+
merged after it. CONFIRMED findings under `## Accepted by a human despite the reviewer's`
|
|
12
|
+
heading are origin `accepted` — a person accepted that verdict — not `tolerated`. A grouped
|
|
13
|
+
issue repeats every distinct acceptance in the group, and the outdated-review sentence of
|
|
14
|
+
every review in it, not only the first finding's.
|
|
15
|
+
|
|
9
16
|
<run> is a plan dir, a task dir, a review dir (`<plan>/final-review`), known_issues.md or qa.last.
|
|
10
17
|
Tasks that did not pass are skipped: their findings still block them.
|
|
11
18
|
|
|
@@ -14,7 +21,7 @@ gh-proman computes, and both tools skip a finding either of them already filed,
|
|
|
14
21
|
When the gh-proman skill is configured for the repository, prefer `gh-proman findings`: it also
|
|
15
22
|
adds the issues to the team's project board with a priority. This command is the fallback.
|
|
16
23
|
|
|
17
|
-
--json lists every finding in `findings` (fp, severity, origin, plan, task, text, `filed`, ...). --offline never
|
|
24
|
+
--json lists every finding in `findings` (fp, severity, origin, plan, task, text, `filed`, `review`, ...). --offline never
|
|
18
25
|
runs `gh` (no filed state, publication of commits judged from local refs only). --only FP[,FP] keeps those
|
|
19
26
|
fingerprints. --all-runs reads <run> as a repository (or a plan dir naming one) and collects every run in its
|
|
20
27
|
registry, the newest plan winning for a finding present in several. A caller filing an explicit selection
|
|
@@ -48,6 +55,7 @@ import errno
|
|
|
48
55
|
import fcntl
|
|
49
56
|
import hashlib
|
|
50
57
|
import json
|
|
58
|
+
import math
|
|
51
59
|
import os
|
|
52
60
|
import re
|
|
53
61
|
import stat
|
|
@@ -76,11 +84,16 @@ TEXT = {
|
|
|
76
84
|
"plausible": "To verify: ",
|
|
77
85
|
"tolerated": "Forge review finding left open: it was below the blocking threshold {threshold}, so the "
|
|
78
86
|
"work was merged without fixing it.",
|
|
87
|
+
"accepted": "A person accepted the work over the reviewer's {status} verdict, so it was merged without fixing it.",
|
|
88
|
+
"accepted_reason": "Reason: {reason}",
|
|
79
89
|
"review": "Forge whole-run review finding left open: check whether it was already fixed before working on it.",
|
|
90
|
+
"outdated": "The review is outdated: {n} {tasks} merged into the integration branch after it{ids}, "
|
|
91
|
+
"so check whether they fixed it first.",
|
|
80
92
|
"plausible_ctx": "PLAUSIBLE forge review finding (low confidence): verify it before fixing. If it turns out "
|
|
81
93
|
"to be wrong, close the issue saying why.",
|
|
82
94
|
"group": "Forge review findings left open in run `{plan}` ({n}).",
|
|
83
95
|
"group_title": "Non-blocking forge findings: {scope}",
|
|
96
|
+
"group_title_accepted": "Accepted forge findings: {scope}",
|
|
84
97
|
"origin": "Origin: forge `{plan}`, task `{task}`, severity {severity}, reviewed by `{reviewer}`{commit}.",
|
|
85
98
|
"headings": ["Context", "Finding", "Tasks", "Closing criteria", "References"],
|
|
86
99
|
"tasks": ["Reproduce the described case", "Fix it", "Add a regression test that fails before the fix"],
|
|
@@ -95,12 +108,18 @@ TEXT = {
|
|
|
95
108
|
"plausible": "Da verificare: ",
|
|
96
109
|
"tolerated": "Segnalazione della review di forge rimasta aperta: era sotto la soglia bloccante {threshold}, "
|
|
97
110
|
"quindi il lavoro è stato integrato senza correggerla.",
|
|
111
|
+
"accepted": "Una persona ha accettato il lavoro nonostante il verdetto {status} del revisore, quindi è "
|
|
112
|
+
"stato integrato senza correggerlo.",
|
|
113
|
+
"accepted_reason": "Motivo: {reason}",
|
|
98
114
|
"review": "Segnalazione della review complessiva di forge rimasta aperta: verificare se è già stata "
|
|
99
115
|
"corretta prima di lavorarci.",
|
|
116
|
+
"outdated": "La review è obsoleta: {n} {tasks} nel branch di integrazione dopo di essa{ids}, quindi "
|
|
117
|
+
"verifica prima se l'hanno già corretta.",
|
|
100
118
|
"plausible_ctx": "Segnalazione PLAUSIBILE della review di forge (confidenza bassa): va verificata prima di "
|
|
101
119
|
"correggere. Se risulta infondata, chiudi l'issue spiegando perché.",
|
|
102
120
|
"group": "Segnalazioni della review di forge rimaste aperte nel run `{plan}` ({n}).",
|
|
103
121
|
"group_title": "Segnalazioni forge non bloccanti: {scope}",
|
|
122
|
+
"group_title_accepted": "Segnalazioni forge accettate: {scope}",
|
|
104
123
|
"origin": "Origine: forge `{plan}`, task `{task}`, gravità {severity}, review di `{reviewer}`{commit}.",
|
|
105
124
|
"headings": ["Contesto", "Segnalazione", "Attività", "Criteri di chiusura", "Riferimenti"],
|
|
106
125
|
"tasks": ["Riprodurre il caso descritto", "Correggere",
|
|
@@ -166,6 +185,222 @@ def reviewer_spec(task_dir: Path):
|
|
|
166
185
|
return ":".join(x for x in (kv.get("alias"), kv.get("effort"), kv.get("harness")) if x) or "?"
|
|
167
186
|
|
|
168
187
|
|
|
188
|
+
# `final-review-<epoch>-<pid>` or `integrate-<epoch>-<pid>`. `at` is that whole-second epoch.
|
|
189
|
+
_REVIEW_KIND = re.compile(r"(final-review|integrate)-([0-9]+)-([0-9]+)\Z")
|
|
190
|
+
# What `git rev-parse` writes for a commit: sha1 (40) or sha256 (64), lowercase hex.
|
|
191
|
+
_COMMIT = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})\Z")
|
|
192
|
+
# accept appends the reviewer's reply under this heading; findings before it were threshold-tolerated.
|
|
193
|
+
_ACCEPTED_HEADING = re.compile(
|
|
194
|
+
r"^## Accepted by a human despite the reviewer's(?:\s+(\S+))?\s+verdict\s*$", re.M)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _epoch(text):
|
|
198
|
+
if len(text) > 15:
|
|
199
|
+
return None
|
|
200
|
+
try:
|
|
201
|
+
at = float(text)
|
|
202
|
+
except (ValueError, OverflowError):
|
|
203
|
+
return None
|
|
204
|
+
return at if math.isfinite(at) else None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def review_clock(review_dir: Path):
|
|
208
|
+
"""(source, at) of a whole-run review directory.
|
|
209
|
+
|
|
210
|
+
`final-review-<epoch>-<pid>` is source `review`. `integrate-<epoch>-<pid>/final-review` is
|
|
211
|
+
source `integrate`, and `at` is the epoch in the integrate directory's name. A directory
|
|
212
|
+
literally named `final-review` with no such parent is a review whose clock is unknown.
|
|
213
|
+
"""
|
|
214
|
+
review_dir = Path(review_dir).resolve()
|
|
215
|
+
if review_dir.name == "final-review":
|
|
216
|
+
match = _REVIEW_KIND.fullmatch(review_dir.parent.name)
|
|
217
|
+
if match and match.group(1) == "integrate":
|
|
218
|
+
return "integrate", _epoch(match.group(2))
|
|
219
|
+
return "review", None
|
|
220
|
+
match = _REVIEW_KIND.fullmatch(review_dir.name)
|
|
221
|
+
if match and match.group(1) == "final-review":
|
|
222
|
+
return "review", _epoch(match.group(2))
|
|
223
|
+
return "review", None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def review_relname(plan_dir, review_dir: Path):
|
|
227
|
+
"""The review directory's path relative to the plan, posix, after resolving links."""
|
|
228
|
+
review_dir = Path(review_dir).resolve()
|
|
229
|
+
if plan_dir:
|
|
230
|
+
try:
|
|
231
|
+
return review_dir.relative_to(Path(plan_dir).resolve()).as_posix()
|
|
232
|
+
except ValueError:
|
|
233
|
+
pass
|
|
234
|
+
return review_dir.name
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _meta_fields(review_dir: Path):
|
|
238
|
+
fields = {}
|
|
239
|
+
for line in read(review_dir / "review.meta").splitlines():
|
|
240
|
+
key, sep, value = line.partition("=")
|
|
241
|
+
if sep and key.strip():
|
|
242
|
+
fields[key.strip()] = value.strip()
|
|
243
|
+
return fields
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _known_integration(meta):
|
|
247
|
+
"""The `integration=` commit, or None when it is missing or not a commit object name.
|
|
248
|
+
|
|
249
|
+
A malformed value is not the commit the review judged, so freshness uses the next rule.
|
|
250
|
+
"""
|
|
251
|
+
value = meta.get("integration") or None
|
|
252
|
+
if not value:
|
|
253
|
+
return None
|
|
254
|
+
return value if _COMMIT.fullmatch(value) else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _task_id_ok(name: str) -> bool:
|
|
258
|
+
"""True for a task id the scheduler and the UI reader both accept.
|
|
259
|
+
|
|
260
|
+
Empty, `.`, `..`, a slash, a NUL or U+FFFD are not ids. An id may start with `_`.
|
|
261
|
+
"""
|
|
262
|
+
return bool(name) and name not in (".", "..") and "/" not in name and "\0" not in name and "\ufffd" not in name
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _listed_task_ids(plan_dir: Path):
|
|
266
|
+
"""Task ids from tasks.tsv, in file order.
|
|
267
|
+
|
|
268
|
+
Only those rows are tasks: `tasks/` also holds forge's own directories (for example
|
|
269
|
+
`tasks/_integration`), which are not tasks unless a row lists them. A row needs seven
|
|
270
|
+
columns, the same minimum the UI reader requires. The first of a duplicate id wins.
|
|
271
|
+
"""
|
|
272
|
+
found, seen = [], set()
|
|
273
|
+
for line in read(Path(plan_dir) / "tasks.tsv").splitlines():
|
|
274
|
+
if not line.strip() or line.startswith("#"):
|
|
275
|
+
continue
|
|
276
|
+
parts = line.split("\t")
|
|
277
|
+
if len(parts) < 7:
|
|
278
|
+
continue
|
|
279
|
+
ident = parts[0]
|
|
280
|
+
if not _task_id_ok(ident) or ident in seen:
|
|
281
|
+
continue
|
|
282
|
+
seen.add(ident)
|
|
283
|
+
found.append(ident)
|
|
284
|
+
return found
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _task_merges(plan_dir: Path):
|
|
288
|
+
"""(id, mtime) of every listed task's `merged` marker, oldest merge first, then id.
|
|
289
|
+
|
|
290
|
+
The ids are `_listed_task_ids`: a listed `_fix` counts, and an unlisted `tasks/_integration`
|
|
291
|
+
does not. This is the set whose merge times the UI uses for `merged_since`.
|
|
292
|
+
"""
|
|
293
|
+
root = Path(plan_dir) / "tasks"
|
|
294
|
+
pairs = []
|
|
295
|
+
for ident in _listed_task_ids(plan_dir):
|
|
296
|
+
marker = root / ident / "merged"
|
|
297
|
+
try:
|
|
298
|
+
if marker.is_file():
|
|
299
|
+
pairs.append((ident, marker.stat().st_mtime))
|
|
300
|
+
except OSError:
|
|
301
|
+
continue
|
|
302
|
+
pairs.sort(key=lambda item: (item[1], item[0]))
|
|
303
|
+
return pairs
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _first_line(path: Path):
|
|
307
|
+
text = read(path)
|
|
308
|
+
return text.splitlines()[0].strip() if text else ""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def review_freshness(accepted, integration, tip, source, at, merges, verdict):
|
|
312
|
+
"""(outdated, merged_since) for one final review. Same rule as forge-ui's FinalReviewSummary.
|
|
313
|
+
|
|
314
|
+
Null when the review has no verdict or the plan has no `accepted.integration`. Otherwise the
|
|
315
|
+
first rule that applies: a known `integration` commit, else `tip` when `source` is `review`,
|
|
316
|
+
else (an older integrate review) any task merged after `at`. A merge counts when its marker
|
|
317
|
+
mtime is `>= at + 1`. `merged_since` is those ids, oldest first, and is empty unless the
|
|
318
|
+
review is outdated.
|
|
319
|
+
"""
|
|
320
|
+
later = []
|
|
321
|
+
if at is not None:
|
|
322
|
+
later = [item for item in merges if item[1] is not None and item[1] >= at + 1]
|
|
323
|
+
later.sort(key=lambda item: (item[1], item[0]))
|
|
324
|
+
ids = [task_id for task_id, _merged_at in later]
|
|
325
|
+
if not verdict or not accepted:
|
|
326
|
+
return None, []
|
|
327
|
+
if integration:
|
|
328
|
+
moved = integration != accepted
|
|
329
|
+
elif source == "review":
|
|
330
|
+
moved = tip != accepted
|
|
331
|
+
else:
|
|
332
|
+
moved = bool(ids)
|
|
333
|
+
return (True, ids) if moved else (False, [])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def review_outdated(plan_dir, review_dir):
|
|
337
|
+
"""(outdated, merged_since) for the whole-run review directory `review_dir` in `plan_dir`."""
|
|
338
|
+
if not plan_dir or not review_dir:
|
|
339
|
+
return None, []
|
|
340
|
+
review_dir = Path(review_dir)
|
|
341
|
+
meta = _meta_fields(review_dir)
|
|
342
|
+
source, at = review_clock(review_dir)
|
|
343
|
+
verdict = (_first_line(review_dir / "verdict").split() or [""])[0]
|
|
344
|
+
return review_freshness(_first_line(Path(plan_dir) / "accepted.integration"), _known_integration(meta),
|
|
345
|
+
meta.get("tip") or None, source, at, _task_merges(plan_dir), verdict)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def review_stamp(plan_dir, review_dir):
|
|
349
|
+
"""The `review` object a whole-run finding carries, or None when there is no review directory."""
|
|
350
|
+
if not review_dir:
|
|
351
|
+
return None
|
|
352
|
+
outdated, merged_since = review_outdated(plan_dir, review_dir)
|
|
353
|
+
return {"name": review_relname(plan_dir, review_dir), "outdated": outdated, "merged_since": merged_since}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def acceptance_of(task_dir):
|
|
357
|
+
"""(status, reason) recorded in `accepted.by`.
|
|
358
|
+
|
|
359
|
+
Key=value (`status`, `reason`, …) is the current record. Free text, which older runs wrote,
|
|
360
|
+
is a reason and no status. `reason` is cut to REASON_MAX characters.
|
|
361
|
+
"""
|
|
362
|
+
if not task_dir:
|
|
363
|
+
return "", ""
|
|
364
|
+
raw = read(Path(task_dir) / "accepted.by")
|
|
365
|
+
if not raw:
|
|
366
|
+
return "", ""
|
|
367
|
+
fields = {}
|
|
368
|
+
for line in raw.splitlines():
|
|
369
|
+
if not line.strip():
|
|
370
|
+
continue
|
|
371
|
+
if "=" not in line:
|
|
372
|
+
return "", raw[:REASON_MAX]
|
|
373
|
+
key, value = line.split("=", 1)
|
|
374
|
+
key = key.strip()
|
|
375
|
+
if not key or re.search(r"\s", key):
|
|
376
|
+
return "", raw[:REASON_MAX]
|
|
377
|
+
fields[key] = value.strip()
|
|
378
|
+
if not fields:
|
|
379
|
+
return "", raw[:REASON_MAX]
|
|
380
|
+
return fields.get("status", ""), fields.get("reason", "")[:REASON_MAX]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def known_findings(text, task_dir, meta):
|
|
384
|
+
"""CONFIRMED findings of a task's known_issues.md, split at the acceptance heading.
|
|
385
|
+
|
|
386
|
+
Findings before `## Accepted by a human despite the reviewer's <status> verdict` stay
|
|
387
|
+
`tolerated`. Findings after it are `accepted`. The heading's status is used only when
|
|
388
|
+
`accepted.by` does not record one.
|
|
389
|
+
"""
|
|
390
|
+
text = text or ""
|
|
391
|
+
match = _ACCEPTED_HEADING.search(text)
|
|
392
|
+
before, after = (text[:match.start()], text[match.end():]) if match else (text, "")
|
|
393
|
+
found = [{**f, **meta, "origin": "tolerated", "review": None}
|
|
394
|
+
for f in parse_findings(before) if f["confidence"] == "CONFIRMED"]
|
|
395
|
+
if match:
|
|
396
|
+
file_status, reason = acceptance_of(task_dir)
|
|
397
|
+
status = file_status or match.group(1) or "UNKNOWN"
|
|
398
|
+
found += [{**f, **meta, "origin": "accepted", "review": None,
|
|
399
|
+
"accepted_status": status, "accepted_reason": reason}
|
|
400
|
+
for f in parse_findings(after) if f["confidence"] == "CONFIRMED"]
|
|
401
|
+
return found
|
|
402
|
+
|
|
403
|
+
|
|
169
404
|
def task_findings(task_dir: Path, plan_dir, include_plausible, reviewers, review=None):
|
|
170
405
|
"""`review` True: task_dir is a whole-run review dir (it may hold a known_issues.md when a threshold applied)."""
|
|
171
406
|
status = (read(task_dir / "status").split() or [""])[0]
|
|
@@ -184,7 +419,10 @@ def task_findings(task_dir: Path, plan_dir, include_plausible, reviewers, review
|
|
|
184
419
|
review = not status and task not in results and not (task_dir / "known_issues.md").exists()
|
|
185
420
|
if review:
|
|
186
421
|
# A review with no task behind it (whole-run review): everything it found is still open.
|
|
187
|
-
|
|
422
|
+
stamp = review_stamp(plan_dir, task_dir) if whole_run else None
|
|
423
|
+
found = [{**f, **meta, "task": None if whole_run else task,
|
|
424
|
+
"origin": "review" if f["confidence"] == "CONFIRMED" else "plausible",
|
|
425
|
+
"threshold": "-", "review": stamp}
|
|
188
426
|
for f in parse_findings(read(task_dir / "qa.last"))
|
|
189
427
|
if f["confidence"] == "CONFIRMED" or include_plausible]
|
|
190
428
|
return found, None
|
|
@@ -192,11 +430,10 @@ def task_findings(task_dir: Path, plan_dir, include_plausible, reviewers, review
|
|
|
192
430
|
return [], f"{task}: status {state or 'unknown'}, its findings still block it"
|
|
193
431
|
threshold = read(task_dir / "qa_threshold") or (read(plan_dir / "qa_threshold") if plan_dir else "") or "?"
|
|
194
432
|
meta["threshold"] = threshold
|
|
195
|
-
found =
|
|
196
|
-
if f["confidence"] == "CONFIRMED"]
|
|
433
|
+
found = known_findings(read(task_dir / "known_issues.md"), task_dir, meta)
|
|
197
434
|
if include_plausible:
|
|
198
|
-
found += [{**f, **meta, "origin": "plausible"
|
|
199
|
-
if f["confidence"] == "PLAUSIBLE"]
|
|
435
|
+
found += [{**f, **meta, "origin": "plausible", "review": None}
|
|
436
|
+
for f in parse_findings(read(task_dir / "qa.last")) if f["confidence"] == "PLAUSIBLE"]
|
|
200
437
|
return found, None
|
|
201
438
|
|
|
202
439
|
|
|
@@ -314,10 +551,16 @@ def collect(path: Path, include_plausible, all_runs=False):
|
|
|
314
551
|
"reviewer": reviewer_spec(parent)}
|
|
315
552
|
meta["plan"], meta["plan_dir"] = (plan_root.name, str(plan_root))
|
|
316
553
|
if is_review:
|
|
317
|
-
|
|
554
|
+
stamp = review_stamp(plan_root, parent)
|
|
555
|
+
found = [{**f, **meta, "origin": "review" if f["confidence"] == "CONFIRMED" else "plausible", "review": stamp}
|
|
318
556
|
for f in parse_findings(read(path)) if f["confidence"] == "CONFIRMED" or include_plausible]
|
|
557
|
+
elif path.name == "known_issues.md":
|
|
558
|
+
found = known_findings(read(path), parent, meta)
|
|
559
|
+
if include_plausible:
|
|
560
|
+
found += [{**f, **meta, "origin": "plausible", "review": None}
|
|
561
|
+
for f in parse_findings(read(path)) if f["confidence"] == "PLAUSIBLE"]
|
|
319
562
|
else:
|
|
320
|
-
found = [{**f, **meta, "origin": "tolerated" if f["confidence"] == "CONFIRMED" else "plausible"}
|
|
563
|
+
found = [{**f, **meta, "origin": "tolerated" if f["confidence"] == "CONFIRMED" else "plausible", "review": None}
|
|
321
564
|
for f in parse_findings(read(path)) if f["confidence"] == "CONFIRMED" or include_plausible]
|
|
322
565
|
# plan_root is the plan dir, or for a file outside any plan (a solo run's) the dir holding it
|
|
323
566
|
return {"plan": plan_root.name, "repo_path": recorded_repo(plan_root)}, found, []
|
|
@@ -395,24 +638,100 @@ def published(slug, repo_path, commit, offline=False):
|
|
|
395
638
|
return run(["gh", "api", f"repos/{slug}/commits/{commit}", "--jq", ".sha"], check=False).returncode == 0
|
|
396
639
|
|
|
397
640
|
|
|
641
|
+
def merged_since_text(ids, lang):
|
|
642
|
+
"""Ids cut after 5, as `(a, b, c, d, e, +N more)`, or "" when there are none."""
|
|
643
|
+
shown = [str(item) for item in ids[:5]]
|
|
644
|
+
extra = len(ids) - len(shown)
|
|
645
|
+
names = ", ".join(shown)
|
|
646
|
+
if extra:
|
|
647
|
+
more = f"+{extra} altri" if lang == "it" else f"+{extra} more"
|
|
648
|
+
names = f"{names}, {more}" if names else more
|
|
649
|
+
return f" ({names})" if names else ""
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def outdated_line(lang, review):
|
|
653
|
+
"""The sentence an outdated whole-run review adds after the context line, or ""."""
|
|
654
|
+
if not review or review.get("outdated") is not True:
|
|
655
|
+
return ""
|
|
656
|
+
ids = list(review.get("merged_since") or [])
|
|
657
|
+
n = len(ids)
|
|
658
|
+
if lang == "it":
|
|
659
|
+
tasks = "task è stato integrato" if n == 1 else "task sono stati integrati"
|
|
660
|
+
else:
|
|
661
|
+
tasks = "task" if n == 1 else "tasks"
|
|
662
|
+
return TEXT[lang]["outdated"].format(n=n, tasks=tasks, ids=merged_since_text(ids, lang))
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def accepted_line(lang, finding):
|
|
666
|
+
"""The sentence for a finding a person accepted, including the recorded reason, or ""."""
|
|
667
|
+
if not finding or finding.get("origin") != "accepted":
|
|
668
|
+
return ""
|
|
669
|
+
t = TEXT[lang]
|
|
670
|
+
line = t["accepted"].format(status=finding.get("accepted_status") or "UNKNOWN")
|
|
671
|
+
reason = finding.get("accepted_reason") or ""
|
|
672
|
+
if reason:
|
|
673
|
+
line += " " + t["accepted_reason"].format(reason=escape(reason))
|
|
674
|
+
return line
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def acceptance_lines(lang, items):
|
|
678
|
+
"""One sentence per distinct acceptance, in first-seen order.
|
|
679
|
+
|
|
680
|
+
`--group run` puts every accepted finding of the plan in one issue. Tasks record their
|
|
681
|
+
own verdict and reason (`accepted.by`); each distinct pair is its own sentence, so a
|
|
682
|
+
later task's acceptance is not replaced by the first finding's.
|
|
683
|
+
"""
|
|
684
|
+
lines, seen = [], set()
|
|
685
|
+
for finding in items:
|
|
686
|
+
line = accepted_line(lang, finding)
|
|
687
|
+
if line and line not in seen:
|
|
688
|
+
seen.add(line)
|
|
689
|
+
lines.append(line)
|
|
690
|
+
return lines
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def outdated_lines(lang, items):
|
|
694
|
+
"""The outdated-review sentence for every distinct review in the group, in first-seen order.
|
|
695
|
+
|
|
696
|
+
Severity order can put a task's PLAUSIBLE finding ahead of the outdated review's, and
|
|
697
|
+
that first finding has no review. The warning still follows the context line, once per
|
|
698
|
+
distinct sentence.
|
|
699
|
+
"""
|
|
700
|
+
lines, seen = [], set()
|
|
701
|
+
for finding in items:
|
|
702
|
+
line = outdated_line(lang, finding.get("review") if finding else None)
|
|
703
|
+
if line and line not in seen:
|
|
704
|
+
seen.add(line)
|
|
705
|
+
lines.append(line)
|
|
706
|
+
return lines
|
|
707
|
+
|
|
708
|
+
|
|
398
709
|
def body_for(slug, repo_path, items, lang, offline=False):
|
|
399
710
|
t = TEXT[lang]
|
|
400
711
|
h = t["headings"]
|
|
401
712
|
first = items[0]
|
|
402
713
|
plan = first["plan"]
|
|
403
714
|
plausible = all(f["origin"] == "plausible" for f in items)
|
|
715
|
+
accepted = acceptance_lines(lang, items)
|
|
404
716
|
if len(items) > 1:
|
|
405
717
|
context = t["group"].format(plan=plan, n=len(items))
|
|
406
718
|
elif plausible:
|
|
407
719
|
context = t["plausible_ctx"]
|
|
408
720
|
elif first["origin"] == "review":
|
|
409
721
|
context = t["review"]
|
|
722
|
+
elif accepted:
|
|
723
|
+
context = accepted[0]
|
|
410
724
|
else:
|
|
411
725
|
context = t["tolerated"].format(threshold=first["threshold"])
|
|
412
726
|
commit = f", commit `{first['commit'][:12]}`" if first.get("commit") else ""
|
|
413
|
-
lines = [f"## {h[0]}", context
|
|
414
|
-
|
|
415
|
-
|
|
727
|
+
lines = [f"## {h[0]}", context]
|
|
728
|
+
# A grouped issue's context line is the group intro, so every distinct acceptance follows it.
|
|
729
|
+
if len(items) > 1:
|
|
730
|
+
lines.extend(accepted)
|
|
731
|
+
lines.extend(outdated_lines(lang, items))
|
|
732
|
+
lines += ["",
|
|
733
|
+
t["origin"].format(plan=plan, task=first["task"] or "final-review", severity=first["severity"],
|
|
734
|
+
reviewer=first["reviewer"], commit=commit), "", f"## {h[1]}"]
|
|
416
735
|
for f in items:
|
|
417
736
|
lines += [f"> {escape(f['raw'])}", ""]
|
|
418
737
|
lines.append(f"## {h[2]}")
|
|
@@ -670,6 +989,7 @@ def finding_entry(f, filed=None, dismissed=None):
|
|
|
670
989
|
"plan": f["plan"], "plan_dir": f["plan_dir"], "task": f["task"], "file": f["file"],
|
|
671
990
|
"summary": (f["text"].splitlines() or [""])[0][:160], "text": f["text"],
|
|
672
991
|
"reviewer": f["reviewer"], "commit": f.get("commit") or "",
|
|
992
|
+
"review": f.get("review"),
|
|
673
993
|
"filed": {"url": filed["url"], "state": filed["state"]} if filed else None,
|
|
674
994
|
"dismissed": dismissed}
|
|
675
995
|
|
|
@@ -725,8 +1045,12 @@ def list_and_file(args, meta, found, notes, repo_path, slug, warnings):
|
|
|
725
1045
|
title = title_for(items[0], args.lang)
|
|
726
1046
|
else:
|
|
727
1047
|
scope = (items[0]["task"] or items[0]["plan"]) if args.group == "task" else items[0]["plan"]
|
|
728
|
-
|
|
729
|
-
|
|
1048
|
+
origin = items[0]["origin"]
|
|
1049
|
+
if origin == "accepted":
|
|
1050
|
+
title = f"[{items[0]['severity']}] {t['group_title_accepted'].format(scope=scope)}"
|
|
1051
|
+
else:
|
|
1052
|
+
prefix = t["plausible"] if origin == "plausible" else ""
|
|
1053
|
+
title = f"[{items[0]['severity']}] {prefix}{t['group_title'].format(scope=scope)}"
|
|
730
1054
|
drafts.append({"title": title, "severity": items[0]["severity"], "origin": items[0]["origin"],
|
|
731
1055
|
"findings": [f["raw"] for f in items], "fingerprints": [f["fp"] for f in items],
|
|
732
1056
|
"same_title": None if offline else open_titles.get(norm_title(title)),
|
|
@@ -735,7 +1059,8 @@ def list_and_file(args, meta, found, notes, repo_path, slug, warnings):
|
|
|
735
1059
|
out = {"repo": slug, "plan": meta["plan"], "notes": notes, "warnings": warnings,
|
|
736
1060
|
"summary": {"findings": len(seen), "already_filed": len(already),
|
|
737
1061
|
"dismissed": sum(1 for e in entries if e["dismissed"]), "drafts": len(drafts),
|
|
738
|
-
**{o: sum(1 for f in todo if f["origin"] == o)
|
|
1062
|
+
**{o: sum(1 for f in todo if f["origin"] == o)
|
|
1063
|
+
for o in ("tolerated", "accepted", "review", "plausible")}},
|
|
739
1064
|
"findings": entries, "already_filed": already, "drafts": drafts}
|
|
740
1065
|
if args.apply:
|
|
741
1066
|
out["created"] = create(slug, drafts, args)
|
|
@@ -779,9 +1104,10 @@ def print_human(out, applied):
|
|
|
779
1104
|
s = out["summary"]
|
|
780
1105
|
dismissed = f", {s['dismissed']} dismissed" if s["dismissed"] else ""
|
|
781
1106
|
print(f"{out['repo']} · run {out['plan']}: {s['findings']} finding(s), {s['already_filed']} already filed{dismissed}, "
|
|
782
|
-
f"{s['drafts']} draft(s) ({s['tolerated']} tolerated, {s['
|
|
1107
|
+
f"{s['drafts']} draft(s) ({s['tolerated']} tolerated, {s['accepted']} accepted, "
|
|
1108
|
+
f"{s['review']} review, {s['plausible']} plausible)")
|
|
783
1109
|
for warning in out["warnings"]:
|
|
784
|
-
print(f" warning {warning}")
|
|
1110
|
+
print(f" warning: {warning}")
|
|
785
1111
|
for note in out["notes"]:
|
|
786
1112
|
print(f" skipped {note}")
|
|
787
1113
|
for a in out["already_filed"]:
|
|
@@ -795,9 +1121,11 @@ def print_human(out, applied):
|
|
|
795
1121
|
else:
|
|
796
1122
|
print(f" skipped {c['title']}: {c['skipped']}{' ' + c['issue'] if c.get('issue') else ''}")
|
|
797
1123
|
return
|
|
1124
|
+
outdated = {e["fp"] for e in out["findings"] if (e.get("review") or {}).get("outdated") is True}
|
|
798
1125
|
for d in out["drafts"]:
|
|
799
1126
|
flag = f" (open issue with the same title: {d['same_title']})" if d["same_title"] else ""
|
|
800
|
-
|
|
1127
|
+
mark = " (outdated review)" if any(fp in outdated for fp in d["fingerprints"]) else ""
|
|
1128
|
+
print(f" draft {d['title']}{mark}{flag}")
|
|
801
1129
|
if out["drafts"]:
|
|
802
1130
|
print("Nothing created: add --apply to file these.")
|
|
803
1131
|
|
|
@@ -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
|
-
#
|
|
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
|