@bongos/core 1.17.0 → 1.17.2

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 (230) hide show
  1. package/.bongos-core.json +363 -312
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
@@ -1,48 +1,15 @@
1
- # `economy` module — credits, cost ledger, streaks, achievements, leaderboard + the `reward` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **third CORE-domain** carved into a module (BV1.R77, [ADR 0093](../../docs/adr/<redacted>.md) §1-2) — after memory (R72) and grading (R73). Like grading, it follows ADR 0091 §4's load-bearing pattern: **keep the lifecycle state machine whole, extract the capability it invokes behind a kernel seam.** The lifecycle no longer imports the credit/achievement code directly; it resolves a `reward` PORT this module registers.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the **builder economy**: the cost-plus + kind-multiplier credit-allocation model (the real-money credit ledger), the idea-promotion bonus, the flat session-token reward, the net-negative (simplification) ship bonus, the cost ledger + dashboard rollups, builder streaks, achievements/gamification, and the leaderboard. `default: true` in `module.json` — every instance rewards its own builders, so it mounts unconditionally unless explicitly disabled.
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The boundary (the one rule)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- This module reaches core **only** through the published doorway `../../src/module-api.js` (`coreVersion ^1.7.0` — it needs the R77 doorway addition: `llmPricing`, the shared token→USD pricing lib). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches economy only through the `reward` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks; R70's `kernel-imports-no-domain` check keeps the kernel from reaching back in.
10
-
11
- ## The `reward` kernel PORT (ADR 0093 §1-2 — the headline of this carve)
12
-
13
- - **Provides — port `reward`** → the economy module's reward surface (`modules/economy/reward.js`). Registered in `routes/reward.js`'s factory (the loader runs it at boot, and again whenever a test rebuilds the GDS router — `hasProvider`-guarded so re-instantiation never trips the single-provider duplicate guard).
14
- - **It is a TRANSACTION-PARTICIPANT port** (unlike the stateless `grade` port): the credit-allocation methods (`insertMultipliedConfirmCredit`, `maybeAwardIdeaPromotionBonus`, `awardNetNegativeBonus`, `awardSessionTokenReward`) and the claim/ship gamification hooks (`evaluateAfterClaim/Ship/CreditAward`) take the **caller's tx `client` as the first arg**. The core lifecycle opens the `withTx`, runs the state-machine SQL (FOR UPDATE, `task_grades` upsert, `UPDATE tasks status`, claim-holder lookup, `logGradeAttempt`) itself, and calls `reward.*(client, …)` inside that same transaction — so the credit write commits atomically with the status flip. Extract the capability; keep the state machine (ADR 0091 §4).
15
- - **Core consumes it WITHOUT importing this module:**
16
- - the LIFECYCLE — `db.applyGrade` / `db.confirmTask` / `db.decideOverrideRequest` / `db.claimTask` / `db.upsertSessionRecordWithReward` resolve it via `seams.resolveOptional('reward')` and call the credit/achievement methods inside their own `withTx`.
17
- - the READ routes — `routes/me.js` (achievement getters + streak preview + `getBuilderCostStatus`), `routes/public.js` (`costSummary` + `getLeaderboardWithAchievements`), `routes/builders.js` (`setBuilderMonthlyBudget`), `routes/sessions.js` (`sumCreditsForTasks`), and `publish-reconciler.js` (`reconcileSessionCosts`) resolve the same port and call its read surface. economy registers ONE port; its provider exposes both the lifecycle methods and the read surface (the grading precedent registers the whole module surface on its single port too).
18
- - This replaced the former `const achievements = require('./achievements')` / `cost` / `cost-summary-shape` core→domain requires in `db.js`, the `db.leaderboard`/`db.logCost`/etc. monolith helpers, and the `require('../streak')` core-route imports.
19
-
20
- ## Files
21
-
22
- | File | What |
23
- |---|---|
24
- | `credits.js` | the credit-allocation model: `KIND_MULTIPLIERS`/`IDEA_BONUS_FRACTION`/`REWARD_MARGIN_PCT`, `applyKindMultiplier`, `computeSessionTokenReward`, `insertMultipliedConfirmCredit`, `maybeAwardIdeaPromotionBonus`, `awardSessionTokenReward`, `awardNetNegativeBonus` (+ `NET_NEGATIVE_BONUS`/`SUBTRACTOR_THRESHOLD`/`netNegativeBonusAmount`), the `leaderboard`, and `sumCreditsForTasks`. Writes `credit_log`. |
25
- | `cost.js` | the cost ledger: `logCost` (the one `cost_log` writer), `reconcileSessionCosts`, `costSummary`/`costCalibrationSummary`, `getBuilderCostStatus`, `setBuilderMonthlyBudget`, `autonomousApiSpendMtd`. |
26
- | `cost-classify.js` | pure cost-attribution audit + budget classification (`classifyCostStatus`, `auditCostAttribution`, `formatAuditReport`, `OWNERLESS_CATEGORIES`, `SOFT_CAP_FRACTION`). Moved from `src/bongos/cost.js` (renamed to avoid colliding with `cost.js`). |
27
- | `cost-summary-shape.js` | pure cost-summary dimension shapers (`shapeBySource`/`shapeByTier`/`shapeSavings`/`shapeCostCalibration`). |
28
- | `streak.js` | streak math + anti-gaming guards (`computeStreak`, `wouldExtendStreak`, …). Reaches the shared pool through the doorway. |
29
- | `achievements.js` | the achievement evaluators (`evaluateAfterClaim/Ship/CreditAward`, `alreadyUnlocked`, `unlockAchievement`, `filesForBadges`) PLUS the getter slice moved from `db.js` (`listAchievements`, `getBuilderAchievements`, `getLeaderboardWithAchievements`, `getLockedAchievementsProgress`, `renderProgressText`). `require('./streak')` is an intra-module sibling. |
30
- | `reward.js` | the `reward` port provider surface — DELEGATES (does not spread) to `credits`/`achievements`/`cost`/`streak` so test stubs bite at call time. |
31
- | `routes/reward.js` | registers the `reward` port (above); no HTTP endpoint. |
32
- | `routes/cost.js` | `POST /cost` — log a cost row (idempotent). Reaches auth/validators via the doorway; the writer via `../cost`. |
33
- | `routes/leaderboard.js` | `GET /leaderboard` — public drachmae-ordered leaderboard, via `../credits`. |
34
-
35
- ## NOT in this module (deliberately left in core)
36
-
37
- - **`llm-pricing.js`** — the shared token→USD pricing lib stays core shared LLM-cost infra (consumed by BOTH this module's reward/cost paths AND core cost scripts `ship.js`/`session-cost.js`, so it can't be economy-owned). It is exposed to this module on the doorway as `api.llmPricing` (a tracked R73-style domain leak, like `api.llmCache`). It relocates behind a port if/when an llm-infra domain is carved (tranche 2). The economy scope-map glob lists `src/bongos/llm-pricing.js` for task-affinity even though the FILE stays in core.
38
- - **The recent-ships feed + estimation-drift + grade-trend reads** (`db.recentShippedTasks`, `<redacted>`, `getEstVsActualRatio`, `estimationDriftSummary`, `recentGradesForBuilder`, `builderRollingAvgGrade`, `rollingAvgGrade`, `gradeTrendByDay`) — these stay `db.*` (lifecycle/sessions reads, not economy ledger SQL); out of scope for this carve.
39
- - **The karma feed** (`recentKarmaForBuilder`, `karmaDelta30d`, `karma_log`) — stays in core `db.js` (a separate social-signal domain).
40
- - **The economy-owned CLIs** — a command travels with its domain but stays under `scripts/`: `backfill-cost.js` (requires `../../modules/economy/cost-classify`), `diagnose-achievements.js` (requires `../../modules/economy/streak`). (The memory/grading-carve precedent.)
41
-
42
- ## Tables
43
-
44
- None owned. `credit_log`, `cost_log`, `cost_baseline`, `achievements`, `builder_achievements` (+ their migrations) were created before the module system and stay in core `migrations/` (the schema-migrations stem-key makes a later relocation a no-op). The module owns the *queries*; any **new** economy migration belongs in `modules/economy/migrations/` namespaced `economy_*` (the additive rule).
45
-
46
- ## Tests
47
-
48
- `tests/credit_grant.mjs` (credits.js), `tests/cost.mjs`/`tests/cost_audit.mjs`/`tests/cost_budget_status.mjs` (cost-classify.js), `tests/cost_summary_shape.mjs`, `tests/streak.mjs`, and `tests/diff_derived_signals.mjs` import from `modules/economy/…`. The lifecycle-attribution tests (`tests/grade_attribution.mjs`, `tests/confirm_attribution.mjs`, `tests/grade_server_authoritative.mjs`, `tests/override_attribution.mjs`, `tests/override_request_ownership.mjs`) register the `reward` port (and stub `achievements` on the live module object) before driving `db.applyGrade`/`confirmTask`/`decideOverrideRequest`. See ADR 0093.
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -424,7 +424,15 @@ async function getLeaderboardWithAchievements(limit = 10, perBuilderCap = 5) {
424
424
  ORDER BY a.sort_order
425
425
  LIMIT $2
426
426
  ) t
427
- ), ARRAY[]::text[]) AS achievements
427
+ ), ARRAY[]::text[]) AS achievements,
428
+ -- task 1473: mirror of credits.leaderboard's tokens_used — total
429
+ -- transcript tokens across this builder's sessions, so the public
430
+ -- leaderboard shows efficiency (tokens per drachma) beside credits.
431
+ COALESCE((
432
+ SELECT SUM(sr.total_tokens)
433
+ FROM session_records sr
434
+ WHERE sr.builder_id = b.id
435
+ ), 0)::bigint AS tokens_used
428
436
  FROM builders b
429
437
  ORDER BY b.total_credits DESC, b.created_at ASC
430
438
  LIMIT $1`,
@@ -37,7 +37,16 @@ async function leaderboard(limit = 20) {
37
37
  ORDER BY a.sort_order
38
38
  LIMIT 5
39
39
  ) t
40
- ), ARRAY[]::text[]) AS achievements
40
+ ), ARRAY[]::text[]) AS achievements,
41
+ -- task 1473: total transcript tokens across all of this builder's
42
+ -- recorded sessions, so the hall leaderboard can show efficiency
43
+ -- (tokens spent per drachma earned) next to credits. Mirrored in
44
+ -- achievements.getLeaderboardWithAchievements (the /public query).
45
+ COALESCE((
46
+ SELECT SUM(sr.total_tokens)
47
+ FROM session_records sr
48
+ WHERE sr.builder_id = builders.id
49
+ ), 0)::bigint AS tokens_used
41
50
  FROM builders
42
51
  ORDER BY total_credits DESC, created_at ASC
43
52
  LIMIT $1`,
@@ -1,42 +1,15 @@
1
- # `grading` module — the grade-a-diff adversarial panel + the `grade` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **second CORE-domain** carved into a module (BV1.R73, [ADR 0091](../../docs/adr/<redacted>.md) §1+§4) — after memory (R72). It is the carve that proves ADR 0091 §4's load-bearing pattern: **keep the lifecycle state machine whole, extract the capability it invokes behind a kernel seam.** The lifecycle no longer imports the grader directly; it resolves a `grade` PORT this module registers.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the **quality gate**: the v4 adversarial worker panel (Narc / Quality / Hacker / Efficiency, unanimous-PASS), the deterministic pre-passes (route-rank + permission-path, run as panel inputs), and the scoring **rubric**. `default: true` in `module.json` — every instance grades its own ships, so it mounts unconditionally unless explicitly disabled.
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The boundary (the one rule)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- This module reaches core **only** through the published doorway `../../src/module-api.js` (`coreVersion ^1.6.0` — it needs the R73 doorway additions: `routeRankCheck`, `permissionPathCheck`, `llmCache`). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches grading only through the `grade` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks; R70's `kernel-imports-no-domain` check keeps the kernel from reaching back in.
10
-
11
- ## The `grade` kernel PORT (ADR 0091 §4 — the headline of this carve)
12
-
13
- - **Provides — port `grade`** → the grader module surface. Registered in `routes/grade.js`'s factory (the loader runs it at boot, and again whenever a test rebuilds the GDS router — `hasProvider`-guarded so re-instantiation never trips the single-provider duplicate guard).
14
- - **Core consumes it WITHOUT importing this module:**
15
- - `routes/tasks.js` (the LIFECYCLE ship path, `POST /tasks/:id/grade`) resolves it via `seams.resolveOptional('grade')` and calls `grade.validateAndScore(...)` then `grade.serverSideGuards(...)`. A grading-disabled instance gets a clean `503 grading_unavailable` instead of a thrown `resolve()`.
16
- - `routes/public.js` (`GET /public/grades`) resolves it the same way and reads `grade.RUBRIC.{threshold,version}`, falling back to `null` when grading is off.
17
- - This replaced the former `const grader = require('../grader')` core→domain import in both files — the edge ADR 0091 §4 set out to sever.
18
-
19
- Why a route factory for a module with **no HTTP routes**: `mountModuleRoutes` is the only loader hook that runs both in the live server and when a test rebuilds the GDS router, so it's where the port registration belongs. The factory returns an empty `express.Router()` purely as the mount vehicle.
20
-
21
- ## Files
22
-
23
- | File | What |
24
- |---|---|
25
- | `grader.js` | the quality gate (~86 KB): v4 adversarial panel + `validateAndScore`/`serverSideGuards`/`gradeSubagent`/`RUBRIC`. The deterministic pre-passes reach `routeRankCheck`/`permissionPathCheck` through the doorway; `runSubagentCached` wraps spawns in the doorway's `llmCache`. |
26
- | `grader-workers/` | the four worker modules (`narc`/`quality`/`hacker`/`efficiency`) + shared kit (`fence`, `worker-prompt`, `worker-verdict`, `rubric`, `extract-json`, `index`). All intra-module relative requires — unchanged by the move. |
27
- | `grader-rubric.json` | the single-source scoring rubric (thresholds, per-worker config). Read by `grader.js` + `grader-workers/rubric.js` via `__dirname`-relative paths — survives the move. |
28
- | `routes/grade.js` | registers the `grade` port (above); no HTTP endpoint. |
29
-
30
- ## NOT in this module (deliberately left in core)
31
-
32
- - **`cascade-dispatch.js`** — stays in `src/bongos/`. It is dormant System 3 (model-tier dispatch, `OTB_AUTONOMY_ENABLED`-gated; nothing calls it but its mock unit test), and its real dependencies are core `task-classifier` (lifecycle) + `skill-prefs` (builder-settings), **not** the grader (the grader is an injected `runner`, never imported). Moving it would force two gratuitous doorway domain-leaks (`classifyTaskKind`, `skill-prefs.getForBuilder`) for capabilities grading never uses — the "ceremony with no boundary benefit" ADR 0091 §Alternatives rejects. It stays a core lib (scoped to `grading` in `module-scope-map.js`) until autonomy/dispatch is carved (tranche 2). The grading scope-map glob lists it as a core file, not part of this dir.
33
- - **`scripts/gds/grade-smoke.js`** + the other grading-owned CLIs — a command travels with its domain but stays under `scripts/gds/` (the memory-carve precedent). They `require('../../modules/grading/grader')`.
34
- - **`llm-cache.js`** — shared LLM-cost infra core also uses (`routes/llm-cache.js`, the cost scripts); it stays core and is exposed to this module on the doorway as `api.llmCache` (a tracked R70 leak).
35
-
36
- ## Tables
37
-
38
- None owned. The grade-attempt rows (`grades`, `grade_attempts`, …) are written by core `db.applyGrade` on the lifecycle side; grading only computes the verdict the route then persists. No `modules/grading/migrations/` yet.
39
-
40
- ## Tests
41
-
42
- `tests/grader.mjs`, `tests/grader_panel.mjs`, `tests/grader_prompt_fencing.mjs`, `tests/grade_server_authoritative.mjs`, `tests/grade_unavailable_display.mjs` import from `modules/grading/…`. `tests/cascade-dispatch.mjs` still imports `src/bongos/cascade-dispatch.js` (it stayed core). `scripts/gds/grade-smoke.js` is the live panel smoke.
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -1,61 +1,15 @@
1
- # `hall-ui` module — the authenticated builders' hall
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > A CORE-domain carve (BV1.R85, [ADR 0093](../../docs/adr/<redacted>.md) §1) — the **second WEB-SURFACE module** ([02-module-map §6](../../docs/design/modular-architecture/02-module-map.md): "a web surface is first-class"), after status-UI (BV1.R84), and the **largest** (~13k LOC). `default: true` — a Cloud Bongos instance ships its hall like it ships its status page, so it mounts unconditionally unless an instance disables it.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- Owns the authenticated builders' hall served at **`builders.<apex>`** (`builders.example.com`) — the roster, the Work Board, standing/karma, the leaderboard, achievements, the citizen's primer, the system maps, the Repo Atlas, Settings, the dev-box pair page, and the Archon-only watch/harbor/gate pages. A no-framework single-page surface (`builders.js` + per-page `*.js`) that polls `/api/gds/*` and the read-only `/api/gds/public/*` surface (live updates ride the SSE channel, `routes/live.js`).
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- ## The shape: a static surface, but with DEDICATED serving (the difference from status-UI)
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
8
13
 
9
- Like status-UI this module has **no `.js` backend, no `db.js` slice, no kernel port, and no routes**. It is purely:
10
- - `public/` — the static hall (HTML/CSS/JS/cursors/chime.wav), moved verbatim from the old repo-root `public-builders/`.
11
- - `module.json` — declares one **`contributes.webSurfaces`** entry `{ host: "builders.", dir: "public" }`.
12
-
13
- **But the hall is NOT served by the generic web-surface loop that serves status-UI.** The status page is a plain static surface; the hall has far richer serving that MUST be byte-identical:
14
- - the apex `/builders/*` tree + the `builders.<apex>` subdomain **rewrite shim** (#726),
15
- - the **canonical-URL redirects** (#744: `/builders/ranks` → `/ranks` on the subdomain; #739: apex `/builders/*` → `builders.<apex>`),
16
- - the **citizen/Archon page gates** (#488/#770: primer, diagrams, work, pair, settings, atlas are sign-in-gated; watch, harbor, gate are Archon-only — the real curl-proof access control),
17
- - per-page **asset-stamping** (#521 content-hash `?v=`),
18
- - the **module hall-widget injection** (BV1.R75 — `injectModuleHallWidgets`),
19
- - the diagram PNG + `primer.md` static mounts.
20
-
21
- So the manifest `webSurfaces` declaration is the **discovery + scope-map + `window.__MODULES__` record**; the bytes are served by the hall's dedicated block in `serve-internal.js`.
22
-
23
- ## The serving seam (how the hall keeps its dedicated block + is still a module)
24
-
25
- `src/bongos/serve-internal.js` resolves `loader.moduleWebSurfaces({ isEnabled })` — the same seam status-UI uses — but **PARTITIONS the hall out** of the generic static loop on the builders-host predicate (`isBuildersHost`), so:
26
- - the **generic loop** owns every web surface EXCEPT the hall (status-UI today),
27
- - the hall's **dedicated block** owns the builders host and reads from `BUILDERS_DIR = modules/hall-ui/public/`.
28
-
29
- The filter is on the **host** (`isBuildersHost`), not the `"hall-ui"` key, so the kernel still names no module key ([ADR 0083](../../docs/adr/<redacted>.md) §Decision #4). `isModuleSurfaceHost` (which the dedicated `/builders` guards use to skip the GENERIC status surface) therefore deliberately EXCLUDES the builders host — the dedicated block is what serves it, so it must not self-skip.
30
-
31
- ## The boundary
32
-
33
- This module reaches **nothing** in core (no doorway require — it has no JS the server requires; `builders.js` etc. are browser code). Core, in turn, must never `require()` a file in here — `serve-internal.js` resolves the surface through `loader.moduleWebSurfaces`/`BUILDERS_DIR`, never an import of `modules/hall-ui/*`. Both directions are enforced by the BV1.R44/R45 fitness checks.
34
-
35
- ## The data it renders STAYS shared core (the judgment call)
36
-
37
- The `/api/gds/me`, `/api/gds/public/*`, and `/api/gds/leaderboard` read endpoints the hall polls **did not move** — they live in `src/bongos/routes/{me,public,builders}.js` and are consumed by the status surface and the off-box mirror too, not just this hall. A read shared by multiple surfaces is correctly core/shared, not hall-exclusive (the same call status-UI / economy / ideas made). So the carve moves the *surface*, not the reads it happens to render.
38
-
39
- ## The hall-widget registry (BV1.R75)
40
-
41
- The hall's `<main>` is filled from a **registry** (`public/dom-utils.js` `contributeHallWidget`; the core set in `public/hall-widgets.js`; a module's own client script otherwise). No widget may be hardcoded as a literal `<section class="scroll">` in `index.html` — a fitness check (`scripts/gds/fitness.js`, `HALL_INDEX` now points at `modules/hall-ui/public/index.html`) reds the build if one creeps back. Module-contributed hall widgets are injected via `serve-internal.js`'s `injectModuleHallWidgets` (the `hallWidgetScripts` loader seam), served at `/modules/<key>/public/hall-widget.js`. (Known pre-existing limitation, idea #465: that asset path 404s on the builders *subdomain* because the rewrite shim shadows it — injection works on both hosts; serving works on the apex. No module ships a hall-widget today so it's a live no-op.)
42
-
43
- ## Files & tooling
44
-
45
- | Path | What |
46
- |---|---|
47
- | `public/index.html` · `public/builders.js` · `public/style.css` | the hall shell + core script + stylesheet (no build step, no framework) |
48
- | `public/{work,roadmap,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,not-ready}.{html,js}` | the per-page surfaces |
49
- | `public/task.html` + `public/task.js` | the task-detail page (`/task/:id`, task 751) — a real, shareable page that replaced the old `#/task/N` overlay. Irregular routing: `serve-internal.js`'s `TASK_DETAIL_PAGE_RE` resolves the two-segment `/builders/task/:id` path to this one file (every other page is a single-segment slug). (The old read-only Task Board twin `board.html`/`board.js` was removed in task 1965 — `/board` now 301s to `/work`.) |
50
- | `public/goals.html` + `public/goals-page.js` | the Goals page (irregular basename — `goals.js` is the index's Goals hall-widget) |
51
- | `public/dom-utils.js` · `public/hall-widgets.js` | the hall-widget + settings-panel registries (browser IIFEs) |
52
- | `public/instances-panel.js` | the "Instances" hall widget (provisioning module, [ADR 0111](../../docs/adr/<redacted>.md) §1, task 1947) — self-registers **gated on `module:'provisioning'`** (`contributeHallWidget` drops it when the module is off); a self-fetching widget (the `goals.js` pattern) that reads the P1 `/provisioning/*` routes and renders own instances always + the Archon fleet + cost-ledger total when `/me` is `archon`. Read/state only. |
53
- | `public/new-project-wizard.js` | the "Start a project" hall widget (provisioning module, [ADR 0111](../../docs/adr/<redacted>.md) / goal 35, task 1983) — the **CREATE** flow the instances-panel deferred: the most accessible form of the canonical onboarding runbook (the `/new-project` skill task 1981 + `bongos onboard` CLI task 1982). Same `module:'provisioning'` gating; a form that files a **standalone** provisioning request (`POST /provisioning/instances` — the web tier only ENQUEUES, trust boundary intact) then renders the irreducibly-human steps UI-first (create repo, OAuth app with the pinned apex callback, first sign-in) + points at the Instances panel for status. |
54
- | `public/cursors/` · `public/chime.wav` | bronze cursors + the volume-slider preview chime |
55
- | `module.json` | `contributes.webSurfaces: [{ host: "builders.", dir: "public" }]`; no port, no routes |
56
-
57
- Generators that WRITE into this surface live under `scripts/` and were re-pointed at `modules/hall-ui/public/`: `scripts/gds/gen-atlas.js` (the `atlas.json` Repo Atlas data, gitignored — regenerated on deploy), `scripts/art/gen-cursors.py` (cursors), `.claude/sounds/generate.js` (the chime mirror).
58
-
59
- ## Tests
60
-
61
- The loader seam is covered in `tests/module_loader.mjs` (`moduleWebSurfaces` + the live hall-ui surface); the manifest `webSurfaces` validation in `tests/module_manifest.mjs`; the carve discovery-key membership in `tests/fitness.mjs` / `tests/module_loader.mjs` / `tests/modules.mjs`; the browser settings-panel seam in `tests/module_contributions.mjs` (reads `public/dom-utils.js`); the Repo Atlas renderer contract in `tests/atlas.mjs`; and the carve proof in `scripts/gds/coreB-carve-proof.sh` (leg 4i).
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -1782,6 +1782,45 @@
1782
1782
  }).join('');
1783
1783
  }
1784
1784
 
1785
+ // ---- idea-inbox triage cue (task 1471) -----------------------------------
1786
+
1787
+ // Render the Metic+ triage widget: the open-idea count + a few recent titles,
1788
+ // each deep-linking to the #/idea/N overlay (which carries the Metic+ Discard
1789
+ // control). Only ever called for a Metic+ viewer (loadAll gates the fetch), so
1790
+ // reaching here is what un-hides the section — a non-triager never loads or
1791
+ // sees any idea data. Fully failure-isolated by its caller.
1792
+ function renderTriageInbox(data) {
1793
+ const section = document.getElementById('triage-inbox');
1794
+ if (!section) return;
1795
+ const open = (data && Array.isArray(data.ideas) ? data.ideas : [])
1796
+ .filter((i) => i && i.status === 'open');
1797
+ const openCount = data && data.open_count != null ? data.open_count : open.length;
1798
+
1799
+ const countEl = document.getElementById('triage-inbox-count');
1800
+ if (countEl) {
1801
+ countEl.textContent = `${fmtNum(openCount)} open`;
1802
+ countEl.hidden = false;
1803
+ }
1804
+ const listEl = document.getElementById('triage-inbox-list');
1805
+ if (listEl) {
1806
+ if (!open.length) {
1807
+ listEl.innerHTML = '<li class="triage-inbox__empty">Inbox clear — nothing awaiting triage.</li>';
1808
+ } else {
1809
+ listEl.innerHTML = open.slice(0, 8).map((i) => {
1810
+ const title = escapeHtml(i.title || `idea #${i.id}`);
1811
+ const kind = i.kind
1812
+ ? `<span class="triage-inbox__kind">${escapeHtml(i.kind)}</span>`
1813
+ : '';
1814
+ return `<li class="triage-inbox__item">`
1815
+ + `<a class="triage-inbox__link" href="#/idea/${encodeURIComponent(i.id)}">${title}</a>`
1816
+ + kind
1817
+ + `</li>`;
1818
+ }).join('');
1819
+ }
1820
+ }
1821
+ section.hidden = false;
1822
+ }
1823
+
1785
1824
  // ---- leaderboard ---------------------------------------------------------
1786
1825
 
1787
1826
  // Locked at 8 for V2 (per limitations/pms-v2.md). Hardcoding here avoids an
@@ -1816,7 +1855,7 @@
1816
1855
  page: 0,
1817
1856
  pageSize: 25, // task 1750: top-100 Roll — 25/page keeps it to a few pages
1818
1857
 
1819
- sortBy: 'drachmae', // 'rank' | 'drachmae' | 'karma'
1858
+ sortBy: 'drachmae', // 'rank' | 'drachmae' | 'karma' | 'tokens'
1820
1859
  sortDir: 'desc', // 'desc' | 'asc'
1821
1860
  };
1822
1861
 
@@ -1852,6 +1891,23 @@
1852
1891
  })
1853
1892
  .map(({ row }) => row);
1854
1893
  }
1894
+ if (lbState.sortBy === 'tokens') {
1895
+ // task 1473: sort by total tokens used. tokens_used arrives as a string
1896
+ // (pg bigint), so coerce. Tie-break on drachmae DESC then server order,
1897
+ // matching the karma branch.
1898
+ return indexed
1899
+ .sort((a, b) => {
1900
+ const at = Number(a.row.tokens_used) || 0;
1901
+ const bt = Number(b.row.tokens_used) || 0;
1902
+ const diff = (at - bt) * dir;
1903
+ if (diff !== 0) return diff;
1904
+ const ac = Number(a.row.total_credits) || 0;
1905
+ const bc = Number(b.row.total_credits) || 0;
1906
+ if (ac !== bc) return bc - ac;
1907
+ return a.idx - b.idx;
1908
+ })
1909
+ .map(({ row }) => row);
1910
+ }
1855
1911
  if (lbState.sortBy === 'drachmae') {
1856
1912
  // Server already DESCs by drachmae, so the default mode is a no-op
1857
1913
  // copy. The ASC branch flips it.
@@ -1869,13 +1925,14 @@
1869
1925
  }
1870
1926
 
1871
1927
  // Map a sortable column id → the lbState.sortBy key. Adding a new sortable
1872
- // column means adding an entry here + a header in index.html with id
1873
- // `lb-sort-<key>`. The handler walks aria-sort across all three headers so
1928
+ // column means adding an entry here + a header in hall-widgets.js with id
1929
+ // `lb-sort-<key>`. The handler walks aria-sort across all sortable headers so
1874
1930
  // only one column shows its arrow at a time.
1875
1931
  const LB_SORT_HEADERS = {
1876
1932
  rank: 'lb-sort-rank',
1877
1933
  drachmae: 'lb-sort-drachmae',
1878
1934
  karma: 'lb-sort-karma',
1935
+ tokens: 'lb-sort-tokens',
1879
1936
  };
1880
1937
 
1881
1938
  // #757: this builder's place on the Roll, ranked by drachmae (total_credits)
@@ -1902,7 +1959,7 @@
1902
1959
  const tbody = $('#leaderboard tbody');
1903
1960
 
1904
1961
  if (!slice.length) {
1905
- tbody.innerHTML = emptyRow(5, 'No more builders to show.');
1962
+ tbody.innerHTML = emptyRow(6, 'No more builders to show.');
1906
1963
  } else {
1907
1964
  tbody.innerHTML = slice.map((b, i) => {
1908
1965
  const place = start + i + 1;
@@ -1922,6 +1979,7 @@
1922
1979
  <td class="ledger__rank">${rank}</td>
1923
1980
  <td class="ledger__drachmae">${fmtNum(b.total_credits)}</td>
1924
1981
  <td class="ledger__drachmae">${fmtNum(b.karma)}</td>
1982
+ <td class="ledger__drachmae" title="${Number(b.total_credits) > 0 ? `${fmtNum(Math.round(Number(b.tokens_used) / Number(b.total_credits)))} tokens per drachma` : 'no drachmae earned yet'}">${fmtNum(b.tokens_used)}</td>
1925
1983
  </tr>
1926
1984
  `;
1927
1985
  }).join('');
@@ -2633,6 +2691,15 @@
2633
2691
  applyNewcomerGate();
2634
2692
  renderNav(me.builder, me.onboarding || null);
2635
2693
 
2694
+ // task 1471: idea-inbox triage cue — Metic+ only (triage is a Metic+ action).
2695
+ // Fetched fire-and-forget so a slow/large inbox can't hold up the hall; the
2696
+ // widget is hiddenByDefault, so a non-triager never loads or sees idea data.
2697
+ if (['metic', 'archon'].includes(normalizeRank(me.builder?.rank))) {
2698
+ getJSON(`${API}/inbox`)
2699
+ .then((data) => renderTriageInbox(data))
2700
+ .catch(() => { /* nice-to-have; never break the hall */ });
2701
+ }
2702
+
2636
2703
  // Then the rest, in parallel. Any individual section failure shouldn't take
2637
2704
  // down the page — log and show an inline placeholder.
2638
2705
  // #726: the per-skill model picker (Voices) moved to /builders/settings,
@@ -0,0 +1,191 @@
1
+ // goal-inbox.js — the home-page, two-sided goal-membership inbox (task 1954, ADR 0112).
2
+ //
3
+ // Surfaces the actionable slice of a builder's private-goals queue in ONE place:
4
+ // • "Invitations to you" — pending invites addressed to me (Accept / Decline).
5
+ // • "Requests to approve" — pending join-requests on goals I own or manage
6
+ // (Approve / Deny).
7
+ // • a quiet "your pending requests" line for my own outstanding asks.
8
+ // Reads GET /goals/inbox (task 1952) which already scopes each bucket to the caller.
9
+ // Registers into the window.OTB hall-widget registry (dom-utils.js) exactly like
10
+ // goals.js — same IIFE guard, same BongosClient result-mode getJSON, view:'home'.
11
+ //
12
+ // The whole card is HIDDEN when the inbox is empty, so a quiet home page pays nothing
13
+ // for it. On an older server without the route (404) — or a signed-out visitor (401) —
14
+ // it renders nothing (feature-detect), never an error card.
15
+ (function () {
16
+ 'use strict';
17
+ if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
18
+ const { escapeHtml, toast, rankLabel } = window.OTB;
19
+ const API = '/api/bongos';
20
+ // The generated typed client (served as window.BongosClient), result-mode so the
21
+ // status-inspecting control flow below (401/404 → render nothing) reads 1:1.
22
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
23
+
24
+ async function getJSON(path) {
25
+ const res = await api.request('GET', path);
26
+ if (res.status === 401) { const e = new Error('auth-needed'); e.code = 'auth-needed'; throw e; }
27
+ if (res.status === 404) { const e = new Error('not-found'); e.code = 'not-found'; throw e; }
28
+ if (!res.ok) throw new Error(`${path} → ${res.status}`);
29
+ return res.data;
30
+ }
31
+ async function postJSON(path, body) {
32
+ const res = await api.request('POST', path, { body: body || {} });
33
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = res.data; throw e; }
34
+ return res.data;
35
+ }
36
+
37
+ let host = null;
38
+ let busy = false;
39
+
40
+ // The goal-detail hash route on the Goals page (matches goals.js's row link).
41
+ const goalHref = (goalId) => `/goals#/goal/${encodeURIComponent(goalId)}`;
42
+ const noteHtml = (note) => (note ? `<p class="ginbox__note">“${escapeHtml(note)}”</p>` : '');
43
+
44
+ function invitationRow(inv) {
45
+ const who = inv.invited_by_name || inv.invited_by_login || 'someone';
46
+ return `<li class="ginbox__item" data-goal="${escapeHtml(String(inv.goal_id))}">
47
+ <div class="ginbox__body">
48
+ <a class="ginbox__goal" href="${goalHref(inv.goal_id)}" title="Open ${escapeHtml(inv.goal_title || 'goal')}">${escapeHtml(inv.goal_title || 'a goal')}</a>
49
+ <span class="ginbox__sub">invited by ${escapeHtml(who)}</span>
50
+ ${noteHtml(inv.note)}
51
+ </div>
52
+ <div class="ginbox__actions">
53
+ <button class="btn-ghost" type="button" data-accept-inv="${escapeHtml(String(inv.id))}">Accept</button>
54
+ <button class="btn-ghost" type="button" data-decline-inv="${escapeHtml(String(inv.id))}">Decline</button>
55
+ </div>
56
+ </li>`;
57
+ }
58
+
59
+ function requestRow(r) {
60
+ const who = r.display_name || r.github_login || 'a builder';
61
+ const rank = r.rank ? ` · ${escapeHtml(rankLabel(r.rank, { empty: '' }))}` : '';
62
+ return `<li class="ginbox__item" data-goal="${escapeHtml(String(r.goal_id))}">
63
+ <div class="ginbox__body">
64
+ <a class="ginbox__goal" href="${goalHref(r.goal_id)}" title="Open ${escapeHtml(r.goal_title || 'goal')}">${escapeHtml(r.goal_title || 'a goal')}</a>
65
+ <span class="ginbox__sub">${escapeHtml(who)}${rank} asks to join</span>
66
+ ${noteHtml(r.note)}
67
+ </div>
68
+ <div class="ginbox__actions">
69
+ <button class="btn-ghost" type="button" data-approve-req="${escapeHtml(String(r.id))}">Approve</button>
70
+ <button class="btn-ghost" type="button" data-deny-req="${escapeHtml(String(r.id))}">Deny</button>
71
+ </div>
72
+ </li>`;
73
+ }
74
+
75
+ function pendingLine(myPending) {
76
+ if (!myPending.length) return '';
77
+ const links = myPending
78
+ .map((r) => `<a href="${goalHref(r.goal_id)}">${escapeHtml(r.goal_title || 'a goal')}</a>`)
79
+ .join(', ');
80
+ const noun = myPending.length === 1 ? 'request' : 'requests';
81
+ return `<p class="ginbox__pending card-foot">Your pending join ${noun}: ${links} — awaiting approval.</p>`;
82
+ }
83
+
84
+ function render(inbox) {
85
+ const invitations = inbox.invitations || [];
86
+ const joinRequests = inbox.joinRequests || [];
87
+ const myPending = inbox.myPendingRequests || [];
88
+ // Hide the whole card when there is nothing to see OR act on.
89
+ if (!invitations.length && !joinRequests.length && !myPending.length) {
90
+ host.hidden = true;
91
+ host.innerHTML = '';
92
+ return;
93
+ }
94
+ const actionable = invitations.length + joinRequests.length;
95
+ let html = `<h2 id="goal-inbox-h" class="scroll__h">Goal inbox${actionable ? `<span class="count-pill">${actionable}</span>` : ''}</h2>`;
96
+ if (actionable) {
97
+ html += '<p class="scroll__lede">Invitations and join-requests waiting on you.</p>';
98
+ }
99
+ if (invitations.length) {
100
+ html += '<h3 class="ginbox__sec-h">Invitations to you</h3>';
101
+ html += `<ul class="ginbox">${invitations.map(invitationRow).join('')}</ul>`;
102
+ }
103
+ if (joinRequests.length) {
104
+ html += '<h3 class="ginbox__sec-h">Requests to approve</h3>';
105
+ html += `<ul class="ginbox">${joinRequests.map(requestRow).join('')}</ul>`;
106
+ }
107
+ html += pendingLine(myPending);
108
+ host.innerHTML = html;
109
+ host.hidden = false;
110
+ }
111
+
112
+ async function load() {
113
+ if (!host) return;
114
+ try {
115
+ const inbox = await getJSON(`${API}/goals/inbox`);
116
+ render(inbox);
117
+ } catch (err) {
118
+ // Signed out (401) or an older server without the route (404) → render nothing.
119
+ host.hidden = true;
120
+ host.innerHTML = '';
121
+ if (err.code !== 'auth-needed' && err.code !== 'not-found') {
122
+ console.error('[hall] goal inbox load failed', err);
123
+ }
124
+ }
125
+ }
126
+
127
+ // One action, then refresh the widget (the row it acted on disappears, and the
128
+ // count-pill/live counter re-derive from the fresh inbox). Disable while in flight
129
+ // so a double-click can't fire the resolve twice (the server 409s a re-resolve, but
130
+ // the UI shouldn't invite it).
131
+ async function act(fn, okMsg) {
132
+ if (busy) return;
133
+ busy = true;
134
+ host.querySelectorAll('button').forEach((b) => { b.disabled = true; });
135
+ try {
136
+ await fn();
137
+ toast(okMsg);
138
+ await load();
139
+ } catch (err) {
140
+ toast((err && err.data && err.data.message) || 'Could not do that just now — refresh and retry.');
141
+ host.querySelectorAll('button').forEach((b) => { b.disabled = false; });
142
+ } finally {
143
+ busy = false;
144
+ }
145
+ }
146
+
147
+ function onClick(e) {
148
+ const item = e.target.closest('.ginbox__item');
149
+ const goalId = item && item.dataset.goal;
150
+ if (!goalId) return;
151
+
152
+ const accept = e.target.closest('[data-accept-inv]');
153
+ if (accept) {
154
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/invitations/${encodeURIComponent(accept.dataset.acceptInv)}/respond`, { action: 'accept' }),
155
+ 'Invitation accepted — you’re now a member.');
156
+ return;
157
+ }
158
+ const decline = e.target.closest('[data-decline-inv]');
159
+ if (decline) {
160
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/invitations/${encodeURIComponent(decline.dataset.declineInv)}/respond`, { action: 'decline' }),
161
+ 'Invitation declined.');
162
+ return;
163
+ }
164
+ const approve = e.target.closest('[data-approve-req]');
165
+ if (approve) {
166
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(approve.dataset.approveReq)}/respond`, { action: 'approve' }),
167
+ 'Approved — they’re now a member.');
168
+ return;
169
+ }
170
+ const deny = e.target.closest('[data-deny-req]');
171
+ if (deny) {
172
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(deny.dataset.denyReq)}/respond`, { action: 'deny' }),
173
+ 'Request denied.');
174
+ }
175
+ }
176
+
177
+ function mount(hostEl) {
178
+ host = hostEl;
179
+ host.hidden = true; // stays hidden until load() finds something actionable
180
+ host.addEventListener('click', onClick);
181
+ load();
182
+ }
183
+
184
+ // order 24 → sits at the top of the Home main zone (just above the idea-triage
185
+ // inbox), since it is personal + actionable. hiddenByDefault so an empty inbox
186
+ // costs nothing until load() un-hides it.
187
+ window.OTB.contributeHallWidget({
188
+ id: 'goal-inbox-scroll', label: 'Goal inbox', order: 24, mount,
189
+ labelledby: 'goal-inbox-h', view: 'home', zone: 'home-main', hiddenByDefault: true,
190
+ });
191
+ })();