@giamme/forge 0.11.0 → 0.12.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 +47 -0
- package/SKILL.md +4 -2
- package/VERSION +1 -1
- package/docs/artifacts.md +46 -12
- package/docs/commands.md +48 -9
- package/docs/decomposed-runs.md +43 -2
- package/docs/forge-ui.md +27 -6
- package/docs/reviews.md +31 -6
- package/package.json +1 -1
- package/references/dataset.md +3 -1
- package/references/decompose.md +159 -17
- package/references/harnesses.md +13 -1
- package/references/ui.md +14 -4
- package/scripts/forge-control.py +268 -25
- package/scripts/forge-dispatch.sh +80 -3
- package/scripts/forge-guard.py +156 -15
- package/scripts/forge-issues.py +1 -1
- package/scripts/forge-lib-plans.sh +9 -3
- package/scripts/forge-lib-review.sh +22 -3
- package/scripts/forge-lib-task.sh +51 -4
- package/scripts/forge-lib-verify.sh +137 -6
- package/scripts/forge-parallel-options.sh +59 -0
- package/scripts/forge-parallel.sh +212 -28
- package/scripts/forge-ripwire.py +1 -1
- package/scripts/forge-route.py +157 -5
- package/scripts/forge-schedule.py +687 -50
- package/scripts/forge-ui.py +8 -4
- package/scripts/forge-verify.py +878 -0
- package/scripts/forge_dataset/forecast.py +29 -4
- package/scripts/forge_dataset/harvest.py +10 -4
- package/scripts/forge_verify_reports.py +225 -0
- package/ui/README.md +19 -4
- package/ui/docs/api.md +139 -25
- package/ui/forge_ui/control.py +26 -7
- package/ui/forge_ui/export.py +20 -7
- package/ui/forge_ui/reader.py +307 -11
- package/ui/forge_ui/server.py +16 -0
- package/ui/forge_ui/static/app.js +2 -1
- package/ui/forge_ui/static/controls.js +217 -33
- package/ui/forge_ui/static/findingsview.js +37 -12
- package/ui/forge_ui/static/gantt.js +14 -9
- package/ui/forge_ui/static/index.html +1 -0
- package/ui/forge_ui/static/panel.js +2 -1
- package/ui/forge_ui/static/runmodel.js +92 -11
- package/ui/forge_ui/static/scene3d.js +9 -5
- package/ui/forge_ui/static/settings.js +48 -12
- package/ui/forge_ui/static/style.css +60 -0
- package/ui/forge_ui/static/verifyview.js +324 -0
package/ui/docs/api.md
CHANGED
|
@@ -43,7 +43,9 @@ written), and the reader must never raise because of one.
|
|
|
43
43
|
| `shadow_qa` | a comma list of specs |
|
|
44
44
|
| `timeout` | seconds |
|
|
45
45
|
| `verification.status` | `PASS`, `FAIL` or another word; `verification.command` the command; `verification.log` its output |
|
|
46
|
-
| `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` |
|
|
46
|
+
| `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`. A lanes verification adds `<TAB>lane<TAB>started` (an epoch) |
|
|
47
|
+
| `verification.retry.log` | the output of a deterministic or Jev rerun of a single command |
|
|
48
|
+
| `verification.lanes.json` | a lanes verification's live and final state, written by `scripts/forge-verify.py` (read up to 4 MB); `verification.lane.<name>.log` and `verification.lane.<name>.run-<k>.log` each lane's output |
|
|
47
49
|
| `verification.commit` | one line, the commit verified |
|
|
48
50
|
| `max_parallel`, `escalate_after`, `verify_retries` | one integer each |
|
|
49
51
|
| `qa_threshold` | `P0`..`P3`, the least severe finding that still blocks; `final_qa` the final review's QA spec |
|
|
@@ -167,6 +169,7 @@ def execution_transcript(path, name, source='plan') -> dict | None # an Executio
|
|
|
167
169
|
def fractal_detail(path, task_id, source='plan') -> dict | None # a TaskFractal
|
|
168
170
|
def fractal_node(path, task_id, node, source='plan') -> dict | None # a FractalNodeDetail
|
|
169
171
|
def fractal_node_diff(path, task_id, node, source='plan') -> dict | None # a DiffDetail
|
|
172
|
+
def verification_log(path, lane=None, k=None, first=None, retry=False, source='plan') -> dict | None # a VerificationLog
|
|
170
173
|
|
|
171
174
|
class Catalog:
|
|
172
175
|
def __init__(self, plans=(), roots=(), registry=True, registry_dir=None): ...
|
|
@@ -178,7 +181,8 @@ class Catalog:
|
|
|
178
181
|
def diff(self, key, task_id, which='current'), execution_diff(self, key, name),
|
|
179
182
|
final_review_diff(self, key, name), files(self, key), findings(self, key), transcript(self, key, name),
|
|
180
183
|
fractal(self, key, task_id),
|
|
181
|
-
fractal_node(self, key, task_id, node), fractal_node_diff(self, key, task_id, node)
|
|
184
|
+
fractal_node(self, key, task_id, node), fractal_node_diff(self, key, task_id, node),
|
|
185
|
+
verification_log(self, key, lane=None, k=None, first=None) -> dict | None
|
|
182
186
|
def models(self) -> dict # Models, over every run
|
|
183
187
|
def locate(self, key) -> tuple[str, str] | None # (run dir, 'plan' | 'solo'); None for an unknown key
|
|
184
188
|
```
|
|
@@ -225,6 +229,10 @@ its last 20,000 behind a leading `…`. Their `truncated` lists the field names
|
|
|
225
229
|
| `GET /api/runs/<key>/files` | `200 Files` |
|
|
226
230
|
| `GET /api/runs/<key>/findings` | `200 RunFindings` |
|
|
227
231
|
| `GET /api/runs/<key>/executions/<qa-or-dwarf>/transcript` | `200 ExecutionTranscript` |
|
|
232
|
+
| `GET /api/runs/<key>/verification/log[/lines/<n>]` | `200 VerificationLog` of `verification.log`: its tail, or with `/lines/<n>` a range |
|
|
233
|
+
| `GET /api/runs/<key>/verification/retry/log[/lines/<n>]` | `200 VerificationLog` of `verification.retry.log`, a single command's rerun |
|
|
234
|
+
| `GET /api/runs/<key>/verification/lanes/<lane>/log[/lines/<n>]` | `200 VerificationLog` of `verification.lane.<lane>.log` |
|
|
235
|
+
| `GET /api/runs/<key>/verification/lanes/<lane>/runs/<k>/log[/lines/<n>]` | `200 VerificationLog` of `verification.lane.<lane>.run-<k>.log` |
|
|
228
236
|
| `GET /api/models` | `200 Models` |
|
|
229
237
|
| `GET /api/health` | `200 {"app": "forge-ui", "version", "build", "pid", "controls": true\|false}`; `controls` is `false` in read-only mode |
|
|
230
238
|
| `GET /api/runs/<key>/controls` | `200` the `forge control show --json` object of a plan run (see Controls); `404` for a solo or unknown run |
|
|
@@ -251,11 +259,14 @@ Fractal node endpoints for an unknown node or a task whose Fractal state is not
|
|
|
251
259
|
diff that is not there yet is a `200` with `available: false`. An execution is looked for only where executions are written:
|
|
252
260
|
`attempts/`, `tasks/<id>/attempts/`, `shadow/<id>/<a>-<k>/attempts/`,
|
|
253
261
|
`final-review-*/attempts/` and `integrate-*/final-review/attempts/`, in that order; a dir
|
|
254
|
-
that resolves outside the run dir (a symlinked `attempts/`, say) is ignored.
|
|
262
|
+
that resolves outside the run dir (a symlinked `attempts/`, say) is ignored. The verification
|
|
263
|
+
log endpoints answer `404` for a solo run, a `<lane>` not matching `[A-Za-z0-9][A-Za-z0-9_-]{0,63}`
|
|
264
|
+
(so no `..`), a `<k>` or `<n>` that is not a positive decimal without a leading zero, and a log
|
|
265
|
+
that is missing, a symlink or not a regular file.
|
|
255
266
|
|
|
256
267
|
JSON responses: `Content-Type: application/json; charset=utf-8`,
|
|
257
268
|
`Cache-Control: no-store`. The page loads `style.css`, `runmodel.js`, `graph.js`, `gantt.js`,
|
|
258
|
-
`panel.js`, `map3d.js` and `app.js` by those relative names, and `scene3d.js` and `iso.js`
|
|
269
|
+
`verifyview.js`, `panel.js`, `map3d.js` and `app.js` by those relative names, and `scene3d.js` and `iso.js`
|
|
259
270
|
(ES modules) only when the 3D view is used.
|
|
260
271
|
|
|
261
272
|
Every response carries `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`
|
|
@@ -342,8 +353,14 @@ Every RunSummary field, plus:
|
|
|
342
353
|
"final_qa": "opus"},
|
|
343
354
|
"verification": {"status": "PASS", "command": "python3 -m unittest", "log_tail": "...",
|
|
344
355
|
"runs": [{"n": 1, "kind": "initial", "exit": 0, "tree_changed": false,
|
|
345
|
-
"duration_s": 12.3, "log": "verification.run-1.log"
|
|
346
|
-
|
|
356
|
+
"duration_s": 12.3, "log": "verification.lane.unit.run-1.log",
|
|
357
|
+
"lane": "unit", "started": 1760622000.5}],
|
|
358
|
+
"commit": "9c1e4b7a...",
|
|
359
|
+
"state": "done", "mode": "lanes", "live": null, "mutated": false,
|
|
360
|
+
"lanes": [VerificationLane, ...],
|
|
361
|
+
"failing": [{"lane": "e2e", "name": "checkout.Pay.test_refund", "suite": "checkout",
|
|
362
|
+
"file": "e2e.xml", "message": "expected 200, got 500", "excerpt": "...",
|
|
363
|
+
"known": false, "log_line": 12}]},
|
|
347
364
|
"waves": [["reader", "page", "graph"], ["server"], ["readme"]],
|
|
348
365
|
"tasks": [Task, ...],
|
|
349
366
|
"timeline": {"started": 1760620000.25, "ended": null, "runs": [Execution], "verifications": [Execution]},
|
|
@@ -369,7 +386,53 @@ Every RunSummary field, plus:
|
|
|
369
386
|
- `verification` is `null` when none of `verification.status`, `.command`, `.log` exists. A
|
|
370
387
|
solo run's `verification` is always `null`. `runs` comes from `verification.runs.tsv`
|
|
371
388
|
(malformed rows are warned and skipped; an empty `exit` is `null`), `commit` is the first
|
|
372
|
-
line of `verification.commit` or `null`.
|
|
389
|
+
line of `verification.commit` or `null`. A run's lanes runs carry `lane` and `started`; a
|
|
390
|
+
single command's have `null` for both. A row of neither 6 nor 8 columns is warned and skipped.
|
|
391
|
+
- `verification.state` is `running` when the run's newest `verification` execution (the newest
|
|
392
|
+
of `timeline.verifications`) is running, `done` when it has ended, and `null` when the run has
|
|
393
|
+
none. While it runs, `status` is `RUNNING`, whatever a `verification.status` left by an earlier
|
|
394
|
+
verification says.
|
|
395
|
+
- `verification.mode` is `lanes` when `verification.lanes.json` exists and was written at or
|
|
396
|
+
after the newest verification execution started (or there is none), else `single`. A lanes file
|
|
397
|
+
an earlier verification left is not read.
|
|
398
|
+
- `verification.live` is `null` unless `state` is `running`; then `{started, step, last_line,
|
|
399
|
+
last_output_at, idle_s, log}`. `started` is the execution's start. In `single` mode the live log is
|
|
400
|
+
whichever of `verification.log` and `verification.retry.log` was modified last: `step` is
|
|
401
|
+
`{name, started: null}` for the last `::forge step name=<x> state=start` in its last 64 KB
|
|
402
|
+
that no later `state=end` closed (a line holds no time), `last_line` its last non-empty line
|
|
403
|
+
without ANSI codes (at most 300 characters), and `last_output_at` its mtime. In `lanes` mode
|
|
404
|
+
they are the running lane's that printed last, with its recorded `step.started`. `idle_s` is
|
|
405
|
+
now minus `last_output_at` (minus `started` before any output). `log` names the live log in
|
|
406
|
+
`single` mode (`verification.log` or `verification.retry.log`; `null` before either exists) and is
|
|
407
|
+
`null` in `lanes` mode.
|
|
408
|
+
- `verification.lanes` is `null` in `single` mode and when the lanes file is unreadable, not a
|
|
409
|
+
JSON object, has no `lanes` list or is over 4 MB (each warned; the request never fails). In
|
|
410
|
+
`lanes` mode it is every lane:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
VerificationLane = {"name": "e2e", "command": "npm run test:e2e", "group": "browser", "first": false,
|
|
414
|
+
"retries": 2, "options": {"retries": 2, "reports": "reports"},
|
|
415
|
+
"state": "fail", "exit": 1, "started": 1760622000.5, "ended": 1760622070.0,
|
|
416
|
+
"duration_s": 69.5, "idle_s": null,
|
|
417
|
+
"runs": [{"k": 1, "kind": "initial", "started": 1760622000.5, "ended": 1760622030.0,
|
|
418
|
+
"exit": 1, "log": "verification.lane.e2e.run-1.log"}],
|
|
419
|
+
"step": {"name": "browser", "started": 1760622010.0} | null,
|
|
420
|
+
"last_line": "2 failed, 30 passed", "last_output_at": 1760622069.8,
|
|
421
|
+
"log": "verification.lane.e2e.log",
|
|
422
|
+
"tests": {"pass": 30, "fail": 2, "skip": 1, "error": 0, "failing": [...],
|
|
423
|
+
"reports": ["e2e.xml"], "notes": [], "truncated": false} | null}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`state` is one of `waiting running pass fail flaky known skipped interrupted`; `duration_s`
|
|
427
|
+
runs from `started` to `ended`, or to now while the lane runs; `idle_s` is now minus
|
|
428
|
+
`last_output_at` for a running lane, else `null`. `tests` is `null` when the lane wrote no
|
|
429
|
+
report; its `failing` holds at most 200 entries (`truncated` says more failed), a `message` at
|
|
430
|
+
most 500 characters and an `excerpt` at most 2,000; `log_line` is the 1-based line of the
|
|
431
|
+
lane's log that first names the test, or `null`.
|
|
432
|
+
- `verification.failing` is every lane's `tests.failing`, in lane order, each with its `lane`:
|
|
433
|
+
at most 200 entries, `[]` in `single` mode.
|
|
434
|
+
- `verification.mutated` is `true` when the checkout changed under the verification (the lanes
|
|
435
|
+
file's `mutated`, or in `single` mode a run with `tree_changed`): reruns after it were skipped.
|
|
373
436
|
- `timeline.started` is the earliest `started` of every execution the detail shows (tasks
|
|
374
437
|
and their shadows, the listed final reviews, the run's own); `ended` is the latest `ended`,
|
|
375
438
|
or `null` while any of them is `running` or when none has ended. `runs` and
|
|
@@ -773,6 +836,30 @@ characters …` (`<key>_cut` holds n). A log over 32 MB is read from its end; pa
|
|
|
773
836
|
4,000,000 characters the middle is left out (a `gap` event, `truncated: true`). Parses are kept by
|
|
774
837
|
the log's `(path, size, mtime_ns)` and the session file's.
|
|
775
838
|
|
|
839
|
+
## VerificationLog
|
|
840
|
+
|
|
841
|
+
The run's own verification logs, read by path (an integration's lane logs are not served yet):
|
|
842
|
+
|
|
843
|
+
```
|
|
844
|
+
{"name": "verification.lane.e2e.log", "size": 220000, "mtime": 1760622070.0,
|
|
845
|
+
"tail": "line 14044\n...", "tail_first_line": 14044, "truncated": true}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
`tail` is the file's last 64 KB, from the first line that starts inside them (when no line
|
|
849
|
+
does, from the first whole character), decoded as UTF-8 with invalid bytes replaced.
|
|
850
|
+
`tail_first_line` is the 1-based number of the tail's first line, found by counting the newlines
|
|
851
|
+
before it, or `null` when that would read past 64 MB. `truncated` says the tail is not the whole
|
|
852
|
+
file. With `/lines/<n>`:
|
|
853
|
+
|
|
854
|
+
```
|
|
855
|
+
{"name": "verification.log", "size": 11000, "first_line": 700, "lines": ["line 00700", ...],
|
|
856
|
+
"truncated": false}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
up to 400 lines from line `n` (1-based), each without its line ending and cut to 4,000
|
|
860
|
+
characters (ending `…`). Lines past the end give `lines: []`. `truncated` is `true` when more
|
|
861
|
+
lines follow, a line was cut, or reaching them took more than 64 MB of reading.
|
|
862
|
+
|
|
776
863
|
## Files
|
|
777
864
|
|
|
778
865
|
`GET /api/runs/<key>/files`: who planned, changed, drifted into or conflicted on each file.
|
|
@@ -942,13 +1029,15 @@ difficulty paths deps model` each), `decomposition_history` (its newest 20, each
|
|
|
942
1029
|
it (server.api_get): `/api/runs` (this run only), the RunDetail, every TaskDetail, every
|
|
943
1030
|
ExecutionDetail the RunDetail names, every FinalReviewDetail, Files, RunFindings, and the
|
|
944
1031
|
Fractal trees and nodes. With `texts=1` also every diff the page can open (each task's diff and its `sources`,
|
|
945
|
-
each final review's, each Fractal node's)
|
|
1032
|
+
each final review's, each Fractal node's), every `dwarf` and `qa` ExecutionTranscript (at most 2 MB each),
|
|
1033
|
+
and the VerificationLog tail of `verification.log`, of each lane's log and of each lane run's log
|
|
1034
|
+
(under their tail paths; line ranges are not stored, so the page shows the tail).
|
|
946
1035
|
- `texts=0` (the default) nulls these keys wherever they hold a string, list or object: `prompt
|
|
947
1036
|
last request log_tail dwarf_tail qa_tail qa_last qa_input tasks_summary known_issues texts text
|
|
948
|
-
planning_last approach retry_findings dwarf_input error
|
|
949
|
-
finding keeps its `summary`.
|
|
950
|
-
- `paths=0` (the default) makes every string that is a whole absolute path (but `/api
|
|
951
|
-
`
|
|
1037
|
+
planning_last approach retry_findings dwarf_input error last_line message excerpt tail lines`, and stores no
|
|
1038
|
+
diff and no transcript. A finding keeps its `summary`, a failing test its `name`.
|
|
1039
|
+
- `paths=0` (the default) makes every string that is a whole absolute path (but `/api/…`, `url`s
|
|
1040
|
+
and `command`s) its last component, and replaces the run dir, the repo and the home dir inside other
|
|
952
1041
|
strings with their last component (`~` for home).
|
|
953
1042
|
- At most 1 MB of JSON per diff and 25 MB in all; what did not fit is listed in `meta.omitted`
|
|
954
1043
|
(`path`, `bytes`, `why`). `meta`: `format` `forge-ui-export`, `version` 1, `app`, `key`,
|
|
@@ -1012,9 +1101,12 @@ of the other mode: it replaces it as it does one running other code. `forge ui s
|
|
|
1012
1101
|
mode (`read_only` in its JSON).
|
|
1013
1102
|
|
|
1014
1103
|
**The CLIs' configuration.** The server `forge ui open` starts runs without any `FORGE_*` variable
|
|
1015
|
-
of the shell it was started from. The CLIs
|
|
1104
|
+
of the shell it was started from. The CLIs it calls, for reads and writes alike, still need the forge configuration, so
|
|
1016
1105
|
`open` hands them these, as that shell has them (an empty value counts as unset; a relative
|
|
1017
|
-
`FORGE_REGISTRY` or `FORGE_DATA_HOME` is made absolute against the shell's directory
|
|
1106
|
+
`FORGE_REGISTRY` or `FORGE_DATA_HOME` is made absolute against the shell's directory, except that a
|
|
1107
|
+
`FORGE_DATA_HOME` starting with `~` is kept as given, because its reader expands the `~`; a
|
|
1108
|
+
`FORGE_REGISTRY` starting with `~` is not expanded by anything, so it is made absolute like any other
|
|
1109
|
+
relative path):
|
|
1018
1110
|
`FORGE_REGISTRY`, `FORGE_DATA_HOME`, `FORGE_DATASET`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME` and
|
|
1019
1111
|
`XDG_STATE_HOME` (`control.CLI_ENV_NAMES`). It writes them to `cli-env.json` (mode `0600`) in its
|
|
1020
1112
|
state dir, the server loads that file once when it starts (`_serve --cli-env FILE`) and every CLI
|
|
@@ -1022,12 +1114,14 @@ call gets them on top of the server's own environment; the server's environment
|
|
|
1022
1114
|
changed. No other variable is passed, and none of these is a secret. `server.json` records them as
|
|
1023
1115
|
`cli_env`. A server started by hand (`python3 -m forge_ui`) passes its own environment unchanged.
|
|
1024
1116
|
|
|
1025
|
-
`open` never reuses a server whose recorded `cli_env` differs from the current shell's.
|
|
1026
|
-
each variable that differs and both values, says to run
|
|
1027
|
-
`5`, prints no link and leaves the server running (it may
|
|
1028
|
-
write). A server
|
|
1029
|
-
|
|
1030
|
-
the new one gets the current shell's
|
|
1117
|
+
`open` never reuses a server whose recorded `cli_env` differs from the current shell's. For a
|
|
1118
|
+
server that takes writes it names each variable that differs and both values, says to run
|
|
1119
|
+
`forge ui stop` and then open again, exits `5`, prints no link and leaves the server running (it may
|
|
1120
|
+
be in the middle of another shell's write). A read-only server (`FORGE_UI_READONLY=1`, asked for
|
|
1121
|
+
again in the same mode) has no write to cut off, so it is replaced, as is one running other code, in
|
|
1122
|
+
the other mode or reading another registry directory: the new one gets the current shell's
|
|
1123
|
+
configuration, `open` prints the link and exits `0`. Read-only servers still run CLIs for their GET
|
|
1124
|
+
routes, so their configuration matters too.
|
|
1031
1125
|
|
|
1032
1126
|
**Serialization.** One write per run key at a time. A second write to a run whose write is still
|
|
1033
1127
|
running is `409 {"error": "busy"}`, without reaching the CLI; other runs are not held up.
|
|
@@ -1056,7 +1150,14 @@ for them only unknown keys and runs the catalog does not hold are `404`. Every w
|
|
|
1056
1150
|
`forge control show <plan dir> --json`: `200` with its object (`plan`, `run_id`, `live`, `fractal`,
|
|
1057
1151
|
`run` (the plan-wide controls), `tasks` (per task: `status`, `dwarf`, `qa`, `qa_threshold`,
|
|
1058
1152
|
`retry_budget`, `retries_used`, `pin`, `escalation_stop`, `escalation_next`) and `log`). No token
|
|
1059
|
-
is needed to read it.
|
|
1153
|
+
is needed to read it. Of `run` the page also reads `max_parallel` (1–64 or `null`), `infra_backoff`
|
|
1154
|
+
(the shape, `exp:60` when unset), `escalate_effort` (`on`/`off`), `drained` and `stopping`
|
|
1155
|
+
(`graceful`, `now` or `null`) while live, and `infra_paused` while live: one
|
|
1156
|
+
`{scope, class, harness, model, tasks, cycle, until, exhausted}` per paused scope (`scope` is
|
|
1157
|
+
`all`, `harness:<h>` or `model:<h>/<m>`; `until` an epoch, `exhausted` true once its retries are
|
|
1158
|
+
spent). `log` is the newest `control.log` lines first; a scheduler's answer to a queued request
|
|
1159
|
+
is a line with `actor: "scheduler"`, the request's `request` id, `outcome` (`applied` or
|
|
1160
|
+
`refused`) and `reason`.
|
|
1060
1161
|
|
|
1061
1162
|
### `POST /api/runs/<key>/controls`
|
|
1062
1163
|
|
|
@@ -1071,10 +1172,22 @@ known or a spec runs is for the CLI to say (its `400` or `409` comes back as abo
|
|
|
1071
1172
|
| `{"action": "set", "key": "retry-budget", "value": "inherit", "task": "reader"}` | `control set <dir> retry-budget inherit --task reader --actor ui --json` |
|
|
1072
1173
|
| `{"action": "pin", "task": "graph", "spec": "sol:high:codex"}` | `control pin <dir> graph sol:high:codex --actor ui --json` |
|
|
1073
1174
|
| `{"action": "unpin", "task": "graph"}` | `control unpin <dir> graph --actor ui --json` |
|
|
1175
|
+
| `{"action": "drain"}` | `control drain <dir> --actor ui --json` |
|
|
1176
|
+
| `{"action": "resume"}` | `control resume <dir> --actor ui --json` |
|
|
1177
|
+
| `{"action": "stop"}` (or `"now": false`) | `control stop <dir> --actor ui --json` |
|
|
1178
|
+
| `{"action": "stop", "now": true}` | `control stop <dir> --now --actor ui --json` |
|
|
1179
|
+
| `{"action": "retry", "task": "graph"}` | `control retry <dir> graph --actor ui --json` |
|
|
1180
|
+
| `{"action": "retry", "task": "graph", "dwarf": "sol:high:codex"}` | `control retry <dir> graph --dwarf sol:high:codex --actor ui --json` |
|
|
1074
1181
|
|
|
1075
1182
|
`value` is always a string. `task` is optional on `set`, and only for the keys the CLI allows it
|
|
1076
|
-
for
|
|
1077
|
-
`
|
|
1183
|
+
for; the page sets `max-parallel`, `infra-backoff` (`none` drops it) and `escalate-effort`
|
|
1184
|
+
(`on`/`off`) plan-wide. `now` is a boolean. `add` is not exposed: a row is added to `tasks.tsv` by
|
|
1185
|
+
hand, so its CLI is the place to queue it. A success is
|
|
1186
|
+
`200 {"ok": true, "changed": {...}, "applies": "...", "controls": {...}}`, where `controls` is the
|
|
1187
|
+
`GET` object after the change; `drain`, `resume` and `stop` answer the same way and are refused
|
|
1188
|
+
(`409`) when no scheduler is live. A `retry` only queues: `200 {"queued": true, "request": "<id>"}`.
|
|
1189
|
+
Its refusals before queueing (not live, a MERGED task, a spec that fails doctor) are the CLI's
|
|
1190
|
+
`409`/`400`; the scheduler applies or refuses it later, in the `log` line with that `request` id.
|
|
1078
1191
|
|
|
1079
1192
|
### `GET /api/runs/<key>/issues`
|
|
1080
1193
|
|
|
@@ -1175,9 +1288,10 @@ Plain scripts unless noted; each defines one global and none parses API text as
|
|
|
1175
1288
|
|
|
1176
1289
|
| File | Global | What it is |
|
|
1177
1290
|
|---|---|---|
|
|
1178
|
-
| `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` |
|
|
1179
|
-
| `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` |
|
|
1291
|
+
| `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, verification lanes included), the finish node's verification LEDs (`verifyLeds`: one per lane in lanes mode, else one per run), filters. Also a CommonJS module, for `node --test` |
|
|
1292
|
+
| `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`. Under the attempts, the finish rows: `verify` (single-command verifications; the newest split into its runs back to back from its start, dashed because that placement is approximate), a `verify · <lane>` row per lane of a lanes verification (each run and rerun at its own time; a run with no exit is UNKNOWN once it ended, never FAIL) and `final review`. A lane or run item opens the verification inspector |
|
|
1180
1293
|
| `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 |
|
|
1294
|
+
| `verifyview.js` | `ForgeVerify` | `render(host, {run, now, fetchJSON, rerender})`: the verification inspector panel.js delegates to: while it runs, the elapsed time, step, last line and a flag past 5 minutes without output, and a live tail of the followed lane's log (or the single log) read again each poll; when it failed, the failing tests first, grouped by lane with new or known, each opening its lane's log at its line (in an export, the tail); a lanes table (state, time, reruns, counts). Pure helpers (`model`, `laneRow`, `groupFailing`, `idleWarning`, `openStep`, `tailLane`, `logPath`, `linesPath`, `tailLines`) are a CommonJS module too |
|
|
1181
1295
|
| `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}` |
|
|
1182
1296
|
| `convo.js` | `ForgeConvo` | `render(host, {run, spec: {task, exec}, brief, live, fetchJSON, rerender, diffHash, closeHref})`: an ExecutionTranscript as a timeline, with search, the errors and, while the execution runs, a read of its log every 2.5 s |
|
|
1183
1297
|
| `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 |
|
package/ui/forge_ui/control.py
CHANGED
|
@@ -43,6 +43,7 @@ _EXITS = {2: (400, 'invalid'), 3: (409, 'refused'), 4: (500, 'failed')}
|
|
|
43
43
|
# of them is a secret (forge-config.py's SECRET_WORDS), and no other FORGE_* variable is ever passed.
|
|
44
44
|
CLI_ENV_NAMES = ('FORGE_REGISTRY', 'FORGE_DATA_HOME', 'FORGE_DATASET', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME')
|
|
45
45
|
_CLI_ENV_PATHS = ('FORGE_REGISTRY', 'FORGE_DATA_HOME') # read relative to the shell's directory, not the server's
|
|
46
|
+
_CLI_ENV_EXPANDS_HOME = ('FORGE_DATA_HOME',) # the paths whose reader expands a leading ~ itself
|
|
46
47
|
cli_env = {} # set once, before the server starts; empty when it was started by hand
|
|
47
48
|
|
|
48
49
|
|
|
@@ -50,14 +51,15 @@ def cli_env_from(environ):
|
|
|
50
51
|
"""The CLI_ENV_NAMES set in `environ` (an empty value counts as unset, as it does for the CLIs).
|
|
51
52
|
|
|
52
53
|
A relative FORGE_REGISTRY or FORGE_DATA_HOME is made absolute here: the server and its CLIs run in
|
|
53
|
-
another directory than the shell that named it.
|
|
54
|
+
another directory than the shell that named it. A leading ~ is kept only where the reader expands it
|
|
55
|
+
(FORGE_DATA_HOME); a FORGE_REGISTRY of '~/x.tsv' is the relative path ./~/x.tsv to every CLI.
|
|
54
56
|
"""
|
|
55
57
|
out = {}
|
|
56
58
|
for name in CLI_ENV_NAMES:
|
|
57
59
|
value = environ.get(name, '')
|
|
58
60
|
if not value:
|
|
59
61
|
continue
|
|
60
|
-
if name in _CLI_ENV_PATHS and not value.startswith('~'):
|
|
62
|
+
if name in _CLI_ENV_PATHS and not (name in _CLI_ENV_EXPANDS_HOME and value.startswith('~')):
|
|
61
63
|
value = os.path.abspath(value)
|
|
62
64
|
out[name] = value
|
|
63
65
|
return out
|
|
@@ -197,9 +199,13 @@ def validate_body(body):
|
|
|
197
199
|
action = body.get('action')
|
|
198
200
|
shapes = {'set': ({'action', 'key', 'value', 'task'}, ('key', 'value')),
|
|
199
201
|
'pin': ({'action', 'task', 'spec'}, ('task', 'spec')),
|
|
200
|
-
'unpin': ({'action', 'task'}, ('task',))
|
|
202
|
+
'unpin': ({'action', 'task'}, ('task',)),
|
|
203
|
+
'retry': ({'action', 'task', 'dwarf'}, ('task',)),
|
|
204
|
+
'drain': ({'action'}, ()),
|
|
205
|
+
'resume': ({'action'}, ()),
|
|
206
|
+
'stop': ({'action', 'now'}, ())}
|
|
201
207
|
if not isinstance(action, str) or action not in shapes:
|
|
202
|
-
raise BadBody('action must be one of
|
|
208
|
+
raise BadBody('action must be one of ' + ', '.join(shapes))
|
|
203
209
|
allowed, required = shapes[action]
|
|
204
210
|
extra = sorted(set(body) - allowed)
|
|
205
211
|
if extra:
|
|
@@ -207,6 +213,12 @@ def validate_body(body):
|
|
|
207
213
|
tail = [_text(body, name) for name in required]
|
|
208
214
|
if action == 'set' and 'task' in body:
|
|
209
215
|
tail += ['--task', _text(body, 'task')]
|
|
216
|
+
if action == 'retry' and 'dwarf' in body:
|
|
217
|
+
tail += ['--dwarf', _text(body, 'dwarf')]
|
|
218
|
+
if action == 'stop' and 'now' in body:
|
|
219
|
+
if not isinstance(body['now'], bool):
|
|
220
|
+
raise BadBody('now must be true or false')
|
|
221
|
+
tail += ['--now'] if body['now'] else []
|
|
210
222
|
return action, tail
|
|
211
223
|
|
|
212
224
|
|
|
@@ -358,9 +370,16 @@ def _issues_file(ctx):
|
|
|
358
370
|
if not isinstance(labels, list) or len(labels) > MAX_LABELS:
|
|
359
371
|
raise BadBody(f'labels must be a list of at most {MAX_LABELS} strings')
|
|
360
372
|
for label in labels:
|
|
361
|
-
if
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
if not isinstance(label, str):
|
|
374
|
+
raise BadBody('a label must be a text')
|
|
375
|
+
if not label.strip():
|
|
376
|
+
raise BadBody('a label cannot be blank')
|
|
377
|
+
if len(label) > MAX_LABEL:
|
|
378
|
+
raise BadBody(f'a label is at most {MAX_LABEL} characters')
|
|
379
|
+
if label.startswith('-'):
|
|
380
|
+
raise BadBody('a label cannot start with "-"')
|
|
381
|
+
if not _no_control_chars(label):
|
|
382
|
+
raise BadBody('a label cannot hold control characters')
|
|
364
383
|
except BadBody as exc:
|
|
365
384
|
return _invalid(str(exc))
|
|
366
385
|
path = ctx.locate()
|
package/ui/forge_ui/export.py
CHANGED
|
@@ -22,11 +22,11 @@ import re
|
|
|
22
22
|
import time
|
|
23
23
|
import urllib.parse
|
|
24
24
|
|
|
25
|
-
from forge_ui import __version__, server
|
|
25
|
+
from forge_ui import __version__, reader, server
|
|
26
26
|
|
|
27
27
|
STATIC = server.STATIC_DIR
|
|
28
28
|
# what the exported page runs, in index.html's order; the cloud, the leaderboard and notifications need a server
|
|
29
|
-
SCRIPTS = ('theme.js', 'icons.js', 'datatable.js', 'shell.js', 'runmodel.js', 'cloudmodel.js', 'graph.js', 'gantt.js', 'panel.js', 'diffview.js', 'convo.js', 'files.js', 'map3d.js',
|
|
29
|
+
SCRIPTS = ('theme.js', 'icons.js', 'datatable.js', 'shell.js', 'runmodel.js', 'cloudmodel.js', 'graph.js', 'gantt.js', 'verifyview.js', 'panel.js', 'diffview.js', 'convo.js', 'files.js', 'map3d.js',
|
|
30
30
|
'search.js', 'app.js')
|
|
31
31
|
MODULES = ('iso.js', 'cloud3d.js', 'findings3d.js', 'stagectl.js', 'scene3d.js') # bundled in dependency order
|
|
32
32
|
DIFF_CAP = 1 << 20 # one diff's JSON
|
|
@@ -37,7 +37,7 @@ _EXEC = re.compile(r'(?:task|dwarf|qa|solo|verification|parallel)-[0-9a-f]{32}')
|
|
|
37
37
|
# texts that can hold code, prompts or private notes: nulled unless texts=True
|
|
38
38
|
_TEXT_KEYS = frozenset(('prompt', 'last', 'request', 'log_tail', 'dwarf_tail', 'qa_tail', 'qa_last', 'qa_input',
|
|
39
39
|
'tasks_summary', 'known_issues', 'texts', 'text', 'planning_last', 'approach', 'retry_findings',
|
|
40
|
-
'dwarf_input', 'error'))
|
|
40
|
+
'dwarf_input', 'error', 'last_line', 'message', 'excerpt', 'tail', 'lines'))
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _enc(value):
|
|
@@ -113,12 +113,13 @@ class _Snapshot:
|
|
|
113
113
|
if isinstance(value, list):
|
|
114
114
|
return [self.clean(v, key) for v in value]
|
|
115
115
|
if isinstance(value, str) and not self.paths and key != 'url':
|
|
116
|
-
return self.hide(value)
|
|
116
|
+
return self.hide(value, key)
|
|
117
117
|
return value
|
|
118
118
|
|
|
119
|
-
def hide(self, text):
|
|
120
|
-
# a whole absolute path becomes its last component; known roots inside longer texts are replaced too
|
|
121
|
-
|
|
119
|
+
def hide(self, text, key=None):
|
|
120
|
+
# a whole absolute path becomes its last component; known roots inside longer texts are replaced too.
|
|
121
|
+
# A command that starts with a path is not one path: only its known roots go.
|
|
122
|
+
if key != 'command' and re.fullmatch(r'(/|[A-Za-z]:[\\/])[^\n]*', text) and not text.startswith('/api/'):
|
|
122
123
|
return os.path.basename(text.rstrip('/\\')) or text
|
|
123
124
|
for root, name in self.roots:
|
|
124
125
|
if root in text:
|
|
@@ -191,6 +192,18 @@ def build(catalog, key, texts=False, paths=False, now=None):
|
|
|
191
192
|
for name in names:
|
|
192
193
|
if name.startswith(('dwarf-', 'qa-')):
|
|
193
194
|
snap.add(f'{base}/executions/{name}/transcript', cap=TRANSCRIPT_CAP)
|
|
195
|
+
# the verification's log tails: its own, each lane's and each lane run's (line ranges stay out)
|
|
196
|
+
verification = run.get('verification') or {}
|
|
197
|
+
if verification:
|
|
198
|
+
snap.add(f'{base}/verification/log')
|
|
199
|
+
for lane in verification.get('lanes') or []:
|
|
200
|
+
name = lane.get('name') if isinstance(lane, dict) else None
|
|
201
|
+
if isinstance(name, str) and reader.LANE_NAME.fullmatch(name):
|
|
202
|
+
snap.add(f'{base}/verification/lanes/{name}/log')
|
|
203
|
+
for item in lane.get('runs') or []:
|
|
204
|
+
k = item.get('k') if isinstance(item, dict) else None
|
|
205
|
+
if isinstance(k, int) and not isinstance(k, bool) and k > 0:
|
|
206
|
+
snap.add(f'{base}/verification/lanes/{name}/runs/{k}/log')
|
|
194
207
|
meta = {'format': 'forge-ui-export', 'version': 1, 'app': __version__, 'key': key,
|
|
195
208
|
'generated': now if now is not None else time.time(), 'texts': bool(texts), 'paths': bool(paths),
|
|
196
209
|
'omitted': snap.omitted}
|