@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.
- package/.bongos-core.json +363 -312
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/CLAUDE.md +11 -238
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +14 -10
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +3 -3
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +20 -19
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +8 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +39 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +63 -8
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/discord/module.json +2 -1
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +6 -4
- package/modules/hall-ui/public/style.css +43 -0
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/CLAUDE.md +11 -40
- package/modules/security/secret-scrub.js +10 -0
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +174 -20
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +112 -16
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +17 -24
- package/scripts/gds/package-core.js +74 -17
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +172 -25
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +24 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/instance-config.js +1 -1
- package/src/module-api.js +24 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +125 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +96 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +106 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +62 -0
- package/tests/package_core.mjs +76 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +44 -2
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +220 -25
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -1,48 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bongos/core — installed platform dependency (not your project's CLAUDE.md)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# @bongos/core — installed platform dependency (not your project's CLAUDE.md)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# @bongos/core — installed platform dependency (not your project's CLAUDE.md)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
1873
|
-
// `lb-sort-<key>`. The handler walks aria-sort across all
|
|
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(
|
|
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
|
+
})();
|