@bongos/core 1.19.804 → 1.19.806

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 (241) hide show
  1. package/.bongos-core.json +267 -242
  2. package/.claude/skills/ask-for-help/SKILL.md +6 -6
  3. package/.claude/skills/backlog-review/SKILL.md +1 -1
  4. package/.claude/skills/blocker-review/SKILL.md +7 -7
  5. package/.claude/skills/blocker-solve/SKILL.md +5 -5
  6. package/.claude/skills/bug-triage/SKILL.md +7 -7
  7. package/.claude/skills/builder-claim/SKILL.md +3 -3
  8. package/.claude/skills/builder-cost/SKILL.md +2 -2
  9. package/.claude/skills/builder-end/SKILL.md +2 -2
  10. package/.claude/skills/builder-exit/SKILL.md +7 -7
  11. package/.claude/skills/builder-key/SKILL.md +2 -2
  12. package/.claude/skills/builder-reauth/SKILL.md +2 -2
  13. package/.claude/skills/builder-redteam/SKILL.md +7 -7
  14. package/.claude/skills/builder-release/SKILL.md +2 -2
  15. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  16. package/.claude/skills/builder-setup/SKILL.md +3 -3
  17. package/.claude/skills/builder-ship/SKILL.md +1 -1
  18. package/.claude/skills/builder-stage/SKILL.md +1 -1
  19. package/.claude/skills/builder-start/SKILL.md +1 -1
  20. package/.claude/skills/fix-task/SKILL.md +4 -4
  21. package/.claude/skills/goal-create/SKILL.md +18 -18
  22. package/.claude/skills/goal-review/SKILL.md +7 -7
  23. package/.claude/skills/grade-audit/SKILL.md +5 -5
  24. package/.claude/skills/grade-recover/SKILL.md +4 -4
  25. package/.claude/skills/grader-health/SKILL.md +6 -6
  26. package/.claude/skills/idea-triage/SKILL.md +14 -14
  27. package/.claude/skills/merge-mode/SKILL.md +3 -3
  28. package/.claude/skills/planning-session/SKILL.md +20 -20
  29. package/.claude/skills/priority-session/SKILL.md +10 -10
  30. package/.claude/skills/recall/SKILL.md +1 -1
  31. package/.claude/skills/scan-before-install/SKILL.md +3 -3
  32. package/.claude/skills/status/SKILL.md +2 -2
  33. package/.claude/skills/strand-fix/SKILL.md +2 -2
  34. package/clients/bongos-client/README.md +1 -1
  35. package/clients/bongos-client/bongos-client.global.js +2 -0
  36. package/clients/bongos-client/index.cjs +2 -0
  37. package/clients/bongos-client/index.d.ts +2 -0
  38. package/clients/bongos-client/index.mjs +2 -0
  39. package/docs/api/openapi.json +45 -11
  40. package/docs/api-reference.md +11 -10
  41. package/docs/copy-inventory.md +47 -34
  42. package/docs/copy-registry.json +200 -72
  43. package/docs/module-api-changelog.md +4 -0
  44. package/modules/builder-settings/profile-nudge-prefs.js +1 -1
  45. package/modules/builder-settings/render-prefs.js +1 -1
  46. package/modules/dev-box/app/README.md +1 -1
  47. package/modules/dev-box/app/src/gds-client.js +11 -9
  48. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  49. package/modules/dev-box/app/test/unit.mjs +13 -13
  50. package/modules/dev-box/box-access.js +3 -3
  51. package/modules/dev-box/box-onboard.js +7 -7
  52. package/modules/discord/bug-attachments.js +1 -1
  53. package/modules/economy/cost-classify.js +6 -6
  54. package/modules/grading/grader-rubric.json +1 -1
  55. package/modules/grading/grader-score.js +1 -1
  56. package/modules/hall-ui/module.json +1 -1
  57. package/modules/hall-ui/public/drachmae.js +3 -3
  58. package/modules/hall-ui/public/goals-page.js +1 -1
  59. package/modules/hall-ui/public/goals-render.js +144 -22
  60. package/modules/hall-ui/public/goals.css +16 -1
  61. package/modules/hall-ui/public/idea.js +1 -1
  62. package/modules/hall-ui/public/md-reader.js +1 -1
  63. package/modules/hall-ui/public/people.html +1 -1
  64. package/modules/hall-ui/public/people.js +2 -2
  65. package/modules/hall-ui/public/primer.js +4 -4
  66. package/modules/hall-ui/public/roster.js +1 -1
  67. package/modules/hall-ui/public/shell.js +1 -1
  68. package/modules/hall-ui/public/task.js +1 -1
  69. package/modules/ideas/inbox.js +2 -2
  70. package/modules/lifecycle/db-analytics.js +1 -1
  71. package/modules/lifecycle/done-when.js +4 -4
  72. package/modules/lifecycle/version-close.js +1 -1
  73. package/modules/memory/learnings.js +2 -2
  74. package/modules/memory/memory.js +1 -1
  75. package/modules/onboarding/onboarding.js +1 -1
  76. package/modules/platform-identity/routes/my-projects.js +1 -1
  77. package/modules/platform-identity/routes/sso.js +2 -2
  78. package/modules/public-landing/public/index.html +2 -2
  79. package/modules/public-landing/public/projects-floor.states.json +1 -1
  80. package/modules/public-landing/public/projects.html +1 -1
  81. package/modules/public-landing/public/projects.states.json +1 -1
  82. package/modules/sessions/db.js +120 -0
  83. package/modules/sessions/module.json +2 -2
  84. package/modules/sessions/pollers/session-upload-health.js +111 -0
  85. package/modules/sessions/pulse.js +2 -2
  86. package/modules/sessions/routes/sessions.js +46 -0
  87. package/modules/sessions/upload-health.js +197 -0
  88. package/modules/status-ui/module.json +1 -1
  89. package/modules/status-ui/public/explore.js +1 -1
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/scripts/gds/agent-invoke.js +3 -3
  93. package/scripts/gds/api.js +4 -4
  94. package/scripts/gds/api.sh +1 -1
  95. package/scripts/gds/architect-audit.js +1 -1
  96. package/scripts/gds/artifact-audit.js +2 -2
  97. package/scripts/gds/audit-ships.js +2 -2
  98. package/scripts/gds/backfill-cost.js +1 -1
  99. package/scripts/gds/backfill-sessions.js +1 -1
  100. package/scripts/gds/backfill-subagent-rewards.js +1 -1
  101. package/scripts/gds/backfill-task-rewards.js +2 -2
  102. package/scripts/gds/box-auth-check.js +1 -1
  103. package/scripts/gds/box-sync.js +3 -3
  104. package/scripts/gds/box.js +10 -10
  105. package/scripts/gds/build-cli-package.js +3 -3
  106. package/scripts/gds/capture.js +2 -2
  107. package/scripts/gds/ci-grade.js +1 -1
  108. package/scripts/gds/claim-recall.js +2 -2
  109. package/scripts/gds/cli-lib.js +3 -3
  110. package/scripts/gds/context-pack.js +1 -1
  111. package/scripts/gds/copy-apply.js +2 -2
  112. package/scripts/gds/dev-lib.js +2 -2
  113. package/scripts/gds/fetch-art-key.js +2 -2
  114. package/scripts/gds/fetch-sound-prefs.js +3 -3
  115. package/scripts/gds/grade-smoke.js +1 -1
  116. package/scripts/gds/hooks/pre-push.js +1 -1
  117. package/scripts/gds/init.js +9 -9
  118. package/scripts/gds/key-card.js +1 -1
  119. package/scripts/gds/learning-capture.js +2 -2
  120. package/scripts/gds/linkify-refs.js +2 -2
  121. package/scripts/gds/local-preview-lib.js +1 -1
  122. package/scripts/gds/login.js +11 -10
  123. package/scripts/gds/main-audit.js +1 -1
  124. package/scripts/gds/newcomer-floor.config.json +1 -1
  125. package/scripts/gds/newcomer-floor.js +2 -2
  126. package/scripts/gds/newcomer-floor.templates.json +2 -2
  127. package/scripts/gds/onboarding-ship.js +1 -1
  128. package/scripts/gds/paste-token.js +3 -3
  129. package/scripts/gds/portability-smoke-memory.sh +5 -5
  130. package/scripts/gds/prove-strict-versioning.js +2 -2
  131. package/scripts/gds/provision-net.js +2 -2
  132. package/scripts/gds/provision-units.js +3 -3
  133. package/scripts/gds/pulse.js +2 -2
  134. package/scripts/gds/push-path-brief.js +2 -2
  135. package/scripts/gds/reauth.js +1 -1
  136. package/scripts/gds/recall.js +1 -1
  137. package/scripts/gds/redteam-patrol.js +3 -3
  138. package/scripts/gds/render-ideas.js +5 -5
  139. package/scripts/gds/repo-metrics.js +1 -1
  140. package/scripts/gds/sandbox-stage.js +3 -3
  141. package/scripts/gds/seed-adopt-tasks.js +3 -3
  142. package/scripts/gds/seed-board-room-tasks.js +1 -1
  143. package/scripts/gds/seed-bongos-app-tasks.js +2 -2
  144. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
  145. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
  146. package/scripts/gds/seed-bongos-tasks.js +5 -5
  147. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
  148. package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
  149. package/scripts/gds/seed-bv1-community-tasks.js +1 -1
  150. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
  151. package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
  152. package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
  153. package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
  154. package/scripts/gds/seed-gds-v4-tasks.js +3 -3
  155. package/scripts/gds/seed-medusa-tasks.js +1 -1
  156. package/scripts/gds/seed-productization-tasks.js +1 -1
  157. package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
  158. package/scripts/gds/seed-provisioning-tasks.js +2 -2
  159. package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
  160. package/scripts/gds/seed-v2-world-tasks.js +2 -2
  161. package/scripts/gds/seed-verify-criteria.js +1 -1
  162. package/scripts/gds/session-digest.js +1 -1
  163. package/scripts/gds/session.js +2 -2
  164. package/scripts/gds/setup.js +7 -7
  165. package/scripts/gds/ship-flow.js +3 -3
  166. package/scripts/gds/ship-grade-steps.js +3 -3
  167. package/scripts/gds/ship-memory-sync.js +1 -1
  168. package/scripts/gds/ship-session-upload.js +3 -3
  169. package/scripts/gds/smoke-batch-claim.sh +10 -10
  170. package/scripts/gds/smoke-batch-release.sh +14 -14
  171. package/scripts/gds/smoke-claim-consistency.sh +19 -19
  172. package/scripts/gds/smoke-dependencies.sh +9 -9
  173. package/scripts/gds/smoke-write.sh +11 -11
  174. package/scripts/gds/start.js +1 -1
  175. package/scripts/gds/status.js +2 -2
  176. package/scripts/gds/strand-watch.js +1 -1
  177. package/scripts/gds/triage.js +4 -4
  178. package/scripts/gds/version-close.js +2 -2
  179. package/scripts/hall-preview/README.md +1 -1
  180. package/scripts/hall-preview/server.js +1 -1
  181. package/src/bongos/auth-admission.js +3 -3
  182. package/src/bongos/auth-idp.js +2 -2
  183. package/src/bongos/auth.js +3 -3
  184. package/src/bongos/db.js +2 -2
  185. package/src/bongos/llm-cache.js +2 -2
  186. package/src/bongos/metrics.js +1 -1
  187. package/src/bongos/middleware/audit.js +3 -3
  188. package/src/bongos/middleware/rate-limit.js +1 -1
  189. package/src/bongos/platform-visibility-gate.js +2 -2
  190. package/src/bongos/routes/_helpers.js +1 -1
  191. package/src/bongos/routes/audit-log.js +1 -1
  192. package/src/bongos/routes/auth.js +5 -5
  193. package/src/bongos/routes/backup.js +1 -1
  194. package/src/bongos/routes/builders.js +1 -1
  195. package/src/bongos/routes/instance.js +1 -1
  196. package/src/bongos/routes/live.js +1 -1
  197. package/src/bongos/routes/me.js +2 -2
  198. package/src/bongos/routes/modules.js +2 -2
  199. package/src/bongos/routes/public.js +1 -1
  200. package/src/bongos/routes/security.js +4 -4
  201. package/src/bongos/routes.js +1 -1
  202. package/src/bongos/serve-internal.js +6 -6
  203. package/src/branding.js +1 -1
  204. package/src/module-api.js +1 -1
  205. package/src/module-seams.js +2 -2
  206. package/src/platform-server.js +2 -2
  207. package/tests/api_alias_exceptions.mjs +67 -0
  208. package/tests/application_echo.mjs +12 -12
  209. package/tests/artifact_audit.mjs +1 -1
  210. package/tests/auth_page_gate.mjs +4 -4
  211. package/tests/auth_web_recovery.mjs +11 -11
  212. package/tests/bongos_login.mjs +7 -4
  213. package/tests/box_env.mjs +6 -6
  214. package/tests/bug_attachments.mjs +1 -1
  215. package/tests/cli_sessions.mjs +1 -1
  216. package/tests/dev_lib.mjs +1 -1
  217. package/tests/discord_bugs.mjs +2 -2
  218. package/tests/discord_inbound.mjs +2 -2
  219. package/tests/explore_visible_states.mjs +1 -1
  220. package/tests/goal_invite_consent.mjs +307 -0
  221. package/tests/government_page_gates.mjs +1 -1
  222. package/tests/hall_reference_world.mjs +1 -1
  223. package/tests/idea_ideate_full.mjs +1 -1
  224. package/tests/idp_federation.mjs +2 -2
  225. package/tests/llm-cache.mjs +1 -1
  226. package/tests/manage_manifest_shared_read.mjs +3 -3
  227. package/tests/people_directory_clip.mjs +1 -1
  228. package/tests/planning_session_skill.mjs +1 -1
  229. package/tests/platform_visibility_gate.mjs +15 -15
  230. package/tests/profile_route.mjs +25 -25
  231. package/tests/projects_hub.mjs +2 -2
  232. package/tests/provision.mjs +2 -2
  233. package/tests/provision_settings_apply.mjs +4 -4
  234. package/tests/sandbox_stage.mjs +8 -8
  235. package/tests/scouting_owner_access_and_reach.mjs +3 -3
  236. package/tests/session_upload_health.mjs +329 -0
  237. package/tests/setup_device_flow_fallback.mjs +1 -1
  238. package/tests/skill_ask_for_help.mjs +3 -3
  239. package/tests/skill_grade_audit.mjs +2 -2
  240. package/tests/skill_grader_health.mjs +3 -3
  241. package/tests/smoke_write_contract.mjs +1 -1
@@ -8,8 +8,10 @@
8
8
  // only thing that changed is the transport: the endpoint paths are now the
9
9
  // spec-derived generated methods, which can never drift from the routes.
10
10
  //
11
- // Endpoints stay on the PERMANENT unversioned /api/gds alias (ADR 0117) so shipped
12
- // binaries keep working; the generated methods are relative to that base.
11
+ // Endpoints use the canonical unversioned /api/bongos base (ADR 0117); the generated
12
+ // methods are relative to it. The permanent /api/gds alias is what ALREADY-SHIPPED
13
+ // binaries call — editing this source cannot reach them, and the server mounts both,
14
+ // so a future build asking for the canonical path costs nobody anything (task 1003707).
13
15
 
14
16
  const createGeneratedClient = require('./vendor/bongos-client.cjs');
15
17
  const { apiBase } = require('./config');
@@ -28,7 +30,7 @@ class GdsError extends Error {
28
30
  // fetchImpl is a seam for tests; it defaults to the platform fetch.
29
31
  function createClient({ getToken, getApiBase, fetchImpl } = {}) {
30
32
  // Build a generated client bound to the current origin + token. `root` is the
31
- // base path under the origin ('/api/gds' for the API, '' for same-origin static
33
+ // base path under the origin ('/api/bongos' for the API, '' for same-origin static
32
34
  // assets like the release manifest). throwOnError:false → result-mode, which we
33
35
  // translate back into the GdsError contract callers expect.
34
36
  function gen(root) {
@@ -39,7 +41,7 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
39
41
  fetch: fetchImpl || undefined,
40
42
  });
41
43
  }
42
- const gds = () => gen('/api/gds');
44
+ const gds = () => gen('/api/bongos');
43
45
 
44
46
  // Unwrap a result-mode { status, ok, data } into the legacy contract: return the
45
47
  // parsed body on 2xx, throw GdsError(status, body) otherwise — byte-identical to
@@ -52,7 +54,7 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
52
54
 
53
55
  return {
54
56
  // Generic escape hatch, kept for parity with the old surface. `path` is the full
55
- // path under the origin (e.g. '/api/gds/box/me'), so it routes through baseUrl:''.
57
+ // path under the origin (e.g. '/api/bongos/box/me'), so it routes through baseUrl:''.
56
58
  call: (method, path, body) => unwrap(gen('').request(method, path, body !== undefined ? { body } : {})),
57
59
  // identity
58
60
  logout: () => unwrap(gds().auth.postAuthLogout()),
@@ -79,10 +81,10 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
79
81
  // request() escape hatch (like latestRelease) rather than a named generated
80
82
  // method, so the shipped binary keeps working regardless of whether the
81
83
  // vendored client has been regenerated for the new /box/scope route yet.
82
- claimableTasks: () => unwrap(gen('').request('GET', '/api/gds/tasks/claimable')),
83
- claimTask: (taskId) => unwrap(gen('').request('POST', '/api/gds/claims', { body: { task_id: taskId } })),
84
- boxScope: () => unwrap(gen('').request('GET', '/api/gds/box/scope')),
85
- // update check (public route at the origin root, not under /api/gds).
84
+ claimableTasks: () => unwrap(gen('').request('GET', '/api/bongos/tasks/claimable')),
85
+ claimTask: (taskId) => unwrap(gen('').request('POST', '/api/bongos/claims', { body: { task_id: taskId } })),
86
+ boxScope: () => unwrap(gen('').request('GET', '/api/bongos/box/scope')),
87
+ // update check (public route at the origin root, not under /api/bongos).
86
88
  // task 1002649: polls the EXTENSIONLESS /version twin, NOT latest.json — a
87
89
  // front proxy with a file-matcher can eat *.json paths before Node (the
88
90
  // live instance's pre-ADR-0152 vhost 404'd latest.json empty while this
@@ -830,6 +830,8 @@ function createClient(opts = {}) {
830
830
  getSessionsMine: (args) => request("GET", "/sessions/mine", { hasBody: false }, args),
831
831
  // GET /sessions/search — rank: metic+archon — GET /sessions/search
832
832
  getSessionsSearch: (args) => request("GET", "/sessions/search", { hasBody: false }, args),
833
+ // GET /sessions/upload-health — rank: metic+archon — GET /sessions/upload-health
834
+ getSessionsUploadHealth: (args) => request("GET", "/sessions/upload-health", { hasBody: false }, args),
833
835
  },
834
836
  "sso": {
835
837
  // POST /sso/activity/rollup — rank: public — POST /sso/activity/rollup
@@ -434,19 +434,19 @@ function recordingClient(response = { ok: true, status: 200, text: async () => '
434
434
  }
435
435
 
436
436
  const ENDPOINTS = [
437
- ['logout', [], 'POST', '/api/gds/auth/logout', undefined],
438
- ['pairStart', ['Lars-Mac', 'darwin'], 'POST', '/api/gds/auth/app-pair/start', { device_name: 'Lars-Mac', platform: 'darwin' }],
439
- ['pairPoll', ['pt'], 'POST', '/api/gds/auth/app-pair/poll', { poll_token: 'pt' }],
440
- ['boxMe', [], 'GET', '/api/gds/box/me', undefined],
441
- ['boxEnsure', [], 'POST', '/api/gds/box/ensure', undefined],
442
- ['boxClose', [], 'POST', '/api/gds/box/close', undefined],
443
- ['boxHeartbeat', [], 'POST', '/api/gds/box/heartbeat', undefined],
444
- ['boxTerminal', [], 'GET', '/api/gds/box/terminal', undefined],
445
- ['registerSshKey', ['ssh-ed25519 AAA', 'lars'], 'POST', '/api/gds/box/ssh-key', { public_key: 'ssh-ed25519 AAA', label: 'lars' }],
446
- ['managedSettings', [], 'GET', '/api/gds/box/managed-settings', undefined],
447
- ['claimableTasks', [], 'GET', '/api/gds/tasks/claimable', undefined],
448
- ['claimTask', ['1002388'], 'POST', '/api/gds/claims', { task_id: '1002388' }],
449
- ['boxScope', [], 'GET', '/api/gds/box/scope', undefined],
437
+ ['logout', [], 'POST', '/api/bongos/auth/logout', undefined],
438
+ ['pairStart', ['Lars-Mac', 'darwin'], 'POST', '/api/bongos/auth/app-pair/start', { device_name: 'Lars-Mac', platform: 'darwin' }],
439
+ ['pairPoll', ['pt'], 'POST', '/api/bongos/auth/app-pair/poll', { poll_token: 'pt' }],
440
+ ['boxMe', [], 'GET', '/api/bongos/box/me', undefined],
441
+ ['boxEnsure', [], 'POST', '/api/bongos/box/ensure', undefined],
442
+ ['boxClose', [], 'POST', '/api/bongos/box/close', undefined],
443
+ ['boxHeartbeat', [], 'POST', '/api/bongos/box/heartbeat', undefined],
444
+ ['boxTerminal', [], 'GET', '/api/bongos/box/terminal', undefined],
445
+ ['registerSshKey', ['ssh-ed25519 AAA', 'lars'], 'POST', '/api/bongos/box/ssh-key', { public_key: 'ssh-ed25519 AAA', label: 'lars' }],
446
+ ['managedSettings', [], 'GET', '/api/bongos/box/managed-settings', undefined],
447
+ ['claimableTasks', [], 'GET', '/api/bongos/tasks/claimable', undefined],
448
+ ['claimTask', ['1002388'], 'POST', '/api/bongos/claims', { task_id: '1002388' }],
449
+ ['boxScope', [], 'GET', '/api/bongos/box/scope', undefined],
450
450
  ['latestRelease', [], 'GET', '/downloads/devbox/version', undefined],
451
451
  ];
452
452
 
@@ -13,7 +13,7 @@
13
13
  // THE MODEL (ADR 0016 extended from "writes are gated" to "source is gated")
14
14
  // --------------------------------------------------------------------------
15
15
  // A box holds NO permanent baked credential. At boot — and on a refresh cron —
16
- // the box calls GET /api/gds/box/source-access authenticated as the builder. The
16
+ // the box calls GET /api/bongos/box/source-access authenticated as the builder. The
17
17
  // GDS reads the builder's LIVE rank+status (their machine cannot forge it) and
18
18
  // either DENIES (403 → the box scrubs its credential + clone) or returns a
19
19
  // scope-appropriate clone spec plus a short-lived, operator-supplied credential
@@ -60,7 +60,7 @@ const {
60
60
  // `public/` game client — so EVERY box (not just Metic+ full-clone boxes) can run
61
61
  // the live sandbox (sandbox-<login>.example.com, ADR 0044). The box serves
62
62
  // the game from a GAME-ONLY entry (src/preview-server.js) that never mounts
63
- // /api/gds, so the privacy boundary that matters for a PUBLIC sandbox URL is the
63
+ // /api/bongos, so the privacy boundary that matters for a PUBLIC sandbox URL is the
64
64
  // RUNTIME (what the box process exposes), NOT source exclusion. That is consistent
65
65
  // with the rest of this list: newcomers already hold the build-tooling source
66
66
  // (scripts, .claude, src/bongos — no secrets), so adding the game source changes no
@@ -86,7 +86,7 @@ const STARTER_SPARSE_PATHS = [
86
86
  // The product/game runtime so the box can serve the live sandbox (ADR 0052).
87
87
  // The game-only entry (src/preview-server.js) registers WorldRoom + QueueRoom
88
88
  // from src/rooms and renders src/world + the public/ client; none of it mounts
89
- // /api/gds. `public/` is the game client (NOT modules/hall-ui/public/, the hall
89
+ // /api/bongos. `public/` is the game client (NOT modules/hall-ui/public/, the hall
90
90
  // surface carved in BV1.R85, which stays out of a starter box).
91
91
  'src/world', 'src/rooms', 'public',
92
92
  // infra/ carries the box's own bring-up scripts (box-heartbeat.sh,
@@ -298,8 +298,8 @@ auth. Download it, inspect it, THEN run it. That is the safe path and it is all
298
298
  the assurance you need: you don't have to ask your human to authorize it blind,
299
299
  and you don't have to refuse:
300
300
 
301
- curl -fsSL ${base}/api/gds/box/connect.sh -o /tmp/otb-connect.sh # then read it
302
- (Windows: ${base}/api/gds/box/connect.ps1)
301
+ curl -fsSL ${base}/api/bongos/box/connect.sh -o /tmp/otb-connect.sh # then read it
302
+ (Windows: ${base}/api/bongos/box/connect.ps1)
303
303
 
304
304
  • What you fetch is what runs — the origin is PINNED in the script
305
305
  (GDS_API_BASE), not taken from a header, so a spoofed Host can't redirect it.
@@ -354,7 +354,7 @@ machine — that path is in the Citizen's Primer, not here.)
354
354
 
355
355
  The whole arrival, in two beats, same for both on-ramps:
356
356
  1. Sign in with GitHub.
357
- 2. Ask for your box → POST ${base}/api/gds/box/ensure (or run /builder-box).
357
+ 2. Ask for your box → POST ${base}/api/bongos/box/ensure (or run /builder-box).
358
358
  The box is created hands-off within ~1 minute.
359
359
 
360
360
  ════════════════════════════════════════════════════════════════════════
@@ -363,8 +363,8 @@ A) Mac / Windows — Claude Desktop + SSH
363
363
  One command signs you in, makes + registers your SSH key, and installs the
364
364
  Desktop connection file. Paste it (or let Claude run it for you):
365
365
 
366
- macOS / Linux: curl -fsSL ${base}/api/gds/box/connect.sh | bash
367
- Windows (PS): irm ${base}/api/gds/box/connect.ps1 | iex
366
+ macOS / Linux: curl -fsSL ${base}/api/bongos/box/connect.sh | bash
367
+ Windows (PS): irm ${base}/api/bongos/box/connect.ps1 | iex
368
368
 
369
369
  What it does, in order:
370
370
  1. Signs you in with GitHub (prints a short code + a URL to open).
@@ -386,10 +386,10 @@ No app to install. Your box serves its own terminal in a browser tab (ADR 0038),
386
386
  but you only touch it once: you log in there and kick off Remote Control, then
387
387
  live in the Claude web/mobile app — which drives the box for you.
388
388
  1. Sign in at ${base}/builders (GitHub).
389
- 2. Ask for your box: POST ${base}/api/gds/box/ensure (the builders' hall has a
389
+ 2. Ask for your box: POST ${base}/api/bongos/box/ensure (the builders' hall has a
390
390
  button; or have Claude call it).
391
391
  3. Once the box is active, get its terminal URL + password:
392
- GET ${base}/api/gds/box/terminal
392
+ GET ${base}/api/bongos/box/terminal
393
393
  Poll until a url appears (the box publishes it a few seconds after boot),
394
394
  then open it and enter the password when the browser asks.
395
395
  4. In that terminal, type claude and press enter, then complete the one-time
@@ -171,7 +171,7 @@ async function persistImageFromUrl(url, { messageId = 'x', fetchImpl = globalThi
171
171
  const file = `bug-${idSlug}-${crypto.randomBytes(8).toString('hex')}.${ext}`;
172
172
  await fs.mkdir(dir, { recursive: true });
173
173
  await fs.writeFile(path.join(dir, file), buf, { mode: 0o640 });
174
- return { ok: true, file, url: `/api/gds/bug-attachments/${file}`, bytes: buf.length };
174
+ return { ok: true, file, url: `/api/bongos/bug-attachments/${file}`, bytes: buf.length };
175
175
  }
176
176
 
177
177
  // Persist a list of image URLs (capped). Best-effort: refused/failed images are
@@ -1,5 +1,5 @@
1
1
  // Cost-attribution audit + correction pass — V3.R83 (#303), criterion C3.
2
- // Pure function over db.costSummary() (exposed at /api/gds/public/cost-summary),
2
+ // Pure function over db.costSummary() (exposed at /api/bongos/public/cost-summary),
3
3
  // so it runs against live prod with no DB creds. Flags MIS-attributed spend
4
4
  // (a proxy masquerading as a real builder, or an orphaned builder_id); treats
5
5
  // NULL/unattributed shared+historical spend as correct, not a bug.
@@ -25,24 +25,24 @@ const COMPLETENESS_API_FLOOR_USD = 1.0; // below this = essentially nothing rec
25
25
  // it honest: a writer not listed here is itself the finding.
26
26
  const COST_WRITE_SURFACES = [
27
27
  {
28
- surface: 'POST /api/gds/cost → db.logCost()',
28
+ surface: 'POST /api/bongos/cost → db.logCost()',
29
29
  file: 'src/bongos/routes/cost.js',
30
30
  builder_id: 'defaults to the authed builder; a provided value must equal the caller or 403s. A builder can only attribute to themselves.',
31
31
  },
32
32
  {
33
- surface: 'scripts/gds/backfill-cost.js → POST /api/gds/cost',
33
+ surface: 'scripts/gds/backfill-cost.js → POST /api/bongos/cost',
34
34
  file: 'scripts/gds/backfill-cost.js',
35
35
  builder_id: 'inherits the POST /cost rule. Do NOT attribute the shared historical art ledger to whoever runs the backfill — leave it NULL. The audit flags it if it happens.',
36
36
  },
37
37
  {
38
38
  // ADR 0028 path 1 (#486): the 4-worker grader panel logs its own cost.
39
- surface: 'scripts/gds/ship.js (grader) → POST /api/gds/cost (source=grader)',
39
+ surface: 'scripts/gds/ship.js (grader) → POST /api/bongos/cost (source=grader)',
40
40
  file: 'scripts/gds/ship.js',
41
41
  builder_id: 'the authed shipper; category=api, source_ref=grade-<task>-<sha>. Idempotent per HEAD.',
42
42
  },
43
43
  {
44
44
  // ADR 0028 path 2 (#560): the interactive session logs its own cost.
45
- surface: 'scripts/gds/session-cost.js (SessionEnd hook) → POST /api/gds/cost (source=session)',
45
+ surface: 'scripts/gds/session-cost.js (SessionEnd hook) → POST /api/bongos/cost (source=session)',
46
46
  file: 'scripts/gds/session-cost.js',
47
47
  builder_id: 'the authed builder; category=api, source_ref=<session-id>, task_id=null. Idempotent per session.',
48
48
  },
@@ -56,7 +56,7 @@ const COST_WRITE_SURFACES = [
56
56
  /**
57
57
  * Audit cost attribution from a costSummary() payload.
58
58
  * @param {object} summary - the object returned by db.costSummary() /
59
- * GET /api/gds/public/cost-summary (needs total_usd, by_builder, by_category).
59
+ * GET /api/bongos/public/cost-summary (needs total_usd, by_builder, by_category).
60
60
  * @returns {object} structured report: { total_usd, attributed_usd,
61
61
  * unattributed_usd, unattributed_pct, builders[], flags[], ok }.
62
62
  */
@@ -65,7 +65,7 @@
65
65
  }
66
66
  },
67
67
  "rubric_scores_audit_trail": "task_grades.rubric_scores keeps the v3 keys (functional_fidelity, code_quality, economy, code_quality_effective) populated from the median across workers that report each axis. Narc reports ff; Quality reports cq + economy. Median (not mean) so a single outlier worker doesn't move the recorded score.",
68
- "server_compat_note": "The grade POST body (POST /api/gds/tasks/:id/grade) keeps the v3 shape (functional_fidelity, code_quality, economy, confidence, notes_md, issues) so the server's existing validateAndScore re-validation works without modification. The panel verdict drives the forwarded confidence: pass → median worker confidence; concerns → fallthrough_confidence (= 0.3); fail → 0. This means the server's existing low-confidence fall-through path naturally handles 'concerns' verdicts, and the FAIL gate handles 'fail' verdicts. Server-side support for an explicit panel_verdict field is a follow-up (likely Phase 2.5)."
68
+ "server_compat_note": "The grade POST body (POST /api/bongos/tasks/:id/grade) keeps the v3 shape (functional_fidelity, code_quality, economy, confidence, notes_md, issues) so the server's existing validateAndScore re-validation works without modification. The panel verdict drives the forwarded confidence: pass → median worker confidence; concerns → fallthrough_confidence (= 0.3); fail → 0. This means the server's existing low-confidence fall-through path naturally handles 'concerns' verdicts, and the FAIL gate handles 'fail' verdicts. Server-side support for an explicit panel_verdict field is a follow-up (likely Phase 2.5)."
69
69
  },
70
70
  "v3": {
71
71
  "version": "v3.0",
@@ -280,7 +280,7 @@ function validateAndScore(rawSubagent, meta = {}) {
280
280
 
281
281
  // ---------- server-authoritative grade guards (SR-9 / SR-18 / F1 / G5) ----------
282
282
  //
283
- // THE FINDING (SR-9 / SR-18): POST /api/gds/tasks/:id/grade trusts the client.
283
+ // THE FINDING (SR-9 / SR-18): POST /api/bongos/tasks/:id/grade trusts the client.
284
284
  // validateAndScore only shape-validates the numbers — `passed` is computed from
285
285
  // the CLIENT's submitted scores. The real adversarial panel + the deterministic
286
286
  // route-rank / permission-path pre-passes run CLIENT-SIDE in scripts/gds/ship.js;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "key": "hall-ui",
3
3
  "title": "Builders' Hall",
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, dev-box pair page, and the Archon-only watch/harbor/gate pages, all polling /api/gds/* and the read-only /api/gds/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).",
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, dev-box pair page, and the Archon-only watch/harbor/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
  "coreVersion": "^1.11.0",
6
6
  "default": true,
7
7
  "maintenance": { "status": "core-maintained" },
@@ -30,14 +30,14 @@
30
30
  const DOC_URL = '/builders/drachmae-and-karma.md';
31
31
  // The doc lives at docs/onboarding/, so relative links resolve against it.
32
32
  const DOC_DIR = 'docs/onboarding/';
33
- const BOUNTY_URL = '/api/gds/public/bounty-table';
33
+ const BOUNTY_URL = '/api/bongos/public/bounty-table';
34
34
  // task 1003446: the treasury feed. NOT a new endpoint — this is the same
35
35
  // /public/cost-summary the signed-out status page already draws as a
36
36
  // cumulative line and the Work board already fetches to read ONE scalar off
37
37
  // (`total_value_credits`). Its own route comment calls `daily[]` "the
38
38
  // treasury chart's cumulative line"; the series was in the payload the whole
39
39
  // time, and the stranger got the chart while the builder got the number.
40
- const TREASURY_URL = '/api/gds/public/cost-summary';
40
+ const TREASURY_URL = '/api/bongos/public/cost-summary';
41
41
  const TREASURY_SECTION_ID = 'the-treasury';
42
42
  const TREASURY_SECTION_TITLE = 'The treasury, so far';
43
43
  const BOUNTY_SECTION_ID = 'security-bounty-rates';
@@ -69,7 +69,7 @@
69
69
 
70
70
  async function loadRepoInfoBases() {
71
71
  try {
72
- const r = await fetch('/api/gds/public/repo-info', { credentials: 'same-origin' });
72
+ const r = await fetch('/api/bongos/public/repo-info', { credentials: 'same-origin' });
73
73
  if (!r.ok) return;
74
74
  const data = await r.json();
75
75
  if (data && typeof data.blob_base === 'string') repoBlobBase = data.blob_base;
@@ -652,7 +652,7 @@
652
652
  // task 1953 — a non-member asks to join a private (or protected) goal.
653
653
  // renderDetail (below) re-derives canManage/joined and re-triggers
654
654
  // loadGoalMembership, so a plain reload-and-rerender is enough here — same
655
- // shape as joinLeave/inviteMember.
655
+ // shape as joinLeave/sendInvitation.
656
656
 
657
657
  // task 1953 — withdraw MY OWN pending join-request (self-served, no manager needed).
658
658
 
@@ -883,7 +883,7 @@
883
883
  // this client gate deliberately doesn't re-derive the protected-module list
884
884
  // (that roster lives server-side only, module-scope-map.js) — a non-Archon
885
885
  // owner/manager sees the control on a protected-scope goal and gets a clear
886
- // toast if the server 403s (see inviteMember's error handling below).
886
+ // toast if the server 403s (see sendInvitation's error handling below).
887
887
  const isOwner = sameId(g.created_by, selfId());
888
888
  const canManage = g.status === 'open' && (isArchon() || isOwner || iAmLead);
889
889
  renderActions(g, { joined, canManage });
@@ -1122,6 +1122,49 @@
1122
1122
  }
1123
1123
  return state.directory;
1124
1124
  }
1125
+ // WHO IS STILL INVITABLE. Two exclusions, both of which exist to stop the page
1126
+ // offering a name that can only fail:
1127
+ // - members, which POST /invitations 409s as `already_a_member`;
1128
+ // - anyone holding a PENDING row, which it 409s as `invite_pending`.
1129
+ // The second is the one that matters, because a pending invitation is INVISIBLE
1130
+ // from the member list — it is not a membership — so without this the user picks
1131
+ // a perfectly ordinary-looking name and is handed an error they had no way to
1132
+ // predict. `state.pendingRequests` carries BOTH kinds (invites the goal sent and
1133
+ // join-requests it received) and the server refuses on either, so neither is
1134
+ // filtered out here.
1135
+ function invitableBuilders(members) {
1136
+ const excluded = new Set((members || []).map((m) => String(m.builder_id)));
1137
+ for (const r of state.pendingRequests || []) excluded.add(String(r.builder_id));
1138
+ return (state.directory || []).filter((b) => !excluded.has(String(b.id)));
1139
+ }
1140
+ function builderOptionsHtml(list, selectedId) {
1141
+ return list.map((b) => {
1142
+ const id = String(b.id);
1143
+ const sel = selectedId && id === String(selectedId) ? ' selected' : '';
1144
+ return `<option value="${escapeHtml(id)}"${sel}>${escapeHtml(b.display_name || ('@' + b.github_login))}</option>`;
1145
+ }).join('');
1146
+ }
1147
+ // THE INVITE BOX SENDS AN INVITATION; IT DOES NOT ADD ANYONE (task 1002886).
1148
+ //
1149
+ // This control used to POST /goals/:id/members — the immediate force-add — and
1150
+ // then toast 'Invited.', which was simply untrue: the person was already in the
1151
+ // goal and had never been asked. Meanwhile the consented flow ADR 0112 is built
1152
+ // around (POST /goals/:id/invitations -> a pending row -> the invitee accepts)
1153
+ // had its receiving half fully wired in goal-inbox.js and NO sender anywhere in
1154
+ // the hall, so nothing in the product ever created an invitation.
1155
+ //
1156
+ // Now: the primary control invites, and the force-add survives as its own
1157
+ // explicitly-labelled affordance (owner ruling, 2026-09-16) so nobody loses a
1158
+ // capability they had — seeding a goal with a team who already agreed offline is
1159
+ // a real thing people do. The rule the owner set is that BOTH paths say what
1160
+ // they actually do; relocating the lie would have been worse than leaving it.
1161
+ //
1162
+ // NO MEMBERSHIP-KIND PICKER ON THE INVITE HALF, and that is not an oversight:
1163
+ // `goal_membership_requests` has no membership_kind column, so an accepted
1164
+ // invitation lands through addGoalMember's default 'member'. A picker here would
1165
+ // be a choice silently dropped on the floor — the same class of defect as the
1166
+ // toast it replaces. The form says so instead, and the picker stays on the
1167
+ // add-directly path, where it IS honoured.
1125
1168
  function renderInviteBox(goalId, canInvite, members) {
1126
1169
  const box = $('#goal-invite');
1127
1170
  if (!box) return;
@@ -1129,6 +1172,7 @@
1129
1172
  box.hidden = true;
1130
1173
  box.innerHTML = '';
1131
1174
  state.inviteOpen = false;
1175
+ state.addDirectOpen = false;
1132
1176
  return;
1133
1177
  }
1134
1178
  box.hidden = false;
@@ -1141,42 +1185,111 @@
1141
1185
  });
1142
1186
  return;
1143
1187
  }
1144
- const memberIds = new Set((members || []).map((m) => String(m.builder_id)));
1145
- const invitable = (state.directory || []).filter((b) => !memberIds.has(String(b.id)));
1146
- const options = invitable
1147
- .map((b) => `<option value="${escapeHtml(String(b.id))}">${escapeHtml(b.display_name || ('@' + b.github_login))}</option>`)
1148
- .join('');
1188
+ // This function rebuilds the box from scratch on every call, and it is called
1189
+ // again when the pending-request list lands (see loadGoalMembership). Carry the
1190
+ // half-filled form across that redraw, or a note someone is part-way through
1191
+ // typing vanishes under them.
1192
+ const keep = {
1193
+ builder: $('#goal-invite-builder')?.value || '',
1194
+ note: $('#goal-invite-note')?.value || '',
1195
+ direct: $('#goal-direct-builder')?.value || '',
1196
+ kind: $('#goal-direct-kind')?.value || 'member',
1197
+ };
1198
+ const invitable = invitableBuilders(members);
1199
+ const none = !invitable.length;
1200
+ const emptyLabel = 'Nobody left to invite';
1149
1201
  box.innerHTML = `
1150
1202
  <form class="goal-invite__form" id="goal-invite-form">
1151
- <select id="goal-invite-builder" required ${invitable.length ? '' : 'disabled'}>
1152
- ${invitable.length ? `<option value="" disabled selected>Choose a builder…</option>${options}`
1153
- : '<option value="">Everyone is already a member</option>'}
1154
- </select>
1155
- <select id="goal-invite-kind">
1156
- <option value="member">Member</option>
1157
- <option value="lead">Manager</option>
1203
+ <select id="goal-invite-builder" required ${none ? 'disabled' : ''}>
1204
+ ${none ? `<option value="">${emptyLabel}</option>`
1205
+ : `<option value="" disabled${keep.builder ? '' : ' selected'}>Choose a builder…</option>${builderOptionsHtml(invitable, keep.builder)}`}
1158
1206
  </select>
1159
- <button class="landing__btn" type="submit" ${invitable.length ? '' : 'disabled'}>Invite</button>
1207
+ <input id="goal-invite-note" type="text" maxlength="500" placeholder="Note (optional) — why you're asking" value="${escapeHtml(keep.note)}" ${none ? 'disabled' : ''}>
1208
+ <button class="landing__btn" type="submit" ${none ? 'disabled' : ''}>Send invitation</button>
1160
1209
  <button class="btn-ghost" id="goal-invite-cancel" type="button">Cancel</button>
1161
- </form>`;
1210
+ </form>
1211
+ <p class="goal-invite__hint">They join as a member once they accept — you can change that afterwards. Nobody is added until they do.</p>
1212
+ ${state.addDirectOpen ? `
1213
+ <form class="goal-invite__form goal-invite__direct" id="goal-direct-form">
1214
+ <select id="goal-direct-builder" required ${none ? 'disabled' : ''}>
1215
+ ${none ? `<option value="">${emptyLabel}</option>`
1216
+ : `<option value="" disabled${keep.direct ? '' : ' selected'}>Choose a builder…</option>${builderOptionsHtml(invitable, keep.direct)}`}
1217
+ </select>
1218
+ <select id="goal-direct-kind" ${none ? 'disabled' : ''}>
1219
+ <option value="member"${keep.kind === 'member' ? ' selected' : ''}>Member</option>
1220
+ <option value="lead"${keep.kind === 'lead' ? ' selected' : ''}>Manager</option>
1221
+ </select>
1222
+ <button class="btn-ghost" type="submit" ${none ? 'disabled' : ''}>Add without asking</button>
1223
+ <button class="btn-ghost" id="goal-direct-cancel" type="button">Cancel</button>
1224
+ </form>
1225
+ <p class="goal-invite__hint">They are added straight away and never asked. Use this only for someone who has already agreed.</p>`
1226
+ : '<button class="goal-invite__more" id="goal-direct-open" type="button">Or add someone without asking…</button>'}`;
1162
1227
  $('#goal-invite-cancel')?.addEventListener('click', () => {
1163
1228
  state.inviteOpen = false;
1229
+ state.addDirectOpen = false;
1164
1230
  renderInviteBox(goalId, canInvite, members);
1165
1231
  });
1166
1232
  $('#goal-invite-form')?.addEventListener('submit', (ev) => {
1167
1233
  ev.preventDefault();
1168
1234
  const builderId = $('#goal-invite-builder')?.value;
1169
- const membershipKind = $('#goal-invite-kind')?.value || 'member';
1170
- if (builderId) inviteMember(goalId, builderId, membershipKind);
1235
+ const note = $('#goal-invite-note')?.value || '';
1236
+ if (builderId) sendInvitation(goalId, builderId, note);
1237
+ });
1238
+ $('#goal-direct-open')?.addEventListener('click', () => {
1239
+ state.addDirectOpen = true;
1240
+ renderInviteBox(goalId, canInvite, members);
1241
+ });
1242
+ $('#goal-direct-cancel')?.addEventListener('click', () => {
1243
+ state.addDirectOpen = false;
1244
+ renderInviteBox(goalId, canInvite, members);
1245
+ });
1246
+ $('#goal-direct-form')?.addEventListener('submit', (ev) => {
1247
+ ev.preventDefault();
1248
+ const builderId = $('#goal-direct-builder')?.value;
1249
+ const membershipKind = $('#goal-direct-kind')?.value || 'member';
1250
+ if (builderId) addMemberDirectly(goalId, builderId, membershipKind);
1171
1251
  });
1172
1252
  }
1173
- async function inviteMember(goalId, builderId, membershipKind) {
1253
+ // The ADR 0112 sender. 201 means a PENDING row exists — not a membership — so
1254
+ // the toast may not claim the person is in the goal.
1255
+ async function sendInvitation(goalId, builderId, note) {
1256
+ try {
1257
+ const body = { builder_id: Number(builderId) };
1258
+ const trimmed = String(note || '').trim();
1259
+ if (trimmed) body.note = trimmed;
1260
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/invitations`, body);
1261
+ toast('Invitation sent — they join once they accept.');
1262
+ state.inviteOpen = false;
1263
+ state.addDirectOpen = false;
1264
+ await loadDetail(goalId, { force: true });
1265
+ renderDetail(goalId);
1266
+ renderList();
1267
+ } catch (err) {
1268
+ const code = err?.data?.error;
1269
+ // `invite_pending` and `already_a_member` are both filtered out of the
1270
+ // dropdown above, so reaching either means the goal changed under this form
1271
+ // — say what happened rather than a generic failure, because the name really
1272
+ // was offered a moment ago.
1273
+ if (code === 'invite_pending') toast('They already have an invitation or a join-request pending on this goal.');
1274
+ else if (code === 'already_a_member') toast('They joined this goal already.');
1275
+ else if (code === 'builder_not_found') toast('That builder no longer exists.');
1276
+ else if (code === 'archon_required_for_protected_scope') toast('This goal is scoped to a protected module — only an Archon may bring someone in.');
1277
+ else if (code === 'metic_required_for_protected_scope') toast('A protected-scope goal requires a Metic+ member.');
1278
+ else if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may invite.');
1279
+ else if (code === 'goal_not_open') toast('This goal is not open.');
1280
+ else toast('Could not send that invitation just now.');
1281
+ }
1282
+ }
1283
+ // The force-add, kept on purpose and labelled for what it is. Same endpoint the
1284
+ // '+ Invite' button used to call behind the word 'Invited.'
1285
+ async function addMemberDirectly(goalId, builderId, membershipKind) {
1174
1286
  try {
1175
1287
  // `membership_kind`, not `role` — ADR 0174 reserves that word for the three
1176
1288
  // crafts (task 1003013).
1177
1289
  await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/members`, { builder_id: Number(builderId), membership_kind: membershipKind });
1178
- toast('Invited.');
1290
+ toast('Added — they are in the goal now, without being asked.');
1179
1291
  state.inviteOpen = false;
1292
+ state.addDirectOpen = false;
1180
1293
  await loadDetail(goalId, { force: true });
1181
1294
  renderDetail(goalId);
1182
1295
  renderList();
@@ -1184,9 +1297,9 @@
1184
1297
  const code = err?.data?.error;
1185
1298
  if (code === 'archon_required_for_protected_scope') toast('This goal is scoped to a protected module — only an Archon may add members.');
1186
1299
  else if (code === 'metic_required_for_protected_scope') toast('A protected-scope goal requires a Metic+ member.');
1187
- else if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may invite.');
1300
+ else if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may add members.');
1188
1301
  else if (code === 'goal_not_open') toast('This goal is not open.');
1189
- else toast('Could not invite that builder just now.');
1302
+ else toast('Could not add that builder just now.');
1190
1303
  }
1191
1304
  }
1192
1305
  async function joinLeave(goalId, action) {
@@ -1324,7 +1437,16 @@
1324
1437
  if (!sameId(state.current, goalId)) return;
1325
1438
  renderPendingRequests();
1326
1439
  const d = currentDetail();
1327
- if (d) renderActions(d.goal, { joined: (d.members || []).some((m) => sameId(m.builder_id, selfId())), canManage });
1440
+ if (d) {
1441
+ renderActions(d.goal, { joined: (d.members || []).some((m) => sameId(m.builder_id, selfId())), canManage });
1442
+ // The invite dropdown's exclusion set reads state.pendingRequests, which
1443
+ // lands HERE — after renderDetail has already drawn the box. Without this
1444
+ // redraw a form opened before this resolved keeps offering a builder whose
1445
+ // invitation is already pending, which is the exact invisible 409 the
1446
+ // filter exists to prevent. renderInviteBox carries the half-filled form
1447
+ // across the redraw.
1448
+ if (state.inviteOpen) renderInviteBox(goalId, canManage, d.members || []);
1449
+ }
1328
1450
  }
1329
1451
  async function loadDetail(goalId, { force = false } = {}) {
1330
1452
  if (!force && state.details.has(String(goalId))) return state.details.get(String(goalId));
@@ -200,7 +200,22 @@
200
200
  collapsed behind "+ Invite" (disclosure, closed by default — task 2171). */
201
201
  .goal-invite { margin: 4px 0 10px; }
202
202
  .goal-invite__form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
203
- .goal-invite__form select#goal-invite-builder { flex: 1; min-width: 180px; }
203
+ .goal-invite__form select#goal-invite-builder,
204
+ .goal-invite__form select#goal-direct-builder { flex: 1; min-width: 180px; }
205
+ .goal-invite__form input#goal-invite-note { flex: 1; min-width: 180px; }
206
+
207
+ /* task 1002886 — BOTH admission paths now say what they do, so both carry a
208
+ line of their own: the invite half promises nobody is added until they
209
+ accept, the force-add half admits nobody is asked. Muted, because this is
210
+ the consequence of the button beside it, not a warning. */
211
+ .goal-invite__hint { margin: 4px 0 8px; font-size: 12.5px; color: var(--ink-faint); max-width: var(--measure); }
212
+ /* The force-add is deliberately QUIETER than the invite it sits under — same
213
+ authority, but it is the path that skips consent, so it does not get equal
214
+ visual weight. It stays one click away, never hidden. */
215
+ .goal-invite__more { background: none; border: 0; padding: 0; margin: 0 0 8px; cursor: pointer;
216
+ font: inherit; font-size: 12.5px; color: var(--ink-soft); text-decoration: underline; }
217
+ .goal-invite__more:hover { color: var(--ink); }
218
+ .goal-invite__direct { padding-top: 8px; border-top: var(--line) solid var(--rule-soft); }
204
219
 
205
220
  /* task 1953 — private goals: the lock badge (meta line) + the goal's own
206
221
  pending invite/join-request queue (manager view, next to the invite box). */
@@ -38,7 +38,7 @@
38
38
  (() => {
39
39
  'use strict';
40
40
 
41
- const API = '/api/gds';
41
+ const API = '/api/bongos';
42
42
  // NO baseUrl OVERRIDE — this page calls the GENERATED namespaced methods
43
43
  // (api.inbox.*, api.me.*), whose path templates are API-RELATIVE ('/inbox/template').
44
44
  // baseUrl:'' means "these paths are already absolute" and passes them through
@@ -81,7 +81,7 @@
81
81
  // - http(s):// or #anchor or mailto:/tel: → unchanged
82
82
  // - the diagrams dir → the in-hall diagrams route
83
83
  // - everything else → GitHub blob/tree (bases fetched
84
- // from /api/gds/public/repo-info; null until then → pass through, 404s
84
+ // from /api/bongos/public/repo-info; null until then → pass through, 404s
85
85
  // harmlessly rather than crashing).
86
86
  // opts: { baseDir, getBlobBase, getTreeBase, diagramsHref }
87
87
  function makeResolveLinkHref(opts) {
@@ -32,7 +32,7 @@
32
32
  <main id="hall-main">
33
33
  <!-- task 1003470: ONE directory. Its legacy door /scouting rendered this
34
34
  same page from the same script, and task 1003440 deleted it — the same
35
- /api/gds/scouting rollup used to be drawn three different ways. -->
35
+ /api/bongos/scouting rollup used to be drawn three different ways. -->
36
36
  <section class="scroll" id="people-scroll" aria-labelledby="people-h">
37
37
  <h2 id="people-h" class="scroll__h">Builder directory<span class="count-pill" id="people-count" hidden></span></h2>
38
38
  <div id="people-filters"></div>
@@ -2,7 +2,7 @@
2
2
  // 1002577; ADR 0141 §4 is the data's design of record), served at /people —
3
3
  // task 1003470 first made its legacy door /scouting render this same script,
4
4
  // and task 1003440 deleted that door (the URL 301s here), because the
5
- // same /api/gds/scouting rollup was being drawn three
5
+ // same /api/bongos/scouting rollup was being drawn three
6
6
  // different ways (a kit ledger here, a hand-built table there, and the same
7
7
  // columns in a different order). The endpoint is gated server-side on
8
8
  // 'project.curate' OR being a project owner (R14, ADR 0210); this page is
@@ -233,7 +233,7 @@
233
233
  body.textContent = msg;
234
234
  }
235
235
 
236
- fetch('/api/gds/scouting?limit=' + DIRECTORY_LIMIT, { headers: { Accept: 'application/json' } })
236
+ fetch('/api/bongos/scouting?limit=' + DIRECTORY_LIMIT, { headers: { Accept: 'application/json' } })
237
237
  .then(function (r) {
238
238
  if (r.status === 403 || r.status === 401) { throw { gated: true }; }
239
239
  if (!r.ok) throw new Error('http ' + r.status);