@bongos/core 1.19.837 → 1.19.839

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();
@@ -352,7 +352,8 @@
352
352
  <svg><use href="#orn-diamond"/></svg>
353
353
  </div>
354
354
  <p class="foot__seal">By order of the Senate</p>
355
- <p>{{worldName}} is the work of <a href="https://hermeslines.com" target="_blank" rel="noopener"><strong>Hermeslines</strong></a>, a third-party logistics house in Cleveland, Ohio.</p>
355
+ <!-- task 1002853: every company field is optional, so each clause drops whole when its pack field is empty -->
356
+ <!--brand-if:companyName--><p>{{worldName}} is the work of <!--brand-if:companySite--><a href="{{companySite}}" target="_blank" rel="noopener"><!--/brand-if:companySite--><strong>{{companyName}}</strong><!--brand-if:companySite--></a><!--/brand-if:companySite--><!--brand-if:companyTagline-->, {{companyTagline}}<!--/brand-if:companyTagline-->.</p><!--/brand-if:companyName-->
356
357
  <p>{{Currency}} are wrought by the lawmen's review; redemption awaits their seal.</p>
357
358
  </footer>
358
359
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.837",
3
+ "version": "1.19.839",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.837",
9
+ "version": "1.19.839",
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.837",
3
+ "version": "1.19.839",
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",
@@ -41,6 +41,9 @@ const { CORE_ROOT, MODULE_ROOTS, REPO_ROOT, rel } = require('./fitness-lib.js');
41
41
  const PROMPT_IDENTITY_LITERALS = [
42
42
  /off the boats/i,
43
43
  /amazonprimea/i,
44
+ // The host's operating company (task 1002853): the scan already covered the status
45
+ // page's HTML, but with only the product listed its footer credited this company for years.
46
+ /hermeslines/i,
44
47
  ];
45
48
  // Scan roots (recursive) + kernel-prompt CLIs that live outside them.
46
49
  // scripts/gds joined at task 1002427: the builder CLI was UNSCANNED, which is how
@@ -101,7 +104,7 @@ const IDENTITY_SCAN_EXTRA_FILES = [];
101
104
  const IDENTITY_SCAN_ALLOWED = {
102
105
  // task 1003206 moved Check 13's own regexes out of fitness.js into THIS file
103
106
  // when the monolith was split, so the detector's self-carve-out moved with them.
104
- 'scripts/gds/fitness-checks-identity.js': 2,
107
+ 'scripts/gds/fitness-checks-identity.js': 3,
105
108
  'scripts/gds/package-core.js': 1,
106
109
  // audit-replay.sh + db-backup-{nightly,offsite}.sh were carved out here as
107
110
  // "host-pinned ops"; task 1002757 RETIRED that justification for them — each
@@ -125,6 +128,11 @@ const IDENTITY_SCAN_ALLOWED = {
125
128
  'scripts/gds/seed-productization-tasks.js': 6,
126
129
  'scripts/gds/seed-provisioning-tasks.js': 2,
127
130
  'scripts/gds/version-close.js': 1,
131
+ // task 1002853 (the company literal joined the list): audit-authorship.sh is itself a
132
+ // detector — its ops allowlist names the founder's commit address so it can exempt it; grade-smoke.js
133
+ // is a protected grader probe whose fixture task prose names the company, the seed-*.js rationale.
134
+ 'scripts/gds/audit-authorship.sh': 1,
135
+ 'scripts/gds/grade-smoke.js': 1,
128
136
  };
129
137
  // `.sh` joined at task 1002567: the extension set was `.js/.html/.css`, so NO shell
130
138
  // script had ever been scanned — which is how scripts/gds/lib.sh kept a hardcoded
@@ -113,6 +113,12 @@ const ROOT_CHAR_HARD_MAX = 42000; // ~10.5K tokens — the always-loaded file mu
113
113
  const ROOT_CHAR_TARGET = 32000; // ~8K tokens — aspirational, mirrors the ~200-line aim.
114
114
  const estTokens = (n) => Math.round(n / 4); // ~4 chars/token for markdown prose.
115
115
 
116
+ // Where the charter BODY lives since task 1003687. It is guidance for EDITING a
117
+ // CLAUDE.md, so it is paths-scoped (`paths:` frontmatter, same mechanism as
118
+ // .claude/rules/hook-authoring.md) and costs nothing in the sessions — the large
119
+ // majority — that never open one. The kernel keeps the heading and a pointer.
120
+ const CHARTER_RULE_REL = '.claude/rules/claudemd-authoring.md';
121
+
116
122
  function checkClaudeMdBudget(opts = {}) {
117
123
  const p = path.join(INSTANCE_ROOT, 'CLAUDE.md'); // the root CLAUDE.md is INSTANCE content (materialized per instance; ADR 0108)
118
124
  const text = opts.text != null ? opts.text : fs.readFileSync(p, 'utf8');
@@ -133,8 +139,35 @@ function checkClaudeMdBudget(opts = {}) {
133
139
  }
134
140
  if (!text.includes('## Working with this file (content charter)')) {
135
141
  violations.push('CLAUDE.md is missing the content-charter section (ADR 0061) — the per-line test + "what goes where" table must stay.');
142
+ } else {
143
+ // Task 1003687 relocated the charter BODY to a paths-scoped rule, so the kernel
144
+ // keeps the heading and a pointer. That makes the heading cheap to fake: a section
145
+ // with nothing behind it satisfies a substring check while the rules it names are
146
+ // gone. So the charter must resolve to something — either inline here, or through a
147
+ // pointer whose target actually exists and still carries the two rules + the table.
148
+ const namesRule = text.includes(CHARTER_RULE_REL);
149
+ const inlineCharter = text.includes('Per-line test:');
150
+ if (namesRule) {
151
+ const rulePath = path.join(INSTANCE_ROOT, CHARTER_RULE_REL); // .claude/ is materialized INSTANCE content (ADR 0108 §3)
152
+ if (!fs.existsSync(rulePath)) {
153
+ violations.push(`CLAUDE.md points its content charter at ${CHARTER_RULE_REL}, but that file does not exist — a pointer to nothing is worse than no pointer (ADR 0061 / task 1003687).`);
154
+ } else {
155
+ const rule = fs.readFileSync(rulePath, 'utf8');
156
+ const missing = [];
157
+ if (!/^---\r?\n[\s\S]*?\bpaths:[\s\S]*?["']\*\*\/CLAUDE\.md["'][\s\S]*?^---$/m.test(rule)) {
158
+ missing.push('`paths:` frontmatter matching `**/CLAUDE.md` (without it the rule never loads, and the charter is simply gone)');
159
+ }
160
+ if (!rule.includes('Per-line test:')) missing.push('the per-line test');
161
+ if (!rule.includes('## What goes where')) missing.push('the "what goes where" table');
162
+ if (missing.length) {
163
+ violations.push(`${CHARTER_RULE_REL} is missing ${missing.join(', ')} — the kernel delegates the charter to it, so the charter is only as real as that file (ADR 0061 / task 1003687).`);
164
+ }
165
+ }
166
+ } else if (!inlineCharter) {
167
+ violations.push('The CLAUDE.md content-charter section is hollow — it has the heading but neither the per-line test inline nor a pointer to `' + CHARTER_RULE_REL + '`. The heading exists to make the rules findable; on its own it only satisfies this check (ADR 0061).');
168
+ }
136
169
  }
137
- return { name: 'root CLAUDE.md budget + charter', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings, note: `≤ ${ROOT_HARD_MAX} lines / ${ROOT_CHAR_HARD_MAX} chars (~${estTokens(ROOT_CHAR_HARD_MAX)} tok); targets ~${ROOT_TARGET} lines / ~${estTokens(ROOT_CHAR_TARGET)} tok; charter present.` };
170
+ return { name: 'root CLAUDE.md budget + charter', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings, note: `≤ ${ROOT_HARD_MAX} lines / ${ROOT_CHAR_HARD_MAX} chars (~${estTokens(ROOT_CHAR_HARD_MAX)} tok); targets ~${ROOT_TARGET} lines / ~${estTokens(ROOT_CHAR_TARGET)} tok; charter resolves (inline or via ${CHARTER_RULE_REL}).` };
138
171
  }
139
172
 
140
173
  // ============================================================================
@@ -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 [];
@@ -48,7 +48,7 @@ const { execFileSync } = require('node:child_process');
48
48
 
49
49
  const REPO_ROOT = path.resolve(__dirname, '..');
50
50
  const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
51
- const { branding, clientBranding } = require('../src/branding');
51
+ const { branding, clientBranding, applyBrandConditionals } = require('../src/branding');
52
52
 
53
53
  // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
54
54
  // served by a static Pages host, NOT the branding-injecting Node server — so this
@@ -63,15 +63,19 @@ function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
63
63
  const tagline = (brand.copy && brand.copy.tagline) || '';
64
64
  const cur = (brand.currency && brand.currency.label) || 'credits';
65
65
  const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
66
+ const company = (brand.identity && brand.identity.company) || {};
66
67
  const esc = (s) => String(s == null ? '' : s)
67
68
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
68
69
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
69
- return html
70
+ return applyBrandConditionals(html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline })
70
71
  .split('{{worldName}}').join(esc(world))
71
72
  .split('{{productName}}').join(esc(product))
72
73
  .split('{{Currency}}').join(esc(Cur))
73
74
  .split('{{currency}}').join(esc(cur))
74
75
  .split('{{tagline}}').join(esc(tagline))
76
+ .split('{{companyName}}').join(esc(company.name))
77
+ .split('{{companySite}}').join(esc(company.site))
78
+ .split('{{companyTagline}}').join(esc(company.tagline))
75
79
  .split('{{publicOrigin}}').join(esc(publicOrigin || ''))
76
80
  .split('{{buildersOrigin}}').join(esc(buildersOrigin || publicOrigin || ''));
77
81
  }
@@ -46,7 +46,7 @@ const { buildStampedHtml } = require('../asset-version');
46
46
  const { buildInfo } = require('../build-info');
47
47
  const { buildDevboxDownloads } = require('../devbox-downloads');
48
48
  const { buildBongosDownloads } = require('../bongos-downloads');
49
- const { clientBranding, branding } = require('../branding');
49
+ const { clientBranding, branding, applyBrandConditionals } = require('../branding');
50
50
  const { prelaunchGate } = require('./prelaunch-gate');
51
51
  const { platformVisibilityGate } = require('./platform-visibility-gate');
52
52
  const { clientModules, isModuleEnabled } = require('../modules');
@@ -205,6 +205,8 @@ function injectBrandingGlobal(html, brand = clientBranding(), mods = clientModul
205
205
  // served page can carry a correct cross-host href or canonical/og: URL: a client
206
206
  // fill cannot reach them, and a baked one would make every instance advertise
207
207
  // whichever instance happened to author the file (task 1002748, direction C5/C6).
208
+ // {{companyName}} {{companySite}} {{companyTagline}} are optional, so a page wraps
209
+ // each clause in a brand-if region that drops whole when its field is empty (task 1002853).
208
210
  // Pure + exported for the test; both origins are injectable so it drives with zero I/O.
209
211
  function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl(), instanceBase = safeInstanceBase(), hubOrigin = safeHubOrigin()) {
210
212
  const world = (brand.identity && brand.identity.worldName) || '';
@@ -213,12 +215,17 @@ function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOri
213
215
  const cur = (brand.currency && brand.currency.label) || 'credits';
214
216
  const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
215
217
  const hall = hallOrigin || origin || '';
216
- return dropEmptyHrefLinks(String(html == null ? '' : html), { publicOrigin: origin || '', buildersOrigin: hall, sourceRepoUrl: sourceRepo || '' })
218
+ const company = (brand.identity && brand.identity.company) || {};
219
+ const conditioned = applyBrandConditionals(html == null ? '' : html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline });
220
+ return dropEmptyHrefLinks(conditioned, { publicOrigin: origin || '', buildersOrigin: hall, sourceRepoUrl: sourceRepo || '' })
217
221
  .split('{{worldName}}').join(esc(world))
218
222
  .split('{{productName}}').join(esc(product))
219
223
  .split('{{Currency}}').join(esc(Cur))
220
224
  .split('{{currency}}').join(esc(cur))
221
225
  .split('{{tagline}}').join(esc(tagline))
226
+ .split('{{companyName}}').join(esc(company.name))
227
+ .split('{{companySite}}').join(esc(company.site))
228
+ .split('{{companyTagline}}').join(esc(company.tagline))
222
229
  .split('{{publicOrigin}}').join(esc(origin || ''))
223
230
  // a pack that names no hall of its own resolves to its OWN apex — never to
224
231
  // whichever instance authored the file
package/src/branding.js CHANGED
@@ -221,7 +221,11 @@ function clientBranding(b = branding()) {
221
221
  identity: {
222
222
  productName: b.identity?.productName || '',
223
223
  worldName: b.identity?.worldName || '',
224
- company: { name: b.identity?.company?.name || '' },
224
+ company: {
225
+ name: b.identity?.company?.name || '',
226
+ site: b.identity?.company?.site || '',
227
+ tagline: b.identity?.company?.tagline || '',
228
+ },
225
229
  },
226
230
  currency: {
227
231
  label: b.currency?.label || 'credits',
@@ -308,4 +312,20 @@ function clientReward() {
308
312
  }
309
313
  }
310
314
 
311
- module.exports = { resolveBranding, loadBranding, deepMerge, branding, clientBranding, userAgent, NEUTRAL_PATH, INSTANCE_PATH };
315
+ // Resolve the `<!--brand-if:KEY-->…<!--/brand-if:KEY-->` regions of a served page:
316
+ // kept (markers removed) when `values[KEY]` is non-empty, dropped whole otherwise,
317
+ // so an optional pack field removes its entire clause instead of rendering half a
318
+ // sentence (task 1002853). Lives here, not in serve-internal, because the off-box
319
+ // status mirror stamps the same page without loading the server. Repeats until
320
+ // stable so a region nested inside a kept one resolves too.
321
+ const BRAND_IF_REGION = /<!--brand-if:(\w+)-->([\s\S]*?)<!--\/brand-if:\1-->/g;
322
+ function applyBrandConditionals(html, values) {
323
+ let out = String(html);
324
+ for (let prev; prev !== out;) {
325
+ prev = out;
326
+ out = out.replace(BRAND_IF_REGION, (_, key, inner) => (values[key] ? inner : ''));
327
+ }
328
+ return out;
329
+ }
330
+
331
+ module.exports = { resolveBranding, loadBranding, deepMerge, branding, clientBranding, userAgent, applyBrandConditionals, NEUTRAL_PATH, INSTANCE_PATH };
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.837'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.839'; // 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');