@bongos/core 1.18.3 → 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 (469) hide show
  1. package/.bongos-core.json +1575 -285
  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 +4 -2
  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 +6 -1
  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/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/status-mirror-sync.js +114 -84
  347. package/src/bongos/api-errors.js +1 -0
  348. package/src/bongos/auth.js +447 -102
  349. package/src/bongos/db-kernel.js +27 -0
  350. package/src/bongos/db.js +39 -0
  351. package/src/bongos/module-scope-map.js +27 -1
  352. package/src/bongos/permission-path-check.js +136 -102
  353. package/src/bongos/repo-info.js +47 -11
  354. package/src/bongos/route-rank-check.js +429 -46
  355. package/src/bongos/routes/_helpers.js +12 -1
  356. package/src/bongos/routes/auth.js +219 -2
  357. package/src/bongos/routes/backup.js +58 -2
  358. package/src/bongos/routes/me.js +35 -1
  359. package/src/bongos/serve-internal.js +84 -15
  360. package/src/branding.js +32 -0
  361. package/src/instance-config.js +30 -0
  362. package/src/module-api.js +112 -1
  363. package/src/platform-server.js +86 -9
  364. package/tests/abandon_task.mjs +59 -3
  365. package/tests/api_cli_flags.mjs +53 -0
  366. package/tests/auth_page_gate.mjs +13 -0
  367. package/tests/backfill_sessions.mjs +127 -1
  368. package/tests/backup_cli.mjs +54 -0
  369. package/tests/bongos_box_connect.mjs +77 -0
  370. package/tests/box_access.mjs +130 -1
  371. package/tests/box_credential.mjs +125 -0
  372. package/tests/box_host_keys.mjs +24 -0
  373. package/tests/box_infra_resolve.mjs +149 -0
  374. package/tests/box_scope_session.mjs +6 -2
  375. package/tests/box_sync.mjs +55 -0
  376. package/tests/box_task_scope.mjs +99 -0
  377. package/tests/branding.mjs +12 -0
  378. package/tests/cf_ruleset_preflight.mjs +142 -0
  379. package/tests/cf_tunnel.mjs +37 -0
  380. package/tests/claim_gate_rebase.mjs +98 -0
  381. package/tests/clawback_confirmed_backlog.mjs +103 -0
  382. package/tests/cli_lib_formatters.mjs +20 -0
  383. package/tests/confirm_attribution.mjs +30 -46
  384. package/tests/credit_grant.mjs +198 -10
  385. package/tests/criterion_authoring_gate.mjs +272 -0
  386. package/tests/criterion_progress.mjs +33 -1
  387. package/tests/dev_lib.mjs +253 -0
  388. package/tests/devbox_artifact_gate.mjs +75 -0
  389. package/tests/devbox_dmg_backdrop.mjs +137 -0
  390. package/tests/devbox_icons.mjs +304 -0
  391. package/tests/devbox_installer_brand.mjs +232 -0
  392. package/tests/devbox_key_register.mjs +234 -0
  393. package/tests/docs_openapi_route.mjs +110 -0
  394. package/tests/doctor_hooks.mjs +141 -0
  395. package/tests/extract_json.mjs +140 -0
  396. package/tests/fitness.mjs +89 -0
  397. package/tests/gen_freshness.mjs +96 -0
  398. package/tests/goal_advisory.mjs +251 -0
  399. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  400. package/tests/goal_routes.mjs +59 -4
  401. package/tests/goal_tier.mjs +12 -3
  402. package/tests/governance_catalog.mjs +129 -0
  403. package/tests/governance_claim_eligibility.mjs +262 -0
  404. package/tests/governance_config.mjs +187 -0
  405. package/tests/governance_ownership_scope.mjs +410 -0
  406. package/tests/governance_page_gates.mjs +324 -0
  407. package/tests/governance_parity.mjs +336 -0
  408. package/tests/governance_principals.mjs +232 -0
  409. package/tests/governance_protected_surfaces.mjs +208 -0
  410. package/tests/governance_require_permission.mjs +223 -0
  411. package/tests/governance_resolver.mjs +84 -0
  412. package/tests/governance_role_sync.mjs +231 -0
  413. package/tests/governance_seed.mjs +127 -0
  414. package/tests/governance_session_scope.mjs +434 -0
  415. package/tests/governance_vanilla_boot.mjs +203 -0
  416. package/tests/grade_attribution.mjs +7 -7
  417. package/tests/hall_audit.mjs +223 -0
  418. package/tests/hall_nav.mjs +191 -0
  419. package/tests/hall_palette.mjs +206 -0
  420. package/tests/hall_signout.mjs +124 -0
  421. package/tests/hook_user_agents.mjs +10 -0
  422. package/tests/idea_triage.mjs +162 -0
  423. package/tests/idp_device_exchange.mjs +93 -0
  424. package/tests/init.mjs +51 -0
  425. package/tests/instance_config.mjs +65 -0
  426. package/tests/instance_mismatch_guard.mjs +23 -6
  427. package/tests/instance_release_config.mjs +104 -0
  428. package/tests/linkify_refs.mjs +15 -2
  429. package/tests/module_contributions.mjs +55 -0
  430. package/tests/module_loader.mjs +10 -5
  431. package/tests/module_scope_active_claims.mjs +94 -0
  432. package/tests/needs_rebase.mjs +92 -0
  433. package/tests/one_active_claim_per_session.mjs +76 -0
  434. package/tests/override_attribution.mjs +10 -8
  435. package/tests/override_request_ownership.mjs +8 -4
  436. package/tests/parse_cookie_decode.mjs +42 -0
  437. package/tests/pay_on_land.mjs +129 -0
  438. package/tests/platform_boot.mjs +100 -1
  439. package/tests/post_login_landing.mjs +42 -0
  440. package/tests/preflight_checks.mjs +72 -0
  441. package/tests/provision.mjs +132 -7
  442. package/tests/publish_branch_route.mjs +53 -0
  443. package/tests/publish_reconciler.mjs +187 -2
  444. package/tests/repo_info_binding.mjs +248 -0
  445. package/tests/reward_gate.mjs +11 -2
  446. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  447. package/tests/reward_mode_surfacing.mjs +168 -0
  448. package/tests/route_permission_check.mjs +301 -0
  449. package/tests/route_rank_check.mjs +254 -12
  450. package/tests/route_validation.mjs +30 -0
  451. package/tests/sequence.mjs +267 -0
  452. package/tests/session_digest.mjs +44 -0
  453. package/tests/session_index.mjs +18 -6
  454. package/tests/session_records.mjs +39 -1
  455. package/tests/session_reward_on_land.mjs +381 -0
  456. package/tests/session_reward_reconcile.mjs +229 -0
  457. package/tests/ship_api_client_pathspec.mjs +100 -0
  458. package/tests/ship_base_flag.mjs +93 -0
  459. package/tests/ship_ci_deploy.mjs +80 -3
  460. package/tests/ship_upstream_deletion.mjs +57 -0
  461. package/tests/smoke_target.mjs +233 -0
  462. package/tests/start_env_guard.mjs +87 -0
  463. package/tests/start_rebase_warning.mjs +103 -0
  464. package/tests/status_mirror_sync.mjs +122 -0
  465. package/tests/stop_refcheck.mjs +19 -1
  466. package/tests/task_goal_link.mjs +15 -6
  467. package/tests/upgrade.mjs +139 -0
  468. package/tests/web_admission_status.mjs +162 -0
  469. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,108 @@
1
+ # Work on a task from your dev box
2
+
3
+ > Your dev box is **task-scoped**: claim a task and the box pulls **only the code
4
+ > that task needs** — the module(s) its work touches, on top of a small base
5
+ > instance — never the whole repo. Your GitHub token never leaves the server; the
6
+ > box fetches its slice through the control plane. See
7
+ > [ADR 0148](../adr/<redacted>.md) (the scope model) and
8
+ > [ADR 0150](../adr/<redacted>.md) (how the box
9
+ > comes up).
10
+
11
+ ## TL;DR
12
+
13
+ In your dev box's terminal:
14
+
15
+ ```bash
16
+ /builder-start # see what you can claim
17
+ /builder-claim <N> # claim task N (locks it to your session)
18
+ /builder-sync # pull task N's code onto the box now (re-scopes in place)
19
+ # …work in /workspace…
20
+ /builder-ship # smoke + review, then land + deploy — awards credits
21
+ ```
22
+
23
+ That's the whole loop. `/builder-sync` is the new step: it materializes the claimed
24
+ task's code within seconds instead of waiting for the box's ~10-minute refresh.
25
+
26
+ ## The model — what's on your box
27
+
28
+ A task-scoped box carries two layers:
29
+
30
+ 1. **The base instance** — always present, whatever you've claimed: the methodology
31
+ (`CLAUDE.md`, `.claude/`), the build tooling (`scripts/`, `src/bongos/`),
32
+ `docs/`, `infra/`, and the **test suite** (`tests/`). Enough for Claude to hold
33
+ context and run the `/builder-*` CLI, and to run your module's tests.
34
+ 2. **Your claimed task's code** — the module directories the task's goal scopes
35
+ (`goals.scope_modules` → globs, [ADR 0086](../adr/<redacted>.md)). Claim a `dev-box` task → you get
36
+ `modules/dev-box/`; claim a `game` task → you get the game world too. Nothing
37
+ else.
38
+
39
+ What you do **not** get: other builders' modules, the product/game runtime (unless
40
+ your task is game-scoped), and — the point — **no GitHub credential on the box**.
41
+ The box asks the control plane (`GET /box/source-access`) for its slice on each
42
+ fetch; the server holds the token and returns a scoped, cone-mode sparse-checkout
43
+ spec. Lose the claim or a rank, and the next fetch is cut off at the server.
44
+
45
+ ## Step by step
46
+
47
+ ### 1. Connect to your box
48
+
49
+ If you haven't yet, run **`/builder-connect`** (or follow the
50
+ [dev-box lifecycle recipe](builder-box-lifecycle.md)) to raise and connect to your
51
+ box, then **`/builder-setup`** once on the box to sign the GDS CLI in. The browser
52
+ terminal drops you into `/workspace` — the one working folder.
53
+
54
+ > **`bongos shell` / `bongos code` re-scope on connect (BV1.R101).** Connecting with
55
+ > either verb runs the sync for you first, so you land in `/workspace` already scoped
56
+ > to your active claim — no manual `/builder-sync` needed just to arrive. (The browser
57
+ > terminal drops you straight into `/workspace`; run `/builder-sync` there if you
58
+ > claimed or switched since your last sync.)
59
+
60
+ ### 2. See what you can claim — `/builder-start`
61
+
62
+ Lists the tasks that are `ready` and parallel-safe against everyone else's active
63
+ claims. Pick one.
64
+
65
+ ### 3. Claim it — `/builder-claim <N>`
66
+
67
+ Locks task N to your session. **This is what drives your box's scope:** source
68
+ access is computed from your **active claim** — its task → goal → `scope_modules`.
69
+
70
+ ### 4. Pull the code now — `/builder-sync`
71
+
72
+ Re-runs the box's source-fetch immediately, so `/workspace` re-scopes to task N's
73
+ modules within seconds. (A ~10-minute cron does this on its own as a backstop — you
74
+ just don't want to wait for it.) Off a box there's nothing to sync — your source is
75
+ your local checkout — and the command tells you so.
76
+
77
+ ### 5. Work — in `/workspace`
78
+
79
+ `/workspace` now holds the base instance **plus** task N's module code. One folder:
80
+ when you switch tasks it **re-scopes in place**, so there are no per-task folders to
81
+ manage.
82
+
83
+ ### 6. Ship — `/builder-ship`
84
+
85
+ Runs the smoke tests and the review gate, then lands + deploys and awards credits.
86
+
87
+ ## Switching tasks
88
+
89
+ One box, one working folder, one active claim at a time (the server enforces
90
+ one-claim-per-session). To move to a different task:
91
+
92
+ 1. `/builder-ship` (or `/builder-release`) the current claim.
93
+ 2. `/builder-claim <M>` the new one.
94
+ 3. `/builder-sync` — the box re-scopes to task M in place.
95
+
96
+ ## Notes + gotchas
97
+
98
+ - **`tests/` is always in the base** — so a module's tests run on the box (they
99
+ live in the top-level `tests/`, not inside `modules/<key>/`).
100
+ - **Idle boxes rest.** A box with no activity is reclaimed after ~10–15 min; the
101
+ next connect re-provisions a fresh one and re-fetches your scope. **Uncommitted
102
+ work on a reaped box is lost** — ship or commit before you step away.
103
+ - **`/builder-sync` is idempotent** — safe to re-run; it just does, on demand, what
104
+ the cron does on its schedule.
105
+ - **A fresh box comes up with the base already cloned.** If a box ever comes up
106
+ *bare* (no code, no terminal), that's the packaging bug
107
+ [ADR 0150](../adr/<redacted>.md) fixed on the
108
+ control plane — flag it rather than working around it.
@@ -223,17 +223,16 @@ CREATE INDEX IF NOT EXISTS idx_session_logs_builder ON session_logs (builder_id)
223
223
  CREATE INDEX IF NOT EXISTS idx_session_logs_ended ON session_logs (ended_at);
224
224
 
225
225
  -- =========================================================================
226
- -- Seed: the two known versions
226
+ -- Seed: the platform PMS-V1 version
227
227
  -- =========================================================================
228
- -- Instance-seed guard (task 1704 / ADR 0105): these are OTB product versions —
229
- -- scope-truth that must NOT seed onto a vanilla / non-owning DB. scripts/migrate.sh
230
- -- sets app.seed_instance_data='off' on such a boot; unset (direct psql / OTB) = seed.
228
+ -- Instance-seed guard (task 1704 / ADR 0105): OTB platform scope-truth that must
229
+ -- NOT seed onto a vanilla / non-owning DB. scripts/migrate.sh sets
230
+ -- app.seed_instance_data='off' on such a boot; unset (direct psql / OTB) = seed.
231
+ -- (Game V1 was product/primea scope; relocated to the amazonprimea instance's
232
+ -- migrations/instance/ per task 1002423 — the neutral core carries no game seed.)
231
233
  DO $seed$ BEGIN IF current_setting('app.seed_instance_data', true) IS DISTINCT FROM 'off' THEN
232
234
  INSERT INTO versions (id, name, track, status, scope_doc_path, started_at, done_when)
233
235
  VALUES
234
- ('V1', 'Game V1 — first playable purchase loop', 'product', 'building',
235
- 'limitations/current-version.md', '2026-05-08'::timestamptz,
236
- 'A visitor can land in the world, walk to the vendor, buy a real item, and Hermeslines fulfills it.'),
237
236
  ('PMS-V1', 'PMS V1 — project management system, parallel internal track', 'internal', 'building',
238
237
  'limitations/pms-v1.md', '2026-05-09'::timestamptz,
239
238
  'Builders auth via GitHub. Slash commands claim/ship tasks atomically. Backlog migrated. Public status page live.')
@@ -337,38 +337,10 @@ ON CONFLICT (version_id, criterion_id) DO UPDATE
337
337
  END IF; END $seed$;
338
338
 
339
339
  -- =========================================================================
340
- -- 6. Seeds: V1 done-when criteria (mirrors limitations/current-version.md)
340
+ -- (Game V1 done-when criteria removed — product/primea scope, not core.)
341
+ -- Relocated to the amazonprimea instance's migrations/instance/ per task
342
+ -- 1002423; the neutral core seeds no game scope-truth.
341
343
  -- =========================================================================
342
- -- The V1 done-when (CLAUDE.md §9): "the experience supports the full
343
- -- purchase loop: land in the world → walk to the vendor → buy a real
344
- -- item → Hermeslines fulfills it." Decomposed into checkable criteria.
345
-
346
- -- Instance-seed guard (task 1704 / ADR 0105): OTB V1 game scope-truth. FK-references
347
- -- the V1 version guarded above, so this must skip on a vanilla DB too.
348
- DO $seed$ BEGIN IF current_setting('app.seed_instance_data', true) IS DISTINCT FROM 'off' THEN
349
- INSERT INTO done_when_criteria (version_id, criterion_id, criterion_md, sort_order) VALUES
350
- ('V1', 'world-land',
351
- 'Visitor lands in the persistent world from amazonprimea.com',
352
- 10),
353
- ('V1', 'walk-to-vendor',
354
- 'Visitor can walk to Hyperion at the shipping container with WASD',
355
- 20),
356
- ('V1', 'auth-magic-link',
357
- 'Visitor authenticates via Stripe-tied magic link before purchase',
358
- 30),
359
- ('V1', 'vendor-purchase',
360
- 'Vendor dialog supports real-product purchase via Stripe',
361
- 40),
362
- ('V1', 'hermeslines-fulfill',
363
- 'Order intake reaches Hermeslines fulfillment system (whatever the format)',
364
- 50),
365
- ('V1', 'order-confirmation',
366
- 'Visitor receives order confirmation in-world',
367
- 60)
368
- ON CONFLICT (version_id, criterion_id) DO UPDATE
369
- SET criterion_md = EXCLUDED.criterion_md,
370
- sort_order = EXCLUDED.sort_order;
371
- END IF; END $seed$;
372
344
 
373
345
  -- =========================================================================
374
346
  -- 7. Migration record
@@ -0,0 +1,28 @@
1
+ -- core_193_task_needs_rebase.sql — ADR 0120 part 2: the `needs_rebase` flag.
2
+ --
3
+ -- A confirmed task whose land the publish-reconciler cannot complete after the
4
+ -- normal auto-land grace (its retries + the ADR 0082 generated-file self-heal)
5
+ -- is flagged needs_rebase=true. It is a SIGNAL, not a status change: the task
6
+ -- stays 'confirmed' (its work is done) — it is simply marked as awaiting *its
7
+ -- builder's* rebase. Attribution to the builder who did the work is derived from
8
+ -- tasks.shipped_by (confirmTask/applyGrade/decideOverrideRequest all COALESCE it
9
+ -- to the claim holder), so no separate owner column is needed.
10
+ --
11
+ -- The claim gate (core claimTask, ADR 0120 part 3) refuses that builder's NEW
12
+ -- claims while any of their tasks is needs_rebase; landing the task (shipTask)
13
+ -- or releasing it clears the flag. This is a CORE migration (not a lifecycle
14
+ -- module migration) because it ALTERs the core `tasks` table — the module
15
+ -- migration namespacing rule forbids a module migration touching non-namespaced
16
+ -- core objects (the discord precedent).
17
+ --
18
+ -- Safe DDL: ADD COLUMN ... NOT NULL DEFAULT <constant> does not rewrite the
19
+ -- table on Postgres 11+ (the default is stored in the catalog).
20
+
21
+ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS needs_rebase boolean NOT NULL DEFAULT false;
22
+ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS needs_rebase_reason text;
23
+ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS needs_rebase_at timestamptz;
24
+
25
+ -- The claim gate + hall/CLI ask "does builder B own any needs_rebase task?",
26
+ -- keyed on shipped_by. Partial index keeps that lookup cheap (only the handful
27
+ -- of flagged rows are indexed).
28
+ CREATE INDEX IF NOT EXISTS idx_tasks_needs_rebase ON tasks (shipped_by) WHERE needs_rebase;
@@ -0,0 +1,63 @@
1
+ -- core_194_confirmed_backlog_clawback.sql — ADR 0120 part 4: one-time clawback
2
+ -- + re-gate of the confirmed backlog that accrued under the OLD pay-on-confirm
3
+ -- rule (parts 1-3 moved BOTH the pay and the queue gate to the LAND).
4
+ --
5
+ -- For every task still at status='confirmed' (verified but NOT yet merged to
6
+ -- main) this migration:
7
+ -- 1. Reverses its task.confirmed per-task credit — a compensating NEGATIVE
8
+ -- credit_log row (reason='task.confirmed_reversal') per (task, builder), so
9
+ -- the builders.total_credits trigger nets it back to 0. The reward is fully
10
+ -- RE-EARNABLE: when the task finally lands, part 1a books a fresh
11
+ -- 'task.shipped' credit. Achievements already unlocked are LEFT INTACT
12
+ -- (reversing gamification is punitive + messy — ADR 0120 §Consequences).
13
+ -- 2. Flags the task needs_rebase=true (reason 'clawback:confirmed_backlog') so
14
+ -- the part-3 claim gate applies the new incentive to the work that stranded
15
+ -- under the old rule, rather than grandfathering it.
16
+ --
17
+ -- Idempotent: a task already carrying a task.confirmed_reversal row is skipped,
18
+ -- and needs_rebase is only set where still false — a re-run is a no-op. It runs
19
+ -- ONCE per DB (schema_migrations), at the deploy that first carries it; that is
20
+ -- WHERE the retroactive sting lands, so it hits real builders (the owner
21
+ -- included) only when this version is actually deployed.
22
+ --
23
+ -- Deliberately NOT reversed: the idea-promotion bonus and the net-negative
24
+ -- bonus. The ADR names only the 'task.confirmed' delta; the idea bonus stays
25
+ -- with its capturer (it is not double-paid on re-land — maybeAwardIdeaPromotion
26
+ -- Bonus guards on the existing 'idea_promotion_bonus' row), and the net-negative
27
+ -- bonus derives from grade-time signals that never moved off confirm.
28
+ --
29
+ -- DML only (no schema change): safe to run inside one transaction.
30
+
31
+ BEGIN;
32
+
33
+ -- 1. Compensating reversal of the task.confirmed credit for still-confirmed tasks.
34
+ INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
35
+ SELECT cl.builder_id,
36
+ cl.task_id,
37
+ -SUM(cl.delta),
38
+ 'task.confirmed_reversal',
39
+ 'ADR 0120 clawback: reverse premature task.confirmed credit for still-confirmed (unlanded) task ' || cl.task_id
40
+ FROM credit_log cl
41
+ JOIN tasks t ON t.id = cl.task_id
42
+ WHERE t.status = 'confirmed'
43
+ AND cl.reason = 'task.confirmed'
44
+ AND NOT EXISTS (
45
+ SELECT 1 FROM credit_log r
46
+ WHERE r.task_id = cl.task_id
47
+ AND r.builder_id = cl.builder_id
48
+ AND r.reason = 'task.confirmed_reversal'
49
+ )
50
+ GROUP BY cl.builder_id, cl.task_id
51
+ HAVING SUM(cl.delta) <> 0;
52
+
53
+ -- 2. Re-gate: flag every still-confirmed task needs_rebase for its builder
54
+ -- (attribution via tasks.shipped_by), so the claim gate (part 3) bites.
55
+ UPDATE tasks
56
+ SET needs_rebase = true,
57
+ needs_rebase_reason = 'clawback:confirmed_backlog',
58
+ needs_rebase_at = COALESCE(needs_rebase_at, now()),
59
+ updated_at = now()
60
+ WHERE status = 'confirmed'
61
+ AND needs_rebase = false;
62
+
63
+ COMMIT;
@@ -0,0 +1,57 @@
1
+ -- core_195_idea_triage_provenance.sql — give a triage VERDICT somewhere to
2
+ -- record what it decided (task 1002447).
3
+ --
4
+ -- Triage has three verbs; only two of them had storage. `status` records THAT an
5
+ -- idea was discarded or merged, and `promoted_to_task_id` records the task a
6
+ -- promotion produced — but nothing recorded WHICH idea a merge folded into, or
7
+ -- WHY a discard happened. `promoted_to_task_id` cannot stand in for the merge
8
+ -- target: it is `REFERENCES tasks(id)`, so writing an idea id there either trips
9
+ -- the FK or silently points at an unrelated task. BOTH callers that documented
10
+ -- or performed that write are corrected alongside this migration — the
11
+ -- /idea-triage skill doc and the interactive walker `scripts/gds/triage.js`.
12
+ -- Without a target, "merge" is just "discard" wearing a different label.
13
+ --
14
+ -- CORE, not modules/ideas/migrations/, even though the ideas module owns every
15
+ -- query against this table: `idea_inbox` is a CORE table, and a module migration
16
+ -- may only touch its own <key>_-prefixed objects — `fitness.js`
17
+ -- checkModuleMigrationsAdditive hard-fails an `ALTER TABLE idea_inbox` from a
18
+ -- module (ADR 0083 §Decision #5). The module's "new ideas migration goes in
19
+ -- modules/ideas/migrations/" note applies to objects the module OWNS, and it
20
+ -- owns none.
21
+ --
22
+ -- Two additive, nullable columns. Forward-safe by construction: every existing
23
+ -- reader SELECTs an explicit column list, so old code on the new schema neither
24
+ -- sees nor needs these (tests/migrations_forward_safe.mjs).
25
+
26
+ BEGIN;
27
+
28
+ ALTER TABLE idea_inbox
29
+ ADD COLUMN IF NOT EXISTS merged_into_idea_id bigint REFERENCES idea_inbox(id);
30
+
31
+ ALTER TABLE idea_inbox
32
+ ADD COLUMN IF NOT EXISTS triage_note text;
33
+
34
+ -- A target exists IFF the idea is merged. Without this, two ordinary PATCHes
35
+ -- build a merge CHAIN: set A's target while leaving A open, then merge C into A
36
+ -- — whose status still reads 'open', so a status-based no-chains check waves it
37
+ -- through. One direction only (`target IS NULL OR status='merged'`), because
38
+ -- every pre-existing merged row has a NULL target and must stay valid.
39
+ -- CHECK constraints have no ADD ... IF NOT EXISTS, so guard the add (the
40
+ -- migration 042 idiom) and keep the file replay-safe.
41
+ DO $$
42
+ BEGIN
43
+ IF NOT EXISTS (
44
+ SELECT 1 FROM pg_constraint
45
+ WHERE conrelid = 'idea_inbox'::regclass
46
+ AND conname = 'idea_inbox_merge_target_needs_merged_status'
47
+ ) THEN
48
+ ALTER TABLE idea_inbox
49
+ ADD CONSTRAINT idea_inbox_merge_target_needs_merged_status
50
+ CHECK (merged_into_idea_id IS NULL OR status = 'merged');
51
+ END IF;
52
+ END $$;
53
+
54
+ INSERT INTO schema_migrations (version) VALUES ('core_195_idea_triage_provenance')
55
+ ON CONFLICT DO NOTHING;
56
+
57
+ COMMIT;
@@ -0,0 +1,69 @@
1
+ -- core_196_session_reward_topup.sql
2
+ -- Let a multi-ship session be paid for ALL of its cost, not just its first ship.
3
+ --
4
+ -- WHY THIS IS A CORE MIGRATION, NOT modules/economy/. credit_log is a CORE table — the
5
+ -- economy module owns the queries against it, not the table itself (ADR 0083 §Decision #5;
6
+ -- modules/economy/CLAUDE.md "Tables: None owned"). A module migration may only touch its own
7
+ -- economy_-namespaced objects, so this shipped as economy_002_* and reddened the fitness gate
8
+ -- three ways (destructive DDL + index + alter on a non-namespaced table). The real defect was
9
+ -- placement, not DDL: as a module migration these columns only existed when the economy module
10
+ -- happened to be ENABLED, which makes core schema depend on module enablement — the exact
11
+ -- coupling the rule forbids. Relocated per the core_195 precedent (task 1002447).
12
+ --
13
+ -- schema_migrations is stem-keyed, so this re-applies once under its new stem. That is safe
14
+ -- BECAUSE every statement below is IF [NOT] EXISTS — the re-run is a no-op on any database
15
+ -- that already had it as economy_002.
16
+ --
17
+ -- THE DEFECT. Migration 091 made the cost-plus session reward idempotent with a
18
+ -- PARTIAL UNIQUE INDEX on credit_log (session_id) WHERE reason='session.token_reward'
19
+ -- — "the hard DB-level backstop against a double payout". That is correct and stays.
20
+ -- But it was the ONLY control, so once a session booked its first reward row, every
21
+ -- later ship in the same session was a hard no-op. A session's cost keeps growing
22
+ -- after its first ship, and that increment had nowhere to land.
23
+ --
24
+ -- Measured: three tasks shipped from one session, priced $14.30 / $27.76 / $40.32,
25
+ -- paid 17 drachmae where the whole session was worth round(40.32 × 1.20) = 48.
26
+ -- Roughly 65% of real, shipped, graded work went unrewarded. Multi-ship sessions are
27
+ -- normal — one claim at a time, but ship -> new worktree -> claim again.
28
+ --
29
+ -- THE FIX is watermark accounting: each landed ship books the DIFFERENCE between what
30
+ -- the whole session is now worth and what it has already been paid. That needs more
31
+ -- than one row per session, so uniqueness moves from (session_id) to
32
+ -- (session_id, reward_basis) — still scoped to the same reason.
33
+ --
34
+ -- WHY A CUMULATIVE BASIS AND NOT THE DELTA. Two concurrent uploads at the SAME cost
35
+ -- level must still collide and DO NOTHING. reward_basis is the CUMULATIVE owed total
36
+ -- at the moment of the row, so it is monotonic and identical for a racing pair. A bare
37
+ -- delta would be wrong: two genuine increments that happened to be equal would
38
+ -- collide and one payment would be silently lost.
39
+ --
40
+ -- Additive and non-destructive. No historical row changes value.
41
+
42
+ -- (a) The cumulative cost-plus total this session was owed when the row was written.
43
+ -- NOT NULL DEFAULT 0 (rather than nullable) so the unique index below has clean
44
+ -- semantics: Postgres treats NULLs as DISTINCT in a unique index, so a nullable
45
+ -- basis would let two legacy rows share a session_id and would WEAKEN the very
46
+ -- backstop 091 added.
47
+ --
48
+ -- Existing rows therefore land on reward_basis = 0, which is the marker for
49
+ -- "booked before top-ups existed". The application refuses to top those up: doing
50
+ -- so would be an uncontrolled retroactive payout across every historical session.
51
+ -- Reconstructing what past sessions are owed is a deliberate, owner-signed-off
52
+ -- backfill, tracked separately.
53
+ ALTER TABLE credit_log
54
+ ADD COLUMN IF NOT EXISTS reward_basis integer NOT NULL DEFAULT 0;
55
+
56
+ -- (b) Swap the uniqueness key. CREATE the replacement BEFORE dropping the original so
57
+ -- the double-pay backstop is never absent, not even inside this transaction.
58
+ --
59
+ -- For legacy rows (all at basis 0) the new index is exactly as strict as the old
60
+ -- one — (session_id, 0) is unique per session — so nothing already paid can be
61
+ -- paid twice.
62
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_credit_session_token_reward_basis
63
+ ON credit_log (session_id, reward_basis)
64
+ WHERE reason = 'session.token_reward';
65
+
66
+ DROP INDEX IF EXISTS uniq_credit_session_token_reward;
67
+
68
+ -- Counter hygiene: as a core migration this consumes core number 196 (next free after
69
+ -- core_195), so the core allocator floor advances with it.
@@ -8,10 +8,12 @@
8
8
  // contributes.pollers:["routine-timers"]; the loader (src/module-loader/loader.js
9
9
  // startModulePollers) requires THIS file and calls start(deps) at boot, stop() at
10
10
  // shutdown — the module-agnostic successor to the hardcoded `routineTimers.start()`
11
- // block server.js used to carry. server.js (the game/droplet entrypoint) is the
12
- // ONLY caller of startModulePollers, never src/platform-server.js, so a future
13
- // second always-on instance on the same DB can't double-run (and the FOR UPDATE
14
- // row lock in tallyPeerVotes is the hard guard if one ever does).
11
+ // block server.js used to carry. src/platform-server.js is now the ONLY caller of
12
+ // startModulePollers (task 1002419) — server.js, the retired game/droplet
13
+ // entrypoint, was the previous one, and while it was gone NO module poller ran at
14
+ // all. It runs as a single process (docker-entrypoint.sh `exec`s it, no cluster),
15
+ // so a second always-on instance on the same DB still can't double-run — and the
16
+ // FOR UPDATE row lock in tallyPeerVotes is the hard guard if one ever does.
15
17
  //
16
18
  // WHY THIS EXISTS
17
19
  // ADR 0078 demoted peer-vote-tally to `mode: deterministic` and built an EXTERNAL
@@ -100,10 +102,9 @@ async function runPeerVoteTally(deps = {}) {
100
102
 
101
103
  // Start the timer(s). Idempotent. Kill-switched by ROUTINE_TIMERS_DISABLED=1.
102
104
  // Unref'd so it never holds the process open on a systemd restart. Started by the
103
- // loader's startModulePollers ONLY on the game/droplet entrypoint (server.js),
104
- // never in src/platform-server.js, so a future second always-on instance on the
105
- // same DB can't double-run (and the FOR UPDATE row lock in tallyPeerVotes is the
106
- // hard guard if one ever does).
105
+ // loader's startModulePollers from src/platform-server.js — the single always-on
106
+ // entrypoint (task 1002419) — so a second instance on the same DB can't double-run
107
+ // (and the FOR UPDATE row lock in tallyPeerVotes is the hard guard if one ever does).
107
108
  function start(deps = {}) {
108
109
  if (process.env.ROUTINE_TIMERS_DISABLED === '1') {
109
110
  console.log('[routine-timers] disabled (ROUTINE_TIMERS_DISABLED=1)');
@@ -18,13 +18,14 @@
18
18
  // blob; future behavior knobs add their own reserved keys. A missing key falls
19
19
  // back to the rank-derived default below, so the column stays sparse.
20
20
  //
21
- // Resolution = default-by-rank, capped-by-rank, server-enforced.
21
+ // Resolution = default-by-role, capped-by-role, server-enforced (see the ROLE
22
+ // METADATA block below for why a ceiling is metadata and not a permission).
22
23
  // - DEFAULT level rides the trust ladder (Xenos→Locked … Archon→Exploratory).
23
- // - MAX (ceiling) per rank: Xenos and Thetes are capped at their default (they
24
- // may dial DOWN for more focus, never UP); Metic and Archon may set any
25
- // level (their rank is a starting default, not a ceiling). Owner decision
24
+ // - CEILING per role: Xenos and Thetes are capped at their default (they may
25
+ // dial DOWN for more focus, never UP); Metic and Archon may set any level
26
+ // (their rank is a starting default, not a ceiling). Owner decision
26
27
  // 2026-06-19: "Thetes is gated; Metic is just a starting default."
27
- // - effectiveLevel = clamp(override ?? default, 0, maxForRank). So a Xenos who
28
+ // - effectiveLevel = clamp(override ?? default, 0, ceiling). So a Xenos who
28
29
  // stores Exploratory is still resolved to Locked — opening the setting up
29
30
  // for self-service later stays safe because the clamp is the enforcement,
30
31
  // not the UI.
@@ -98,15 +99,54 @@ const LEVEL_CONTRACT = Object.freeze({
98
99
  + 'investigations worth the user\'s attention as you go, while still landing the claimed task.',
99
100
  });
100
101
 
101
- // Default level per rank (the array index in LEVELS). Rides the live rank
102
- // ladder (xenos<thetes<metic<archon — db.RANKS / boxes.RANK_ORDER). A rank not
103
- // listed (e.g. a reserved/penalty rank, or an unknown string) falls back to the
104
- // safest floor: Locked.
105
- const RANK_DEFAULT = Object.freeze({ xenos: 0, thetes: 1, metic: 2, archon: 3 });
102
+ // ---- the wandering ceiling as ROLE METADATA (BV1.R107, ADR 0151) ----
103
+ //
104
+ // The governance model makes a permission a BOOLEAN atom and a rank a seeded
105
+ // ROLE. A wandering ceiling is neither: it is a NUMBER in 0..3. Spelling it as
106
+ // permissions would need one catalog key per level (`me.wandering.level.0..3`) —
107
+ // four keys no route consumes individually, against the catalog's first rule
108
+ // ("a permission no route consumes does not exist") and multiplying the key count
109
+ // to encode a quantity that was never a yes/no capability.
110
+ //
111
+ // So the ceiling is ROLE METADATA: a property carried BY the role, alongside —
112
+ // not inside — the permission that authorizes the write. The two compose:
113
+ // • the PERMISSION `me.wandering.set` (catalog: floor xenos, scope 'own',
114
+ // resource 'builder_prefs') answers MAY this builder set their OWN level. It
115
+ // is ownership-scoped, so it is answered by resolver.authorizeOwned(), never
116
+ // by an unconditional "do they hold it" — that is the route gate's business.
117
+ // • the ROLE METADATA below answers HOW HIGH they may set it. Holding the
118
+ // permission is never sufficient on its own: effectiveLevel() clamps to the
119
+ // role's ceiling server-side, so the ceiling is enforcement, not UI copy.
120
+ //
121
+ // Keyed by ROLE KEY. The four seeded rank-roles are named for the ranks they seed
122
+ // (governance_002), so a role key and a rank string coincide — which is why the
123
+ // rank-keyed helpers below still resolve correctly, and why BV1.R108 can fold this
124
+ // table onto the one live ladder without changing a value. A role this table does
125
+ // not name carries NO wandering metadata and resolves to the safest floor
126
+ // (Locked), the same fail-closed posture the resolver takes for a rank with no
127
+ // seeded role.
128
+ const ROLE_WANDERING = Object.freeze({
129
+ xenos: Object.freeze({ default: 0, ceiling: 0 }),
130
+ thetes: Object.freeze({ default: 1, ceiling: 1 }),
131
+ metic: Object.freeze({ default: 2, ceiling: 3 }),
132
+ archon: Object.freeze({ default: 3, ceiling: 3 }),
133
+ });
134
+ const NO_ROLE_WANDERING = Object.freeze({ default: 0, ceiling: 0 });
135
+
136
+ function roleWandering(roleKey) {
137
+ return Object.prototype.hasOwnProperty.call(ROLE_WANDERING, roleKey)
138
+ ? ROLE_WANDERING[roleKey]
139
+ : NO_ROLE_WANDERING;
140
+ }
106
141
 
107
- // Ceiling per rank. Below Metic the default IS the ceiling (dial down only);
108
- // Metic+ may pick any level. Unknown rank → floor (0).
109
- const RANK_MAX = Object.freeze({ xenos: 0, thetes: 1, metic: 3, archon: 3 });
142
+ // DERIVED projections of the metadata above, kept because scripts/gds/session-eval.js
143
+ // and the /me payload read them by rank. Deriving them means the two views cannot drift.
144
+ const RANK_DEFAULT = Object.freeze(
145
+ Object.fromEntries(Object.entries(ROLE_WANDERING).map(([role, meta]) => [role, meta.default])),
146
+ );
147
+ const RANK_MAX = Object.freeze(
148
+ Object.fromEntries(Object.entries(ROLE_WANDERING).map(([role, meta]) => [role, meta.ceiling])),
149
+ );
110
150
 
111
151
  const TOP_LEVEL = LEVELS.length - 1; // 3
112
152
  const RESERVED_KEY = 'wandering';
@@ -120,9 +160,10 @@ for (const key of LEVELS) {
120
160
  for (let i = 0; i < LEVELS.length; i++) {
121
161
  if (LEVEL_META[LEVELS[i]].n !== i) throw new Error(`wandering-prefs: LEVEL_META.${LEVELS[i]}.n must equal its index ${i}`);
122
162
  }
123
- for (const rank of Object.keys(RANK_DEFAULT)) {
124
- if (!(rank in RANK_MAX)) throw new Error(`wandering-prefs: RANK_DEFAULT has '${rank}' but RANK_MAX does not`);
125
- if (RANK_DEFAULT[rank] > RANK_MAX[rank]) throw new Error(`wandering-prefs: default > max for rank '${rank}'`);
163
+ for (const [role, meta] of Object.entries(ROLE_WANDERING)) {
164
+ if (!isLevelInt(meta.default)) throw new Error(`wandering-prefs: role '${role}' has a non-level default`);
165
+ if (!isLevelInt(meta.ceiling)) throw new Error(`wandering-prefs: role '${role}' has a non-level ceiling`);
166
+ if (meta.default > meta.ceiling) throw new Error(`wandering-prefs: default > ceiling for role '${role}'`);
126
167
  }
127
168
 
128
169
  // ---- pure helpers ----
@@ -141,11 +182,11 @@ function levelIndex(key) {
141
182
  }
142
183
 
143
184
  function defaultLevelForRank(rank) {
144
- return Object.prototype.hasOwnProperty.call(RANK_DEFAULT, rank) ? RANK_DEFAULT[rank] : 0;
185
+ return roleWandering(rank).default;
145
186
  }
146
187
 
147
188
  function maxLevelForRank(rank) {
148
- return Object.prototype.hasOwnProperty.call(RANK_MAX, rank) ? RANK_MAX[rank] : 0;
189
+ return roleWandering(rank).ceiling;
149
190
  }
150
191
 
151
192
  // The override a builder has stored (the raw 'wandering' key off behavior_prefs),
@@ -262,6 +303,8 @@ module.exports = {
262
303
  LEVELS,
263
304
  LEVEL_META,
264
305
  LEVEL_CONTRACT,
306
+ ROLE_WANDERING,
307
+ roleWandering,
265
308
  RANK_DEFAULT,
266
309
  RANK_MAX,
267
310
  TOP_LEVEL,
@@ -0,0 +1,73 @@
1
+ # OTB Dev Box — the desktop switch for a builder's hosted box
2
+
3
+ A menu-bar (macOS) / system-tray (Windows) app that turns a builder's hosted
4
+ dev box on and off with one switch. Built for the newcomer who should never
5
+ see a terminal: sign in with browser clicks (app pairing, ADR 0045), flip the
6
+ switch, and connect from Claude Desktop or the browser terminal.
7
+
8
+ What it does beyond the switch — each fixing a failure observed in the
9
+ 2026-06-10 onboarding sessions that killed the old `curl | bash` flow:
10
+
11
+ - **Pairing sign-in** — opens the builders' hall, one Approve click, token
12
+ delivered over the app's own secret channel (`/api/gds/auth/app-pair/*`).
13
+ The session is written to `~/.config/otb/gds-session.json`, the same file
14
+ the `/builder-*` CLI uses — pairing the app signs in the CLI too.
15
+ - **SSH key done right** — generates `~/.ssh/otb_builder` (ed25519) with real
16
+ 600 perms (icacls ACL on Windows) and registers it via `POST /box/ssh-key`.
17
+ - **known_hosts kept fresh** — Claude Desktop's embedded ssh2 reads ONLY
18
+ `~/.ssh/known_hosts`, and host keys change on every re-provision; the app
19
+ re-scans and replaces entries each time the box turns active.
20
+ - **Claude Desktop connection file** — installs the managed-settings file to
21
+ the system path with a native elevation prompt (osascript / UAC), the step
22
+ the agent-driven flow could never finish.
23
+ - **Native notifications** — box ready, rested while idle.
24
+ - **No heartbeats** — the app never sends `claude_active`; the droplet's idle
25
+ sweep stays authoritative so an unused box still reclaims itself (~10 min).
26
+
27
+ ## Layout
28
+
29
+ - `src/main.js` — Electron main: tray, window, IPC, wiring.
30
+ - `src/brand.js` — every brand string (ADR 0004: the civ name lives here only).
31
+ - `src/{pairing,box-controller,gds-client,session-store,ssh-setup,updater}.js` — the
32
+ logic, Electron-free and unit-tested in `test/unit.mjs`.
33
+ - `src/updater.js` — in-app update check (#924): semver compare + platform→target
34
+ resolution. `main.js` polls `GET /downloads/devbox/version` on open + every 12h
35
+ and, when the published release is strictly newer, shows the Update banner; the
36
+ Update button downloads the installer and hands it to the OS (browser-download
37
+ fallback on any error). **Bumping `config/devbox-app.json` version is what makes
38
+ the banner appear in the field** — see [`docs/recipes/devbox-app-release.md`](../docs/recipes/devbox-app-release.md).
39
+ - `renderer/` — the panel UI (sandboxed, contextIsolated; preload exposes
40
+ named verbs only).
41
+ - `scripts/gen-icons.js` — regenerates all icons from pixel grids (`npm run
42
+ icons`; borrows `sharp` from the repo root's node_modules).
43
+
44
+ ## Develop
45
+
46
+ ```
47
+ cd devbox-app
48
+ npm install
49
+ npm test # hermetic unit tests, no Electron needed
50
+ npm start # run against prod (or OTB_DEVBOX_API_BASE=http://… npm start)
51
+ ```
52
+
53
+ Dev/test env hooks: `OTB_DEVBOX_API_BASE` (point at a stub/staging GDS),
54
+ `OTB_DEVBOX_NO_ELEVATE=1` (skip the managed-settings elevation prompt),
55
+ `OTB_DEVBOX_SCREENSHOT_DIR` (write a PNG of the panel on every state change —
56
+ how CI/verification captures proof).
57
+
58
+ ## Build & release
59
+
60
+ ```
61
+ npm run dist:mac # dmg, arm64 + x64 (unsigned → ad-hoc)
62
+ npm run dist:win # NSIS one-click installer, x64 + arm64
63
+ ```
64
+
65
+ CI builds both on every push that touches `devbox-app/**`
66
+ (`.github/workflows/build-devbox-app.yml`) and uploads them as artifacts.
67
+ Publishing = attach the artifacts to a release on the PUBLIC releases repo
68
+ named in `config/devbox-app.json`, then bump that config and ship — the
69
+ branded `/downloads/devbox/*` redirects follow the config. Full recipe:
70
+ `docs/recipes/devbox-app-release.md`.
71
+
72
+ This subproject is self-contained on purpose: its `package.json` never feeds
73
+ the droplet's `npm ci`, and nothing under `devbox-app/` runs server-side.