@giamme/forge 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +22 -3
  4. package/VERSION +1 -1
  5. package/docs/README.md +26 -0
  6. package/docs/add-ons.md +33 -0
  7. package/docs/artifacts.md +84 -0
  8. package/docs/commands.md +365 -0
  9. package/docs/dataset.md +29 -0
  10. package/docs/decomposed-runs.md +310 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +128 -0
  13. package/docs/fractal.md +181 -0
  14. package/docs/install.md +160 -0
  15. package/docs/memory.md +156 -0
  16. package/docs/models.md +118 -0
  17. package/docs/reviews.md +245 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +20 -0
  21. package/package.json +3 -2
  22. package/references/dataset.md +18 -1
  23. package/references/decompose.md +20 -2
  24. package/references/fractal.md +1 -1
  25. package/references/harnesses.md +18 -6
  26. package/references/solo.md +1 -1
  27. package/references/ui.md +27 -5
  28. package/scripts/forge-config.py +347 -0
  29. package/scripts/forge-control.py +645 -0
  30. package/scripts/forge-dispatch.sh +7 -1
  31. package/scripts/forge-issues.py +806 -0
  32. package/scripts/forge-lib-task.sh +234 -35
  33. package/scripts/forge-parallel-options.sh +3 -1
  34. package/scripts/forge-parallel.sh +3 -2
  35. package/scripts/forge-route.py +184 -27
  36. package/scripts/forge-runs-rm.py +840 -0
  37. package/scripts/forge-runs.py +139 -0
  38. package/scripts/forge-runtime.py +59 -6
  39. package/scripts/forge-schedule.py +227 -11
  40. package/scripts/forge-ui.py +85 -19
  41. package/scripts/forge-version.py +54 -12
  42. package/scripts/forge_dataset/__init__.py +77 -6
  43. package/scripts/forge_dataset/capture.py +5 -2
  44. package/scripts/forge_dataset/harvest.py +23 -1
  45. package/scripts/forge_dataset/importer.py +420 -18
  46. package/scripts/forge_fractal/__init__.py +2 -1
  47. package/scripts/forge_registry.py +168 -16
  48. package/ui/README.md +116 -23
  49. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  50. package/ui/docs/api.md +283 -23
  51. package/ui/forge_ui/__main__.py +10 -2
  52. package/ui/forge_ui/control.py +575 -0
  53. package/ui/forge_ui/export.py +33 -9
  54. package/ui/forge_ui/fractal.py +1 -2
  55. package/ui/forge_ui/reader.py +121 -8
  56. package/ui/forge_ui/server.py +135 -5
  57. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  58. package/ui/forge_ui/static/app.js +358 -131
  59. package/ui/forge_ui/static/cloud3d.js +3 -2
  60. package/ui/forge_ui/static/cloudui.js +8 -5
  61. package/ui/forge_ui/static/controls.js +619 -0
  62. package/ui/forge_ui/static/convo.js +295 -0
  63. package/ui/forge_ui/static/datatable.js +232 -0
  64. package/ui/forge_ui/static/diffview.js +29 -10
  65. package/ui/forge_ui/static/findings3d.js +134 -0
  66. package/ui/forge_ui/static/findingsview.js +412 -0
  67. package/ui/forge_ui/static/graph.js +3 -3
  68. package/ui/forge_ui/static/icons.js +94 -0
  69. package/ui/forge_ui/static/index.html +31 -13
  70. package/ui/forge_ui/static/iso.js +70 -19
  71. package/ui/forge_ui/static/leaderboard.js +44 -48
  72. package/ui/forge_ui/static/panel.js +92 -36
  73. package/ui/forge_ui/static/rundelete.js +388 -0
  74. package/ui/forge_ui/static/runmodel.js +17 -1
  75. package/ui/forge_ui/static/scene3d.js +518 -43
  76. package/ui/forge_ui/static/search.js +2 -1
  77. package/ui/forge_ui/static/settings.js +399 -0
  78. package/ui/forge_ui/static/shell.js +137 -0
  79. package/ui/forge_ui/static/stagectl.js +150 -0
  80. package/ui/forge_ui/static/style.css +1099 -469
  81. package/ui/forge_ui/static/theme.js +25 -0
  82. package/ui/forge_ui/static/uikit.js +161 -0
  83. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,25 @@
1
+ /* forge-ui theme: system, light or dark. Loaded in <head> without defer, so the page never paints in the wrong theme.
2
+ <html data-theme="light|dark"> overrides the system setting; no attribute follows it. The resolved theme is announced
3
+ with a 'forge-theme' event on window ({detail: 'light'|'dark'}) whenever it changes, for the 3D stage.
4
+ window.ForgeTheme = { get(), set('system'|'light'|'dark'), resolved() } */
5
+ window.ForgeTheme = (() => {
6
+ 'use strict';
7
+ const KEY = 'forge-ui-theme', root = document.documentElement;
8
+ const media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
9
+ const read = () => { try { const v = JSON.parse(localStorage.getItem(KEY)); return v === 'light' || v === 'dark' ? v : 'system'; } catch (_) { return 'system'; } };
10
+ let choice = read(), last = null;
11
+ const resolved = () => choice === 'system' ? (media && media.matches ? 'dark' : 'light') : choice;
12
+ function apply() {
13
+ if (choice === 'system') root.removeAttribute('data-theme'); else root.setAttribute('data-theme', choice);
14
+ const now = resolved();
15
+ if (now !== last) { last = now; window.dispatchEvent(new CustomEvent('forge-theme', { detail: now })); }
16
+ }
17
+ function set(value) {
18
+ choice = value === 'light' || value === 'dark' ? value : 'system';
19
+ try { localStorage.setItem(KEY, JSON.stringify(choice)); } catch (_) { /* private mode: not remembered */ }
20
+ apply();
21
+ }
22
+ if (media) media.addEventListener('change', apply);
23
+ apply();
24
+ return { get: () => choice, set, resolved };
25
+ })();
@@ -0,0 +1,161 @@
1
+ /* forge-ui shared write helpers: window.ForgeUI, and a CommonJS module under node:test. Every page that changes a run
2
+ uses these and none re-implements them.
3
+ el(tag, text?, className?) a DOM element; text enters as text
4
+ token() the write token from <meta name="forge-ui-token">, or null
5
+ controlsEnabled() Promise<bool>: a token AND /api/health says controls:true; asked once per page load
6
+ get(path) / post(path, body) Promise<{ok, status, data, error, detail, result}>; post sends the token and never retries
7
+ outcome(host, res, okText) an inline result line (a lamp and a sentence) in an aria-live region
8
+ confirmInline(host, text, {danger}) Promise<bool>: an inline Confirm / Cancel strip, Escape cancels
9
+ The write API is described in ui/docs/api.md, "Controls (write endpoints)". All API values enter the DOM as text. */
10
+ (function (root, factory) {
11
+ 'use strict';
12
+ const api = factory();
13
+ if (typeof module === 'object' && module.exports) module.exports = api;
14
+ else root.ForgeUI = api.create(root);
15
+ })(typeof self !== 'undefined' ? self : this, function () {
16
+ 'use strict';
17
+
18
+ const TOKEN_HEADER = 'X-Forge-UI-Token';
19
+ const TIMED_OUT = 'timed out: it may have partly completed. Reload to see the current state.';
20
+
21
+ // The fetch options for a request. A POST carries the token and a JSON body; nothing else ever does.
22
+ function requestInit(method, body, token) {
23
+ const init = { method, cache: 'no-store', credentials: 'same-origin', headers: {} };
24
+ if (method === 'POST') {
25
+ init.headers[TOKEN_HEADER] = token;
26
+ init.headers['Content-Type'] = 'application/json';
27
+ init.body = JSON.stringify(body === undefined ? {} : body);
28
+ }
29
+ return init;
30
+ }
31
+
32
+ const object = v => v && typeof v === 'object' && !Array.isArray(v) ? v : null;
33
+
34
+ // An HTTP answer as {ok, status, data, error, detail, result}. `status` 0 is "no answer" (the network, or no token).
35
+ // `result` is what the server passes through from the forge CLI's own stdout (an error body may hold it).
36
+ function mapResponse(status, data) {
37
+ const body = object(data);
38
+ if (status >= 200 && status < 300) {
39
+ if (body) return { ok: true, status, data: body, error: null, detail: null, result: object(body.result) };
40
+ return { ok: false, status, data: null, error: 'bad response', detail: 'the server answered without a JSON object', result: null };
41
+ }
42
+ const error = body && typeof body.error === 'string' ? body.error : status === 0 ? 'network' : 'http ' + status;
43
+ const detail = body && typeof body.detail === 'string' ? body.detail : status === 0 ? 'could not reach forge-ui' : null;
44
+ return { ok: false, status, data: body, error, detail, result: body ? object(body.result) : null };
45
+ }
46
+
47
+ // What an inline result line says: {tone: 'ok' | 'bad', text}.
48
+ function outcomeText(res, okText) {
49
+ if (res && res.ok) return { tone: 'ok', text: okText || 'done' };
50
+ const r = res || {};
51
+ if (r.status === 409 && r.error === 'busy') return { tone: 'bad', text: 'busy, try again' };
52
+ if (r.status === 504 || r.error === 'timeout') return { tone: 'bad', text: TIMED_OUT };
53
+ if (r.status === 403) return { tone: 'bad', text: 'not allowed: reload the page (forge-ui may have restarted)' };
54
+ if (r.status === 405) return { tone: 'bad', text: 'this server is read-only' };
55
+ return { tone: 'bad', text: r.detail || r.error || 'failed' };
56
+ }
57
+
58
+ // The helpers over one environment ({document, fetch}); the page makes one over `window`, a test one over fakes.
59
+ function create(env) {
60
+ let enabled = null; // Promise<bool>, kept for the page load
61
+ const open = new WeakMap(); // host → the cancel of the confirm strip it holds
62
+
63
+ function el(tag, text, className) {
64
+ const node = env.document.createElement(tag);
65
+ if (text !== undefined && text !== null) node.textContent = String(text);
66
+ if (className) node.className = className;
67
+ return node;
68
+ }
69
+
70
+ function token() {
71
+ const meta = env.document && env.document.querySelector('meta[name="forge-ui-token"]');
72
+ const value = meta && meta.getAttribute('content');
73
+ return value ? value : null;
74
+ }
75
+
76
+ async function request(path, init) {
77
+ let response;
78
+ try { response = await env.fetch(path, init); } catch (_) { return mapResponse(0, null); }
79
+ let data = null;
80
+ try { data = await response.json(); } catch (_) { /* an empty or non-JSON body maps to the status alone */ }
81
+ return mapResponse(response.status, data);
82
+ }
83
+ const get = path => request(path, requestInit('GET'));
84
+ // Never retried: a write that timed out may have happened, and the caller says what to do next.
85
+ function post(path, body) {
86
+ const t = token();
87
+ if (!t) return Promise.resolve(mapResponse(0, { error: 'no token', detail: 'this page is read-only: it has no write token' }));
88
+ return request(path, requestInit('POST', body, t));
89
+ }
90
+
91
+ function controlsEnabled() {
92
+ if (enabled) return enabled;
93
+ if (!token()) return (enabled = Promise.resolve(false));
94
+ const asked = get('/api/health').then(res => {
95
+ if (res.status === 0) enabled = null; // the server did not answer: ask again next time
96
+ return res.ok && res.data.controls === true;
97
+ });
98
+ return (enabled = asked);
99
+ }
100
+
101
+ function paint(line, o, silent) {
102
+ line.className = 'ui-outcome ' + o.tone;
103
+ line.setAttribute('aria-live', silent ? 'off' : 'polite');
104
+ line.replaceChildren(el('i', undefined, 'lamp st-' + (o.tone === 'ok' ? 'pass' : 'fail')), el('span', o.text));
105
+ }
106
+ // A live region has to exist before its text changes to be announced, so a new one is filled on the next tick.
107
+ // `opts.silent` draws a result again without announcing it twice.
108
+ function outcome(host, res, okText, opts) {
109
+ const o = outcomeText(res, okText), silent = !!(opts && opts.silent);
110
+ let line = Array.from(host.children).find(c => c.classList.contains('ui-outcome'));
111
+ if (line) { paint(line, o, silent); return line; }
112
+ line = el('p', undefined, 'ui-outcome');
113
+ line.setAttribute('role', 'status');
114
+ line.setAttribute('aria-atomic', 'true');
115
+ line.setAttribute('aria-live', silent ? 'off' : 'polite');
116
+ host.append(line);
117
+ if (silent) paint(line, o, true); else setTimeout(() => paint(line, o, false), 0);
118
+ return line;
119
+ }
120
+
121
+ // One strip per host: asking again cancels the first. Escape and Cancel answer false, Confirm true. Focus goes to
122
+ // Confirm (to Cancel when `danger`, so a stray Enter cannot delete) and returns to where it was.
123
+ function confirmInline(host, text, opts) {
124
+ const danger = !!(opts && opts.danger);
125
+ if (open.has(host)) open.get(host)(false);
126
+ return new Promise(resolve => {
127
+ const returnTo = env.document.activeElement;
128
+ const strip = el('div', undefined, 'ui-confirm' + (danger ? ' danger' : ''));
129
+ strip.setAttribute('role', 'group');
130
+ strip.setAttribute('aria-label', String(text));
131
+ const yes = el('button', 'Confirm', danger ? 'ui-danger' : 'primary'), no = el('button', 'Cancel');
132
+ yes.type = 'button'; no.type = 'button';
133
+ strip.append(el('span', undefined, 'ui-confirm-lamp lamp ' + (danger ? 'st-fail' : 'lamp-route')), el('span', text, 'ui-confirm-text'), yes, no);
134
+ let done = false;
135
+ const finish = answer => {
136
+ if (done) return;
137
+ done = true;
138
+ open.delete(host);
139
+ const inside = strip.contains(env.document.activeElement);
140
+ strip.remove();
141
+ if (inside && returnTo && returnTo.isConnected && returnTo.focus) returnTo.focus({ preventScroll: true });
142
+ resolve(answer);
143
+ };
144
+ open.set(host, finish);
145
+ yes.addEventListener('click', () => finish(true));
146
+ no.addEventListener('click', () => finish(false));
147
+ strip.addEventListener('keydown', e => {
148
+ if (e.key !== 'Escape') return;
149
+ e.preventDefault(); e.stopPropagation(); // the page's own Escape handlers are not for this
150
+ finish(false);
151
+ });
152
+ host.append(strip);
153
+ (danger ? no : yes).focus({ preventScroll: true });
154
+ });
155
+ }
156
+
157
+ return { el, token, controlsEnabled, get, post, outcome, confirmInline, requestInit, mapResponse, outcomeText };
158
+ }
159
+
160
+ return { create, requestInit, mapResponse, outcomeText, TOKEN_HEADER };
161
+ });