@bongos/core 1.19.836 → 1.19.838
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 +59 -24
- package/docs/adr/0302-the-third-identical-failure-stops-the-session-and-asks.md +74 -0
- package/docs/adr/README.md +1 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/agents/migrations/agents_003_output_ref_idx.sql +41 -0
- package/modules/agents/module.json +1 -1
- package/modules/agents/pollers/event-router.js +737 -0
- package/modules/discord/module.json +1 -1
- package/modules/discord/routes/discord.js +25 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box.js +5 -1
- package/scripts/gds/doctor.js +28 -1
- package/scripts/gds/plain-cards.js +2 -0
- package/scripts/gds/provision.js +4 -1
- package/scripts/gds/runner-drift.js +151 -0
- package/src/module-api.js +1 -1
- package/tests/agents_event_router.mjs +523 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +1 -0
- package/tests/provision.mjs +18 -0
- package/tests/runner_drift.mjs +173 -0
- package/tests/stuck_detector.mjs +233 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"pollers": ["discord-bot"],
|
|
12
12
|
"uiSections": []
|
|
13
13
|
},
|
|
14
|
-
"provides": ["discord.isLinked"],
|
|
14
|
+
"provides": ["discord.isLinked", "discord.post", "discord.hasChannel"],
|
|
15
15
|
"consumes": ["ideas.capture", "government", "lifecycle"],
|
|
16
16
|
"dependencies": { "discord.js": "^14.26.4" }
|
|
17
17
|
}
|
|
@@ -22,6 +22,7 @@ const db = require('../db');
|
|
|
22
22
|
const shipBroadcast = require('../ship-broadcast');
|
|
23
23
|
const boardBroadcast = require('../board-broadcast');
|
|
24
24
|
const roleSync = require('../discord-role-sync');
|
|
25
|
+
const webhook = require('../webhook');
|
|
25
26
|
|
|
26
27
|
// The module's kernel seams, registered when this factory runs (the loader runs
|
|
27
28
|
// it at boot, already gated to enabled instances — ADR 0083 / BV1.R47). They are
|
|
@@ -60,6 +61,30 @@ function registerSeams() {
|
|
|
60
61
|
return { guildId, roleIds: roleId ? [roleId] : [] };
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
// `discord.post` — the GENERIC outbound line (task 1002494). Until now every
|
|
65
|
+
// post this module makes was driven by a listener on an event it already knew
|
|
66
|
+
// about, so a module with something to say and no event of its own — the
|
|
67
|
+
// agents event-router, whose findings declare a `discord` sink — had nothing
|
|
68
|
+
// to deliver to, and could not require a sibling module to get one (ADR 0083).
|
|
69
|
+
//
|
|
70
|
+
// THE CALLER NAMES A CHANNEL KEY, NEVER A URL. `webhook.js` owns the
|
|
71
|
+
// key→webhook allowlist and throws on an unknown key, so this port cannot be
|
|
72
|
+
// used to post anywhere the operator has not configured — and `hasWebhook`
|
|
73
|
+
// lets a caller check that BEFORE doing expensive work it would then throw
|
|
74
|
+
// away. Still outbound only (ADR 0033 §8): nothing here reads from Discord,
|
|
75
|
+
// and nothing posted through it carries authority.
|
|
76
|
+
if (!api.hasProvider('discord.post')) {
|
|
77
|
+
api.registerProvider('discord.post', async ({ channel, text } = {}) => {
|
|
78
|
+
if (!webhook.hasWebhook(channel)) {
|
|
79
|
+
return { posted: false, reason: 'no_webhook_for_channel', channel: channel || null };
|
|
80
|
+
}
|
|
81
|
+
await webhook.postWebhook(channel, String(text == null ? '' : text));
|
|
82
|
+
return { posted: true, channel };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!api.hasProvider('discord.hasChannel')) {
|
|
86
|
+
api.registerProvider('discord.hasChannel', (channel) => webhook.hasWebhook(channel));
|
|
87
|
+
}
|
|
63
88
|
if (_shipListenerOff) _shipListenerOff();
|
|
64
89
|
_shipListenerOff = api.on('task.shipped', (task) => shipBroadcast.postShipBroadcast(task));
|
|
65
90
|
if (_strandListenerOff) _strandListenerOff();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.838",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.838",
|
|
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.19.
|
|
3
|
+
"version": "1.19.838",
|
|
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/scripts/gds/box.js
CHANGED
|
@@ -54,6 +54,8 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
54
54
|
// INSTANCE root, riding ADR 0108's resolvers, instead of assuming it sits beside
|
|
55
55
|
// box.js. ADR 0150 (BV1.R97/R98).
|
|
56
56
|
const instanceConfig = require(path.join(REPO_ROOT, 'src/instance-config'));
|
|
57
|
+
const { installedCoreVersion, runnerBanner } = require('./runner-drift.js');
|
|
58
|
+
const { coreVersionSafe } = require('./provision-config.js');
|
|
57
59
|
const BOX_INFRA_TEMPLATE = 'builder-box-cloud-init.yaml';
|
|
58
60
|
|
|
59
61
|
// Ordered candidate infra/ dirs — first that actually holds the cloud-init
|
|
@@ -1120,6 +1122,8 @@ async function cmdReconcileDrift(pool, boxes, { apply }) {
|
|
|
1120
1122
|
const MAX_INTENT_ATTEMPTS = numEnv('BOX_INTENT_MAX_ATTEMPTS', 3);
|
|
1121
1123
|
|
|
1122
1124
|
async function cmdRunIntents(pool, boxes, boxOnboard, { apply }) {
|
|
1125
|
+
const instanceRoot = process.env.BONGOS_INSTANCE_ROOT || process.cwd();
|
|
1126
|
+
console.log(runnerBanner({ runnerVersion: coreVersionSafe(), runnerRoot: REPO_ROOT, instanceVersion: installedCoreVersion(instanceRoot), instanceRoot }));
|
|
1123
1127
|
if (!apply) {
|
|
1124
1128
|
const { rows } = await pool.query(
|
|
1125
1129
|
`SELECT bi.*, b.github_login FROM box_intents bi
|
|
@@ -1325,4 +1329,4 @@ async function main() {
|
|
|
1325
1329
|
|
|
1326
1330
|
if (require.main === module) main();
|
|
1327
1331
|
|
|
1328
|
-
module.exports = { cmdProvision, deprovisionOne, parkOne, cmdSweepIdle, cmdSweepDormant, deleteWithRetry, cmdReconcileDrift, runDnsHook, runDnsHookBlackhole, markErrorUnlessDestroyed, resolveBoxInfraDir, boxInfraCandidates, readCloudInit, buildBoxEnv, BOX_INFRA_TEMPLATE };
|
|
1332
|
+
module.exports = { cmdProvision, cmdRunIntents, deprovisionOne, parkOne, cmdSweepIdle, cmdSweepDormant, deleteWithRetry, cmdReconcileDrift, runDnsHook, runDnsHookBlackhole, markErrorUnlessDestroyed, resolveBoxInfraDir, boxInfraCandidates, readCloudInit, buildBoxEnv, BOX_INFRA_TEMPLATE };
|
package/scripts/gds/doctor.js
CHANGED
|
@@ -375,12 +375,38 @@ function checkDeployTooling() {
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// Intent-runner drift (task 1002722): a runner on a stale core once turned a domain-attach
|
|
379
|
+
// into a teardown (task 1002716). Only a host with *intent-runner* units shows this section,
|
|
380
|
+
// so a builder laptop or dev box sees nothing.
|
|
381
|
+
function runnerDriftRows(results) {
|
|
382
|
+
return (results || []).map((r) => {
|
|
383
|
+
const msg = `${r.name}: ${r.reason}`;
|
|
384
|
+
if (r.status === 'ok') return { status: 'ok', msg };
|
|
385
|
+
if (r.status === 'drift') return { status: 'fail', msg: `${msg} — repoint the unit's WorkingDirectory + ExecStart at the installed core (a systemd drop-in, as task 1002716 did).` };
|
|
386
|
+
return { status: 'warn', msg };
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function checkRunnerDrift() {
|
|
391
|
+
let rows = [];
|
|
392
|
+
try {
|
|
393
|
+
const { collectRunnerDrift } = require('./runner-drift.js');
|
|
394
|
+
rows = runnerDriftRows(collectRunnerDrift({ instanceRoot: process.env.BONGOS_INSTANCE_ROOT || process.cwd() }));
|
|
395
|
+
} catch (_) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (!rows.length) return;
|
|
399
|
+
section('Intent runners');
|
|
400
|
+
const emit = { ok, warn, fail };
|
|
401
|
+
for (const row of rows) emit[row.status]('runners', row.msg);
|
|
402
|
+
}
|
|
403
|
+
|
|
378
404
|
// --------------------------------------------------------------------------
|
|
379
405
|
// main
|
|
380
406
|
// --------------------------------------------------------------------------
|
|
381
407
|
// Guarded so requiring this file for its pure helpers (tests/doctor_hooks.mjs)
|
|
382
408
|
// does not run the checks, which shell out to git and hit the network.
|
|
383
|
-
module.exports = { classifyHooksState };
|
|
409
|
+
module.exports = { classifyHooksState, runnerDriftRows };
|
|
384
410
|
|
|
385
411
|
if (require.main === module) (async () => {
|
|
386
412
|
console.log(`${C.bold}GDS Doctor${C.reset} — local-machine health check`);
|
|
@@ -390,6 +416,7 @@ if (require.main === module) (async () => {
|
|
|
390
416
|
checkGitPlumbing();
|
|
391
417
|
await checkRepoState();
|
|
392
418
|
checkEnvironment();
|
|
419
|
+
checkRunnerDrift();
|
|
393
420
|
|
|
394
421
|
const fails = findings.filter((f) => f.status === 'fail').length;
|
|
395
422
|
const warns = findings.filter((f) => f.status === 'warn').length;
|
|
@@ -333,6 +333,8 @@ function cardFor(code) {
|
|
|
333
333
|
// AGENT SURFACE ONLY: a card is written in the agent's voice ("I will…") for a
|
|
334
334
|
// session to relay. A person running the `bongos` CLI reads the text above it directly,
|
|
335
335
|
// and several cards name slash commands that do not exist there (tests/cli_surface.mjs).
|
|
336
|
+
// The labels below are READ BACK by .claude/hooks/stuck-detector.js as its failure
|
|
337
|
+
// signature (task 1003990); tests/stuck_detector.mjs reds if they drift.
|
|
336
338
|
function plainCardLines(code, env = process.env) {
|
|
337
339
|
const c = isTerminal(env) ? null : cardFor(code);
|
|
338
340
|
if (!c) return [];
|
package/scripts/gds/provision.js
CHANGED
|
@@ -54,6 +54,7 @@ const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbN
|
|
|
54
54
|
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
55
|
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
|
|
57
|
+
const { installedCoreVersion, runnerBanner } = require('./runner-drift.js');
|
|
57
58
|
const { teardownInstance } = require('./provision-teardown.js');
|
|
58
59
|
const { disconnectInstance } = require('./provision-disconnect.js');
|
|
59
60
|
const { coreUpgradeInstance, snapshotAllCoreVersions } = require('./provision-core-upgrade.js'); // the owner's deploy door, ADR 0293
|
|
@@ -1221,6 +1222,8 @@ async function drainOAuthManifests(deps) {
|
|
|
1221
1222
|
// ---------------------------------------------------------------------------
|
|
1222
1223
|
async function cmdRunIntents(deps) {
|
|
1223
1224
|
const { db, provisioning, log, apply } = deps;
|
|
1225
|
+
const instanceRoot = deps.instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
|
|
1226
|
+
log(runnerBanner({ runnerVersion: coreVersionSafe(), runnerRoot: REPO_ROOT, instanceVersion: installedCoreVersion(instanceRoot), instanceRoot }));
|
|
1224
1227
|
if (!apply) {
|
|
1225
1228
|
const { rows } = await db.query(
|
|
1226
1229
|
`SELECT pi.*, i.slug FROM provisioning_intents pi
|
|
@@ -1254,7 +1257,7 @@ async function cmdRunIntents(deps) {
|
|
|
1254
1257
|
else if (intent.action === 'restart') result = await restartInstance(inst, deps);
|
|
1255
1258
|
else if (intent.action === 'disconnect') result = await disconnectInstance(inst, deps);
|
|
1256
1259
|
else if (intent.action === 'core-upgrade') result = await coreUpgradeInstance(inst, deps, intent); // also reads the INTENT: the version the owner previewed (ADR 0293 D4)
|
|
1257
|
-
else result = { ok: false, error: `
|
|
1260
|
+
else result = { ok: false, error: `unknown_action '${intent.action}' — runner core ${coreVersionSafe() || '?'} does not know it (runner too old?); refusing to guess` };
|
|
1258
1261
|
if (result && result.ok === false) {
|
|
1259
1262
|
if (intent.attempts >= MAX_INTENT_ATTEMPTS) {
|
|
1260
1263
|
await provisioning.resolveIntent(db, intent.id, 'error', result.error || 'op failed');
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/runner-drift.js — do the intent-runner units run the core this instance installed? (task 1002722)
|
|
3
|
+
//
|
|
4
|
+
// WHY. For weeks the prod intent runners ran core 1.19.2 from a retired clone while the
|
|
5
|
+
// instance served a current core, so an owner's domain-attach click executed as a TEARDOWN
|
|
6
|
+
// (task 1002716). The hotfix repointed the units with systemd drop-ins; nothing noticed the
|
|
7
|
+
// drift, and the only tell was a PWD in sudo logs. This reads each `*intent-runner*` unit the
|
|
8
|
+
// way systemd does (a drop-in overrides the base file), finds the core package the ExecStart
|
|
9
|
+
// script lives in, and compares its version to the instance's installed @bongos/core.
|
|
10
|
+
//
|
|
11
|
+
// A unit whose version cannot be read — a shell wrapper that picks the core at runtime, a
|
|
12
|
+
// missing package — is UNRESOLVED, never ok: a gate that guesses green is the incident again.
|
|
13
|
+
//
|
|
14
|
+
// node scripts/gds/runner-drift.js [--instance-root <dir>] [--json]
|
|
15
|
+
// exit 0 = every unit resolved and matched (or no systemd here), 1 = drift, 2 = bad usage.
|
|
16
|
+
// Unresolved units are listed but do not fail the run. `bongos doctor` runs the same check.
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { spawnSync } = require('node:child_process');
|
|
22
|
+
|
|
23
|
+
const CORE_NAMES = ['@bongos/core', '@cloudbongos/core'];
|
|
24
|
+
const P = path.posix; // unit files describe the Linux control plane, whatever runs the tests
|
|
25
|
+
|
|
26
|
+
// The effective WorkingDirectory + ExecStart lines from `systemctl cat` output. Later files
|
|
27
|
+
// are drop-ins: a later WorkingDirectory wins, an empty `ExecStart=` clears the list, and a
|
|
28
|
+
// drop-in that only ADDS one leaves the base command running too (oneshot runs them all).
|
|
29
|
+
function effectiveUnit(catText) {
|
|
30
|
+
let workingDirectory = null;
|
|
31
|
+
let execs = [];
|
|
32
|
+
for (const raw of String(catText || '').split(/\r?\n/)) {
|
|
33
|
+
const line = raw.trim();
|
|
34
|
+
const m = line.match(/^(WorkingDirectory|ExecStart)=(.*)$/);
|
|
35
|
+
if (!m) continue;
|
|
36
|
+
const value = m[2].trim();
|
|
37
|
+
if (m[1] === 'WorkingDirectory') workingDirectory = value.replace(/^-/, '') || null;
|
|
38
|
+
else if (value === '') execs = [];
|
|
39
|
+
else execs.push(value);
|
|
40
|
+
}
|
|
41
|
+
return { workingDirectory, execStarts: execs };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The absolute path of the .js script an ExecStart runs, or null when its first argument is
|
|
45
|
+
// not one (a shell wrapper decides at runtime which core it loads).
|
|
46
|
+
function runnerScriptPath({ workingDirectory, execStart }) {
|
|
47
|
+
const tokens = String(execStart || '').trim().split(/\s+/).filter(Boolean);
|
|
48
|
+
if (!tokens.length) return null;
|
|
49
|
+
let i = 0;
|
|
50
|
+
if (P.basename(tokens[i].replace(/^[@\-:+!]+/, '')) === 'env') {
|
|
51
|
+
i++;
|
|
52
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
|
|
53
|
+
}
|
|
54
|
+
const script = tokens.slice(i + 1).find((tok) => !tok.startsWith('-'));
|
|
55
|
+
if (!script || !/\.[cm]?js$/.test(script)) return null;
|
|
56
|
+
if (script.startsWith('/')) return script;
|
|
57
|
+
return workingDirectory ? P.join(workingDirectory, script) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJson(p, fsImpl) {
|
|
61
|
+
try { return JSON.parse(fsImpl.readFileSync(p, 'utf8')); } catch { return null; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The core version a script belongs to: the nearest enclosing @bongos/core package, or a
|
|
65
|
+
// source checkout's CORE_VERSION. Null when neither is found.
|
|
66
|
+
function coreVersionFor(scriptPath, { fsImpl = fs } = {}) {
|
|
67
|
+
let dir = P.dirname(String(scriptPath || ''));
|
|
68
|
+
for (;;) {
|
|
69
|
+
const pkg = readJson(P.join(dir, 'package.json'), fsImpl);
|
|
70
|
+
if (pkg && CORE_NAMES.includes(pkg.name) && pkg.version) return String(pkg.version);
|
|
71
|
+
try {
|
|
72
|
+
const m = String(fsImpl.readFileSync(P.join(dir, 'src', 'module-api.js'), 'utf8')).match(/CORE_VERSION\s*=\s*['"]([^'"]+)['"]/);
|
|
73
|
+
if (m) return m[1];
|
|
74
|
+
} catch { /* not a checkout root */ }
|
|
75
|
+
const up = P.dirname(dir);
|
|
76
|
+
if (up === dir) return null;
|
|
77
|
+
dir = up;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function installedCoreVersion(instanceRoot, { fsImpl = fs } = {}) {
|
|
82
|
+
if (!instanceRoot) return null;
|
|
83
|
+
for (const name of CORE_NAMES) {
|
|
84
|
+
const pkg = readJson(P.join(instanceRoot, 'node_modules', name, 'package.json'), fsImpl);
|
|
85
|
+
if (pkg && pkg.version) return String(pkg.version);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assessExecStart({ workingDirectory, execStart, instanceVersion, instanceRoot, fsImpl }) {
|
|
91
|
+
const scriptPath = runnerScriptPath({ workingDirectory, execStart });
|
|
92
|
+
const base = { scriptPath, runnerVersion: null, instanceVersion };
|
|
93
|
+
if (!scriptPath) return { ...base, status: 'unresolved', reason: `ExecStart does not run a .js script directly (${execStart}) — cannot tell which core it loads` };
|
|
94
|
+
const runnerVersion = coreVersionFor(scriptPath, { fsImpl });
|
|
95
|
+
const found = { ...base, runnerVersion };
|
|
96
|
+
if (!runnerVersion) return { ...found, status: 'unresolved', reason: `no core version found above ${scriptPath}` };
|
|
97
|
+
if (!instanceVersion) return { ...found, status: 'unresolved', reason: `no installed @bongos/core under ${instanceRoot}` };
|
|
98
|
+
if (runnerVersion === instanceVersion) return { ...found, status: 'ok', reason: `runs core ${runnerVersion}` };
|
|
99
|
+
return { ...found, status: 'drift', reason: `runs core ${runnerVersion} (${scriptPath}) but the instance installed ${instanceVersion} at ${instanceRoot}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// One verdict per unit, from its WORST ExecStart: drift, then unresolved, then ok.
|
|
103
|
+
function assessRunnerUnit({ name, catText, instanceRoot, fsImpl = fs }) {
|
|
104
|
+
const { workingDirectory, execStarts } = effectiveUnit(catText);
|
|
105
|
+
const instanceVersion = installedCoreVersion(instanceRoot, { fsImpl });
|
|
106
|
+
if (!execStarts.length) return { name, status: 'unresolved', reason: 'no ExecStart in the unit', scriptPath: null, runnerVersion: null, instanceVersion };
|
|
107
|
+
const all = execStarts.map((execStart) => assessExecStart({ workingDirectory, execStart, instanceVersion, instanceRoot, fsImpl }));
|
|
108
|
+
const worst = ['drift', 'unresolved', 'ok'].map((s) => all.find((r) => r.status === s)).find(Boolean);
|
|
109
|
+
return { name, ...worst };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseUnitList(stdout) {
|
|
113
|
+
return String(stdout || '').split(/\r?\n/).map((l) => l.trim().split(/\s+/)[0]).filter((n) => n && n.endsWith('.service'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The first line of every run-intents, so a journal grep shows a stale runner at once.
|
|
117
|
+
function runnerBanner({ runnerVersion, runnerRoot, instanceVersion, instanceRoot }) {
|
|
118
|
+
const tail = instanceVersion ? `instance core ${instanceVersion} at ${instanceRoot}` : `no installed core at ${instanceRoot}`;
|
|
119
|
+
const line = `runner core ${runnerVersion || '?'} from ${runnerRoot} (${tail})`;
|
|
120
|
+
return runnerVersion && instanceVersion && runnerVersion !== instanceVersion ? `⚠ DRIFT: ${line}` : line;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Every intent-runner unit on this host, assessed. Null when there is no systemd to ask.
|
|
124
|
+
function collectRunnerDrift({ instanceRoot }) {
|
|
125
|
+
const run = (args) => spawnSync('systemctl', args, { encoding: 'utf8', timeout: 15000 });
|
|
126
|
+
const list = run(['list-units', '--all', '--plain', '--no-legend', '--no-pager', '--type=service', '*intent-runner*']);
|
|
127
|
+
if (list.error || list.status !== 0) return null;
|
|
128
|
+
return parseUnitList(list.stdout).map((name) => {
|
|
129
|
+
const cat = run(['cat', '--no-pager', name]);
|
|
130
|
+
return assessRunnerUnit({ name, catText: cat.status === 0 ? cat.stdout : '', instanceRoot });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function main(argv) {
|
|
135
|
+
const at = argv.indexOf('--instance-root');
|
|
136
|
+
const instanceRoot = (at >= 0 ? argv[at + 1] : null) || process.env.BONGOS_INSTANCE_ROOT;
|
|
137
|
+
if (!instanceRoot) {
|
|
138
|
+
console.error('runner-drift: name the instance root — --instance-root <dir> or BONGOS_INSTANCE_ROOT');
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
const results = collectRunnerDrift({ instanceRoot });
|
|
142
|
+
if (argv.includes('--json')) console.log(JSON.stringify({ instanceRoot, systemd: results !== null, units: results || [] }, null, 2));
|
|
143
|
+
else if (results === null) console.log('runner-drift: no systemd on this host — nothing to check');
|
|
144
|
+
else if (!results.length) console.log('runner-drift: no *intent-runner* units on this host');
|
|
145
|
+
else for (const r of results) console.log(`${r.status === 'ok' ? '✓' : r.status === 'drift' ? '✖' : '?'} ${r.name}: ${r.reason}`);
|
|
146
|
+
return results && results.some((r) => r.status === 'drift') ? 1 : 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { effectiveUnit, runnerScriptPath, coreVersionFor, installedCoreVersion, assessRunnerUnit, parseUnitList, runnerBanner, collectRunnerDrift };
|
|
150
|
+
|
|
151
|
+
if (require.main === module) process.exitCode = main(process.argv.slice(2));
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.838'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|