@bongos/core 1.21.151 → 1.21.153

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.
Files changed (55) hide show
  1. package/.bongos-core.json +73 -58
  2. package/docs/copy-inventory.md +323 -317
  3. package/docs/copy-registry.json +423 -369
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/modules-contract.md +30 -0
  6. package/docs/page-readings.json +234 -227
  7. package/modules/agents/module.json +1 -0
  8. package/modules/autonomy/module.json +1 -0
  9. package/modules/builder-settings/module.json +1 -0
  10. package/modules/copy-desk/module.json +1 -0
  11. package/modules/design-styles/module.json +1 -0
  12. package/modules/discord/module.json +1 -0
  13. package/modules/economy/module.json +1 -0
  14. package/modules/government/module.json +1 -0
  15. package/modules/grading/module.json +1 -0
  16. package/modules/hall-ui/module.json +1 -0
  17. package/modules/hall-ui/public/modules.html +13 -0
  18. package/modules/hall-ui/public/modules.js +101 -4
  19. package/modules/hall-ui/public/modules.states.json +2 -1
  20. package/modules/hall-ui/records/catalog.md +2 -0
  21. package/modules/ideas/module.json +1 -0
  22. package/modules/lifecycle/github-failure.js +50 -0
  23. package/modules/lifecycle/module.json +1 -0
  24. package/modules/lifecycle/routes/tasks.js +2 -2
  25. package/modules/memory/module.json +1 -0
  26. package/modules/npm-release/module.json +1 -0
  27. package/modules/onboarding/module.json +1 -0
  28. package/modules/page-notes/module.json +1 -0
  29. package/modules/pixel-art/module.json +1 -0
  30. package/modules/platform-identity/module.json +1 -0
  31. package/modules/provisioning/github-setup.js +51 -1
  32. package/modules/provisioning/module.json +1 -0
  33. package/modules/provisioning/routes/provisioning.js +7 -2
  34. package/modules/public-landing/module.json +1 -0
  35. package/modules/public-landing/public/projects.html +33 -0
  36. package/modules/render-deploy/module.json +1 -0
  37. package/modules/security/module.json +1 -0
  38. package/modules/sessions/module.json +1 -0
  39. package/modules/specialities/module.json +1 -0
  40. package/modules/status-ui/module.json +1 -0
  41. package/modules/ui-design/module.json +1 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/release-notes.json +20 -0
  45. package/scripts/gds/module.js +4 -2
  46. package/src/bongos/module-listing.js +76 -2
  47. package/src/module-api.js +1 -1
  48. package/src/module-loader/catalog.js +15 -1
  49. package/src/module-loader/manifest-schema.js +40 -1
  50. package/tests/github_failure.mjs +79 -0
  51. package/tests/github_install_nudge.mjs +219 -0
  52. package/tests/hall_catalog_world.mjs +23 -3
  53. package/tests/module_catalog.mjs +34 -0
  54. package/tests/module_manifest.mjs +36 -0
  55. package/tests/module_store_listing.mjs +78 -1
@@ -4,6 +4,7 @@
4
4
  "description": "Declarative agent definitions (.claude/agents/*.md) reconciled at deploy into a DB-backed registry (agents_definitions) plus a per-fire ledger (agents_runs) — BONGOS-V1 goal 1000038, Phase 1. Files-are-source: agents-sync upserts file-sourced definitions and never clobbers an instance-authored db row, and stamps author_rank from the author's LIVE DB rank because a committed file cannot grant authority (ADR 0016). Spawns consume the EXISTING `grade` port (runSubagentCached), which is fixed read-only, so v1 agents are observe-and-report only — no write principals — and every fire bills through the shared LLM cost cache. default:false: shipped is not enabled, and an instance opts in per ADR 0083. spend.requiresPayer (task 1003884): a fire spends real money for whoever called it (~$0.90, and POST /agents/:name/invoke answers 202 without waiting), so the core refuses to ENABLE this module on an instance whose join door is `open` with no payer configured — an account created seconds ago would otherwise spend the instance's money with nobody to bill. Every run then records its payer beside cost_usd (agents_002_payer.sql) and a per-payer monthly ceiling fails closed when it is exhausted OR unreadable (lib/spend-ceiling.js) — distinct from lib/fire-budget.js, which stays a rate ceiling.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "work" },
7
8
  "default": false,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "routes": ["agents"], "pollers": ["event-router"], "migrations": true },
@@ -4,6 +4,7 @@
4
4
  "description": "The Multi-Agent System's autonomy surfaces (ADR 0024): the $0 deterministic autonomy gate (System 3 — a precheck route + the gate CLI, inert until OTB_AUTONOMY_ENABLED=1), the conductor mid-flight dispatch CLI (Phase 4), the Architect bounded first-principles --scope audit CLI (Phase 5), and the in-process routine timers (System 4 / ADR 0078 amendment — the weekly peer-vote-tally poller). A core-domain module carved out of the GDS core in BV1.R83 (ADR 0093 §1); default-on because the autonomy machinery + the durable routine timer ship to every instance (both are inert/no-op until armed or the pool is reachable). Cross-cutting: it OBSERVES lifecycle events. The routine timer reaches the lifecycle `tallyPeerVotes` capability through the published doorway (a tracked lifecycle leak until R86), never a core require; the precheck route + the gate/conductor/architect CLIs travel with this domain (the CLIs stay under scripts/, outside the kernel boundary, so they may still read the ideas/economy/lifecycle signals + the grader directly). Registers no port — core never calls back in.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.11.0",
7
+ "category": { "parent": "work" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": {
@@ -4,6 +4,7 @@
4
4
  "description": "The per-builder PREFERENCE domain — the wandering knob (how readily the agent opens off-task threads, rank-clamped), per-skill model ceilings, per-sound + per-event-sound on/off, and the render-widgets toggle — plus builder-needs (the 'the system needs an input from you' list). A core-domain module carved out of the GDS core in BV1.R80 (ADR 0093 §1); default-on because every instance configures its own builders. The `/me/*-prefs` routes mount here; the `GET /me` aggregator stays a thin core facade that resolves the `builder-settings` kernel PORT for its wandering/render/needs slices (02-module-map §2).",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.8.0",
7
+ "category": { "parent": "people" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "routes": ["settings"] },
@@ -4,6 +4,7 @@
4
4
  "description": "The COPY half of the design contract (goal 1000074, the artist's loop). Named for the editorial desk copy passes over before it is published — which is exactly the job, and which keeps the module key off the generic word 'copy' that the branding contract already owns (branding.copy.tagline/hallName/notFound). ADR 0081 made the repo the source of truth for design TOKENS and component surfaces; there was no equivalent for user-facing TEXT. This module owns that half: the FLAG — anyone, in any role, marks a string or a whole surface as needing an artist's attention, with a required reason — and the QUEUE an artist reads it from. The inventory a flag points at is the committed registry docs/copy-registry.json, generated by scripts/gds/copy-inventory.js (R01); this module reads it and never writes it. R03 (task 1003114) added the PROPOSAL: an artist writes the replacement wording and it becomes a GDS task carrying a patch, which scripts/gds/copy-apply.js lands as a real diff under a claim — the design half's adapter.export() contract (ADR 0081) applied to text. NO LIVE CMS is the goal's hard non-goal: nothing here can change a rendered string. A flag is a work request; a proposal is a work request with a suggested fix; the fix ships as a claimed, graded GDS task like every other change. default-on — every instance shows a person words, so every instance can flag them.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.8.0",
7
+ "category": { "parent": "quality" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "routes": ["copy-desk", "copy-desk-artist"], "migrations": true, "skills": ["tweak"] },
@@ -4,6 +4,7 @@
4
4
  "description": "The thirteen design-STYLE skills split out of ui-design (task 1004470, owner ruling on blocker 1000139): the taste core (design-taste-frontend, high-end-visual-design, redesign-existing-projects), the impeccable craft method, the /style session, the four looks (minimalist-ui, industrial-brutalist-ui, gpt-taste, stitch-design-taste) and the image family (image-to-code, imagegen-frontend-web, imagegen-frontend-mobile, brandkit). Every one is an in-house rebuild or a first-party original (ADR 0220, ADR 0231), carrying the PLATFORM PREAMBLE from skills/PREAMBLE.md and governed by the vendoring policy in skills/policy.json (ADR 0198). ui-design keeps the method every instance needs — /design, the two sync skills, the contract, the kit and the style library (modules/ui-design/styles/, which these skills name as their palette source) — and stays default-on; these are opt-in because their descriptions alone are ~5,000 chars resident in every session. /design names them when this module is on and says plainly that they are off when it is not. Declaration-only: no routes, no migrations, no port.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": false,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "skills": ["design-taste-frontend", "high-end-visual-design", "redesign-existing-projects", "impeccable", "style", "minimalist-ui", "industrial-brutalist-ui", "gpt-taste", "stitch-design-taste", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"] },
@@ -4,6 +4,7 @@
4
4
  "description": "Outbound ship broadcast + the inbound #ideas/#bugs bot + the #support answerer + role-sync + channel management + account-link state. Mirror only, never authority (ADR 0033).",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.4.0",
7
+ "category": { "parent": "communication" },
7
8
  "default": false,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": {
@@ -4,6 +4,7 @@
4
4
  "description": "Credit allocation (the cost-plus + kind-multiplier reward model), the cost ledger + dashboard rollups, builder streaks, achievements/gamification, and the leaderboard. A core-domain module carved out of the GDS core in BV1.R77 (ADR 0093 §1-2); default-on because every instance rewards its own builders. Registers the `reward` kernel PORT the lifecycle resolves at confirm/ship/claim time.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.7.0",
7
+ "category": { "parent": "money" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "routes": ["cost", "credits", "earnings", "leaderboard", "reward"] },
@@ -4,6 +4,7 @@
4
4
  "description": "Discord-style configurable ranks and permissions (ADR 0151, BONGOS-V1 goal 1000030; renamed governance\u2192government by ADR 0174). Permissions are the atomic authority unit and the four ranks (xenos/thetes/metic/archon) are the positions in the government; this module owns the code-defined permission catalog (each key flagged `system` = trust-boundary, `substrate` = enforced out of band, or operational = delegable) plus the compatibility seed that reproduces the rank thresholds exactly. A core-domain module, default-on: a Cloud Bongos instance ships configurable ranks the way it ships its status page. The word `role` is deliberately absent here \u2014 it is reserved for the three crafts (Artist, Ideator, Builder). Preserves ADR 0016 (authority resolved server-side, per request, uncached).",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.18.0",
7
+ "category": { "parent": "people" },
7
8
  "default": true,
8
9
  "maintenance": {
9
10
  "status": "core-maintained"
@@ -4,6 +4,7 @@
4
4
  "description": "The grade-a-diff adversarial worker panel (Narc/Quality/Hacker/Efficiency) + deterministic route-rank/permission-path pre-passes + the scoring rubric. A core-domain module carved out of the GDS core in BV1.R73 (ADR 0091); default-on because every instance grades its own ships. Registers the `grade` kernel PORT the lifecycle resolves at confirm/ship time.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.6.0",
7
+ "category": { "parent": "quality" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": {
@@ -4,6 +4,7 @@
4
4
  "description": "The authenticated builders' hall served at builders.<apex> (builders.example.com) — the roster, work board, standing/karma, leaderboard, achievements, primer, system maps, settings, and the Archon-only watch/gate pages, all polling /api/bongos/* and the read-only /api/bongos/public/* surface. The largest WEB-SURFACE core-domain module (~13k LOC), carved out of the GDS core in BV1.R85 (ADR 0093 §1; 02-module-map §6). default-on: a Cloud Bongos instance ships its hall like it ships its status page. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir). Unlike the generic status surface, the hall keeps its dedicated serving block in serve-internal.js (the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection) — the webSurfaces declaration is the discovery + scope-map + window.__MODULES__ record; serve-internal serves it from this module's public/ dir with that richer machinery. The me/public/leaderboard read endpoints it renders stay SHARED core in routes/{me,public,builders}.js (the status surface + the off-box mirror read them too).",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.11.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": {
@@ -73,6 +73,19 @@
73
73
  <ul class="ov-rows" id="own-grid"></ul>
74
74
  <p class="ov-foot" id="submit-lede">A module built here can be submitted upstream for review, so it can join the shared catalog with your name and this project credited.</p>
75
75
  </div>
76
+
77
+ <!-- In the store — what GET /store/modules lists, each with what it is
78
+ strong and weak at (task 1003799, ADR 0343 D4), so two modules with
79
+ the same score can be told apart. Hidden until the store answers
80
+ with something to list. -->
81
+ <div class="ov-group" id="store-group" hidden>
82
+ <div class="ov-group__head">
83
+ <span class="ov-group__name">In the store</span>
84
+ <span class="ov-group__n" id="store-n"></span>
85
+ </div>
86
+ <ul class="ov-rows" id="store-grid"></ul>
87
+ <p class="ov-foot" id="store-lede">A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module.</p>
88
+ </div>
76
89
  </section>
77
90
 
78
91
  <!-- One store version's how-to (task 1004366, ADR 0347): the HOWTO.md that
@@ -3,8 +3,9 @@
3
3
  // ADR 0107 §9 (task 1776) + §2/§3 the browsable catalog (task 1001772);
4
4
  // redesigned onto the oversight family's row in task 1003472. Jobs:
5
5
  // 1. BROWSE the catalog — every module the core ships that this instance can
6
- // enable, each with its author + origin credit, filterable by one search
7
- // over name/author/origin/licence and a state lens. Split from the
6
+ // enable, each with its author + origin credit and its category (ADR 0342),
7
+ // filterable by one search over name/author/origin/licence/category and a
8
+ // state lens. Split from the
8
9
  // instance's OWN modules by `source` (which TREE it ships from — the core
9
10
  // package or this repo), which GET /api/bongos/modules reads structurally
10
11
  // off the loader's discovery roots. That is a different axis from
@@ -73,7 +74,8 @@
73
74
  function matchesQuery(m, q) {
74
75
  if (!q) return true;
75
76
  const credit = m.credit || {};
76
- return [m.key, m.title, m.description, credit.author, credit.origin, m.license]
77
+ const cat = m.category || {};
78
+ return [m.key, m.title, m.description, credit.author, credit.origin, m.license, cat.parentTitle, cat.sub]
77
79
  .some((f) => f && String(f).toLowerCase().includes(q));
78
80
  }
79
81
  function matchesLens(m) {
@@ -82,6 +84,16 @@
82
84
  return true;
83
85
  }
84
86
 
87
+ // Where the module is browsed (ADR 0342): the parent's label, and the
88
+ // sub-category once one is approved. The label comes from the server's row
89
+ // (catalog.js), so the hall keeps no copy of the parent list.
90
+ function categoryFact(m) {
91
+ const c = m.category;
92
+ if (!c || !c.parentTitle) return '';
93
+ const sub = c.sub ? ` · ${escapeHtml(c.sub)}` : '';
94
+ return `<span class="ov-fact">in <b>${escapeHtml(c.parentTitle)}</b>${sub}</span>`;
95
+ }
96
+
85
97
  // Upkeep posture (ADR 0107 §7 / ADR 0166) — who keeps this module true as the
86
98
  // core advances. Colour marks the EXCEPTION: an unowned entry is the thing the
87
99
  // policy exists to catch, so it is the warn pill; a maintained one is a plain
@@ -185,13 +197,96 @@
185
197
  <div class="ov-row__main">
186
198
  <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
187
199
  ${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
188
- <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}${restartFact(m)}</div>
200
+ <div class="ov-row__facts">${provenancePill(m)}${categoryFact(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}${restartFact(m)}</div>
189
201
  ${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
190
202
  </div>
191
203
  <div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
192
204
  </li>`;
193
205
  }
194
206
 
207
+ // ---- the store listing (task 1003799, ADR 0343 D4) ----
208
+ //
209
+ // GET /store/modules entries carry their parts read as strengths, weaknesses and
210
+ // unmeasured parts (src/bongos/module-listing.js readParts). The row shows the
211
+ // short labels; the sentence behind each is one click away in the details, the
212
+ // splitCriterion idea: scannable in a list, thorough on demand.
213
+ let storeModules = [];
214
+
215
+ function partsList(items) {
216
+ return items.map((p) => `${escapeHtml(p.title)} <b>${escapeHtml(String(p.score))}</b>`).join(', ');
217
+ }
218
+
219
+ function storeFacts(m) {
220
+ const facts = [m.is_new
221
+ ? '<span class="fact-pill fact-pill--info" title="No real-world data yet, so it is listed but not ranked.">new</span>'
222
+ : `<span class="ov-fact">scores <b>${escapeHtml(String(m.overall))}</b></span>`];
223
+ const strong = Array.isArray(m.strengths) ? m.strengths : [];
224
+ const weak = Array.isArray(m.weaknesses) ? m.weaknesses : [];
225
+ if (strong.length) facts.push(`<span class="ov-fact">strong at ${partsList(strong)}</span>`);
226
+ // A weakness is the exception a buyer must not miss, so it is the warn pill.
227
+ if (weak.length) facts.push(`<span class="fact-pill fact-pill--warn">weak at ${partsList(weak)}</span>`);
228
+ return facts.join('');
229
+ }
230
+
231
+ function storeDetailLines(m) {
232
+ const lines = [...(m.strengths || []), ...(m.weaknesses || [])]
233
+ .map((p) => `<li><span class="ov-key ov-key--faint">${escapeHtml(p.title)}</span><span class="ov-lines__v">${escapeHtml(p.detail)}</span></li>`);
234
+ const missing = (m.unmeasured || []).map((p) => p.title);
235
+ if (missing.length) {
236
+ lines.push(`<li><span class="ov-key ov-key--faint">not measured yet</span><span class="ov-lines__v">${escapeHtml(missing.join(', '))}</span></li>`);
237
+ }
238
+ return lines.length ? `<ul class="ov-lines">${lines.join('')}</ul>` : '';
239
+ }
240
+
241
+ function storeRowHTML(m) {
242
+ const key = `store:${m.key}`;
243
+ const lines = storeDetailLines(m);
244
+ return `
245
+ <li class="ov-row mod-row" data-key="${escapeHtml(key)}">
246
+ <div class="ov-row__main">
247
+ <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
248
+ <div class="ov-row__facts">${storeFacts(m)}</div>
249
+ ${lines ? `<details class="mod-more"${openKeys.has(key) ? ' open' : ''}><summary aria-label="Strengths and weaknesses of ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
250
+ </div>
251
+ <div class="ov-row__right mod-row__right"><span class="ov-mono">${escapeHtml(String(m.version || ''))}</span></div>
252
+ </li>`;
253
+ }
254
+
255
+ function matchesStoreQuery(m, q) {
256
+ if (!q) return true;
257
+ return [m.key, m.title, ...(m.strengths || []).map((p) => p.title), ...(m.weaknesses || []).map((p) => p.title)]
258
+ .some((f) => f && String(f).toLowerCase().includes(q));
259
+ }
260
+
261
+ function renderStore() {
262
+ const group = $('#store-group');
263
+ if (!group) return;
264
+ group.hidden = !storeModules.length;
265
+ if (!storeModules.length) return;
266
+ const shown = storeModules.filter((m) => matchesStoreQuery(m, query));
267
+ const list = $('#store-grid');
268
+ if (list) {
269
+ list.innerHTML = shown.length ? shown.map(storeRowHTML).join('')
270
+ : `<li>${kit.emptyStateHtml('No store module matches that search.')}</li>`;
271
+ }
272
+ const n = $('#store-n');
273
+ if (n) n.textContent = String(shown.length);
274
+ }
275
+
276
+ // Best-effort and last: the store is a second read, and the modules on disk ARE
277
+ // this page, so a store that cannot be read costs this group and nothing else.
278
+ async function loadStore() {
279
+ try {
280
+ const res = await api.request('GET', `${API}/store/modules`);
281
+ if (!res.ok) return;
282
+ const body = res.data || {};
283
+ storeModules = [...(body.listed || []), ...(body.new || [])];
284
+ renderStore();
285
+ } catch (err) {
286
+ console.error('[modules] store listing failed to load', err);
287
+ }
288
+ }
289
+
195
290
  // `source` splits the page: 'core' = shipped by the core package (the
196
291
  // catalog you opt into), 'instance' = lives in this repo. Older payloads
197
292
  // predate the field, so fall back to the credit axis rather than emptying
@@ -236,6 +331,7 @@
236
331
  if (coreN) coreN.textContent = String(core.length);
237
332
  const ownN = $('#own-n');
238
333
  if (ownN) ownN.textContent = String(own.length);
334
+ renderStore();
239
335
 
240
336
  // The count line speaks only while a filter narrows the list — the figure
241
337
  // band under it already says how many there are.
@@ -354,6 +450,7 @@
354
450
  if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
355
451
  if (!res.ok) throw new Error(`modules → ${res.status}`);
356
452
  render(res.data);
453
+ loadStore();
357
454
  } catch (err) {
358
455
  console.error('[modules] failed to load', err);
359
456
  const list = $('#core-grid');
@@ -13,7 +13,8 @@
13
13
  "/api/(gds|bongos)/goals",
14
14
  "/api/(gds|bongos)/inbox/",
15
15
  "/api/(gds|bongos)/versions",
16
- "/api/(gds|bongos)/modules"
16
+ "/api/(gds|bongos)/modules",
17
+ "/api/(gds|bongos)/store/modules$"
17
18
  ],
18
19
  "ignoreConsole": [
19
20
  "/api/(gds|bongos)/",
@@ -10,3 +10,5 @@
10
10
  Also gone, and the one rule in that list: `.ab-caption` takes the lede's `--measure`, never a fresh `90ch`. **Gotcha:** `gen-atlas.js` run from the MAIN checkout walks `.claude/worktrees/*` and emits thousands of context nodes — run it from a worktree, or copy a worktree's `atlas.json` over, before previewing the atlas there.
11
11
 
12
12
  **A store version's how-to is a view of the Modules page, not a new page (task 1004366).** `/builders/modules?howto=<key>&version=<X.Y.Z>` hides the catalog and shows that version's `HOWTO.md` in the reading room's article card: the page adds `body.room` so `room.css`'s sizes apply, and the markdown goes through `md-reader.js` (author-written, so every byte is escaped; only absolute http(s), mailto and #anchor links become links). A query, not a `#/` route — the hash namespace is frozen to the idea and blocker overlays. "Save as PDF" is `window.print()` with an `@media print` block in `modules.css` that keeps only the document and prints in the system ink (`CanvasText`), because a literal colour is the token tier's. Harness fixture: `store__modules__howto.json`.
13
+
14
+ **The store's modules are a third group on the same panel, read as strengths and weaknesses (task 1003799, ADR 0343 D4).** `#store-group` ("In the store") lists what `GET /store/modules` lists, hidden until that read answers with something; it loads after the modules on disk and best-effort, so a store that cannot be read costs this group and nothing else. A row is the same `.ov-row`: the overall score as a plain fact (or the `new` info pill), `strong at Tests 96, How-to 88` as a plain fact, and `weak at …` as the warn pill, because a weakness is the exception a buyer must not miss. The sentence behind each part ("Tests scored 96 out of 100, from 40 test cases.") and the parts with no data yet sit in the row's `<details class="mod-more">`, the `splitCriterion` idea: short label in the list, the detail one click away. The reading is the server's (`readParts` in `src/bongos/module-listing.js`: strong at 80 or more, weak below the listing floor, unmeasured never weak); the page re-derives nothing. Harness fixture: `store__modules.json`.
@@ -4,6 +4,7 @@
4
4
  "description": "The idea inbox (capture, triage, criterion-proposal ratification) and the blockers queue — the methodology surfaces that feed the backlog. A core-domain module carved out of the GDS core in BV1.R78 (ADR 0093 §1); default-on because every instance captures ideas and tracks what's stuck on its owner. Registers the `ideas.capture` kernel PORT the discord #ideas inbound resolves (ADR 0093 §5 — retiring the temporary `captureIdea` doorway leak).",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.8.0",
7
+ "category": { "parent": "work" },
7
8
  "default": true,
8
9
  "maintenance": {
9
10
  "status": "core-maintained"
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/github-failure.js — say WHICH GitHub call failed, and what
4
+ // GitHub answered (task 1004766).
5
+ //
6
+ // WHY. GET /tasks/:id/publish-status answered every unexpected GitHub failure with
7
+ // a bare 502 `status_failed`, and logged only the error's code. On the toast
8
+ // instance (2026-10-20) the GitHub App was installed and the merge was on main,
9
+ // yet every probe 502'd: the builder saw "no explanation", and the operator's log
10
+ // said one word. The thrown errors already carry what is needed — the code says
11
+ // which step broke (minting the App token, listing PRs, reading a branch) and
12
+ // `status` / `detail` hold GitHub's own HTTP status and message — they were just
13
+ // dropped on the floor.
14
+ //
15
+ // SAFE TO SHOW. `detail` is GitHub's response body (an error message such as
16
+ // "Bad credentials" or "Resource not accessible by integration"), never a token:
17
+ // tokens travel in request headers, and no caller puts one in an error. It is
18
+ // trimmed and capped anyway. PURE.
19
+
20
+ const STEPS = Object.freeze({
21
+ APP_TOKEN_FAILED: "the server could not get a GitHub App token (GitHub refused the App's key or its installation)",
22
+ STATUS_FETCH_FAILED: 'GitHub refused a read of the repository',
23
+ PUSH_UNCONFIGURED: 'the server has no GitHub credential',
24
+ BAD_BRANCH: 'the branch name is not one the server will ask GitHub about',
25
+ });
26
+
27
+ const MAX_DETAIL = 300;
28
+
29
+ function describeGithubFailure(e) {
30
+ const code = (e && e.code) || null;
31
+ const status = e && Number.isInteger(e.status) ? e.status : null;
32
+ const detail = e && e.detail ? String(e.detail).replace(/\s+/g, ' ').trim().slice(0, MAX_DETAIL) : null;
33
+ const step = (code && STEPS[code]) || 'an unexpected error while asking GitHub';
34
+ const github = status ? ` GitHub answered HTTP ${status}${detail ? `: ${detail}` : ''}.` : '';
35
+ return {
36
+ message: `Could not read the land status: ${step}.${github}`,
37
+ details: { cause: code, github_status: status, github_detail: detail },
38
+ log: `${code || (e && e.message) || 'unknown'}${status ? ` (GitHub HTTP ${status}${detail ? `: ${detail}` : ''})` : ''}`,
39
+ };
40
+ }
41
+
42
+ // The publish-status route's answer, in one call so the (size-capped) route file
43
+ // stays one line: log the step + GitHub's reply, answer 502 with the same words.
44
+ function failGithubStatus(res, log, e) {
45
+ const why = describeGithubFailure(e);
46
+ if (log) log.error('[gds] GET /tasks/:id/publish-status', why.log);
47
+ return res.fail('status_failed', { status: 502, message: why.message, details: why.details });
48
+ }
49
+
50
+ module.exports = { describeGithubFailure, failGithubStatus };
@@ -4,6 +4,7 @@
4
4
  "description": "The build state machine \u2014 tasks \u00b7 claims \u00b7 versions \u00b7 done-when criteria \u00b7 goals \u00b7 dependencies \u00b7 the claim\u2192ship\u2192grade\u2192credit lifecycle \u00b7 override requests \u00b7 the ship/confirm/promote orchestration \u00b7 github-push + merge-lease + conflict-resolve + the publish reconciler. The LAST and most-coupled core domain carved out of the GDS core in BV1.R86 (ADR 0093 \u00a71,\u00a73, 02-module-map \u00a74 \u2014 kept whole, never split). default-on; every instance runs the lifecycle. Registers the `lifecycle` kernel PORT (createTask \u00b7 VOTABLE_TASK_STATUSES \u00b7 classifyTaskKind \u00b7 tallyPeerVotes + the work-tracking reads) that core read routes + the ideas/discord/autonomy modules resolve. As a transaction-participant it RESOLVES the `reward` + `onboarding` ports inside its claim/ship/confirm txns \u2014 but via resolveOptional (graceful, like sessions\u2192reward), so consumes is empty: a vanilla instance without those modules degrades the credit/onboarding writes to best-effort no-ops, it does not fail.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.12.0",
7
+ "category": { "parent": "work" },
7
8
  "default": true,
8
9
  "maintenance": {
9
10
  "status": "core-maintained"
@@ -55,6 +55,7 @@ const { makeAbandonHandler } = require('./abandon-handler');
55
55
  const githubPush = require('../github-push');
56
56
  const prAssign = require('../pr-assign');
57
57
  const shipAssurance = require('../ship-assurance');
58
+ const { failGithubStatus } = require('../github-failure');
58
59
  const RECONCILE_PROOF = Object.freeze({ kind: 'proof', reason: 'publish-status reconcile: land proven' });
59
60
  // task 1003764: publish-status with no ?branch= and no PR titled for the task — the shape
60
61
  // getPublishStatus gives a branch with no PR, plus how to ask about one. Never a 400.
@@ -1345,8 +1346,7 @@ module.exports = function buildTasksRouter() {
1345
1346
  } catch (e) {
1346
1347
  if (e.code === 'NO_REPO_INFO') return res.fail('no_repo_info', 500);
1347
1348
  if (e.code === 'APP_NOT_INSTALLED') return res.fail('push_unconfigured', { status: 503, message: appNotInstalledMessage(e) });
1348
- log.error('[gds] GET /tasks/:id/publish-status', e.code || e.message);
1349
- return res.fail('status_failed', 502);
1349
+ return failGithubStatus(res, log, e); // task 1004766: names the step + GitHub's answer
1350
1350
  }
1351
1351
  });
1352
1352
 
@@ -4,6 +4,7 @@
4
4
  "description": "Per-builder session memory, the lexical recall/search retrieval layer (ADR 0060), and durable learnings. A core-domain module carved out of the GDS core in BV1.R72 (ADR 0091); default-on because every instance needs the build system's memory.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.5.0",
7
+ "category": { "parent": "knowledge" },
7
8
  "default": true,
8
9
  "maintenance": {
9
10
  "status": "core-maintained"
@@ -4,6 +4,7 @@
4
4
  "description": "The deploy page for a project that publishes an npm package from its GitHub repo — where every recently shipped task has got to (not merged, merged, in a version, live, released) and each version's list of work, read from the release notes the package itself carries (task 1002622) — and the same line on a task's own page (task 1004301). Built for the Bongos hall, which publishes @bongos/core; off everywhere else.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "hosting" },
7
8
  "default": false,
8
9
  "author": "Cloud Bongos",
9
10
  "origin": "Cloud Bongos core",
@@ -4,6 +4,7 @@
4
4
  "description": "The newcomer ADMISSION + ORIENTATION domain — the public access-request → Archon-approval gate, the server-ticked onboarding state machine (the 'Path into the City' checklist that graduates a Xenos to Thetes), the friction analyzer (feedback-without-asking on consented onboarding transcripts), and the evergreen newcomer-chore restock (a fresh first-builder task is always waiting). A core-domain module carved out of the GDS core in BV1.R81 (ADR 0093 §1); default-on because every instance onboards its own builders. The access-requests admission routes mount here; the core lifecycle (db.js claim/ship), auth callbacks, and the me/builders profile readers resolve the `onboarding` kernel PORT for the state-machine + restock surface they need — never importing the module.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.532",
7
+ "category": { "parent": "people" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "routes": ["onboarding"], "migrations": true },
@@ -4,6 +4,7 @@
4
4
  "description": "An artist's feedback doc on one hall page (task 1004496, goal 1000095 WA6). In Artistry mode the palette button opens a doc for the page you are on: as long as it needs to be, with uploaded reference images (PNG, JPG, GIF, WebP, up to 4 MB, eight per doc). On submit an AI splits it into separate ideas in the idea inbox, each attributed to the artist and linking back to the doc, and the doc stays as the page's design reference, readable from the page (?note=N) and from every idea it became. Owner decisions 2026-10-04: an AI splits it; images only. Kept apart from the copy desk on purpose: that module may hold no wording, and a feedback doc is wording about a page (not of it). default-on, like the copy desk: every instance has pages an artist can comment on.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "migrations": true,
@@ -4,6 +4,7 @@
4
4
  "description": "The five pixel-art SKILLS an instance with a pixel-art world turns on: otb-tile-generate, otb-design-review, otb-character-review, otb-feedback-capture and otb-figma-sync (task 1004470, owner ruling on blocker 1000139). They were orphans in the core's .claude/skills/: docs attributed them to art-pipeline / character-anim, neither of which has a module.json (ADR 0272), and modules/art-pipeline/ never publishes because it carries copyrighted reference maps (ADR 0098). This module carries ONLY the skill text, never a reference map, a palette, a rubric or the generation pipeline — those stay with the instance's own art-pipeline host module, which the skills read at their documented paths. default: false because most instances have no pixel-art world, and every skill description is resident in every session whether it is used or not. Declaration-only: no routes, no migrations, no port, so turning it off withdraws only these skills.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": false,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "skills": ["otb-tile-generate", "otb-design-review", "otb-character-review", "otb-feedback-capture", "otb-figma-sync"] },
@@ -4,6 +4,7 @@
4
4
  "description": "The Cloud Bongos hub identity-provider surface (ADR 0141): platform accounts, an SSO client registry, and an OAuth authorization-code login handoff so one platform account reaches any hosted project's builders hall with no second GitHub sign-in. Identity ONLY — the hub never mints or carries rank/credits/admission (the ADR 0016 trust boundary). Enabled ONLY on the hub instance (cloudbongos.com); ordinary hosted + self-hosted instances leave it off, so it adds zero attack surface for the common case.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.14.0",
7
+ "category": { "parent": "people" },
7
8
  "default": false,
8
9
  "author": "Cloud Bongos",
9
10
  "origin": "Cloud Bongos platform (cloudbongos.com)",
@@ -247,7 +247,57 @@ async function githubSetupRead(db, inst, row, status, { now = Date.now(), withSt
247
247
  };
248
248
  }
249
249
 
250
+ // The standing install nudge (task 1004763). A project made before the wizard's install
251
+ // step existed (task 1004649), or whose founder closed the GitHub tab, has an app nobody
252
+ // installed on its repo, and every ship then ends land-not-proven because the platform
253
+ // cannot read the repo. The checklist above only shows inside the creation wizard, so the
254
+ // project list and the project's own page carry this instead: the install link and the
255
+ // repo, from the latest attempt, until the runner has SEEN the install. null when there is
256
+ // no app of ours (nothing to install) or GitHub has confirmed it. PURE.
257
+ function installNudge(setup) {
258
+ if (!setup || !setup.checklist || setup.checklist.install === 'done' || !setup.install_url) return null;
259
+ return { install_url: setup.install_url, repo: setup.repo || null };
260
+ }
261
+
262
+ // The nudges for a whole project list in TWO reads, never one per project: each project's
263
+ // latest attempt, then which of those GitHub has been seen installed on since that attempt
264
+ // began. Only the install fact matters here: with an app of ours recorded, the checklist's
265
+ // install step is 'done' exactly when the runner saw it (githubChecklist), so the full
266
+ // read is not needed. Returns a Map of instance id → nudge or null. A switched-off project
267
+ // is silent; a failed read is no nudge for anyone, never a failed list.
268
+ async function installNudgesFor(db, insts) {
269
+ const out = new Map();
270
+ const live = (insts || []).filter((i) => i && i.id != null && i.status !== 'torn_down' && i.status !== 'tearing_down');
271
+ for (const i of insts || []) if (i && i.id != null) out.set(String(i.id), null);
272
+ if (!live.length) return out;
273
+ try {
274
+ const { rows: attempts } = await db.query(
275
+ `SELECT DISTINCT ON (instance_id) instance_id, app_slug, created_at
276
+ FROM provisioning_oauth_manifests
277
+ WHERE instance_id = ANY($1::bigint[])
278
+ ORDER BY instance_id, created_at DESC`,
279
+ [live.map((i) => i.id)]);
280
+ const withApp = attempts.filter((a) => a.app_slug);
281
+ if (!withApp.length) return out;
282
+ const { rows: seen } = await db.query(
283
+ `SELECT DISTINCT e.instance_id
284
+ FROM provisioning_events e
285
+ JOIN unnest($1::bigint[], $2::timestamptz[]) AS s(id, since)
286
+ ON e.instance_id = s.id AND e.created_at >= s.since
287
+ WHERE e.event = $3`,
288
+ [withApp.map((a) => a.instance_id), withApp.map((a) => new Date(a.created_at).toISOString()), SETUP_EVENTS.installed]);
289
+ const installed = new Set(seen.map((r) => String(r.instance_id)));
290
+ const byId = new Map(live.map((i) => [String(i.id), i]));
291
+ for (const a of withApp) {
292
+ const id = String(a.instance_id);
293
+ if (installed.has(id) || !byId.has(id)) continue;
294
+ out.set(id, { install_url: installUrlFor({ appSlug: a.app_slug }), repo: targetRepo(byId.get(id)) });
295
+ }
296
+ } catch (_) { for (const k of out.keys()) out.set(k, null); }
297
+ return out;
298
+ }
299
+
250
300
  module.exports = {
251
301
  SETUP_EVENTS, VERIFY_WINDOW_MS, installUrlFor, targetRepo, githubSetupFacts, githubChecklist, stillVerifying, githubSetupRead,
252
- validGithubLogin, pickUpFor, mintGuard, requeueConversion, recordSignInSeen,
302
+ validGithubLogin, pickUpFor, mintGuard, requeueConversion, recordSignInSeen, installNudge, installNudgesFor,
253
303
  };
@@ -4,6 +4,7 @@
4
4
  "description": "Stand up + operate a Cloud Bongos instance's own infrastructure (server, database, domain, DNS, TLS) via the API, so an owner does not manage infra by hand (ADR 0111). Owns the web-tier REQUEST + STATE surface only: it enqueues provisioning intents + reads instance state — it holds NO cloud-provider tokens and calls NO cloud API. A separate control-plane runner (scripts/gds/provision.js, task P3) is the sole token-holder that drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute WORKLOADS.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.14.0",
7
+ "category": { "parent": "hosting" },
7
8
  "default": false,
8
9
  "author": "Cloud Bongos",
9
10
  "origin": "Cloud Bongos core",
@@ -621,12 +621,15 @@ module.exports = function provisioningRoutes() {
621
621
  router.get('/provisioning/instances', auth.requireBuilder, async (req, res) => {
622
622
  try {
623
623
  const rows = await provisioning.listInstancesForOwner(pool, req.builder.id);
624
+ // the standing 'Install the GitHub App' nudge (task 1004763): two reads for the whole list
625
+ const nudges = await githubSetup.installNudgesFor(pool, rows);
624
626
  const instances = [];
625
627
  for (const row of rows) {
626
628
  const open = await provisioning.getOpenIntent(pool, row.id);
627
629
  instances.push({
628
630
  ...ownerInstance(row),
629
631
  open_intent: open ? { action: open.action, state: open.state } : null,
632
+ github_install: nudges.get(String(row.id)) || null,
630
633
  });
631
634
  }
632
635
  res.json({ instances });
@@ -660,13 +663,15 @@ module.exports = function provisioningRoutes() {
660
663
  // The GitHub-App leg's truth rides the same read (task 1003235), so the done
661
664
  // panel recovers it from server state after a reload — not from sessionStorage.
662
665
  const manifest = await provisioning.getLatestOAuthManifestForInstance(pool, id);
666
+ const githubSetupNow = await githubSetup.githubSetupRead(pool, inst, manifest, manifest ? provisioning.publicGithubApp(manifest).status : null, { withState: false });
663
667
  res.json({
664
- instance: ownerInstance(inst),
668
+ // the standing install nudge rides the instance, as on the list read (task 1004763)
669
+ instance: { ...ownerInstance(inst), github_install: githubSetup.installNudge(githubSetupNow) },
665
670
  open_intent: open ? { action: open.action, state: open.state } : null,
666
671
  github_app: provisioning.publicGithubApp(manifest),
667
672
  // the four-step checklist from what the runner verified (task 1004649) — the
668
673
  // install link rides WITHOUT the state token this read never carries
669
- github_setup: await githubSetup.githubSetupRead(pool, inst, manifest, manifest ? provisioning.publicGithubApp(manifest).status : null, { withState: false }),
674
+ github_setup: githubSetupNow,
670
675
  });
671
676
  } catch (err) {
672
677
  log.error('[provisioning] GET /provisioning/instances/:id', err);
@@ -4,6 +4,7 @@
4
4
  "description": "The instance's public front door served at the APEX ROOT — the landing page (/), the projects hub (/projects, and the /create redirect into its ?view=new wizard), the three footer doors (/privacy, /terms, /contact) and every asset behind them. The third WEB-SURFACE core-domain module after status-ui (BV1.R84) and hall-ui (BV1.R85), carved in task 1003322 (ADR 0218; named in ADR 0197 §8 — the front door keeps its own key, because a customer of the ui-design capability is not part of it). default-on: an instance ships its front door like it ships its hall. Registers no port and contributes no routes — it declares a webSurface, and where hall-ui and status-ui name a Host PREFIX this one declares apexRoot:true, since the apex is whatever host is left once the subdomains are taken (ADR 0218 §2; host:\"\" is rejected, because every host string startsWith \"\"). Like the hall, the declaration is the discovery + scope-map + window.__MODULES__ record: the bytes are served by platform-server.js's dedicated landing block, which keeps the apex-only sentinel strip, the branding-token stamp and the ?v= content stamp and only re-points its root at this module's public/ dir. Disabling the module removes the front door outright — the apex then answers the game-less 404 tail. The world it renders (DESIGN.md, the token contract, the direction records) is the INSTANCE's; the method for building one stays in modules/ui-design/.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": {