@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,1419 @@
1
+ // src/bongos/routes/goals.js — the Goal tier's HTTP surface (ADR 0086 §3, BV1.R57).
2
+ //
3
+ // A goal is a shared, module-scoped workspace (the `goals` + `goal_members`
4
+ // tables from R56). This router lets a Metic+ create one, lets builders view and
5
+ // join/leave them, and adds a `?goal_id=` filter to GET /tasks (in tasks.js).
6
+ //
7
+ // SCOPE WALL (ADR 0086 §3/§4). `scope_modules` is the wall, set at create time by
8
+ // a trusted rank and never self-granted:
9
+ // - POST /goals is metic+archon, but ARCHON-ONLY when scope_modules names a
10
+ // `protected` module (module-scope-map.js, R54) — the protected core.
11
+ // - Joining a protected-scope goal is Archon-mediated: a non-Archon cannot
12
+ // self-join; an Archon admits them via POST /goals/:id/members, and only a
13
+ // Metic+ may be admitted. Per ADR 0086's Deferred section, a sub-Metic builder
14
+ // working INSIDE a protected-scope goal is held for the Phase-5 security-
15
+ // reviewed ADR — until then protected-scope goals require a Metic+ holder, and
16
+ // the self-service "request → approve" queue is not built here (it co-evolves
17
+ // with that ADR). Normal (non-protected) goals are open-join.
18
+ //
19
+ // The bounded task-authoring gate (POST /goals/:id/tasks) is R60, not here; this
20
+ // router references no task-authoring grant. The grader/route-rank fitness check
21
+ // pins these routes' ranks in route-rank-check.EXPECTED_RANKS.
22
+
23
+ const express = require('express');
24
+ // Intra-module + the published doorway — a module never requires a core internal.
25
+ const api = require('../../../src/module-api');
26
+ const auth = api;
27
+ const db = require('../db');
28
+ const hierarchy = require('../hierarchy');
29
+ const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, TASK_FIELD_BOUNDS } = require('../task-classifier');
30
+ const { validateOrRespond, parseId, LIMITS } = api;
31
+ const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
32
+ // goalScopeCheck — the PURE scope wall (BV1.R59 / ADR 0086 §3), reached through
33
+ // the published doorway (a module never requires the core file directly — the
34
+ // module-boundary fitness check would red the build). The create gate below calls
35
+ // its check() VERBATIM; the two walls (scope-subset + protected-path) live there,
36
+ // never re-implemented here.
37
+ const goalScopeCheck = api.goalScopeCheck;
38
+ // The pure conflict detector (task 1736) — a module sibling, like ../db.
39
+ const goalConflicts = require('../goal-conflicts');
40
+
41
+ const VALID_GOAL_STATUSES = ['open', 'achieved', 'archived'];
42
+ const VALID_MEMBER_ROLES = ['lead', 'member'];
43
+ const METIC_PLUS = ['metic', 'archon'];
44
+ const RESOLVE_ACTIONS = ['sequence', 'flag_release', 'rescope'];
45
+ // Private goals (task 1952 / ADR 0112): the visibility values, and the resolve
46
+ // actions for each side of the membership-request queue.
47
+ const VALID_VISIBILITIES = ['public', 'private'];
48
+ const INVITE_RESPONSE_ACTIONS = ['accept', 'decline']; // the invitee resolves their own invite
49
+ const JOIN_REQUEST_RESPONSE_ACTIONS = ['approve', 'deny']; // an owner/manager/Archon resolves a request
50
+
51
+ // Shared gate for the goal lifecycle + scope mutations (BV1.R64): the caller must
52
+ // be a member of the goal, or an Archon (who may act on any goal). Returns true to
53
+ // proceed; a false result means "not a member" → the route should 403. This is the
54
+ // membership half of the wall; the protected-module rank check is enforced per-route
55
+ // (scope is never self-granted).
56
+ async function isMemberOrArchon(builder, goalId) {
57
+ if (builder.rank === 'archon') return true;
58
+ return db.isGoalMember({ goalId, builderId: builder.id });
59
+ }
60
+
61
+ // authorizeGoalTaskCreate — the ORDERED, FAIL-CLOSED authorization decision for
62
+ // POST /goals/:id/tasks (BV1.R60 / ADR 0086 §3): the single point that lets a
63
+ // lower-rank builder author their OWN work safely. PURE + exported (the
64
+ // shouldRefuseSessionClaim precedent in claims.js) so every rejection is pinned
65
+ // by a DB-free unit test AND reused by the BV1.R61 abuse matrix — no live route
66
+ // or DB needed.
67
+ //
68
+ // The route fetches goal / version / membership, then calls this; it rejects on
69
+ // the FIRST failure (fail-closed — a missing/ambiguous input denies):
70
+ // goal_not_found → goal_not_open → not_a_member → version_not_found →
71
+ // version_closed → (scope-subset + protected-path, via goalScopeCheck.check).
72
+ // The scope+protected wall is goal-scope-check.check() VERBATIM (R59) — itself
73
+ // ordered + fail-closed (empty-scope → empty-touches → unknown-scope →
74
+ // scope-subset → protected-path) — never re-implemented here. `isMember` is the
75
+ // resolved "member OR Archon" boolean the route computes (isMemberOrArchon).
76
+ //
77
+ // This is the CREATE-time filter: it sees only DECLARED touches[] (advisory — git
78
+ // merge is the real collision detector, ADR 0049), so it is deliberately the FIRST
79
+ // filter, NOT the last. The ship-time gates stay authoritative on the REAL diff
80
+ // (grader permission/route-rank pre-passes + serverSideGuards, gate-review
81
+ // ESCALATE floor, post-push main-audit — ADR 0086 §7). Returns { ok:true } or
82
+ // { ok:false, status, body } (the exact HTTP status + JSON the route sends).
83
+ function authorizeGoalTaskCreate({ goal, version, isMember, builderRank, touches }) {
84
+ if (!goal) {
85
+ return { ok: false, status: 404, body: { error: 'goal_not_found' } };
86
+ }
87
+ if (goal.status !== 'open') {
88
+ return { ok: false, status: 409, body: {
89
+ error: 'goal_not_open', status: goal.status,
90
+ message: 'Tasks can only be authored under an open goal.',
91
+ } };
92
+ }
93
+ if (!isMember) {
94
+ return { ok: false, status: 403, body: {
95
+ error: 'not_a_member',
96
+ message: 'Join the goal (or be an Archon) to author tasks within it.',
97
+ } };
98
+ }
99
+ if (!version) {
100
+ return { ok: false, status: 404, body: {
101
+ error: 'version_not_found',
102
+ message: `The goal's version '${goal.version_id}' no longer exists.`,
103
+ } };
104
+ }
105
+ if (version.status === 'shipped') {
106
+ return { ok: false, status: 409, body: {
107
+ error: 'version_closed',
108
+ message: `Version '${goal.version_id}' has shipped — it is closed history and cannot take new tasks.`,
109
+ } };
110
+ }
111
+ // The wall — scope-subset + protected-path in one pure call (R59). It bounds
112
+ // EVERYONE by the goal's modules (scope-subset), and additionally blocks a
113
+ // sub-Metic member from authoring into the protected core (protected-path),
114
+ // reusing the SINGLE protected roster (permission-path-check) — no drift.
115
+ const gate = goalScopeCheck.check(goal.scope_modules, touches, builderRank);
116
+ if (!gate.ok) {
117
+ return { ok: false, status: 403, body: {
118
+ error: gate.code, reason: gate.reason, details: gate.details,
119
+ } };
120
+ }
121
+ return { ok: true };
122
+ }
123
+
124
+ // The goal AUTHORITY MODEL (task 1735). Two layers, deliberately different kinds:
125
+ // - OWNER — goals.created_by, the fixed authority. Not a role row: it is set at
126
+ // create time (the creator) and moves only via the explicit transfer route.
127
+ // The owner holds every goal power.
128
+ // - MANAGER — the goal_members.role 'lead' value (the hall renders it
129
+ // "manager"). Owner-equivalent EXCEPT: a manager cannot DEMOTE a fellow
130
+ // manager (owner-only), and cannot transfer ownership.
131
+ // A goal with created_by NULL (the migration-160 system defaults) has no owner;
132
+ // Archons act as one — the archon arm of both gates below.
133
+ function isGoalOwner(goal, builderId) {
134
+ return goal.created_by != null && String(goal.created_by) === String(builderId);
135
+ }
136
+
137
+ // authorizeMemberRoleChange — the ORDERED, FAIL-CLOSED authorization decision for
138
+ // PATCH /goals/:id/members/:builderId (task 1735). Today the only role-assignment
139
+ // path is the Archon admission route; this gate lets the goal's own authority
140
+ // manage roles without an Archon in the loop:
141
+ // - the owner, a manager, or an Archon may promote member→lead(manager);
142
+ // - ONLY the owner (or an Archon) may demote lead(manager)→member;
143
+ // - the OWNER's row can never be demoted — owner authority is created_by, not
144
+ // the role value, so a demoted owner row would just lie to the member list.
145
+ // PURE + exported (the authorizeGoalTaskCreate precedent above) so every
146
+ // rejection is pinned by a DB-free unit test. The route fetches goal /
147
+ // memberships, then calls this; it rejects on the FIRST failure (fail-closed —
148
+ // a missing/ambiguous input denies):
149
+ // goal_not_found → goal_not_open → not_owner_or_manager → not_a_member
150
+ // (target) → no-op same-role → owner_only_demotion → owner_role_fixed.
151
+ // Authority (not_owner_or_manager) is checked BEFORE the target lookup so an
152
+ // unauthorized actor never learns who is or isn't a member (the O1 no-leak
153
+ // ordering precedent in the abuse matrix). The DB helper (setGoalMemberRole)
154
+ // re-asserts the owner-demotion guard atomically in the UPDATE itself — this
155
+ // gate is the testable contract, the SQL guard is the backstop.
156
+ // Returns { ok:true }, { ok:true, noop:true } (role already as requested), or
157
+ // { ok:false, status, body } (the exact HTTP status + JSON the route sends).
158
+ function authorizeMemberRoleChange({ goal, actorId, actorRank, actorMembership, targetMembership, targetId, role }) {
159
+ if (!goal) {
160
+ return { ok: false, status: 404, body: { error: 'goal_not_found' } };
161
+ }
162
+ if (goal.status !== 'open') {
163
+ return { ok: false, status: 409, body: {
164
+ error: 'goal_not_open', status: goal.status,
165
+ message: 'Member roles can only be changed on an open goal.',
166
+ } };
167
+ }
168
+ const actorIsOwner = isGoalOwner(goal, actorId);
169
+ const actorIsManager = !!actorMembership && actorMembership.role === 'lead';
170
+ if (actorRank !== 'archon' && !actorIsOwner && !actorIsManager) {
171
+ return { ok: false, status: 403, body: {
172
+ error: 'not_owner_or_manager',
173
+ message: 'Only this goal\'s owner or a manager (or an Archon) may change member roles.',
174
+ } };
175
+ }
176
+ if (!targetMembership) {
177
+ return { ok: false, status: 404, body: {
178
+ error: 'not_a_member',
179
+ message: 'That builder is not a member of this goal — admit them first.',
180
+ } };
181
+ }
182
+ if (targetMembership.role === role) {
183
+ return { ok: true, noop: true };
184
+ }
185
+ if (role === 'member') {
186
+ // A demotion. Managers promote; only the owner (or an Archon) demotes.
187
+ if (actorRank !== 'archon' && !actorIsOwner) {
188
+ return { ok: false, status: 403, body: {
189
+ error: 'owner_only_demotion',
190
+ message: 'Only the goal owner (or an Archon) may demote a manager.',
191
+ } };
192
+ }
193
+ if (isGoalOwner(goal, targetId)) {
194
+ return { ok: false, status: 409, body: {
195
+ error: 'owner_role_fixed',
196
+ message: 'The goal owner cannot be demoted — transfer ownership first.',
197
+ } };
198
+ }
199
+ }
200
+ return { ok: true };
201
+ }
202
+
203
+ // authorizeGoalManagement — the shared ORDERED, FAIL-CLOSED prefix for actions
204
+ // a goal's own authority performs (task 1736: conflict resolution; the same
205
+ // three steps open authorizeMemberRoleChange above): goal exists → goal open →
206
+ // actor is owner|manager|Archon. Returns { ok:true } or { ok:false, status,
207
+ // body }. PURE + exported, same precedent as its siblings.
208
+ function authorizeGoalManagement({ goal, actorId, actorRank, actorMembership }) {
209
+ if (!goal) {
210
+ return { ok: false, status: 404, body: { error: 'goal_not_found' } };
211
+ }
212
+ if (goal.status !== 'open') {
213
+ return { ok: false, status: 409, body: {
214
+ error: 'goal_not_open', status: goal.status,
215
+ message: 'Only an open goal can be managed.',
216
+ } };
217
+ }
218
+ const actorIsOwner = isGoalOwner(goal, actorId);
219
+ const actorIsManager = !!actorMembership && actorMembership.role === 'lead';
220
+ if (actorRank !== 'archon' && !actorIsOwner && !actorIsManager) {
221
+ return { ok: false, status: 403, body: {
222
+ error: 'not_owner_or_manager',
223
+ message: 'Only this goal\'s owner or a manager (or an Archon) may do this.',
224
+ } };
225
+ }
226
+ return { ok: true };
227
+ }
228
+
229
+ // authorizeOwnershipTransfer — the ORDERED, FAIL-CLOSED decision for
230
+ // POST /goals/:id/transfer (task 1735): the owner hands the goal to another
231
+ // builder (the old owner keeps their manager row). Ownership is creator-
232
+ // equivalent authority and goal CREATION is Metic+ — so the transfer target
233
+ // must be Metic+ too (fail-closed; this also keeps the protected-scope
234
+ // membership wall intact, since a protected-scope goal requires Metic+ members).
235
+ // Ordering: goal_not_found → goal_not_open → not_owner → builder_not_found →
236
+ // metic_required_for_ownership → no-op self-transfer.
237
+ function authorizeOwnershipTransfer({ goal, actorId, actorRank, targetBuilder }) {
238
+ if (!goal) {
239
+ return { ok: false, status: 404, body: { error: 'goal_not_found' } };
240
+ }
241
+ if (goal.status !== 'open') {
242
+ return { ok: false, status: 409, body: {
243
+ error: 'goal_not_open', status: goal.status,
244
+ message: 'Ownership can only be transferred on an open goal.',
245
+ } };
246
+ }
247
+ if (actorRank !== 'archon' && !isGoalOwner(goal, actorId)) {
248
+ return { ok: false, status: 403, body: {
249
+ error: 'not_owner',
250
+ message: 'Only the goal owner (or an Archon) may transfer ownership.',
251
+ } };
252
+ }
253
+ if (!targetBuilder) {
254
+ return { ok: false, status: 404, body: { error: 'builder_not_found' } };
255
+ }
256
+ if (!METIC_PLUS.includes(targetBuilder.rank)) {
257
+ return { ok: false, status: 403, body: {
258
+ error: 'metic_required_for_ownership',
259
+ message: 'Goal ownership is creator-equivalent authority — the new owner must be Metic+.',
260
+ } };
261
+ }
262
+ if (isGoalOwner(goal, targetBuilder.id)) {
263
+ return { ok: true, noop: true };
264
+ }
265
+ return { ok: true };
266
+ }
267
+
268
+ // -------------------------------------------------------------------------
269
+ // Private goals: the invite / join-request queue (task 1952 / ADR 0112). A
270
+ // private goal is join-GATED (not hidden): a non-member can't self-join — they
271
+ // are INVITED (a consented, pending row they accept) or REQUEST to join (an
272
+ // owner/manager/Archon approves). Both directions live in goal_membership_requests
273
+ // (kind='invite'|'request'); the resolve is atomic (db.resolveMembershipRequest
274
+ // moves only from status='pending', so a re-submit can't double-add a member).
275
+ // -------------------------------------------------------------------------
276
+
277
+ // protectedScopeAdmissionBlock — the SINGLE protected-scope admission rule reused
278
+ // by BOTH new admission paths (invite-create and join-request-approve) AND the
279
+ // existing POST /goals/:id/members (whose inline checks this generalizes). A
280
+ // protected-scope goal admits members only via an Archon, and only Metic+ builders
281
+ // — sub-Metic membership inside protected scope is deferred to ADR 0086's Phase-5
282
+ // ADR. Centralized so the protected wall can never drift between paths (the SEC
283
+ // #863 single-roster lesson). Returns { status, body } to send, or null to proceed.
284
+ function protectedScopeAdmissionBlock({ scopeProtected, actorRank, targetRank }) {
285
+ if (!scopeProtected) return null;
286
+ if (actorRank !== 'archon') {
287
+ return { status: 403, body: {
288
+ error: 'archon_required_for_protected_scope',
289
+ message: 'This goal is scoped to a protected module; only an Archon may admit members.',
290
+ } };
291
+ }
292
+ if (!METIC_PLUS.includes(targetRank)) {
293
+ return { status: 403, body: {
294
+ error: 'metic_required_for_protected_scope',
295
+ message: 'A protected-scope goal requires a Metic+ member; sub-Metic membership is deferred to the Phase-5 ADR.',
296
+ } };
297
+ }
298
+ return null;
299
+ }
300
+
301
+ // authorizeCreateJoinRequest — the ORDERED, FAIL-CLOSED gate for
302
+ // POST /goals/:id/requests (task 1952): a builder asks to join. A request only
303
+ // makes sense where self-join is gated — a PUBLIC, NON-protected goal is open-join,
304
+ // so a request there is refused with a "just join" hint. Ordering:
305
+ // goal_not_found → goal_not_open → already_a_member → goal_is_open (public+unprotected).
306
+ // isMember is the caller's own membership; scopeProtected is scopeIncludesProtected(goal).
307
+ // PURE + exported (the authorizeGoalTaskCreate precedent).
308
+ function authorizeCreateJoinRequest({ goal, isMember, scopeProtected }) {
309
+ if (!goal) return { ok: false, status: 404, body: { error: 'goal_not_found' } };
310
+ if (goal.status !== 'open') {
311
+ return { ok: false, status: 409, body: {
312
+ error: 'goal_not_open', status: goal.status,
313
+ message: 'You can only request to join an open goal.',
314
+ } };
315
+ }
316
+ if (isMember) {
317
+ return { ok: false, status: 409, body: {
318
+ error: 'already_a_member', message: 'You are already a member of this goal.',
319
+ } };
320
+ }
321
+ if (goal.visibility !== 'private' && !scopeProtected) {
322
+ return { ok: false, status: 409, body: {
323
+ error: 'goal_is_open',
324
+ message: 'This goal is open — just join it (POST /goals/:id/join); no request needed.',
325
+ } };
326
+ }
327
+ return { ok: true };
328
+ }
329
+
330
+ // authorizeInvitationResponse — the ORDERED, FAIL-CLOSED gate for
331
+ // POST /goals/:id/invitations/:reqId/respond (task 1952): ONLY the invited builder
332
+ // may accept/decline their OWN pending invite. Containment first — the row must
333
+ // belong to THIS goal and be an 'invite' — so the invitation route can never touch
334
+ // a join-request row. Ordering: request_not_found → wrong_goal → not_an_invitation
335
+ // → not_pending → not_your_invitation. PURE + exported.
336
+ function authorizeInvitationResponse({ request, goalId, actorId }) {
337
+ if (!request) return { ok: false, status: 404, body: { error: 'request_not_found' } };
338
+ if (String(request.goal_id) !== String(goalId)) {
339
+ return { ok: false, status: 404, body: { error: 'request_not_found', message: 'No such invitation on this goal.' } };
340
+ }
341
+ if (request.kind !== 'invite') {
342
+ return { ok: false, status: 400, body: { error: 'not_an_invitation', message: 'That row is a join-request, not an invitation.' } };
343
+ }
344
+ if (request.status !== 'pending') {
345
+ return { ok: false, status: 409, body: { error: 'not_pending', status: request.status } };
346
+ }
347
+ if (String(request.builder_id) !== String(actorId)) {
348
+ return { ok: false, status: 403, body: {
349
+ error: 'not_your_invitation',
350
+ message: 'Only the invited builder may accept or decline this invitation.',
351
+ } };
352
+ }
353
+ return { ok: true };
354
+ }
355
+
356
+ // authorizeJoinRequestResponse — the gate for POST /goals/:id/requests/:reqId/respond
357
+ // (task 1952): an owner|manager|Archon approves/denies a pending join-request. The
358
+ // authority prefix is authorizeGoalManagement VERBATIM (goal exists/open + owner|
359
+ // manager|Archon — checked BEFORE the request lookup so an unauthorized actor never
360
+ // learns whether a request exists, the O1 no-leak ordering). Then request
361
+ // containment + state: request_not_found → wrong_goal → not_a_join_request →
362
+ // not_pending. PURE + exported.
363
+ function authorizeJoinRequestResponse({ goal, actorId, actorRank, actorMembership, request, goalId }) {
364
+ const mgmt = authorizeGoalManagement({ goal, actorId, actorRank, actorMembership });
365
+ if (!mgmt.ok) return mgmt;
366
+ if (!request) return { ok: false, status: 404, body: { error: 'request_not_found' } };
367
+ if (String(request.goal_id) !== String(goalId)) {
368
+ return { ok: false, status: 404, body: { error: 'request_not_found', message: 'No such join-request on this goal.' } };
369
+ }
370
+ if (request.kind !== 'request') {
371
+ return { ok: false, status: 400, body: { error: 'not_a_join_request', message: 'That row is an invitation, not a join-request.' } };
372
+ }
373
+ if (request.status !== 'pending') {
374
+ return { ok: false, status: 409, body: { error: 'not_pending', status: request.status } };
375
+ }
376
+ return { ok: true };
377
+ }
378
+
379
+ module.exports = function buildGoalsRouter() {
380
+ const router = express.Router();
381
+
382
+ // POST /goals — create a goal.
383
+ // rank: metic+archon — a Metic or Archon may create a goal; creation of a goal
384
+ // whose scope_modules includes a PROTECTED module is Archon-only (in-handler).
385
+ router.post('/goals', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
386
+ if (validateOrRespond(req, res, {
387
+ version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
388
+ title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
389
+ description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
390
+ scope_modules: { type: 'array', maxItems: 64, itemsType: 'string' },
391
+ succeeds_goal_id: { type: 'integer', min: 1 },
392
+ })) return;
393
+
394
+ const scopeModules = req.body.scope_modules || [];
395
+ // Fail closed on an unknown module key — the scope wall is meaningless if it
396
+ // can name a module that doesn't exist.
397
+ const unknown = scopeModules.filter((k) => !isModuleKey(k));
398
+ if (unknown.length) {
399
+ return res.fail('unknown_module', 400, { unknown, valid: moduleKeys() });
400
+ }
401
+ // The scope is set by a trusted rank at planning time; a protected-module
402
+ // scope can only be set by an Archon (ADR 0086 §3/§4).
403
+ if (scopeIncludesProtected(scopeModules) && req.builder.rank !== 'archon') {
404
+ return res.fail('archon_required_for_protected_scope', { status: 403, message: 'A goal scoped to a protected module can only be created by an Archon.' });
405
+ }
406
+
407
+ try {
408
+ const version = await db.getVersion(req.body.version_id);
409
+ if (!version) {
410
+ return res.fail('version_not_found', { status: 404, message: 'No such version.' });
411
+ }
412
+ // Rescope carry-forward (ADR 0086 §6 / BV1.R64): a new-version goal may declare
413
+ // the (achieved) goal it continues. Validate the predecessor exists BEFORE
414
+ // creating the successor, so a bad ref can't leave an orphan goal behind.
415
+ const succeedsId = req.body.succeeds_goal_id ?? null;
416
+ if (succeedsId != null) {
417
+ const predecessor = await db.getGoal(succeedsId);
418
+ if (!predecessor) {
419
+ return res.fail('predecessor_not_found', { status: 404, message: 'No such goal to carry forward.' });
420
+ }
421
+ }
422
+ const goal = await db.createGoal({
423
+ versionId: req.body.version_id,
424
+ title: req.body.title,
425
+ description: (req.body.description || '').trim() || null,
426
+ scopeModules,
427
+ createdBy: req.builder.id,
428
+ });
429
+ // Stamp the predecessor's succeeded_by pointer at the new goal (the lineage).
430
+ if (succeedsId != null) {
431
+ await db.setGoalSuccessor({ goalId: succeedsId, successorId: goal.id });
432
+ }
433
+ return res.status(201).json({ ok: true, goal, succeeds_goal_id: succeedsId });
434
+ } catch (err) {
435
+ console.error('[gds] POST /goals', err);
436
+ return res.fail('goal_create_failed', { status: 500, message: 'internal error' });
437
+ }
438
+ });
439
+
440
+ // GET /goals?version=&status= — list goals (read).
441
+ // rank: any-builder — viewing the goal board.
442
+ router.get('/goals', auth.requireBuilder, async (req, res) => {
443
+ const { version, status } = req.query;
444
+ if (status && !VALID_GOAL_STATUSES.includes(status)) {
445
+ return res.fail('bad_status', 400, { valid: VALID_GOAL_STATUSES });
446
+ }
447
+ try {
448
+ const goals = await db.listGoals({ versionId: version || null, status: status || null });
449
+ return res.json({ goals });
450
+ } catch (err) {
451
+ console.error('[gds] GET /goals', err);
452
+ return res.fail('list_failed', { status: 500, message: 'internal error' });
453
+ }
454
+ });
455
+
456
+ // POST /goals/reorder — Archon drag-to-reorder: set the priority order of
457
+ // every goal WITHIN one version (task 1760). ordered_ids must be exactly
458
+ // that version's goal ids, each once (db.reorderGoals fails closed
459
+ // otherwise) — a partial list can't silently orphan a goal's priority.
460
+ // Mounted BEFORE GET /goals/:id so 'reorder' is never captured as an id.
461
+ // rank: archon — priority is an owner decision, never self-granted by a
462
+ // goal's own member/manager (unlike the goal-authority routes above).
463
+ router.post('/goals/reorder', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
464
+ if (validateOrRespond(req, res, {
465
+ version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
466
+ ordered_ids: { required: true, type: 'array', maxItems: 500, itemsType: 'integer' },
467
+ })) return;
468
+ const versionId = req.body.version_id;
469
+ const orderedIds = req.body.ordered_ids;
470
+ if (orderedIds.length === 0 || orderedIds.some((id) => !Number.isInteger(id) || id <= 0)) {
471
+ return res.fail('bad_ordered_ids', { status: 400, message: 'ordered_ids must be a non-empty list of positive integers.' });
472
+ }
473
+ try {
474
+ const version = await db.getVersion(versionId);
475
+ if (!version) return res.fail('version_not_found', 404);
476
+ const goals = await db.reorderGoals({ versionId, orderedIds });
477
+ return res.json({ ok: true, goals });
478
+ } catch (err) {
479
+ if (err && err.code === 'SET_MISMATCH') {
480
+ return res.fail('set_mismatch', { status: 400, message: err.message });
481
+ }
482
+ console.error('[gds] POST /goals/reorder', err);
483
+ return res.fail('reorder_failed', { status: 500, message: 'internal error' });
484
+ }
485
+ });
486
+
487
+ // GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112):
488
+ // { invitations: invites addressed to me (accept/decline); joinRequests: pending
489
+ // join-requests on goals I own or manage (approve/deny); myPendingRequests: my own
490
+ // outstanding requests }. Powers the home-page inbox widget (task 1954) + the
491
+ // goals page. Mounted BEFORE GET /goals/:id so the literal 'inbox' path is matched
492
+ // as its own route, not captured as an id (the /goals/reorder precedent).
493
+ // rank: any-builder — every builder reads their OWN inbox (db.listInboxFor scopes
494
+ // by req.builder.id; a builder can't read another's).
495
+ router.get('/goals/inbox', auth.requireBuilder, async (req, res) => {
496
+ try {
497
+ const inbox = await db.listInboxFor(req.builder.id);
498
+ return res.json(inbox);
499
+ } catch (err) {
500
+ console.error('[gds] GET /goals/inbox', err);
501
+ return res.fail('inbox_failed', { status: 500, message: 'internal error' });
502
+ }
503
+ });
504
+
505
+ // GET /goals/:id — one goal with its members + linked criteria (read).
506
+ // rank: any-builder.
507
+ router.get('/goals/:id', auth.requireBuilder, async (req, res) => {
508
+ const id = parseId(req, res, { code: 'bad_goal_id' });
509
+ if (id === null) return;
510
+ try {
511
+ const goal = await db.getGoal(id);
512
+ if (!goal) return res.fail('goal_not_found', 404);
513
+ const [members, criteria, dependencies] = await Promise.all([
514
+ db.listGoalMembers(id),
515
+ db.listCriteriaForGoal(id),
516
+ db.getGoalDependencies(id), // task 1761 — prerequisite goals + satisfied state
517
+ ]);
518
+ return res.json({ goal, members, criteria, dependencies });
519
+ } catch (err) {
520
+ console.error('[gds] GET /goals/:id', err);
521
+ return res.fail('fetch_failed', { status: 500, message: 'internal error' });
522
+ }
523
+ });
524
+
525
+ // POST /goals/:id/join — join an open goal.
526
+ // rank: any-builder — open join for a NORMAL goal. A PROTECTED-scope goal is
527
+ // Archon-mediated: a non-Archon cannot self-join (an Archon admits them via
528
+ // POST /goals/:id/members). Sub-Metic membership inside protected scope is
529
+ // deferred to the Phase-5 ADR (ADR 0086 Deferred).
530
+ router.post('/goals/:id/join', auth.requireBuilder, async (req, res) => {
531
+ const id = parseId(req, res, { code: 'bad_goal_id' });
532
+ if (id === null) return;
533
+ try {
534
+ const goal = await db.getGoal(id);
535
+ if (!goal) return res.fail('goal_not_found', 404);
536
+ if (goal.status !== 'open') {
537
+ return res.fail('goal_not_open', 409, { status: goal.status });
538
+ }
539
+ if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
540
+ return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; an Archon must add you (POST /goals/:id/members).' });
541
+ }
542
+ // Private goals are join-GATED (task 1952 / ADR 0112): a non-member can't
543
+ // self-join — they request to join or wait for an invite. An existing member
544
+ // re-joining is a harmless upsert, and an Archon may still self-join anything.
545
+ if (goal.visibility === 'private' && req.builder.rank !== 'archon') {
546
+ const alreadyMember = await db.isGoalMember({ goalId: id, builderId: req.builder.id });
547
+ if (!alreadyMember) {
548
+ return res.fail('goal_private', { status: 409, message: 'This goal is private — request to join (POST /goals/:id/requests) or wait for an invitation.' });
549
+ }
550
+ }
551
+ const member = await db.addGoalMember({ goalId: id, builderId: req.builder.id, role: 'member' });
552
+ return res.status(201).json({ ok: true, member });
553
+ } catch (err) {
554
+ console.error('[gds] POST /goals/:id/join', err);
555
+ return res.fail('join_failed', { status: 500, message: 'internal error' });
556
+ }
557
+ });
558
+
559
+ // POST /goals/:id/leave — leave a goal you're in.
560
+ // rank: any-builder.
561
+ router.post('/goals/:id/leave', auth.requireBuilder, async (req, res) => {
562
+ const id = parseId(req, res, { code: 'bad_goal_id' });
563
+ if (id === null) return;
564
+ try {
565
+ const { removed } = await db.removeGoalMember({ goalId: id, builderId: req.builder.id });
566
+ if (!removed) return res.fail('not_a_member', 409);
567
+ return res.json({ ok: true });
568
+ } catch (err) {
569
+ console.error('[gds] POST /goals/:id/leave', err);
570
+ return res.fail('leave_failed', { status: 500, message: 'internal error' });
571
+ }
572
+ });
573
+
574
+ // PATCH /goals/:id/visibility — make a goal public|private (task 1952 / ADR 0112).
575
+ // The owner, a manager, or an Archon (authorizeGoalManagement) flips it; a private
576
+ // goal is join-gated (see POST /goals/:id/join). 'public' is the default.
577
+ // rank: any-builder (requireBuilder) + the in-handler owner|manager|Archon gate.
578
+ // Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
579
+ router.patch('/goals/:id/visibility', auth.requireBuilder, async (req, res) => {
580
+ const id = parseId(req, res, { code: 'bad_goal_id' });
581
+ if (id === null) return;
582
+ if (validateOrRespond(req, res, { visibility: { required: true, type: 'string' } })) return;
583
+ const visibility = req.body.visibility;
584
+ if (!VALID_VISIBILITIES.includes(visibility)) {
585
+ return res.fail('bad_visibility', 400, { valid: VALID_VISIBILITIES });
586
+ }
587
+ try {
588
+ const goal = await db.getGoal(id);
589
+ const actorMembership = goal
590
+ ? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
591
+ : null;
592
+ const decision = authorizeGoalManagement({
593
+ goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
594
+ });
595
+ if (!decision.ok) return res.status(decision.status).json(decision.body);
596
+ const updated = await db.setGoalVisibility({ goalId: id, visibility });
597
+ return res.json({ ok: true, goal: updated });
598
+ } catch (err) {
599
+ console.error('[gds] PATCH /goals/:id/visibility', err);
600
+ return res.fail('visibility_update_failed', { status: 500, message: 'internal error' });
601
+ }
602
+ });
603
+
604
+ // POST /goals/:id/requests — a builder asks to join a (private or protected)
605
+ // goal (task 1952 / ADR 0112). Refused on a public, non-protected goal (that's
606
+ // open-join — just POST /goals/:id/join). Creates a pending kind='request' the
607
+ // goal's owner/manager/Archon approves from their inbox. The pending-unique index
608
+ // (one open ask per goal+builder) surfaces as 409 request_pending.
609
+ // rank: any-builder (requireBuilder) + the in-handler authorizeCreateJoinRequest.
610
+ router.post('/goals/:id/requests', auth.requireBuilder, async (req, res) => {
611
+ const id = parseId(req, res, { code: 'bad_goal_id' });
612
+ if (id === null) return;
613
+ if (validateOrRespond(req, res, { note: { type: 'string', maxLength: LIMITS.NOTES } })) return;
614
+ try {
615
+ const goal = await db.getGoal(id);
616
+ const isMember = goal ? await db.isGoalMember({ goalId: id, builderId: req.builder.id }) : false;
617
+ const scopeProtected = goal ? scopeIncludesProtected(goal.scope_modules) : false;
618
+ const decision = authorizeCreateJoinRequest({ goal, isMember, scopeProtected });
619
+ if (!decision.ok) return res.status(decision.status).json(decision.body);
620
+ const request = await db.createMembershipRequest({
621
+ goalId: id, builderId: req.builder.id, kind: 'request',
622
+ createdBy: req.builder.id, note: (req.body.note || '').trim() || null,
623
+ });
624
+ return res.status(201).json({ ok: true, request });
625
+ } catch (err) {
626
+ if (err && err.code === 'PENDING_EXISTS') {
627
+ return res.fail('request_pending', { status: 409, message: 'You already have a pending request or invitation for this goal.' });
628
+ }
629
+ console.error('[gds] POST /goals/:id/requests', err);
630
+ return res.fail('request_failed', { status: 500, message: 'internal error' });
631
+ }
632
+ });
633
+
634
+ // POST /goals/:id/requests/:reqId/respond { action: approve|deny } — an owner|
635
+ // manager|Archon resolves a pending join-request (task 1952). approve → the
636
+ // requester is added as a member; deny → the row is rejected (kept as the audit
637
+ // trail). The membership add is atomic with the resolve (resolveMembershipRequest
638
+ // moves only from status='pending'). A protected-scope goal still admits only
639
+ // Metic+ via an Archon — the same wall as invite + POST /members.
640
+ // rank: any-builder (requireBuilder) + the in-handler authorizeJoinRequestResponse.
641
+ router.post('/goals/:id/requests/:reqId/respond', auth.requireBuilder, async (req, res) => {
642
+ const id = parseId(req, res, { code: 'bad_goal_id' });
643
+ if (id === null) return;
644
+ const reqId = parseId(req, res, { param: 'reqId', code: 'bad_request_id', positiveInt: true });
645
+ if (reqId === null) return;
646
+ if (validateOrRespond(req, res, { action: { required: true, type: 'string' } })) return;
647
+ const action = req.body.action;
648
+ if (!JOIN_REQUEST_RESPONSE_ACTIONS.includes(action)) {
649
+ return res.fail('bad_action', 400, { valid: JOIN_REQUEST_RESPONSE_ACTIONS });
650
+ }
651
+ try {
652
+ const goal = await db.getGoal(id);
653
+ const [actorMembership, request] = goal
654
+ ? await Promise.all([
655
+ db.getGoalMember({ goalId: id, builderId: req.builder.id }),
656
+ db.getMembershipRequest(reqId),
657
+ ])
658
+ : [null, null];
659
+ const decision = authorizeJoinRequestResponse({
660
+ goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership, request, goalId: id,
661
+ });
662
+ if (!decision.ok) return res.status(decision.status).json(decision.body);
663
+
664
+ if (action === 'deny') {
665
+ const resolved = await db.resolveMembershipRequest({ id: reqId, status: 'rejected', resolvedBy: req.builder.id });
666
+ if (!resolved) return res.fail('not_pending', { status: 409, message: 'The request changed underneath this response — re-read and retry.' });
667
+ return res.json({ ok: true, action, request: resolved });
668
+ }
669
+
670
+ // approve — the protected-scope admission wall, read against the goal's
671
+ // CURRENT scope (re-fetched here, not the top-of-handler read, so an Archon
672
+ // re-scope since then is seen — the invitation-accept path's pattern). The
673
+ // residual micro-race with a concurrent re-scope is defense-in-depth: ADR
674
+ // 0086 §7 makes the SHIP-TIME gates authoritative on the real diff, and this
675
+ // mirrors the POST /goals/:id/members admission path — a stale membership row
676
+ // never grants actual protected-code access.
677
+ const freshGoal = (await db.getGoal(id)) || goal;
678
+ if (scopeIncludesProtected(freshGoal.scope_modules)) {
679
+ const target = await api.getBuilderById(request.builder_id);
680
+ const block = protectedScopeAdmissionBlock({
681
+ scopeProtected: true, actorRank: req.builder.rank, targetRank: target && target.rank,
682
+ });
683
+ if (block) return res.status(block.status).json(block.body);
684
+ }
685
+ // Resolve + add atomically (one statement); the requester (request.builder_id)
686
+ // becomes the member. null = the request was resolved out from under us → 409.
687
+ const outcome = await db.acceptMembershipRequestAndAddMember({ id: reqId, resolvedBy: req.builder.id });
688
+ if (!outcome) return res.fail('not_pending', { status: 409, message: 'The request changed underneath this response — re-read and retry.' });
689
+ return res.json({ ok: true, action, request: outcome.request, member: outcome.member });
690
+ } catch (err) {
691
+ console.error('[gds] POST /goals/:id/requests/:reqId/respond', err);
692
+ return res.fail('request_respond_failed', { status: 500, message: 'internal error' });
693
+ }
694
+ });
695
+
696
+ // POST /goals/:id/invitations { builder_id, note? } — an owner|manager|Archon
697
+ // invites a builder (task 1952). Creates a PENDING kind='invite' the invitee
698
+ // accepts from their inbox (a consented add — distinct from POST /goals/:id/members,
699
+ // the immediate force-add, kept as-is). Protected-scope invites keep the Archon-
700
+ // actor + Metic+-target wall (protectedScopeAdmissionBlock). 409 if the target is
701
+ // already a member or already has a pending row.
702
+ // rank: any-builder (requireBuilder) + the in-handler owner|manager|Archon gate.
703
+ router.post('/goals/:id/invitations', auth.requireBuilder, async (req, res) => {
704
+ const id = parseId(req, res, { code: 'bad_goal_id' });
705
+ if (id === null) return;
706
+ if (validateOrRespond(req, res, {
707
+ note: { type: 'string', maxLength: LIMITS.NOTES },
708
+ builder_id: {}, // number or numeric string (coerced below); untyped for strict-mode
709
+ })) return;
710
+ // builder_id accepts a JSON number OR a numeric string (the /members precedent —
711
+ // pg serializes bigint ids as strings, so a client echoing an id would fail an
712
+ // integer-typed validator).
713
+ const builderId = Number(req.body.builder_id);
714
+ if (!Number.isInteger(builderId) || builderId <= 0) {
715
+ return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
716
+ }
717
+ try {
718
+ const goal = await db.getGoal(id);
719
+ const actorMembership = goal
720
+ ? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
721
+ : null;
722
+ const decision = authorizeGoalManagement({
723
+ goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
724
+ });
725
+ if (!decision.ok) return res.status(decision.status).json(decision.body);
726
+ const target = await api.getBuilderById(builderId);
727
+ if (!target) return res.fail('builder_not_found', 404);
728
+ // Protected-scope wall against the goal's CURRENT scope (re-fetched, not the
729
+ // top-of-handler read — matches the respond paths). Residual re-scope race is
730
+ // defense-in-depth (ADR 0086 §7 — ship-time gates authoritative; mirrors
731
+ // POST /goals/:id/members).
732
+ const freshGoal = (await db.getGoal(id)) || goal;
733
+ const block = protectedScopeAdmissionBlock({
734
+ scopeProtected: scopeIncludesProtected(freshGoal.scope_modules),
735
+ actorRank: req.builder.rank, targetRank: target.rank,
736
+ });
737
+ if (block) return res.status(block.status).json(block.body);
738
+ if (await db.isGoalMember({ goalId: id, builderId })) {
739
+ return res.fail('already_a_member', { status: 409, message: 'That builder is already a member of this goal.' });
740
+ }
741
+ const request = await db.createMembershipRequest({
742
+ goalId: id, builderId, kind: 'invite',
743
+ createdBy: req.builder.id, note: (req.body.note || '').trim() || null,
744
+ });
745
+ return res.status(201).json({ ok: true, request });
746
+ } catch (err) {
747
+ if (err && err.code === 'PENDING_EXISTS') {
748
+ return res.fail('invite_pending', { status: 409, message: 'That builder already has a pending invitation or request for this goal.' });
749
+ }
750
+ console.error('[gds] POST /goals/:id/invitations', err);
751
+ return res.fail('invite_failed', { status: 500, message: 'internal error' });
752
+ }
753
+ });
754
+
755
+ // POST /goals/:id/invitations/:reqId/respond { action: accept|decline } — ONLY the
756
+ // invited builder resolves their OWN pending invite (task 1952). accept → they're
757
+ // added as a member; decline → the row is rejected. A protected-scope goal still
758
+ // requires a Metic+ member, re-checked against the accepting builder's current rank
759
+ // (the goal could have been re-scoped after the invite).
760
+ // rank: any-builder (requireBuilder) + the in-handler authorizeInvitationResponse.
761
+ router.post('/goals/:id/invitations/:reqId/respond', auth.requireBuilder, async (req, res) => {
762
+ const id = parseId(req, res, { code: 'bad_goal_id' });
763
+ if (id === null) return;
764
+ const reqId = parseId(req, res, { param: 'reqId', code: 'bad_request_id', positiveInt: true });
765
+ if (reqId === null) return;
766
+ if (validateOrRespond(req, res, { action: { required: true, type: 'string' } })) return;
767
+ const action = req.body.action;
768
+ if (!INVITE_RESPONSE_ACTIONS.includes(action)) {
769
+ return res.fail('bad_action', 400, { valid: INVITE_RESPONSE_ACTIONS });
770
+ }
771
+ try {
772
+ const request = await db.getMembershipRequest(reqId);
773
+ const decision = authorizeInvitationResponse({ request, goalId: id, actorId: req.builder.id });
774
+ if (!decision.ok) return res.status(decision.status).json(decision.body);
775
+
776
+ if (action === 'decline') {
777
+ const resolved = await db.resolveMembershipRequest({ id: reqId, status: 'rejected', resolvedBy: req.builder.id });
778
+ if (!resolved) return res.fail('not_pending', { status: 409, message: 'The invitation changed underneath this response — re-read and retry.' });
779
+ return res.json({ ok: true, action, request: resolved });
780
+ }
781
+
782
+ // accept — re-assert the protected-scope Metic+ membership invariant against
783
+ // the accepting builder's CURRENT rank (the goal may have been re-scoped since
784
+ // the invite). The invitee is consenting, so the actor-is-Archon half of the
785
+ // admission rule does NOT apply here — only the Metic+ membership floor does.
786
+ const goal = await db.getGoal(id);
787
+ if (goal && scopeIncludesProtected(goal.scope_modules) && !METIC_PLUS.includes(req.builder.rank)) {
788
+ return res.fail('metic_required_for_protected_scope', { status: 403, message: 'This goal is scoped to a protected module and requires a Metic+ member.' });
789
+ }
790
+ // Resolve + add atomically (one statement) — never an 'accepted' invite
791
+ // with no membership (the task-1952 review fix).
792
+ const outcome = await db.acceptMembershipRequestAndAddMember({ id: reqId, resolvedBy: req.builder.id });
793
+ if (!outcome) return res.fail('not_pending', { status: 409, message: 'The invitation changed underneath this response — re-read and retry.' });
794
+ return res.json({ ok: true, action, request: outcome.request, member: outcome.member });
795
+ } catch (err) {
796
+ console.error('[gds] POST /goals/:id/invitations/:reqId/respond', err);
797
+ return res.fail('invitation_respond_failed', { status: 500, message: 'internal error' });
798
+ }
799
+ });
800
+
801
+ // POST /goals/:id/members — the goal's own authority (owner | manager | Archon)
802
+ // invites a builder to the goal (task 1762: widened from Archon-only so a
803
+ // manager doesn't need an Archon in the loop for a normal goal). This stays the
804
+ // approval grant for a protected-scope goal, which a non-Archon cannot self-
805
+ // join AND a non-Archon cannot invite into (archon_required_for_protected_scope
806
+ // below) — protected-scope admission is deliberately Archon-mediated on BOTH
807
+ // sides of the grant. For a protected-scope goal the target must additionally
808
+ // be Metic+ (ADR 0086 Deferred: protected-scope goals require a Metic+ holder
809
+ // until the Phase-5 ADR).
810
+ // rank: any-builder (requireBuilder) + the in-handler (owner | manager |
811
+ // Archon) gate (authorizeGoalManagement) — like the sibling owner/manager
812
+ // routes, the wall is in-handler, NOT a rank middleware. Pinned in
813
+ // route-rank-check.EXPECTED_RANKS.
814
+ router.post('/goals/:id/members', auth.requireBuilder, async (req, res) => {
815
+ const id = parseId(req, res, { code: 'bad_goal_id' });
816
+ if (id === null) return;
817
+ if (validateOrRespond(req, res, {
818
+ role: { type: 'string' },
819
+ // builder_id accepts a JSON number OR a numeric string (coerced below);
820
+ // declared untyped so strict-mode accepts either form.
821
+ builder_id: {},
822
+ })) return;
823
+ // builder_id accepts a JSON number OR a numeric string: ids round-trip
824
+ // through JSON as strings (pg serializes bigint as a string), so a client
825
+ // echoing an id from a goal-members response would otherwise be rejected.
826
+ const builderId = Number(req.body.builder_id);
827
+ if (!Number.isInteger(builderId) || builderId <= 0) {
828
+ return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
829
+ }
830
+ const role = req.body.role || 'member';
831
+ if (!VALID_MEMBER_ROLES.includes(role)) {
832
+ return res.fail('bad_role', 400, { valid: VALID_MEMBER_ROLES });
833
+ }
834
+ try {
835
+ const goal = await db.getGoal(id);
836
+ const actorMembership = goal
837
+ ? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
838
+ : null;
839
+ const decision = authorizeGoalManagement({
840
+ goal,
841
+ actorId: req.builder.id,
842
+ actorRank: req.builder.rank,
843
+ actorMembership,
844
+ });
845
+ if (!decision.ok) {
846
+ return res.status(decision.status).json(decision.body);
847
+ }
848
+ // Protected-scope admission is Archon-mediated on the ACTOR side too — a
849
+ // goal's own owner/manager cannot use this widened grant to route around
850
+ // the protected-module wall (ADR 0086 §3/§4; mirrors the join-route check).
851
+ if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
852
+ return res.fail('archon_required_for_protected_scope', { status: 403, message: 'This goal is scoped to a protected module; only an Archon may add members.' });
853
+ }
854
+ const target = await api.getBuilderById(builderId);
855
+ if (!target) return res.fail('builder_not_found', 404);
856
+ if (scopeIncludesProtected(goal.scope_modules) && !METIC_PLUS.includes(target.rank)) {
857
+ return res.fail('metic_required_for_protected_scope', { status: 403, message: 'A protected-scope goal requires a Metic+ member; sub-Metic membership is deferred to the Phase-5 ADR.' });
858
+ }
859
+ const member = await db.addGoalMember({ goalId: id, builderId, role });
860
+ return res.status(201).json({ ok: true, member });
861
+ } catch (err) {
862
+ console.error('[gds] POST /goals/:id/members', err);
863
+ return res.fail('add_member_failed', { status: 500, message: 'internal error' });
864
+ }
865
+ });
866
+
867
+ // PATCH /goals/:id/members/:builderId — role management by the goal's own
868
+ // authority (task 1735): the owner, a manager, or an Archon promotes
869
+ // member→lead(manager); ONLY the owner (or an Archon) demotes; the owner's
870
+ // row is never demotable. The decision is the pure ordered gate above
871
+ // (authorizeMemberRoleChange); the DB write re-asserts the owner-demotion
872
+ // guard atomically (setGoalMemberRole), so a race cannot demote an owner.
873
+ // rank: any-builder (requireBuilder) + the in-handler (owner | manager |
874
+ // Archon) gate — like the authoring keystone, the wall is in-handler, NOT a
875
+ // rank middleware. Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
876
+ router.patch('/goals/:id/members/:builderId', auth.requireBuilder, async (req, res) => {
877
+ const id = parseId(req, res, { code: 'bad_goal_id' });
878
+ if (id === null) return;
879
+ const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
880
+ if (builderId === null) return;
881
+ if (validateOrRespond(req, res, {
882
+ role: { required: true, type: 'string' },
883
+ })) return;
884
+ const role = req.body.role;
885
+ if (!VALID_MEMBER_ROLES.includes(role)) {
886
+ return res.fail('bad_role', 400, { valid: VALID_MEMBER_ROLES });
887
+ }
888
+ try {
889
+ const goal = await db.getGoal(id);
890
+ // The gate's reads — only taken when the goal exists (a 404 needs none).
891
+ const [actorMembership, targetMembership] = goal
892
+ ? await Promise.all([
893
+ db.getGoalMember({ goalId: id, builderId: req.builder.id }),
894
+ db.getGoalMember({ goalId: id, builderId }),
895
+ ])
896
+ : [null, null];
897
+ const decision = authorizeMemberRoleChange({
898
+ goal,
899
+ actorId: req.builder.id,
900
+ actorRank: req.builder.rank,
901
+ actorMembership,
902
+ targetMembership,
903
+ targetId: builderId,
904
+ role,
905
+ });
906
+ if (!decision.ok) {
907
+ return res.status(decision.status).json(decision.body);
908
+ }
909
+ if (decision.noop) {
910
+ return res.json({ ok: true, member: targetMembership, changed: false });
911
+ }
912
+ const member = await db.setGoalMemberRole({ goalId: id, builderId, role });
913
+ if (!member) {
914
+ // The atomic backstop fired: the membership vanished, or ownership
915
+ // moved onto the target between the gate's read and this write.
916
+ // Honest conflict, not silent success.
917
+ return res.fail('role_change_conflict', { status: 409, message: 'The goal membership changed underneath this request — re-read the goal and retry.' });
918
+ }
919
+ return res.json({ ok: true, member, changed: true });
920
+ } catch (err) {
921
+ console.error('[gds] PATCH /goals/:id/members/:builderId', err);
922
+ return res.fail('role_change_failed', { status: 500, message: 'internal error' });
923
+ }
924
+ });
925
+
926
+ // POST /goals/:id/transfer — hand the goal to a new owner (task 1735). Owner
927
+ // (or Archon) only; the target must be Metic+ (ownership is creator-equivalent
928
+ // authority — see authorizeOwnershipTransfer). The DB move is atomic: created_by
929
+ // reassigns and the new owner's membership upserts to 'lead' in one statement;
930
+ // the OLD owner keeps their lead row — they drop to manager, not out.
931
+ // rank: any-builder (requireBuilder) + the in-handler (owner | Archon) gate.
932
+ // Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
933
+ router.post('/goals/:id/transfer', auth.requireBuilder, async (req, res) => {
934
+ const id = parseId(req, res, { code: 'bad_goal_id' });
935
+ if (id === null) return;
936
+ if (validateOrRespond(req, res, {
937
+ builder_id: {}, // new owner id — number or numeric string (coerced below)
938
+ })) return;
939
+ // builder_id accepts a JSON number OR a numeric string (the /members
940
+ // precedent — pg serializes bigint ids as strings).
941
+ const newOwnerId = Number(req.body && req.body.builder_id);
942
+ if (!Number.isInteger(newOwnerId) || newOwnerId <= 0) {
943
+ return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
944
+ }
945
+ try {
946
+ const goal = await db.getGoal(id);
947
+ const targetBuilder = goal ? await api.getBuilderById(newOwnerId) : null;
948
+ const decision = authorizeOwnershipTransfer({
949
+ goal,
950
+ actorId: req.builder.id,
951
+ actorRank: req.builder.rank,
952
+ targetBuilder,
953
+ });
954
+ if (!decision.ok) {
955
+ return res.status(decision.status).json(decision.body);
956
+ }
957
+ if (decision.noop) {
958
+ return res.json({ ok: true, goal, changed: false });
959
+ }
960
+ const updated = await db.transferGoalOwnership({ goalId: id, newOwnerId });
961
+ if (!updated) {
962
+ return res.fail('transfer_conflict', { status: 409, message: 'The goal changed underneath this request — re-read it and retry.' });
963
+ }
964
+ return res.json({ ok: true, goal: updated, changed: true });
965
+ } catch (err) {
966
+ console.error('[gds] POST /goals/:id/transfer', err);
967
+ return res.fail('transfer_failed', { status: 500, message: 'internal error' });
968
+ }
969
+ });
970
+
971
+ // GET /goals/:id/conflicts — the server-side task-conflict view (task 1736).
972
+ // Replaces the hall Goals page's client-side exact-string approximation (the
973
+ // page feature-detects on 404): glob-aware touches[] intersection via the
974
+ // canonical path-match machinery, claim-aware severity, and cross-goal
975
+ // overlaps against other OPEN goals sharing scope_modules. Detection only —
976
+ // resolution is the POST below.
977
+ // rank: any-builder — a read; the same audience as GET /goals/:id.
978
+ router.get('/goals/:id/conflicts', auth.requireBuilder, async (req, res) => {
979
+ const id = parseId(req, res, { code: 'bad_goal_id' });
980
+ if (id === null) return;
981
+ try {
982
+ const goal = await db.getGoal(id);
983
+ if (!goal) return res.fail('goal_not_found', 404);
984
+ const goalTasks = await db.listTasks({ goalId: id });
985
+ // Sibling scope: other open goals sharing at least one scope module. A
986
+ // scopeless goal (the migration-160 defaults) shares territory with
987
+ // nobody by declaration — only its own tasks are compared.
988
+ const scope = goal.scope_modules || [];
989
+ let crossGoal = [];
990
+ if (scope.length) {
991
+ const openGoals = await db.listGoals({ status: 'open' });
992
+ const siblings = openGoals.filter((g) =>
993
+ String(g.id) !== String(goal.id)
994
+ && (g.scope_modules || []).some((m) => scope.includes(m)));
995
+ crossGoal = await Promise.all(siblings.map(async (g) => ({
996
+ goal: g,
997
+ tasks: await db.listTasks({ goalId: g.id }),
998
+ })));
999
+ }
1000
+ const allIds = [...goalTasks, ...crossGoal.flatMap((c) => c.tasks)].map((t) => t.id);
1001
+ const activeClaims = await db.listActiveClaimsForTasks(allIds);
1002
+ const activeClaimTaskIds = new Set(activeClaims.map((c) => String(c.task_id)));
1003
+ const conflicts = goalConflicts.computeGoalConflicts({ goalTasks, activeClaimTaskIds, crossGoal });
1004
+ return res.json({
1005
+ ok: true,
1006
+ goal_id: Number(goal.id),
1007
+ conflicts,
1008
+ cross_goals_considered: crossGoal.map((c) => ({ id: Number(c.goal.id), title: c.goal.title })),
1009
+ });
1010
+ } catch (err) {
1011
+ console.error('[gds] GET /goals/:id/conflicts', err);
1012
+ return res.fail('conflicts_failed', { status: 500, message: 'internal error' });
1013
+ }
1014
+ });
1015
+
1016
+ // POST /goals/:id/conflicts/resolve — the owner/manager resolution actions
1017
+ // for a detected conflict (task 1736; authority model ADR 0106):
1018
+ // sequence {first_task_id, then_task_id} — order the work: `then`
1019
+ // gains a dependency on `first` via the ADR 0015 edge CRUD
1020
+ // (cycle-checked), so the claim gate serializes them.
1021
+ // flag_release {task_id, note?} — soft-nudge the active claim holder to
1022
+ // wrap up/release; surfaces on their own /me claim reads.
1023
+ // rescope {task_id, touches[]} — rewrite the DECLARED blast radius at
1024
+ // planning time (ADR 0049 advisory semantics). Refused while
1025
+ // the task holds an active claim — mid-claim touches rewriting
1026
+ // is the drift-expand task 879 removed; flag_release instead.
1027
+ // All targets must belong to THIS goal (containment — the parent-attach
1028
+ // precedent: a manager can't reshape another goal's tasks through this route).
1029
+ // rank: any-builder (requireBuilder) + the in-handler (owner | manager |
1030
+ // Archon) gate (authorizeGoalManagement). Pinned in
1031
+ // route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
1032
+ router.post('/goals/:id/conflicts/resolve', auth.requireBuilder, async (req, res) => {
1033
+ const id = parseId(req, res, { code: 'bad_goal_id' });
1034
+ if (id === null) return;
1035
+ if (validateOrRespond(req, res, {
1036
+ action: { required: true, type: 'string' },
1037
+ note: { type: 'string', maxLength: LIMITS.NOTES },
1038
+ touches: { type: 'array', maxItems: 500, itemsType: 'string' },
1039
+ // Task-id operands for the sequence/dedup actions (number or numeric
1040
+ // string, resolved below) — declared so strict-mode accepts them.
1041
+ first_task_id: {},
1042
+ then_task_id: {},
1043
+ task_id: {},
1044
+ })) return;
1045
+ const action = req.body.action;
1046
+ if (!RESOLVE_ACTIONS.includes(action)) {
1047
+ return res.fail('bad_action', 400, { valid: RESOLVE_ACTIONS });
1048
+ }
1049
+ try {
1050
+ const goal = await db.getGoal(id);
1051
+ const actorMembership = goal
1052
+ ? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
1053
+ : null;
1054
+ const decision = authorizeGoalManagement({
1055
+ goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
1056
+ });
1057
+ if (!decision.ok) {
1058
+ return res.status(decision.status).json(decision.body);
1059
+ }
1060
+
1061
+ // Containment: resolve a task id from the body and require it to live in
1062
+ // THIS goal. Accepts a number or a numeric string (ids round-trip through
1063
+ // JSON as strings — the /members precedent).
1064
+ const goalTask = async (raw, field) => {
1065
+ const tid = Number(raw);
1066
+ if (!Number.isInteger(tid) || tid <= 0) {
1067
+ return { err: { status: 400, body: { error: 'bad_task_id', field } } };
1068
+ }
1069
+ const task = await db.getTask(tid);
1070
+ if (!task) {
1071
+ return { err: { status: 404, body: { error: 'task_not_found', task_id: tid } } };
1072
+ }
1073
+ if (String(task.goal_id ?? '') !== String(goal.id)) {
1074
+ return { err: { status: 400, body: {
1075
+ error: 'task_outside_goal', task_id: tid,
1076
+ message: 'Resolution actions apply only to this goal\'s own tasks.',
1077
+ } } };
1078
+ }
1079
+ return { task };
1080
+ };
1081
+
1082
+ if (action === 'sequence') {
1083
+ const first = await goalTask(req.body.first_task_id, 'first_task_id');
1084
+ if (first.err) return res.status(first.err.status).json(first.err.body);
1085
+ const then = await goalTask(req.body.then_task_id, 'then_task_id');
1086
+ if (then.err) return res.status(then.err.status).json(then.err.body);
1087
+ try {
1088
+ await db.addTaskDependency(then.task.id, first.task.id);
1089
+ } catch (err) {
1090
+ if (err && err.code === 'SELF_DEP') return res.fail('self_dep', 400);
1091
+ if (err && err.code === 'CYCLE') return res.fail('cycle', 409, { path: err.path });
1092
+ if (err && err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
1093
+ throw err;
1094
+ }
1095
+ const dependencies = await db.getTaskDependencies(then.task.id);
1096
+ return res.json({
1097
+ ok: true, action,
1098
+ first_task_id: Number(first.task.id), then_task_id: Number(then.task.id),
1099
+ dependencies,
1100
+ });
1101
+ }
1102
+
1103
+ if (action === 'flag_release') {
1104
+ const t = await goalTask(req.body.task_id, 'task_id');
1105
+ if (t.err) return res.status(t.err.status).json(t.err.body);
1106
+ const flagged = await db.requestClaimRelease({
1107
+ taskId: t.task.id,
1108
+ requestedBy: req.builder.id,
1109
+ note: (req.body.note || '').trim() || null,
1110
+ });
1111
+ if (!flagged) {
1112
+ return res.fail('no_active_claim', { status: 409, message: 'That task holds no active claim to flag.' });
1113
+ }
1114
+ return res.json({ ok: true, action, claim: flagged });
1115
+ }
1116
+
1117
+ // action === 'rescope'
1118
+ const t = await goalTask(req.body.task_id, 'task_id');
1119
+ if (t.err) return res.status(t.err.status).json(t.err.body);
1120
+ const touches = req.body.touches;
1121
+ if (!Array.isArray(touches) || touches.length === 0) {
1122
+ return res.fail('touches_required', { status: 400, message: 'rescope needs a non-empty touches[] — the declared blast radius.' });
1123
+ }
1124
+ // The SAME scope wall the R60 create-time gate runs (goal-scope-check
1125
+ // VERBATIM): rescope is a second authoring surface for the touches field,
1126
+ // so without this a member could author an in-scope task and then rewrite
1127
+ // it out of the goal's territory (or at protected paths) past the wall.
1128
+ // Bounds everyone (scope-subset), floors sub-Metic off protected paths.
1129
+ const wall = goalScopeCheck.check(goal.scope_modules, touches, req.builder.rank);
1130
+ if (!wall.ok) {
1131
+ return res.fail(wall.code, 403, { reason: wall.reason, details: wall.details });
1132
+ }
1133
+ const active = await db.listActiveClaimsForTasks([t.task.id]);
1134
+ if (active.length) {
1135
+ return res.fail('task_claimed', { status: 409, message: 'That task has an active claim — ask the holder to wrap up (flag_release) instead of rescoping under them.' });
1136
+ }
1137
+ const updated = await db.rescopeTaskTouches({
1138
+ taskId: t.task.id,
1139
+ touches,
1140
+ securitySensitive: t.task.security_sensitive === true,
1141
+ currentRequiresRank: t.task.requires_rank ?? 'xenos',
1142
+ });
1143
+ if (!updated) {
1144
+ return res.fail('rescope_conflict', { status: 409, message: 'The task changed underneath this request — re-read and retry.' });
1145
+ }
1146
+ return res.json({
1147
+ ok: true, action,
1148
+ task_id: Number(updated.id),
1149
+ touches: updated.touches,
1150
+ requires_rank: updated.requires_rank,
1151
+ });
1152
+ } catch (err) {
1153
+ console.error('[gds] POST /goals/:id/conflicts/resolve', err);
1154
+ return res.fail('conflict_resolve_failed', { status: 500, message: 'internal error' });
1155
+ }
1156
+ });
1157
+
1158
+ // POST /goals/:id/archive — retire a goal (open|achieved → archived). Archived
1159
+ // goals are re-openable (below). This is the lifecycle disposition the /goal-review
1160
+ // skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64).
1161
+ // rank: metic+archon — a goal member (Metic+) or an Archon dispositions the goal.
1162
+ router.post('/goals/:id/archive', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
1163
+ const id = parseId(req, res, { code: 'bad_goal_id' });
1164
+ if (id === null) return;
1165
+ try {
1166
+ const goal = await db.getGoal(id);
1167
+ if (!goal) return res.fail('goal_not_found', 404);
1168
+ if (!(await isMemberOrArchon(req.builder, id))) {
1169
+ return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to disposition it.' });
1170
+ }
1171
+ if (goal.status === 'archived') {
1172
+ return res.fail('already_archived', 409);
1173
+ }
1174
+ const updated = await db.setGoalStatus({ goalId: id, status: 'archived' });
1175
+ return res.json({ ok: true, goal: updated });
1176
+ } catch (err) {
1177
+ console.error('[gds] POST /goals/:id/archive', err);
1178
+ return res.fail('archive_failed', { status: 500, message: 'internal error' });
1179
+ }
1180
+ });
1181
+
1182
+ // POST /goals/:id/reopen — bring an archived goal back to open (ADR 0086 §6 /
1183
+ // BV1.R64). Only archived → open; an already open/achieved goal is a 409.
1184
+ // rank: metic+archon — same gate as archive.
1185
+ router.post('/goals/:id/reopen', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
1186
+ const id = parseId(req, res, { code: 'bad_goal_id' });
1187
+ if (id === null) return;
1188
+ try {
1189
+ const goal = await db.getGoal(id);
1190
+ if (!goal) return res.fail('goal_not_found', 404);
1191
+ if (!(await isMemberOrArchon(req.builder, id))) {
1192
+ return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to reopen it.' });
1193
+ }
1194
+ if (goal.status !== 'archived') {
1195
+ return res.fail('not_archived', 409, { status: goal.status });
1196
+ }
1197
+ const updated = await db.setGoalStatus({ goalId: id, status: 'open' });
1198
+ return res.json({ ok: true, goal: updated });
1199
+ } catch (err) {
1200
+ console.error('[gds] POST /goals/:id/reopen', err);
1201
+ return res.fail('reopen_failed', { status: 500, message: 'internal error' });
1202
+ }
1203
+ });
1204
+
1205
+ // POST /goals/:id/scope — "ask to widen": add module(s) to a goal's scope_modules
1206
+ // (ADR 0086 §4 / BV1.R64). Scope is NEVER self-granted, so this is the gated
1207
+ // approve-and-apply: requireRank('metic','archon') + the caller must be a member
1208
+ // (or Archon), and a PROTECTED module can only be added by an Archon (mirrors the
1209
+ // POST /goals create gate). A sub-Metic member therefore cannot widen at all — they
1210
+ // ask a Metic+/Archon out-of-band, exactly as joining a protected goal works (R57).
1211
+ // The goal must be open. Body: { add: ["module-key", ...] }; idempotent (set-union).
1212
+ // rank: metic+archon — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.
1213
+ router.post('/goals/:id/scope', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
1214
+ const id = parseId(req, res, { code: 'bad_goal_id' });
1215
+ if (id === null) return;
1216
+ if (validateOrRespond(req, res, {
1217
+ add: { required: true, type: 'array', maxItems: 64, itemsType: 'string' },
1218
+ })) return;
1219
+ const add = [...new Set(req.body.add)];
1220
+ if (add.length === 0) {
1221
+ return res.fail('empty_add', { status: 400, message: 'add must name at least one module.' });
1222
+ }
1223
+ const unknown = add.filter((k) => !isModuleKey(k));
1224
+ if (unknown.length) {
1225
+ return res.fail('unknown_module', 400, { unknown, valid: moduleKeys() });
1226
+ }
1227
+ // The wall: a protected module can only be added by an Archon — the scope is set
1228
+ // by a trusted rank, never self-granted (ADR 0086 §4).
1229
+ if (scopeIncludesProtected(add) && req.builder.rank !== 'archon') {
1230
+ return res.fail('archon_required_for_protected_scope', { status: 403, message: 'Adding a protected module to a goal scope can only be done by an Archon.' });
1231
+ }
1232
+ try {
1233
+ const goal = await db.getGoal(id);
1234
+ if (!goal) return res.fail('goal_not_found', 404);
1235
+ if (goal.status !== 'open') {
1236
+ return res.fail('goal_not_open', 409, { status: goal.status });
1237
+ }
1238
+ if (!(await isMemberOrArchon(req.builder, id))) {
1239
+ return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to widen its scope.' });
1240
+ }
1241
+ const updated = await db.addGoalScopeModules({ goalId: id, modules: add });
1242
+ return res.json({ ok: true, goal: updated });
1243
+ } catch (err) {
1244
+ console.error('[gds] POST /goals/:id/scope', err);
1245
+ return res.fail('scope_widen_failed', { status: 500, message: 'internal error' });
1246
+ }
1247
+ });
1248
+
1249
+ // POST /goals/:id/tasks — bounded task authoring within a goal's scope
1250
+ // (BV1.R60 / ADR 0086 §3). THE ENFORCEMENT KEYSTONE of the goal-scoped work
1251
+ // hierarchy: the one route that lets a member (even a sub-Metic one) author
1252
+ // their OWN work — bounded by the goal's module wall, and never able to mint
1253
+ // work they couldn't otherwise be trusted to claim.
1254
+ //
1255
+ // requireBuilder + the in-handler, ORDERED, FAIL-CLOSED gate
1256
+ // (authorizeGoalTaskCreate above). On pass, db.createTask stamps goal_id with
1257
+ // requires_rank AUTO-DERIVED (ADR 0084) — NEVER taken from client input: a
1258
+ // protected/sensitive task auto-floors to Metic, so a sub-Metic author simply
1259
+ // cannot claim it (there is no self-granted escape). The task's version is the
1260
+ // GOAL's version (never client-supplied) and it is born 'ready' so the author
1261
+ // can immediately claim + work it — the maneuverability grant (the scope wall +
1262
+ // auto-rank-floor already bound WHAT they may create). One-level sub-tasks
1263
+ // attach via hierarchy.attachParent (the depth-cap + version-match authority),
1264
+ // constrained to the SAME goal.
1265
+ //
1266
+ // rank: any-builder (requireBuilder) + goal membership + the scope wall. Pinned
1267
+ // in route-rank-check.EXPECTED_RANKS['lifecycle'] by BV1.R61.
1268
+ router.post('/goals/:id/tasks', auth.requireBuilder, async (req, res) => {
1269
+ const id = parseId(req, res, { code: 'bad_goal_id' });
1270
+ if (id === null) return;
1271
+ if (validateOrRespond(req, res, {
1272
+ title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
1273
+ description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
1274
+ // touches[] is REQUIRED — it is the blast radius the scope wall checks. A
1275
+ // task with no declared touches can't be bounded to the goal, so the pure
1276
+ // check() denies it too (empty_touches); requiring it gives a clean 400 up
1277
+ // front rather than a 403.
1278
+ touches: { required: true, type: 'array', maxItems: 500, itemsType: 'string' },
1279
+ est_minutes: { type: 'integer', min: 0, max: 100000 },
1280
+ priority: { type: 'integer', min: TASK_FIELD_BOUNDS.priority.min, max: TASK_FIELD_BOUNDS.priority.max },
1281
+ kind: { type: 'string', maxLength: 64 },
1282
+ discipline: { type: 'string', maxLength: 64 },
1283
+ value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
1284
+ parent_task_id: { type: 'integer', min: 1 },
1285
+ // Deliberately NOT read from the client (validateOrRespond ignores unknown
1286
+ // fields, so any of these a client smuggles in are simply never used):
1287
+ // • version_id — derived from the goal (a member can't author cross-version);
1288
+ // • requires_rank — AUTO-derived (ADR 0084); this is the self-raised-rank
1289
+ // escape the whole model exists to prevent, so the route never forwards it;
1290
+ // • status — server-set to 'ready';
1291
+ // • security_sensitive / module_key — out of R60 scope.
1292
+ })) return;
1293
+
1294
+ const body = req.body || {};
1295
+ const touches = Array.isArray(body.touches) ? [...body.touches] : [];
1296
+
1297
+ try {
1298
+ // Fetch the inputs the ordered gate consults. version is the GOAL's version
1299
+ // (never client input). The couple of extra indexed reads on a reject path
1300
+ // are immaterial and keep the whole decision in ONE testable place.
1301
+ const goal = await db.getGoal(id);
1302
+ const isMember = goal ? await isMemberOrArchon(req.builder, id) : false;
1303
+ const version = goal ? await db.getVersion(goal.version_id) : null;
1304
+
1305
+ const decision = authorizeGoalTaskCreate({
1306
+ goal, version, isMember, builderRank: req.builder.rank, touches,
1307
+ });
1308
+ if (!decision.ok) {
1309
+ return res.status(decision.status).json(decision.body);
1310
+ }
1311
+
1312
+ // One-level sub-task (optional). Validate the parent BEFORE creating
1313
+ // anything, so a bad parent is a clean 4xx with no orphan task: it must
1314
+ // exist, live in THIS goal (containment — a member can't graft work onto
1315
+ // another goal's tree), and not itself be a sub-task (the depth cap; a fresh
1316
+ // task can have no children, so that half is trivially met).
1317
+ const parentTaskId = body.parent_task_id ?? null;
1318
+ if (parentTaskId !== null) {
1319
+ const parent = await db.getTask(parentTaskId);
1320
+ if (!parent) {
1321
+ return res.fail('parent_not_found', { status: 404, message: `No task #${parentTaskId} to attach under.` });
1322
+ }
1323
+ if (String(parent.goal_id ?? '') !== String(goal.id)) {
1324
+ return res.fail('parent_outside_goal', { status: 400, message: `Parent task #${parentTaskId} is not part of this goal — a sub-task attaches to a parent within the same goal.` });
1325
+ }
1326
+ if (parent.parent_task_id !== null) {
1327
+ return res.fail('parent_has_parent', { status: 409, message: `Task #${parentTaskId} is already a sub-task — the hierarchy is one level deep (ADR 0086 §2).` });
1328
+ }
1329
+ }
1330
+
1331
+ // Resolve kind/discipline like POST /tasks — a goal-authored task is a
1332
+ // first-class task, so classify it for routing/reward. Caller wins if valid;
1333
+ // otherwise the heuristic (confidence < 0.5 → 'unclassified' for triage).
1334
+ let kind = body.kind;
1335
+ if (kind && !VALID_KINDS.has(kind)) {
1336
+ return res.fail('bad_kind', 400, { valid: Array.from(VALID_KINDS) });
1337
+ }
1338
+ if (!kind || kind === 'unclassified') {
1339
+ const result = classifyTaskKind(body.title, body.description || '');
1340
+ kind = result.confidence >= 0.5 ? result.kind : 'unclassified';
1341
+ }
1342
+ const discipline = body.discipline ?? 'unclassified';
1343
+ if (!VALID_DISCIPLINES.has(discipline)) {
1344
+ return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
1345
+ }
1346
+
1347
+ // Create. version = the goal's (not client-supplied); status 'ready' so the
1348
+ // author can claim it immediately; requiresRank OMITTED → auto-derived
1349
+ // (ADR 0084), so a protected/sensitive task floors to Metic and a sub-Metic
1350
+ // author cannot claim it. goal_id is stamped. credits_reward omitted →
1351
+ // auto-assigned at creation (ADR 0096).
1352
+ const task = await db.createTask({
1353
+ versionId: goal.version_id,
1354
+ title: body.title,
1355
+ description: body.description || '',
1356
+ status: 'ready',
1357
+ touches,
1358
+ estMinutes: body.est_minutes ?? null,
1359
+ priority: body.priority ?? null,
1360
+ valueSummary: body.value_summary ?? null,
1361
+ kind,
1362
+ discipline,
1363
+ source: 'goal-authored',
1364
+ goalId: goal.id,
1365
+ });
1366
+
1367
+ // Attach the sub-task parent (validated above; attachParent re-checks the
1368
+ // depth cap + version match atomically under row locks — the authority). A
1369
+ // failure here is a rare race (parent gained a parent / was deleted between
1370
+ // the pre-check and the attach): the task exists as a top-level goal task, so
1371
+ // surface the attach failure honestly rather than pretend success.
1372
+ let attachedParentId = null;
1373
+ if (parentTaskId !== null) {
1374
+ try {
1375
+ const updated = await hierarchy.attachParent(task.id, parentTaskId);
1376
+ attachedParentId = updated.parent_task_id != null ? Number(updated.parent_task_id) : null;
1377
+ } catch (err) {
1378
+ console.error('[gds] POST /goals/:id/tasks attachParent', err);
1379
+ return res.fail('parent_attach_failed', { status: 409, message: `Task #${task.id} was created under the goal, but attaching it under parent #${parentTaskId} failed (${(err && err.code) || 'unknown'}).`, details: { code: err && err.code, task } });
1380
+ }
1381
+ }
1382
+
1383
+ return res.status(201).json({
1384
+ ok: true,
1385
+ task,
1386
+ goal_id: goal.id,
1387
+ parent_task_id: attachedParentId,
1388
+ credits_reward: task.credits_reward,
1389
+ requires_rank: task.requires_rank,
1390
+ });
1391
+ } catch (err) {
1392
+ console.error('[gds] POST /goals/:id/tasks', err);
1393
+ return res.fail('goal_task_create_failed', { status: 500, message: 'internal error' });
1394
+ }
1395
+ });
1396
+
1397
+ return router;
1398
+ };
1399
+
1400
+ // BV1.R60 / ADR 0086 §3: the pure ordered gate for POST /goals/:id/tasks —
1401
+ // exported for tests/goal_routes.mjs (DB-free) and the BV1.R61 abuse matrix, and
1402
+ // for reuse. The shouldRefuseSessionClaim (claims.js) precedent.
1403
+ module.exports.authorizeGoalTaskCreate = authorizeGoalTaskCreate;
1404
+ // task 1735: the pure ordered gates for PATCH /goals/:id/members/:builderId and
1405
+ // POST /goals/:id/transfer — exported for tests/goal_member_roles.mjs (DB-free),
1406
+ // same precedent.
1407
+ module.exports.authorizeMemberRoleChange = authorizeMemberRoleChange;
1408
+ module.exports.authorizeOwnershipTransfer = authorizeOwnershipTransfer;
1409
+ // task 1736: the shared management prefix gate (conflict resolution) —
1410
+ // exported for tests/goal_conflicts.mjs (DB-free), same precedent.
1411
+ module.exports.authorizeGoalManagement = authorizeGoalManagement;
1412
+ module.exports.RESOLVE_ACTIONS = RESOLVE_ACTIONS;
1413
+ // task 1952 / ADR 0112: the private-goal invite/join-request pure gates +
1414
+ // the shared protected-scope admission rule — exported for
1415
+ // tests/goal_membership_requests.mjs (DB-free), same precedent.
1416
+ module.exports.protectedScopeAdmissionBlock = protectedScopeAdmissionBlock;
1417
+ module.exports.authorizeCreateJoinRequest = authorizeCreateJoinRequest;
1418
+ module.exports.authorizeInvitationResponse = authorizeInvitationResponse;
1419
+ module.exports.authorizeJoinRequestResponse = authorizeJoinRequestResponse;