@bongos/core 1.24.14 → 1.24.16

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 (165) hide show
  1. package/.bongos-core.json +308 -138
  2. package/.claude/skills/blocker-review/SKILL.md +2 -0
  3. package/.claude/skills/session-board/SKILL.md +29 -0
  4. package/.claude/skills/strand-fix/SKILL.md +1 -1
  5. package/clients/bongos-client/README.md +1 -1
  6. package/clients/bongos-client/bongos-client.global.js +14 -0
  7. package/clients/bongos-client/index.cjs +14 -0
  8. package/clients/bongos-client/index.d.ts +23 -2
  9. package/clients/bongos-client/index.mjs +14 -0
  10. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
  11. package/docs/api/openapi.json +369 -6
  12. package/docs/api-reference.md +19 -4
  13. package/docs/architecture.md +6 -0
  14. package/docs/copy-inventory.md +231 -204
  15. package/docs/copy-registry.json +488 -255
  16. package/docs/file-map.md +1 -0
  17. package/docs/module-api-changelog.md +4 -2
  18. package/docs/page-inventory.json +7 -1
  19. package/docs/page-readings.json +1132 -1103
  20. package/migrations/core_289_task_short_name.sql +34 -0
  21. package/modules/ai-studio/db.js +10 -0
  22. package/modules/ai-studio/gemini.js +73 -5
  23. package/modules/ai-studio/openrouter.js +17 -2
  24. package/modules/ai-studio/routes/ai-studio.js +7 -1
  25. package/modules/ai-studio/studio.js +8 -0
  26. package/modules/builder-settings/checkout-freshness.js +71 -0
  27. package/modules/builder-settings/routes/settings.js +26 -0
  28. package/modules/builder-settings/settings.js +2 -0
  29. package/modules/economy/migrations/economy_002_project_account.sql +83 -0
  30. package/modules/economy/module.json +1 -1
  31. package/modules/economy/project-account.js +166 -0
  32. package/modules/economy/routes/project-account.js +40 -0
  33. package/modules/hall-ui/public/ai-studio.css +3 -0
  34. package/modules/hall-ui/public/ai-studio.html +2 -1
  35. package/modules/hall-ui/public/ai-studio.js +72 -1
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/city.html +1 -1
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/commands.html +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +1 -1
  44. package/modules/hall-ui/public/diagrams.html +1 -1
  45. package/modules/hall-ui/public/drachmae.html +1 -1
  46. package/modules/hall-ui/public/fleet.html +1 -1
  47. package/modules/hall-ui/public/gate.html +1 -1
  48. package/modules/hall-ui/public/github-chip.js +85 -0
  49. package/modules/hall-ui/public/goal-map.html +1 -1
  50. package/modules/hall-ui/public/goals.html +1 -1
  51. package/modules/hall-ui/public/government.html +1 -1
  52. package/modules/hall-ui/public/idea.html +1 -1
  53. package/modules/hall-ui/public/ideas.html +1 -1
  54. package/modules/hall-ui/public/index.html +1 -1
  55. package/modules/hall-ui/public/market-watch-item.html +2 -2
  56. package/modules/hall-ui/public/market-watch-item.js +10 -3
  57. package/modules/hall-ui/public/market-watch-item.states.json +41 -0
  58. package/modules/hall-ui/public/market-watch-lib.js +36 -5
  59. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  60. package/modules/hall-ui/public/market-watch-settings.html +1 -1
  61. package/modules/hall-ui/public/market-watch.css +5 -0
  62. package/modules/hall-ui/public/market-watch.html +2 -2
  63. package/modules/hall-ui/public/modules.html +1 -1
  64. package/modules/hall-ui/public/my-agents.html +1 -1
  65. package/modules/hall-ui/public/my-agents.js +3 -0
  66. package/modules/hall-ui/public/panel.css +5 -0
  67. package/modules/hall-ui/public/primer.html +1 -1
  68. package/modules/hall-ui/public/profile.html +1 -1
  69. package/modules/hall-ui/public/project-settings.html +1 -1
  70. package/modules/hall-ui/public/ranks.html +1 -1
  71. package/modules/hall-ui/public/roadmap.html +1 -1
  72. package/modules/hall-ui/public/roster.html +1 -1
  73. package/modules/hall-ui/public/sessions.html +1 -1
  74. package/modules/hall-ui/public/settings-age.js +65 -0
  75. package/modules/hall-ui/public/settings.html +27 -1
  76. package/modules/hall-ui/public/settings.js +1 -0
  77. package/modules/hall-ui/public/shell.js +22 -22
  78. package/modules/hall-ui/public/street.html +1 -1
  79. package/modules/hall-ui/public/studio.html +1 -1
  80. package/modules/hall-ui/public/style.css +28 -0
  81. package/modules/hall-ui/public/task.html +1 -1
  82. package/modules/hall-ui/public/thinking.html +1 -1
  83. package/modules/hall-ui/public/tweak-editor.html +1 -1
  84. package/modules/hall-ui/public/watch.html +1 -1
  85. package/modules/hall-ui/public/work.html +1 -1
  86. package/modules/ideas/blockers.js +13 -7
  87. package/modules/lifecycle/db-claim-reads.js +1 -1
  88. package/modules/lifecycle/db-ship.js +2 -2
  89. package/modules/lifecycle/db-tasks.js +4 -4
  90. package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
  91. package/modules/lifecycle/module.json +1 -0
  92. package/modules/lifecycle/pay-type-gates.js +180 -0
  93. package/modules/lifecycle/publish-reconciler.js +31 -1
  94. package/modules/lifecycle/rebase-gate.js +3 -2
  95. package/modules/lifecycle/routes/claims.js +3 -2
  96. package/modules/lifecycle/routes/pay-type-gates.js +59 -0
  97. package/modules/lifecycle/routes/project-settings.js +4 -0
  98. package/modules/lifecycle/routes/task-write-routes.js +43 -0
  99. package/modules/lifecycle/ship-card.js +4 -1
  100. package/modules/lifecycle/task-name-db.js +55 -0
  101. package/modules/lifecycle/task-name.js +140 -0
  102. package/modules/market-watch/db.js +10 -1
  103. package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
  104. package/modules/market-watch/pages.js +20 -1
  105. package/modules/market-watch/proposals.js +57 -7
  106. package/modules/market-watch/routes/pages.js +2 -0
  107. package/modules/market-watch/routes/rules.js +4 -0
  108. package/modules/platform-identity/terms-acceptance.js +9 -9
  109. package/modules/public-landing/public/account-google.states.json +14 -0
  110. package/modules/public-landing/public/assets/cosmos.css +12 -0
  111. package/modules/public-landing/public/assets/hub-bar.js +44 -1
  112. package/modules/public-landing/public/privacy.html +5 -1
  113. package/modules/public-landing/public/projects-google.states.json +14 -0
  114. package/modules/public-landing/public/projects.html +39 -0
  115. package/modules/public-landing/public/terms.html +12 -2
  116. package/modules/ui-design/kit/fixtures/me-google.json +10 -0
  117. package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
  118. package/modules/ui-design/kit/serve.js +7 -1
  119. package/package-lock.json +2 -2
  120. package/package.json +1 -1
  121. package/release-notes.json +55 -1
  122. package/scripts/bongos/claim.js +4 -2
  123. package/scripts/bongos/land-watch.js +1 -0
  124. package/scripts/bongos/report-checkout.js +30 -0
  125. package/scripts/bongos/run-unit-tests.js +11 -0
  126. package/scripts/bongos/sessions-board.js +264 -0
  127. package/scripts/bongos/ship-finish.js +1 -0
  128. package/scripts/bongos/ship-flow.js +5 -2
  129. package/scripts/bongos/strand-lease.js +5 -3
  130. package/scripts/bongos/strand-watch.js +38 -7
  131. package/scripts/gds/report-checkout.js +7 -0
  132. package/scripts/gds/sessions-board.js +7 -0
  133. package/src/bongos/agent-standing-rules.js +13 -3
  134. package/src/bongos/routes/me.js +7 -1
  135. package/src/module-api.js +1 -1
  136. package/tests/agent_standing_rules.mjs +18 -1
  137. package/tests/ai_studio_billing.mjs +13 -0
  138. package/tests/ai_studio_gemini_models.mjs +81 -0
  139. package/tests/ai_studio_requests.mjs +24 -0
  140. package/tests/blocker_detail_hydration.mjs +5 -3
  141. package/tests/blocker_tasks_named_db.mjs +74 -0
  142. package/tests/claim_gate_ci_unblock.mjs +1 -1
  143. package/tests/github_chip.mjs +217 -0
  144. package/tests/hall_ai_studio.mjs +129 -5
  145. package/tests/host_topology_skips.mjs +1 -0
  146. package/tests/hub_account_page.mjs +3 -2
  147. package/tests/landing_legal_pages.mjs +3 -2
  148. package/tests/market_watch_data_db.mjs +6 -0
  149. package/tests/market_watch_pages.mjs +87 -2
  150. package/tests/market_watch_rules.mjs +101 -5
  151. package/tests/market_watch_rules_db.mjs +42 -0
  152. package/tests/nav_permission_atoms.mjs +1 -1
  153. package/tests/pay_terms.mjs +8 -0
  154. package/tests/pay_type_gates.mjs +234 -0
  155. package/tests/pay_type_gates_db.mjs +84 -0
  156. package/tests/project_account.mjs +125 -0
  157. package/tests/project_account_db.mjs +97 -0
  158. package/tests/self_healing_start.mjs +162 -0
  159. package/tests/sessions_board.mjs +153 -0
  160. package/tests/settings_age.mjs +110 -0
  161. package/tests/stop_refcheck.mjs +40 -0
  162. package/tests/task_name.mjs +237 -0
  163. package/tests/task_name_db.mjs +79 -0
  164. package/tests/terms_acceptance.mjs +25 -3
  165. package/tests/terms_grandfather_db.mjs +11 -4
@@ -4416,6 +4416,34 @@ a.stat-tile:hover .stat-tile__sub { color: var(--ink-soft); }
4416
4416
  white-space: nowrap;
4417
4417
  }
4418
4418
 
4419
+ /* ---------- the Link GitHub chip (task 1004996, github-chip.js) ----------
4420
+ Very small and quiet on purpose: shown only to someone with no GitHub linked,
4421
+ it is a plain link, never a banner — ideating and art go on without it. The
4422
+ engineer's glyph says what it is for; the label folds away on a phone. */
4423
+ .gh-chip {
4424
+ display: inline-flex;
4425
+ align-items: center;
4426
+ gap: 5px;
4427
+ min-height: 24px;
4428
+ min-width: 24px;
4429
+ padding: 0 8px 0 6px;
4430
+ border: 1px solid var(--rule);
4431
+ border-radius: var(--radius-pill, 999px);
4432
+ color: var(--ink-soft);
4433
+ font-size: 12px;
4434
+ line-height: 1;
4435
+ text-decoration: none;
4436
+ white-space: nowrap;
4437
+ flex-shrink: 0;
4438
+ }
4439
+ .gh-chip:hover { color: var(--ink); border-color: var(--ink-faint); }
4440
+ .gh-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
4441
+ .gh-chip svg { width: 13px; height: 13px; flex-shrink: 0; }
4442
+ @media (max-width: 640px) {
4443
+ .gh-chip { padding: 0; justify-content: center; }
4444
+ .gh-chip__label { display: none; }
4445
+ }
4446
+
4419
4447
  .palette { position: fixed; inset: 0; z-index: 2100; }
4420
4448
  .palette__scrim { position: absolute; inset: 0; background: var(--scrim); }
4421
4449
  .palette__panel {
@@ -59,7 +59,7 @@
59
59
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
60
60
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
62
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
62
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
63
63
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
@@ -141,7 +141,7 @@
141
141
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
142
142
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
143
143
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
144
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
144
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
145
145
  <script src="/builders/palette.js" defer></script>
146
146
 
147
147
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -145,7 +145,7 @@
145
145
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
146
146
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
147
147
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
148
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
148
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
149
149
  <script src="/builders/palette.js" defer></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
151
151
  <script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw27"></script>
@@ -166,7 +166,7 @@
166
166
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
167
167
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
168
168
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
169
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
169
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
170
170
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
171
171
  <script src="/builders/palette.js" defer></script>
172
172
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -174,7 +174,7 @@
174
174
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
175
175
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
176
176
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
177
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
177
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
178
178
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
179
179
  <script src="/builders/palette.js" defer></script>
180
180
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -12,11 +12,17 @@ const { pool } = require('../../src/module-api');
12
12
  // Shared by listOpenBlockers + getBlocker so the two reads can never drift
13
13
  // (task 1002459 — getBlocker used to omit this column entirely, so a
14
14
  // link-then-verify against the detail route read as a false negative).
15
- const BLOCKING_TASK_IDS_SQL = `COALESCE(
16
- (SELECT array_agg(tb.task_id ORDER BY tb.task_id)
17
- FROM task_blockers tb WHERE tb.blocker_id = b.id),
18
- ARRAY[]::bigint[]
19
- ) AS blocking_task_ids`;
15
+ // task 1004958: ONE lateral read gives both shapes — the ids (the hall reads those)
16
+ // and the same tasks with their 2-3 word names, so a session can write
17
+ // "task N - payout approvals" instead of a bare number.
18
+ const BLOCKING_TASK_IDS_SQL = `COALESCE(bl.ids, ARRAY[]::bigint[]) AS blocking_task_ids,
19
+ COALESCE(bl.tasks, '[]'::json) AS blocking_tasks`;
20
+ const BLOCKING_TASKS_JOIN = `LEFT JOIN LATERAL (
21
+ SELECT array_agg(tb.task_id ORDER BY tb.task_id) AS ids,
22
+ json_agg(json_build_object('id', tb.task_id, 'short_name', bt.short_name) ORDER BY tb.task_id) AS tasks
23
+ FROM task_blockers tb LEFT JOIN tasks bt ON bt.id = tb.task_id
24
+ WHERE tb.blocker_id = b.id
25
+ ) bl ON true`;
20
26
 
21
27
  // Optional { limit, offset } pushes the page into SQL so a small slice (the Home
22
28
  // attention inbox reads limit=5 on every load) never scans the whole open set
@@ -32,7 +38,7 @@ async function listOpenBlockers({ limit, offset } = {}) {
32
38
  `SELECT b.id, b.title, b.body_md, b.status, b.created_at,
33
39
  b.source, b.source_ref,
34
40
  ${BLOCKING_TASK_IDS_SQL}
35
- FROM blockers b
41
+ FROM blockers b ${BLOCKING_TASKS_JOIN}
36
42
  WHERE b.status = 'open'
37
43
  ORDER BY b.id${page}`,
38
44
  params
@@ -50,7 +56,7 @@ async function getBlocker(id) {
50
56
  `SELECT b.id, b.title, b.body_md, b.status, b.created_at, b.resolved_at, b.resolved_by,
51
57
  b.resolution_note, b.source, b.source_ref,
52
58
  ${BLOCKING_TASK_IDS_SQL}
53
- FROM blockers b WHERE b.id = $1`,
59
+ FROM blockers b ${BLOCKING_TASKS_JOIN} WHERE b.id = $1`,
54
60
  [id]
55
61
  );
56
62
  return rows[0] || null;
@@ -21,7 +21,7 @@ async function getActiveClaimsForBuilder(builderId) {
21
21
  `SELECT c.id AS claim_id, c.task_id, c.worktree_name, c.claimed_at,
22
22
  c.head_sha, c.reserved_migration_number,
23
23
  c.release_requested_by, c.release_requested_at, c.release_requested_note,
24
- t.title, t.touches, t.priority, t.credits_reward, t.version_id, t.module_key,
24
+ t.title, t.short_name, t.touches, t.priority, t.credits_reward, t.version_id, t.module_key,
25
25
  -- task 1003855: the craft of the claimed work. A speciality contract is
26
26
  -- craft-scoped, so the Conductor needs to know which craft is in play
27
27
  -- before it can pick the right one to inject.
@@ -485,7 +485,7 @@ const UNPROVABLE_STRAND_REASON = 'strand:no_branch_no_tip';
485
485
  // release it.
486
486
  async function gatingRebaseTasksForBuilder(builderId, client = pool, graceMs = UNPROVABLE_GATE_GRACE_MS) {
487
487
  const { rows } = await client.query(
488
- `SELECT id, title, needs_rebase_reason, needs_rebase_at
488
+ `SELECT id, title, short_name, needs_rebase_reason, needs_rebase_at
489
489
  FROM tasks
490
490
  WHERE needs_rebase = true AND status = 'confirmed' AND shipped_by = $1
491
491
  AND NOT (
@@ -501,7 +501,7 @@ async function gatingRebaseTasksForBuilder(builderId, client = pool, graceMs = U
501
501
 
502
502
  async function needsRebaseTasksForBuilder(builderId, client = pool) {
503
503
  const { rows } = await client.query(
504
- `SELECT id, title, needs_rebase_reason, needs_rebase_at
504
+ `SELECT id, title, short_name, needs_rebase_reason, needs_rebase_at
505
505
  FROM tasks
506
506
  WHERE needs_rebase = true AND status = 'confirmed' AND shipped_by = $1
507
507
  ORDER BY needs_rebase_at ASC NULLS LAST, id ASC`,
@@ -24,6 +24,7 @@ const { gateOnSpecTask } = require('./goal-spec-task.js');
24
24
  const { ensureArtistReviewGoal } = require('./artist-review-goal.js');
25
25
  const { notPageMarkerSql } = require('./page-tweak-artist.js');
26
26
  const { needsFilterWhere, capabilityFeedWhere } = require('./task-needs-gate.js');
27
+ const { deriveTaskName } = require('./task-name.js');
27
28
 
28
29
  // -------------------------------------------------------------------------
29
30
  // Tasks
@@ -270,13 +271,13 @@ async function createTask({
270
271
  est_minutes, est_cost_usd, manual_degree, priority,
271
272
  automation_tag, credits_reward, source, source_ref, value_summary,
272
273
  kind, discipline, parallel_safe, newcomer_friendly, needs_migration,
273
- security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at, needs)
274
+ security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at, needs, short_name)
274
275
  VALUES ($1, $2, $3, $4, $5,
275
276
  $6, $7, $8, $9,
276
277
  $10, $11, $12, $13, $14,
277
278
  $15, $16, $17, $18, $19,
278
279
  $20, $21, $22, $23, $24,
279
- CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END, $25)
280
+ CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END, $25, $26)
280
281
  RETURNING *`,
281
282
  [
282
283
  versionId,
@@ -303,7 +304,7 @@ async function createTask({
303
304
  moduleKey ?? null,
304
305
  goalIdEffective ?? null,
305
306
  createdBy ?? null,
306
- Array.isArray(needs) ? needs : [],
307
+ Array.isArray(needs) ? needs : [], deriveTaskName(title, description), // $26: the short name (task 1004958)
307
308
  ]
308
309
  );
309
310
  // task 1677 / ADR 0096: audit each creation-time auto-assignment so the trail is
@@ -486,7 +487,6 @@ async function updateTaskPriority(id, priority) { return updateTaskColumn(id, 'p
486
487
  // keeps on every other value_summary write.
487
488
  async function updateTaskValueSummary(id, valueSummary) { return updateTaskColumn(id, 'value_summary', valueSummary); }
488
489
 
489
-
490
490
  async function updateTaskDiscipline(id, discipline) { return updateTaskColumn(id, 'discipline', discipline); }
491
491
 
492
492
  // #360 / ADR 0018 + V3.R15 #239: open (or close) a task to Xenos claims by
@@ -0,0 +1,50 @@
1
+ -- lifecycle_019_pay_type_gates.sql — task 1004991 (goal 1000092; ADR 0385, "Pay-type
2
+ -- gates").
3
+ --
4
+ -- A pay type becomes switchable only after its gate is cleared and recorded. A
5
+ -- gate is a short list of requirements per pay type (the list lives in
6
+ -- modules/lifecycle/pay-type-gates.js, taken from the ADR's table); this table
7
+ -- records, for each requirement, who said it is met, when, and the evidence they
8
+ -- point at. The gate is cleared when every requirement's NEWEST row says 'cleared'.
9
+ -- The instance is the project, so there is no project column: one database, one
10
+ -- project.
11
+ --
12
+ -- A LIFECYCLE MIGRATION, NOT A CORE ONE: a brand-new table the lifecycle module
13
+ -- owns (pay-type-gates.js is its only reader and writer), the lifecycle_018_pay_terms
14
+ -- precedent.
15
+ --
16
+ -- RECORDS ONLY. Nothing here moves money. Clearing a requirement is a person's act
17
+ -- (often with counsel) and the evidence column holds only a REFERENCE to it (a
18
+ -- document title, a ticket, a processor verdict id) — never identity, tax or KYC
19
+ -- data, which Bongos does not store.
20
+ --
21
+ -- APPEND-ONLY. Every clearing or revoking is a new row; the newest row per
22
+ -- (pay_type, requirement) is current, and a row trigger refuses UPDATE and DELETE,
23
+ -- so the table is its own audit trail. The database also refuses outright any row for
24
+ -- eu_labour: until a later ADR enables it, no code path, however forged, can clear it.
25
+
26
+ CREATE TABLE IF NOT EXISTS lifecycle_pay_type_gates (
27
+ id bigserial PRIMARY KEY,
28
+ pay_type text NOT NULL CHECK (pay_type IN
29
+ ('cash', 'equity', 'module_share', 'credit_pool', 'non_monetary', 'self_managed')),
30
+ requirement text NOT NULL CHECK (char_length(requirement) BETWEEN 1 AND 64),
31
+ state text NOT NULL CHECK (state IN ('cleared', 'revoked')),
32
+ evidence_ref text NOT NULL CHECK (char_length(evidence_ref) BETWEEN 1 AND 500),
33
+ decided_by bigint NOT NULL REFERENCES builders(id),
34
+ decided_at timestamptz NOT NULL DEFAULT now()
35
+ );
36
+
37
+ -- The current state of a requirement: newest first.
38
+ CREATE INDEX IF NOT EXISTS lifecycle_pay_type_gates_current
39
+ ON lifecycle_pay_type_gates (pay_type, requirement, decided_at DESC, id DESC);
40
+
41
+ CREATE OR REPLACE FUNCTION lifecycle_pay_type_gates_refuse_change() RETURNS trigger AS $$
42
+ BEGIN
43
+ RAISE EXCEPTION 'pay-type gates are a history: % on % is refused; record a new row instead', TG_OP, TG_TABLE_NAME
44
+ USING ERRCODE = 'check_violation';
45
+ END;
46
+ $$ LANGUAGE plpgsql;
47
+
48
+ DROP TRIGGER IF EXISTS lifecycle_pay_type_gates_append_only ON lifecycle_pay_type_gates;
49
+ CREATE TRIGGER lifecycle_pay_type_gates_append_only BEFORE UPDATE OR DELETE ON lifecycle_pay_type_gates
50
+ FOR EACH ROW EXECUTE FUNCTION lifecycle_pay_type_gates_refuse_change();
@@ -29,6 +29,7 @@
29
29
  "kickoff-seed",
30
30
  "rot",
31
31
  "project-settings",
32
+ "pay-type-gates",
32
33
  "artist-gate",
33
34
  "removal-proposals",
34
35
  "sky",
@@ -0,0 +1,180 @@
1
+ // modules/lifecycle/pay-type-gates.js — a pay type may be put on a task only after its
2
+ // gate is cleared for the project (task 1004991, goal 1000092; ADR 0385 "Pay-type gates").
3
+ //
4
+ // A gate is a fixed list of requirements per pay type, taken from the ADR's table and
5
+ // written down here as data. A person records each requirement as cleared (with a
6
+ // reference to the evidence) or revoked; the gate is cleared when every requirement's
7
+ // newest record says cleared. The pay-terms write paths ask `refuseUnclearedGates`
8
+ // before they write, so the server refuses a pay term whose type has an open gate.
9
+ //
10
+ // FOUR RULES THIS FILE HOLDS:
11
+ // 1. The list is data, not prose: REQUIREMENTS is the one place the ADR's table lives
12
+ // in code, and the table pins it against the pay-type list pay-terms.js accepts.
13
+ // 2. eu_labour can never be cleared. It is not in any list a write accepts and the
14
+ // table's CHECK refuses a row for it, so no request, however forged, reaches a
15
+ // cleared state (a later ADR enables it, per ADR 0385's EU labour row).
16
+ // 3. self_managed clears with one act, the acknowledgement that Bongos moves and
17
+ // guarantees nothing. Nothing else is asked of it.
18
+ // 4. Records only. Evidence is a REFERENCE a person typed; Bongos stores no identity,
19
+ // tax or KYC data here, and nothing in this file moves money.
20
+ //
21
+ // Who may record a requirement is decided by the ROUTE (the project owner, on the
22
+ // server — ADR 0016); who recorded it comes from the signed-in builder, never the body.
23
+
24
+ const api = require('../../src/module-api');
25
+ const { PAY_TYPES, SELF_MANAGED_LABEL } = require('./pay-terms');
26
+
27
+ const NEVER_CLEARABLE = ['eu_labour'];
28
+ const ACKNOWLEDGEMENT = 'bongos_moves_and_guarantees_nothing';
29
+ const EVIDENCE_MAX = 500;
30
+
31
+ // The machine-checkable list per pay type (ADR 0385, "Must exist first").
32
+ const REQUIREMENTS = {
33
+ cash: [
34
+ ['processor_onboarding', 'Processor onboarding done'],
35
+ ['tax_details', 'Tax details collected'],
36
+ ['age_check', 'Age check passed'],
37
+ ['sanctions_screening', 'Sanctions screening done'],
38
+ ['signed_rule', 'Signed pay rule on file'],
39
+ ],
40
+ equity: [
41
+ ['counsel_approved_structure', 'The project\'s own counsel-approved structure'],
42
+ ['one_signature_per_grant', 'One signature per grant'],
43
+ ['no_percentage_fee', 'No percentage fee taken'],
44
+ ['wording_filter', 'Credit and valuation wording filter in force'],
45
+ ],
46
+ module_share: [
47
+ ['standard_module_licence', 'Standard module licence'],
48
+ ['takedown_contact', 'Takedown contact named'],
49
+ ['payee_onboarding', 'Payee onboarding done'],
50
+ ],
51
+ credit_pool: [
52
+ ['credit_wording_in_force', 'Credit wording rule (ADR 0385 decision 9) in force'],
53
+ ['model_inputs_visible', 'Model inputs visible to the people affected'],
54
+ ['signed_rule', 'Signed pay rule on file'],
55
+ ],
56
+ non_monetary: [
57
+ ['guardian_consent_13_15', 'Guardian consent for ages 13 to 15'],
58
+ ['no_cash_redemption', 'No cash redemption of any credit'],
59
+ ],
60
+ self_managed: [
61
+ [ACKNOWLEDGEMENT, 'Acknowledged: Bongos moves and guarantees nothing'],
62
+ ],
63
+ };
64
+
65
+ function requirementIds(payType) {
66
+ return (REQUIREMENTS[payType] || []).map(([id]) => id);
67
+ }
68
+
69
+ // PURE. The verdict for one pay type, from its current rows ({ requirement, state,
70
+ // evidence_ref, decided_by, decided_at }, newest per requirement). A requirement with
71
+ // no row, or whose newest row is revoked, is missing. Rows for requirements not in the
72
+ // list are ignored, so a stray row can never clear a gate.
73
+ function gateVerdict(payType, currentRows) {
74
+ if (NEVER_CLEARABLE.includes(payType)) {
75
+ return { pay_type: payType, cleared: false, never: true, requirements: [], missing: [] };
76
+ }
77
+ const byReq = new Map((currentRows || []).filter((r) => r.pay_type == null || r.pay_type === payType).map((r) => [r.requirement, r]));
78
+ const requirements = (REQUIREMENTS[payType] || []).map(([id, label]) => {
79
+ const r = byReq.get(id);
80
+ const met = !!r && r.state === 'cleared';
81
+ return {
82
+ requirement: id,
83
+ label,
84
+ met,
85
+ evidence_ref: r ? r.evidence_ref : null,
86
+ cleared_by: met ? r.decided_by : null,
87
+ cleared_at: met ? r.decided_at : null,
88
+ };
89
+ });
90
+ const missing = requirements.filter((x) => !x.met).map((x) => x.requirement);
91
+ return { pay_type: payType, cleared: requirements.length > 0 && missing.length === 0, never: false, requirements, missing };
92
+ }
93
+
94
+ // The newest row per (pay_type, requirement) for these pay types, one read.
95
+ async function readCurrentRows(payTypes, { db } = {}) {
96
+ const exec = db || api.pool;
97
+ const { rows } = await exec.query(
98
+ `SELECT DISTINCT ON (pay_type, requirement) pay_type, requirement, state, evidence_ref, decided_by, decided_at
99
+ FROM lifecycle_pay_type_gates
100
+ WHERE pay_type = ANY($1::text[])
101
+ ORDER BY pay_type, requirement, decided_at DESC, id DESC`, [payTypes]);
102
+ return rows;
103
+ }
104
+
105
+ // Every pay type's verdict, for the owner's view (and the settings page).
106
+ async function readGates({ db } = {}) {
107
+ const rows = await readCurrentRows(PAY_TYPES, { db });
108
+ const gates = PAY_TYPES.map((p) => gateVerdict(p, rows.filter((r) => r.pay_type === p)));
109
+ for (const p of NEVER_CLEARABLE) gates.push(gateVerdict(p, []));
110
+ return gates;
111
+ }
112
+
113
+ // PURE. A verdict as a non-owner may see it: met or missing per requirement, without the
114
+ // evidence reference a person typed or who cleared it (those are the owner's own records).
115
+ function withoutEvidence(gate) {
116
+ return { ...gate, requirements: gate.requirements.map(({ evidence_ref, cleared_by, cleared_at, ...rest }) => rest) };
117
+ }
118
+
119
+ // Checks a list of normalized terms against the gates. Returns null when every type
120
+ // in it is cleared, else { code, message, pay_type, missing } for the first that is
121
+ // not. One read however many terms there are, and none for an empty list.
122
+ async function refuseUnclearedGates(terms, { db } = {}) {
123
+ const types = [...new Set((terms || []).map((t) => t.pay_type))];
124
+ if (!types.length) return null;
125
+ const rows = await readCurrentRows(types, { db });
126
+ for (const payType of types) {
127
+ const v = gateVerdict(payType, rows.filter((r) => r.pay_type === payType));
128
+ if (!v.cleared) {
129
+ return {
130
+ code: 'pay_type_gate_not_cleared',
131
+ pay_type: payType,
132
+ missing: v.missing,
133
+ message: `The ${payType} pay type is not switched on for this project: its gate is not cleared (still needed: ${v.missing.join(', ')}).`,
134
+ };
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ // PURE. Checks a request body to record one requirement. Returns { state, evidenceRef }
141
+ // or { status, code, error }. The recorder is never read from the body.
142
+ function normalizeGateWrite(payType, requirement, body) {
143
+ if (NEVER_CLEARABLE.includes(payType)) {
144
+ return { status: 409, code: 'pay_type_never_clearable', error: `${payType} cannot be cleared; it stays off until a later decision enables it.` };
145
+ }
146
+ if (!PAY_TYPES.includes(payType)) return { status: 404, code: 'unknown_pay_type', error: `pay type must be one of ${PAY_TYPES.join(', ')}.` };
147
+ if (!requirementIds(payType).includes(requirement)) {
148
+ return { status: 404, code: 'unknown_requirement', error: `${payType} has no such requirement; it has ${requirementIds(payType).join(', ')}.` };
149
+ }
150
+ const b = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
151
+ const extra = Object.keys(b).filter((k) => !['state', 'evidence_ref', 'acknowledged'].includes(k));
152
+ if (extra.length) return { status: 400, code: 'unknown_field', error: `unknown field: ${extra.join(', ')}.` };
153
+ if (b.state !== 'cleared' && b.state !== 'revoked') return { status: 400, code: 'bad_state', error: 'state is "cleared" or "revoked".' };
154
+ if (requirement === ACKNOWLEDGEMENT && b.state === 'cleared') {
155
+ if (b.acknowledged !== true) {
156
+ return { status: 400, code: 'acknowledgement_required', error: `Send acknowledged: true. It records: ${SELF_MANAGED_LABEL}` };
157
+ }
158
+ return { state: 'cleared', evidenceRef: SELF_MANAGED_LABEL };
159
+ }
160
+ const ev = typeof b.evidence_ref === 'string' ? b.evidence_ref.trim() : '';
161
+ if (!ev || ev.length > EVIDENCE_MAX) {
162
+ return { status: 400, code: 'bad_evidence_ref', error: `evidence_ref is a reference to the evidence (or the reason, for a revoke), 1 to ${EVIDENCE_MAX} characters.` };
163
+ }
164
+ return { state: b.state, evidenceRef: ev };
165
+ }
166
+
167
+ // Appends one record. Returns the gate's new verdict for that pay type.
168
+ async function recordRequirement({ payType, requirement, state, evidenceRef, builderId }, { db } = {}) {
169
+ const exec = db || api.pool;
170
+ await exec.query(
171
+ `INSERT INTO lifecycle_pay_type_gates (pay_type, requirement, state, evidence_ref, decided_by)
172
+ VALUES ($1, $2, $3, $4, $5)`, [payType, requirement, state, evidenceRef, builderId]);
173
+ const rows = await readCurrentRows([payType], { db: exec });
174
+ return gateVerdict(payType, rows);
175
+ }
176
+
177
+ module.exports = {
178
+ REQUIREMENTS, ACKNOWLEDGEMENT,
179
+ requirementIds, gateVerdict, readGates, refuseUnclearedGates, normalizeGateWrite, recordRequirement, withoutEvidence,
180
+ };
@@ -981,6 +981,34 @@ async function runCraftAchievementBackfill(deps = {}) {
981
981
  }
982
982
  }
983
983
 
984
+ // task 1004958: name the tasks that have no short name yet — every task created
985
+ // before names existed, and any row a path outside createTask inserted. Pure DB,
986
+ // idempotent (it writes only `short_name IS NULL` rows), and bounded: at most
987
+ // TASK_NAME_BATCHES batches a run, so the first run on a big board drains it in
988
+ // one go and every later run is one indexed read that finds next to nothing.
989
+ const TASK_NAME_BATCHES = 20;
990
+ async function runTaskNameBackfill(deps = {}) {
991
+ const { logFn, errFn } = loggers(deps);
992
+ const tasksDb = (deps.tasksDb && typeof deps.tasksDb.backfillTaskNames === 'function')
993
+ ? deps.tasksDb
994
+ : require('./task-name-db.js');
995
+ const exec = deps.pool || seams.pool;
996
+ let named = 0;
997
+ try {
998
+ for (let i = 0, after = 0; i < TASK_NAME_BATCHES; i++) {
999
+ const out = await tasksDb.backfillTaskNames({ exec, after });
1000
+ named += out.named;
1001
+ after = out.next;
1002
+ if (!out.scanned) break;
1003
+ }
1004
+ if (named) logFn(`[publish-reconciler] task names: ${named} task(s) named`);
1005
+ return { named };
1006
+ } catch (e) {
1007
+ errFn(`[publish-reconciler] task name backfill (non-blocking): ${e && e.message}`);
1008
+ return { skipped: 'error', error: e && e.message, named };
1009
+ }
1010
+ }
1011
+
984
1012
  // The sweeps one tick fires. Each is INDEPENDENT on purpose: the cost (task 1366)
985
1013
  // and criterion (task 1002069) sweeps are pure DB — no push credential — and a
986
1014
  // failure in either must never touch the confirmed→shipped work.
@@ -989,7 +1017,7 @@ async function runCraftAchievementBackfill(deps = {}) {
989
1017
  // between runs; none means every tick (INTERVAL_MS). The publish, cost and
990
1018
  // criterion sweeps keep the tick — a strand, an unpriced session and a newly
991
1019
  // satisfiable criterion are all things someone is waiting on. The promotion sweep
992
- // and the craft backfill run HOURLY: the ship-time triggers already promote and
1020
+ // and the craft and task-name backfills run HOURLY: the ship-time triggers already promote and
993
1021
  // unlock on the event, so these are backlog drains, and a drain an hour late
994
1022
  // costs nobody anything while running it every 5 minutes cost every instance
995
1023
  // ~288 scans a day.
@@ -1000,6 +1028,7 @@ const SWEEPS = [
1000
1028
  ['criterion sweep', runCriterionSweep],
1001
1029
  ['promotion sweep', runPromotionSweep, HOURLY_MS],
1002
1030
  ['craft achievement backfill', runCraftAchievementBackfill, HOURLY_MS],
1031
+ ['task name backfill', runTaskNameBackfill, HOURLY_MS],
1003
1032
  ];
1004
1033
 
1005
1034
  // buildTick — one interval round, with a per-sweep overlap guard (task 1004924).
@@ -1083,6 +1112,7 @@ module.exports = {
1083
1112
  runPromotionSweep,
1084
1113
  runCriterionSweep,
1085
1114
  runCraftAchievementBackfill,
1115
+ runTaskNameBackfill,
1086
1116
  buildTick,
1087
1117
  couldBeStaleGenerated,
1088
1118
  describeTarget,
@@ -70,8 +70,9 @@ function splitRebaseGate(rows, { limit = gateLimit() } = {}) {
70
70
  // the hint and the CLI can say "waiting, not on you".
71
71
  function rebaseRefusalTasks(split) {
72
72
  return [
73
- ...split.gating.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason })),
74
- ...split.waiting.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason, waiting: true, waits_on: t.waits_on })),
73
+ // task 1004958: short_name rides along so every surface can write "task N - name".
74
+ ...split.gating.map((t) => ({ id: t.id, short_name: t.short_name || null, title: t.title, reason: t.needs_rebase_reason })),
75
+ ...split.waiting.map((t) => ({ id: t.id, short_name: t.short_name || null, title: t.title, reason: t.needs_rebase_reason, waiting: true, waits_on: t.waits_on })),
75
76
  ];
76
77
  }
77
78
 
@@ -100,12 +100,13 @@ function taskNotReadyHint(taskId, currentStatus) {
100
100
  // task 1004788: only the FIXABLE tasks gate (rebase-gate.js); a task set aside
101
101
  // because it waits on something outside the builder is listed after them as
102
102
  // "waiting, not on you" so nobody chases it.
103
+ const { taskRef } = require('../task-name.js'); // task 1004958
103
104
  function rebaseRequiredHint(rebaseTasks) {
104
105
  const all = rebaseTasks || [];
105
- const ids = all.filter((t) => !t.waiting).map((t) => `task ${t.id}`);
106
+ const ids = all.filter((t) => !t.waiting).map((t) => taskRef(t.id, t.short_name));
106
107
  const waiting = all.filter((t) => t.waiting);
107
108
  const aside = waiting.length
108
- ? ` Waiting, not on you (set aside; they land on their own when what they wait on clears): ${waiting.map((t) => `task ${t.id} (${t.waits_on || 'outside you'})`).join('; ')}.`
109
+ ? ` Waiting, not on you (set aside; they land on their own when what they wait on clears): ${waiting.map((t) => `${taskRef(t.id, t.short_name)} (${t.waits_on || 'outside you'})`).join('; ')}.`
109
110
  : '';
110
111
  return `Your queue is gated: ${ids.length} confirmed task(s) of yours are waiting to land and need you (${ids.join(', ') || 'unknown'}).${aside} Clear it with /strand-fix N (works on any machine), or bring the branch up to date with \`git merge origin/main\` and re-ship — MERGE, never rebase: server-mediated publish is append-only, and rewriting a published branch makes it un-pushable (task 1002788). /merge-mode only if this machine has a GitHub push credential. To give up on one instead, /builder-release it. If CI is simply backed up, waiting is correct: a task already landed clears the gate on its own. If main's CI is red for a reason outside your branch, the fix is still claimable: a P1 kind=infra task passes this gate (task 1004370).`;
111
112
  }
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/routes/pay-type-gates.js — read, and record, the gate each pay type
4
+ // must clear before it can be put on a task (task 1004991, goal 1000092; ADR 0385
5
+ // "Pay-type gates"). The rules live in ../pay-type-gates.js; this file is the doorway.
6
+ //
7
+ // GET /pay-type-gates — every pay type's gate: each requirement, whether it is met, the
8
+ // evidence reference, and who cleared it and when. eu_labour is listed as never clearable.
9
+ // PUT /pay-type-gates/:payType/:requirement — body { state: 'cleared'|'revoked',
10
+ // evidence_ref } (or { state: 'cleared', acknowledged: true } for the self-managed
11
+ // acknowledgement). Appends one record; the recorder is the signed-in owner, never the
12
+ // body. Clearing a real gate is a person's act, so nothing here clears one by itself.
13
+ //
14
+ // MODULE BOUNDARY: reaches core only through `../../../src/module-api`.
15
+
16
+ const express = require('express');
17
+ const api = require('../../../src/module-api');
18
+ const gates = require('../pay-type-gates');
19
+
20
+ const log = api.logger('lifecycle');
21
+
22
+ module.exports = function buildPayTypeGatesRouter() {
23
+ const router = express.Router();
24
+
25
+ // rank: any-builder for met/missing (the same pay information `GET /pay-terms` shows, no person's
26
+ // money); the evidence references and who cleared each requirement are the owner's records, so
27
+ // they come back for an archon only.
28
+ router.get('/pay-type-gates', api.requireBuilder, async (req, res) => {
29
+ try {
30
+ const all = await gates.readGates();
31
+ res.json({ gates: req.builder.rank === 'archon' ? all : all.map(gates.withoutEvidence) });
32
+ } catch (err) {
33
+ log.error(`GET /pay-type-gates: ${err && err.message}`);
34
+ res.fail('pay_type_gates_read_failed', { status: 500, message: 'internal error' });
35
+ }
36
+ });
37
+
38
+ // rank: payout.approve (archon floor). Whether a pay type is legal for this project is the
39
+ // owner's call, often with counsel, and it is the same authority that approves payouts — no new
40
+ // atom is minted for it (ADR 0016: enforced here, on the server).
41
+ router.put('/pay-type-gates/:payType/:requirement', api.requireBuilder, api.requirePermission('payout.approve'), async (req, res) => {
42
+ const { payType, requirement } = req.params;
43
+ // Unknown fields (a forged cleared_by, say) are refused here; the values are checked by the lib.
44
+ if (api.validateOrRespond(req, res, { state: {}, evidence_ref: {}, acknowledged: {} })) return;
45
+ const parsed = gates.normalizeGateWrite(payType, requirement, req.body);
46
+ if (parsed.error) return res.fail(parsed.code, { status: parsed.status, message: parsed.error });
47
+ try {
48
+ const gate = await gates.recordRequirement({
49
+ payType, requirement, state: parsed.state, evidenceRef: parsed.evidenceRef, builderId: req.builder.id,
50
+ });
51
+ res.json({ ok: true, gate });
52
+ } catch (err) {
53
+ log.error(`PUT /pay-type-gates: ${err && err.message}`);
54
+ res.fail('pay_type_gates_write_failed', { status: 500, message: 'internal error' });
55
+ }
56
+ });
57
+
58
+ return router;
59
+ };
@@ -35,6 +35,7 @@ const api = require('../../../src/module-api');
35
35
  const goalCategory = require('../goal-category');
36
36
  const autopickup = require('../autopickup');
37
37
  const payTermsLib = require('../pay-terms'); // task 1004990
38
+ const payTypeGates = require('../pay-type-gates'); // task 1004991
38
39
 
39
40
  const log = api.logger('lifecycle');
40
41
 
@@ -551,6 +552,9 @@ module.exports = function buildProjectSettingsRouter() {
551
552
  const parsed = payTermsLib.normalizePayTerms(req.body.terms);
552
553
  if (parsed.error) return res.fail('bad_pay_terms', { status: 400, message: parsed.error });
553
554
  try {
555
+ // task 1004991: the project's standing terms obey the same gate as a task's (ADR 0385).
556
+ const gateRefusal = await payTypeGates.refuseUnclearedGates(parsed.terms);
557
+ if (gateRefusal) return res.fail(gateRefusal.code, { status: 409, message: gateRefusal.message });
554
558
  await payTermsLib.setPayTerms({ taskId: null, terms: parsed.terms, builderId: req.builder.id });
555
559
  const policy = await payTermsLib.readProjectPayTerms();
556
560
  res.json({ ok: true, row: payTermsRow(policy) });