@giamme/forge 0.8.0 → 0.9.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 (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +3 -2
  4. package/VERSION +1 -1
  5. package/docs/README.md +26 -0
  6. package/docs/add-ons.md +33 -0
  7. package/docs/artifacts.md +69 -0
  8. package/docs/commands.md +315 -0
  9. package/docs/dataset.md +20 -0
  10. package/docs/decomposed-runs.md +268 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +92 -0
  13. package/docs/fractal.md +181 -0
  14. package/docs/install.md +160 -0
  15. package/docs/memory.md +156 -0
  16. package/docs/models.md +118 -0
  17. package/docs/reviews.md +231 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +5 -0
  21. package/package.json +3 -2
  22. package/references/decompose.md +1 -1
  23. package/references/fractal.md +1 -1
  24. package/references/harnesses.md +18 -6
  25. package/references/solo.md +1 -1
  26. package/scripts/forge-dispatch.sh +7 -1
  27. package/scripts/forge-runs.py +121 -0
  28. package/scripts/forge-runtime.py +59 -6
  29. package/scripts/forge-version.py +54 -12
  30. package/scripts/forge_dataset/harvest.py +1 -1
  31. package/scripts/forge_dataset/importer.py +1 -1
  32. package/scripts/forge_fractal/__init__.py +2 -1
  33. package/ui/README.md +80 -20
  34. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  35. package/ui/docs/api.md +99 -17
  36. package/ui/forge_ui/export.py +16 -6
  37. package/ui/forge_ui/fractal.py +1 -2
  38. package/ui/forge_ui/reader.py +95 -7
  39. package/ui/forge_ui/server.py +10 -1
  40. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  41. package/ui/forge_ui/static/app.js +279 -124
  42. package/ui/forge_ui/static/cloud3d.js +3 -2
  43. package/ui/forge_ui/static/cloudui.js +8 -5
  44. package/ui/forge_ui/static/convo.js +295 -0
  45. package/ui/forge_ui/static/datatable.js +232 -0
  46. package/ui/forge_ui/static/diffview.js +29 -10
  47. package/ui/forge_ui/static/findings3d.js +134 -0
  48. package/ui/forge_ui/static/graph.js +3 -3
  49. package/ui/forge_ui/static/icons.js +94 -0
  50. package/ui/forge_ui/static/index.html +25 -13
  51. package/ui/forge_ui/static/iso.js +70 -19
  52. package/ui/forge_ui/static/leaderboard.js +44 -48
  53. package/ui/forge_ui/static/panel.js +68 -32
  54. package/ui/forge_ui/static/runmodel.js +17 -1
  55. package/ui/forge_ui/static/scene3d.js +518 -43
  56. package/ui/forge_ui/static/search.js +2 -1
  57. package/ui/forge_ui/static/shell.js +137 -0
  58. package/ui/forge_ui/static/stagectl.js +150 -0
  59. package/ui/forge_ui/static/style.css +924 -469
  60. package/ui/forge_ui/static/theme.js +25 -0
  61. package/ui/forge_ui/transcripts.py +608 -0
@@ -47,8 +47,7 @@ def supported():
47
47
 
48
48
 
49
49
  def state_root():
50
- """forge_fractal.state_root(), except that an empty XDG_STATE_HOME counts as unset (forge
51
- resolves '' to its working directory)."""
50
+ """forge_fractal.state_root(): an empty XDG_STATE_HOME counts as unset."""
52
51
  base = os.environ.get('XDG_STATE_HOME') or os.path.join(os.path.expanduser('~'), '.local', 'state')
53
52
  return os.path.join(os.path.realpath(os.path.expanduser(base)), 'forge', 'fractal')
54
53
 
@@ -11,7 +11,7 @@ import re
11
11
  import stat
12
12
  import urllib.parse
13
13
 
14
- from forge_ui import diffs, executions, findings, fractal
14
+ from forge_ui import diffs, executions, findings, fractal, transcripts
15
15
  from forge_ui.findings import verdict as _verdict
16
16
 
17
17
 
@@ -824,9 +824,8 @@ def task_detail(path, task_id, source='plan'):
824
824
  reviews=reviews, truncated=reader.truncated, warnings=reader.warnings)
825
825
 
826
826
 
827
- def execution_detail(path, name, source='plan'):
828
- """ExecutionDetail of one attempts/<role>-<uuid>/ dir of the run, or None."""
829
- reader = _Read(path)
827
+ def _execution(reader, name):
828
+ """(Execution, owner, owner dir) of the execution `name` of the run, or None."""
830
829
  kind = _kind(reader.path)
831
830
  if kind is None or not executions.NAME.fullmatch(name or ''):
832
831
  return None
@@ -856,8 +855,16 @@ def execution_detail(path, name, source='plan'):
856
855
  execs = _final_review(reader, items[index], index == 0, _published(reader))[1]
857
856
  owner, owner_dir = dict(kind='final_review', id=parts[0]), items[index][3]
858
857
  item = next((candidate for candidate in execs if candidate['id'] == name), None)
859
- if item is None:
858
+ return None if item is None else (item, owner, owner_dir)
859
+
860
+
861
+ def execution_detail(path, name, source='plan'):
862
+ """ExecutionDetail of one attempts/<role>-<uuid>/ dir of the run, or None."""
863
+ reader = _Read(path)
864
+ found = _execution(reader, name)
865
+ if found is None:
860
866
  return None
867
+ item, owner, owner_dir = found
861
868
  role, running = item['role'], item['state'] == 'running'
862
869
  resolved = executions.resolved_of(reader, f'{item.rel}/{role}.resolved')
863
870
  prompt = last = listed = None
@@ -880,6 +887,37 @@ def execution_detail(path, name, source='plan'):
880
887
  return result
881
888
 
882
889
 
890
+ def execution_transcript(path, name, source='plan'):
891
+ """ExecutionTranscript of a dwarf or qa execution: its agent's conversation, read from its `.log`; None
892
+ when there is no such execution or it is of another role."""
893
+ reader = _Read(path)
894
+ found = _execution(reader, name)
895
+ if found is None or found[0]['role'] not in ('dwarf', 'qa'):
896
+ return None
897
+ item, owner, owner_dir = found
898
+ role, running = item['role'], item['state'] == 'running'
899
+ # A finished execution's log is its copy; the owner dir's is the one being written, or the next one's.
900
+ rel = posixpath.join(owner_dir, role + '.log') if running else f'{item.rel}/{role}.log'
901
+ log = None
902
+ try:
903
+ real = Path(os.path.realpath(reader.path / rel))
904
+ if executions.inside(reader.path, str(real)) and stat.S_ISREG(real.stat().st_mode):
905
+ log = real
906
+ elif real.exists():
907
+ reader.warn(rel, 'outside the run dir or not a regular file')
908
+ except (OSError, ValueError):
909
+ pass
910
+ resolved = executions.resolved_of(reader, f'{item.rel}/{role}.resolved')
911
+ if resolved is None and running:
912
+ resolved = executions.parse_resolved(reader.text(posixpath.join(owner_dir, role + '.resolved'))) or None
913
+ model = item['model'] or {}
914
+ result = transcripts.transcript(log, model.get('harness'), running)
915
+ result.update(run=run_key(reader.path), execution=name, role=role, state=item['state'], attempt=item['attempt'],
916
+ spec=model.get('spec'), harness=model.get('harness'), repo=(resolved or {}).get('repo') or None,
917
+ owner=owner, warnings=reader.warnings + result['warnings'])
918
+ return result
919
+
920
+
883
921
  def final_review_detail(path, name, source='plan'):
884
922
  """FinalReviewDetail of final-review-<e>-<p> or integrate-<e>-<p>, or None."""
885
923
  reader = _Read(path)
@@ -1028,8 +1066,8 @@ def _diff_detail(reader, key, origin, sources, loaded, reason, reviews):
1028
1066
  found = []
1029
1067
  for review in reviews:
1030
1068
  text = reader.text(review['last']) if review['last'] else None
1031
- for item in findings.listing(text) or []:
1032
- found.append(dict(review={key: review[key] for key in ('kind', 'execution', 'attempt', 'spec')},
1069
+ for index, item in enumerate(findings.listing(text) or []):
1070
+ found.append(dict(id=_finding_id(review, index), review={key: review[key] for key in ('kind', 'execution', 'attempt', 'spec')},
1033
1071
  severity=item['severity'], status=item['status'], location=item['location'],
1034
1072
  summary=item['summary'], text=item['text']))
1035
1073
  return dict(run=key, source=origin, sources=sources, available=loaded is not None,
@@ -1141,6 +1179,50 @@ def final_review_diff(path, name, source='plan'):
1141
1179
  _NOT_INLINED if prompted else 'no review prompt yet', [review])
1142
1180
 
1143
1181
 
1182
+ # --- findings: every review's findings of a run, for the stage's marks ---------------------
1183
+
1184
+ def _finding_id(review, index):
1185
+ """A Finding's id: its review's execution (its kind when it has none) and its place in that reply."""
1186
+ return f"{review['execution'] or review['kind']}:{index}"
1187
+
1188
+
1189
+ def _findings_of(reader, review):
1190
+ listed = findings.listing(reader.text(review['last'])) if review['last'] else None
1191
+ return [dict(id=_finding_id(review, index), severity=item['severity'], status=item['status'],
1192
+ location=item['location'], summary=item['summary'])
1193
+ for index, item in enumerate(listed or [])]
1194
+
1195
+
1196
+ def run_findings(path, source='plan'):
1197
+ """RunFindings: each review's Findings, without their text, by task and by final review; None if no run."""
1198
+ reader = _Read(path)
1199
+ kind = _kind(reader.path)
1200
+ if kind is None:
1201
+ return None
1202
+ keys = ('kind', 'execution', 'attempt', 'spec', 'verdict')
1203
+ tasks = []
1204
+ for task_id in ['solo'] if kind == 'solo' else [task['id'] for task in _tasks(reader)]:
1205
+ ctx = _diff_task(reader, task_id)
1206
+ reviews = [dict({key: review[key] for key in keys}, findings=found)
1207
+ for review in (ctx['reviews'] if ctx else []) for found in [_findings_of(reader, review)] if found]
1208
+ if reviews:
1209
+ tasks.append(dict(id=task_id, reviews=reviews))
1210
+ finals = []
1211
+ if kind == 'plan':
1212
+ items, published = executions.review_dirs(reader)[0], _published(reader)
1213
+ for index, item in enumerate(items[:_MAX_REVIEWS]):
1214
+ summary = _final_review(reader, item, index == 0, published)[0]
1215
+ # the same review final_review_diff pins, so a Finding's id opens it there
1216
+ review = _diff_review('final', None, None, summary['spec'], summary['verdict'], item[3] + '/qa.last', [],
1217
+ summary['execution'])
1218
+ found = _findings_of(reader, review)
1219
+ if found:
1220
+ finals.append(dict(name=item[0], execution=summary['execution'], spec=summary['spec'],
1221
+ verdict=summary['verdict'], findings=found))
1222
+ return dict(run=run_key(reader.path), available=findings.available(), tasks=tasks, final_reviews=finals,
1223
+ truncated=reader.truncated, warnings=reader.warnings)
1224
+
1225
+
1144
1226
  # --- files: who planned, changed, drifted into or conflicted on each file --------------------
1145
1227
 
1146
1228
  _MAX_FILES, _MAX_TASKS, _MAX_OVERLAPS = 2000, 300, 5000
@@ -1316,12 +1398,18 @@ class Catalog:
1316
1398
  def execution_diff(self, key, name):
1317
1399
  return self._find(key, execution_diff, name)
1318
1400
 
1401
+ def transcript(self, key, name):
1402
+ return self._find(key, execution_transcript, name)
1403
+
1319
1404
  def final_review_diff(self, key, name):
1320
1405
  return self._find(key, final_review_diff, name)
1321
1406
 
1322
1407
  def files(self, key):
1323
1408
  return self._find(key, files_detail)
1324
1409
 
1410
+ def findings(self, key):
1411
+ return self._find(key, run_findings)
1412
+
1325
1413
  def fractal(self, key, task_id):
1326
1414
  return self._find(key, fractal_detail, task_id)
1327
1415
 
@@ -28,16 +28,19 @@ _EXEC_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/executions/((?:task|dwarf|qa|
28
28
  _REVIEW_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/final-reviews/((?:final-review|integrate)-[0-9]+-[0-9]+)')
29
29
  _TASK_DIFF_PATH = re.compile(_TASK_PATH.pattern + r'/diff(?:/(current|prev|committed|existing))?')
30
30
  _EXEC_DIFF_PATH = re.compile(_EXEC_PATH.pattern + '/diff')
31
+ _TRANSCRIPT_PATH = re.compile(_EXEC_PATH.pattern + '/transcript')
31
32
  _REVIEW_DIFF_PATH = re.compile(_REVIEW_PATH.pattern + '/diff')
32
33
  _FILES_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/files')
34
+ _FINDINGS_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/findings')
33
35
  _EXPORT_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/export')
34
36
  _FRACTAL_PATH = re.compile(_TASK_PATH.pattern + r'/fractal(?:/nodes/([A-Za-z0-9][A-Za-z0-9_-]{0,100})(/diff)?)?')
35
- _STATIC_PATH = re.compile(r'/([A-Za-z0-9_.-]+\.(?:html|js|css|svg))')
37
+ _STATIC_PATH = re.compile(r'/([A-Za-z0-9_.-]+\.(?:html|js|css|svg|woff2))')
36
38
  _TYPES = {
37
39
  'html': 'text/html; charset=utf-8',
38
40
  'js': 'text/javascript; charset=utf-8',
39
41
  'css': 'text/css; charset=utf-8',
40
42
  'svg': 'image/svg+xml',
43
+ 'woff2': 'font/woff2',
41
44
  }
42
45
  _JSON = 'application/json; charset=utf-8'
43
46
  _CSP = ("default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; "
@@ -106,9 +109,15 @@ def api_get(catalog, path):
106
109
  match = _REVIEW_DIFF_PATH.fullmatch(path)
107
110
  if match:
108
111
  return catalog.final_review_diff(*match.groups())
112
+ match = _TRANSCRIPT_PATH.fullmatch(path)
113
+ if match:
114
+ return catalog.transcript(*match.groups())
109
115
  match = _FILES_PATH.fullmatch(path)
110
116
  if match:
111
117
  return catalog.files(match.group(1))
118
+ match = _FINDINGS_PATH.fullmatch(path)
119
+ if match:
120
+ return catalog.findings(match.group(1))
112
121
  match = _FRACTAL_PATH.fullmatch(path)
113
122
  if match:
114
123
  key, task_id, node, diff = match.group(1), _task_id(match.group(2)), match.group(3), match.group(4)