@bongos/core 1.21.113 → 1.21.114
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 +603 -183
- 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 +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- 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.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- 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 +122 -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/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
|
@@ -271,6 +271,7 @@ const REQUESTS = {
|
|
|
271
271
|
'POST /render-deploy/act': ['POST', '/render-deploy/act', { action: 'launch' }],
|
|
272
272
|
'POST /npm-release/release': ['POST', '/npm-release/release', { version: 'v-next' }],
|
|
273
273
|
'PATCH /builders/:id/budget': ['PATCH', `/builders/${BAD}/budget`, { monthly_budget_usd: 5 }],
|
|
274
|
+
'PATCH /project-settings/spend-cap': ['PATCH', '/project-settings/spend-cap', { usd: 'lots' }],
|
|
274
275
|
};
|
|
275
276
|
|
|
276
277
|
test('every listed route has a request in this test (a new act cannot go unexercised)', () => {
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// tests/government_agent_asks.mjs — AGENTS WRITE ASKS LIKE HUMANS (task 1004704,
|
|
2
|
+
// goal 1000111). The ask's shape and its server-side refusal are task 1004703
|
|
3
|
+
// (tests/government_asks.mjs); this file guards the agent side of it.
|
|
4
|
+
//
|
|
5
|
+
// WHAT IT GUARDS.
|
|
6
|
+
// 1. Real past asks come out as short cards: every request in the past-asks
|
|
7
|
+
// corpus (tests/fixtures/government-ask-cards.json) that was written as
|
|
8
|
+
// prose (the 238-word override ask among them)
|
|
9
|
+
// reads, as its card, in a question of at most twelve words and a handful of
|
|
10
|
+
// lines; the prose itself, sent as the ask, is refused.
|
|
11
|
+
// 2. An agent that files prose is refused and rewrites: through the real
|
|
12
|
+
// routers, a blocker an AI (CLI) session files by hand without an ask
|
|
13
|
+
// answers 400 `ask_required`; prose sent as the ask answers `ask_refused`
|
|
14
|
+
// naming every fix; the helper (scripts/gds/ask.js check) shows the same
|
|
15
|
+
// fixes; the rewritten card passes the helper and files, stored. A person on
|
|
16
|
+
// the hall (a web session) and the unattended scripts still file without one.
|
|
17
|
+
// 3. The scripts that file templated blockers send a card that has the shape,
|
|
18
|
+
// for every input they can see, and their sources are exactly the route's
|
|
19
|
+
// allow-list; a card the server refuses never costs the blocker.
|
|
20
|
+
// 4. The Conductor reminder fires when a session is about to ask a person, stays
|
|
21
|
+
// quiet otherwise, and states the ask's own numbers.
|
|
22
|
+
//
|
|
23
|
+
// DB-free: every read and write is stubbed by property.
|
|
24
|
+
// Run: node --test --test-reporter=tap tests/government_agent_asks.mjs
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import { test, after } from 'node:test';
|
|
28
|
+
import http from 'node:http';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import os from 'node:os';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
38
|
+
const FX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
39
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
40
|
+
// real past asks as the cards they should have been (test data; the hall never reads it)
|
|
41
|
+
const CARDS = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'government-ask-cards.json'), 'utf8')).cards;
|
|
42
|
+
|
|
43
|
+
const asks = require(path.join(GOV, 'asks.js'));
|
|
44
|
+
const asksDb = require(path.join(GOV, 'asks-db.js'));
|
|
45
|
+
const askCli = require(path.join(ROOT, 'scripts', 'gds', 'ask.js'));
|
|
46
|
+
const shipEscalation = require(path.join(ROOT, 'scripts', 'gds', 'ship-escalation.js'));
|
|
47
|
+
const artifactAudit = require(path.join(ROOT, 'scripts', 'gds', 'artifact-audit.js'));
|
|
48
|
+
const autobongosVerify = require(path.join(ROOT, 'scripts', 'gds', 'autobongos-verify.js'));
|
|
49
|
+
const conductor = require(path.join(ROOT, '.claude', 'hooks', 'conductor.js'));
|
|
50
|
+
|
|
51
|
+
const words = (t) => asks.words(t).length;
|
|
52
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-asks-'));
|
|
53
|
+
const writeCard = (name, body) => { const f = path.join(TMP, name); fs.writeFileSync(f, JSON.stringify(body)); return f; };
|
|
54
|
+
|
|
55
|
+
// The override ask as an agent first wrote it: the matter's title and its prose.
|
|
56
|
+
const OVERRIDE = fx('government__docket').matters.find((x) => x.id === 'override_requests:1000017');
|
|
57
|
+
const OVERRIDE_PROSE = [OVERRIDE.title, OVERRIDE.detail].filter(Boolean).join('\n\n');
|
|
58
|
+
// ...and as the card it should have been (the corpus's card for the same matter).
|
|
59
|
+
const OVERRIDE_CARD = (() => {
|
|
60
|
+
const a = CARDS.find((x) => x.id === 'override_requests:1000017');
|
|
61
|
+
return { question: a.question, options: a.options, recommend: a.recommend, waits_on: a.waits_on, if_nobody_answers: a.if_nobody_answers };
|
|
62
|
+
})();
|
|
63
|
+
|
|
64
|
+
// ── 1. real past asks come out as short cards ───────────────────────────────
|
|
65
|
+
|
|
66
|
+
test('the 238-word override ask comes out as a short card', () => {
|
|
67
|
+
assert.equal(words(OVERRIDE_PROSE), 238, "the override ask as an agent wrote it");
|
|
68
|
+
assert.ok(words(OVERRIDE.detail) >= 200, 'it is the long one');
|
|
69
|
+
const prose = asks.checkAsk({ question: OVERRIDE_PROSE });
|
|
70
|
+
assert.equal(prose.ok, false, 'the prose is not an ask');
|
|
71
|
+
const card = asks.checkAsk(OVERRIDE_CARD);
|
|
72
|
+
assert.equal(card.ok, true, JSON.stringify(card.errors));
|
|
73
|
+
const text = asks.cardText(card.ask);
|
|
74
|
+
assert.equal(text.split('\n')[0], 'Pay this task despite the grader crashing on it?');
|
|
75
|
+
assert.ok(words(text) <= 70, `the card is ${words(text)} words`);
|
|
76
|
+
assert.ok(words(text) * 3 < words(OVERRIDE_PROSE), 'a third of the prose or less');
|
|
77
|
+
assert.match(text, /^ {2}1\. Approve \(recommended\): The task counts as passed and its builder is paid\.$/m);
|
|
78
|
+
assert.match(text, /^ {2}2\. Decline: /m);
|
|
79
|
+
assert.match(text, /^Why: The 0\.0 is a crashed grader/m);
|
|
80
|
+
assert.match(text, /^Waits on the owner\. If nobody answers: The task stays at completed/m);
|
|
81
|
+
assert.doesNotMatch(text, /undefined|null|\[object/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('every real past ask written as prose comes out as a short card', () => {
|
|
85
|
+
let long = 0;
|
|
86
|
+
for (const a of CARDS) {
|
|
87
|
+
const raw = a.raw || {};
|
|
88
|
+
const prose = [raw.title, raw.detail].filter(Boolean).join('\n\n');
|
|
89
|
+
const r = asks.checkAsk(a);
|
|
90
|
+
assert.equal(r.ok, true, `${a.id}: ${JSON.stringify(r.errors)}`);
|
|
91
|
+
const text = asks.cardText(r.ask);
|
|
92
|
+
const lines = text.split('\n');
|
|
93
|
+
assert.ok(words(lines[0]) <= asks.ASK_RULES.question_words, `${a.id}: the question`);
|
|
94
|
+
assert.ok(lines.length <= 2 + asks.ASK_RULES.options_max + 1, `${a.id}: ${lines.length} lines`);
|
|
95
|
+
assert.ok(words(text) <= 90, `${a.id}: ${words(text)} words`);
|
|
96
|
+
if (words(prose) >= 50) {
|
|
97
|
+
long += 1;
|
|
98
|
+
assert.equal(asks.checkAsk({ question: prose }).ok, false, `${a.id}: its prose is refused`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
assert.ok(long >= 15, `${long} long prose asks`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('each seat reads as a person would say it', () => {
|
|
105
|
+
assert.equal(asks.seatWords('owner'), 'the owner');
|
|
106
|
+
assert.equal(asks.seatWords('lead:release'), 'the Release lead');
|
|
107
|
+
assert.equal(asks.seatWords('board:charter'), 'the Charter board');
|
|
108
|
+
assert.equal(asks.seatWords('committee:review.art_direction'), 'the Review art direction committee');
|
|
109
|
+
assert.equal(asks.cardText(null), '');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── 2. through the real routers: prose is refused, the rewrite files ───────
|
|
113
|
+
|
|
114
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
115
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
116
|
+
const authMod = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
117
|
+
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
118
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
119
|
+
const blockersDb = require(path.join(ROOT, 'modules', 'ideas', 'blockers.js'));
|
|
120
|
+
const blockersRoutes = require(path.join(ROOT, 'modules', 'ideas', 'routes', 'blockers.js'));
|
|
121
|
+
const express = require('express');
|
|
122
|
+
|
|
123
|
+
const real = {
|
|
124
|
+
apiRequireBuilder: api.requireBuilder, authRequireBuilder: authMod.requireBuilder, requirePermission: authMod.requirePermission,
|
|
125
|
+
resolve: resolver.resolveBuilderPermissions, saveAsk: asksDb.saveAsk, asksFor: asksDb.asksFor,
|
|
126
|
+
create: blockersDb.createBlocker, list: blockersDb.listOpenBlockers, count: blockersDb.countOpenBlockers, get: blockersDb.getBlocker,
|
|
127
|
+
};
|
|
128
|
+
// The session's source rides a header here: `cli` is an agent's tools, `web` the hall.
|
|
129
|
+
const fakeBuilder = (req, res, next) => {
|
|
130
|
+
req.builder = { id: 42, rank: 'metic', github_login: 'someone' };
|
|
131
|
+
req.gdsSession = { builder_id: 42, source: req.headers['x-session-source'] || 'web' };
|
|
132
|
+
next();
|
|
133
|
+
};
|
|
134
|
+
api.requireBuilder = fakeBuilder;
|
|
135
|
+
authMod.requireBuilder = fakeBuilder;
|
|
136
|
+
authMod.requirePermission = () => (req, res, next) => next();
|
|
137
|
+
resolver.resolveBuilderPermissions = async () => ['board.vote.cast'];
|
|
138
|
+
const store = new Map();
|
|
139
|
+
const filed = [];
|
|
140
|
+
asksDb.saveAsk = async ({ matterId, ask }) => { const row = { ...ask, written_at: '2026-10-09T12:00:00.000Z' }; store.set(matterId, row); return row; };
|
|
141
|
+
asksDb.asksFor = async (ids) => new Map(ids.filter((id) => store.has(id)).map((id) => [id, store.get(id)]));
|
|
142
|
+
let nextId = 500;
|
|
143
|
+
blockersDb.createBlocker = async ({ title, bodyMd, source }) => { const b = { id: String(nextId++), title, body_md: bodyMd, source, status: 'open' }; filed.push(b); return b; };
|
|
144
|
+
blockersDb.listOpenBlockers = async () => filed.slice();
|
|
145
|
+
blockersDb.countOpenBlockers = async () => filed.length;
|
|
146
|
+
blockersDb.getBlocker = async (id) => filed.find((b) => b.id === String(id)) || null;
|
|
147
|
+
|
|
148
|
+
seams._reset();
|
|
149
|
+
const app = express();
|
|
150
|
+
app.use(express.json());
|
|
151
|
+
app.use(attachFail);
|
|
152
|
+
app.use(require(path.join(GOV, 'routes', 'government.js'))());
|
|
153
|
+
app.use(blockersRoutes());
|
|
154
|
+
const server = http.createServer(app);
|
|
155
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
156
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
157
|
+
after(() => {
|
|
158
|
+
server.close();
|
|
159
|
+
api.requireBuilder = real.apiRequireBuilder; authMod.requireBuilder = real.authRequireBuilder; authMod.requirePermission = real.requirePermission;
|
|
160
|
+
resolver.resolveBuilderPermissions = real.resolve; asksDb.saveAsk = real.saveAsk; asksDb.asksFor = real.asksFor;
|
|
161
|
+
blockersDb.createBlocker = real.create; blockersDb.listOpenBlockers = real.list; blockersDb.countOpenBlockers = real.count; blockersDb.getBlocker = real.get;
|
|
162
|
+
seams._reset();
|
|
163
|
+
fs.rmSync(TMP, { recursive: true, force: true });
|
|
164
|
+
});
|
|
165
|
+
async function call(method, p, body, source) {
|
|
166
|
+
const headers = { 'content-type': 'application/json' };
|
|
167
|
+
if (source) headers['x-session-source'] = source;
|
|
168
|
+
const res = await fetch(`${BASE}${p}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
169
|
+
let json = null; try { json = await res.json(); } catch { /* none */ }
|
|
170
|
+
return { ok: res.ok, status: res.status, data: json };
|
|
171
|
+
}
|
|
172
|
+
// The helper's client: the CLI's request shape, against these routers.
|
|
173
|
+
const client = { request: (method, p, opts = {}) => call(method, p, opts.body, 'cli') };
|
|
174
|
+
|
|
175
|
+
test('an agent that files prose is refused, rewrites with the helper, and files the card', async () => {
|
|
176
|
+
const before = filed.length;
|
|
177
|
+
const blocker = { title: OVERRIDE.title, body_md: OVERRIDE.detail, source: 'task', source_ref: '1004704' };
|
|
178
|
+
|
|
179
|
+
// 1. prose alone, from an AI session: refused, told what to send and how to check it
|
|
180
|
+
const bare = await call('POST', '/blockers', blocker, 'cli');
|
|
181
|
+
assert.equal(bare.status, 400);
|
|
182
|
+
assert.equal(bare.data.error.code, 'ask_required');
|
|
183
|
+
assert.equal(bare.data.error.message, asks.ASK_REQUIRED_MESSAGE);
|
|
184
|
+
assert.match(bare.data.error.message, /at most 12 words/);
|
|
185
|
+
assert.match(bare.data.error.message, /node scripts\/gds\/ask\.js check/);
|
|
186
|
+
|
|
187
|
+
// 2. the prose sent as the ask: refused, every fix named
|
|
188
|
+
const prose = { question: OVERRIDE_PROSE, options: [], waits_on: '', if_nobody_answers: '' };
|
|
189
|
+
const slop = await call('POST', '/blockers', { ...blocker, ask: prose }, 'cli');
|
|
190
|
+
assert.equal(slop.data.error.code, 'ask_refused');
|
|
191
|
+
assert.match(slop.data.error.message, /The question is \d+ words; 12 at most\./);
|
|
192
|
+
assert.equal(filed.length, before, 'nothing was filed');
|
|
193
|
+
|
|
194
|
+
// 3. the helper says the same, through the server's dry run, and names each fix
|
|
195
|
+
const proseFile = writeCard('prose.json', { ...blocker, ask: prose });
|
|
196
|
+
const refused = await askCli.check(proseFile, { client });
|
|
197
|
+
assert.equal(refused.code, 1);
|
|
198
|
+
const out = refused.lines.join('\n');
|
|
199
|
+
assert.equal(refused.lines[0], `✗ ${slop.data.error.message}`, 'the server\'s own words');
|
|
200
|
+
assert.doesNotMatch(out, /checked here/, 'the server answered');
|
|
201
|
+
assert.match(out, /^ {2}- question: The question is \d+ words; 12 at most\.$/m);
|
|
202
|
+
assert.match(out, /^ {2}- options: Offer 2 or 3 tap answers, not 0\.$/m);
|
|
203
|
+
assert.match(out, /^ {2}- waits_on: Say who this waits on\.$/m);
|
|
204
|
+
|
|
205
|
+
// 4. the rewrite passes the helper, which prints the card as a person reads it
|
|
206
|
+
const rewrite = { ...blocker, ask: { ...OVERRIDE_CARD, detail: OVERRIDE.detail } };
|
|
207
|
+
const passed = await askCli.check(writeCard('card.json', rewrite), { client });
|
|
208
|
+
assert.equal(passed.code, 0, passed.lines.join('\n'));
|
|
209
|
+
assert.match(passed.lines[0], /^✓ This ask has the shape\. As a person reads it:$/);
|
|
210
|
+
assert.equal(passed.lines.slice(2).join('\n'), asks.cardText(OVERRIDE_CARD));
|
|
211
|
+
|
|
212
|
+
// 5. and files, stored against the blocker
|
|
213
|
+
const ok = await call('POST', '/blockers', rewrite, 'cli');
|
|
214
|
+
assert.equal(ok.status, 201, JSON.stringify(ok.data));
|
|
215
|
+
assert.equal(store.get(`blockers:${ok.data.blocker.id}`).question, OVERRIDE_CARD.question);
|
|
216
|
+
assert.equal(store.get(`blockers:${ok.data.blocker.id}`).detail, OVERRIDE.detail.trim(), 'the prose rides along');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('every source an agent files with is held to it; the hall and the templated scripts are not', async () => {
|
|
220
|
+
for (const source of [undefined, '', 'manual', 'task', 'something-new']) {
|
|
221
|
+
const r = await call('POST', '/blockers', { title: 'Stuck', ...(source === undefined ? {} : { source }) }, 'cli');
|
|
222
|
+
assert.equal(r.status, 400, `source ${JSON.stringify(source)}`);
|
|
223
|
+
assert.equal(r.data.error.code, 'ask_required');
|
|
224
|
+
}
|
|
225
|
+
// a person on the hall's form (a web session)
|
|
226
|
+
assert.equal((await call('POST', '/blockers', { title: 'Typed by a person' }, 'web')).status, 201);
|
|
227
|
+
// the unattended scripts, from a CLI session: they file with or without a card
|
|
228
|
+
for (const source of blockersRoutes.TEMPLATED_SOURCES) {
|
|
229
|
+
const r = await call('POST', '/blockers', { title: `From ${source}`, source, source_ref: `x-${source}` }, 'cli');
|
|
230
|
+
assert.equal(r.status, 201, source);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('the helper checks locally, and says so, when the server cannot answer', async () => {
|
|
235
|
+
const offline = { request: async () => { throw new Error('ECONNREFUSED'); } };
|
|
236
|
+
const r = await askCli.check(writeCard('slop2.json', { question: 'Can you look at the deploy and tell me what you think about it all?' }), { client: offline });
|
|
237
|
+
assert.equal(r.code, 1);
|
|
238
|
+
assert.match(r.lines.join('\n'), /checked here; the server could not be asked/);
|
|
239
|
+
const ok = await askCli.check(writeCard('ok2.json', OVERRIDE_CARD), { local: true });
|
|
240
|
+
assert.equal(ok.code, 0);
|
|
241
|
+
assert.match(ok.lines[0], /checked here/);
|
|
242
|
+
const bad = await askCli.check(path.join(TMP, 'missing.json'), { local: true });
|
|
243
|
+
assert.equal(bad.code, 2);
|
|
244
|
+
assert.match(bad.lines[0], /ask\.js template/);
|
|
245
|
+
// the blank card it hands out has the shape, so an agent starts from a valid one
|
|
246
|
+
assert.equal(asks.checkAsk(askCli.TEMPLATE).ok, true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ── 3. the scripts that file templated blockers ─────────────────────────────
|
|
250
|
+
|
|
251
|
+
test('each templated blocker carries a card with the shape, for every input it can see', () => {
|
|
252
|
+
const cards = [
|
|
253
|
+
shipEscalation.escalationAsk(1003822),
|
|
254
|
+
shipEscalation.escalationAsk('99999999999999'),
|
|
255
|
+
artifactAudit.blockerAsk([{ id: 1 }]),
|
|
256
|
+
artifactAudit.blockerAsk(Array.from({ length: 250 }, (_, i) => ({ id: i }))),
|
|
257
|
+
];
|
|
258
|
+
for (const shape of ['undetermined', 'pending_land', 'shipped_no_artifact', 'abandoned', 'still_held', 'blocked', 'not_started']) {
|
|
259
|
+
const row = autobongosVerify.blockerFor({ shape, status: 'active', reason: 'r' }, { taskId: '99999999999999' });
|
|
260
|
+
assert.deepEqual(row.ask, autobongosVerify.blockerAsk({ shape }, { taskId: '99999999999999' }));
|
|
261
|
+
cards.push(row.ask);
|
|
262
|
+
}
|
|
263
|
+
for (const c of cards) {
|
|
264
|
+
const r = asks.checkAsk(c);
|
|
265
|
+
assert.equal(r.ok, true, `${c.question}: ${JSON.stringify(r.errors)}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('the route\'s templated sources are exactly the ones the scripts send', async () => {
|
|
270
|
+
const sent = [];
|
|
271
|
+
const capture = async (_m, _p, body) => { sent.push(body); return { ok: true, status: 201, data: { blocker: { id: 1 } } }; };
|
|
272
|
+
await shipEscalation.fileNoArtifactEscalation({ taskId: 7, title: 't' }, { call: capture });
|
|
273
|
+
await artifactAudit.fileBlocker([{ id: '1', status: 'shipped', shipped_at: '2026-09-01', title: 'x', touches: ['a.js'] }], { call: capture });
|
|
274
|
+
const gen = { blockers: { postBlockers: async ({ body }) => { sent.push(body); return { ok: true, status: 201, data: { blocker: { id: 2 } } }; } } };
|
|
275
|
+
await autobongosVerify.fileBlocker(gen, autobongosVerify.blockerFor({ shape: 'still_held', status: 'active', reason: 'r' }, { taskId: 7 }), { taskId: 7, link: false });
|
|
276
|
+
assert.deepEqual(sent.map((b) => b.source).sort(), [...blockersRoutes.TEMPLATED_SOURCES].sort());
|
|
277
|
+
for (const b of sent) assert.equal(asks.checkAsk(b.ask).ok, true, `${b.source} sends its card`);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('a card the server refuses never costs the blocker; a broken card is never sent', async () => {
|
|
281
|
+
const sent = [];
|
|
282
|
+
const refusing = async (body) => {
|
|
283
|
+
sent.push(body);
|
|
284
|
+
return body.ask ? { ok: false, status: 400, data: { error: { code: 'ask_refused' } } } : { ok: true, status: 201, data: { blocker: { id: 9 } } };
|
|
285
|
+
};
|
|
286
|
+
const r = await askCli.postWithAsk(refusing, { title: 't', source: 'autobongos' }, OVERRIDE_CARD);
|
|
287
|
+
assert.equal(r.status, 201);
|
|
288
|
+
assert.deepEqual(sent.map((b) => !!b.ask), [true, false], 'sent with the card, then again without it');
|
|
289
|
+
|
|
290
|
+
sent.length = 0;
|
|
291
|
+
await askCli.postWithAsk(refusing, { title: 't' }, { question: 'not a card' });
|
|
292
|
+
assert.deepEqual(sent.map((b) => !!b.ask), [false], 'a card that breaks a rule is not sent');
|
|
293
|
+
|
|
294
|
+
// a failure that is not about the card is reported, not retried
|
|
295
|
+
sent.length = 0;
|
|
296
|
+
const down = async (body) => { sent.push(body); return { ok: false, status: 503, data: {} }; };
|
|
297
|
+
assert.equal((await askCli.postWithAsk(down, { title: 't' }, OVERRIDE_CARD)).status, 503);
|
|
298
|
+
assert.equal(sent.length, 1);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ── 4. the Conductor reminder ───────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
test('the Conductor reminds a session about to ask a person, and only then', () => {
|
|
304
|
+
const ids = (prompt) => conductor.evaluateTriggers({ prompt, activeClaims: [] }).map((a) => a.id);
|
|
305
|
+
for (const p of ['file a blocker for this', 'I am stuck on the owner', 'ask the owner which one', 'please confirm the list',
|
|
306
|
+
'get a review of this', 'Bongos feedback: claim is slow', '/builder-ship', 'ship it', "I'm done", 'wrap it up', '/ask-for-help 12']) {
|
|
307
|
+
assert.ok(ids(p).includes('ask-shape'), p);
|
|
308
|
+
}
|
|
309
|
+
for (const p of ['explain the architecture', 'add a tile to the map', 'how does the grader work?']) {
|
|
310
|
+
assert.ok(!ids(p).includes('ask-shape'), p);
|
|
311
|
+
}
|
|
312
|
+
const a = conductor.evaluateTriggers({ prompt: 'file a blocker' }).find((x) => x.id === 'ask-shape');
|
|
313
|
+
assert.equal(a.text, conductor.ASK_SHAPE_TEXT);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('the reminder states the ask\'s own numbers', () => {
|
|
317
|
+
const R = asks.ASK_RULES;
|
|
318
|
+
assert.match(conductor.ASK_SHAPE_TEXT, new RegExp(`at most ${R.question_words} words`));
|
|
319
|
+
assert.match(conductor.ASK_SHAPE_TEXT, new RegExp(`${R.options_min} or ${R.options_max} tap answers`));
|
|
320
|
+
assert.match(asks.ASK_REQUIRED_MESSAGE, new RegExp(`at most ${R.question_words} words, ${R.options_min} or ${R.options_max} tap answers`));
|
|
321
|
+
assert.match(conductor.ASK_SHAPE_TEXT, /node scripts\/gds\/ask\.js check/);
|
|
322
|
+
});
|