@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,1703 @@
1
+ const express = require('express');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ // Intra-module + the published doorway — a module never requires a core internal.
6
+ const api = require('../../../src/module-api');
7
+ const auth = api;
8
+ const db = require('../db');
9
+ const hierarchy = require('../hierarchy');
10
+ // grading carved to modules/grading/ (BV1.R73 / ADR 0091 §4): the lifecycle no
11
+ // longer imports the grader — it resolves the `grade` kernel PORT at grade time
12
+ // (seams.resolveOptional('grade'), in POST /tasks/:id/grade below).
13
+ const githubPush = require('../github-push');
14
+ const seams = api;
15
+ const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS } = require('../task-classifier');
16
+ const { isModuleKey, moduleKeys } = api.moduleScopeMap;
17
+ const { validateOrRespond, LIMITS, parseId } = api;
18
+ // The onboarding state machine + the evergreen newcomer-chore restock carved into
19
+ // modules/onboarding/ (BV1.R81 / ADR 0093 §1): GET /tasks/claimable marks
20
+ // first_start and POST /tasks/newcomer-restock tops up the chore shelf — both
21
+ // through the `onboarding` kernel port (seams.resolveOptional('onboarding')),
22
+ // never a direct require of a module file.
23
+ const { pool } = api;
24
+ const { branding } = api;
25
+
26
+ // ---- server-mediated branch publish (ADR 0055 / task 1025) -----------------
27
+ // Body caps for POST /tasks/:id/publish-branch. The branch arrives as a base64
28
+ // git bundle; base64 inflates ~33%, so the encoded string + JSON body limit sit
29
+ // above the 20 MB decoded ceiling. The decoded bytes are re-checked in the
30
+ // handler (the JSON `limit` only bounds the ENCODED payload — SR-17 posture).
31
+ const PUBLISH_BODY_LIMIT = '32mb';
32
+ const PUBLISH_BUNDLE_B64_MAX = 30 * 1024 * 1024; // validator guard on the b64 string
33
+ const PUBLISH_BUNDLE_MAX_BYTES = 20 * 1024 * 1024; // 20 MB decoded bundle
34
+
35
+ // De-`#` text bound for a GitHub-rendered surface (PR title/body): a bare "#NNN"
36
+ // autolinks to GitHub's own issue space (dead links for GDS task ids — task 916).
37
+ // Mirrors ship.js sanitizeGitHubRefs (which lives in scripts/ and isn't requirable
38
+ // here).
39
+ function deHashRefs(text) {
40
+ return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
41
+ }
42
+
43
+ // tasks.discipline — orthogonal to kind. See migrations 014 + 086.
44
+ // VALID_DISCIPLINES (engineer = builds code · artist = makes visual assets ·
45
+ // ideator = thinks/writes/scopes · unclassified = un-triaged) is imported above
46
+ // from ../task-classifier — the single source (D1).
47
+
48
+ // ?fit= on /tasks/claimable — narrows to tasks the autonomous overnight
49
+ // session-runner (#358) may pick up. See db.listClaimableTasks.
50
+ const VALID_FITS = new Set(['autonomous', 'parallel']);
51
+
52
+ // V3.R48 (#267): auto-detect schema-change tasks so the touches[] scanner
53
+ // reserves a migration number even when the builder forgets to list
54
+ // 'migrations/'. The keyword set is intentionally conservative — it matches
55
+ // whole tokens ("migration"/"migrations", "schema") and the two SQL phrases
56
+ // that almost always imply a migration, so unrelated words ("schematic",
57
+ // "reschema", a button color) don't false-positive. Case-insensitive.
58
+ //
59
+ // Migration-keyword detection. POST /tasks uses this to set the
60
+ // tasks.needs_migration boolean (migration 081, touches[] cleanup [1/8] /
61
+ // task 876), which is what the claim-time allocator now keys off — replacing
62
+ // the old "inject 'migrations/' into touches[]" sentinel. Conservative,
63
+ // whole-token, case-insensitive. The former scripts/gds/capture.js mirror was
64
+ // deleted with the same task: captured ideas have no touches[] and the server
65
+ // re-detects on create, so the CLI copy earned its keep no longer.
66
+ const MIGRATION_KEYWORD_RE =
67
+ /\bmigrations?\b|\bschema\b|\balter\s+table\b|\bcreate\s+table\b/i;
68
+
69
+ function detectsMigration(...texts) {
70
+ return texts.some((t) => typeof t === 'string' && MIGRATION_KEYWORD_RE.test(t));
71
+ }
72
+
73
+ // ---- peer votes (V3.R25 #245) ---------------------------------------------
74
+ // The peer-vote SQL lives here (inline, via the route module's `pool`) rather
75
+ // than in db.js on purpose: it's a self-contained feature, declared in this
76
+ // task's touches[] (migrations/, this file, modules/hall-ui/), and keeping it
77
+ // out of the large shared db.js avoids a parallel-safety conflict on that file.
78
+ // Same deliberate-locality call as the MIGRATION_KEYWORD_RE mirror above.
79
+
80
+ // VOTABLE_TASK_STATUSES — a task is votable once it's verified work the project
81
+ // keeps (confirmed = past the grader, credits landed; or shipped = deployed);
82
+ // backlog/ready/completed are not. Imported above from ../task-classifier (D1).
83
+
84
+ // #853 (ADR 0041): temporary, default-OFF global grader bypass. When the env
85
+ // GRADER_BYPASS_ENABLED is exactly '1', the subagent quality grader is treated
86
+ // as unavailable and any builder may land work via /tasks/:id/confirm (normally
87
+ // archon-only). Read live on each request — flipping the env (+ redeploy) is the
88
+ // on/off switch; no caching, so turning it off re-arms the trust boundary at
89
+ // once. Exported so /me can surface the same flag to ship.js.
90
+ function graderBypassEnabled() {
91
+ return process.env.GRADER_BYPASS_ENABLED === '1';
92
+ }
93
+
94
+ // SECURITY (#871): claim-ownership gate for the lifecycle-advancing routes
95
+ // (/grade, /confirm, /ship). These are requireBuilder-only (the grade is meant
96
+ // to be an objective subagent output; the supervisor hot path must not need an
97
+ // Archon), which — combined with applyGrade/confirmTask crediting work — let any
98
+ // authenticated builder act on ANOTHER builder's task:
99
+ // * /grade (requireBuilder, no rank gate): a builder could POST a fabricated
100
+ // passing grade on someone else's `completed` task. Pre-#871 applyGrade then
101
+ // credited the CALLER → cross-builder credit theft + ship-attribution hijack
102
+ // + a full bypass of the quality grader.
103
+ // * /confirm: archon-only normally, but the GRADER_BYPASS killswitch (ON
104
+ // during the grader outage) relaxes it to ANY builder, so anyone could
105
+ // force-confirm anyone's `completed` work past the (absent) grader.
106
+ // * /ship: any-builder supervisor hot path (so /merge-mode can land the queue).
107
+ //
108
+ // Allow when: the caller is an Archon (full authority / --regrade); OR — for
109
+ // /ship only (allowSupervisor) — the caller is Metic, the trusted rank that runs
110
+ // /merge-mode to sweep the confirmed queue across builders; OR the caller is the
111
+ // task's most-recent claim holder (the builder who did the work).
112
+ //
113
+ // A NEVER-CLAIMED task (degenerate state, e.g. status set directly via SQL) is
114
+ // NOT an open door: the Archon and supervisor early-returns above already cover
115
+ // the legitimate actors who land such a task. By the time we read the claim
116
+ // holder, the caller is necessarily a non-Archon (and not a supervisor Metic) —
117
+ // so a missing claim must FAIL CLOSED with the same 403, not silently ALLOW.
118
+ // (L2 residual, red-team 2026-06-12: combined with the GRADER_BYPASS relaxation
119
+ // on /confirm, the old "no claim → ALLOW" let any builder confirm a claimless
120
+ // `completed` task and have the credit fall to themselves.) An Archon (or the
121
+ // /merge-mode supervisor) can still land a claimless task — they pass above.
122
+ // Returns true if allowed; otherwise writes a 403 and returns false.
123
+ async function gateTaskOwnership(req, res, taskId, { allowSupervisor = false } = {}) {
124
+ const rank = req.builder && req.builder.rank;
125
+ if (rank === 'archon') return true;
126
+ if (allowSupervisor && rank === 'metic') return true;
127
+ const holder = await db.getLastClaimHolder(taskId);
128
+ if (holder && String(holder.builder_id) === String(req.builder.id)) return true;
129
+ res.fail('not_your_task', { status: 403, message: 'Only the task\'s claim holder (or an Archon) may grade/confirm/ship it.' });
130
+ return false;
131
+ }
132
+
133
+ // task 1053 (F): a verification task — one whose deliverable IS a verification
134
+ // (e.g. a hardware-verify run). Confirming one requires an explicit attestation
135
+ // that the verify actually passed, so a hardware-verify can't be confirmed on a
136
+ // passing code-grade before the verify was even performed (the #1027 trap).
137
+ // Keyed on the explicit kind (set by the planner), NOT a title regex — a task
138
+ // ABOUT verify work would false-positive on text. Pure + exported for tests.
139
+ function isVerifyTask(task) {
140
+ return !!task && task.kind === 'verify';
141
+ }
142
+
143
+ // task 1139 / 1169 (ADR 0058): the trustless server rank-attestation behind the
144
+ // gate-review CI check. Only the SERVER can set it — it holds the authenticated
145
+ // builder's server-resolved rank AND the App/PAT credential — so CI can trust it
146
+ // where a PR-author-controlled signal (a PR title) would be spoofable. A Metic+
147
+ // author's gate-author-trust=success clears a NEEDS_TRUST gate surface with no
148
+ // owner click; below Metic gets no attestation (the gate escalates → owner review).
149
+ //
150
+ // rankAttestsGateTrust is the PURE rank decision (no network) — exported for tests.
151
+ function rankAttestsGateTrust(rank) {
152
+ const { tierOf, MIN_TIER } = api.permissionPathCheck;
153
+ const tier = tierOf(rank);
154
+ return tier != null && tier >= MIN_TIER;
155
+ }
156
+
157
+ // postGateAuthorTrust — set (or skip) the gate-author-trust commit status for an
158
+ // author's rank on a pushed SHA. Shared by the server-mediated publish path
159
+ // (POST /tasks/:id/publish-branch) and the laptop ci-land path's attest endpoint
160
+ // (POST /tasks/:id/attest-gate). Best-effort + never throws: a failed/skipped
161
+ // status post leaves the PR un-attested, which ESCALATES (the safe direction — no
162
+ // rank bypass without a real server signal). Returns { attested, reason } for UX.
163
+ async function postGateAuthorTrust({ rank, sha, taskId }) {
164
+ if (!rankAttestsGateTrust(rank)) return { attested: false, reason: 'rank_below_metic' };
165
+ try {
166
+ const ok = await githubPush.setCommitStatus({
167
+ sha,
168
+ state: 'success',
169
+ context: 'gate-author-trust',
170
+ description: `Metic+ author (${rank}) — task ${taskId}`,
171
+ });
172
+ return { attested: !!ok, reason: ok ? null : 'status_post_failed' };
173
+ } catch (_) {
174
+ return { attested: false, reason: 'status_post_failed' };
175
+ }
176
+ }
177
+
178
+ async function peerVoteNet(taskId) {
179
+ const { rows } = await pool.query(
180
+ `SELECT COALESCE(SUM(vote), 0)::int AS net, COUNT(*)::int AS total
181
+ FROM peer_votes WHERE target_task_id = $1`,
182
+ [taskId]
183
+ );
184
+ return { net: rows[0].net, total: rows[0].total };
185
+ }
186
+
187
+ // #514: bad_kind responses carried a `valid` array but no human-readable line,
188
+ // so a caller (or CLI that only prints .message) couldn't tell at a glance what
189
+ // went wrong — the #506 session burned a turn guessing kind=chore. Add a
190
+ // .message that spells out the allowed values while keeping the valid[] array.
191
+ function badKind(res) {
192
+ const valid = Array.from(VALID_KINDS);
193
+ return res.fail('bad_kind', { status: 400, message: `kind must be one of: ${valid.join(', ')}`, details: { valid } });
194
+ }
195
+
196
+ module.exports = function buildTasksRouter() {
197
+ const router = express.Router();
198
+
199
+ // rank: any-builder — reads (filtered task list); preserved per CRITICAL §4.
200
+ router.get('/tasks', auth.requireBuilder, async (req, res) => {
201
+ const { version, status, kind, discipline, goal_id } = req.query;
202
+ if (kind && !VALID_KINDS.has(kind)) {
203
+ return badKind(res);
204
+ }
205
+ if (discipline && !VALID_DISCIPLINES.has(discipline)) {
206
+ return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
207
+ }
208
+ // ?goal_id= filters tasks to one goal (ADR 0086 §3 / BV1.R57). A non-numeric
209
+ // value is a 400 rather than a silently-ignored filter.
210
+ let goalId;
211
+ if (goal_id !== undefined) {
212
+ goalId = Number(goal_id);
213
+ if (!Number.isInteger(goalId) || goalId <= 0) {
214
+ return res.fail('bad_goal_id', { status: 400, message: 'goal_id must be a positive integer.' });
215
+ }
216
+ }
217
+ const tasks = await db.listTasks({ versionId: version, status, kind, discipline, goalId });
218
+ res.json({ tasks });
219
+ });
220
+
221
+ // NOTE: /tasks/claimable must come BEFORE /tasks/:id or Express will
222
+ // route 'claimable' to the :id param.
223
+ // rank: any-builder — claimable feed (read); preserved per CRITICAL §4 (supervisor hot path).
224
+ // #766: the primer-read onboarding gate that once sat here was removed — the
225
+ // Xenos path no longer requires reading the citizen's primer before surveying
226
+ // the works. Newcomers browse the claimable queue straight away; db.listClaimableTasks
227
+ // still filters Xenos to newcomer_friendly rows on its own.
228
+ router.get('/tasks/claimable',
229
+ auth.requireBuilder,
230
+ async (req, res) => {
231
+ const { version, discipline, fit } = req.query;
232
+ if (discipline && !VALID_DISCIPLINES.has(discipline)) {
233
+ return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
234
+ }
235
+ if (fit && !VALID_FITS.has(fit)) {
236
+ return res.fail('bad_fit', 400, { valid: Array.from(VALID_FITS) });
237
+ }
238
+ const [claimable, active] = await Promise.all([
239
+ db.listClaimableTasks({
240
+ versionId: version || null,
241
+ builderId: req.builder.id,
242
+ discipline: discipline || null,
243
+ fit: fit || null,
244
+ }),
245
+ db.listActiveClaims(),
246
+ ]);
247
+ // V3.R75 (#294): tick the onboarding state machine for first_start. Fire
248
+ // and forget — the response should never wait on or fail because of the
249
+ // onboarding write. safeMarkStage swallows errors and handles the
250
+ // graduation broadcast. Through the `onboarding` kernel port (BV1.R81 / ADR
251
+ // 0093 §1); ?. degrades to a no-op when the module is off.
252
+ seams.resolveOptional('onboarding')?.safeMarkStage(pool, req.builder.id, 'first_start', req.builder);
253
+ res.json({ claimable, active_claims: active });
254
+ });
255
+
256
+ // GET /tasks/newcomer-floor — supply gauge for the newcomer queue (task 614).
257
+ // Per-discipline counts of newcomer_friendly tasks in the two unclaimed live
258
+ // statuses: `ready` (claimable now — what the floor is measured against, per
259
+ // the canonical query in task 614) and `backlog` (held / pending review —
260
+ // proposals an Archon hasn't promoted yet). The AI floor replenisher
261
+ // (scripts/gds/newcomer-floor.js) reads this to decide which disciplines are
262
+ // under-supplied, counting `ready + backlog` so a re-run never piles fresh
263
+ // proposals on top of ones already awaiting review (idempotency). The hall can
264
+ // also render it as a live gauge.
265
+ //
266
+ // Global across open versions on purpose — a Xenos can claim any
267
+ // newcomer_friendly ready task regardless of version, so the floor is a
268
+ // project-wide promise, not a per-version one. Read-only, any-builder; the
269
+ // inline SQL follows the same self-contained-feature-locality call as the
270
+ // peer-vote queries above (kept out of the shared db.js).
271
+ // MUST stay registered before '/tasks/:id' or Express routes the literal
272
+ // 'newcomer-floor' into the :id param (same ordering constraint as /claimable).
273
+ router.get('/tasks/newcomer-floor', auth.requireBuilder, async (_req, res) => {
274
+ try {
275
+ const { rows } = await pool.query(
276
+ `SELECT COALESCE(discipline, 'unclassified') AS discipline, status, COUNT(*)::int AS n
277
+ FROM tasks
278
+ WHERE newcomer_friendly = true AND status IN ('ready', 'backlog')
279
+ GROUP BY COALESCE(discipline, 'unclassified'), status`
280
+ );
281
+ const disciplines = {};
282
+ for (const d of VALID_DISCIPLINES) disciplines[d] = { ready: 0, backlog: 0 };
283
+ for (const r of rows) {
284
+ // discipline is COALESCE'd to a non-null string above; a value outside
285
+ // the known set (shouldn't happen) still gets a bucket rather than throwing.
286
+ if (!disciplines[r.discipline]) disciplines[r.discipline] = { ready: 0, backlog: 0 };
287
+ disciplines[r.discipline][r.status] = r.n;
288
+ }
289
+ res.json({ disciplines });
290
+ } catch (err) {
291
+ console.error('[gds] GET /tasks/newcomer-floor', err);
292
+ res.fail('newcomer_floor_failed', 500);
293
+ }
294
+ });
295
+
296
+ // POST /tasks/newcomer-restock (task 1168) — bring every evergreen newcomer
297
+ // chore's standing pool up to its configured floor. This is the SEED + manual
298
+ // BACKSTOP for the on-claim restock (the live mechanism is the post-claim hook
299
+ // in routes/claims.js). Archon-gated like all task creation (ADR 0016). Must
300
+ // be registered BEFORE GET /tasks/:id so ':id' doesn't swallow the literal.
301
+ router.post('/tasks/newcomer-restock', auth.requireBuilder, auth.requireRank('archon'), async (_req, res) => {
302
+ try {
303
+ // The newcomer-chore restock carved into modules/onboarding/ (BV1.R81 / ADR
304
+ // 0093 §1): resolve the `onboarding` kernel port — never a direct require.
305
+ // Default-on (present in prod); a module-less instance has no chore shelf,
306
+ // so the backstop is a clean no-op (zero stamped).
307
+ const onboarding = seams.resolveOptional('onboarding');
308
+ const results = onboarding ? await onboarding.restockAll() : [];
309
+ const stamped = results.reduce((n, r) => n + (Array.isArray(r.stamped) ? r.stamped.length : 0), 0);
310
+ res.json({ ok: true, stamped, results });
311
+ } catch (err) {
312
+ console.error('[gds] POST /tasks/newcomer-restock', err);
313
+ res.fail('newcomer_restock_failed', { status: 500, message: 'internal error' });
314
+ }
315
+ });
316
+
317
+ // rank: any-builder — single-task read; preserved per CRITICAL §4 (supervisor hot path).
318
+ router.get('/tasks/:id', auth.requireBuilder, async (req, res) => {
319
+ const id = Number(req.params.id);
320
+ const task = await db.getTask(id);
321
+ if (!task) return res.fail('task_not_found', 404);
322
+ // #678: the `--regrade` CLI needs the most recent claim holder to gate "you
323
+ // last held this claim, or you're Archon". It's opt-in via ?include=last_claim
324
+ // so this CRITICAL §4 supervisor hot path doesn't pay for an extra query on
325
+ // every read (grader quality finding). Additive sibling key when requested.
326
+ const includes = String(req.query.include || '').split(',');
327
+ const wantLastClaim = includes.includes('last_claim');
328
+ const wantGrade = includes.includes('grade');
329
+ const [dependencies, dependents, lastClaim, grade] = await Promise.all([
330
+ db.getTaskDependencies(id),
331
+ db.getTaskDependents(id),
332
+ wantLastClaim ? db.getLastClaimHolder(id) : Promise.resolve(null),
333
+ wantGrade ? db.getTaskGrade(id) : Promise.resolve(null),
334
+ ]);
335
+ const body = { task: { ...task, dependencies, dependents } };
336
+ if (wantLastClaim) body.last_claim = lastClaim;
337
+ if (wantGrade) body.grade = grade; // null when no grade exists yet
338
+ res.json(body);
339
+ });
340
+
341
+ // GET /tasks/:id/dependencies — same data, surfaced as its own resource.
342
+ // Returns { dependencies, dependents } — both arrays of task summaries.
343
+ // rank: any-builder — dep graph read; pure read, no mutation.
344
+ router.get('/tasks/:id/dependencies', auth.requireBuilder, async (req, res) => {
345
+ const id = parseId(req, res, { code: 'bad_task_id' });
346
+ if (id === null) return;
347
+ const task = await db.getTask(id);
348
+ if (!task) return res.fail('task_not_found', 404);
349
+ const [dependencies, dependents] = await Promise.all([
350
+ db.getTaskDependencies(id),
351
+ db.getTaskDependents(id),
352
+ ]);
353
+ res.json({ dependencies, dependents });
354
+ });
355
+
356
+ // POST /tasks/:id/dependencies — body: { depends_on_task_id } OR
357
+ // { depends_on_task_ids: [N, ...] }
358
+ // Returns 409 with code='cycle' if accepting would create a cycle.
359
+ // rank: metic+archon — task-shaping; Metic may wire deps of a task it can author (ADR 0090).
360
+ router.post('/tasks/:id/dependencies', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
361
+ if (validateOrRespond(req, res, {
362
+ depends_on_task_ids: { type: 'array', maxItems: 200 },
363
+ // depends_on_task_id can be a number or a string-coerced number — leave
364
+ // the strict integer check to the existing Number.isFinite guard below
365
+ // (declared untyped so strict-mode accepts the singular form too).
366
+ depends_on_task_id: {},
367
+ })) return;
368
+ const id = parseId(req, res, { code: 'bad_task_id' });
369
+ if (id === null) return;
370
+ const body = req.body || {};
371
+ let depIds = [];
372
+ if (Array.isArray(body.depends_on_task_ids)) {
373
+ depIds = body.depends_on_task_ids.map(Number).filter(Number.isFinite);
374
+ } else if (body.depends_on_task_id != null) {
375
+ depIds = [Number(body.depends_on_task_id)].filter(Number.isFinite);
376
+ }
377
+ if (depIds.length === 0) {
378
+ return res.fail('depends_on_task_id_required', 400);
379
+ }
380
+ const added = [];
381
+ for (const depId of depIds) {
382
+ try {
383
+ await db.addTaskDependency(id, depId);
384
+ added.push(depId);
385
+ } catch (err) {
386
+ if (err && err.code === 'SELF_DEP') {
387
+ return res.fail('self_dep', 400, { task_id: id });
388
+ }
389
+ if (err && err.code === 'TASK_NOT_FOUND') {
390
+ return res.fail('task_not_found', 404);
391
+ }
392
+ if (err && err.code === 'CYCLE') {
393
+ return res.fail('cycle', 409, { path: err.path });
394
+ }
395
+ throw err;
396
+ }
397
+ }
398
+ const dependencies = await db.getTaskDependencies(id);
399
+ res.json({ ok: true, added, dependencies });
400
+ });
401
+
402
+ // DELETE /tasks/:id/dependencies/:depId — removes the edge.
403
+ // rank: metic+archon — removes a dep edge; task-shaping, Metic+ (ADR 0090).
404
+ router.delete('/tasks/:id/dependencies/:depId', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
405
+ const id = Number(req.params.id);
406
+ const depId = Number(req.params.depId);
407
+ if (!Number.isFinite(id) || !Number.isFinite(depId)) {
408
+ return res.fail('bad_task_id', 400);
409
+ }
410
+ const removed = await db.removeTaskDependency(id, depId);
411
+ res.json({ ok: true, removed });
412
+ });
413
+
414
+ // PUT /tasks/:id/dependencies — replace the full dep set.
415
+ // body: { depends_on_task_ids: [N, ...] }
416
+ // rank: metic+archon — replaces the full dep set; task-shaping, Metic+ (ADR 0090).
417
+ router.put('/tasks/:id/dependencies', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
418
+ if (validateOrRespond(req, res, {
419
+ depends_on_task_ids: { required: true, type: 'array', maxItems: 200 },
420
+ })) return;
421
+ const id = parseId(req, res, { code: 'bad_task_id' });
422
+ if (id === null) return;
423
+ const body = req.body || {};
424
+ if (!Array.isArray(body.depends_on_task_ids)) {
425
+ return res.fail('depends_on_task_ids_required', 400);
426
+ }
427
+ try {
428
+ const written = await db.setTaskDependencies(id, body.depends_on_task_ids);
429
+ const dependencies = await db.getTaskDependencies(id);
430
+ res.json({ ok: true, written, dependencies });
431
+ } catch (err) {
432
+ if (err && err.code === 'SELF_DEP') {
433
+ return res.fail('self_dep', 400);
434
+ }
435
+ if (err && err.code === 'CYCLE') {
436
+ return res.fail('cycle', 409, { path: err.path });
437
+ }
438
+ throw err;
439
+ }
440
+ });
441
+
442
+ // ---- task↔criterion links (task 438 / ADR 0025) — mirrors the dependency
443
+ // endpoints above. A criterion ref is a numeric done_when_criteria.id, a
444
+ // positional "Cn" token, or a criterion_id slug; the manage endpoints resolve
445
+ // it against the TASK's version so a cross-version link can't be formed.
446
+
447
+ // GET /tasks/:id/criteria — the criteria this task gates (with live "Cn" +
448
+ // source). Read-open like GET /tasks/:id/dependencies.
449
+ router.get('/tasks/:id/criteria', auth.requireBuilder, async (req, res) => {
450
+ const id = parseId(req, res, { code: 'bad_task_id' });
451
+ if (id === null) return;
452
+ const criteria = await db.getTaskCriteria(id);
453
+ res.json({ criteria });
454
+ });
455
+
456
+ // POST /tasks/:id/criteria — body: { criterion_ids: [ref, ...] } OR
457
+ // { criterion_id: ref }. Metic+.
458
+ // rank: metic+archon — links a task it can author to a criterion; mirrors POST deps (ADR 0090).
459
+ router.post('/tasks/:id/criteria', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
460
+ if (validateOrRespond(req, res, {
461
+ criterion_ids: { type: 'array', maxItems: 100 },
462
+ // criterion_id is the singular alt-form (a numeric id, "Cn" token, or slug)
463
+ // — untyped so strict-mode accepts it alongside the array form.
464
+ criterion_id: {},
465
+ })) return;
466
+ const id = parseId(req, res, { code: 'bad_task_id' });
467
+ if (id === null) return;
468
+ const body = req.body || {};
469
+ let refs = [];
470
+ if (Array.isArray(body.criterion_ids)) refs = body.criterion_ids;
471
+ else if (body.criterion_id != null) refs = [body.criterion_id];
472
+ if (refs.length === 0) return res.fail('criterion_id_required', 400);
473
+
474
+ const task = await db.getTask(id);
475
+ if (!task) return res.fail('task_not_found', 404);
476
+ const { ids, unresolved } = await db.resolveCriterionIds(task.version_id, refs);
477
+ if (unresolved.length > 0) {
478
+ return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${task.version_id}': ${unresolved.join(', ')}.`, details: { unresolved } });
479
+ }
480
+ const added = [];
481
+ for (const cid of ids) {
482
+ try {
483
+ await db.addTaskCriterion(id, cid, 'manual');
484
+ added.push(cid);
485
+ } catch (err) {
486
+ if (err && err.code === 'CRITERION_VERSION_MISMATCH') {
487
+ return res.fail('criterion_version_mismatch', 400);
488
+ }
489
+ if (err && err.code === 'CRITERION_NOT_FOUND') return res.fail('criterion_not_found', 404);
490
+ if (err && err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
491
+ throw err;
492
+ }
493
+ }
494
+ const criteria = await db.getTaskCriteria(id);
495
+ res.json({ ok: true, added, criteria });
496
+ });
497
+
498
+ // DELETE /tasks/:id/criteria/:criterionId — removes one link (criterionId is
499
+ // the numeric done_when_criteria.id, mirroring DELETE deps/:depId).
500
+ // rank: metic+archon — removes a criterion↔task edge; task-shaping, Metic+ (ADR 0090).
501
+ router.delete('/tasks/:id/criteria/:criterionId', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
502
+ const id = Number(req.params.id);
503
+ const criterionId = Number(req.params.criterionId);
504
+ if (!Number.isFinite(id) || !Number.isFinite(criterionId)) {
505
+ return res.fail('bad_task_id', 400);
506
+ }
507
+ const removed = await db.removeTaskCriterion(id, criterionId);
508
+ res.json({ ok: true, removed });
509
+ });
510
+
511
+ // Task create/promote/edit (plan/scope mutation) is Metic+ as of ADR 0090
512
+ // (owner decision 2026-06-26): a Metic who can run idea-triage may also seed +
513
+ // shape tasks. Was Archon-only (task #106 / ADR 0016; three-rank model #360 /
514
+ // ADR 0018). A Xenos/Thetes still cannot author — they file ideas. Setting rank
515
+ // and closing versions stay Archon. The every-route audit is task #231 (V3.R06).
516
+ // rank: metic+archon — create work (plan/scope mutation); ADR 0090 (was ADR 0016/0018 archon).
517
+ router.post('/tasks', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
518
+ if (validateOrRespond(req, res, {
519
+ version_id: { required: true, type: 'string', maxLength: 64 },
520
+ title: { required: true, type: 'string', maxLength: LIMITS.TITLE, minLength: 1 },
521
+ description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
522
+ status: { type: 'string', maxLength: 32 },
523
+ touches: { type: 'array', maxItems: 500, itemsType: 'string' },
524
+ est_minutes: { type: 'integer', min: 0, max: 100000 },
525
+ est_cost_usd: { type: 'number', min: 0 },
526
+ manual_degree: { type: 'integer', min: TASK_FIELD_BOUNDS.manual_degree.min, max: TASK_FIELD_BOUNDS.manual_degree.max },
527
+ priority: { type: 'integer', min: TASK_FIELD_BOUNDS.priority.min, max: TASK_FIELD_BOUNDS.priority.max },
528
+ automation_tag: { type: 'string', maxLength: 64 },
529
+ credits_reward: { type: 'integer', min: 0, max: 1000000 },
530
+ source: { type: 'string', maxLength: LIMITS.SOURCE },
531
+ source_ref: { type: 'string', maxLength: LIMITS.SOURCE_REF },
532
+ value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
533
+ kind: { type: 'string', maxLength: 64 },
534
+ discipline: { type: 'string', maxLength: 64 },
535
+ parallel_safe: {}, // accepts true/false/null — type checked below
536
+ newcomer_friendly: { type: 'boolean' }, // V3.R15 #239 — Xenos-claimable flag
537
+ security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
538
+ requires_rank: { type: 'string', maxLength: 16 }, // task 1498 / ADR 0084 — per-task rank floor (auto-derived; explicit value can only raise it)
539
+ auto_detect_touches: { type: 'boolean' },
540
+ // task 438: link the new task to done-when criteria at creation time. Each
541
+ // element is a numeric done_when_criteria.id, a positional "Cn" token, or a
542
+ // criterion_id slug — resolved against this task's version below (ADR 0025).
543
+ criterion_ids: { type: 'array', maxItems: 100 },
544
+ // BV1.R53 (task 1422): module affinity label. Drives module-disjointness at
545
+ // claim time and diff⊆module drift detection at ship time.
546
+ module_key: { type: 'string', maxLength: 64 },
547
+ // Parent the new task under a specific goal at create (task 1763 added the
548
+ // PATCH-move vector; this is the create-time counterpart so a planning seed
549
+ // lands tasks under their goal, not the version's default catch-all).
550
+ // Validated to exist + belong to this version below.
551
+ goal_id: { type: 'integer', min: 1 },
552
+ })) return;
553
+ const body = req.body || {};
554
+ if (!body.version_id || !body.title) {
555
+ return res.fail('version_id_and_title_required', 400);
556
+ }
557
+ // task 975: a version is the ledger of one bounded chunk of work. Once it
558
+ // ships it is sealed history — its progress bar reads 100% and its scope
559
+ // is archived. Filing new work against it would reopen a closed book and
560
+ // (until the view fix above) silently drag the bar back below 100%. Reject
561
+ // creation against a missing or shipped version; the caller should target
562
+ // an open version or file the idea to idea_inbox for the next planning
563
+ // session. (POST /tasks is the only API create-vector — PATCH cannot
564
+ // re-bind version_id — so this is the complete guard.)
565
+ const targetVersion = await db.getVersion(body.version_id);
566
+ if (!targetVersion) {
567
+ return res.fail('version_not_found', { status: 404, message: `No version '${body.version_id}'. Create the task under an existing, open version.` });
568
+ }
569
+ // task 386 (idea #85): idempotency on (source, source_ref). A re-submitted
570
+ // create — a retried POST, a capture/promotion re-run — must resolve to the
571
+ // ORIGINAL task, not silently mint a duplicate (the #248-dup-of-#252 class of
572
+ // bug). Runs BEFORE the version-closed guard so re-POSTing the same ref after
573
+ // its task already shipped (version now closed) returns the existing row
574
+ // instead of a spurious 409. `source` defaults to 'manual' to match the
575
+ // createTask call below, so the lookup key is exactly what would be written.
576
+ const effectiveSource = body.source ?? 'manual';
577
+ if (body.source_ref) {
578
+ const existing = await db.findTaskBySourceRef({ source: effectiveSource, sourceRef: body.source_ref });
579
+ if (existing) {
580
+ const criteria = await db.getTaskCriteria(existing.id);
581
+ return res.status(200).json({
582
+ task: existing,
583
+ needs_migration: existing.needs_migration === true,
584
+ criteria,
585
+ idempotent: true,
586
+ });
587
+ }
588
+ }
589
+ if (targetVersion.status === 'shipped') {
590
+ return res.fail('version_closed', { status: 409, message: `Version '${body.version_id}' has shipped — it is closed history and cannot take new tasks. ` +
591
+ `Target an open version, or capture this in idea_inbox for the next planning session.` });
592
+ }
593
+ // task 438: resolve any criterion_ids BEFORE creating the task, so a bad ref
594
+ // is a clean 400 with nothing created. Scoped to this task's version, so "C8"
595
+ // is unambiguous and a numeric id from another version doesn't resolve.
596
+ let criterionIdsResolved = [];
597
+ if (Array.isArray(body.criterion_ids) && body.criterion_ids.length > 0) {
598
+ const { ids, unresolved } = await db.resolveCriterionIds(body.version_id, body.criterion_ids);
599
+ if (unresolved.length > 0) {
600
+ return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${body.version_id}': ` +
601
+ `${unresolved.join(', ')}. Use a numeric done_when_criteria.id, a "Cn" position, or a criterion_id slug.`, details: { unresolved } });
602
+ }
603
+ criterionIdsResolved = ids;
604
+ }
605
+ // Resolve kind: caller wins if they specified a valid non-unclassified
606
+ // value; otherwise run the heuristic classifier on title+description.
607
+ // Confidence < 0.5 falls through to 'unclassified' for triage.
608
+ let kind = body.kind;
609
+ if (kind && !VALID_KINDS.has(kind)) {
610
+ return badKind(res);
611
+ }
612
+ if (!kind || kind === 'unclassified') {
613
+ const result = classifyTaskKind(body.title, body.description || '');
614
+ kind = result.confidence >= 0.5 ? result.kind : 'unclassified';
615
+ }
616
+ const discipline = body.discipline ?? 'unclassified';
617
+ if (!VALID_DISCIPLINES.has(discipline)) {
618
+ return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
619
+ }
620
+ // task 1498 / ADR 0084: an explicit requires_rank must be a live rank. The
621
+ // floor auto-derives from touches + security_sensitive; createTask clamps
622
+ // any explicit value UP to that floor (never below), so this only rejects a
623
+ // malformed enum.
624
+ if (body.requires_rank != null) {
625
+ const LIVE_RANKS = new Set(db.LIVE_RANK_LADDER);
626
+ if (!LIVE_RANKS.has(body.requires_rank)) {
627
+ return res.fail('bad_requires_rank', { status: 400, message: `requires_rank must be one of ${db.LIVE_RANK_LADDER.join(' | ')}. The floor auto-derives from touches + security_sensitive; an explicit value can only raise it.`, details: { valid: db.LIVE_RANK_LADDER } });
628
+ }
629
+ }
630
+ // BV1.R53 (task 1422): validate module_key against MODULE_SCOPE_MAP.
631
+ if (body.module_key != null && !isModuleKey(body.module_key)) {
632
+ return res.fail('bad_module_key', { status: 400, message: `module_key must be one of the known module keys, or omitted. Valid keys: ${moduleKeys().join(', ')}.`, details: { valid: moduleKeys() } });
633
+ }
634
+ // V3.R48 (#267) → migration 081 (task 876): if the title/description reads
635
+ // like a schema change, flag tasks.needs_migration so the claim-time
636
+ // allocator reserves a migration number. This used to inject the literal
637
+ // 'migrations/' into touches[]; it now sets a dedicated boolean and leaves
638
+ // touches[] alone. Opt out with { auto_detect_touches: false }.
639
+ const touches = Array.isArray(body.touches) ? [...body.touches] : [];
640
+ const autoDetect = body.auto_detect_touches !== false;
641
+ const needsMigration = autoDetect && detectsMigration(body.title, body.description);
642
+ // Validate an explicit goal_id: it must exist AND belong to THIS version (a task
643
+ // can't be parented under another version's goal). Absent → the default-goal
644
+ // trigger files it under the version's catch-all goal, exactly as before.
645
+ if (body.goal_id != null) {
646
+ const goal = await db.getGoal(body.goal_id);
647
+ if (!goal || String(goal.version_id) !== String(body.version_id)) {
648
+ return res.fail('bad_goal_id', { status: 400, message: `goal_id ${body.goal_id} does not exist on version '${body.version_id}'.` });
649
+ }
650
+ }
651
+ try {
652
+ const task = await db.createTask({
653
+ versionId: body.version_id,
654
+ title: body.title,
655
+ description: body.description || '',
656
+ status: body.status || 'backlog',
657
+ touches,
658
+ estMinutes: body.est_minutes ?? null,
659
+ estCostUsd: body.est_cost_usd ?? null,
660
+ manualDegree: body.manual_degree ?? null,
661
+ priority: body.priority ?? null,
662
+ automationTag: body.automation_tag ?? null,
663
+ creditsReward: body.credits_reward ?? 0,
664
+ source: effectiveSource,
665
+ sourceRef: body.source_ref ?? null,
666
+ valueSummary: body.value_summary ?? null,
667
+ kind,
668
+ discipline,
669
+ parallelSafe: typeof body.parallel_safe === 'boolean' ? body.parallel_safe : null,
670
+ newcomerFriendly: body.newcomer_friendly === true,
671
+ securitySensitive: body.security_sensitive === true,
672
+ needsMigration,
673
+ requiresRank: body.requires_rank ?? null,
674
+ moduleKey: body.module_key ?? null,
675
+ goalId: body.goal_id ?? null,
676
+ });
677
+ // task 438: link resolved criteria (source='planning' — task-create is the
678
+ // planning vector). The ids were version-checked above, so this is a single
679
+ // idempotent batch insert; surface the linked criteria in the response so the
680
+ // caller (and planning-session) can confirm the task is attributed.
681
+ let criteria = [];
682
+ if (criterionIdsResolved.length > 0) {
683
+ await db.linkTaskCriteria(task.id, criterionIdsResolved, 'planning');
684
+ criteria = await db.getTaskCriteria(task.id);
685
+ }
686
+ // task 1677 / ADR 0096: surface the assigned credits_reward so the creator sees
687
+ // the drachmae value the task was born with — auto-assigned at creation when the
688
+ // caller supplied no positive value (0/absent flows into createTask as "unset").
689
+ // task 1957: credits_reward is an ESTIMATE, not a guaranteed payout. The hall
690
+ // renders it as "<Currency> Est." until the task settles (confirmed/shipped),
691
+ // then switches to the real ledger figure (credits_awarded from the task read).
692
+ res.status(201).json({
693
+ task,
694
+ credits_reward: task.credits_reward,
695
+ needs_migration: needsMigration,
696
+ criteria,
697
+ });
698
+ } catch (err) {
699
+ console.error('[gds] POST /tasks', err);
700
+ res.fail('create_failed', { status: 500, message: 'internal error' });
701
+ }
702
+ });
703
+
704
+ // PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1),
705
+ // body.discipline (mig 014), body.parallel_safe
706
+ // (mig 028), and body.newcomer_friendly (#360 / ADR 0018 + V3.R15 #239 —
707
+ // body.touches was removed in touches[] cleanup [4/8] (task 879) along with
708
+ // the drift scanner —
709
+ // open a task to the Xenos sandbox; alias body.xenos_claimable accepted for
710
+ // pre-rename callers). Pass parent_task_id: null to detach; parallel_safe
711
+ // accepts true|false|null; newcomer_friendly is a boolean. Validation lives
712
+ // in hierarchy.js for parent; the rest are validated inline.
713
+ // rank: metic+archon — edit work (incl. newcomer_friendly flag); ADR 0090 (was ADR 0016 archon).
714
+ router.patch('/tasks/:id', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
715
+ if (validateOrRespond(req, res, {
716
+ kind: { type: 'string', maxLength: 64 },
717
+ discipline: { type: 'string', maxLength: 64 },
718
+ requires_rank: { type: 'string', maxLength: 16 }, // task 1498 / ADR 0084 — override the per-task rank floor (upward only)
719
+ module_key: { type: 'string', maxLength: 64 }, // BV1.R53 (task 1422) — module affinity label
720
+ goal_id: { type: 'integer', min: 1 }, // task 1763 — (re)assign the task's goal (must be in the task's version)
721
+ // touches[] cleanup [4/8] (task 879): touches is no longer PATCH-able —
722
+ // the drift scanner + mid-claim expansion were removed. The rest are
723
+ // checked inline below but must be DECLARED so strict-mode (R11/#1998)
724
+ // accepts them instead of rejecting them as unknown fields.
725
+ parent_task_id: {}, // number id, or null to detach (hasOwnProperty-gated below)
726
+ parallel_safe: {}, // true/false/null — checked inline
727
+ newcomer_friendly: { type: 'boolean' }, // V3.R15 — Xenos-claimable flag
728
+ xenos_claimable: { type: 'boolean' }, // pre-rename alias for newcomer_friendly
729
+ security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
730
+ automation_tag: { type: 'string', maxLength: 64 },
731
+ })) return;
732
+ const id = parseId(req, res, { code: 'bad_task_id', positiveInt: true });
733
+ if (id === null) return;
734
+ const body = req.body || {};
735
+ const hasParent = Object.prototype.hasOwnProperty.call(body, 'parent_task_id');
736
+ const hasKind = Object.prototype.hasOwnProperty.call(body, 'kind');
737
+ const hasDiscipline = Object.prototype.hasOwnProperty.call(body, 'discipline');
738
+ // V3.R15 #239: newcomer_friendly is the canonical name; xenos_claimable is
739
+ // the pre-rename alias and still accepted. Either presence triggers the
740
+ // same update path.
741
+ const hasNewcomerFriendly = Object.prototype.hasOwnProperty.call(body, 'newcomer_friendly');
742
+ const hasXenosClaimable = Object.prototype.hasOwnProperty.call(body, 'xenos_claimable');
743
+ const hasNewcomerOrXenos = hasNewcomerFriendly || hasXenosClaimable;
744
+ const hasParallelSafe = Object.prototype.hasOwnProperty.call(body, 'parallel_safe');
745
+ const hasSecuritySensitive = Object.prototype.hasOwnProperty.call(body, 'security_sensitive');
746
+ const hasAutomationTag = Object.prototype.hasOwnProperty.call(body, 'automation_tag');
747
+ const hasRequiresRank = Object.prototype.hasOwnProperty.call(body, 'requires_rank');
748
+ const hasModuleKey = Object.prototype.hasOwnProperty.call(body, 'module_key');
749
+ const hasGoalId = Object.prototype.hasOwnProperty.call(body, 'goal_id');
750
+ // #514: status is a lifecycle field, not a PATCH-able column — it moves only
751
+ // through the dedicated transition routes. The #506 session tried
752
+ // `PATCH status=ready`, got the generic no_supported_fields error, and had
753
+ // to go spelunking for the promote route. Detect status explicitly and point
754
+ // the caller straight at the right endpoint.
755
+ const STATUS_HINT =
756
+ 'status is not editable via PATCH; use POST /tasks/:id/promote (backlog->ready), ' +
757
+ '/tasks/:id/confirm, /tasks/:id/ship, or /claims/:id/resolve to move a task through its lifecycle.';
758
+ if (Object.prototype.hasOwnProperty.call(body, 'status')) {
759
+ return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
760
+ }
761
+ if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
762
+ return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
763
+ }
764
+ let updated = null;
765
+ if (hasParallelSafe) {
766
+ if (body.parallel_safe !== null && typeof body.parallel_safe !== 'boolean') {
767
+ return res.fail('bad_parallel_safe', { status: 400, message: 'parallel_safe must be true, false, or null' });
768
+ }
769
+ try {
770
+ updated = await db.updateTaskParallelSafe(id, body.parallel_safe);
771
+ if (!updated) return res.fail('task_not_found', 404);
772
+ } catch (err) {
773
+ console.error('[gds] PATCH /tasks/:id parallel_safe', err);
774
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
775
+ }
776
+ }
777
+ if (hasSecuritySensitive) {
778
+ // task 990: explicit Archon-only-broadcast flag. Boolean only (no null) —
779
+ // it gates whether ship.js routes the broadcast to the Archon-only
780
+ // #security-ship-feed channel instead of the public feeds.
781
+ // ADR 0090: PATCH /tasks/:id opened to Metic+, but security_sensitive is a
782
+ // SECURITY control (broadcast suppression + it feeds the auto-derived rank
783
+ // floor, ADR 0084), not a planning field — so it stays Archon-only via this
784
+ // in-handler gate. A Metic editing any other field is unaffected; only a
785
+ // requires_rank-style escalation surface is withheld. (Setting it at create
786
+ // time is safe — a brand-new task has no existing sensitivity to clear.)
787
+ if (!auth.rankMeetsThreshold(req.builder.rank, ['archon'])) {
788
+ return res.fail('rank_forbidden', 403, { required: ['archon'], actual: req.builder.rank, reason: 'security_sensitive is an Archon-only security control (ADR 0090); a Metic may edit every other task field.' });
789
+ }
790
+ if (typeof body.security_sensitive !== 'boolean') {
791
+ return res.fail('bad_security_sensitive', { status: 400, message: 'security_sensitive must be a boolean' });
792
+ }
793
+ try {
794
+ updated = await db.updateTaskSecuritySensitive(id, body.security_sensitive);
795
+ if (!updated) return res.fail('task_not_found', 404);
796
+ } catch (err) {
797
+ console.error('[gds] PATCH /tasks/:id security_sensitive', err);
798
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
799
+ }
800
+ }
801
+ if (hasAutomationTag) {
802
+ const tag = body.automation_tag;
803
+ const VALID_AUTOMATION_TAGS = new Set(['overnight-safe', 'live-only', 'needs-input']);
804
+ if (tag !== null && (typeof tag !== 'string' || !VALID_AUTOMATION_TAGS.has(tag))) {
805
+ return res.fail('bad_automation_tag', { status: 400, message: 'automation_tag must be one of overnight-safe | live-only | needs-input, or null to clear', details: { valid: Array.from(VALID_AUTOMATION_TAGS) } });
806
+ }
807
+ try {
808
+ updated = await db.updateTaskAutomationTag(id, tag);
809
+ if (!updated) return res.fail('task_not_found', 404);
810
+ } catch (err) {
811
+ console.error('[gds] PATCH /tasks/:id automation_tag', err);
812
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
813
+ }
814
+ }
815
+ if (hasModuleKey) {
816
+ // BV1.R53 (task 1422): assign or clear the module affinity label. null clears.
817
+ const key = body.module_key;
818
+ if (key !== null && !isModuleKey(key)) {
819
+ return res.fail('bad_module_key', { status: 400, message: `module_key must be one of the known module keys, or null to clear. Valid keys: ${moduleKeys().join(', ')}.`, details: { valid: moduleKeys() } });
820
+ }
821
+ try {
822
+ updated = await db.updateTaskModuleKey(id, key ?? null);
823
+ if (!updated) return res.fail('task_not_found', 404);
824
+ } catch (err) {
825
+ console.error('[gds] PATCH /tasks/:id module_key', err);
826
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
827
+ }
828
+ }
829
+ if (hasGoalId) {
830
+ // task 1763: (re)assign the task's goal — the documented "Archon/Metic
831
+ // mechanism" to set goal_id (this route is metic+archon). A task belongs to
832
+ // exactly one goal WITHIN its own version: both the per-version general goals
833
+ // and goal-authored tasks hold this, and the /status rollup walks
834
+ // version→goal→task, so a cross-version link would drop the task out of its
835
+ // version's rollup. Refuse that (409). goal_id is never cleared to null here
836
+ // (that reintroduces an orphan) — a positive integer is required.
837
+ const goalId = body.goal_id;
838
+ if (!Number.isInteger(goalId) || goalId <= 0) {
839
+ return res.fail('bad_goal_id', { status: 400, message: 'goal_id must be a positive integer — a task belongs to exactly one goal.' });
840
+ }
841
+ try {
842
+ const task = await db.getTask(id);
843
+ if (!task) return res.fail('task_not_found', 404);
844
+ const goal = await db.getGoal(goalId);
845
+ if (!goal) return res.fail('goal_not_found', { status: 404, message: `No goal #${goalId}.` });
846
+ if (String(goal.version_id) !== String(task.version_id)) {
847
+ return res.fail('goal_version_mismatch', { status: 409, message: `Goal #${goalId} is in version '${goal.version_id}' but task #${id} is in '${task.version_id}'. A task can only join a goal in its own version.` });
848
+ }
849
+ updated = await db.updateTaskGoal(id, goalId);
850
+ if (!updated) return res.fail('task_not_found', 404);
851
+ } catch (err) {
852
+ console.error('[gds] PATCH /tasks/:id goal_id', err);
853
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
854
+ }
855
+ }
856
+ if (hasRequiresRank) {
857
+ // task 1498 / ADR 0084: the Archon override of the per-task rank floor.
858
+ // Validate the enum here; db.updateTaskRequiresRank clamps the value UP to
859
+ // the auto-derived floor (touches + security_sensitive), so a PATCH can
860
+ // only RAISE the gate — never open a sensitive task to a lower rank. Runs
861
+ // AFTER the security_sensitive block above, so a same-request flag flip is
862
+ // already reflected in the derived floor.
863
+ const LIVE_RANKS = new Set(db.LIVE_RANK_LADDER);
864
+ if (typeof body.requires_rank !== 'string' || !LIVE_RANKS.has(body.requires_rank)) {
865
+ return res.fail('bad_requires_rank', { status: 400, message: `requires_rank must be one of ${db.LIVE_RANK_LADDER.join(' | ')}. An override can only raise the floor; it is clamped up to the auto-derived minimum.`, details: { valid: db.LIVE_RANK_LADDER } });
866
+ }
867
+ try {
868
+ updated = await db.updateTaskRequiresRank(id, body.requires_rank);
869
+ if (!updated) return res.fail('task_not_found', 404);
870
+ } catch (err) {
871
+ console.error('[gds] PATCH /tasks/:id requires_rank', err);
872
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
873
+ }
874
+ }
875
+ if (hasKind) {
876
+ if (!VALID_KINDS.has(body.kind)) {
877
+ return badKind(res);
878
+ }
879
+ try {
880
+ updated = await db.updateTaskKind(id, body.kind);
881
+ if (!updated) return res.fail('task_not_found', 404);
882
+ } catch (err) {
883
+ console.error('[gds] PATCH /tasks/:id kind', err);
884
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
885
+ }
886
+ }
887
+ if (hasDiscipline) {
888
+ if (!VALID_DISCIPLINES.has(body.discipline)) {
889
+ return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
890
+ }
891
+ try {
892
+ updated = await db.updateTaskDiscipline(id, body.discipline);
893
+ if (!updated) return res.fail('task_not_found', 404);
894
+ } catch (err) {
895
+ console.error('[gds] PATCH /tasks/:id discipline', err);
896
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
897
+ }
898
+ }
899
+ if (hasNewcomerOrXenos) {
900
+ // V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
901
+ // xenos_claimable (pre-rename alias). Both map to the same DB column.
902
+ const incoming = hasNewcomerFriendly ? body.newcomer_friendly : body.xenos_claimable;
903
+ if (typeof incoming !== 'boolean') {
904
+ const field = hasNewcomerFriendly ? 'newcomer_friendly' : 'xenos_claimable';
905
+ return res.fail(`bad_${field}`, { status: 400, message: `${field} must be a boolean` });
906
+ }
907
+ try {
908
+ updated = await db.updateTaskNewcomerFriendly(id, incoming);
909
+ if (!updated) return res.fail('task_not_found', 404);
910
+ } catch (err) {
911
+ console.error('[gds] PATCH /tasks/:id newcomer_friendly', err);
912
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
913
+ }
914
+ }
915
+ if (!hasParent) return res.json({ task: updated });
916
+ try {
917
+ const task = await hierarchy.attachParent(id, body.parent_task_id);
918
+ res.json({ task });
919
+ } catch (err) {
920
+ if (err && err.code) {
921
+ const status =
922
+ err.code === 'TASK_NOT_FOUND' ? 404 :
923
+ err.code === 'PARENT_NOT_FOUND' ? 404 :
924
+ err.code === 'SELF_PARENT' ? 409 :
925
+ err.code === 'VERSION_MISMATCH' ? 409 :
926
+ err.code === 'PARENT_HAS_PARENT' ? 409 :
927
+ err.code === 'TARGET_HAS_CHILDREN' ? 409 :
928
+ 500;
929
+ return res.fail(err.code, status, { ...err });
930
+ }
931
+ console.error('[gds] PATCH /tasks/:id', err);
932
+ res.fail('patch_failed', { status: 500, message: 'internal error' });
933
+ }
934
+ });
935
+
936
+ // rank: metic+archon — backlog→ready promotion; scope mutation; ADR 0090 (was archon).
937
+ router.post('/tasks/:id/promote', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
938
+ const id = Number(req.params.id);
939
+ const task = await db.getTask(id);
940
+ if (!task) return res.fail('task_not_found', 404);
941
+ if (!['backlog', 'blocked'].includes(task.status)) {
942
+ return res.fail('cannot_promote', 409, { current_status: task.status });
943
+ }
944
+ // task 1670 / ADR 0096 + task 1673 amendment (2026-06-28): a task must never
945
+ // become workable paying 0 drachmae — but the gate AUTO-ASSIGNS rather than
946
+ // BLOCKS. If credits_reward is 0/NULL, assign the capped suggestion (from
947
+ // kind/est, cap 60) and PROCEED to promote. The promote is never refused for
948
+ // a missing reward. This reverses ADR 0023's strict author-set stance for the
949
+ // gate-time value only (capped + audited); explicit author-set values via
950
+ // PATCH /tasks/:id and the ungameable cost-plus session reward (ADR 0054)
951
+ // remain the primary drivers. AUDIT: setCreditsReward logs + the visible
952
+ // credits_reward change.
953
+ let assignedReward = null;
954
+ const rewardAssign = db.rewardGateAssignment(task);
955
+ if (rewardAssign) {
956
+ const updated = await db.setCreditsReward(id, rewardAssign.assign);
957
+ assignedReward = updated ? updated.credits_reward : rewardAssign.assign;
958
+ }
959
+ await db.updateTaskStatus(id, 'ready');
960
+ res.json({ ok: true, ...(assignedReward !== null ? { assigned_credits_reward: assignedReward } : {}) });
961
+ });
962
+
963
+ // POST /tasks/:id/demote — the strict inverse of /promote: flip a task
964
+ // ready→backlog and nothing else. Use case (task 1223): a dependency-gated task
965
+ // that was wrongly promoted to ready needs to go back to backlog without
966
+ // hand-SQL on prod. Mirrors /promote's auth (Metic+ — scope mutation, ADR 0090).
967
+ //
968
+ // Strictly ready→backlog: refuse any other current status (blocked, active, or
969
+ // any later lifecycle state). A claimed task is `active`, so this gate also
970
+ // refuses demoting work that's already in flight — demote is not a way to yank a
971
+ // task out from under its claim holder. No reward side-effects (the reward gate
972
+ // is a promote-only concern; backlog is the pre-workable state).
973
+ // rank: metic+archon — ready→backlog demotion; scope mutation; mirrors /promote (ADR 0090).
974
+ router.post('/tasks/:id/demote', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
975
+ const id = Number(req.params.id);
976
+ const task = await db.getTask(id);
977
+ if (!task) return res.fail('task_not_found', 404);
978
+ if (task.status !== 'ready') {
979
+ return res.fail('cannot_demote', 409, { current_status: task.status });
980
+ }
981
+ await db.updateTaskStatus(id, 'backlog');
982
+ res.json({ ok: true });
983
+ });
984
+
985
+ // Phase 5 lifecycle transitions: completed → confirmed → shipped.
986
+ // /resolve handles active → completed (and the auto-completed-to-confirmed
987
+ // promotion when verification passes). These endpoints handle the manual
988
+ // overrides + the final merge step.
989
+
990
+ // confirmed → shipped. Called by /merge-mode (or the auto-merge chain in
991
+ // /builder-ship) once the branch is on main + deployed.
992
+ // rank: any-builder — confirmed→shipped on builder's own work; supervisor hot path
993
+ // (preserved per CRITICAL §4 — no requireRank).
994
+ router.post('/tasks/:id/ship', auth.requireBuilder, async (req, res) => {
995
+ const id = parseId(req, res, { code: 'bad_task_id' });
996
+ if (id === null) return;
997
+ // SECURITY (#871): confirmed→shipped is an any-builder supervisor hot path so
998
+ // /merge-mode can sweep the queue — but a Xenos/Thetes should only flip their
999
+ // OWN confirmed task. allowSupervisor lets Metic+ (who run /merge-mode) ship
1000
+ // across builders; the claim holder ships their own; everyone else is 403'd.
1001
+ if (!(await gateTaskOwnership(req, res, id, { allowSupervisor: true }))) return;
1002
+ try {
1003
+ const task = await db.shipTask({ taskId: id, builderId: req.builder.id });
1004
+ // task 1749: emit task.shipped on THIS entry point too, not just the
1005
+ // publish-status reconcile-on-read path (line ~1545) + the reconciler
1006
+ // sweep. A direct ship here (e.g. /merge-mode sweeping the confirmed
1007
+ // queue) otherwise lands with no Discord broadcast. shipTask is the
1008
+ // confirmed→shipped flip and throws TASK_NOT_CONFIRMED on a repeat, so
1009
+ // whichever entry point wins the transition is the only one that reaches
1010
+ // this line — the emit fires exactly ONCE per ship across all three
1011
+ // paths. The server is thus the SINGLE ship broadcaster (the box/laptop
1012
+ // local post in scripts/gds/ship.js was removed as a duplicate, task
1013
+ // 1749). Best-effort: emitAsync isolates listener errors so a broadcast
1014
+ // hiccup can never turn a successful ship into a 500.
1015
+ try { await seams.emitAsync('task.shipped', task); } catch (_) { /* non-blocking */ }
1016
+ res.json({ ok: true, task });
1017
+ } catch (err) {
1018
+ if (err.code === 'TASK_NOT_CONFIRMED') {
1019
+ return res.fail('task_not_confirmed', 409);
1020
+ }
1021
+ console.error('[gds] POST /tasks/:id/ship', err);
1022
+ res.fail('ship_failed', { status: 500, message: 'internal error' });
1023
+ }
1024
+ });
1025
+
1026
+ // completed → confirmed. Manual override for cases where automated
1027
+ // verification at /resolve time was inconclusive but a human has since
1028
+ // verified. Awards credits if not already credited (idempotent).
1029
+ //
1030
+ // Archon-only (#231): this bypasses the subagent quality grader, so it must
1031
+ // not be self-serviceable by a builder pushing their own work past a failed
1032
+ // grade. /ship and /grade below stay open — they're part of any builder's
1033
+ // own ship flow (the grade is computed by an objective subagent).
1034
+ //
1035
+ // #853 (ADR 0041): a temporary, default-OFF env kill-switch
1036
+ // GRADER_BYPASS_ENABLED relaxes this gate to any-builder during a grader
1037
+ // OUTAGE (e.g. a provider 429), so every builder can land work without the
1038
+ // subagent grader. When the env is unset/!=='1' the archon-only gate from
1039
+ // #231 stays in force, preserving the ADR 0016 trust boundary by default.
1040
+ // The relaxation is logged so the audit trail shows every bypass confirm.
1041
+ // rank: archon — grader-bypass override (relaxed to any-builder only while
1042
+ // GRADER_BYPASS_ENABLED==='1'; archon-only otherwise).
1043
+ const confirmRankGate = (req, res, next) => {
1044
+ if (graderBypassEnabled()) return next();
1045
+ return auth.requireRank('archon')(req, res, next);
1046
+ };
1047
+ router.post('/tasks/:id/confirm', auth.requireBuilder, confirmRankGate, async (req, res) => {
1048
+ if (validateOrRespond(req, res, {
1049
+ // { verified: true } attestation for verify-kind tasks (checked below);
1050
+ // absent/any-shape otherwise. Untyped so the supervisor's `{}` and the
1051
+ // `--verified` boolean both pass; strict rejects a typo'd extra field.
1052
+ verified: {},
1053
+ })) return;
1054
+ const id = parseId(req, res, { code: 'bad_task_id' });
1055
+ if (id === null) return;
1056
+ // SECURITY (#871): the grader-bypass relaxation lets ANY builder reach this
1057
+ // route during the outage. Confirm is meant to land a builder's OWN work past
1058
+ // the (absent) grader — NOT to let anyone confirm anyone's task. Gate to the
1059
+ // claim holder (or Archon). In the normal (non-bypass) path the route is
1060
+ // already archon-only, and an Archon always passes this check.
1061
+ if (!(await gateTaskOwnership(req, res, id))) return;
1062
+ // task 1053 (F): a verification task must not be confirmed before the verify
1063
+ // actually ran. A passing code-grade (or the skip-grade bypass) says nothing
1064
+ // about whether the hardware-verify was performed — that gap let #1027 sit at
1065
+ // 'confirmed' unverified. Require an explicit { verified: true } attestation.
1066
+ // Defense-in-depth: /builder-ship passes it from the --verified flag.
1067
+ const confirmTaskRow = await db.getTask(id);
1068
+ if (isVerifyTask(confirmTaskRow) && req.body?.verified !== true) {
1069
+ return res.fail('verify_attestation_required', { status: 409, message: `Task #${id} is a verification task (kind='verify') — confirm it only AFTER the verification ` +
1070
+ `actually passed, with { "verified": true }. Re-run /builder-ship ${id} --verified once it has.` });
1071
+ }
1072
+ if (graderBypassEnabled()) {
1073
+ console.log(`[gds] grader-bypass confirm: builder ${req.builder.id} (${req.builder.rank}) confirming task ${id} with GRADER_BYPASS_ENABLED on`);
1074
+ }
1075
+ try {
1076
+ const result = await db.confirmTask({ taskId: id, builderId: req.builder.id });
1077
+ res.json({ ok: true, ...result });
1078
+ } catch (err) {
1079
+ if (err.code === 'TASK_NOT_COMPLETED') {
1080
+ return res.fail('task_not_completed', 409);
1081
+ }
1082
+ console.error('[gds] POST /tasks/:id/confirm', err);
1083
+ res.fail('confirm_failed', { status: 500, message: 'internal error' });
1084
+ }
1085
+ });
1086
+
1087
+ // POST /tasks/:id/abandon — terminal "retire this task" transition (task 987).
1088
+ // The lifecycle had promote/ship/confirm/resolve but no first-class way to KILL
1089
+ // a task that is no longer relevant: the only paths to status='abandoned' were
1090
+ // offboarding a builder or a raw SQL UPDATE, so stale/moot tasks lingered in
1091
+ // 'ready' as claimable noise. Refuses on a shipped task (immutable history) and
1092
+ // on a task with an active claim (release it first); idempotent on an already-
1093
+ // abandoned task. The abandon + its `reason` land in audit_log via the writing
1094
+ // middleware, so the ledger keeps a record of who retired what.
1095
+ // rank: archon — terminal lifecycle mutation / scope removal (ADR 0016). This
1096
+ // is the inverse of /promote and belongs to the same archon-gated scope-control
1097
+ // surface, never self-serviceable by a lower rank.
1098
+ router.post('/tasks/:id/abandon', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
1099
+ if (validateOrRespond(req, res, {
1100
+ reason: { type: 'string' }, // free-text retire reason (sliced to 2000 below)
1101
+ })) return;
1102
+ const id = parseId(req, res, { code: 'bad_task_id', positiveInt: true });
1103
+ if (id === null) return;
1104
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason.slice(0, 2000) : '';
1105
+ try {
1106
+ const result = await db.abandonTask({ taskId: id, reason });
1107
+ res.json({ ok: true, ...result });
1108
+ } catch (err) {
1109
+ if (err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
1110
+ if (err.code === 'CANNOT_ABANDON_SHIPPED') {
1111
+ return res.fail('cannot_abandon_shipped', { status: 409, message: 'shipped tasks are immutable history' });
1112
+ }
1113
+ if (err.code === 'TASK_HAS_ACTIVE_CLAIM') {
1114
+ return res.fail('task_has_active_claim', { status: 409, message: 'release the active claim before abandoning' });
1115
+ }
1116
+ console.error('[gds] POST /tasks/:id/abandon', err);
1117
+ res.fail('abandon_failed', { status: 500, message: 'internal error' });
1118
+ }
1119
+ });
1120
+
1121
+ // ==== peer votes (V3.R25 #245) — criterion C2 karma signal sourcing =======
1122
+ // These three sit BEFORE the generic /tasks/:id-shaped routes that take a
1123
+ // numeric id, but Express keys on the literal path so /tasks/peer-votes/feed
1124
+ // (3 segments, non-numeric) never collides with /tasks/:id (2 segments).
1125
+
1126
+ // GET /tasks/peer-votes/feed — recent confirmed/shipped work by OTHER
1127
+ // builders, each with its net vote total and the caller's own current vote,
1128
+ // so the hall can render a vote widget per row. Any builder may view; only
1129
+ // Metic+ may actually vote (enforced on POST below). Excludes the caller's
1130
+ // own ships (you can't vote on yourself).
1131
+ // rank: any-builder — read of the votable feed.
1132
+ router.get('/tasks/peer-votes/feed', auth.requireBuilder, async (req, res) => {
1133
+ // R14 (#2001 / ADR 0119): shared helper; per-endpoint cap preserved (default 25, max 100).
1134
+ const { limit } = api.parsePagination(req, { defaultLimit: 25, maxLimit: 100 });
1135
+ try {
1136
+ const { rows } = await pool.query(
1137
+ `SELECT t.id, t.title, t.value_summary, t.version_id, t.status,
1138
+ t.shipped_at, t.shipped_by,
1139
+ b.github_login, b.display_name,
1140
+ COALESCE(pv.net, 0)::int AS net_votes,
1141
+ mine.vote AS my_vote
1142
+ FROM tasks t
1143
+ LEFT JOIN builders b ON b.id = t.shipped_by
1144
+ LEFT JOIN LATERAL (
1145
+ SELECT SUM(vote) AS net
1146
+ FROM peer_votes WHERE target_task_id = t.id
1147
+ ) pv ON true
1148
+ LEFT JOIN peer_votes mine
1149
+ ON mine.target_task_id = t.id AND mine.voter_builder_id = $1
1150
+ WHERE t.status IN ('confirmed', 'shipped')
1151
+ AND t.shipped_by IS NOT NULL
1152
+ AND t.shipped_by <> $1
1153
+ AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
1154
+ ORDER BY COALESCE(t.shipped_at, t.updated_at) DESC NULLS LAST
1155
+ LIMIT $2`,
1156
+ [req.builder.id, limit]
1157
+ );
1158
+ res.json({ tasks: rows, can_vote: ['metic', 'archon'].includes(req.builder.rank) });
1159
+ } catch (err) {
1160
+ // Log the detail server-side; don't return err.message — a raw Postgres
1161
+ // error can leak schema internals (table/column/constraint names).
1162
+ console.error('[gds] GET /tasks/peer-votes/feed', err);
1163
+ res.fail('feed_failed', 500);
1164
+ }
1165
+ });
1166
+
1167
+ // POST /tasks/peer-votes/tally — fold matured votes (≥7 days old, not yet
1168
+ // counted) into karma_log, one net row per shipping builder, then mark them
1169
+ // tallied. Idempotent + transactional. Driven by the weekly cron
1170
+ // (.claude/scheduled-tasks/peer-vote-tally/). Archon-gated — it writes karma.
1171
+ // rank: archon — writes karma_log on others' behalf; scope-shaping reputation.
1172
+ // The tally transaction lives in db.tallyPeerVotes() (task 1382, ADR 0078
1173
+ // amendment) so this route AND the server-internal weekly timer (the carved
1174
+ // autonomy module's poller, modules/autonomy/pollers/routine-timers.js, which
1175
+ // reaches it via the doorway api.tallyPeerVotes — BV1.R83) share one code path
1176
+ // and can never diverge. This route keeps the Archon gate (the HTTP surface
1177
+ // stays Archon-only); the in-process timer has no requester to authorize.
1178
+ router.post('/tasks/peer-votes/tally', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
1179
+ try {
1180
+ const result = await db.tallyPeerVotes();
1181
+ res.json(result);
1182
+ } catch (err) {
1183
+ console.error('[gds] POST /tasks/peer-votes/tally', err);
1184
+ res.fail('tally_failed', 500);
1185
+ }
1186
+ });
1187
+
1188
+ // POST /tasks/:id/vote { vote: 1 | -1 | 0 } — cast/change/clear a peer vote on
1189
+ // a confirmed/shipped task. Metic+ only (Xenos read-only on the project).
1190
+ // - 403 anti-self-vote if the caller shipped the task
1191
+ // - 409 if the task isn't votable, or the vote was already folded into karma
1192
+ // - vote: 0 clears (deletes the row, only while still untallied)
1193
+ // rank: metic+archon — a trusted-builder reputation action.
1194
+ router.post('/tasks/:id/vote', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
1195
+ if (validateOrRespond(req, res, {
1196
+ vote: { required: true, type: 'integer', enum: [-1, 0, 1] },
1197
+ })) return;
1198
+ const id = parseId(req, res, { code: 'bad_task_id' });
1199
+ if (id === null) return;
1200
+ const vote = req.body ? req.body.vote : undefined;
1201
+ if (![-1, 0, 1].includes(vote)) {
1202
+ return res.fail('bad_vote', 400, { detail: 'vote must be 1, -1, or 0 (clear)' });
1203
+ }
1204
+ try {
1205
+ const { rows: taskRows } = await pool.query(
1206
+ `SELECT id, status, shipped_by FROM tasks WHERE id = $1`,
1207
+ [id]
1208
+ );
1209
+ if (!taskRows.length) return res.fail('task_not_found', 404);
1210
+ const task = taskRows[0];
1211
+ if (!VOTABLE_TASK_STATUSES.has(task.status)) {
1212
+ return res.fail('task_not_votable', 409, { detail: 'only confirmed or shipped work can be voted on', status: task.status });
1213
+ }
1214
+ if (task.shipped_by == null) {
1215
+ return res.fail('no_shipping_builder', 409, { detail: 'task has no shipped_by to credit' });
1216
+ }
1217
+ if (Number(task.shipped_by) === Number(req.builder.id)) {
1218
+ return res.fail('self_vote_forbidden', 403, { detail: 'you cannot vote on your own work' });
1219
+ }
1220
+ // A vote already folded into karma is frozen. The freeze-check and the
1221
+ // write are done in ONE statement so the weekly tally can't commit
1222
+ // between a separate check and write (no TOCTOU): the conditional
1223
+ // ON CONFLICT … WHERE tallied_at IS NULL only mutates an untallied row,
1224
+ // and RETURNING tells us whether it took. Clearing keeps the same
1225
+ // tallied_at IS NULL guard. (recorded_at is left untouched on change so
1226
+ // the 7-day lag clock isn't reset.)
1227
+ if (vote === 0) {
1228
+ const del = await pool.query(
1229
+ `DELETE FROM peer_votes WHERE voter_builder_id = $1 AND target_task_id = $2 AND tallied_at IS NULL RETURNING id`,
1230
+ [req.builder.id, id]
1231
+ );
1232
+ // A 0-row delete is ambiguous: either there was no vote (harmless
1233
+ // no-op) or the vote is frozen (tallied). Disambiguate so we don't tell
1234
+ // the client "cleared" when a tallied vote actually survived — same
1235
+ // freeze semantics as the cast path below.
1236
+ if (del.rowCount === 0) {
1237
+ const ex = await pool.query(
1238
+ `SELECT 1 FROM peer_votes WHERE voter_builder_id = $1 AND target_task_id = $2`,
1239
+ [req.builder.id, id]
1240
+ );
1241
+ if (ex.rowCount > 0) {
1242
+ return res.fail('vote_already_counted', 409, { detail: 'this vote was folded into karma and can no longer be changed' });
1243
+ }
1244
+ }
1245
+ } else {
1246
+ const up = await pool.query(
1247
+ `INSERT INTO peer_votes (voter_builder_id, target_task_id, vote)
1248
+ VALUES ($1, $2, $3)
1249
+ ON CONFLICT (voter_builder_id, target_task_id)
1250
+ DO UPDATE SET vote = EXCLUDED.vote
1251
+ WHERE peer_votes.tallied_at IS NULL
1252
+ RETURNING id`,
1253
+ [req.builder.id, id, vote]
1254
+ );
1255
+ // rowCount 0 ⟺ the conflicting row was already tallied (the WHERE
1256
+ // filtered the update and the insert was a no-op on conflict).
1257
+ if (up.rowCount === 0) {
1258
+ return res.fail('vote_already_counted', 409, { detail: 'this vote was folded into karma and can no longer be changed' });
1259
+ }
1260
+ }
1261
+ const { net, total } = await peerVoteNet(id);
1262
+ res.json({ ok: true, task_id: id, my_vote: vote === 0 ? null : vote, net_votes: net, total_votes: total });
1263
+ } catch (err) {
1264
+ console.error('[gds] POST /tasks/:id/vote', err);
1265
+ res.fail('vote_failed', 500);
1266
+ }
1267
+ });
1268
+
1269
+ // GDS-V3 task V3.R18 (#209). Subagent grader credits gate.
1270
+ //
1271
+ // ship.js (or any caller) spawns an Opus subagent against the diff + task
1272
+ // description, then POSTs the subagent's JSON to this endpoint. The
1273
+ // endpoint validates+scores the JSON, writes the task_grades row, and —
1274
+ // if both functional_fidelity and code_quality clear threshold — promotes
1275
+ // the task from completed→confirmed and awards credits.
1276
+ //
1277
+ // Request body:
1278
+ // {
1279
+ // subagent_output: <object or string — the subagent's JSON>,
1280
+ // signals?: <object — optional run metadata: model, run_ms, etc.>
1281
+ // }
1282
+ //
1283
+ // Failure modes (none of which throw — all return a graded row + 200):
1284
+ // - malformed subagent JSON → passed=false, schema/parse_error in signals
1285
+ // - functional_fidelity < threshold → passed=false
1286
+ // - code_quality < threshold → passed=false
1287
+ // - confidence < low-confidence cap → passed=false
1288
+ // Any of these leaves the task at `completed`; builder can manual-confirm
1289
+ // via POST /tasks/:id/confirm if they think the grade was wrong.
1290
+ // rank: any-builder — grader writes a row + auto-promotes only if grade clears
1291
+ // threshold; the grader subagent is the objectivity guarantee. Supervisor hot
1292
+ // path (preserved per CRITICAL §4 — no requireRank).
1293
+ router.post('/tasks/:id/grade', auth.requireBuilder, async (req, res) => {
1294
+ // Permissive on subagent_output type — the grader handles both object AND
1295
+ // JSON-string forms (see tests/grader.mjs "accepts JSON-string input").
1296
+ // Only enforce presence + light shape on signals (extras pass through).
1297
+ if (validateOrRespond(req, res, {
1298
+ // subagent_output is an object OR a JSON string — the grader handles both
1299
+ // (tests/grader.mjs); declared untyped so strict-mode accepts it (its deep
1300
+ // shape is validated by the grader, and its presence by the guard below).
1301
+ subagent_output: {},
1302
+ signals: { type: 'object' },
1303
+ // touches[] cleanup [3/8] (task 878): ship.js sends the real committed
1304
+ // changed-files so the shipper badges key off the diff, not touches[].
1305
+ committed_files: { type: 'array', maxItems: 5000, itemsType: 'string' },
1306
+ diff_hash: { type: 'string', maxLength: 128 },
1307
+ })) return;
1308
+ const id = parseId(req, res, { code: 'bad_task_id' });
1309
+ if (id === null) return;
1310
+
1311
+ // SECURITY (#871): only the claim holder (or an Archon for --regrade) may
1312
+ // grade — a builder must not be able to grade work they never claimed.
1313
+ if (!(await gateTaskOwnership(req, res, id))) return;
1314
+
1315
+ const { subagent_output: subagentOutput, signals, committed_files: committedFiles, diff_hash: diffHash } = req.body || {};
1316
+ if (subagentOutput == null) {
1317
+ return res.fail('subagent_output_required', 400);
1318
+ }
1319
+
1320
+ // grading carved to modules/grading/ (BV1.R73 / ADR 0091 §4): resolve the
1321
+ // `grade` kernel PORT. resolveOptional (not resolve) so a grading-disabled
1322
+ // instance returns a clean 503 rather than a thrown required-port error;
1323
+ // on OTB grading is default-on, so this is byte-identical to the old import.
1324
+ const grading = seams.resolveOptional('grade');
1325
+ if (!grading) {
1326
+ return res.fail('grading_unavailable', { status: 503, message: 'grading is disabled on this instance' });
1327
+ }
1328
+ const clientGrade = grading.validateAndScore(subagentOutput, signals || {});
1329
+
1330
+ // SR-9 / SR-18 (task 992): server-authoritative hardening. validateAndScore
1331
+ // above computes `passed` from the CLIENT's submitted scores — the real
1332
+ // adversarial panel + the deterministic pre-passes run client-side in
1333
+ // ship.js and the server never re-runs them, so a builder could POST a
1334
+ // forged {ff:10,cq:10,issues:[]} to self-pass. serverSideGuards re-runs the
1335
+ // two DETERMINISTIC pre-passes here (route-rank against the server's own
1336
+ // route files; permission-path against the caller's LIVE, authoritative
1337
+ // rank — req.builder.rank, read fresh from the DB per request), rejects an
1338
+ // implausible empty-issues PASS on a non-trivial diff, and neutralizes an
1339
+ // obviously-forged net-negative credit signal (SR-18). It is DOWNGRADE-ONLY:
1340
+ // it can flip a client PASS to a hard FAIL or manual-confirm, but never
1341
+ // upgrades a fail and never relaxes the existing gate — so a legitimate
1342
+ // honest grade (and the Archon / GRADER_BYPASS confirm path, which lands via
1343
+ // /tasks/:id/confirm, NOT this route) is unaffected.
1344
+ const gradeResult = grading.serverSideGuards(clientGrade, {
1345
+ committedFiles: Array.isArray(committedFiles) ? committedFiles : null,
1346
+ builderRank: req.builder && req.builder.rank, // SERVER-KNOWN authoritative rank
1347
+ diffStats: (signals && signals.diff_stats) || null,
1348
+ });
1349
+
1350
+ try {
1351
+ const result = await db.applyGrade({
1352
+ taskId: id,
1353
+ builderId: req.builder.id,
1354
+ gradeResult,
1355
+ committedFiles: Array.isArray(committedFiles) ? committedFiles : null,
1356
+ diffHash: typeof diffHash === 'string' && diffHash.length > 0 ? diffHash : null,
1357
+ });
1358
+ res.json({ ok: true, ...result, just_unlocked: result.justUnlocked ?? [] });
1359
+ } catch (err) {
1360
+ if (err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
1361
+ if (err.code === 'TASK_NOT_GRADABLE') {
1362
+ return res.fail('task_not_gradable', 409, { current_status: err.current_status });
1363
+ }
1364
+ console.error('[gds] POST /tasks/:id/grade', err);
1365
+ res.fail('grade_failed', { status: 500, message: 'internal error' });
1366
+ }
1367
+ });
1368
+
1369
+ // POST /tasks/:id/override-request — V3.R74 (#293). The rare path: a builder
1370
+ // whose ship failed the subagent grader believes the grader was wrong and
1371
+ // asks an Archon to flip the task to confirmed anyway. Audit-logged via the
1372
+ // global write-audit middleware. Open requests surface in the Archon queue.
1373
+ // rank: any-builder — opens a request; the decision still rests with Archon.
1374
+ router.post('/tasks/:id/override-request', auth.requireBuilder, async (req, res) => {
1375
+ const id = parseId(req, res, { code: 'bad_task_id' });
1376
+ if (id === null) return;
1377
+ if (validateOrRespond(req, res, {
1378
+ rationale: { required: true, type: 'string', minLength: 20, maxLength: 4000 },
1379
+ })) return;
1380
+ // SECURITY (SR-16 / #996 — mirrors the #871 gate on /grade, /confirm, /ship):
1381
+ // only the task's most-recent claim holder (or an Archon) may file an
1382
+ // override-request. Without this, a SECOND builder could open a request on
1383
+ // another builder's `completed` task; on Archon approval that historically
1384
+ // routed the credit + ship attribution toward the requester, so the gate is
1385
+ // the front-line defence against cross-builder credit/attribution theft (the
1386
+ // decideOverrideRequest attribution fix from #538 is the second layer). An
1387
+ // Archon always passes; a never-claimed task falls back to ALLOW so a legit
1388
+ // edge case is never wedged.
1389
+ if (!(await gateTaskOwnership(req, res, id))) return;
1390
+ try {
1391
+ const row = await db.createOverrideRequest({
1392
+ taskId: id,
1393
+ requesterId: req.builder.id,
1394
+ rationale: req.body.rationale.trim(),
1395
+ });
1396
+ res.status(201).json({ ok: true, request: row });
1397
+ } catch (err) {
1398
+ if (err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
1399
+ if (err.code === 'TASK_NOT_COMPLETED') {
1400
+ return res.fail('task_not_completed', 409, { current_status: err.currentStatus });
1401
+ }
1402
+ if (err.code === 'OPEN_REQUEST_EXISTS') {
1403
+ return res.fail('open_request_exists', 409);
1404
+ }
1405
+ console.error('[gds] POST /tasks/:id/override-request', err);
1406
+ res.fail('override_request_failed', { status: 500, message: 'internal error' });
1407
+ }
1408
+ });
1409
+
1410
+ // ==========================================================================
1411
+ // Server-mediated branch publish (ADR 0055 / task 1025)
1412
+ // --------------------------------------------------------------------------
1413
+ // A builder on a cloud dev box has NO GitHub push credential (#919/ADR 0053
1414
+ // box-scope + credential-less origin), so in `ci` deploy mode ship.js cannot
1415
+ // `git push` + `gh pr create`. Instead the box uploads its branch as a THIN
1416
+ // git bundle here; the server pushes it + opens the PR + enables auto-merge
1417
+ // using its own push credential — a GitHub App installation token (preferred,
1418
+ // task 1028) or the GITHUB_PUSH_TOKEN PAT fallback; single repo, Contents:write
1419
+ // + PR:write + Actions:read — it CANNOT push to branch-protected main nor bypass
1420
+ // required checks. The box holds NO github credential, so the #919 model is
1421
+ // preserved.
1422
+ //
1423
+ // AUTH: requireBuilder + gateTaskOwnership, deliberately NOT allowBoxScope — a
1424
+ // box-scoped session is 403'd here (it must `/builder-reauth` to a real cli
1425
+ // session first). The grade already ran client-side against this exact HEAD
1426
+ // (ship.js grades BEFORE this call), and the bundle-tip == claimed head_sha
1427
+ // check below binds the pushed code to that graded sha.
1428
+ const publishUpload = express.json({ limit: PUBLISH_BODY_LIMIT });
1429
+ router.post('/tasks/:id/publish-branch', auth.requireBuilder, publishUpload, async (req, res) => {
1430
+ const id = parseId(req, res, { code: 'bad_task_id' });
1431
+ if (id === null) return;
1432
+ if (validateOrRespond(req, res, {
1433
+ branch: { required: true, type: 'string', maxLength: 200, minLength: 1 },
1434
+ head_sha: { required: true, type: 'string', maxLength: 64, minLength: 7 },
1435
+ bundle_b64: { required: true, type: 'string', maxLength: PUBLISH_BUNDLE_B64_MAX },
1436
+ value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
1437
+ })) return;
1438
+ // SECURITY (#871 gate, same as /grade /confirm /ship): only the claim holder
1439
+ // (or an Archon) may publish this task's branch.
1440
+ if (!(await gateTaskOwnership(req, res, id))) return;
1441
+ if (!githubPush.isConfigured()) {
1442
+ return res.fail('push_unconfigured', { status: 503, message: 'Server-mediated push is not configured (no GitHub App nor GITHUB_PUSH_TOKEN on the server). ' +
1443
+ 'Ask the operator to provision it; meanwhile ship from a machine with its own git push credentials.' });
1444
+ }
1445
+ const { branch, head_sha: headSha, value_summary: valueSummary } = req.body;
1446
+ if (!githubPush.isSafeBranch(branch)) return res.fail('bad_branch', 400);
1447
+ if (!/^[0-9a-f]{7,64}$/.test(headSha)) return res.fail('bad_head_sha', 400);
1448
+
1449
+ // Decode + byte-bound the bundle. The JSON `limit` bounds the ENCODED payload;
1450
+ // base64 inflates ~33%, so cap the DECODED bytes too (SR-17 disk-fill posture).
1451
+ let bundleBuf;
1452
+ try {
1453
+ bundleBuf = Buffer.from(req.body.bundle_b64, 'base64');
1454
+ } catch (_) {
1455
+ return res.fail('bad_bundle_encoding', 400);
1456
+ }
1457
+ if (!bundleBuf.length) return res.fail('empty_bundle', 400);
1458
+ if (bundleBuf.length > PUBLISH_BUNDLE_MAX_BYTES) {
1459
+ return res.fail('bundle_too_large', 413, { max_bytes: PUBLISH_BUNDLE_MAX_BYTES });
1460
+ }
1461
+
1462
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-publish-in-'));
1463
+ const bundlePath = path.join(tmpDir, 'branch.bundle');
1464
+ try {
1465
+ fs.writeFileSync(bundlePath, bundleBuf);
1466
+ const { head_sha: pushedSha } = await githubPush.pushBundle({ bundlePath, branch, headSha });
1467
+ const title = `task ${id}: ${deHashRefs(valueSummary) || branch}`.slice(0, 240);
1468
+ const body =
1469
+ `Auto-opened by GDS server-mediated publish (ADR 0055) for task ${id}.\n\n` +
1470
+ `${deHashRefs(valueSummary) || '(no value summary)'}\n\n` +
1471
+ `Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
1472
+ `Auto-merge is enabled — GitHub lands it once the required checks pass.`;
1473
+ const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
1474
+ const autoMerge = await githubPush.enableAutoMerge({ nodeId: pr.node_id });
1475
+ // task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
1476
+ // gate-review check can auto-approve a Metic+ author's gate-surface PR with no
1477
+ // owner click. Only the SERVER can set this (it holds req.builder's server-
1478
+ // resolved rank + the App credential) — CI cannot forge it, and a PR-title rank
1479
+ // lookup would be spoofable. Shared with POST /tasks/:id/attest-gate (the laptop
1480
+ // ci-land path — task 1169). Best-effort: a failure leaves the PR un-attested,
1481
+ // so it escalates (the safe direction).
1482
+ await postGateAuthorTrust({ rank: req.builder.rank, sha: pushedSha, taskId: id });
1483
+ // task 1547: persist the tip we ACTUALLY pushed so the confirmed->shipped
1484
+ // reconcile proves THIS task's land from its own commit, not from a (reusable)
1485
+ // branch name. Without it, a reused worktree branch can match an older task's
1486
+ // already-merged PR and false-flip confirmed->shipped (the BV1.R04/R05 strand:
1487
+ // branch claude/jovial-rosalind-84d6c3 had earlier been PR #386 / task 1297).
1488
+ // Best-effort: a miss just falls the reconcile back to the PR-ref-this-task check.
1489
+ try {
1490
+ await db.setClaimPublishedSha({ taskId: id, builderId: req.builder.id, sha: pushedSha });
1491
+ } catch (e) {
1492
+ console.error('[gds] setClaimPublishedSha (non-blocking)', id, e && e.message);
1493
+ }
1494
+ return res.status(201).json({
1495
+ ok: true, pr_url: pr.url, pr_number: pr.number, head_sha: pushedSha, auto_merge: autoMerge,
1496
+ });
1497
+ } catch (e) {
1498
+ if (e.code === 'TIP_MISMATCH') {
1499
+ return res.fail('tip_mismatch', { status: 400, message: `The uploaded bundle's tip (${String(e.tip || '').slice(0, 8)}) does not match the ` +
1500
+ `claimed head_sha (${headSha.slice(0, 8)}). Commit your work and re-ship from a clean tree.` });
1501
+ }
1502
+ if (e.code === 'BAD_BRANCH') return res.fail('bad_branch', 400);
1503
+ if (e.code === 'BAD_SHA') return res.fail('bad_head_sha', 400);
1504
+ if (e.code === 'NO_REPO_INFO') return res.fail('no_repo_info', 500);
1505
+ if (e.code === 'PUSH_UNCONFIGURED') return res.fail('push_unconfigured', 503);
1506
+ if (e.code === 'PR_CREATE_FAILED') return res.fail('pr_create_failed', 502, { detail: e.detail });
1507
+ // task 1547: GIT_FAILED now carries WHICH git step failed + the credential-
1508
+ // scrubbed stderr (github-push.defaultExec). Log both so the next push outage
1509
+ // is a one-line diagnosis, not a multi-hour opaque-`publish_failed` mystery
1510
+ // (the BV1.R04/R05 incident: ~2h of bare GIT_FAILED with no cause). Return the
1511
+ // step + a short scrubbed reason to the (ownership-gated) builder too.
1512
+ console.error(
1513
+ '[gds] POST /tasks/:id/publish-branch', e.code || e.message,
1514
+ e.git_step ? `step=${e.git_step}` : '',
1515
+ e.stderr ? `stderr=${e.stderr}` : ''
1516
+ );
1517
+ return res.fail('publish_failed', 500, { ...(e.git_step ? { step: e.git_step } : {}), ...(e.stderr ? { reason: String(e.stderr).slice(0, 200) } : {}) });
1518
+ } finally {
1519
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
1520
+ }
1521
+ });
1522
+
1523
+ // GET /tasks/:id/publish-status?branch=<branch> — poll the PR + prod-deploy
1524
+ // state for a server-mediated publish so ship.js can confirm the land WITHOUT
1525
+ // gh on the box. Ownership-gated like the POST. Derives state live from GitHub.
1526
+ router.get('/tasks/:id/publish-status', auth.requireBuilder, async (req, res) => {
1527
+ const id = parseId(req, res, { code: 'bad_task_id' });
1528
+ if (id === null) return;
1529
+ if (!(await gateTaskOwnership(req, res, id))) return;
1530
+ if (!githubPush.isConfigured()) return res.fail('push_unconfigured', 503);
1531
+ const branch = req.query.branch;
1532
+ if (!githubPush.isSafeBranch(branch)) return res.fail('bad_branch', 400);
1533
+ try {
1534
+ const status = await githubPush.getPublishStatus({ branch });
1535
+ // task 1049 (B — server-side reconcile-on-read): when GitHub shows the PR
1536
+ // MERGED and deploy-prod SUCCEEDED, finish the task right here so a
1537
+ // confirmed-but-unshipped task never strands waiting on a human to re-poll
1538
+ // (the #1027 retrospective: a successful publish left the task at confirmed
1539
+ // forever). Idempotent + safe: db.shipTask is the exact flip POST /ship
1540
+ // runs, throws TASK_NOT_CONFIRMED when the task isn't at 'confirmed'
1541
+ // (already shipped, or never confirmed) — which we swallow. Same authority
1542
+ // as /ship: this route is gateTaskOwnership-gated.
1543
+ let reconciled = false;
1544
+ let reconcile_blocked = null;
1545
+ // task 1547: landConfirmed alone is NOT enough — getPublishStatus matches the
1546
+ // most-recent PR for a branch NAME, and worktree branch names get reused, so a
1547
+ // merged-but-OLDER PR can satisfy landConfirmed for a task whose own push never
1548
+ // landed (the BV1.R04/R05 false-ship off PR #386 / task 1297). Require proof that
1549
+ // THIS task's published tip is in main before flipping confirmed->shipped.
1550
+ if (githubPush.landConfirmed(status)) {
1551
+ const publishedSha = await db.getClaimPublishedSha(id);
1552
+ const proof = await githubPush.landProvenForTask({ taskId: id, publishedSha, status });
1553
+ if (!proof.proven) {
1554
+ reconcile_blocked = proof.reason;
1555
+ console.warn(`[gds] publish-status reconcile BLOCKED task ${id}: ${proof.reason}`);
1556
+ }
1557
+ }
1558
+ if (githubPush.landConfirmed(status) && !reconcile_blocked) {
1559
+ try {
1560
+ const shipped = await db.shipTask({ taskId: id, builderId: req.builder.id });
1561
+ reconciled = true;
1562
+ // task 1045: the box that published can't post the Discord broadcast —
1563
+ // it holds no webhook secret (ADR 0053 "box holds nothing"), so a box
1564
+ // ship was silently skipping the public announcement. The server holds
1565
+ // the secrets and just confirmed the deploy, so it fires the broadcast
1566
+ // here — exactly ONCE, on the confirmed→shipped transition. shipTask
1567
+ // throws TASK_NOT_CONFIRMED on a repeat poll, so this never re-posts.
1568
+ // Best-effort: emit task.shipped; the discord module (if enabled) posts
1569
+ // the #ship-news / #ship-feed broadcast on its listener. emitAsync
1570
+ // isolates listener errors, so a broadcast hiccup can never turn a
1571
+ // successful reconcile into a 502 (ADR 0083 / BV1.R47 — replaces the old
1572
+ // direct require('../ship-broadcast') core→discord import).
1573
+ await seams.emitAsync('task.shipped', shipped);
1574
+ } catch (err) {
1575
+ if (err.code !== 'TASK_NOT_CONFIRMED') throw err;
1576
+ }
1577
+ }
1578
+ // task 1162: if GitHub still shows the PR open (not merged) and we didn't
1579
+ // just ship, GitHub's auto-merge daemon may have stalled on an already-green
1580
+ // PR (the PR #125 case: 7/7 checks green, CLEAN, never merged). NUDGE the
1581
+ // merge server-side AND surface the real blocker, so a credential-less
1582
+ // session reports the truth (pending/failing check, review required) instead
1583
+ // of guessing a non-existent gate and asking the owner to click merge. This
1584
+ // mirrors the reconcile-on-read precedent above (a GET that finishes the
1585
+ // land); best-effort so a merge-driver miss never turns the poll into a 502.
1586
+ let merge_driver = null;
1587
+ if (!reconciled && !status.merged && status.pr_state === 'open') {
1588
+ try {
1589
+ merge_driver = await githubPush.mergeIfGreen({ branch });
1590
+ } catch (e) {
1591
+ console.error('[gds] publish-status merge-driver (non-blocking):', e && e.message);
1592
+ }
1593
+ }
1594
+ return res.json({ ok: true, ...status, reconciled, reconcile_blocked, merge_driver });
1595
+ } catch (e) {
1596
+ if (e.code === 'NO_REPO_INFO') return res.fail('no_repo_info', 500);
1597
+ console.error('[gds] GET /tasks/:id/publish-status', e.code || e.message);
1598
+ return res.fail('status_failed', 502);
1599
+ }
1600
+ });
1601
+
1602
+ // POST /tasks/:id/attest-gate — body: { head_sha }. The LAPTOP ci-land analogue
1603
+ // of the publish-branch attestation (task 1169). ship.js's local push path
1604
+ // (pushVia 'local') pushes the branch + opens the PR with its OWN gh credential
1605
+ // but CANNOT forge the trustless gate-author-trust commit status — only the server
1606
+ // can (it holds req.builder's server-resolved rank). Without this, an ENFORCING
1607
+ // ADR-0058 gate-review BLOCKS a Metic+ author's gate-surface PR shipped from a
1608
+ // laptop and they can't self-approve their own PR (hit live on task 1162 / PR #131).
1609
+ //
1610
+ // AUTH mirrors publish-branch EXACTLY: requireBuilder + gateTaskOwnership, and
1611
+ // deliberately NOT allowBoxScope (a box-scoped session 403s — it must reauth). The
1612
+ // attestation power is bounded: gate-author-trust clears a NEEDS_TRUST surface for
1613
+ // a Metic+ author only and NEVER the hard floor (which always needs an owner
1614
+ // review), so a Metic+ vouching for their own task's head stays within their tier.
1615
+ router.post('/tasks/:id/attest-gate', auth.requireBuilder, async (req, res) => {
1616
+ const id = parseId(req, res, { code: 'bad_task_id' });
1617
+ if (id === null) return;
1618
+ if (validateOrRespond(req, res, {
1619
+ head_sha: { required: true, type: 'string', maxLength: 64, minLength: 7 },
1620
+ })) return;
1621
+ if (!(await gateTaskOwnership(req, res, id))) return;
1622
+ const { head_sha: headSha } = req.body;
1623
+ if (!/^[0-9a-f]{7,64}$/.test(headSha)) return res.fail('bad_head_sha', 400);
1624
+ if (!githubPush.isConfigured()) {
1625
+ return res.fail('push_unconfigured', { status: 503, message: 'No server push credential (GitHub App / GITHUB_PUSH_TOKEN) to post the gate attestation. ' +
1626
+ 'A gate-surface PR will need an owner review until the operator provisions it.' });
1627
+ }
1628
+ const { attested, reason } = await postGateAuthorTrust({ rank: req.builder.rank, sha: headSha, taskId: id });
1629
+ return res.json({ ok: true, attested, reason });
1630
+ });
1631
+
1632
+ // GET /override-requests?status=open — Archon queue. Status defaults to open;
1633
+ // pass ?status=approved or ?status=denied for the resolved history.
1634
+ // rank: archon — same gate as the audit log + Archon view.
1635
+ router.get('/override-requests', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
1636
+ const status = req.query.status || 'open';
1637
+ if (!['open', 'approved', 'denied'].includes(status)) {
1638
+ return res.fail('bad_status', 400);
1639
+ }
1640
+ try {
1641
+ const rows = await db.listOverrideRequests({ status });
1642
+ res.json({ requests: rows });
1643
+ } catch (err) {
1644
+ console.error('[gds] GET /override-requests', err);
1645
+ res.fail('list_failed', { status: 500, message: 'internal error' });
1646
+ }
1647
+ });
1648
+
1649
+ // POST /override-requests/:id/decide — body: { decision: 'approve'|'deny', note? }
1650
+ // On approve, atomically flips the task completed→confirmed and awards the
1651
+ // credits to the original requester (NOT the Archon). On deny, only the row
1652
+ // updates; the task stays at completed and the builder must release or
1653
+ // re-ship after fixing whatever the grader caught.
1654
+ // rank: archon — grader-bypass authority; same gate as POST /tasks/:id/confirm.
1655
+ router.post('/override-requests/:id/decide', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
1656
+ const id = parseId(req, res, { code: 'bad_request_id' });
1657
+ if (id === null) return;
1658
+ if (validateOrRespond(req, res, {
1659
+ decision: { required: true, type: 'string' },
1660
+ note: { type: 'string', maxLength: 4000 },
1661
+ })) return;
1662
+ const { decision, note } = req.body || {};
1663
+ if (decision !== 'approve' && decision !== 'deny') {
1664
+ return res.fail('bad_decision', { status: 400, message: "decision must be 'approve' or 'deny'" });
1665
+ }
1666
+ try {
1667
+ const result = await db.decideOverrideRequest({
1668
+ requestId: id,
1669
+ archonId: req.builder.id,
1670
+ decision,
1671
+ note,
1672
+ });
1673
+ res.json({ ok: true, ...result });
1674
+ } catch (err) {
1675
+ if (err.code === 'REQUEST_NOT_OPEN') {
1676
+ return res.fail('request_not_open', 409);
1677
+ }
1678
+ if (err.code === 'OVERRIDE_NO_REQUESTER') {
1679
+ // Data-integrity guard (#538): an approved override with no requester to
1680
+ // credit. Should never happen (NOT NULL column); refuse rather than
1681
+ // misattribute. 409 — the request is left open for inspection.
1682
+ return res.fail('override_no_requester', { status: 409, message: 'internal error' });
1683
+ }
1684
+ console.error('[gds] POST /override-requests/:id/decide', err);
1685
+ res.fail('decide_failed', { status: 500, message: 'internal error' });
1686
+ }
1687
+ });
1688
+
1689
+ return router;
1690
+ };
1691
+
1692
+ // Exported for unit testing + so callers can reuse the exact detection logic.
1693
+ module.exports.detectsMigration = detectsMigration;
1694
+ module.exports.graderBypassEnabled = graderBypassEnabled;
1695
+ module.exports.MIGRATION_KEYWORD_RE = MIGRATION_KEYWORD_RE;
1696
+ // #871: exported so tests/task_ownership_gate.mjs can drive the claim-ownership
1697
+ // gate directly (with a stubbed db.getLastClaimHolder) — no server/DB needed.
1698
+ module.exports.gateTaskOwnership = gateTaskOwnership;
1699
+ // task 1053 (F): exported so the verify-attestation guard is unit-tested.
1700
+ module.exports.isVerifyTask = isVerifyTask;
1701
+ // task 1139 / 1169 (ADR 0058): exported so the pure rank-attestation decision
1702
+ // (Metic+ → gate-author-trust) is unit-tested without a server/DB/network.
1703
+ module.exports.rankAttestsGateTrust = rankAttestsGateTrust;