@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,1790 @@
1
+ // GDS-V3 task #410 (MAS Phase 2.0) — Quality grader v4 (worker panel).
2
+ //
3
+ // Phase 2.0 of the Multi-Agent System (ADR 0024). v3 was a 3-shot generalist
4
+ // (one prompt, 3 parallel runs, unanimous PASS). v4 trades sampling-variance
5
+ // for lens-specialization: 3 specialist workers replace the 3 shots. Each
6
+ // worker is a distinct prompt focused on one axis; gate is unanimous PASS
7
+ // across the GATING workers (any gating fail blocks; any gating concerns falls
8
+ // through). The Narc (functional_fidelity) is ADVISORY since #481 — its
9
+ // concerns never block and its fail downgrades to manual-confirm; see
10
+ // aggregatePanel.
11
+ //
12
+ // Phase 2.0 ships 2 of the 3 planned workers (narc + quality); 2.1 adds
13
+ // hacker (security), 2.2 adds efficiency_monger. The framework is the
14
+ // load-bearing change here — adding workers later is a one-line plug-in.
15
+ //
16
+ // ## Phase 1 (v3) carry-overs that remain true in v4
17
+ //
18
+ // 1. CROSS-FAMILY. Default grader model is Sonnet 4.6; the assumed builder
19
+ // is Opus. selectGraderModel() flips the model based on the builder's
20
+ // model when known. Each worker inherits the same selection.
21
+ // 2. DEFAULT-FAIL FRAMING. Each worker's prompt is independently default-
22
+ // fail, and each must enumerate concrete issues before any pass verdict.
23
+ //
24
+ // ## What's new in v4
25
+ //
26
+ // 3. WORKER PANEL. gradeSubagent runs DEFAULT_PANEL (narc + quality) in
27
+ // parallel; aggregatePanel applies the gate. Each worker has its own
28
+ // prompt, parser, scoring, and per-worker pass/concerns/fail verdict.
29
+ // 4. STRUCTURED VERDICTS. pass | concerns | fail (not just pass/fail).
30
+ // 'concerns' falls through to the manual-confirm path the same way
31
+ // low-confidence did in v3.
32
+ // 5. SERVER-COMPAT FORWARDING. The grade POST body still uses the v3
33
+ // shape (ff, cq, economy, confidence, issues) so the server's
34
+ // validateAndScore re-validation runs cleanly. The panel's verdict
35
+ // drives the forwarded confidence so the server's gate aligns:
36
+ // pass → forward median confidence (server passes)
37
+ // concerns → forward 0.3 (server fails on low-conf → fallthrough)
38
+ // fail → forward 0 + the failing worker's worst score
39
+ //
40
+ // ## Surface (v4)
41
+ //
42
+ // gradeSubagent({task, valueSummary, diff, changedFiles, diffStats,
43
+ // workers?, builderModel, runOpts})
44
+ // → end-to-end panel grade-result. workers? defaults to DEFAULT_PANEL
45
+ // (narc + quality). Wraps subprocess failures so resolveClaim/applyGrade
46
+ // can always write task_grades.
47
+ //
48
+ // aggregatePanel(workerResults, meta)
49
+ // → final grade-result from per-worker WorkerResults. Applies the
50
+ // any-fail / any-concerns / all-pass gate. Outputs are server-
51
+ // compatible with the v3 grade POST shape.
52
+ //
53
+ // selectGraderModel({builderModel})
54
+ // → unchanged from v3.
55
+ //
56
+ // ## v3 surface (kept exported for diagnostics + back-compat)
57
+ //
58
+ // buildPrompt({...}) — the v3 generalist default-fail prompt. NOT used by
59
+ // the v4 panel; kept for explicit single-prompt diagnostic callers.
60
+ //
61
+ // validateAndScore(rawSubagent, meta) — v3 per-shot validator. Workers
62
+ // have their own (more specific) validators; this remains exported for
63
+ // the legacy single-shot diagnostics path and for tests.
64
+ //
65
+ // aggregateShots(shotResults, meta) — v3 multi-shot aggregator. Same
66
+ // reasoning: kept for diagnostic callers.
67
+ //
68
+ // runSubagent({prompt, opts}) / runOneShot(...) — subprocess plumbing,
69
+ // unchanged from v3. Workers spawn via runSubagent through the workers
70
+ // module (which receives it via dependency injection to avoid a circular
71
+ // require).
72
+ //
73
+ // ## Legacy v1 (still exported)
74
+ //
75
+ // gradeDeterministic(...) — v1 deterministic path, kept for
76
+ // scripts/gds/audit-ships.js (retroactive audit). NOT invoked at ship time.
77
+
78
+ const path = require('node:path');
79
+ const fs = require('node:fs');
80
+ const { spawn } = require('node:child_process');
81
+
82
+ const RUBRIC = JSON.parse(
83
+ fs.readFileSync(path.join(__dirname, 'grader-rubric.json'), 'utf8')
84
+ );
85
+
86
+ // V4 = the active panel-based gate. V3 / V1 are kept under their fields for
87
+ // the two paths that still read them: V3 = the trivial-diff multi-shot grader
88
+ // (ship.js #513), V1 = the audit-ships.js retroactive deterministic path. The
89
+ // v2 binding + its rubric block were dead (no reader) and removed in task 466.
90
+ const V4 = RUBRIC.v4;
91
+ const V3 = RUBRIC.v3;
92
+ const V1 = RUBRIC.v1;
93
+ const THRESHOLD = V4.threshold;
94
+ const VERSION = V4.version;
95
+ const LOW_CONFIDENCE = V4.low_confidence_threshold;
96
+ // DEFAULT_SHOTS is kept for the v3 multi-shot path (still exported for
97
+ // diagnostics); v4 doesn't use shots — it uses workers.
98
+ const DEFAULT_SHOTS = V3.shots || 3;
99
+ const ASSUMED_BUILDER_MODEL = V4.assumed_builder_model || 'claude-opus-4-7';
100
+ const DEFAULT_GRADER_MODEL = V4.default_model || 'claude-sonnet-4-6';
101
+ const FALLTHROUGH_CONFIDENCE = V4.fallthrough_confidence ?? 0.3;
102
+ const workers = require('./grader-workers');
103
+ // Carved into modules/grading/ (BV1.R73 / ADR 0091 §1+§4): the two deterministic
104
+ // trust classifiers are KERNEL (KERNEL_FILES, R70) — reached through the doorway,
105
+ // never a deep core require. The worker panel + extract-json are intra-module.
106
+ const api = require('../../src/module-api');
107
+ const routeRankCheck = api.routeRankCheck;
108
+ const permissionPathCheck = api.permissionPathCheck;
109
+
110
+ // Deterministic pre-pass: the sub-Metic permission-path gate (SEC #863). Runs
111
+ // ONLY when the change set touches a permission-sensitive path AND the builder
112
+ // rank resolves below Metic — otherwise null (zero cost / no finding). Like the
113
+ // route-rank pre-pass, the result is a pseudo-worker appended to the panel so
114
+ // aggregatePanel's any-fail gate hard-fails a sub-Metic ship of protected code
115
+ // with no gate-logic change. builderRank comes from the caller's live /me read
116
+ // (ship.js) or req.builder.rank (server); unknown rank fails OPEN here (the
117
+ // post-push main audit, which reads the real author rank from the DB, owns the
118
+ // unknown/dishonest case).
119
+ function permissionPathPrePass(changedFiles, builderRank) {
120
+ const result = permissionPathCheck.checkPermissionPaths({ changedFiles, builderRank });
121
+ if (!result.hardFail) return null;
122
+ return { findings: result.findings, workerResult: permissionPathCheck.toWorkerResult(result) };
123
+ }
124
+
125
+ // Deterministic pre-pass: the route-rank invariant (#475 / C7). Runs ONLY when
126
+ // a src/bongos/routes/ file is in the change set (zero cost otherwise). Returns
127
+ // the findings (fed to the Hacker worker as input) plus a pseudo-worker result
128
+ // appended to the panel so aggregatePanel's any-fail gate handles it directly.
129
+ // Shared verbatim by gradeSubagent (ship-time) and conductor-dispatch.js
130
+ // (mid-flight) so the route-parsing logic lives in exactly one place.
131
+ function routeRankPrePass(changedFiles) {
132
+ const touched = (changedFiles || []).some((f) => routeRankCheck.ROUTE_FILE_RE.test(f));
133
+ if (!touched) return null;
134
+ const result = routeRankCheck.checkRouteRanks();
135
+ return { findings: result.findings, workerResult: routeRankCheck.toWorkerResult(result) };
136
+ }
137
+
138
+ function r1(n) {
139
+ return Math.round(Number(n) * 10) / 10;
140
+ }
141
+
142
+ function clamp(n, lo, hi) {
143
+ return Math.max(lo, Math.min(hi, n));
144
+ }
145
+
146
+ function median(arr) {
147
+ const v = arr.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b);
148
+ if (v.length === 0) return null;
149
+ const mid = Math.floor(v.length / 2);
150
+ return v.length % 2 ? r1(v[mid]) : r1((v[mid - 1] + v[mid]) / 2);
151
+ }
152
+
153
+ // ---------- v4.1: trivial-diff fast-path (#513) ----------
154
+ //
155
+ // The #506 post-mortem: a +1/-1 welcome-copy tweak ran the full 4-worker
156
+ // adversarial Opus/Sonnet panel — disproportionate cost for a one-word change.
157
+ // isTrivialDiff classifies a change as "copy/docs only, small, and touching no
158
+ // logic path". ship.js routes those to a SINGLE generalist grader shot instead
159
+ // of the panel ("lighter, not skipped" — the 1-shot still scores ff+cq+economy
160
+ // +confidence so the server gate applies, and the deterministic scanner + smoke
161
+ // + route-rank checks run regardless). Everything else keeps the full panel.
162
+ //
163
+ // Deliberately conservative — defaults to "not trivial" (full panel) whenever
164
+ // in doubt (missing diffStats, zero files, any logic-path file, oversized diff).
165
+ const TRIVIAL_MAX_FILES = 3;
166
+ const TRIVIAL_MAX_LINES = 30;
167
+ // Copy/docs/front-end surfaces eligible for the lighter single-shot grade.
168
+ // (Both web surfaces moved under modules/<key>/public/: the status dashboard to
169
+ // modules/status-ui/public/ in BV1.R84 and the hall to modules/hall-ui/public/ in
170
+ // BV1.R85.)
171
+ const TRIVIAL_PATH_RE = /(?:^|\/)[^/]+\.md$|^docs\/|^public\/|^modules\/hall-ui\/public\/|^modules\/status-ui\/public\//;
172
+ // Logic / infra / test surfaces that force the full panel even on a tiny diff —
173
+ // these are where a subtle one-liner can do real damage, so they never qualify.
174
+ const LOGIC_PATH_RE = /^src\/|^scripts\/|^migrations\/|^server\.js$|^tests\/|^\.claude\//;
175
+ // Executable code AND config are NEVER trivial, even under public*/ (#513
176
+ // Hacker). A copy tweak and an XSS / client-authz regression are
177
+ // indistinguishable to a path+size heuristic, and the builders-hall + game
178
+ // front-ends carry auth flows and game logic — so any code extension forces the
179
+ // full adversarial panel. Config/data files (.json/.yaml/.env/…) are excluded
180
+ // too: a client-side config change is security-relevant and shouldn't get the
181
+ // lighter shot. Only genuinely static content (markdown, CSS/HTML, images,
182
+ // plain text) takes the single shot.
183
+ // HTML and SVG are executable in a browser (inline <script>, on* handlers,
184
+ // <svg onload>), so they are NOT "static" for grading purposes — a small
185
+ // edit could introduce client-side XSS that only the full panel's Hacker
186
+ // would catch. Excluded alongside code + config (#513 Hacker).
187
+ const NON_TRIVIAL_EXT_RE = /\.(?:js|mjs|cjs|jsx|ts|tsx|py|sh|bash|rb|go|sql|json|ya?ml|toml|ini|env|html?|svg)$|(?:^|\/)\.env(?:\.|$)/;
188
+
189
+ function isTrivialDiff({ changedFiles, diffStats } = {}) {
190
+ if (!diffStats || typeof diffStats !== 'object') return false;
191
+ const files = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : [];
192
+ if (files.length === 0) return false;
193
+ const fc = Number(diffStats.files_changed);
194
+ const lines = (Number(diffStats.lines_added) || 0) + (Number(diffStats.lines_removed) || 0);
195
+ // Cap on BOTH the shortstat count and the actual file-list length — a rename
196
+ // can disagree between the two, so take the stricter of the two (#513 nit).
197
+ if (!Number.isFinite(fc) || fc <= 0 || fc > TRIVIAL_MAX_FILES) return false;
198
+ if (files.length > TRIVIAL_MAX_FILES) return false;
199
+ if (lines <= 0 || lines > TRIVIAL_MAX_LINES) return false;
200
+ // Every changed file must be a copy/docs path, none in a logic path, and none
201
+ // an executable-code or config file (those always get the full panel).
202
+ return files.every((f) =>
203
+ TRIVIAL_PATH_RE.test(f) && !LOGIC_PATH_RE.test(f) && !NON_TRIVIAL_EXT_RE.test(f));
204
+ }
205
+
206
+ // ---------- v3: cross-family model selection (ADR 0024) ----------
207
+
208
+ // Returns the grader model given the builder's model — a DIFFERENT family from
209
+ // the builder is the cross-family adversarial guarantee. With no builderModel
210
+ // the caller is assuming the default Opus builder (CLAUDE.md §2), so it resolves
211
+ // through the opus branch to Sonnet — that is the legitimate default path the
212
+ // live callers (selectGraderModel({})) take.
213
+ //
214
+ // #117: an EXPLICITLY-supplied model we don't recognize is a misconfiguration,
215
+ // not a default. We used to silently fall back to Sonnet, which would mask a
216
+ // future model family (one whose id lacks 'opus'/'sonnet'/'haiku') and quietly
217
+ // grade it on the wrong cross-family assumption. Throw loudly instead so the
218
+ // caller fixes the input rather than getting a silently-wrong grader model.
219
+ function selectGraderModel({ builderModel } = {}) {
220
+ const bm = (builderModel || ASSUMED_BUILDER_MODEL).toLowerCase();
221
+ if (bm.includes('opus')) return 'claude-sonnet-4-6';
222
+ if (bm.includes('sonnet')) return 'claude-opus-4-7';
223
+ if (bm.includes('haiku')) return 'claude-opus-4-7';
224
+ throw new Error(
225
+ `selectGraderModel: unrecognized builder model ${JSON.stringify(builderModel)} — ` +
226
+ `expected an id containing 'opus', 'sonnet', or 'haiku'. Refusing to silently ` +
227
+ `fall back to a default grader model (#117).`
228
+ );
229
+ }
230
+
231
+ // ---------- v3: prompt builder (default-fail framing) ----------
232
+
233
+ // The instance's project name, injected into the grader's identity line so a
234
+ // non-OTB Cloud Bongos instance is graded against ITS OWN project, never a
235
+ // hardcoded "Off the Boats" (ADR 0062 §3/§5; the branding contract's
236
+ // llmProjectName field). Reached through the doorway like every other host-
237
+ // context read in this module — never a deep core require. Resolved once at
238
+ // module load (the brand is a per-process singleton); falls back to a neutral
239
+ // phrase if branding fails to resolve so the gate never crashes on a config error.
240
+ const LLM_PROJECT = (() => {
241
+ try { return api.branding().llmProjectName || 'this project'; }
242
+ catch { return 'this project'; }
243
+ })();
244
+
245
+ const PROMPT_HEADER = `You are an adversarial code-review subagent for the ${LLM_PROJECT} project. You are reviewing one shipped change to decide whether it deserves to ship.
246
+
247
+ Your job is to FIND WHAT IS WRONG with this change. **The default verdict is FAIL.** To return a PASS, you must affirmatively conclude — after a careful read of the diff and (using your Read tool) the relevant post-merge files — that no material problems remain.
248
+
249
+ Begin by hunting for these specific failure modes. When you find one, CITE the diff line or name the failing acceptance criterion:
250
+
251
+ - TODOs, FIXMEs, "for now", "temporary" or "stub" comments left in shipped code.
252
+ - Hardcoded shortcuts: assuming one builder, one user, one project root, one tenant, one environment, or one identity. Look for literal "lars", "lars589", absolute /Users paths, hardcoded ports, hardcoded IDs.
253
+ - Scope gaps: acceptance criteria in the description that the diff DOES NOT satisfy. Read the description's "Acceptance" section line by line.
254
+ - Demo / dummy / mock code that lies: returns a fixed value, fakes a successful result, mocks at the wrong layer, or pretends to call something it doesn't.
255
+ - Missing tests for new logic: any non-trivial new function, route handler, branch, or aggregation that ships without a covering test.
256
+ - Broken invariants: methodology rules in CLAUDE.md, cross-verified Python/JS autotile lookups, ground-vs-object cell split per ADR 0012, the "every change is backed by a claimed GDS task" rule, the trust-boundary rule per ADR 0016 ("server-enforced permissions"), the no-hardcoded-Archon-identity rule per V3.R12 #236.
257
+ - Dead code: helpers, exports, branches, or files added but never called from anywhere in the post-merge tree. Use Read on the receiving site if unsure.
258
+ - Reads/writes outside the declared touches[]: if the diff edits files the task didn't claim, that's a scope creep — call it out.
259
+
260
+ You return three scores (0-10), notes, an issues array, and a confidence value:
261
+
262
+ 1. functional_fidelity (0-10): did the diff implement what the description scoped? 10 = every acceptance criterion satisfied by code or explicitly out-of-scope per the description. 6 = mostly there with one NAMED gap. Below 6 = scope gaps, broken acceptance criteria, or demo code that lies.
263
+ 2. code_quality (0-10): is the code well-written for THIS codebase? 10 = production-quality, would-merge-without-comment. 6 = ships but with a clear improvement a reviewer would suggest. Below 6 = broken style, dead code, broken invariants, missing tests where warranted.
264
+ 3. economy (0-10, ADVISORY): is the change as small and simple as the task warrants? 10 = minimal and focused. 6 = a bit more than necessary. Below 6 = bloated, speculative abstraction, duplicated logic.
265
+
266
+ **The hard gate** (applied downstream, you do not enforce it yourself; just score honestly): functional_fidelity >= 6 AND code_quality >= 6 AND confidence >= 0.4. Below 6 on either axis is a FAIL. Economy is advisory only.
267
+
268
+ **For every issue you list:** include severity (blocker | major | minor | nit), the file path, and a one-line summary that quotes the offending diff line or names the failing acceptance criterion. Vague summaries like "code quality could be better" are not acceptable — be specific or omit it.
269
+
270
+ **Sanity checks:**
271
+ - A PASS with no issues[] entries is suspicious. If you found nothing material wrong, briefly say WHY in notes_md: which acceptance criteria you verified, which files you Read, which could-have-gone-wrong didn't.
272
+ - A FAIL with no issues[] entries is invalid. At least one concrete issue must explain the verdict.
273
+ - A score below 6 with no issue at severity major or blocker is inconsistent — name the major problem.
274
+
275
+ **Confidence:** 0-1, your self-reported confidence in your two scores. If you cannot judge (description too vague to grade against, diff too large to fit comfortably in context, key context missing), set confidence LOW (< 0.4). Low confidence falls through to manual confirm — be honest about uncertainty rather than guessing.
276
+
277
+ Return ONLY JSON matching this schema — no prose before or after, no markdown fences:
278
+ {
279
+ "functional_fidelity": <number 0-10>,
280
+ "code_quality": <number 0-10>,
281
+ "economy": <number 0-10>,
282
+ "notes_md": <string>,
283
+ "confidence": <number 0-1>,
284
+ "issues": [{"severity": "blocker"|"major"|"minor"|"nit", "file": <path>, "summary": <string>}, ...]
285
+ }
286
+
287
+ You have a Read tool. Spend reads checking that removed code is unreferenced elsewhere, that new helpers match surrounding style, that the diff's value_summary matches what the diff actually does, and that acceptance criteria you can't verify from the diff alone are actually satisfied in the post-merge file. Do not run tests, do not edit files, do not call the network.`;
288
+
289
+ function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats }) {
290
+ const t = task || {};
291
+ const desc = (t.description || '(no description)').trim();
292
+ const title = (t.title || '(no title)').trim();
293
+ const vs = (valueSummary || '(no value summary)').trim();
294
+ const files = Array.isArray(changedFiles) ? changedFiles : [];
295
+ const diffText = (diff || '(empty diff)').trim();
296
+
297
+ const lines = [];
298
+ lines.push(PROMPT_HEADER);
299
+ lines.push('');
300
+ lines.push('---');
301
+ lines.push('## Task');
302
+ lines.push('');
303
+ lines.push(`**ID:** ${t.id ?? '(unknown)'}`);
304
+ lines.push(`**Title:** ${title}`);
305
+ lines.push(`**Kind:** ${t.kind ?? 'unclassified'} · **Discipline:** ${t.discipline ?? 'unclassified'}`);
306
+ lines.push(`**Estimated effort:** ${t.est_minutes != null ? `${t.est_minutes} min` : '(not estimated)'} — use this to judge whether the diff size is proportionate to the work scoped.`);
307
+ if (t.version_id) lines.push(`**Version:** ${t.version_id}`);
308
+ lines.push('');
309
+ lines.push('**Description:**');
310
+ lines.push('');
311
+ lines.push(desc);
312
+ lines.push('');
313
+ lines.push('---');
314
+ lines.push('## Builder\'s value summary');
315
+ lines.push('');
316
+ lines.push(vs);
317
+ lines.push('');
318
+ lines.push('---');
319
+ lines.push(`## Changed files (${files.length})`);
320
+ lines.push('');
321
+ for (const f of files) lines.push(`- ${f}`);
322
+ if (files.length === 0) lines.push('(no files changed — this is suspicious)');
323
+ lines.push('');
324
+ lines.push('You can Read any of these files to see their post-merge state.');
325
+ lines.push('');
326
+ lines.push('---');
327
+ lines.push('## Diff size (for the economy axis)');
328
+ lines.push('');
329
+ if (diffStats && typeof diffStats === 'object') {
330
+ const fc = diffStats.files_changed ?? '?';
331
+ const la = diffStats.lines_added ?? '?';
332
+ const lr = diffStats.lines_removed ?? '?';
333
+ lines.push(`- files changed: ${fc}`);
334
+ lines.push(`- lines added: ${la}`);
335
+ lines.push(`- lines removed: ${lr}`);
336
+ lines.push('');
337
+ lines.push('Weigh this against the task description, kind, and estimated effort above when scoring economy. A diff much larger than the scoped work warrants — or one padded with speculative abstraction, duplication, or dead code — should score low on economy.');
338
+ } else {
339
+ lines.push('(diff stats unavailable — score economy from the diff itself.)');
340
+ }
341
+ lines.push('');
342
+ lines.push('---');
343
+ lines.push('## Unified diff');
344
+ lines.push('');
345
+ lines.push('```diff');
346
+ lines.push(diffText);
347
+ lines.push('```');
348
+ lines.push('');
349
+ lines.push('Now grade. Default-fail unless your read of the diff and post-merge files turns up no material problems. Output ONLY the JSON object — nothing before or after it.');
350
+ return lines.join('\n');
351
+ }
352
+
353
+ // ---------- v3: per-shot validator (same as v2 logic) ----------
354
+
355
+ // Tries to extract a JSON object from a subagent response. The subagent is
356
+ // instructed to return ONLY JSON, but in practice models sometimes wrap it
357
+ // in markdown fences or add a line of prose. Be tolerant.
358
+ // #79 (#380, completed #493): robust JSON extraction now lives in the shared
359
+ // leaf module so grader.js AND every worker module use ONE implementation.
360
+ // Re-exported here for back-compat (validateAndScore + tests use
361
+ // grader.extractJson).
362
+ const { extractJson } = require('./grader-workers/extract-json');
363
+
364
+ function isNum(n) {
365
+ return Number.isFinite(n);
366
+ }
367
+
368
+ // Validates ONE shot's subagent JSON and produces a per-shot grade-result.
369
+ // Never throws — malformed inputs produce a passed=false result with notes_md.
370
+ // The downstream aggregateShots collapses N of these into a final verdict.
371
+ function validateAndScore(rawSubagent, meta = {}) {
372
+ const parsed =
373
+ typeof rawSubagent === 'string' ? extractJson(rawSubagent) : rawSubagent;
374
+
375
+ const signals = {
376
+ grader_kind: 'subagent',
377
+ grader_version: VERSION,
378
+ model: meta.model || DEFAULT_GRADER_MODEL,
379
+ threshold: THRESHOLD,
380
+ ...meta,
381
+ };
382
+
383
+ if (!parsed || typeof parsed !== 'object') {
384
+ return {
385
+ score: 0,
386
+ passed: false,
387
+ threshold: THRESHOLD,
388
+ grader_kind: 'subagent',
389
+ grader_version: VERSION,
390
+ rubric_scores: {},
391
+ signals: { ...signals, parse_error: true },
392
+ notes_md: `Subagent grader ${VERSION}: could not parse subagent output as JSON.`,
393
+ issues: [],
394
+ };
395
+ }
396
+
397
+ const ff = Number(parsed.functional_fidelity);
398
+ const cq = Number(parsed.code_quality);
399
+ const economyRaw = Number(parsed.economy);
400
+ const economy =
401
+ isNum(economyRaw) && economyRaw >= 0 && economyRaw <= 10 ? economyRaw : null;
402
+ // MAS Phase 2.5 (#423): security_score (from Hacker, Phase 2.1) and
403
+ // efficiency_score (from Efficiency Monger, Phase 2.2). Both ADVISORY —
404
+ // recorded in rubric_scores but do not independently gate the ship. Same
405
+ // back-compat shape as economy: out-of-range / non-numeric / missing →
406
+ // recorded as null, no penalty. Older callers that don't forward these
407
+ // fields grade exactly as before.
408
+ const securityRaw = Number(parsed.security_score);
409
+ const securityScore =
410
+ isNum(securityRaw) && securityRaw >= 0 && securityRaw <= 10 ? securityRaw : null;
411
+ const efficiencyRaw = Number(parsed.efficiency_score);
412
+ const efficiencyScore =
413
+ isNum(efficiencyRaw) && efficiencyRaw >= 0 && efficiencyRaw <= 10 ? efficiencyRaw : null;
414
+ // Treat null/undefined confidence as ABSENT, not as 0. Number(null) === 0,
415
+ // and isNum(0) === true — a naive `isNum(Number(parsed.confidence))` check
416
+ // would accept null as a valid low-confidence signal and FAIL the ship for
417
+ // a missing field. The MAS Phase 1 self-ship (#395) hit this exactly when
418
+ // aggregateShots forwarded a null top-level confidence to the server's
419
+ // validateAndScore. Explicit null-check first; coerce only when present.
420
+ const confidence = parsed.confidence == null
421
+ ? null
422
+ : isNum(Number(parsed.confidence)) ? Number(parsed.confidence) : null;
423
+ const issues = Array.isArray(parsed.issues)
424
+ ? parsed.issues.filter((it) => it && typeof it === 'object').slice(0, 50)
425
+ : [];
426
+ const notesFromSubagent = typeof parsed.notes_md === 'string' ? parsed.notes_md : '';
427
+
428
+ signals.confidence = confidence;
429
+ signals.issue_count = issues.length;
430
+
431
+ if (!isNum(ff) || !isNum(cq) || ff < 0 || ff > 10 || cq < 0 || cq > 10) {
432
+ return {
433
+ score: 0,
434
+ passed: false,
435
+ threshold: THRESHOLD,
436
+ grader_kind: 'subagent',
437
+ grader_version: VERSION,
438
+ rubric_scores: {},
439
+ signals: { ...signals, schema_error: true },
440
+ notes_md: `Subagent grader ${VERSION}: subagent returned scores outside [0,10] or non-numeric. Got functional_fidelity=${parsed.functional_fidelity}, code_quality=${parsed.code_quality}.`,
441
+ issues,
442
+ };
443
+ }
444
+
445
+ const ffR = r1(clamp(ff, 0, 10));
446
+ const cqR = r1(clamp(cq, 0, 10));
447
+ const economyR = economy != null ? r1(clamp(economy, 0, 10)) : null;
448
+ const securityR = securityScore != null ? r1(clamp(securityScore, 0, 10)) : null;
449
+ const efficiencyR = efficiencyScore != null ? r1(clamp(efficiencyScore, 0, 10)) : null;
450
+ const economyPenalty =
451
+ economyR != null ? Math.max(0, (THRESHOLD - economyR) / 2) : 0;
452
+ const cqEffective = economyR != null ? r1(clamp(cqR - economyPenalty, 0, 10)) : cqR;
453
+ const blended = r1((ffR + cqR) / 2);
454
+
455
+ // confidence must be present (not null) for a PASS. A subagent that omits
456
+ // confidence entirely silently bypassed the low-confidence gate under the
457
+ // old `confidence != null && confidence < LOW_CONFIDENCE` check — null made
458
+ // lowConfidence=false and let high ff/cq scores through unchecked (#489 / #56).
459
+ // Distinguishing "absent" from "low" gives a clear verdict message and leaves
460
+ // the aggregateShots #395 fix intact: shots that omit confidence now FAIL here,
461
+ // so aggregateShots can never reach unanimousPASS with a null confidenceMedian.
462
+ const confidenceAbsent = confidence === null;
463
+ const lowConfidence = !confidenceAbsent && confidence < LOW_CONFIDENCE;
464
+ const bothPass = ffR >= THRESHOLD && cqR >= THRESHOLD;
465
+ const passed = bothPass && !lowConfidence && !confidenceAbsent;
466
+
467
+ const verdict = passed
468
+ ? 'PASS'
469
+ : confidenceAbsent
470
+ ? 'FAIL — confidence absent'
471
+ : lowConfidence
472
+ ? 'FAIL — low confidence'
473
+ : bothPass
474
+ ? 'FAIL'
475
+ : ffR < THRESHOLD && cqR < THRESHOLD
476
+ ? `FAIL — both axes below ${THRESHOLD}`
477
+ : ffR < THRESHOLD
478
+ ? `FAIL — functional_fidelity below ${THRESHOLD}`
479
+ : `FAIL — code_quality below ${THRESHOLD}`;
480
+
481
+ const notesLines = [];
482
+ notesLines.push(`Shot ${signals.shot_index != null ? signals.shot_index : '?'} (${signals.model}) — ${verdict}`);
483
+ notesLines.push(` ff=${ffR}/10 cq=${cqR}/10${economyR != null ? ` econ=${economyR}/10 (advisory)` : ''} conf=${confidence ?? '?'}`);
484
+ if (notesFromSubagent) {
485
+ notesLines.push('');
486
+ notesLines.push(notesFromSubagent.trim());
487
+ }
488
+
489
+ return {
490
+ score: blended,
491
+ passed,
492
+ threshold: THRESHOLD,
493
+ grader_kind: 'subagent',
494
+ grader_version: VERSION,
495
+ rubric_scores: {
496
+ functional_fidelity: ffR,
497
+ code_quality: cqR,
498
+ economy: economyR,
499
+ code_quality_effective: cqEffective,
500
+ security_score: securityR,
501
+ efficiency_score: efficiencyR,
502
+ },
503
+ signals,
504
+ notes_md: notesLines.join('\n'),
505
+ issues,
506
+ };
507
+ }
508
+
509
+ // ---------- server-authoritative grade guards (SR-9 / SR-18 / F1 / G5) ----------
510
+ //
511
+ // THE FINDING (SR-9 / SR-18): POST /api/gds/tasks/:id/grade trusts the client.
512
+ // validateAndScore only shape-validates the numbers — `passed` is computed from
513
+ // the CLIENT's submitted scores. The real adversarial panel + the deterministic
514
+ // route-rank / permission-path pre-passes run CLIENT-SIDE in scripts/gds/ship.js;
515
+ // the server never re-runs them. So a builder can POST
516
+ // {functional_fidelity:10, code_quality:10, confidence:1, issues:[]} to self-pass.
517
+ // AND the credit/achievement side-effects (net-negative "Subtractor" bonus,
518
+ // shipper badges) are driven by client-supplied signals.diff_stats +
519
+ // committed_files the server never re-derives (SR-18).
520
+ //
521
+ // WHAT THE SERVER CAN AND CAN'T DO (the boundary — see report):
522
+ // * The web tier is checked out at `main`. The builder's feature-branch
523
+ // commits are NOT on the server at grade time (they only arrive when the
524
+ // task merges to main). So the server CANNOT independently re-derive the
525
+ // committed diff at the claimed head_sha. The diff, diff_stats, and
526
+ // committed_files in the request body remain CLIENT-SUPPLIED.
527
+ // * BUT the server DOES know the caller's LIVE, authoritative rank
528
+ // (req.builder.rank, read fresh from the DB per request — ADR 0016). So the
529
+ // deterministic permission-path pre-pass can run with a TRUSTED rank against
530
+ // the (client-supplied) committed_files — strictly raising the bar.
531
+ // * The route-rank pre-pass reads the route files in the server's OWN
532
+ // checkout (fs) — fully server-authoritative, independent of any client
533
+ // input. (NB: it audits `main`, not the feature branch — the post-push main
534
+ // audit owns feature-branch route changes. Re-running here still hard-fails
535
+ // a forged pass while `main` already carries an ungated write route.)
536
+ //
537
+ // THE GUARD (additive, fail-toward-reject-only): serverSideGuards re-runs the
538
+ // two deterministic pre-passes server-side and applies plausibility/bounding
539
+ // checks. It can ONLY downgrade a client PASS to a non-pass (hard FAIL or
540
+ // manual-confirm); it NEVER upgrades a fail, never relaxes the existing gate,
541
+ // and is a no-op on a client that already reported not-passed. A legitimate
542
+ // honest grade is unaffected.
543
+ //
544
+ // The Archon / GRADER_BYPASS paths do not flow through /grade's passed-gate the
545
+ // way an ordinary ship does (they confirm via /tasks/:id/confirm, which is a
546
+ // separate route), so this guard does not touch them — see the route comment.
547
+
548
+ // Plausibility threshold: a client PASS that reports NO issues on a NON-TRIVIAL
549
+ // diff is not a credible adversarial-panel verdict — the panel is default-fail
550
+ // and every worker must enumerate concrete issues before a pass; a clean pass on
551
+ // a large diff with an empty issues[] is the exact shape of a forged
552
+ // {ff:10,cq:10,issues:[]} self-pass (SR-9). We don't reject it outright (a small
553
+ // honest change can legitimately have no issues) — we DOWNGRADE it to
554
+ // manual-confirm so a human lands it via /confirm. Thresholds mirror the
555
+ // trivial-diff fast-path's spirit (a change past these sizes is "real work"):
556
+ // - more than this many changed files, OR
557
+ // - more than this many added+removed lines
558
+ // makes an empty-issues PASS implausible-as-automatic and forces manual-confirm.
559
+ const IMPLAUSIBLE_PASS_MAX_FILES = TRIVIAL_MAX_FILES; // 3 — same "small change" bar
560
+ const IMPLAUSIBLE_PASS_MAX_LINES = TRIVIAL_MAX_LINES; // 30
561
+
562
+ // Re-derive the size of the change from the submitted committed_files +
563
+ // diff_stats. Both are client-supplied (see boundary note) — we take the
564
+ // STRICTER (larger) of the file-list length and the shortstat files_changed so a
565
+ // forger can't shrink one to dodge the plausibility bar while the other betrays
566
+ // a large diff. Returns { files, lines } with NaN coerced to 0.
567
+ function changeMagnitude({ committedFiles, diffStats } = {}) {
568
+ const listLen = Array.isArray(committedFiles)
569
+ ? committedFiles.filter((f) => typeof f === 'string' && f.trim()).length
570
+ : 0;
571
+ const statFiles = diffStats && Number.isFinite(Number(diffStats.files_changed))
572
+ ? Number(diffStats.files_changed)
573
+ : 0;
574
+ const files = Math.max(listLen, statFiles);
575
+ const lines =
576
+ (Number(diffStats?.lines_added) || 0) + (Number(diffStats?.lines_removed) || 0);
577
+ return { files, lines };
578
+ }
579
+
580
+ // serverSideGuards — the server-authoritative hardening layer (SR-9 / SR-18).
581
+ //
582
+ // Inputs:
583
+ // gradeResult — the output of validateAndScore on the SUBMITTED body. Its
584
+ // `passed` is computed from the client's numbers. We may
585
+ // override it (downgrade only).
586
+ // committedFiles — client-supplied list of committed paths (or null).
587
+ // builderRank — the caller's LIVE rank (req.builder.rank — SERVER-KNOWN,
588
+ // authoritative). Drives the permission-path pre-pass.
589
+ // diffStats — client-supplied { files_changed, lines_added, lines_removed }
590
+ // (gradeResult.signals.diff_stats if not passed explicitly).
591
+ // routesDir — optional override for tests; defaults to the live routes dir.
592
+ //
593
+ // Returns a NEW grade-result (never mutates the input). When a guard trips, the
594
+ // returned result has passed=false and a `server_guard` block in signals naming
595
+ // every triggered guard + the residual notes. When no guard trips, the input is
596
+ // returned with a passed-through `server_guard: { applied: [...], ok: true }`
597
+ // audit marker so the trail shows the guards RAN (and found nothing) rather than
598
+ // being silently absent.
599
+ //
600
+ // CONTRACT: downgrade-only. If the client already reported not-passed, we leave
601
+ // it not-passed (no resurrection). We only ever flip true→false.
602
+ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats, routesDir } = {}) {
603
+ const base = gradeResult || {};
604
+ const ds = diffStats || base.signals?.diff_stats || null;
605
+ const guardsTripped = []; // [{ guard, severity, summary }]
606
+ const extraIssues = []; // surfaced into the result's issues[]
607
+ let downgradeTo = null; // 'fail' | 'concerns' | null
608
+
609
+ // The guards only have teeth on a CLIENT PASS — a non-pass is already
610
+ // blocked/fallen-through by the existing gate, and downgrade-only means we
611
+ // never touch it. We still RUN the deterministic checks (their findings are a
612
+ // useful audit signal even on a fail) but only let them flip the verdict when
613
+ // base.passed === true.
614
+ const clientPassed = base.passed === true;
615
+
616
+ // --- Guard 1: deterministic pre-passes, server-side (SR-9 / F1) ---
617
+ // route-rank: reads the server's own route files (fs) — server-authoritative.
618
+ // permission-path: runs with the TRUSTED live rank against the (client) files.
619
+ // Either hard-fail flips a client PASS to a hard FAIL regardless of submitted
620
+ // scores — this is the core "re-run the deterministic checks server-side"
621
+ // hardening. A non-hardFail (e.g. an unclassified READ route → 'concerns') is
622
+ // recorded but does NOT block here: the client-side panel's Hacker already
623
+ // adjudicates read-route intent, and a server-side downgrade-to-concerns on a
624
+ // read route would be a behavior change with no SR-9 security value.
625
+ let routeRank = null;
626
+ try {
627
+ routeRank = routeRankCheck.checkRouteRanks(routesDir ? { routesDir } : {});
628
+ } catch (_) { routeRank = null; /* fs error → no server-side route finding; client panel + main audit still gate */ }
629
+ const permPath = permissionPathCheck.checkPermissionPaths({ changedFiles: committedFiles || [], builderRank });
630
+
631
+ if (routeRank && routeRank.hardFail) {
632
+ guardsTripped.push({ guard: 'route_rank', severity: 'blocker',
633
+ summary: 'server-side route-rank pre-pass hard-failed (an ungated write route exists) — a client PASS cannot override this (SR-9)' });
634
+ for (const f of routeRank.findings.filter((x) => x.severity === 'blocker')) {
635
+ extraIssues.push({ severity: 'blocker', file: f.file, summary: `[server route-rank] ${f.summary}`, worker: 'server-route-rank' });
636
+ }
637
+ if (clientPassed) downgradeTo = 'fail';
638
+ }
639
+ if (permPath && permPath.hardFail) {
640
+ guardsTripped.push({ guard: 'permission_path', severity: 'blocker',
641
+ summary: `server-side permission-path pre-pass hard-failed: rank '${builderRank}' is below Metic and the submitted committed_files touch protected machinery — a client PASS cannot override this (SR-9 / SEC #863)` });
642
+ for (const f of permPath.findings) {
643
+ extraIssues.push({ severity: 'blocker', file: f.file, summary: `[server permission-path] ${f.summary}`, worker: 'server-permission-path' });
644
+ }
645
+ if (clientPassed) downgradeTo = 'fail';
646
+ }
647
+
648
+ // --- Guard 2: implausible client verdict (SR-9) ---
649
+ // A client PASS with an empty issues[] on a non-trivial diff is not a credible
650
+ // default-fail panel verdict. Downgrade to manual-confirm (a human lands it via
651
+ // /confirm) rather than auto-confirm + auto-credit. Only escalates the verdict
652
+ // (never de-escalates a Guard-1 hard FAIL).
653
+ const issueCount = Array.isArray(base.issues) ? base.issues.length : 0;
654
+ const { files: changeFiles, lines: changeLines } = changeMagnitude({ committedFiles, diffStats: ds });
655
+ const nonTrivial = changeFiles > IMPLAUSIBLE_PASS_MAX_FILES || changeLines > IMPLAUSIBLE_PASS_MAX_LINES;
656
+ if (clientPassed && issueCount === 0 && nonTrivial) {
657
+ guardsTripped.push({ guard: 'implausible_pass', severity: 'major',
658
+ summary: `client PASS with empty issues[] on a non-trivial diff (${changeFiles} file(s), ${changeLines} line(s) > ${IMPLAUSIBLE_PASS_MAX_FILES}/${IMPLAUSIBLE_PASS_MAX_LINES}) is not a credible default-fail panel verdict → manual-confirm (SR-9)` });
659
+ extraIssues.push({ severity: 'major', file: '(grade)', summary:
660
+ `[server plausibility] a passing grade with no enumerated issues on a ${changeFiles}-file / ${changeLines}-line change must be human-confirmed via /confirm`, worker: 'server-plausibility' });
661
+ if (downgradeTo !== 'fail') downgradeTo = 'concerns';
662
+ }
663
+
664
+ // --- Guard 3: bound the credit/achievement signals (SR-18) ---
665
+ // The net-negative "Subtractor" bonus + shipper badges key off client-supplied
666
+ // signals.diff_stats + committed_files the server can't re-derive. We can't
667
+ // make them server-authoritative without the feature-branch diff (residual,
668
+ // see report), but we CAN strip an OBVIOUSLY-FORGED net-negative claim:
669
+ // diff_stats that claims a net-negative (removed > added) while the submitted
670
+ // committed_files list is large is internally inconsistent with a real
671
+ // simplification, and is the cheap way to farm the Subtractor bonus. When the
672
+ // signals are internally implausible we NEUTRALIZE the net-negative reward by
673
+ // zeroing lines_removed in the forwarded diff_stats (so awardNetNegativeBonus
674
+ // sees removed<=added → bonus 0) WITHOUT altering the displayed/audited stats.
675
+ // Conservative: only fires on the inconsistent shape; an honest net-negative
676
+ // simplification (few files, removed>added) is untouched.
677
+ let sanitizedSignals = base.signals;
678
+ const claimsNetNegative =
679
+ ds && (Number(ds.lines_removed) || 0) > (Number(ds.lines_added) || 0);
680
+ const NET_NEGATIVE_MAX_FILES = 25; // a "simplification" touching >25 files is implausibly broad
681
+ if (claimsNetNegative && changeFiles > NET_NEGATIVE_MAX_FILES) {
682
+ guardsTripped.push({ guard: 'forged_net_negative', severity: 'minor',
683
+ summary: `net-negative diff_stats (removed ${ds.lines_removed} > added ${ds.lines_added}) across ${changeFiles} files is implausibly broad for a simplification — net-negative bonus neutralized (SR-18)` });
684
+ // Clone signals + zero the credit-driving field WITHOUT mutating the input
685
+ // or the displayed diff_stats audit value (we add a sibling marker instead).
686
+ sanitizedSignals = {
687
+ ...(base.signals || {}),
688
+ diff_stats: { ...ds, lines_removed: Number(ds.lines_added) || 0 },
689
+ diff_stats_reported: ds, // preserve what the client claimed for the audit trail
690
+ };
691
+ }
692
+
693
+ // No guard tripped (or only audit-level findings on a non-pass) → return the
694
+ // input with a passed-through audit marker so the trail shows guards ran.
695
+ if (downgradeTo == null && guardsTripped.length === 0 && sanitizedSignals === base.signals) {
696
+ return {
697
+ ...base,
698
+ signals: {
699
+ ...(base.signals || {}),
700
+ server_guard: { applied: ['route_rank', 'permission_path', 'implausible_pass', 'forged_net_negative'], ok: true, tripped: [] },
701
+ },
702
+ };
703
+ }
704
+
705
+ // Build the downgraded result. Downgrade-only: if base wasn't passing we keep
706
+ // it not-passing; we never resurrect. Map the panel verdict onto the v3
707
+ // server gate the same way aggregatePanel does (fail → confidence 0; concerns
708
+ // → FALLTHROUGH_CONFIDENCE below the low-confidence cap; pass left as-is).
709
+ const newPassed = clientPassed && downgradeTo == null;
710
+ const forwardedConfidence =
711
+ downgradeTo === 'fail'
712
+ ? 0
713
+ : downgradeTo === 'concerns'
714
+ ? FALLTHROUGH_CONFIDENCE
715
+ : (base.signals?.confidence ?? null);
716
+
717
+ const guardNote = guardsTripped.length
718
+ ? `\n\nServer-authoritative guards (SR-9 / SR-18) ${downgradeTo ? `DOWNGRADED this grade to ${downgradeTo.toUpperCase()}` : 'flagged'}:\n`
719
+ + guardsTripped.map((g) => ` [${g.severity}] ${g.guard}: ${g.summary}`).join('\n')
720
+ : '';
721
+
722
+ return {
723
+ ...base,
724
+ passed: newPassed,
725
+ issues: [...(base.issues || []), ...extraIssues],
726
+ signals: {
727
+ ...sanitizedSignals,
728
+ // confidence drives the server-side gate alignment (validateAndScore on the
729
+ // body already ran; this is the authoritative post-hoc adjustment).
730
+ confidence: forwardedConfidence,
731
+ server_guard: {
732
+ applied: ['route_rank', 'permission_path', 'implausible_pass', 'forged_net_negative'],
733
+ ok: guardsTripped.length === 0,
734
+ downgraded_to: downgradeTo,
735
+ client_passed: clientPassed,
736
+ tripped: guardsTripped,
737
+ },
738
+ },
739
+ notes_md: `${base.notes_md || ''}${guardNote}`,
740
+ };
741
+ }
742
+
743
+ // ---------- v3: multi-shot aggregation ----------
744
+
745
+ // Collapses N per-shot grade-results into the final v3 grade-result.
746
+ // Pass requires unanimous PASS on all shots; anything else (any FAIL, any
747
+ // error, any disagreement) → not passed. Issues are deduped across shots
748
+ // by (file, summary). Displayed rubric_scores use the median across shots
749
+ // so the trend dashboard stays one-number-per-task.
750
+ function aggregateShots(shotResults, meta = {}) {
751
+ const N = shotResults.length;
752
+ const verdicts = shotResults.map((s) => ({
753
+ passed: s.passed,
754
+ ff: s.rubric_scores?.functional_fidelity ?? null,
755
+ cq: s.rubric_scores?.code_quality ?? null,
756
+ economy: s.rubric_scores?.economy ?? null,
757
+ cq_effective: s.rubric_scores?.code_quality_effective ?? null,
758
+ confidence: s.signals?.confidence ?? null,
759
+ model: s.signals?.model ?? null,
760
+ run_ms: s.signals?.run_ms ?? null,
761
+ shot_index: s.signals?.shot_index ?? null,
762
+ notes_md: s.notes_md,
763
+ issues: s.issues || [],
764
+ error:
765
+ s.signals?.parse_error
766
+ ? 'parse_error'
767
+ : s.signals?.schema_error
768
+ ? 'schema_error'
769
+ : s.signals?.subprocess_error || null,
770
+ }));
771
+
772
+ const ff = median(verdicts.map((v) => v.ff));
773
+ const cq = median(verdicts.map((v) => v.cq));
774
+ const economy = median(verdicts.map((v) => v.economy));
775
+ const cqEffective = median(verdicts.map((v) => v.cq_effective));
776
+ const blended = ff != null && cq != null ? r1((ff + cq) / 2) : 0;
777
+ // Top-level aggregated confidence = median of per-shot confidences. Without
778
+ // this, ship.js forwards a null top-level confidence to /tasks/:id/grade,
779
+ // and the server's validateAndScore (re-running on the POST body) hits the
780
+ // Number(null)=0 trap → FAIL even though every shot independently passed.
781
+ // Caught on the MAS Phase 1 self-ship (#395).
782
+ //
783
+ // Computed UNROUNDED (no r1()) — confidence sits near a 0.4 gate cap so
784
+ // rounding to one decimal could nudge a borderline value across the cap
785
+ // in either direction. Per-shot confidences are already 0-1 floats.
786
+ const confVals = verdicts
787
+ .map((v) => v.confidence)
788
+ .filter((n) => Number.isFinite(n))
789
+ .sort((a, b) => a - b);
790
+ const confidenceMedian = confVals.length === 0
791
+ ? null
792
+ : confVals.length % 2
793
+ ? confVals[(confVals.length - 1) / 2]
794
+ : (confVals[confVals.length / 2 - 1] + confVals[confVals.length / 2]) / 2;
795
+
796
+ // Dedupe issues across shots by (file, summary). Keep first-seen severity.
797
+ const seen = new Set();
798
+ const issues = [];
799
+ for (const v of verdicts) {
800
+ for (const it of v.issues) {
801
+ if (!it || typeof it !== 'object') continue;
802
+ const key = `${it.file ?? ''}::${(it.summary ?? '').trim()}`;
803
+ if (seen.has(key)) continue;
804
+ seen.add(key);
805
+ issues.push(it);
806
+ }
807
+ }
808
+
809
+ const passCount = verdicts.filter((v) => v.passed === true).length;
810
+ const failCount = verdicts.filter((v) => v.passed === false && !v.error).length;
811
+ const errorCount = verdicts.filter((v) => v.error).length;
812
+ const allPassed = N > 0 && passCount === N;
813
+
814
+ let verdict;
815
+ let passed;
816
+ let aggregationKind;
817
+ if (errorCount > 0) {
818
+ passed = false;
819
+ verdict = `FAIL — ${errorCount}/${N} shot(s) errored`;
820
+ aggregationKind = 'errored';
821
+ } else if (allPassed) {
822
+ passed = true;
823
+ verdict = `PASS — ${N}/${N} shots unanimous`;
824
+ aggregationKind = 'unanimous_pass';
825
+ } else if (passCount === 0) {
826
+ passed = false;
827
+ verdict = `FAIL — ${failCount}/${N} shots`;
828
+ aggregationKind = 'unanimous_fail';
829
+ } else {
830
+ passed = false;
831
+ verdict = `FAIL — ${passCount} pass, ${failCount} fail (no unanimous pass)`;
832
+ aggregationKind = 'disagreement';
833
+ }
834
+
835
+ const lines = [];
836
+ lines.push(`Subagent grader ${VERSION} (multi-shot, ${N}× shots) — ${verdict}`);
837
+ lines.push(` functional_fidelity (median): ${ff ?? '?'}/10`);
838
+ lines.push(` code_quality (median): ${cq ?? '?'}/10`);
839
+ if (economy != null) {
840
+ lines.push(` economy (advisory, median): ${economy}/10`);
841
+ }
842
+ lines.push(` blended (median): ${blended}/10 (threshold ${THRESHOLD}, both axes must clear on EVERY shot)`);
843
+ lines.push('');
844
+ lines.push('Per-shot:');
845
+ for (let i = 0; i < verdicts.length; i++) {
846
+ const v = verdicts[i];
847
+ const status = v.error ? 'ERROR' : v.passed === true ? 'PASS' : 'FAIL';
848
+ const ffStr = v.ff != null ? `${v.ff}` : '?';
849
+ const cqStr = v.cq != null ? `${v.cq}` : '?';
850
+ const confStr = v.confidence != null ? ` conf=${v.confidence}` : '';
851
+ const errStr = v.error ? ` (${v.error})` : '';
852
+ lines.push(` shot[${i}] ${status} ff=${ffStr} cq=${cqStr}${confStr} ${v.model ?? '?'}${errStr}`);
853
+ }
854
+ if (issues.length > 0) {
855
+ lines.push('');
856
+ lines.push(`Issues (${issues.length} unique across shots):`);
857
+ for (const it of issues.slice(0, 20)) {
858
+ const sev = String(it.severity || '?').padEnd(7);
859
+ const file = String(it.file || '?');
860
+ const summary = String(it.summary || '').replace(/\s+/g, ' ').trim();
861
+ lines.push(` [${sev}] ${file} — ${summary}`);
862
+ }
863
+ if (issues.length > 20) {
864
+ lines.push(` ... ${issues.length - 20} more (full set in signals.shots[].issues)`);
865
+ }
866
+ }
867
+
868
+ // #486 (ADR 0028 path 1): sum each shot's own subprocess cost (envelope
869
+ // total_cost_usd) so the trivial-diff path reports panel_cost_usd the same way
870
+ // the full panel does. The single-shot trivial path is the common case here.
871
+ const panelCostUsd = shotResults.reduce((s, r) => s + (Number(r.cost_usd) || 0), 0);
872
+
873
+ return {
874
+ score: blended,
875
+ passed,
876
+ threshold: THRESHOLD,
877
+ grader_kind: 'subagent-multishot',
878
+ grader_version: VERSION,
879
+ panel_cost_usd: panelCostUsd, // #486: panel's own API spend (USD)
880
+ rubric_scores: {
881
+ functional_fidelity: ff,
882
+ code_quality: cq,
883
+ economy,
884
+ code_quality_effective: cqEffective,
885
+ },
886
+ signals: {
887
+ grader_kind: 'subagent-multishot',
888
+ grader_version: VERSION,
889
+ threshold: THRESHOLD,
890
+ // Top-level confidence = median across shots. ship.js forwards this to
891
+ // POST /tasks/:id/grade so the server-side re-validation has a real
892
+ // number rather than null (see Number(null)=0 fix in validateAndScore).
893
+ confidence: confidenceMedian,
894
+ shots: verdicts,
895
+ shot_count: N,
896
+ pass_count: passCount,
897
+ fail_count: failCount,
898
+ error_count: errorCount,
899
+ shot_aggregation: aggregationKind,
900
+ ...meta,
901
+ },
902
+ notes_md: lines.join('\n'),
903
+ issues,
904
+ };
905
+ }
906
+
907
+ // ---------- v3: subagent subprocess invoker ----------
908
+
909
+ // Unchanged from v2 — spawns `claude -p --print --model <model> --output-
910
+ // format json` with stdin = prompt. The v3 caller varies `model` per shot
911
+ // via opts.model (set by gradeSubagent below).
912
+ function runSubagent({ prompt, opts = {} }) {
913
+ return new Promise((resolve, reject) => {
914
+ const timeoutMs = opts.timeoutMs ?? 240_000;
915
+ const cwd = opts.cwd ?? process.cwd();
916
+ const model = opts.model ?? DEFAULT_GRADER_MODEL;
917
+ // Env escape hatch (OTB_CLAUDE_BIN / CLAUDE_BIN) for installs where the CLI
918
+ // isn't named `claude` on PATH — covers any OS, not just Windows.
919
+ const bin = opts.claudeBin ?? process.env.OTB_CLAUDE_BIN ?? process.env.CLAUDE_BIN ?? 'claude';
920
+
921
+ const args = [
922
+ '-p',
923
+ '--print',
924
+ '--model', model,
925
+ '--output-format', 'json',
926
+ '--tools', 'Read',
927
+ '--disable-slash-commands',
928
+ '--permission-mode', 'bypassPermissions',
929
+ ];
930
+
931
+ const startedAt = Date.now();
932
+ const child = spawn(bin, args, {
933
+ cwd,
934
+ stdio: ['pipe', 'pipe', 'pipe'],
935
+ // On Windows the `claude` CLI is a `claude.cmd` shim that Node's spawn
936
+ // can't launch without a shell (ENOENT -> SUBAGENT_SPAWN_FAILED, observed
937
+ // on #662). Route through cmd.exe on win32 only; Mac/Linux keep
938
+ // shell:false (the default, and the proven-working path). args carry no
939
+ // user input (the prompt goes in via stdin), so shell injection is a
940
+ // non-issue. windowsHide suppresses the transient console window.
941
+ shell: process.platform === 'win32',
942
+ windowsHide: true,
943
+ // MAS Phase 3 (#427): mark this child as a subagent so the Conductor
944
+ // UserPromptSubmit hook (.claude/hooks/conductor.js) bails instead of
945
+ // injecting advisory context into the grader worker's prompt. Any
946
+ // subagent we spawn inherits this flag and is likewise suppressed.
947
+ env: { ...process.env, OTB_SUBAGENT: '1' },
948
+ });
949
+
950
+ let stdout = '';
951
+ let stderr = '';
952
+ let timedOut = false;
953
+
954
+ const killTimer = setTimeout(() => {
955
+ timedOut = true;
956
+ try { child.kill('SIGTERM'); } catch (_) {}
957
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch (_) {} }, 5_000);
958
+ }, timeoutMs);
959
+
960
+ child.stdout.on('data', (b) => { stdout += b.toString('utf8'); });
961
+ child.stderr.on('data', (b) => { stderr += b.toString('utf8'); });
962
+
963
+ child.on('error', (err) => {
964
+ clearTimeout(killTimer);
965
+ reject(Object.assign(new Error(`subagent spawn failed: ${err.message}`), {
966
+ code: 'SUBAGENT_SPAWN_FAILED',
967
+ cause: err,
968
+ }));
969
+ });
970
+
971
+ child.on('close', (exitCode) => {
972
+ clearTimeout(killTimer);
973
+ const runMs = Date.now() - startedAt;
974
+ if (timedOut) {
975
+ return reject(Object.assign(new Error(`subagent timed out after ${timeoutMs}ms`), {
976
+ code: 'SUBAGENT_TIMEOUT',
977
+ run_ms: runMs,
978
+ stdout,
979
+ stderr,
980
+ }));
981
+ }
982
+ if (exitCode !== 0) {
983
+ return reject(Object.assign(new Error(`subagent exited with code ${exitCode}`), {
984
+ code: 'SUBAGENT_NONZERO_EXIT',
985
+ exit_code: exitCode,
986
+ run_ms: runMs,
987
+ stdout,
988
+ stderr,
989
+ }));
990
+ }
991
+ resolve({ stdout, stderr, run_ms: runMs, exit_code: exitCode, cost_usd: extractCostUsd(stdout) });
992
+ });
993
+
994
+ child.stdin.write(prompt, 'utf8');
995
+ child.stdin.end();
996
+ });
997
+ }
998
+
999
+ // System 5 (task 1359): an OPT-IN cached path around runSubagent. When the caller
1000
+ // supplies a `cache` spec (from llm-cache.makeCacheSpec), a hit replays the stored
1001
+ // result at cost_usd:0 (so $0 flows naturally into extractCostUsd → session-cost);
1002
+ // a miss runs the untouched runSubagent and stores a clean (exit 0) result. With
1003
+ // no cache spec it's exactly runSubagent — the default path is never altered. The
1004
+ // llm-cache reaches us through the doorway (BV1.R73: it's shared infra core also
1005
+ // uses, so a module can't own it); if it's somehow absent the path degrades to an
1006
+ // ordinary uncached run, exactly as the old lazy-require catch did.
1007
+ async function runSubagentCached({ prompt, opts = {}, cache = null }) {
1008
+ if (!cache || !cache.key) return runSubagent({ prompt, opts });
1009
+ const llmCache = api.llmCache;
1010
+ if (!llmCache) return runSubagent({ prompt, opts });
1011
+ return llmCache.withCache(cache, () => runSubagent({ prompt, opts }));
1012
+ }
1013
+
1014
+ // #486 (ADR 0028 path 1): the `claude -p --output-format json` envelope reports
1015
+ // the subprocess's own dollar cost in `total_cost_usd` (verified present:
1016
+ // {type, ..., total_cost_usd, usage, modelUsage}). We were parsing only
1017
+ // `env.result` (extractAssistantResult) and silently discarding that cost — the
1018
+ // single biggest reason cost_log's `api` category sat at $0.60 across the whole
1019
+ // project (#482 audit F1). Pull it out so the panel's spend can be logged
1020
+ // (category=api, source=grader). Returns null on non-JSON / partial output, so
1021
+ // a cost signal never fabricates a number it doesn't have.
1022
+ function extractCostUsd(rawStdout) {
1023
+ try {
1024
+ const env = JSON.parse(rawStdout);
1025
+ if (env && typeof env.total_cost_usd === 'number' && env.total_cost_usd >= 0) {
1026
+ return env.total_cost_usd;
1027
+ }
1028
+ } catch (_) { /* non-JSON or partial stdout — no cost signal */ }
1029
+ return null;
1030
+ }
1031
+
1032
+ function extractAssistantResult(rawStdout) {
1033
+ try {
1034
+ const env = JSON.parse(rawStdout);
1035
+ if (env && typeof env.result === 'string') return env.result;
1036
+ if (env && typeof env === 'object' && 'functional_fidelity' in env) return env;
1037
+ return rawStdout;
1038
+ } catch (_) {
1039
+ return rawStdout;
1040
+ }
1041
+ }
1042
+
1043
+ // Run one shot end-to-end: build the prompt (caller did this), spawn the
1044
+ // subagent at the requested model, parse + validate. Returns a per-shot
1045
+ // grade-result; subprocess failures become passed=false per-shot results so
1046
+ // aggregateShots can still produce a final verdict.
1047
+ async function runOneShot({ prompt, model, shotIndex, runOpts }) {
1048
+ try {
1049
+ const { stdout, run_ms, exit_code, cost_usd } = await runSubagent({
1050
+ prompt,
1051
+ opts: { ...runOpts, model },
1052
+ });
1053
+ const assistantResult = extractAssistantResult(stdout);
1054
+ const scored = validateAndScore(assistantResult, {
1055
+ run_ms,
1056
+ exit_code,
1057
+ raw_chars: stdout.length,
1058
+ model,
1059
+ shot_index: shotIndex,
1060
+ });
1061
+ // #486 (ADR 0028 path 1): carry this shot's own subprocess cost so the
1062
+ // trivial-diff path (gradeSubagentV3MultiShot → aggregateShots) can sum it
1063
+ // into panel_cost_usd just like the full panel does.
1064
+ scored.cost_usd = cost_usd ?? null;
1065
+ return scored;
1066
+ } catch (err) {
1067
+ return {
1068
+ score: 0,
1069
+ passed: false,
1070
+ threshold: THRESHOLD,
1071
+ grader_kind: 'subagent',
1072
+ grader_version: VERSION,
1073
+ rubric_scores: {},
1074
+ signals: {
1075
+ grader_kind: 'subagent',
1076
+ grader_version: VERSION,
1077
+ model,
1078
+ threshold: THRESHOLD,
1079
+ shot_index: shotIndex,
1080
+ run_ms: err.run_ms ?? null,
1081
+ exit_code: err.exit_code ?? null,
1082
+ subprocess_error: err.code || 'UNKNOWN',
1083
+ stderr_tail: (err.stderr || '').slice(-500),
1084
+ },
1085
+ notes_md: `Shot ${shotIndex}: subprocess failed (${err.code || 'UNKNOWN'}: ${err.message}).`,
1086
+ issues: [],
1087
+ };
1088
+ }
1089
+ }
1090
+
1091
+ // ---------- v4: panel aggregation (MAS Phase 2.0) ----------
1092
+
1093
+ // Collapses N per-worker WorkerResults into the final v4 grade-result.
1094
+ //
1095
+ // Gate logic:
1096
+ // - Any worker `fail` → overall FAIL (a real finding blocks).
1097
+ // - Else any worker errored → overall FALLTHROUGH (manual-confirm) —
1098
+ // a crash is missing signal, not a
1099
+ // finding, so it must not hard-block
1100
+ // on infra flakiness (#126).
1101
+ // - Else any worker `concerns` → overall FALLTHROUGH (manual-confirm).
1102
+ // - All workers `pass` → overall PASS.
1103
+ //
1104
+ // Server-compat forwarding:
1105
+ // The grade POST shape (ff, cq, economy, confidence, issues) is preserved
1106
+ // so the server's existing validateAndScore re-validation aligns with the
1107
+ // panel's verdict. We achieve this by setting the forwarded confidence:
1108
+ // pass → median worker confidence (server's gate also passes)
1109
+ // concerns → FALLTHROUGH_CONFIDENCE (below LOW_CONFIDENCE → server fails)
1110
+ // fail → 0 + a failing axis preserved at < threshold (server fails)
1111
+ //
1112
+ // rubric_scores:
1113
+ // Each axis is the median across workers that reported it (Narc reports
1114
+ // functional_fidelity; Quality reports code_quality + economy). Preserves
1115
+ // the trend dashboard's one-number-per-task contract.
1116
+ //
1117
+ // signals.workers[] carries the full per-worker audit trail.
1118
+ function aggregatePanel(workerResults, meta = {}) {
1119
+ const N = workerResults.length;
1120
+
1121
+ // Score axes — median across workers that reported each. Reuses the
1122
+ // module-level median() helper (rounded to one decimal).
1123
+ const ff = median(workerResults.map((w) => w.scores?.functional_fidelity));
1124
+ const cq = median(workerResults.map((w) => w.scores?.code_quality));
1125
+ const economy = median(workerResults.map((w) => w.scores?.economy));
1126
+ // Phase 2.1 (#416): Hacker reports security_score. Added to rubric_scores
1127
+ // for the audit trail and trend dashboard; not directly server-gated
1128
+ // (Hacker's verdict still flows through the existing confidence-forwarding).
1129
+ const security = median(workerResults.map((w) => w.scores?.security_score));
1130
+ // Phase 2.2 (#421): Efficiency Monger reports efficiency_score. Same
1131
+ // additive treatment as security_score.
1132
+ const efficiencyScore = median(workerResults.map((w) => w.scores?.efficiency_score));
1133
+
1134
+ const economyPenalty =
1135
+ economy != null ? Math.max(0, (THRESHOLD - economy) / 2) : 0;
1136
+ const cqEffective = cq != null && economy != null ? r1(clamp(cq - economyPenalty, 0, 10)) : cq;
1137
+ const blended = ff != null && cq != null ? r1((ff + cq) / 2) : 0;
1138
+
1139
+ // Verdict counts.
1140
+ const passCount = workerResults.filter((w) => w.verdict === 'pass').length;
1141
+ const concernsCount = workerResults.filter((w) => w.verdict === 'concerns').length;
1142
+ // A crashed worker sets verdict='fail' as a DEFAULT in its computeVerdict()
1143
+ // subprocess_error / parse_error / schema_error early returns (see each
1144
+ // worker module) — that is MISSING SIGNAL, not a substantive finding, and
1145
+ // those early returns bail before any issues are extracted (error set ⟹ no
1146
+ // findings). Treat such a worker as "missing signal" so it counts ONLY toward
1147
+ // errorCount; otherwise the errorCount fall-through below is unreachable dead
1148
+ // code (#473 grader catch).
1149
+ //
1150
+ // DEFENSE-IN-DEPTH (#473 Hacker [major]): the above invariant is the load-
1151
+ // bearing claim. Rather than trust it blindly, a worker is "missing signal"
1152
+ // only if it errored AND surfaced NO blocking/major issue. So even if a future
1153
+ // worker ever set both `error` and a real finding, that finding still blocks
1154
+ // (counts as a substantive fail) instead of being reclassified as a flake.
1155
+ const hasSubstantiveIssue = (w) =>
1156
+ (w.issues || []).some((i) => i.severity === 'blocker' || i.severity === 'major');
1157
+ const isMissingSignal = (w) => !!w.error && !hasSubstantiveIssue(w);
1158
+ const failCount = workerResults.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
1159
+ // NOTE (#473/#589): signals.error_count counts only isMissingSignal workers — i.e. a worker that
1160
+ // errored AND surfaced no blocking issue. A worker that errored but still raised a blocker/major
1161
+ // is a real finding, so it counts in fail_count (above), NOT here. This is a deliberate semantic
1162
+ // change from the old "all w.error" meaning; keep error_count = missing-signal-only.
1163
+ const errorCount = workerResults.filter((w) => isMissingSignal(w)).length;
1164
+
1165
+ // V3.R85 (#481): the Narc (functional_fidelity) is ADVISORY in the gate —
1166
+ // Lars's call 2026-05-30 after #303, where over-literal acceptance-criteria
1167
+ // objections from this one lens repeatedly blocked ships that Quality,
1168
+ // Hacker, and Efficiency all cleared. The Narc keeps its lens (scores +
1169
+ // issues are still recorded and displayed), but its weight in the GATE drops:
1170
+ // - a Narc 'concerns' → advisory only; never blocks.
1171
+ // - a Narc-only 'fail' → downgrades to manual-confirm (CONCERNS), not a
1172
+ // hard panel FAIL — so a genuine functional gap still surfaces and pauses
1173
+ // for a human, but isn't an automatic rejection.
1174
+ // - Quality / Hacker / Efficiency keep their hard veto unchanged.
1175
+ // The raw counts above are untouched, so the audit trail + dashboards still
1176
+ // see every worker's real verdict.
1177
+ //
1178
+ // DEFENSE-IN-DEPTH (#481 Hacker [major]): a worker is advisory ONLY if it
1179
+ // carries no blocker-severity issue. So even if a future change routed a
1180
+ // gating finding through an advisory kind (e.g. the deterministic route-rank
1181
+ // pre-pass — today kind='route-rank', NOT advisory — or any relabel), a
1182
+ // blocker-severity finding can never be silently downgraded to manual-confirm.
1183
+ // This mirrors the isMissingSignal defense-in-depth (#473): a real finding
1184
+ // always blocks. Over-literal Narc objections are major/minor, never blocker,
1185
+ // so the loosening still bites where intended.
1186
+ const ADVISORY_WORKERS = new Set(['narc']);
1187
+ const hasBlocker = (w) => (w.issues || []).some((i) => i.severity === 'blocker');
1188
+ const isAdvisory = (w) => ADVISORY_WORKERS.has(w.kind) && !hasBlocker(w);
1189
+ // If the panel were somehow advisory-only, fall back to gating on everyone so
1190
+ // a lone Narc still counts (defensive — the default panel always has 3+
1191
+ // gating workers).
1192
+ const nonAdvisory = workerResults.filter((w) => !isAdvisory(w));
1193
+ const gating = nonAdvisory.length ? nonAdvisory : workerResults;
1194
+ const gatingFail = gating.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
1195
+ const gatingError = gating.filter((w) => isMissingSignal(w)).length;
1196
+ const gatingConcerns = gating.filter((w) => w.verdict === 'concerns').length;
1197
+ const gatingAllPass = gating.length > 0 && gating.every((w) => w.verdict === 'pass');
1198
+ // #574 (idea 130): the Hacker (security lens) is the ONE worker whose absence
1199
+ // must not soften the gate. A diff crafted to RELIABLY crash the Hacker
1200
+ // (adversarial content → parse_error/SUBAGENT_TIMEOUT) would otherwise convert
1201
+ // the outcome from auto-block to lenient manual-confirm. Detect a gating Hacker
1202
+ // that produced no signal (missing-signal = errored AND no substantive finding;
1203
+ // a Hacker that errored but still raised a blocker is handled by gatingFail).
1204
+ const securityLensErrored = gating.some((w) => w.kind === 'hacker' && isMissingSignal(w));
1205
+ // #943: TOTAL grader outage — EVERY gating worker is missing-signal (its
1206
+ // subprocess failed to spawn / timed out / produced unparseable output). With
1207
+ // no quality signal at all, the panel has not actually GRADED anything; this
1208
+ // is an infra/outage condition, NOT a 0/10 quality rejection. Distinguished
1209
+ // from the partial-error case (some gating workers errored, others reported)
1210
+ // so callers (ship.js) can present "grader unavailable" instead of a FAIL.
1211
+ const gatingErrorAll = gating.length > 0 && gating.every((w) => isMissingSignal(w));
1212
+ // Advisory-worker outcomes — never a hard block. Uses isAdvisory so a Narc
1213
+ // that DID raise a blocker is treated as gating (in `nonAdvisory`), not here.
1214
+ const advisoryFail = workerResults.some((w) => isAdvisory(w) && w.verdict === 'fail' && !isMissingSignal(w));
1215
+ const advisoryConcerns = workerResults.some((w) => isAdvisory(w) && w.verdict === 'concerns');
1216
+
1217
+ // Confidence — median for pass, fallthrough for concerns, 0 for fail.
1218
+ // (Drives the server-side gate; see file header.) Computed UNROUNDED —
1219
+ // confidence sits near the 0.4 gate cap so the module-level median()
1220
+ // helper (which rounds via r1()) would risk nudging a borderline value
1221
+ // across the boundary. Inlined for that one-decimal-precision reason.
1222
+ const confVals = workerResults
1223
+ .map((w) => w.confidence)
1224
+ .filter((n) => Number.isFinite(n))
1225
+ .slice()
1226
+ .sort((a, b) => a - b);
1227
+ const medianConf = confVals.length === 0
1228
+ ? null
1229
+ : confVals.length % 2
1230
+ ? confVals[(confVals.length - 1) / 2]
1231
+ : (confVals[confVals.length / 2 - 1] + confVals[confVals.length / 2]) / 2;
1232
+
1233
+ let verdict;
1234
+ let panelOutcome;
1235
+ let forwardedConfidence;
1236
+ // Gate on the GATING (non-advisory) workers. The Narc, when advisory, can at
1237
+ // most push the panel to CONCERNS (manual-confirm) — never PASS-block-free
1238
+ // nor hard-FAIL.
1239
+ const advisoryNote = advisoryFail
1240
+ ? ' — narc (advisory) flagged a functional-fidelity failure: confirm only after a human checks the scope is met'
1241
+ : '';
1242
+ if (gatingFail > 0) {
1243
+ // A substantive failure from a GATING worker hard-blocks (errored gating
1244
+ // workers, if any, are noted but the real failure is what governs).
1245
+ verdict = (gatingError > 0
1246
+ ? `FAIL — ${gatingFail}/${N} gating worker(s) failed, ${gatingError}/${N} errored`
1247
+ : `FAIL — ${gatingFail}/${N} gating worker(s) failed`) + advisoryNote;
1248
+ panelOutcome = 'fail';
1249
+ forwardedConfidence = 0;
1250
+ } else if (gatingErrorAll) {
1251
+ // #943: EVERY gating worker failed to run — a total grader outage (the
1252
+ // grader subprocess couldn't spawn / timed out across the board, e.g. the
1253
+ // `claude` CLI 401s in a desktop session, or an API outage). There is NO
1254
+ // quality signal, so this is NOT a quality rejection — it is "the grader
1255
+ // could not run". A distinct 'unavailable' outcome (vs the partial-error
1256
+ // 'concerns' below) lets ship.js present it as "grader unavailable" rather
1257
+ // than the misleading "FAIL · 0/10" that reached the builder in session
1258
+ // 1659. passed stays false → routes to manual-confirm (never auto-pass).
1259
+ const erroredKinds = gating.filter((w) => w.error).map((w) => w.kind);
1260
+ verdict =
1261
+ `GRADER UNAVAILABLE — all ${gatingError}/${gating.length} gating worker(s) failed to run (${erroredKinds.join(', ')}); no quality signal was produced. This is an infra/outage condition, NOT a quality rejection — manual-confirm available.` +
1262
+ advisoryNote;
1263
+ panelOutcome = 'unavailable';
1264
+ forwardedConfidence = FALLTHROUGH_CONFIDENCE;
1265
+ } else if (securityLensErrored) {
1266
+ // #574 (idea 130): the security lens (Hacker) went dark while the rest of the
1267
+ // panel ran — the adversarial signature. Unlike other lenses, a missing
1268
+ // Hacker HARD-BLOCKS rather than falling through to manual-confirm: a diff
1269
+ // engineered to reliably crash the Hacker (parse_error/SUBAGENT_TIMEOUT) would
1270
+ // otherwise downgrade an auto-block into "await Archon", quietly removing the
1271
+ // one lens that catches injected XSS / client-authz regressions. Errored
1272
+ // workers are already auto-retried once (#465), so a Hacker still dark here has
1273
+ // failed twice — treat that as a FAIL, not flakiness. (The all-errored infra
1274
+ // outage is the 'unavailable' case above (#943); a real Hacker blocker is the
1275
+ // gatingFail case above — both take precedence over this branch.) Re-run the
1276
+ // grader once the diff/grader is healthy to clear it.
1277
+ const otherErrored = gating.filter((w) => w.error && w.kind !== 'hacker').map((w) => w.kind);
1278
+ verdict =
1279
+ `FAIL — ⚠ SECURITY LENS MISSING: the Hacker (security) worker errored and produced no signal; security is non-optional, so a reliably-missing Hacker hard-blocks (re-run the grader once healthy)` +
1280
+ (otherErrored.length ? ` — also errored: ${otherErrored.join(', ')}` : '') +
1281
+ advisoryNote;
1282
+ panelOutcome = 'fail';
1283
+ forwardedConfidence = 0;
1284
+ } else if (gatingError > 0) {
1285
+ // #126: a GATING worker that crashed (parse_error / SUBAGENT_TIMEOUT /
1286
+ // subprocess error) produced NO signal — missing information, not a
1287
+ // finding. With no substantive failures (and at least one gating worker
1288
+ // that DID report), fall through to manual-confirm rather than hard-FAIL on
1289
+ // infrastructure flakiness. Name the errored workers. The security (Hacker)
1290
+ // lens is NOT among them here — a missing Hacker is hard-blocked by the
1291
+ // securityLensErrored branch above (#574). An errored ADVISORY worker (Narc)
1292
+ // is not counted here: its lens is advisory, so its absence doesn't gate.
1293
+ // (The all-errored total-outage case is handled above, #943.)
1294
+ const erroredKinds = gating.filter((w) => w.error).map((w) => w.kind);
1295
+ verdict =
1296
+ `CONCERNS — ${gatingError}/${N} gating worker(s) errored (${erroredKinds.join(', ')}), none failed (manual-confirm available)` +
1297
+ advisoryNote;
1298
+ panelOutcome = 'concerns';
1299
+ forwardedConfidence = FALLTHROUGH_CONFIDENCE;
1300
+ } else if (gatingConcerns > 0 || advisoryFail) {
1301
+ // Gating-worker concerns, OR an advisory Narc 'fail' (downgraded here from a
1302
+ // hard block) → manual-confirm.
1303
+ const reasons = [];
1304
+ if (gatingConcerns > 0) reasons.push(`${gatingConcerns}/${N} gating worker(s) flagged concerns`);
1305
+ if (advisoryFail) reasons.push('narc (advisory) flagged a functional-fidelity failure');
1306
+ verdict = `CONCERNS — ${reasons.join('; ')} (manual-confirm available)`;
1307
+ panelOutcome = 'concerns';
1308
+ forwardedConfidence = FALLTHROUGH_CONFIDENCE;
1309
+ } else if (gatingAllPass) {
1310
+ // Every gating worker passed. A Narc 'concerns' here is advisory only.
1311
+ verdict = `PASS — ${gating.length}/${gating.length} gating worker(s) cleared${advisoryConcerns ? ' (narc advisory: concerns)' : ''}`;
1312
+ panelOutcome = 'pass';
1313
+ forwardedConfidence = medianConf;
1314
+ } else {
1315
+ // Should be unreachable (no fail/concerns and not all gating pass implies an
1316
+ // unknown verdict) but be defensive.
1317
+ verdict = `FAIL — unknown verdict state (pass=${passCount} concerns=${concernsCount} fail=${failCount} of ${N})`;
1318
+ panelOutcome = 'fail';
1319
+ forwardedConfidence = 0;
1320
+ }
1321
+
1322
+ const passed = panelOutcome === 'pass';
1323
+
1324
+ // Dedupe issues by (worker kind, file, summary) — intentional. If Narc and
1325
+ // Quality both flag the same (file, summary), the two appear separately
1326
+ // because they are independent confirmations from different lenses; merging
1327
+ // would erase that signal.
1328
+ const seen = new Set();
1329
+ const issues = [];
1330
+ for (const w of workerResults) {
1331
+ for (const it of (w.issues || [])) {
1332
+ if (!it || typeof it !== 'object') continue;
1333
+ const key = `${w.kind}::${it.file ?? ''}::${(it.summary ?? '').trim()}`;
1334
+ if (seen.has(key)) continue;
1335
+ seen.add(key);
1336
+ issues.push({ ...it, worker: w.kind });
1337
+ }
1338
+ }
1339
+
1340
+ // Build display notes.
1341
+ const lines = [];
1342
+ lines.push(`Subagent grader ${VERSION} (panel of ${N}: ${workerResults.map((w) => w.kind).join(', ')}) — ${verdict}`);
1343
+ if (ff != null) lines.push(` functional_fidelity (Narc): ${ff}/10`);
1344
+ if (cq != null) lines.push(` code_quality (Quality): ${cq}/10`);
1345
+ if (economy != null) lines.push(` economy (Quality, adv.): ${economy}/10`);
1346
+ if (security != null) lines.push(` security_score (Hacker): ${security}/10`);
1347
+ if (efficiencyScore != null) lines.push(` efficiency_score (EffMng): ${efficiencyScore}/10`);
1348
+ lines.push(` blended: ${blended}/10 (threshold ${THRESHOLD}, gating workers must PASS; narc advisory — #481)`);
1349
+ lines.push(` forwarded confidence: ${forwardedConfidence ?? '?'} (panel: ${panelOutcome})`);
1350
+ lines.push('');
1351
+ lines.push('Per-worker:');
1352
+ for (const w of workerResults) {
1353
+ const status = w.error ? 'ERROR' : w.verdict.toUpperCase();
1354
+ const scores = Object.entries(w.scores || {})
1355
+ .filter(([_, v]) => Number.isFinite(v))
1356
+ .map(([k, v]) => `${k.split('_').map((p) => p[0]).join('')}=${v}`)
1357
+ .join(' ');
1358
+ const confStr = w.confidence != null ? ` conf=${w.confidence}` : '';
1359
+ const errStr = w.error ? ` (${w.error})` : '';
1360
+ const retryStr = w.retried ? ' (retried — #465)' : '';
1361
+ lines.push(` ${w.kind.padEnd(10)} ${status.padEnd(8)} ${scores}${confStr} ${w.model ?? '?'}${errStr}${retryStr}`);
1362
+ }
1363
+ if (issues.length > 0) {
1364
+ lines.push('');
1365
+ lines.push(`Issues (${issues.length} unique across workers):`);
1366
+ for (const it of issues.slice(0, 30)) {
1367
+ const sev = String(it.severity || '?').padEnd(7);
1368
+ const file = String(it.file || '?');
1369
+ const summary = String(it.summary || '').replace(/\s+/g, ' ').trim();
1370
+ lines.push(` [${sev}] ${file} (${it.worker}) — ${summary}`);
1371
+ }
1372
+ if (issues.length > 30) {
1373
+ lines.push(` ... ${issues.length - 30} more (full set in signals.workers[].issues)`);
1374
+ }
1375
+ }
1376
+
1377
+ // #486 (ADR 0028 path 1): sum each worker's own subprocess cost (from its
1378
+ // `claude -p` envelope total_cost_usd). Workers that errored / never produced
1379
+ // a JSON envelope contribute null → 0. The deterministic route-rank pseudo-
1380
+ // worker has no subprocess and no cost_usd, so it's naturally excluded.
1381
+ const panelCostUsd = workerResults.reduce((s, w) => s + (Number(w.cost_usd) || 0), 0);
1382
+
1383
+ return {
1384
+ // server-compat top-level
1385
+ score: blended,
1386
+ passed,
1387
+ threshold: THRESHOLD,
1388
+ grader_kind: 'subagent-panel',
1389
+ grader_version: VERSION,
1390
+ // #486: the panel's own API spend (USD), for cost_log (source=grader).
1391
+ panel_cost_usd: panelCostUsd,
1392
+ // rubric_scores keep the v3 keys so the trend dashboard + audit-ships
1393
+ // legacy v1 entry-points continue to read them. Phase 2.1 added
1394
+ // security_score; Phase 2.2 adds efficiency_score. Both additive —
1395
+ // consumers that don't know about them just ignore the fields.
1396
+ rubric_scores: {
1397
+ functional_fidelity: ff,
1398
+ code_quality: cq,
1399
+ economy,
1400
+ code_quality_effective: cqEffective,
1401
+ security_score: security,
1402
+ efficiency_score: efficiencyScore,
1403
+ },
1404
+ signals: {
1405
+ grader_kind: 'subagent-panel',
1406
+ grader_version: VERSION,
1407
+ threshold: THRESHOLD,
1408
+ // forwardedConfidence drives the server-side gate alignment
1409
+ confidence: forwardedConfidence,
1410
+ panel_outcome: panelOutcome,
1411
+ workers: workerResults,
1412
+ worker_count: N,
1413
+ pass_count: passCount,
1414
+ concerns_count: concernsCount,
1415
+ fail_count: failCount,
1416
+ error_count: errorCount,
1417
+ panel_cost_usd: panelCostUsd, // #486: mirror into signals for the audit trail
1418
+ ...meta,
1419
+ },
1420
+ notes_md: lines.join('\n'),
1421
+ issues,
1422
+ };
1423
+ }
1424
+
1425
+ // #943: synthesize the distinct "grader unavailable / bypassed" result WITHOUT
1426
+ // spawning any worker. Same shape as aggregatePanel's return so every consumer
1427
+ // (buildGradePayload, the server's validateAndScore re-run, ship.js's
1428
+ // printGradeOutcome) works unchanged. ff/cq are null (nothing was graded) and
1429
+ // the forwarded confidence is FALLTHROUGH_CONFIDENCE, so the server gate treats
1430
+ // it as manual-confirm — never an auto-pass, never an auto-credit. panel_outcome
1431
+ // carries the 'unavailable' tag so ship.js presents "grader unavailable" rather
1432
+ // than a misleading "FAIL · 0/10". `reason` is recorded for the audit trail.
1433
+ function graderUnavailableResult({ reason, note, meta = {} } = {}) {
1434
+ const notes = [
1435
+ `Subagent grader ${VERSION} — GRADER UNAVAILABLE (${reason}).`,
1436
+ note || 'The grader did not run — this is an infra/outage/bypass condition, NOT a quality rejection. Manual-confirm available.',
1437
+ ].join('\n');
1438
+ return {
1439
+ score: 0,
1440
+ passed: false,
1441
+ threshold: THRESHOLD,
1442
+ grader_kind: 'subagent-panel',
1443
+ grader_version: VERSION,
1444
+ panel_cost_usd: 0,
1445
+ rubric_scores: {
1446
+ functional_fidelity: null,
1447
+ code_quality: null,
1448
+ economy: null,
1449
+ code_quality_effective: null,
1450
+ security_score: null,
1451
+ efficiency_score: null,
1452
+ },
1453
+ signals: {
1454
+ grader_kind: 'subagent-panel',
1455
+ grader_version: VERSION,
1456
+ threshold: THRESHOLD,
1457
+ confidence: FALLTHROUGH_CONFIDENCE,
1458
+ panel_outcome: 'unavailable',
1459
+ grader_unavailable_reason: reason,
1460
+ workers: [],
1461
+ worker_count: 0,
1462
+ pass_count: 0,
1463
+ concerns_count: 0,
1464
+ fail_count: 0,
1465
+ error_count: 0,
1466
+ panel_cost_usd: 0,
1467
+ ...meta,
1468
+ },
1469
+ notes_md: notes,
1470
+ issues: [],
1471
+ };
1472
+ }
1473
+
1474
+ // End-to-end v4 grader. Runs the worker panel through the cross-family
1475
+ // grader model and returns the panel-aggregated grade-result.
1476
+ //
1477
+ // workers: defaults to DEFAULT_PANEL (narc + quality). An array of worker
1478
+ // kinds (strings) like ['narc', 'quality']. Phase 2.1/2.2 will extend.
1479
+ // builderModel: the model the builder used. Defaults to ASSUMED_BUILDER_MODEL
1480
+ // (Opus, per CLAUDE.md §2). Drives selectGraderModel — workers pick the
1481
+ // cross-family alternative.
1482
+ async function gradeSubagent({ task, valueSummary, diff, changedFiles, diffStats, workers: workerKinds, builderModel, builderRank, runOpts = {} }) {
1483
+ // #943: bypass / disabled short-circuit — BEFORE resolving the panel or
1484
+ // spawning any worker. When the grader is globally bypassed (the
1485
+ // GRADER_BYPASS_ENABLED killswitch, ADR 0041) or a caller explicitly disables
1486
+ // it (runOpts.bypass), do NOT spawn the panel. Spawning into an outage/bypass
1487
+ // window is exactly what manufactured the "FAIL · 0/10" false-verdict in
1488
+ // session 1659 — the bypass was honored only DOWNSTREAM of the spawn. Return
1489
+ // the distinct 'unavailable' outcome so the result reads as "grader bypassed",
1490
+ // never a quality 0/10.
1491
+ if (runOpts.bypass === true || process.env.GRADER_BYPASS_ENABLED === '1') {
1492
+ const reason = runOpts.bypass === true ? 'bypass_requested' : 'grader_bypass_enabled';
1493
+ return graderUnavailableResult({
1494
+ reason,
1495
+ note: 'Grader bypassed before worker spawn (GRADER_BYPASS_ENABLED / runOpts.bypass) — no panel ran. Land via manual-confirm (/confirm).',
1496
+ meta: { builder_model_assumed: builderModel || ASSUMED_BUILDER_MODEL, bypassed: true },
1497
+ });
1498
+ }
1499
+ const panel = workers.resolvePanel(workerKinds ?? workers.DEFAULT_PANEL);
1500
+ const graderModel = selectGraderModel({ builderModel });
1501
+
1502
+ // Deterministic route-rank pre-pass — its findings feed the Hacker as input;
1503
+ // its pseudo-worker result is appended to the panel below.
1504
+ const routeRank = routeRankPrePass(changedFiles);
1505
+ // Deterministic permission-path pre-pass (SEC #863) — a sub-Metic diff that
1506
+ // touches the rank/permission/ship-grade/deploy machinery hard-fails.
1507
+ const permPath = permissionPathPrePass(changedFiles, builderRank);
1508
+ const deterministicFindings = [
1509
+ ...(routeRank ? routeRank.findings : []),
1510
+ ...(permPath ? permPath.findings : []),
1511
+ ];
1512
+
1513
+ const startedAt = Date.now();
1514
+ const workerPromises = panel.map((worker) => workers.runWorker(worker, {
1515
+ task,
1516
+ valueSummary,
1517
+ diff,
1518
+ changedFiles,
1519
+ diffStats,
1520
+ model: graderModel,
1521
+ runOpts,
1522
+ runSubagentFn: runSubagent,
1523
+ extractAssistantResultFn: extractAssistantResult,
1524
+ deterministicFindings: deterministicFindings.length ? deterministicFindings : null,
1525
+ }));
1526
+ const workerResults = await Promise.all(workerPromises);
1527
+ // Append the deterministic checks as pseudo-workers so the any-fail gate
1528
+ // treats a write-route 'unknown' OR a sub-Metic permission-path edit as a
1529
+ // hard block with no gate-logic change.
1530
+ if (routeRank) workerResults.push(routeRank.workerResult);
1531
+ if (permPath) workerResults.push(permPath.workerResult);
1532
+ const totalMs = Date.now() - startedAt;
1533
+
1534
+ return aggregatePanel(workerResults, {
1535
+ builder_model_assumed: builderModel || ASSUMED_BUILDER_MODEL,
1536
+ grader_model: graderModel,
1537
+ total_ms: totalMs,
1538
+ });
1539
+ }
1540
+
1541
+ // ---------- v3 multi-shot path (kept exported for explicit diagnostics) ----------
1542
+ //
1543
+ // Same shape as the original v3 gradeSubagent; renamed so the public name
1544
+ // (gradeSubagent) points at the v4 panel. Useful for re-running a single
1545
+ // generalist prompt N times for diagnostic comparison against the panel.
1546
+ async function gradeSubagentV3MultiShot({ task, valueSummary, diff, changedFiles, diffStats, shots = DEFAULT_SHOTS, builderModel, runOpts = {} }) {
1547
+ const prompt = buildPrompt({ task, valueSummary, diff, changedFiles, diffStats });
1548
+ const graderModel = selectGraderModel({ builderModel });
1549
+ const N = Math.max(1, Math.trunc(shots));
1550
+
1551
+ const startedAt = Date.now();
1552
+ const shotPromises = [];
1553
+ for (let i = 0; i < N; i++) {
1554
+ shotPromises.push(runOneShot({ prompt, model: graderModel, shotIndex: i, runOpts }));
1555
+ }
1556
+ const shotResults = await Promise.all(shotPromises);
1557
+ const totalMs = Date.now() - startedAt;
1558
+
1559
+ return aggregateShots(shotResults, {
1560
+ builder_model_assumed: builderModel || ASSUMED_BUILDER_MODEL,
1561
+ grader_model: graderModel,
1562
+ total_ms: totalMs,
1563
+ });
1564
+ }
1565
+
1566
+ // =========================================================================
1567
+ // Legacy v1 (deterministic) — kept for scripts/gds/audit-ships.js only.
1568
+ // =========================================================================
1569
+
1570
+ function scoreEstActualBand(actualMinutes, estMinutes) {
1571
+ if (!Number.isFinite(actualMinutes) || actualMinutes <= 0) return 5;
1572
+ if (!Number.isFinite(estMinutes) || estMinutes <= 0) return 6;
1573
+ const ratio = actualMinutes / estMinutes;
1574
+ if (ratio >= 0.5 && ratio <= 2.0) return 10;
1575
+ if (ratio < 0.5) {
1576
+ if (ratio <= 0.1) return 0;
1577
+ return r1(10 * ((ratio - 0.1) / 0.4));
1578
+ }
1579
+ if (ratio > 2.0) {
1580
+ if (ratio >= 10) return 0;
1581
+ return r1(10 * (1 - (ratio - 2.0) / 8.0));
1582
+ }
1583
+ return 5;
1584
+ }
1585
+
1586
+ function scoreValueSummary(valueSummary) {
1587
+ const s = (valueSummary || '').trim();
1588
+ if (s.length === 0) return 0;
1589
+ if (s.length < 40) return 5;
1590
+ if (s.length < 120) return 8;
1591
+ return /\b(add|fix|update|remove|refactor|wire|land|migrate|enable|disable|introduce|deprecate|ship|deploy|prevent|capture|gate|allocate|carry|expose|surface)\b/i.test(s)
1592
+ ? 10
1593
+ : 8;
1594
+ }
1595
+
1596
+ function scoreDiffHygiene(diffStats) {
1597
+ if (!diffStats) return 5;
1598
+ const files = Number(diffStats.files_changed) || 0;
1599
+ const added = Number(diffStats.lines_added) || 0;
1600
+ const removed = Number(diffStats.lines_removed) || 0;
1601
+ if (files === 0 || added + removed === 0) return 0;
1602
+ if (files > 50) return 5;
1603
+ return 10;
1604
+ }
1605
+
1606
+ function scoreScannerSmoke(verification) {
1607
+ if (!verification) return 5;
1608
+ return verification.scanner_clean && verification.smoke_clean ? 10 : 5;
1609
+ }
1610
+
1611
+ function scoreScopingSignal(scopingConfidence) {
1612
+ if (scopingConfidence == null) return 6;
1613
+ const c = Number(scopingConfidence);
1614
+ if (c <= 0) return 2;
1615
+ if (c === 1) return 4;
1616
+ if (c === 2) return 6;
1617
+ if (c === 3) return 7;
1618
+ if (c === 4) return 8;
1619
+ return 10;
1620
+ }
1621
+
1622
+ function scoreLocNetNegative(diffStats) {
1623
+ if (!diffStats) return 5;
1624
+ const added = Number(diffStats.lines_added) || 0;
1625
+ const removed = Number(diffStats.lines_removed) || 0;
1626
+ if (removed > added) return 10;
1627
+ if (removed === added) return 5;
1628
+ return 0;
1629
+ }
1630
+
1631
+ function scoreLearningRow(learningRow) {
1632
+ if (!learningRow) return 0;
1633
+ const body = (learningRow.body_md || '').trim();
1634
+ if (body.length > 100) return 10;
1635
+ if (body.length > 0) return 5;
1636
+ return 0;
1637
+ }
1638
+
1639
+ function scoreArtRubricAvg(value) {
1640
+ if (!Number.isFinite(value)) return null;
1641
+ return clamp(r1(value), 0, 10);
1642
+ }
1643
+
1644
+ function blendWeightsForKind(kind) {
1645
+ const blended = {};
1646
+ for (const [name, def] of Object.entries(V1.weights.common)) {
1647
+ blended[name] = def.weight;
1648
+ }
1649
+ const kindBonus = V1.weights.kind_specific[kind];
1650
+ if (kindBonus) {
1651
+ for (const [name, def] of Object.entries(kindBonus)) {
1652
+ if (name === 'loc_net_negative' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
1653
+ if (name === 'learning_row' && 'diff_hygiene' in blended) delete blended.diff_hygiene;
1654
+ blended[name] = def.weight;
1655
+ }
1656
+ }
1657
+ const total = Object.values(blended).reduce((a, b) => a + b, 0);
1658
+ for (const k of Object.keys(blended)) blended[k] = blended[k] / total;
1659
+ return blended;
1660
+ }
1661
+
1662
+ // Legacy v1 entry. Renamed from `grade()` in V3.R18 (#209). Now ONLY
1663
+ // called from scripts/gds/audit-ships.js (retroactive #204 audit). The
1664
+ // runtime ship path uses gradeSubagent (v3 multi-shot).
1665
+ function gradeDeterministic({ task, verification, actualMinutes, valueSummary, learningRow }) {
1666
+ const kind = task?.kind || 'unclassified';
1667
+ const weights = blendWeightsForKind(kind);
1668
+ const rubricScores = {};
1669
+ const signals = {
1670
+ kind,
1671
+ est_minutes: task?.est_minutes ?? null,
1672
+ actual_minutes: actualMinutes ?? null,
1673
+ scoping_confidence: task?.scoping_confidence ?? null,
1674
+ scanner_clean: verification?.scanner_clean ?? null,
1675
+ smoke_clean: verification?.smoke_clean ?? null,
1676
+ diff_stats: verification?.diff_stats ?? null,
1677
+ art_rubric_avg: verification?.art_rubric_avg ?? null,
1678
+ value_summary_chars: (valueSummary || '').trim().length,
1679
+ };
1680
+
1681
+ if ('est_actual_band' in weights) {
1682
+ rubricScores.est_actual_band = scoreEstActualBand(actualMinutes, task?.est_minutes);
1683
+ }
1684
+ if ('value_summary' in weights) {
1685
+ rubricScores.value_summary = scoreValueSummary(valueSummary);
1686
+ }
1687
+ if ('diff_hygiene' in weights) {
1688
+ rubricScores.diff_hygiene = scoreDiffHygiene(verification?.diff_stats);
1689
+ }
1690
+ if ('scanner_smoke' in weights) {
1691
+ rubricScores.scanner_smoke = scoreScannerSmoke(verification);
1692
+ }
1693
+ if ('scoping_signal' in weights) {
1694
+ rubricScores.scoping_signal = scoreScopingSignal(task?.scoping_confidence);
1695
+ }
1696
+ if ('loc_net_negative' in weights) {
1697
+ rubricScores.loc_net_negative = scoreLocNetNegative(verification?.diff_stats);
1698
+ }
1699
+ if ('learning_row' in weights) {
1700
+ rubricScores.learning_row = scoreLearningRow(learningRow);
1701
+ }
1702
+ if ('art_rubric_avg' in weights) {
1703
+ const s = scoreArtRubricAvg(verification?.art_rubric_avg);
1704
+ if (s == null) {
1705
+ const w = weights.art_rubric_avg;
1706
+ delete weights.art_rubric_avg;
1707
+ const remaining = Object.values(weights).reduce((a, b) => a + b, 0);
1708
+ if (remaining > 0) {
1709
+ for (const k of Object.keys(weights)) weights[k] = weights[k] / remaining;
1710
+ }
1711
+ } else {
1712
+ rubricScores.art_rubric_avg = s;
1713
+ }
1714
+ }
1715
+
1716
+ let score = 0;
1717
+ for (const [name, w] of Object.entries(weights)) {
1718
+ if (rubricScores[name] != null) score += rubricScores[name] * w;
1719
+ }
1720
+ score = r1(clamp(score, 0, 10));
1721
+
1722
+ const threshold = V1.threshold;
1723
+ const passed = score >= threshold;
1724
+
1725
+ const lines = [];
1726
+ lines.push(`Auto-grader ${V1.version} (deprecated) — task_kind=${kind}.`);
1727
+ lines.push(`Score ${score}/10 vs threshold ${threshold} → ${passed ? 'PASS' : 'FAIL'}.`);
1728
+ lines.push('');
1729
+ lines.push('Per-criterion:');
1730
+ for (const [name, w] of Object.entries(weights)) {
1731
+ const s = rubricScores[name];
1732
+ if (s == null) continue;
1733
+ const contrib = r1(s * w);
1734
+ lines.push(` - ${name.padEnd(20)} ${String(s).padStart(4)}/10 × ${w.toFixed(2)} = ${contrib}`);
1735
+ }
1736
+
1737
+ return {
1738
+ score,
1739
+ passed,
1740
+ threshold,
1741
+ grader_kind: 'auto',
1742
+ grader_version: V1.version,
1743
+ rubric_scores: rubricScores,
1744
+ signals,
1745
+ notes_md: lines.join('\n'),
1746
+ };
1747
+ }
1748
+
1749
+ module.exports = {
1750
+ // v4 surface (active panel-based gate)
1751
+ aggregatePanel,
1752
+ gradeSubagent, // panel by default
1753
+ graderUnavailableResult, // #943: distinct grader-unavailable / bypassed result (no spawn)
1754
+ isTrivialDiff, // #513: copy/docs fast-path classifier
1755
+ WORKERS: workers, // registry re-export
1756
+ routeRankPrePass, // deterministic route-rank pre-pass (#475)
1757
+ permissionPathPrePass, // deterministic sub-Metic permission-path gate (SEC #863)
1758
+ serverSideGuards, // SR-9 / SR-18: server-authoritative grade hardening (downgrade-only)
1759
+ changeMagnitude, // SR-9 helper: file/line magnitude from committed_files + diff_stats
1760
+ FALLTHROUGH_CONFIDENCE,
1761
+ // v3 surface (kept exported for diagnostics + tests)
1762
+ buildPrompt,
1763
+ selectGraderModel,
1764
+ validateAndScore,
1765
+ aggregateShots,
1766
+ runSubagent,
1767
+ runSubagentCached, // System 5 (task 1359): opt-in content-addressed cached path
1768
+ runOneShot,
1769
+ gradeSubagentV3MultiShot, // explicit v3 multi-shot diagnostic path
1770
+ extractJson,
1771
+ extractAssistantResult,
1772
+ extractCostUsd, // #486: parse total_cost_usd from the envelope
1773
+ THRESHOLD,
1774
+ VERSION,
1775
+ DEFAULT_GRADER_MODEL,
1776
+ DEFAULT_SHOTS,
1777
+ ASSUMED_BUILDER_MODEL,
1778
+ // legacy v1 (audit-ships.js only)
1779
+ gradeDeterministic,
1780
+ scoreEstActualBand,
1781
+ scoreValueSummary,
1782
+ scoreDiffHygiene,
1783
+ scoreScannerSmoke,
1784
+ scoreScopingSignal,
1785
+ scoreLocNetNegative,
1786
+ scoreLearningRow,
1787
+ scoreArtRubricAvg,
1788
+ blendWeightsForKind,
1789
+ RUBRIC,
1790
+ };