@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.
- package/CHANGELOG.md +24 -0
- package/README.md +106 -1766
- package/SKILL.md +3 -2
- package/VERSION +1 -1
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +5 -0
- package/package.json +3 -2
- package/references/decompose.md +1 -1
- package/references/fractal.md +1 -1
- package/references/harnesses.md +18 -6
- package/references/solo.md +1 -1
- package/scripts/forge-dispatch.sh +7 -1
- package/scripts/forge-runs.py +121 -0
- package/scripts/forge-runtime.py +59 -6
- package/scripts/forge-version.py +54 -12
- package/scripts/forge_dataset/harvest.py +1 -1
- package/scripts/forge_dataset/importer.py +1 -1
- package/scripts/forge_fractal/__init__.py +2 -1
- package/ui/README.md +80 -20
- package/ui/THIRD_PARTY_NOTICES.md +108 -0
- package/ui/docs/api.md +99 -17
- package/ui/forge_ui/export.py +16 -6
- package/ui/forge_ui/fractal.py +1 -2
- package/ui/forge_ui/reader.py +95 -7
- package/ui/forge_ui/server.py +10 -1
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +279 -124
- package/ui/forge_ui/static/cloud3d.js +3 -2
- package/ui/forge_ui/static/cloudui.js +8 -5
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +29 -10
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/graph.js +3 -3
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +25 -13
- package/ui/forge_ui/static/iso.js +70 -19
- package/ui/forge_ui/static/leaderboard.js +44 -48
- package/ui/forge_ui/static/panel.js +68 -32
- package/ui/forge_ui/static/runmodel.js +17 -1
- package/ui/forge_ui/static/scene3d.js +518 -43
- package/ui/forge_ui/static/search.js +2 -1
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +924 -469
- package/ui/forge_ui/static/theme.js +25 -0
- 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'
|
|
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
|
-
|
|
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.** *
|
|
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;
|
|
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
|
|
110
|
-
|
|
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,
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
the
|
|
135
|
-
|
|
136
|
-
|
|
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="
|
|
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
|
|
196
|
-
|
|
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;
|
|
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.
|
|
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
|
-
|
|
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),
|
|
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
|
|
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`
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
`<script type="
|
|
792
|
-
|
|
793
|
-
|
|
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
|
|
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
|
|
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 |
|
package/ui/forge_ui/export.py
CHANGED
|
@@ -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')
|
|
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
|
-
|
|
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
|
|
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
|
|
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),
|