@bongos/core 1.19.803 → 1.19.805

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 (235) hide show
  1. package/.bongos-core.json +261 -236
  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/module-api-changelog.md +4 -0
  42. package/modules/builder-settings/profile-nudge-prefs.js +1 -1
  43. package/modules/builder-settings/render-prefs.js +1 -1
  44. package/modules/dev-box/app/README.md +1 -1
  45. package/modules/dev-box/app/src/gds-client.js +11 -9
  46. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  47. package/modules/dev-box/app/test/unit.mjs +13 -13
  48. package/modules/dev-box/box-access.js +3 -3
  49. package/modules/dev-box/box-onboard.js +7 -7
  50. package/modules/discord/bug-attachments.js +1 -1
  51. package/modules/economy/cost-classify.js +6 -6
  52. package/modules/grading/grader-rubric.json +1 -1
  53. package/modules/grading/grader-score.js +1 -1
  54. package/modules/hall-ui/module.json +1 -1
  55. package/modules/hall-ui/public/drachmae.js +3 -3
  56. package/modules/hall-ui/public/idea.js +1 -1
  57. package/modules/hall-ui/public/md-reader.js +1 -1
  58. package/modules/hall-ui/public/people.html +5 -1
  59. package/modules/hall-ui/public/people.js +44 -5
  60. package/modules/hall-ui/public/primer.js +4 -4
  61. package/modules/hall-ui/public/roster.js +1 -1
  62. package/modules/hall-ui/public/shell.js +1 -1
  63. package/modules/hall-ui/public/task.js +1 -1
  64. package/modules/ideas/inbox.js +2 -2
  65. package/modules/lifecycle/db-analytics.js +1 -1
  66. package/modules/lifecycle/done-when.js +4 -4
  67. package/modules/lifecycle/version-close.js +1 -1
  68. package/modules/memory/learnings.js +2 -2
  69. package/modules/memory/memory.js +1 -1
  70. package/modules/onboarding/onboarding.js +1 -1
  71. package/modules/platform-identity/routes/my-projects.js +1 -1
  72. package/modules/platform-identity/routes/sso.js +2 -2
  73. package/modules/public-landing/public/index.html +2 -2
  74. package/modules/public-landing/public/projects-floor.states.json +1 -1
  75. package/modules/public-landing/public/projects.html +1 -1
  76. package/modules/public-landing/public/projects.states.json +1 -1
  77. package/modules/sessions/db.js +120 -0
  78. package/modules/sessions/module.json +2 -2
  79. package/modules/sessions/pollers/session-upload-health.js +111 -0
  80. package/modules/sessions/pulse.js +2 -2
  81. package/modules/sessions/routes/sessions.js +46 -0
  82. package/modules/sessions/upload-health.js +197 -0
  83. package/modules/status-ui/module.json +1 -1
  84. package/modules/status-ui/public/explore.js +1 -1
  85. package/package-lock.json +2 -2
  86. package/package.json +1 -1
  87. package/scripts/gds/agent-invoke.js +3 -3
  88. package/scripts/gds/api.js +4 -4
  89. package/scripts/gds/api.sh +1 -1
  90. package/scripts/gds/architect-audit.js +1 -1
  91. package/scripts/gds/artifact-audit.js +2 -2
  92. package/scripts/gds/audit-ships.js +2 -2
  93. package/scripts/gds/backfill-cost.js +1 -1
  94. package/scripts/gds/backfill-sessions.js +1 -1
  95. package/scripts/gds/backfill-subagent-rewards.js +1 -1
  96. package/scripts/gds/backfill-task-rewards.js +2 -2
  97. package/scripts/gds/box-auth-check.js +1 -1
  98. package/scripts/gds/box-sync.js +3 -3
  99. package/scripts/gds/box.js +10 -10
  100. package/scripts/gds/build-cli-package.js +3 -3
  101. package/scripts/gds/capture.js +2 -2
  102. package/scripts/gds/ci-grade.js +1 -1
  103. package/scripts/gds/claim-recall.js +2 -2
  104. package/scripts/gds/cli-lib.js +3 -3
  105. package/scripts/gds/context-pack.js +1 -1
  106. package/scripts/gds/copy-apply.js +2 -2
  107. package/scripts/gds/dev-lib.js +2 -2
  108. package/scripts/gds/fetch-art-key.js +2 -2
  109. package/scripts/gds/fetch-sound-prefs.js +3 -3
  110. package/scripts/gds/grade-smoke.js +1 -1
  111. package/scripts/gds/hooks/pre-push.js +1 -1
  112. package/scripts/gds/init.js +9 -9
  113. package/scripts/gds/key-card.js +1 -1
  114. package/scripts/gds/learning-capture.js +2 -2
  115. package/scripts/gds/linkify-refs.js +2 -2
  116. package/scripts/gds/local-preview-lib.js +1 -1
  117. package/scripts/gds/login.js +11 -10
  118. package/scripts/gds/main-audit.js +1 -1
  119. package/scripts/gds/newcomer-floor.config.json +1 -1
  120. package/scripts/gds/newcomer-floor.js +2 -2
  121. package/scripts/gds/newcomer-floor.templates.json +2 -2
  122. package/scripts/gds/onboarding-ship.js +1 -1
  123. package/scripts/gds/paste-token.js +3 -3
  124. package/scripts/gds/portability-smoke-memory.sh +5 -5
  125. package/scripts/gds/prove-strict-versioning.js +2 -2
  126. package/scripts/gds/provision-net.js +2 -2
  127. package/scripts/gds/provision-units.js +3 -3
  128. package/scripts/gds/pulse.js +2 -2
  129. package/scripts/gds/push-path-brief.js +2 -2
  130. package/scripts/gds/reauth.js +1 -1
  131. package/scripts/gds/recall.js +1 -1
  132. package/scripts/gds/redteam-patrol.js +3 -3
  133. package/scripts/gds/render-ideas.js +5 -5
  134. package/scripts/gds/repo-metrics.js +1 -1
  135. package/scripts/gds/sandbox-stage.js +3 -3
  136. package/scripts/gds/seed-adopt-tasks.js +3 -3
  137. package/scripts/gds/seed-board-room-tasks.js +1 -1
  138. package/scripts/gds/seed-bongos-app-tasks.js +2 -2
  139. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
  140. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
  141. package/scripts/gds/seed-bongos-tasks.js +5 -5
  142. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
  143. package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
  144. package/scripts/gds/seed-bv1-community-tasks.js +1 -1
  145. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
  146. package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
  147. package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
  148. package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
  149. package/scripts/gds/seed-gds-v4-tasks.js +3 -3
  150. package/scripts/gds/seed-medusa-tasks.js +1 -1
  151. package/scripts/gds/seed-productization-tasks.js +1 -1
  152. package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
  153. package/scripts/gds/seed-provisioning-tasks.js +2 -2
  154. package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
  155. package/scripts/gds/seed-v2-world-tasks.js +2 -2
  156. package/scripts/gds/seed-verify-criteria.js +1 -1
  157. package/scripts/gds/session-digest.js +1 -1
  158. package/scripts/gds/session.js +2 -2
  159. package/scripts/gds/setup.js +7 -7
  160. package/scripts/gds/ship-flow.js +3 -3
  161. package/scripts/gds/ship-grade-steps.js +3 -3
  162. package/scripts/gds/ship-memory-sync.js +1 -1
  163. package/scripts/gds/ship-session-upload.js +3 -3
  164. package/scripts/gds/smoke-batch-claim.sh +10 -10
  165. package/scripts/gds/smoke-batch-release.sh +14 -14
  166. package/scripts/gds/smoke-claim-consistency.sh +19 -19
  167. package/scripts/gds/smoke-dependencies.sh +9 -9
  168. package/scripts/gds/smoke-write.sh +11 -11
  169. package/scripts/gds/start.js +1 -1
  170. package/scripts/gds/status.js +2 -2
  171. package/scripts/gds/strand-watch.js +1 -1
  172. package/scripts/gds/triage.js +4 -4
  173. package/scripts/gds/version-close.js +2 -2
  174. package/scripts/hall-preview/README.md +1 -1
  175. package/scripts/hall-preview/server.js +1 -1
  176. package/src/bongos/auth-admission.js +3 -3
  177. package/src/bongos/auth-idp.js +2 -2
  178. package/src/bongos/auth.js +3 -3
  179. package/src/bongos/db.js +2 -2
  180. package/src/bongos/llm-cache.js +2 -2
  181. package/src/bongos/metrics.js +1 -1
  182. package/src/bongos/middleware/audit.js +3 -3
  183. package/src/bongos/middleware/rate-limit.js +1 -1
  184. package/src/bongos/platform-visibility-gate.js +2 -2
  185. package/src/bongos/routes/_helpers.js +1 -1
  186. package/src/bongos/routes/audit-log.js +1 -1
  187. package/src/bongos/routes/auth.js +5 -5
  188. package/src/bongos/routes/backup.js +1 -1
  189. package/src/bongos/routes/builders.js +1 -1
  190. package/src/bongos/routes/instance.js +1 -1
  191. package/src/bongos/routes/live.js +1 -1
  192. package/src/bongos/routes/me.js +2 -2
  193. package/src/bongos/routes/modules.js +2 -2
  194. package/src/bongos/routes/public.js +1 -1
  195. package/src/bongos/routes/security.js +4 -4
  196. package/src/bongos/routes.js +1 -1
  197. package/src/bongos/serve-internal.js +6 -6
  198. package/src/branding.js +1 -1
  199. package/src/module-api.js +1 -1
  200. package/src/module-seams.js +2 -2
  201. package/src/platform-server.js +2 -2
  202. package/tests/api_alias_exceptions.mjs +67 -0
  203. package/tests/application_echo.mjs +12 -12
  204. package/tests/artifact_audit.mjs +1 -1
  205. package/tests/auth_page_gate.mjs +4 -4
  206. package/tests/auth_web_recovery.mjs +11 -11
  207. package/tests/bongos_login.mjs +7 -4
  208. package/tests/box_env.mjs +6 -6
  209. package/tests/bug_attachments.mjs +1 -1
  210. package/tests/cli_sessions.mjs +1 -1
  211. package/tests/dev_lib.mjs +1 -1
  212. package/tests/discord_bugs.mjs +2 -2
  213. package/tests/discord_inbound.mjs +2 -2
  214. package/tests/explore_visible_states.mjs +1 -1
  215. package/tests/government_page_gates.mjs +1 -1
  216. package/tests/hall_reference_world.mjs +1 -1
  217. package/tests/idea_ideate_full.mjs +1 -1
  218. package/tests/idp_federation.mjs +2 -2
  219. package/tests/llm-cache.mjs +1 -1
  220. package/tests/manage_manifest_shared_read.mjs +3 -3
  221. package/tests/people_directory_clip.mjs +219 -0
  222. package/tests/planning_session_skill.mjs +1 -1
  223. package/tests/platform_visibility_gate.mjs +15 -15
  224. package/tests/profile_route.mjs +37 -26
  225. package/tests/projects_hub.mjs +2 -2
  226. package/tests/provision.mjs +2 -2
  227. package/tests/provision_settings_apply.mjs +4 -4
  228. package/tests/sandbox_stage.mjs +8 -8
  229. package/tests/scouting_owner_access_and_reach.mjs +3 -3
  230. package/tests/session_upload_health.mjs +329 -0
  231. package/tests/setup_device_flow_fallback.mjs +1 -1
  232. package/tests/skill_ask_for_help.mjs +3 -3
  233. package/tests/skill_grade_audit.mjs +2 -2
  234. package/tests/skill_grader_health.mjs +3 -3
  235. package/tests/smoke_write_contract.mjs +1 -1
@@ -1426,7 +1426,7 @@ test('what the project RUNS comes only from its own manifest, and every answer i
1426
1426
  const runs = fnHub('loadVisRuns');
1427
1427
  const manifest = fnHub('ensureManifest');
1428
1428
  assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
1429
- assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/gds\/instance'/,
1429
+ assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/bongos\/instance'/,
1430
1430
  'the same public GET /instance the platform’s runner reads after a restart');
1431
1431
  assert.doesNotMatch(runs, /fetch\(/, 'the card fires no GET of its own — one manifest read serves the pair (task 1003554)');
1432
1432
  assert.match(runs, /m\.brand && m\.brand\.project/);
@@ -1600,7 +1600,7 @@ test('the join door is read on entry + tab return, like every other settings sec
1600
1600
  // door yet, and a line that could only ever say "your project doesn’t report
1601
1601
  // this" is worse than no line. The read-back lands with the enforcement.
1602
1602
  const load = fnHub('loadDoor');
1603
- assert.doesNotMatch(load, /api\/gds\/instance/, 'this section asks the project’s manifest nothing');
1603
+ assert.doesNotMatch(load, /api\/bongos\/instance/, 'this section asks the project’s manifest nothing');
1604
1604
  });
1605
1605
 
1606
1606
  test('the joinability choices are the platform’s vocabulary, each in plain words, and the pick is a real radio group', () => {
@@ -220,7 +220,7 @@ t('webEnvBody: an instance is never pinned to the neutral pack, so it serves its
220
220
  // into every instance's web.env — a leftover from the shared-checkout co-tenant model. Under
221
221
  // ADR 0108 each instance has its own checkout and its own config/branding.json, and
222
222
  // src/branding.js's ENV_OVERRIDES cover domains/auth.idp/landing but NEVER identity.productName.
223
- // So the pin made every hosted instance answer GET /api/gds/instance with the PLATFORM's name:
223
+ // So the pin made every hosted instance answer GET /api/bongos/instance with the PLATFORM's name:
224
224
  // hermeslines-marketing, mercury and demo all said "Cloud Bongos" while their own packs on disk
225
225
  // said otherwise, and `bongos login <instance>` greeted newcomers with the wrong project.
226
226
  const b = P.webEnvBody({ ...coTenant, domain: 'mercury.example.com' });
@@ -262,7 +262,7 @@ t('identityVerdict: a live instance serving the PLATFORM name is caught, not shr
262
262
 
263
263
  t('identityCmd: probes the manifest over loopback, like healthzCmd', () => {
264
264
  const N = require('../scripts/gds/provision-net.js');
265
- assert.match(N.identityCmd({ port: 3011 }), /127\.0\.0\.1:3011\/api\/gds\/instance/);
265
+ assert.match(N.identityCmd({ port: 3011 }), /127\.0\.0\.1:3011\/api\/bongos\/instance/);
266
266
  });
267
267
 
268
268
  t('webEnvBody: a domain writes the single-host origin overrides so the hall loads (task 1972)', () => {
@@ -151,7 +151,7 @@ t('null (unknown) when the manifest carries no project block — a core that pre
151
151
  assert.equal(P.settingsConsumed(JSON.stringify({ brand: { project: 'nope' } }), GATED), null);
152
152
  });
153
153
  t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
154
- assert.equal(P.instanceManifestCmd({ port: 3010 }), 'curl -sS --max-time 5 http://127.0.0.1:3010/api/gds/instance');
154
+ assert.equal(P.instanceManifestCmd({ port: 3010 }), 'curl -sS --max-time 5 http://127.0.0.1:3010/api/bongos/instance');
155
155
  });
156
156
 
157
157
  // ── the handler ──────────────────────────────────────────────────────────────
@@ -166,7 +166,7 @@ function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, health
166
166
  cmds.push(cmd);
167
167
  if (/\bcat /.test(cmd)) return readFails ? { ok: true, stdout: '', softFailed: true } : { ok: true, stdout: envBody };
168
168
  if (/healthz/.test(cmd)) return healthzFails ? { ok: true, stdout: '', softFailed: true } : { ok: true, stdout: '{"ok":true}' };
169
- if (/api\/gds\/instance/.test(cmd)) return { ok: true, stdout: manifestOut };
169
+ if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestOut };
170
170
  if (/systemctl restart/.test(cmd) && restartThrows && apply) throw new Error(`Command failed: ${cmd}`);
171
171
  return { ok: true, stdout: '' };
172
172
  };
@@ -218,7 +218,7 @@ await ta('--apply, active standalone: read web.env → patch ONLY the settings l
218
218
  assert.ok(idx(/systemctl restart spike/) >= 0, 'RESTART (not reload) so the process re-reads its env');
219
219
  assert.ok(idx(/cat /) < idx(/restart spike/), 'read before restart');
220
220
  assert.ok(idx(/restart spike/) < idx(/healthz/), 'healthz after the restart');
221
- assert.ok(idx(/healthz/) < idx(/api\/gds\/instance/), 'the read-back only after it answers');
221
+ assert.ok(idx(/healthz/) < idx(/api\/bongos\/instance/), 'the read-back only after it answers');
222
222
  assert.ok(!cmds.some((c) => /systemctl (stop|disable)|caddy|box-dns/.test(c)), 'no teardown / DNS / caddy legs — settings only');
223
223
  });
224
224
 
@@ -355,7 +355,7 @@ await ta('--apply: a restart that never answers /healthz is a VERDICT after the
355
355
  assert.deepEqual(probes, [{ id: 18, up: false }]);
356
356
  assert.match(events[0].detail, /not answering \/healthz yet/);
357
357
  assert.equal(cmds.filter((c) => /healthz/.test(c)).length, MANIFEST_VERIFY_TRIES, 'the same patience as the manifest probe (F23) — a slow boot is not slandered');
358
- assert.ok(!cmds.some((c) => /api\/gds\/instance/.test(c)), 'no manifest probe when it never answered');
358
+ assert.ok(!cmds.some((c) => /api\/bongos\/instance/.test(c)), 'no manifest probe when it never answered');
359
359
  });
360
360
 
361
361
  await ta('--apply: a domainless instance that answers records up=true immediately (provisionInstance step-7 rule)', async () => {
@@ -242,9 +242,9 @@ function boxDeps(markerFile, overrides = {}) {
242
242
  return '';
243
243
  },
244
244
  // task 1002607: emulate a real PREVIEW — 200 on the health URL, 404 on the
245
- // /api/gds/healthz discriminator (the game-only entry mounts no /api/gds).
245
+ // /api/bongos/healthz discriminator (the game-only entry mounts no /api/bongos).
246
246
  // A flat always-200 mock would now read as "the MAIN server answered".
247
- fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
247
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/bongos/healthz') ? 404 : 200 }),
248
248
  markerPath: markerFile,
249
249
  cwd: tmp,
250
250
  pollMs: 1,
@@ -541,7 +541,7 @@ await ta('1002607 waitHealthy: 404 at the health URL is NOT healthy (was: anythi
541
541
 
542
542
  await ta('1002607 waitHealthy: a 200 answered by the MAIN server (api healthz 200) is an impostor, not a healthy preview', async () => {
543
543
  // The full server binds the same 127.0.0.1:3000 — root answers 200, and so does
544
- // /api/gds/healthz. The game-only preview (ADR 0052) would 404 the latter.
544
+ // /api/bongos/healthz. The game-only preview (ADR 0052) would 404 the latter.
545
545
  const r = await sbx.waitHealthy({ fetchImpl: async () => ({ status: 200 }), timeoutMs: 1000, pollMs: 1 });
546
546
  assert.equal(r.ok, false, 'a dead preview must not report healthy off the main server');
547
547
  assert.match(r.reason, /MAIN server/);
@@ -550,7 +550,7 @@ await ta('1002607 waitHealthy: a 200 answered by the MAIN server (api healthz 20
550
550
 
551
551
  await ta('1002607 waitHealthy: a real preview (root 200, api healthz 404) is healthy', async () => {
552
552
  const r = await sbx.waitHealthy({
553
- fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
553
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/bongos/healthz') ? 404 : 200 }),
554
554
  timeoutMs: 1000, pollMs: 1,
555
555
  });
556
556
  assert.equal(r.ok, true);
@@ -559,7 +559,7 @@ await ta('1002607 waitHealthy: a real preview (root 200, api healthz 404) is hea
559
559
  await ta('1002607 waitHealthy: a discriminator probe that ERRORS counts as "not the main server" (preview healthy)', async () => {
560
560
  const r = await sbx.waitHealthy({
561
561
  fetchImpl: async (url) => {
562
- if (String(url).includes('/api/gds/healthz')) throw new Error('ECONNREFUSED on a path the preview never mounts');
562
+ if (String(url).includes('/api/bongos/healthz')) throw new Error('ECONNREFUSED on a path the preview never mounts');
563
563
  return { status: 200 };
564
564
  },
565
565
  timeoutMs: 1000, pollMs: 1,
@@ -568,8 +568,8 @@ await ta('1002607 waitHealthy: a discriminator probe that ERRORS counts as "not
568
568
  });
569
569
 
570
570
  t('1002607 apiHealthzProbeUrl: same origin as the health URL, api path; null on garbage', () => {
571
- assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000/api/gds/healthz');
572
- assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3100/healthz'), 'http://127.0.0.1:3100/api/gds/healthz');
571
+ assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000/api/bongos/healthz');
572
+ assert.equal(sbx.apiHealthzProbeUrl('http://127.0.0.1:3100/healthz'), 'http://127.0.0.1:3100/api/bongos/healthz');
573
573
  assert.equal(sbx.apiHealthzProbeUrl('not a url'), null);
574
574
  });
575
575
 
@@ -599,7 +599,7 @@ function localDeps(markerFile, overrides = {}) {
599
599
  },
600
600
  execFileSync: (cmd, args, opts) => (cmd === 'git' ? execFileSync(cmd, args, opts) : ''),
601
601
  // task 1002607: preview-shaped mock — health 200, API-healthz discriminator 404.
602
- fetchImpl: async (url) => ({ status: String(url).includes('/api/gds/healthz') ? 404 : 200 }),
602
+ fetchImpl: async (url) => ({ status: String(url).includes('/api/bongos/healthz') ? 404 : 200 }),
603
603
  markerPath: markerFile,
604
604
  cwd: tmp,
605
605
  pollMs: 1,
@@ -364,7 +364,7 @@ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
364
364
 
365
365
  const app = express();
366
366
  app.use(attachFail);
367
- app.use('/api/gds', buildScoutingRoutes());
367
+ app.use('/api/bongos', buildScoutingRoutes());
368
368
  const server = app.listen(0, '127.0.0.1');
369
369
  await new Promise((r) => server.once('listening', r));
370
370
  const port = server.address().port;
@@ -381,7 +381,7 @@ function getScouting(caller, query = '') {
381
381
  currentCaller = caller;
382
382
  calls = [];
383
383
  return new Promise((resolve, reject) => {
384
- const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: `/api/gds/scouting${query}` }, (res) => {
384
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: `/api/bongos/scouting${query}` }, (res) => {
385
385
  const chunks = [];
386
386
  res.on('data', (c) => chunks.push(c));
387
387
  res.on('end', () => resolve({ status: res.statusCode, text: Buffer.concat(chunks).toString('utf8') }));
@@ -606,7 +606,7 @@ await t('CALIBRATION: the interpreter refuses what it cannot model, rather than
606
606
 
607
607
  await t('/people is the SAME endpoint, so "absent from /people" is true by construction', async () => {
608
608
  const page = fs.readFileSync(path.join(repo, 'modules/hall-ui/public/people.js'), 'utf8');
609
- assert.match(page, /fetch\('\/api\/gds\/scouting/,
609
+ assert.match(page, /fetch\('\/api\/bongos\/scouting/,
610
610
  'the hall directory reads GET /scouting; if it ever grows its own query, this proof stops covering it');
611
611
  const source = fs.readFileSync(path.join(repo, 'modules/platform-identity/platform-identity.js'), 'utf8');
612
612
  assert.match(source, /scoutingListableSql\('pa'\)/,
@@ -0,0 +1,329 @@
1
+ // tests/session_upload_health.mjs — the session-upload outage signal
2
+ // (task 1003865, goal 1000118).
3
+ //
4
+ // THE INCIDENT. A Windows path-encoding bug made every transcript lookup miss, so
5
+ // each ship skipped its session upload with `transcript_not_found`. No
6
+ // session_records row was written, so no `session.token_reward` credit booked, and
7
+ // since ADR 0146 made cost-plus the SOLE reward stream the affected builder earned
8
+ // nothing for real shipped work — for weeks, before a human noticed. The only
9
+ // trace was a jsonl file on that builder's own laptop, which never reaches the
10
+ // server: from here, a skipped upload and a builder who did not work are the same
11
+ // absence.
12
+ //
13
+ // WHAT THIS FILE PINS, in order of what would actually go wrong:
14
+ // (1) the firing condition, against the REAL outage window's measured numbers;
15
+ // (2) that it stays quiet on the real healthy months, because a check that
16
+ // cries wolf is the state we started in with extra steps;
17
+ // (3) that the predicate never touches session_records.task_ids, which is EMPTY
18
+ // on 54% of rows and would report the linkage gap as an outage;
19
+ // (4) that the alert names the builder, the arithmetic, and the consequence.
20
+ //
21
+ // Run: node tests/session_upload_health.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { test } from 'node:test';
25
+ import { createRequire } from 'node:module';
26
+
27
+ process.env.NODE_ENV = 'test';
28
+ const require = createRequire(import.meta.url);
29
+ const health = require('../modules/sessions/upload-health.js');
30
+ const poller = require('../modules/sessions/pollers/session-upload-health.js');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // (1)+(2) THE REAL NUMBERS.
34
+ //
35
+ // Measured 2026-09-16 against the live corpora (4,495 session records and 2,640
36
+ // ships carrying shipped_at + shipped_by, paged out of /sessions/search and
37
+ // /tasks), replaying the check as a rolling 14-day lookback stepped one day at a
38
+ // time over the ~92 evaluable days. These are per-builder counts within one
39
+ // lookback, not inventions: the OUTAGE rows are the real peak, and the HEALTHY
40
+ // rows are the real 2026-09 shape the same sweep saw.
41
+ //
42
+ // The COUNTS are the measurement and are exact. The logins are NOT — real builder
43
+ // identities have no business in a fixture (audit-authorship.sh forbids the
44
+ // founder's outright, and the others are people too), so they are anonymised to
45
+ // the roles that matter: the builder whose uploads broke, and the ones whose
46
+ // did not.
47
+ // ---------------------------------------------------------------------------
48
+
49
+ // 2026-07-25, the peak of the outage: one builder, 19 ships, not one covered.
50
+ // The row's own account says "at least 16 ships" — this found 19 without being
51
+ // told where to look.
52
+ const OUTAGE_PEAK = [
53
+ { builder_id: '900001', github_login: 'affected-builder', ships: 19, uncovered: 19 },
54
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 41, uncovered: 0 },
55
+ ];
56
+
57
+ // 2026-08-05, the last day the sweep fired — a PARTIAL outage, which is the shape
58
+ // that matters most: a check tuned only to total failure would have gone quiet
59
+ // here while the builder was still losing pay.
60
+ const OUTAGE_TAIL = [
61
+ { builder_id: '900001', github_login: 'affected-builder', ships: 30, uncovered: 11 },
62
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 22, uncovered: 0 },
63
+ ];
64
+
65
+ // A healthy fortnight: the real 2026-09 shape (0.2% corpus-wide uncovered).
66
+ const HEALTHY = [
67
+ { builder_id: '900003', github_login: 'third-builder', ships: 200, uncovered: 0 },
68
+ { builder_id: '900001', github_login: 'affected-builder', ships: 147, uncovered: 1 },
69
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 78, uncovered: 0 },
70
+ ];
71
+
72
+ test('it fires on the real outage peak, and says whose machine', () => {
73
+ const v = health.uploadHealthVerdict(OUTAGE_PEAK);
74
+ assert.equal(v.firing, true, 'the outage this task was filed about must fire');
75
+ assert.equal(v.status, 'uploads-missing');
76
+ assert.equal(v.offenders.length, 1, 'exactly the one builder, not the whole roster');
77
+ assert.equal(v.offenders[0].github_login, 'affected-builder');
78
+ // Naming the builder is what makes it actionable — the failure was one machine.
79
+ assert.match(v.reason, /affected-builder/);
80
+ assert.match(v.reason, /19 of 19/, 'carries its own arithmetic');
81
+ // And it must say what a missing row COSTS, or it reads as missing telemetry.
82
+ assert.match(v.reason, /reward is not booking/i);
83
+ });
84
+
85
+ test('it still fires on the partial tail of the same outage', () => {
86
+ // 11 of 30 is 36.7% — well under half. A threshold set at "mostly broken" would
87
+ // have gone quiet here while the builder was still losing pay for real ships.
88
+ const v = health.uploadHealthVerdict(OUTAGE_TAIL);
89
+ assert.equal(v.firing, true);
90
+ assert.equal(v.offenders[0].github_login, 'affected-builder');
91
+ assert.ok(v.offenders[0].uncovered_share < 0.5, 'and it is genuinely a partial outage');
92
+ });
93
+
94
+ test('it stays quiet on the real healthy month', () => {
95
+ const v = health.uploadHealthVerdict(HEALTHY);
96
+ assert.equal(v.firing, false, 'a healthy fortnight must not page anyone');
97
+ assert.equal(v.status, 'healthy');
98
+ assert.deepEqual(v.offenders, []);
99
+ });
100
+
101
+ test('THE CORPUS-WIDE SHARE IS THE WRONG STATISTIC — the reason for per-builder', () => {
102
+ // The design decision, pinned because it is not obvious and the obvious version
103
+ // is worse than nothing. Folded into one row the way a corpus-wide check would
104
+ // fold it, the real outage peak is 19 uncovered out of 60 ships — and its own
105
+ // tail is 11 of 52, which a single ratio cannot separate from noise. Measured
106
+ // corpus-wide, the outage MONTH ran at 3.4% against 1.7-2.0% in healthy ones.
107
+ const folded = (rows) => [{
108
+ builder_id: 'all', github_login: null,
109
+ ships: rows.reduce((a, r) => a + r.ships, 0),
110
+ uncovered: rows.reduce((a, r) => a + r.uncovered, 0),
111
+ }];
112
+ assert.equal(health.uploadHealthVerdict(folded(OUTAGE_TAIL)).firing, false,
113
+ 'folded into one number, the real outage tail does not fire — which is why the SQL groups by builder');
114
+ // And the same rows, kept per builder, do fire. Same data, different key.
115
+ assert.equal(health.uploadHealthVerdict(OUTAGE_TAIL).firing, true);
116
+ });
117
+
118
+ test('a quiet fortnight is refused, not judged', () => {
119
+ // Three uncovered out of four is 75% and means nothing. A check that fires on
120
+ // someone's slow week teaches people to mute it, which is the state this task
121
+ // is fixing.
122
+ const v = health.uploadHealthVerdict([
123
+ { builder_id: '9', github_login: 'someone', ships: 4, uncovered: 4 },
124
+ ]);
125
+ assert.equal(v.firing, false);
126
+ assert.equal(v.status, 'insufficient-data');
127
+ assert.match(v.reason, /not enough to judge/);
128
+ // But the SAME share over enough ships is the outage.
129
+ const loud = health.uploadHealthVerdict([
130
+ { builder_id: '9', github_login: 'someone', ships: 20, uncovered: 15 },
131
+ ]);
132
+ assert.equal(loud.firing, true);
133
+ });
134
+
135
+ test('garbage counts cannot manufacture an outage', () => {
136
+ // These rows come from SQL, but the verdict decides whether someone is woken
137
+ // up: an uncovered count above the ship count is a query bug, not an outage
138
+ // several times over.
139
+ const v = health.uploadHealthVerdict([
140
+ { builder_id: '9', github_login: 'x', ships: 20, uncovered: 400 },
141
+ { builder_id: null, github_login: 'ghost', ships: 99, uncovered: 99 },
142
+ null,
143
+ ]);
144
+ assert.equal(v.builders.length, 1, 'a row with no builder is dropped, not trusted');
145
+ assert.equal(v.builders[0].uncovered, 20, 'uncovered is clamped to ships');
146
+ assert.equal(health.uploadHealthVerdict(null).status, 'insufficient-data', 'and no rows at all is not an outage');
147
+ assert.equal(health.uploadHealthVerdict([]).firing, false);
148
+ });
149
+
150
+ test('the clamps bound what reaches an SQL interval', () => {
151
+ assert.equal(health.clampLookbackDays(undefined), health.DEFAULT_LOOKBACK_DAYS);
152
+ assert.equal(health.clampLookbackDays('nonsense'), health.DEFAULT_LOOKBACK_DAYS);
153
+ assert.equal(health.clampLookbackDays(1e9), 365, 'a hostile ?days cannot ask for the whole history');
154
+ assert.equal(health.clampLookbackDays(-5), 1);
155
+ assert.equal(health.clampWindowHours(1e9), 720);
156
+ assert.equal(health.clampWindowHours(0), 1);
157
+ assert.equal(health.clampSampleLimit(null), 20);
158
+ assert.equal(health.clampSampleLimit(10_000), 100, 'the sample is evidence, never the corpus');
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // (3) THE PREDICATE, as SQL — the trap the scoping task measured and warned about.
163
+ // ---------------------------------------------------------------------------
164
+
165
+ test('the emitted SQL keys on builder_id + time and never on task_ids', async () => {
166
+ // Captured from the query the db slice ACTUALLY runs, not grepped from source:
167
+ // both files mention task_ids in prose explaining why they do not use it, so a
168
+ // source scan would pass on a version that used it.
169
+ const api = require('../src/module-api.js');
170
+ const db = require('../modules/sessions/db.js');
171
+ const seen = [];
172
+ const prev = api.pool.query;
173
+ api.pool.query = async (sql, params) => { seen.push({ sql: String(sql), params }); return { rows: [] }; };
174
+ try {
175
+ await db.sessionUploadCoverage({ days: 14, windowHours: 24 });
176
+ await db.shipsMissingSessionRecord({ days: 14, windowHours: 24, limit: 20 });
177
+ } finally {
178
+ api.pool.query = prev;
179
+ }
180
+ assert.equal(seen.length, 2, 'both queries ran');
181
+ for (const { sql, params } of seen) {
182
+ assert.ok(!/task_ids/.test(sql), 'task_ids is EMPTY on 54% of rows — keying on it reports the linkage gap as an outage');
183
+ assert.match(sql, /sr\.builder_id = t\.shipped_by/, 'keyed on the builder');
184
+ assert.match(sql, /sr\.uploaded_at >=[\s\S]*sr\.uploaded_at <=/, 'and bounded by a time window on both sides');
185
+ assert.match(sql, /status = 'shipped'/, 'over ships, which is the half the server knows for certain');
186
+ // The interval inputs are BOUND, never interpolated — they come from a URL.
187
+ assert.ok(sql.includes("$1 * interval '1 day'"), 'the lookback is a bound parameter');
188
+ assert.ok(sql.includes("$2 * interval '1 hour'"), 'and so is the window');
189
+ assert.equal(params[0], 14);
190
+ assert.equal(params[1], 24);
191
+ }
192
+ });
193
+
194
+ test('the clamps are applied by the db slice, not merely available to it', async () => {
195
+ // The route is not the only caller, so a clamp the route applies is not a bound.
196
+ const api = require('../src/module-api.js');
197
+ const db = require('../modules/sessions/db.js');
198
+ const seen = [];
199
+ const prev = api.pool.query;
200
+ api.pool.query = async (sql, params) => { seen.push({ sql: String(sql), params }); return { rows: [] }; };
201
+ try {
202
+ await db.sessionUploadCoverage({ days: 1e9, windowHours: 1e9 });
203
+ await db.shipsMissingSessionRecord({ days: 1e9, windowHours: 1e9, limit: 1e9 });
204
+ } finally {
205
+ api.pool.query = prev;
206
+ }
207
+ assert.deepEqual(seen[0].params.slice(0, 2), [365, 720]);
208
+ assert.deepEqual(seen[1].params.slice(0, 2), [365, 720]);
209
+ assert.match(seen[1].sql, /LIMIT 100\b/, 'the sample limit is clamped into the SQL, not passed through');
210
+ });
211
+
212
+ test('the healthy path costs one query — the sample is only fetched when it exists', async () => {
213
+ const api = require('../src/module-api.js');
214
+ const db = require('../modules/sessions/db.js');
215
+ const prev = api.pool.query;
216
+ let calls = 0;
217
+ api.pool.query = async () => {
218
+ calls += 1;
219
+ return { rows: calls === 1 ? [{ builder_id: '3', github_login: 'x', ships: 40, uncovered: 0 }] : [] };
220
+ };
221
+ try {
222
+ const clean = await db.sessionUploadHealth({});
223
+ assert.equal(calls, 1, 'nothing uncovered, so no sample query');
224
+ assert.equal(clean.verdict.firing, false);
225
+ assert.deepEqual(clean.sample, []);
226
+
227
+ calls = 0;
228
+ api.pool.query = async () => {
229
+ calls += 1;
230
+ return { rows: calls === 1 ? [{ builder_id: '3', github_login: 'x', ships: 40, uncovered: 30 }] : [{ id: '7' }] };
231
+ };
232
+ const bad = await db.sessionUploadHealth({});
233
+ assert.equal(calls, 2, 'something uncovered, so the evidence is fetched');
234
+ assert.equal(bad.verdict.firing, true);
235
+ assert.deepEqual(bad.sample, [{ id: '7' }]);
236
+ } finally {
237
+ api.pool.query = prev;
238
+ }
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // (4) THE ALERT. A line that says "unhealthy" and no more is the same silence
243
+ // with a timestamp on it.
244
+ // ---------------------------------------------------------------------------
245
+
246
+ test('the firing log line names the builder, the cost, and where to look', () => {
247
+ const lines = poller.logLinesFor({
248
+ days: 14,
249
+ verdict: health.uploadHealthVerdict(OUTAGE_PEAK),
250
+ sample: [{ id: '1002101' }, { id: '1002100' }],
251
+ }).join('\n');
252
+ assert.match(lines, /ALERT/);
253
+ assert.match(lines, /affected-builder/, 'whose machine');
254
+ assert.match(lines, /19 of 19/, 'the arithmetic');
255
+ assert.match(lines, /unpaid shipped work/, 'what it costs — not "missing telemetry"');
256
+ assert.match(lines, /sessions\/upload-health/, 'where to look next');
257
+ assert.match(lines, /session-upload-skips\.jsonl/, 'and the likely client-side cause');
258
+ assert.match(lines, /#1002101/, 'with the uncovered ships named, so the unpaid work is recoverable');
259
+ });
260
+
261
+ test('the quiet log line is one line, and says which quiet it is', () => {
262
+ const quiet = poller.logLinesFor({ days: 14, verdict: health.uploadHealthVerdict(HEALTHY), sample: [] });
263
+ assert.equal(quiet.length, 1, 'a healthy sweep must not be noisy — hourly, that is 24 lines a day');
264
+ assert.match(quiet[0], /healthy/);
265
+ // 'insufficient-data' must not read as healthy: they are different situations
266
+ // and only one of them is evidence that uploads work.
267
+ const thin = poller.logLinesFor({ days: 14, verdict: health.uploadHealthVerdict([]), sample: [] });
268
+ assert.match(thin[0], /insufficient-data/);
269
+ });
270
+
271
+ test('runSweep is the whole chain, and the loader calls IT — not the helpers', async () => {
272
+ // The (helper-nothing-calls) lesson: logLinesFor and sessionUploadHealth can
273
+ // both be perfect while the poller the loader actually starts reaches neither.
274
+ // Drive the entry point end to end against a patched pool and read what it
275
+ // would have logged.
276
+ const api = require('../src/module-api.js');
277
+ const prevQuery = api.pool.query;
278
+ const prevLogger = api.logger;
279
+ const emitted = [];
280
+ api.logger = () => ({
281
+ info: (...a) => emitted.push(['info', a.map(String).join(' ')]),
282
+ error: (...a) => emitted.push(['error', a.map(String).join(' ')]),
283
+ });
284
+ let calls = 0;
285
+ api.pool.query = async () => {
286
+ calls += 1;
287
+ return {
288
+ rows: calls === 1
289
+ ? [{ builder_id: '900001', github_login: 'affected-builder', ships: 19, uncovered: 19 }]
290
+ : [{ id: '1002101' }],
291
+ };
292
+ };
293
+ // The logger is resolved at module load, so re-import the poller under the stub.
294
+ const fresh = require('node:module').createRequire(import.meta.url);
295
+ delete fresh.cache[fresh.resolve('../modules/sessions/pollers/session-upload-health.js')];
296
+ const live = fresh('../modules/sessions/pollers/session-upload-health.js');
297
+ try {
298
+ const health = await live.runSweep();
299
+ assert.equal(health.verdict.firing, true, 'the sweep reaches the verdict');
300
+ assert.equal(calls, 2, 'and fetches the evidence when there is any');
301
+ const errors = emitted.filter(([lvl]) => lvl === 'error');
302
+ assert.ok(errors.length >= 1, 'a firing sweep logs at ERROR so a level filter cannot hide it');
303
+ assert.match(errors.map(([, m]) => m).join('\n'), /ALERT[\s\S]*affected-builder/,
304
+ 'and what it logs is the alert, not a bare status');
305
+ } finally {
306
+ api.pool.query = prevQuery;
307
+ api.logger = prevLogger;
308
+ delete fresh.cache[fresh.resolve('../modules/sessions/pollers/session-upload-health.js')];
309
+ }
310
+ });
311
+
312
+ test('the poller reports and never writes', () => {
313
+ // It runs beside the reward reconciler, which MOVES MONEY. A telemetry sweep
314
+ // that could book, delete or update anything would need its own safety
315
+ // argument; this one needs none, and that must stay true.
316
+ const src = require('node:fs').readFileSync(
317
+ new URL('../modules/sessions/pollers/session-upload-health.js', import.meta.url), 'utf8');
318
+ const code = src.replace(/\/\/[^\n]*/g, '');
319
+ for (const forbidden of ['INSERT', 'UPDATE', 'DELETE', 'withTx', 'reconcile', 'award']) {
320
+ assert.ok(!new RegExp(forbidden, 'i').test(code), `the health sweep must not ${forbidden}`);
321
+ }
322
+ assert.match(code, /db\.sessionUploadHealth/, 'it reads through the one read entry point');
323
+ assert.equal(typeof poller.start, 'function');
324
+ assert.equal(typeof poller.stop, 'function');
325
+ // Kill-switched and separate from the payout sweep's switch, so silencing the
326
+ // telemetry can never silence the money.
327
+ assert.match(code, /SESSION_UPLOAD_HEALTH_DISABLED/);
328
+ assert.ok(!/SESSION_REWARD_RECONCILE/.test(code), 'and it does not share the payout poller\'s switch');
329
+ });
@@ -120,7 +120,7 @@ const server = http.createServer((req, res) => {
120
120
  res.writeHead(status, { 'Content-Type': 'application/json' });
121
121
  res.end(JSON.stringify(body));
122
122
  };
123
- if (req.method === 'GET' && req.url.endsWith('/api/gds/me')) {
123
+ if (req.method === 'GET' && req.url.endsWith('/api/bongos/me')) {
124
124
  if ((req.headers.authorization || '') === `Bearer ${GOOD_TOKEN}`) {
125
125
  return send(200, { builder: { id: 6, github_login: 'newbuilder', display_name: 'New Builder' } });
126
126
  }
@@ -27,11 +27,11 @@ test('/ask-for-help files a help request and routes its near-neighbours', (t) =>
27
27
  const md = readFileSync(SKILL, 'utf8');
28
28
 
29
29
  // The write path, named literally — this is the whole point of the skill.
30
- assert.match(md, /POST \/(?:api\/gds\/)?help-requests/, 'names the help-request write route');
30
+ assert.match(md, /POST \/(?:api\/bongos\/)?help-requests/, 'names the help-request write route');
31
31
  assert.match(md, /--body-file/, 'files via --body-file (api.js drops a positional body)');
32
32
 
33
33
  // Resolve the addressee — never guess a builder id.
34
- assert.match(md, /GET \/api\/gds\/builders\/directory/, 'resolves a named builder through the directory');
34
+ assert.match(md, /GET \/api\/bongos\/builders\/directory/, 'resolves a named builder through the directory');
35
35
  assert.match(md, /never guess/i, 'forbids guessing a builder id');
36
36
 
37
37
  // The two XOR rules the table's CHECK constraints enforce.
@@ -57,7 +57,7 @@ test('/ask-for-help files a help request and routes its near-neighbours', (t) =>
57
57
  assert.match(md, /help-requests\/for-me/, 'names the addressed read the recipient sees');
58
58
 
59
59
  // Settling is the asker's job — an unsettled pile is noise.
60
- assert.match(md, /PATCH \/api\/gds\/help-requests\/<id>/, 'names the settle route');
60
+ assert.match(md, /PATCH \/api\/bongos\/help-requests\/<id>/, 'names the settle route');
61
61
  assert.match(md, /answered/, 'names the answered status');
62
62
  assert.match(md, /withdrawn/, 'names the withdrawn status');
63
63
 
@@ -23,9 +23,9 @@ test('/grade-audit exists, covers the sweep legs, and stays describe-and-queue o
23
23
  const md = readFileSync(SKILL, 'utf8');
24
24
 
25
25
  // Data surfaces.
26
- assert.match(md, /\/api\/gds\/public\/recent-shipped/, 'sweeps the recent-shipped window');
26
+ assert.match(md, /\/api\/bongos\/public\/recent-shipped/, 'sweeps the recent-shipped window');
27
27
  assert.match(md, /include=grade/, 'reads each task\'s persisted grade');
28
- assert.match(md, /\/api\/gds\/override-requests/, 'cross-references the override ledger');
28
+ assert.match(md, /\/api\/bongos\/override-requests/, 'cross-references the override ledger');
29
29
 
30
30
  // Leg 1 — override ledger: shipped-with-failing-grade, infra vs real split.
31
31
  assert.match(md, /panel_outcome='unavailable'/, 'splits infra outage from a real fail');
@@ -22,9 +22,9 @@ test('/grader-health exists and covers the four health axes on live data', (t) =
22
22
  const md = readFileSync(SKILL, 'utf8');
23
23
 
24
24
  // Live data surfaces, named concretely.
25
- assert.match(md, /\/api\/gds\/public\/grades/, 'reads the public grade trend');
26
- assert.match(md, /\/api\/gds\/grades\/by-builder/, 'reads the per-builder split (Archon)');
27
- assert.match(md, /\/api\/gds\/public\/cost-summary/, 'reads grader spend');
25
+ assert.match(md, /\/api\/bongos\/public\/grades/, 'reads the public grade trend');
26
+ assert.match(md, /\/api\/bongos\/grades\/by-builder/, 'reads the per-builder split (Archon)');
27
+ assert.match(md, /\/api\/bongos\/public\/cost-summary/, 'reads grader spend');
28
28
 
29
29
  // Axis 1 — outage runs: the 21×0.0 pattern and outage-vs-rejection confirmation.
30
30
  assert.match(md, /n_unavailable/, 'names the outage bucket the aggregates expose');
@@ -47,7 +47,7 @@ test('goal_id is the RESOLVED id, never a literal', () => {
47
47
  });
48
48
 
49
49
  test('GOAL_ID is resolved at runtime, from the is_maintenance flag', () => {
50
- assert.match(SH, /GOAL_ID=\$\(call GET "\/api\/gds\/goals\?version=\$\{VERSION_ID\}"/,
50
+ assert.match(SH, /GOAL_ID=\$\(call GET "\/api\/bongos\/goals\?version=\$\{VERSION_ID\}"/,
51
51
  'it asks the API for this version-s goals');
52
52
  assert.match(SH, /is_maintenance/, 'and picks the maintenance goal by its flag');
53
53
  // Exactly one open maintenance goal exists per version (BV1.R18). Ambiguity here