@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
|
@@ -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
|
|
@@ -73,16 +74,31 @@ const LOG_ROTATE_BYTES = 20 * 1024 * 1024;
|
|
|
73
74
|
function runnerHome(env = process.env, home = os.homedir(), repoRoot = REPO_ROOT) {
|
|
74
75
|
if (env.AUTOBONGOS_HOME) return path.resolve(env.AUTOBONGOS_HOME);
|
|
75
76
|
if (path.basename(repoRoot) === 'repo' && isRunnerCopy(repoRoot)) return path.dirname(repoRoot);
|
|
76
|
-
return path.join(home,
|
|
77
|
+
return path.join(home, RUNNER_DIR);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
80
|
+
// The runner's folder in the person's home. Named once: the read-deny rule that
|
|
81
|
+
// keeps agents out of the runner's login names this same folder
|
|
82
|
+
// (scripts/bongos/claude-materialize.js RUNNER_CONFIG_DENY), and a test holds the
|
|
83
|
+
// two together.
|
|
84
|
+
const RUNNER_DIR = '.cloudbongos-runner';
|
|
85
|
+
|
|
86
|
+
// hostPaths — every file this program owns. `configRoot` is handed to the
|
|
87
|
+
// supervisor as BONGOS_CONFIG_ROOT, which is what keeps its login apart.
|
|
88
|
+
//
|
|
89
|
+
// THE LOGIN LIVES IN ONE PLACE ON EVERY DEVICE (task 1005186): configRoot is always
|
|
90
|
+
// <home>/.cloudbongos-runner/config, WHATEVER AUTOBONGOS_HOME says. That variable
|
|
91
|
+
// moves the runner's copy of the project, its logs and its lock; it does not move
|
|
92
|
+
// the login. The reason is the deny rule: agents are refused reads under exactly
|
|
93
|
+
// that folder, and a login that could be put somewhere else by an environment
|
|
94
|
+
// variable would be a login the rule does not cover — on precisely the device
|
|
95
|
+
// someone had customised. One runner per device already, so there is nothing a
|
|
96
|
+
// second config folder would be for.
|
|
81
97
|
function hostPaths(env = process.env, home = os.homedir()) {
|
|
82
98
|
const root = runnerHome(env, home);
|
|
83
99
|
return {
|
|
84
100
|
root,
|
|
85
|
-
configRoot: path.join(
|
|
101
|
+
configRoot: path.join(home, RUNNER_DIR, 'config'),
|
|
86
102
|
logDir: path.join(root, 'logs'),
|
|
87
103
|
log: path.join(root, 'logs', 'autobongos.log'),
|
|
88
104
|
lock: path.join(root, 'host.json'),
|
|
@@ -755,6 +771,12 @@ async function waitForCheckIn({ session, fetchImpl = fetch, timeoutMs = 90000, s
|
|
|
755
771
|
return false;
|
|
756
772
|
}
|
|
757
773
|
|
|
774
|
+
// The digest CLI and its arguments, for `digest`: the same file a builder would
|
|
775
|
+
// run by hand, with whatever they passed handed through.
|
|
776
|
+
function digestArgv(rest = []) {
|
|
777
|
+
return [path.join(REPO_ROOT, 'scripts', 'bongos', 'autobongos-digest.js'), ...rest];
|
|
778
|
+
}
|
|
779
|
+
|
|
758
780
|
// ── the commands ───────────────────────────────────────────────────────────
|
|
759
781
|
|
|
760
782
|
async function runCommand({ paths = hostPaths() } = {}) {
|
|
@@ -819,6 +841,13 @@ async function main(argv) {
|
|
|
819
841
|
console.log('Whether it is WORKING is on its card in Settings: the card reads its check-ins.');
|
|
820
842
|
return 0;
|
|
821
843
|
}
|
|
844
|
+
if (cmd === 'digest') {
|
|
845
|
+
// The runner's run log lives under ITS config root, so the digest has to be
|
|
846
|
+
// run with that root or it reads the builder's own (empty) one and reports a
|
|
847
|
+
// missing log (task 1005097).
|
|
848
|
+
const r = spawnSync(process.execPath, digestArgv(argv.slice(1)), { stdio: 'inherit', env: supervisorEnv(process.env, paths, dirName) });
|
|
849
|
+
return r.status === null ? 1 : r.status;
|
|
850
|
+
}
|
|
822
851
|
if (cmd === 'install') {
|
|
823
852
|
const e = install({ paths });
|
|
824
853
|
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 +871,7 @@ async function main(argv) {
|
|
|
842
871
|
for (const line of stopReport(result, { installed })) console.log(line);
|
|
843
872
|
return result.outcome === 'still_running' || (cmd === 'uninstall' && installed) ? 1 : 0;
|
|
844
873
|
}
|
|
845
|
-
console.error('usage: autobongos-host.js setup --code <code> --api-base <url> | status [--json] | start | stop | install | uninstall | run');
|
|
874
|
+
console.error('usage: autobongos-host.js setup --code <code> --api-base <url> | status [--json] | start | stop | install | uninstall | digest [--since 24h] | run');
|
|
846
875
|
return 2;
|
|
847
876
|
}
|
|
848
877
|
|
|
@@ -865,5 +894,5 @@ module.exports = {
|
|
|
865
894
|
pidAlive, lockHolder, wantsAwake, keepAwakeCommand, makeKeepAwake, runLoop,
|
|
866
895
|
windowsRunArgv, windowsRunCommand, windowsStartSpawn, legacyBootTaskInstalled, launchdPlist,
|
|
867
896
|
stopRunner, stopReport, exchangeCode, claudeState, waitForCheckIn,
|
|
868
|
-
signIn, cleanBase, install, removeStartEntry, installedState, status,
|
|
897
|
+
signIn, cleanBase, install, removeStartEntry, installedState, status, digestArgv,
|
|
869
898
|
};
|
|
@@ -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
|
|
@@ -481,10 +481,29 @@ const STARTER_DENY_SECRETS = [
|
|
|
481
481
|
'Read(**/*.pem)', 'Read(**/id_rsa*)', 'Read(**/id_ed25519*)',
|
|
482
482
|
];
|
|
483
483
|
const SESSION_STORE_DENY = `Read(~/.config/${ic.FALLBACK_DIR}/instances/**)`;
|
|
484
|
+
// An Autobongos runner keeps ITS OWN login and files in a folder of its own (task 1005095:
|
|
485
|
+
// BONGOS_CONFIG_ROOT, so adding a laptop as a runner never touches the builder's CLI session).
|
|
486
|
+
// That put the runner's session file, and the session store beside it, outside every rule above,
|
|
487
|
+
// which all stop at ~/.config — so on a runner device a worker could open the file and read the
|
|
488
|
+
// runner's login (task 1005186). The whole folder is denied rather than one file name: the
|
|
489
|
+
// runner's heartbeat and run log sit there too and no agent has a reason to read any of it, and
|
|
490
|
+
// a rule on the folder cannot be stepped around by a session file under a new name.
|
|
491
|
+
// The folder name is the device program's (scripts/bongos/autobongos-host.js hostPaths); it is
|
|
492
|
+
// spelled here rather than required from there because this file is the installer and must not
|
|
493
|
+
// load the runner, and tests/claude_materialize.mjs fails if the two ever name different places.
|
|
494
|
+
const RUNNER_CONFIG_DENY = 'Read(~/.cloudbongos-runner/config/**)';
|
|
495
|
+
|
|
496
|
+
// The rules that name a FIXED place, whatever the instance is called.
|
|
497
|
+
const FIXED_PLACE_DENY = [SESSION_STORE_DENY, RUNNER_CONFIG_DENY];
|
|
484
498
|
|
|
485
499
|
// A checkout's settings.json takes the identity rebrand like every copied file, and the store's
|
|
486
500
|
// dir is one of the core's `.config/<dir>` literals it rewrites. The store does not move with the
|
|
487
501
|
// brand (task 1003741), so its rule is put back where the CLI reads it (task 1004155).
|
|
502
|
+
//
|
|
503
|
+
// The runner rule needs no putting back: the rebrand rewrites `.config/<core dir>` literals, and
|
|
504
|
+
// the runner's folder is not one (measured: with the put-back removed, the three-brand case in
|
|
505
|
+
// tests/claude_materialize.mjs still holds). That case is what would notice a future rebrand
|
|
506
|
+
// rule broad enough to carry it off.
|
|
488
507
|
function rebrandSettings(text, rebrand) {
|
|
489
508
|
const moved = rebrand(SESSION_STORE_DENY);
|
|
490
509
|
return rebrand(text).split(JSON.stringify(moved)).join(JSON.stringify(SESSION_STORE_DENY));
|
|
@@ -504,7 +523,7 @@ function credentialDeny(branding = {}) {
|
|
|
504
523
|
return [
|
|
505
524
|
...STARTER_DENY_SECRETS,
|
|
506
525
|
...dirs.flatMap((dir) => credentialFiles.map((file) => `Read(~/.config/${dir}/${file})`)),
|
|
507
|
-
|
|
526
|
+
...FIXED_PLACE_DENY,
|
|
508
527
|
];
|
|
509
528
|
}
|
|
510
529
|
|
|
@@ -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.79'; // 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');
|
|
@@ -221,6 +221,21 @@ test('the supervisor runs under the runner\'s own config root, and keeps a gauge
|
|
|
221
221
|
assert.equal(host.supervisorEnv({ AUTOBONGOS_GAUGE_FILE: '/set/by/hand' }, paths).AUTOBONGOS_GAUGE_FILE, '/set/by/hand');
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
test('AUTOBONGOS_HOME moves the runner\'s copy, logs and lock, and never the login', () => {
|
|
225
|
+
// The login must stay where the read-deny rule looks for it (task 1005186): a
|
|
226
|
+
// device someone customised is the last one that should fall outside the rule.
|
|
227
|
+
const home = path.join(tmp, 'fixed-login-home');
|
|
228
|
+
const standard = host.hostPaths({}, home);
|
|
229
|
+
const custom = host.hostPaths({ AUTOBONGOS_HOME: path.join(tmp, 'somewhere-else') }, home);
|
|
230
|
+
assert.equal(standard.configRoot, path.join(home, '.cloudbongos-runner', 'config'));
|
|
231
|
+
assert.equal(custom.configRoot, standard.configRoot, 'the same folder under the person\'s home, whatever the variable says');
|
|
232
|
+
assert.equal(custom.root, path.join(tmp, 'somewhere-else'));
|
|
233
|
+
assert.equal(custom.log, path.join(tmp, 'somewhere-else', 'logs', 'autobongos.log'));
|
|
234
|
+
assert.equal(custom.lock, path.join(tmp, 'somewhere-else', 'host.json'));
|
|
235
|
+
// And that is the folder the supervisor and its workers are pointed at.
|
|
236
|
+
assert.equal(host.supervisorEnv({ AUTOBONGOS_HOME: path.join(tmp, 'somewhere-else') }, custom).BONGOS_CONFIG_ROOT, standard.configRoot);
|
|
237
|
+
});
|
|
238
|
+
|
|
224
239
|
test('under BONGOS_CONFIG_ROOT every session path moves, and the builder\'s own session is not a fallback', () => {
|
|
225
240
|
const root = fresh('cfgroot');
|
|
226
241
|
const probe = `
|
|
@@ -617,3 +632,34 @@ test('the PowerShell bootstrap stops with a sentence on an incomplete command, a
|
|
|
617
632
|
// long dash turns into mojibake inside a string — or a parse error.
|
|
618
633
|
assert.equal(/[^\x00-\x7F]/.test(readFileSync(PS1, 'utf8')), false);
|
|
619
634
|
});
|
|
635
|
+
|
|
636
|
+
// ── the night, read from the runner's own files (task 1005097) ───────────────
|
|
637
|
+
|
|
638
|
+
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', () => {
|
|
639
|
+
const home = fresh('digest-home');
|
|
640
|
+
const paths = host.hostPaths({ AUTOBONGOS_HOME: home }, home);
|
|
641
|
+
const dirName = require('../src/instance-config').configDirName();
|
|
642
|
+
const logDir = path.join(paths.configRoot, dirName);
|
|
643
|
+
mkdirSync(logDir, { recursive: true });
|
|
644
|
+
writeFileSync(path.join(logDir, 'autobongos-runs.jsonl'), `${JSON.stringify({
|
|
645
|
+
at: new Date().toISOString(), event: 'worked', task_id: 424242, title: 'the task the runner built',
|
|
646
|
+
outcome: 'claims_shipped', verified: true, ledger_status: 'shipped', ledger_shape: 'shipped', cost_usd: 1.5,
|
|
647
|
+
})}\n`);
|
|
648
|
+
|
|
649
|
+
const argv = host.digestArgv(['--all', '--json']);
|
|
650
|
+
assert.equal(path.basename(argv[0]), 'autobongos-digest.js');
|
|
651
|
+
assert.deepEqual(argv.slice(1), ['--all', '--json'], 'whatever the person passed is handed through');
|
|
652
|
+
|
|
653
|
+
// Through the device program: the runner's night.
|
|
654
|
+
// The login folder is always under the person's home (task 1005186), so the
|
|
655
|
+
// child is given this test's stand-in home: HOME on a Mac or Linux, USERPROFILE
|
|
656
|
+
// on Windows. Otherwise it would read the real one.
|
|
657
|
+
const viaHost = spawnSync(process.execPath, [HOST_JS, 'digest', '--all', '--json'], { encoding: 'utf8', env: { ...process.env, HOME: home, USERPROFILE: home, AUTOBONGOS_HOME: home, AUTOBONGOS_LOG_FILE: '' } });
|
|
658
|
+
assert.equal(viaHost.status, 0, viaHost.stderr);
|
|
659
|
+
assert.ok(viaHost.stdout.includes('424242'), 'the task the runner built is in it');
|
|
660
|
+
|
|
661
|
+
// Bare, with an empty folder standing in for the builder's own: nothing to read.
|
|
662
|
+
const bare = spawnSync(process.execPath, argv, { encoding: 'utf8', env: { ...process.env, BONGOS_CONFIG_ROOT: fresh('digest-builder-root'), AUTOBONGOS_LOG_FILE: '' } });
|
|
663
|
+
assert.equal(bare.status, 0, 'the digest never fails a morning');
|
|
664
|
+
assert.equal(bare.stdout.includes('424242'), false, 'which is why the guide sends people through the device program');
|
|
665
|
+
});
|
|
@@ -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 ──
|