@giamme/forge 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -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 +365 -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 +671 -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-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -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 +240 -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 +84 -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 +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -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 +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -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 +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Pinned Fractal agents.py backend. Every invocation uses a fresh Forge dispatch."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from fractal.core.agent import Agent, Invocation, StreamEvent, StreamParser
|
|
9
|
+
|
|
10
|
+
from . import SCRIPTS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ForgeParser(StreamParser):
|
|
14
|
+
def feed(self, line: str) -> list[StreamEvent]:
|
|
15
|
+
try:
|
|
16
|
+
value = json.loads(line)
|
|
17
|
+
except ValueError:
|
|
18
|
+
return []
|
|
19
|
+
if not isinstance(value, dict) or value.get('forge_event') != 'result':
|
|
20
|
+
return []
|
|
21
|
+
self.model = value['model']
|
|
22
|
+
self.cost = value.get('cost')
|
|
23
|
+
self.final = True
|
|
24
|
+
if value['exit_code']:
|
|
25
|
+
self.errors.append(value['text'])
|
|
26
|
+
return [StreamEvent(kind='result', text=value['text'], model=self.model,
|
|
27
|
+
failed=bool(value['exit_code']), final=True, cost=self.cost)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ForgeAgent(Agent):
|
|
31
|
+
name = 'forge-bridge'
|
|
32
|
+
mints_session = True
|
|
33
|
+
enforces_budget = False
|
|
34
|
+
__parser__ = ForgeParser
|
|
35
|
+
|
|
36
|
+
def _invocation(self, prompt, *, mode, session, model, effort, budget) -> Invocation:
|
|
37
|
+
# The coordinator gives each work step its own immutable request directory.
|
|
38
|
+
step = self.forge_step
|
|
39
|
+
prompt_path = step / ('input-' + uuid.uuid4().hex + '.md')
|
|
40
|
+
prompt_path.write_text(prompt)
|
|
41
|
+
argv = (sys.executable, str(SCRIPTS / 'forge-fractal.py'), '_bridge',
|
|
42
|
+
str(step), str(prompt_path), model)
|
|
43
|
+
return Invocation(agent=self.name, argv=argv, cwd=self.forge_workspace)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ['ForgeAgent']
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Small CLI; inspection remains usable with no Fractal runtime installed."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
from . import DEFAULTS, identifier, managed_run, read_json, safe_file, write_json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parser() -> argparse.ArgumentParser:
|
|
16
|
+
p = argparse.ArgumentParser(prog='forge fractal', description='Opt-in Fractal execution and read-only observability for Forge')
|
|
17
|
+
sub = p.add_subparsers(dest='command', required=True)
|
|
18
|
+
install = sub.add_parser('install')
|
|
19
|
+
for flag in ('dry-run', 'yes', 'with-prerequisites'):
|
|
20
|
+
install.add_argument('--' + flag, action='store_true')
|
|
21
|
+
doctor = sub.add_parser('doctor')
|
|
22
|
+
doctor.add_argument('--spec', action='append', default=[])
|
|
23
|
+
doctor.add_argument('--json', action='store_true')
|
|
24
|
+
listing = sub.add_parser('runs')
|
|
25
|
+
listing.add_argument('--repo')
|
|
26
|
+
listing.add_argument('--json', action='store_true')
|
|
27
|
+
listing.add_argument('--html', type=Path)
|
|
28
|
+
for name in ('status', 'tree', 'activity', 'logs', 'costs', 'messages', 'config', 'report', 'pause', 'resume', 'stop'):
|
|
29
|
+
command = sub.add_parser(name)
|
|
30
|
+
command.add_argument('run')
|
|
31
|
+
command.add_argument('--task')
|
|
32
|
+
command.add_argument('--node')
|
|
33
|
+
command.add_argument('--json', action='store_true')
|
|
34
|
+
command.add_argument('--html', type=Path)
|
|
35
|
+
command.add_argument('--offset', type=int, default=0)
|
|
36
|
+
command.add_argument('--limit', type=int, default=100)
|
|
37
|
+
if name == 'logs':
|
|
38
|
+
command.add_argument('--follow', action='store_true')
|
|
39
|
+
opening = sub.add_parser('open')
|
|
40
|
+
opening.add_argument('run', nargs='?')
|
|
41
|
+
opening.add_argument('--port', type=int, default=0)
|
|
42
|
+
selection = sub.add_parser('select')
|
|
43
|
+
selection.add_argument('--run-dir', required=True)
|
|
44
|
+
selection.add_argument('--repo', required=True)
|
|
45
|
+
selection.add_argument('--mode', choices=('solo', 'parallel'), required=True)
|
|
46
|
+
selection.add_argument('--choice', choices=('', 'on', 'off'), default='')
|
|
47
|
+
selection.add_argument('--dwarf', default='')
|
|
48
|
+
selection.add_argument('--qa', default='')
|
|
49
|
+
selection.add_argument('--fractal-auto-decompose', action='store_true')
|
|
50
|
+
selection.add_argument('--fractal-planner', default='')
|
|
51
|
+
for tier in ('any', 'low', 'medium', 'high'):
|
|
52
|
+
selection.add_argument('--dwarf-' + tier, default='')
|
|
53
|
+
for key in DEFAULTS:
|
|
54
|
+
selection.add_argument('--fractal-' + key, dest=key, type=int)
|
|
55
|
+
selection.add_argument('--fractal-max-cost', dest='max_cost', type=float)
|
|
56
|
+
for flag in ('yolo-dwarf', 'yolo-qa', 'dry-run'):
|
|
57
|
+
selection.add_argument('--' + flag, action='store_true')
|
|
58
|
+
return p
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def control(args, run: Path) -> dict:
|
|
62
|
+
from .execution import event, launch
|
|
63
|
+
from .inspection import capture
|
|
64
|
+
if args.node and not args.task:
|
|
65
|
+
raise ValueError('--node requires --task to make the target unambiguous')
|
|
66
|
+
capture(run, task_id=args.task, node_id=args.node)
|
|
67
|
+
key = identifier(args.task) if args.task else 'all'
|
|
68
|
+
if args.node:
|
|
69
|
+
key += ':' + identifier(args.node)
|
|
70
|
+
receipt = dict(action=args.command, target=key, requested_at=time.time(), observed='requested')
|
|
71
|
+
target = safe_file(run, 'controls/' + key + '.json')
|
|
72
|
+
write_json(target, receipt)
|
|
73
|
+
event(run, 'control_requested', **receipt)
|
|
74
|
+
if args.command == 'resume':
|
|
75
|
+
from .pipeline import resume_pipeline
|
|
76
|
+
resumed_pipeline = resume_pipeline(run) if not args.task else False
|
|
77
|
+
for task in (run / 'tasks').iterdir():
|
|
78
|
+
if args.task and task.name != args.task:
|
|
79
|
+
continue
|
|
80
|
+
state = read_json(task / 'state.json')
|
|
81
|
+
if state['status'] != 'completed':
|
|
82
|
+
launch(run, task)
|
|
83
|
+
receipt['observed'] = 'pipeline relaunched' if resumed_pipeline else 'existing coordinator notified'
|
|
84
|
+
else:
|
|
85
|
+
receipt['observed'] = 'request persisted; coordinator observes at the next supervision tick'
|
|
86
|
+
deadline = time.monotonic() + 2
|
|
87
|
+
while time.monotonic() < deadline:
|
|
88
|
+
observed = capture(run, task_id=args.task, node_id=args.node)
|
|
89
|
+
states = {t['id']: t['state']['status'] for t in observed['tasks']}
|
|
90
|
+
receipt['observed_states'] = states
|
|
91
|
+
receipt['observed_qa_states'] = {t['id']: t['state'].get('qa_status', 'unknown') for t in observed['tasks']}
|
|
92
|
+
receipt['observed_node_states'] = {t['id']: {n['id']: n['status'] for n in t['nodes']} for t in observed['tasks']}
|
|
93
|
+
if not states or all(s in ('paused', 'stopped', 'completed', 'failed', 'timeout') for s in states.values()):
|
|
94
|
+
break
|
|
95
|
+
if args.command == 'resume' and all(s == 'active' for s in states.values()):
|
|
96
|
+
break
|
|
97
|
+
time.sleep(.1)
|
|
98
|
+
receipt['observed_at'] = time.time()
|
|
99
|
+
write_json(target, receipt)
|
|
100
|
+
event(run, 'control_observed', **receipt)
|
|
101
|
+
return receipt
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def internal(argv: list[str]) -> int:
|
|
105
|
+
from .execution import bridge, dispatch, worker, supervise
|
|
106
|
+
if argv[0] == '_dispatch':
|
|
107
|
+
return dispatch(argv[1:])
|
|
108
|
+
if argv[0] == '_bridge':
|
|
109
|
+
return bridge(Path(argv[1]), Path(argv[2]), argv[3])
|
|
110
|
+
if argv[0] == '_worker':
|
|
111
|
+
run = managed_run(argv[1])
|
|
112
|
+
return worker(run, safe_file(run, 'tasks/' + identifier(argv[2])))
|
|
113
|
+
if argv[0] == '_supervise':
|
|
114
|
+
run = managed_run(argv[1])
|
|
115
|
+
return supervise(run, safe_file(run, 'tasks/' + identifier(argv[2])))
|
|
116
|
+
if argv[0] == '_outcome':
|
|
117
|
+
from .pipeline import outcome
|
|
118
|
+
outcome(managed_run(Path(os.environ['FORGE_FRACTAL_RUN']).name), Path(argv[1]).resolve(), int(argv[2]))
|
|
119
|
+
return 0
|
|
120
|
+
if argv[0] == '_checkpoint':
|
|
121
|
+
from .pipeline import checkpoint
|
|
122
|
+
return 0 if checkpoint(managed_run(Path(os.environ['FORGE_FRACTAL_RUN']).name), Path(argv[1])) else 1
|
|
123
|
+
if argv[0] == '_remember':
|
|
124
|
+
from .pipeline import remember
|
|
125
|
+
remember(managed_run(Path(os.environ['FORGE_FRACTAL_RUN']).name), argv[1:])
|
|
126
|
+
return 0
|
|
127
|
+
if argv[0] == '_lock':
|
|
128
|
+
import fcntl
|
|
129
|
+
handle = (Path(argv[1]) / 'pipeline.lock').open('a')
|
|
130
|
+
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
131
|
+
os.set_inheritable(handle.fileno(), True)
|
|
132
|
+
os.execve('/bin/bash', ['/bin/bash', *argv[2:]], dict(os.environ, FORGE_SOLO_LOCK=argv[1]))
|
|
133
|
+
raise ValueError('Unknown internal command')
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main(argv: list[str] | None = None) -> int:
|
|
137
|
+
from .execution import Halt
|
|
138
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
139
|
+
try:
|
|
140
|
+
if argv and argv[0].startswith('_'):
|
|
141
|
+
return internal(argv)
|
|
142
|
+
args = parser().parse_args(argv)
|
|
143
|
+
from . import inspection, install
|
|
144
|
+
if args.command == 'install':
|
|
145
|
+
return install.install(args)
|
|
146
|
+
if args.command == 'select':
|
|
147
|
+
from .selection import select
|
|
148
|
+
print(select(args))
|
|
149
|
+
return 0
|
|
150
|
+
if args.command == 'doctor':
|
|
151
|
+
from .selection import resolve
|
|
152
|
+
data = install.doctor()
|
|
153
|
+
data['harnesses'] = [resolve(s, 'dwarf') for s in args.spec]
|
|
154
|
+
print(json.dumps(data, indent=2))
|
|
155
|
+
return 0 if data['ready'] else 3
|
|
156
|
+
if args.command == 'open':
|
|
157
|
+
if args.run:
|
|
158
|
+
managed_run(args.run)
|
|
159
|
+
inspection.serve(args.run, args.port)
|
|
160
|
+
return 0
|
|
161
|
+
if args.command == 'runs':
|
|
162
|
+
data = inspection.runs(args.repo)
|
|
163
|
+
else:
|
|
164
|
+
run = managed_run(args.run)
|
|
165
|
+
if args.command in ('pause', 'resume', 'stop'):
|
|
166
|
+
print(json.dumps(control(args, run), indent=2))
|
|
167
|
+
return 0
|
|
168
|
+
if args.node:
|
|
169
|
+
identifier(args.node)
|
|
170
|
+
if args.task:
|
|
171
|
+
identifier(args.task)
|
|
172
|
+
if args.offset < 0 or not 1 <= args.limit <= 1000:
|
|
173
|
+
raise ValueError('History requires offset >=0 and limit between 1 and 1000')
|
|
174
|
+
data = inspection.capture(run, task_id=args.task, node_id=args.node,
|
|
175
|
+
logs=args.command in ('logs', 'report') or bool(args.html),
|
|
176
|
+
offset=args.offset, limit=args.limit, portable=args.command == 'report' or bool(args.html))
|
|
177
|
+
if args.command == 'report' and not args.html:
|
|
178
|
+
args.html = Path(args.run + '.html')
|
|
179
|
+
if args.html:
|
|
180
|
+
args.html.write_text(inspection.html(data))
|
|
181
|
+
print(str(args.html.resolve()))
|
|
182
|
+
else:
|
|
183
|
+
print(json.dumps(data, indent=2))
|
|
184
|
+
if args.command == 'logs' and args.follow:
|
|
185
|
+
previous = json.dumps(data)
|
|
186
|
+
while True:
|
|
187
|
+
time.sleep(1)
|
|
188
|
+
data = inspection.capture(run, task_id=args.task, node_id=args.node, logs=True)
|
|
189
|
+
data.pop('captured_at', None)
|
|
190
|
+
rendered = json.dumps(data)
|
|
191
|
+
if rendered != previous:
|
|
192
|
+
print(rendered, flush=True)
|
|
193
|
+
previous = rendered
|
|
194
|
+
return 0
|
|
195
|
+
except KeyboardInterrupt:
|
|
196
|
+
return 130
|
|
197
|
+
except (Halt, subprocess.TimeoutExpired) as error:
|
|
198
|
+
print('forge fractal: ' + str(error), file=sys.stderr)
|
|
199
|
+
return 7 if isinstance(error, subprocess.TimeoutExpired) or str(error) == 'timeout' else 4
|
|
200
|
+
except (OSError, ValueError, RuntimeError, KeyError) as error:
|
|
201
|
+
print('forge fractal: ' + str(error), file=sys.stderr)
|
|
202
|
+
return 3
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
:root{color-scheme:dark;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;background:#111315;color:#ecedef}
|
|
2
|
+
*{box-sizing:border-box}
|
|
3
|
+
body{margin:0}
|
|
4
|
+
button,input,select{font:inherit}
|
|
5
|
+
button{cursor:pointer}
|
|
6
|
+
a{color:#a5c6ff}
|
|
7
|
+
button,a,input,select{outline-offset:3px}
|
|
8
|
+
:focus-visible{outline:2px solid #a9c9ff}
|
|
9
|
+
h1,h2,h3,p{margin:0}
|
|
10
|
+
h1{font-size:17px;font-weight:650}
|
|
11
|
+
h2{font-size:20px;line-height:1.25}
|
|
12
|
+
h3{font-size:14px}
|
|
13
|
+
pre,code{font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
14
|
+
pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:0}
|
|
15
|
+
.app{height:100dvh;display:flex;flex-direction:column;min-height:420px}
|
|
16
|
+
.topbar,.runbar{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:0 24px;border-bottom:1px solid #30353a}
|
|
17
|
+
.topbar{height:54px;flex:none;background:#191c1f}
|
|
18
|
+
.brand{display:flex;align-items:center;gap:9px;font-weight:700;letter-spacing:.02em}
|
|
19
|
+
.brand-mark{display:grid;place-items:center;width:24px;height:24px;border:1px solid #638ac9;border-radius:5px;color:#b5d0ff;font-size:13px}
|
|
20
|
+
.brand-muted,.muted,.run-meta,.eyebrow,.nav-heading span{color:#aab0b8}
|
|
21
|
+
.top-actions,.run-title,.run-meta{display:flex;align-items:center;gap:14px;min-width:0}
|
|
22
|
+
.runbar{min-height:62px;flex:none;background:#15181b}
|
|
23
|
+
.run-title{gap:10px;white-space:nowrap;overflow:hidden}
|
|
24
|
+
.run-title h1{overflow:hidden;text-overflow:ellipsis}
|
|
25
|
+
.run-title a{text-decoration:none}
|
|
26
|
+
.slash{color:#69727b}
|
|
27
|
+
.run-meta{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
28
|
+
.connection{font-size:12px;color:#aeb8c3}
|
|
29
|
+
.connection.stale{color:#f1bf80}
|
|
30
|
+
.workspace{display:grid;grid-template-columns:minmax(280px,320px) minmax(0,1fr);min-height:0;flex:1}
|
|
31
|
+
.navigator{overflow:auto;border-right:1px solid #30353a;background:#171a1d;padding:20px 12px 28px}
|
|
32
|
+
.nav-heading{display:flex;align-items:baseline;justify-content:space-between;padding:0 11px 13px}
|
|
33
|
+
.nav-heading h2{font-size:13px;text-transform:uppercase;letter-spacing:.1em;color:#c0c6cd}
|
|
34
|
+
.nav-heading span{font-size:12px}
|
|
35
|
+
.search-label{display:block;color:#aeb8c3;font-size:12px;margin:0 11px 6px}
|
|
36
|
+
input[type=search],select{width:calc(100% - 22px);min-height:40px;background:#22262a;border:1px solid #454d55;border-radius:7px;color:#f3f5f7;padding:8px 10px;margin:0 11px 16px}
|
|
37
|
+
input::placeholder{color:#8e99a5}
|
|
38
|
+
.tree{display:flex;flex-direction:column;gap:4px}
|
|
39
|
+
.tree-row{display:flex;align-items:stretch;min-width:0;border-radius:7px}
|
|
40
|
+
.tree-row.selected{background:#23344c;box-shadow:inset 2px 0 #78aaff}
|
|
41
|
+
.tree-row:hover:not(.selected){background:#22282e}
|
|
42
|
+
.tree-row button{border:0;background:none;color:inherit}
|
|
43
|
+
.tree-toggle{width:30px;flex:none;color:#aeb8c3!important;padding:0;font-size:12px}
|
|
44
|
+
.tree-toggle.empty{visibility:hidden}
|
|
45
|
+
.tree-select{flex:1;text-align:left;min-width:0;padding:9px 9px 9px 0;min-height:53px}
|
|
46
|
+
.tree-row.task .tree-select{padding-left:9px}
|
|
47
|
+
.row-top{display:flex;align-items:center;gap:6px;min-width:0}
|
|
48
|
+
.row-label{font-weight:630;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
49
|
+
.row-goal{display:block;font-size:12px;color:#aeb8c3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px}
|
|
50
|
+
.tree-row.selected .row-goal{color:#c4d2e7}
|
|
51
|
+
.indent{display:block;flex:none;width:calc(var(--depth) * 13px)}
|
|
52
|
+
.detail{min-width:0;overflow:auto;padding:29px clamp(22px,4vw,58px) 70px;background:#111315}
|
|
53
|
+
.detail-header{display:flex;gap:12px;align-items:flex-start;max-width:1100px}
|
|
54
|
+
.detail-header>div{min-width:0}
|
|
55
|
+
.detail-header h2{font-size:27px;overflow-wrap:anywhere}
|
|
56
|
+
.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:700;margin-bottom:5px}
|
|
57
|
+
.detail-header .muted{margin-top:5px;overflow-wrap:anywhere}
|
|
58
|
+
.tabs{display:flex;gap:20px;overflow:auto;max-width:1100px;border-bottom:1px solid #33383e;margin-top:25px}
|
|
59
|
+
.tabs button{background:none;border:0;border-bottom:2px solid transparent;color:#aeb8c3;padding:10px 2px;white-space:nowrap}
|
|
60
|
+
.tabs button[aria-current=page]{border-color:#8bb7ff;color:#dceaff}
|
|
61
|
+
.detail-body{max-width:1100px;padding-top:24px}
|
|
62
|
+
.section{margin-bottom:31px}
|
|
63
|
+
.section-title{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:#b9c0c8;margin-bottom:12px}
|
|
64
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}
|
|
65
|
+
.card,.panel{background:#1b1f22;border:1px solid #353b41;border-radius:9px}
|
|
66
|
+
.card{padding:16px}
|
|
67
|
+
.card-label{font-size:12px;color:#aeb8c3;margin-bottom:8px}
|
|
68
|
+
.card-value{font-size:18px;font-weight:650;overflow-wrap:anywhere}
|
|
69
|
+
.card-sub{font-size:12px;color:#aeb8c3;margin-top:5px}
|
|
70
|
+
.panel{padding:20px;margin-bottom:12px}
|
|
71
|
+
.panel h3{font-size:15px;margin-bottom:10px}
|
|
72
|
+
.panel p+p{margin-top:9px}
|
|
73
|
+
.field-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:17px}
|
|
74
|
+
.field dt{color:#aeb8c3;font-size:12px;margin-bottom:3px}
|
|
75
|
+
.field dd{margin:0;overflow-wrap:anywhere}
|
|
76
|
+
.status-line{display:inline-flex;align-items:center;gap:7px;font-weight:650}
|
|
77
|
+
.status-dot{width:7px;height:7px;border-radius:50%;background:#9aa6b3}
|
|
78
|
+
.status-line[data-tone=good] .status-dot{background:#66c69c}
|
|
79
|
+
.status-line[data-tone=bad] .status-dot{background:#e7968b}
|
|
80
|
+
.status-line[data-tone=active] .status-dot{background:#8fb8f8}
|
|
81
|
+
.status-line[data-tone=warning] .status-dot{background:#e8bf79}
|
|
82
|
+
.status-line[data-tone=good]{color:#b2ebce}
|
|
83
|
+
.status-line[data-tone=bad]{color:#f2b0a8}
|
|
84
|
+
.status-line[data-tone=active]{color:#b9d4ff}
|
|
85
|
+
.status-line[data-tone=warning]{color:#f0d29e}
|
|
86
|
+
.notice{border:1px solid #535044;background:#29251d;color:#f2d6a3;padding:12px 16px;border-radius:8px;margin-bottom:18px}
|
|
87
|
+
.empty{border:1px dashed #50575f;border-radius:9px;color:#b5bdc6;padding:27px;text-align:center}
|
|
88
|
+
.toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:13px}
|
|
89
|
+
.toolbar select{width:auto;margin:0;min-width:180px}
|
|
90
|
+
.button{background:#292f35;border:1px solid #56616d;border-radius:6px;color:#e5edf6;padding:7px 11px;min-height:36px}
|
|
91
|
+
.button:hover{background:#35404b}
|
|
92
|
+
.button[disabled]{opacity:.5;cursor:default}
|
|
93
|
+
.top-actions button{background:#292f35;border:1px solid #56616d;border-radius:6px;color:#e5edf6;padding:5px 9px}
|
|
94
|
+
.artifact{max-height:65vh;overflow:auto;background:#171a1d;border:1px solid #3b4249;border-radius:8px;padding:14px}
|
|
95
|
+
.diff-line{display:block;white-space:pre;min-width:max-content}
|
|
96
|
+
.diff-line.add{color:#b7e4c7;background:#1d2a22}
|
|
97
|
+
.diff-line.remove{color:#f1b5ae;background:#2b201f}
|
|
98
|
+
.diff-line.header{color:#a9c9ff;background:#202936}
|
|
99
|
+
.history-list{display:flex;flex-direction:column;gap:7px}
|
|
100
|
+
.history-row{padding:10px 13px;border:1px solid #343b41;background:#1b1f22;border-radius:7px}
|
|
101
|
+
.history-row pre{margin-top:4px}
|
|
102
|
+
.small{font-size:12px;color:#aeb8c3}
|
|
103
|
+
.command{position:relative}
|
|
104
|
+
.command pre{padding-right:70px}
|
|
105
|
+
.copy{position:absolute;right:10px;top:10px}
|
|
106
|
+
.run-list{display:flex;flex-direction:column;gap:9px}
|
|
107
|
+
.run-link{display:block;text-decoration:none;color:#ecedef}
|
|
108
|
+
.run-link:hover{border-color:#769aca}
|
|
109
|
+
.run-link strong{display:block}
|
|
110
|
+
.run-link span{display:block;color:#aeb8c3;font-size:12px;margin-top:5px;overflow-wrap:anywhere}
|
|
111
|
+
#mobile-back{display:none}
|
|
112
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
113
|
+
[hidden]{display:none!important}
|
|
114
|
+
@media(max-width:680px){.topbar,.runbar{padding:0 15px}
|
|
115
|
+
.brand-muted{display:none}
|
|
116
|
+
.top-actions{gap:7px}
|
|
117
|
+
.connection{max-width:115px;text-align:right;line-height:1.2}
|
|
118
|
+
.run-meta{display:none}
|
|
119
|
+
.workspace{display:block}
|
|
120
|
+
.navigator,.detail{height:100%;width:100%}
|
|
121
|
+
.navigator{border-right:0}
|
|
122
|
+
.detail{display:none;padding:22px 18px 50px}
|
|
123
|
+
.app.show-detail .navigator{display:none}
|
|
124
|
+
.app.show-detail .detail{display:block}
|
|
125
|
+
#mobile-back{display:inline-block;flex:none;background:#292f35;border:1px solid #56616d;border-radius:6px;color:#e5edf6;padding:7px 10px;min-height:40px}
|
|
126
|
+
.detail-header h2{font-size:22px}
|
|
127
|
+
.tabs{gap:14px}
|
|
128
|
+
.cards{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
129
|
+
.card{padding:12px}
|
|
130
|
+
.card-value{font-size:15px}
|
|
131
|
+
}
|
|
132
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}
|
|
133
|
+
}
|
|
134
|
+
.row-status[data-tone=good],.card-value[data-tone=good]{color:#a9e4c4}
|
|
135
|
+
.row-status[data-tone=bad],.card-value[data-tone=bad]{color:#f0aea6}
|
|
136
|
+
.row-status[data-tone=active],.card-value[data-tone=active]{color:#b2cffb}
|
|
137
|
+
.row-status[data-tone=warning],.card-value[data-tone=warning]{color:#e8c98e}
|
|
138
|
+
.row-model{color:#9ba9b8;font-size:11px;max-width:82px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<title>Forge · Run inspector</title>
|
|
8
|
+
<style>/*__STYLE__*/</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="app">
|
|
12
|
+
<header class="topbar">
|
|
13
|
+
<div class="brand"><span class="brand-mark" aria-hidden="true">F</span><span>Forge <span class="brand-muted">/ run inspector</span></span></div>
|
|
14
|
+
<div class="top-actions">
|
|
15
|
+
<span id="connection" class="connection" role="status" aria-live="polite"></span>
|
|
16
|
+
<button id="retry" type="button" hidden>Retry</button>
|
|
17
|
+
<button id="pause-updates" type="button" aria-pressed="false" hidden>Pause live updates</button>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
<div class="runbar">
|
|
21
|
+
<div class="run-title"><a id="all-runs" href="?run=">All runs</a><span class="slash">/</span><h1 id="run-heading">Runs</h1></div>
|
|
22
|
+
<div id="run-meta" class="run-meta"></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="workspace">
|
|
25
|
+
<aside id="navigator" class="navigator" aria-label="Run navigation">
|
|
26
|
+
<div class="nav-heading"><h2>Navigate</h2><span id="nav-count"></span></div>
|
|
27
|
+
<label class="search-label" for="search">Search tasks and nodes</label>
|
|
28
|
+
<input id="search" type="search" placeholder="Goal, name, model, status…" autocomplete="off">
|
|
29
|
+
<div id="tree" class="tree"></div>
|
|
30
|
+
</aside>
|
|
31
|
+
<main id="detail" class="detail" tabindex="-1">
|
|
32
|
+
<div class="detail-header"><button id="mobile-back" type="button">Back to navigator</button><div><p id="eyebrow" class="eyebrow"></p><h2 id="detail-title"></h2><p id="detail-subtitle" class="muted"></p></div></div>
|
|
33
|
+
<nav id="tabs" class="tabs" aria-label="Detail views"></nav>
|
|
34
|
+
<div id="detail-body" class="detail-body"></div>
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<script id="capture" type="application/json">__CAPTURE__</script>
|
|
39
|
+
<script>const FORGE_LIVE = __LIVE__; /*__SCRIPT__*/</script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|