@bongos/core 1.19.1081 → 1.20.1

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 (251) hide show
  1. package/.bongos-core.json +526 -191
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +32 -0
  5. package/clients/bongos-client/index.cjs +32 -0
  6. package/clients/bongos-client/index.d.ts +51 -6
  7. package/clients/bongos-client/index.mjs +32 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  14. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  15. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +851 -19
  18. package/docs/api-reference.md +33 -9
  19. package/docs/architecture.md +6 -2
  20. package/docs/copy-inventory.md +620 -553
  21. package/docs/copy-registry.json +1467 -820
  22. package/docs/file-map.md +2 -0
  23. package/docs/module-api-changelog.md +6 -0
  24. package/docs/modules-contract.md +1 -0
  25. package/docs/page-inventory.json +38 -4
  26. package/docs/page-readings.json +1674 -1566
  27. package/migrations/core_260_goals_working_area.sql +63 -0
  28. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  29. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  30. package/modules/agents/lib/validate.js +43 -0
  31. package/modules/autonomy/gauge.js +38 -2
  32. package/modules/grading/grader-subagent.js +37 -2
  33. package/modules/grading/grader-workers/reader.js +65 -5
  34. package/modules/hall-ui/public/approval-queue.css +33 -7
  35. package/modules/hall-ui/public/approval-queue.js +9 -2
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/brand-holes.js +121 -0
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/collab.js +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +6 -1
  44. package/modules/hall-ui/public/deploy.js +22 -8
  45. package/modules/hall-ui/public/diagrams.html +1 -1
  46. package/modules/hall-ui/public/dom-utils.js +20 -0
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.html +1 -1
  50. package/modules/hall-ui/public/goals.html +1 -1
  51. package/modules/hall-ui/public/government.html +1 -1
  52. package/modules/hall-ui/public/idea.html +1 -1
  53. package/modules/hall-ui/public/idea.js +28 -0
  54. package/modules/hall-ui/public/ideas.html +1 -1
  55. package/modules/hall-ui/public/ideas.js +19 -3
  56. package/modules/hall-ui/public/index.html +5 -1
  57. package/modules/hall-ui/public/modules.html +1 -1
  58. package/modules/hall-ui/public/primer.html +1 -1
  59. package/modules/hall-ui/public/profile-nudge.js +21 -4
  60. package/modules/hall-ui/public/profile.html +1 -1
  61. package/modules/hall-ui/public/profile.js +1 -1
  62. package/modules/hall-ui/public/project-settings.html +1 -1
  63. package/modules/hall-ui/public/ranks.html +1 -1
  64. package/modules/hall-ui/public/roadmap.html +1 -1
  65. package/modules/hall-ui/public/roster.html +1 -1
  66. package/modules/hall-ui/public/sessions.html +1 -1
  67. package/modules/hall-ui/public/settings.html +19 -30
  68. package/modules/hall-ui/public/settings.js +24 -177
  69. package/modules/hall-ui/public/settings.states.json +1 -1
  70. package/modules/hall-ui/public/shell.js +4 -2
  71. package/modules/hall-ui/public/studio.css +14 -8
  72. package/modules/hall-ui/public/studio.html +5 -4
  73. package/modules/hall-ui/public/task.html +1 -1
  74. package/modules/hall-ui/public/thinking.css +6 -3
  75. package/modules/hall-ui/public/thinking.html +1 -1
  76. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  77. package/modules/hall-ui/public/tweak-editor.css +142 -11
  78. package/modules/hall-ui/public/tweak-editor.html +25 -9
  79. package/modules/hall-ui/public/tweak-editor.js +243 -10
  80. package/modules/hall-ui/public/watch.html +1 -1
  81. package/modules/hall-ui/public/work.html +1 -1
  82. package/modules/hall-ui/records/approval-queue.md +6 -0
  83. package/modules/hall-ui/records/tweak-editor.md +6 -0
  84. package/modules/ideas/ratify.js +36 -8
  85. package/modules/ideas/routes/ratify-goal.js +6 -0
  86. package/modules/lifecycle/db-analytics.js +79 -10
  87. package/modules/lifecycle/db-claims.js +48 -1
  88. package/modules/lifecycle/db-deps-criteria.js +94 -40
  89. package/modules/lifecycle/db-goals.js +4 -1
  90. package/modules/lifecycle/db-grade.js +9 -1
  91. package/modules/lifecycle/db-relevance-flags.js +183 -0
  92. package/modules/lifecycle/db-versions.js +4 -3
  93. package/modules/lifecycle/db.js +19 -0
  94. package/modules/lifecycle/done-when.js +5 -3
  95. package/modules/lifecycle/est-advisory.js +31 -4
  96. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  97. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  98. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  99. package/modules/lifecycle/module.json +2 -1
  100. package/modules/lifecycle/routes/claims.js +8 -1
  101. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  102. package/modules/lifecycle/routes/goals.js +1 -1
  103. package/modules/lifecycle/routes/tasks.js +27 -24
  104. package/modules/lifecycle/routes/visuals.js +9 -0
  105. package/modules/lifecycle/ship-preflight.js +7 -11
  106. package/modules/lifecycle/task-visuals.js +109 -5
  107. package/modules/npm-release/module.json +2 -1
  108. package/modules/npm-release/preview/commands.js +81 -0
  109. package/modules/npm-release/preview/divert.js +95 -0
  110. package/modules/npm-release/preview/env.js +78 -0
  111. package/modules/npm-release/preview/proxy.js +107 -0
  112. package/modules/npm-release/preview/runtime.js +83 -0
  113. package/modules/npm-release/preview/supervisor.js +175 -0
  114. package/modules/npm-release/public/work.js +65 -1
  115. package/modules/npm-release/routes/preview.js +133 -0
  116. package/modules/provisioning/app-status.js +83 -0
  117. package/modules/provisioning/module.json +2 -1
  118. package/modules/provisioning/pollers/app-liveness.js +110 -0
  119. package/modules/provisioning/render-lookup.js +68 -0
  120. package/modules/provisioning/render-standup.js +44 -1
  121. package/modules/provisioning/routes/render-standup.js +81 -6
  122. package/modules/provisioning/starter-bundles.js +4 -1
  123. package/modules/public-landing/public/account-private.states.json +14 -0
  124. package/modules/public-landing/public/account.html +513 -0
  125. package/modules/public-landing/public/account.probes.json +37 -0
  126. package/modules/public-landing/public/account.states.json +16 -0
  127. package/modules/public-landing/public/index.html +5 -2
  128. package/modules/public-landing/public/projects.html +661 -18
  129. package/modules/public-landing/public/projects.states.json +8 -1
  130. package/modules/render-deploy/deploys.js +203 -0
  131. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  132. package/modules/render-deploy/module.json +25 -0
  133. package/modules/render-deploy/public/deploy.css +10 -0
  134. package/modules/render-deploy/public/deploy.js +249 -0
  135. package/modules/render-deploy/render.js +56 -0
  136. package/modules/render-deploy/routes/act.js +88 -0
  137. package/modules/render-deploy/routes/app.js +65 -0
  138. package/modules/render-deploy/routes/history.js +38 -0
  139. package/modules/specialities/db.js +27 -8
  140. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  141. package/modules/specialities/routes/specialities.js +51 -3
  142. package/modules/specialities/skills.js +94 -0
  143. package/modules/specialities/specialities.js +56 -9
  144. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  145. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  146. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  147. package/modules/ui-design/kit/lib.js +3 -1
  148. package/modules/ui-design/kit/serve.js +96 -0
  149. package/package-lock.json +2 -2
  150. package/package.json +1 -1
  151. package/release-notes.json +163 -0
  152. package/scripts/gds/agents-sync.js +13 -3
  153. package/scripts/gds/autobongos-run.js +80 -2
  154. package/scripts/gds/autobongos-service.cmd +12 -0
  155. package/scripts/gds/copy-apply.js +14 -0
  156. package/scripts/gds/dev-box-guard.js +3 -2
  157. package/scripts/gds/fitness.js +9 -0
  158. package/scripts/gds/grade-correlation-audit.js +42 -12
  159. package/scripts/gds/grade-replay.js +1 -1
  160. package/scripts/gds/provision-core-upgrade.js +38 -7
  161. package/scripts/gds/provision-pin-key.js +176 -0
  162. package/scripts/gds/provision-render.js +62 -31
  163. package/scripts/gds/provision-teardown.js +7 -2
  164. package/scripts/gds/provision-units.js +31 -0
  165. package/scripts/gds/provision.js +10 -10
  166. package/scripts/gds/publish-manifest.js +1 -0
  167. package/scripts/gds/render-api.js +19 -0
  168. package/scripts/gds/render-payload.js +39 -65
  169. package/scripts/gds/run-unit-tests.js +10 -0
  170. package/scripts/gds/ship-finish.js +19 -20
  171. package/scripts/gds/smoke-dependencies.sh +37 -8
  172. package/scripts/gds/status.js +12 -4
  173. package/scripts/gds/upgrade.js +2 -2
  174. package/scripts/public-mirror-export.js +7 -1
  175. package/src/bongos/module-scope-map.js +13 -0
  176. package/src/bongos/serve-internal.js +3 -1
  177. package/src/module-api.js +10 -1
  178. package/src/platform-server.js +39 -0
  179. package/tests/account_privacy_flags.mjs +10 -6
  180. package/tests/agents_authoring.mjs +15 -1
  181. package/tests/agents_sync.mjs +106 -5
  182. package/tests/agents_validate.mjs +45 -0
  183. package/tests/api_path_404.mjs +6 -0
  184. package/tests/autobongos_cadence.mjs +7 -0
  185. package/tests/autobongos_loop.mjs +136 -1
  186. package/tests/autonomy_gauge.mjs +62 -0
  187. package/tests/blocker_hall_live_proof.mjs +275 -0
  188. package/tests/claim_gate_ci_unblock.mjs +116 -0
  189. package/tests/claim_gate_rebase.mjs +15 -3
  190. package/tests/collab_page.mjs +10 -0
  191. package/tests/conductor_main_e2e.mjs +97 -0
  192. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  193. package/tests/core_upgrade_runner.mjs +1 -0
  194. package/tests/dependency_writes_atomic.mjs +191 -0
  195. package/tests/deploy_page_projects.mjs +2 -1
  196. package/tests/effective_visibility_predicate.mjs +15 -0
  197. package/tests/est_advisory.mjs +27 -0
  198. package/tests/goal_map_page.mjs +26 -0
  199. package/tests/goal_task_relevance.mjs +518 -0
  200. package/tests/goal_working_area.mjs +146 -0
  201. package/tests/grade_attempts.mjs +61 -0
  202. package/tests/grade_attribution.mjs +23 -0
  203. package/tests/grade_correlation_audit.mjs +54 -1
  204. package/tests/grader_reader_lens.mjs +55 -2
  205. package/tests/grader_root_outage.mjs +6 -1
  206. package/tests/grader_subagent_tools_arg.mjs +102 -0
  207. package/tests/hall_approval_queue.mjs +54 -2
  208. package/tests/hall_settings_world.mjs +3 -1
  209. package/tests/hall_tweak_editor.mjs +316 -11
  210. package/tests/hub_account_page.mjs +394 -0
  211. package/tests/idea_detail_page.mjs +64 -0
  212. package/tests/idea_goal_ratification.mjs +64 -0
  213. package/tests/idea_spark_hall.mjs +60 -9
  214. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  215. package/tests/landing_page.mjs +6 -3
  216. package/tests/module-scope-map.mjs +31 -8
  217. package/tests/module_api.mjs +1 -0
  218. package/tests/module_loader.mjs +1 -1
  219. package/tests/nav_permission_atoms.mjs +1 -1
  220. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  221. package/tests/npm_release_preview_commands.mjs +89 -0
  222. package/tests/npm_release_preview_divert.mjs +164 -0
  223. package/tests/npm_release_preview_env.mjs +100 -0
  224. package/tests/npm_release_preview_proxy.mjs +162 -0
  225. package/tests/npm_release_preview_routes.mjs +208 -0
  226. package/tests/npm_release_preview_supervisor.mjs +203 -0
  227. package/tests/pin_write_key.mjs +383 -0
  228. package/tests/planning_session_skill.mjs +27 -0
  229. package/tests/platform_boot.mjs +71 -0
  230. package/tests/profile_nudge_links.mjs +86 -0
  231. package/tests/profile_ui_cross_project.mjs +1 -1
  232. package/tests/projects_hub.mjs +2 -2
  233. package/tests/projects_hub_app_status.mjs +249 -0
  234. package/tests/projects_hub_app_step.mjs +282 -52
  235. package/tests/projects_hub_render_connect.mjs +295 -0
  236. package/tests/provision_render.mjs +121 -28
  237. package/tests/provision_settings_apply.mjs +62 -0
  238. package/tests/provisioning_app_status.mjs +204 -0
  239. package/tests/provisioning_render_route.mjs +126 -6
  240. package/tests/render_check.mjs +2 -1
  241. package/tests/render_deploy.mjs +495 -0
  242. package/tests/ship_preflight.mjs +61 -8
  243. package/tests/smoke_dependencies_witness.mjs +46 -0
  244. package/tests/speciality_skills.mjs +214 -0
  245. package/tests/studio_room_height.mjs +70 -0
  246. package/tests/task_detail_includes.mjs +10 -0
  247. package/tests/task_visuals_instance_root.mjs +150 -0
  248. package/tests/tweak_batch_apply.mjs +21 -0
  249. package/tests/ui_design_kit.mjs +36 -4
  250. package/tests/upgrade.mjs +1 -1
  251. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -148,7 +148,7 @@ function answer(sql, params) {
148
148
  description: description ?? null, scope_modules: scopeModules || [], status,
149
149
  visibility: 'public', accepting_requests: true, created_by: createdBy ?? null,
150
150
  succeeded_by_goal_id: null, sort_order: order, category_id: categoryId ?? null,
151
- is_maintenance: false, created_at: 'T1', updated_at: 'T1',
151
+ is_maintenance: false, working_area: null, created_at: 'T1', updated_at: 'T1',
152
152
  };
153
153
  db.goals.set(goal.id, goal);
154
154
  // The two CTE side-writes, modelled because they are what makes `created_by`
@@ -564,14 +564,17 @@ test('the Projects band deep-links the hub\'s views; About and Docs are footer l
564
564
  assert.match(body, /<a href="#about">About<\/a>\s*<a href="\/docs">Docs<\/a>\s*<a href="\/privacy">Privacy<\/a>/, 'About and Docs open the footer row');
565
565
  assert.match(body, /<section class="band" id="about">/, 'About has a real anchor: the lifecycle band');
566
566
  assert.doesNotMatch(body.split('<main')[0], /aria-current/, 'the landing is none of those views');
567
- // the profile band: the person, My projects, the deferred slot, Sign out as the honest anchor
567
+ // the profile band: the person, My projects, Account, Sign out as the honest anchor.
568
+ // Account fills the slot that waited for a settings page (task 1004259) and is
569
+ // apex-only, because /account is served only where platform-identity runs.
568
570
  assert.match(body, /<div class="menuBand" id="menuProfile" data-menu="profile" hidden>/);
569
571
  assert.match(body, /<p class="bandWho" id="bandWho"><\/p>/);
570
572
  assert.match(body, /<a class="bandT" href="\/projects\?view=mine">My projects<\/a>/);
571
- assert.match(body, /<li class="bandSlot" aria-hidden="true"><\/li>/);
573
+ assert.match(body, /<!--apex-only:start--><li class="bandCell"><a class="bandT" href="\/account">Account<\/a><\/li><!--apex-only:end-->/);
574
+ assert.doesNotMatch(body, /class="bandSlot"/, 'the slot is filled, not kept beside its item');
572
575
  assert.match(body, /<a class="bandT signout" href="\/api\/bongos\/auth\/logout">Sign out<\/a>/);
573
576
  // a crawler reads every destination without scripting on
574
- for (const copy of ['Sky', 'Table', 'Your projects', 'New project', 'About', 'Docs', 'My projects', 'Sign out']) {
577
+ for (const copy of ['Sky', 'Table', 'Your projects', 'New project', 'About', 'Docs', 'My projects', 'Account', 'Sign out']) {
575
578
  assert.ok(body.includes('>' + copy + '<'), `"${copy}" is only reachable with scripting on`);
576
579
  }
577
580
  // the menu container is .menuBand: .band is this page's own content sections
@@ -591,14 +591,11 @@ function ownersOf(p) {
591
591
  return scope.moduleKeys().filter((k) => matchOne(p, scope.scopeForModule(k).globs) !== null);
592
592
  }
593
593
 
594
- // The one PROTECTED_GLOBS entry still owned by nobody, exempted BY NAME so the
595
- // tripwire stays true rather than being weakened to accommodate it.
596
- // src/bongos/routes/ is a blanket directory: protection covers all 18 files under
597
- // it, authoring covers 2 (me.js -> builder-settings, security.js -> kernel).
598
- // Closing it means deciding who owns the other sixteen live route files, which is
599
- // an ownership call and not a missing glob — filed as task 1004116. Removing this
600
- // entry is that task's first step.
601
- const UNOWNED_EXEMPT = ['src/bongos/routes/'];
594
+ // PROTECTED_GLOBS entries still owned by nobody, exempted BY NAME so the
595
+ // tripwire stays true rather than being weakened to accommodate one. EMPTY since
596
+ // task 1004116 gave src/bongos/routes/ to kernel; keep it empty — a new entry
597
+ // here is a new hole, and the test below refuses one that has stopped applying.
598
+ const UNOWNED_EXEMPT = [];
602
599
 
603
600
  test('every PROTECTED_GLOBS entry is owned by at least one module', () => {
604
601
  const unowned = PROTECTED_GLOBS
@@ -610,6 +607,32 @@ test('every PROTECTED_GLOBS entry is owned by at least one module', () => {
610
607
  );
611
608
  });
612
609
 
610
+ // task 1004116: the entry-level tripwire above passes on a blanket DIRECTORY as
611
+ // soon as some module owns the directory — but it said nothing about the FILES
612
+ // inside one, which is exactly how sixteen route files sat under a protected
613
+ // prefix that no goal could author. So walk every file under every blanket
614
+ // (trailing-'/') protected glob and ask the wall's own question of each: does
615
+ // some module's territory cover THIS path? matchOne takes an ARRAY (the probe
616
+ // trap pinned below).
617
+ test('every FILE under a blanket protected directory has an owning module (task 1004116)', () => {
618
+ const walk = (rel) => {
619
+ const abs = path.join(ROOT, rel);
620
+ if (!fs.existsSync(abs)) return [];
621
+ return fs.readdirSync(abs, { withFileTypes: true }).flatMap((d) =>
622
+ d.isDirectory() ? walk(rel + d.name + '/') : [rel + d.name]);
623
+ };
624
+ const blankets = PROTECTED_GLOBS.filter((g) => g.endsWith('/'));
625
+ const files = blankets.flatMap(walk);
626
+ assert.ok(files.length > 0, 'no files found under any blanket protected glob — the walk is broken, not clean');
627
+ assert.ok(files.includes('src/bongos/routes/public.js'), 'the walk must reach the route directory');
628
+ const unowned = files.filter((p) => ownersOf(p).length === 0);
629
+ assert.deepEqual(unowned, [], 'these protected files are authorable by NO goal');
630
+ for (const f of ['audit-log.js', 'auth.js', 'core-update.js', 'public.js', '_helpers.js', 'CLAUDE.md']) {
631
+ const p = 'src/bongos/routes/' + f;
632
+ assert.equal(gsc.scopeSubset(['kernel'], [p]).ok, true, p + ' must be authorable by a kernel-scoped goal');
633
+ }
634
+ });
635
+
613
636
  test('the exemption list is not a place to hide a new hole', () => {
614
637
  // An exemption that silently stops applying is worse than none: it would sit
615
638
  // in the file implying a known gap that no longer exists, and the next unowned
@@ -35,6 +35,7 @@ const PUBLISHED_SURFACE = [
35
35
  'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
36
36
  'repoInfo', 'moduleScopeMap', 'pathMatch', // BV1.R86: kernel repo-identity + scope-wall + touches-overlap for the carved lifecycle (1.12.0)
37
37
  'goalScopeCheck', // BV1.R60: kernel scope wall for the goal-authored-task create gate (1.13.0; ADR 0086 §3)
38
+ 'cascadeDispatch', // task 1001691 / ADR 0101 Part 1: cheapest-model-first cascade router (1.20.0; the lifecycle relevance judge's first real caller)
38
39
  'insertAuditLog', 'LIVE_RANK_LADDER', // createTask + VOTABLE_TASK_STATUSES + tallyPeerVotes RETIRED at 1.12.0 (BV1.R86 → lifecycle port); captureIdea retired at 1.8.0 (BV1.R78 → ideas.capture port)
39
40
  'RANK_TIER', // BV1.R108: the canonical 1-based rank→tier map, derived from LIVE_RANK_LADDER (tests/rank_tier_single_source.mjs)
40
41
  'llmCache', // BV1.R73: shared LLM-cost cache (1.6.0; tracked domain leak)
@@ -276,7 +276,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
276
276
  // That is precisely how the agents regression reached an open PR (task 1002485).
277
277
  loader._reset();
278
278
  const snap = loader.loaderSnapshot();
279
- assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'platform-identity', 'provisioning', 'public-landing', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
279
+ assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'platform-identity', 'provisioning', 'public-landing', 'render-deploy', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
280
280
  assert.deepEqual(snap.errors, []);
281
281
  assert.deepEqual(snap.skipped, []);
282
282
  loader._reset();
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '0c646e5bac9336e8';
90
+ const SHELL_SHA = 'dd33737d54dccbb4';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -0,0 +1,24 @@
1
+ // tests/no_phantom_mirror_workflow.mjs — task 1003405.
2
+ // No tracked file may point at a sync-public-mirror workflow that does not exist, unless it
3
+ // says so plainly ("never built" / "no sync-public-mirror workflow"). Publishing the public
4
+ // mirror is a manual step (node scripts/public-mirror-export.js ... --push).
5
+ import { strict as assert } from 'node:assert';
6
+ import { execFileSync } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ if (!fs.existsSync(path.join(root, '.github/workflows/sync-public-mirror.yml'))) {
13
+ const files = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 26 }).split('\n').filter(Boolean);
14
+ const bad = [];
15
+ for (const f of files) {
16
+ if (f === 'tests/no_phantom_mirror_workflow.mjs') continue;
17
+ const p = path.join(root, f);
18
+ if (!fs.existsSync(p) || fs.statSync(p).size > 2e6) continue;
19
+ const t = fs.readFileSync(p, 'utf8');
20
+ if (t.includes('sync-public-mirror') && !/never built|no sync-public-mirror workflow/.test(t)) bad.push(f);
21
+ }
22
+ assert.deepEqual(bad, [], 'these files reference a sync-public-mirror workflow that does not exist: ' + bad.join(', '));
23
+ }
24
+ console.log('ok - no phantom sync-public-mirror workflow reference');
@@ -0,0 +1,89 @@
1
+ // tests/npm_release_preview_commands.mjs — the commands a preview start runs (task 1004300).
2
+ //
3
+ // WHAT IS AT RISK. Two things go wrong quietly here. (1) A template copy of the database
4
+ // (createdb -T) needs the source to have no other connections; the live hall holds a pool
5
+ // open, so it would fail, or evict the live pool. The copy must be a dump and a restore,
6
+ // and NO command may ever carry -T / --template. (2) A version string that reaches a command
7
+ // line unchecked. Commands are data, so both are asserted on the data, spawning nothing.
8
+ //
9
+ // Run: node --test tests/npm_release_preview_commands.mjs
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import { createRequire } from 'node:module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const c = require('../modules/npm-release/preview/commands.js');
16
+ const { PREVIEW_DB } = require('../modules/npm-release/preview/env.js');
17
+
18
+ // Paths are compared with forward slashes so the file passes on a Windows checkout too.
19
+ const fwd = (x) => String(x).split(String.fromCharCode(92)).join('/');
20
+ const CACHE = '/home/hall/.cache/bongos-preview';
21
+ const V = '1.19.1000';
22
+
23
+ function everyCommand() {
24
+ return [
25
+ c.installCommand({ cacheRoot: CACHE, version: V }),
26
+ c.dropDbCommand(),
27
+ c.createDbCommand(),
28
+ c.dumpCommand({ liveDb: 'live_db', dumpFile: '/tmp/x.dump' }),
29
+ c.restoreCommand({ dumpFile: '/tmp/x.dump' }),
30
+ c.migrateCommand({ cacheRoot: CACHE, version: V, env: { PATH: '/bin' } }),
31
+ ];
32
+ }
33
+
34
+ test('no command ever copies the database from a template (-T / --template)', () => {
35
+ for (const cmd of everyCommand()) {
36
+ for (const a of cmd.args) {
37
+ assert.notEqual(a, '-T', `${cmd.cmd} carries -T`);
38
+ assert.doesNotMatch(a, /^--template/, `${cmd.cmd} carries --template`);
39
+ }
40
+ assert.doesNotMatch(cmd.args.join(' '), /(^|\s)-T(\s|$)/);
41
+ }
42
+ });
43
+
44
+ test('the copy is a custom-format dump of the live database, restored with no owner', () => {
45
+ const dump = c.dumpCommand({ liveDb: 'live_db', dumpFile: '/tmp/x.dump' });
46
+ assert.equal(dump.cmd, 'pg_dump');
47
+ assert.deepEqual(dump.args, ['-Fc', '-f', '/tmp/x.dump', 'live_db']);
48
+ const restore = c.restoreCommand({ dumpFile: '/tmp/x.dump' });
49
+ assert.equal(restore.cmd, 'pg_restore');
50
+ assert.deepEqual(restore.args, ['--no-owner', '-d', PREVIEW_DB, '/tmp/x.dump']);
51
+ });
52
+
53
+ test('the copy is created empty and dropped if it is already there', () => {
54
+ assert.deepEqual(c.createDbCommand(), { cmd: 'createdb', args: [PREVIEW_DB] });
55
+ assert.deepEqual(c.dropDbCommand(), { cmd: 'dropdb', args: ['--if-exists', PREVIEW_DB] });
56
+ });
57
+
58
+ test('the install is one version of the core into its own directory, production dependencies only', () => {
59
+ const i = c.installCommand({ cacheRoot: CACHE, version: V });
60
+ assert.equal(i.cmd, 'npm');
61
+ assert.deepEqual(i.args.map(fwd), ['install', '--prefix', `${CACHE}/${V}`, `@bongos/core@${V}`, '--omit=dev']);
62
+ });
63
+
64
+ test('migrations run the installed version against the copy, with the live pool and URL out of reach', () => {
65
+ const m = c.migrateCommand({ cacheRoot: CACHE, version: V, env: { PATH: '/bin', PGDATABASE: 'live_db', DATABASE_URL: 'postgres://live' } });
66
+ assert.equal(m.cmd, 'bash');
67
+ assert.match(fwd(m.args[0]), new RegExp(`${V}/node_modules/@bongos/core/scripts/migrate\.sh$`));
68
+ assert.equal(fwd(m.env.INIT_CWD), `${CACHE}/${V}`);
69
+ assert.equal(m.env.PGDATABASE, PREVIEW_DB);
70
+ assert.equal(m.env.DATABASE_URL, '');
71
+ assert.equal(fwd(m.cwd), `${CACHE}/${V}`);
72
+ });
73
+
74
+ test('the preview process is the installed core server entry', () => {
75
+ assert.equal(fwd(c.entryPath(CACHE, V)), `${CACHE}/${V}/node_modules/@bongos/core/src/platform-server.js`);
76
+ });
77
+
78
+ test('a version that is not three plain numbers never reaches a command', () => {
79
+ for (const bad of ['1.19', '1.19.x', '1.19.1000; rm -rf /', '../../etc', '1.19.1000-beta', '', '$(id)', '1.19.1000\n']) {
80
+ assert.throws(() => c.installCommand({ cacheRoot: CACHE, version: bad }), /not a version/, JSON.stringify(bad));
81
+ assert.throws(() => c.migrateCommand({ cacheRoot: CACHE, version: bad, env: {} }), /not a version/);
82
+ assert.throws(() => c.entryPath(CACHE, bad), /not a version/);
83
+ }
84
+ });
85
+
86
+ test('the preview database can never be the source of its own copy', () => {
87
+ assert.throws(() => c.dumpCommand({ liveDb: PREVIEW_DB, dumpFile: '/tmp/x' }), /preview database itself/);
88
+ assert.throws(() => c.dumpCommand({ liveDb: '', dumpFile: '/tmp/x' }), /live database name/);
89
+ });
@@ -0,0 +1,164 @@
1
+ // tests/npm_release_preview_divert.mjs — which requests the version preview takes (task 1004300).
2
+ //
3
+ // WHAT IS AT RISK. The provider decides, on every request to the whole hall, whether the
4
+ // live hall or a copy answers. Get it wrong one way and a person keeps working in a preview
5
+ // they thought they had left, or a CLI call writes to a copy and is lost; the other way and
6
+ // somebody without the deploy permission is shown production data through a preview. Every
7
+ // rule is a case here, and the gates are the kernel's own shape (express-style, refusing
8
+ // through res.fail), stubbed with no database.
9
+ //
10
+ // Run: node --test tests/npm_release_preview_divert.mjs
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { createDivert, cookieVersion, clearCookie } = require('../modules/npm-release/preview/divert.js');
17
+
18
+ const RUNNING = { state: 'running', version: '1.19.1000', port: 3190 };
19
+
20
+ // A builder table the stub gates read: cookie sid -> { id, perms }.
21
+ function rig({ status = RUNNING, builders = { good: { id: 7, perms: ['core.pin.move'] } }, throwIn } = {}) {
22
+ const out = { forwarded: 0, touched: 0, asked: [], gateRuns: 0 };
23
+ const supervisor = { status: () => status, touch: () => { out.touched += 1; } };
24
+ const requireBuilder = async (req, res, next) => {
25
+ out.gateRuns += 1;
26
+ if (throwIn === 'builder') throw new Error('db down');
27
+ const sid = /sid=(\w+)/.exec(req.headers.cookie || '');
28
+ const b = sid && builders[sid[1]];
29
+ if (!b) return res.fail('unauthenticated', 401);
30
+ req.builder = b;
31
+ return next();
32
+ };
33
+ const requirePermission = (...atoms) => {
34
+ out.asked.push(atoms);
35
+ return async (req, res, next) => {
36
+ if (throwIn === 'permission') throw new Error('resolver down');
37
+ return atoms.every((a) => req.builder.perms.includes(a)) ? next() : res.fail('permission_forbidden', 403);
38
+ };
39
+ };
40
+ const divert = createDivert({ supervisor, forward: () => { out.forwarded += 1; }, requireBuilder, requirePermission });
41
+ return { divert, out, builders };
42
+ }
43
+
44
+ function req(over = {}) {
45
+ return { originalUrl: '/builders', headers: { cookie: 'sid=good; bongos_preview=1.19.1000', ...(over.headers || {}) }, ...over };
46
+ }
47
+
48
+ async function run(divert, r) {
49
+ const set = [];
50
+ let passed = 0;
51
+ await divert(r, { append: (k, v) => set.push([k, v]) }, () => { passed += 1; });
52
+ return { passed, set };
53
+ }
54
+
55
+ test('the cookie is read only when it names a real version', () => {
56
+ assert.equal(cookieVersion('a=1; bongos_preview=1.19.1000'), '1.19.1000');
57
+ assert.equal(cookieVersion('bongos_preview=latest'), null);
58
+ assert.equal(cookieVersion('bongos_preview=1.19.1000x'), null);
59
+ assert.equal(cookieVersion(''), null);
60
+ assert.equal(cookieVersion(undefined), null);
61
+ });
62
+
63
+ test('a signed-in holder of core.pin.move with the cookie is forwarded, and the preview is kept alive', async () => {
64
+ const { divert, out } = rig();
65
+ const r = await run(divert, req());
66
+ assert.equal(out.forwarded, 1);
67
+ assert.equal(r.passed, 0);
68
+ assert.equal(out.touched, 1);
69
+ });
70
+
71
+ test('the permission asked for is exactly core.pin.move', () => {
72
+ const { out } = rig();
73
+ assert.deepEqual(out.asked, [['core.pin.move']]);
74
+ });
75
+
76
+ test('no cookie: straight through, and no gate is even run', async () => {
77
+ const { divert, out } = rig();
78
+ const r = await run(divert, req({ headers: { cookie: 'sid=good' } }));
79
+ assert.equal(r.passed, 1);
80
+ assert.equal(out.forwarded, 0);
81
+ assert.equal(out.gateRuns, 0);
82
+ });
83
+
84
+ test('forwarding is refused without the permission, and the cookie is cleared', async () => {
85
+ const { divert, out } = rig({ builders: { good: { id: 7, perms: ['some.other'] } } });
86
+ const r = await run(divert, req());
87
+ assert.equal(out.forwarded, 0);
88
+ assert.equal(r.passed, 1);
89
+ assert.deepEqual(r.set, [['Set-Cookie', clearCookie()]]);
90
+ });
91
+
92
+ test('forwarding is refused when nobody is signed in', async () => {
93
+ const { divert, out } = rig();
94
+ const r = await run(divert, req({ headers: { cookie: 'bongos_preview=1.19.1000' } }));
95
+ assert.equal(out.forwarded, 0);
96
+ assert.equal(r.passed, 1);
97
+ });
98
+
99
+ test('the permission is re-checked on every request: revoked between two clicks, the second is not forwarded', async () => {
100
+ const { divert, out, builders } = rig();
101
+ await run(divert, req());
102
+ assert.equal(out.forwarded, 1);
103
+ builders.good.perms = [];
104
+ const r = await run(divert, req());
105
+ assert.equal(out.forwarded, 1, 'still one');
106
+ assert.equal(r.passed, 1);
107
+ });
108
+
109
+ test('a gate that throws forwards nothing (fail closed)', async () => {
110
+ for (const throwIn of ['builder', 'permission']) {
111
+ const { divert, out } = rig({ throwIn });
112
+ const r = await run(divert, req());
113
+ assert.equal(out.forwarded, 0, throwIn);
114
+ assert.equal(r.passed, 1, throwIn);
115
+ }
116
+ });
117
+
118
+ test('sign-in is never forwarded: /api/bongos/auth/* and the /api/gds alias reach the live hall', async () => {
119
+ for (const url of ['/api/bongos/auth/web/start', '/api/bongos/auth/callback?code=1', '/api/gds/auth/web/start', '/api/bongos/auth', '/api/bongos/v1/auth/web/start', '/api/gds/v1/auth/web/start']) {
120
+ const { divert, out } = rig();
121
+ const r = await run(divert, req({ originalUrl: url }));
122
+ assert.equal(out.forwarded, 0, url);
123
+ assert.equal(r.passed, 1, url);
124
+ }
125
+ });
126
+
127
+ test('leaving a preview is never forwarded: the exit route (and every preview route) reaches the live hall', async () => {
128
+ for (const url of ['/api/bongos/npm-release/preview/exit', '/api/bongos/npm-release/preview', '/api/bongos/npm-release/preview/enter?v=1.19.1000', '/api/bongos/v1/npm-release/preview/exit', '/api/bongos/v1/npm-release/preview']) {
129
+ const { divert, out } = rig();
130
+ const r = await run(divert, req({ originalUrl: url }));
131
+ assert.equal(out.forwarded, 0, url);
132
+ assert.equal(r.passed, 1, url);
133
+ }
134
+ });
135
+
136
+ test('a look-alike path is still forwarded: only the exact prefixes are excluded', async () => {
137
+ for (const url of ['/api/bongos/npm-release/previews', '/api/bongos/authors', '/api/bongos/npm-release/work']) {
138
+ const { divert, out } = rig();
139
+ await run(divert, req({ originalUrl: url }));
140
+ assert.equal(out.forwarded, 1, url);
141
+ }
142
+ });
143
+
144
+ test('a CLI or API call (an Authorization header) is never forwarded', async () => {
145
+ const { divert, out } = rig();
146
+ const r = await run(divert, req({ headers: { cookie: 'sid=good; bongos_preview=1.19.1000', authorization: 'Bearer abc' } }));
147
+ assert.equal(out.forwarded, 0);
148
+ assert.equal(r.passed, 1);
149
+ assert.equal(out.gateRuns, 0, 'decided before any gate runs');
150
+ });
151
+
152
+ test('a cookie for a preview that is not running is dropped and the live hall answers', async () => {
153
+ for (const status of [{ state: 'idle', version: null }, { state: 'starting', version: '1.19.1000' }, { state: 'running', version: '1.19.999' }, { state: 'failed', version: '1.19.1000' }]) {
154
+ const { divert, out } = rig({ status });
155
+ const r = await run(divert, req());
156
+ assert.equal(out.forwarded, 0, JSON.stringify(status));
157
+ assert.equal(r.passed, 1);
158
+ assert.deepEqual(r.set, [['Set-Cookie', clearCookie()]]);
159
+ }
160
+ });
161
+
162
+ test('the clearing cookie expires the steering cookie for the whole site', () => {
163
+ assert.match(clearCookie(), /^bongos_preview=; Path=\/; Max-Age=0/);
164
+ });
@@ -0,0 +1,100 @@
1
+ // tests/npm_release_preview_env.mjs — the environment a version preview runs in (task 1004300).
2
+ //
3
+ // WHAT IS AT RISK. The preview is another Bongos process on the live droplet. If its
4
+ // environment is a COPY of ours with a few names removed, the next secret anyone adds to the
5
+ // live environment (a new webhook, a new API key) is silently inherited by a process that
6
+ // runs an old version against a copy of production data. So the property under test is
7
+ // deny-by-default: a name we have never heard of does not get through.
8
+ //
9
+ // Run: node --test tests/npm_release_preview_env.mjs
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import { createRequire } from 'node:module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const { buildPreviewEnv, CARRIED, POLLERS_OFF, MODULES_OFF, PREVIEW_DB } = require('../modules/npm-release/preview/env.js');
16
+
17
+ const LIVE = {
18
+ PATH: '/usr/bin',
19
+ NODE_ENV: 'production',
20
+ PGHOST: '/var/run/postgresql',
21
+ PGUSER: 'hall',
22
+ PORT: '3000',
23
+ HOST: '0.0.0.0',
24
+ HOME: '/home/hall',
25
+ PGDATABASE: 'live_db',
26
+ DATABASE_URL: 'postgres://live/live_db',
27
+ GITHUB_TOKEN: 'ghp_x',
28
+ GITHUB_CLIENT_SECRET: 'cs',
29
+ DISCORD_BOT_TOKEN: 'dt',
30
+ DISCORD_WEBHOOK_URL: 'https://discord.invalid/hook',
31
+ ANTHROPIC_API_KEY: 'sk-ant-x',
32
+ GEMINI_API_KEY: 'g',
33
+ NPM_TOKEN: 'npm_x',
34
+ DO_TOKEN: 'do',
35
+ A_SECRET_ADDED_NEXT_YEAR: 'nobody-listed-me',
36
+ };
37
+
38
+ const build = () => buildPreviewEnv({ env: LIVE, port: 3190, scratchHome: '/tmp/scratch-home' });
39
+
40
+ test('deny by default: only the allow-listed names come across from the live environment', () => {
41
+ const out = build();
42
+ for (const k of ['GITHUB_TOKEN', 'GITHUB_CLIENT_SECRET', 'DISCORD_BOT_TOKEN', 'DISCORD_WEBHOOK_URL',
43
+ 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'NPM_TOKEN', 'DO_TOKEN', 'DATABASE_URL', 'A_SECRET_ADDED_NEXT_YEAR']) {
44
+ assert.equal(k in out, false, `${k} must not reach the preview`);
45
+ }
46
+ // Every name in the output is either carried by the allow-list or one the builder sets itself.
47
+ const own = new Set([...CARRIED, 'PORT', 'HOST', 'HOME', 'PGDATABASE', 'CHAT_DRY_RUN', ...POLLERS_OFF, ...MODULES_OFF]);
48
+ for (const k of Object.keys(out)) assert.ok(own.has(k), `unexpected name ${k}`);
49
+ });
50
+
51
+ test('the returned object is a fresh one, never the live environment', () => {
52
+ const out = build();
53
+ assert.notEqual(out, LIVE);
54
+ out.PATH = 'changed';
55
+ assert.equal(LIVE.PATH, '/usr/bin');
56
+ });
57
+
58
+ test('what a process needs to boot and reach Postgres is carried', () => {
59
+ const out = build();
60
+ assert.equal(out.PATH, '/usr/bin');
61
+ assert.equal(out.NODE_ENV, 'production');
62
+ assert.equal(out.PGHOST, '/var/run/postgresql');
63
+ assert.equal(out.PGUSER, 'hall');
64
+ });
65
+
66
+ test('it binds its own loopback port and points at the copy, never the live database', () => {
67
+ const out = build();
68
+ assert.equal(out.PORT, '3190');
69
+ assert.equal(out.HOST, '127.0.0.1');
70
+ assert.equal(out.PGDATABASE, PREVIEW_DB);
71
+ assert.notEqual(out.PGDATABASE, LIVE.PGDATABASE);
72
+ });
73
+
74
+ test('HOME is the empty scratch directory, so the real home config is not found', () => {
75
+ assert.equal(build().HOME, '/tmp/scratch-home');
76
+ });
77
+
78
+ test('outbound chat is a dry run and every poller is switched off', () => {
79
+ const out = build();
80
+ assert.equal(out.CHAT_DRY_RUN, '1');
81
+ for (const k of POLLERS_OFF) assert.equal(out[k], '1', k);
82
+ assert.equal(POLLERS_OFF.length, 9);
83
+ });
84
+
85
+ test('the modules that talk outward are off, and a preview cannot offer previews', () => {
86
+ const out = build();
87
+ for (const k of ['BONGOS_MODULE_DISCORD', 'BONGOS_MODULE_AGENTS', 'BONGOS_MODULE_PROVISIONING', 'BONGOS_MODULE_NPM_RELEASE']) {
88
+ assert.equal(out[k], '0', k);
89
+ }
90
+ });
91
+
92
+ test('NODE_ENV defaults to production when the live process set none', () => {
93
+ const out = buildPreviewEnv({ env: { PATH: '/bin' }, port: 1, scratchHome: '/h' });
94
+ assert.equal(out.NODE_ENV, 'production');
95
+ });
96
+
97
+ test('a missing port or scratch home is refused rather than guessed', () => {
98
+ assert.throws(() => buildPreviewEnv({ env: LIVE, scratchHome: '/h' }), /port/);
99
+ assert.throws(() => buildPreviewEnv({ env: LIVE, port: 1 }), /scratchHome/);
100
+ });