@giamme/forge 0.8.0 → 0.10.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 +50 -0
- package/README.md +106 -1766
- package/SKILL.md +22 -3
- package/VERSION +1 -1
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +84 -0
- package/docs/commands.md +365 -0
- package/docs/dataset.md +29 -0
- package/docs/decomposed-runs.md +310 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +128 -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 +245 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +20 -0
- package/package.json +3 -2
- package/references/dataset.md +18 -1
- package/references/decompose.md +20 -2
- package/references/fractal.md +1 -1
- package/references/harnesses.md +18 -6
- package/references/solo.md +1 -1
- package/references/ui.md +27 -5
- package/scripts/forge-config.py +347 -0
- package/scripts/forge-control.py +645 -0
- package/scripts/forge-dispatch.sh +7 -1
- package/scripts/forge-issues.py +806 -0
- package/scripts/forge-lib-task.sh +234 -35
- package/scripts/forge-parallel-options.sh +3 -1
- package/scripts/forge-parallel.sh +3 -2
- package/scripts/forge-route.py +184 -27
- package/scripts/forge-runs-rm.py +840 -0
- package/scripts/forge-runs.py +139 -0
- package/scripts/forge-runtime.py +59 -6
- package/scripts/forge-schedule.py +227 -11
- package/scripts/forge-ui.py +85 -19
- package/scripts/forge-version.py +54 -12
- package/scripts/forge_dataset/__init__.py +77 -6
- package/scripts/forge_dataset/capture.py +5 -2
- package/scripts/forge_dataset/harvest.py +23 -1
- package/scripts/forge_dataset/importer.py +420 -18
- package/scripts/forge_fractal/__init__.py +2 -1
- package/scripts/forge_registry.py +168 -16
- package/ui/README.md +116 -23
- package/ui/THIRD_PARTY_NOTICES.md +108 -0
- package/ui/docs/api.md +283 -23
- package/ui/forge_ui/__main__.py +10 -2
- package/ui/forge_ui/control.py +575 -0
- package/ui/forge_ui/export.py +33 -9
- package/ui/forge_ui/fractal.py +1 -2
- package/ui/forge_ui/reader.py +121 -8
- package/ui/forge_ui/server.py +135 -5
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +358 -131
- package/ui/forge_ui/static/cloud3d.js +3 -2
- package/ui/forge_ui/static/cloudui.js +8 -5
- package/ui/forge_ui/static/controls.js +619 -0
- 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/findingsview.js +412 -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 +31 -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 +92 -36
- package/ui/forge_ui/static/rundelete.js +388 -0
- 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/settings.js +399 -0
- 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 +1099 -469
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/static/uikit.js +161 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -5,6 +5,17 @@ run-dir path per line, with a ``#`` header on a new file. ``forge jev``, ``forge
|
|
|
5
5
|
dataset import --registry`` and the forge-ui launcher all use it, so it does not
|
|
6
6
|
live inside the Jev package.
|
|
7
7
|
|
|
8
|
+
Two writers change those files: ``register_run_dir`` (every runner, on every start) and
|
|
9
|
+
``unregister_run_dir`` (``forge runs rm``). Both take an flock on ``<registry dir>/.lock``,
|
|
10
|
+
a stable sibling file that is never replaced. Register never blocks: it waits at most
|
|
11
|
+
``REGISTER_LOCK_WAIT_S`` for the lock and then appends anyway, because a rare duplicate
|
|
12
|
+
line is harmless and a lost run is not. An unlocked append can then land in the file an
|
|
13
|
+
unregister has read and is about to replace, so the two meet halfway: unregister, once it
|
|
14
|
+
has replaced a file, copies onto the new one whatever was appended to the old one after
|
|
15
|
+
its read, and register, once it has appended, checks that the file it wrote is still the
|
|
16
|
+
registry and appends again to the new one if it is not. Either the append reached the old
|
|
17
|
+
file before unregister looked at it again, or register sees the replacement.
|
|
18
|
+
|
|
8
19
|
Run as a script, ``forge_registry.py register <run-dir>`` records a run dir from the
|
|
9
20
|
shell runners (forge-ui-options.sh ``forge_ui_register``): every plan, run and solo run
|
|
10
21
|
is registered, not only the ones opened with ``--ui``, because forge-ui's map of a
|
|
@@ -12,12 +23,27 @@ repo's runs reads this registry. ``FORGE_RUN_REGISTRY=off`` turns that off.
|
|
|
12
23
|
"""
|
|
13
24
|
from __future__ import annotations
|
|
14
25
|
|
|
26
|
+
import contextlib
|
|
27
|
+
import errno
|
|
28
|
+
import fcntl
|
|
15
29
|
import hashlib
|
|
16
30
|
import os
|
|
17
31
|
import stat
|
|
18
32
|
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
19
35
|
from pathlib import Path
|
|
20
36
|
|
|
37
|
+
# register_run_dir runs before forge ui open's own timeouts: it waits this long for the lock,
|
|
38
|
+
# then appends without it. unregister_run_dir is only ever called by `forge runs rm`, which
|
|
39
|
+
# can afford to wait for a busy lock, but not for ever.
|
|
40
|
+
REGISTER_LOCK_WAIT_S = 0.5
|
|
41
|
+
UNREGISTER_LOCK_WAIT_S = 10.0
|
|
42
|
+
# How many times an unlocked register appends again after an unregister replaced the file
|
|
43
|
+
# under it. One is enough unless unregisters keep finishing back to back.
|
|
44
|
+
REGISTER_TRIES = 5
|
|
45
|
+
LOCK_NAME = '.lock'
|
|
46
|
+
|
|
21
47
|
|
|
22
48
|
def registry_dir() -> Path:
|
|
23
49
|
"""The ``runs`` directory under forge's config home.
|
|
@@ -51,11 +77,45 @@ def runs_registry(repo) -> Path:
|
|
|
51
77
|
return registry_dir() / (repo_key(repo) + '.txt')
|
|
52
78
|
|
|
53
79
|
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def _registry_lock(wait_s: float):
|
|
82
|
+
"""Hold an exclusive flock on ``<registry dir>/.lock`` for the ``with`` body.
|
|
83
|
+
|
|
84
|
+
The lock is on a file of its own and never on a registry file, because unregister
|
|
85
|
+
replaces those: a lock on a file that is renamed over locks nothing. Polls without
|
|
86
|
+
blocking for up to ``wait_s`` seconds, then raises BlockingIOError; raises OSError if
|
|
87
|
+
the lock file cannot be opened or is not a regular file (a FIFO would block a reader).
|
|
88
|
+
A filesystem that cannot flock at all (ENOLCK, EOPNOTSUPP) gets no locking, as it
|
|
89
|
+
would anywhere else in forge, rather than a registry nobody can change.
|
|
90
|
+
"""
|
|
91
|
+
fd = os.open(registry_dir() / LOCK_NAME, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
|
|
92
|
+
try:
|
|
93
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
94
|
+
raise OSError(errno.EINVAL, 'the registry lock is not a regular file', LOCK_NAME)
|
|
95
|
+
deadline = time.monotonic() + wait_s
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
99
|
+
break
|
|
100
|
+
except OSError as error:
|
|
101
|
+
if error.errno not in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EACCES):
|
|
102
|
+
break
|
|
103
|
+
if time.monotonic() >= deadline:
|
|
104
|
+
raise BlockingIOError(errno.EWOULDBLOCK, 'the run registry is locked', LOCK_NAME) from error
|
|
105
|
+
time.sleep(0.01)
|
|
106
|
+
yield
|
|
107
|
+
finally:
|
|
108
|
+
os.close(fd)
|
|
109
|
+
|
|
110
|
+
|
|
54
111
|
def register_run_dir(repo: Path, run_dir: Path) -> None:
|
|
55
112
|
"""Record an absolute run-dir path for this repo, deduplicated.
|
|
56
113
|
|
|
57
114
|
Never raises and never blocks: failing to note where a run lives must not fail the run,
|
|
58
|
-
and ``forge ui open`` calls this before any of its own timeouts.
|
|
115
|
+
and ``forge ui open`` calls this before any of its own timeouts. The registry lock is
|
|
116
|
+
tried for at most ``REGISTER_LOCK_WAIT_S``; if it stays busy the line is appended without
|
|
117
|
+
it (see the module docstring), and appended again if an unregister replaced the file
|
|
118
|
+
it went to.
|
|
59
119
|
"""
|
|
60
120
|
try:
|
|
61
121
|
# A relative XDG_CONFIG_HOME resolves below the current directory -- for a runner or
|
|
@@ -65,26 +125,118 @@ def register_run_dir(repo: Path, run_dir: Path) -> None:
|
|
|
65
125
|
registry = runs_registry(repo)
|
|
66
126
|
registry.parent.mkdir(parents=True, exist_ok=True)
|
|
67
127
|
line = str(Path(run_dir).resolve())
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
with contextlib.ExitStack() as held:
|
|
129
|
+
try:
|
|
130
|
+
held.enter_context(_registry_lock(REGISTER_LOCK_WAIT_S))
|
|
131
|
+
except OSError:
|
|
132
|
+
pass # busy past the wait, or no lock file to be had: append unlocked
|
|
133
|
+
for _ in range(REGISTER_TRIES):
|
|
134
|
+
existing = []
|
|
135
|
+
if registry.is_file():
|
|
136
|
+
existing = [x.strip() for x in registry.read_text(errors='replace').splitlines()]
|
|
137
|
+
if line in existing:
|
|
138
|
+
return
|
|
139
|
+
header = [] if existing else ['# forge run dirs for ' + str(Path(repo).resolve())]
|
|
140
|
+
# O_NONBLOCK: a FIFO with no reader fails at once (ENXIO) instead of blocking the
|
|
141
|
+
# open, and anything that is not a regular file is left alone.
|
|
142
|
+
fd = os.open(registry, os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NONBLOCK, 0o644)
|
|
143
|
+
with os.fdopen(fd, 'a') as handle:
|
|
144
|
+
written = os.fstat(fd)
|
|
145
|
+
if not stat.S_ISREG(written.st_mode):
|
|
146
|
+
return
|
|
147
|
+
handle.write(''.join(entry + '\n' for entry in header + [line]))
|
|
148
|
+
handle.flush()
|
|
149
|
+
# Still the registry: the line is in it, or in the old file an unregister copies
|
|
150
|
+
# from once it has replaced it. Replaced already: the line may be in neither.
|
|
151
|
+
try:
|
|
152
|
+
current = os.lstat(registry)
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
continue
|
|
155
|
+
if (current.st_dev, current.st_ino) == (written.st_dev, written.st_ino):
|
|
156
|
+
return
|
|
83
157
|
except (OSError, ValueError, RuntimeError):
|
|
84
158
|
# ValueError: a NUL byte in a path; RuntimeError: a symlink loop (pathlib before 3.13)
|
|
85
159
|
pass
|
|
86
160
|
|
|
87
161
|
|
|
162
|
+
def _rewrite_without(path: Path, wanted: set) -> bool:
|
|
163
|
+
"""Atomically drop the lines of one registry file that name a path in ``wanted``.
|
|
164
|
+
|
|
165
|
+
Every other byte is kept as it was. Returns whether the file changed. A registry file
|
|
166
|
+
that is not a regular file is skipped, and read without blocking.
|
|
167
|
+
|
|
168
|
+
The caller holds the registry lock, but a register that gave up waiting for it appends
|
|
169
|
+
anyway, possibly to this file after it was read. So the old file stays open across the
|
|
170
|
+
replace, and whatever it gained since the read is appended to the new one.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
|
|
174
|
+
except OSError as error:
|
|
175
|
+
if error.errno in (errno.ENOENT, errno.ELOOP):
|
|
176
|
+
return False
|
|
177
|
+
raise
|
|
178
|
+
with os.fdopen(fd, 'rb') as old:
|
|
179
|
+
info = os.fstat(fd)
|
|
180
|
+
if not stat.S_ISREG(info.st_mode):
|
|
181
|
+
return False
|
|
182
|
+
data = old.read()
|
|
183
|
+
lines = data.splitlines(keepends=True)
|
|
184
|
+
kept = [line for line in lines if os.fsdecode(line).strip() not in wanted]
|
|
185
|
+
if len(kept) == len(lines):
|
|
186
|
+
return False
|
|
187
|
+
content = b''.join(kept)
|
|
188
|
+
temporary = None
|
|
189
|
+
try:
|
|
190
|
+
out, temporary = tempfile.mkstemp(dir=str(path.parent), prefix='.' + path.name + '.', suffix='.tmp')
|
|
191
|
+
with os.fdopen(out, 'wb') as handle:
|
|
192
|
+
handle.write(content)
|
|
193
|
+
handle.flush()
|
|
194
|
+
os.fsync(handle.fileno())
|
|
195
|
+
os.chmod(temporary, stat.S_IMODE(info.st_mode))
|
|
196
|
+
os.replace(temporary, path)
|
|
197
|
+
temporary = None
|
|
198
|
+
finally:
|
|
199
|
+
if temporary is not None:
|
|
200
|
+
try:
|
|
201
|
+
os.unlink(temporary)
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
# An append that comes later still is register's to notice: the path no longer
|
|
205
|
+
# names the file it wrote.
|
|
206
|
+
late = old.read()
|
|
207
|
+
if late:
|
|
208
|
+
extra = [line for line in late.splitlines(keepends=True) if os.fsdecode(line).strip() not in wanted]
|
|
209
|
+
if extra:
|
|
210
|
+
if content and not content.endswith(b'\n'):
|
|
211
|
+
extra.insert(0, b'\n')
|
|
212
|
+
out = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
213
|
+
with os.fdopen(out, 'ab') as handle:
|
|
214
|
+
handle.write(b''.join(extra))
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def unregister_run_dir(run_dir) -> list:
|
|
219
|
+
"""Remove a run dir from every registry file that lists it; return the files rewritten.
|
|
220
|
+
|
|
221
|
+
The reverse of ``register_run_dir``, for ``forge runs rm``, so unlike it this reports
|
|
222
|
+
failure: it raises OSError (BlockingIOError when the registry lock stays busy) and the
|
|
223
|
+
caller says the registry needs repair. Each file is rewritten atomically under the
|
|
224
|
+
registry lock, so a concurrent register loses nothing. ``run_dir`` need not exist any
|
|
225
|
+
more: entries were written resolved, so the path is resolved the same way, and the
|
|
226
|
+
path as given is matched too.
|
|
227
|
+
"""
|
|
228
|
+
directory = registry_dir()
|
|
229
|
+
if not directory.is_absolute() or not directory.is_dir():
|
|
230
|
+
return []
|
|
231
|
+
wanted = {os.path.realpath(run_dir), os.path.abspath(run_dir)}
|
|
232
|
+
changed = []
|
|
233
|
+
with _registry_lock(UNREGISTER_LOCK_WAIT_S):
|
|
234
|
+
for path in sorted(directory.glob('*.txt')):
|
|
235
|
+
if _rewrite_without(path, wanted):
|
|
236
|
+
changed.append(path)
|
|
237
|
+
return changed
|
|
238
|
+
|
|
239
|
+
|
|
88
240
|
# A path, not a file's worth of data: the first line of <run-dir>/repo is all that is read.
|
|
89
241
|
_REPO_LINE_MAX = 64 * 1024
|
|
90
242
|
|
package/ui/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# forge-ui
|
|
2
2
|
|
|
3
|
-
forge-ui is a small
|
|
3
|
+
forge-ui is a small local web UI for forge runs. It reads everything, and changes things only by
|
|
4
|
+
calling forge's own commands (see [Controls](#controls)). It draws them the way
|
|
4
5
|
[archivision](https://github.com/Giamme/archivision) draws a codebase: a 3D isometric map of
|
|
5
6
|
blocks on a plate, with cables between them and lamps that light up as things happen.
|
|
6
7
|
|
|
7
8
|
- **A map of every run**, one district per repo and one block per run, with a lamp per task.
|
|
8
9
|
Filter it by state, repo, kind, model, age or review verdict; what does not match fades
|
|
9
10
|
instead of disappearing.
|
|
10
|
-
- **A run, as planned and as it ran.** *
|
|
11
|
+
- **A run, as planned and as it ran.** *Plan* puts each task in its planned wave, with
|
|
11
12
|
dependency cables, and ends in a *finish* line of three nodes: integration, verification
|
|
12
13
|
and the whole-run **final review**. *Execution* lays the same work out on wall-clock time:
|
|
13
14
|
one block per attempt, in the lane it really ran in, with its dwarf and review inside, the
|
|
@@ -26,12 +27,13 @@ blocks on a plate, with cables between them and lamps that light up as things ha
|
|
|
26
27
|
with the primary and how sure Clef was that each task would pass.
|
|
27
28
|
- **Beyond one run:** a full-screen **run cloud** of every run, grouped by project, in five
|
|
28
29
|
layouts with filters and runs you can drag anywhere; **diffs** with each finding under the
|
|
29
|
-
line it names;
|
|
30
|
+
line it names; each dwarf's and reviewer's **conversation**, read from its CLI's log; a
|
|
31
|
+
**model leaderboard**; a **files heatmap**; **Fractal** child-task trees;
|
|
30
32
|
**quick jump** and **notifications**; and **export** of a run as one HTML file. See
|
|
31
33
|
[What you see](#what-you-see).
|
|
32
34
|
|
|
33
35
|
<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.">
|
|
36
|
+
<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
37
|
</p>
|
|
36
38
|
|
|
37
39
|
The 3D view loads three.js from a CDN the first time it is used (see [Privacy](#privacy)).
|
|
@@ -105,11 +107,21 @@ with the first source winning. Discovery and disk reads repeat on each API reque
|
|
|
105
107
|
|
|
106
108
|
## What you see
|
|
107
109
|
|
|
110
|
+
forge-ui looks like a signal box's route-setting desk: the run is drawn on a panel, every heading
|
|
111
|
+
is an engraved plate, every switch is a push button whose lamp lights when it is on, and lamps
|
|
112
|
+
keep one meaning everywhere (amber running, green merged or passed, red failed, unlit pending).
|
|
113
|
+
|
|
114
|
+
- **Layout:** the *Runs*, *Inspector* and *Tasks* modules sit around the stage. *Immersive* (the
|
|
115
|
+
default) floats them over a stage that fills the window; *Workbench* docks them beside it, and
|
|
116
|
+
the runs list folds to a rail of lamps. Switch with the two layout keys in the header; the
|
|
117
|
+
module keys beside them, or `1`, `2` and `3`, fold and open the runs, the inspector and the
|
|
118
|
+
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
|
|
119
|
+
also switches the theme (system, light or dark). All of this is remembered in this browser.
|
|
108
120
|
- **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
|
-
|
|
121
|
+
60 runs from the past 14 days; *older runs too* shows the rest. The runs list follows the same
|
|
122
|
+
filter, and sorts by latest activity, state or repo.
|
|
111
123
|
|
|
112
|
-
<img src="docs/media/map.png" width="760" alt="The map: one block per run, in a district per repo, green when done,
|
|
124
|
+
<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
125
|
- **Run cloud (`#/cloud`):** every run at once, full screen, floating in 3D and grouped by project
|
|
114
126
|
(repo). Pick a layout: *Helix* (one spiral per project, oldest at the bottom), *Sphere*,
|
|
115
127
|
*Galaxy* (a disc per project, newest at the centre), *Timeline* (left to right by start time,
|
|
@@ -128,12 +140,38 @@ with the first source winning. Discovery and disk reads repeat on each API reque
|
|
|
128
140
|
filtered runs and offers *Load 3D*.
|
|
129
141
|
|
|
130
142
|
<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
|
-
|
|
143
|
+
- **Run (`#/runs/<key>`):** a header with the goal, the repo, a describer (each state's count,
|
|
144
|
+
the wall time, how long since anything moved), a bank of push buttons (*Plan* /
|
|
145
|
+
*Execution* / *Files*, *3D* / *2D*, and *View*: height and colour overlays, the *Layers* that
|
|
146
|
+
switch each part of the 3D drawing on or off, and the *Marks*: *Critical path*, *Review marks*,
|
|
147
|
+
*Projected*) and the replay's time base; then the stage, the inspector and the
|
|
148
|
+
task table. The stage frames the run in the part of the window the modules leave free. The
|
|
149
|
+
inspector opens when you choose a task, an attempt or a finish node; with
|
|
150
|
+
nothing chosen it shows the run's timeline, finish and record (*Run details* in the header:
|
|
151
|
+
settings, request, waves, verification output).
|
|
152
|
+
- **Stage (3D):** each review finding stands over its task's block as a mark shaped by its
|
|
153
|
+
severity (P0 spike, P1 diamond, P2 cone, P3 pin, a ring when it has none; a plausible one smaller
|
|
154
|
+
and paler); on an opened task it moves onto the review that raised it, and the final review's sit
|
|
155
|
+
on *Final review*. Hover one for its location and summary; click it to open the diff that
|
|
156
|
+
review read at that finding. Choosing a task keeps its route lit, what it waits on and what
|
|
157
|
+
waits on it (`I` switches this off). Hovering a task shows its card: status, attempts, busy
|
|
158
|
+
time, tokens, models, its worst finding and what waits on it. A block whose roof text is too
|
|
159
|
+
small to read gets its name on a tag that faces you. A merge sends a pulse to *Integration*, a
|
|
160
|
+
failed attempt a red ring across the plate, an escalation a hop with the two models, a final
|
|
161
|
+
review a ring in its verdict's colour, live and during a replay (with reduced motion the block
|
|
162
|
+
lights instead). The keys at the stage's edge fit what is shown (`F`), zoom (`+` `−`), change
|
|
163
|
+
the projection (`V`: perspective, true isometric, top-down), reset the view (`0`), open the
|
|
164
|
+
legend of what the stage draws now (`L`), go full screen, and list every key (`?`). The layers,
|
|
165
|
+
the projection and the route are remembered.
|
|
166
|
+
- **Lamps:** green merged or passed, amber running, red failed, unlit pending. A hollow lamp is
|
|
167
|
+
that family's other case: passed or accepted without a merge (green), interrupted (amber), an
|
|
168
|
+
infrastructure failure, a timeout or blocked behind a failure (red). Review findings are never
|
|
169
|
+
a lamp: their severity is a shape (P0 spike, P1 diamond, P2 cone, P3 pin).
|
|
170
|
+
- **Tables:** the task table and the leaderboard's tables sort by any column (click its header:
|
|
171
|
+
ascending, descending, off), reorder by dragging a header (or Alt+← → on it), and show or hide
|
|
172
|
+
columns from *Columns*, which also resets them. Each table remembers its columns.
|
|
173
|
+
|
|
174
|
+
<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
175
|
- **Inspector:** for the run, its timeline, critical path, peak concurrency and finish line;
|
|
138
176
|
for a task, its models and routing, escalations, timings (busy, model time, overhead,
|
|
139
177
|
queue wait after its dependencies merged), attempts and their executions, review findings
|
|
@@ -147,7 +185,7 @@ with the first source winning. Discovery and disk reads repeat on each API reque
|
|
|
147
185
|
most tasks, and hide any kind of mark; a cell selects its task and offers that file's diff.
|
|
148
186
|
Below: overlaps, drift, merge conflicts and deferrals as lists.
|
|
149
187
|
|
|
150
|
-
<img src="docs/media/files.png" width="
|
|
188
|
+
<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
189
|
- **Models (`#/models`):** a leaderboard of the models forge used. Implementers: tasks, first-attempt
|
|
152
190
|
pass rate with its 95% Wilson interval, censored attempts, retries, escalations away from the
|
|
153
191
|
model, timeouts, gate and human accepts, median model time and tokens. Reviewers: primary,
|
|
@@ -158,22 +196,35 @@ with the first source winning. Discovery and disk reads repeat on each API reque
|
|
|
158
196
|
run cloud filtered to its runs.
|
|
159
197
|
|
|
160
198
|
<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
|
|
199
|
+
- **Diffs (`…/tasks/<id>/diff[/<source>][/file/<path> | /finding/<id>]`, `…/final-review/<name>/diff[/finding/<id>]`):** a task's
|
|
162
200
|
`changes.diff`, its previous or committed diff, or exactly the diff one reviewer read (the last
|
|
163
201
|
diff in that review's prompt), picked from a row of sources with each review's verdict; a final
|
|
164
202
|
review's combined diff. Files are listed with their +/− counts and how many findings point
|
|
165
203
|
into them; each finding is shown under the line it names, those on a line outside the changed
|
|
166
204
|
hunks at the top of their file, and those whose file is not in the diff (or that name no
|
|
167
205
|
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
|
|
206
|
+
Open it from a task, an attempt's review, an execution or the final review in the inspector, or
|
|
207
|
+
from a finding's mark on the stage (`/finding/<id>` scrolls to that finding and focuses it).
|
|
169
208
|
|
|
170
209
|
<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.">
|
|
210
|
+
- **Conversations (`…/tasks/<id>/executions/<dwarf-or-qa>/conversation`, `…/executions/<id>/conversation`):**
|
|
211
|
+
what one dwarf or reviewer said and did, read from its agent CLI's `<role>.log`, in the diff's
|
|
212
|
+
place: its messages, its thinking folded, each command with its exit and output, each edit with
|
|
213
|
+
its patch and a link to the file in the diff its review read, other tool calls, and the final
|
|
214
|
+
answer with the tokens and cost. Search it (`/`), keep only what it said, ran or changed, and
|
|
215
|
+
step through its errors (`E`). While the agent works the log is read again every few seconds and
|
|
216
|
+
*Follow* keeps the newest event in view. codex, grok, claude, openclaude and opencode logs are
|
|
217
|
+
read event by event; when a claude run logged only its final answer, the conversation comes from
|
|
218
|
+
Claude's own session file for that run, if it is still on this machine; any other log is shown
|
|
219
|
+
as it is. Open it from an attempt's dwarf or reviewer in the inspector.
|
|
220
|
+
|
|
221
|
+
<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
222
|
- **Fractal child tasks:** when a run used Fractal, a task's inspector shows the tree it was split
|
|
172
223
|
into (status, difficulty, model, phase, cost, the split decision), and the opened task in 3D
|
|
173
224
|
carries the same tree floating above it. A node (`…/tasks/<id>/fractal/<node>`) shows its goal,
|
|
174
225
|
paths, timings, cost and tokens, its decomposition and the children it admitted, its last
|
|
175
226
|
messages, and its candidate diff. Fractal's state is read under `$XDG_STATE_HOME/forge/fractal`
|
|
176
|
-
(default `~/.local/state`), read
|
|
227
|
+
(default `~/.local/state`), only read, without following symlinks.
|
|
177
228
|
- **Export:** *Export…* in a run's header downloads the run as one HTML file that opens in any
|
|
178
229
|
browser without forge-ui, offline included (in 2D; in 3D when cdn.jsdelivr.net is reachable).
|
|
179
230
|
It asks each time: by default prompts, reviewer reports, logs, finding details and diffs are
|
|
@@ -192,14 +243,47 @@ with the first source winning. Discovery and disk reads repeat on each API reque
|
|
|
192
243
|
while the tab is in the background, and a hidden tab checks the run list every 30 seconds
|
|
193
244
|
(browsers may check less often after a while). Open tabs share one log and notify once. Runs
|
|
194
245
|
that have not changed for two hours never notify.
|
|
195
|
-
- **Keys on the stage:** ← → move between tasks
|
|
196
|
-
|
|
246
|
+
- **Keys on the stage:** with the stage focused, ← → move between tasks (on the map, between
|
|
247
|
+
runs; ↑ ↓ between repos, Enter opens the run), Esc closes, `e` switches plan and execution.
|
|
248
|
+
Anywhere on the page while the stage shows: `F`, `+` `−`, `V`, `0`, `L`, `I`, Shift+F (full
|
|
249
|
+
screen) and `?`. Drag to orbit, right-drag to pan, scroll to zoom once the stage is clicked.
|
|
197
250
|
|
|
198
251
|
URLs such as `http://127.0.0.1:8765/#/runs/<key>/tasks/<id>` are shareable within the
|
|
199
252
|
machine while this server is running and discovering that run dir. *Live refresh* pauses
|
|
200
253
|
polling; a hidden tab polls only the run list, every 30 seconds, and only with browser
|
|
201
254
|
notifications on. Failed requests keep the last good data and show a banner.
|
|
202
255
|
|
|
256
|
+
## Controls
|
|
257
|
+
|
|
258
|
+
The page can change a run, and every change is one call of a forge CLI; the server writes
|
|
259
|
+
nothing itself, so a change is validated, locked and logged exactly as if typed in a terminal
|
|
260
|
+
(`control.log` records `ui` as the actor). Each change is confirmed first and says when it
|
|
261
|
+
takes effect. The routes are in [docs/api.md](docs/api.md).
|
|
262
|
+
|
|
263
|
+
- **Run and task controls (`forge control`):** in a plan's inspector, the QA threshold,
|
|
264
|
+
timeout, escalate-after and verify retries, and while a scheduler is live the retry and
|
|
265
|
+
infra-retry budgets. A task has a threshold override (`Inherit`, P0–P3 or `None`, which
|
|
266
|
+
exempts it from the run's threshold), its own retry budget and a **model pin**: the model
|
|
267
|
+
its next full retry uses, once. A control that cannot apply (no live scheduler, a merged
|
|
268
|
+
task, a Fractal plan's pin) is disabled and says why.
|
|
269
|
+
- **Open findings (`forge issues`):** the findings a run, or every run of its repository,
|
|
270
|
+
left open. The list opens from a quick offline listing and then fills in what is already
|
|
271
|
+
filed; select findings to file as GitHub issues, or dismiss them (and undo that).
|
|
272
|
+
- **Delete a run (`forge runs rm`):** a dry run first, then the deletion. Its traces stay in
|
|
273
|
+
the dataset; an archive is written unless you decline it; `worktrees` and `branches` remove
|
|
274
|
+
the run's clean worktrees and its branches that nothing has checked out. Foreign or dirty
|
|
275
|
+
worktrees are listed and left.
|
|
276
|
+
- **Settings (`forge config`):** the registry, Jev, the repository's `.forge` files, the
|
|
277
|
+
dataset and `FORGE_*` variables (secrets shown as `<set>`), with edits for `repo.verify`,
|
|
278
|
+
`repo.setup`, `repo.verify-subset` and `jev.enabled`.
|
|
279
|
+
|
|
280
|
+
**Protection.** Each server start makes a random token and serves it in the page; every change
|
|
281
|
+
must send it, from a `Host` of `127.0.0.1` or `localhost`, with an `Origin` equal to the host
|
|
282
|
+
when one is sent, as JSON of at most 64 KiB, or it is refused before any CLI runs. There are no
|
|
283
|
+
CORS headers. One write per run at a time (`409 busy`). `--read-only` or `FORGE_UI_READONLY=1`
|
|
284
|
+
turns all of it off: no token is issued, every change is a 405, and the controls say the page cannot change the run.
|
|
285
|
+
`forge ui open` never reuses a server running in the other mode.
|
|
286
|
+
|
|
203
287
|
## Layout of the code
|
|
204
288
|
|
|
205
289
|
- `forge_ui/reader.py`: discover runs, parse their files and produce summaries and details.
|
|
@@ -210,12 +294,18 @@ notifications on. Failed requests keep the last good data and show a banner.
|
|
|
210
294
|
- `forge_ui/__main__.py`: CLI options, local startup and optional browser opening.
|
|
211
295
|
- `forge_ui/__init__.py`: package version and the build id `forge ui` compares to find a stale server.
|
|
212
296
|
- `forge_ui/static/index.html`, `style.css`, `app.js`: the page shell, styles, routes, views and polling.
|
|
297
|
+
- `forge_ui/static/theme.js`, `shell.js`, `icons.js`, `datatable.js`: the theme switch, the Immersive and
|
|
298
|
+
Workbench layouts with their modules, keys and splitters, the icon set, and the sortable tables
|
|
299
|
+
(their pure parts are node-tested). `D-DINCondensed-Bold.woff2` is the engraved labels' face.
|
|
213
300
|
- `forge_ui/static/runmodel.js`: the pure run model (layouts, schedule projection, critical path, replay).
|
|
214
301
|
- `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)
|
|
302
|
+
- `forge_ui/static/map3d.js`, `scene3d.js`, `iso.js`: the 3D loader, scenes and engine (ported from archivision);
|
|
303
|
+
`findings3d.js` the findings' marks, `stagectl.js` the stage's keys, legend and keys sheet.
|
|
216
304
|
- `forge_ui/static/cloudmodel.js`, `cloud3d.js`, `cloudui.js`: the run cloud's pure model (projects,
|
|
217
305
|
filters, layouts, layout files), its scene on the shared stage, and its controls.
|
|
218
306
|
- `forge_ui/static/diffview.js`: the diff viewer, with findings pinned to their lines.
|
|
307
|
+
- `forge_ui/static/convo.js`, `forge_ui/transcripts.py`: an execution's conversation, and the
|
|
308
|
+
reader of each agent CLI's log format behind it.
|
|
219
309
|
- `forge_ui/static/files.js`: the files heatmap (`matrix()` is pure and node-tested).
|
|
220
310
|
- `forge_ui/export.py`: a run as one HTML file (the page's scripts and styles inlined, the API
|
|
221
311
|
responses it reads, the 3D stage's modules as one module text, a hash-based policy).
|
|
@@ -267,7 +357,7 @@ skips otherwise. Its 3D tests need three.js: the CDN, or a local copy of the npm
|
|
|
267
357
|
|
|
268
358
|
The pictures in this README and forge's come from `tools/readme_media.py`. After a change you
|
|
269
359
|
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;
|
|
360
|
+
Chromium with Playwright (three.js as for the browser tests; libwebp's `img2webp` and ImageMagick
|
|
271
361
|
to encode the animations and PNGs). `--only cloud diff` retakes some of them.
|
|
272
362
|
|
|
273
363
|
```sh
|
|
@@ -277,8 +367,11 @@ python3 tools/readme_media.py # writes docs/media/
|
|
|
277
367
|
## Privacy
|
|
278
368
|
|
|
279
369
|
Run dirs can contain prompts, diffs and review text from private repos. The server binds
|
|
280
|
-
only `127.0.0.1
|
|
281
|
-
|
|
370
|
+
only `127.0.0.1` and makes no network requests. It writes nothing itself; a change you
|
|
371
|
+
confirmed in the page is a call of forge's own CLI, protected by a per-launch token
|
|
372
|
+
(`--read-only` turns that off). Outside the runs it reads one kind of file: for a claude execution whose log
|
|
373
|
+
holds only its final answer, that run's session file, `<session id>.jsonl` under
|
|
374
|
+
`~/.claude/projects/` (or `$CLAUDE_CONFIG_DIR/projects/`). Anyone with access to the local server can read the runs it discovers, and
|
|
282
375
|
forge now registers every run, so that is every run on the machine.
|
|
283
376
|
|
|
284
377
|
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
|
+
```
|