@bongos/core 1.21.113 → 1.21.115
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 +608 -188
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +709 -626
- package/docs/copy-registry.json +1673 -858
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +4875 -4748
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects-hall-live.states.json +0 -5
- package/modules/public-landing/public/projects.html +169 -6
- package/modules/public-landing/public/projects.probes.json +10 -6
- package/modules/public-landing/public/projects.states.json +13 -13
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/projects_hub_pre_uat.mjs +4 -2
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
- package/tests/wizard_physics.mjs +1 -1
- package/tests/wizard_physics_more.mjs +133 -0
package/src/bongos/routes/me.js
CHANGED
|
@@ -122,7 +122,9 @@ const discordIsLinked = (builderId) =>
|
|
|
122
122
|
// `idea.triage` (task 1004231): the sky's panel draws "nod — make it claimable"
|
|
123
123
|
// on a thought awaiting a nod only for a builder who may triage ideas; the nod
|
|
124
124
|
// itself runs the existing POST /tasks/:id/promote, which stays the wall.
|
|
125
|
-
|
|
125
|
+
// `board.item.open` (task 1004714): My agents offers 'Make it a law' — a laws vote,
|
|
126
|
+
// POST /government/board/laws — only to a builder who may open a board item.
|
|
127
|
+
const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move', 'idea.triage', 'board.item.open']);
|
|
126
128
|
|
|
127
129
|
// navPermissionsFor(builderId) → { '<atom>': boolean } over NAV_PERMISSION_ATOMS,
|
|
128
130
|
// or null when authority could not be established (government off, port missing,
|
|
@@ -187,7 +189,7 @@ module.exports = function buildMeRouter() {
|
|
|
187
189
|
recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, cliAuthed,
|
|
188
190
|
behaviorPrefs, geminiKeyRow, needsRebaseTasks, foundingBuilder, helpRequestCount,
|
|
189
191
|
taskRecommendationCount, boardVotesPending, socialMode, mingleIntroductionCount,
|
|
190
|
-
navPermissions, foundingMode,
|
|
192
|
+
navPermissions, foundingMode, sessionLaws, govPositions,
|
|
191
193
|
] = await Promise.all([
|
|
192
194
|
db.getBuilderById(req.builder.id),
|
|
193
195
|
lc().getActiveClaimsForBuilder(req.builder.id),
|
|
@@ -318,7 +320,44 @@ module.exports = function buildMeRouter() {
|
|
|
318
320
|
return await onboarding.foundingModeFor();
|
|
319
321
|
} catch (_) { return null; }
|
|
320
322
|
})(),
|
|
323
|
+
// task 1004707: the project's LAWS — what every AI session on the project
|
|
324
|
+
// reads, via the `government` port (core imports no module, ADR 0093).
|
|
325
|
+
// Read per request, uncached, so a law passed a moment ago is in the next
|
|
326
|
+
// session. null when the module is off or the read failed: the session then
|
|
327
|
+
// gets no laws block, and the settings slices resolve without locks.
|
|
328
|
+
(async () => {
|
|
329
|
+
try {
|
|
330
|
+
const government = seams.resolveOptional('government');
|
|
331
|
+
if (!government || typeof government.lawsForSession !== 'function') return null;
|
|
332
|
+
return await government.lawsForSession();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
log.error({ err: err && err.message }, 'GET /me laws');
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
})(),
|
|
338
|
+
// task 1004713: the government POSITIONS this builder holds (owner, Lead,
|
|
339
|
+
// a building's sitting, the board), via the `government` port. Read per
|
|
340
|
+
// request, uncached. null when the module is off or a read failed.
|
|
341
|
+
(async () => {
|
|
342
|
+
try {
|
|
343
|
+
const government = seams.resolveOptional('government');
|
|
344
|
+
if (!government || typeof government.positionsFor !== 'function') return null;
|
|
345
|
+
return await government.positionsFor(req.builder.id);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
log.error({ err: err && err.message }, 'GET /me positions');
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
})(),
|
|
321
351
|
]);
|
|
352
|
+
// task 1004707: a law's LOCKS bite on the three settings slices below
|
|
353
|
+
// (interaction, wandering, model allocation): their effective values and
|
|
354
|
+
// injected contracts are resolved with every lock applied, server-side —
|
|
355
|
+
// the builder-settings port's describeWithLaws is the SSOT. A builder's own
|
|
356
|
+
// stored choice is still reported as theirs.
|
|
357
|
+
const instanceModels = (() => { try { return branding().models || {}; } catch { return {}; } })();
|
|
358
|
+
const lawed = settings && typeof settings.describeWithLaws === 'function'
|
|
359
|
+
? settings.describeWithLaws(behaviorPrefs, (sessionLaws && sessionLaws.laws) || [], { rank: builder && builder.rank, instanceModels })
|
|
360
|
+
: null;
|
|
322
361
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
323
362
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
324
363
|
// profile banner read it without a follow-up call; the ship + art-pipeline
|
|
@@ -401,6 +440,14 @@ module.exports = function buildMeRouter() {
|
|
|
401
440
|
// floor. null when authority could not be established, which the shell
|
|
402
441
|
// reads as nothing-visible. Cosmetic only — ADR 0016, the server enforces.
|
|
403
442
|
permissions: navPermissions,
|
|
443
|
+
// task 1004713 (owner ruling 2026-10-09: governance is a HELD POSITION, not a
|
|
444
|
+
// picked craft): { held: [{ kind, seat, building, title }], title,
|
|
445
|
+
// has_position, from } — the seats this builder holds, resolved server-side
|
|
446
|
+
// (modules/government/positions.js). The hall shows the governing surfaces
|
|
447
|
+
// and names the Governing mode by `title` only for a holder. null when it
|
|
448
|
+
// could not be read, which the hall reads as unknown (today's nav), never
|
|
449
|
+
// "holds none". Cosmetic only — every governing page keeps its own gate.
|
|
450
|
+
positions: govPositions,
|
|
404
451
|
unlocked,
|
|
405
452
|
locked_with_progress: lockedWithProgress,
|
|
406
453
|
streak: {
|
|
@@ -469,13 +516,27 @@ module.exports = function buildMeRouter() {
|
|
|
469
516
|
// level (clamped by rank) + its behavior contract. The Conductor + the
|
|
470
517
|
// /settings UI read this; the builder-settings port's describeWandering is
|
|
471
518
|
// the SSOT (BV1.R80). null when the prefs module is off.
|
|
472
|
-
wandering: settings ? settings.describeWandering(behaviorPrefs, builder.rank) : null,
|
|
519
|
+
wandering: lawed ? lawed.wandering : (settings ? settings.describeWandering(behaviorPrefs, builder.rank) : null),
|
|
473
520
|
// task 2010: the per-builder interaction profile (tone / technical level /
|
|
474
521
|
// toolchain familiarity), resolved to an effective profile + an injectable
|
|
475
522
|
// contract. The old §2 "The Prompter" owner-defaults, now per-builder. The
|
|
476
523
|
// session-start injection + /settings UI read this; describeInteraction is
|
|
477
524
|
// the SSOT (builder-settings port). null when the prefs module is off.
|
|
478
|
-
interaction: settings && settings.describeInteraction ? settings.describeInteraction(behaviorPrefs) : null,
|
|
525
|
+
interaction: lawed ? lawed.interaction : (settings && settings.describeInteraction ? settings.describeInteraction(behaviorPrefs) : null),
|
|
526
|
+
// task 1004707: the project's LAWS, read into every AI session. The
|
|
527
|
+
// Conductor hook relays `contract` verbatim beside the interaction profile
|
|
528
|
+
// (inject:false, and no block, when no law is in force). `laws` is the book
|
|
529
|
+
// as the session reads it, numbered; `locks` is what each law's lock did to
|
|
530
|
+
// THIS builder's settings ({ setting, law_id, enforced, from, to } — a lock on
|
|
531
|
+
// a setting no table holds yet is enforced:false, its law's words still
|
|
532
|
+
// apply). The server writes all of it; nothing local can change it. null when
|
|
533
|
+
// the laws could not be read.
|
|
534
|
+
laws: sessionLaws ? {
|
|
535
|
+
inject: !!sessionLaws.inject,
|
|
536
|
+
contract: sessionLaws.contract || '',
|
|
537
|
+
laws: Array.isArray(sessionLaws.laws) ? sessionLaws.laws : [],
|
|
538
|
+
locks: lawed ? lawed.locks : [],
|
|
539
|
+
} : null,
|
|
479
540
|
// task 1003855: the builder's ACTIVE specialities, one per craft at most
|
|
480
541
|
// (the speciality_adoptions partial unique index enforces that). Each entry
|
|
481
542
|
// carries its own injectable contract; the Conductor picks the one matching
|
|
@@ -491,9 +552,9 @@ module.exports = function buildMeRouter() {
|
|
|
491
552
|
// task 2011: the resolved subagent model allocation (main + subagent tiers),
|
|
492
553
|
// builder override → instance default (branding.models) → code default. The
|
|
493
554
|
// agent consults `effective` when spawning subagents; §2 links here.
|
|
494
|
-
model_allocation: settings && settings.describeModelAllocation
|
|
495
|
-
? settings.describeModelAllocation(behaviorPrefs,
|
|
496
|
-
: null,
|
|
555
|
+
model_allocation: lawed ? lawed.model_allocation : (settings && settings.describeModelAllocation
|
|
556
|
+
? settings.describeModelAllocation(behaviorPrefs, instanceModels)
|
|
557
|
+
: null),
|
|
497
558
|
// task 1279: the per-builder "render widgets" knob (shares behavior_prefs).
|
|
498
559
|
// { widgets_enabled } tells the start/claim/ship CLIs whether to emit the
|
|
499
560
|
// rich HTML card or the cheap text fallback. No rank cap (cost/taste, not
|
|
@@ -1245,8 +1245,12 @@ function mountInternalSurfaces(app) {
|
|
|
1245
1245
|
// task 1004473: `commands`, the Commands page — every skill and every bongos CLI
|
|
1246
1246
|
// verb. Both reads behind it are requireBuilder, so it sits on this tier;
|
|
1247
1247
|
// reading the list grants nothing (ADR 0310 §1).
|
|
1248
|
+
// task 1004685 (the living-city prototype): `my-agents` — how the viewer's own
|
|
1249
|
+
// Claude works for them. Every read behind it is the viewer's own and
|
|
1250
|
+
// requireBuilder (GET /me and its /me/* prefs, /specialities), so sign-in is the
|
|
1251
|
+
// whole gate; the governing reads it adds are gated by their own routes.
|
|
1248
1252
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1249
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|commands)(?:\.html)?\/?$/;
|
|
1253
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|my-agents|commands)(?:\.html)?\/?$/;
|
|
1250
1254
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1251
1255
|
// page that embeds them.
|
|
1252
1256
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.115'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -473,6 +473,12 @@ module.exports = {
|
|
|
473
473
|
get SHIP_MODES() { return require('./bongos/project-settings').SHIP_MODES; },
|
|
474
474
|
get parseShipMode() { return require('./bongos/project-settings').parseShipMode; },
|
|
475
475
|
get resolveShipMode() { return require('./bongos/project-settings').resolveShipMode; },
|
|
476
|
+
// The spend cap (task 1004712, ADR 0379): off by default (no limit, the owner's
|
|
477
|
+
// ruling). The treasury reads it; PATCH /project-settings/spend-cap writes it.
|
|
478
|
+
get parseSpendCap() { return require('./bongos/project-settings').parseSpendCap; },
|
|
479
|
+
get resolveSpendCap() { return require('./bongos/project-settings').resolveSpendCap; },
|
|
480
|
+
get setSpendCap() { return require('./bongos/project-settings').setSpendCap; },
|
|
481
|
+
get describeSpendCap() { return require('./bongos/project-settings').describeSpendCap; },
|
|
476
482
|
get describeShipMode() { return require('./bongos/project-settings').describeShipMode; },
|
|
477
483
|
// Tweak auto-pickup (task 1004677, ADR 0371 D3/D5): the switch and the monthly cap.
|
|
478
484
|
get AUTOPICKUP_MODES() { return require('./bongos/project-settings').AUTOPICKUP_MODES; },
|
package/src/platform-server.js
CHANGED
|
@@ -316,6 +316,16 @@ function buildPlatformApp() {
|
|
|
316
316
|
if (!html) return next();
|
|
317
317
|
res.type('html').send(html);
|
|
318
318
|
});
|
|
319
|
+
// THE CONNECTIONS PAGE (task 1004278, ADR 0337 D5.4): the hub is the primary
|
|
320
|
+
// home for connections — your connections across every project, the requests
|
|
321
|
+
// in both directions, and "Builders you may know". Hub-only for the /account
|
|
322
|
+
// reason: every read and write it makes is a platform-identity route.
|
|
323
|
+
app.get('/connections', (req, res, next) => {
|
|
324
|
+
if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
|
|
325
|
+
const html = landingPage('connections.html');
|
|
326
|
+
if (!html) return next();
|
|
327
|
+
res.type('html').send(html);
|
|
328
|
+
});
|
|
319
329
|
// THE GUILD PAGE (task 1002301, R25; ADR 0336 D4/D6): /g/<slug>, the namespace
|
|
320
330
|
// D6 reserved. Hub-only for the /account reason — every read it makes is a
|
|
321
331
|
// platform-identity route. ONE shell for every slug: the page reads the public
|
|
@@ -310,12 +310,12 @@ const pool = {
|
|
|
310
310
|
|
|
311
311
|
// A vouched re-application taking over an unvouched pending row (ADR 0333 D4).
|
|
312
312
|
// The SET list is matched exactly, like the resolve's below.
|
|
313
|
-
if ((m = /^UPDATE access_requests SET display_name = \$2, note = \$3, applicant_github_id = \$4, applicant_handle = \$5, vouched_at = \$6 WHERE (.+?)$/.exec(sql))) {
|
|
313
|
+
if ((m = /^UPDATE access_requests SET display_name = \$2, note = \$3, applicant_github_id = \$4, applicant_handle = \$5, vouched_at = \$6 WHERE (.+?) RETURNING id, github_login, status, created_at$/.exec(sql))) {
|
|
314
314
|
const hit = rows.filter(predicateFor(m[1], params));
|
|
315
315
|
for (const r of hit) {
|
|
316
316
|
[r.display_name, r.note, r.applicant_github_id, r.applicant_handle, r.vouched_at] = params.slice(1, 6);
|
|
317
317
|
}
|
|
318
|
-
return { rows:
|
|
318
|
+
return { rows: hit.map((r) => ({ id: r.id, github_login: r.github_login, status: r.status, created_at: r.created_at })), rowCount: hit.length };
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// The resolve — one UPDATE, one WHERE, and that WHERE is the whole rule.
|
|
@@ -354,11 +354,13 @@ const poolPath = require.resolve('../src/bongos/pool.js');
|
|
|
354
354
|
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool } };
|
|
355
355
|
|
|
356
356
|
// Discord broadcast is fire-and-forget on the public write; stubbed so the suite
|
|
357
|
-
// is hermetic (it is best-effort in production and never affects the queue)
|
|
357
|
+
// is hermetic (it is best-effort in production and never affects the queue), and
|
|
358
|
+
// recorded so a test can see which #member-approvals lines would have gone out.
|
|
359
|
+
let broadcasts = [];
|
|
358
360
|
const broadcastPath = require.resolve('../modules/onboarding/approval-broadcast.js');
|
|
359
361
|
require.cache[broadcastPath] = {
|
|
360
362
|
id: broadcastPath, filename: broadcastPath, loaded: true,
|
|
361
|
-
exports: { postMemberApprovalRequest: async () => ({ posted: false, skipped: 'test' }
|
|
363
|
+
exports: { postMemberApprovalRequest: async (request) => { broadcasts.push(request); return { posted: false, skipped: 'test' }; } },
|
|
362
364
|
};
|
|
363
365
|
|
|
364
366
|
// The hub's side of the vouch, for real: a generated Ed25519 keypair, the real
|
|
@@ -452,7 +454,7 @@ function call(method, pathname, body, extraHeaders = {}) {
|
|
|
452
454
|
req.end(payload);
|
|
453
455
|
});
|
|
454
456
|
}
|
|
455
|
-
const reset = (rank = 'archon') => { seed(); actingRank = rank; actingId = 42; notices = []; hubViews = {}; hubAsked = []; };
|
|
457
|
+
const reset = (rank = 'archon') => { seed(); actingRank = rank; actingId = 42; notices = []; hubViews = {}; hubAsked = []; broadcasts = []; };
|
|
456
458
|
|
|
457
459
|
// ===========================================================================
|
|
458
460
|
// The interpreter is the instrument, so it is calibrated first
|
|
@@ -618,6 +620,19 @@ test('a vouched re-application takes over an UNVOUCHED pending row for its own l
|
|
|
618
620
|
assert.equal(row(1).status, 'pending', 'and it decides nothing');
|
|
619
621
|
});
|
|
620
622
|
|
|
623
|
+
// The first #member-approvals line still quotes the words the row held before the
|
|
624
|
+
// takeover, and a webhook cannot edit it; so the takeover posts the row's words now.
|
|
625
|
+
test('a vouched takeover re-posts the #member-approvals line with the words the row holds now', async () => {
|
|
626
|
+
reset();
|
|
627
|
+
await applyAs('ada', { [VOUCH_HEADER]: vouchFor('ada', { githubId: 8001, handle: 'ada-h' }) });
|
|
628
|
+
assert.equal(broadcasts.length, 1, 'one line, for the takeover — the duplicate INSERT posts nothing');
|
|
629
|
+
const [line] = broadcasts;
|
|
630
|
+
assert.equal(line.id, 1, 'the same request, so a 👍 on either line resolves the one row');
|
|
631
|
+
assert.equal(line.refiled, true);
|
|
632
|
+
assert.equal(line.display_name, 'New');
|
|
633
|
+
assert.match(line.note, /via platform My Projects/);
|
|
634
|
+
});
|
|
635
|
+
|
|
621
636
|
test('a re-application never re-stamps an ALREADY vouched row, and an unvouched one changes nothing', async () => {
|
|
622
637
|
reset();
|
|
623
638
|
const bo = { ...row(2) };
|
|
@@ -626,6 +641,33 @@ test('a re-application never re-stamps an ALREADY vouched row, and an unvouched
|
|
|
626
641
|
const ada = { ...row(1) };
|
|
627
642
|
await applyAs('ada');
|
|
628
643
|
assert.deepEqual(row(1), ada, 'an unvouched re-apply is the idempotent success it always was');
|
|
644
|
+
assert.deepEqual(broadcasts, [], 'and neither re-posts a line: nothing on the row changed');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// END TO END: a row written by the REAL vouch writer is the row the queue resolves
|
|
648
|
+
// from the hub. The profile tests below seed their vouched rows; this one proves the
|
|
649
|
+
// link between the two halves — the id the writer stores is the id the read asks for.
|
|
650
|
+
test('list-pending: the real vouch writer’s row resolves its profile and guild from the hub', async () => {
|
|
651
|
+
reset();
|
|
652
|
+
const profile = { view: 'public', account: { handle: 'newcomer-h', bio: 'hi' }, cross_project: null };
|
|
653
|
+
hubViews['7001'] = { profile, guild: { slug: 'lantern-works', name: 'Lantern Works' } };
|
|
654
|
+
await applyAs('Newcomer', { [VOUCH_HEADER]: vouchFor('newcomer') });
|
|
655
|
+
const r = await call('GET', '/api/gds/access-requests');
|
|
656
|
+
const got = r.json.requests.find((x) => x.github_login === 'Newcomer');
|
|
657
|
+
assert.ok(hubAsked.includes('7001'), 'the hub was asked about the id the vouch carried');
|
|
658
|
+
assert.deepEqual(got.profile, profile);
|
|
659
|
+
assert.deepEqual(got.guild, { slug: 'lantern-works', name: 'Lantern Works' });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('list-pending: a vouched TAKEOVER resolves the vouched applicant, not whoever filed first', async () => {
|
|
663
|
+
reset();
|
|
664
|
+
hubViews['8001'] = { profile: { view: 'sliver', sliver: { disciplines: ['engineer'] } }, guild: null };
|
|
665
|
+
await applyAs('ada', { [VOUCH_HEADER]: vouchFor('ada', { githubId: 8001, handle: 'ada-h' }) });
|
|
666
|
+
const r = await call('GET', '/api/gds/access-requests');
|
|
667
|
+
const ada = r.json.requests.find((x) => x.github_login === 'ada');
|
|
668
|
+
assert.equal(ada.vouched, true);
|
|
669
|
+
assert.deepEqual(ada.profile, hubViews['8001'].profile);
|
|
670
|
+
assert.ok(hubAsked.includes('8001'));
|
|
629
671
|
});
|
|
630
672
|
|
|
631
673
|
test('list-pending: the queue carries the handle the vouch brought, beside the vouch mark', async () => {
|
|
@@ -128,4 +128,25 @@ await test('outside any claimed worktree no craft is guessed', async () => {
|
|
|
128
128
|
assert.ok(r.out.includes(INTERACTION), 'and the rest of the hook still runs');
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
// task 1004707: the project's LAWS, built by the SERVER's own helper (what the
|
|
132
|
+
// government port hands GET /me), run through the real hook.
|
|
133
|
+
const lawsLib = require('../modules/government/laws.js');
|
|
134
|
+
const LAW_BOOK = lawsLib.forSession([
|
|
135
|
+
{ id: 11, text: 'Ask like a human', effect_on_ai: 'One short card with tap answers.', locks: [{ setting: 'verbosity', value: 'concise' }], repealed_at: null },
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
await test('a session on a project with laws is told the laws, first, in the server\'s own words', async () => {
|
|
139
|
+
const r = await runHook({ ...mePayload(), laws: { inject: LAW_BOOK.inject, contract: LAW_BOOK.contract, laws: LAW_BOOK.laws, locks: [] } });
|
|
140
|
+
assert.equal(r.code, 0);
|
|
141
|
+
assert.ok(r.out.includes('1. Ask like a human — One short card with tap answers.'), `the law reaches the session; stdout was: ${JSON.stringify(r.out.slice(0, 400))}`);
|
|
142
|
+
assert.ok(r.out.indexOf('Project laws') < r.out.indexOf(INTERACTION), 'ahead of the builder\'s own profile');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await test('a project with no laws sends no laws block', async () => {
|
|
146
|
+
const none = lawsLib.forSession([]);
|
|
147
|
+
const r = await runHook({ ...mePayload(), laws: { inject: none.inject, contract: none.contract, laws: [], locks: [] } });
|
|
148
|
+
assert.ok(!r.out.includes('Project laws'));
|
|
149
|
+
assert.ok(r.out.includes(INTERACTION), 'and the rest of the hook still runs');
|
|
150
|
+
});
|
|
151
|
+
|
|
131
152
|
summary();
|
|
@@ -309,6 +309,24 @@ test('list: my connections, the count IS the array length, and paging is real',
|
|
|
309
309
|
"…and applying the PUBLIC predicate here would erase your own connections from your own list");
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
test('list: ?here=1 is the project cut — the same read, narrowed to this instance’s active builders', async () => {
|
|
313
|
+
expect(meActive(), result([]));
|
|
314
|
+
await call('GET', '/api/bongos/connections?here=1&limit=2&offset=4');
|
|
315
|
+
assert.ok(calls[1].text.includes(conn.HERE_SQL),
|
|
316
|
+
'the hall tab’s cut (ADR 0337 D5.4): connections who also build on this project, joined on github_id');
|
|
317
|
+
assert.match(conn.HERE_SQL, /b\.status = 'active'/, 'a deactivated seat is not building here');
|
|
318
|
+
assert.deepEqual(calls[1].params, [ME.github_id, 2, 4],
|
|
319
|
+
'a literal term with no new parameter — paging and the FANOUT_1 cap are the full list’s');
|
|
320
|
+
assert.match(calls[1].text, /\(pa\.profile_state = 'active' AND pa\.terms_accepted_at IS NOT NULL\)/,
|
|
321
|
+
'still own scope on the consent floor — the cut narrows, it never swaps the predicate');
|
|
322
|
+
|
|
323
|
+
for (const q of ['', '?here=0', '?here=true', '?here=']) {
|
|
324
|
+
expect(meActive(), result([]));
|
|
325
|
+
await call('GET', `/api/bongos/connections${q}`);
|
|
326
|
+
assert.doesNotMatch(calls[1].text, /\bbuilders\b/, `"${q}" is the full list, unchanged`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
312
330
|
test('list: the limit is clamped to FANOUT_1 however large a caller asks', async () => {
|
|
313
331
|
expect(meActive(), result([]));
|
|
314
332
|
await call('GET', '/api/bongos/connections?limit=999999');
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -128,8 +128,10 @@ const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
|
|
|
128
128
|
|
|
129
129
|
test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and Profile, inside every mode', () => {
|
|
130
130
|
const { craft } = boot();
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// task 1004685 (the living-city prototype): My agents joins as a PROPOSAL for the
|
|
132
|
+
// owner — every craft has agents, and round-3 board 6 pairs it with the city.
|
|
133
|
+
assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile', 'my-agents'],
|
|
134
|
+
'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03); My agents (prototype, task 1004685)');
|
|
133
135
|
for (const m of CRAFTS) {
|
|
134
136
|
const tabs = craft.tabsFor(m);
|
|
135
137
|
assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
|
|
@@ -187,7 +189,7 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
|
|
|
187
189
|
p.signIn('thetes');
|
|
188
190
|
assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
|
|
189
191
|
assert.ok(!p.headShowing('government'), 'nor its group head');
|
|
190
|
-
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'city', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
192
|
+
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'city', 'profile', 'my-agents', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
191
193
|
p.signIn('archon');
|
|
192
194
|
assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
|
|
193
195
|
assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// tests/craft_mode_tallies.mjs — Governing mode's header tallies (task 1004563, goal
|
|
2
|
-
// 1000095 WA6): "
|
|
2
|
+
// 1000095 WA6): "to do N · moving M" beside the eyebrow, on the city and the
|
|
3
|
+
// street only (task 1004701, owner ruling 2026-10-09).
|
|
3
4
|
//
|
|
4
5
|
// Runs the REAL craft-mode.js over a small DOM with a stubbed fetch, and the REAL
|
|
5
6
|
// server count (modules/lifecycle/moving.js) over a fake pool, so the two halves are
|
|
6
7
|
// pinned where they actually decide: which statuses "moving" counts (owner decision
|
|
7
8
|
// 2026-10-08: active + confirmed), and that a figure the page could not read is left
|
|
8
|
-
// out rather than drawn as a zero that says nothing waits on you.
|
|
9
|
+
// out rather than drawn as a zero that says nothing waits on you. Since the living
|
|
10
|
+
// city (task 1004685) both figures are the city's own (gov-living.js countsOf over
|
|
11
|
+
// gov-feeds.js), so the real gov-living.js and gov-feeds.js run beside it here;
|
|
12
|
+
// tests/hall_gov_counts.mjs holds the tally equal to every other surface.
|
|
9
13
|
import assert from 'node:assert/strict';
|
|
10
14
|
import { test } from 'node:test';
|
|
11
15
|
import fs from 'node:fs';
|
|
@@ -16,6 +20,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
20
|
|
|
17
21
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
22
|
const CRAFT = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
|
|
23
|
+
const LIVING = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/gov-living.js'), 'utf8');
|
|
24
|
+
const FEEDS = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/gov-feeds.js'), 'utf8');
|
|
19
25
|
const require = createRequire(import.meta.url);
|
|
20
26
|
process.env.NODE_ENV = 'test';
|
|
21
27
|
const { countMoving, movingHandler, MOVING_STATUSES } = require('../modules/lifecycle/moving.js');
|
|
@@ -44,7 +50,8 @@ function node(tag, id, cls) {
|
|
|
44
50
|
function all(root) { return root.children.flatMap((c) => [c, ...all(c)]); }
|
|
45
51
|
|
|
46
52
|
// answers: path → JSON body, or a number (an HTTP failure status), or 'throw'.
|
|
47
|
-
|
|
53
|
+
// page: the body's data-page (the tally stands on the city and the street only)
|
|
54
|
+
function boot(mode, answers, page = 'city') {
|
|
48
55
|
const sidebar = node('NAV', 'app-sidebar');
|
|
49
56
|
sidebar.appendChild(node('DIV', '', 'app-sidebar__brand'));
|
|
50
57
|
const topbar = node('DIV', 'app-topbar');
|
|
@@ -52,7 +59,7 @@ function boot(mode, answers) {
|
|
|
52
59
|
const root = node('HTML');
|
|
53
60
|
const asked = [];
|
|
54
61
|
const document = {
|
|
55
|
-
documentElement: root, body: node('BODY'), activeElement: null,
|
|
62
|
+
documentElement: root, body: (() => { const b = node('BODY'); if (page) b.setAttribute('data-page', page); return b; })(), activeElement: null,
|
|
56
63
|
getElementById: (id) => [sidebar, topbar, ...all(sidebar), ...all(topbar)].find((n) => n.id === id) || null,
|
|
57
64
|
querySelectorAll: () => [],
|
|
58
65
|
createElement: (t) => node(t.toUpperCase()),
|
|
@@ -68,20 +75,34 @@ function boot(mode, answers) {
|
|
|
68
75
|
if (typeof a === 'number') return Promise.resolve({ ok: false, status: a, json: () => Promise.resolve({}) });
|
|
69
76
|
return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(a) });
|
|
70
77
|
};
|
|
71
|
-
|
|
78
|
+
window.fetch = fetch;
|
|
79
|
+
const sandbox = { window, document, fetch, Date, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
|
|
72
80
|
vm.createContext(sandbox);
|
|
81
|
+
// the living city's one counter and one reader, as the city and the street carry them
|
|
82
|
+
// (task 1004685): the tally prints their numbers, read through gov-feeds.js
|
|
83
|
+
vm.runInContext(LIVING, sandbox, { filename: 'gov-living.js' });
|
|
84
|
+
vm.runInContext(FEEDS, sandbox, { filename: 'gov-feeds.js' });
|
|
73
85
|
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
74
86
|
const craft = window.OTBCraftMode;
|
|
75
87
|
craft.mount();
|
|
76
88
|
const box = () => all(topbar).find((n) => n.id === 'craft-tallies') || null;
|
|
77
89
|
return { craft, topbar, asked, box };
|
|
78
90
|
}
|
|
79
|
-
const settle = () => new Promise((r) => setImmediate(r));
|
|
91
|
+
const settle = async () => { for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r)); };
|
|
80
92
|
const text = (html) => html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
// Since the living city (task 1004685) the tally prints the city's own counts:
|
|
95
|
+
// gov-living.js countsOf over gov-feeds.js COUNT_FEEDS, and since task 1004716 those
|
|
96
|
+
// stand on ONE server read, GET /government/counts. Here three matters on the
|
|
97
|
+
// viewer, no agent at work, twelve moving.
|
|
98
|
+
const COUNTS = { viewer: { id: '1' }, on_you: 3, on_others: 0, open: 3, agents_at_work: 0, moving: 12, routed: true, yours: ['b:1', 'b:2', 'b:3'], others: [] };
|
|
99
|
+
const BOTH = {
|
|
100
|
+
'/government/counts': COUNTS,
|
|
101
|
+
'/me': { builder: { id: '1', github_login: 'builder-1', rank: 'archon' } },
|
|
102
|
+
};
|
|
103
|
+
const COUNT_PATHS = ['/government/counts', '/me'];
|
|
83
104
|
|
|
84
|
-
test('Governing reads
|
|
105
|
+
test('Governing reads the city\'s counts and draws "to do 3 · moving 12" right after the eyebrow', async () => {
|
|
85
106
|
const { topbar, box, asked } = boot('governing', BOTH);
|
|
86
107
|
await settle();
|
|
87
108
|
const b = box();
|
|
@@ -89,24 +110,25 @@ test('Governing reads both counts and draws "needs you 3 · moving 12" right aft
|
|
|
89
110
|
const eyebrow = all(topbar).find((n) => n.id === 'craft-eyebrow');
|
|
90
111
|
assert.equal(eyebrow.nextSibling, b, 'they sit directly after the mode words');
|
|
91
112
|
assert.equal(b.getAttribute('hidden'), null);
|
|
92
|
-
assert.equal(text(b.innerHTML), '
|
|
113
|
+
assert.equal(text(b.innerHTML), 'to do 3 · moving 12');
|
|
93
114
|
assert.match(b.innerHTML, /craft-tally craft-tally--due/, 'something waits, so needs-you carries the warm dot');
|
|
94
|
-
assert.deepEqual(asked.map((u) => u.replace('/api/bongos', '')).sort(),
|
|
115
|
+
assert.deepEqual(asked.map((u) => u.replace('/api/bongos', '')).sort(), COUNT_PATHS,
|
|
116
|
+
'one server count, once, beside the viewer the shell already reads — never a task list');
|
|
95
117
|
});
|
|
96
118
|
|
|
97
|
-
test('nothing waiting draws "
|
|
98
|
-
const { box } = boot('governing', { ...BOTH, '/
|
|
119
|
+
test('nothing waiting draws "to do 0" without the dot', async () => {
|
|
120
|
+
const { box } = boot('governing', { ...BOTH, '/government/counts': { ...COUNTS, on_you: 0, open: 0, yours: [] } });
|
|
99
121
|
await settle();
|
|
100
|
-
assert.equal(text(box().innerHTML), '
|
|
122
|
+
assert.equal(text(box().innerHTML), 'to do 0 · moving 12');
|
|
101
123
|
assert.doesNotMatch(box().innerHTML, /craft-tally--due/);
|
|
102
124
|
});
|
|
103
125
|
|
|
104
126
|
test('a count that cannot be read is LEFT OUT, never drawn as zero', async () => {
|
|
105
|
-
const noMoving = boot('governing', { ...BOTH, '/
|
|
127
|
+
const noMoving = boot('governing', { ...BOTH, '/government/counts': { ...COUNTS, moving: null } });
|
|
106
128
|
await settle();
|
|
107
|
-
assert.equal(text(noMoving.box().innerHTML), '
|
|
129
|
+
assert.equal(text(noMoving.box().innerHTML), 'to do 3', 'a moving count the server could not read drops only its own figure');
|
|
108
130
|
|
|
109
|
-
const offline = boot('governing', { '/
|
|
131
|
+
const offline = boot('governing', { '/government/counts': 'throw' });
|
|
110
132
|
await settle();
|
|
111
133
|
assert.equal(offline.box().getAttribute('hidden'), '', 'nothing readable: the slot stays hidden, no zeros');
|
|
112
134
|
assert.equal(offline.box().innerHTML, '');
|
|
@@ -126,6 +148,20 @@ test('only Governing wears the tallies, and leaving it removes them', async () =
|
|
|
126
148
|
assert.equal(box(), null, 'switching away takes them down');
|
|
127
149
|
});
|
|
128
150
|
|
|
151
|
+
test('the top bar carries the tally on the city and the street only (owner ruling, task 1004701)', async () => {
|
|
152
|
+
for (const page of ['city', 'street']) {
|
|
153
|
+
const { box } = boot('governing', BOTH, page);
|
|
154
|
+
await settle();
|
|
155
|
+
assert.ok(box(), `${page} carries the tally`);
|
|
156
|
+
}
|
|
157
|
+
for (const page of ['my-agents', 'board', 'home', null]) {
|
|
158
|
+
const { box, asked } = boot('governing', BOTH, page);
|
|
159
|
+
await settle();
|
|
160
|
+
assert.equal(box(), null, `${page || 'a page with no name'} draws no tally`);
|
|
161
|
+
assert.deepEqual(asked, [], `${page || 'a page with no name'} asks the server for nothing`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
129
165
|
test('the server counts active + confirmed only, and sums them', async () => {
|
|
130
166
|
assert.deepEqual([...MOVING_STATUSES], ['active', 'confirmed']);
|
|
131
167
|
let seen;
|
|
@@ -72,6 +72,17 @@ await t('memberApprovalLine round-trips through parseApprovalMarker', () => {
|
|
|
72
72
|
assert.deepEqual(ap.parseApprovalMarker(line), { kind: 'member', id: 99 });
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// A vouched takeover (ADR 0333 D4) re-posts the request with the row's words now; it
|
|
76
|
+
// must still lead with the SAME marker, or a 👍 on it would resolve nothing.
|
|
77
|
+
await t('the refiled line carries the new words and round-trips to the same request', () => {
|
|
78
|
+
const line = bcast.memberApprovalLine({ id: 99, github_login: 'carol', display_name: 'Carol', note: 'the real me', refiled: true });
|
|
79
|
+
assert.deepEqual(ap.parseApprovalMarker(line), { kind: 'member', id: 99 });
|
|
80
|
+
assert.match(line, /Carol/);
|
|
81
|
+
assert.match(line, /the real me/);
|
|
82
|
+
assert.match(line, /replace the earlier line for #99/);
|
|
83
|
+
assert.doesNotMatch(bcast.memberApprovalLine({ id: 99, github_login: 'carol' }), /replace/, 'a first line says nothing about replacing');
|
|
84
|
+
});
|
|
85
|
+
|
|
75
86
|
// ---- recordApprovalReaction (orchestration via fakes) ----------------------
|
|
76
87
|
const GOOD_ID = '123456789012345678';
|
|
77
88
|
const archon = { id: 1, rank: 'archon', github_login: 'example-owner' };
|
|
@@ -361,6 +361,26 @@ await t('the real config declares the board-room channel, visible to every build
|
|
|
361
361
|
assert.ok(board.topic && board.topic.length > 0, 'board-room needs a topic');
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
// task 1002536: the AI-news feed is the ZERO-CODE form the owner chose at the
|
|
365
|
+
// 2026-07-29 triage — an off-the-shelf RSS service posts into one channel, and
|
|
366
|
+
// nothing on our host polls anything. The channel is the whole repo surface, so
|
|
367
|
+
// pin it: builders-only by INHERITING the BUILDERS category (spelling `view`
|
|
368
|
+
// out would repeat the category and drift), and a topic that names #ideas,
|
|
369
|
+
// because pasting a link there is the article → idea path (the inbound handler
|
|
370
|
+
// reads only typed text, never a forwarded post, so Forward files no link).
|
|
371
|
+
await t('the real config declares ai-news, builders-only, pointing readers at #ideas', () => {
|
|
372
|
+
const cfg = ch.loadConfig();
|
|
373
|
+
const cat = cfg.categories.find((c) => (c.channels || []).some((x) => x.key === 'ai-news'));
|
|
374
|
+
assert.ok(cat, 'expected a channel keyed ai-news');
|
|
375
|
+
const news = cat.channels.find((x) => x.key === 'ai-news');
|
|
376
|
+
assert.equal(ch.slugify(news.name), 'ai-news', 'the name must slug to its key, or apply creates a duplicate');
|
|
377
|
+
assert.equal(cat.name, 'BUILDERS');
|
|
378
|
+
assert.equal(news.view, undefined, 'ai-news must inherit, not repeat, its category view');
|
|
379
|
+
assert.equal(cat.view, 'xenos', 'so every builder, and only builders, can read it');
|
|
380
|
+
assert.ok(news.topic && news.topic.length <= 1024, 'Discord refuses a topic over 1024 characters');
|
|
381
|
+
assert.match(news.topic, /#ideas/, 'the topic must say where an article becomes an idea');
|
|
382
|
+
});
|
|
383
|
+
|
|
364
384
|
await t('the real config plans cleanly against an empty guild and never deletes', () => {
|
|
365
385
|
const cfg = ch.loadConfig();
|
|
366
386
|
const { ops } = ch.planChannelReconcile({ categories: [], channels: [] }, cfg, CTX_BOT);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// tests/fixtures/gov-seated.mjs — A SEATED GOVERNMENT, as test data for the
|
|
2
|
+
// hall's living-city tests (task 1004717). The hub has seated no Lead and built
|
|
3
|
+
// nothing, so the harness fixtures alone cannot show a Lead's floor, a build-out
|
|
4
|
+
// going up or one waiting on its vote. This builds them in the exact shapes the
|
|
5
|
+
// routes answer — GET /government/leads with five Leads appointed by People votes,
|
|
6
|
+
// Review's Testers room standing, a floor going up at Release and an annex
|
|
7
|
+
// waiting on its sitting at Treasury — and routes the docket to those seats by
|
|
8
|
+
// the server's own rule (modules/government/routing.js). The hall never reads it.
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
const routing = require(path.join(ROOT, 'modules', 'government', 'routing.js'));
|
|
18
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', name + '.json'), 'utf8'));
|
|
19
|
+
|
|
20
|
+
// five Leads (the fixture's viewer, 1000025, leads Planning)
|
|
21
|
+
export const SEATED = Object.freeze([
|
|
22
|
+
{ building: 'planning', lead: '1000025', since: '2026-09-12', scope: 'Goals, priorities, the order of work.' },
|
|
23
|
+
{ building: 'release', lead: '1000027', since: '2026-09-05', scope: 'Release floors and the path to going live.' },
|
|
24
|
+
{ building: 'review', lead: '1000095', since: '2026-09-20', scope: 'Who checks finished work, and how.' },
|
|
25
|
+
{ building: 'treasury', lead: '1000094', since: '2026-09-28', scope: 'Pots and the reserve.' },
|
|
26
|
+
{ building: 'people', lead: '1000070', since: '2026-10-02', scope: 'Who joins, and the ranks they hold.' },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// GET /government/leads with them in office and the three build-outs on the record
|
|
30
|
+
export function leadsRead() {
|
|
31
|
+
const r = fx('government__leads');
|
|
32
|
+
const roster = fx('builders__roster').builders;
|
|
33
|
+
const at = (k) => r.buildings.find((b) => b.building === k);
|
|
34
|
+
for (const l of SEATED) {
|
|
35
|
+
const p = roster.find((x) => String(x.id) === l.lead) || { id: l.lead };
|
|
36
|
+
at(l.building).lead = { id: 'L' + l.building, building: l.building, builder: { id: String(p.id), login: p.github_login || null, name: p.display_name || null }, charter: l.scope, since: l.since, appointed_by: { item_id: '1' } };
|
|
37
|
+
at(l.building).answers = { seat: 'lead:' + l.building, builder_ids: [String(p.id)], fell_to_owner: false };
|
|
38
|
+
}
|
|
39
|
+
at('review').structure = { standing: [{ id: 2, capsule: 'committee', name: 'Testers', line: 'Who checks finished work.', state: 'built', progress: 1 }], proposed: [] };
|
|
40
|
+
at('release').structure = { standing: [{ id: 1, capsule: 'floor', name: 'Staging floor', line: '', state: 'building', progress: 0.55, allowed: { path: 'at_once', why: 'Its holder builds it out at once.' } }], proposed: [] };
|
|
41
|
+
at('treasury').structure = { standing: [], proposed: [{ capsule: 'annex', name: 'Investments arm', line: 'A wing for the reserve.', docket_id: 'board:77', item_id: '77' }] };
|
|
42
|
+
return r;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// the annex's sitting, open on the docket: nobody has voted yet
|
|
46
|
+
function sitting(seats) {
|
|
47
|
+
return {
|
|
48
|
+
id: 'board:77', source: 'board', building: 'treasury', rule: 'majority', state: 'moving', title: 'Add an annex to Treasury: Investments arm', short: 'an annex',
|
|
49
|
+
opened_at: '2026-10-08T10:00:00.000Z', deadline: null, tally: { for: 0, against: 0, open: seats }, ref: { type: 'board_item', id: '77' }, acts: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// GET /government/docket with the sitting, every matter routed to those seats
|
|
54
|
+
export function docketRouted() {
|
|
55
|
+
const d = fx('government__docket');
|
|
56
|
+
d.matters.push(sitting(d.buildings.find((b) => b.key === 'charter').holders.length));
|
|
57
|
+
const read = leadsRead();
|
|
58
|
+
const seats = { leads: read.buildings.filter((b) => b.lead).map((b) => ({ building: b.building, builder_id: b.lead.builder.id })), owner: read.owner, holders: {} };
|
|
59
|
+
d.matters.forEach((m) => {
|
|
60
|
+
const { route, ...bare } = m;
|
|
61
|
+
m.route = routing.routeMatter(bare, d.viewer.id, seats, Date.parse(d.generated_at));
|
|
62
|
+
});
|
|
63
|
+
return d;
|
|
64
|
+
}
|