@giamme/forge 0.0.0-stage → 0.8.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 +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
package/ui/docs/api.md
ADDED
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
# forge-ui data contract
|
|
2
|
+
|
|
3
|
+
This file is the contract between the three parts of forge-ui. It is fixed for tasks and
|
|
4
|
+
dwarves: implement it as written, and do not edit it as part of a task. Its owner revised it
|
|
5
|
+
deliberately for 0.3.0 (executions, findings, final reviews, the lazy endpoints and the 3D view)
|
|
6
|
+
and for 0.4.0 (diffs with pinned findings, the files heatmap, model facts and Fractal child tasks).
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
forge_ui/reader.py reads forge run dirs from disk, returns plain dicts (no I/O beyond reads)
|
|
10
|
+
forge_ui/executions.py the attempts/<role>-<uuid>/ dirs: parsing, nesting, attempt numbers, a parse cache
|
|
11
|
+
forge_ui/findings.py QA findings and gate lines, parsed by scripts/forge-contract.py
|
|
12
|
+
forge_ui/diffs.py unified diffs: parsing, the ```diff fence of a review prompt, pinning findings, cached reads
|
|
13
|
+
forge_ui/models.py per-run model facts (attempts and reviews) for the leaderboard, cached per run
|
|
14
|
+
forge_ui/fractal.py Fractal child tasks under $XDG_STATE_HOME/forge/fractal, read through dir fds
|
|
15
|
+
forge_ui/server.py serves those dicts as JSON plus the static page, 127.0.0.1 only
|
|
16
|
+
forge_ui/static/ the page: index.html, app.js, style.css, graph.js and the scripts they load
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.9+ standard library only. No third-party packages, no build step. The server makes
|
|
20
|
+
no network requests; the page loads three.js 0.160.0 from cdn.jsdelivr.net only when the 3D
|
|
21
|
+
view is used, and falls back to the 2D graph when it can't. The UI is read-only: nothing in
|
|
22
|
+
forge-ui writes to a run dir, a repo or forge's config.
|
|
23
|
+
|
|
24
|
+
## What a forge plan dir looks like
|
|
25
|
+
|
|
26
|
+
A decomposed forge run lives in a *plan dir*. Everything is optional except `tasks.tsv`;
|
|
27
|
+
any file may be missing, empty, truncated or malformed (runs are read while they are being
|
|
28
|
+
written), and the reader must never raise because of one.
|
|
29
|
+
|
|
30
|
+
| Path | Format |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `tasks.tsv` | tab-separated, 7 columns: `id deps difficulty files dwarf qa title`. Lines starting with `#` and blank lines are skipped. `deps` and `files` are comma lists or `-`. `dwarf`/`qa` are model specs (`alias[:effort[:harness]]`), `-` (not yet resolved) or `UNASSIGNED` |
|
|
33
|
+
| `waves.tsv` | `wave<TAB>id` per line, wave is an integer from 1 |
|
|
34
|
+
| `results.tsv` | `id<TAB>status<TAB>branch` per line, e.g. `reader MERGED forge/20261016-1530/reader` |
|
|
35
|
+
| `run_id` | one line, e.g. `20261016-153012-ab12` |
|
|
36
|
+
| `repo` | one line, absolute path of the target repo |
|
|
37
|
+
| `goal.txt`, `request.md` | free text |
|
|
38
|
+
| `decompose_level` | `low`, `medium` or `high` |
|
|
39
|
+
| `explore`, `shadow_rate` | one number each (e.g. `0.5`) |
|
|
40
|
+
| `shadow_qa` | a comma list of specs |
|
|
41
|
+
| `timeout` | seconds |
|
|
42
|
+
| `verification.status` | `PASS`, `FAIL` or another word; `verification.command` the command; `verification.log` its output |
|
|
43
|
+
| `verification.runs.tsv` | `n<TAB>kind<TAB>exit<TAB>tree_changed<TAB>duration_s<TAB>logfile` per run of the command; `kind` is `initial`, `rerun` or `jev_retry`, `exit` may be empty, `tree_changed` is `0` or `1` |
|
|
44
|
+
| `verification.commit` | one line, the commit verified |
|
|
45
|
+
| `max_parallel`, `escalate_after`, `verify_retries` | one integer each |
|
|
46
|
+
| `qa_threshold` | `P0`..`P3`, the least severe finding that still blocks; `final_qa` the final review's QA spec |
|
|
47
|
+
| `deferrals.txt` | one line per task the wave planner deferred |
|
|
48
|
+
| `integration.base`, `accepted.integration` | one commit each: where the integration branch started, the revision the run accepted |
|
|
49
|
+
| `attempts/<role>-<uuid>/` | one *execution* (below) of the run itself: `parallel` (a `run`) or `verification` |
|
|
50
|
+
| `final-review-<epoch>-<pid>/` | one whole-run review: `qa.input`, `qa.last`, `verdict` (`PASS FAIL UNKNOWN INVALIDATED ACCEPTED`; absent while it runs or when nothing was judged), `qa.infra`, `qa.gate`, `known_issues.md`, `review.meta` (`key=value`: `base tip source spec threshold`, `threshold=none` when unset), `tasks.summary`, `attempts/qa-<uuid>/` |
|
|
51
|
+
| `final-review` | a symlink to the newest *published* review dir; `final-review.verdict` a copy of its verdict |
|
|
52
|
+
| `integrate-<epoch>-<pid>/` | one `integrate`: `setup.log`, `merge.log`, `verification.*`, `attempts/verification-<uuid>/`, and `final-review/` (a review dir as above) when it ran one |
|
|
53
|
+
| `tasks/<id>/merged` | exists when the task was merged; its mtime is when |
|
|
54
|
+
| `tasks/<id>/status` | one word: `RUNNING PASS FAIL ERROR INFRA TIMEOUT UNKNOWN INVALIDATED BLOCKED INTERRUPTED NOCHANGES CONFLICT` |
|
|
55
|
+
| `tasks/<id>/attempt` | the current attempt number |
|
|
56
|
+
| `tasks/<id>/attempts.tsv` | `epoch<TAB>attempt<TAB>status<TAB>infra_class` per line, one per attempt entry; status may also be `MERGED`, `infra_class` may be empty, `-` or missing |
|
|
57
|
+
| `tasks/<id>/escalations.tsv` | `epoch<TAB>attempt<TAB>from_spec<TAB>to_spec` per line |
|
|
58
|
+
| `tasks/<id>/qa.last` | the reviewer's final message; its last line matching `FORGE_VERDICT: PASS` or `FORGE_VERDICT: FAIL` is the verdict |
|
|
59
|
+
| `tasks/<id>/prompt.md`, `approach.md`, `retry_findings.md` | free text |
|
|
60
|
+
| `tasks/<id>/accepted.by` | who accepted a task over a reviewer's FAIL, and why (`key=value` lines in current forge; returned as text) |
|
|
61
|
+
| `tasks/<id>/qa.gate` | `threshold<TAB>decision<TAB>worst<TAB>n_confirmed<TAB>n_unlabelled<TAB>n_below` (`decision` is `accept` or `keep`, `-` for none) |
|
|
62
|
+
| `tasks/<id>/known_issues.md` | the confirmed findings a threshold tolerated |
|
|
63
|
+
| `tasks/<id>/dwarf.input`, `qa.input` | the prompts forge assembled; `<role>.delivered`, `<role>.resolved` the live dispatch files |
|
|
64
|
+
| `tasks/<id>/attempts/<role>-<uuid>/` | one execution of the task: `task`, `dwarf` or `qa` |
|
|
65
|
+
| `tasks/<id>/changes.diff` | `git diff --binary` of the task's work since its base, cumulative, rewritten every attempt; `changes.prev.diff` the previous attempt's; `committed.diff` base..the reviewed commit |
|
|
66
|
+
| `tasks/<id>/drift.txt`, `conflict.files` | one path per line: files the diff changed without declaring them; files that conflicted on merge |
|
|
67
|
+
| `attempts/qa-<uuid>/qa.prompt` | the copy of the prompt a reviewer read; the diff it reviewed is its **last** fenced block: a line exactly ` ```diff `, the diff, a line exactly ` ``` ` (a section may follow) |
|
|
68
|
+
| `final-review-*/combined.diff` | the diff a final review read, absent when larger than 400 KB (then not inlined in `qa.input`) |
|
|
69
|
+
| `fractal-selection.json` | `{"enabled": true, "run": "run-<20 hex>"}` (or `enabled: false`): the run's Fractal choice, see Fractal below |
|
|
70
|
+
| `tasks/<id>/route.jsonl` | one JSON object per line, one per routing decision: `at` (epoch), `task`, `tier`, `epsilon`, `candidates` (list of specs), `default` (spec), `chosen` (spec), `explored` (bool), `propensity` (probability the chosen spec had), `inputs` (object), `decision` (a short hash identifying the decision). Keep unknown fields |
|
|
71
|
+
| `tasks/<id>/route.explored` | exists when the task's route was an exploratory pick |
|
|
72
|
+
| `tasks/<id>/clef.first_pass` | JSON: `p` (predicted probability the first attempt passes), `model`, `at`, `plan_table`, `assignment` (`{dwarf, qa}`) |
|
|
73
|
+
| `tasks/<id>/clef.retry_pass.<n>` | JSON, same shape plus `attempt` = n |
|
|
74
|
+
| `shadow/<id>/<attempt>-<k>/shadow.json` | JSON: `task`, `attempt`, `reviewed`, `primary`, `spec`, `pick`, `started`. A second, never-read review of attempt `<attempt>` |
|
|
75
|
+
| `shadow/<id>/<attempt>-<k>/shadow.status` | one word |
|
|
76
|
+
| `shadow/<id>/<attempt>-<k>/qa.last` | the shadow reviewer's final message, verdict as above |
|
|
77
|
+
| `shadow/<id>/<attempt>-<k>/attempts/qa-<uuid>/` | the shadow review's execution |
|
|
78
|
+
|
|
79
|
+
An **execution** is one dir `attempts/<role>-<uuid>/` that forge-runtime.py writes, `<role>`
|
|
80
|
+
one of `task dwarf qa solo verification parallel` and `<uuid>` 32 lowercase hex digits:
|
|
81
|
+
|
|
82
|
+
| Path | Format |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `metrics.json` | `role`, `started` (monotonic), `wall_started` (epoch), `phase`, `phase_started` (monotonic), `elapsed_s` (seconds per phase, `total` once done), `exit_code` (`null` while in flight), `harness`, `prompt_bytes`, `input_tokens`, `cached_input_tokens`, `output_tokens`, `infra_class`, `shadow` |
|
|
85
|
+
| `dataset.json` | may be absent: `parent` (the execution that started this one, e.g. `task-<uuid>`), `trigger`, `attempt_start` and `resume` (task executions) |
|
|
86
|
+
| `<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
|
+
The reader never opens an execution's `.log` or `.cmd`.
|
|
89
|
+
|
|
90
|
+
**Task status** is `MERGED` if `tasks/<id>/merged` exists, otherwise the first word of
|
|
91
|
+
`tasks/<id>/status`, otherwise `PENDING`.
|
|
92
|
+
|
|
93
|
+
Only the ids listed in `tasks.tsv` are tasks: `tasks/` may also hold forge's own dirs (e.g.
|
|
94
|
+
`tasks/_integration/`), and a task dir holds many other files (`attempts/` is a directory of
|
|
95
|
+
receipts, not `attempts.tsv`), all of which the reader ignores.
|
|
96
|
+
|
|
97
|
+
## What a solo run dir looks like
|
|
98
|
+
|
|
99
|
+
A solo run has no `tasks.tsv`. It is a directory whose `prompt.md` and `repo` are both regular
|
|
100
|
+
files. Anything else may be missing, empty, truncated or malformed, and the reader must never
|
|
101
|
+
raise because of one. Older solo dirs have `verdict` and not `status`.
|
|
102
|
+
|
|
103
|
+
| Path | Format |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `prompt.md` | the implementation prompt |
|
|
106
|
+
| `repo` | one line, absolute path of the target repo |
|
|
107
|
+
| `goal.txt`, `request.md` | free text; the summary goal is the first line of `goal.txt` |
|
|
108
|
+
| `status` | one word: `RUNNING`, then `PASS`, `FAIL`, `UNKNOWN`, `NOCHANGES`, `INVALIDATED`, `TIMEOUT`, `INFRA`, `INTERRUPTED` or `ERROR` |
|
|
109
|
+
| `verdict` | one word, the legacy status (`PASS`, `FAIL`, `UNKNOWN`, `NOCHANGES`, `INVALIDATED`, …) |
|
|
110
|
+
| `dwarf.spec`, `qa.spec` | one line, a model spec (`alias[:effort[:harness]]`) |
|
|
111
|
+
| `dwarf.resolved`, `qa.resolved` | `key=value` lines; `alias=` and `effort=` name the model that ran |
|
|
112
|
+
| `attempts.tsv` | `epoch<TAB>n<TAB>status<TAB>infra class<TAB>dwarf spec`. The fifth column is recorded for forge and is not a separate attempt field |
|
|
113
|
+
| `attempts/<role>-<uuid>/` | the run's executions: `solo`, `dwarf`, `qa` |
|
|
114
|
+
| `dwarf.input`, `qa.input`, `qa.gate`, `known_issues.md` | as for a plan task |
|
|
115
|
+
| `dwarf.last`, `qa.last` | the role's final message. `qa.last` carries `FORGE_VERDICT:` the same way a plan task does |
|
|
116
|
+
| `dwarf.log`, `qa.log` | raw logs. The summary stats them and does not read them, so `updated` moves while a model writes |
|
|
117
|
+
| `changes.diff`, `existing.diff` | the run's change, and the uncommitted changes the repo already had when it started |
|
|
118
|
+
| `fractal-selection.json` | as for a plan |
|
|
119
|
+
|
|
120
|
+
**Solo status** is the first word of `status`, otherwise the first word of `verdict`, otherwise
|
|
121
|
+
`UNKNOWN`.
|
|
122
|
+
|
|
123
|
+
**Which dirs are runs:** `tasks.tsv` means a plan, even when `prompt.md` and `repo` are also
|
|
124
|
+
present. Otherwise `prompt.md` and `repo`, both regular files, means a solo run. Anything else
|
|
125
|
+
is not a run.
|
|
126
|
+
|
|
127
|
+
## Where runs come from
|
|
128
|
+
|
|
129
|
+
1. `--plan DIR` (repeatable): that dir, if it is a plan or a solo run.
|
|
130
|
+
2. `--root DIR` (repeatable): every plan dir at depth 0, 1 or 2 under DIR (DIR itself,
|
|
131
|
+
its children, its grandchildren). Solo runs are not picked up, so a directory that merely
|
|
132
|
+
contains `prompt.md` is ignored. Hidden dirs (`.name`) are not descended into.
|
|
133
|
+
3. forge's run registry, unless `--no-registry`: every `*.txt` file in
|
|
134
|
+
`$XDG_CONFIG_HOME/forge/runs/` (default `~/.config/forge/runs/`). Each file is a
|
|
135
|
+
`# ...` header line then one absolute path per line. Paths that no longer exist or are
|
|
136
|
+
not plan or solo runs are skipped silently.
|
|
137
|
+
|
|
138
|
+
Duplicates (same `os.path.realpath`) are listed once. The first source that found a dir
|
|
139
|
+
wins for `source`.
|
|
140
|
+
|
|
141
|
+
## Python API (`forge_ui/reader.py`)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
def run_key(path) -> str
|
|
145
|
+
"""Stable opaque id of a run dir: sha1 of its realpath, first 12 hex chars."""
|
|
146
|
+
|
|
147
|
+
def discover(plans=(), roots=(), registry=True, registry_dir=None) -> list[tuple[Path, str]]
|
|
148
|
+
"""(realpath, source) for every run dir found, source in 'plan' | 'root' | 'registry'.
|
|
149
|
+
`--plan` and the registry accept plan and solo dirs. `--root` yields plan dirs only.
|
|
150
|
+
registry_dir overrides the registry location (tests use it)."""
|
|
151
|
+
|
|
152
|
+
def summarize(path, source='plan') -> dict # a RunSummary, below
|
|
153
|
+
def detail(path, source='plan') -> dict # a RunDetail, below
|
|
154
|
+
def task_detail(path, task_id, source='plan') -> dict | None # a TaskDetail
|
|
155
|
+
def execution_detail(path, name, source='plan') -> dict | None # an ExecutionDetail
|
|
156
|
+
def final_review_detail(path, name, source='plan') -> dict | None # a FinalReviewDetail
|
|
157
|
+
def task_diff(path, task_id, which='current', source='plan') -> dict | None # a DiffDetail
|
|
158
|
+
def execution_diff(path, name, source='plan') -> dict | None # a DiffDetail
|
|
159
|
+
def final_review_diff(path, name, source='plan') -> dict | None # a DiffDetail
|
|
160
|
+
def files_detail(path, source='plan') -> dict | None # Files
|
|
161
|
+
def fractal_detail(path, task_id, source='plan') -> dict | None # a TaskFractal
|
|
162
|
+
def fractal_node(path, task_id, node, source='plan') -> dict | None # a FractalNodeDetail
|
|
163
|
+
def fractal_node_diff(path, task_id, node, source='plan') -> dict | None # a DiffDetail
|
|
164
|
+
|
|
165
|
+
class Catalog:
|
|
166
|
+
def __init__(self, plans=(), roots=(), registry=True, registry_dir=None): ...
|
|
167
|
+
def runs(self) -> list[dict] # RunSummary list, newest `updated` first
|
|
168
|
+
def get(self, key) -> dict | None # RunDetail for that run_key, or None
|
|
169
|
+
def task(self, key, task_id) -> dict | None
|
|
170
|
+
def execution(self, key, name) -> dict | None
|
|
171
|
+
def final_review(self, key, name) -> dict | None
|
|
172
|
+
def diff(self, key, task_id, which='current'), execution_diff(self, key, name),
|
|
173
|
+
final_review_diff(self, key, name), files(self, key), fractal(self, key, task_id),
|
|
174
|
+
fractal_node(self, key, task_id, node), fractal_node_diff(self, key, task_id, node) -> dict | None
|
|
175
|
+
def models(self) -> dict # Models, over every run
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`Catalog` re-discovers and re-reads on every call: runs change while they are watched. Files
|
|
179
|
+
that stop changing once written (`metrics.json`, `dataset.json`, `*.resolved` and `qa.last`
|
|
180
|
+
copies, findings of a reply) are parsed once and kept by `(dev, inode, size, mtime_ns)`; a
|
|
181
|
+
file is still stat'ed on every call, and one younger than 3 seconds is never kept, since a
|
|
182
|
+
rewrite within one mtime tick can keep its size. Every returned value is JSON-serializable
|
|
183
|
+
with `json.dumps(allow_nan=False)` (no `Path`, no sets, no bytes, no NaN), and a detail read
|
|
184
|
+
twice from unchanged files is equal: nothing depends on the clock.
|
|
185
|
+
|
|
186
|
+
Findings are parsed by `scripts/forge-contract.py` (`parse_findings`, `_LOCATION`), loaded
|
|
187
|
+
once on first use. `build_id()` does not hash it: a running server keeps the parser it
|
|
188
|
+
loaded. When it cannot be loaded, `findings_available` is `false` and every findings field
|
|
189
|
+
is `null`; verdicts and `gate` do not need it.
|
|
190
|
+
|
|
191
|
+
Text fields are capped so one huge file cannot blow up a response: `request`, `prompt`,
|
|
192
|
+
`approach`, `retry_findings` keep their first 20,000 characters; `qa_tail`, `dwarf_tail`
|
|
193
|
+
and `verification.log_tail` keep their last 4,000. A capped field ends (or, for tails,
|
|
194
|
+
starts) with `…` and the value of `truncated` lists the field names that were cut.
|
|
195
|
+
|
|
196
|
+
The lazy details cap differently: a cut text keeps its head and its tail around `\n…\n`
|
|
197
|
+
(two bounded reads, never the whole file). TaskDetail `texts`, FinalReviewDetail
|
|
198
|
+
`tasks_summary`, `qa_last` and `known_issues` keep 20,000 + 8,000 characters; ExecutionDetail
|
|
199
|
+
`prompt` and FinalReviewDetail `qa_input` keep 40,000 + 20,000; ExecutionDetail `last` keeps
|
|
200
|
+
its last 20,000 behind a leading `…`. Their `truncated` lists the field names (`prompt`,
|
|
201
|
+
`qa_input`, `last`, ...). A Finding's `text` keeps 20,000 characters, its `summary` 240.
|
|
202
|
+
|
|
203
|
+
## JSON API (`forge_ui/server.py`)
|
|
204
|
+
|
|
205
|
+
| Request | Response |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `GET /api/runs` | `200 {"runs": [RunSummary, ...]}` |
|
|
208
|
+
| `GET /api/runs/<key>` | `200 RunDetail`, or `404 {"error": "not found"}` |
|
|
209
|
+
| `GET /api/runs/<key>/tasks/<id>` | `200 TaskDetail`; `<id>` is percent-decoded (strict UTF-8) and must be a `tasks.tsv` id (`solo` for a solo run). An id that decodes to `/`, NUL, U+FFFD, `.` or `..` is a 404 without a read |
|
|
210
|
+
| `GET /api/runs/<key>/executions/<role>-<uuid>` | `200 ExecutionDetail` |
|
|
211
|
+
| `GET /api/runs/<key>/final-reviews/<name>` | `200 FinalReviewDetail`; `<name>` is `final-review-<e>-<p>` or `integrate-<e>-<p>` |
|
|
212
|
+
| `GET /api/runs/<key>/tasks/<id>/diff[/<which>]` | `200 DiffDetail`; `<which>` is `current` (the default: `changes.diff`), `prev`, `committed` (plan tasks), `existing` (solo) |
|
|
213
|
+
| `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
|
+
| `GET /api/runs/<key>/final-reviews/<name>/diff` | `200 DiffDetail` |
|
|
215
|
+
| `GET /api/runs/<key>/files` | `200 Files` |
|
|
216
|
+
| `GET /api/models` | `200 Models` |
|
|
217
|
+
| `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
|
+
| `GET /api/runs/<key>/tasks/<id>/fractal` | `200 TaskFractal` |
|
|
219
|
+
| `GET /api/runs/<key>/tasks/<id>/fractal/nodes/<node>[/diff]` | `200 FractalNodeDetail`, or with `/diff` a `DiffDetail` of its `candidate.diff`; `<node>` matches `[A-Za-z0-9][A-Za-z0-9_-]{0,100}` |
|
|
220
|
+
| `GET /` | `forge_ui/static/index.html` |
|
|
221
|
+
| `GET /<name>` | that file from `forge_ui/static/` (`.html .js .css .svg` only, no subdirs) |
|
|
222
|
+
| anything else | `404 {"error": "not found"}`; any method but GET or HEAD: `405` |
|
|
223
|
+
|
|
224
|
+
The lazy endpoints answer `404 {"error": "not found"}` for an unknown run, task,
|
|
225
|
+
execution or review; the diff endpoints also for a `<which>` the run kind has not (`existing`
|
|
226
|
+
of a plan task, `prev` of a solo run) and for an execution that is not `qa` or `dwarf`; the
|
|
227
|
+
Fractal node endpoints for an unknown node or a task whose Fractal state is not `available`. A
|
|
228
|
+
diff that is not there yet is a `200` with `available: false`. An execution is looked for only where executions are written:
|
|
229
|
+
`attempts/`, `tasks/<id>/attempts/`, `shadow/<id>/<a>-<k>/attempts/`,
|
|
230
|
+
`final-review-*/attempts/` and `integrate-*/final-review/attempts/`, in that order; a dir
|
|
231
|
+
that resolves outside the run dir (a symlinked `attempts/`, say) is ignored.
|
|
232
|
+
|
|
233
|
+
JSON responses: `Content-Type: application/json; charset=utf-8`,
|
|
234
|
+
`Cache-Control: no-store`. The page loads `style.css`, `runmodel.js`, `graph.js`, `gantt.js`,
|
|
235
|
+
`panel.js`, `map3d.js` and `app.js` by those relative names, and `scene3d.js` and `iso.js`
|
|
236
|
+
(ES modules) only when the 3D view is used.
|
|
237
|
+
|
|
238
|
+
Every response carries `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`
|
|
239
|
+
and exactly one `Content-Security-Policy`. An `.html` file gets the page policy
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
default-src 'self'; style-src 'self'; script-src 'self' https://cdn.jsdelivr.net/npm/three@0.160.0/ <hashes>; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
where `<hashes>` is `'sha256-<base64>'` for each `<script type="importmap">…</script>` in the
|
|
246
|
+
file as served (tag case-insensitive; CRLF and CR read as LF first, as browsers do), and
|
|
247
|
+
nothing when there is none. Every other response keeps
|
|
248
|
+
`default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'`.
|
|
249
|
+
|
|
250
|
+
## RunSummary
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"key": "3f9a0c1d2e4b",
|
|
255
|
+
"path": "/Users/me/dev/forge-ui-plan",
|
|
256
|
+
"source": "plan",
|
|
257
|
+
"kind": "plan",
|
|
258
|
+
"run_id": "20261016-153012-ab12",
|
|
259
|
+
"repo": "/Users/me/dev/forge-ui",
|
|
260
|
+
"goal": "first line of goal.txt",
|
|
261
|
+
"decompose_level": "medium",
|
|
262
|
+
"updated": 1760621412.5,
|
|
263
|
+
"task_count": 5,
|
|
264
|
+
"counts": {"MERGED": 3, "RUNNING": 1, "PENDING": 1},
|
|
265
|
+
"verification_status": "PASS",
|
|
266
|
+
"created": 1760619000.0,
|
|
267
|
+
"task_statuses": [{"id": "reader", "status": "MERGED", "difficulty": "high", "title": "Read plan dirs"},
|
|
268
|
+
{"id": "page", "status": "RUNNING", "difficulty": "medium", "title": "Draw the page"}],
|
|
269
|
+
"models": ["opus", "sol:xhigh"],
|
|
270
|
+
"dwarf_models": ["sol:xhigh"],
|
|
271
|
+
"qa_models": ["opus"],
|
|
272
|
+
"final_review": "FAIL"
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- `task_statuses`: every task in `tasks.tsv` order, with its `difficulty` (`null` when empty) and
|
|
277
|
+
`title` (its first 160 characters, then `…`). A solo run: `[{"id": "solo", "status": <solo status>,
|
|
278
|
+
"difficulty": null, "title": <goal or "Solo run">}]`.
|
|
279
|
+
- `created`: when the run began: the earliest birth time (else mtime) of the files a run writes
|
|
280
|
+
once at its start (plan: `run_id`, `repo`, `request.md`, `goal.txt`, `tasks.tsv`; solo: `prompt.md`,
|
|
281
|
+
`repo`, `request.md`, `goal.txt`), else `null`. These are stat'ed only, and never move `updated`.
|
|
282
|
+
- `dwarf_models`, `qa_models`: `models` split by role.
|
|
283
|
+
- `models`: the sorted distinct specs of the `dwarf` and `qa` columns (a solo run: its `dwarf`
|
|
284
|
+
and `qa` specs as its task has them), nulls left out.
|
|
285
|
+
- `final_review`: the first word of `final-review.verdict`, else `null`; always `null` for a
|
|
286
|
+
solo run. (RunDetail replaces it with an object, below.)
|
|
287
|
+
|
|
288
|
+
`kind` is `"plan"` or `"solo"`. `updated` is the newest mtime (epoch seconds, float) among
|
|
289
|
+
the run dir and the files the reader read. Any field it cannot determine is `null`
|
|
290
|
+
(`counts` is then `{}`, `task_count` 0).
|
|
291
|
+
|
|
292
|
+
For a solo run, `run_id` is the directory name, `decompose_level` and `verification_status`
|
|
293
|
+
are `null`, `task_count` is 1, and `counts` is `{<status>: 1}` using the solo status above.
|
|
294
|
+
The summary reads only `repo`, `goal.txt`, `status`, `verdict`, `dwarf.spec`, `qa.spec` and,
|
|
295
|
+
when a spec file is missing, `*.resolved`. It also stats `dwarf.log` and `qa.log` without
|
|
296
|
+
reading them, so `updated` moves while a model is writing a log. A plan summary reads
|
|
297
|
+
`final-review.verdict` besides the files it always read.
|
|
298
|
+
|
|
299
|
+
## RunDetail
|
|
300
|
+
|
|
301
|
+
Every RunSummary field, plus:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"request": "the user's request, verbatim",
|
|
306
|
+
"settings": {"explore": 0.5, "shadow_rate": 0.5, "shadow_qa": ["opus", "sol:xhigh"], "timeout": 3600,
|
|
307
|
+
"max_parallel": 2, "qa_threshold": "P2", "escalate_after": 3, "verify_retries": 1,
|
|
308
|
+
"final_qa": "opus"},
|
|
309
|
+
"verification": {"status": "PASS", "command": "python3 -m unittest", "log_tail": "...",
|
|
310
|
+
"runs": [{"n": 1, "kind": "initial", "exit": 0, "tree_changed": false,
|
|
311
|
+
"duration_s": 12.3, "log": "verification.run-1.log"}],
|
|
312
|
+
"commit": "9c1e4b7a..."},
|
|
313
|
+
"waves": [["reader", "page", "graph"], ["server"], ["readme"]],
|
|
314
|
+
"tasks": [Task, ...],
|
|
315
|
+
"timeline": {"started": 1760620000.25, "ended": null, "runs": [Execution], "verifications": [Execution]},
|
|
316
|
+
"integration": {"base": "1a2b...", "accepted": "3c4d...",
|
|
317
|
+
"merged": [{"id": "reader", "at": 1760621100.0}],
|
|
318
|
+
"integrations": [{"name": "integrate-1760622000-4300", "at": 1760622000.0,
|
|
319
|
+
"verification_status": "PASS",
|
|
320
|
+
"final_review": "integrate-1760622000-4300/final-review",
|
|
321
|
+
"ended": 1760622300.0}]},
|
|
322
|
+
"final_review": FinalReviewSummary,
|
|
323
|
+
"final_reviews": [FinalReviewSummary, ...],
|
|
324
|
+
"deferrals": ["wave 2: docs deferred (file overlap with a task already in this wave)"],
|
|
325
|
+
"findings_available": true,
|
|
326
|
+
"warnings": ["tasks/graph/route.jsonl line 2: not JSON"],
|
|
327
|
+
"truncated": ["tasks/reader/prompt"]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- `settings` values are `null` when the file is missing or unparsable; `shadow_qa` is a
|
|
332
|
+
list (empty when missing). `max_parallel`, `escalate_after`, `verify_retries` are integers,
|
|
333
|
+
`qa_threshold` and `final_qa` the first line of their file. A solo run has no plan
|
|
334
|
+
settings: every value is `null`, including `shadow_qa`.
|
|
335
|
+
- `verification` is `null` when none of `verification.status`, `.command`, `.log` exists. A
|
|
336
|
+
solo run's `verification` is always `null`. `runs` comes from `verification.runs.tsv`
|
|
337
|
+
(malformed rows are warned and skipped; an empty `exit` is `null`), `commit` is the first
|
|
338
|
+
line of `verification.commit` or `null`.
|
|
339
|
+
- `timeline.started` is the earliest `started` of every execution the detail shows (tasks
|
|
340
|
+
and their shadows, the listed final reviews, the run's own); `ended` is the latest `ended`,
|
|
341
|
+
or `null` while any of them is `running` or when none has ended. `runs` and
|
|
342
|
+
`verifications` are the run's own `parallel` and `verification` executions. A solo run's
|
|
343
|
+
comes from its executions, with `runs` and `verifications` empty.
|
|
344
|
+
- `integration` is `null` unless `integration.base`, `accepted.integration`, a merged marker
|
|
345
|
+
or an `integrate-*` dir exists. `merged` is every merged task with its marker's mtime,
|
|
346
|
+
oldest first. `integrations` is every `integrate-<e>-<p>/`, newest first: `at` is `<e>`,
|
|
347
|
+
`verification_status` the first word of its `verification.status`, `final_review` the
|
|
348
|
+
path of its review dir or `null`, `ended` the latest end of its verification and review
|
|
349
|
+
executions (`null` while one is unfinished or there is none).
|
|
350
|
+
- `final_review` is the newest FinalReviewSummary (review and integrate dirs together, by the
|
|
351
|
+
epoch in the name), or `null`; `final_reviews` lists the newest 20, newest first.
|
|
352
|
+
- `deferrals`: the non-empty lines of `deferrals.txt`, `[]` when it is missing.
|
|
353
|
+
- `findings_available`: whether `scripts/forge-contract.py` could be loaded.
|
|
354
|
+
- `fractal`: `null` without `fractal-selection.json`, else `{"enabled", "run", "available", "reason"}`
|
|
355
|
+
(Fractal below). Plan and solo runs alike.
|
|
356
|
+
- A solo run has `integration` `null`, `final_review` `null`, `final_reviews` and
|
|
357
|
+
`deferrals` `[]`.
|
|
358
|
+
- `waves` comes from `waves.tsv` (index 0 is wave 1); `[]` when it is missing. A solo run's
|
|
359
|
+
`waves` is `[["solo"]]`.
|
|
360
|
+
- `tasks` keeps the order of `tasks.tsv`. A solo run has one task, described below.
|
|
361
|
+
- `warnings` names every file or line the reader skipped as malformed. Missing files are
|
|
362
|
+
not warnings.
|
|
363
|
+
|
|
364
|
+
## Task
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"id": "reader",
|
|
369
|
+
"title": "Read plan dirs into plain dicts",
|
|
370
|
+
"deps": [],
|
|
371
|
+
"difficulty": "high",
|
|
372
|
+
"files": ["forge_ui/reader.py", "tests/test_reader.py"],
|
|
373
|
+
"dwarf": "astra",
|
|
374
|
+
"qa": "opus",
|
|
375
|
+
"wave": 1,
|
|
376
|
+
"status": "MERGED",
|
|
377
|
+
"branch": "forge/20261016-153012-ab12/reader",
|
|
378
|
+
"attempt": 2,
|
|
379
|
+
"attempts": [{"at": 1760620000.0, "attempt": 1, "status": "FAIL", "infra": null},
|
|
380
|
+
{"at": 1760621000.0, "attempt": 2, "status": "PASS", "infra": null}],
|
|
381
|
+
"escalations": [{"at": 1760620500.0, "attempt": 2, "from": "luna", "to": "sol"}],
|
|
382
|
+
"verdict": "PASS",
|
|
383
|
+
"accepted_by": null,
|
|
384
|
+
"explored": true,
|
|
385
|
+
"route": [{"at": 1760619000.0, "task": "reader", "tier": "high", "epsilon": 0.5,
|
|
386
|
+
"candidates": ["astra", "opus"], "default": "opus", "chosen": "astra",
|
|
387
|
+
"explored": true, "propensity": 0.25, "inputs": {}, "decision": "9c1e4b7a0d2f3e58"}],
|
|
388
|
+
"clef": {"first_pass": {"p": 0.71, "model": "clef", "at": 1760619100.0,
|
|
389
|
+
"assignment": {"dwarf": "astra", "qa": "opus"}},
|
|
390
|
+
"retry_pass": {"2": {"p": 0.64, "model": "clef", "at": 1760620600.0, "attempt": 2,
|
|
391
|
+
"assignment": {"dwarf": "sol", "qa": "opus"}}}},
|
|
392
|
+
"shadow": [{"attempt": 1, "k": 0, "spec": "sol:xhigh", "primary": "opus",
|
|
393
|
+
"status": "PASS", "verdict": "FAIL", "started": 1760620100.0}],
|
|
394
|
+
"prompt": "full prompt.md text",
|
|
395
|
+
"approach": null,
|
|
396
|
+
"retry_findings": "...",
|
|
397
|
+
"qa_tail": "last 4000 chars of qa.last",
|
|
398
|
+
"dwarf_tail": "last 4000 chars of dwarf.last",
|
|
399
|
+
"executions": [Execution, ...],
|
|
400
|
+
"merged_at": 1760621100.0,
|
|
401
|
+
"findings": FindingsSummary,
|
|
402
|
+
"gate": {"threshold": "P2", "decision": "keep", "worst": "P3", "n_confirmed": 1,
|
|
403
|
+
"n_unlabelled": 0, "n_below": 1},
|
|
404
|
+
"known_issues": true
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- `deps`, `files`: lists; `-` or empty becomes `[]`.
|
|
409
|
+
- `dwarf`, `qa`: the `tasks.tsv` value; `-`, empty and `UNASSIGNED` become `null`.
|
|
410
|
+
- `wave`: from `waves.tsv`, `null` if absent.
|
|
411
|
+
- `branch`: from `results.tsv`; else `forge/<run_id>/<id>` when `run_id` is known and the
|
|
412
|
+
task has a `tasks/<id>/` dir; else `null`.
|
|
413
|
+
- `attempt`: integer from `tasks/<id>/attempt`, else the highest attempt in `attempts`,
|
|
414
|
+
else `null`.
|
|
415
|
+
- `infra` in an attempt: `null` for empty, `-` or missing.
|
|
416
|
+
- `verdict`: `"PASS"`, `"FAIL"` or `null`, from `qa.last` as described above.
|
|
417
|
+
- `dwarf_tail`: the last 4,000 characters of `dwarf.last` (`tasks/<id>/dwarf.last` for a
|
|
418
|
+
plan task, the run dir's `dwarf.last` for a solo run), or `null` when the file is missing.
|
|
419
|
+
Capped like `qa_tail`.
|
|
420
|
+
- `explored`: `true` if `route.explored` exists or any `route` entry has `explored: true`.
|
|
421
|
+
- `clef.first_pass` is `null` when absent; `clef.retry_pass` is `{}` when there are none;
|
|
422
|
+
its keys are the attempt numbers as strings.
|
|
423
|
+
- `shadow`: one entry per `shadow/<id>/<attempt>-<k>/` dir, sorted by attempt then k.
|
|
424
|
+
`status` is `shadow.status`, `verdict` is parsed from that dir's `qa.last`; fields from
|
|
425
|
+
`shadow.json` (`spec`, `primary`, `started`, `reviewed`, `pick`, as they are) that are
|
|
426
|
+
missing are `null`, and a missing `started` falls back to the execution's. Each entry also
|
|
427
|
+
has `execution` (the id of its newest `attempts/qa-*` execution, or `null`), `ended` and
|
|
428
|
+
`duration_s` (that execution's), `findings` (FindingsSummary of its `qa.last`),
|
|
429
|
+
`primary_verdict` (the verdict of the newest non-shadow `qa` execution of the task with the
|
|
430
|
+
same attempt, or `null`) and `agreement`: `agree` or `disagree` when both verdicts are
|
|
431
|
+
PASS or FAIL, else `unknown`.
|
|
432
|
+
- `executions`: every execution in `tasks/<id>/attempts/` (`task`, `dwarf`, `qa`); for a solo
|
|
433
|
+
run, the run dir's `attempts/` (`solo`, `dwarf`, `qa`). Shadow executions are not in it.
|
|
434
|
+
- `merged_at`: the mtime of `tasks/<id>/merged`, or `null`.
|
|
435
|
+
- `findings`: FindingsSummary of the current `qa.last`, or `null`.
|
|
436
|
+
- `gate`: the parsed `qa.gate` (`n_*` integers, `-` is `null`), or `null` when missing or
|
|
437
|
+
malformed (malformed is warned).
|
|
438
|
+
- `known_issues`: whether `known_issues.md` is a non-empty file.
|
|
439
|
+
- Text fields are `null` when the file is missing.
|
|
440
|
+
|
|
441
|
+
A solo run's one task uses this same shape. `id` is `solo` and `title` is the goal, or
|
|
442
|
+
`"Solo run"` when `goal.txt` has no first line. `dwarf` and `qa` come from `dwarf.spec` and
|
|
443
|
+
`qa.spec`, otherwise `alias[:effort]` from the matching `*.resolved` (`effort` is omitted when
|
|
444
|
+
it is missing or `<harness default>`). `wave` is 1, `branch` is `null`, and `attempts` come
|
|
445
|
+
from the run dir's `attempts.tsv`. `status` is the solo status; `attempt` is the highest `n`
|
|
446
|
+
in `attempts.tsv`, or `null`. `escalations`, `route` and `shadow` are empty. `verdict` and
|
|
447
|
+
`qa_tail` come from `qa.last`, and `prompt` comes from `prompt.md`. `deps` and `files` are
|
|
448
|
+
`[]`; `difficulty`, `accepted_by`, `approach` and `retry_findings` are `null`; `explored` is
|
|
449
|
+
false; `clef` is `{first_pass: null, retry_pass: {}}`. `request` on the detail is `request.md`.
|
|
450
|
+
|
|
451
|
+
## Execution
|
|
452
|
+
|
|
453
|
+
```json
|
|
454
|
+
{"id": "dwarf-0f1e2d3c4b5a69788796a5b4c3d2e1f0", "role": "dwarf",
|
|
455
|
+
"parent": "task-9c1e4b7a0d2f3e589c1e4b7a0d2f3e58", "attempt": 2, "shadow": false,
|
|
456
|
+
"state": "done", "started": 1760620000.25, "ended": 1760620612.75, "duration_s": 612.5,
|
|
457
|
+
"phase": "complete", "phase_started": 1760620612.75,
|
|
458
|
+
"phases": {"preparation": 1.2, "preflight": 0.4, "model": 590.1, "snapshot": 3.3, "verification": 0.0},
|
|
459
|
+
"exit_code": 0, "infra_class": null, "timed_out": false,
|
|
460
|
+
"model": {"spec": "sol:xhigh", "alias": "sol", "harness": "codex", "model": "gpt-sol", "effort": "xhigh",
|
|
461
|
+
"clamped": null},
|
|
462
|
+
"tokens": {"input": 120000, "cached": 90000, "output": 8000}, "prompt_bytes": 45678,
|
|
463
|
+
"verdict": null, "findings": null}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- `started` is `wall_started`; `ended` is `started + elapsed_s.total` and `duration_s` is
|
|
467
|
+
`elapsed_s.total`, both `null` until `exit_code` is set; `phase_started` is
|
|
468
|
+
`wall_started + (phase_started - started)`. `phases` keeps the numeric `elapsed_s` entries
|
|
469
|
+
except `total`. A value of the wrong type is dropped with a warning; a missing or invalid
|
|
470
|
+
`metrics.json` leaves every field it gives `null`.
|
|
471
|
+
- `state`: `done` once `exit_code` is an integer. Otherwise `running` when its owner is live
|
|
472
|
+
and it is the newest execution of its role in its dir, else `orphaned`. The owner is live
|
|
473
|
+
when: the task's status is `RUNNING` (task executions and their children); the solo
|
|
474
|
+
status is `RUNNING`; a shadow has no `shadow.status`; a final review's `state` is
|
|
475
|
+
`running`; always, for the run's own `parallel` and `verification`. A `dwarf` or `qa`
|
|
476
|
+
whose parent is not `running` is `orphaned`.
|
|
477
|
+
- `parent`: for `dwarf` and `qa`, the `task` (or `solo`) execution `dataset.json` names, else
|
|
478
|
+
the latest one whose run (`started` to `ended`, open while unfinished) contains this
|
|
479
|
+
start, else `null`. `task`, `solo`, `parallel` and `verification` have no parent.
|
|
480
|
+
- `attempt`: a `task` or `solo` execution takes the `attempts.tsv` row whose epoch is within
|
|
481
|
+
120 s of its `ended` (closest first, each row once); else `dataset.json`'s
|
|
482
|
+
`attempt_start + 1` (`attempt_start` when `resume` is set); else, while running, the
|
|
483
|
+
task's `attempt` (a solo run: one more than its `attempts.tsv` rows). Children take their
|
|
484
|
+
parent's; a shadow's is its dir's attempt; anything else is `null`.
|
|
485
|
+
- `model`: from the `<role>.resolved` copy (a running execution without one: the owner dir's
|
|
486
|
+
live `<role>.resolved`); `spec` is `alias[:effort]`, `effort` is `null` for
|
|
487
|
+
`<harness default>`. `null` for the pipeline roles and when there is no alias.
|
|
488
|
+
- `tokens` is `null` when all three counts are; `timed_out` is whether `<role>.timedout` exists.
|
|
489
|
+
- `verdict` and `findings` (FindingsSummary): `qa` only, from its own `qa.last` copy once done.
|
|
490
|
+
- Executions are ordered by `started` (unknown last), then `id`.
|
|
491
|
+
|
|
492
|
+
## FindingsSummary and Finding
|
|
493
|
+
|
|
494
|
+
```json
|
|
495
|
+
{"P0": 0, "P1": 1, "P2": 2, "P3": 0, "unlabelled": 0, "plausible": 1, "confirmed": 3, "worst": "P1"}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`P0`..`P3` count CONFIRMED findings by severity, `confirmed` all CONFIRMED ones, `unlabelled`
|
|
499
|
+
the CONFIRMED ones without a severity plus the UNSTATED ones (as the gate's `n_unlabelled`),
|
|
500
|
+
`plausible` the PLAUSIBLE ones; `worst` is the most severe CONFIRMED label or `null`. A
|
|
501
|
+
FindingsSummary is `null` when there is no text or no contract module.
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{"severity": "P1", "status": "CONFIRMED", "location": "src/x.py:12", "line": 7,
|
|
505
|
+
"summary": "- [P1][CONFIRMED] src/x.py:12 — first line, at most 240 characters",
|
|
506
|
+
"text": "the whole finding block"}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`status` is `CONFIRMED`, `PLAUSIBLE` or `UNSTATED`; `severity` may be `null`; `location` is the
|
|
510
|
+
first match of the contract's `_LOCATION` in the first line, or `null`; `line` is the
|
|
511
|
+
block's 1-based line in the reply. Lists of Findings keep the first 200.
|
|
512
|
+
|
|
513
|
+
## FinalReviewSummary
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{"name": "final-review-1760622000-4242", "source": "review", "at": 1760622000.0, "published": true,
|
|
517
|
+
"verdict": "FAIL", "state": "done", "spec": "opus", "threshold": "P2", "base": "1a2b...", "tip": "3c4d...",
|
|
518
|
+
"gate": {...}, "findings": FindingsSummary, "execution": "qa-...",
|
|
519
|
+
"started": 1760622010.0, "ended": 1760622400.0, "duration_s": 390.0}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
- `name` is the dir name; an integration's review lives in `integrate-<e>-<p>/final-review/`
|
|
523
|
+
and is named `integrate-<e>-<p>` with `source` `integrate`. `at` is `<e>`.
|
|
524
|
+
- `published`: the plan's `final-review` link resolves to this dir (a link leaving the plan
|
|
525
|
+
publishes nothing).
|
|
526
|
+
- `state`: `done` with a verdict, `infra` with `qa.infra`, `running` when it is the newest
|
|
527
|
+
review and has `qa.input`, else `orphaned`.
|
|
528
|
+
- `spec`, `threshold`, `base`, `tip` come from `review.meta` (`threshold=none` is `null`).
|
|
529
|
+
- `execution` is its newest `attempts/qa-*`; `started`, `ended`, `duration_s` are that
|
|
530
|
+
execution's.
|
|
531
|
+
|
|
532
|
+
## Lazy details
|
|
533
|
+
|
|
534
|
+
TaskDetail:
|
|
535
|
+
|
|
536
|
+
```json
|
|
537
|
+
{"run": "3f9a0c1d2e4b", "id": "reader",
|
|
538
|
+
"texts": {"prompt": "...", "approach": null, "retry_findings": "...", "accepted_by": null,
|
|
539
|
+
"known_issues": null, "dwarf_input": "...", "qa_input": "..."},
|
|
540
|
+
"gate": {...},
|
|
541
|
+
"reviews": [{"kind": "primary", "execution": "qa-...", "attempt": 1, "spec": "opus",
|
|
542
|
+
"verdict": "FAIL", "state": "done", "findings": [Finding, ...]}],
|
|
543
|
+
"truncated": [], "warnings": []}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`texts` read `prompt.md`, `approach.md`, `retry_findings.md`, `accepted.by`,
|
|
547
|
+
`known_issues.md`, `dwarf.input` and `qa.input` (a solo run: `prompt.md`, `known_issues.md`,
|
|
548
|
+
`dwarf.input`, `qa.input`; the others are `null`). `reviews` lists each non-shadow `qa`
|
|
549
|
+
execution with the findings of its own `qa.last` copy (`null` until it is done); then the
|
|
550
|
+
task's current `qa.last` as a `primary` review with `execution` `null`, only when no
|
|
551
|
+
execution's copy is identical to it; then one `shadow` review per shadow dir.
|
|
552
|
+
|
|
553
|
+
ExecutionDetail: the Execution, plus `resolved` (every `key=value` of `<role>.resolved`, or
|
|
554
|
+
`null`), `dataset` (`{trigger, parent, attempt_start, resume}` or `null`), `prompt` (the
|
|
555
|
+
`<role>.prompt` copy; while running without one, the owner dir's `<role>.delivered`, then
|
|
556
|
+
`.prompt`, then `.input`), `last` (the `<role>.last` copy), `findings` (a list of Findings for
|
|
557
|
+
a finished `qa`, else `null`, in place of the summary), `owner`
|
|
558
|
+
(`{"kind": "task" | "shadow" | "final_review" | "run", "id": ...}`: the task id (`solo` for a
|
|
559
|
+
solo run), `<task>/<attempt>-<k>`, the review name, or `null`), `truncated` and `warnings`.
|
|
560
|
+
|
|
561
|
+
FinalReviewDetail: the FinalReviewSummary, plus `meta` (every `review.meta` key),
|
|
562
|
+
`tasks_summary`, `qa_input`, `qa_last`, `known_issues` (texts or `null`), `findings` (a list
|
|
563
|
+
of Findings, in place of the summary), `truncated` and `warnings`.
|
|
564
|
+
|
|
565
|
+
## DiffDetail (`forge_ui/diffs.py`)
|
|
566
|
+
|
|
567
|
+
```json
|
|
568
|
+
{"run": "3f9a0c1d2e4b",
|
|
569
|
+
"source": {"kind": "task", "id": "reader", "which": "current", "execution": null, "attempt": 2,
|
|
570
|
+
"file": "tasks/reader/changes.diff"},
|
|
571
|
+
"sources": [{"id": "current", "label": "changes.diff", "bytes": 1234, "url": "/api/runs/…/tasks/reader/diff"},
|
|
572
|
+
{"id": "prev", "label": "changes.prev.diff", "bytes": 980, "url": "…/tasks/reader/diff/prev"},
|
|
573
|
+
{"id": "qa-…", "label": "reviewed diff, attempt 1", "attempt": 1, "verdict": "FAIL",
|
|
574
|
+
"kind": "primary", "bytes": 980, "url": "/api/runs/…/executions/qa-…/diff"}],
|
|
575
|
+
"available": true, "reason": null, "bytes": 1234, "truncated": false,
|
|
576
|
+
"files": [{"old": "ui/reader.py", "new": "ui/reader.py", "status": "modified", "binary": false,
|
|
577
|
+
"additions": 5, "deletions": 1, "truncated": false,
|
|
578
|
+
"hunks": [{"old_start": 38, "old_lines": 7, "new_start": 38, "new_lines": 10,
|
|
579
|
+
"section": "def _tasks(reader):", "lines": [" ctx", "-old", "+new"]}]}],
|
|
580
|
+
"findings": [{"review": {"kind": "primary", "execution": "qa-…", "attempt": 2, "spec": "opus"},
|
|
581
|
+
"severity": "P1", "status": "CONFIRMED", "location": "ui/reader.py:42",
|
|
582
|
+
"summary": "…", "text": "…",
|
|
583
|
+
"pin": {"file": 0, "path": "ui/reader.py", "line": 42, "side": "new", "hunk": 0, "index": 5},
|
|
584
|
+
"off_diff": null}],
|
|
585
|
+
"warnings": [], "truncated_fields": []}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
| Field | Meaning |
|
|
589
|
+
|---|---|
|
|
590
|
+
| `source.kind` | `task`, `execution`, `final_review` or `fractal`; `id` the task id, execution name, review name or node |
|
|
591
|
+
| `source.which` | `current` `prev` `committed` `existing` (task), `reviewed` (execution, final review), `candidate` (fractal) |
|
|
592
|
+
| `source.execution`, `attempt` | the qa execution whose prompt held the diff (a final review: its newest), its attempt; a task's `current` and `committed` carry the task's attempt, `prev` and `existing` `null` |
|
|
593
|
+
| `source.file` | the file read, relative to the run dir (fractal: to the Fractal state's `runs/<run>/tasks/`); `null` when none was |
|
|
594
|
+
| `sources` | what else can be shown. Task (and its executions): every diff file present (`id` = `which`, `label` = file name, `bytes` its size), then every primary and shadow qa execution whose prompt has a fence (`id` the execution, `label` `reviewed diff, attempt N` / `shadow review, attempt N`, `attempt`, `verdict`, `kind` `primary`/`shadow`, `bytes` of the fenced diff). Final review: itself plus the newest 20 reviews that kept a `combined.diff` (`id` the name, `kind` `final`, `verdict`, `bytes` or `null`). Fractal: `[{"id": "candidate", "label": "candidate.diff", "bytes"}]`. Every entry but fractal's has `url`, the endpoint that shows it |
|
|
595
|
+
| `available`, `reason` | whether a diff was read; else why: `no changes.diff` (any missing file's name), `unreadable (see warnings)`, `no diff in the review prompt`, `no review prompt`, `no review of this attempt` (a dwarf), `not inlined (larger than 400 KB)` (final reviews), `no review prompt yet`, `no candidate diff yet` |
|
|
596
|
+
| `bytes` | the file's size, or the fenced diff's (UTF-8); `null` when unavailable |
|
|
597
|
+
| `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
|
+
| `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
|
+
| `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` |
|
|
601
|
+
| `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
|
+
| `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
|
+
|
|
604
|
+
Which diff: a task's `changes.diff` etc. from its dir (a solo run's from the run dir). An
|
|
605
|
+
execution's is the last fence of its `qa.prompt` copy; a running one's, of its owner's
|
|
606
|
+
`qa.delivered`, `.prompt`, then `.input`; a shadow's falls back to its dir's `qa.input`; a
|
|
607
|
+
dwarf's is its attempt's primary review's (same parent, else same attempt). A final review's
|
|
608
|
+
is `combined.diff`, else the fence of its newest execution's `qa.prompt`, then of its
|
|
609
|
+
`qa.input`.
|
|
610
|
+
|
|
611
|
+
Which findings: a task diff gets every review (primary, the task's own `qa.last` when no copy
|
|
612
|
+
matches it, shadows) of the task's latest attempt; an execution diff that review's (a
|
|
613
|
+
primary's also brings the shadows of its attempt); a final review's its `qa.last`'s; a fractal
|
|
614
|
+
node's none.
|
|
615
|
+
|
|
616
|
+
Parsing: `git diff --binary` output and plain unified diffs; renames, copies, mode changes,
|
|
617
|
+
`/dev/null`, C-quoted paths (`"a/caf\303\251"`), CRLF, the mnemonic and no-prefix forms.
|
|
618
|
+
A hunk's lines are counted, so a deleted `-- x` (the line `--- x`) stays content. At most 300
|
|
619
|
+
files, 20,000 hunk lines in all and 2,000 characters a line (then `…`).
|
|
620
|
+
|
|
621
|
+
Pinning: the path is the Finding's `location` (`a.py:10-12` arrives as `a.py:10`); without one,
|
|
622
|
+
the first `path:N` of its first line that names a file of this diff (`Makefile:3`). `line N`
|
|
623
|
+
alone pins only in a one-file diff. Paths are matched after `\` → `/` and dropping `./`, with
|
|
624
|
+
and without an `a/`/`b/` prefix: exactly first, then as a `/`-suffix either way; two files
|
|
625
|
+
matching is `ambiguous path`. The line is looked up on the new side (old for a deleted file).
|
|
626
|
+
|
|
627
|
+
Reads stay inside the run dir after symlinks are resolved, open regular files only (a FIFO is
|
|
628
|
+
never opened) and read at most 8 MiB; parses are kept by `(dev, inode, size, mtime_ns)` as
|
|
629
|
+
other parses are.
|
|
630
|
+
|
|
631
|
+
## Files
|
|
632
|
+
|
|
633
|
+
`GET /api/runs/<key>/files`: who planned, changed, drifted into or conflicted on each file.
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{"run": "3f9a0c1d2e4b",
|
|
637
|
+
"files": [{"path": "ui/reader.py", "dir": "ui", "planned_by": ["reader"], "touched_by": ["reader"],
|
|
638
|
+
"drift_by": [], "conflict_by": []}],
|
|
639
|
+
"planned_dirs": [{"path": "ui/", "planned_by": ["page"]}],
|
|
640
|
+
"tasks": [{"id": "reader", "wave": 1, "status": "MERGED", "planned": ["ui/reader.py"], "planned_all": false,
|
|
641
|
+
"attempts": [{"attempt": 1, "execution": "qa-…", "files": ["ui/reader.py"]}],
|
|
642
|
+
"current": ["ui/reader.py"], "drift": [], "conflicts": []}],
|
|
643
|
+
"overlaps": [{"a": "reader", "b": "docs", "planned": true, "touched": ["ui/reader.py"], "same_wave": false}],
|
|
644
|
+
"deferrals": ["…"], "truncated": false, "warnings": []}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
- `tasks[]`: `planned` is the `files` column, normalised (`ui/` → `ui`); `planned_all` when it
|
|
648
|
+
has `.`. `attempts`: each primary qa execution whose prompt has a fence, with the paths that
|
|
649
|
+
diff touched (from its headers: the new path, a deleted file's old one). `current`: those of
|
|
650
|
+
`changes.diff`. `drift`, `conflicts`: the lines of `drift.txt`, `conflict.files`. A solo
|
|
651
|
+
run has one task `solo` (wave 1, nothing planned) with also `existing`, the paths of
|
|
652
|
+
`existing.diff`.
|
|
653
|
+
- `files[]`, by path: every path touched (by `current` or any attempt), drifted, conflicted, or
|
|
654
|
+
planned as a file. `dir` is its dirname (`""` at the root). `planned_by`: tasks with a planned
|
|
655
|
+
entry equal to it or a dir above it (a `.` task is left out: see `planned_all`);
|
|
656
|
+
`touched_by`, `drift_by`, `conflict_by` in `tasks.tsv` order.
|
|
657
|
+
- `planned_dirs`: planned entries that end in `/` or hold a touched file, with a trailing `/`.
|
|
658
|
+
- `overlaps`: each pair of tasks (in `tasks.tsv` order) whose planned entries overlap by
|
|
659
|
+
forge-schedule's rule (equal, one a dir above the other, or either `.`) or that touched a
|
|
660
|
+
file in common (`touched`, sorted); `same_wave` from `waves.tsv`.
|
|
661
|
+
- At most 2,000 files, 300 tasks and 5,000 overlaps; past them `truncated` is true.
|
|
662
|
+
|
|
663
|
+
## Models
|
|
664
|
+
|
|
665
|
+
`GET /api/models`: facts about every run, newest `updated` first, for the page to aggregate.
|
|
666
|
+
|
|
667
|
+
```json
|
|
668
|
+
{"generated": 1760621412.5, "runs": 12, "truncated": false,
|
|
669
|
+
"tasks": [{"run": "3f9a0c1d2e4b", "task": "reader", "kind": "plan", "repo": "/x/y", "difficulty": "high",
|
|
670
|
+
"created": 1760619000.0, "explored": true, "propensity": 0.125, "final_status": "MERGED",
|
|
671
|
+
"accepted": true,
|
|
672
|
+
"attempts": [{"n": 1, "alias": "luna", "spec": "luna", "source": "execution", "outcome": "fail",
|
|
673
|
+
"reason": null, "gate": false, "model_s": 297.0, "duration_s": 300.0,
|
|
674
|
+
"tokens": {"input": 120000, "cached": 90000, "output": 8000}, "timed_out": false}],
|
|
675
|
+
"escalations": [{"n": 1, "from": "luna", "to": "sol:xhigh"}]}],
|
|
676
|
+
"reviews": [{"run": "3f9a0c1d2e4b", "task": "reader", "n": 1, "kind": "primary", "alias": "opus", "spec": "opus",
|
|
677
|
+
"verdict": "FAIL", "findings": FindingsSummary, "duration_s": 270.0, "tokens": {...},
|
|
678
|
+
"agreement": null, "primary_alias": null}],
|
|
679
|
+
"warnings": ["3f9a0c1d2e4b: tasks/x/attempts.tsv line 2: invalid attempt"]}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
- `tasks[]`: one per task (`solo` for a solo run). `explored` as Task's; `propensity` that of
|
|
683
|
+
the newest `route.jsonl` decision, else `null`; `final_status` the task (or solo) status;
|
|
684
|
+
`accepted` whether `accepted.by` is non-empty (`false` for a solo run). `escalations` is
|
|
685
|
+
`escalations.tsv` as it is (`n` its attempt column, `from`/`to` specs).
|
|
686
|
+
- `attempts[]`, from `attempts.tsv` (`epoch n status infra spec`): a row with an infra class
|
|
687
|
+
or status `INFRA` was refunded and is dropped; the last row of a number wins (a QA-only
|
|
688
|
+
resume reuses it). `outcome`: `PASS`/`MERGED` `pass`, `FAIL` `fail`. `TIMEOUT`/`ERROR` with a
|
|
689
|
+
primary qa execution of that attempt is `censored` with `reason` `reviewer_timeout`/
|
|
690
|
+
`reviewer_error`; else `TIMEOUT` is a `fail` when the attempt's last dwarf execution exited 7
|
|
691
|
+
or timed out, otherwise `censored`, `dwarf_*` (it exited non-zero) or `forge_*`. Any other
|
|
692
|
+
status is `censored` with the status lowercased (`nochanges`, `interrupted`, ...), as
|
|
693
|
+
`scripts/forge_dataset/export.py` labels them.
|
|
694
|
+
- `alias`/`spec`/`source`: the last dwarf execution's model (`execution`), else the fifth
|
|
695
|
+
`attempts.tsv` column (`attempts.tsv`), else for attempt 1 only the `tasks.tsv` dwarf (a solo
|
|
696
|
+
run's `dwarf.spec`/`dwarf.resolved`) (`plan`), else all `null`.
|
|
697
|
+
- `gate`: the attempt passed although its (last) primary review said FAIL. `model_s`,
|
|
698
|
+
`duration_s`, `tokens`, `timed_out`: that dwarf execution's `phases.model`, `duration_s`,
|
|
699
|
+
`tokens`, `timed_out`, else `null`/`false`.
|
|
700
|
+
- `reviews[]`: every finished primary qa execution (`n` its attempt); every shadow whose
|
|
701
|
+
`shadow.status` is `done` (`alias`/`spec` its execution's model, else `shadow.json`'s spec;
|
|
702
|
+
`agreement` and `primary_alias` against the primary review of its attempt); every finished
|
|
703
|
+
final review (`kind` `final`, `task` and `n` `null`, `verdict` `PASS`/`FAIL` else `null`,
|
|
704
|
+
timing from its newest execution).
|
|
705
|
+
- Per run the facts are kept, keyed by its realpath, until its RunSummary's `updated`,
|
|
706
|
+
`task_count`, `counts` or `final_review` changes (4,096 runs); a run with a `RUNNING` task is
|
|
707
|
+
read again after 120 s. One computation runs at a time. Past 50,000 task rows the next run
|
|
708
|
+
is left out and `truncated` is true. `warnings` (first 500) are prefixed by the run key.
|
|
709
|
+
|
|
710
|
+
## Fractal (`forge_ui/fractal.py`)
|
|
711
|
+
|
|
712
|
+
A run that chose Fractal (`fractal-selection.json`) has its tasks executed by
|
|
713
|
+
`scripts/forge_fractal` as trees of child tasks, in a managed run under the state root:
|
|
714
|
+
`realpath($XDG_STATE_HOME or ~/.local/state)/forge/fractal` (unlike forge, an empty
|
|
715
|
+
`XDG_STATE_HOME` counts as unset). forge-ui never imports that package.
|
|
716
|
+
|
|
717
|
+
| Path under `runs/<run>/` | Read |
|
|
718
|
+
|---|---|
|
|
719
|
+
| `run.json` (≤ 1 MiB) | its `runner` must be the run dir's realpath |
|
|
720
|
+
| `tasks/task-<sha256(T)[:16]>/` | the Fractal task of forge task `T` = realpath of `tasks/<id>` (plan) or of the run dir (solo) |
|
|
721
|
+
| `…/request.json` (≤ 2 MiB) | its `output` must be `T` |
|
|
722
|
+
| `…/state.json`, `outcome.json` (≤ 256 KiB) | `state`, `outcome` |
|
|
723
|
+
| `…/events.jsonl` | its last 64 KiB |
|
|
724
|
+
| `…/nodes/<n>/node.json` (≤ 4 MiB) | one node; a bigger one is skipped with a warning |
|
|
725
|
+
| `…/nodes/<n>/candidate.diff` | the node's imported change |
|
|
726
|
+
|
|
727
|
+
Everything is opened through directory fds, one component at a time with `O_NOFOLLOW`
|
|
728
|
+
(files also `O_NONBLOCK`, regular only, bounded): a symlink anywhere is refused with a
|
|
729
|
+
warning. `nodes/` is the only dir listed, and only names matching
|
|
730
|
+
`[A-Za-z0-9][A-Za-z0-9_-]{0,100}`. `control/`, `nodes/*/product/`, `planning/`, `steps/` and
|
|
731
|
+
the ledger are never opened, nor any absolute path a node stores. Parses are kept by
|
|
732
|
+
signature and path.
|
|
733
|
+
|
|
734
|
+
RunDetail `fractal`: `{"enabled", "run", "available", "reason"}`. `enabled` is true only for
|
|
735
|
+
`"enabled": true`; `run` must match `run-[0-9a-f]{20}`. Reasons: `Fractal is off for this run`,
|
|
736
|
+
`invalid fractal-selection.json` (not a ≤ 64 KiB JSON object), `invalid run id`,
|
|
737
|
+
`managed run not found`, `invalid run.json`, `runner mismatch`, `not supported on this
|
|
738
|
+
platform`.
|
|
739
|
+
|
|
740
|
+
TaskFractal:
|
|
741
|
+
|
|
742
|
+
```json
|
|
743
|
+
{"run": "3f9a0c1d2e4b", "task": "reader", "fractal_run": "run-…", "fractal_task": "task-…",
|
|
744
|
+
"available": true, "reason": null,
|
|
745
|
+
"state": {"status": "active", "acceptance": "pending", "elapsed": 120.5, "started_at": 1760600020.0,
|
|
746
|
+
"ended_at": null, "exit_code": null, "error": null, "qa_status": null},
|
|
747
|
+
"outcome": {"acceptance": "PASS", "verification": "passed", "task_status": "PASS"},
|
|
748
|
+
"nodes": [{"id": "implementation", "parent": null, "depth": 0, "goal": "…", "difficulty": "medium",
|
|
749
|
+
"paths": ["ui"], "deps": [], "model": "sol:xhigh", "resolved_model": "…", "status": "waiting",
|
|
750
|
+
"ui_status": "RUNNING", "phase": "implementation", "iteration": 1, "started_at": 1760600022.0,
|
|
751
|
+
"ended_at": null, "cost": 0.42, "unknown_cost_steps": 0,
|
|
752
|
+
"tokens": {"input_tokens": 1, "cached_input_tokens": 0, "output_tokens": 2},
|
|
753
|
+
"decomposition": {"phase": "accepted", "decision": "split", "reason": "…", "children": ["a", "b"]},
|
|
754
|
+
"error": null, "has_diff": false, "reserved": false}],
|
|
755
|
+
"events": [{"at": 1760600021.0, "kind": "node_created", "node": "implementation"}],
|
|
756
|
+
"warnings": [], "truncated": []}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
- `available: false` (`state`, `outcome` `null`, `nodes`, `events` `[]`) with the RunDetail
|
|
760
|
+
reasons, or `Fractal is not selected for this run`, `no Fractal task for this task yet`,
|
|
761
|
+
`request does not match this task`, `invalid request.json`.
|
|
762
|
+
- `nodes`: tree order (a parent, then its children by id), at most 500 node dirs (else
|
|
763
|
+
`truncated` has `nodes`). Children a decomposition admitted (`decomposition.admission` and
|
|
764
|
+
`decomposition_history[].admission`) that have no dir yet are listed with `status`
|
|
765
|
+
`reserved` and `reserved: true`. `depth` counts parents. `goal` and `error` keep 240
|
|
766
|
+
characters, a decision's `reason` 2,000. Values of the wrong type are `null` (lists `[]`).
|
|
767
|
+
`has_diff`: `candidate.diff` is a regular file.
|
|
768
|
+
- `ui_status`: `pending` `PENDING`, `active`/`waiting` `RUNNING`, `paused` `BLOCKED`,
|
|
769
|
+
`completed` `MERGED`, `timeout` `TIMEOUT`, `interrupted`/`stopped` `INTERRUPTED`, `reserved`
|
|
770
|
+
`PENDING`, any other status `FAIL`, none `UNKNOWN`.
|
|
771
|
+
- `events`: the newest 50 JSON lines of `events.jsonl` with a `kind`, oldest first: `at`,
|
|
772
|
+
`kind`, `node`, and those of `phase decision status acceptance exit_code iteration cost
|
|
773
|
+
difficulty model reason` the line has. Other lines are counted in a warning.
|
|
774
|
+
|
|
775
|
+
FractalNodeDetail (`…/fractal/nodes/<node>`): the node as listed, plus `goal`, `error` (20,000
|
|
776
|
+
characters), `last` and `planning_last` (their last 20,000, behind `…`), `planning_tokens`,
|
|
777
|
+
`decomposition` in full (`phase decision reason attempts error`, and `admission`: `name goal
|
|
778
|
+
difficulty paths deps model` each), `decomposition_history` (its newest 20, each like
|
|
779
|
+
`decomposition`, `reason` and `error` 2,000), and `run`, `task`, `fractal_run`,
|
|
780
|
+
`fractal_task`, `warnings`, `truncated` (the texts cut). A reserved node has these `null` or
|
|
781
|
+
`[]`, with its admission's `goal`.
|
|
782
|
+
|
|
783
|
+
## Export (`forge_ui/export.py`)
|
|
784
|
+
|
|
785
|
+
`GET /api/runs/<key>/export` answers one self-contained HTML file named
|
|
786
|
+
`forge-run-<run_id>.html` (the run id with anything but `[A-Za-z0-9._-]` made `-`).
|
|
787
|
+
|
|
788
|
+
- It holds `style.css` and the classic scripts the run view uses (`runmodel.js cloudmodel.js
|
|
789
|
+
graph.js gantt.js panel.js diffview.js files.js map3d.js search.js app.js`), inline; the import
|
|
790
|
+
map; a `<script type="application/json" id="forge-snapshot">` with `{meta, api}`; and a
|
|
791
|
+
`<script type="text/plain" id="forge-stage">` holding `iso.js`, `cloud3d.js` and `scene3d.js`
|
|
792
|
+
bundled into one module (each module in its own scope, relative imports bound to the earlier
|
|
793
|
+
ones' exports, `createStage` exported), which `map3d.js` imports from a `blob:` URL.
|
|
794
|
+
- `api` maps each path the page requests to that endpoint's response as the server would answer
|
|
795
|
+
it (server.api_get): `/api/runs` (this run only), the RunDetail, every TaskDetail, every
|
|
796
|
+
ExecutionDetail the RunDetail names, every FinalReviewDetail, Files, and the Fractal trees and
|
|
797
|
+
nodes. With `texts=1` also every diff the page can open (each task's diff and its `sources`,
|
|
798
|
+
each final review's, each Fractal node's).
|
|
799
|
+
- `texts=0` (the default) nulls these keys wherever they hold a string, list or object: `prompt
|
|
800
|
+
last request log_tail dwarf_tail qa_tail qa_last qa_input tasks_summary known_issues texts text
|
|
801
|
+
planning_last approach retry_findings dwarf_input error`, and stores no diff. A finding keeps its
|
|
802
|
+
`summary`.
|
|
803
|
+
- `paths=0` (the default) makes every string that is a whole absolute path (but `/api/…` and
|
|
804
|
+
`url`s) its last component, and replaces the run dir, the repo and the home dir inside other
|
|
805
|
+
strings with their last component (`~` for home).
|
|
806
|
+
- At most 1 MB of JSON per diff and 25 MB in all; what did not fit is listed in `meta.omitted`
|
|
807
|
+
(`path`, `bytes`, `why`). `meta`: `format` `forge-ui-export`, `version` 1, `app`, `key`,
|
|
808
|
+
`generated`, `texts`, `paths`, `omitted`.
|
|
809
|
+
- The page's `<meta http-equiv="Content-Security-Policy">`: `default-src 'none'; script-src
|
|
810
|
+
<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';
|
|
812
|
+
form-action 'none'; object-src 'none'`. Inside a script, `</script` and `<!--` are escaped; the
|
|
813
|
+
JSON escapes `<`, `>` and `&`.
|
|
814
|
+
- In the page, `app.js` reads the snapshot instead of the network, never polls, shows only the
|
|
815
|
+
exported run (any other route goes to it) and a banner with the export's date and what it left
|
|
816
|
+
out; the cloud, the leaderboard and notifications are not there.
|
|
817
|
+
|
|
818
|
+
## Status vocabulary for the page and the graph
|
|
819
|
+
|
|
820
|
+
Both the page and the graph mark a task by its status with the CSS class
|
|
821
|
+
`st-<status in lowercase>`, e.g. `st-merged`, `st-running`, `st-pending`. `style.css`
|
|
822
|
+
(owned by the page) defines, for every status above plus `pending`, a custom property
|
|
823
|
+
`--st` holding that status's color, e.g. `.st-merged { --st: #2f9e44; }`. No script
|
|
824
|
+
hard-codes status colors: `graph.js` and `gantt.js` give their SVG elements classes that
|
|
825
|
+
`style.css` fills with `var(--st, #888)` (the page CSP refuses style attributes, so a script
|
|
826
|
+
never writes one), and the 3D stage reads `--st` from a hidden `.st-<status>` probe. The
|
|
827
|
+
stage's own background and accent are `--stage-bg` and `--stage-accent` on `.stage-slot`.
|
|
828
|
+
|
|
829
|
+
Suggested meaning: merged and pass green, running blue, pending grey, fail, error and conflict
|
|
830
|
+
red, infra and timeout orange, blocked, interrupted, invalidated, unknown and nochanges muted
|
|
831
|
+
purple.
|
|
832
|
+
|
|
833
|
+
## The graph (`forge_ui/static/graph.js`)
|
|
834
|
+
|
|
835
|
+
A plain script (no modules) that defines one global:
|
|
836
|
+
|
|
837
|
+
```js
|
|
838
|
+
window.ForgeGraph = {
|
|
839
|
+
render(container, run, options) // run: a RunDetail; options: {selected, onSelect}
|
|
840
|
+
};
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
`render` replaces the contents of `container` with one SVG showing every task of
|
|
844
|
+
`run.tasks` as a node and every dependency as an edge from the dep to the dependent.
|
|
845
|
+
Columns are waves (`task.wave`); a task with no wave is placed one column right of its
|
|
846
|
+
deepest dependency (column 1 when it has none). Each node shows the task id and its
|
|
847
|
+
`dwarf` spec; a node with `explored: true` is visibly marked (e.g. a dashed outline and a
|
|
848
|
+
`*` after the spec). `options.selected` is a task id drawn highlighted.
|
|
849
|
+
Clicking a node calls `options.onSelect(taskId)`. It must work with 0 tasks (renders an
|
|
850
|
+
empty-state message), with deps that name unknown tasks (edge skipped), and with cycles
|
|
851
|
+
(no infinite loop).
|
|
852
|
+
|
|
853
|
+
## The page's other scripts
|
|
854
|
+
|
|
855
|
+
Plain scripts unless noted; each defines one global and none parses API text as HTML.
|
|
856
|
+
|
|
857
|
+
| File | Global | What it is |
|
|
858
|
+
|---|---|---|
|
|
859
|
+
| `runmodel.js` | `ForgeRunModel` | pure data, no DOM: the plate layout (archivision's `plate()`), the atlas, plan and execution models, forge-schedule's rules projected forward, the critical path, concurrency, the state of every node at a time `t` (replay), filters. Also a CommonJS module, for `node --test` |
|
|
860
|
+
| `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
|
+
| `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
|
+
| `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}` |
|
|
863
|
+
| `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
|
+
| `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
|
+
| `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 |
|
|
866
|
+
| `modelstats.js` | `ForgeModelStats` | pure: the leaderboard's aggregation of `/api/models` (`leaderboard`, `wilson`, `options`). Also a CommonJS module |
|
|
867
|
+
| `leaderboard.js` | `ForgeLeaderboard` | `render(host, data, {runs, prefs, savePrefs, modelHref, now})`: the `#/models` page |
|
|
868
|
+
| `search.js` | `ForgeSearch` | `init({runs, commands, go, cloudFilter})`: the quick jump (Ctrl/⌘ K, `/`), ranked by `ForgeRunModel.searchRank` |
|
|
869
|
+
| `notify.js` | `ForgeNotify` | `init({go})`, `observe(runs)` on each poll of the run list (`ForgeRunModel.statusEvents`), `open()`, `background()`: the bell, its log, the title count, the favicon dot and browser notifications |
|
|
870
|
+
| `map3d.js` | `ForgeMap3D` | `load()` imports `scene3d.js` (and so three.js) on first use, with a 12 s timeout, or in an export the bundled stage from a `blob:` URL; `state` is `idle`, `loading`, `ready` or `unavailable`, `message()` says why |
|
|
871
|
+
| `scene3d.js` | (ES module) | `createStage(host, {onSelect, onOpenRun, onBack, onMode, cloud})`: the atlas, plan and execution scenes, live status updates without rebuilding, overlays, replay, Fractal trees over opened tasks (`setFractal`), and `showCloud(runs, opts)` |
|
|
872
|
+
| `cloud3d.js` | (ES module) | `createCloud(engine, host, callbacks)`: the run cloud on the stage's engine (blocks or orbs, labels, frames, drag and drop kept per layout in this browser) |
|
|
873
|
+
| `iso.js` | (ES module) | the iso-glow engine ported from archivision: renderer, materials, blocks, labels, cables, camera (plate and free profiles), picking and grabbing, shared solids, sprites, drawn on demand |
|
|
874
|
+
|
|
875
|
+
Routes (the URL hash): `#/` the map of every run; `#/cloud`, `#/cloud/runs/<key>` (a run
|
|
876
|
+
selected), `#/cloud/projects/<id>` (a project in focus), `#/cloud/models/<alias>` (the cloud
|
|
877
|
+
filtered to that model); `#/models` the leaderboard; `#/runs/<key>`; `…/tasks/<id>`;
|
|
878
|
+
`…/tasks/<id>/executions/<name>`; `…/executions/<name>` (an execution that belongs to no
|
|
879
|
+
task); `…/final-review[/<name>]`; `…/verification`; `…/integration`; `…/tasks/<id>/fractal/<node>`;
|
|
880
|
+
and the diff viewer: `…/tasks/<id>/diff[/<source>][/file/<path>]` (`source` is `current`, `prev`,
|
|
881
|
+
`committed`, `existing` or an execution), `…/final-review/<name>/diff[/file/<path>]`,
|
|
882
|
+
`…/tasks/<id>/fractal/<node>/diff[/file/<path>]` (`path` percent-encoded whole).
|
|
883
|
+
|
|
884
|
+
Kept in the browser (localStorage, every access guarded): `forge-ui-view`, `-mode`, `-overlay`,
|
|
885
|
+
`-speed`, `-map-filter`; `forge-ui-cloud` (filter, layout, shape, panel) and
|
|
886
|
+
`forge-ui-cloud-layout-v1` (dragged positions per layout, at most 5,000 per layout);
|
|
887
|
+
`forge-ui-models` (leaderboard filters); `forge-ui-recent` (quick-jump picks); `forge-ui-notify`,
|
|
888
|
+
`-notify-log` (the last 100) and `-notify-seen` (ten minutes, to notify once across tabs).
|