@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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// node scripts/bongos/autobongos-host.js status [--json]
|
|
9
9
|
// node scripts/bongos/autobongos-host.js start | stop
|
|
10
10
|
// node scripts/bongos/autobongos-host.js install | uninstall
|
|
11
|
+
// node scripts/bongos/autobongos-host.js digest [--since 24h] # the night, in five answers
|
|
11
12
|
// node scripts/bongos/autobongos-host.js run # what the start entry runs
|
|
12
13
|
//
|
|
13
14
|
// WHY. The runner could only be hosted one way: a Windows scheduled task
|
|
@@ -755,6 +756,12 @@ async function waitForCheckIn({ session, fetchImpl = fetch, timeoutMs = 90000, s
|
|
|
755
756
|
return false;
|
|
756
757
|
}
|
|
757
758
|
|
|
759
|
+
// The digest CLI and its arguments, for `digest`: the same file a builder would
|
|
760
|
+
// run by hand, with whatever they passed handed through.
|
|
761
|
+
function digestArgv(rest = []) {
|
|
762
|
+
return [path.join(REPO_ROOT, 'scripts', 'bongos', 'autobongos-digest.js'), ...rest];
|
|
763
|
+
}
|
|
764
|
+
|
|
758
765
|
// ── the commands ───────────────────────────────────────────────────────────
|
|
759
766
|
|
|
760
767
|
async function runCommand({ paths = hostPaths() } = {}) {
|
|
@@ -819,6 +826,13 @@ async function main(argv) {
|
|
|
819
826
|
console.log('Whether it is WORKING is on its card in Settings: the card reads its check-ins.');
|
|
820
827
|
return 0;
|
|
821
828
|
}
|
|
829
|
+
if (cmd === 'digest') {
|
|
830
|
+
// The runner's run log lives under ITS config root, so the digest has to be
|
|
831
|
+
// run with that root or it reads the builder's own (empty) one and reports a
|
|
832
|
+
// missing log (task 1005097).
|
|
833
|
+
const r = spawnSync(process.execPath, digestArgv(argv.slice(1)), { stdio: 'inherit', env: supervisorEnv(process.env, paths, dirName) });
|
|
834
|
+
return r.status === null ? 1 : r.status;
|
|
835
|
+
}
|
|
822
836
|
if (cmd === 'install') {
|
|
823
837
|
const e = install({ paths });
|
|
824
838
|
console.log(e.kind === 'none' ? 'This system has no start-at-sign-in entry yet; use start after each restart.' : 'The runner now starts whenever you sign in to this device.');
|
|
@@ -842,7 +856,7 @@ async function main(argv) {
|
|
|
842
856
|
for (const line of stopReport(result, { installed })) console.log(line);
|
|
843
857
|
return result.outcome === 'still_running' || (cmd === 'uninstall' && installed) ? 1 : 0;
|
|
844
858
|
}
|
|
845
|
-
console.error('usage: autobongos-host.js setup --code <code> --api-base <url> | status [--json] | start | stop | install | uninstall | run');
|
|
859
|
+
console.error('usage: autobongos-host.js setup --code <code> --api-base <url> | status [--json] | start | stop | install | uninstall | digest [--since 24h] | run');
|
|
846
860
|
return 2;
|
|
847
861
|
}
|
|
848
862
|
|
|
@@ -865,5 +879,5 @@ module.exports = {
|
|
|
865
879
|
pidAlive, lockHolder, wantsAwake, keepAwakeCommand, makeKeepAwake, runLoop,
|
|
866
880
|
windowsRunArgv, windowsRunCommand, windowsStartSpawn, legacyBootTaskInstalled, launchdPlist,
|
|
867
881
|
stopRunner, stopReport, exchangeCode, claudeState, waitForCheckIn,
|
|
868
|
-
signIn, cleanBase, install, removeStartEntry, installedState, status,
|
|
882
|
+
signIn, cleanBase, install, removeStartEntry, installedState, status, digestArgv,
|
|
869
883
|
};
|
|
@@ -455,12 +455,18 @@ function worktreeName(taskId) {
|
|
|
455
455
|
// A failure of EITHER read returns the shape that refuses. An unreachable
|
|
456
456
|
// instance and a thrown kill switch are different facts, but they license the
|
|
457
457
|
// same action: do nothing.
|
|
458
|
+
// THIS DEVICE is named on the read (task 1005094): the server folds the device's
|
|
459
|
+
// own Run switch into the answer, so a builder's Stop on this machine arrives as
|
|
460
|
+
// `enabled: false` with nothing for this file to decide. The name is the same
|
|
461
|
+
// os.hostname() the heartbeat reports, which is how the two rows line up.
|
|
462
|
+
const fenceArgs = () => ({ query: { host: os.hostname() } });
|
|
463
|
+
|
|
458
464
|
async function readFence(deps = {}) {
|
|
459
465
|
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
460
466
|
let raw = null;
|
|
461
467
|
let graderBypassed = null;
|
|
462
468
|
try {
|
|
463
|
-
const r = await api.autonomy.getAutonomyFence();
|
|
469
|
+
const r = await api.autonomy.getAutonomyFence(fenceArgs());
|
|
464
470
|
if (r && r.ok) raw = r.data;
|
|
465
471
|
} catch (_) { /* unreadable → refuses below */ }
|
|
466
472
|
try {
|
|
@@ -1011,7 +1017,9 @@ async function pollSignals(deps = {}) {
|
|
|
1011
1017
|
if (r) head = r;
|
|
1012
1018
|
} catch (_) { /* null */ }
|
|
1013
1019
|
try {
|
|
1014
|
-
|
|
1020
|
+
// Named the same way, so a press of Run on this device moves the stamp the
|
|
1021
|
+
// wait is watching and the runner wakes inside a poll instead of an idle.
|
|
1022
|
+
const f = await api.autonomy.getAutonomyFence(fenceArgs());
|
|
1015
1023
|
if (f && f.ok) fenceUpdatedAt = f.data.updated_at || null;
|
|
1016
1024
|
} catch (_) { /* null */ }
|
|
1017
1025
|
try {
|
|
@@ -1233,9 +1241,15 @@ async function forever(opts, deps = {}) {
|
|
|
1233
1241
|
// `config_idle:<code>` instead of "waiting", so the hall shows the fix rather
|
|
1234
1242
|
// than "alive" for ten hours. Any row that is not that refusal ends the stretch.
|
|
1235
1243
|
let idle = null;
|
|
1244
|
+
// When a resting runner resumes, in epoch seconds (task 1005094): the reset the
|
|
1245
|
+
// last hold named. Sent on every beat that is not about a task, so the device's
|
|
1246
|
+
// Run button can show "Resting until 3:40" for the whole wait, not just for the
|
|
1247
|
+
// one beat that followed the hold. Any row that is not such a hold clears it.
|
|
1248
|
+
let restUntil = null;
|
|
1236
1249
|
const clock = () => (deps.now ? deps.now() : Date.now());
|
|
1237
1250
|
const beat = async (rawFields) => {
|
|
1238
|
-
const
|
|
1251
|
+
const resting = restUntil && !(rawFields && rawFields.working_task_id) ? { ...rawFields, hold_until: restUntil } : rawFields;
|
|
1252
|
+
const fields = configIdle.markBeat(resting, idle, clock());
|
|
1239
1253
|
// The runner state is passed, not defaulted: the file names `disk_head` from
|
|
1240
1254
|
// it (task 1004406), and an injected state must reach the beat that reports it.
|
|
1241
1255
|
try { beatFile(Date.now(), deps.state || RUNNER_STATE); } catch (_) { /* a runner that cannot write its file still runs */ }
|
|
@@ -1281,6 +1295,7 @@ async function forever(opts, deps = {}) {
|
|
|
1281
1295
|
row = await iterate(opts, deps);
|
|
1282
1296
|
emit(row);
|
|
1283
1297
|
idle = configIdle.trackIdle(idle, row, clock());
|
|
1298
|
+
restUntil = row.event === 'hold' && Number(row.sleep_until) > 0 ? Number(row.sleep_until) : null;
|
|
1284
1299
|
// Carry the task id so the hall can tell "quiet because it is building
|
|
1285
1300
|
// task 1234" from "quiet because it is dead". A worker may hold the loop for
|
|
1286
1301
|
// ninety minutes, and without this every honest hour of work looks like a
|
|
@@ -30,6 +30,19 @@
|
|
|
30
30
|
// MERGE commit's body, where GitHub echoes the branch's subject. A subject-only scan
|
|
31
31
|
// misses the one case this file exists for.
|
|
32
32
|
//
|
|
33
|
+
// A SECOND FORM, because the convention moved (task 1005187). Builders, and ship.js's
|
|
34
|
+
// own regeneration commits, now mostly write the task at the START of the subject:
|
|
35
|
+
// `task N - words`, `task N: words`, with an optional conventional prefix in front
|
|
36
|
+
// (`fix: task N - words`, `wip(scope): task N - words`). Measured over the last 400
|
|
37
|
+
// commits on main on 2026-11-10: 140 subjects in that form against 53 lines ending
|
|
38
|
+
// `(task N)`, covering 18 distinct tasks — under the floor of 20 the live-repo test
|
|
39
|
+
// holds, which is how main went red. So both forms count. The new one is read off the
|
|
40
|
+
// SUBJECT only, and only at its start, followed by a dash or a colon: the same
|
|
41
|
+
// "this commit DID task N" statement, and still not "mentions N" (a subject such as
|
|
42
|
+
// "see task N for why", or any body line naming a task, is not counted). A merge
|
|
43
|
+
// commit's subject is GitHub's, so for one of those the first line of its body — the
|
|
44
|
+
// pull request's own title, which GitHub echoes there — is read the same way.
|
|
45
|
+
//
|
|
33
46
|
// MEASURED on this repo before shipping: 877 ids are named as an authoring subject
|
|
34
47
|
// across 4,784 commits, and ZERO of them are open `ready`/`backlog` rows (434 of
|
|
35
48
|
// those). So today's board is clean and the signal is not merely quiet — the same
|
|
@@ -66,13 +79,37 @@ const SEP = `${SOH}COMMIT${SOH}`;
|
|
|
66
79
|
// mentioned N", and the second is the noise that made the supersession half undoable.
|
|
67
80
|
const AUTHORED_RE = /\(task (\d{6,7})\)\s*$/;
|
|
68
81
|
|
|
82
|
+
// The second convention: the task leads the subject, after an optional conventional
|
|
83
|
+
// prefix, and is followed by a dash or a colon. Read off a TITLE line only (see
|
|
84
|
+
// titleLines) — never off every line, which is where "mentions N" lives.
|
|
85
|
+
const LEADING_RE = /^(?:[a-z]+(?:\([^)]*\))?!?:\s*)?task (\d{6,7})\s*[-:\u2013\u2014]/i;
|
|
86
|
+
const MERGE_SUBJECT_RE = /^Merge pull request #\d+ /;
|
|
87
|
+
|
|
88
|
+
// The lines of a message that are a TITLE: its subject, and for a GitHub merge commit
|
|
89
|
+
// the first line of the body too, where GitHub echoes the pull request's own title.
|
|
90
|
+
function titleLines(lines) {
|
|
91
|
+
const at = lines.findIndex((l) => l);
|
|
92
|
+
if (at === -1) return [];
|
|
93
|
+
const out = [lines[at]];
|
|
94
|
+
if (MERGE_SUBJECT_RE.test(lines[at])) {
|
|
95
|
+
const echoed = lines.slice(at + 1).find((l) => l);
|
|
96
|
+
if (echoed) out.push(echoed);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
// PURE — every task id a commit message claims to have DONE. Exported so the rule is
|
|
70
102
|
// testable without a repo: a scanner that quietly stops matching reports a clean board,
|
|
71
103
|
// which is indistinguishable from a healthy one.
|
|
72
104
|
function authoredIds(message) {
|
|
73
105
|
const out = new Set();
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const lines = String(message || '').split('\n').map((l) => l.trim());
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const m = AUTHORED_RE.exec(line);
|
|
109
|
+
if (m) out.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
for (const line of titleLines(lines)) {
|
|
112
|
+
const m = LEADING_RE.exec(line);
|
|
76
113
|
if (m) out.add(m[1]);
|
|
77
114
|
}
|
|
78
115
|
return out;
|
|
@@ -214,6 +214,13 @@ const CANNED_WRITES = [
|
|
|
214
214
|
['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
|
|
215
215
|
['POST', /^ai-studio\/requests$/, 'ai-studio__requests.post'],
|
|
216
216
|
['POST', /^ai-studio\/topup$/, 'ai-studio__topup.post'],
|
|
217
|
+
// Settings > Your Autobongos runner (task 1005094): a press of Run or Stop, a
|
|
218
|
+
// setup code for Add a device, and Remove (the login, then the card), so the
|
|
219
|
+
// Starting face, the setup command and the removal toast render here.
|
|
220
|
+
['POST', /^autonomy\/runners\/[^/]+\/run$/, 'autonomy__runners__run.post'],
|
|
221
|
+
['POST', /^auth\/runner-code\/issue$/, 'auth__runner-code__issue.post'],
|
|
222
|
+
['POST', /^me\/sessions\/revoke$/, 'me__sessions__revoke.post'],
|
|
223
|
+
['DELETE', /^autonomy\/runners\/[^/]+$/, 'autonomy__runners.delete'],
|
|
217
224
|
];
|
|
218
225
|
|
|
219
226
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.78'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -617,3 +617,31 @@ test('the PowerShell bootstrap stops with a sentence on an incomplete command, a
|
|
|
617
617
|
// long dash turns into mojibake inside a string — or a parse error.
|
|
618
618
|
assert.equal(/[^\x00-\x7F]/.test(readFileSync(PS1, 'utf8')), false);
|
|
619
619
|
});
|
|
620
|
+
|
|
621
|
+
// ── the night, read from the runner's own files (task 1005097) ───────────────
|
|
622
|
+
|
|
623
|
+
test('the digest is read from the RUNNER’s run log: bare, the digest CLI looks in the builder’s own folder and finds none', () => {
|
|
624
|
+
const home = fresh('digest-home');
|
|
625
|
+
const paths = host.hostPaths({ AUTOBONGOS_HOME: home }, home);
|
|
626
|
+
const dirName = require('../src/instance-config').configDirName();
|
|
627
|
+
const logDir = path.join(paths.configRoot, dirName);
|
|
628
|
+
mkdirSync(logDir, { recursive: true });
|
|
629
|
+
writeFileSync(path.join(logDir, 'autobongos-runs.jsonl'), `${JSON.stringify({
|
|
630
|
+
at: new Date().toISOString(), event: 'worked', task_id: 424242, title: 'the task the runner built',
|
|
631
|
+
outcome: 'claims_shipped', verified: true, ledger_status: 'shipped', ledger_shape: 'shipped', cost_usd: 1.5,
|
|
632
|
+
})}\n`);
|
|
633
|
+
|
|
634
|
+
const argv = host.digestArgv(['--all', '--json']);
|
|
635
|
+
assert.equal(path.basename(argv[0]), 'autobongos-digest.js');
|
|
636
|
+
assert.deepEqual(argv.slice(1), ['--all', '--json'], 'whatever the person passed is handed through');
|
|
637
|
+
|
|
638
|
+
// Through the device program: the runner's night.
|
|
639
|
+
const viaHost = spawnSync(process.execPath, [HOST_JS, 'digest', '--all', '--json'], { encoding: 'utf8', env: { ...process.env, AUTOBONGOS_HOME: home, AUTOBONGOS_LOG_FILE: '' } });
|
|
640
|
+
assert.equal(viaHost.status, 0, viaHost.stderr);
|
|
641
|
+
assert.ok(viaHost.stdout.includes('424242'), 'the task the runner built is in it');
|
|
642
|
+
|
|
643
|
+
// Bare, with an empty folder standing in for the builder's own: nothing to read.
|
|
644
|
+
const bare = spawnSync(process.execPath, argv, { encoding: 'utf8', env: { ...process.env, BONGOS_CONFIG_ROOT: fresh('digest-builder-root'), AUTOBONGOS_LOG_FILE: '' } });
|
|
645
|
+
assert.equal(bare.status, 0, 'the digest never fails a morning');
|
|
646
|
+
assert.equal(bare.stdout.includes('424242'), false, 'which is why the guide sends people through the device program');
|
|
647
|
+
});
|
|
@@ -223,10 +223,15 @@ test('the hall draws idle_config as a warning with the fix, on the Gate page and
|
|
|
223
223
|
const gate = fs.readFileSync('modules/hall-ui/public/gate.js', 'utf8');
|
|
224
224
|
assert.match(gate, /idle_config: 'warn'/, 'the dot is a warning, not ok');
|
|
225
225
|
assert.match(gate, /h\.state === 'idle_config'[\s\S]{0,200}h\.remedy/, 'the checklist carries the remedy');
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
// In Settings the device's own card says it (task 1005094): asked of the real
|
|
227
|
+
// button logic, with the verdict the server attaches to that runner.
|
|
228
|
+
const RB = require('../modules/hall-ui/public/runner-button.js');
|
|
229
|
+
const idle = describeRunner({ host: 'winbox', last_seen_at: new Date().toISOString(), last_event: 'config_idle:gauge_unknown', run_requested: true }, { nowMs: Date.now() });
|
|
230
|
+
assert.equal(idle.state, 'idle_config');
|
|
231
|
+
const open = { enabled: true, scope: { goals: [1] }, project: { enabled: true, goals: [{ goal_id: 1 }] } };
|
|
232
|
+
const b = RB.buttonState({ runner: { host: 'winbox', run_requested: true, last_seen_at: new Date().toISOString(), health: idle }, fence: open, nowMs: Date.now() });
|
|
233
|
+
assert.equal(b.tone, 'warn', 'a warning on the card, not ok');
|
|
234
|
+
assert.ok(b.sub.includes(idle.why) && b.sub.includes(idle.remedy), 'the card carries why it is idle and the fix');
|
|
230
235
|
});
|
|
231
236
|
|
|
232
237
|
// ── the route: POST /autonomy/heartbeat posts through the discord.post port ──
|