@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
@@ -25,6 +25,17 @@
25
25
 
26
26
  const { pool } = require('../../src/module-api');
27
27
 
28
+ // isPendingReview — the ONE derivation of the "met — pending review" auto-flag
29
+ // (ADR 0086 §6 / BV1.R63): a criterion with >=1 linked task, every one shipped,
30
+ // and not yet confirmed. Pure + shared so every read model agrees — the rollup
31
+ // below (buildCriterionProgress) and the hall's goal-detail read
32
+ // (db.listCriteriaForGoal, task 1002164) both call it; listPendingReviewCriteria
33
+ // expresses the same rule as a SQL HAVING clause. Counts are coerced because
34
+ // node-postgres renders count() as a string unless cast.
35
+ function isPendingReview({ satisfied, linked, unshipped }) {
36
+ return !satisfied && Number(linked) > 0 && Number(unshipped) === 0;
37
+ }
38
+
28
39
  // -------------------------------------------------------------------------
29
40
  // Reads
30
41
  // -------------------------------------------------------------------------
@@ -262,9 +273,12 @@ function buildCriterionProgress(versionId, rows, unattributedCount = 0, goalRows
262
273
  // Auto-flag (ADR 0086 §6 / BV1.R63): a criterion is "met — pending review"
263
274
  // when it has ≥1 linked task and every one has shipped, but it has NOT yet
264
275
  // been confirmed (satisfied=false). Purely DERIVED — no stored column, no
265
- // manual propose step. /goal-review surfaces these for a Metic+ to confirm
266
- // (→ satisfied) or reject; /status renders the flag inline.
267
- const pending_review = !c.satisfied && c.tasks.length > 0 && remaining.length === 0;
276
+ // manual propose step. /goal-review and the hall's goal-detail page (task
277
+ // 1002164) surface these for a Metic+ to confirm (→ satisfied) or reject;
278
+ // /status renders the flag inline.
279
+ const pending_review = isPendingReview({
280
+ satisfied: c.satisfied, linked: c.tasks.length, unshipped: remaining.length,
281
+ });
268
282
  return { ...c, counts: { ...counts, shipped, remaining: remaining.length }, remaining, pending_review };
269
283
  });
270
284
 
@@ -350,6 +364,7 @@ module.exports = {
350
364
  criterionProgress,
351
365
  buildCriterionProgress,
352
366
  buildGoals,
367
+ isPendingReview,
353
368
  markCriterion,
354
369
  unmarkCriterion,
355
370
  };
@@ -383,7 +383,8 @@ async function enableAutoMerge({ nodeId }, deps = {}) {
383
383
  // getPublishStatus — report the PR + prod-deploy state so ship.js can poll
384
384
  // WITHOUT gh on the box. Returns:
385
385
  // { pr_state, merged, merge_sha, deploy_status } where
386
- // pr_state ∈ open|closed|none, deploy_status ∈ pending|success|none.
386
+ // pr_state ∈ open|closed|none, deploy_status ∈ pending|success|none|unconfigured
387
+ // ('unconfigured' = this repo has no deploy-prod.yml — task 1002472).
387
388
  // Mirrors ship.js pollCiDeploy's two-stage check (PR merged -> deploy run).
388
389
  async function getPublishStatus({ branch }, deps = {}) {
389
390
  const token = await resolveToken(deps);
@@ -432,7 +433,17 @@ async function getPublishStatus({ branch }, deps = {}) {
432
433
  (r) => r && r.head_sha === mergeSha && r.status === 'completed' && r.conclusion === 'success'
433
434
  );
434
435
  if (hit) deployStatus = 'success';
436
+ } else if (runsRes.status === 404) {
437
+ // task 1002472 — the workflow FILE does not exist in this repo, so there is no
438
+ // deploy to wait for and 'pending' would never resolve. The 404 is unambiguous:
439
+ // the /pulls fetch above already threw STATUS_FETCH_FAILED if the repo were
440
+ // unreachable, so reaching here means the repo resolved and only the workflow is
441
+ // missing — a permanent condition. Instances that publish by pin-bump
442
+ // (package-core → bongos upgrade, ADR 0108) never have a deploy-prod.yml at all;
443
+ // reporting 'pending' pinned every merged PR at confirmed forever.
444
+ deployStatus = 'unconfigured';
435
445
  }
446
+ // Any other non-ok (5xx, 403, rate limit) is transient → stay 'pending' and retry.
436
447
  } catch (_) {
437
448
  /* leave pending on a transient runs-API miss */
438
449
  }
@@ -652,8 +663,14 @@ async function setCommitStatus({ sha, state, context, description, targetUrl },
652
663
  // publish-status route flips the task confirmed→shipped so a successful publish
653
664
  // never strands at confirmed waiting on a human to re-poll (the #1027
654
665
  // retrospective). Pure + exported so the route's reconcile trigger is unit-tested.
666
+ // task 1002472 — 'unconfigured' (no deploy-prod.yml in the repo, see getPublishStatus)
667
+ // counts as landed too: on an instance that publishes by pin-bump (ADR 0108) the MERGE
668
+ // is the land, and requiring a deploy that can never run pinned every merged PR at
669
+ // 'confirmed' forever. MERGED stays mandatory, and this is only the gate on the
670
+ // branch/PR path — landProvenForTask still has to prove the land is THIS task's.
655
671
  function landConfirmed(status) {
656
- return !!status && status.merged === true && status.deploy_status === 'success';
672
+ if (!status || status.merged !== true) return false;
673
+ return status.deploy_status === 'success' || status.deploy_status === 'unconfigured';
657
674
  }
658
675
 
659
676
  // task 1547 — is a landConfirmed status PROVEN to be THIS task's land, and not a
@@ -0,0 +1,114 @@
1
+ // modules/lifecycle/goal-advisory.js — the ORPHAN-TASK advisory (task 1002539).
2
+ //
3
+ // THE HOLE. Nothing errors when a task has no goal to live in. POST /tasks with no
4
+ // goal_id returns 201 every time: db.createTask defaults a goal-less task into its
5
+ // version's catch-all "<version> — general" goal, and if that goal does not exist
6
+ // the fallback is NULL and the task is created goal-less, silently.
7
+ //
8
+ // And for a modern version it does not exist. The catch-all rows were created ONLY
9
+ // by migrations/160_goal_tier.sql, by selecting over the `versions` table AT THAT
10
+ // MOMENT. db.createVersion does not create one and there is no trigger — so every
11
+ // version created after migration 160 gives each of its tasks goal_id = NULL, with
12
+ // no signal to anyone. A task with no goal has no scope wall, no done-when criteria
13
+ // gating it, and no workspace it gets reviewed in (ADR 0086 §2).
14
+ //
15
+ // This module makes that visible WITHOUT blocking anything: POST /tasks attaches
16
+ // the verdict to its 201 body as `goal_advisory`, the /goal-create skill picks it
17
+ // up, and a task that lands somewhere real gets `null` — silence.
18
+ //
19
+ // PURE + exported so every branch is pinned by a DB-free unit test (the
20
+ // criterion-suggest / goal-authz precedent). A module sibling, like ../db.
21
+
22
+ // The catch-all goal migration 160 created per version. ONE definition, shared with
23
+ // db.generalGoalIdForVersion, which used to spell it inline under a "keep the two in
24
+ // sync" comment. The dash is an EM DASH (U+2014) with spaces either side — an ASCII
25
+ // hyphen or an en dash is a different, ordinary goal and must not match.
26
+ function generalGoalTitle(versionId) {
27
+ return `${versionId} — general`;
28
+ }
29
+
30
+ // adviseGoal — PURE, FAIL-OPEN. Returns null when the task has a real home, or the
31
+ // advisory body when it does not. Two reasons, checked in order:
32
+ //
33
+ // no_goal — goal_id is NULL. The version has no catch-all at all (created after
34
+ // migration 160), so the task is structurally orphaned.
35
+ // catch_all — the task landed in "<version> — general": a bucket, not a workspace.
36
+ // No scope wall, no criteria, no members, no review. Better than NULL,
37
+ // still not a home.
38
+ //
39
+ // Fail-OPEN on ambiguity (return null, say nothing) — the opposite posture from an
40
+ // authorization gate. This is advice appended to a request that ALREADY SUCCEEDED;
41
+ // a wrong guess here nags a builder about a task that is fine, which is worse than
42
+ // staying quiet. A missing versionId or an unreadable goal title yields null.
43
+ function adviseGoal({ versionId, goalId, goalTitle } = {}) {
44
+ if (goalId == null) {
45
+ return {
46
+ reason: 'no_goal',
47
+ message: 'This task has no goal to live in — no scope wall, no done-when criteria gating it, '
48
+ + 'and no workspace it gets reviewed in (ADR 0086 §2). Its version has no catch-all goal, '
49
+ + 'which is normal for any version created after the goal tier shipped.',
50
+ version_id: versionId ?? null,
51
+ goal_id: null,
52
+ };
53
+ }
54
+ if (versionId == null || typeof goalTitle !== 'string') return null;
55
+ if (goalTitle !== generalGoalTitle(versionId)) return null;
56
+ return {
57
+ reason: 'catch_all',
58
+ message: `This task landed in "${goalTitle}", the version's catch-all bucket rather than a real goal — `
59
+ + 'no scope wall, no criteria, and no owner reviewing it.',
60
+ version_id: versionId,
61
+ goal_id: goalId,
62
+ };
63
+ }
64
+
65
+ // candidateGoals — PURE. The version's OPEN goals minus the catch-all, shaped down
66
+ // to what a caller needs to choose a home. Order is preserved (listGoals already
67
+ // sorts by sort_order), so the caller can trust the first entries are the ranked
68
+ // ones. Non-array input yields [] rather than throwing — same fail-open posture.
69
+ function candidateGoals(goals, versionId) {
70
+ if (!Array.isArray(goals)) return [];
71
+ const catchAll = versionId == null ? null : generalGoalTitle(versionId);
72
+ return goals
73
+ .filter((g) => g && g.title !== catchAll)
74
+ .map((g) => ({ id: g.id, title: g.title, scope_modules: g.scope_modules || [] }));
75
+ }
76
+
77
+ // How many alternative goals the advisory offers. Small on purpose — this is a hint
78
+ // for a human/agent choosing a home, not a browsable list.
79
+ const CANDIDATE_LIMIT = 5;
80
+
81
+ // buildGoalAdvisory — the whole advisory, DB reads included, so POST /tasks calls one
82
+ // line and this is directly unit-testable with a stub db (no route simulation).
83
+ //
84
+ // It swallows its own failures on purpose. The task row is ALREADY COMMITTED when
85
+ // this runs, so a throw would turn a created task into a 500 that the caller then
86
+ // retries — and source_ref idempotency only saves callers who set one, so a bare
87
+ // retry mints a DUPLICATE task. Degrading to null (silence) is always correct here.
88
+ // Only the unhappy path pays for the second query.
89
+ async function buildGoalAdvisory({ db, versionId, task }) {
90
+ try {
91
+ const homeGoal = task.goal_id != null ? await db.getGoal(task.goal_id) : null;
92
+ const verdict = adviseGoal({
93
+ versionId,
94
+ goalId: task.goal_id,
95
+ goalTitle: homeGoal && homeGoal.title,
96
+ });
97
+ if (!verdict) return null;
98
+ // LIMIT 6, not 6-in-JS: Postgres truncates instead of shipping every open goal
99
+ // on the version to be discarded here. Six because candidateGoals drops the
100
+ // catch-all, so 6 rows still guarantee the 5 candidates below.
101
+ const open = await db.listGoals({ versionId, status: 'open', limit: CANDIDATE_LIMIT + 1 });
102
+ return {
103
+ ...verdict,
104
+ candidate_goals: candidateGoals(open, versionId).slice(0, CANDIDATE_LIMIT),
105
+ next_step: 'Run /goal-create to plan a goal for this work, or file it under an existing one: '
106
+ + `PATCH /tasks/${task.id} {"goal_id": <id>}`,
107
+ };
108
+ } catch (e) {
109
+ console.error('[gds] goal-advisory (non-blocking)', e && e.message);
110
+ return null;
111
+ }
112
+ }
113
+
114
+ module.exports = { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory };
@@ -0,0 +1,62 @@
1
+ // modules/lifecycle/goal-authz.js — the goal AUTHORITY MODEL, as pure functions.
2
+ //
3
+ // Extracted from routes/goals.js (task 1002538 / ADR 0154) so a SECOND route can
4
+ // share it: POST /versions/:id/done-when now lets a goal's own authority author
5
+ // that goal's criteria, and authorization logic must have exactly one definition.
6
+ // routes/goals.js re-exports authorizeGoalManagement (isGoalOwner it imports for
7
+ // internal use only), so its existing DB-free unit tests (goal_conflicts,
8
+ // goal_member_roles, goal_membership_requests) are unchanged.
9
+ //
10
+ // A module sibling, like ../goal-conflicts — routes consume pure files here; no
11
+ // route requires another route.
12
+
13
+ // The goal AUTHORITY MODEL (task 1735). Two layers, deliberately different kinds:
14
+ // - OWNER — goals.created_by, the fixed authority. Not a role row: it is set at
15
+ // create time (the creator) and moves only via the explicit transfer route.
16
+ // The owner holds every goal power.
17
+ // - MANAGER — the goal_members.role 'lead' value (the hall renders it
18
+ // "manager"). Owner-equivalent EXCEPT: a manager cannot DEMOTE a fellow
19
+ // manager (owner-only), and cannot transfer ownership.
20
+ // A goal with created_by NULL (the migration-160 system defaults) has no owner;
21
+ // Archons act as one — the archon arm of both gates below.
22
+ function isGoalOwner(goal, builderId) {
23
+ return goal.created_by != null && String(goal.created_by) === String(builderId);
24
+ }
25
+
26
+ // isGoalOwnerOrManager — the AUTHORITY predicate on its own, with no goal-state
27
+ // check in front of it. authorizeGoalManagement answers "may this actor manage
28
+ // this goal *right now*" (state first, so an owner gets a useful goal_not_open);
29
+ // a caller that must not LEAK the goal's existence needs the authority question
30
+ // alone, asked first. ADR 0154: POST /versions/:id/done-when uses this to mask an
31
+ // unauthorized caller behind the same 400 as a non-existent goal.
32
+ function isGoalOwnerOrManager({ goal, actorId, actorRank, actorMembership }) {
33
+ if (actorRank === 'archon') return true;
34
+ if (!goal) return false;
35
+ return isGoalOwner(goal, actorId) || (!!actorMembership && actorMembership.role === 'lead');
36
+ }
37
+
38
+ // authorizeGoalManagement — the shared ORDERED, FAIL-CLOSED prefix for actions
39
+ // a goal's own authority performs (task 1736: conflict resolution; the same
40
+ // three steps open authorizeMemberRoleChange): goal exists → goal open →
41
+ // actor is owner|manager|Archon. Returns { ok:true } or { ok:false, status,
42
+ // body }. PURE + exported, same precedent as its siblings.
43
+ function authorizeGoalManagement({ goal, actorId, actorRank, actorMembership }) {
44
+ if (!goal) {
45
+ return { ok: false, status: 404, body: { error: 'goal_not_found' } };
46
+ }
47
+ if (goal.status !== 'open') {
48
+ return { ok: false, status: 409, body: {
49
+ error: 'goal_not_open', status: goal.status,
50
+ message: 'Only an open goal can be managed.',
51
+ } };
52
+ }
53
+ if (!isGoalOwnerOrManager({ goal, actorId, actorRank, actorMembership })) {
54
+ return { ok: false, status: 403, body: {
55
+ error: 'not_owner_or_manager',
56
+ message: 'Only this goal\'s owner or a manager (or an Archon) may do this.',
57
+ } };
58
+ }
59
+ return { ok: true };
60
+ }
61
+
62
+ module.exports = { isGoalOwner, isGoalOwnerOrManager, authorizeGoalManagement };
@@ -48,6 +48,10 @@ module.exports = {
48
48
  recentGradesForBuilder: (...a) => db.recentGradesForBuilder(...a),
49
49
  countShippedTasksForBuilder: (...a) => db.countShippedTasksForBuilder(...a),
50
50
  recentShippedTasksByBuilder: (...a) => db.recentShippedTasksByBuilder(...a),
51
+ // ADR 0120 part 5: the confirmed tasks a builder owes a rebase on — the same
52
+ // read the part-3 claim gate uses, surfaced to me.js so the CLI + hall can
53
+ // show the owed-rebase warning that gates new claims (reward lands on ship).
54
+ needsRebaseTasksForBuilder: (...a) => db.needsRebaseTasksForBuilder(...a),
51
55
  // builders.js (the Archon roster): the roster + stale-claim + per-builder grade.
52
56
  getBuildersRoster: (...a) => db.getBuildersRoster(...a),
53
57
  getActiveClaimsForBuilderWithStale: (...a) => db.getActiveClaimsForBuilderWithStale(...a),
@@ -55,4 +59,9 @@ module.exports = {
55
59
  // analytics.js (the AI-dev-curve): the builder + ecosystem analytics.
56
60
  builderAnalytics: (...a) => db.builderAnalytics(...a),
57
61
  ecosystemAnalytics: (...a) => db.ecosystemAnalytics(...a),
62
+ // dev-box (modules/dev-box/routes/box.js, via resolveOptional('lifecycle')): the
63
+ // module-key scope of a builder's active claims — the task-scoped source-access
64
+ // resolver (goal 1000051 / R92, ADR 0148 §2). Keys only; box-access maps them to
65
+ // git sparse-checkout globs through the doorway `moduleScopeMap`.
66
+ moduleScopeForActiveClaims: (...a) => db.moduleScopeForActiveClaims(...a),
58
67
  };
@@ -72,6 +72,12 @@ const healAttempts = new Map(); // branch -> regen-heal attempts this process
72
72
 
73
73
  let intervalTimer = null;
74
74
  let firstTimer = null;
75
+ // task 1002461: the no-credential skip is WARN'd once per unconfigured streak, not
76
+ // once per tick — a 5-min poller would otherwise bury the journal. Re-armed by any
77
+ // sweep that proceeds, so a credential LOSS after a healthy period is reported.
78
+ // Injectable (deps.warnState) for the same reason healAttempts is: the tests in this
79
+ // suite run concurrently, so a shared module latch would leak across them.
80
+ const warnState = { unconfigured: false };
75
81
 
76
82
  // task 1915 — is this mergeIfGreen result the "mergeable but a check is RED" shape
77
83
  // whose commonest cause is a STALE generated index (see refreshStaleGenerated)? Only
@@ -86,6 +92,32 @@ function couldBeStaleGenerated(m) {
86
92
  return /^failing_check:/.test(reason) || /^non_required_check_failing:/.test(reason);
87
93
  }
88
94
 
95
+ // task 1002461 — render the reconciler's operating target: which repo it resolved
96
+ // and whether it holds a credential. This is the line that makes a WRONG repo bind
97
+ // visible without owner access (the live failure: the hub bound
98
+ // cloudbongos/cloudbongos-instance while the PRs were in lars589/cloud-bongos).
99
+ // Pure — takes the resolved facts, returns the string — so the wording is
100
+ // unit-tested without a boot.
101
+ function describeTarget({ configured, repo } = {}) {
102
+ const cred = configured ? 'credential present' : 'NO PUSH CREDENTIAL';
103
+ if (!repo || repo.error) {
104
+ return `target: UNRESOLVED (${(repo && repo.detail) || (repo && repo.error) || 'no repo info'}) · ${cred}`;
105
+ }
106
+ return `target: ${repo.owner}/${repo.repo} (branch ${repo.branch || 'main'}) · ${cred}`;
107
+ }
108
+
109
+ // Resolve the repo the reconciler would act on, never throwing — an observability
110
+ // helper must not be able to break the sweep it is describing. `deps.repoInfo` is
111
+ // the test seam.
112
+ function resolveTarget(deps = {}) {
113
+ if (deps.repoInfo) return deps.repoInfo;
114
+ try {
115
+ return seams.repoInfo.loadRepoInfo();
116
+ } catch (e) {
117
+ return { error: 'lookup_failed', detail: e && e.message };
118
+ }
119
+ }
120
+
89
121
  // One sweep. Pure w.r.t. injected deps (db / githubPush / emit / loggers)
90
122
  // so the whole thing is unit-tested with no DB and no network. Returns a summary
91
123
  // { skipped?, checked, reconciled, errors } for observability + tests.
@@ -99,9 +131,23 @@ async function runSweep(deps = {}) {
99
131
 
100
132
  // No server push credential (local dev, tests, an un-provisioned host) → there
101
133
  // is nothing to reconcile against, and getPublishStatus would 503. No-op.
134
+ //
135
+ // task 1002461: this used to return SILENTLY while start() logged "started —
136
+ // sweeping confirmed→shipped every Ns", so the journal showed a healthy poller
137
+ // that could never land anything — the stranding went unnoticed for 6+ days. Say
138
+ // it, once per unconfigured streak, and name the repo so a wrong bind shows up in
139
+ // the same line.
140
+ const warn = deps.warnState || warnState;
102
141
  if (!push.isConfigured()) {
142
+ if (!warn.unconfigured) {
143
+ warn.unconfigured = true;
144
+ errFn(
145
+ `[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID + GITHUB_APP_INSTALLATION_ID + GITHUB_APP_PRIVATE_KEY, or GITHUB_PUSH_TOKEN). Subsequent sweeps stay quiet until this changes.`
146
+ );
147
+ }
103
148
  return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0 };
104
149
  }
150
+ warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
105
151
 
106
152
  let rows;
107
153
  try {
@@ -146,6 +192,10 @@ async function runSweep(deps = {}) {
146
192
  let landed = false;
147
193
  let landNote = '';
148
194
  let branchStatus = null;
195
+ // task 1002472 — set when a PR exists but has not fully landed. Suppresses the
196
+ // land proof (an open PR must never ship) WITHOUT suppressing the observability
197
+ // below, which the old bare `continue` did.
198
+ let prNotLanded = false;
149
199
 
150
200
  if (branchUsable) {
151
201
  // Active merge driver (task 1162): GitHub's auto-merge fires on check-completion
@@ -209,6 +259,17 @@ async function runSweep(deps = {}) {
209
259
  logFn(
210
260
  `[publish-reconciler] task ${r.task_id} (${branch}) PR #${m.number} still red after ${n} regen-heal attempt(s) (${m.reason}) — capped (persistent staleness treadmill or a real check failure); left for a human`
211
261
  );
262
+ // ADR 0120 part 2: the auto-land grace (retries + ADR 0082 self-heal)
263
+ // is exhausted and the PR is still red — a real check failure or a
264
+ // hand-written conflict only the builder can resolve. Flag needs_rebase
265
+ // so their queue is gated until they fix + re-land (or release it).
266
+ try {
267
+ if (await db.flagTaskNeedsRebase(r.task_id, `failing_check_capped:${m.reason}`)) {
268
+ logFn(`[publish-reconciler] task ${r.task_id} flagged needs_rebase (regen-heal cap) — its builder must fix + re-land`);
269
+ }
270
+ } catch (e) {
271
+ errFn(`[publish-reconciler] flagNeedsRebase (cap) task ${r.task_id} (non-blocking): ${e && e.message}`);
272
+ }
212
273
  } else {
213
274
  try {
214
275
  const rr = await resolver.refreshStaleGenerated({ branch });
@@ -247,7 +308,14 @@ async function runSweep(deps = {}) {
247
308
  if (!push.landConfirmed(status) && status && status.pr_state !== 'none') {
248
309
  // A PR exists but hasn't fully landed yet (open / merged-but-deploy-pending /
249
310
  // blocked). Respect it — wait for the deploy — rather than risk shipping early.
250
- continue;
311
+ //
312
+ // task 1002472: this used to be a bare `continue`, which jumped PAST the
313
+ // unprovable counter, the STALE WARN and flagTaskNeedsRebase below — so this
314
+ // strand shape was completely invisible (tasks 1002427/1002428 sat 50h at
315
+ // 'confirmed' reporting needs_rebase:false). Flag it instead and fall through
316
+ // to the observability block. The flag still suppresses the land proof, so an
317
+ // un-landed PR is no more shippable than it was.
318
+ prNotLanded = true;
251
319
  }
252
320
  branchStatus = status;
253
321
  }
@@ -262,7 +330,7 @@ async function runSweep(deps = {}) {
262
330
  // references this task id. landProvenForTask still cures the task-1313 strand class
263
331
  // (PR deleted post-merge / no worktree_name / ambiguous branch) — now keyed on the
264
332
  // unambiguous published tip rather than the base.
265
- if (!landed && typeof push.landProvenForTask === 'function') {
333
+ if (!landed && !prNotLanded && typeof push.landProvenForTask === 'function') {
266
334
  try {
267
335
  const proof = await push.landProvenForTask({
268
336
  taskId: r.task_id, publishedSha: r.published_head_sha, status: branchStatus,
@@ -284,12 +352,31 @@ async function runSweep(deps = {}) {
284
352
  // (codeless confirm, or a land whose published tip was never recorded on the
285
353
  // claim) now surfaces in the logs instead of stranding silently.
286
354
  unprovable++;
287
- const reason = (!branch && !r.published_head_sha) ? 'no_branch_no_tip' : 'land_not_proven';
355
+ // task 1002472: 'pr_not_landed' distinguishes "a PR exists and we are waiting on
356
+ // it" from "we looked for proof and found none" — the two need different
357
+ // remediation (wait/unblock the PR vs. push or rebase the branch).
358
+ const reason = prNotLanded
359
+ ? 'pr_not_landed'
360
+ : (!branch && !r.published_head_sha) ? 'no_branch_no_tip' : 'land_not_proven';
288
361
  const ageMs = r.confirmed_at ? (nowMs - new Date(r.confirmed_at).getTime()) : null;
289
362
  if (ageMs != null && ageMs > STALE_CONFIRMED_MS) {
290
363
  staleStrands++;
364
+ // ADR 0120 part 2: an aged-out, unprovable confirmed task is a strand
365
+ // whose land its BUILDER must complete — rebase a branch gone stale after
366
+ // a big codebase change, or push a branch that only ever lived on their
367
+ // machine (the `no_branch_no_tip` case). Flag needs_rebase (attributed via
368
+ // shipped_by = the claim holder); the claim gate then blocks that builder's
369
+ // NEW claims until they land or release it. Since the reward now lands only
370
+ // on ship (part 1), they are motivated to clear it. NOTE it is no longer
371
+ // "credited" — part 1 moved the payout from confirm to the land.
372
+ let flagged = false;
373
+ try {
374
+ flagged = await db.flagTaskNeedsRebase(r.task_id, `strand:${reason}`);
375
+ } catch (e) {
376
+ errFn(`[publish-reconciler] flagNeedsRebase (strand) task ${r.task_id} (non-blocking): ${e && e.message}`);
377
+ }
291
378
  errFn(
292
- `[publish-reconciler] STRAND task ${r.task_id}: confirmed ~${Math.round(ageMs / 3_600_000)}h ago, unprovable (${reason}) — credited but no landed proof; needs owner triage`
379
+ `[publish-reconciler] STRAND task ${r.task_id}: confirmed ~${Math.round(ageMs / 3_600_000)}h ago, unprovable (${reason}) — no landed proof; ${flagged ? 'flagged needs_rebase for its builder' : 'needs_rebase already set'}`
293
380
  );
294
381
  }
295
382
  continue;
@@ -399,8 +486,16 @@ function start(deps = {}) {
399
486
  intervalTimer = setInterval(tick, INTERVAL_MS);
400
487
  if (intervalTimer.unref) intervalTimer.unref();
401
488
 
489
+ // task 1002461: the bare "started" line was actively misleading — it printed the
490
+ // same whether the poller could work or not. Always state the resolved target +
491
+ // credential alongside it, so a wrong repo bind or a missing credential is
492
+ // visible at boot instead of needing a simulation to prove. loadRepoInfo caches
493
+ // on first call, so this is also the value every later sweep uses (an env change
494
+ // needs a service restart to take effect).
495
+ const push = deps.githubPush || githubPush; // same resolution runSweep uses
496
+ const target = describeTarget({ configured: push.isConfigured(), repo: resolveTarget(deps) });
402
497
  console.log(
403
- `[publish-reconciler] started — sweeping confirmed→shipped every ${Math.round(INTERVAL_MS / 1000)}s`
498
+ `[publish-reconciler] started — sweeping confirmed→shipped every ${Math.round(INTERVAL_MS / 1000)}s · ${target}`
404
499
  );
405
500
  }
406
501
 
@@ -415,4 +510,12 @@ function stop() {
415
510
  }
416
511
  }
417
512
 
418
- module.exports = { start, stop, runSweep, runCostSweep, couldBeStaleGenerated, INTERVAL_MS };
513
+ module.exports = {
514
+ start,
515
+ stop,
516
+ runSweep,
517
+ runCostSweep,
518
+ couldBeStaleGenerated,
519
+ describeTarget,
520
+ INTERVAL_MS,
521
+ };
@@ -41,28 +41,38 @@ function taskNotReadyHint(taskId, currentStatus) {
41
41
  : `Task #${idNum} is '${currentStatus}', not 'ready' — only 'ready' tasks are claimable (promote applies to 'backlog': POST /api/bongos/tasks/${idNum}/promote).`;
42
42
  }
43
43
 
44
- // task 1642 — the pure session-scoped one-active-claim decision. Working two
45
- // tasks from one worktree means every commit lands on the single branch that
46
- // worktree owns; /builder-ship publishes the whole branch, so shipping one task
47
- // drags the other's commits with it (commingled ships). The structural cure: a
48
- // CLI session (≈ one worktree) may hold at most ONE active claim at a time.
44
+ // task 1642 (+ the worktree-keying fix) — the pure one-active-claim decision.
45
+ // Working two tasks from one WORKING TREE means every commit lands on the single
46
+ // branch that tree owns; /builder-ship publishes the whole branch, so shipping one
47
+ // task drags the other's commits with it (commingled ships). The structural cure:
48
+ // a working tree may hold at most ONE active claim at a time.
49
49
  //
50
- // Keyed on creator_session_id, NOT builder id — a builder may run many sessions
51
- // in parallel, each with its own worktree and exactly one claim; that norm must
52
- // keep working. Session-scoped, all ranks (generalizes the old Xenos-only,
53
- // builder-scoped gateNewcomerClaim).
50
+ // The hazard boundary is the WORKING TREE (one worktree = one branch = one HEAD),
51
+ // NOT the auth session. Originally this keyed on creator_session_id — but that is
52
+ // the AUTH-TOKEN session, which a builder shares across EVERY concurrent worktree
53
+ // on one login (the CLI token lives in ~/.config, not per-worktree). So the guard
54
+ // wrongly blocked the second of two genuinely-parallel worktrees — contradicting
55
+ // its own stated intent ("a builder may run many sessions in parallel, each with
56
+ // its own worktree and exactly one claim; that norm must keep working"). The fix:
57
+ // scope refusal to the SAME working tree. Two claims can coexist for one auth
58
+ // session AS LONG AS they land in different worktrees; a second claim into the
59
+ // SAME tree (or a second worktree-less claim off one plain clone, where the single
60
+ // branch really would commingle) is still refused.
54
61
  //
55
- // NULL-session handling: a web claim (bind_session=false) stores
56
- // creator_session_id=NULL and is adoptable by any of the builder's CLI sessions.
57
- // We MUST skip the check when the *incoming* claim has no session (nothing to
58
- // scope to) — the caller passes creatorSessionId=null and we allow. The DB
59
- // helper that feeds activeClaimsForSession already filters to the SAME non-null
60
- // session, so a pile of NULL-session web claims can never count against a CLI
61
- // session either.
62
+ // `worktreeName` is the incoming claim's worktree (claims.worktree_name — the CLI
63
+ // sends it; the server enforces its cross-active-claim uniqueness, V3.R51/#270).
64
+ // Two trees are "the same" iff their worktree_names are equal, OR both are null
65
+ // (no worktree info → assume one shared clone → commingle risk).
66
+ //
67
+ // NULL-session handling is unchanged: a web claim (bind_session=false) stores
68
+ // creator_session_id=NULL and is adoptable by any of the builder's CLI sessions —
69
+ // the incoming caller passes creatorSessionId=null and we allow (nothing to scope).
70
+ // The DB helper filters activeClaimsForSession to the SAME non-null session, so a
71
+ // pile of NULL-session web claims can never count against a CLI session either.
62
72
  //
63
73
  // Pure + exported so the decision is unit-testable without a DB or a route.
64
74
  // Returns { refuse, inFlightTaskId, inFlightTitle, reason }.
65
- function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId }) {
75
+ function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName = null }) {
66
76
  // No session to scope to (web/unbound claim, or a CLI caller with no session):
67
77
  // there is nothing this guard can key on — let it through. The per-task unique
68
78
  // index and the newcomer gate still apply.
@@ -70,10 +80,19 @@ function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId })
70
80
  return { refuse: false, inFlightTaskId: null, inFlightTitle: null, reason: null };
71
81
  }
72
82
  const claims = Array.isArray(activeClaimsForSession) ? activeClaimsForSession : [];
73
- // Defence in depth: only count claims that actually belong to THIS session and
74
- // are genuinely session-bound. A NULL-session row must never trip the block.
83
+ const incomingTree = worktreeName == null ? null : worktreeName;
84
+ // Two claims share a WORKING TREE iff their worktree_names match, or both are
85
+ // null (a worktree-less clone — the single branch genuinely commingles).
86
+ const sameWorkingTree = (name) => {
87
+ const t = name == null ? null : name;
88
+ return t === incomingTree;
89
+ };
90
+ // Defence in depth: only count claims that belong to THIS session, are genuinely
91
+ // session-bound (a NULL-session row must never trip the block), AND would land in
92
+ // the SAME working tree as the incoming claim.
75
93
  const inFlight = claims.find(
76
94
  (c) => c && c.creator_session_id != null && c.creator_session_id === creatorSessionId
95
+ && sameWorkingTree(c.worktree_name ?? null)
77
96
  );
78
97
  if (inFlight) {
79
98
  return {
@@ -100,18 +119,23 @@ async function gateOneActiveClaimPerSession(req, res, next) {
100
119
  // never do a needless lookup and never block a web claim.
101
120
  const creatorSessionId = resolveCreatorSessionId(req.body?.bind_session, req.gdsSession?.session_id);
102
121
  if (!creatorSessionId) return next();
122
+ // The incoming claim's working tree — the hazard boundary the guard keys on. A
123
+ // second claim in a DIFFERENT worktree is safe (different branch); only a second
124
+ // claim into the SAME tree (or off one worktree-less clone) commingles.
125
+ const worktreeName = req.body?.worktree_name ?? null;
103
126
  let activeClaimsForSession;
104
127
  try {
105
128
  activeClaimsForSession = await db.getActiveClaimsForSession(creatorSessionId);
106
129
  } catch (err) {
107
130
  // FAIL-CLOSED. A lookup failure must not let a commingling second claim land.
108
131
  console.error('[gds] gateOneActiveClaimPerSession lookup failed (failing closed):', err);
109
- return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this session has no other claim in flight. Try again, or start a fresh session/worktree for this task.' });
132
+ return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this worktree has no other claim in flight. Try again, or start a fresh worktree for this task.' });
110
133
  }
111
- const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId });
134
+ const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName });
112
135
  if (decision.refuse) {
113
136
  const n = decision.inFlightTaskId;
114
- return res.fail('session_already_has_active_claim', { status: 409, message: `This session already has task ${n} in flight — ship or release it first, or start a new session/worktree for this one.`, details: { active_task_id: n, active_task_title: decision.inFlightTitle, what_to_do: `Run /builder-ship ${n} (or /builder-release ${n}) before claiming new work, or open a fresh worktree/session for this task.` } });
137
+ const where = worktreeName ? `worktree "${worktreeName}"` : 'this worktree';
138
+ return res.fail('session_already_has_active_claim', { status: 409, message: `${where} already has task ${n} in flight — ship or release it first, or claim this one from a different worktree.`, details: { active_task_id: n, active_task_title: decision.inFlightTitle, worktree_name: worktreeName, what_to_do: `Run /builder-ship ${n} (or /builder-release ${n}) before claiming new work in ${where}, or open a separate worktree for this task (each worktree may hold one claim).` } });
115
139
  }
116
140
  next();
117
141
  }
@@ -206,7 +230,18 @@ module.exports = function buildClaimsRouter() {
206
230
  // silently disambiguated it (e.g. "foo" → "foo-2") and the claim still
207
231
  // succeeded — so the caller must read claim.worktree_name back, not assume
208
232
  // it got the name it asked for.
209
- res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null });
233
+ // ADR 0146 / task 1002469: ride the instance's resolved reward policy along
234
+ // with the claim so the CLI banner can label `task.credits_reward` honestly
235
+ // in the same breath it advertises it. A cost-plus-only instance never pays
236
+ // that estimate, and the claim banner is where the expectation forms.
237
+ // Resolved by economy (never re-derived); null when economy is disabled.
238
+ let rewardMode = null;
239
+ try {
240
+ const rw = seams.resolveOptional('reward');
241
+ const p = rw && typeof rw.rewardConfig === 'function' ? rw.rewardConfig() : null;
242
+ if (p && typeof p.mode === 'string') rewardMode = { mode: p.mode, perTaskCredit: p.perTaskCredit !== false };
243
+ } catch (_) { /* display-only: a claim must never fail over this */ }
244
+ res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, reward: rewardMode });
210
245
  } catch (err) {
211
246
  if (err && err.code) {
212
247
  const status =
@@ -215,6 +250,10 @@ module.exports = function buildClaimsRouter() {
215
250
  err.code === 'TOUCHES_CONFLICT' ? 409 :
216
251
  err.code === 'MODULE_CONFLICT' ? 409 :
217
252
  err.code === 'DEPS_NOT_SHIPPED' ? 409 :
253
+ // ADR 0120 part 3: the builder owes a rebase on an un-landed confirmed
254
+ // task (reconciler-flagged needs_rebase); NEW claims are gated until
255
+ // they land or release it. Existing claims are unaffected.
256
+ err.code === 'REBASE_REQUIRED' ? 409 :
218
257
  // task 1670 / ADR 0096: an unrewarded (0/NULL credits_reward) task can't
219
258
  // be claimed — the claim-time backstop to the promote gate. Grandfathered
220
259
  // tasks (already 'ready' at migration 177) are exempt and never reach here.
@@ -236,6 +275,12 @@ module.exports = function buildClaimsRouter() {
236
275
  if (err.code === 'TASK_NOT_READY') {
237
276
  err.hint = taskNotReadyHint(taskId, err.currentStatus);
238
277
  }
278
+ // ADR 0120 part 3: name the offending task(s) + the two ways to clear the
279
+ // gate, so the builder isn't left guessing why their claim was refused.
280
+ if (err.code === 'REBASE_REQUIRED') {
281
+ const ids = (err.rebaseTasks || []).map((t) => `task ${t.id}`);
282
+ err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
283
+ }
239
284
  return res.fail(err.code, status, { ...err });
240
285
  }
241
286
  console.error('[gds] POST /claims', err);