@bongos/core 1.17.0

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 (1142) hide show
  1. package/.bongos-core.json +5727 -0
  2. package/.claude/skills/blocker-review/SKILL.md +91 -0
  3. package/.claude/skills/blocker-solve/SKILL.md +96 -0
  4. package/.claude/skills/builder-backup/SKILL.md +54 -0
  5. package/.claude/skills/builder-box/SKILL.md +45 -0
  6. package/.claude/skills/builder-claim/SKILL.md +76 -0
  7. package/.claude/skills/builder-connect/SKILL.md +59 -0
  8. package/.claude/skills/builder-cost/SKILL.md +47 -0
  9. package/.claude/skills/builder-end/SKILL.md +81 -0
  10. package/.claude/skills/builder-exit/SKILL.md +88 -0
  11. package/.claude/skills/builder-key/SKILL.md +40 -0
  12. package/.claude/skills/builder-reauth/SKILL.md +50 -0
  13. package/.claude/skills/builder-redteam/SKILL.md +109 -0
  14. package/.claude/skills/builder-release/SKILL.md +41 -0
  15. package/.claude/skills/builder-setup/SKILL.md +62 -0
  16. package/.claude/skills/builder-ship/SKILL.md +79 -0
  17. package/.claude/skills/builder-stage/SKILL.md +51 -0
  18. package/.claude/skills/builder-start/SKILL.md +46 -0
  19. package/.claude/skills/close-box/SKILL.md +43 -0
  20. package/.claude/skills/design-sync/SKILL.md +69 -0
  21. package/.claude/skills/dev/SKILL.md +49 -0
  22. package/.claude/skills/feedback/SKILL.md +32 -0
  23. package/.claude/skills/figma-design-sync/SKILL.md +74 -0
  24. package/.claude/skills/goal-review/SKILL.md +89 -0
  25. package/.claude/skills/idea-triage/SKILL.md +144 -0
  26. package/.claude/skills/ideate/SKILL.md +82 -0
  27. package/.claude/skills/merge-mode/SKILL.md +169 -0
  28. package/.claude/skills/new-project/SKILL.md +108 -0
  29. package/.claude/skills/otb-character-review/SKILL.md +71 -0
  30. package/.claude/skills/otb-design-review/SKILL.md +47 -0
  31. package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
  32. package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
  33. package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
  34. package/.claude/skills/paint/SKILL.md +70 -0
  35. package/.claude/skills/planning-session/SKILL.md +299 -0
  36. package/.claude/skills/priority-session/SKILL.md +134 -0
  37. package/.claude/skills/read-session-export/SKILL.md +58 -0
  38. package/.claude/skills/recall/SKILL.md +52 -0
  39. package/.claude/skills/session-handoff/SKILL.md +46 -0
  40. package/.claude/skills/status/SKILL.md +33 -0
  41. package/.devcontainer/Dockerfile +69 -0
  42. package/.devcontainer/README.md +73 -0
  43. package/.devcontainer/devcontainer-lock.json +19 -0
  44. package/.devcontainer/devcontainer.json +93 -0
  45. package/.devcontainer/healthcheck.sh +84 -0
  46. package/.devcontainer/init-firewall.sh +198 -0
  47. package/.devcontainer/post-create.sh +153 -0
  48. package/.dockerignore +36 -0
  49. package/.env.local.example +32 -0
  50. package/.gitattributes +83 -0
  51. package/.gitignore +91 -0
  52. package/CLAUDE.md +242 -0
  53. package/CODE_OF_CONDUCT.md +46 -0
  54. package/CONTRIBUTING.md +51 -0
  55. package/Dockerfile +46 -0
  56. package/GOVERNANCE.md +44 -0
  57. package/LICENSE +661 -0
  58. package/README.md +88 -0
  59. package/bin/bongos.js +222 -0
  60. package/clients/bongos-client/README.md +48 -0
  61. package/clients/bongos-client/bongos-client.global.js +617 -0
  62. package/clients/bongos-client/examples/hello-world.mjs +62 -0
  63. package/clients/bongos-client/index.cjs +619 -0
  64. package/clients/bongos-client/index.d.ts +793 -0
  65. package/clients/bongos-client/index.mjs +614 -0
  66. package/clients/bongos-client/package.json +32 -0
  67. package/config/branding.neutral.json +60 -0
  68. package/config/design-tokens.neutral.json +51 -0
  69. package/config/design-tokens.schema.json +96 -0
  70. package/config/modules.neutral.json +9 -0
  71. package/docker-compose.yml +52 -0
  72. package/docker-entrypoint.sh +30 -0
  73. package/docs/adr/0001-stack-choice-single-vps.md +45 -0
  74. package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
  75. package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
  76. package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
  77. package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
  78. package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
  79. package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
  80. package/docs/adr/0008-family-based-tile-generation.md +86 -0
  81. package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
  82. package/docs/adr/0009-autotile-architecture.md +181 -0
  83. package/docs/adr/0010-pms-architecture.md +70 -0
  84. package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
  85. package/docs/adr/0012-ground-object-cell-split.md +208 -0
  86. package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
  87. package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
  88. package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
  89. package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
  90. package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
  91. package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
  92. package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
  93. package/docs/adr/0020-security-model-public-stub.md +70 -0
  94. package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
  95. package/docs/adr/0022-secrets-policy.md +124 -0
  96. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
  97. package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
  98. package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
  99. package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
  100. package/docs/adr/0025-structured-criterion-task-link.md +44 -0
  101. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
  102. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
  103. package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
  104. package/docs/adr/0029-status-page-outage-resilience.md +59 -0
  105. package/docs/adr/0030-builders-hall-live-updates.md +59 -0
  106. package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
  107. package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
  108. package/docs/adr/0033-discord-bot-service-principal.md +82 -0
  109. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
  110. package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
  111. package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
  112. package/docs/adr/0037-discord-channel-management.md +51 -0
  113. package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
  114. package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
  115. package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
  116. package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
  117. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
  118. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
  119. package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
  120. package/docs/adr/0044-per-box-live-game-preview.md +57 -0
  121. package/docs/adr/0045-devbox-desktop-app.md +133 -0
  122. package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
  123. package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
  124. package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
  125. package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
  126. package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
  127. package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
  128. package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
  129. package/docs/adr/0053-scoped-dev-box-session.md +55 -0
  130. package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
  131. package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
  132. package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
  133. package/docs/adr/0057-container-cost-ledger.md +28 -0
  134. package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
  135. package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
  136. package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
  137. package/docs/adr/0060-gds-retrieval-layer.md +97 -0
  138. package/docs/adr/0061-context-layer-decomposition.md +61 -0
  139. package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
  140. package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
  141. package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
  142. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
  143. package/docs/adr/0066-generated-file-map-sections.md +51 -0
  144. package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
  145. package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
  146. package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
  147. package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
  148. package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
  149. package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
  150. package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
  151. package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
  152. package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
  153. package/docs/adr/0074-memory-map-stays-light.md +32 -0
  154. package/docs/adr/0075-per-system-cost-attribution.md +37 -0
  155. package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
  156. package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
  157. package/docs/adr/0078-deterministic-first-cron.md +48 -0
  158. package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
  159. package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
  160. package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
  161. package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
  162. package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
  163. package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
  164. package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
  165. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
  166. package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
  167. package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
  168. package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
  169. package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
  170. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
  171. package/docs/adr/0090-metic-task-authoring.md +73 -0
  172. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
  173. package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
  174. package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
  175. package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
  176. package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
  177. package/docs/adr/0095-cross-agent-context-management.md +58 -0
  178. package/docs/adr/0096-require-reward-before-workable.md +209 -0
  179. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
  180. package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
  181. package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
  182. package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
  183. package/docs/adr/0100-two-repo-instance-model.md +60 -0
  184. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
  185. package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
  186. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
  187. package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
  188. package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
  189. package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
  190. package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
  191. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
  192. package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
  193. package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
  194. package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
  195. package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
  196. package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
  197. package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
  198. package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
  199. package/docs/adr/0115-scheduled-routines-portability.md +31 -0
  200. package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
  201. package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
  202. package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
  203. package/docs/adr/0119-api-pagination-contract.md +34 -0
  204. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
  205. package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
  206. package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
  207. package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
  208. package/docs/adr/0124-typed-api-response-bodies.md +37 -0
  209. package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
  210. package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
  211. package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
  212. package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
  213. package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
  214. package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
  215. package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
  216. package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
  217. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
  218. package/docs/adr/0134-private-first-npm-distribution.md +38 -0
  219. package/docs/adr/README.md +208 -0
  220. package/docs/api/openapi.json +13720 -0
  221. package/docs/api-reference.md +437 -0
  222. package/docs/architecture.md +244 -0
  223. package/docs/branding-contract.md +66 -0
  224. package/docs/branding-fill-prompt.md +25 -0
  225. package/docs/canonical-permissions.md +241 -0
  226. package/docs/design/container-exterior-tiles.md +49 -0
  227. package/docs/design/container-exterior.md +81 -0
  228. package/docs/design/container-interior.md +77 -0
  229. package/docs/design/modular-architecture/00-research-report.md +107 -0
  230. package/docs/design/modular-architecture/01-architecture.md +125 -0
  231. package/docs/design/modular-architecture/02-module-map.md +116 -0
  232. package/docs/design/modular-architecture/03-builder-flows.md +104 -0
  233. package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
  234. package/docs/design/modular-architecture/README.md +27 -0
  235. package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
  236. package/docs/design/world-map-scroll.md +41 -0
  237. package/docs/design-contract.md +320 -0
  238. package/docs/file-map.md +418 -0
  239. package/docs/handoff-template.md +92 -0
  240. package/docs/modules-contract.md +293 -0
  241. package/docs/onboarding/browser-terminal-guide.md +73 -0
  242. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
  243. package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
  244. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
  245. package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
  246. package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
  247. package/docs/onboarding/diagrams/README.md +94 -0
  248. package/docs/onboarding/diagrams/assertions.json +170 -0
  249. package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
  250. package/docs/onboarding/diagrams/render-config.json +9 -0
  251. package/docs/onboarding/drachmae-and-karma.md +178 -0
  252. package/docs/onboarding/primer.md +202 -0
  253. package/docs/project-context.template.md +67 -0
  254. package/docs/recipes/README.md +35 -0
  255. package/docs/recipes/audit-replay.md +100 -0
  256. package/docs/recipes/bongos-cli-release.md +66 -0
  257. package/docs/recipes/builder-box-lifecycle.md +152 -0
  258. package/docs/recipes/claude-design-loop.md +143 -0
  259. package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
  260. package/docs/recipes/cloudbongos-staging-gate.md +139 -0
  261. package/docs/recipes/cloudbongos-standup.md +111 -0
  262. package/docs/recipes/devbox-app-release.md +93 -0
  263. package/docs/recipes/devbox-app-windows-test.md +128 -0
  264. package/docs/recipes/failed-ship-recovery-proof.md +102 -0
  265. package/docs/recipes/figma-design-loop.md +129 -0
  266. package/docs/recipes/gds-db-backup.md +255 -0
  267. package/docs/recipes/instance-deploy.md +37 -0
  268. package/docs/recipes/managed-settings-remote-control.md +416 -0
  269. package/docs/recipes/memory-backup.md +71 -0
  270. package/docs/recipes/memory-sync-conflicts.md +98 -0
  271. package/docs/recipes/multi-builder-merge.md +78 -0
  272. package/docs/recipes/ops-gotchas.md +212 -0
  273. package/docs/recipes/overnight-builder.md +53 -0
  274. package/docs/recipes/packaging-the-core.md +49 -0
  275. package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
  276. package/docs/recipes/parallel-load-test.md +32 -0
  277. package/docs/recipes/pixel-art-pipeline.md +411 -0
  278. package/docs/recipes/private-npm-distribution.md +79 -0
  279. package/docs/recipes/search-index-ingestion.md +67 -0
  280. package/docs/recipes/self-host.md +60 -0
  281. package/docs/recipes/ship-and-regrade.md +72 -0
  282. package/docs/recipes/staging-data.md +50 -0
  283. package/docs/recipes/staging-environment.md +215 -0
  284. package/docs/recipes/upgrading-the-core.md +58 -0
  285. package/docs/research/cursor-competitive-analysis.md +255 -0
  286. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
  287. package/migrations/003_pms.sql +284 -0
  288. package/migrations/004_pms_cost_idempotency.sql +13 -0
  289. package/migrations/005_pms_smoke_filter.sql +44 -0
  290. package/migrations/006_pms_v2.sql +378 -0
  291. package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
  292. package/migrations/008_pms_v2_migration_allocator.sql +27 -0
  293. package/migrations/009_pms_v2_blockers_table.sql +68 -0
  294. package/migrations/010_uptime.sql +29 -0
  295. package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
  296. package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
  297. package/migrations/013_claim_locking.sql +22 -0
  298. package/migrations/014_pms_task_discipline.sql +86 -0
  299. package/migrations/015_pms_estimation_calibration.sql +140 -0
  300. package/migrations/016_pms_task_grades.sql +60 -0
  301. package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
  302. package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
  303. package/migrations/020_task_dependencies.sql +99 -0
  304. package/migrations/021_v3_renumber_and_deps.sql +224 -0
  305. package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
  306. package/migrations/023_pms_rank.sql +30 -0
  307. package/migrations/025_builder_exit_status.sql +15 -0
  308. package/migrations/027_builders_preferred_disciplines.sql +46 -0
  309. package/migrations/028_tasks_parallel_safe.sql +71 -0
  310. package/migrations/029_rank_three_live.sql +32 -0
  311. package/migrations/030_vulnerability_reports.sql +61 -0
  312. package/migrations/031_reward_simplification.sql +57 -0
  313. package/migrations/032_repo_health_snapshots.sql +56 -0
  314. package/migrations/033_audit_log.sql +64 -0
  315. package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
  316. package/migrations/035_auth_token_hygiene.sql +66 -0
  317. package/migrations/037_builder_skill_model_prefs.sql +44 -0
  318. package/migrations/038_task_override_requests.sql +51 -0
  319. package/migrations/039_newcomer_friendly_column.sql +124 -0
  320. package/migrations/040_builder_karma.sql +58 -0
  321. package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
  322. package/migrations/042_criterion_proposal_workflow.sql +78 -0
  323. package/migrations/043_builder_onboarding_observability.sql +66 -0
  324. package/migrations/044_tasks_requires_rank.sql +97 -0
  325. package/migrations/045_builder_onboarding_state.sql +147 -0
  326. package/migrations/046_cost_log_builder_attribution.sql +42 -0
  327. package/migrations/054_builder_memory.sql +108 -0
  328. package/migrations/055_task_criteria.sql +90 -0
  329. package/migrations/058_peer_votes.sql +41 -0
  330. package/migrations/059_onboarding_pick_disciplines.sql +59 -0
  331. package/migrations/060_access_requests.sql +46 -0
  332. package/migrations/061_backfill_override_ship_attribution.sql +29 -0
  333. package/migrations/062_session_records.sql +34 -0
  334. package/migrations/063_session_records_tool_errors_idx.sql +11 -0
  335. package/migrations/064_grade_attempts.sql +18 -0
  336. package/migrations/065_bfg_principal.sql +58 -0
  337. package/migrations/066_live_notify_triggers.sql +69 -0
  338. package/migrations/067_builder_boxes.sql +117 -0
  339. package/migrations/067_discord_bot_foundation.sql +69 -0
  340. package/migrations/068_thetes_requires_rank.sql +27 -0
  341. package/migrations/069_builder_ssh_keys.sql +66 -0
  342. package/migrations/070_box_intents.sql +101 -0
  343. package/migrations/071_box_first_provision_approval.sql +46 -0
  344. package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
  345. package/migrations/073_box_terminal_access.sql +46 -0
  346. package/migrations/075_builder_sound_prefs.sql +28 -0
  347. package/migrations/078_box_claude_active.sql +22 -0
  348. package/migrations/079_box_first_connected_at.sql +20 -0
  349. package/migrations/081_tasks_needs_migration.sql +34 -0
  350. package/migrations/084_tasks_security_sensitive.sql +24 -0
  351. package/migrations/085_security_engagement_docs.sql +34 -0
  352. package/migrations/086_rename_disciplines.sql +125 -0
  353. package/migrations/088_builders_anthropic_link.sql +28 -0
  354. package/migrations/089_builders_monthly_budget.sql +19 -0
  355. package/migrations/090_box_scoped_sessions.sql +38 -0
  356. package/migrations/091_session_token_reward.sql +44 -0
  357. package/migrations/093_tasks_sandbox_review.sql +22 -0
  358. package/migrations/102_remove_box_first_provision_approval.sql +58 -0
  359. package/migrations/103_search_chunks.sql +90 -0
  360. package/migrations/104_builder_box_blocked.sql +34 -0
  361. package/migrations/105_box_host_keys.sql +35 -0
  362. package/migrations/111_medusa_customer0_framing.sql +35 -0
  363. package/migrations/112_drop_track_check.sql +25 -0
  364. package/migrations/113_merge_lease.sql +16 -0
  365. package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
  366. package/migrations/118_currency_label_decoupling.sql +23 -0
  367. package/migrations/119_builder_behavior_prefs.sql +31 -0
  368. package/migrations/121_box_code_version.sql +37 -0
  369. package/migrations/122_builder_gemini_key.sql +30 -0
  370. package/migrations/123_cost_log_tiers.sql +33 -0
  371. package/migrations/124_cost_baseline.sql +45 -0
  372. package/migrations/125_llm_cache.sql +54 -0
  373. package/migrations/126_autonomy_runs.sql +33 -0
  374. package/migrations/143_cost_calibration.sql +63 -0
  375. package/migrations/145_backfill_task_requires_rank.sql +84 -0
  376. package/migrations/160_goal_tier.sql +88 -0
  377. package/migrations/163_polymorphic_dependencies.sql +187 -0
  378. package/migrations/164_event_sound_prefs.sql +20 -0
  379. package/migrations/165_task_module_key.sql +15 -0
  380. package/migrations/171_claims_published_head_sha.sql +24 -0
  381. package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
  382. package/migrations/175_task_grades_diff_hash.sql +9 -0
  383. package/migrations/176_promotion_respects_open_blockers.sql +58 -0
  384. package/migrations/176_ui_discipline.sql +40 -0
  385. package/migrations/177_require_reward_before_workable.sql +76 -0
  386. package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
  387. package/migrations/179_search_chunk_usefulness.sql +33 -0
  388. package/migrations/180_one_active_claim_per_session.sql +54 -0
  389. package/migrations/181_cost_log_skill_name.sql +33 -0
  390. package/migrations/184_claim_release_requested.sql +25 -0
  391. package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
  392. package/migrations/187_goal_sort_order.sql +30 -0
  393. package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
  394. package/migrations/README.md +88 -0
  395. package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
  396. package/migrations/core_189_core_upgrades_ledger.sql +37 -0
  397. package/migrations/core_189_credit_log_task_id_index.sql +19 -0
  398. package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
  399. package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
  400. package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
  401. package/migrations/instance/110_medusa_planning_seed.sql +70 -0
  402. package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
  403. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
  404. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
  405. package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
  406. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
  407. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
  408. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
  409. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
  410. package/modules/autonomy/CLAUDE.md +49 -0
  411. package/modules/autonomy/db.js +50 -0
  412. package/modules/autonomy/module.json +13 -0
  413. package/modules/autonomy/pollers/routine-timers.js +146 -0
  414. package/modules/autonomy/routes/autonomy.js +37 -0
  415. package/modules/builder-settings/CLAUDE.md +58 -0
  416. package/modules/builder-settings/builder-needs.js +115 -0
  417. package/modules/builder-settings/db.js +168 -0
  418. package/modules/builder-settings/display-name.js +84 -0
  419. package/modules/builder-settings/event-sound-prefs.js +169 -0
  420. package/modules/builder-settings/interaction-prefs.js +201 -0
  421. package/modules/builder-settings/model-alloc-prefs.js +147 -0
  422. package/modules/builder-settings/module.json +10 -0
  423. package/modules/builder-settings/render-prefs.js +103 -0
  424. package/modules/builder-settings/routes/settings.js +417 -0
  425. package/modules/builder-settings/settings.js +52 -0
  426. package/modules/builder-settings/skill-prefs.js +196 -0
  427. package/modules/builder-settings/sound-prefs.js +178 -0
  428. package/modules/builder-settings/wandering-prefs.js +282 -0
  429. package/modules/dev-box/CLAUDE.md +38 -0
  430. package/modules/dev-box/box-access.js +275 -0
  431. package/modules/dev-box/box-credential.js +94 -0
  432. package/modules/dev-box/box-onboard.js +486 -0
  433. package/modules/dev-box/boxes.js +842 -0
  434. package/modules/dev-box/db.js +39 -0
  435. package/modules/dev-box/module.json +14 -0
  436. package/modules/dev-box/routes/box.js +977 -0
  437. package/modules/discord/CLAUDE.md +50 -0
  438. package/modules/discord/auth.js +54 -0
  439. package/modules/discord/bug-attachments.js +210 -0
  440. package/modules/discord/db.js +96 -0
  441. package/modules/discord/discord-approvals.js +145 -0
  442. package/modules/discord/discord-audit.js +29 -0
  443. package/modules/discord/discord-bot.js +545 -0
  444. package/modules/discord/discord-bugs.js +257 -0
  445. package/modules/discord/discord-channels.js +412 -0
  446. package/modules/discord/discord-inbound.js +147 -0
  447. package/modules/discord/discord-message-buffer.js +54 -0
  448. package/modules/discord/discord-reaction.js +186 -0
  449. package/modules/discord/discord-role-sync.js +318 -0
  450. package/modules/discord/module.json +14 -0
  451. package/modules/discord/pollers/discord-bot.js +19 -0
  452. package/modules/discord/routes/bug-attachments.js +44 -0
  453. package/modules/discord/routes/discord.js +102 -0
  454. package/modules/discord/ship-broadcast.js +78 -0
  455. package/modules/discord/webhook.js +74 -0
  456. package/modules/economy/CLAUDE.md +48 -0
  457. package/modules/economy/achievements.js +594 -0
  458. package/modules/economy/cost-classify.js +331 -0
  459. package/modules/economy/cost-summary-shape.js +90 -0
  460. package/modules/economy/cost.js +438 -0
  461. package/modules/economy/credits.js +430 -0
  462. package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
  463. package/modules/economy/module.json +10 -0
  464. package/modules/economy/reward.js +61 -0
  465. package/modules/economy/routes/cost.js +97 -0
  466. package/modules/economy/routes/leaderboard.js +26 -0
  467. package/modules/economy/routes/reward.js +42 -0
  468. package/modules/economy/streak.js +285 -0
  469. package/modules/grading/CLAUDE.md +42 -0
  470. package/modules/grading/db.js +39 -0
  471. package/modules/grading/grader-rubric.json +151 -0
  472. package/modules/grading/grader-workers/efficiency.js +213 -0
  473. package/modules/grading/grader-workers/extract-json.js +80 -0
  474. package/modules/grading/grader-workers/fence.js +100 -0
  475. package/modules/grading/grader-workers/hacker.js +239 -0
  476. package/modules/grading/grader-workers/index.js +183 -0
  477. package/modules/grading/grader-workers/narc.js +207 -0
  478. package/modules/grading/grader-workers/quality.js +198 -0
  479. package/modules/grading/grader-workers/rubric.js +20 -0
  480. package/modules/grading/grader-workers/worker-prompt.js +89 -0
  481. package/modules/grading/grader-workers/worker-verdict.js +50 -0
  482. package/modules/grading/grader.js +1790 -0
  483. package/modules/grading/module.json +12 -0
  484. package/modules/grading/routes/grade.js +51 -0
  485. package/modules/hall-ui/CLAUDE.md +61 -0
  486. package/modules/hall-ui/module.json +14 -0
  487. package/modules/hall-ui/public/atlas.html +180 -0
  488. package/modules/hall-ui/public/atlas.js +336 -0
  489. package/modules/hall-ui/public/box-panel.js +423 -0
  490. package/modules/hall-ui/public/brand-apply.js +117 -0
  491. package/modules/hall-ui/public/builders.js +3028 -0
  492. package/modules/hall-ui/public/chime.wav +0 -0
  493. package/modules/hall-ui/public/copy-on-click.js +174 -0
  494. package/modules/hall-ui/public/cursors/arrow.png +0 -0
  495. package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
  496. package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
  497. package/modules/hall-ui/public/cursors/magnifier.png +0 -0
  498. package/modules/hall-ui/public/diagram-viewer.css +125 -0
  499. package/modules/hall-ui/public/diagram-viewer.js +330 -0
  500. package/modules/hall-ui/public/diagrams.html +113 -0
  501. package/modules/hall-ui/public/dom-utils.js +212 -0
  502. package/modules/hall-ui/public/drachmae.html +68 -0
  503. package/modules/hall-ui/public/drachmae.js +136 -0
  504. package/modules/hall-ui/public/gate.html +143 -0
  505. package/modules/hall-ui/public/gate.js +228 -0
  506. package/modules/hall-ui/public/goals-page.js +1297 -0
  507. package/modules/hall-ui/public/goals.html +376 -0
  508. package/modules/hall-ui/public/goals.js +119 -0
  509. package/modules/hall-ui/public/hall-widgets.js +189 -0
  510. package/modules/hall-ui/public/harbor.html +167 -0
  511. package/modules/hall-ui/public/harbor.js +335 -0
  512. package/modules/hall-ui/public/index.html +116 -0
  513. package/modules/hall-ui/public/instances-panel.js +172 -0
  514. package/modules/hall-ui/public/md-reader.js +234 -0
  515. package/modules/hall-ui/public/modules.html +119 -0
  516. package/modules/hall-ui/public/modules.js +154 -0
  517. package/modules/hall-ui/public/new-project-wizard.js +186 -0
  518. package/modules/hall-ui/public/not-ready.html +48 -0
  519. package/modules/hall-ui/public/pair.html +119 -0
  520. package/modules/hall-ui/public/pair.js +132 -0
  521. package/modules/hall-ui/public/primer.css +404 -0
  522. package/modules/hall-ui/public/primer.html +85 -0
  523. package/modules/hall-ui/public/primer.js +641 -0
  524. package/modules/hall-ui/public/profile.html +85 -0
  525. package/modules/hall-ui/public/profile.js +236 -0
  526. package/modules/hall-ui/public/ranks.html +69 -0
  527. package/modules/hall-ui/public/ranks.js +206 -0
  528. package/modules/hall-ui/public/roadmap.html +209 -0
  529. package/modules/hall-ui/public/roadmap.js +375 -0
  530. package/modules/hall-ui/public/sessions.html +92 -0
  531. package/modules/hall-ui/public/sessions.js +222 -0
  532. package/modules/hall-ui/public/settings.html +640 -0
  533. package/modules/hall-ui/public/settings.js +1219 -0
  534. package/modules/hall-ui/public/shell.js +495 -0
  535. package/modules/hall-ui/public/style.css +1748 -0
  536. package/modules/hall-ui/public/task-detail.js +253 -0
  537. package/modules/hall-ui/public/task.html +83 -0
  538. package/modules/hall-ui/public/task.js +326 -0
  539. package/modules/hall-ui/public/watch.html +333 -0
  540. package/modules/hall-ui/public/watch.js +859 -0
  541. package/modules/hall-ui/public/work.html +469 -0
  542. package/modules/hall-ui/public/work.js +1292 -0
  543. package/modules/ideas/CLAUDE.md +37 -0
  544. package/modules/ideas/blockers.js +100 -0
  545. package/modules/ideas/inbox.js +459 -0
  546. package/modules/ideas/module.json +10 -0
  547. package/modules/ideas/routes/blockers.js +120 -0
  548. package/modules/ideas/routes/inbox.js +230 -0
  549. package/modules/lifecycle/CLAUDE.md +63 -0
  550. package/modules/lifecycle/conflict-resolve.js +436 -0
  551. package/modules/lifecycle/db.js +4970 -0
  552. package/modules/lifecycle/done-when.js +355 -0
  553. package/modules/lifecycle/github-push.js +969 -0
  554. package/modules/lifecycle/goal-conflicts.js +133 -0
  555. package/modules/lifecycle/hierarchy.js +104 -0
  556. package/modules/lifecycle/lifecycle.js +58 -0
  557. package/modules/lifecycle/merge-lock.js +261 -0
  558. package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
  559. package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
  560. package/modules/lifecycle/module.json +13 -0
  561. package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
  562. package/modules/lifecycle/publish-reconciler.js +418 -0
  563. package/modules/lifecycle/routes/analytics.js +63 -0
  564. package/modules/lifecycle/routes/claims.js +506 -0
  565. package/modules/lifecycle/routes/dependencies.js +82 -0
  566. package/modules/lifecycle/routes/done-when.js +136 -0
  567. package/modules/lifecycle/routes/gate-approvals.js +197 -0
  568. package/modules/lifecycle/routes/goals.js +1419 -0
  569. package/modules/lifecycle/routes/lifecycle.js +43 -0
  570. package/modules/lifecycle/routes/tasks.js +1703 -0
  571. package/modules/lifecycle/routes/versions.js +128 -0
  572. package/modules/lifecycle/ship-card.js +505 -0
  573. package/modules/lifecycle/ship-preflight.js +225 -0
  574. package/modules/lifecycle/task-classifier.js +512 -0
  575. package/modules/lifecycle/version-close.js +106 -0
  576. package/modules/memory/CLAUDE.md +40 -0
  577. package/modules/memory/learnings.js +275 -0
  578. package/modules/memory/memory.js +572 -0
  579. package/modules/memory/module.json +12 -0
  580. package/modules/memory/routes/learnings.js +90 -0
  581. package/modules/memory/routes/memory.js +916 -0
  582. package/modules/memory/routes/search.js +188 -0
  583. package/modules/memory/search-chunker.js +252 -0
  584. package/modules/memory/search.js +320 -0
  585. package/modules/onboarding/CLAUDE.md +55 -0
  586. package/modules/onboarding/approval-broadcast.js +59 -0
  587. package/modules/onboarding/db.js +54 -0
  588. package/modules/onboarding/module.json +10 -0
  589. package/modules/onboarding/newcomer-restock.js +212 -0
  590. package/modules/onboarding/onboarding-state.js +532 -0
  591. package/modules/onboarding/onboarding.js +508 -0
  592. package/modules/onboarding/port.js +63 -0
  593. package/modules/onboarding/routes/access-requests.js +208 -0
  594. package/modules/onboarding/routes/onboarding.js +54 -0
  595. package/modules/security/CLAUDE.md +44 -0
  596. package/modules/security/module.json +10 -0
  597. package/modules/security/routes/reports.js +251 -0
  598. package/modules/security/secret-scrub.js +201 -0
  599. package/modules/security/security.js +294 -0
  600. package/modules/sessions/CLAUDE.md +39 -0
  601. package/modules/sessions/bfg.js +357 -0
  602. package/modules/sessions/db.js +326 -0
  603. package/modules/sessions/module.json +10 -0
  604. package/modules/sessions/pulse.js +149 -0
  605. package/modules/sessions/routes/sessions.js +331 -0
  606. package/modules/status-ui/CLAUDE.md +47 -0
  607. package/modules/status-ui/module.json +14 -0
  608. package/modules/status-ui/public/README.md +29 -0
  609. package/modules/status-ui/public/cursors/arrow.png +0 -0
  610. package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
  611. package/modules/status-ui/public/index.html +363 -0
  612. package/modules/status-ui/public/snapshots/.gitkeep +0 -0
  613. package/modules/status-ui/public/status.js +1375 -0
  614. package/modules/status-ui/public/style.css +1342 -0
  615. package/package-lock.json +975 -0
  616. package/package.json +28 -0
  617. package/public-docs/index.html +35 -0
  618. package/public-docs/redoc.standalone.js +1838 -0
  619. package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
  620. package/public-landing/buddha.png +0 -0
  621. package/public-landing/create.html +868 -0
  622. package/public-landing/index.html +168 -0
  623. package/scripts/bongos-mcp.js +28 -0
  624. package/scripts/cron/style_guide_audit.sh +134 -0
  625. package/scripts/discord/README.md +52 -0
  626. package/scripts/discord/lib.js +80 -0
  627. package/scripts/discord/post.js +51 -0
  628. package/scripts/gds/CLAUDE.md +144 -0
  629. package/scripts/gds/abandon.js +58 -0
  630. package/scripts/gds/adopt-detect.js +123 -0
  631. package/scripts/gds/adopt-import.js +111 -0
  632. package/scripts/gds/adopt-preflight.js +121 -0
  633. package/scripts/gds/api.js +78 -0
  634. package/scripts/gds/architect-audit.js +288 -0
  635. package/scripts/gds/art-stage.js +152 -0
  636. package/scripts/gds/audit-authorship.sh +243 -0
  637. package/scripts/gds/audit-deps.js +121 -0
  638. package/scripts/gds/audit-exports.js +128 -0
  639. package/scripts/gds/audit-replay.sh +161 -0
  640. package/scripts/gds/audit-ships.js +251 -0
  641. package/scripts/gds/autonomy-gate.js +135 -0
  642. package/scripts/gds/backfill-cost.js +165 -0
  643. package/scripts/gds/backfill-sessions.js +351 -0
  644. package/scripts/gds/backfill-task-rewards.js +408 -0
  645. package/scripts/gds/backup.js +94 -0
  646. package/scripts/gds/bfg-principal-token.js +65 -0
  647. package/scripts/gds/box-auth-check.js +160 -0
  648. package/scripts/gds/box-connect-lib.js +453 -0
  649. package/scripts/gds/box.js +1258 -0
  650. package/scripts/gds/build-bongos.js +94 -0
  651. package/scripts/gds/capture.js +123 -0
  652. package/scripts/gds/cf-tunnel.js +419 -0
  653. package/scripts/gds/ci-grade.js +82 -0
  654. package/scripts/gds/claim-recall.js +112 -0
  655. package/scripts/gds/claim.js +333 -0
  656. package/scripts/gds/classify-discipline.js +153 -0
  657. package/scripts/gds/claude-materialize.js +176 -0
  658. package/scripts/gds/cleanup-orphans.js +74 -0
  659. package/scripts/gds/cli-lib.js +518 -0
  660. package/scripts/gds/code.js +101 -0
  661. package/scripts/gds/codemod-rename-src-gds.js +145 -0
  662. package/scripts/gds/conductor-dispatch.js +426 -0
  663. package/scripts/gds/context-pack.js +228 -0
  664. package/scripts/gds/coreB-carve-proof.sh +716 -0
  665. package/scripts/gds/cost.js +47 -0
  666. package/scripts/gds/db-backup-nightly.sh +160 -0
  667. package/scripts/gds/db-backup-offsite.sh +152 -0
  668. package/scripts/gds/demote.js +40 -0
  669. package/scripts/gds/design-sync.js +62 -0
  670. package/scripts/gds/diagnose-achievements.js +118 -0
  671. package/scripts/gds/diagram-facts.js +319 -0
  672. package/scripts/gds/discipline-modes.json +25 -0
  673. package/scripts/gds/discord-channels.js +66 -0
  674. package/scripts/gds/do-api.js +176 -0
  675. package/scripts/gds/docs-entropy.js +403 -0
  676. package/scripts/gds/doctor.js +389 -0
  677. package/scripts/gds/feedback-latest.js +94 -0
  678. package/scripts/gds/fetch-art-key.js +106 -0
  679. package/scripts/gds/fetch-sound-prefs.js +88 -0
  680. package/scripts/gds/figma-design-sync.js +61 -0
  681. package/scripts/gds/fitness.js +1446 -0
  682. package/scripts/gds/gate-review.js +322 -0
  683. package/scripts/gds/gating.js +325 -0
  684. package/scripts/gds/gds-db-backup.service +24 -0
  685. package/scripts/gds/gds-db-backup.timer +20 -0
  686. package/scripts/gds/gds-db-offsite.service +29 -0
  687. package/scripts/gds/gds-db-offsite.timer +18 -0
  688. package/scripts/gds/gen-api-client.js +508 -0
  689. package/scripts/gds/gen-api-docs.js +1037 -0
  690. package/scripts/gds/gen-atlas.js +366 -0
  691. package/scripts/gds/gen-diagrams.js +608 -0
  692. package/scripts/gds/gen-file-map.js +196 -0
  693. package/scripts/gds/gen-repo-map.js +519 -0
  694. package/scripts/gds/gen-session-index.js +210 -0
  695. package/scripts/gds/git-merge-regen.js +147 -0
  696. package/scripts/gds/grade-smoke.js +205 -0
  697. package/scripts/gds/hooks/pre-push.js +169 -0
  698. package/scripts/gds/http-api-client.js +88 -0
  699. package/scripts/gds/init.js +903 -0
  700. package/scripts/gds/install-git-hooks.js +114 -0
  701. package/scripts/gds/key-card.js +213 -0
  702. package/scripts/gds/key-status.js +126 -0
  703. package/scripts/gds/land-watch.js +168 -0
  704. package/scripts/gds/learning-capture.js +145 -0
  705. package/scripts/gds/lib.sh +44 -0
  706. package/scripts/gds/linkify-refs.js +473 -0
  707. package/scripts/gds/local-preview-lib.js +159 -0
  708. package/scripts/gds/local-preview.js +95 -0
  709. package/scripts/gds/login.js +225 -0
  710. package/scripts/gds/main-audit.js +279 -0
  711. package/scripts/gds/me.js +88 -0
  712. package/scripts/gds/memory-map.js +174 -0
  713. package/scripts/gds/merge-lock.js +71 -0
  714. package/scripts/gds/migration-namespace.js +200 -0
  715. package/scripts/gds/mirror-lineage-check.js +319 -0
  716. package/scripts/gds/mirror-redact.js +305 -0
  717. package/scripts/gds/module.js +482 -0
  718. package/scripts/gds/newcomer-floor.config.json +13 -0
  719. package/scripts/gds/newcomer-floor.js +415 -0
  720. package/scripts/gds/newcomer-floor.templates.json +86 -0
  721. package/scripts/gds/newcomer-floor.test.js +235 -0
  722. package/scripts/gds/next-number.js +163 -0
  723. package/scripts/gds/oauth-secret.js +197 -0
  724. package/scripts/gds/onboard.js +217 -0
  725. package/scripts/gds/onboarding-analyze.js +226 -0
  726. package/scripts/gds/onboarding-config.js +178 -0
  727. package/scripts/gds/onboarding-ship.js +92 -0
  728. package/scripts/gds/package-core.js +684 -0
  729. package/scripts/gds/paste-token.js +131 -0
  730. package/scripts/gds/portability-smoke-art.sh +85 -0
  731. package/scripts/gds/portability-smoke-memory.sh +147 -0
  732. package/scripts/gds/provision.js +1324 -0
  733. package/scripts/gds/publish-manifest.js +307 -0
  734. package/scripts/gds/pulse.js +309 -0
  735. package/scripts/gds/push-main.js +45 -0
  736. package/scripts/gds/read-session-export.js +359 -0
  737. package/scripts/gds/reauth.js +149 -0
  738. package/scripts/gds/recall.js +135 -0
  739. package/scripts/gds/release.js +71 -0
  740. package/scripts/gds/render-ideas.js +173 -0
  741. package/scripts/gds/repo-metrics.js +363 -0
  742. package/scripts/gds/resolve-skill-model.js +57 -0
  743. package/scripts/gds/run-routine.js +197 -0
  744. package/scripts/gds/run-unit-tests.js +178 -0
  745. package/scripts/gds/sandbox-stage.js +653 -0
  746. package/scripts/gds/search-ingest-db.js +209 -0
  747. package/scripts/gds/search-ingest-md.js +225 -0
  748. package/scripts/gds/security-baseline-check.sh +180 -0
  749. package/scripts/gds/seed-adopt-tasks.js +75 -0
  750. package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
  751. package/scripts/gds/seed-bongos-app-tasks.js +294 -0
  752. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
  753. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
  754. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
  755. package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
  756. package/scripts/gds/seed-bongos-tasks.js +214 -0
  757. package/scripts/gds/seed-gds-v4-tasks.js +624 -0
  758. package/scripts/gds/seed-governance-tasks.js +340 -0
  759. package/scripts/gds/seed-medusa-tasks.js +440 -0
  760. package/scripts/gds/seed-module-upstreaming.js +219 -0
  761. package/scripts/gds/seed-productization-tasks.js +235 -0
  762. package/scripts/gds/seed-provisioning-tasks.js +83 -0
  763. package/scripts/gds/seed-staging.sh +104 -0
  764. package/scripts/gds/seed-v2-world-tasks.js +283 -0
  765. package/scripts/gds/seed-v3-tasks.js +1981 -0
  766. package/scripts/gds/session-cost.js +393 -0
  767. package/scripts/gds/session-digest.js +179 -0
  768. package/scripts/gds/session-eval.js +576 -0
  769. package/scripts/gds/session.js +404 -0
  770. package/scripts/gds/setup.js +504 -0
  771. package/scripts/gds/shell.js +100 -0
  772. package/scripts/gds/ship.js +3618 -0
  773. package/scripts/gds/smoke-batch-claim.sh +213 -0
  774. package/scripts/gds/smoke-batch-release.sh +296 -0
  775. package/scripts/gds/smoke-box.sh +150 -0
  776. package/scripts/gds/smoke-claim-consistency.sh +212 -0
  777. package/scripts/gds/smoke-dependencies.sh +84 -0
  778. package/scripts/gds/smoke-gds.js +125 -0
  779. package/scripts/gds/smoke-write.sh +168 -0
  780. package/scripts/gds/start.js +782 -0
  781. package/scripts/gds/status.js +365 -0
  782. package/scripts/gds/triage.js +366 -0
  783. package/scripts/gds/upgrade.js +421 -0
  784. package/scripts/gds/validate-design.js +254 -0
  785. package/scripts/gds/version-close.js +549 -0
  786. package/scripts/migrate.sh +183 -0
  787. package/scripts/migrate_cost_ledger.sh +178 -0
  788. package/scripts/public-mirror-export.js +291 -0
  789. package/scripts/render-diagrams.sh +88 -0
  790. package/scripts/security/dep-audit.js +276 -0
  791. package/scripts/setup-builder.js +274 -0
  792. package/scripts/setup-builder.sh +28 -0
  793. package/scripts/snapshots/backfill-history.mjs +292 -0
  794. package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
  795. package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
  796. package/scripts/snapshots/render-world.mjs +322 -0
  797. package/scripts/status-mirror-sync.js +192 -0
  798. package/scripts/status-probe.mjs +185 -0
  799. package/server.js +212 -0
  800. package/src/asset-version.js +71 -0
  801. package/src/bongos/CLAUDE.md +64 -0
  802. package/src/bongos/api-errors.js +281 -0
  803. package/src/bongos/api-path-404.js +67 -0
  804. package/src/bongos/api-prefix.js +38 -0
  805. package/src/bongos/app-pair.js +172 -0
  806. package/src/bongos/auth.js +979 -0
  807. package/src/bongos/cascade-dispatch.js +123 -0
  808. package/src/bongos/db-kernel.js +252 -0
  809. package/src/bongos/db.js +911 -0
  810. package/src/bongos/goal-scope-check.js +120 -0
  811. package/src/bongos/hierarchy-config.js +189 -0
  812. package/src/bongos/live-channel.js +145 -0
  813. package/src/bongos/llm-cache.js +260 -0
  814. package/src/bongos/llm-pricing.js +220 -0
  815. package/src/bongos/metrics.js +286 -0
  816. package/src/bongos/middleware/audit.js +274 -0
  817. package/src/bongos/middleware/error-envelope.js +93 -0
  818. package/src/bongos/middleware/rate-limit.js +195 -0
  819. package/src/bongos/module-scope-map.js +271 -0
  820. package/src/bongos/optimizer.js +166 -0
  821. package/src/bongos/path-match.js +47 -0
  822. package/src/bongos/permission-path-check.js +248 -0
  823. package/src/bongos/pool.js +55 -0
  824. package/src/bongos/repo-info.js +91 -0
  825. package/src/bongos/route-rank-check.js +420 -0
  826. package/src/bongos/routes/CLAUDE.md +43 -0
  827. package/src/bongos/routes/_helpers.js +369 -0
  828. package/src/bongos/routes/audit-log.js +59 -0
  829. package/src/bongos/routes/auth.js +755 -0
  830. package/src/bongos/routes/backup.js +104 -0
  831. package/src/bongos/routes/builders.js +526 -0
  832. package/src/bongos/routes/healthz.js +16 -0
  833. package/src/bongos/routes/instance.js +129 -0
  834. package/src/bongos/routes/live.js +125 -0
  835. package/src/bongos/routes/llm-cache.js +119 -0
  836. package/src/bongos/routes/me.js +380 -0
  837. package/src/bongos/routes/modules.js +131 -0
  838. package/src/bongos/routes/optimizer.js +54 -0
  839. package/src/bongos/routes/public.js +476 -0
  840. package/src/bongos/routes/security.js +207 -0
  841. package/src/bongos/routes.js +166 -0
  842. package/src/bongos/secret-box.js +177 -0
  843. package/src/bongos/sensitive-surfaces.js +30 -0
  844. package/src/bongos/serve-internal.js +889 -0
  845. package/src/bongos/uptime-poller.js +90 -0
  846. package/src/bongos-downloads.js +70 -0
  847. package/src/branding.js +182 -0
  848. package/src/build-info.js +77 -0
  849. package/src/devbox-downloads.js +67 -0
  850. package/src/instance-config.js +200 -0
  851. package/src/module-api.js +366 -0
  852. package/src/module-loader/loader.js +268 -0
  853. package/src/module-loader/manifest-schema.js +140 -0
  854. package/src/module-loader/provenance.js +28 -0
  855. package/src/module-loader/semver.js +100 -0
  856. package/src/module-seams.js +291 -0
  857. package/src/modules.js +254 -0
  858. package/src/platform-server.js +135 -0
  859. package/src/ui/adapters/claude-design/index.js +333 -0
  860. package/src/ui/adapters/figma/index.js +328 -0
  861. package/tests/abandon_task.mjs +138 -0
  862. package/tests/achievements_reconcile.mjs +167 -0
  863. package/tests/adopt_detect.mjs +141 -0
  864. package/tests/adopt_import.mjs +132 -0
  865. package/tests/adopt_preflight.mjs +105 -0
  866. package/tests/anti_grid.mjs +89 -0
  867. package/tests/api_client.mjs +231 -0
  868. package/tests/api_docs.mjs +283 -0
  869. package/tests/api_error_envelope_buildcheck.mjs +56 -0
  870. package/tests/api_error_envelope_middleware.mjs +99 -0
  871. package/tests/api_error_registry.mjs +108 -0
  872. package/tests/api_pagination_contract.mjs +51 -0
  873. package/tests/api_path_404.mjs +96 -0
  874. package/tests/app_pair.mjs +168 -0
  875. package/tests/architect.mjs +140 -0
  876. package/tests/art_key.mjs +121 -0
  877. package/tests/art_stage.mjs +121 -0
  878. package/tests/asset_version.mjs +94 -0
  879. package/tests/atlas.mjs +69 -0
  880. package/tests/audit_deps.mjs +74 -0
  881. package/tests/audit_middleware.mjs +385 -0
  882. package/tests/auth_admission_gate.mjs +164 -0
  883. package/tests/auth_anthropic_identity.mjs +95 -0
  884. package/tests/auth_duplicate_cookie.mjs +56 -0
  885. package/tests/auth_error_brand.mjs +77 -0
  886. package/tests/auth_flow_slice1.mjs +223 -0
  887. package/tests/auth_inactive_builder.mjs +94 -0
  888. package/tests/auth_page_gate.mjs +92 -0
  889. package/tests/auth_session_ttl.mjs +155 -0
  890. package/tests/autonomous_fit_protected_paths.mjs +115 -0
  891. package/tests/autonomy_gate.mjs +85 -0
  892. package/tests/autotile_variants.mjs +256 -0
  893. package/tests/autotile_xverify.mjs +133 -0
  894. package/tests/backfill_sessions.mjs +101 -0
  895. package/tests/backfill_task_rewards.mjs +152 -0
  896. package/tests/backup_cli.mjs +104 -0
  897. package/tests/bfg.mjs +327 -0
  898. package/tests/bfg_write.mjs +423 -0
  899. package/tests/bongos_box_connect.mjs +89 -0
  900. package/tests/bongos_dispatch.mjs +74 -0
  901. package/tests/bongos_downloads.mjs +118 -0
  902. package/tests/bongos_login.mjs +34 -0
  903. package/tests/box_access.mjs +186 -0
  904. package/tests/box_auth_check.mjs +114 -0
  905. package/tests/box_code_staleness.mjs +116 -0
  906. package/tests/box_connect_e2e.mjs +196 -0
  907. package/tests/box_cost.mjs +70 -0
  908. package/tests/box_dns_repoint.mjs +151 -0
  909. package/tests/box_host_keys.mjs +102 -0
  910. package/tests/box_onboard.mjs +317 -0
  911. package/tests/box_scope_session.mjs +115 -0
  912. package/tests/box_ship_permissions.mjs +71 -0
  913. package/tests/box_terminal_ssrf.mjs +90 -0
  914. package/tests/boxes.mjs +1255 -0
  915. package/tests/branding.mjs +88 -0
  916. package/tests/bug_attachments.mjs +189 -0
  917. package/tests/build_info.mjs +88 -0
  918. package/tests/builder_needs.mjs +99 -0
  919. package/tests/cap_newcomer_tasks.mjs +114 -0
  920. package/tests/cascade-dispatch.mjs +95 -0
  921. package/tests/cf_tunnel.mjs +459 -0
  922. package/tests/claim_recall.mjs +150 -0
  923. package/tests/claims_bind_session.mjs +47 -0
  924. package/tests/claude_materialize.mjs +148 -0
  925. package/tests/cli_box_scope.mjs +52 -0
  926. package/tests/cli_lib_formatters.mjs +87 -0
  927. package/tests/cli_token_reissue.mjs +147 -0
  928. package/tests/client_bundle_route.mjs +47 -0
  929. package/tests/cloud_bongos_pack.mjs +85 -0
  930. package/tests/conductor.mjs +496 -0
  931. package/tests/confirm_attribution.mjs +156 -0
  932. package/tests/conflict_resolve.mjs +462 -0
  933. package/tests/context_pack.mjs +240 -0
  934. package/tests/cost.mjs +94 -0
  935. package/tests/cost_audit.mjs +122 -0
  936. package/tests/cost_budget_status.mjs +78 -0
  937. package/tests/cost_reconcile.mjs +102 -0
  938. package/tests/cost_summary_shape.mjs +94 -0
  939. package/tests/credit_grant.mjs +376 -0
  940. package/tests/criterion_progress.mjs +220 -0
  941. package/tests/criterion_proposal.mjs +109 -0
  942. package/tests/criterion_ref_parse.mjs +53 -0
  943. package/tests/currency_label.mjs +119 -0
  944. package/tests/day_cycle.mjs +197 -0
  945. package/tests/dependencies_migration.mjs +77 -0
  946. package/tests/dependencies_polymorphic.mjs +212 -0
  947. package/tests/deploy_instance.mjs +78 -0
  948. package/tests/devbox_downloads.mjs +122 -0
  949. package/tests/diff_derived_signals.mjs +115 -0
  950. package/tests/discord_approvals.mjs +174 -0
  951. package/tests/discord_autojoin.mjs +158 -0
  952. package/tests/discord_bot.mjs +94 -0
  953. package/tests/discord_bugs.mjs +297 -0
  954. package/tests/discord_channels.mjs +337 -0
  955. package/tests/discord_foundation.mjs +76 -0
  956. package/tests/discord_inbound.mjs +190 -0
  957. package/tests/discord_message_buffer.mjs +87 -0
  958. package/tests/discord_oauth.mjs +55 -0
  959. package/tests/discord_reaction.mjs +170 -0
  960. package/tests/discord_role_sync.mjs +151 -0
  961. package/tests/display_name.mjs +123 -0
  962. package/tests/dns_preflight.mjs +74 -0
  963. package/tests/event_sound_prefs.mjs +194 -0
  964. package/tests/eviction_policy.mjs +279 -0
  965. package/tests/figma_adapter.mjs +199 -0
  966. package/tests/file_map.mjs +91 -0
  967. package/tests/first_admin_bootstrap.mjs +40 -0
  968. package/tests/fitness.mjs +435 -0
  969. package/tests/fitness_two_root.mjs +104 -0
  970. package/tests/fixtures/session-506-digest.json +34 -0
  971. package/tests/gate_approvals.mjs +451 -0
  972. package/tests/gate_review.mjs +152 -0
  973. package/tests/git_merge_regen.mjs +151 -0
  974. package/tests/github_push_ancestry.mjs +119 -0
  975. package/tests/github_push_land_proof.mjs +127 -0
  976. package/tests/github_push_merge_driver.mjs +175 -0
  977. package/tests/goal_abuse_matrix.mjs +187 -0
  978. package/tests/goal_achievement.mjs +146 -0
  979. package/tests/goal_conflicts.mjs +305 -0
  980. package/tests/goal_dependencies_read.mjs +73 -0
  981. package/tests/goal_member_roles.mjs +420 -0
  982. package/tests/goal_membership_requests.mjs +332 -0
  983. package/tests/goal_routes.mjs +203 -0
  984. package/tests/goal_scope_check.mjs +167 -0
  985. package/tests/goal_tier.mjs +312 -0
  986. package/tests/goal_wall_proof.mjs +112 -0
  987. package/tests/grade_attempts.mjs +73 -0
  988. package/tests/grade_attribution.mjs +117 -0
  989. package/tests/grade_diff_hash.mjs +191 -0
  990. package/tests/grade_server_authoritative.mjs +305 -0
  991. package/tests/grade_unavailable_display.mjs +66 -0
  992. package/tests/grader.mjs +1960 -0
  993. package/tests/grader_bypass.mjs +57 -0
  994. package/tests/grader_panel.mjs +252 -0
  995. package/tests/grader_prompt_fencing.mjs +192 -0
  996. package/tests/helpers.mjs +52 -0
  997. package/tests/hierarchy-config.mjs +81 -0
  998. package/tests/init.mjs +463 -0
  999. package/tests/instance_config.mjs +76 -0
  1000. package/tests/instance_manifest.mjs +110 -0
  1001. package/tests/interaction_prefs.mjs +72 -0
  1002. package/tests/key-card.mjs +110 -0
  1003. package/tests/key-status.mjs +45 -0
  1004. package/tests/land_watch.mjs +115 -0
  1005. package/tests/lifecycle-card.mjs +145 -0
  1006. package/tests/lifecycle_breakdown_view.mjs +236 -0
  1007. package/tests/linkify_refs.mjs +264 -0
  1008. package/tests/live_channel.mjs +128 -0
  1009. package/tests/live_sse.mjs +122 -0
  1010. package/tests/llm-cache-route.mjs +110 -0
  1011. package/tests/llm-cache.mjs +140 -0
  1012. package/tests/llm-pricing.mjs +152 -0
  1013. package/tests/local_preview.mjs +136 -0
  1014. package/tests/main_audit.mjs +137 -0
  1015. package/tests/memory_isolation.mjs +431 -0
  1016. package/tests/memory_map.mjs +107 -0
  1017. package/tests/memory_pull.mjs +429 -0
  1018. package/tests/merge_lease.mjs +100 -0
  1019. package/tests/merge_lock.mjs +312 -0
  1020. package/tests/metrics.mjs +147 -0
  1021. package/tests/migration_allocation.mjs +146 -0
  1022. package/tests/migration_namespace.mjs +185 -0
  1023. package/tests/migrations_fresh_replay_guard.mjs +38 -0
  1024. package/tests/mirror_lineage_fulltree.mjs +45 -0
  1025. package/tests/mirror_lineage_proof.mjs +72 -0
  1026. package/tests/mirror_redact.mjs +193 -0
  1027. package/tests/model_alloc_prefs.mjs +63 -0
  1028. package/tests/module-scope-map.mjs +88 -0
  1029. package/tests/module_api.mjs +110 -0
  1030. package/tests/module_cli.mjs +292 -0
  1031. package/tests/module_contributions.mjs +148 -0
  1032. package/tests/module_loader.mjs +286 -0
  1033. package/tests/module_manifest.mjs +194 -0
  1034. package/tests/module_owned_tests.mjs +105 -0
  1035. package/tests/module_provenance.mjs +34 -0
  1036. package/tests/module_route_rank.mjs +147 -0
  1037. package/tests/module_seams.mjs +68 -0
  1038. package/tests/modules.mjs +156 -0
  1039. package/tests/modules_route.mjs +38 -0
  1040. package/tests/newcomer_floor.mjs +17 -0
  1041. package/tests/newcomer_restock.mjs +80 -0
  1042. package/tests/next_number.mjs +154 -0
  1043. package/tests/oauth_secret.mjs +60 -0
  1044. package/tests/onboard.mjs +70 -0
  1045. package/tests/onboarding_gate.mjs +267 -0
  1046. package/tests/onboarding_observability.mjs +344 -0
  1047. package/tests/onboarding_route_signals.mjs +175 -0
  1048. package/tests/onboarding_state.mjs +346 -0
  1049. package/tests/one_active_claim_per_session.mjs +111 -0
  1050. package/tests/orphaned_claim_ship.mjs +135 -0
  1051. package/tests/override_attribution.mjs +142 -0
  1052. package/tests/override_request_ownership.mjs +235 -0
  1053. package/tests/package_core.mjs +435 -0
  1054. package/tests/parallel-load.mjs +402 -0
  1055. package/tests/path_match.mjs +62 -0
  1056. package/tests/permission_path.mjs +151 -0
  1057. package/tests/platform_boot.mjs +167 -0
  1058. package/tests/pre_compact.mjs +60 -0
  1059. package/tests/preview_server.mjs +127 -0
  1060. package/tests/profile_route.mjs +197 -0
  1061. package/tests/provision.mjs +805 -0
  1062. package/tests/provisioning_github_app_routes.mjs +98 -0
  1063. package/tests/public_mirror_export.mjs +124 -0
  1064. package/tests/public_task_resolver.mjs +112 -0
  1065. package/tests/publish_branch_route.mjs +422 -0
  1066. package/tests/publish_manifest.mjs +184 -0
  1067. package/tests/publish_reconciler.mjs +728 -0
  1068. package/tests/rank_gate.mjs +463 -0
  1069. package/tests/rate_limit_public.mjs +217 -0
  1070. package/tests/read_session_export.mjs +89 -0
  1071. package/tests/recall_cli.mjs +82 -0
  1072. package/tests/recall_hints.mjs +119 -0
  1073. package/tests/recent_shipped_window.mjs +80 -0
  1074. package/tests/regrade_eligibility.mjs +202 -0
  1075. package/tests/render_prefs.mjs +122 -0
  1076. package/tests/repo_map.mjs +92 -0
  1077. package/tests/reward_gate.mjs +284 -0
  1078. package/tests/route_rank_check.mjs +243 -0
  1079. package/tests/route_validation.mjs +490 -0
  1080. package/tests/routine_timers.mjs +72 -0
  1081. package/tests/rubric_fixtures.mjs +140 -0
  1082. package/tests/run_routine.mjs +69 -0
  1083. package/tests/sandbox_stage.mjs +582 -0
  1084. package/tests/scrubber_corpus.mjs +257 -0
  1085. package/tests/search_chunker.mjs +209 -0
  1086. package/tests/search_ingest.mjs +96 -0
  1087. package/tests/search_ingest_db.mjs +88 -0
  1088. package/tests/search_isolation.mjs +204 -0
  1089. package/tests/search_route.mjs +95 -0
  1090. package/tests/secret_box.mjs +189 -0
  1091. package/tests/security_docs.mjs +186 -0
  1092. package/tests/security_route.mjs +108 -0
  1093. package/tests/served_paths_shipped.mjs +118 -0
  1094. package/tests/session-cards.mjs +142 -0
  1095. package/tests/session_506_regression.mjs +69 -0
  1096. package/tests/session_caps.mjs +238 -0
  1097. package/tests/session_cost.mjs +133 -0
  1098. package/tests/session_digest.mjs +73 -0
  1099. package/tests/session_eval.mjs +386 -0
  1100. package/tests/session_index.mjs +105 -0
  1101. package/tests/session_records.mjs +369 -0
  1102. package/tests/session_search.mjs +78 -0
  1103. package/tests/session_start_freshness.mjs +123 -0
  1104. package/tests/setup_smoke.mjs +154 -0
  1105. package/tests/ship_autolinkify.mjs +60 -0
  1106. package/tests/ship_automerge_injection.mjs +174 -0
  1107. package/tests/ship_broadcast_routing.mjs +125 -0
  1108. package/tests/ship_broadcast_server.mjs +175 -0
  1109. package/tests/ship_card.mjs +280 -0
  1110. package/tests/ship_ci_deploy.mjs +374 -0
  1111. package/tests/ship_preflight.mjs +368 -0
  1112. package/tests/ship_premerge.mjs +75 -0
  1113. package/tests/ship_resume.mjs +129 -0
  1114. package/tests/ship_upload_skip.mjs +89 -0
  1115. package/tests/ship_zero_commit.mjs +60 -0
  1116. package/tests/single_host_hall.mjs +98 -0
  1117. package/tests/skill_prefs.mjs +150 -0
  1118. package/tests/smoke_preflight.mjs +66 -0
  1119. package/tests/snapshot_picker.mjs +233 -0
  1120. package/tests/sound_prefs.mjs +198 -0
  1121. package/tests/sprite_facing.mjs +35 -0
  1122. package/tests/status-probe.mjs +89 -0
  1123. package/tests/status_backfill.mjs +94 -0
  1124. package/tests/stop_refcheck.mjs +127 -0
  1125. package/tests/streak.mjs +175 -0
  1126. package/tests/subdomain_auth.mjs +58 -0
  1127. package/tests/task_classifier_vocab.mjs +41 -0
  1128. package/tests/task_create_idempotency.mjs +96 -0
  1129. package/tests/task_goal_link.mjs +93 -0
  1130. package/tests/task_ownership_gate.mjs +125 -0
  1131. package/tests/terrain_picker.mjs +298 -0
  1132. package/tests/touches_overlap.mjs +75 -0
  1133. package/tests/triage_development.mjs +140 -0
  1134. package/tests/turn_in_place.mjs +280 -0
  1135. package/tests/upgrade.mjs +357 -0
  1136. package/tests/verify_task_guard.mjs +64 -0
  1137. package/tests/version_close.mjs +76 -0
  1138. package/tests/versions_route.mjs +93 -0
  1139. package/tests/walkability.mjs +188 -0
  1140. package/tests/wandering_prefs.mjs +176 -0
  1141. package/tests/worktree_disambiguation.mjs +105 -0
  1142. package/tests/worktree_guard.mjs +90 -0
@@ -0,0 +1,1297 @@
1
+ // goals-page.js — the dedicated Goals page (task 1734, owner request from the
2
+ // task-1723 review): list every goal, then per-goal detail — completion card,
3
+ // members with lead (manager) roles, work to do vs completed, and a conflicts
4
+ // panel. Hash-routed (#/goal/N) inside one page; the server gate is
5
+ // SIGNEDIN_BUILDERS_PAGE_RE in serve-internal.js.
6
+ //
7
+ // task 1888 — compact detail layout: the conflicts panel lives in the top band
8
+ // (Completion | Members | Conflicts) so a long work list can't bury it, and the
9
+ // two task tables share one Work card behind a To do / Completed tab toggle,
10
+ // each capped at ROW_CAP rows with a show-all pager.
11
+ //
12
+ // Reads: GET /goals, GET /goals/:id (members carry builder identity since task
13
+ // 1734's listGoalMembers enrichment), GET /tasks?goal_id=N, GET /goals/:id/conflicts
14
+ // (feature-detected — the server-side detector is task 1736; until it lands the
15
+ // panel computes a client-side exact-path overlap of declared touches[]),
16
+ // GET /builders/directory (task 1877 — every active builder, for the invite
17
+ // dropdown; supersedes task 1762's GET /leaderboard lookup, which was bounded
18
+ // to the top-100-by-credits window).
19
+ // Writes: POST /goals/:id/join | /leave | /members (task 1762 — owner/manager
20
+ // invite, mirrors the server's authorizeGoalManagement gate; task 1877 picks
21
+ // the invitee from a dropdown instead of typing a GitHub login). Lead role
22
+ // management is task 1735.
23
+ (function () {
24
+ 'use strict';
25
+ const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
26
+ const API = '/api/bongos';
27
+ // Generated typed client (task 1996 / R09), served as window.BongosClient by
28
+ // /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
29
+ // builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
30
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
31
+
32
+ const DONE_STATUSES = new Set(['completed', 'confirmed', 'shipped']);
33
+ // task 1888 — the Work tables and the conflicts list render capped; the pager
34
+ // under each expands to the full set.
35
+ const CONFLICT_CAP = 3;
36
+ // task 1828 — the band's Completion / Members / Conflicts columns each cap
37
+ // their list and offer a "View all →" into a dedicated full view (these grow:
38
+ // more members, more criteria). Conflicts uses CONFLICT_CAP above.
39
+ const MEMBER_CAP = 5;
40
+ const CRITERIA_CAP = 4;
41
+ // task 1932 — the Goals LIST (goal-card grid) is paginated with a "Per page"
42
+ // dropdown. Options + the localStorage key that persists the choice; below
43
+ // LIST_MIN_PER_PAGE the whole grid fits one page so the bar hides.
44
+ const LIST_PER_PAGE_OPTIONS = [10, 25, 50, 100];
45
+ const LIST_MIN_PER_PAGE = LIST_PER_PAGE_OPTIONS[0];
46
+ const GOALS_PER_PAGE_KEY = 'otb.goals.perPage';
47
+ function loadGoalsPerPage() {
48
+ try {
49
+ const v = Number(window.localStorage.getItem(GOALS_PER_PAGE_KEY));
50
+ return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
51
+ } catch (_) { return 25; }
52
+ }
53
+ // task 1956 — the goal-detail Work tables (To do / Completed) paginate like the
54
+ // grid; each reads/persists its own per-page key.
55
+ const WORK_PER_PAGE_KEY = { todo: 'otb.goals.todoPerPage', done: 'otb.goals.donePerPage' };
56
+ function loadPerPageFor(key) {
57
+ try {
58
+ const v = Number(window.localStorage.getItem(key));
59
+ return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
60
+ } catch (_) { return 25; }
61
+ }
62
+ // Terminal "deliberately cut" states — neither work-to-do nor done. Excluded
63
+ // from the todo list and the conflict scan the same way migration 017 keeps
64
+ // them out of version_progress ("abandoned is a terminal state (deliberately
65
+ // cut), not pending work"). Without this, an abandoned task shows up under a
66
+ // goal's "Work to do" as if it were still queued.
67
+ const CUT_STATUSES = new Set(['abandoned', 'dropped']);
68
+ // Genuinely-open work under a goal: not done, not cut → {backlog, ready,
69
+ // active, blocked}.
70
+ const isTodoStatus = (s) => !DONE_STATUSES.has(s) && !CUT_STATUSES.has(s);
71
+ // Record deep-links (#/task|idea|blocker/N) are owned by the shared overlay
72
+ // (task-detail.js). This page must NOT treat them as a route change (Bug 2).
73
+ const DEEPLINK_RE = /^#\/(?:task|idea|blocker)\/\d+$/;
74
+ const state = {
75
+ me: null,
76
+ goals: [], // list rows
77
+ details: new Map(), // goal_id -> {goal, members, criteria}
78
+ current: null, // open goal id (detail/conflicts view) or null (list)
79
+ view: 'list', // task 1828 — 'list' | 'detail' | 'conflicts'
80
+ inviteOpen: false, // task 1762 — the invite form's open/closed state
81
+ directory: null, // task 1877 — GET /builders/directory, fetched once and cached
82
+ workTab: 'todo', // task 1888 — active pane in the Work card ('todo' | 'done')
83
+ // task 1828 — per-table column sort. by=null → natural order (todo: server
84
+ // order; done: shipped-desc, applied at load). dir: 'desc' | 'asc'.
85
+ workSort: { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } },
86
+ work: { todo: [], done: [] }, // task 1888 — the open goal's tasks, split once on load
87
+ workLoaded: false, // task 1828 — tasks fetched? (gates member stats)
88
+ claimsByTask: new Map(), // task 1828 — String(task_id) → {claim_id, builder_id, login} (active claims; powers the Release button)
89
+ conflictPairs: null, // task 1888 — unified pairs (server or client estimate)
90
+ conflictSource: null, // 'server' | 'client' | null (while loading / on error)
91
+ listPager: { page: 1, perPage: loadGoalsPerPage() }, // task 1932 — goal-grid pagination
92
+ // task 1956 — goal-detail Work-table pagination (per table: To do / Completed).
93
+ workPager: {
94
+ todo: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.todo) },
95
+ done: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.done) },
96
+ },
97
+ };
98
+
99
+ async function getJSON(path) {
100
+ const res = await api.request('GET', path);
101
+ if (res.status === 401) {
102
+ window.location.assign(`${API}/auth/web/start?return=${encodeURIComponent(window.location.pathname + window.location.hash)}`);
103
+ throw new Error('redirecting to auth');
104
+ }
105
+ const data = res.data || {};
106
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
107
+ return data;
108
+ }
109
+ async function postJSON(path, body) {
110
+ const res = await api.request('POST', path, { body: body || {} });
111
+ const data = res.data || {};
112
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
113
+ return data;
114
+ }
115
+
116
+ const curLabel = () => window.OTBBranding?.currencyLabel?.() ?? 'credits';
117
+ // task 1957 — a task's drachmae is an ESTIMATE until it SETTLES (confirmed/
118
+ // shipped); after that the ledger figure (credits_awarded) is the real payout.
119
+ // The goal's Done tab groups 'completed' (unsettled, credits not yet landed)
120
+ // alongside confirmed/shipped, so per-row settle-awareness is required. Local
121
+ // wrappers with a defensive fallback for the (hall-wide) brand-apply helpers.
122
+ const taskSettled = (t) => window.OTBBranding?.taskSettled?.(t) ?? (!!t && (t.status === 'confirmed' || t.status === 'shipped'));
123
+ const creditVal = (t) => Number(window.OTBBranding?.taskCreditValue?.(t) ?? t?.credits_reward) || 0;
124
+ const selfId = () => state.me?.builder?.id ?? null;
125
+ const sameId = (a, b) => a != null && b != null && String(a) === String(b);
126
+
127
+ function statusBadge(status) {
128
+ const cls = status === 'achieved' ? 'goal-badge--achieved'
129
+ : (status === 'archived' ? 'goal-badge--archived' : 'goal-badge--open');
130
+ return `<span class="goal-badge ${cls}">${escapeHtml(status)}</span>`;
131
+ }
132
+
133
+ function ring(satisfied, total, achieved, size = 42) {
134
+ const pct = total > 0 ? satisfied / total : (achieved ? 1 : 0);
135
+ const r = 16;
136
+ const circ = 2 * Math.PI * r;
137
+ const dash = (pct * circ).toFixed(1);
138
+ const gap = (circ - pct * circ).toFixed(1);
139
+ const stroke = achieved ? 'var(--ok)' : 'var(--accent)';
140
+ return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
141
+ <circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
142
+ <circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
143
+ <text x="21" y="25" text-anchor="middle" class="goal-ring__label">${satisfied}/${total}</text>
144
+ </svg>`;
145
+ }
146
+
147
+ function memberName(m) {
148
+ return m.display_name || (m.github_login ? `@${m.github_login}` : `builder #${m.builder_id}`);
149
+ }
150
+
151
+ /* ---------------- list view ---------------- */
152
+
153
+ function populateVersionFilter() {
154
+ const sel = $('#f-goal-version');
155
+ if (!sel) return;
156
+ const seen = new Set();
157
+ for (const g of state.goals) {
158
+ if (g.version_id && !seen.has(g.version_id)) {
159
+ seen.add(g.version_id);
160
+ const opt = document.createElement('option');
161
+ opt.value = g.version_id;
162
+ opt.textContent = g.version_id;
163
+ sel.appendChild(opt);
164
+ }
165
+ }
166
+ }
167
+
168
+ // task 1760 — priority is a WITHIN-version ordering, so the drag affordance
169
+ // only makes sense once the grid is narrowed to exactly one version (the
170
+ // "any" filter mixes versions and dragging across them would be meaningless).
171
+ // Priority is always visible in that state; only an Archon gets the handle —
172
+ // the server-side rank gate on POST /goals/reorder is the real enforcement.
173
+ const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
174
+
175
+ // task 1932 — the current filtered goal set, in display order. Shared by
176
+ // renderList (what to page) and the drag-reorder dragend (which must submit
177
+ // the FULL version order, not just the visible page).
178
+ function filteredGoals() {
179
+ const ver = $('#f-goal-version')?.value || '';
180
+ const status = $('#f-goal-status')?.value || '';
181
+ const mineOnly = $('#f-goal-mine')?.value === 'mine';
182
+ const myId = selfId();
183
+ const iAmMember = (g) => {
184
+ const d = state.details.get(String(g.id));
185
+ return d ? (d.members || []).some((m) => sameId(m.builder_id, myId)) : false;
186
+ };
187
+ return state.goals.filter((g) =>
188
+ (!ver || g.version_id === ver) &&
189
+ (!status || g.status === status) &&
190
+ (!mineOnly || iAmMember(g)));
191
+ }
192
+
193
+ // task 1932 — the goal grid's pagination bar (range, Per-page <select>,
194
+ // Prev/Next around "Page X of Y"). Hidden when the whole set fits one page
195
+ // (total <= LIST_MIN_PER_PAGE); otherwise shown so the per-page control stays
196
+ // reachable. Pure render from the slice math renderList computed.
197
+ function renderGoalsPager(total, page, totalPages, start, shownCount) {
198
+ const pager = $('#goals-pager');
199
+ if (!pager) return;
200
+ if (total <= LIST_MIN_PER_PAGE) { pager.hidden = true; return; }
201
+ pager.hidden = false;
202
+ const range = $('#goals-range');
203
+ if (range) range.textContent = total
204
+ ? `Showing ${start + 1}–${start + shownCount} of ${total}`
205
+ : 'Showing 0 of 0';
206
+ const ind = $('#goals-page-ind');
207
+ if (ind) ind.textContent = `Page ${page} of ${totalPages}`;
208
+ const prev = $('#goals-prev');
209
+ if (prev) prev.disabled = page <= 1;
210
+ const next = $('#goals-next');
211
+ if (next) next.disabled = page >= totalPages;
212
+ const sel = $('#goals-per-page');
213
+ if (sel && sel.value !== String(state.listPager.perPage)) sel.value = String(state.listPager.perPage);
214
+ }
215
+
216
+ function renderList() {
217
+ const grid = $('#goal-grid');
218
+ if (!grid) return;
219
+ const ver = $('#f-goal-version')?.value || '';
220
+ // task 1932 — filter via the shared helper (drag-reorder reuses it).
221
+ const rows = filteredGoals();
222
+ const reorderable = !!ver && isArchon();
223
+
224
+ const count = $('#goals-count');
225
+ if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
226
+
227
+ if (!rows.length) {
228
+ grid.innerHTML = '<div class="profile__placeholder">No goals match these filters.</div>';
229
+ state.listPager.page = 1;
230
+ const emptyPager = $('#goals-pager');
231
+ if (emptyPager) emptyPager.hidden = true;
232
+ return;
233
+ }
234
+
235
+ // task 1932 — paginate the grid. Clamp the page (the filtered set may have
236
+ // shrunk under a re-render), slice to the current page, then paint the bar.
237
+ // Priority numbers reflect the GLOBAL position (start + i + 1), not the
238
+ // page-local index, so drag-reorder priorities stay meaningful across pages.
239
+ const total = rows.length;
240
+ const perPage = state.listPager.perPage;
241
+ const totalPages = Math.max(1, Math.ceil(total / perPage));
242
+ if (state.listPager.page > totalPages) state.listPager.page = totalPages;
243
+ if (state.listPager.page < 1) state.listPager.page = 1;
244
+ const start = (state.listPager.page - 1) * perPage;
245
+ const pageRows = rows.slice(start, start + perPage);
246
+ grid.innerHTML = pageRows.map((g, i) => {
247
+ const d = state.details.get(String(g.id));
248
+ const crit = d ? (d.criteria || []) : [];
249
+ const sat = crit.filter((c) => c.satisfied).length;
250
+ const members = d ? (d.members || []) : [];
251
+ const leads = members.filter((m) => m.role === 'lead');
252
+ const joined = members.some((m) => sameId(m.builder_id, selfId()));
253
+ // task 1761: an open goal blocked by a not-yet-achieved prerequisite goal is
254
+ // greyed + flagged; the tooltip names what it waits on.
255
+ const unmetDeps = (d ? (d.dependencies || []) : []).filter((dep) => !dep.satisfied);
256
+ const blocked = g.status === 'open' && unmetDeps.length > 0;
257
+ const blockedPill = blocked
258
+ ? `<span class="goal-badge goal-badge--blocked" title="Waiting on: ${escapeHtml(unmetDeps.map((u) => u.title || ('goal ' + u.id)).join(', '))}">🔒 blocked</span>`
259
+ : '';
260
+ const priority = ver ? `<span class="goal-card__priority" title="Priority within ${escapeHtml(ver)}">${start + i + 1}</span>` : '';
261
+ const handle = reorderable ? `<span class="goal-card__handle" draggable="true" title="Drag to reprioritize" aria-hidden="true">⠿</span>` : '';
262
+ return `<a class="goal-card${blocked ? ' goal-card--blocked' : ''}" href="#/goal/${escapeHtml(String(g.id))}" data-goal-id="${escapeHtml(String(g.id))}"${reorderable ? ' draggable="true"' : ''}>
263
+ <div class="goal-card__head">
264
+ ${handle}${priority}
265
+ ${d ? ring(sat, crit.length, g.status === 'achieved') : ''}
266
+ <span class="goal-card__title">${escapeHtml(g.title || 'Untitled goal')}</span>
267
+ </div>
268
+ <div class="goal-card__meta">
269
+ <span>${escapeHtml(g.version_id || '')}</span>
270
+ ${statusBadge(g.status)}
271
+ ${blockedPill}
272
+ ${joined ? '<span class="role-pill">joined</span>' : ''}
273
+ </div>
274
+ <div class="goal-card__foot">
275
+ <span><strong>${members.length}</strong> member${members.length === 1 ? '' : 's'}</span>
276
+ ${leads.length ? `<span>lead: ${escapeHtml(leads.map(memberName).join(', '))}</span>` : ''}
277
+ </div>
278
+ </a>`;
279
+ }).join('');
280
+ renderGoalsPager(total, state.listPager.page, totalPages, start, pageRows.length);
281
+ }
282
+
283
+ // ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
284
+ let dragEl = null;
285
+
286
+ function wireDragReorder() {
287
+ const grid = $('#goal-grid');
288
+ if (!grid) return;
289
+ grid.addEventListener('dragstart', (e) => {
290
+ const el = e.target.closest('.goal-card');
291
+ if (!el || !el.hasAttribute('draggable')) return;
292
+ dragEl = el;
293
+ el.classList.add('goal-card--dragging');
294
+ e.dataTransfer.effectAllowed = 'move';
295
+ });
296
+ grid.addEventListener('dragover', (e) => {
297
+ if (!dragEl) return;
298
+ const over = e.target.closest('.goal-card');
299
+ if (!over || over === dragEl || !grid.contains(over)) return;
300
+ e.preventDefault();
301
+ const rect = over.getBoundingClientRect();
302
+ const after = (e.clientX - rect.left) > rect.width / 2;
303
+ grid.insertBefore(dragEl, after ? over.nextSibling : over);
304
+ });
305
+ grid.addEventListener('drop', (e) => { if (dragEl) e.preventDefault(); });
306
+ grid.addEventListener('dragend', async () => {
307
+ if (!dragEl) return;
308
+ const el = dragEl;
309
+ el.classList.remove('goal-card--dragging');
310
+ dragEl = null;
311
+ const ver = $('#f-goal-version')?.value || '';
312
+ if (!ver) return;
313
+ // task 1932 — with pagination the grid shows only the current PAGE, so the
314
+ // reorder must submit the FULL version order: the goals before this page +
315
+ // the page's cards in their new DOM order + the goals after it. Without
316
+ // this, POSTing only the visible page's ids would corrupt the priorities
317
+ // of the goals on other pages. Priority numbers reflect the global
318
+ // position (start + i + 1).
319
+ const filtered = filteredGoals();
320
+ const start = (state.listPager.page - 1) * state.listPager.perPage;
321
+ const cards = [...grid.querySelectorAll('.goal-card')];
322
+ cards.forEach((card, i) => {
323
+ const num = card.querySelector('.goal-card__priority');
324
+ if (num) num.textContent = String(start + i + 1);
325
+ });
326
+ const pageIds = cards.map((card) => Number(card.dataset.goalId));
327
+ const beforeIds = filtered.slice(0, start).map((g) => Number(g.id));
328
+ const afterIds = filtered.slice(start + pageIds.length).map((g) => Number(g.id));
329
+ const orderedIds = [...beforeIds, ...pageIds, ...afterIds];
330
+ try {
331
+ await postJSON(`${API}/goals/reorder`, { version_id: ver, ordered_ids: orderedIds });
332
+ toast('Priority order saved.');
333
+ } catch (err) {
334
+ toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
335
+ state.goals = (await getJSON(`${API}/goals`).catch(() => ({ goals: state.goals }))).goals || state.goals;
336
+ renderList();
337
+ }
338
+ });
339
+ }
340
+
341
+ /* ---------------- detail view ---------------- */
342
+
343
+ // task 1828 — the row's inline action, mirroring the Work Board:
344
+ // ready → Claim (POST /claims via doGoalClaim)
345
+ // active + mine → Release (POST /claims/:id/resolve via doGoalRelease)
346
+ // active + someone's → a muted "claimed · @who" (release is owner-only)
347
+ // Signed-out or any other status → empty. The row still opens the overlay on click.
348
+ function actionCell(t) {
349
+ if (!state.me) return '<td class="ledger__action"></td>';
350
+ const id = escapeHtml(String(t.id));
351
+ if (t.status === 'ready') {
352
+ return `<td class="ledger__action"><button class="goal-claim-btn" type="button" data-claim-task="${id}" aria-label="Claim task ${id}">Claim</button></td>`;
353
+ }
354
+ if (t.status === 'active') {
355
+ const c = state.claimsByTask.get(String(t.id));
356
+ if (c && sameId(c.builder_id, selfId())) {
357
+ return `<td class="ledger__action"><button class="goal-release-btn" type="button" data-release-claim="${escapeHtml(String(c.claim_id))}" data-release-task="${id}" aria-label="Release task ${id}">Release</button></td>`;
358
+ }
359
+ const who = c && (c.login || c.name) ? ` · @${escapeHtml(c.login || c.name)}` : '';
360
+ return `<td class="ledger__action"><span class="ledger__claimed" title="Claimed by another builder">claimed${who}</span></td>`;
361
+ }
362
+ return '<td class="ledger__action"></td>';
363
+ }
364
+ function taskRowsTodo(tasks) {
365
+ return tasks.map((t) => `
366
+ <tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
367
+ <td class="ledger__num">#${escapeHtml(String(t.id))}</td>
368
+ <td>${escapeHtml(t.title || '')}</td>
369
+ <td>${escapeHtml(t.kind || '—')}</td>
370
+ <td><span class="task__tag">${escapeHtml(t.status || '')}</span></td>
371
+ <td class="ledger__drachmae">${fmtNum(creditVal(t))}</td>
372
+ ${actionCell(t)}
373
+ </tr>`).join('');
374
+ }
375
+ function taskRowsDone(tasks) {
376
+ return tasks.map((t) => `
377
+ <tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
378
+ <td class="ledger__num">#${escapeHtml(String(t.id))}</td>
379
+ <td>${escapeHtml(t.title || '')}</td>
380
+ <td>${escapeHtml(t.kind || '—')}</td>
381
+ <td class="ledger__drachmae">${fmtNum(creditVal(t))}${taskSettled(t) ? '' : ' <span class="ledger__est" title="Estimate — credits settle when the task is confirmed">est.</span>'}</td>
382
+ <td class="ledger__date">${fmtDate(t.shipped_at)}</td>
383
+ </tr>`).join('');
384
+ }
385
+
386
+ // Client-side conflict estimate: exact/prefix overlap between the declared
387
+ // touches[] of not-yet-done tasks. Deliberately conservative — the glob-aware
388
+ // server-side detector (with claim collisions) is task 1736; when
389
+ // GET /goals/:id/conflicts exists this panel prefers it.
390
+ function pathOverlap(a, b) {
391
+ const norm = (p) => String(p).replace(/\/?\*+$/, '').replace(/\/+$/, '');
392
+ const hits = [];
393
+ for (const pa of a) {
394
+ for (const pb of b) {
395
+ const na = norm(pa); const nb = norm(pb);
396
+ if (!na || !nb) continue;
397
+ if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) {
398
+ hits.push(na.length >= nb.length ? na : nb);
399
+ }
400
+ }
401
+ }
402
+ return [...new Set(hits)];
403
+ }
404
+ function computeConflicts(tasks) {
405
+ const open = tasks.filter((t) => isTodoStatus(t.status) && Array.isArray(t.touches) && t.touches.length);
406
+ const out = [];
407
+ for (let i = 0; i < open.length; i++) {
408
+ for (let j = i + 1; j < open.length; j++) {
409
+ const paths = pathOverlap(open[i].touches, open[j].touches);
410
+ if (paths.length) out.push({ a: open[i], b: open[j], paths });
411
+ }
412
+ }
413
+ return out;
414
+ }
415
+
416
+ // task 1888 — count pill helper (card headers + Work tabs).
417
+ function setCount(sel, n) {
418
+ const el = $(sel);
419
+ if (!el) return;
420
+ el.hidden = false;
421
+ el.textContent = String(n);
422
+ }
423
+
424
+ // task 1956 — a Work table's pagination bar (range, Per-page <select>, Prev/Next
425
+ // around "Page X of Y"), keyed by DOM prefix (goal-todo / goal-done). Hidden
426
+ // when the whole table fits one minimum page.
427
+ function renderWorkPager(prefix, pager, total, totalPages, start, shownCount) {
428
+ const bar = $(`#${prefix}-pager`);
429
+ if (!bar) return;
430
+ if (total <= LIST_MIN_PER_PAGE) { bar.hidden = true; return; }
431
+ bar.hidden = false;
432
+ const range = $(`#${prefix}-range`);
433
+ if (range) range.textContent = total
434
+ ? `Showing ${start + 1}–${start + shownCount} of ${total}`
435
+ : 'Showing 0 of 0';
436
+ const ind = $(`#${prefix}-page-ind`);
437
+ if (ind) ind.textContent = `Page ${pager.page} of ${totalPages}`;
438
+ const prev = $(`#${prefix}-prev`);
439
+ if (prev) prev.disabled = pager.page <= 1;
440
+ const next = $(`#${prefix}-next`);
441
+ if (next) next.disabled = pager.page >= totalPages;
442
+ const sel = $(`#${prefix}-per-page`);
443
+ if (sel && sel.value !== String(pager.perPage)) sel.value = String(pager.perPage);
444
+ }
445
+
446
+ // Slice `rows` to a Work table's current page (clamped), paint its bar, return
447
+ // the slice to render.
448
+ function paginateWork(rows, pager, prefix) {
449
+ const total = rows.length;
450
+ const totalPages = Math.max(1, Math.ceil(total / pager.perPage));
451
+ if (pager.page > totalPages) pager.page = totalPages;
452
+ if (pager.page < 1) pager.page = 1;
453
+ const start = (pager.page - 1) * pager.perPage;
454
+ const slice = rows.slice(start, start + pager.perPage);
455
+ renderWorkPager(prefix, pager, total, totalPages, start, slice.length);
456
+ return slice;
457
+ }
458
+
459
+ // task 1888 — the Work card's To do / Completed tab toggle. Both tables stay
460
+ // in the DOM (already rendered); switching only flips pane visibility.
461
+ function setWorkTab(tab) {
462
+ state.workTab = tab;
463
+ const isTodo = tab === 'todo';
464
+ $('#goal-tab-todo')?.setAttribute('aria-selected', String(isTodo));
465
+ $('#goal-tab-done')?.setAttribute('aria-selected', String(!isTodo));
466
+ const todoPane = $('#goal-todo-pane');
467
+ const donePane = $('#goal-done-pane');
468
+ if (todoPane) todoPane.hidden = !isTodo;
469
+ if (donePane) donePane.hidden = isTodo;
470
+ }
471
+
472
+ // task 1828 — sortable column key → header id, per table. Adding a column
473
+ // means an entry here + a matching <th id> in goals.html.
474
+ const WORK_SORT_HEADERS = {
475
+ todo: { kind: 'goal-todo-sort-kind', status: 'goal-todo-sort-status', credits: 'goal-todo-sort-credits' },
476
+ done: { kind: 'goal-done-sort-kind', credits: 'goal-done-sort-credits', shipped: 'goal-done-sort-shipped' },
477
+ };
478
+
479
+ // Stable sort by the active column; null `by` returns the natural order
480
+ // untouched (todo: server order; done: shipped-desc from load).
481
+ function sortWorkRows(rows, sort) {
482
+ if (!sort || !sort.by) return rows.slice();
483
+ const dir = sort.dir === 'asc' ? 1 : -1;
484
+ const val = (t) => {
485
+ switch (sort.by) {
486
+ case 'kind': return String(t.kind || '').toLowerCase();
487
+ case 'status': return String(t.status || '').toLowerCase();
488
+ case 'credits': return creditVal(t);
489
+ case 'shipped': return new Date(t.shipped_at || 0).getTime();
490
+ default: return 0;
491
+ }
492
+ };
493
+ return rows
494
+ .map((row, idx) => ({ row, idx }))
495
+ .sort((a, b) => {
496
+ const av = val(a.row); const bv = val(b.row);
497
+ const diff = (typeof av === 'number')
498
+ ? (av - bv) * dir
499
+ : String(av).localeCompare(String(bv)) * dir;
500
+ return diff !== 0 ? diff : a.idx - b.idx; // stable tiebreak
501
+ })
502
+ .map(({ row }) => row);
503
+ }
504
+
505
+ // Reflect the active sort on that table's headers (only the active column
506
+ // shows an arrow; the rest reset to "none" — the ARIA-sort contract).
507
+ function reflectSortHeaders(table) {
508
+ const sort = state.workSort[table];
509
+ for (const [key, id] of Object.entries(WORK_SORT_HEADERS[table])) {
510
+ const th = document.getElementById(id);
511
+ if (!th) continue;
512
+ th.setAttribute('aria-sort',
513
+ sort.by === key ? (sort.dir === 'asc' ? 'ascending' : 'descending') : 'none');
514
+ }
515
+ }
516
+
517
+ // Cycle a column: pick → desc → asc → natural (clear). Mirrors the
518
+ // leaderboard's three-click-clears behaviour so there's no dead state.
519
+ function onWorkSortClick(table, key) {
520
+ const sort = state.workSort[table];
521
+ if (sort.by === key) {
522
+ if (sort.dir === 'desc') sort.dir = 'asc';
523
+ else { sort.by = null; sort.dir = 'desc'; }
524
+ } else {
525
+ sort.by = key; sort.dir = 'desc';
526
+ }
527
+ renderWork();
528
+ }
529
+
530
+ function renderWork() {
531
+ const todoBody = document.querySelector('#goal-todo tbody');
532
+ const doneBody = document.querySelector('#goal-done tbody');
533
+ const todo = sortWorkRows(state.work.todo, state.workSort.todo);
534
+ const done = sortWorkRows(state.work.done, state.workSort.done);
535
+ // task 1956 — paginate each table (was ROW_CAP + a show-all toggle).
536
+ const todoSlice = paginateWork(todo, state.workPager.todo, 'goal-todo');
537
+ const doneSlice = paginateWork(done, state.workPager.done, 'goal-done');
538
+ if (todoBody) {
539
+ todoBody.innerHTML = todo.length
540
+ ? taskRowsTodo(todoSlice)
541
+ : `<tr><td colspan="6" class="ledger__empty">Nothing queued under this goal. Members can author bounded tasks here${state.me ? '' : ' once signed in'}.</td></tr>`;
542
+ }
543
+ if (doneBody) {
544
+ doneBody.innerHTML = done.length
545
+ ? taskRowsDone(doneSlice)
546
+ : '<tr><td colspan="5" class="ledger__empty">Nothing completed yet.</td></tr>';
547
+ }
548
+ setCount('#goal-todo-count', state.work.todo.length);
549
+ setCount('#goal-done-count', state.work.done.length);
550
+ reflectSortHeaders('todo');
551
+ reflectSortHeaders('done');
552
+ }
553
+
554
+ // task 1828 — claim a ready task straight from a goal's Work list. Mirrors the
555
+ // Work Board's doClaim (POST /claims, bind_session:false) + its typed-error
556
+ // messages; on success the goal's tasks reload so the row updates in place.
557
+ const pendingClaims = new Set();
558
+ function friendlyClaimError(status, data) {
559
+ switch (data && data.error) {
560
+ case 'ALREADY_CLAIMED': return 'Someone else claimed that task first. Reloading.';
561
+ case 'TASK_NOT_READY': return 'That task is no longer ready to be claimed.';
562
+ case 'TOUCHES_CONFLICT': {
563
+ const titles = Array.isArray(data.conflicts) ? data.conflicts.map((c) => c.title).filter(Boolean) : [];
564
+ return titles.length ? `That task overlaps an active claim: ${titles.join('; ')}.` : 'That task overlaps another active claim.';
565
+ }
566
+ case 'DEPS_NOT_SHIPPED': return 'That task waits on unshipped dependencies.';
567
+ case 'rank_too_low_for_task':
568
+ case 'XENOS_RESTRICTED':
569
+ case 'INSUFFICIENT_RANK': return 'Your rank is not yet high enough for that task.';
570
+ case 'newcomer_must_ship_first': return data.what_to_do || 'Ship your current claim before opening another.';
571
+ case 'NOT_YOUR_CLAIM': return 'That claim is not yours to release.';
572
+ case 'ALREADY_RESOLVED': return 'That claim was already resolved. Reloading.';
573
+ case 'BUILDER_INACTIVE': return 'Your account is inactive. Contact an Archon.';
574
+ default: return `The board could not do that (${status}). Try again.`;
575
+ }
576
+ }
577
+ async function doGoalClaim(taskId, btn) {
578
+ const key = `claim:${taskId}`;
579
+ if (pendingClaims.has(key)) return;
580
+ pendingClaims.add(key);
581
+ if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
582
+ try {
583
+ // bind_session:false — a web claim has no working session (matches the
584
+ // Work Board), so the builder can ship it later from any terminal.
585
+ await postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
586
+ toast(`You claimed #${taskId}. Open a terminal to start work.`);
587
+ if (state.current != null) await loadGoalTasks(state.current);
588
+ } catch (err) {
589
+ if (err && err.message === 'redirecting to auth') return;
590
+ toast(friendlyClaimError(err.status, err.data));
591
+ if (btn) { btn.disabled = false; btn.textContent = 'Claim'; }
592
+ } finally {
593
+ pendingClaims.delete(key);
594
+ }
595
+ }
596
+
597
+ // task 1828 — release an active claim from the goal's Work list. Mirrors the
598
+ // Work Board's doRelease: confirm, POST /claims/:id/resolve {abandoned} (there
599
+ // is no DELETE /claims/:id), owner-scoped server-side (NOT_YOUR_CLAIM). On
600
+ // success the task returns to 'ready' and the row reloads.
601
+ async function doGoalRelease(claimId, taskId, btn) {
602
+ const key = `release:${claimId}`;
603
+ if (pendingClaims.has(key)) return;
604
+ if (!window.confirm(`Release your claim on #${taskId}? It returns to the queue and no ${curLabel()} are earned.`)) return;
605
+ pendingClaims.add(key);
606
+ if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
607
+ try {
608
+ await postJSON(`${API}/claims/${encodeURIComponent(claimId)}/resolve`, { outcome: 'abandoned' });
609
+ toast(`Released #${taskId}. It returns to the queue.`);
610
+ if (state.current != null) await loadGoalTasks(state.current);
611
+ } catch (err) {
612
+ if (err && err.message === 'redirecting to auth') return;
613
+ toast(friendlyClaimError(err.status, err.data));
614
+ if (btn) { btn.disabled = false; btn.textContent = 'Release'; }
615
+ } finally {
616
+ pendingClaims.delete(key);
617
+ }
618
+ }
619
+
620
+ function conflictHtml(c) {
621
+ const sev = c.severity
622
+ ? `<span class="conflict__sev conflict__sev--${escapeHtml(c.severity)}">${escapeHtml(c.severity)}</span>`
623
+ : '';
624
+ const cross = c.crossGoalTitle ? ` <small>(with ${escapeHtml(c.crossGoalTitle)})</small>` : '';
625
+ return `<div class="conflict">
626
+ <div class="conflict__pair">#${escapeHtml(String(c.a))} ${escapeHtml(c.titleA || '')} ↔ #${escapeHtml(String(c.b))} ${escapeHtml(c.titleB || '')}${cross}${sev}</div>
627
+ <div class="conflict__paths">${(c.paths || []).map(escapeHtml).join(' · ')}</div>
628
+ </div>`;
629
+ }
630
+
631
+ // task 1888/1828 — renders from state.conflictPairs (unified server/client
632
+ // shape: {a, b, titleA, titleB, paths, severity, crossGoalTitle}); null means
633
+ // the load failed. Fills a container with the given pairs, or a placeholder.
634
+ function fillConflicts(container, pairs) {
635
+ if (!container) return;
636
+ if (pairs == null) {
637
+ container.innerHTML = '<div class="profile__placeholder">Conflicts could not be computed.</div>';
638
+ return;
639
+ }
640
+ const note = state.conflictSource === 'client'
641
+ ? '<p class="conflict-note">Path-based estimate from declared files. Glob-aware detection with claim collisions — and lead resolution actions (sequencing, rescoping) — light up when the server-side detector lands (tasks 1735/1736).</p>'
642
+ : '';
643
+ container.innerHTML = (pairs.length
644
+ ? pairs.map(conflictHtml).join('')
645
+ : '<div class="profile__placeholder">No conflicts detected among this goal’s open tasks.</div>') + note;
646
+ }
647
+
648
+ const currentDetail = () => (state.current == null ? null : state.details.get(String(state.current)));
649
+
650
+ // task 1828 — a band column shows the first `cap` of a growing list; when
651
+ // there's more, this foot offers "View all N <label> →" into the dedicated
652
+ // full view at #/goal/N/<view> (the shared pattern behind Conflicts, Members,
653
+ // and Completion). Hidden when nothing is truncated.
654
+ function renderViewAllFoot(footSel, total, cap, view, label) {
655
+ const foot = $(footSel);
656
+ if (!foot) return;
657
+ if (total > cap && state.current != null) {
658
+ foot.hidden = false;
659
+ foot.innerHTML = `<a class="pill-link" href="#/goal/${escapeHtml(String(state.current))}/${view}">View all ${total} ${label} →</a>`;
660
+ } else {
661
+ foot.hidden = true;
662
+ foot.innerHTML = '';
663
+ }
664
+ }
665
+
666
+ // task 1828 — the band column shows the first CONFLICT_CAP; the foot routes
667
+ // to the dedicated full-width conflicts view. Count + amber accent on the card.
668
+ function renderConflicts() {
669
+ const pairs = state.conflictPairs;
670
+ const card = $('#goal-conflicts-card');
671
+ fillConflicts($('#goal-conflicts'), pairs == null ? null : pairs.slice(0, CONFLICT_CAP));
672
+ const total = pairs ? pairs.length : 0;
673
+ renderViewAllFoot('#goal-conflicts-foot', total, CONFLICT_CAP, 'conflicts', 'conflicts');
674
+ setCount('#goal-conflicts-count', total);
675
+ card?.classList.toggle('goal-conflicts-card--live', total > 0);
676
+ renderConflictsFull();
677
+ }
678
+
679
+ // task 1828 — the standalone #/goal/N/conflicts view: the whole set, no cap.
680
+ function renderConflictsFull() {
681
+ const pairs = state.conflictPairs;
682
+ const total = pairs ? pairs.length : 0;
683
+ fillConflicts($('#goal-conflicts-full'), pairs);
684
+ setCount('#goal-conflicts-view-count', total);
685
+ $('#goal-conflicts-view-card')?.classList.toggle('goal-conflicts-card--live', total > 0);
686
+ const back = $('#goal-conflicts-back');
687
+ if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
688
+ }
689
+
690
+ /* ----- Completion (band column + #/goal/N/completion full view) ----- */
691
+
692
+ function completionSummaryHtml(g, crit, sat, ringSize) {
693
+ const line = g.status === 'achieved' ? 'Goal achieved — every criterion confirmed.'
694
+ : (g.status === 'archived' ? 'Archived.' : 'Open — satisfied criteria await confirmation in /goal-review.');
695
+ return `<div style="display:flex;align-items:center;gap:16px">
696
+ ${ring(sat, crit.length, g.status === 'achieved', ringSize)}
697
+ <div>
698
+ <div style="font-weight:650">${sat} of ${crit.length} criteria satisfied</div>
699
+ <div style="font-size:12.5px;color:var(--ink-faint)">${line}</div>
700
+ </div>
701
+ </div>`;
702
+ }
703
+ // withDetail shows each criterion's full detail always inline — the extra room
704
+ // is the point of clicking into the dedicated completion view. The capped band
705
+ // (withDetail=false) instead reveals detail via a per-criterion <details> expand
706
+ // (BV1.R79) so the full text is one click away, not hover-only.
707
+ function critListHtml(list, withDetail) {
708
+ if (!list.length) return '<div class="profile__placeholder">No done-when criteria yet.</div>';
709
+ return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
710
+ const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
711
+ const body = withDetail
712
+ ? `<div class="crit__col"><span class="crit__text">${escapeHtml(parts.label || '')}</span>${parts.detail ? `<span class="crit__detail">${escapeHtml(parts.detail)}</span>` : ''}</div>`
713
+ : (parts.detail
714
+ ? `<details class="crit-d"><summary class="crit__text">${escapeHtml(parts.label || '')}</summary><div class="crit__detail">${escapeHtml(parts.detail)}</div></details>`
715
+ : `<span class="crit__text">${escapeHtml(parts.label || '')}</span>`);
716
+ return `<li class="crit${c.satisfied ? ' crit--done' : ''}"><span class="crit__mark">${c.satisfied ? '✓' : ''}</span>${body}</li>`;
717
+ }).join('')}</ul>`;
718
+ }
719
+ function renderCompletion() {
720
+ const d = currentDetail();
721
+ if (!d) return;
722
+ const g = d.goal;
723
+ const crit = d.criteria || [];
724
+ const sat = crit.filter((c) => c.satisfied).length;
725
+ const band = $('#goal-completion-body');
726
+ if (band) band.innerHTML = completionSummaryHtml(g, crit, sat, 56) + critListHtml(crit.slice(0, CRITERIA_CAP), false);
727
+ renderViewAllFoot('#goal-completion-foot', crit.length, CRITERIA_CAP, 'completion', 'criteria');
728
+ setCount('#goal-completion-count', crit.length);
729
+ const full = $('#goal-completion-full');
730
+ if (full) full.innerHTML = completionSummaryHtml(g, crit, sat, 64) + critListHtml(crit, true);
731
+ const back = $('#goal-completion-back');
732
+ if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
733
+ }
734
+
735
+ /* ----- Members (band column + #/goal/N/members full view) ----- */
736
+
737
+ function memberAvatar(m) {
738
+ const name = memberName(m);
739
+ const initials = String(name).replace(/^@/, '').trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase();
740
+ return m.avatar_url ? `<img src="${escapeHtml(m.avatar_url)}" alt="">` : escapeHtml(initials);
741
+ }
742
+ function memberLi(m) {
743
+ return `<li class="member">
744
+ <span class="member__avatar">${memberAvatar(m)}</span>
745
+ <span class="member__name">${escapeHtml(memberName(m))} ${m.rank ? `<small>· ${escapeHtml(rankLabel(m.rank))}</small>` : ''}</span>
746
+ <span class="role-pill${m.role === 'lead' ? ' role-pill--lead' : ''}">${m.role === 'lead' ? 'Manager' : 'Member'}</span>
747
+ </li>`;
748
+ }
749
+
750
+ // task 1828 — per-member contribution TO THIS GOAL, from the goal's shipped
751
+ // tasks (state.work.done carries shipped_by + credits_reward). Returns a Map
752
+ // keyed by String(builder_id) → { shipped, earned }.
753
+ function goalMemberStats() {
754
+ const stats = new Map();
755
+ for (const t of state.work.done) {
756
+ if (t.shipped_by == null) continue;
757
+ const key = String(t.shipped_by);
758
+ const s = stats.get(key) || { shipped: 0, earned: 0 };
759
+ s.shipped += 1;
760
+ // Only SETTLED tasks have really-earned drachmae; a 'completed' (pre-confirm)
761
+ // task in state.work.done has not landed credits yet, so it contributes 0.
762
+ s.earned += taskSettled(t) ? creditVal(t) : 0;
763
+ stats.set(key, s);
764
+ }
765
+ return stats;
766
+ }
767
+ function memberStatLi(m, stats) {
768
+ const s = stats.get(String(m.builder_id)) || { shipped: 0, earned: 0 };
769
+ const cur = curLabel();
770
+ const statLine = state.workLoaded
771
+ ? `<strong>${fmtNum(s.shipped)}</strong> shipped · <strong>${fmtNum(s.earned)}</strong> ${escapeHtml(cur)}`
772
+ : '<span class="member__stats--pending">…</span>';
773
+ return `<li class="member member--stat">
774
+ <span class="member__avatar">${memberAvatar(m)}</span>
775
+ <span class="member__name">${escapeHtml(memberName(m))} ${m.rank ? `<small>· ${escapeHtml(rankLabel(m.rank))}</small>` : ''}</span>
776
+ <span class="member__stats" title="Contribution to this goal">${statLine}</span>
777
+ <span class="role-pill${m.role === 'lead' ? ' role-pill--lead' : ''}">${m.role === 'lead' ? 'Manager' : 'Member'}</span>
778
+ </li>`;
779
+ }
780
+
781
+ function renderMembers() {
782
+ const d = currentDetail();
783
+ if (!d) return;
784
+ const members = d.members || [];
785
+ const lede = $('#goal-members-lede');
786
+ if (lede) {
787
+ const leads = members.filter((m) => m.role === 'lead');
788
+ lede.textContent = leads.length
789
+ ? 'Leads manage this goal’s scope and (soon) resolve its conflicts.'
790
+ : 'No lead assigned yet — an Archon can appoint one.';
791
+ }
792
+ setCount('#goal-members-count', members.length);
793
+ const list = $('#goal-members');
794
+ if (list) {
795
+ list.innerHTML = members.length
796
+ ? members.slice(0, MEMBER_CAP).map(memberLi).join('')
797
+ : '<li class="member"><span class="profile__placeholder">No members yet — be the first to join.</span></li>';
798
+ }
799
+ renderViewAllFoot('#goal-members-foot', members.length, MEMBER_CAP, 'members', 'members');
800
+ renderMembersFull(d);
801
+ }
802
+
803
+ // task 1828 — the standalone #/goal/N/members view: every member plus their
804
+ // contribution to this goal, and a goal-level aggregate header.
805
+ function renderMembersFull(d) {
806
+ const members = d.members || [];
807
+ setCount('#goal-members-view-count', members.length);
808
+ const stats = goalMemberStats();
809
+ const agg = $('#goal-members-stats');
810
+ if (agg) {
811
+ if (state.workLoaded) {
812
+ const shipped = state.work.done.length;
813
+ const earned = state.work.done.reduce((n, t) => n + (taskSettled(t) ? creditVal(t) : 0), 0);
814
+ const open = state.work.todo.length;
815
+ const cur = curLabel();
816
+ agg.innerHTML = `
817
+ <span class="mstat"><strong>${fmtNum(members.length)}</strong> member${members.length === 1 ? '' : 's'}</span>
818
+ <span class="mstat"><strong>${fmtNum(shipped)}</strong> task${shipped === 1 ? '' : 's'} shipped</span>
819
+ <span class="mstat"><strong>${fmtNum(earned)}</strong> ${escapeHtml(cur)} earned</span>
820
+ <span class="mstat"><strong>${fmtNum(open)}</strong> open</span>`;
821
+ } else {
822
+ agg.innerHTML = '<span class="mstat profile__placeholder">Loading this goal’s work…</span>';
823
+ }
824
+ }
825
+ const full = $('#goal-members-full');
826
+ if (full) {
827
+ full.innerHTML = members.length
828
+ ? `<ul class="member-list">${members.map((m) => memberStatLi(m, stats)).join('')}</ul>`
829
+ : '<div class="profile__placeholder">No members yet — be the first to join.</div>';
830
+ }
831
+ const back = $('#goal-members-back');
832
+ if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
833
+ }
834
+
835
+ function renderDetail(goalId) {
836
+ const d = state.details.get(String(goalId));
837
+ if (!d) return;
838
+ const g = d.goal;
839
+ const crit = d.criteria || [];
840
+ const members = d.members || [];
841
+ const sat = crit.filter((c) => c.satisfied).length;
842
+ const joined = members.some((m) => sameId(m.builder_id, selfId()));
843
+ const iAmLead = members.some((m) => sameId(m.builder_id, selfId()) && m.role === 'lead');
844
+
845
+ const h = $('#goal-detail-h');
846
+ if (h) h.textContent = g.title || 'Untitled goal';
847
+ const desc = $('#goal-detail-desc');
848
+ if (desc) desc.textContent = g.description || '';
849
+ const meta = $('#goal-detail-meta');
850
+ if (meta) {
851
+ const scope = Array.isArray(g.scope_modules) && g.scope_modules.length
852
+ ? `<span title="Module scope — tasks authored in this goal are bounded to these modules">scope: ${g.scope_modules.map(escapeHtml).join(', ')}</span>` : '';
853
+ // task 1761: this goal's prerequisite goals, each ✓ (achieved) or ○ (not yet).
854
+ // An open goal with any unmet prerequisite is flagged blocked.
855
+ const deps = d.dependencies || [];
856
+ const unmet = deps.filter((dep) => !dep.satisfied);
857
+ const depsSpan = deps.length
858
+ ? `<span class="goal-deps${(g.status === 'open' && unmet.length) ? ' goal-deps--blocked' : ''}" title="This goal is blocked until each prerequisite goal is achieved">${(g.status === 'open' && unmet.length) ? '🔒 ' : ''}depends on: ${deps.map((dep) => `${escapeHtml(dep.title || ('goal ' + dep.id))} ${dep.satisfied ? '✓' : '○'}`).join(', ')}</span>`
859
+ : '';
860
+ meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${scope} ${depsSpan}`;
861
+ }
862
+
863
+ const actions = $('#goal-detail-actions');
864
+ if (actions) {
865
+ actions.innerHTML = g.status === 'open'
866
+ ? (joined
867
+ ? '<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>'
868
+ : '<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>')
869
+ : '';
870
+ $('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
871
+ $('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
872
+ }
873
+
874
+ // task 1828 — Completion + Members each render into their band column AND
875
+ // their dedicated full view (capped in the band, "View all →" into the view).
876
+ renderCompletion();
877
+ // task 1762: the goal's own owner/manager (or an Archon) can invite without
878
+ // an Archon in the loop — mirrors the server's authorizeGoalManagement gate.
879
+ // A protected-scope goal stays Archon-only on the SERVER (goals.js); this
880
+ // client gate deliberately doesn't re-derive the protected-module list (that
881
+ // roster lives server-side only, module-scope-map.js) — a non-Archon owner/
882
+ // manager sees the control on a protected-scope goal and gets a clear toast
883
+ // if the server 403s (see inviteMember's error handling below).
884
+ const isOwner = sameId(g.created_by, selfId());
885
+ const canInvite = g.status === 'open' && (isArchon() || isOwner || iAmLead);
886
+ renderInviteBox(g.id, canInvite, members);
887
+ renderMembers();
888
+
889
+ loadGoalTasks(g.id);
890
+ void sat; void crit; // now read inside renderCompletion()
891
+ void iAmLead; // lead-only resolution actions arrive with tasks 1735/1736
892
+ }
893
+
894
+ // task 1828 — the active-claim feed (all builders' in-flight claims) powers the
895
+ // Release button + the "claimed · @who" label. Best-effort: the feed is
896
+ // primer-gated server-side, so a 403/error just leaves the map empty (Claim
897
+ // still works; Release simply doesn't show). Keyed by String(task_id).
898
+ async function loadActiveClaims() {
899
+ try {
900
+ const c = await getJSON(`${API}/tasks/claimable`);
901
+ const map = new Map();
902
+ for (const ac of (c.active_claims || [])) {
903
+ map.set(String(ac.task_id), { claim_id: ac.claim_id, builder_id: ac.builder_id, login: ac.github_login, name: ac.display_name });
904
+ }
905
+ state.claimsByTask = map;
906
+ } catch (_err) {
907
+ state.claimsByTask = new Map();
908
+ }
909
+ }
910
+
911
+ async function loadGoalTasks(goalId) {
912
+ try {
913
+ const [resp] = await Promise.all([
914
+ getJSON(`${API}/tasks?goal_id=${encodeURIComponent(goalId)}`),
915
+ loadActiveClaims(), // fills state.claimsByTask (best-effort)
916
+ ]);
917
+ if (!sameId(state.current, goalId)) return; // navigated away while loading
918
+ const tasks = resp.tasks || [];
919
+ state.work = {
920
+ todo: tasks.filter((t) => isTodoStatus(t.status)),
921
+ done: tasks.filter((t) => DONE_STATUSES.has(t.status))
922
+ .sort((a, b) => new Date(b.shipped_at || 0) - new Date(a.shipped_at || 0)),
923
+ };
924
+ state.workLoaded = true; // task 1828 — member stats can now compute
925
+ renderWork();
926
+ renderMembers(); // refresh per-member goal stats now that tasks landed
927
+ // Prefer the server-side detector when it exists (task 1736); any failure
928
+ // (404/501 pre-landing, or a transient error) falls back to the client
929
+ // estimate from declared touches[].
930
+ try {
931
+ const server = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}/conflicts`);
932
+ if (!sameId(state.current, goalId)) return;
933
+ state.conflictPairs = (server.conflicts || []).map((c) => ({
934
+ a: c.task_a, b: c.task_b, titleA: c.title_a || '', titleB: c.title_b || '',
935
+ paths: c.overlapping_paths || [], severity: c.severity || null,
936
+ crossGoalTitle: c.cross_goal ? (c.goal_b_title || '') : '',
937
+ }));
938
+ state.conflictSource = 'server';
939
+ } catch (err) {
940
+ if (!sameId(state.current, goalId)) return;
941
+ state.conflictPairs = computeConflicts(tasks).map((c) => ({
942
+ a: c.a.id, b: c.b.id, titleA: c.a.title || '', titleB: c.b.title || '',
943
+ paths: c.paths, severity: null, crossGoalTitle: '',
944
+ }));
945
+ state.conflictSource = 'client';
946
+ }
947
+ renderConflicts();
948
+ } catch (err) {
949
+ if (err && err.message === 'redirecting to auth') return;
950
+ console.error('[goals] tasks load failed', err);
951
+ if (!sameId(state.current, goalId)) return;
952
+ const todoBody = document.querySelector('#goal-todo tbody');
953
+ const doneBody = document.querySelector('#goal-done tbody');
954
+ if (todoBody) todoBody.innerHTML = '<tr><td colspan="6" class="ledger__empty">Tasks could not be loaded.</td></tr>';
955
+ if (doneBody) doneBody.innerHTML = '<tr><td colspan="5" class="ledger__empty">Tasks could not be loaded.</td></tr>';
956
+ state.conflictPairs = null;
957
+ state.conflictSource = null;
958
+ renderConflicts();
959
+ }
960
+ }
961
+
962
+ // task 1877 — the any-builder-readable directory backing the invite dropdown
963
+ // (superseded task 1762's GET /leaderboard lookup, which missed anyone
964
+ // outside the top-100-by-credits window). Fetched once per page load and
965
+ // cached — the roster doesn't change fast enough to warrant a re-fetch per
966
+ // goal, and loadDetail's force-reload on invite already refreshes members.
967
+ async function loadDirectory() {
968
+ if (state.directory) return state.directory;
969
+ try {
970
+ const d = await getJSON(`${API}/builders/directory`);
971
+ state.directory = d.builders || [];
972
+ } catch (err) {
973
+ state.directory = [];
974
+ }
975
+ return state.directory;
976
+ }
977
+
978
+ // task 1762/1877 — owner/manager invite control. Shows a compact form (a
979
+ // builder dropdown + role) in place of a button while open. Idempotent
980
+ // re-render: always rebuilds the box from scratch, so state.inviteOpen is the
981
+ // only thing that survives a renderDetail() call while the form is open.
982
+ function renderInviteBox(goalId, canInvite, members) {
983
+ const box = $('#goal-invite');
984
+ if (!box) return;
985
+ if (!canInvite) {
986
+ box.hidden = true;
987
+ box.innerHTML = '';
988
+ state.inviteOpen = false;
989
+ return;
990
+ }
991
+ box.hidden = false;
992
+ if (!state.inviteOpen) {
993
+ box.innerHTML = '<button class="btn-ghost" id="goal-invite-open" type="button">+ Invite member</button>';
994
+ $('#goal-invite-open')?.addEventListener('click', async () => {
995
+ state.inviteOpen = true;
996
+ await loadDirectory();
997
+ renderInviteBox(goalId, canInvite, members);
998
+ });
999
+ return;
1000
+ }
1001
+ const memberIds = new Set((members || []).map((m) => String(m.builder_id)));
1002
+ const invitable = (state.directory || []).filter((b) => !memberIds.has(String(b.id)));
1003
+ const options = invitable
1004
+ .map((b) => `<option value="${escapeHtml(String(b.id))}">${escapeHtml(b.display_name || ('@' + b.github_login))}</option>`)
1005
+ .join('');
1006
+ box.innerHTML = `
1007
+ <form class="goal-invite__form" id="goal-invite-form">
1008
+ <select id="goal-invite-builder" required ${invitable.length ? '' : 'disabled'}>
1009
+ ${invitable.length ? `<option value="" disabled selected>Choose a builder…</option>${options}`
1010
+ : '<option value="">Everyone is already a member</option>'}
1011
+ </select>
1012
+ <select id="goal-invite-role">
1013
+ <option value="member">Member</option>
1014
+ <option value="lead">Manager</option>
1015
+ </select>
1016
+ <button class="landing__btn" type="submit" ${invitable.length ? '' : 'disabled'}>Invite</button>
1017
+ <button class="btn-ghost" id="goal-invite-cancel" type="button">Cancel</button>
1018
+ </form>`;
1019
+ $('#goal-invite-cancel')?.addEventListener('click', () => {
1020
+ state.inviteOpen = false;
1021
+ renderInviteBox(goalId, canInvite, members);
1022
+ });
1023
+ $('#goal-invite-form')?.addEventListener('submit', (ev) => {
1024
+ ev.preventDefault();
1025
+ const builderId = $('#goal-invite-builder')?.value;
1026
+ const role = $('#goal-invite-role')?.value || 'member';
1027
+ if (builderId) inviteMember(goalId, builderId, role);
1028
+ });
1029
+ }
1030
+
1031
+ async function inviteMember(goalId, builderId, role) {
1032
+ try {
1033
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/members`, { builder_id: Number(builderId), role });
1034
+ toast('Invited.');
1035
+ state.inviteOpen = false;
1036
+ await loadDetail(goalId, { force: true });
1037
+ renderDetail(goalId);
1038
+ renderList();
1039
+ } catch (err) {
1040
+ const code = err?.data?.error;
1041
+ if (code === 'archon_required_for_protected_scope') toast('This goal is scoped to a protected module — only an Archon may add members.');
1042
+ else if (code === 'metic_required_for_protected_scope') toast('A protected-scope goal requires a Metic+ member.');
1043
+ else if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may invite.');
1044
+ else if (code === 'goal_not_open') toast('This goal is not open.');
1045
+ else toast('Could not invite that builder just now.');
1046
+ }
1047
+ }
1048
+
1049
+ async function joinLeave(goalId, action) {
1050
+ try {
1051
+ await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/${action}`);
1052
+ toast(action === 'join' ? 'Joined the goal.' : 'Left the goal.');
1053
+ await loadDetail(goalId, { force: true });
1054
+ renderDetail(goalId);
1055
+ renderList();
1056
+ } catch (err) {
1057
+ const code = err?.data?.error;
1058
+ if (code === 'archon_approval_required') toast('This goal is Archon-mediated — an Archon must add you.');
1059
+ else if (code === 'not_a_member') toast('You are not a member of this goal.');
1060
+ else if (code === 'goal_not_open') toast('This goal is not open.');
1061
+ else toast(`Could not ${action} just now.`);
1062
+ }
1063
+ }
1064
+
1065
+ /* ---------------- data + routing ---------------- */
1066
+
1067
+ async function loadDetail(goalId, { force = false } = {}) {
1068
+ if (!force && state.details.has(String(goalId))) return state.details.get(String(goalId));
1069
+ const d = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}`);
1070
+ state.details.set(String(goalId), d);
1071
+ return d;
1072
+ }
1073
+
1074
+ // task 1888 — a freshly opened goal starts from the compact defaults: To do
1075
+ // tab selected, capped lists, counts hidden until its own data lands.
1076
+ function resetWorkUi() {
1077
+ state.workTab = 'todo';
1078
+ // task 1956 — a freshly opened goal starts both Work tables on page 1 (per-page persists).
1079
+ state.workPager.todo.page = 1;
1080
+ state.workPager.done.page = 1;
1081
+ state.workSort = { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } };
1082
+ state.work = { todo: [], done: [] };
1083
+ state.workLoaded = false;
1084
+ state.claimsByTask = new Map();
1085
+ state.conflictPairs = null;
1086
+ state.conflictSource = null;
1087
+ setWorkTab('todo');
1088
+ ['#goal-todo-count', '#goal-done-count', '#goal-conflicts-count', '#goal-conflicts-view-count',
1089
+ '#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
1090
+ const el = $(sel);
1091
+ if (el) { el.hidden = true; el.textContent = ''; }
1092
+ });
1093
+ ['#goal-todo-pager', '#goal-done-pager', '#goal-conflicts-foot', '#goal-members-foot', '#goal-completion-foot'].forEach((sel) => {
1094
+ const el = $(sel);
1095
+ if (el) el.hidden = true;
1096
+ });
1097
+ ['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
1098
+ reflectSortHeaders('todo');
1099
+ reflectSortHeaders('done');
1100
+ const todoBody = document.querySelector('#goal-todo tbody');
1101
+ const doneBody = document.querySelector('#goal-done tbody');
1102
+ if (todoBody) todoBody.innerHTML = '<tr><td colspan="6" class="ledger__empty">…</td></tr>';
1103
+ if (doneBody) doneBody.innerHTML = '<tr><td colspan="5" class="ledger__empty">…</td></tr>';
1104
+ const conflicts = $('#goal-conflicts');
1105
+ if (conflicts) conflicts.innerHTML = '<div class="profile__placeholder">…</div>';
1106
+ const conflictsFull = $('#goal-conflicts-full');
1107
+ if (conflictsFull) conflictsFull.innerHTML = '<div class="profile__placeholder">…</div>';
1108
+ }
1109
+
1110
+ // task 1828 — views sharing this page: the list, a goal's detail, and the
1111
+ // goal's three full views (conflicts / members / completion). `view` defaults
1112
+ // from goalId (null → list).
1113
+ const SUB_VIEWS = ['conflicts', 'members', 'completion'];
1114
+ const VIEW_SECTION = {
1115
+ list: '#goals-list-view',
1116
+ detail: '#goal-detail-view',
1117
+ conflicts: '#goal-conflicts-view',
1118
+ members: '#goal-members-view',
1119
+ completion: '#goal-completion-view',
1120
+ };
1121
+ const SUB_SUBTITLE = {
1122
+ conflicts: 'Goal conflicts — every open task pair whose declared files overlap.',
1123
+ members: 'Goal members — who’s on this goal and what they’ve contributed to it.',
1124
+ completion: 'Goal completion — every done-when criterion, with its full detail.',
1125
+ };
1126
+ function setView(goalId, view) {
1127
+ view = view || (goalId == null ? 'list' : 'detail');
1128
+ if (goalId !== state.current) {
1129
+ state.inviteOpen = false;
1130
+ if (goalId != null) resetWorkUi();
1131
+ }
1132
+ state.current = goalId;
1133
+ state.view = view;
1134
+ for (const [name, sel] of Object.entries(VIEW_SECTION)) {
1135
+ const el = $(sel);
1136
+ if (el) el.hidden = view !== name;
1137
+ }
1138
+ const sub = $('#goals-sub');
1139
+ if (sub) {
1140
+ sub.textContent = view === 'list'
1141
+ ? 'Shared goals: progress, members, and the work remaining.'
1142
+ : (SUB_SUBTITLE[view] || 'Goal detail — completion, members, conflicts, and the work queue.');
1143
+ }
1144
+ window.scrollTo({ top: 0 });
1145
+ }
1146
+
1147
+ async function route() {
1148
+ const hash = window.location.hash;
1149
+ // A record deep-link (#/task|idea|blocker/N) belongs to the shared overlay
1150
+ // (task-detail.js), which opens ON TOP of this page. Leave whatever goal
1151
+ // view is underneath intact — collapsing to the list here is what yanked
1152
+ // the task board out from behind the card (task 1753 regression). On a cold
1153
+ // load straight to a deep-link there's no detail open yet, so render the
1154
+ // list as the backdrop behind the card.
1155
+ if (DEEPLINK_RE.test(hash)) {
1156
+ if (state.current == null) renderList();
1157
+ return;
1158
+ }
1159
+ // task 1828 — a goal's dedicated full views (conflicts / members /
1160
+ // completion). renderDetail renders all three full views + kicks
1161
+ // loadGoalTasks, so a cold load straight to one still fills in once data
1162
+ // lands (member stats / work / conflicts refresh async).
1163
+ const mSub = /^#\/goal\/(\d+)\/(conflicts|members|completion)$/.exec(hash);
1164
+ if (mSub) {
1165
+ const id = mSub[1];
1166
+ const view = mSub[2];
1167
+ try {
1168
+ await loadDetail(id);
1169
+ setView(id, view);
1170
+ renderDetail(id);
1171
+ } catch (err) {
1172
+ if (err && err.message === 'redirecting to auth') return;
1173
+ toast(err?.status === 404 ? 'No such goal.' : 'Goal could not be loaded.');
1174
+ window.location.hash = '';
1175
+ setView(null);
1176
+ renderList();
1177
+ }
1178
+ return;
1179
+ }
1180
+ const m = /^#\/goal\/(\d+)$/.exec(hash);
1181
+ if (m) {
1182
+ const id = m[1];
1183
+ try {
1184
+ await loadDetail(id);
1185
+ setView(id, 'detail');
1186
+ renderDetail(id);
1187
+ } catch (err) {
1188
+ if (err && err.message === 'redirecting to auth') return;
1189
+ toast(err?.status === 404 ? 'No such goal.' : 'Goal could not be loaded.');
1190
+ window.location.hash = '';
1191
+ setView(null);
1192
+ }
1193
+ } else {
1194
+ setView(null);
1195
+ renderList();
1196
+ }
1197
+ }
1198
+
1199
+ async function boot() {
1200
+ try {
1201
+ state.me = await getJSON(`${API}/me`).catch(() => null);
1202
+ // task 1828 — "My goals" needs an identity; disable it when signed out.
1203
+ const mineSel = $('#f-goal-mine');
1204
+ if (mineSel && !state.me) { mineSel.disabled = true; mineSel.title = 'Sign in to filter to your goals'; }
1205
+ const gres = await getJSON(`${API}/goals`);
1206
+ state.goals = gres.goals || [];
1207
+ populateVersionFilter();
1208
+ // Prefetch details (ring + members on the cards). Sequentially-parallel,
1209
+ // failure-tolerant: a card without detail still renders.
1210
+ await Promise.allSettled(state.goals.map((g) => loadDetail(g.id)));
1211
+ await route();
1212
+ } catch (err) {
1213
+ if (err && err.message === 'redirecting to auth') return;
1214
+ console.error('[goals] boot failed', err);
1215
+ const sub = $('#goals-sub');
1216
+ if (sub) sub.textContent = 'Goals could not be loaded. Refresh to try again.';
1217
+ const grid = $('#goal-grid');
1218
+ if (grid) grid.innerHTML = '<div class="profile__placeholder">Goals could not be loaded.</div>';
1219
+ }
1220
+ }
1221
+
1222
+ // task 1932 — any list filter change re-paginates the grid from page 1.
1223
+ const onListFilterChange = () => { state.listPager.page = 1; renderList(); };
1224
+ $('#f-goal-version')?.addEventListener('change', onListFilterChange);
1225
+ $('#f-goal-status')?.addEventListener('change', onListFilterChange);
1226
+ $('#f-goal-mine')?.addEventListener('change', onListFilterChange); // task 1828 — My goals filter
1227
+ // task 1932 — the goal grid's pagination controls (per-page dropdown + prev/next).
1228
+ $('#goals-per-page')?.addEventListener('change', (e) => {
1229
+ const v = Number(e.target.value);
1230
+ if (!LIST_PER_PAGE_OPTIONS.includes(v)) return;
1231
+ state.listPager.perPage = v;
1232
+ state.listPager.page = 1;
1233
+ try { window.localStorage.setItem(GOALS_PER_PAGE_KEY, String(v)); } catch (_) { /* private mode → in-memory only */ }
1234
+ renderList();
1235
+ });
1236
+ $('#goals-prev')?.addEventListener('click', () => {
1237
+ if (state.listPager.page > 1) { state.listPager.page -= 1; renderList(); }
1238
+ });
1239
+ $('#goals-next')?.addEventListener('click', () => {
1240
+ state.listPager.page += 1; renderList(); // renderList clamps to the last page
1241
+ });
1242
+ $('#goal-back')?.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = ''; });
1243
+ // task 1828 — "← Back to goal" from any of the full views.
1244
+ ['#goal-conflicts-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
1245
+ $(sel)?.addEventListener('click', (e) => {
1246
+ e.preventDefault();
1247
+ if (state.current != null) window.location.hash = `#/goal/${state.current}`;
1248
+ });
1249
+ });
1250
+ // task 1888 — the Work card's tab toggle.
1251
+ $('#goal-tab-todo')?.addEventListener('click', () => setWorkTab('todo'));
1252
+ $('#goal-tab-done')?.addEventListener('click', () => setWorkTab('done'));
1253
+ // task 1956 — pagination controls for each Work table (per-page dropdown + prev/next).
1254
+ [['goal-todo', 'todo'], ['goal-done', 'done']].forEach(([prefix, key]) => {
1255
+ $(`#${prefix}-per-page`)?.addEventListener('change', (e) => {
1256
+ const v = Number(e.target.value);
1257
+ if (!LIST_PER_PAGE_OPTIONS.includes(v)) return;
1258
+ state.workPager[key].perPage = v;
1259
+ state.workPager[key].page = 1;
1260
+ try { window.localStorage.setItem(WORK_PER_PAGE_KEY[key], String(v)); } catch (_) { /* private mode → in-memory only */ }
1261
+ renderWork();
1262
+ });
1263
+ $(`#${prefix}-prev`)?.addEventListener('click', () => {
1264
+ if (state.workPager[key].page > 1) { state.workPager[key].page -= 1; renderWork(); }
1265
+ });
1266
+ $(`#${prefix}-next`)?.addEventListener('click', () => {
1267
+ state.workPager[key].page += 1; renderWork(); // renderWork clamps to the last page
1268
+ });
1269
+ });
1270
+ // task 1828 — click / keyboard sort on the Work-table column headers.
1271
+ Object.entries(WORK_SORT_HEADERS).forEach(([table, cols]) => {
1272
+ Object.entries(cols).forEach(([key, id]) => {
1273
+ const th = document.getElementById(id);
1274
+ if (!th) return;
1275
+ th.addEventListener('click', () => onWorkSortClick(table, key));
1276
+ th.addEventListener('keydown', (e) => {
1277
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onWorkSortClick(table, key); }
1278
+ });
1279
+ });
1280
+ });
1281
+ // task 1753/1828: a row opens the shared task-detail overlay; the inline Claim
1282
+ // button (task 1828) claims without opening it.
1283
+ ['#goal-todo', '#goal-done'].forEach((sel) => {
1284
+ $(sel)?.addEventListener('click', (e) => {
1285
+ const claimBtn = e.target.closest('[data-claim-task]');
1286
+ if (claimBtn) { e.stopPropagation(); doGoalClaim(claimBtn.dataset.claimTask, claimBtn); return; }
1287
+ const releaseBtn = e.target.closest('[data-release-claim]');
1288
+ if (releaseBtn) { e.stopPropagation(); doGoalRelease(releaseBtn.dataset.releaseClaim, releaseBtn.dataset.releaseTask, releaseBtn); return; }
1289
+ const row = e.target.closest('.ledger__row--task');
1290
+ if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
1291
+ });
1292
+ });
1293
+ window.addEventListener('hashchange', route);
1294
+ wireDragReorder();
1295
+
1296
+ boot();
1297
+ })();