@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
@@ -94,6 +94,13 @@
94
94
  todo: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.todo) },
95
95
  done: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.done) },
96
96
  },
97
+ // task 1953 — private goals: my own pending join-request on the OPEN goal (row
98
+ // from GET /goals/inbox's myPendingRequests, filtered to this goal), or null.
99
+ myPendingRequest: null,
100
+ // task 1953 — the open goal's own pending invite/join-request queue (manager
101
+ // view, GET /goals/:id/requests), or null while loading / on error / not a
102
+ // manager. Feature-detected: an older server 404s and the panel just stays hidden.
103
+ pendingRequests: null,
97
104
  };
98
105
 
99
106
  async function getJSON(path) {
@@ -112,6 +119,13 @@
112
119
  if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
113
120
  return data;
114
121
  }
122
+ // task 1953 — the visibility toggle is the only PATCH this page makes.
123
+ async function patchJSON(path, body) {
124
+ const res = await api.request('PATCH', path, { body: body || {} });
125
+ const data = res.data || {};
126
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
127
+ return data;
128
+ }
115
129
 
116
130
  const curLabel = () => window.OTBBranding?.currencyLabel?.() ?? 'credits';
117
131
  // task 1957 — a task's drachmae is an ESTIMATE until it SETTLES (confirmed/
@@ -857,38 +871,74 @@
857
871
  const depsSpan = deps.length
858
872
  ? `<span class="goal-deps${(g.status === 'open' && unmet.length) ? ' goal-deps--blocked' : ''}" title="This goal is blocked until each prerequisite goal is achieved">${(g.status === 'open' && unmet.length) ? '🔒 ' : ''}depends on: ${deps.map((dep) => `${escapeHtml(dep.title || ('goal ' + dep.id))} ${dep.satisfied ? '✓' : '○'}`).join(', ')}</span>`
859
873
  : '';
860
- meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${scope} ${depsSpan}`;
874
+ // task 1953 — a private goal is join-GATED, not hidden, so the lock badge is
875
+ // informational for every viewer (the toggle that flips it lives in Actions,
876
+ // manager-only).
877
+ const lockSpan = g.visibility === 'private'
878
+ ? '<span class="goal-badge goal-badge--locked" title="Private — join is by invitation or an approved request">🔒 Private</span>' : '';
879
+ meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${scope} ${depsSpan}`;
861
880
  }
862
881
 
863
- const actions = $('#goal-detail-actions');
864
- if (actions) {
865
- actions.innerHTML = g.status === 'open'
866
- ? (joined
867
- ? '<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>'
868
- : '<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>')
869
- : '';
870
- $('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
871
- $('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
872
- }
882
+ // task 1762: the goal's own owner/manager (or an Archon) can invite/manage
883
+ // without an Archon in the loop — mirrors the server's authorizeGoalManagement
884
+ // gate. A protected-scope goal stays Archon-only on the SERVER (goals.js);
885
+ // this client gate deliberately doesn't re-derive the protected-module list
886
+ // (that roster lives server-side only, module-scope-map.js) — a non-Archon
887
+ // owner/manager sees the control on a protected-scope goal and gets a clear
888
+ // toast if the server 403s (see inviteMember's error handling below).
889
+ const isOwner = sameId(g.created_by, selfId());
890
+ const canManage = g.status === 'open' && (isArchon() || isOwner || iAmLead);
891
+ renderActions(g, { joined, canManage });
873
892
 
874
893
  // task 1828 — Completion + Members each render into their band column AND
875
894
  // their dedicated full view (capped in the band, "View all →" into the view).
876
895
  renderCompletion();
877
- // task 1762: the goal's own owner/manager (or an Archon) can invite without
878
- // an Archon in the loop — mirrors the server's authorizeGoalManagement gate.
879
- // A protected-scope goal stays Archon-only on the SERVER (goals.js); this
880
- // client gate deliberately doesn't re-derive the protected-module list (that
881
- // roster lives server-side only, module-scope-map.js) — a non-Archon owner/
882
- // manager sees the control on a protected-scope goal and gets a clear toast
883
- // if the server 403s (see inviteMember's error handling below).
884
- const isOwner = sameId(g.created_by, selfId());
885
- const canInvite = g.status === 'open' && (isArchon() || isOwner || iAmLead);
886
- renderInviteBox(g.id, canInvite, members);
896
+ renderInviteBox(g.id, canManage, members);
887
897
  renderMembers();
888
898
 
889
899
  loadGoalTasks(g.id);
900
+ // task 1953 — my own pending request (any viewer) + the goal's pending queue
901
+ // (managers only); async, re-renders Actions + the pending panel on arrival.
902
+ loadGoalMembership(g.id, canManage);
890
903
  void sat; void crit; // now read inside renderCompletion()
891
- void iAmLead; // lead-only resolution actions arrive with tasks 1735/1736
904
+ }
905
+
906
+ // task 1953 — the goal-detail Actions row: Leave/Join (unchanged), a manager's
907
+ // Private/Public toggle (PATCH /goals/:id/visibility), and — for a non-member
908
+ // viewer of a PRIVATE goal — Request to join / Requested (withdraw) in place of
909
+ // Join (private goals are join-GATED: POST /goals/:id/join 409s goal_private for
910
+ // a non-Archon non-member). An Archon keeps the plain Join button: the server
911
+ // lets them self-join any goal regardless of visibility (POST /goals/:id/join),
912
+ // so "request" would be a pointless extra step for them.
913
+ function renderActions(g, { joined, canManage }) {
914
+ const actions = $('#goal-detail-actions');
915
+ if (!actions) return;
916
+ const parts = [];
917
+ if (canManage) {
918
+ const nextVisibility = g.visibility === 'private' ? 'public' : 'private';
919
+ const label = g.visibility === 'private' ? '🔒 Private — make public' : 'Make private';
920
+ parts.push(`<button class="btn-ghost" id="goal-visibility-btn" type="button" data-next-visibility="${escapeHtml(nextVisibility)}">${label}</button>`);
921
+ }
922
+ if (g.status === 'open') {
923
+ if (joined) {
924
+ parts.push('<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>');
925
+ } else if (g.visibility === 'private' && !isArchon()) {
926
+ if (state.myPendingRequest) {
927
+ parts.push('<span class="role-pill" title="Awaiting the owner/manager\'s approval">Requested</span>');
928
+ parts.push('<button class="btn-ghost" id="goal-withdraw-btn" type="button">Withdraw</button>');
929
+ } else {
930
+ parts.push('<button class="btn-ghost" id="goal-request-btn" type="button">🔒 Request to join</button>');
931
+ }
932
+ } else {
933
+ parts.push('<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>');
934
+ }
935
+ }
936
+ actions.innerHTML = parts.join(' ');
937
+ $('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
938
+ $('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
939
+ $('#goal-visibility-btn')?.addEventListener('click', (e) => toggleVisibility(g.id, e.currentTarget.dataset.nextVisibility));
940
+ $('#goal-request-btn')?.addEventListener('click', () => requestToJoin(g.id));
941
+ $('#goal-withdraw-btn')?.addEventListener('click', () => withdrawRequest(g.id, state.myPendingRequest && state.myPendingRequest.id));
892
942
  }
893
943
 
894
944
  // task 1828 — the active-claim feed (all builders' in-flight claims) powers the
@@ -1056,12 +1106,153 @@
1056
1106
  } catch (err) {
1057
1107
  const code = err?.data?.error;
1058
1108
  if (code === 'archon_approval_required') toast('This goal is Archon-mediated — an Archon must add you.');
1109
+ else if (code === 'goal_private') toast('This goal is private — request to join instead.');
1059
1110
  else if (code === 'not_a_member') toast('You are not a member of this goal.');
1060
1111
  else if (code === 'goal_not_open') toast('This goal is not open.');
1061
1112
  else toast(`Could not ${action} just now.`);
1062
1113
  }
1063
1114
  }
1064
1115
 
1116
+ // task 1953 — a manager (or Archon) flips a goal public|private.
1117
+ async function toggleVisibility(goalId, visibility) {
1118
+ try {
1119
+ await patchJSON(`${API}/goals/${encodeURIComponent(goalId)}/visibility`, { visibility });
1120
+ toast(visibility === 'private' ? 'Goal is now private.' : 'Goal is now public.');
1121
+ await loadDetail(goalId, { force: true });
1122
+ renderDetail(goalId);
1123
+ renderList();
1124
+ } catch (err) {
1125
+ const code = err?.data?.error;
1126
+ if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may do this.');
1127
+ else if (code === 'goal_not_open') toast('Only an open goal can be managed.');
1128
+ else toast('Could not update visibility just now.');
1129
+ }
1130
+ }
1131
+
1132
+ // task 1953 — a non-member asks to join a private (or protected) goal.
1133
+ // renderDetail (below) re-derives canManage/joined and re-triggers
1134
+ // loadGoalMembership, so a plain reload-and-rerender is enough here — same
1135
+ // shape as joinLeave/inviteMember.
1136
+ async function requestToJoin(goalId) {
1137
+ try {
1138
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests`, {});
1139
+ toast('Request sent — the owner or a manager will review it.');
1140
+ await loadDetail(goalId, { force: true });
1141
+ renderDetail(goalId);
1142
+ } catch (err) {
1143
+ const code = err?.data?.error;
1144
+ if (code === 'request_pending') toast('You already have a pending request or invitation for this goal.');
1145
+ else if (code === 'already_a_member') toast('You are already a member of this goal.');
1146
+ else if (code === 'goal_is_open') toast('This goal is open — joining directly instead.');
1147
+ else toast('Could not send the request just now.');
1148
+ }
1149
+ }
1150
+
1151
+ // task 1953 — withdraw MY OWN pending join-request (self-served, no manager needed).
1152
+ async function withdrawRequest(goalId, reqId) {
1153
+ if (reqId == null) return;
1154
+ try {
1155
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(reqId)}/withdraw`, {});
1156
+ toast('Request withdrawn.');
1157
+ await loadDetail(goalId, { force: true });
1158
+ renderDetail(goalId);
1159
+ } catch (err) {
1160
+ toast((err && err.data && err.data.message) || 'Could not withdraw that request just now.');
1161
+ }
1162
+ }
1163
+
1164
+ // task 1953 — a manager approves/denies a pending join-request from the goal's
1165
+ // own pending panel (mirrors the home-inbox action, POST …/requests/:reqId/respond).
1166
+ const pendingActionsPending = new Set();
1167
+ async function respondToJoinRequest(goalId, reqId, action, btn) {
1168
+ const key = `respond:${reqId}`;
1169
+ if (pendingActionsPending.has(key)) return;
1170
+ pendingActionsPending.add(key);
1171
+ if (btn) btn.disabled = true;
1172
+ try {
1173
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(reqId)}/respond`, { action });
1174
+ toast(action === 'approve' ? 'Approved — they’re now a member.' : 'Request denied.');
1175
+ await loadDetail(goalId, { force: true });
1176
+ renderDetail(goalId);
1177
+ renderList();
1178
+ } catch (err) {
1179
+ toast((err && err.data && err.data.message) || 'Could not resolve that request just now.');
1180
+ if (btn) btn.disabled = false;
1181
+ } finally {
1182
+ pendingActionsPending.delete(key);
1183
+ }
1184
+ }
1185
+
1186
+ // task 1953 — every pending row on THIS goal, rendered for a manager: join
1187
+ // requests get Approve/Deny; outstanding invites are informational (only the
1188
+ // invitee can accept/decline theirs — the server has no manager-cancel route).
1189
+ function pendingRequestWho(r) {
1190
+ return r.display_name || (r.github_login ? `@${r.github_login}` : `builder #${r.builder_id}`);
1191
+ }
1192
+ function pendingRequestsHtml(rows) {
1193
+ const requests = rows.filter((r) => r.kind === 'request');
1194
+ const invites = rows.filter((r) => r.kind === 'invite');
1195
+ const requestLi = (r) => `<li class="pending-row">
1196
+ <span class="pending-row__who">${escapeHtml(pendingRequestWho(r))}</span>
1197
+ ${r.note ? `<span class="pending-row__note">“${escapeHtml(r.note)}”</span>` : ''}
1198
+ <span class="pending-row__actions">
1199
+ <button class="btn-ghost" type="button" data-approve-req="${escapeHtml(String(r.id))}">Approve</button>
1200
+ <button class="btn-ghost" type="button" data-deny-req="${escapeHtml(String(r.id))}">Deny</button>
1201
+ </span>
1202
+ </li>`;
1203
+ const inviteLi = (r) => `<li class="pending-row">
1204
+ <span class="pending-row__who">${escapeHtml(pendingRequestWho(r))}</span>
1205
+ <span class="pending-row__note">invited — awaiting their response</span>
1206
+ </li>`;
1207
+ const parts = [];
1208
+ if (requests.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Join requests<span class="goal-count">${requests.length}</span></h3><ul class="pending-list">${requests.map(requestLi).join('')}</ul></div>`);
1209
+ if (invites.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Pending invitations<span class="goal-count">${invites.length}</span></h3><ul class="pending-list">${invites.map(inviteLi).join('')}</ul></div>`);
1210
+ return parts.join('');
1211
+ }
1212
+ function renderPendingRequests() {
1213
+ const box = $('#goal-pending');
1214
+ if (!box) return;
1215
+ const rows = state.pendingRequests;
1216
+ if (!rows || !rows.length) { box.hidden = true; box.innerHTML = ''; return; }
1217
+ box.hidden = false;
1218
+ box.innerHTML = pendingRequestsHtml(rows);
1219
+ }
1220
+
1221
+ // task 1953 — async membership-queue load, kicked off (fire-and-forget) from
1222
+ // renderDetail like loadGoalTasks: my own pending request on this goal (any
1223
+ // viewer, from GET /goals/inbox's myPendingRequests) + — for a manager — the
1224
+ // goal's own pending queue (GET /goals/:id/requests, task 1953's new route).
1225
+ // Feature-detected the way the conflicts panel 404-detects the server-side
1226
+ // detector: an older server (or a non-manager 403) just hides the panel.
1227
+ async function loadGoalMembership(goalId, canManage) {
1228
+ try {
1229
+ const inbox = await getJSON(`${API}/goals/inbox`);
1230
+ if (!sameId(state.current, goalId)) return;
1231
+ state.myPendingRequest = (inbox.myPendingRequests || []).find((r) => sameId(r.goal_id, goalId)) || null;
1232
+ } catch (err) {
1233
+ if (err && err.message === 'redirecting to auth') return;
1234
+ if (!sameId(state.current, goalId)) return;
1235
+ state.myPendingRequest = null;
1236
+ }
1237
+ if (canManage) {
1238
+ try {
1239
+ const r = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests`);
1240
+ if (!sameId(state.current, goalId)) return;
1241
+ state.pendingRequests = r.requests || [];
1242
+ } catch (err) {
1243
+ if (err && err.message === 'redirecting to auth') return;
1244
+ if (!sameId(state.current, goalId)) return;
1245
+ state.pendingRequests = null;
1246
+ }
1247
+ } else {
1248
+ state.pendingRequests = null;
1249
+ }
1250
+ if (!sameId(state.current, goalId)) return;
1251
+ renderPendingRequests();
1252
+ const d = currentDetail();
1253
+ if (d) renderActions(d.goal, { joined: (d.members || []).some((m) => sameId(m.builder_id, selfId())), canManage });
1254
+ }
1255
+
1065
1256
  /* ---------------- data + routing ---------------- */
1066
1257
 
1067
1258
  async function loadDetail(goalId, { force = false } = {}) {
@@ -1084,6 +1275,12 @@
1084
1275
  state.claimsByTask = new Map();
1085
1276
  state.conflictPairs = null;
1086
1277
  state.conflictSource = null;
1278
+ // task 1953 — a freshly opened goal starts with no known membership-queue
1279
+ // state; loadGoalMembership (kicked off from renderDetail) fills it in.
1280
+ state.myPendingRequest = null;
1281
+ state.pendingRequests = null;
1282
+ const pending = $('#goal-pending');
1283
+ if (pending) { pending.hidden = true; pending.innerHTML = ''; }
1087
1284
  setWorkTab('todo');
1088
1285
  ['#goal-todo-count', '#goal-done-count', '#goal-conflicts-count', '#goal-conflicts-view-count',
1089
1286
  '#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
@@ -1290,6 +1487,14 @@
1290
1487
  if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
1291
1488
  });
1292
1489
  });
1490
+ // task 1953 — Approve/Deny on the goal's own pending-requests panel.
1491
+ $('#goal-pending')?.addEventListener('click', (e) => {
1492
+ if (state.current == null) return;
1493
+ const approveBtn = e.target.closest('[data-approve-req]');
1494
+ if (approveBtn) { respondToJoinRequest(state.current, approveBtn.dataset.approveReq, 'approve', approveBtn); return; }
1495
+ const denyBtn = e.target.closest('[data-deny-req]');
1496
+ if (denyBtn) { respondToJoinRequest(state.current, denyBtn.dataset.denyReq, 'deny', denyBtn); return; }
1497
+ });
1293
1498
  window.addEventListener('hashchange', route);
1294
1499
  wireDragReorder();
1295
1500
 
@@ -150,6 +150,18 @@
150
150
  .goal-invite__form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
151
151
  .goal-invite__form select#goal-invite-builder { flex: 1; min-width: 180px; }
152
152
 
153
+ /* task 1953 — private goals: the lock badge (meta line) + the goal's own
154
+ pending invite/join-request queue (manager view, next to the invite box). */
155
+ .goal-badge--locked { background: var(--warn-weak); color: var(--warn); }
156
+ .goal-pending { margin: 4px 0 10px; display: flex; flex-direction: column; gap: 10px; }
157
+ .pending-group__h { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-faint); margin: 0 0 4px; display: flex; align-items: center; gap: 6px; }
158
+ .pending-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
159
+ .pending-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; flex-wrap: wrap; }
160
+ .pending-row:last-child { border-bottom: none; }
161
+ .pending-row__who { font-weight: 600; }
162
+ .pending-row__note { color: var(--ink-faint); font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
163
+ .pending-row__actions { display: flex; gap: 6px; margin-left: auto; }
164
+
153
165
  .conflict { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); background: var(--warn-weak); border-radius: var(--radius-sm); padding: 10px 14px; font-size: 13px; margin: 10px 0 0; }
154
166
  .conflict__pair { font-weight: 600; }
155
167
  .conflict__paths { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11.5px; color: var(--ink-soft); margin-top: 4px; word-break: break-all; }
@@ -246,6 +258,10 @@
246
258
  <h2 class="scroll__h">Members<span class="goal-count" id="goal-members-count" hidden></span></h2>
247
259
  <p class="scroll__lede" id="goal-members-lede"></p>
248
260
  <div class="goal-invite" id="goal-invite" hidden></div>
261
+ <!-- task 1953 — the goal's own pending invite/join-request queue
262
+ (manager view; feature-detected, hidden on an older server or
263
+ when the viewer isn't owner/manager/Archon). -->
264
+ <div class="goal-pending" id="goal-pending" hidden></div>
249
265
  <ul class="member-list" id="goal-members"></ul>
250
266
  <div class="goal-col__foot" id="goal-members-foot" hidden></div>
251
267
  </div>
@@ -73,6 +73,18 @@
73
73
 
74
74
  // (Goals — goals.js registers itself into the other half of the Home band.)
75
75
 
76
+ // 0d-triage. Idea inbox triage cue — task 1471. Surfaces the open idea_inbox
77
+ // (count + a few recent titles) so a triager knows when triage is due. Starts
78
+ // hidden; builders.js fetches /inbox and un-hides ONLY for Metic+ (triage is
79
+ // Metic+), so no idea data loads for anyone who can't triage. Each row deep-
80
+ // links to #/idea/N — the task-detail overlay opens it with the Metic+ Discard
81
+ // control, so triage happens in place.
82
+ w('triage-inbox', 'Idea inbox', 26,
83
+ '<h2 id="triage-inbox-h" class="scroll__h">Idea inbox<span class="count-pill" id="triage-inbox-count" hidden></span></h2>' +
84
+ '<p class="scroll__lede">Open ideas awaiting triage. Open one to promote it to a task or discard it.</p>' +
85
+ '<ul class="triage-inbox" id="triage-inbox-list"><li class="profile__placeholder">…</li></ul>',
86
+ { view: 'home', zone: 'home-main', labelledby: 'triage-inbox-h', hiddenByDefault: true });
87
+
76
88
  // 0e. Recent work — compact 5-row table (task 1723). Filled by builders.js
77
89
  // renderRecentShips from the same feed as the full Your-work table.
78
90
  w('home-recent', 'Recent work', 28,
@@ -179,7 +191,8 @@
179
191
  '<th class="ledger__sortable" id="lb-sort-rank" aria-sort="none" title="Sort by rank">Rank</th>' +
180
192
  '<th class="ledger__sortable" id="lb-sort-drachmae" aria-sort="descending" title="Sort by ' + curLabel() + ' (work shipped)" data-currency-label="caps">Credits</th>' +
181
193
  '<th class="ledger__sortable" id="lb-sort-karma" aria-sort="none" title="Sort by karma (peer recognition + approvals)">Karma</th>' +
182
- '</tr></thead><tbody><tr><td colspan="5" class="ledger__empty">…</td></tr></tbody></table>' +
194
+ '<th class="ledger__sortable" id="lb-sort-tokens" aria-sort="none" title="Sort by tokens used across recorded sessions — efficiency is tokens ÷ credits (hover a row for tokens-per-drachma)">Tokens</th>' +
195
+ '</tr></thead><tbody><tr><td colspan="6" class="ledger__empty">…</td></tr></tbody></table>' +
183
196
  '<div class="pager">' +
184
197
  '<button class="btn-ghost" id="lb-prev" disabled>← earlier</button>' +
185
198
  '<span id="lb-range" class="pager__range">1–10</span>' +
@@ -107,6 +107,9 @@
107
107
  <script src="/builders/hall-widgets.js?v=2026-06-27-r75" defer></script>
108
108
  <!-- BV1.R78 (task 1694): the Goals widget registers into the same registry. -->
109
109
  <script src="/builders/goals.js?v=2026-07-01-goals" defer></script>
110
+ <!-- ADR 0112 (task 1954): the home-page goal-membership inbox — invitations to
111
+ accept + join-requests to approve. Self-registers; hidden when empty. -->
112
+ <script src="/builders/goal-inbox.js?v=2026-07-10-goal-inbox" defer></script>
110
113
  <!-- BV1-prov-P6 (task 1947): the Instances widget — self-registers, gated on the provisioning module. -->
111
114
  <script src="/builders/new-project-wizard.js?v=2026-07-04-goal35" defer></script>
112
115
  <script src="/builders/instances-panel.js?v=2026-07-03-prov-p6" defer></script>
@@ -74,20 +74,22 @@
74
74
  const oa = domain ? oauthAppValues(domain) : null;
75
75
  const dnsNote = domain ? '' : '<li>Add a domain you own, then re-run to wire the address (a domain is needed before sign-in works).</li>';
76
76
  const repoStep = adopt
77
- ? `<li><strong>Layer onto your existing repo</strong> — in your repo run <code>bongos init --adopt</code>. It shows a conflict pre-flight (migrations, CI, protected paths) and writes <em>nothing</em> until you accept; it never overwrites your files, and it imports your existing backlog (GitHub issues / a TODO/ROADMAP file). Commit + push.</li>`
78
- : '<li><strong>Create the empty GitHub repo</strong> — github.com → New repository, leave it empty.</li>';
77
+ ? `<li data-onboard-step="github-repo"><strong>Layer onto your existing repo</strong> — in your repo run <code>bongos init --adopt</code>. It shows a conflict pre-flight (migrations, CI, protected paths) and writes <em>nothing</em> until you accept; it never overwrites your files, and it imports your existing backlog (GitHub issues / a TODO/ROADMAP file). Commit + push.</li>`
78
+ : '<li data-onboard-step="github-repo"><strong>Create the empty GitHub repo</strong> — github.com → New repository, leave it empty.</li>';
79
79
  // The one-click GitHub sign-in step + a collapsible manual fallback (only when a
80
80
  // domain is set — the App needs a real callback host).
81
- const signInStep = oa ? `<li><strong>Set up GitHub sign-in — one click</strong>. We’ll open GitHub with a pre-filled app; approve it, and we place the credentials on your instance automatically (no secret to copy).
81
+ const signInStep = oa ? `<li data-onboard-step="oauth-app"><strong>Set up GitHub sign-in — one click</strong>. We’ll open GitHub with a pre-filled app; approve it, and we place the credentials on your instance automatically (no secret to copy).
82
82
  <div class="np-signin-actions">
83
83
  <button id="np-github-app" type="button" class="np-create-btn" data-instance="${escapeHtml(String(instanceId))}">Set up GitHub sign-in</button>
84
84
  <span id="np-github-app-msg" class="ledger__note"></span>
85
85
  </div>
86
+ <p class="ledger__note">Terminal CLI only: after approving, enable <strong>Device Flow</strong> on the app (GitHub → Settings → Developer settings → GitHub Apps → your app → “Enable Device Flow”) so <code>bongos login</code> works. Web sign-in needs nothing.</p>
86
87
  <details class="np-manual"><summary class="ledger__note">Prefer to do it by hand?</summary>
87
88
  <ol class="np-steps">
88
89
  <li><strong>Create the GitHub OAuth app</strong> — GitHub → <em>your account's</em> Settings → Developer settings → OAuth Apps → New OAuth App.
89
90
  <div class="np-kv"><span>Homepage URL</span><code>${escapeHtml(oa.homepageUrl)}</code></div>
90
91
  <div class="np-kv"><span>Authorization callback URL</span><code>${escapeHtml(oa.callbackUrl)}</code></div>
92
+ <div class="np-kv"><span>Enable Device Flow</span><code>✅ required — the terminal login uses it (off by default)</code></div>
91
93
  </li>
92
94
  <li><strong>Place the secret — verified</strong> (never hand-edit the env file): on the control plane run
93
95
  <code>bongos … oauth-secret place ${escapeHtml(slug)} --client-id &lt;id&gt; --client-secret &lt;secret&gt; --apply</code>.</li>
@@ -101,7 +103,7 @@
101
103
  ${repoStep}
102
104
  ${dnsNote}
103
105
  ${signInStep}
104
- ${oa ? `<li><strong>First sign-in</strong> — once the instance is live, open <code>${escapeHtml(oa.homepageUrl)}</code> and sign in with GitHub; that makes you the owner (Archon).</li>
106
+ ${oa ? `<li data-onboard-step="first-sign-in"><strong>First sign-in</strong> — once the instance is live, open <code>${escapeHtml(oa.homepageUrl)}</code> and sign in with GitHub; that makes you the owner (Archon).</li>
105
107
  <li><strong>Verify</strong> — <code>${escapeHtml(oa.homepageUrl)}/healthz</code> should report <code>auth_configured: true</code>.</li>` : ''}
106
108
  </ol>
107
109
  <p class="ledger__note">This is the same sequence the <code>/new-project</code> assistant and <code>bongos onboard</code> use.</p>
@@ -1746,3 +1746,46 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
1746
1746
  .np-kv span { color: var(--ink-faint); font-size: 0.85rem; min-width: 12rem; }
1747
1747
  .np-kv code, .np-steps code { background: var(--surface-2); padding: 0.1rem 0.35rem; border-radius: 4px; word-break: break-all; }
1748
1748
  .np-done { margin-top: 0.5rem; }
1749
+
1750
+ /* ---- task 1471: idea-inbox triage cue (Home widget, Metic+) ---- */
1751
+ /* Shared .count-pill — work.html defines its own page-scoped copy; the hall
1752
+ index widget needs the same look, so it lives in the shared sheet now. */
1753
+ .count-pill {
1754
+ display: inline-flex; align-items: center; justify-content: center;
1755
+ min-width: 20px; height: 18px; padding: 0 6px; margin-left: 8px;
1756
+ border-radius: 999px; background: var(--surface-2); color: var(--ink-soft);
1757
+ font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
1758
+ vertical-align: 2px;
1759
+ }
1760
+ .triage-inbox { list-style: none; margin: 0; padding: 0; }
1761
+ .triage-inbox__item {
1762
+ display: flex; align-items: baseline; gap: 0.5rem;
1763
+ padding: 0.3rem 0; border-top: 1px solid var(--rule-soft);
1764
+ }
1765
+ .triage-inbox__item:first-child { border-top: 0; }
1766
+ .triage-inbox__link { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1767
+ .triage-inbox__kind {
1768
+ flex: 0 0 auto; font-size: 11px; color: var(--ink-faint);
1769
+ text-transform: uppercase; letter-spacing: 0.04em;
1770
+ }
1771
+ .triage-inbox__empty { color: var(--ink-faint); padding: 0.3rem 0; }
1772
+
1773
+ /* Goal inbox (task 1954, ADR 0112) — the home-page two-sided membership queue:
1774
+ invitations to accept + join-requests to approve. Matches the home widgets'
1775
+ card language (.scroll host); each row is body-left / actions-right. */
1776
+ .ginbox { list-style: none; margin: 0 0 4px; padding: 0; }
1777
+ .ginbox__sec-h {
1778
+ margin: 14px 0 6px; font-size: 12px; font-weight: 600;
1779
+ text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-faint);
1780
+ }
1781
+ .ginbox__item {
1782
+ display: flex; align-items: flex-start; gap: 12px;
1783
+ padding: 9px 0; border-top: 1px solid var(--rule-soft);
1784
+ }
1785
+ .ginbox__item:first-child { border-top: 0; }
1786
+ .ginbox__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
1787
+ .ginbox__goal { font-weight: 600; font-size: 13.5px; }
1788
+ .ginbox__sub { font-size: 12.5px; color: var(--ink-soft); }
1789
+ .ginbox__note { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-soft); font-style: italic; }
1790
+ .ginbox__actions { flex: 0 0 auto; display: flex; gap: 6px; }
1791
+ .ginbox__pending { font-size: 12.5px; margin: 10px 0 0; color: var(--ink-soft); }
@@ -1,37 +1,15 @@
1
- # `ideas` module — the idea inbox + blockers + the `ideas.capture` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **fourth CORE-domain** carved into a module (BV1.R78, [ADR 0093](../../docs/adr/<redacted>.md) §1) — after memory (R72), grading (R73), and economy (R77). A clean, self-contained slice: its SQL already lived in dedicated `inbox.js`/`blockers.js` files (not the `db.js` monolith), so the carve is a move + a doorway re-point, OTB byte-identical. `default: true` in `module.json` — every instance captures ideas and tracks what's stuck on its owner, so it mounts unconditionally unless explicitly disabled.
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 **methodology intake surfaces** that feed the backlog: the `idea_inbox` (free-form capture → triage → promote/discard/merge, plus the Metic-proposes / Archon-ratifies `criterion-proposal` flow that writes `done_when_criteria`), and the `blockers` queue (a thing stuck on a human; resolving one auto-promotes its linked `blocked` tasks via a DB trigger).
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.8.0` — it needs the R78 doorway addition `parseId`, the route-param validator alongside `validateOrRespond`/`LIMITS`). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches ideas only through the `ideas.capture` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks.
10
-
11
- ## The `ideas.capture` kernel PORT (ADR 0093 §5 — the headline of this carve)
12
-
13
- - **Provides — port `ideas.capture`** → `inbox.captureIdea`. Registered in `routes/inbox.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). Mirrors `modules/economy/routes/reward.js` (a capability registered from a route factory).
14
- - **It retires the `captureIdea` doorway leak.** ADR 0091 §3 parked `captureIdea` on `src/module-api.js` as a temporary "domain-leak-pending-carve" because the ideas domain wasn't a module yet. This carve deletes that re-export; the discord `#ideas` inbound (`modules/discord/discord-inbound.js`, the **sole** consumer) now resolves `ideas.capture` at call time instead of importing the doorway helper. This is the codebase's **first module→module port** (every prior port is consumed by core); discord declares it in `consumes`.
15
- - **Why a verb-noun port, not a doorway re-export:** the same reason `grade`/`reward` are ports — a module capability another module invokes goes behind the kernel switchboard, never a direct import (ADR 0091 §4).
16
-
17
- ## Files
18
-
19
- | File | What |
20
- |---|---|
21
- | `inbox.js` | the `idea_inbox` domain: `captureIdea` (the port provider) + the keyword `classify` fallback, `listOpenIdeas`/`getIdea`/`countOpenIdeas` (with the V3.R70 skill-friction rank-visibility gate), `updateIdea`, and the `ratifyCriterionProposal` transaction (criterion-proposal → `done_when_criteria` + `karma_log` + `criterion_proposal_audit`, all in one `pool.connect()` tx). Reaches the shared pool through the doorway. |
22
- | `blockers.js` | the `blockers` domain: `listOpenBlockers`/`getBlocker`/`createBlocker`/`resolveBlocker`/`linkBlockerToTask`. `resolveBlocker` relies on the DB trigger on `blockers.status` to auto-promote linked `blocked` tasks. |
23
- | `routes/inbox.js` | `POST/GET /inbox`, `GET /inbox/:id`, `PATCH /inbox/:id` (Metic+ triage), `POST /inbox/:id/ratify` (Archon). Its factory **registers the `ideas.capture` port**. |
24
- | `routes/blockers.js` | `GET /blockers` (**public** — the status page reads it), `GET /blockers/:id`, `POST /blockers` (any builder files), `POST /blockers/:id/{resolve,link}` (Metic+). |
25
-
26
- ## NOT in this module (deliberately left in core)
27
-
28
- - **The public idea/blocker projections** — `routes/public.js` (`projectPublicIdea`/`projectPublicBlocker`) reads `idea_inbox`/`blockers` with its **own** inline `pool.query` SELECTs (existence/ref checks for the hall's `#NNN` deep-link resolver + the public sitemap). Core querying a shared table with its own SQL is allowed; it does **not** import this module, so there is no vendored twin to keep in sync — the boundary is about code imports, not table access.
29
- - **The ideas/blockers CLIs** — `scripts/gds/{capture,triage,render-ideas}.js` travel with the domain but stay under `scripts/`; they now `require('../../modules/ideas/inbox')` (the economy/memory CLI precedent — `scripts/gds/autonomy-gate.js` does the same for its idea-inbox read).
30
-
31
- ## Tables
32
-
33
- None owned. `idea_inbox`, `blockers`, `task_blockers`, `criterion_proposal_audit` (+ the `done_when_criteria`/`karma_log` rows the ratify tx touches) 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** ideas migration belongs in `modules/ideas/migrations/` namespaced `ideas_*` (the additive rule).
34
-
35
- ## Tests
36
-
37
- `tests/criterion_proposal.mjs` imports `modules/ideas/inbox` (the ratify transaction). `tests/discord_inbound.mjs` exercises the consumer (it injects a stub `captureIdea` via dependency injection, so it never hits the real port). `tests/route_validation.mjs` exercises the inbox/blocker write-schemas via the dependency-free `validate()` helper (it imports `_helpers`, not these routes). The doorway-surface contract (`tests/module_api.mjs`) asserts `captureIdea` is **gone** and `parseId` is present. See ADR 0093.
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -1,63 +1,15 @@
1
- # `lifecycle` module — the build state machine + the `lifecycle` kernel port
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > The **LAST and most-coupled CORE-domain** carved into a module (BV1.R86, [ADR 0093](../../docs/adr/<redacted>.md) §1,§3) — after memory (R72), grading (R73), economy (R77), ideas (R78), security (R79), builder-settings (R80), onboarding (R81), sessions (R82), autonomy (R83), status-UI (R84), hall-UI (R85). Carved last on purpose: by now every capability it INVOKES (`grade`, `reward`, `onboarding`) is a kernel port it resolves, so the lifecycle imports only the kernel + resolves those ports ([02-module-map §4](../../docs/design/modular-architecture/02-module-map.md): keep the state machine WHOLE, never split). `default: true` — every instance runs the lifecycle. This carve makes the `db.js` residue exactly what ADR 0093 §3 predicted: the kernel identity/auth/twin slice + `db-kernel.js`.
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 **build state machine**: tasks · claims · versions · done-when criteria · goals · dependencies · the claim→ship→grade→credit lifecycle · override requests · the ship/confirm/promote orchestration · `github-push` (server-mediated branch publish) · `merge-lock` · `conflict-resolve` · the `publish-reconciler` (confirmed→shipped auto-heal) · `ship-card`/`ship-preflight` · `version-close` · the peer-vote→karma fold-in · the work-tracking analytics (est-drift, grade trend, AI-dev curve).
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`](../../src/module-api.js) (`coreVersion ^1.12.0` — it needs the R86 doorway additions: `repoInfo`, `moduleScopeMap`, `pathMatch`, `userAgent`, `asyncHandler`, `corsPublicGet`). It must **never** `require()` a core internal directly, and never another module. Core, in turn, must never `require()` a file in here — it reaches the lifecycle only through the `lifecycle` port (below). Both directions are enforced by the BV1.R44/R45 fitness checks.
10
-
11
- ## The `lifecycle` kernel PORT (ADR 0093 §3 — the headline of this carve)
12
-
13
- The inverse of every prior carve: the lifecycle is carved last, so every capability it *invokes* (`grade`/`reward`/`onboarding`) is already a port it RESOLVES; the `lifecycle` port is the surface **core and the other modules resolve to reach the build state machine** without importing this module.
14
-
15
- - **Provides — port `lifecycle`** → the surface in [`lifecycle.js`](lifecycle.js) (DELEGATES, does not spread, to `./db` + `./task-classifier` so test stubs bite at call time — the economy `reward` precedent). Registered in [`routes/lifecycle.js`](routes/lifecycle.js)'s factory (the loader runs it at boot + on every test router rebuild; `hasProvider`-guarded). It **retires the three ADR-0091 §3 doorway leaks** — `createTask`, `VOTABLE_TASK_STATUSES`, `tallyPeerVotes` — plus exposes `classifyTaskKind` and the work-tracking READS the staying-core read routes need.
16
- - **Core + the other modules consume it WITHOUT importing this module** (all via `seams.resolveOptional('lifecycle')`):
17
- - `src/bongos/routes/public.js` (the status surface) — `getTask`/`listVersions`/`versionProgress`/`recentShippedTasks`/`estimationDriftSummary`/`rollingAvgGrade`/`gradeTrendByDay`.
18
- - `src/bongos/routes/me.js` (the profile reader) — `<redacted>`/`getEstVsActualRatio`/`recentGradesForBuilder`/`<redacted>`/`<redacted>`.
19
- - `src/bongos/routes/builders.js` (the Archon roster) — `getBuildersRoster`/`<redacted>`/`gradeByBuilder`/`<redacted>`.
20
- - `src/bongos/cascade-dispatch.js` (dormant System 3) — `classifyTaskKind`.
21
- - `modules/onboarding/db.js` (newcomer-restock) + `modules/discord/db.js` (#bugs→task) — `createTask`.
22
- - `modules/discord/discord-reaction.js` — `VOTABLE_TASK_STATUSES`.
23
- - `modules/autonomy/pollers/routine-timers.js` — `tallyPeerVotes`.
24
- - **It RESOLVES (as a transaction-participant consumer)** the `reward` + `onboarding` ports inside its own `withTx` — `claimTask`/`confirmTask`/`decideOverrideRequest`/`applyGrade` open the tx, run the state-machine SQL, and call `seams.resolveOptional('reward')|('onboarding').*(client, …)` so the credit/achievement/stage writes commit atomically with the status flip (ADR 0093 §2). It resolves via `resolveOptional` (graceful), so `consumes: []` — a vanilla instance without economy/onboarding degrades those writes to best-effort no-ops, it does not fail.
25
-
26
- ## Files
27
-
28
- | File | What |
29
- |---|---|
30
- | `db.js` | the lifecycle data layer — ~97 functions: tasks/claims/versions/goals/dependencies/criteria, the claim→ship→grade→credit lifecycle (`claimTask`/`resolveClaim`/`confirmTask`/`shipTask`/`applyGrade`/`abandonTask`), overrides, estimation + work analytics, the merge lease, `tallyPeerVotes`, `createTask`. Reaches the pool/withTx/seams via the doorway; resolves `reward`/`onboarding` as a consumer. |
31
- | `task-classifier.js` | the `tasks.kind`/discipline heuristic + `VOTABLE_TASK_STATUSES`/`VALID_KINDS`/`VALID_DISCIPLINES`/`TASK_FIELD_BOUNDS`. `branding()` (instance blocker-vendors) via the doorway. |
32
- | `done-when.js` · `hierarchy.js` · `version-close.js` | the done-when criterion read/write surface, the shallow one-level task hierarchy, and the version-close gate evaluator. Pool via the doorway. |
33
- | `github-push.js` · `conflict-resolve.js` · `publish-reconciler.js` | server-mediated branch publish ([ADR 0055](../../docs/adr/<redacted>.md)) + the [ADR 0082](../../docs/adr/<redacted>.md) generated-file conflict resolver + the confirmed→shipped reconciler. `repoInfo`/`branding`/`userAgent` via the doorway; siblings require each other. |
34
- | `ship-card.js` · `ship-preflight.js` · `merge-lock.js` | the CLI lifecycle card, the ship preflight guards, and the cross-machine merge lease serializer (pure; `branding` via the doorway). |
35
- | `lifecycle.js` | the `lifecycle` port provider surface (delegates to `./db` + `./task-classifier`). |
36
- | `routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js` | the lifecycle HTTP surface — moved out of `src/bongos/routes/`; the loader mounts them via `contributes.routes`. Auth/validators/seams via the doorway; `./db` + libs are siblings. |
37
- | `routes/lifecycle.js` | the route factory that registers the `lifecycle` port (no HTTP endpoint). |
38
- | `pollers/publish-reconciler.js` | the loader poller entry (`start`/`stop`) for the publish reconciler — inverts the old `server.js` hardcoded start (the autonomy routine-timers precedent). |
39
-
40
- ## Boundary-forced duplication (keep in sync)
41
-
42
- Two helpers are **deliberately duplicated** with core `src/bongos/db.js` (ADR 0091 §2 — "boundary-forced duplication is allowed + documented"): the same SQL runs on both sides of the kernel↔module line, and a module can't import core:
43
-
44
- - **`sealSessionLog`** — core `deactivateBuilder` (rank/identity management, stays core) writes the same `session_logs` close row; here `resolveClaim`/`shipTask` write it.
45
- - **`_applyRankChangeTx` (+ its `RANKS` / `singleRungPromotionError` deps)** — core `setBuilderRank` (Archon-driven, stays core) runs the same in-tx rank change; here it backs the automatic xenos→thetes graduation on ship (`maybeAutoGraduateToThetes`).
46
- - **the `gradePassCount` SQL fragment** — core `builderRollingAvgGrade` (the grade-trend read the sessions BFG also consumes, kept on the doorway) + the module's grade reads share it.
47
-
48
- ## NOT in this module (the db.js residue — stays core)
49
-
50
- - **Builder IDENTITY + admission** (`upsertBuilder`/`getBuilderByGithubId`/`hasInvitedAccessRequest`/`linkAnthropicAccount`/`updateBuilderDisciplines`) — `auth.js` imports these; they CANNOT live in a module.
51
- - **Rank + status MANAGEMENT** (`setBuilderRank`/`_applyRankChangeTx`/`<redacted>`/`deactivate`/`reactivateBuilder`) — Archon identity ops consumed by `routes/builders.js`, NOT the task lifecycle.
52
- - **AUTH sessions** (`createSession`/`lookupSession`/`revoke*`/`hasCliSession`/`SESSION_TTL_MS`) — the `builder_sessions` token store `auth.js` drives.
53
- - **TWIN / social** (Discord link flow, karma feed, `listAuditLog`, the grade-trend `builderRollingAvgGrade`) + the **security_docs** store (the kernel security-PROPERTY plane).
54
- - **The Gemini-key leak** (`getBuilderGeminiKeyRow`/…) — art-pipeline credential infra.
55
- - **The lifecycle CLIs** — a command travels with its domain but stays a CLI under `scripts/gds/` (`ship.js`/`claim.js`/`release.js`/`merge-mode.js`/`confirm.js`/`merge-lock.js`/`version-close.js`/`main-audit.js`/`gate-review.js`/`push-main.js`/`ci-grade.js`), outside the kernel boundary — they may require this module directly (the grading/economy precedent).
56
-
57
- ## Tables
58
-
59
- None owned. `tasks`/`claims`/`versions`/`goals`/`done_when_criteria`/`dependencies`/`task_grades`/`task_override_requests`/`merge_leases` (+ their migrations) were created before the module system and stay in core `migrations/` (the shared-schema additive rule). The module owns the *queries*; any **new** lifecycle migration belongs in `modules/lifecycle/migrations/` namespaced `lifecycle_*`.
60
-
61
- ## Tests
62
-
63
- The DB-free carved-surface tests in `scripts/gds/coreB-carve-proof.sh` (`task_classifier_vocab`, `criterion_progress`, `github_push_*`, `ship_card`, `ship_preflight`, `merge_lock`, `conflict_resolve`, `publish_reconciler`, `goal_achievement`, `context_pack`) import from `modules/lifecycle/…` and pass byte-identical. The DB-integration tests (`grade_attempts`, `rank_gate`, `abandon_task`, `confirm_attribution`/`override_attribution`/`grade_attribution`, `goal_routes`/`goal_tier`/`goal_achievement`, `dependencies_*`, `task_create_idempotency`, `worktree_disambiguation`, `cap_newcomer_tasks`, `migration_allocation`, `task_ownership_gate`, `verify_task_guard`, `versions_route`, `gate_approvals`, `merge_lease`, `autonomous_fit_protected_paths`, …) require/read `modules/lifecycle/{db,routes/*,libs}.js`; the route-consumer tests (`onboarding_route_signals`, `onboarding_observability`, `discord_reaction`) register the `lifecycle` port. See [ADR 0093](../../docs/adr/<redacted>.md).
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._