@bongos/core 1.19.871 → 1.19.872
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 +163 -93
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
|
@@ -35,6 +35,13 @@ const PER_TEST_TIMEOUT_MS = 90_000;
|
|
|
35
35
|
// DB-free unit file, so the `--integration` gate (task 1002221 / audit H1) gives each one a much
|
|
36
36
|
// longer budget than the 90s unit cap.
|
|
37
37
|
const INTEGRATION_TIMEOUT_MS = 300_000;
|
|
38
|
+
// A PARKED test is expected to fail, and a failing test fails fast — it throws or
|
|
39
|
+
// trips an assertion. Handing it the full run-to-completion budget only buys the
|
|
40
|
+
// right to burn five billable CPU-minutes per HUNG entry on every main push, and
|
|
41
|
+
// this set is re-checked on each one. A minute is ample for a verdict that is
|
|
42
|
+
// expected to arrive in seconds; an entry that needs longer than that to fail is
|
|
43
|
+
// itself the finding.
|
|
44
|
+
const PARKED_RECHECK_TIMEOUT_MS = 60_000;
|
|
38
45
|
|
|
39
46
|
// Feature-module test sets (R59 / task 1198). A test that exercises a feature
|
|
40
47
|
// MODULE runs only when this instance enables that module — a vanilla Cloud
|
|
@@ -268,6 +275,79 @@ function discoverModuleTests({ modulesDir = MODULES_DIR, isEnabled = isModuleEna
|
|
|
268
275
|
return { included, skippedDisabled };
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
// ── INTEGRATION_PARKED — the integration lane's known-failing set (task 1004034) ──
|
|
279
|
+
//
|
|
280
|
+
// The integration lane (task 1000862) gave these 20 their FIRST EVER run on
|
|
281
|
+
// 2026-09-17 and all 20 failed. None is a regression: nothing had ever executed
|
|
282
|
+
// them, in any lane, so nothing was keeping them honest. Parked by owner decision
|
|
283
|
+
// so the lane can gate the tests that DO pass — a gate that is already red gates
|
|
284
|
+
// nothing, the same argument task 1002617 made for the unit lane.
|
|
285
|
+
//
|
|
286
|
+
// WHY A SEPARATE SET, AND NOT KNOWN_FAILING. KNOWN_FAILING is paired with
|
|
287
|
+
// tests/known_failing_ratchet.mjs, which re-executes each parked test and reds the
|
|
288
|
+
// suite the moment one starts passing. That ratchet runs in the DB-FREE lane: an
|
|
289
|
+
// integration test executed there fails for want of a database, so `still fails`
|
|
290
|
+
// would be trivially true and a genuine fix would go unnoticed. Parking these there
|
|
291
|
+
// would have bought the appearance of tracking and none of it.
|
|
292
|
+
//
|
|
293
|
+
// So the guarantee is kept where it can actually hold: `--integration-parked` runs
|
|
294
|
+
// this set WITH the database and FAILS when one PASSES. Same ratchet semantics, in
|
|
295
|
+
// the only environment that can tell the difference. The integration workflow runs
|
|
296
|
+
// it as a blocking step, so fixing one of these reds the lane until its entry here
|
|
297
|
+
// is removed.
|
|
298
|
+
//
|
|
299
|
+
// Triage per test, and the owner decision, are on task 1004034. The causes are
|
|
300
|
+
// heterogeneous — stale post-de-branding expectations, unseeded fixtures, temp-dir
|
|
301
|
+
// assumptions, a missing core tarball, DB-state collisions — so these come off this
|
|
302
|
+
// list one at a time, not in a batch.
|
|
303
|
+
//
|
|
304
|
+
// acclaim_feed_evidence_db WAS LEFT OUT OF THE FIRST PASS, deliberately and on the
|
|
305
|
+
// evidence then available: it PASSED in CI run 35291948008, and parking a test that
|
|
306
|
+
// passes makes --integration-parked red the lane immediately. Two later readings
|
|
307
|
+
// reversed that — it fails 2/2 alone on a freshly migrated database, and it FAILED in
|
|
308
|
+
// run 35404478232, the first run after the parking landed. The single pass was the
|
|
309
|
+
// anomaly. It fails on `duplicate key value violates unique constraint
|
|
310
|
+
// "versions_one_planning_idx"`: the seeds create a version at status='planning', a
|
|
311
|
+
// partial unique index permits exactly one, and the test adds another — so it leans on
|
|
312
|
+
// the seeded state of `versions` rather than establishing its own, which is also how
|
|
313
|
+
// its verdict moves between runs. needs_rebase trips the same constraint and is
|
|
314
|
+
// already here. IF THE PARKED RATCHET EVER REDS NAMING THIS TEST, that is this known
|
|
315
|
+
// oscillation, not a fix: the real repair is to stop it colliding — give it its own
|
|
316
|
+
// version row, or roll back in a transaction the way the other DB-writing entries do.
|
|
317
|
+
//
|
|
318
|
+
// handle_machinery IS THE SAME DEFECT SEEN FROM THE OTHER SIDE, and it is why the
|
|
319
|
+
// ratchet moved to its own job with its own database (task 1004043). It PASSES once
|
|
320
|
+
// the suite has run and FAILS from a pristine one — verified by migrating an empty
|
|
321
|
+
// database and running it alone. So it is correctly parked: it fails from exactly the
|
|
322
|
+
// state the gate starts in. Do NOT unpark it on the strength of a green post-suite
|
|
323
|
+
// reading; that reading answers a different question. Like acclaim_feed_evidence_db
|
|
324
|
+
// above, the real repair is for the test to establish the state it needs instead of
|
|
325
|
+
// inheriting whatever ran before it.
|
|
326
|
+
//
|
|
327
|
+
// THE THREE DEV-BOX TESTS ARE PARKED, NOT DELETED, and that reverses a first pass
|
|
328
|
+
// of this task. The owner ruled that dev-box work can go, and boxes / box_onboard /
|
|
329
|
+
// box_connect_e2e were duly deleted — but modules/dev-box/ is still present, still
|
|
330
|
+
// mounted and unmodified by that change, and those files carry the only regression
|
|
331
|
+
// coverage for LIVE guards: the setBoxState column allowlist (SQL injection), the
|
|
332
|
+
// rank/authz floors, the spoofed X-Forwarded-Host that must never reach the served
|
|
333
|
+
// connect.sh, and the authorized_keys command= / embedded-newline rejections.
|
|
334
|
+
// Deleting a security regression test for code that still runs is not retirement,
|
|
335
|
+
// it is removing the alarm and leaving the door. modules/dev-box/CLAUDE.md also
|
|
336
|
+
// still names boxes + box_onboard as that module's required suite, and
|
|
337
|
+
// scripts/gds/smoke-box.sh still invokes all three by path — deleting the files
|
|
338
|
+
// turned that script into an immediate MODULE_NOT_FOUND. They come out when the
|
|
339
|
+
// module does, in the change that removes it.
|
|
340
|
+
const INTEGRATION_PARKED = new Set([
|
|
341
|
+
'acclaim_feed_evidence_db',
|
|
342
|
+
'bfg', 'bfg_write', 'box_connect_e2e', 'box_onboard', 'boxes',
|
|
343
|
+
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
344
|
+
'clawback_confirmed_backlog', 'consumer_layout_boot', 'copy_desk_flags_db',
|
|
345
|
+
'government_board_membership_db', 'handle_machinery', 'hub_profile_leak',
|
|
346
|
+
'idea_credit_streams_db', 'memory_isolation', 'memory_pull', 'needs_rebase',
|
|
347
|
+
'profile_fields', 'project_visibility_db', 'reward_mode_cost_plus_only',
|
|
348
|
+
'session_earnings_mirror_db', 'setup_smoke', 'status-probe',
|
|
349
|
+
]);
|
|
350
|
+
|
|
271
351
|
function main() {
|
|
272
352
|
// Two gates from one runner (task 1002221 / audit H1):
|
|
273
353
|
// default — the DB-free UNIT gate: every test file MINUS the integration set (+ known-
|
|
@@ -278,6 +358,44 @@ function main() {
|
|
|
278
358
|
// is what promotes tests/consumer_layout_boot.mjs from "excluded + ad-hoc" to a
|
|
279
359
|
// first-class gate a release step can invoke with one command.
|
|
280
360
|
const integrationMode = process.argv.includes('--integration');
|
|
361
|
+
|
|
362
|
+
// --integration-parked — the ratchet for INTEGRATION_PARKED. Runs ONLY the parked
|
|
363
|
+
// set and inverts the verdict: a parked test that PASSES fails this run, because an
|
|
364
|
+
// allowlist that outlives the failures it documents is how a graveyard starts. Needs
|
|
365
|
+
// the same database the integration lane provides; running it DB-free would report
|
|
366
|
+
// every entry as still-failing and prove nothing.
|
|
367
|
+
if (process.argv.includes('--integration-parked')) {
|
|
368
|
+
const parked = [...INTEGRATION_PARKED].sort();
|
|
369
|
+
console.log(`run-unit-tests --integration-parked: re-checking ${parked.length} parked test(s)`);
|
|
370
|
+
const nowPassing = [];
|
|
371
|
+
for (const name of parked) {
|
|
372
|
+
const file = path.join(TESTS_DIR, `${name}.mjs`);
|
|
373
|
+
if (!fs.existsSync(file)) {
|
|
374
|
+
console.log(` GONE ${name} — parked but the file no longer exists; remove its entry`);
|
|
375
|
+
nowPassing.push(name);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
execFileSync(process.execPath, [file], {
|
|
380
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
381
|
+
timeout: PARKED_RECHECK_TIMEOUT_MS,
|
|
382
|
+
env: { ...process.env, NODE_ENV: 'test' },
|
|
383
|
+
});
|
|
384
|
+
console.log(` PASSES ${name} — fixed; remove it from INTEGRATION_PARKED`);
|
|
385
|
+
nowPassing.push(name);
|
|
386
|
+
} catch (_) {
|
|
387
|
+
console.log(` still red ${name}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (nowPassing.length) {
|
|
391
|
+
console.error(`\nrun-unit-tests --integration-parked: ${nowPassing.length} parked test(s) no longer fail — ${nowPassing.join(', ')}.`);
|
|
392
|
+
console.error('Remove each from INTEGRATION_PARKED in scripts/gds/run-unit-tests.js so the lane starts gating it again (task 1004034).');
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
console.log('every parked test still fails — the list is current.');
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
281
399
|
const perTestTimeout = integrationMode ? INTEGRATION_TIMEOUT_MS : PER_TEST_TIMEOUT_MS;
|
|
282
400
|
|
|
283
401
|
const files = fs
|
|
@@ -285,7 +403,7 @@ function main() {
|
|
|
285
403
|
.filter((f) => f.endsWith('.mjs'))
|
|
286
404
|
.map((f) => f.replace(/\.mjs$/, ''))
|
|
287
405
|
.filter((name) => integrationMode
|
|
288
|
-
? (INTEGRATION.has(name) && !KNOWN_FAILING.has(name) && !SUPPORT_MODULES.has(name))
|
|
406
|
+
? (INTEGRATION.has(name) && !KNOWN_FAILING.has(name) && !INTEGRATION_PARKED.has(name) && !SUPPORT_MODULES.has(name))
|
|
289
407
|
: (!INTEGRATION.has(name) && !KNOWN_FAILING.has(name) && !SUPPORT_MODULES.has(name) && !MODULE_DISABLED.has(name)))
|
|
290
408
|
.sort();
|
|
291
409
|
|
|
@@ -385,4 +503,4 @@ function main() {
|
|
|
385
503
|
|
|
386
504
|
if (require.main === module) main();
|
|
387
505
|
|
|
388
|
-
module.exports = { discoverModuleTests, isCleanTeardownAbort, describeFailures, STATUS_DESCRIPTION_MAX };
|
|
506
|
+
module.exports = { discoverModuleTests, isCleanTeardownAbort, describeFailures, STATUS_DESCRIPTION_MAX, INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING };
|
package/scripts/gds/sequence.js
CHANGED
|
@@ -576,6 +576,9 @@ module.exports = {
|
|
|
576
576
|
readArgv,
|
|
577
577
|
edgeIds,
|
|
578
578
|
rowsOf,
|
|
579
|
+
// exported for the Autobongos supervisor (task 1003902) so the claimable feed
|
|
580
|
+
// is read one way, not copied into a second place that can drift.
|
|
581
|
+
fetchClaimableIds,
|
|
579
582
|
fetchGoalTasks,
|
|
580
583
|
fetchGoalTaskRows,
|
|
581
584
|
declaredSurfaceExcludesMigrations,
|
package/scripts/gds/start.js
CHANGED
|
@@ -221,9 +221,15 @@ function cell(s) {
|
|
|
221
221
|
// Per-column values, shared by the markdown table and the widget. Version
|
|
222
222
|
// (V2 / GDS-V4) is its own column; Type is the work lane (Engineer / Artist /
|
|
223
223
|
// Ideator), its own column rather than baked into the title.
|
|
224
|
-
|
|
224
|
+
// task 1003936: ONE word for this axis — Role, per ADR 0174 Decision 5. The hall's
|
|
225
|
+
// copy of this lives in modules/hall-ui/public/dom-utils.js (OTB.roleLabel); this
|
|
226
|
+
// card is a separate runtime and cannot read it, so the word is restated here and
|
|
227
|
+
// nowhere else in this file. 'ui' keeps its upper case, as the hall's does.
|
|
228
|
+
const ROLE_AXIS_LABEL = 'Role';
|
|
229
|
+
function roleLabel(t) {
|
|
225
230
|
const d = t.discipline;
|
|
226
|
-
|
|
231
|
+
if (!d || d === 'unclassified') return '';
|
|
232
|
+
return d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1);
|
|
227
233
|
}
|
|
228
234
|
// Per-lane accent — the OTB palette (sea / terracotta / olive); mid-ramp hexes
|
|
229
235
|
// that read in both light and dark. Colours the Type column so the work lane
|
|
@@ -249,11 +255,11 @@ function claimableTable(rows, rewardFromServer) {
|
|
|
249
255
|
// and footnote what IS paid below the table (task 1002469).
|
|
250
256
|
const perTask = rewardPolicy(rewardFromServer).perTaskCredit;
|
|
251
257
|
const rewardHead = perTask ? 'Reward' : 'Reward (est.)';
|
|
252
|
-
const headers = ['#', 'Version',
|
|
258
|
+
const headers = ['#', 'Version', ROLE_AXIS_LABEL, rewardHead, 'Est.', 'What it is'];
|
|
253
259
|
const body = rows.map((t) => [
|
|
254
260
|
surface.taskRef(t.id, HALL_TASK),
|
|
255
261
|
cell(t.version_id || '?'),
|
|
256
|
-
cell(
|
|
262
|
+
cell(roleLabel(t) || '—'),
|
|
257
263
|
`${t.credits_reward ?? 0}c`,
|
|
258
264
|
estimateShort(t),
|
|
259
265
|
cell(whatItIs(t)),
|
|
@@ -462,7 +468,7 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
462
468
|
return `<tr class="bs-tr" role="button" tabindex="0" title="${titleAttr}" onclick="sendPrompt('${claimMsg}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();sendPrompt('${claimMsg}')}">`
|
|
463
469
|
+ TD(`#${hesc(t.id)}`, 'bs-id')
|
|
464
470
|
+ TD(hesc(t.version_id || '?'), 'bs-nowrap')
|
|
465
|
-
+ TD(`${laneDot}${hesc(
|
|
471
|
+
+ TD(`${laneDot}${hesc(roleLabel(t) || '—')}`, 'bs-ellip-sub')
|
|
466
472
|
+ TD(hesc((t.credits_reward ?? 0) + 'c'))
|
|
467
473
|
+ TD(hesc(estimateShort(t)), 'bs-sub')
|
|
468
474
|
+ TD(hesc(t.title || ''), 'bs-ellip')
|
|
@@ -472,7 +478,7 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
472
478
|
const menu = claimable.length
|
|
473
479
|
? `<table class="bs-table">
|
|
474
480
|
<colgroup><col style="width:44px;"><col style="width:52px;"><col style="width:68px;"><col style="width:48px;"><col style="width:44px;"><col></colgroup>
|
|
475
|
-
<thead><tr class="bs-thead">${TH('#')}${TH('Version')}${TH(
|
|
481
|
+
<thead><tr class="bs-thead">${TH('#')}${TH('Version')}${TH(ROLE_AXIS_LABEL)}${TH('Reward')}${TH('Est.')}${TH('What it is')}</tr></thead>
|
|
476
482
|
<tbody>${rows}</tbody>
|
|
477
483
|
</table>`
|
|
478
484
|
: `<div class="bs-line">Nothing claimable right now — everything is in flight, blocked, or shipped. <a href="#" onclick="event.preventDefault();sendPrompt('Run blocker-review')" class="bs-link">See what's stuck ↗</a></div>`;
|
|
@@ -487,9 +493,9 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
|
|
|
487
493
|
let crossLane = '';
|
|
488
494
|
if (recommendations.length > 0) {
|
|
489
495
|
const ids = recommendations.map((t) => '#' + t.id).join(', ');
|
|
490
|
-
crossLane = ` · <a href="#" onclick="event.preventDefault();sendPrompt('${jsq('Show me the tasks outside my disciplines: ' + ids)}')" class="bs-link">Outside your
|
|
496
|
+
crossLane = ` · <a href="#" onclick="event.preventDefault();sendPrompt('${jsq('Show me the tasks outside my disciplines: ' + ids)}')" class="bs-link">Outside your roles: ${hesc(ids)} ↗</a>`;
|
|
491
497
|
} else if (routingByPreference) {
|
|
492
|
-
crossLane = ` · <a href="#" onclick="event.preventDefault();sendPrompt('Show me every
|
|
498
|
+
crossLane = ` · <a href="#" onclick="event.preventDefault();sendPrompt('Show me every role, not just mine')" class="bs-link">Show all roles ↗</a>`;
|
|
493
499
|
}
|
|
494
500
|
|
|
495
501
|
const summary = `builder-start: ${claimable.length} task${claimable.length === 1 ? '' : 's'} you can claim now, plus your current build status. Click a task to claim it.`;
|
|
@@ -977,10 +983,10 @@ async function main() {
|
|
|
977
983
|
if (recommendations.length > 0) {
|
|
978
984
|
const ids = recommendations.map((t) => `#${t.id}`).join(', ');
|
|
979
985
|
console.log('');
|
|
980
|
-
console.log(`Outside your
|
|
986
|
+
console.log(`Outside your roles: ${ids} — say the word to see them, or run with \`--all-disciplines\`.`);
|
|
981
987
|
} else if (routingByPreference) {
|
|
982
988
|
console.log('');
|
|
983
|
-
console.log('_Showing your
|
|
989
|
+
console.log('_Showing your roles — run with `--all-disciplines` to see every role._');
|
|
984
990
|
}
|
|
985
991
|
|
|
986
992
|
// ---------- next ----------
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/core-update.js — "is a newer @bongos/core published, and could THIS
|
|
4
|
+
// instance take it?", answered by the instance itself (task 1004045).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS IS NOT THE PROVISIONING SNAPSHOT, which already holds the answer for
|
|
7
|
+
// every project the platform hosts. `provisioning_core_version_snapshot` (task
|
|
8
|
+
// 1003159) lives in the CONTROL PLANE's database, and the provisioning module is
|
|
9
|
+
// not installed on a hosted project at all — probed 2026-09-18:
|
|
10
|
+
// `/api/bongos/provisioning/health` answers 200 on the platform and 404 on every
|
|
11
|
+
// tenant. So a banner in an owner's OWN hall cannot read it, and a self-hosted
|
|
12
|
+
// instance with no control plane anywhere could not read it even in principle.
|
|
13
|
+
// Asking the registry directly is the only source EVERY instance has, which is
|
|
14
|
+
// what "every owner's hall" requires.
|
|
15
|
+
//
|
|
16
|
+
// NEVER `npm view` HERE. update-channel.js's `listAvailableVersions` shells out
|
|
17
|
+
// SYNCHRONOUSLY, and ADR 0293 names exactly that as the reason the deploy route
|
|
18
|
+
// does not consult the registry: it would block the event loop for every other
|
|
19
|
+
// request on the process. This is a plain HTTPS GET of the abbreviated packument,
|
|
20
|
+
// cached for hours, with a hard timeout — and fail-SOFT, because an instance
|
|
21
|
+
// behind a firewall that cannot reach the registry must render no banner rather
|
|
22
|
+
// than a wrong one.
|
|
23
|
+
//
|
|
24
|
+
// THE CHANNEL RULE IS NOT RESTATED HERE. `resolveChannelTarget` is the runner's
|
|
25
|
+
// OWN function (scripts/gds/update-channel.js) and is imported, not copied: the
|
|
26
|
+
// banner must never offer a version the runner would then refuse, and ADR 0280
|
|
27
|
+
// is the record of what a second copy of this contract costs when it drifts.
|
|
28
|
+
|
|
29
|
+
const https = require('node:https');
|
|
30
|
+
const {
|
|
31
|
+
CORE_PKG,
|
|
32
|
+
DEFAULT_CHANNEL,
|
|
33
|
+
normalizeChannel,
|
|
34
|
+
resolveChannelTarget,
|
|
35
|
+
} = require('../../scripts/gds/update-channel.js');
|
|
36
|
+
|
|
37
|
+
const REGISTRY_ORIGIN = 'https://registry.npmjs.org';
|
|
38
|
+
// Six hours. The core publishes many times a day, but a banner is not a deploy
|
|
39
|
+
// signal — it is "you are behind", which does not become more true for being
|
|
40
|
+
// re-checked every minute, and each check is an outbound request from someone
|
|
41
|
+
// else's server.
|
|
42
|
+
const TTL_MS = 6 * 60 * 60 * 1000;
|
|
43
|
+
const FETCH_TIMEOUT_MS = 4000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The banner's whole decision. PURE — every input injected, no clock, no I/O.
|
|
47
|
+
*
|
|
48
|
+
* `reading` is a named state rather than a bare null so a caller cannot render
|
|
49
|
+
* "up to date" over an ANSWER WE DO NOT HAVE. previewFromSnapshot carries the
|
|
50
|
+
* same field for the same reason; the failure it prevents is a project that is
|
|
51
|
+
* months behind being told, confidently, that it is current.
|
|
52
|
+
*/
|
|
53
|
+
function coreUpdateVerdict({ running, available, channel } = {}) {
|
|
54
|
+
const ch = normalizeChannel(channel == null ? DEFAULT_CHANNEL : channel);
|
|
55
|
+
const versions = Array.isArray(available) ? available.filter((v) => typeof v === 'string') : [];
|
|
56
|
+
if (!running || typeof running !== 'string' || !versions.length) {
|
|
57
|
+
return { running: running || null, latest: null, behind: false, reading: 'no_answer', channel: ch };
|
|
58
|
+
}
|
|
59
|
+
// null when nothing published is both newer AND within the channel — which is
|
|
60
|
+
// the ordinary "you are current" case, not an error.
|
|
61
|
+
const latest = resolveChannelTarget({ installed: running, channel: ch, available: versions });
|
|
62
|
+
return { running, latest: latest || null, behind: !!latest, reading: 'ok', channel: ch };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Published versions of the core, from the registry. Fail-SOFT: any error, any
|
|
67
|
+
* non-200, any unparseable body ⇒ { ok:false, versions:[] }, never a throw.
|
|
68
|
+
*
|
|
69
|
+
* Asks for the ABBREVIATED packument (the `install-v1` media type): the full
|
|
70
|
+
* document for a package with ~900 releases is megabytes, and the only field
|
|
71
|
+
* this needs is the version list.
|
|
72
|
+
*/
|
|
73
|
+
function fetchPublishedVersions({ pkg = CORE_PKG, origin = REGISTRY_ORIGIN, timeoutMs = FETCH_TIMEOUT_MS, get = https.get } = {}) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let settled = false;
|
|
76
|
+
const done = (v) => { if (!settled) { settled = true; resolve(v); } };
|
|
77
|
+
let req;
|
|
78
|
+
try {
|
|
79
|
+
req = get(`${origin}/${pkg.replace('/', '%2f')}`, {
|
|
80
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
81
|
+
timeout: timeoutMs,
|
|
82
|
+
}, (res) => {
|
|
83
|
+
if (res.statusCode !== 200) { res.resume(); return done({ ok: false, versions: [], error: `registry ${res.statusCode}` }); }
|
|
84
|
+
let body = '';
|
|
85
|
+
res.setEncoding('utf8');
|
|
86
|
+
res.on('data', (c) => { body += c; });
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try {
|
|
89
|
+
const doc = JSON.parse(body);
|
|
90
|
+
const versions = Object.keys((doc && doc.versions) || {});
|
|
91
|
+
done(versions.length ? { ok: true, versions } : { ok: false, versions: [], error: 'no versions in packument' });
|
|
92
|
+
} catch (e) { done({ ok: false, versions: [], error: `unparseable packument: ${e.message}` }); }
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
} catch (e) { return done({ ok: false, versions: [], error: e.message }); }
|
|
96
|
+
if (req && req.on) {
|
|
97
|
+
req.on('error', (e) => done({ ok: false, versions: [], error: e.message }));
|
|
98
|
+
// `timeout` only fires the event; it does not abort the request.
|
|
99
|
+
req.on('timeout', () => { try { req.destroy(); } catch { /* already gone */ } done({ ok: false, versions: [], error: 'registry timeout' }); });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Process-local cache. Deliberately not a table: it is a derived fact with a
|
|
105
|
+
// six-hour life, and a restart re-deriving it costs one request.
|
|
106
|
+
const cache = { at: 0, versions: [], ok: false, inflight: null };
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async function publishedVersions({ now = Date.now, ttlMs = TTL_MS, fetch = fetchPublishedVersions, store = cache } = {}) {
|
|
111
|
+
const t = now();
|
|
112
|
+
if (store.ok && t - store.at < ttlMs) return { ok: true, versions: store.versions, cached: true };
|
|
113
|
+
// IN-FLIGHT DE-DUPE. Without it, every request arriving during a cold-cache
|
|
114
|
+
// window starts its OWN registry read — the cache only collapses them once the
|
|
115
|
+
// first one has landed. Small, because the audience is archon-only and the TTL
|
|
116
|
+
// is hours, but a second outbound call to someone else's registry for a fact
|
|
117
|
+
// already being fetched is simply wrong, and the fix is one shared promise.
|
|
118
|
+
if (store.inflight) return store.inflight;
|
|
119
|
+
store.inflight = (async () => {
|
|
120
|
+
try {
|
|
121
|
+
const r = await fetch();
|
|
122
|
+
if (r.ok) { store.ok = true; store.at = t; store.versions = r.versions; }
|
|
123
|
+
return { ok: r.ok, versions: r.versions, cached: false, error: r.error };
|
|
124
|
+
} finally {
|
|
125
|
+
store.inflight = null;
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
return store.inflight;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What the route answers — and the module's ONLY export, deliberately.
|
|
133
|
+
*
|
|
134
|
+
* Everything above is reachable through it: `get` swaps the HTTPS transport (so
|
|
135
|
+
* the real packument parsing and its failure modes are exercised, not stubbed
|
|
136
|
+
* past), `fetch` swaps the whole read, and `now`/`ttlMs`/`store` drive the cache.
|
|
137
|
+
* Exporting the internals individually instead would add eight exports that no
|
|
138
|
+
* non-test caller uses, which the dead-code ratchet counts as regression — and
|
|
139
|
+
* the fix for that is a smaller surface, never a raised baseline.
|
|
140
|
+
*/
|
|
141
|
+
async function readCoreUpdate({ running, channel, get, ...deps } = {}) {
|
|
142
|
+
const fetch = deps.fetch || (get ? () => fetchPublishedVersions({ get }) : fetchPublishedVersions);
|
|
143
|
+
const r = await publishedVersions({ ...deps, fetch });
|
|
144
|
+
return coreUpdateVerdict({ running, available: r.ok ? r.versions : [], channel });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { readCoreUpdate };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/routes/core-update.js — GET /api/bongos/core-update (task 1004045).
|
|
4
|
+
//
|
|
5
|
+
// "Is this instance behind, and what would it move to?" — the one fact the hall's
|
|
6
|
+
// upgrade banner needs. It lives in the CORE, not in the provisioning module, for
|
|
7
|
+
// the reason src/bongos/core-update.js opens with: a hosted project does not have
|
|
8
|
+
// the provisioning module, so a banner that depended on it could never appear in
|
|
9
|
+
// the hall of any project but the platform's own — which is the whole ask.
|
|
10
|
+
//
|
|
11
|
+
// GATED ON `core.pin.move`, the same atom as the deploy door, and for the same two
|
|
12
|
+
// reasons ADR 0293 gives for gating its PREVIEW as tightly as its move:
|
|
13
|
+
// 1. It names the exact core this box runs and how far behind it is, which is a
|
|
14
|
+
// vulnerability-window disclosure (ADR 0043) and must not sit on an
|
|
15
|
+
// unauthenticated surface.
|
|
16
|
+
// 2. The banner it feeds offers an action. Showing it to someone who cannot
|
|
17
|
+
// press the button is a worse experience than showing nothing.
|
|
18
|
+
// The gate costs its real audience nothing: an owner holds the atom on their own
|
|
19
|
+
// instance by construction.
|
|
20
|
+
|
|
21
|
+
const express = require('express');
|
|
22
|
+
const { asyncHandler } = require('./_helpers');
|
|
23
|
+
const auth = require('../auth');
|
|
24
|
+
const { readCoreUpdate } = require('../core-update');
|
|
25
|
+
|
|
26
|
+
module.exports = function buildCoreUpdateRouter() {
|
|
27
|
+
const router = express.Router();
|
|
28
|
+
|
|
29
|
+
router.get(
|
|
30
|
+
'/core-update',
|
|
31
|
+
auth.requireBuilder,
|
|
32
|
+
auth.requirePermission('core.pin.move'),
|
|
33
|
+
// asyncHandler(TAG, handler, opts) — the tag is the first argument, and
|
|
34
|
+
// passing the handler there instead makes `handler` undefined, so EVERY
|
|
35
|
+
// request 500s with "handler is not a function". That is exactly the bug the
|
|
36
|
+
// grader caught here, and it is invisible to any test that does not drive a
|
|
37
|
+
// real request through the assembled router; tests/core_update_banner.mjs now
|
|
38
|
+
// does, and fails if this call is mis-shaped again.
|
|
39
|
+
asyncHandler('GET /core-update', async (_req, res) => {
|
|
40
|
+
// Fail-soft on the version read, exactly as GET /version does: an instance
|
|
41
|
+
// that cannot name its own core reports `no_answer` and renders no banner.
|
|
42
|
+
let running = null;
|
|
43
|
+
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
44
|
+
res.json({ ok: true, update: await readCoreUpdate({ running }) });
|
|
45
|
+
}, { errorCode: 'core_update_failed', message: false }),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return router;
|
|
49
|
+
};
|
package/src/bongos/routes.js
CHANGED
|
@@ -19,6 +19,7 @@ const express = require('express');
|
|
|
19
19
|
|
|
20
20
|
const buildHealthzRouter = require('./routes/healthz');
|
|
21
21
|
const buildInstanceRouter = require('./routes/instance');
|
|
22
|
+
const buildCoreUpdateRouter = require('./routes/core-update');
|
|
22
23
|
const buildAuthRouter = require('./routes/auth');
|
|
23
24
|
const buildGithubRouter = require('./routes/github');
|
|
24
25
|
const buildMeRouter = require('./routes/me');
|
|
@@ -133,6 +134,7 @@ function buildGdsRouter() {
|
|
|
133
134
|
|
|
134
135
|
router.use(buildHealthzRouter());
|
|
135
136
|
router.use(buildInstanceRouter());
|
|
137
|
+
router.use(buildCoreUpdateRouter());
|
|
136
138
|
router.use(buildAuthRouter());
|
|
137
139
|
router.use(buildGithubRouter());
|
|
138
140
|
router.use(buildMeRouter());
|
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.872'; // 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');
|
|
@@ -329,7 +329,9 @@ const GRANT_WORDS = {
|
|
|
329
329
|
handle: /your name/,
|
|
330
330
|
display_name: /your name/,
|
|
331
331
|
avatar_url: /your picture/,
|
|
332
|
-
|
|
332
|
+
// task 1003936: the axis is named "role" to a person now; the invariant is that
|
|
333
|
+
// the toggle NAMES it in the sliver a recruiter receives, not which word it used.
|
|
334
|
+
disciplines: /your roles/,
|
|
333
335
|
active_recently: /active in the last 30 days/,
|
|
334
336
|
};
|
|
335
337
|
|