@bongos/core 1.18.0 → 1.18.18

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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -58,7 +58,13 @@
58
58
  // Cookie-authed PATCH; throws { status, data } on a non-2xx. The server enforces
59
59
  // the Metic+ gate (ADR 0018); the button only renders for metic+ below.
60
60
  async function patchJSON(path, body) {
61
- var res = await api.request('PATCH', path, { body: body || {} });
61
+ return writeJSON('PATCH', path, body);
62
+ }
63
+ async function postJSON(path, body) {
64
+ return writeJSON('POST', path, body);
65
+ }
66
+ async function writeJSON(method, path, body) {
67
+ var res = await api.request(method, path, { body: body || {} });
62
68
  if (res.status === 401) { window.location.assign(API + '/auth/web/start'); throw new Error('redirecting to auth'); }
63
69
  var data = null;
64
70
  try { data = res.data; } catch (_) { /* empty / non-JSON */ }
@@ -66,6 +72,16 @@
66
72
  return data;
67
73
  }
68
74
 
75
+ // dom-utils.js already publishes escapeHtml on window.OTB (this file uses
76
+ // OTB.toast the same way) — a local copy would be its 9th byte-identical twin.
77
+ // The fallback keeps the file's self-contained contract if OTB hasn't loaded.
78
+ function esc(s) {
79
+ if (window.OTB && window.OTB.escapeHtml) return window.OTB.escapeHtml(s);
80
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
81
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
82
+ });
83
+ }
84
+
69
85
  async function fetchRef(kind, id) {
70
86
  if (kind === 'blocker') {
71
87
  var db = await getJSON(API + '/blockers/' + encodeURIComponent(id), { softAuth: true });
@@ -102,37 +118,342 @@
102
118
  }
103
119
  if (r.shipped_at) nodes.push(h('p', 'deeplink-card__note', 'Shipped ' + fmtRel(r.shipped_at)));
104
120
 
105
- // task 759: Metic+ idea triage — a Discard control on an OPEN idea, right in
106
- // the overlay (no /idea-triage skill / LLM in the loop). The button only
107
- // renders for metic/archon; the server enforces the same gate regardless.
121
+ // task 759 → task 1002447: Metic+ idea triage, right in the overlay (no
122
+ // /idea-triage skill and no LLM in the loop). 759 shipped Discard alone;
123
+ // 1002447 adds the other two verbs so the whole verdict set is reachable
124
+ // from the browser — criterion `gcarry-bx-hall`. The panel only renders for
125
+ // metic/archon; the server enforces the same gate regardless.
108
126
  var rank = (viewer && viewer.rank) ? String(viewer.rank).toLowerCase() : '';
109
127
  if (kind === 'idea' && r.status === 'open' && (rank === 'metic' || rank === 'archon')) {
110
- var actions = h('div', 'td-actions');
111
- var discardBtn = document.createElement('button');
112
- discardBtn.type = 'button';
113
- discardBtn.className = 'td-btn';
114
- discardBtn.textContent = 'Discard';
115
- discardBtn.addEventListener('click', function () {
116
- discardBtn.disabled = true;
117
- discardBtn.textContent = '…';
118
- patchJSON(API + '/inbox/' + encodeURIComponent(r.id), { status: 'discarded' })
119
- .then(function () { toast('Idea ' + label + ' discarded.'); clearLink(); })
120
- .catch(function (err) {
121
- if (err && err.message === 'redirecting to auth') return;
122
- var code = err && err.data && err.data.error;
123
- toast(code === 'INSUFFICIENT_RANK' || err.status === 403
124
- ? 'Only Metic+ can triage ideas.'
125
- : 'Could not discard that idea. Try again.');
126
- discardBtn.disabled = false;
127
- discardBtn.textContent = 'Discard';
128
- });
129
- });
130
- actions.appendChild(discardBtn);
131
- nodes.push(actions);
128
+ nodes.push(triagePanel(r));
129
+ }
130
+ // A closed idea says what triage decided — otherwise the verdict is a status
131
+ // word with no story behind it.
132
+ if (kind === 'idea' && r.status === 'merged' && r.merged_into_idea_id) {
133
+ var m = h('p', 'deeplink-card__note');
134
+ m.appendChild(document.createTextNode('Merged into '));
135
+ var ml = h('a', 'deeplink-card__link', '#' + r.merged_into_idea_id);
136
+ ml.href = '#/idea/' + encodeURIComponent(r.merged_into_idea_id);
137
+ m.appendChild(ml);
138
+ nodes.push(m);
139
+ }
140
+ if (kind === 'idea' && r.triage_note) {
141
+ nodes.push(h('p', 'deeplink-card__note', 'Triage note: ' + r.triage_note));
132
142
  }
133
143
  return nodes;
134
144
  }
135
145
 
146
+ // ---- the triage panel (task 1002447) ---------------------------------------
147
+ //
148
+ // Promote → POST /tasks, then PATCH the idea promoted + linked to it.
149
+ // Merge → PATCH merged, pointing at the canonical idea (core_195).
150
+ // Discard → PATCH discarded, with the reason (core_195).
151
+ //
152
+ // Each verb is a disclosure: the action row swaps for a compact form and back,
153
+ // the goals-page invite-box idiom (goals-page.js renderInviteBox). One panel
154
+ // owns its mode and always rebuilds from scratch, so there is no half-updated
155
+ // state to reason about.
156
+
157
+ // Mirrors LIMITS.TITLE / LIMITS.DESCRIPTION (src/bongos/routes/_helpers.js):
158
+ // the form stops where POST /tasks stops, so a builder can't write past the
159
+ // server's cap and get a 400 back for it.
160
+ var TITLE_MAX = 200;
161
+ var DESCRIPTION_MAX = 50000;
162
+
163
+ // The open versions a promotion can target. POST /tasks 409s on a shipped
164
+ // version, so offering one would be an invitation to fail. Cached for the page.
165
+ var versionsCache = null;
166
+ async function openVersions() {
167
+ if (versionsCache) return versionsCache;
168
+ var d = await getJSON(API + '/versions?limit=200', { softAuth: true });
169
+ versionsCache = (d && d.versions ? d.versions : []).filter(function (v) { return v.status !== 'shipped'; });
170
+ return versionsCache;
171
+ }
172
+
173
+ // Home's attention inbox holds the same rows; tell it a verdict landed so the
174
+ // queue advances behind the overlay instead of going stale until a reload.
175
+ function announce(id, verdict) {
176
+ try {
177
+ window.dispatchEvent(new CustomEvent('otb:idea-triaged', { detail: { id: id, verdict: verdict } }));
178
+ } catch (_) { /* CustomEvent unsupported — the refresh is a nicety */ }
179
+ }
180
+
181
+ // The R01/ADR-0116 envelope is `{ error: { code, message, details } }` — the
182
+ // code is NESTED. Reading err.data.error directly yields the OBJECT, so every
183
+ // `code === '…'` test below would silently never match. The string arm is the
184
+ // pre-envelope shape, kept so an older server still maps.
185
+ function errCode(err) {
186
+ var d = err && err.data;
187
+ var e = d && d.error;
188
+ if (e && typeof e === 'object') return e.code || '';
189
+ if (typeof e === 'string') return e;
190
+ return (d && d.code) || '';
191
+ }
192
+ function errMessage(err) {
193
+ var d = err && err.data;
194
+ var e = d && d.error;
195
+ if (e && typeof e === 'object' && e.message) return e.message;
196
+ return (d && d.message) || '';
197
+ }
198
+
199
+ function triageError(err, fallback) {
200
+ if (err && err.message === 'redirecting to auth') return null;
201
+ var code = errCode(err);
202
+ // 'rank_forbidden' is what auth.js requireRank actually emits; the 403 arm
203
+ // also covers POST /tasks, which is Metic+ for the same reason.
204
+ if (code === 'rank_forbidden' || (err && err.status === 403)) return 'Only Metic+ can triage ideas.';
205
+ if (code === 'MERGE_SELF') return 'An idea can’t be merged into itself.';
206
+ if (code === 'MERGE_TARGET_NOT_FOUND') return 'No idea with that number.';
207
+ if (code === 'MERGE_TARGET_MERGED') return 'That idea was itself merged — merge into the one it folded into.';
208
+ if (code === 'version_closed') return 'That version has shipped and can’t take new work.';
209
+ if (code === 'version_not_found') return 'That version no longer exists.';
210
+ return errMessage(err) || fallback;
211
+ }
212
+
213
+ function triagePanel(idea) {
214
+ var box = h('div', 'td-triage');
215
+ var mode = 'idle';
216
+
217
+ // [data-err] marks the one paragraph errors belong in; the merge form also
218
+ // carries a hint paragraph, so a class-based lookup would race it.
219
+ function note(text) {
220
+ var p = box.querySelector('[data-err]');
221
+ if (p) p.textContent = text || '';
222
+ }
223
+ function busy(on, submitLabel) {
224
+ box.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = on; });
225
+ var submit = box.querySelector('[data-submit]');
226
+ if (submit) submit.textContent = on ? '…' : submitLabel;
227
+ }
228
+ function go(next) { mode = next; render(); }
229
+
230
+ // A verdict that leaves nothing to look at closes the overlay; promote
231
+ // produces a task worth linking to, so it stays open on a result line.
232
+ function settled(verdict, message) {
233
+ announce(idea.id, verdict);
234
+ toast(message);
235
+ clearLink();
236
+ }
237
+
238
+ function render() {
239
+ if (mode === 'idle') return renderIdle();
240
+ if (mode === 'promote') return renderPromote();
241
+ if (mode === 'merge') return renderMerge();
242
+ if (mode === 'discard') return renderDiscard();
243
+ }
244
+
245
+ function renderIdle() {
246
+ box.innerHTML =
247
+ '<div class="td-actions">'
248
+ + '<button type="button" class="td-btn td-btn--primary" data-go="promote">Promote</button>'
249
+ + '<button type="button" class="td-btn" data-go="merge">Merge</button>'
250
+ + '<button type="button" class="td-btn" data-go="discard">Discard</button>'
251
+ + '</div>';
252
+ }
253
+
254
+ async function renderPromote() {
255
+ box.innerHTML = '<p class="deeplink-card__note">Loading versions…</p>';
256
+ // Both dead ends offer the way back through the same delegated [data-go]
257
+ // handler the forms use — a builder must never be stranded mid-panel.
258
+ function bail(message) {
259
+ box.innerHTML = '<p class="td-action-note">' + esc(message) + '</p>'
260
+ + '<div class="td-actions"><button type="button" class="td-btn" data-go="idle">Back</button></div>';
261
+ }
262
+ var versions;
263
+ try {
264
+ versions = await openVersions();
265
+ } catch (_) {
266
+ bail('Could not load the version list — try again.');
267
+ return;
268
+ }
269
+ if (!versions.length) {
270
+ bail('No open version to file this against.');
271
+ return;
272
+ }
273
+ var preferred = versions.some(function (v) { return v.id === idea.suggested_version; })
274
+ ? idea.suggested_version : versions[0].id;
275
+ var options = versions.map(function (v) {
276
+ return '<option value="' + esc(v.id) + '"' + (v.id === preferred ? ' selected' : '') + '>'
277
+ + esc(v.id) + ' — ' + esc(v.name || '') + '</option>';
278
+ }).join('');
279
+ // suggested_priority is accepted 0-100 on capture but tasks.priority is
280
+ // CHECKed 1-5, so clamp rather than let an out-of-range value match no
281
+ // option and leave the browser silently defaulting to P1.
282
+ var pri = Math.min(5, Math.max(1, Number(idea.suggested_priority) || 3));
283
+ var priOptions = [1, 2, 3, 4, 5].map(function (p) {
284
+ return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
285
+ }).join('');
286
+ box.innerHTML =
287
+ '<form class="td-form" data-form="promote">'
288
+ + '<label class="td-field td-field--wide"><span>Title</span>'
289
+ + '<input type="text" name="title" maxlength="' + TITLE_MAX + '" required value="' + esc(idea.title || '') + '"></label>'
290
+ + '<label class="td-field td-field--wide"><span>Version</span><select name="version">' + options + '</select></label>'
291
+ + '<label class="td-field"><span>Priority</span><select name="priority">' + priOptions + '</select></label>'
292
+ + '<label class="td-field"><span>Estimate (min)</span>'
293
+ + '<input type="number" name="est" min="0" max="100000" step="5" placeholder="—"></label>'
294
+ + '<label class="td-field"><span>Reward</span>'
295
+ + '<input type="number" name="credits" min="0" max="1000000" step="1" placeholder="0"></label>'
296
+ + '<div class="td-actions">'
297
+ + '<button type="submit" class="td-btn td-btn--primary" data-submit>Create task</button>'
298
+ + '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
299
+ + '</div><p class="td-action-note" data-err></p></form>';
300
+ box.querySelector('[data-form="promote"]').addEventListener('submit', function (ev) {
301
+ ev.preventDefault();
302
+ submitPromote(ev.target);
303
+ });
304
+ }
305
+
306
+ async function submitPromote(form) {
307
+ var title = String(form.title.value || '').trim();
308
+ if (!title) { note('A task needs a title.'); return; }
309
+ note('');
310
+ busy(true, 'Create task');
311
+ // source/source_ref make the create idempotent on (source, source_ref):
312
+ // a retried promote resolves to the SAME task instead of minting a twin.
313
+ // body_md is itself capped at LIMITS.DESCRIPTION, so appending provenance
314
+ // to a maximal one would push the task description past the same cap.
315
+ var provenance = 'Promoted from idea #' + idea.id + '.';
316
+ var body = (idea.body_md ? idea.body_md + '\n\n' : '').slice(0, DESCRIPTION_MAX - provenance.length);
317
+ var payload = {
318
+ version_id: form.version.value,
319
+ title: title,
320
+ description: body + provenance,
321
+ priority: Number(form.priority.value) || 3,
322
+ source: 'idea_inbox',
323
+ source_ref: 'idea:' + idea.id,
324
+ };
325
+ if (form.est.value !== '') payload.est_minutes = Number(form.est.value);
326
+ if (form.credits.value !== '') payload.credits_reward = Number(form.credits.value);
327
+ // 'criterion-proposal' is an idea-only kind; everything else is shared with
328
+ // tasks.kind, so carrying it over saves the classifier a guess.
329
+ if (idea.kind && idea.kind !== 'criterion-proposal') payload.kind = idea.kind;
330
+ // Promote is two writes. If the second fails the task exists but the idea
331
+ // is still open — say so precisely, because "could not create that task"
332
+ // would send the builder to make a duplicate. Retrying is safe and is the
333
+ // right advice: the idempotent create returns the SAME task, then the link
334
+ // is attempted again.
335
+ var task = null;
336
+ try {
337
+ var created = await postJSON(API + '/tasks', payload);
338
+ task = created && created.task ? created.task : null;
339
+ if (!task || task.id == null) throw new Error('no task in response');
340
+ } catch (err) {
341
+ var createMsg = triageError(err, 'Could not create that task. Try again.');
342
+ if (createMsg === null) return;
343
+ busy(false, 'Create task');
344
+ note(createMsg);
345
+ return;
346
+ }
347
+ try {
348
+ await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
349
+ status: 'promoted', promoted_to_task_id: Number(task.id),
350
+ });
351
+ announce(idea.id, 'promoted');
352
+ box.innerHTML = '<p class="deeplink-card__note">Promoted to task '
353
+ + '<a class="deeplink-card__link" href="' + hallBase() + '/task/' + encodeURIComponent(task.id) + '">#'
354
+ + esc(task.id) + '</a> — ' + esc(task.title || '') + '</p>';
355
+ } catch (err) {
356
+ if (err && err.message === 'redirecting to auth') return;
357
+ busy(false, 'Create task');
358
+ note('Task #' + task.id + ' was created, but the idea could not be marked promoted. '
359
+ + 'Press Create task again — it links the same task, it does not make a second one.');
360
+ }
361
+ }
362
+
363
+ function renderMerge() {
364
+ box.innerHTML =
365
+ '<form class="td-form" data-form="merge">'
366
+ + '<label class="td-field td-field--wide"><span>Fold this idea into #</span>'
367
+ + '<input type="number" name="target" min="1" step="1" required placeholder="idea number"></label>'
368
+ + '<p class="td-form-hint" data-target-name></p>'
369
+ + '<label class="td-field td-field--wide"><span>Note (optional)</span>'
370
+ + '<input type="text" name="note" maxlength="1000" placeholder="why these are the same"></label>'
371
+ + '<div class="td-actions">'
372
+ + '<button type="submit" class="td-btn td-btn--primary" data-submit>Merge</button>'
373
+ + '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
374
+ + '</div><p class="td-action-note" data-err></p></form>';
375
+ // Echo the target's title so a merge is confirmed against a real idea, not
376
+ // a number the builder half-remembered.
377
+ var input = box.querySelector('[name="target"]');
378
+ input.addEventListener('change', async function () {
379
+ var out = box.querySelector('[data-target-name]');
380
+ var v = String(input.value || '').trim();
381
+ if (!out) return;
382
+ if (!v) { out.textContent = ''; return; }
383
+ try {
384
+ var d = await getJSON(API + '/inbox/' + encodeURIComponent(v), { softAuth: true });
385
+ var t = d && (d.idea || d);
386
+ out.textContent = t && t.title ? '→ ' + t.title : '→ no idea #' + v;
387
+ } catch (_) {
388
+ out.textContent = '→ no idea #' + v;
389
+ }
390
+ });
391
+ box.querySelector('[data-form="merge"]').addEventListener('submit', function (ev) {
392
+ ev.preventDefault();
393
+ submitMerge(ev.target);
394
+ });
395
+ }
396
+
397
+ async function submitMerge(form) {
398
+ var target = Number(form.target.value);
399
+ if (!Number.isInteger(target) || target < 1) { note('Give the idea number to merge into.'); return; }
400
+ note('');
401
+ busy(true, 'Merge');
402
+ try {
403
+ await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
404
+ status: 'merged',
405
+ merged_into_idea_id: target,
406
+ triage_note: String(form.note.value || '').trim() || null,
407
+ });
408
+ settled('merged', 'Idea #' + idea.id + ' merged into #' + target + '.');
409
+ } catch (err) {
410
+ var msg = triageError(err, 'Could not merge that idea. Try again.');
411
+ if (msg === null) return;
412
+ busy(false, 'Merge');
413
+ note(msg);
414
+ }
415
+ }
416
+
417
+ function renderDiscard() {
418
+ box.innerHTML =
419
+ '<form class="td-form" data-form="discard">'
420
+ + '<label class="td-field td-field--wide"><span>Reason (optional)</span>'
421
+ + '<input type="text" name="note" maxlength="1000" placeholder="why this isn’t worth doing"></label>'
422
+ + '<div class="td-actions">'
423
+ + '<button type="submit" class="td-btn td-btn--primary" data-submit>Discard</button>'
424
+ + '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
425
+ + '</div><p class="td-action-note" data-err></p></form>';
426
+ box.querySelector('[data-form="discard"]').addEventListener('submit', function (ev) {
427
+ ev.preventDefault();
428
+ submitDiscard(ev.target);
429
+ });
430
+ }
431
+
432
+ async function submitDiscard(form) {
433
+ note('');
434
+ busy(true, 'Discard');
435
+ try {
436
+ await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
437
+ status: 'discarded',
438
+ triage_note: String(form.note.value || '').trim() || null,
439
+ });
440
+ settled('discarded', 'Idea #' + idea.id + ' discarded.');
441
+ } catch (err) {
442
+ var msg = triageError(err, 'Could not discard that idea. Try again.');
443
+ if (msg === null) return;
444
+ busy(false, 'Discard');
445
+ note(msg);
446
+ }
447
+ }
448
+
449
+ box.addEventListener('click', function (e) {
450
+ var btn = e.target.closest('[data-go]');
451
+ if (btn) go(btn.dataset.go);
452
+ });
453
+ render();
454
+ return box;
455
+ }
456
+
136
457
  function bodyFor(state) {
137
458
  var nodes = [];
138
459
  var label = '#' + state.id;
@@ -49,12 +49,15 @@
49
49
  <header class="app-topbar" id="app-topbar"></header>
50
50
  <div class="app-content">
51
51
  <div class="page-head">
52
+ <!-- task 2175: the breadcrumb sits in the page head, above the card, so a
53
+ deep-linked visitor (this page is deliberately not in the nav) has a
54
+ way back before the task body has loaded. -->
55
+ <a class="task-detail__back" href="/work">← Back to Tasks</a>
52
56
  <p class="page-head__sub" id="task-sub">Loading task…</p>
53
57
  </div>
54
58
 
55
59
  <main>
56
60
  <section class="scroll" id="task-scroll" aria-labelledby="task-h">
57
- <a class="task-detail__back" href="/work">← Back to the Work Board</a>
58
61
  <div id="task-body">
59
62
  <p class="deeplink-card__title" id="task-h">Loading…</p>
60
63
  </div>
@@ -71,6 +74,8 @@
71
74
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
72
75
  time so the chrome exists before the deferred page scripts execute. -->
73
76
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
77
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
78
+ <script src="/builders/palette.js" defer></script>
74
79
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
75
80
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils" defer></script>
76
81
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
@@ -148,6 +148,8 @@
148
148
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
149
149
  chrome exists before the page script runs. -->
150
150
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
151
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
152
+ <script src="/builders/palette.js" defer></script>
151
153
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
152
154
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
153
155
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Work — {{worldName}}</title>
6
+ <title>Tasks — {{worldName}}</title>
7
7
  <meta name="description" content="What {{worldName}} is building right now, and what waits to be claimed.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -15,7 +15,7 @@
15
15
  live in style.css's hall-kit section. -->
16
16
  <link rel="stylesheet" href="/builders/work.css">
17
17
  </head>
18
- <body data-page="work" data-page-title="Work">
18
+ <body data-page="work" data-page-title="Tasks">
19
19
 
20
20
  <div class="app">
21
21
  <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
@@ -24,7 +24,7 @@
24
24
  <header class="app-topbar" id="app-topbar"></header>
25
25
  <div class="app-content">
26
26
  <div class="page-head">
27
- <p class="page-head__sub" id="work-sub">Loading the work board…</p>
27
+ <p class="page-head__sub" id="work-sub">Loading tasks…</p>
28
28
  </div>
29
29
 
30
30
  <main>
@@ -36,7 +36,7 @@
36
36
  <section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
37
37
  <h2 id="gate-h" class="scroll__h">Primer required</h2>
38
38
  <p class="scroll__lede" id="gate-body">
39
- The work board opens once you have read the builder primer.
39
+ Tasks open once you have read the builder primer.
40
40
  <a href="/primer">Read it here</a>, then mark it as read on the Home page.
41
41
  </p>
42
42
  </section>
@@ -45,12 +45,12 @@
45
45
  (OTBKit.makeTabs toggles [hidden] — the window stays the only
46
46
  scroller): Ready (default) | In Progress | Backlog | Completed.
47
47
  Deep links: /work#in-progress etc. -->
48
- <nav id="work-tabs" aria-label="Work board sections"></nav>
48
+ <nav id="work-tabs" aria-label="Task sections"></nav>
49
49
 
50
50
  <!-- READY — the claimable queue: the page's primary object, above the
51
51
  fold as the default tab. -->
52
52
  <section class="scroll" id="claim-scroll" aria-labelledby="claim-h">
53
- <h2 id="claim-h" class="scroll__h">Ready — work to do<span class="count-pill" id="ready-count" hidden></span></h2>
53
+ <h2 id="claim-h" class="scroll__h">Ready — tasks to do<span class="count-pill" id="ready-count" hidden></span></h2>
54
54
  <p class="scroll__lede">Click <span class="kbd">Claim</span> to take a task right here — no terminal needed. Then click <span class="kbd">📋</span> to copy the task prompt and paste it into a fresh Claude Code session to begin.</p>
55
55
 
56
56
  <!-- task 2170: the filter bar is built by OTBKit.makeFilterBar —
@@ -129,6 +129,8 @@
129
129
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
130
130
  time so the chrome exists before the deferred page scripts execute. -->
131
131
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
132
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
133
+ <script src="/builders/palette.js" defer></script>
132
134
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
133
135
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils" defer></script>
134
136
  <!-- shared component kit (window.OTBKit) — tabs, pager, filter bar (task 2166). -->
@@ -384,7 +384,7 @@
384
384
  </div>
385
385
  <div class="task__credits">
386
386
  <span class="task__credits-num">${fmtNum(window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward)}</span>
387
- <span class="task__credits-label">${window.OTBBranding?.taskCreditLabel?.(t) ?? ((window.OTBBranding?.currencyLabel?.() ?? 'credits') + ' Est.')}</span>
387
+ <span class="task__credits-label" title="${window.OTBBranding?.rewardModeNote?.() ?? ''}">${window.OTBBranding?.taskCreditLabel?.(t) ?? ((window.OTBBranding?.currencyLabel?.() ?? 'credits') + ' Est.')}</span>
388
388
  </div>
389
389
  <div class="task__actions">
390
390
  <button class="task__claim" type="button" data-task-id="${t.id}" aria-label="Claim task ${t.id}">Claim</button>
@@ -1,8 +1,9 @@
1
1
  // GDS idea_inbox helpers — structured replacement for free-form generative-ideas.md.
2
2
  //
3
- // Schema (migration 006_pms_v2.sql):
3
+ // Schema (migration 006_pms_v2.sql, + core_195_idea_triage_provenance.sql):
4
4
  // idea_inbox(id, title, body_md, kind, suggested_version, suggested_priority,
5
- // captured_at, captured_by, status, promoted_to_task_id, reviewed_at)
5
+ // captured_at, captured_by, status, promoted_to_task_id, reviewed_at,
6
+ // merged_into_idea_id, triage_note)
6
7
  //
7
8
  // kind values match tasks.kind (feature | bug | infra | refactor | spike |
8
9
  // learning-capture | cleanup | decision | blocker-resolution | unclassified).
@@ -36,6 +37,16 @@ const VALID_KINDS = [
36
37
  ];
37
38
  const VALID_STATUSES = ['open', 'promoted', 'discarded', 'merged'];
38
39
 
40
+ // The one column list every idea read returns. Four copies had to stay in step
41
+ // (capture / list / get / update-RETURNING); core_195 added two columns to each,
42
+ // so it is now a constant rather than four places to forget.
43
+ const IDEA_COLS = `id, title, body_md, kind, suggested_version, suggested_priority,
44
+ captured_at, captured_by, status, promoted_to_task_id, reviewed_at,
45
+ merged_into_idea_id, triage_note`;
46
+
47
+ // A discard reason / merge note is a sentence, not a document.
48
+ const TRIAGE_NOTE_MAX = 1000;
49
+
39
50
  // -------------------------------------------------------------------------
40
51
  // Skill-friction tagging (task V3.R70 / #289)
41
52
  //
@@ -130,8 +141,7 @@ async function captureIdea({
130
141
  `INSERT INTO idea_inbox
131
142
  (title, body_md, kind, suggested_version, suggested_priority, captured_by, status)
132
143
  VALUES ($1, $2, $3, $4, $5, $6, 'open')
133
- RETURNING id, title, body_md, kind, suggested_version, suggested_priority,
134
- captured_at, captured_by, status, promoted_to_task_id, reviewed_at`,
144
+ RETURNING ${IDEA_COLS}`,
135
145
  [title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority, capturedBy]
136
146
  );
137
147
  return rows[0];
@@ -161,8 +171,7 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
161
171
  }
162
172
  params.push(limit);
163
173
  const { rows } = await pool.query(
164
- `SELECT id, title, body_md, kind, suggested_version, suggested_priority,
165
- captured_at, captured_by, status, promoted_to_task_id, reviewed_at
174
+ `SELECT ${IDEA_COLS}
166
175
  FROM idea_inbox
167
176
  WHERE ${where.join(' AND ')}
168
177
  ORDER BY captured_at DESC
@@ -174,8 +183,7 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
174
183
 
175
184
  async function getIdea(id) {
176
185
  const { rows } = await pool.query(
177
- `SELECT id, title, body_md, kind, suggested_version, suggested_priority,
178
- captured_at, captured_by, status, promoted_to_task_id, reviewed_at
186
+ `SELECT ${IDEA_COLS}
179
187
  FROM idea_inbox
180
188
  WHERE id = $1`,
181
189
  [id]
@@ -206,7 +214,40 @@ async function countOpenIdeas({ viewerRank = null } = {}) {
206
214
  // Update / merge
207
215
  // -------------------------------------------------------------------------
208
216
 
209
- async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at } = {}) {
217
+ // merged_into_idea_id records WHICH idea a merge folded into — the thing
218
+ // `status='merged'` alone never said. It cannot reuse promoted_to_task_id
219
+ // (that column is REFERENCES tasks(id)), hence core_195. Guarded here rather
220
+ // than left to the constraints so the caller gets a structured 400/404/409
221
+ // instead of a violation 500.
222
+ //
223
+ // The invariant, also enforced by the core_195 CHECK: a target exists IFF the
224
+ // idea is merged. Without the status half, two ordinary PATCHes build the chain
225
+ // the no-chains guard exists to stop — set A's target with no status (A stays
226
+ // open), then merge C into A, whose status still reads 'open'. So the chain
227
+ // guard tests the TARGET COLUMN, not the target's status, and a target write
228
+ // must carry status='merged'.
229
+ async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at, merged_into_idea_id, triage_note } = {}) {
230
+ if (merged_into_idea_id !== undefined && merged_into_idea_id !== null) {
231
+ if (status !== 'merged') {
232
+ throw { code: 'MERGE_STATUS_REQUIRED', merged_into_idea_id };
233
+ }
234
+ if (String(merged_into_idea_id) === String(id)) {
235
+ throw { code: 'MERGE_SELF', merged_into_idea_id };
236
+ }
237
+ const target = await getIdea(merged_into_idea_id);
238
+ if (!target) throw { code: 'MERGE_TARGET_NOT_FOUND', merged_into_idea_id };
239
+ if (target.merged_into_idea_id != null) {
240
+ throw { code: 'MERGE_TARGET_MERGED', merged_into_idea_id, merged_into: target.merged_into_idea_id };
241
+ }
242
+ }
243
+ // Clearing the link is the un-merge: it must move the idea off 'merged' too,
244
+ // or the row claims to be merged into nothing.
245
+ if (merged_into_idea_id === null && status === 'merged') {
246
+ throw { code: 'MERGE_STATUS_REQUIRED', merged_into_idea_id: null };
247
+ }
248
+ if (triage_note !== undefined && triage_note !== null && String(triage_note).length > TRIAGE_NOTE_MAX) {
249
+ throw { code: 'TRIAGE_NOTE_TOO_LONG', max: TRIAGE_NOTE_MAX };
250
+ }
210
251
  const sets = [];
211
252
  const params = [id];
212
253
  if (status !== undefined) {
@@ -227,6 +268,14 @@ async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at }
227
268
  params.push(reviewed_at);
228
269
  sets.push(`reviewed_at = $${params.length}`);
229
270
  }
271
+ if (merged_into_idea_id !== undefined) {
272
+ params.push(merged_into_idea_id);
273
+ sets.push(`merged_into_idea_id = $${params.length}`);
274
+ }
275
+ if (triage_note !== undefined) {
276
+ params.push(triage_note);
277
+ sets.push(`triage_note = $${params.length}`);
278
+ }
230
279
  if (sets.length === 0) {
231
280
  return getIdea(id);
232
281
  }
@@ -234,8 +283,7 @@ async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at }
234
283
  `UPDATE idea_inbox
235
284
  SET ${sets.join(', ')}
236
285
  WHERE id = $1
237
- RETURNING id, title, body_md, kind, suggested_version, suggested_priority,
238
- captured_at, captured_by, status, promoted_to_task_id, reviewed_at`,
286
+ RETURNING ${IDEA_COLS}`,
239
287
  params
240
288
  );
241
289
  if (rows.length === 0) throw { code: 'IDEA_NOT_FOUND' };
@@ -439,6 +487,8 @@ module.exports = {
439
487
  classify,
440
488
  VALID_KINDS,
441
489
  VALID_STATUSES,
490
+ // the triage-note cap, so the route validator declares it from one place
491
+ TRIAGE_NOTE_MAX,
442
492
  // skill-friction tagging + rank visibility gate (V3.R70 / #289)
443
493
  SKILL_FRICTION_TAG,
444
494
  XENOS_RANK,