@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.
@@ -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.836",
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.836",
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.836",
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",
@@ -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 };
@@ -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 [];
@@ -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: `unknown intent action '${intent.action}' — refusing to guess` };
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.836'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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');