@bongos/core 1.24.77 → 1.24.79
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/.bongos-core.json +91 -46
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +12 -1
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +15 -0
- package/docs/api/openapi.json +209 -4
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +109 -92
- package/docs/copy-registry.json +291 -108
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -0
- package/docs/page-readings.json +111 -97
- package/docs/recipes/autobongos-runner-setup.md +148 -0
- package/docs/recipes/autobongos-windows-host.md +10 -7
- package/modules/autonomy/cadence.js +3 -0
- package/modules/autonomy/db.js +86 -7
- package/modules/autonomy/fence.js +20 -5
- package/modules/autonomy/migrations/autonomy_006_runner_device.sql +45 -0
- package/modules/autonomy/module.json +2 -1
- package/modules/autonomy/routes/autonomy.js +165 -3
- package/modules/autonomy/runner-health.js +27 -0
- package/modules/autonomy/skills/autobongos-setup/SKILL.md +86 -0
- package/modules/hall-ui/public/commands-lib.js +1 -1
- package/modules/hall-ui/public/gate.js +12 -5
- package/modules/hall-ui/public/runner-button.css +55 -0
- package/modules/hall-ui/public/runner-button.js +187 -0
- package/modules/hall-ui/public/settings-autobongos.js +349 -63
- package/modules/hall-ui/public/settings.html +23 -10
- package/modules/hall-ui/records/runner-button.md +16 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/bongos/autobongos-host.js +35 -6
- package/scripts/bongos/autobongos-run.js +18 -3
- package/scripts/bongos/claude-materialize.js +20 -1
- package/scripts/bongos/stranded-rows.js +39 -2
- package/scripts/hall-preview/server.js +7 -0
- package/src/module-api.js +1 -1
- package/tests/autobongos_host.mjs +46 -0
- package/tests/autonomy_config_idle.mjs +9 -4
- package/tests/autonomy_runner_device.mjs +444 -0
- package/tests/autonomy_runner_identity.mjs +3 -1
- package/tests/claude_materialize.mjs +74 -0
- package/tests/hall_runner_button.mjs +467 -0
- package/tests/hall_runner_panel_connect.mjs +73 -0
- package/tests/stranded_rows.mjs +50 -0
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
// modules/hall-ui/public/settings-autobongos.js — the "Your Autobongos runner"
|
|
2
|
-
// panel on /builders/settings (
|
|
2
|
+
// panel on /builders/settings (tasks 1004501, 1005094).
|
|
3
3
|
//
|
|
4
|
-
// WHY THE PANEL EXISTS.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// WHY THE PANEL EXISTS. Each builder runs their own runner on their own devices,
|
|
5
|
+
// and says which work it takes. Owner direction, 2026-11-09: that must be as
|
|
6
|
+
// simple as one button — add a device with one copied command, press Run on its
|
|
7
|
+
// card, and it goes. So the panel is three things, top to bottom:
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// 1. YOUR DEVICES — one card per device that has checked in, each with ONE
|
|
10
|
+
// button whose face is the device's state (Run, Starting, Running, Resting
|
|
11
|
+
// until a time, Stopping, Device is off). What the face says is decided by
|
|
12
|
+
// runner-button.js, which is pure and tested; this file only draws it. A
|
|
13
|
+
// device that is switched on also carries the stay-on note: keep it on and
|
|
14
|
+
// plugged in.
|
|
15
|
+
// 2. ADD A DEVICE — one command to copy, Windows or Mac, with a one-time code
|
|
16
|
+
// built in. The card appears by itself when the device first checks in.
|
|
17
|
+
// 3. WHAT IT WORKS ON — the goal picks (task 1004501), unchanged.
|
|
15
18
|
//
|
|
16
|
-
// WHAT IT
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
19
|
+
// WHAT IT READS. GET /autonomy/fence (the caller's own fence, the project's
|
|
20
|
+
// allowlist riding along as `project`) and GET /autonomy/runners (their devices,
|
|
21
|
+
// each with the server's health verdict and the device's switch). Both are read
|
|
22
|
+
// again on a timer while the page is visible, so a card moves from Starting to
|
|
23
|
+
// Running without a reload. The server decides every fact; this page draws them.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IT WRITES. POST /autonomy/runners/:host/run for the button, PUT
|
|
26
|
+
// /autonomy/me/goals for the picks, POST /auth/runner-code/issue for a setup
|
|
27
|
+
// code, and Remove: POST /me/sessions/revoke for the device's own login, then
|
|
28
|
+
// DELETE /autonomy/runners/:host for its card.
|
|
29
|
+
//
|
|
30
|
+
// THE FOUR SLOTS ARE IN THE PAGE (settings.html: #ab-notices, #ab-devices,
|
|
31
|
+
// #ab-add, #ab-goals), and this file only ever fills one of them. That keeps the
|
|
32
|
+
// redraws apart, and lets tests/hall_runner_button.mjs run this file for real
|
|
33
|
+
// against stand-in elements instead of reading it.
|
|
34
|
+
//
|
|
35
|
+
// THE GOAL FORM IS DRAWN ONCE, NOT ON THE TIMER. The devices redraw every few
|
|
36
|
+
// seconds; a form redrawn under someone ticking boxes would untick them.
|
|
20
37
|
//
|
|
21
38
|
// Its own file on the settings-interaction.js precedent: settings.js sits against
|
|
22
39
|
// the 1500-line ratchet, and this panel shares nothing with the rest of the page.
|
|
@@ -27,27 +44,41 @@
|
|
|
27
44
|
const API = '/api/bongos';
|
|
28
45
|
const { escapeHtml, $ } = window.OTB;
|
|
29
46
|
const { emptyStateHtml } = window.OTBKit;
|
|
47
|
+
const RB = window.OTBRunnerButton;
|
|
30
48
|
const toast = (msg) => window.OTB.toast(msg);
|
|
31
49
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
32
50
|
|
|
33
51
|
const toAuth = () => window.location.assign(`${API}/auth/web/start`);
|
|
34
52
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
53
|
+
// How often the devices are read again: quickly while a press is in flight or
|
|
54
|
+
// a new device is being added, slowly otherwise.
|
|
55
|
+
const POLL_FAST_MS = 3000;
|
|
56
|
+
const POLL_SLOW_MS = 15000;
|
|
57
|
+
|
|
58
|
+
const view = {
|
|
59
|
+
fence: null,
|
|
60
|
+
runners: [],
|
|
61
|
+
busyHost: null, // a press whose request has not answered yet
|
|
62
|
+
add: null, // { commands, platform, known:Set<host> } | { error } | null
|
|
63
|
+
timer: null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The server's own sentence when it sent one; otherwise `fallback`, which is a
|
|
67
|
+
// whole sentence in the page's one wording for a failed action ("Could not …").
|
|
68
|
+
const errorText = (res, fallback) => {
|
|
69
|
+
const env = res && res.data && res.data.error;
|
|
70
|
+
return (env && typeof env === 'object' && typeof env.message === 'string' && env.message) || fallback;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ── the lines above everything ────────────────────────────────────────────
|
|
74
|
+
// Why NO device of yours can work, when that is the project's or the owner's
|
|
75
|
+
// doing. A device that is merely idle on one of these says so on its own card.
|
|
37
76
|
function noticesHtml(fence) {
|
|
38
77
|
const project = fence.project || {};
|
|
39
78
|
const scope = fence.scope || {};
|
|
40
79
|
const out = [];
|
|
41
|
-
// First, because it is the one that has already cost time: a runner of yours
|
|
42
|
-
// that has been idle on a setting past its grace (task 1004539). The verdict
|
|
43
|
-
// and the fix are the server's (runner-health.js); this only draws them.
|
|
44
|
-
for (const r of myRunners) {
|
|
45
|
-
const h = r.health || {};
|
|
46
|
-
if (h.state !== 'idle_config') continue;
|
|
47
|
-
out.push(`<strong>Your runner${h.host ? ` on ${escapeHtml(h.host)}` : ''} is idle</strong> because ${escapeHtml(h.why || 'of a setting')}. ${escapeHtml(h.remedy || '')}`);
|
|
48
|
-
}
|
|
49
80
|
if (project.enabled !== true) {
|
|
50
|
-
out.push(`The project's runner switch is off right now${project.paused_reason ? `: ${escapeHtml(project.paused_reason)}` : ''}. Your choices are kept and apply when it is back on.`);
|
|
81
|
+
out.push(`The project's runner switch is off right now${project.paused_reason ? `: ${escapeHtml(project.paused_reason)}` : ''}. Your devices and choices are kept and apply when it is back on.`);
|
|
51
82
|
}
|
|
52
83
|
if (scope.owner_paused) {
|
|
53
84
|
out.push(`The owner paused your runner${scope.owner_paused_reason ? `: ${escapeHtml(scope.owner_paused_reason)}` : ''}. Only the owner can resume it.`);
|
|
@@ -70,31 +101,204 @@
|
|
|
70
101
|
}
|
|
71
102
|
const goalLabel = (id) => goalNames.get(Number(id)) || `Goal ${id}`;
|
|
72
103
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
// ── your devices ──────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function deviceCardHtml(runner, nowMs) {
|
|
107
|
+
const host = String(runner.host || '');
|
|
108
|
+
const h = runner.health || {};
|
|
109
|
+
const b = RB.buttonState({ runner, fence: view.fence, nowMs });
|
|
110
|
+
const pressing = view.busyHost === host;
|
|
111
|
+
// The accent door when a press starts it; the quiet button otherwise.
|
|
112
|
+
const look = b.act === 'run' ? 'btn rb-button' : `settings-btn rb-button${b.busy || !b.act ? ' rb-button--busy' : ''}`;
|
|
113
|
+
const face = [
|
|
114
|
+
b.status ? `<span class="rb-button__status">${escapeHtml(b.status)}</span>` : '',
|
|
115
|
+
b.countdown ? `<span class="rb-button__count">${escapeHtml(b.countdown)}</span>` : '',
|
|
116
|
+
b.label ? `<span class="rb-button__act">${escapeHtml(b.label)}</span>` : '',
|
|
117
|
+
].join('');
|
|
118
|
+
return `
|
|
119
|
+
<li class="rb-card" data-host="${escapeHtml(host)}">
|
|
120
|
+
<div class="rb-card__head">
|
|
121
|
+
<span class="rb-dot rb-dot--${escapeHtml(b.tone)}" aria-hidden="true"></span>
|
|
122
|
+
<strong class="rb-card__name">${escapeHtml(h.host || host)}</strong>
|
|
123
|
+
${h.account ? `<span class="rb-card__meta">Claude: ${escapeHtml(h.account)}</span>` : ''}
|
|
124
|
+
<button type="button" class="settings-btn settings-btn--quiet rb-card__remove" data-focus="remove:${escapeHtml(host)}" data-remove="${escapeHtml(host)}">Remove</button>
|
|
125
|
+
</div>
|
|
126
|
+
<button type="button" class="${look}" data-focus="press:${escapeHtml(host)}"
|
|
127
|
+
${b.act ? `data-press="${escapeHtml(b.act)}"` : ''} data-host="${escapeHtml(host)}"${!b.act || pressing ? ' disabled' : ''}>${face}</button>
|
|
128
|
+
<p class="rb-card__sub">${escapeHtml(b.sub)}</p>
|
|
129
|
+
${b.stayOn ? '<p class="rb-card__note">Keep this device on and plugged in while it runs. If it sleeps, the runner pauses and picks up when it wakes.</p>' : ''}
|
|
130
|
+
${b.alt ? `<button type="button" class="settings-btn settings-btn--quiet" data-focus="alt:${escapeHtml(host)}" data-press="${escapeHtml(b.alt.act)}" data-host="${escapeHtml(host)}">${escapeHtml(b.alt.label)}</button>` : ''}
|
|
131
|
+
</li>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderDevices() {
|
|
135
|
+
const el = $('#ab-devices');
|
|
136
|
+
if (!el) return;
|
|
137
|
+
const active = document.activeElement;
|
|
138
|
+
const focusKey = active && typeof el.contains === 'function' && el.contains(active) && typeof active.getAttribute === 'function'
|
|
139
|
+
? active.getAttribute('data-focus') : null;
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
el.innerHTML = `
|
|
142
|
+
<h3 class="rb-h">Your devices</h3>
|
|
143
|
+
${view.runners.length
|
|
144
|
+
? `<ul class="rb-cards">${view.runners.map((r) => deviceCardHtml(r, now)).join('')}</ul>`
|
|
145
|
+
: emptyStateHtml('No device is connected yet. Add this one, or any Windows PC or Mac you can leave on.')}`;
|
|
146
|
+
// A redraw replaces the buttons, so put the keyboard back where it was.
|
|
147
|
+
if (focusKey) {
|
|
148
|
+
const again = [...el.querySelectorAll('[data-focus]')].find((n) => n.getAttribute('data-focus') === focusKey);
|
|
149
|
+
if (again && !again.disabled) again.focus();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function press(host, act) {
|
|
154
|
+
const run = act === 'run';
|
|
155
|
+
view.busyHost = host;
|
|
156
|
+
renderDevices();
|
|
77
157
|
try {
|
|
78
|
-
const res = await api.request('
|
|
79
|
-
if (res.
|
|
80
|
-
|
|
158
|
+
const res = await api.request('POST', `${API}/autonomy/runners/${encodeURIComponent(host)}/run`, { body: { run } });
|
|
159
|
+
if (res.status === 401) { toAuth(); return; }
|
|
160
|
+
if (!res.ok) throw new Error(errorText(res, `Could not ${run ? 'start' : 'stop'} it on that device. Try again.`));
|
|
161
|
+
// Draw the press at once from the server's own answer, then read the rest.
|
|
162
|
+
const mine = view.runners.find((r) => r.host === host);
|
|
163
|
+
if (mine) { mine.run_requested = res.data.run_requested; mine.run_changed_at = res.data.run_changed_at; }
|
|
164
|
+
toast(run ? 'Starting it on that device.' : 'Stopping it on that device.');
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error('[settings] runner press failed', err);
|
|
167
|
+
toast(err.message || `Could not ${run ? 'start' : 'stop'} it on that device. Try again.`);
|
|
168
|
+
} finally {
|
|
169
|
+
view.busyHost = null;
|
|
170
|
+
renderDevices();
|
|
171
|
+
refresh();
|
|
172
|
+
}
|
|
81
173
|
}
|
|
82
174
|
|
|
83
|
-
function
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
175
|
+
async function removeDevice(host, trigger) {
|
|
176
|
+
const runner = view.runners.find((r) => r.host === host);
|
|
177
|
+
if (!runner) return;
|
|
178
|
+
const own = !!runner.session_id;
|
|
179
|
+
const ok = await window.OTBDialog.confirm({
|
|
180
|
+
title: `Remove ${host}?`,
|
|
181
|
+
body: own
|
|
182
|
+
? 'Its runner is signed out at once and stops working for you. Anything it was building is handed back. To use this device again, add it again.'
|
|
183
|
+
: 'This device runs on your own command-line sign-in, not a login of its own, so removing it here only clears its card. If its runner is still running it will come back; stop it on the device.',
|
|
184
|
+
confirmLabel: 'Remove',
|
|
185
|
+
danger: true,
|
|
186
|
+
trigger,
|
|
187
|
+
});
|
|
188
|
+
if (!ok) return;
|
|
189
|
+
try {
|
|
190
|
+
if (own) {
|
|
191
|
+
// The login first: a card that is gone while its login still works would
|
|
192
|
+
// be a runner nobody can see.
|
|
193
|
+
const rev = await api.request('POST', `${API}/me/sessions/revoke`, { body: { session_id: runner.session_id } });
|
|
194
|
+
if (rev.status === 401) { toAuth(); return; }
|
|
195
|
+
if (!rev.ok) throw new Error(errorText(rev, 'Could not sign that device out. Try again.'));
|
|
196
|
+
}
|
|
197
|
+
const del = await api.request('DELETE', `${API}/autonomy/runners/${encodeURIComponent(host)}`);
|
|
198
|
+
if (!del.ok && del.status !== 404) throw new Error(errorText(del, 'Could not remove that device. Try again.'));
|
|
199
|
+
toast(`${host} removed.`);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error('[settings] remove device failed', err);
|
|
202
|
+
toast(err.message || 'Could not remove that device. Try again.');
|
|
203
|
+
}
|
|
204
|
+
refresh();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── add a device ──────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function addPanelHtml() {
|
|
210
|
+
const a = view.add;
|
|
211
|
+
if (!a) return '<button type="button" class="settings-btn" id="ab-add-open">Add a device</button>';
|
|
212
|
+
if (a.error) {
|
|
213
|
+
return `<p class="prow__hint">${escapeHtml(a.error)}</p>
|
|
214
|
+
<button type="button" class="settings-btn" id="ab-add-open">Try again</button>`;
|
|
215
|
+
}
|
|
216
|
+
const cmd = a.commands[a.platform];
|
|
217
|
+
const where = a.platform === 'mac' ? 'Terminal' : 'PowerShell (not as administrator)';
|
|
218
|
+
return `
|
|
219
|
+
<div class="rb-add">
|
|
220
|
+
<div class="rb-add__tabs" role="group" aria-label="Which kind of device">
|
|
221
|
+
<button type="button" class="settings-btn${a.platform === 'windows' ? '' : ' settings-btn--quiet'}" data-platform="windows" aria-pressed="${a.platform === 'windows'}">Windows</button>
|
|
222
|
+
<button type="button" class="settings-btn${a.platform === 'mac' ? '' : ' settings-btn--quiet'}" data-platform="mac" aria-pressed="${a.platform === 'mac'}">Mac</button>
|
|
223
|
+
</div>
|
|
224
|
+
<ol class="rb-add__steps">
|
|
225
|
+
<li>On the device, open ${escapeHtml(where)}.</li>
|
|
226
|
+
<li>Paste this line and press Enter.</li>
|
|
227
|
+
<li>Sign in to Claude and to GitHub if it asks. Nothing else needs you.</li>
|
|
228
|
+
</ol>
|
|
229
|
+
<div class="quote">
|
|
230
|
+
<p class="quote__eyebrow">setup command</p>
|
|
231
|
+
<pre class="quote__pre" id="ab-add-cmd">${escapeHtml(cmd)}</pre>
|
|
232
|
+
<div class="quote__foot">
|
|
233
|
+
<button type="button" class="settings-btn settings-btn--quiet" id="ab-add-copy">Copy</button>
|
|
234
|
+
<span class="settings-card__hint">The code in it works once, for 15 minutes.</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<p class="rb-add__wait" role="status">Waiting for the device to check in. Its card appears above by itself.</p>
|
|
238
|
+
<button type="button" class="settings-btn settings-btn--quiet" id="ab-add-close">Done</button>
|
|
239
|
+
</div>`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderAdd() {
|
|
243
|
+
const el = $('#ab-add');
|
|
244
|
+
if (!el) return;
|
|
245
|
+
el.innerHTML = addPanelHtml();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The project's repository address, from the same public read the Primer uses.
|
|
249
|
+
async function repoUrl() {
|
|
250
|
+
try {
|
|
251
|
+
const res = await api.request('GET', `${API}/public/repo-info`);
|
|
252
|
+
const base = res.ok && res.data && typeof res.data.blob_base === 'string' ? res.data.blob_base : '';
|
|
253
|
+
const cut = base.indexOf('/blob/');
|
|
254
|
+
return cut > 0 ? base.slice(0, cut) : null;
|
|
255
|
+
} catch (_) { return null; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function openAdd() {
|
|
259
|
+
view.add = null;
|
|
260
|
+
try {
|
|
261
|
+
const [issued, repo] = await Promise.all([
|
|
262
|
+
api.request('POST', `${API}/auth/runner-code/issue`, { body: {} }),
|
|
263
|
+
repoUrl(),
|
|
264
|
+
]);
|
|
265
|
+
if (issued.status === 401) { toAuth(); return; }
|
|
266
|
+
if (!issued.ok) throw new Error(errorText(issued, 'Could not make a setup code. Try again.'));
|
|
267
|
+
const commands = RB.setupCommands({
|
|
268
|
+
apiBase: issued.data.api_base || window.location.origin,
|
|
269
|
+
code: issued.data.code,
|
|
270
|
+
repoUrl: repo,
|
|
271
|
+
});
|
|
272
|
+
if (!commands) throw new Error('This project has no repository address set, so the setup command cannot be built. Tell the project owner.');
|
|
273
|
+
view.add = {
|
|
274
|
+
commands,
|
|
275
|
+
platform: RB.guessPlatform(window.navigator.userAgent),
|
|
276
|
+
// The devices as last read (the timer keeps that fresh). A device that is
|
|
277
|
+
// not in this set when it next appears is the one just added.
|
|
278
|
+
known: new Set(view.runners.map((r) => r.host)),
|
|
279
|
+
};
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error('[settings] add device failed', err);
|
|
282
|
+
view.add = { error: err.message || 'Could not start adding a device. Try again.' };
|
|
90
283
|
}
|
|
284
|
+
renderAdd();
|
|
285
|
+
schedule();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── what it works on ──────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
function renderGoals(fence) {
|
|
291
|
+
const el = $('#ab-goals');
|
|
292
|
+
if (!el) return;
|
|
293
|
+
const scope = fence.scope || {};
|
|
91
294
|
const goals = (fence.project && Array.isArray(fence.project.goals)) ? fence.project.goals : [];
|
|
92
295
|
if (!goals.length) {
|
|
93
|
-
|
|
296
|
+
el.innerHTML = `<h3 class="rb-h">What it works on</h3>${emptyStateHtml('The project has not allowed any goals for runners yet, so there is nothing to choose from.')}`;
|
|
94
297
|
return;
|
|
95
298
|
}
|
|
96
299
|
const picked = new Set((scope.goals || []).map(Number));
|
|
97
|
-
|
|
300
|
+
el.innerHTML = `
|
|
301
|
+
<h3 class="rb-h">What it works on</h3>
|
|
98
302
|
<form id="autobongos-form">
|
|
99
303
|
<div class="prows">
|
|
100
304
|
${goals.map((g) => {
|
|
@@ -113,18 +317,6 @@
|
|
|
113
317
|
<span class="settings-card__hint">${picked.size ? `Your runner works ${picked.size} goal${picked.size === 1 ? '' : 's'}.` : 'Your runner takes no work until you choose at least one goal.'}</span>
|
|
114
318
|
</div>
|
|
115
319
|
</form>`;
|
|
116
|
-
$('#autobongos-form').addEventListener('submit', (e) => {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
const ids = [...e.target.querySelectorAll('input[name="autobongos-goal"]:checked')].map((i) => Number(i.value));
|
|
119
|
-
save(ids);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function load() {
|
|
124
|
-
const res = await api.request('GET', `${API}/autonomy/fence`);
|
|
125
|
-
if (res.status === 401) { toAuth(); return null; }
|
|
126
|
-
if (!res.ok) throw new Error(`fence GET → ${res.status}`);
|
|
127
|
-
return res.data;
|
|
128
320
|
}
|
|
129
321
|
|
|
130
322
|
async function save(goalIds) {
|
|
@@ -135,15 +327,12 @@
|
|
|
135
327
|
try {
|
|
136
328
|
const res = await api.request('PUT', `${API}/autonomy/me/goals`, { body: { goal_ids: goalIds } });
|
|
137
329
|
if (res.status === 401) { toAuth(); return; }
|
|
138
|
-
if (!res.ok)
|
|
139
|
-
const env = res.data && res.data.error;
|
|
140
|
-
throw new Error((env && env.message) || `could not save (${res.status})`);
|
|
141
|
-
}
|
|
330
|
+
if (!res.ok) throw new Error(errorText(res, 'Could not save your runner’s goals. Try again.'));
|
|
142
331
|
const msg = goalIds.length ? `Saved — your runner works ${goalIds.length} goal${goalIds.length === 1 ? '' : 's'}.` : 'Saved — your runner takes no work.';
|
|
143
332
|
if (status) status.textContent = msg;
|
|
144
333
|
toast(msg);
|
|
145
|
-
const fence = await
|
|
146
|
-
if (fence)
|
|
334
|
+
const fence = await loadFence();
|
|
335
|
+
if (fence) { view.fence = fence; renderGoals(fence); renderNotices(); renderDevices(); }
|
|
147
336
|
} catch (err) {
|
|
148
337
|
console.error('[settings] save autobongos goals failed', err);
|
|
149
338
|
if (status) status.textContent = `Could not save: ${err.message}`;
|
|
@@ -152,20 +341,117 @@
|
|
|
152
341
|
}
|
|
153
342
|
}
|
|
154
343
|
|
|
344
|
+
// ── reads, and the timer ──────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
async function loadFence() {
|
|
347
|
+
const res = await api.request('GET', `${API}/autonomy/fence`);
|
|
348
|
+
if (res.status === 401) { toAuth(); return null; }
|
|
349
|
+
if (!res.ok) throw new Error(`fence GET → ${res.status}`);
|
|
350
|
+
return res.data;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The caller's own devices, with the server's health verdict attached. Null
|
|
354
|
+
// when the read failed: the last answer stays on screen rather than every card
|
|
355
|
+
// vanishing over one bad request.
|
|
356
|
+
async function loadRunners() {
|
|
357
|
+
try {
|
|
358
|
+
const res = await api.request('GET', `${API}/autonomy/runners`);
|
|
359
|
+
if (res.ok && res.data && Array.isArray(res.data.runners)) return res.data.runners;
|
|
360
|
+
} catch (_) { /* keep the last answer */ }
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderNotices() {
|
|
365
|
+
const el = $('#ab-notices');
|
|
366
|
+
if (el && view.fence) el.innerHTML = noticesHtml(view.fence);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function refresh() {
|
|
370
|
+
const [runners, fence] = await Promise.all([loadRunners(), loadFence().catch(() => null)]);
|
|
371
|
+
if (runners) view.runners = runners;
|
|
372
|
+
if (fence) view.fence = fence;
|
|
373
|
+
// A device that was not there when the command was copied has checked in.
|
|
374
|
+
if (view.add && view.add.known && view.runners.some((r) => !view.add.known.has(r.host))) {
|
|
375
|
+
view.add = null;
|
|
376
|
+
renderAdd();
|
|
377
|
+
toast('Device connected. Press Run on its card.');
|
|
378
|
+
}
|
|
379
|
+
renderNotices();
|
|
380
|
+
renderDevices();
|
|
381
|
+
schedule();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function wantsFastPoll() {
|
|
385
|
+
if (view.add && view.add.commands) return true;
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
return view.runners.some((r) => RB.buttonState({ runner: r, fence: view.fence, nowMs: now }).busy);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function schedule() {
|
|
391
|
+
if (view.timer) window.clearTimeout(view.timer);
|
|
392
|
+
if (document.hidden) return; // a hidden tab reads nothing; it catches up when shown
|
|
393
|
+
view.timer = window.setTimeout(refresh, wantsFastPoll() ? POLL_FAST_MS : POLL_SLOW_MS);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function wire(body) {
|
|
397
|
+
// One listener for the whole panel: the devices redraw on a timer, and a
|
|
398
|
+
// listener per button would be re-attached every few seconds.
|
|
399
|
+
body.addEventListener('click', (e) => {
|
|
400
|
+
const t = e.target.closest('button');
|
|
401
|
+
if (!t || t.disabled || !body.contains(t)) return;
|
|
402
|
+
if (t.dataset.press) { press(t.dataset.host, t.dataset.press); return; }
|
|
403
|
+
if (t.dataset.remove) { removeDevice(t.dataset.remove, t); return; }
|
|
404
|
+
if (t.id === 'ab-add-open') { openAdd(); return; }
|
|
405
|
+
if (t.id === 'ab-add-close') { view.add = null; renderAdd(); schedule(); return; }
|
|
406
|
+
if (t.dataset.platform && view.add && view.add.commands) { view.add.platform = t.dataset.platform; renderAdd(); return; }
|
|
407
|
+
if (t.id === 'ab-add-copy' && view.add && view.add.commands) {
|
|
408
|
+
const text = view.add.commands[view.add.platform];
|
|
409
|
+
window.navigator.clipboard.writeText(text).then(
|
|
410
|
+
() => toast('Setup command copied.'),
|
|
411
|
+
// The same words the CLI token panel on this page uses for the same thing.
|
|
412
|
+
() => toast('Could not copy automatically — select and copy manually.'),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
body.addEventListener('submit', (e) => {
|
|
417
|
+
const form = e.target;
|
|
418
|
+
if (!form || form.id !== 'autobongos-form') return;
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
save([...form.querySelectorAll('input[name="autobongos-goal"]:checked')].map((i) => Number(i.value)));
|
|
421
|
+
});
|
|
422
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
|
|
423
|
+
}
|
|
424
|
+
|
|
155
425
|
async function setup() {
|
|
156
426
|
const section = $('#autobongos-scroll');
|
|
157
427
|
if (!section) return;
|
|
428
|
+
const body = $('#autobongos-body');
|
|
158
429
|
try {
|
|
159
|
-
const [fence] = await Promise.all([
|
|
430
|
+
const [fence, , runners] = await Promise.all([loadFence(), loadGoalNames(), loadRunners()]);
|
|
160
431
|
if (!fence) return;
|
|
161
|
-
|
|
432
|
+
view.fence = fence;
|
|
433
|
+
view.runners = runners || [];
|
|
434
|
+
const scope = fence.scope || {};
|
|
435
|
+
if (scope.may_run !== true) {
|
|
436
|
+
// No devices, no Add a device, no picks: one line, in the devices slot.
|
|
437
|
+
$('#ab-devices').innerHTML = emptyStateHtml('Your rank cannot run an Autobongos runner on this project. Ask the owner if you need one.');
|
|
438
|
+
section.hidden = false;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
wire(body);
|
|
442
|
+
renderNotices();
|
|
443
|
+
renderDevices();
|
|
444
|
+
renderAdd();
|
|
445
|
+
renderGoals(fence);
|
|
162
446
|
section.hidden = false;
|
|
447
|
+
schedule();
|
|
163
448
|
} catch (err) {
|
|
164
449
|
// Its own try/catch, like every other panel: a failing read shows this
|
|
165
450
|
// panel's own error rather than taking the settings page down with it.
|
|
166
451
|
console.error('[settings] failed to load autobongos scope', err);
|
|
167
452
|
section.hidden = false;
|
|
168
|
-
$('#
|
|
453
|
+
const slot = $('#ab-devices') || body;
|
|
454
|
+
slot.innerHTML =
|
|
169
455
|
'<p class="voices-empty">Your runner settings could not be loaded. Reload the page to try again.</p>';
|
|
170
456
|
}
|
|
171
457
|
}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
has — the rail layout, the group head, the token ladder, the voices grid. -->
|
|
19
19
|
<link rel="stylesheet" href="/builders/panel.css">
|
|
20
20
|
<link rel="stylesheet" href="/builders/settings.css">
|
|
21
|
+
<!-- task 1005094: the device cards and their Run button in Your Autobongos runner. -->
|
|
22
|
+
<link rel="stylesheet" href="/builders/runner-button.css">
|
|
21
23
|
</head>
|
|
22
24
|
<body data-page="settings" data-page-title="Settings">
|
|
23
25
|
|
|
@@ -217,20 +219,29 @@
|
|
|
217
219
|
<p class="section-note">Hold the card to your reader or touch it when your browser asks. Keep more than one registered in case one is lost.</p>
|
|
218
220
|
</section>
|
|
219
221
|
|
|
220
|
-
<!-- YOUR AUTOBONGOS RUNNER —
|
|
222
|
+
<!-- YOUR AUTOBONGOS RUNNER — your devices, each with ONE Run button
|
|
223
|
+
(task 1005094), a way to add one, and which goals your runner
|
|
221
224
|
works (task 1004501). Rendered by settings-autobongos.js against
|
|
222
|
-
GET /autonomy/
|
|
223
|
-
|
|
224
|
-
work. The project allowlist and the master
|
|
225
|
-
owner's, on
|
|
225
|
+
GET /autonomy/runners + GET /autonomy/fence; what each button
|
|
226
|
+
says is decided by runner-button.js. Picking no goals means your
|
|
227
|
+
runner takes no work. The project allowlist and the master
|
|
228
|
+
switch stay the owner's, on Watch > Autobongos. -->
|
|
226
229
|
<section class="scroll" id="autobongos-scroll" aria-labelledby="autobongos-h" data-group="access" hidden>
|
|
227
230
|
<h2 id="autobongos-h" class="scroll__h">Your Autobongos runner</h2>
|
|
228
231
|
<p class="scroll__lede">
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
Add a device, press Run on its card, and it builds the goals you
|
|
233
|
+
choose below while you are away. Any Windows PC or Mac works, as
|
|
234
|
+
long as it is on and you are signed in to it.
|
|
232
235
|
</p>
|
|
233
|
-
|
|
236
|
+
<!-- Four slots, each filled on its own (settings-autobongos.js). Not a
|
|
237
|
+
live region: the devices redraw on a timer, and a region that
|
|
238
|
+
re-announced every redraw would talk over the reader. -->
|
|
239
|
+
<div id="autobongos-body">
|
|
240
|
+
<div id="ab-notices"></div>
|
|
241
|
+
<div id="ab-devices"></div>
|
|
242
|
+
<div id="ab-add" class="rb-addwrap"></div>
|
|
243
|
+
<div id="ab-goals"></div>
|
|
244
|
+
</div>
|
|
234
245
|
<p class="settings-status" id="autobongos-status" role="status" aria-live="polite"></p>
|
|
235
246
|
</section>
|
|
236
247
|
|
|
@@ -418,7 +429,9 @@
|
|
|
418
429
|
<script src="/builders/update-pill.js?v=2026-11-03-pill"></script>
|
|
419
430
|
<script src="/builders/settings-software-update.js?v=2026-11-03-pill"></script>
|
|
420
431
|
<!-- task 1004501: Settings → Your Autobongos runner. Own file on the same precedent. -->
|
|
421
|
-
|
|
432
|
+
<!-- task 1005094: what a device's Run button says (pure; loaded before the panel). -->
|
|
433
|
+
<script src="/builders/runner-button.js"></script>
|
|
434
|
+
<script src="/builders/settings-autobongos.js?v=2026-11-09-run-button"></script>
|
|
422
435
|
<!-- task 1003793: Settings → Module install reports. Own file on the same precedent. -->
|
|
423
436
|
<script src="/builders/settings-usage-reports.js?v=2026-10-08-usage-reports"></script>
|
|
424
437
|
</body>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The runner panel — Settings > Your Autobongos runner: a card per device, one Run button each (task 1005094)
|
|
2
|
+
|
|
3
|
+
**Goal 1000139, [ADR 0307](../../../docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md) §6.** Owner direction, 2026-11-09: running Autobongos on a device is ONE button, on whatever goals it is configured for, and any cooldown is built into the button itself. The panel on `/settings#autobongos` is three things, top to bottom: your devices, Add a device, and what the runner works on (the goal picks of task 1004501, unchanged). Six things it settled:
|
|
4
|
+
|
|
5
|
+
- **The button's face IS the device's state.** Run · Starting… · Running · Resting until a time (with a countdown) · Stopping… · Updating… · Device is off or asleep · Ready (on, but held, with the reason on the line below). There is no second status line to hunt for. What the face says is decided by `public/runner-button.js` `buttonState`, which is **pure** (the `roadmap-timeline.js` precedent: no document, no fetch, `nowMs` is an argument), so `tests/hall_runner_button.mjs` asks it one face at a time. `settings-autobongos.js` draws what it returns and nothing else.
|
|
6
|
+
- **It is one of the hall's shared buttons, made larger.** `.btn` (the accent door) when a press starts the runner, `.settings-btn` when the device is on and a press would stop it; `runner-button.css` adds only size and the three spans inside the face. No colour of its own; type and corners are scale tokens.
|
|
7
|
+
- **A press is drawn from the server's own answer, then the devices are read again.** The page never decides a press took. "Starting" is `run_changed_at` being newer than the device's last check-in, capped at two minutes, so a device that never picks the press up goes on to read as off instead of starting forever.
|
|
8
|
+
- **The devices redraw on a timer; the goal form does not.** Four fixed slots in `settings.html` (`#ab-notices`, `#ab-devices`, `#ab-add`, `#ab-goals`), each filled on its own: 3 seconds while a card is busy or a device is being added, 15 otherwise, nothing while the tab is hidden. A form redrawn under someone ticking boxes would untick them. The panel is **not** a live region for the same reason: it would re-announce every redraw.
|
|
9
|
+
- **The stay-on note is on the card of a device that is switched on** ("Keep this device on and plugged in while it runs…"), and not on one its builder stopped: a stopped device has nothing to stay awake for. `buttonState` says which (`stayOn`).
|
|
10
|
+
- **Remove signs the device's own login out FIRST, then clears its card** (`POST /me/sessions/revoke`, then `DELETE /autonomy/runners/:host`). A card gone while its login still works would be a runner nobody can see. A device with no login of its own (a runner still on its builder's command-line sign-in) says so in the dialog and revokes nothing.
|
|
11
|
+
|
|
12
|
+
Add a device shows ONE command per platform (`setupCommands`), built from the one-time code, the project's own address and its repository (`GET /public/repo-info`); it refuses to build a command from any part that is not the exact shape it expects, because the line ends up in a shell, and from any project address that is not https (a project on this very machine excepted), because the line downloads a script and runs it. The card appears by itself when the device first checks in, and starts **stopped**, so the first press of Run is the person's.
|
|
13
|
+
|
|
14
|
+
The owner's own panel (Watch > Autobongos, `gate.js`) learned the `stopped` state and nothing else.
|
|
15
|
+
|
|
16
|
+
**Verifying it:** `node scripts/hall-preview/server.js --fixture-me`, then `/settings#autobongos`. The fixture carries four devices, one per face that needs no wall-clock time (running, stopped, held, off); a resting device is covered by the tests, not the fixture, because a clock time in the page's visible text would change the page reading on every read.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.79",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.79",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.79",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11493,5 +11493,25 @@
|
|
|
11493
11493
|
"id": "1005095",
|
|
11494
11494
|
"text": "The Autobongos runner can now be set up on any Windows PC or Mac, including a laptop, with no administrator step. One program installs it, starts it whenever you sign in to the device, keeps it running, updates it by itself, a"
|
|
11495
11495
|
}
|
|
11496
|
+
],
|
|
11497
|
+
"1.24.78": [
|
|
11498
|
+
{
|
|
11499
|
+
"id": "1005097",
|
|
11500
|
+
"text": "There is now a plain guide for putting Autobongos on your own Windows PC or Mac, and a /autobongos-setup command in Claude Code that walks you through it: paste one line from Settings, press Run, and read the button to see wha"
|
|
11501
|
+
},
|
|
11502
|
+
{
|
|
11503
|
+
"id": "1005187",
|
|
11504
|
+
"text": "Fixed the automatic check that had turned red for everyone and was blocking all new work from landing. A safety tool that spots finished work still listed as to-do only understood an older way of labelling changes; most change"
|
|
11505
|
+
},
|
|
11506
|
+
{
|
|
11507
|
+
"id": "1005094",
|
|
11508
|
+
"text": "Running Autobongos on a device is now one button. In Settings, Add a device gives you one line to paste on any Windows PC or Mac; its card then appears by itself with a Run button. The button's face tells you what the device i"
|
|
11509
|
+
}
|
|
11510
|
+
],
|
|
11511
|
+
"1.24.79": [
|
|
11512
|
+
{
|
|
11513
|
+
"id": "1005186",
|
|
11514
|
+
"text": "Closed a gap opened by letting Autobongos run on any device. The runner now keeps its own sign-in in its own folder, and the rule that stops an AI session from opening sign-in files did not yet cover that folder. It does now,"
|
|
11515
|
+
}
|
|
11496
11516
|
]
|
|
11497
11517
|
}
|