@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -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>