@bongos/core 1.24.77 → 1.24.78
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 +89 -44
- 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 +2 -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 +14 -0
- package/scripts/bongos/autobongos-host.js +16 -2
- package/scripts/bongos/autobongos-run.js +18 -3
- 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 +28 -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/hall_runner_button.mjs +467 -0
- package/tests/hall_runner_panel_connect.mjs +73 -0
- package/tests/stranded_rows.mjs +50 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: autobongos-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Set up THIS device as an Autobongos runner, or fix one that is not working: one copied command, then one Run button. Metic+. Triggers: "/autobongos-setup", "set up Autobongos on this PC", "add this Mac as a runner", "my runner is not running".
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
plain: >-
|
|
7
|
+
Turns the computer you are sitting at into one that builds the project's tasks by itself while you are away, and walks you through the two sign-ins it needs.
|
|
8
|
+
reach-for: >-
|
|
9
|
+
When you want this computer to work on the project unattended, or its runner has stopped checking in.
|
|
10
|
+
cost: >-
|
|
11
|
+
Setting it up is free and changes nothing in the project. Once you press Run, the runner spends your own Claude usage on the goals you chose, and each task it hands in runs a paid quality review charged to the project.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are setting up the Autobongos runner on the device this session is running on, with the person beside you. The whole flow is **one copied command, then one button**. Your job is to get them through it in plain words and to read what the device says back — never to rebuild the flow by hand.
|
|
15
|
+
|
|
16
|
+
Design of record: [ADR 0307](../../../../docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md) §5. The guide a person can follow alone: [docs/recipes/autobongos-runner-setup.md](../../../../docs/recipes/autobongos-runner-setup.md).
|
|
17
|
+
|
|
18
|
+
## What you must not do
|
|
19
|
+
|
|
20
|
+
- **Never run the runner from this checkout.** `autobongos-host.js run` resets its checkout to the latest code every time it starts, and it refuses any copy that is not marked as the runner's own. Do not mark this one. The setup command makes the runner its own copy under `~/.cloudbongos-runner`.
|
|
21
|
+
- **Never ask for the setup code in chat, and never type one yourself.** The person copies the whole command from the hall and pastes it into their own terminal. A code is a one-time login.
|
|
22
|
+
- **Never turn on the project's runner switch or change which goals runners may work.** Those are the owner's, on Watch → Autobongos. If they are what is holding the runner, say so and stop.
|
|
23
|
+
|
|
24
|
+
## 1. Is it already here?
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node ~/.cloudbongos-runner/repo/scripts/bongos/autobongos-host.js status
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **The file does not exist** → the device has no runner yet. Go to step 2.
|
|
31
|
+
- **`running : yes`** → it is set up and running. Skip to step 4 (is it *working*?).
|
|
32
|
+
- **`running : no`, signed in** → start it: `node ~/.cloudbongos-runner/repo/scripts/bongos/autobongos-host.js start`, then step 4.
|
|
33
|
+
- **No runner login** → its login was removed or never made. Go to step 2; the same command repairs it.
|
|
34
|
+
|
|
35
|
+
On Windows the folder is `%USERPROFILE%\.cloudbongos-runner`.
|
|
36
|
+
|
|
37
|
+
## 2. The one command
|
|
38
|
+
|
|
39
|
+
Tell the person, in these words or close to them:
|
|
40
|
+
|
|
41
|
+
> Open the hall, go to **Settings → Access & keys → Your Autobongos runner**, and press **Add a device**. Pick Windows or Mac. Copy the line it shows and paste it into a new **PowerShell** window (Windows, an ordinary one, not "as administrator") or **Terminal** (Mac). It works once and for 15 minutes, so paste it straight away.
|
|
42
|
+
|
|
43
|
+
Then wait for them. What the command does, so you can answer questions without guessing:
|
|
44
|
+
|
|
45
|
+
1. Finds git, Node and Claude Code, and installs whichever is missing. On Windows, Windows itself may ask "allow this app to make changes" while it installs; that is expected. On a Mac with no developer tools, macOS opens its own install window — they press Install, wait, and paste the command again.
|
|
46
|
+
2. Makes the runner **its own copy** of the project. GitHub may ask them to sign in; it must be an account that can see the project.
|
|
47
|
+
3. Signs the device in to the project with **a login of its own**. Their own command-line sign-in on this device is not touched.
|
|
48
|
+
4. Checks Claude Code is signed in, and opens the Claude sign-in if it is not.
|
|
49
|
+
5. Registers the runner to start whenever they sign in to the device, starts it, and waits for its first check-in.
|
|
50
|
+
|
|
51
|
+
It ends with `Done. This device is connected…`. If it ends with `Autobongos setup stopped here: …`, read them the **What to do** line — it is written for them — and run the command again after they have done it. Running it twice is safe.
|
|
52
|
+
|
|
53
|
+
## 3. Press Run
|
|
54
|
+
|
|
55
|
+
A device added this way starts **stopped**, on purpose: the first press is theirs.
|
|
56
|
+
|
|
57
|
+
> Back in Settings, your device now has a card. Tick the goals you want it to work under **What it works on**, press **Save**, then press **Run** on the card.
|
|
58
|
+
|
|
59
|
+
The button then says what the device is doing: **Starting** (under a minute), **Running**, **Resting until** a time (their Claude usage is spent; it resumes by itself), or **Device is off or asleep**.
|
|
60
|
+
|
|
61
|
+
Tell them the one rule of thumb: **keep the device on and plugged in while it runs.** If it sleeps, the runner pauses and picks up when it wakes. The runner keeps the device from idle-sleeping while it is allowed to work, but a closed laptop lid still sleeps.
|
|
62
|
+
|
|
63
|
+
## 4. Is it working?
|
|
64
|
+
|
|
65
|
+
The card in Settings is the answer; read it with them. If the button's second line says it is on **but** something holds it, that line names the cause:
|
|
66
|
+
|
|
67
|
+
| The card says | Whose move |
|
|
68
|
+
|---|---|
|
|
69
|
+
| the project's runner switch is off | the owner's (Watch → Autobongos). Not yours to flip. |
|
|
70
|
+
| the owner paused your runner | the owner's |
|
|
71
|
+
| no goals are chosen for it yet | theirs: tick a goal under **What it works on**, Save |
|
|
72
|
+
| the project has not allowed any goals for runners yet | the owner's |
|
|
73
|
+
| idle because … | read them the rest of the line; it carries the fix |
|
|
74
|
+
| Device is off or asleep | theirs: wake it and sign in |
|
|
75
|
+
|
|
76
|
+
For anything the card does not explain, the device's own log is at `~/.cloudbongos-runner/logs/autobongos.log`. Read the last screen of it; do not paste it at the person.
|
|
77
|
+
|
|
78
|
+
## 5. Stopping and removing
|
|
79
|
+
|
|
80
|
+
- **Stop for now:** the **Stop** button on the card. It finishes the task in hand first.
|
|
81
|
+
- **Stop it on the device itself** (maintenance): `node ~/.cloudbongos-runner/repo/scripts/bongos/autobongos-host.js stop`. It says `Stopped` only after it has watched the processes exit; if it says `NOT STOPPED`, read them the command it prints.
|
|
82
|
+
- **Take the device out for good:** **Remove** on the card signs the device's login out at once. Then on the device: `…autobongos-host.js uninstall`, and delete the `.cloudbongos-runner` folder.
|
|
83
|
+
|
|
84
|
+
## An always-on PC that must start with nobody signed in
|
|
85
|
+
|
|
86
|
+
The setup above starts the runner when the person signs in, which is right for a laptop or a desktop. A Windows PC that must come back after a power cut with nobody at the keyboard uses the boot-time install instead, which needs an Administrator window once: [docs/recipes/autobongos-windows-host.md](../../../../docs/recipes/autobongos-windows-host.md). One runner per device: setup refuses to add a second beside it.
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
{
|
|
47
47
|
key: 'setup', label: 'Your account and new projects',
|
|
48
48
|
blurb: 'Signing in, keys, costs, and starting a project from scratch.',
|
|
49
|
-
names: ['builder-setup', 'builder-reauth', 'builder-key', 'builder-cost', 'new-project'],
|
|
49
|
+
names: ['builder-setup', 'builder-reauth', 'builder-key', 'builder-cost', 'autobongos-setup', 'new-project'],
|
|
50
50
|
},
|
|
51
51
|
];
|
|
52
52
|
const OTHER_GROUP = { key: 'other', label: 'Everything else', blurb: 'Skills this project has that no group above names yet.' };
|
|
@@ -96,8 +96,11 @@
|
|
|
96
96
|
// only the order to pick the runner to describe in, and the colour of its dot.
|
|
97
97
|
// idle_config (task 1004539): talking, but idle on a setting the owner can fix —
|
|
98
98
|
// it carries the fix as `remedy`, and draws as a warning, never as "ok".
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// stopped (task 1005094): checking in, and its builder pressed Stop on that
|
|
100
|
+
// device in Settings. Connected, not an alarm, and not the one to describe
|
|
101
|
+
// while another device is working.
|
|
102
|
+
const HEALTH_RANK = { alive: 0, late: 1, idle_config: 2, restarting: 3, stopped: 4, dead: 5, never_seen: 6 };
|
|
103
|
+
const HEALTH_DOT = { alive: 'ok', late: 'ok', idle_config: 'warn', restarting: 'info', stopped: 'info', dead: 'danger', never_seen: 'info' };
|
|
101
104
|
|
|
102
105
|
// The panel is drawn from both reads at once; each is kept so a write that only
|
|
103
106
|
// reloads one of them redraws with the other's last answer.
|
|
@@ -121,11 +124,13 @@
|
|
|
121
124
|
// `project`, and then there is no second layer to check.
|
|
122
125
|
function readiness(runner, fence, mine) {
|
|
123
126
|
const h = (runner && runner.health) || {};
|
|
124
|
-
const connected = ['alive', 'late', 'restarting', 'idle_config'].includes(h.state);
|
|
127
|
+
const connected = ['alive', 'late', 'restarting', 'idle_config', 'stopped'].includes(h.state);
|
|
125
128
|
const goals = (fence && fence.goals) || [];
|
|
126
129
|
const items = [];
|
|
127
130
|
if (!runner) {
|
|
128
|
-
|
|
131
|
+
// The step that used to say nothing about HOW (task 1005097): it opens the
|
|
132
|
+
// Settings flow, where Add a device hands out the one setup command.
|
|
133
|
+
items.push({ key: 'pc', state: 'todo', text: 'Connect a PC', hint: 'Add a device in Settings: one copied command on any Windows PC or Mac. It shows up here when it first checks in.', action: 'add-device' });
|
|
129
134
|
} else if (!connected) {
|
|
130
135
|
items.push({ key: 'pc', state: 'todo', text: `${h.host || 'The PC'} has gone quiet`, hint: `No check-in for ${h.age || 'a while'}. Check the PC is on, then restart the runner.` });
|
|
131
136
|
} else {
|
|
@@ -165,6 +170,7 @@
|
|
|
165
170
|
function headline(runner) {
|
|
166
171
|
const h = (runner && runner.health) || {};
|
|
167
172
|
if (h.state === 'restarting') return 'Restarting to pick up new code';
|
|
173
|
+
if (h.state === 'stopped') return 'Stopped on its device';
|
|
168
174
|
if (h.workingTaskId) return `Working on task ${h.workingTaskId}`;
|
|
169
175
|
return 'Ready — waiting for a task';
|
|
170
176
|
}
|
|
@@ -179,6 +185,7 @@
|
|
|
179
185
|
${i.hint && i.state !== 'ok' ? `<span class="rn-check__hint">${escapeHtml(i.hint)}</span>` : ''}</span>
|
|
180
186
|
${i.action === 'add-goal' ? '<button type="button" class="btn btn--sm" data-rn="add-goal">Choose goals</button>' : ''}
|
|
181
187
|
${i.action === 'settings' ? '<a class="btn btn--sm" href="/builders/settings#autobongos">Open Settings</a>' : ''}
|
|
188
|
+
${i.action === 'add-device' ? '<a class="btn btn--sm" href="/builders/settings#autobongos">Add a device</a>' : ''}
|
|
182
189
|
</li>`).join('')}</ul>`;
|
|
183
190
|
}
|
|
184
191
|
|
|
@@ -381,7 +388,7 @@
|
|
|
381
388
|
: 'has picked no goals, so their runner takes no work';
|
|
382
389
|
const beats = (b.runners || []).map((r) => {
|
|
383
390
|
const h = r.health || {};
|
|
384
|
-
const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting', idle_config: `idle on a setting — ${h.remedy || 'see Settings'}` }[h.state] || 'unknown';
|
|
391
|
+
const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting', stopped: 'stopped on that device', idle_config: `idle on a setting — ${h.remedy || 'see Settings'}` }[h.state] || 'unknown';
|
|
385
392
|
return `<span class="ov-fact"><span class="rn-dot rn-dot--sm rn-dot--${HEALTH_DOT[h.state] || 'info'}" aria-hidden="true"></span> ${escapeHtml(h.host || r.host || '')} ${escapeHtml(label)}</span>`;
|
|
386
393
|
}).join('');
|
|
387
394
|
const paused = b.owner_paused === true;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* runner-button.css — a device's card and its one Run button on Settings >
|
|
2
|
+
Your Autobongos runner (task 1005094). Loaded by settings.html only.
|
|
3
|
+
|
|
4
|
+
The button's FACE is the device's state (Run, Starting, Running, Resting until
|
|
5
|
+
a time, Stopping, Device is off), so it is drawn larger than a form button and
|
|
6
|
+
carries a status word, an optional countdown and the action word side by side.
|
|
7
|
+
Tokens only: the pack's inks and grounds and the hall's status vocabulary
|
|
8
|
+
(--ok / --warn / --info), never a colour of its own. Both axes of every
|
|
9
|
+
target clear the 24px floor; the button itself is 52px tall. */
|
|
10
|
+
|
|
11
|
+
.rb-h { margin: var(--space-4) 0 var(--space-2); font-size: var(--ds-text-sm); font-weight: 500; color: var(--ink-soft); }
|
|
12
|
+
.rb-cards { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
|
|
13
|
+
.rb-card { display: grid; gap: var(--space-2); padding: var(--space-3); border: var(--line) solid var(--rule-soft); border-radius: var(--radius); background: var(--bg-card); min-width: 0; }
|
|
14
|
+
.rb-card__head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-width: 0; }
|
|
15
|
+
.rb-card__name { font-size: var(--ds-text-lg); color: var(--ink); overflow-wrap: anywhere; }
|
|
16
|
+
.rb-card__meta { font-size: var(--ds-text-sm); color: var(--ink-faint); overflow-wrap: anywhere; }
|
|
17
|
+
.rb-card__sub { margin: 0; font-size: var(--ds-text-md); color: var(--ink); }
|
|
18
|
+
/* Remove sits at the end of the card's first line: there, but not the thing the
|
|
19
|
+
eye lands on. The stay-on note is on the card of a device that is switched on. */
|
|
20
|
+
.rb-card__remove { margin-left: auto; min-height: 32px; padding: 0 var(--space-3); font-size: var(--ds-text-sm); }
|
|
21
|
+
.rb-card__note { margin: 0; font-size: var(--ds-text-sm); color: var(--ink-faint); max-width: var(--measure); }
|
|
22
|
+
|
|
23
|
+
.rb-dot { width: 10px; height: 10px; border-radius: var(--radius-pill); flex: none; background: var(--rule); }
|
|
24
|
+
.rb-dot--ok { background: var(--ok); }
|
|
25
|
+
.rb-dot--warn { background: var(--warn); }
|
|
26
|
+
.rb-dot--info { background: var(--info); }
|
|
27
|
+
|
|
28
|
+
/* The button. It IS one of the hall's shared buttons (controls.css): the accent
|
|
29
|
+
door (.btn) when a press starts the runner, the quiet one (.settings-btn) when
|
|
30
|
+
the device is on and a press would stop it. This sheet only makes it larger and
|
|
31
|
+
lets its face hold a status word, a countdown and the action side by side. */
|
|
32
|
+
.rb-button {
|
|
33
|
+
justify-self: start; gap: var(--space-2); flex-wrap: wrap;
|
|
34
|
+
min-height: 52px; min-width: 148px; max-width: 100%;
|
|
35
|
+
padding: var(--space-1) var(--space-4);
|
|
36
|
+
white-space: normal; text-align: center;
|
|
37
|
+
}
|
|
38
|
+
.rb-button--busy { border-style: dashed; cursor: default; }
|
|
39
|
+
.rb-button__status { overflow-wrap: anywhere; }
|
|
40
|
+
.rb-button__count { font-family: var(--font-mono); font-size: var(--ds-text-sm); }
|
|
41
|
+
.rb-button__act { padding: 2px 10px; border: var(--line) solid var(--rule); border-radius: var(--radius-pill); font-size: var(--ds-text-sm); }
|
|
42
|
+
|
|
43
|
+
/* Add a device. */
|
|
44
|
+
.rb-addwrap { margin-top: var(--space-3); }
|
|
45
|
+
.rb-add { display: grid; gap: var(--space-2); }
|
|
46
|
+
.rb-add__tabs { display: flex; gap: var(--space-1); flex-wrap: wrap; }
|
|
47
|
+
.rb-add__steps { margin: 0; padding-left: 20px; display: grid; gap: 4px; font-size: var(--ds-text-md); color: var(--ink); max-width: var(--measure); }
|
|
48
|
+
.rb-add__wait { margin: 0; font-size: var(--ds-text-sm); color: var(--ink-faint); }
|
|
49
|
+
.rb-add .quote__pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
50
|
+
.rb-add__tabs [aria-pressed="true"] { color: var(--ink); border-color: var(--ink); }
|
|
51
|
+
.rb-add > .settings-btn { justify-self: start; }
|
|
52
|
+
/* On the accent Run button the action IS the face, so it is not drawn as a chip. */
|
|
53
|
+
.btn .rb-button__act { border: 0; padding: 0; font-size: inherit; }
|
|
54
|
+
/* The notices above the devices read as prose, so they keep to the measure. */
|
|
55
|
+
#ab-notices .prow__hint { max-width: var(--measure); }
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// modules/hall-ui/public/runner-button.js — what a device's Run button says
|
|
2
|
+
// (task 1005094, goal 1000139).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Owner direction, 2026-11-09: running Autobongos on a device is ONE button,
|
|
5
|
+
// and any cooldown is built into the button itself. So the button's face is the
|
|
6
|
+
// device's state, not a label beside a status line someone has to find: Run,
|
|
7
|
+
// Starting, Running, Resting until a time, Stopping, Device is off. This file
|
|
8
|
+
// decides which, from facts the server already sent — the runner's check-in and
|
|
9
|
+
// its health verdict (modules/autonomy/runner-health.js), the device's switch,
|
|
10
|
+
// and the caller's fence — and builds the one setup command per platform.
|
|
11
|
+
//
|
|
12
|
+
// PURE, and that is the testability decision (the roadmap-timeline.js precedent):
|
|
13
|
+
// no document, no fetch, no clock of its own (`nowMs` is an argument). The page
|
|
14
|
+
// script (settings-autobongos.js) draws what this returns and nothing else, so a
|
|
15
|
+
// test runs the real decision instead of agreeing with a copy of it. Assigned to
|
|
16
|
+
// window only when one exists, so node can require it.
|
|
17
|
+
|
|
18
|
+
(function (root) {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
// How long a press of Run may read as "Starting". The runner notices a press on
|
|
22
|
+
// its next fence poll (every 60s while it waits) and then checks in, so two
|
|
23
|
+
// minutes is that plus slack. Past it the button says what is actually true —
|
|
24
|
+
// usually that the device is off — instead of promising a start forever.
|
|
25
|
+
const STARTING_MS = 2 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
const ms = (t) => { const n = Date.parse(t || ''); return Number.isFinite(n) ? n : null; };
|
|
28
|
+
|
|
29
|
+
// "in 42 min", "in 3 h 5 min" — the part of the cooldown that ticks.
|
|
30
|
+
function countdown(leftMs) {
|
|
31
|
+
if (!Number.isFinite(leftMs) || leftMs <= 0) return '';
|
|
32
|
+
const mins = Math.max(1, Math.round(leftMs / 60000));
|
|
33
|
+
if (mins < 60) return `${mins} min`;
|
|
34
|
+
const h = Math.floor(mins / 60);
|
|
35
|
+
const m = mins % 60;
|
|
36
|
+
return m ? `${h} h ${m} min` : `${h} h`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A clock time today, a weekday and time within the week, a date beyond that.
|
|
40
|
+
function clock(atMs, nowMs) {
|
|
41
|
+
const d = new Date(atMs);
|
|
42
|
+
const time = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
43
|
+
const sameDay = new Date(nowMs).toDateString() === d.toDateString();
|
|
44
|
+
if (sameDay) return time;
|
|
45
|
+
if (atMs - nowMs < 6 * 24 * 3600 * 1000) return `${d.toLocaleDateString([], { weekday: 'short' })} ${time}`;
|
|
46
|
+
return d.toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Why a device that is switched on is not working, from the caller's own fence
|
|
50
|
+
// (GET /autonomy/fence with no device named), or null when nothing holds it.
|
|
51
|
+
function fenceHold(fence) {
|
|
52
|
+
if (!fence || typeof fence !== 'object') return null;
|
|
53
|
+
const scope = fence.scope || {};
|
|
54
|
+
const project = fence.project || {};
|
|
55
|
+
if (fence.enabled === false) {
|
|
56
|
+
if (fence.stop_code === 'builder_paused') return `the owner paused your runner${scope.owner_paused_reason ? ` (${scope.owner_paused_reason})` : ''}`;
|
|
57
|
+
if (fence.stop_code === 'not_permitted') return 'your rank cannot run a runner on this project';
|
|
58
|
+
return `the project’s runner switch is off${project.paused_reason ? ` (${project.paused_reason})` : ''}`;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(project.goals) && project.goals.length === 0) return 'the project has not allowed any goals for runners yet';
|
|
61
|
+
if (Array.isArray(scope.goals) && scope.goals.length === 0) return 'no goals are chosen for it yet — pick at least one below';
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// buttonState — one device's button. Returns:
|
|
66
|
+
// status the words on the button's face that say what the device is doing
|
|
67
|
+
// ('' when the face is just the action)
|
|
68
|
+
// act 'run' | 'stop' | null — what a press does; null = it cannot be pressed
|
|
69
|
+
// label the action word shown on the button ('Run' | 'Stop' | '')
|
|
70
|
+
// sub the one line under the button
|
|
71
|
+
// tone 'ok' | 'warn' | 'info' | 'neutral' — the dot's colour
|
|
72
|
+
// busy true while the face should show it is working on the press
|
|
73
|
+
// alt { act, label } | null — a second, small action (undo a Stop that
|
|
74
|
+
// is still finishing a task)
|
|
75
|
+
// stayOn true when the card should carry the stay-on note: the device is
|
|
76
|
+
// switched on, so whether it stays awake is what decides if it works
|
|
77
|
+
function buttonState(args = {}) {
|
|
78
|
+
const b = decideButton(args);
|
|
79
|
+
// Switched on (or about to stop after a task): the note is relevant. A
|
|
80
|
+
// device its builder stopped has nothing to stay awake for.
|
|
81
|
+
return { ...b, stayOn: ((args.runner || {}).run_requested !== false) || b.status === 'Stopping…' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function decideButton({ runner, fence = null, nowMs = Date.now() } = {}) {
|
|
85
|
+
const r = runner || {};
|
|
86
|
+
const h = r.health || {};
|
|
87
|
+
const want = r.run_requested !== false;
|
|
88
|
+
const task = h.workingTaskId || null;
|
|
89
|
+
const quiet = h.state === 'dead' || h.state === 'never_seen' || (h.state === 'late' && !task);
|
|
90
|
+
const changed = ms(r.run_changed_at);
|
|
91
|
+
const seen = ms(r.last_seen_at);
|
|
92
|
+
|
|
93
|
+
if (!want) {
|
|
94
|
+
if (quiet) {
|
|
95
|
+
return { status: '', act: 'run', label: 'Run', tone: 'neutral', busy: false, alt: null,
|
|
96
|
+
sub: 'This device is off or asleep. Press Run and it starts when it is back on.' };
|
|
97
|
+
}
|
|
98
|
+
if (task) {
|
|
99
|
+
return { status: 'Stopping…', act: null, label: '', tone: 'info', busy: true, alt: { act: 'run', label: 'Keep it running' },
|
|
100
|
+
sub: `Finishing task ${task} first, then it stops.` };
|
|
101
|
+
}
|
|
102
|
+
return { status: '', act: 'run', label: 'Run', tone: 'neutral', busy: false, alt: null, sub: 'Stopped on this device.' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Switched on. Has the device picked the press up yet?
|
|
106
|
+
const pressedRecently = changed !== null && nowMs - changed < STARTING_MS;
|
|
107
|
+
if (pressedRecently && (seen === null || seen < changed)) {
|
|
108
|
+
return { status: 'Starting…', act: null, label: '', tone: 'info', busy: true, alt: null,
|
|
109
|
+
sub: 'The device picks this up in under a minute.' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (quiet) {
|
|
113
|
+
return { status: 'Device is off or asleep', act: 'stop', label: 'Stop', tone: 'warn', busy: false, alt: null,
|
|
114
|
+
sub: `No check-in for ${h.age || 'a while'}. Turn it on and sign in — it picks up by itself.` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A device that is building says so before anything else: whatever a setting
|
|
118
|
+
// now says, the task in its hands is the truth about this minute.
|
|
119
|
+
if (task) {
|
|
120
|
+
return { status: 'Running', act: 'stop', label: 'Stop', tone: 'ok', busy: false, alt: null, sub: `Working on task ${task}.` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hold = fenceHold(fence);
|
|
124
|
+
if (hold) {
|
|
125
|
+
return { status: 'Ready', act: 'stop', label: 'Stop', tone: 'warn', busy: false, alt: null,
|
|
126
|
+
sub: `It is on, but ${hold}.` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (h.state === 'idle_config') {
|
|
130
|
+
return { status: 'Ready', act: 'stop', label: 'Stop', tone: 'warn', busy: false, alt: null,
|
|
131
|
+
sub: `It is on, but idle because ${h.why || 'of a setting'}. ${h.remedy || ''}`.trim() };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// THE COOLDOWN, on the button. The runner said when it resumes (the reset of
|
|
135
|
+
// the Claude usage it is waiting on); the face carries that time and how
|
|
136
|
+
// long is left, and a press still stops it.
|
|
137
|
+
const until = ms(h.holdUntil);
|
|
138
|
+
if (until !== null && until > nowMs) {
|
|
139
|
+
const leftMs = until - nowMs;
|
|
140
|
+
return { status: `Resting until ${clock(until, nowMs)}`, act: 'stop', label: 'Stop', tone: 'info', busy: false, alt: null,
|
|
141
|
+
// A usage window is a week at most; a longer wait is not counted down.
|
|
142
|
+
countdown: leftMs < 8 * 24 * 3600 * 1000 ? countdown(leftMs) : '',
|
|
143
|
+
sub: 'Its Claude usage is spent for now. It starts again by itself at that time.' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (h.state === 'restarting') {
|
|
147
|
+
return { status: 'Updating…', act: 'stop', label: 'Stop', tone: 'info', busy: true, alt: null,
|
|
148
|
+
sub: 'Restarting to pick up new code. Back in a minute or two.' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { status: 'Running', act: 'stop', label: 'Stop', tone: 'ok', busy: false, alt: null, sub: 'On — waiting for work.' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The setup command for each platform. One line to copy: it fetches the first
|
|
155
|
+
// lines of setup from this project and runs them with the one-time code.
|
|
156
|
+
// Returns null when any part is missing or is not the shape it must be — a
|
|
157
|
+
// command with a hole in it is worse than no command, and these strings end up
|
|
158
|
+
// in a shell, so nothing outside the expected characters is ever placed in one.
|
|
159
|
+
//
|
|
160
|
+
// HTTPS ONLY. The line downloads a script from the project and runs it, so over
|
|
161
|
+
// plain http anyone on the network between the device and the project could
|
|
162
|
+
// swap that script for their own. The one exception is a project on this very
|
|
163
|
+
// machine (localhost, 127.0.0.1, a .localhost name), which is a developer's own
|
|
164
|
+
// instance and never leaves the device. scripts/bongos/autobongos-host.js
|
|
165
|
+
// cleanBase applies the same rule again when the code is traded.
|
|
166
|
+
const SECURE_BASE = /^https:\/\/[A-Za-z0-9.-]+(:\d+)?$/;
|
|
167
|
+
const LOCAL_BASE = /^http:\/\/(localhost|127\.0\.0\.1|[a-z0-9-]+\.localhost)(:\d+)?$/i;
|
|
168
|
+
function setupCommands({ apiBase, code, repoUrl } = {}) {
|
|
169
|
+
const base = String(apiBase || '').replace(/\/+$/, '');
|
|
170
|
+
if (!SECURE_BASE.test(base) && !LOCAL_BASE.test(base)) return null;
|
|
171
|
+
if (!/^abr_[0-9a-f]{16,128}$/.test(String(code || ''))) return null;
|
|
172
|
+
if (!/^https:\/\/[A-Za-z0-9.-]+\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(String(repoUrl || ''))) return null;
|
|
173
|
+
return {
|
|
174
|
+
windows: `& ([scriptblock]::Create((irm '${base}/api/bongos/autonomy/runner-setup.ps1'))) -Code '${code}' -ApiBase '${base}' -Repo '${repoUrl}'`,
|
|
175
|
+
mac: `curl -fsSL '${base}/api/bongos/autonomy/runner-setup.sh' | bash -s -- --code '${code}' --api-base '${base}' --repo '${repoUrl}'`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Which platform's command to show first, from the browser's own words.
|
|
180
|
+
function guessPlatform(userAgent) {
|
|
181
|
+
return /Mac OS X|Macintosh/i.test(String(userAgent || '')) ? 'mac' : 'windows';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const api = { STARTING_MS, buttonState, fenceHold, countdown, clock, setupCommands, guessPlatform };
|
|
185
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
186
|
+
if (root) root.OTBRunnerButton = api;
|
|
187
|
+
})(typeof window !== 'undefined' ? window : null);
|