@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
@@ -28,7 +28,7 @@ from . import SCRIPTS, put_blob, put_file, sha256
28
28
  DISPATCH_ROLES = ('dwarf', 'qa', 'planner')
29
29
  PIPELINE_ROLES = ('task', 'solo', 'verification', 'parallel')
30
30
  TASKS_COLS = ('id', 'deps', 'difficulty', 'files', 'dwarf', 'qa', 'title')
31
- STRUCTURED = ('codex', 'claude', 'grok')
31
+ STRUCTURED = ('codex', 'claude', 'openclaude', 'grok', 'opencode') # as forge-runtime.py's: .last is the message, not the log
32
32
  # Plan files that change what a plan decides, added to its rid only when present (see plan_event).
33
33
  PLAN_RID_LATER = ('decompose_level', 'shadow_qa', 'shadow_rate', 'explore')
34
34
 
@@ -347,7 +347,7 @@ def main(args) -> int:
347
347
  if line.strip() and not line.startswith('#') and Path(line.strip()).is_dir():
348
348
  targets.append(Path(line.strip()))
349
349
  if args.fractal:
350
- state = Path(os.environ.get('XDG_STATE_HOME', str(Path.home() / '.local' / 'state'))).expanduser()
350
+ state = Path(os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local' / 'state')).expanduser()
351
351
  targets.append(state / 'forge' / 'fractal' / 'runs')
352
352
  ledgers = [(Path(r).expanduser() / '.forge' / 'ledger.tsv', str(Path(r).expanduser().resolve())) for r in args.ledger]
353
353
  archives = []
@@ -18,7 +18,8 @@ DEFAULTS = dict(depth=2, children=3, nodes=12, iterations=6, concurrency=3, dead
18
18
 
19
19
 
20
20
  def state_root() -> Path:
21
- return Path(os.environ.get('XDG_STATE_HOME', str(Path.home() / '.local/state'))).expanduser().resolve() / 'forge/fractal'
21
+ # An empty XDG_STATE_HOME counts as unset; Path('') would be the working directory.
22
+ return Path(os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local/state')).expanduser().resolve() / 'forge/fractal'
22
23
 
23
24
 
24
25
  def runtime_python() -> Path:
package/ui/README.md CHANGED
@@ -7,7 +7,7 @@ blocks on a plate, with cables between them and lamps that light up as things ha
7
7
  - **A map of every run**, one district per repo and one block per run, with a lamp per task.
8
8
  Filter it by state, repo, kind, model, age or review verdict; what does not match fades
9
9
  instead of disappearing.
10
- - **A run, as planned and as it ran.** *Planned waves* puts each task in its wave, with
10
+ - **A run, as planned and as it ran.** *Plan* puts each task in its planned wave, with
11
11
  dependency cables, and ends in a *finish* line of three nodes: integration, verification
12
12
  and the whole-run **final review**. *Execution* lays the same work out on wall-clock time:
13
13
  one block per attempt, in the lane it really ran in, with its dwarf and review inside, the
@@ -26,12 +26,13 @@ blocks on a plate, with cables between them and lamps that light up as things ha
26
26
  with the primary and how sure Clef was that each task would pass.
27
27
  - **Beyond one run:** a full-screen **run cloud** of every run, grouped by project, in five
28
28
  layouts with filters and runs you can drag anywhere; **diffs** with each finding under the
29
- line it names; a **model leaderboard**; a **files heatmap**; **Fractal** child-task trees;
29
+ line it names; each dwarf's and reviewer's **conversation**, read from its CLI's log; a
30
+ **model leaderboard**; a **files heatmap**; **Fractal** child-task trees;
30
31
  **quick jump** and **notifications**; and **export** of a run as one HTML file. See
31
32
  [What you see](#what-you-see).
32
33
 
33
34
  <p align="center">
34
- <img src="docs/media/hero.webp" width="860" alt="A decomposed run on its 3D plate: eight tasks in their planned waves, then the same run on wall-clock time, where two first attempts that failed review show in red beside the retries that passed.">
35
+ <img src="docs/media/hero.webp" width="860" alt="A decomposed run on its 3D plate: eight tasks in their planned waves with a mark for each review finding, then the same run on wall-clock time, where two first attempts that failed review show in red beside the retries that passed.">
35
36
  </p>
36
37
 
37
38
  The 3D view loads three.js from a CDN the first time it is used (see [Privacy](#privacy)).
@@ -105,11 +106,21 @@ with the first source winning. Discovery and disk reads repeat on each API reque
105
106
 
106
107
  ## What you see
107
108
 
109
+ forge-ui looks like a signal box's route-setting desk: the run is drawn on a panel, every heading
110
+ is an engraved plate, every switch is a push button whose lamp lights when it is on, and lamps
111
+ keep one meaning everywhere (amber running, green merged or passed, red failed, unlit pending).
112
+
113
+ - **Layout:** the *Runs*, *Inspector* and *Tasks* modules sit around the stage. *Immersive* (the
114
+ default) floats them over a stage that fills the window; *Workbench* docks them beside it, and
115
+ the runs list folds to a rail of lamps. Switch with the two layout keys in the header; the
116
+ module keys beside them, or `1`, `2` and `3`, fold and open the runs, the inspector and the
117
+ tasks. On your first visit the inspector and the tasks start folded, so a run opens on its plate. Drag a module's inner edge, or focus it and use the arrow keys, to resize it. The header
118
+ also switches the theme (system, light or dark). All of this is remembered in this browser.
108
119
  - **Map (`#/`):** every discovered run, newest first, grouped by repo. By default the last
109
- 60 runs from the past 14 days; *older runs too* shows the rest. The sidebar list follows
110
- the same filter.
120
+ 60 runs from the past 14 days; *older runs too* shows the rest. The runs list follows the same
121
+ filter, and sorts by latest activity, state or repo.
111
122
 
112
- <img src="docs/media/map.png" width="760" alt="The map: one block per run, in a district per repo, green when done, blue while running, red with failures.">
123
+ <img src="docs/media/map.png" width="760" alt="The map: one block per run, in a district per repo, green when done, amber while running, red with failures.">
113
124
  - **Run cloud (`#/cloud`):** every run at once, full screen, floating in 3D and grouped by project
114
125
  (repo). Pick a layout: *Helix* (one spiral per project, oldest at the bottom), *Sphere*,
115
126
  *Galaxy* (a disc per project, newest at the centre), *Timeline* (left to right by start time,
@@ -128,12 +139,38 @@ with the first source winning. Discovery and disk reads repeat on each API reque
128
139
  filtered runs and offers *Load 3D*.
129
140
 
130
141
  <img src="docs/media/cloud.webp" width="760" alt="The run cloud orbited, then laid out as galaxies and as a timeline with one lane per project.">
131
- - **Run (`#/runs/<key>`):** a header (goal, repo, counts, and *Run details*: settings, request,
132
- waves, verification output), a toolbar (*Planned waves* / *Execution*, *3D* / *2D*, height
133
- and colour overlays, *Critical path*, *Review marks*, *Projected*, *Replay*), the stage with
134
- the inspector beside it, and the task table.
135
-
136
- <img src="docs/media/run.png" width="49%" alt="Planned waves: a run's tasks on a 3D plate with dependency cables and the finish nodes, beside the run's inspector."> <img src="docs/media/execution.png" width="49%" alt="Execution: one block per attempt on wall-clock time, failed attempts in red, with the timeline and critical path in the inspector.">
142
+ - **Run (`#/runs/<key>`):** a header with the goal, the repo, a describer (each state's count,
143
+ the wall time, how long since anything moved), a bank of push buttons (*Plan* /
144
+ *Execution* / *Files*, *3D* / *2D*, and *View*: height and colour overlays, the *Layers* that
145
+ switch each part of the 3D drawing on or off, and the *Marks*: *Critical path*, *Review marks*,
146
+ *Projected*) and the replay's time base; then the stage, the inspector and the
147
+ task table. The stage frames the run in the part of the window the modules leave free. The
148
+ inspector opens when you choose a task, an attempt or a finish node; with
149
+ nothing chosen it shows the run's timeline, finish and record (*Run details* in the header:
150
+ settings, request, waves, verification output).
151
+ - **Stage (3D):** each review finding stands over its task's block as a mark shaped by its
152
+ severity (P0 spike, P1 diamond, P2 cone, P3 pin, a ring when it has none; a plausible one smaller
153
+ and paler); on an opened task it moves onto the review that raised it, and the final review's sit
154
+ on *Final review*. Hover one for its location and summary; click it to open the diff that
155
+ review read at that finding. Choosing a task keeps its route lit, what it waits on and what
156
+ waits on it (`I` switches this off). Hovering a task shows its card: status, attempts, busy
157
+ time, tokens, models, its worst finding and what waits on it. A block whose roof text is too
158
+ small to read gets its name on a tag that faces you. A merge sends a pulse to *Integration*, a
159
+ failed attempt a red ring across the plate, an escalation a hop with the two models, a final
160
+ review a ring in its verdict's colour, live and during a replay (with reduced motion the block
161
+ lights instead). The keys at the stage's edge fit what is shown (`F`), zoom (`+` `−`), change
162
+ the projection (`V`: perspective, true isometric, top-down), reset the view (`0`), open the
163
+ legend of what the stage draws now (`L`), go full screen, and list every key (`?`). The layers,
164
+ the projection and the route are remembered.
165
+ - **Lamps:** green merged or passed, amber running, red failed, unlit pending. A hollow lamp is
166
+ that family's other case: passed or accepted without a merge (green), interrupted (amber), an
167
+ infrastructure failure, a timeout or blocked behind a failure (red). Review findings are never
168
+ a lamp: their severity is a shape (P0 spike, P1 diamond, P2 cone, P3 pin).
169
+ - **Tables:** the task table and the leaderboard's tables sort by any column (click its header:
170
+ ascending, descending, off), reorder by dragging a header (or Alt+← → on it), and show or hide
171
+ columns from *Columns*, which also resets them. Each table remembers its columns.
172
+
173
+ <img src="docs/media/run.png" width="49%" alt="Planned waves: a run's tasks on a 3D plate with dependency cables, finding marks and the finish nodes, between the runs list and the run's inspector."> <img src="docs/media/execution.png" width="49%" alt="Execution: one block per attempt on wall-clock time, failed attempts in red, with the timeline and critical path in the inspector.">
137
174
  - **Inspector:** for the run, its timeline, critical path, peak concurrency and finish line;
138
175
  for a task, its models and routing, escalations, timings (busy, model time, overhead,
139
176
  queue wait after its dependencies merged), attempts and their executions, review findings
@@ -147,7 +184,7 @@ with the first source winning. Discovery and disk reads repeat on each API reque
147
184
  most tasks, and hide any kind of mark; a cell selects its task and offers that file's diff.
148
185
  Below: overlaps, drift, merge conflicts and deferrals as lists.
149
186
 
150
- <img src="docs/media/files.png" width="560" alt="The files heatmap: files by directory against tasks by wave, with planned, touched, drift and same-wave overlap marks.">
187
+ <img src="docs/media/files.png" width="760" alt="The files heatmap: files by directory against tasks by wave, with planned, touched, drift and same-wave overlap marks.">
151
188
  - **Models (`#/models`):** a leaderboard of the models forge used. Implementers: tasks, first-attempt
152
189
  pass rate with its 95% Wilson interval, censored attempts, retries, escalations away from the
153
190
  model, timeouts, gate and human accepts, median model time and tokens. Reviewers: primary,
@@ -158,16 +195,29 @@ with the first source winning. Discovery and disk reads repeat on each API reque
158
195
  run cloud filtered to its runs.
159
196
 
160
197
  <img src="docs/media/models.png" width="760" alt="The implementers table: tasks, first-attempt pass rate with its 95% interval, retries, escalations, timeouts, accepts, median model time and tokens per model.">
161
- - **Diffs (`…/tasks/<id>/diff[/<source>][/file/<path>]`, `…/final-review/<name>/diff`):** a task's
198
+ - **Diffs (`…/tasks/<id>/diff[/<source>][/file/<path> | /finding/<id>]`, `…/final-review/<name>/diff[/finding/<id>]`):** a task's
162
199
  `changes.diff`, its previous or committed diff, or exactly the diff one reviewer read (the last
163
200
  diff in that review's prompt), picked from a row of sources with each review's verdict; a final
164
201
  review's combined diff. Files are listed with their +/− counts and how many findings point
165
202
  into them; each finding is shown under the line it names, those on a line outside the changed
166
203
  hunks at the top of their file, and those whose file is not in the diff (or that name no
167
204
  location) in a section of their own. n and p move between findings, j and k between files.
168
- Open it from a task, an attempt's review, an execution or the final review in the inspector.
205
+ Open it from a task, an attempt's review, an execution or the final review in the inspector, or
206
+ from a finding's mark on the stage (`/finding/<id>` scrolls to that finding and focuses it).
169
207
 
170
208
  <img src="docs/media/diff.png" width="760" alt="The diff a reviewer read, with a P1 finding under the line that compares token digests and a P2 finding under the revoke function.">
209
+ - **Conversations (`…/tasks/<id>/executions/<dwarf-or-qa>/conversation`, `…/executions/<id>/conversation`):**
210
+ what one dwarf or reviewer said and did, read from its agent CLI's `<role>.log`, in the diff's
211
+ place: its messages, its thinking folded, each command with its exit and output, each edit with
212
+ its patch and a link to the file in the diff its review read, other tool calls, and the final
213
+ answer with the tokens and cost. Search it (`/`), keep only what it said, ran or changed, and
214
+ step through its errors (`E`). While the agent works the log is read again every few seconds and
215
+ *Follow* keeps the newest event in view. codex, grok, claude, openclaude and opencode logs are
216
+ read event by event; when a claude run logged only its final answer, the conversation comes from
217
+ Claude's own session file for that run, if it is still on this machine; any other log is shown
218
+ as it is. Open it from an attempt's dwarf or reviewer in the inspector.
219
+
220
+ <img src="docs/media/conversation.png" width="760" alt="A dwarf's conversation: its thinking, the files it wrote, a test run that failed with exit 1 and the run that passed after the fix.">
171
221
  - **Fractal child tasks:** when a run used Fractal, a task's inspector shows the tree it was split
172
222
  into (status, difficulty, model, phase, cost, the split decision), and the opened task in 3D
173
223
  carries the same tree floating above it. A node (`…/tasks/<id>/fractal/<node>`) shows its goal,
@@ -192,8 +242,10 @@ with the first source winning. Discovery and disk reads repeat on each API reque
192
242
  while the tab is in the background, and a hidden tab checks the run list every 30 seconds
193
243
  (browsers may check less often after a while). Open tabs share one log and notify once. Runs
194
244
  that have not changed for two hours never notify.
195
- - **Keys on the stage:** ← → move between tasks, Esc closes, `e` switches plan and
196
- execution. Drag to orbit, right-drag to pan, scroll to zoom once the stage is clicked.
245
+ - **Keys on the stage:** with the stage focused, ← → move between tasks (on the map, between
246
+ runs; ↑ ↓ between repos, Enter opens the run), Esc closes, `e` switches plan and execution.
247
+ Anywhere on the page while the stage shows: `F`, `+` `−`, `V`, `0`, `L`, `I`, Shift+F (full
248
+ screen) and `?`. Drag to orbit, right-drag to pan, scroll to zoom once the stage is clicked.
197
249
 
198
250
  URLs such as `http://127.0.0.1:8765/#/runs/<key>/tasks/<id>` are shareable within the
199
251
  machine while this server is running and discovering that run dir. *Live refresh* pauses
@@ -210,12 +262,18 @@ notifications on. Failed requests keep the last good data and show a banner.
210
262
  - `forge_ui/__main__.py`: CLI options, local startup and optional browser opening.
211
263
  - `forge_ui/__init__.py`: package version and the build id `forge ui` compares to find a stale server.
212
264
  - `forge_ui/static/index.html`, `style.css`, `app.js`: the page shell, styles, routes, views and polling.
265
+ - `forge_ui/static/theme.js`, `shell.js`, `icons.js`, `datatable.js`: the theme switch, the Immersive and
266
+ Workbench layouts with their modules, keys and splitters, the icon set, and the sortable tables
267
+ (their pure parts are node-tested). `D-DINCondensed-Bold.woff2` is the engraved labels' face.
213
268
  - `forge_ui/static/runmodel.js`: the pure run model (layouts, schedule projection, critical path, replay).
214
269
  - `forge_ui/static/panel.js`, `gantt.js`, `graph.js`: the inspector, the execution chart and strip, the 2D graph.
215
- - `forge_ui/static/map3d.js`, `scene3d.js`, `iso.js`: the 3D loader, scenes and engine (ported from archivision).
270
+ - `forge_ui/static/map3d.js`, `scene3d.js`, `iso.js`: the 3D loader, scenes and engine (ported from archivision);
271
+ `findings3d.js` the findings' marks, `stagectl.js` the stage's keys, legend and keys sheet.
216
272
  - `forge_ui/static/cloudmodel.js`, `cloud3d.js`, `cloudui.js`: the run cloud's pure model (projects,
217
273
  filters, layouts, layout files), its scene on the shared stage, and its controls.
218
274
  - `forge_ui/static/diffview.js`: the diff viewer, with findings pinned to their lines.
275
+ - `forge_ui/static/convo.js`, `forge_ui/transcripts.py`: an execution's conversation, and the
276
+ reader of each agent CLI's log format behind it.
219
277
  - `forge_ui/static/files.js`: the files heatmap (`matrix()` is pure and node-tested).
220
278
  - `forge_ui/export.py`: a run as one HTML file (the page's scripts and styles inlined, the API
221
279
  responses it reads, the 3D stage's modules as one module text, a hash-based policy).
@@ -267,7 +325,7 @@ skips otherwise. Its 3D tests need three.js: the CDN, or a local copy of the npm
267
325
 
268
326
  The pictures in this README and forge's come from `tools/readme_media.py`. After a change you
269
327
  can see, run it again: it builds the demo runs, serves them, and captures each view in
270
- Chromium with Playwright (three.js as for the browser tests; ffmpeg with libwebp and ImageMagick
328
+ Chromium with Playwright (three.js as for the browser tests; libwebp's `img2webp` and ImageMagick
271
329
  to encode the animations and PNGs). `--only cloud diff` retakes some of them.
272
330
 
273
331
  ```sh
@@ -278,7 +336,9 @@ python3 tools/readme_media.py # writes docs/media/
278
336
 
279
337
  Run dirs can contain prompts, diffs and review text from private repos. The server binds
280
338
  only `127.0.0.1`, makes no network requests, and writes nothing to run dirs, repositories or
281
- forge's config. Anyone with access to the local server can read the runs it discovers, and
339
+ forge's config. Outside the runs it reads one kind of file: for a claude execution whose log
340
+ holds only its final answer, that run's session file, `<session id>.jsonl` under
341
+ `~/.claude/projects/` (or `$CLAUDE_CONFIG_DIR/projects/`). Anyone with access to the local server can read the runs it discovers, and
282
342
  forge now registers every run, so that is every run on the machine.
283
343
 
284
344
  The 3D view loads three.js 0.160.0 from `cdn.jsdelivr.net` the first time it is shown in a
@@ -67,3 +67,111 @@ SOFTWARE.
67
67
 
68
68
  three.js 0.160.0 (MIT, Copyright © 2010-2023 three.js authors) is not part of forge. The page
69
69
  loads it, with its `examples/jsm` add-ons, from `cdn.jsdelivr.net` when the 3D view is used.
70
+
71
+ ## D-DIN Condensed (font)
72
+
73
+ `forge_ui/static/D-DINCondensed-Bold.woff2`, unmodified, is the D-DIN Condensed Bold face by Datto Inc.
74
+ (https://www.datto.com/fonts/d-din), used for forge-ui's engraved labels. It is licensed under the SIL Open
75
+ Font License 1.1; "D-DIN" and "D-DIN Condensed" are Reserved Font Names.
76
+
77
+ ```
78
+ Copyright (C) 2017 Datto Inc. https://www.datto.com/fonts/d-din,
79
+ with Reserved Font Names "D-DIN".
80
+
81
+ Copyright (C) 2017 Datto Inc. https://www.datto.com/fonts/d-din,
82
+ with Reserved Font Names "D-DIN Condensed".
83
+
84
+ Copyright (C) 2017 Datto Inc. https://www.datto.com/fonts/d-din,
85
+ with Reserved Font Names "D-DIN Expanded".
86
+
87
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
88
+ This license is copied below, and is also available with a FAQ at:
89
+ http://scripts.sil.org/OFL
90
+
91
+ -----------------------------------------------------------
92
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
93
+ -----------------------------------------------------------
94
+
95
+ PREAMBLE
96
+ The goals of the Open Font License (OFL) are to stimulate worldwide
97
+ development of collaborative font projects, to support the font creation
98
+ efforts of academic and linguistic communities, and to provide a free and
99
+ open framework in which fonts may be shared and improved in partnership
100
+ with others.
101
+
102
+ The OFL allows the licensed fonts to be used, studied, modified and
103
+ redistributed freely as long as they are not sold by themselves. The
104
+ fonts, including any derivative works, can be bundled, embedded,
105
+ redistributed and/or sold with any software provided that any reserved
106
+ names are not used by derivative works. The fonts and derivatives,
107
+ however, cannot be released under any other type of license. The
108
+ requirement for fonts to remain under this license does not apply
109
+ to any document created using the fonts or their derivatives.
110
+
111
+ DEFINITIONS
112
+ "Font Software" refers to the set of files released by the Copyright
113
+ Holder(s) under this license and clearly marked as such. This may
114
+ include source files, build scripts and documentation.
115
+
116
+ "Reserved Font Name" refers to any names specified as such after the
117
+ copyright statement(s).
118
+
119
+ "Original Version" refers to the collection of Font Software components as
120
+ distributed by the Copyright Holder(s).
121
+
122
+ "Modified Version" refers to any derivative made by adding to, deleting,
123
+ or substituting -- in part or in whole -- any of the components of the
124
+ Original Version, by changing formats or by porting the Font Software to a
125
+ new environment.
126
+
127
+ "Author" refers to any designer, engineer, programmer, technical
128
+ writer or other person who contributed to the Font Software.
129
+
130
+ PERMISSION & CONDITIONS
131
+ Permission is hereby granted, free of charge, to any person obtaining
132
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
133
+ redistribute, and sell modified and unmodified copies of the Font
134
+ Software, subject to the following conditions:
135
+
136
+ 1) Neither the Font Software nor any of its individual components,
137
+ in Original or Modified Versions, may be sold by itself.
138
+
139
+ 2) Original or Modified Versions of the Font Software may be bundled,
140
+ redistributed and/or sold with any software, provided that each copy
141
+ contains the above copyright notice and this license. These can be
142
+ included either as stand-alone text files, human-readable headers or
143
+ in the appropriate machine-readable metadata fields within text or
144
+ binary files as long as those fields can be easily viewed by the user.
145
+
146
+ 3) No Modified Version of the Font Software may use the Reserved Font
147
+ Name(s) unless explicit written permission is granted by the corresponding
148
+ Copyright Holder. This restriction only applies to the primary font name as
149
+ presented to the users.
150
+
151
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
152
+ Software shall not be used to promote, endorse or advertise any
153
+ Modified Version, except to acknowledge the contribution(s) of the
154
+ Copyright Holder(s) and the Author(s) or with their explicit written
155
+ permission.
156
+
157
+ 5) The Font Software, modified or unmodified, in part or in whole,
158
+ must be distributed entirely under this license, and must not be
159
+ distributed under any other license. The requirement for fonts to
160
+ remain under this license does not apply to any document created
161
+ using the Font Software.
162
+
163
+ TERMINATION
164
+ This license becomes null and void if any of the above conditions are
165
+ not met.
166
+
167
+ DISCLAIMER
168
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
169
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
170
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
171
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
172
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
173
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
174
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
175
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
176
+ OTHER DEALINGS IN THE FONT SOFTWARE.
177
+ ```
package/ui/docs/api.md CHANGED
@@ -12,6 +12,7 @@ forge_ui/findings.py QA findings and gate lines, parsed by scripts/forge-cont
12
12
  forge_ui/diffs.py unified diffs: parsing, the ```diff fence of a review prompt, pinning findings, cached reads
13
13
  forge_ui/models.py per-run model facts (attempts and reviews) for the leaderboard, cached per run
14
14
  forge_ui/fractal.py Fractal child tasks under $XDG_STATE_HOME/forge/fractal, read through dir fds
15
+ forge_ui/transcripts.py an agent CLI's `<role>.log` read as one conversation, whichever harness wrote it
15
16
  forge_ui/server.py serves those dicts as JSON plus the static page, 127.0.0.1 only
16
17
  forge_ui/static/ the page: index.html, app.js, style.css, graph.js and the scripts they load
17
18
  ```
@@ -85,7 +86,8 @@ one of `task dwarf qa solo verification parallel` and `<uuid>` 32 lowercase hex
85
86
  | `dataset.json` | may be absent: `parent` (the execution that started this one, e.g. `task-<uuid>`), `trigger`, `attempt_start` and `resume` (task executions) |
86
87
  | `<role>.resolved`, `.prompt`, `.last`, `.timedout` | copies made when it finished: the model resolution (`key=value`), the prompt, the final message, the timeout marker |
87
88
 
88
- The reader never opens an execution's `.log` or `.cmd`.
89
+ Only the transcript endpoint opens an execution's `.log` (a `dwarf`'s or `qa`'s, see
90
+ ExecutionTranscript); no read opens a `.cmd`.
89
91
 
90
92
  **Task status** is `MERGED` if `tasks/<id>/merged` exists, otherwise the first word of
91
93
  `tasks/<id>/status`, otherwise `PENDING`.
@@ -158,6 +160,8 @@ def task_diff(path, task_id, which='current', source='plan') -> dict | None # a
158
160
  def execution_diff(path, name, source='plan') -> dict | None # a DiffDetail
159
161
  def final_review_diff(path, name, source='plan') -> dict | None # a DiffDetail
160
162
  def files_detail(path, source='plan') -> dict | None # Files
163
+ def run_findings(path, source='plan') -> dict | None # RunFindings
164
+ def execution_transcript(path, name, source='plan') -> dict | None # an ExecutionTranscript
161
165
  def fractal_detail(path, task_id, source='plan') -> dict | None # a TaskFractal
162
166
  def fractal_node(path, task_id, node, source='plan') -> dict | None # a FractalNodeDetail
163
167
  def fractal_node_diff(path, task_id, node, source='plan') -> dict | None # a DiffDetail
@@ -170,7 +174,8 @@ class Catalog:
170
174
  def execution(self, key, name) -> dict | None
171
175
  def final_review(self, key, name) -> dict | None
172
176
  def diff(self, key, task_id, which='current'), execution_diff(self, key, name),
173
- final_review_diff(self, key, name), files(self, key), fractal(self, key, task_id),
177
+ final_review_diff(self, key, name), files(self, key), findings(self, key), transcript(self, key, name),
178
+ fractal(self, key, task_id),
174
179
  fractal_node(self, key, task_id, node), fractal_node_diff(self, key, task_id, node) -> dict | None
175
180
  def models(self) -> dict # Models, over every run
176
181
  ```
@@ -213,6 +218,8 @@ its last 20,000 behind a leading `…`. Their `truncated` lists the field names
213
218
  | `GET /api/runs/<key>/executions/<qa-or-dwarf>/diff` | `200 DiffDetail` of the diff that review read (a dwarf: its attempt's primary review) |
214
219
  | `GET /api/runs/<key>/final-reviews/<name>/diff` | `200 DiffDetail` |
215
220
  | `GET /api/runs/<key>/files` | `200 Files` |
221
+ | `GET /api/runs/<key>/findings` | `200 RunFindings` |
222
+ | `GET /api/runs/<key>/executions/<qa-or-dwarf>/transcript` | `200 ExecutionTranscript` |
216
223
  | `GET /api/models` | `200 Models` |
217
224
  | `GET /api/runs/<key>/export?texts=0\|1&paths=0\|1` | `200` the run as one HTML file (see Export), `Content-Disposition: attachment`, `Content-Security-Policy: default-src 'none'; sandbox`; `404` for an unknown run |
218
225
  | `GET /api/runs/<key>/tasks/<id>/fractal` | `200 TaskFractal` |
@@ -223,7 +230,8 @@ its last 20,000 behind a leading `…`. Their `truncated` lists the field names
223
230
 
224
231
  The lazy endpoints answer `404 {"error": "not found"}` for an unknown run, task,
225
232
  execution or review; the diff endpoints also for a `<which>` the run kind has not (`existing`
226
- of a plan task, `prev` of a solo run) and for an execution that is not `qa` or `dwarf`; the
233
+ of a plan task, `prev` of a solo run), the diff and transcript endpoints for an execution that is
234
+ not `qa` or `dwarf`; the
227
235
  Fractal node endpoints for an unknown node or a task whose Fractal state is not `available`. A
228
236
  diff that is not there yet is a `200` with `available: false`. An execution is looked for only where executions are written:
229
237
  `attempts/`, `tasks/<id>/attempts/`, `shadow/<id>/<a>-<k>/attempts/`,
@@ -577,7 +585,7 @@ of Findings, in place of the summary), `truncated` and `warnings`.
577
585
  "additions": 5, "deletions": 1, "truncated": false,
578
586
  "hunks": [{"old_start": 38, "old_lines": 7, "new_start": 38, "new_lines": 10,
579
587
  "section": "def _tasks(reader):", "lines": [" ctx", "-old", "+new"]}]}],
580
- "findings": [{"review": {"kind": "primary", "execution": "qa-…", "attempt": 2, "spec": "opus"},
588
+ "findings": [{"id": "qa-…:0", "review": {"kind": "primary", "execution": "qa-…", "attempt": 2, "spec": "opus"},
581
589
  "severity": "P1", "status": "CONFIRMED", "location": "ui/reader.py:42",
582
590
  "summary": "…", "text": "…",
583
591
  "pin": {"file": 0, "path": "ui/reader.py", "line": 42, "side": "new", "hunk": 0, "index": 5},
@@ -597,7 +605,7 @@ of Findings, in place of the summary), `truncated` and `warnings`.
597
605
  | `truncated` | the read stopped at 8 MiB (at a line end), the fence never closed, more than 300 files, or a file is `truncated` |
598
606
  | `files[]` | per file of the diff: `old` (`null` when added), `new` (`null` when deleted), `status` `modified added deleted renamed copied`, `binary` (`Binary files … differ` or a `GIT binary patch`, whose body is skipped), `additions`, `deletions` (of the whole file, even when cut), `truncated`, `hunks` |
599
607
  | `hunks[]` | `old_start`, `old_lines`, `new_start`, `new_lines` (a count left out is 1), `section` (after the second `@@`, or `null`), `lines`: each raw line with its marker (` `, `-`, `+`, `\` for ``) |
600
- | `findings[]` | the Findings (as above, without `line`) of the reviews attached, each with its `review` (`kind` `primary` `shadow` `final`, `execution`, `attempt`, `spec`), `pin` and `off_diff` |
608
+ | `findings[]` | the Findings (as above, without `line`) of the reviews attached, each with its `id` (see RunFindings), its `review` (`kind` `primary` `shadow` `final`, `execution`, `attempt`, `spec`), `pin` and `off_diff` |
601
609
  | `pin` | `file` (index in `files`), `path`, `line`, `side` (`new`; `old` for a deleted file or a renamed file's old path), `hunk` and `index` (into that hunk's `lines`), or `null` |
602
610
  | `off_diff` | `null` when pinned to a line, else `no location`, `file not in this diff`, `ambiguous path` or `line outside the changed hunks` (then `pin` has `file`, `line`, `side` and `hunk`/`index` `null`) |
603
611
 
@@ -628,6 +636,77 @@ Reads stay inside the run dir after symlinks are resolved, open regular files on
628
636
  never opened) and read at most 8 MiB; parses are kept by `(dev, inode, size, mtime_ns)` as
629
637
  other parses are.
630
638
 
639
+ ## RunFindings
640
+
641
+ Every review's Findings in one read, for the stage's marks: what a TaskDetail or DiffDetail
642
+ lists, for all tasks and final reviews at once, without `text` or `line`.
643
+
644
+ ```json
645
+ {"run": "3f9a0c1d2e4b", "available": true,
646
+ "tasks": [{"id": "reader", "reviews": [{"kind": "primary", "execution": "qa-…", "attempt": 1, "spec": "opus",
647
+ "verdict": "FAIL", "findings": [{"id": "qa-…:0", "severity": "P1", "status": "CONFIRMED",
648
+ "location": "ui/reader.py:42", "summary": "…"}]}]}],
649
+ "final_reviews": [{"name": "final-review-…", "execution": "qa-…", "spec": "sol", "verdict": "FAIL",
650
+ "findings": [{"id": "qa-…:0", "severity": "P2", "status": "CONFIRMED", "location": null, "summary": "…"}]}],
651
+ "truncated": [], "warnings": []}
652
+ ```
653
+
654
+ A task's `reviews` are every primary and shadow review of every attempt that listed a Finding
655
+ (a solo run has one task, `solo`); `final_reviews` the newest 20 final reviews that did. A
656
+ Finding's `id` is `<execution>:<n>`, `n` its 0-based place in the reply (`primary:<n>` or
657
+ `final:<n>` for a reply without an execution); the DiffDetail that pins it gives it the same
658
+ `id`, so a mark on the stage opens that Finding in the diff. `available` is `false` without the
659
+ contract module, and the lists are then empty.
660
+
661
+ ## ExecutionTranscript (`forge_ui/transcripts.py`)
662
+
663
+ What the agent of one `dwarf` or `qa` execution said and did, read from its `<role>.log`: the
664
+ copy in its execution dir once it has finished, the owner dir's log while it runs (up to its
665
+ last whole line, so the page can poll it). The format is told from the log itself:
666
+
667
+ | Log | Written by | Read as |
668
+ |---|---|---|
669
+ | one JSON event per line: `thread.started`, `item.*`, `turn.*` | codex `--json` | `format` `codex` |
670
+ | one message per line in Claude's shape: `system`, `assistant`, `user`, `result` | grok `--output-format streaming-messages-json`; claude and openclaude `--output-format stream-json` | `claude` |
671
+ | one event per line: `text`, `reasoning`, `tool_use`, `step_finish`, `error` | opencode `--format json` | `opencode` |
672
+ | one `result` object with a `session_id` | claude `--output-format json` | the session file, see below |
673
+ | other JSON lines | e.g. agy `--output-format stream-json` (its `{"event": "result"}` is read as the result) | `json`: each line shown as it is |
674
+ | anything else | antigravity's printed output; openclaude and opencode runs from before forge streamed them | `text`: one event |
675
+
676
+ A claude log that holds only its result names the session the conversation is in. That one
677
+ file is read, `<config dir>/projects/<one dir>/<session id>.jsonl`, the config dir being
678
+ `$CLAUDE_CONFIG_DIR` then `~/.claude`, and only when the id is a UUID; it is the one file
679
+ outside the run dir forge-ui reads. Its subagent (`isSidechain`) and meta lines are left out.
680
+ Without it, the result alone is shown with a warning.
681
+
682
+ ```json
683
+ {"run": "3f9a0c1d2e4b", "execution": "dwarf-…", "role": "dwarf", "state": "done", "attempt": 1, "spec": "luna",
684
+ "harness": "codex", "repo": "/work/worktrees/reader", "owner": {"kind": "task", "id": "reader"},
685
+ "format": "codex", "source": "log", "session": "0199…", "model": null, "cost": null,
686
+ "tokens": {"input": 12000, "cached": 9000, "output": 800},
687
+ "events": [{"kind": "reasoning", "text": "…"},
688
+ {"kind": "command", "command": "rg -n status ui/reader.py", "output": "…", "exit": 0},
689
+ {"kind": "edit", "files": ["/repo/ui/reader.py"], "patch": "-old\n+new"},
690
+ {"kind": "message", "text": "…"}],
691
+ "truncated": false, "warnings": []}
692
+ ```
693
+
694
+ `attempt`, `spec` and `owner` are the Execution's; `harness` and `repo` (the worktree the agent ran
695
+ in, so the page can make its absolute paths relative) come from `<role>.resolved`. `source` is
696
+ `log`, `session` (claude's session file) or `none` (no log). An event has `kind`
697
+ and the keys that kind uses: `prompt` (`text`: the task as a session file holds it), `message`,
698
+ `reasoning`, `command` (`command`, `output`, `exit`), `edit` (`files`, `patch`: `-`/`+` lines,
699
+ or `input` when there are none), `tool` (`tool`, `input`, `output`), `result` (`text`, left out
700
+ with `same: true` when the last message said it; `tokens`), `error` (`text`), `text` (`text`,
701
+ `stream: "stderr"` for lines between JSON events) and `gap` (`count` events left out). Any event
702
+ may have `t` (epoch seconds), `id` (the harness's call id), `error` (`true` for a failed call or a
703
+ non-zero exit, `false` for a call answered without one) and `running: true` (a call not finished yet). `tokens` are the run's totals
704
+ (`input` counts cached input too). A message, a reasoning or a result keeps 20,000 characters, a
705
+ command, an input or an output its first two thirds and last third of 8,000 around `… n
706
+ characters …` (`<key>_cut` holds n). A log over 32 MB is read from its end; past 5,000 events or
707
+ 4,000,000 characters the middle is left out (a `gap` event, `truncated: true`). Parses are kept by
708
+ the log's `(path, size, mtime_ns)` and the session file's.
709
+
631
710
  ## Files
632
711
 
633
712
  `GET /api/runs/<key>/files`: who planned, changed, drifted into or conflicted on each file.
@@ -785,21 +864,23 @@ difficulty paths deps model` each), `decomposition_history` (its newest 20, each
785
864
  `GET /api/runs/<key>/export` answers one self-contained HTML file named
786
865
  `forge-run-<run_id>.html` (the run id with anything but `[A-Za-z0-9._-]` made `-`).
787
866
 
788
- - It holds `style.css` and the classic scripts the run view uses (`runmodel.js cloudmodel.js
789
- graph.js gantt.js panel.js diffview.js files.js map3d.js search.js app.js`), inline; the import
790
- map; a `<script type="application/json" id="forge-snapshot">` with `{meta, api}`; and a
791
- `<script type="text/plain" id="forge-stage">` holding `iso.js`, `cloud3d.js` and `scene3d.js`
792
- bundled into one module (each module in its own scope, relative imports bound to the earlier
793
- ones' exports, `createStage` exported), which `map3d.js` imports from a `blob:` URL.
867
+ - It holds `style.css` (with the labels' face, `D-DINCondensed-Bold.woff2`, inlined as a `data:`
868
+ URL) and the classic scripts the run view uses (`theme.js icons.js datatable.js shell.js
869
+ runmodel.js cloudmodel.js graph.js gantt.js panel.js diffview.js convo.js files.js map3d.js search.js
870
+ app.js`), inline; the import map; a `<script type="application/json" id="forge-snapshot">` with
871
+ `{meta, api}`; and a `<script type="text/plain" id="forge-stage">` holding `iso.js`,
872
+ `cloud3d.js`, `findings3d.js`, `stagectl.js` and `scene3d.js` bundled into one module (each
873
+ module in its own scope, relative imports bound to the earlier ones' exports, `createStage`
874
+ exported), which `map3d.js` imports from a `blob:` URL.
794
875
  - `api` maps each path the page requests to that endpoint's response as the server would answer
795
876
  it (server.api_get): `/api/runs` (this run only), the RunDetail, every TaskDetail, every
796
- ExecutionDetail the RunDetail names, every FinalReviewDetail, Files, and the Fractal trees and
797
- nodes. With `texts=1` also every diff the page can open (each task's diff and its `sources`,
798
- each final review's, each Fractal node's).
877
+ ExecutionDetail the RunDetail names, every FinalReviewDetail, Files, RunFindings, and the
878
+ Fractal trees and nodes. With `texts=1` also every diff the page can open (each task's diff and its `sources`,
879
+ each final review's, each Fractal node's) and every `dwarf` and `qa` ExecutionTranscript (at most 2 MB each).
799
880
  - `texts=0` (the default) nulls these keys wherever they hold a string, list or object: `prompt
800
881
  last request log_tail dwarf_tail qa_tail qa_last qa_input tasks_summary known_issues texts text
801
- planning_last approach retry_findings dwarf_input error`, and stores no diff. A finding keeps its
802
- `summary`.
882
+ planning_last approach retry_findings dwarf_input error`, and stores no diff and no transcript. A
883
+ finding keeps its `summary`.
803
884
  - `paths=0` (the default) makes every string that is a whole absolute path (but `/api/…` and
804
885
  `url`s) its last component, and replaces the run dir, the repo and the home dir inside other
805
886
  strings with their last component (`~` for home).
@@ -808,7 +889,7 @@ difficulty paths deps model` each), `decomposition_history` (its newest 20, each
808
889
  `generated`, `texts`, `paths`, `omitted`.
809
890
  - The page's `<meta http-equiv="Content-Security-Policy">`: `default-src 'none'; script-src
810
891
  <sha256 of the import map and each inline script> blob: https://cdn.jsdelivr.net/npm/three@0.160.0/;
811
- style-src <sha256 of the style>; img-src data: blob:; connect-src 'none'; base-uri 'none';
892
+ style-src <sha256 of the style>; img-src data: blob:; font-src data:; connect-src 'none'; base-uri 'none';
812
893
  form-action 'none'; object-src 'none'`. Inside a script, `</script` and `<!--` are escaped; the
813
894
  JSON escapes `<`, `>` and `&`.
814
895
  - In the page, `app.js` reads the snapshot instead of the network, never polls, shows only the
@@ -860,6 +941,7 @@ Plain scripts unless noted; each defines one global and none parses API text as
860
941
  | `gantt.js` | `ForgeGantt` | `render(container, run, {now, onSelect, sel, replay, critical, ghosts})`: attempts on wall-clock time in lanes; `strip(container, run, {now, replay})`: busy attempts and model calls against `max_parallel` |
861
942
  | `panel.js` | `ForgePanel` | `render(host, ctx)`: the inspector for a run, task, execution, final review, verification, integration or Fractal node; fetches the lazy endpoints once per node; `ctx.diffHash(spec)` links into the diff viewer, `ctx.onFractal(task, TaskFractal)` hands a task's tree to the stage |
862
943
  | `diffview.js` | `ForgeDiff` | `render(host, {run, spec, fetchJSON, rerender, href, closeHref})`: a DiffDetail with its findings under their lines; `spec` is `{kind: task, task, source, file}`, `{kind: final, name, file}` or `{kind: fractal, task, node, file}` |
944
+ | `convo.js` | `ForgeConvo` | `render(host, {run, spec: {task, exec}, brief, live, fetchJSON, rerender, diffHash, closeHref})`: an ExecutionTranscript as a timeline, with search, the errors and, while the execution runs, a read of its log every 2.5 s |
863
945
  | `files.js` | `ForgeFiles` | `matrix(data, run, opts)` (pure: Files as rows of dirs and files against task columns by wave) and `render(host, data, {run, sel, onSelectTask, diffHref, prefs, savePrefs})`: the run's Files mode. Also a CommonJS module |
864
946
  | `cloudmodel.js` | `ForgeCloudModel` | pure: projects, per-run facts, the cloud's filters, its five layouts (anchors from every run, dragged runs kept), label picking, layout files. Also a CommonJS module |
865
947
  | `cloudui.js` | `ForgeCloudUI` | `create(root, ctx)`: the cloud's filter panel, layout and shape switches, position tools, legend and run card; a list of the same runs without 3D |
@@ -26,10 +26,11 @@ from forge_ui import __version__, server
26
26
 
27
27
  STATIC = server.STATIC_DIR
28
28
  # what the exported page runs, in index.html's order; the cloud, the leaderboard and notifications need a server
29
- SCRIPTS = ('runmodel.js', 'cloudmodel.js', 'graph.js', 'gantt.js', 'panel.js', 'diffview.js', 'files.js', 'map3d.js',
29
+ SCRIPTS = ('theme.js', 'icons.js', 'datatable.js', 'shell.js', 'runmodel.js', 'cloudmodel.js', 'graph.js', 'gantt.js', 'panel.js', 'diffview.js', 'convo.js', 'files.js', 'map3d.js',
30
30
  'search.js', 'app.js')
31
- MODULES = ('iso.js', 'cloud3d.js', 'scene3d.js') # bundled in dependency order
31
+ MODULES = ('iso.js', 'cloud3d.js', 'findings3d.js', 'stagectl.js', 'scene3d.js') # bundled in dependency order
32
32
  DIFF_CAP = 1 << 20 # one diff's JSON
33
+ TRANSCRIPT_CAP = 2 << 20 # one execution's transcript
33
34
  TOTAL_CAP = 25 << 20 # every response together
34
35
  CDN = 'https://cdn.jsdelivr.net/npm/three@0.160.0/'
35
36
  _EXEC = re.compile(r'(?:task|dwarf|qa|solo|verification|parallel)-[0-9a-f]{32}')
@@ -145,12 +146,14 @@ def build(catalog, key, texts=False, paths=False, now=None):
145
146
  tasks = [t for t in (detail.get('tasks') or []) if isinstance(t, dict) and t.get('id')]
146
147
  for task in tasks:
147
148
  snap.add(f"{base}/tasks/{_enc(task['id'])}")
148
- for name in sorted(set(_EXEC.findall(json.dumps(run)))):
149
+ names = sorted(set(_EXEC.findall(json.dumps(run))))
150
+ for name in names:
149
151
  snap.add(f'{base}/executions/{name}')
150
152
  reviews = [r.get('name') for r in (run.get('final_reviews') or []) if isinstance(r, dict) and r.get('name')]
151
153
  for name in reviews:
152
154
  snap.add(f'{base}/final-reviews/{_enc(name)}')
153
155
  snap.add(f'{base}/files')
156
+ snap.add(f'{base}/findings')
154
157
  fractal = run.get('fractal') or {}
155
158
  if fractal.get('enabled') and fractal.get('available'):
156
159
  for task in tasks:
@@ -170,6 +173,10 @@ def build(catalog, key, texts=False, paths=False, now=None):
170
173
  snap.add(url, cap=DIFF_CAP)
171
174
  for name in reviews:
172
175
  snap.add(f'{base}/final-reviews/{_enc(name)}/diff', cap=DIFF_CAP)
176
+ # what each dwarf and reviewer said and ran: its conversation is all text
177
+ for name in names:
178
+ if name.startswith(('dwarf-', 'qa-')):
179
+ snap.add(f'{base}/executions/{name}/transcript', cap=TRANSCRIPT_CAP)
173
180
  meta = {'format': 'forge-ui-export', 'version': 1, 'app': __version__, 'key': key,
174
181
  'generated': now if now is not None else time.time(), 'texts': bool(texts), 'paths': bool(paths),
175
182
  'omitted': snap.omitted}
@@ -182,14 +189,17 @@ def build(catalog, key, texts=False, paths=False, now=None):
182
189
  def _page(snapshot, run):
183
190
  index = (STATIC / 'index.html').read_text(encoding='utf-8')
184
191
  importmap = re.search(r'<script type="importmap">(.*?)</script>', index, re.DOTALL).group(1)
185
- body = re.search(r'<body>(.*)</body>', index, re.DOTALL).group(1)
192
+ body_tag, body = re.search(r'(<body[^>]*>)(.*)</body>', index, re.DOTALL).groups()
193
+ body = re.sub(r'\s*<script src="[^"]*"></script>', '', body) # the page's own scripts are inlined below
186
194
  css = (STATIC / 'style.css').read_text(encoding='utf-8')
195
+ # the engraving face travels inside the file
196
+ css = re.sub(r'url\(([A-Za-z0-9_.-]+\.woff2)\)', lambda m: 'url(data:font/woff2;base64,%s)' % base64.b64encode((STATIC / m.group(1)).read_bytes()).decode('ascii'), css)
187
197
  scripts = [_script_safe((STATIC / name).read_text(encoding='utf-8')) for name in SCRIPTS if (STATIC / name).is_file()]
188
198
  data = json.dumps(snapshot, ensure_ascii=False, separators=(',', ':')).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
189
199
  stage = _script_safe(bundle())
190
200
  icon = 'data:image/svg+xml;base64,' + base64.b64encode((STATIC / 'favicon.svg').read_bytes()).decode('ascii')
191
201
  csp = ("default-src 'none'; script-src " + ' '.join([_sha(importmap)] + [_sha(s) for s in scripts]) + f" blob: {CDN}; "
192
- f"style-src {_sha(css)}; img-src data: blob:; connect-src 'none'; base-uri 'none'; form-action 'none'; object-src 'none'")
202
+ f"style-src {_sha(css)}; img-src data: blob:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'; object-src 'none'")
193
203
  title = 'forge-ui · ' + re.sub(r'[<>&"]', '', str(run.get('goal') or run.get('run_id') or 'run').split('\n')[0])[:120]
194
204
  return ''.join([
195
205
  '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n',
@@ -197,7 +207,7 @@ def _page(snapshot, run):
197
207
  '<meta name="referrer" content="no-referrer">\n',
198
208
  f'<meta http-equiv="Content-Security-Policy" content="{csp}">\n',
199
209
  f'<title>{title}</title>\n<link rel="icon" href="{icon}">\n',
200
- f'<script type="importmap">{importmap}</script>\n<style>{css}</style>\n</head>\n<body>{body}',
210
+ f'<script type="importmap">{importmap}</script>\n<style>{css}</style>\n</head>\n{body_tag}{body}',
201
211
  f'<script type="application/json" id="forge-snapshot">{data}</script>\n',
202
212
  f'<script type="text/plain" id="forge-stage">{stage}</script>\n',
203
213
  ''.join(f'<script>{s}</script>\n' for s in scripts),