@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,1960 @@
1
+ // tests/grader.mjs
2
+ //
3
+ // Grader tests. Covers both:
4
+ // - the v1 deterministic process-audit grader (kept for audit-ships.js)
5
+ // - the v2 subagent quality grader (the actual credits gate from V3.R18 #209)
6
+ //
7
+ // The v2 subagent itself is a real-network call, so the tests stop at the
8
+ // validator + prompt-builder boundary; the network-touching end-to-end check
9
+ // lives in scripts/gds/grade-smoke.js and is run manually.
10
+ //
11
+ // Run: node tests/grader.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const grader = require('../modules/grading/grader.js');
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+ function test(name, fn) {
22
+ try {
23
+ const maybePromise = fn();
24
+ if (maybePromise && typeof maybePromise.then === 'function') {
25
+ throw new Error('async tests not supported in this runner');
26
+ }
27
+ console.log(` ok ${name}`);
28
+ passed++;
29
+ } catch (err) {
30
+ console.error(` FAIL ${name}`);
31
+ console.error(' ', err.message);
32
+ failed++;
33
+ }
34
+ }
35
+
36
+ // Async variant (#465) — the sync `test()` above rejects promises, but a few
37
+ // checks (runWorker's retry control flow) are genuinely async. Call with
38
+ // top-level `await testAsync(...)` so it resolves before the summary runs.
39
+ async function testAsync(name, fn) {
40
+ try {
41
+ await fn();
42
+ console.log(` ok ${name}`);
43
+ passed++;
44
+ } catch (err) {
45
+ console.error(` FAIL ${name}`);
46
+ console.error(' ', err.message);
47
+ failed++;
48
+ }
49
+ }
50
+
51
+ // ========================================================================
52
+ // v1 — deterministic grader (legacy, used only by audit-ships.js)
53
+ // ========================================================================
54
+
55
+ test('v1 scoreEstActualBand: ratio inside [0.5, 2.0] → 10', () => {
56
+ assert.equal(grader.scoreEstActualBand(60, 60), 10);
57
+ assert.equal(grader.scoreEstActualBand(30, 60), 10);
58
+ assert.equal(grader.scoreEstActualBand(120, 60), 10);
59
+ });
60
+
61
+ test('v1 scoreEstActualBand: ratio outside band drops linearly', () => {
62
+ assert.equal(grader.scoreEstActualBand(18, 60), 5);
63
+ assert.equal(grader.scoreEstActualBand(360, 60), 5);
64
+ });
65
+
66
+ test('v1 scoreEstActualBand: extreme ratios → 0', () => {
67
+ assert.equal(grader.scoreEstActualBand(6, 60), 0);
68
+ assert.equal(grader.scoreEstActualBand(600, 60), 0);
69
+ });
70
+
71
+ test('v1 scoreEstActualBand: missing est → 6, missing actual → 5', () => {
72
+ assert.equal(grader.scoreEstActualBand(60, null), 6);
73
+ assert.equal(grader.scoreEstActualBand(null, 60), 5);
74
+ });
75
+
76
+ test('v1 scoreValueSummary: maps length + verb', () => {
77
+ assert.equal(grader.scoreValueSummary(''), 0);
78
+ assert.equal(grader.scoreValueSummary(null), 0);
79
+ assert.equal(grader.scoreValueSummary('short note'), 5);
80
+ assert.equal(grader.scoreValueSummary('a moderately worded summary that lands above forty characters'), 8);
81
+ const s = 'wire the auto-grader into the completed→confirmed transition so every task gets a quality mark before its credits are paid out';
82
+ assert.ok(s.length > 120);
83
+ assert.equal(grader.scoreValueSummary(s), 10);
84
+ });
85
+
86
+ test('v1 scoreDiffHygiene: phantom ship → 0, normal → 10, too-broad → 5', () => {
87
+ assert.equal(grader.scoreDiffHygiene({ files_changed: 0, lines_added: 0, lines_removed: 0 }), 0);
88
+ assert.equal(grader.scoreDiffHygiene({ files_changed: 5, lines_added: 200, lines_removed: 30 }), 10);
89
+ assert.equal(grader.scoreDiffHygiene({ files_changed: 80, lines_added: 1000, lines_removed: 200 }), 5);
90
+ });
91
+
92
+ test('v1 scoreScopingSignal: maps each level', () => {
93
+ assert.equal(grader.scoreScopingSignal(0), 2);
94
+ assert.equal(grader.scoreScopingSignal(1), 4);
95
+ assert.equal(grader.scoreScopingSignal(2), 6);
96
+ assert.equal(grader.scoreScopingSignal(3), 7);
97
+ assert.equal(grader.scoreScopingSignal(4), 8);
98
+ assert.equal(grader.scoreScopingSignal(5), 10);
99
+ assert.equal(grader.scoreScopingSignal(null), 6);
100
+ });
101
+
102
+ test('v1 scoreLocNetNegative', () => {
103
+ assert.equal(grader.scoreLocNetNegative({ lines_added: 20, lines_removed: 100 }), 10);
104
+ assert.equal(grader.scoreLocNetNegative({ lines_added: 100, lines_removed: 20 }), 0);
105
+ });
106
+
107
+ test('v1 scoreLearningRow', () => {
108
+ assert.equal(grader.scoreLearningRow({ body_md: 'x'.repeat(200) }), 10);
109
+ assert.equal(grader.scoreLearningRow(null), 0);
110
+ });
111
+
112
+ test('v1 scoreArtRubricAvg clamps + nulls', () => {
113
+ assert.equal(grader.scoreArtRubricAvg(8.7), 8.7);
114
+ assert.equal(grader.scoreArtRubricAvg(15), 10);
115
+ assert.equal(grader.scoreArtRubricAvg(-1), 0);
116
+ assert.equal(grader.scoreArtRubricAvg(NaN), null);
117
+ });
118
+
119
+ test('v1 blendWeightsForKind: feature renorms to 1.0', () => {
120
+ const w = grader.blendWeightsForKind('feature');
121
+ const total = Object.values(w).reduce((a, b) => a + b, 0);
122
+ assert.ok(Math.abs(total - 1.0) < 1e-6, `total=${total}, expected 1.0`);
123
+ });
124
+
125
+ test('v1 blendWeightsForKind: cleanup replaces diff_hygiene', () => {
126
+ const w = grader.blendWeightsForKind('cleanup');
127
+ assert.ok('loc_net_negative' in w);
128
+ assert.ok(!('diff_hygiene' in w));
129
+ });
130
+
131
+ test('v1 gradeDeterministic: well-shipped feature passes', () => {
132
+ const result = grader.gradeDeterministic({
133
+ task: { id: 99, kind: 'feature', est_minutes: 240, actual_minutes: 200, scoping_confidence: 4 },
134
+ verification: {
135
+ scanner_clean: true, smoke_clean: true,
136
+ diff_stats: { files_changed: 10, lines_added: 400, lines_removed: 50 },
137
+ },
138
+ actualMinutes: 200,
139
+ valueSummary: 'wire the auto-grader into ship.js so every task gets a quality mark before credits are paid; gates the completed→confirmed transition',
140
+ });
141
+ assert.ok(result.score >= 9.0, `score=${result.score}`);
142
+ assert.equal(result.passed, true);
143
+ assert.equal(result.grader_kind, 'auto');
144
+ assert.equal(result.grader_version, grader.RUBRIC.v1.version);
145
+ assert.equal(result.threshold, grader.RUBRIC.v1.threshold);
146
+ });
147
+
148
+ test('v1 gradeDeterministic: phantom ship scores low', () => {
149
+ const result = grader.gradeDeterministic({
150
+ task: { id: 100, kind: 'feature', est_minutes: 240, actual_minutes: 30, scoping_confidence: 2 },
151
+ verification: {
152
+ scanner_clean: true, smoke_clean: true,
153
+ diff_stats: { files_changed: 0, lines_added: 0, lines_removed: 0 },
154
+ },
155
+ actualMinutes: 30,
156
+ valueSummary: '',
157
+ });
158
+ assert.ok(result.score < 5.0, `score=${result.score}`);
159
+ assert.equal(result.passed, false);
160
+ });
161
+
162
+ test('v1 gradeDeterministic: notes_md says deprecated', () => {
163
+ const result = grader.gradeDeterministic({
164
+ task: { id: 106, kind: 'feature', est_minutes: 60, actual_minutes: 60, scoping_confidence: 4 },
165
+ verification: { scanner_clean: true, smoke_clean: true, diff_stats: { files_changed: 3, lines_added: 50, lines_removed: 5 } },
166
+ actualMinutes: 60,
167
+ valueSummary: 'a reasonable shipped change',
168
+ });
169
+ assert.match(result.notes_md, /v1.*deprecated/i);
170
+ });
171
+
172
+ // ========================================================================
173
+ // v2 — subagent grader (the credits gate as of V3.R18 #209)
174
+ // ========================================================================
175
+
176
+ test('v4 exports the expected surface (current panel-based gate)', () => {
177
+ const required = [
178
+ // v4 entry points
179
+ 'gradeSubagent', 'aggregatePanel', 'WORKERS', 'FALLTHROUGH_CONFIDENCE',
180
+ // v3 surface kept exported for diagnostics + tests
181
+ 'buildPrompt', 'selectGraderModel', 'validateAndScore', 'aggregateShots',
182
+ 'runSubagent', 'runOneShot', 'gradeSubagentV3MultiShot',
183
+ // constants
184
+ 'THRESHOLD', 'VERSION', 'DEFAULT_GRADER_MODEL', 'DEFAULT_SHOTS', 'ASSUMED_BUILDER_MODEL',
185
+ // utilities
186
+ 'extractJson', 'extractAssistantResult', 'extractCostUsd', 'RUBRIC',
187
+ ];
188
+ for (const name of required) {
189
+ assert.ok(name in grader, `missing export: ${name}`);
190
+ }
191
+ assert.ok(typeof grader.THRESHOLD === 'number');
192
+ assert.ok(grader.THRESHOLD >= 5.0 && grader.THRESHOLD <= 10.0);
193
+ assert.ok(grader.VERSION.startsWith('v4'), `expected v4.*, got ${grader.VERSION}`);
194
+ assert.equal(grader.DEFAULT_SHOTS, 3); // kept for v3 multi-shot diagnostic path
195
+ assert.equal(grader.DEFAULT_GRADER_MODEL, 'claude-sonnet-4-6');
196
+ assert.equal(grader.ASSUMED_BUILDER_MODEL, 'claude-opus-4-7');
197
+ assert.equal(grader.FALLTHROUGH_CONFIDENCE, 0.3);
198
+ // Worker registry sanity — Phase 2.2 added efficiency as the 4th worker
199
+ // (after Phase 2.1's hacker).
200
+ assert.deepEqual(grader.WORKERS.DEFAULT_PANEL, ['narc', 'quality', 'hacker', 'efficiency']);
201
+ assert.ok(grader.WORKERS.REGISTRY.narc);
202
+ assert.ok(grader.WORKERS.REGISTRY.quality);
203
+ assert.ok(grader.WORKERS.REGISTRY.hacker);
204
+ assert.ok(grader.WORKERS.REGISTRY.efficiency);
205
+ });
206
+
207
+ test('#486 extractCostUsd: pulls total_cost_usd from the claude -p envelope', () => {
208
+ assert.equal(grader.extractCostUsd(JSON.stringify({ type: 'result', total_cost_usd: 0.1981, result: 'x' })), 0.1981);
209
+ assert.equal(grader.extractCostUsd(JSON.stringify({ total_cost_usd: 0 })), 0); // free turn is a real 0
210
+ assert.equal(grader.extractCostUsd(JSON.stringify({ result: 'no cost field' })), null);
211
+ assert.equal(grader.extractCostUsd(JSON.stringify({ total_cost_usd: '0.5' })), null); // string, not a number
212
+ assert.equal(grader.extractCostUsd(JSON.stringify({ total_cost_usd: -1 })), null); // negative rejected
213
+ assert.equal(grader.extractCostUsd('not json at all'), null); // partial/non-JSON stdout
214
+ });
215
+
216
+ test('#486 aggregatePanel sums worker cost_usd into panel_cost_usd (null → 0)', () => {
217
+ const mkWorker = (kind, cost) => ({
218
+ kind, name: kind, specialty: '', verdict: 'pass',
219
+ scores: { functional_fidelity: 9, code_quality: 9 },
220
+ issues: [], notes_md: '', confidence: 0.9, model: 'm', run_ms: 1,
221
+ cost_usd: cost, retried: false, error: null,
222
+ });
223
+ const res = grader.aggregatePanel([
224
+ mkWorker('narc', 0.2), mkWorker('quality', 0.3),
225
+ mkWorker('hacker', null), mkWorker('efficiency', 0.05), // null contributes 0
226
+ ]);
227
+ assert.ok(Math.abs(res.panel_cost_usd - 0.55) < 1e-9, `expected 0.55, got ${res.panel_cost_usd}`);
228
+ assert.ok(Math.abs(res.signals.panel_cost_usd - 0.55) < 1e-9, 'mirrored into signals');
229
+ });
230
+
231
+ test('#486 aggregateShots sums shot cost_usd into panel_cost_usd (trivial-diff path)', () => {
232
+ const mkShot = (cost) => ({
233
+ passed: true,
234
+ rubric_scores: { functional_fidelity: 9, code_quality: 9 },
235
+ signals: { confidence: 0.9, model: 'm', run_ms: 1, shot_index: 0 },
236
+ notes_md: '', issues: [], cost_usd: cost,
237
+ });
238
+ const res = grader.aggregateShots([mkShot(0.4), mkShot(0.1), mkShot(null)]); // null → 0
239
+ assert.ok(Math.abs(res.panel_cost_usd - 0.5) < 1e-9, `expected 0.5, got ${res.panel_cost_usd}`);
240
+ });
241
+
242
+ test('v2 buildPrompt: includes task title, description, value_summary, file list, diff', () => {
243
+ const prompt = grader.buildPrompt({
244
+ task: {
245
+ id: 42,
246
+ title: 'V3.R18 — Replace v1 grader',
247
+ description: 'Replace the v1 deterministic grader with a subagent.',
248
+ kind: 'feature',
249
+ discipline: 'engineering',
250
+ version_id: 'GDS-V3',
251
+ },
252
+ valueSummary: 'Subagent grader now gates credits.',
253
+ diff: 'diff --git a/foo b/foo\n+added\n',
254
+ changedFiles: ['src/bongos/grader.js', 'tests/grader.mjs'],
255
+ });
256
+ assert.match(prompt, /V3\.R18 — Replace v1 grader/);
257
+ assert.match(prompt, /Replace the v1 deterministic grader with a subagent/);
258
+ assert.match(prompt, /Subagent grader now gates credits/);
259
+ assert.match(prompt, /src\/bongos\/grader\.js/);
260
+ assert.match(prompt, /tests\/grader\.mjs/);
261
+ assert.match(prompt, /\+added/);
262
+ assert.match(prompt, /Output ONLY the JSON object/);
263
+ });
264
+
265
+ test('v2 buildPrompt: empty changedFiles is flagged as suspicious', () => {
266
+ const prompt = grader.buildPrompt({
267
+ task: { id: 1, title: 't', description: 'd' },
268
+ valueSummary: 's',
269
+ diff: '',
270
+ changedFiles: [],
271
+ });
272
+ assert.match(prompt, /no files changed.*suspicious/i);
273
+ });
274
+
275
+ test('v2 extractJson: pulls JSON from ```json fenced output', () => {
276
+ const wrapped = '```json\n{"functional_fidelity": 8, "code_quality": 7, "notes_md": "ok", "confidence": 0.8, "issues": []}\n```';
277
+ const parsed = grader.extractJson(wrapped);
278
+ assert.ok(parsed);
279
+ assert.equal(parsed.functional_fidelity, 8);
280
+ });
281
+
282
+ test('v2 extractJson: pulls JSON when subagent adds leading prose', () => {
283
+ const wrapped = 'Here is the grade:\n{"functional_fidelity": 9, "code_quality": 8, "notes_md": "good", "confidence": 0.9, "issues": []}\nThanks.';
284
+ const parsed = grader.extractJson(wrapped);
285
+ assert.ok(parsed);
286
+ assert.equal(parsed.functional_fidelity, 9);
287
+ });
288
+
289
+ test('v2 extractJson: malformed → null', () => {
290
+ assert.equal(grader.extractJson('totally not json'), null);
291
+ assert.equal(grader.extractJson(''), null);
292
+ assert.equal(grader.extractJson('{not valid'), null);
293
+ });
294
+
295
+ // #79 (#380): robustness against the cases the old first-'{'-to-last-'}' slice
296
+ // mangled into a spurious parse_error.
297
+ test('#79 extractJson: trailing prose after the object (with a stray brace) still parses', () => {
298
+ const r = grader.extractJson('{"verdict":"pass","code_quality":7}\n\nThat is my assessment. }');
299
+ assert.equal(r.verdict, 'pass');
300
+ assert.equal(r.code_quality, 7);
301
+ });
302
+
303
+ test('#79 extractJson: leading prose containing braces, then a fenced grade block', () => {
304
+ const r = grader.extractJson('Review notes (see {context}):\n```json\n{"functional_fidelity":9}\n```');
305
+ assert.equal(r.functional_fidelity, 9);
306
+ });
307
+
308
+ test('#79 extractJson: picks the grade-shaped object when an incidental object precedes it', () => {
309
+ const r = grader.extractJson('prefix {"unrelated":1} then the real one {"functional_fidelity":8,"code_quality":6}');
310
+ assert.equal(r.functional_fidelity, 8);
311
+ assert.equal(r.code_quality, 6);
312
+ });
313
+
314
+ test('#79 extractJson: an incidental {"verdict":...} in prose does NOT win over the real score object', () => {
315
+ const r = grader.extractJson('My verdict object {"verdict":"maybe"} — but the grade is {"functional_fidelity":7,"code_quality":8}');
316
+ assert.equal(r.functional_fidelity, 7);
317
+ assert.equal(r.code_quality, 8);
318
+ });
319
+
320
+ test('#79/#493 worker extractJson delegates to the shared robust extractor (panel path)', () => {
321
+ // The v4 panel uses each worker's extractJson — they must be the hardened
322
+ // shared one, not the old first-{-to-last-} copy. Trailing prose with a stray
323
+ // brace would have broken the copy.
324
+ for (const kind of ['narc', 'quality', 'hacker', 'efficiency']) {
325
+ const r = grader.WORKERS[kind].extractJson('{"functional_fidelity":8,"code_quality":7}\n\nThat is all. }');
326
+ assert.equal(r.functional_fidelity, 8, `${kind} should parse robustly`);
327
+ assert.equal(r.code_quality, 7);
328
+ }
329
+ });
330
+
331
+ test('#79 extractJson: clean JSON-only response is returned as-is', () => {
332
+ const r = grader.extractJson('{"functional_fidelity":10,"confidence":0.9}');
333
+ assert.equal(r.functional_fidelity, 10);
334
+ assert.equal(r.confidence, 0.9);
335
+ });
336
+
337
+ test('v2 validateAndScore: both axes above threshold → PASS', () => {
338
+ const r = grader.validateAndScore({
339
+ functional_fidelity: 8,
340
+ code_quality: 7,
341
+ notes_md: 'looks fine',
342
+ confidence: 0.85,
343
+ issues: [],
344
+ });
345
+ assert.equal(r.passed, true);
346
+ assert.equal(r.grader_kind, 'subagent');
347
+ assert.equal(r.grader_version, grader.VERSION);
348
+ assert.equal(r.rubric_scores.functional_fidelity, 8);
349
+ assert.equal(r.rubric_scores.code_quality, 7);
350
+ assert.equal(r.score, 7.5); // blended (8+7)/2
351
+ });
352
+
353
+ test('v2 validateAndScore: functional_fidelity below threshold → FAIL', () => {
354
+ const r = grader.validateAndScore({
355
+ functional_fidelity: 4,
356
+ code_quality: 9,
357
+ notes_md: 'did not implement description',
358
+ confidence: 0.9,
359
+ issues: [{ severity: 'blocker', file: 'foo.js', summary: 'missing impl' }],
360
+ });
361
+ assert.equal(r.passed, false);
362
+ assert.match(r.notes_md, /functional_fidelity below/);
363
+ assert.equal(r.issues.length, 1);
364
+ });
365
+
366
+ test('v2 validateAndScore: code_quality below threshold → FAIL', () => {
367
+ const r = grader.validateAndScore({
368
+ functional_fidelity: 9,
369
+ code_quality: 4,
370
+ notes_md: 'tons of dead code',
371
+ confidence: 0.9,
372
+ issues: [{ severity: 'major', file: 'bar.js', summary: 'unused fn' }],
373
+ });
374
+ assert.equal(r.passed, false);
375
+ assert.match(r.notes_md, /code_quality below/);
376
+ });
377
+
378
+ test('v2 validateAndScore: low confidence → FAIL even with high scores', () => {
379
+ const r = grader.validateAndScore({
380
+ functional_fidelity: 9,
381
+ code_quality: 9,
382
+ notes_md: 'can\'t really tell, description was vague',
383
+ confidence: 0.2,
384
+ issues: [],
385
+ });
386
+ assert.equal(r.passed, false);
387
+ assert.match(r.notes_md, /low confidence/i);
388
+ });
389
+
390
+ // #489 / finding #56: null confidence must NOT silently pass the gate.
391
+ test('#489 validateAndScore: missing confidence → FAIL (not a silent gate-walk)', () => {
392
+ // A subagent that omits confidence entirely should fail, not slip through.
393
+ // High ff/cq do not save it — the gate requires confidence to be present.
394
+ const r = grader.validateAndScore({
395
+ functional_fidelity: 9,
396
+ code_quality: 9,
397
+ notes_md: 'great code but no confidence field',
398
+ issues: [],
399
+ // confidence: intentionally absent
400
+ });
401
+ assert.equal(r.passed, false);
402
+ assert.match(r.notes_md, /confidence absent/i);
403
+ assert.equal(r.signals.confidence, null);
404
+ });
405
+
406
+ test('#489 validateAndScore: explicit null confidence → FAIL (same as absent)', () => {
407
+ const r = grader.validateAndScore({
408
+ functional_fidelity: 9,
409
+ code_quality: 9,
410
+ notes_md: 'explicit null',
411
+ confidence: null,
412
+ issues: [],
413
+ });
414
+ assert.equal(r.passed, false);
415
+ assert.match(r.notes_md, /confidence absent/i);
416
+ });
417
+
418
+ // #489 aggregateShots safety: a unanimousPASS result can never produce a null
419
+ // confidenceMedian, because any shot that passes must now have non-null confidence.
420
+ test('#489 aggregateShots unanimousPASS always has non-null median confidence', () => {
421
+ // Construct two shots that pass with real confidence values.
422
+ const mkPassShot = (conf) => ({
423
+ passed: true,
424
+ rubric_scores: { functional_fidelity: 9, code_quality: 9 },
425
+ signals: { confidence: conf, model: 'm', run_ms: 1, shot_index: 0 },
426
+ notes_md: '', issues: [], cost_usd: 0,
427
+ });
428
+ const res = grader.aggregateShots([mkPassShot(0.8), mkPassShot(0.9)]);
429
+ assert.equal(res.passed, true);
430
+ assert.ok(res.signals.confidence !== null, 'unanimousPASS must have non-null confidenceMedian');
431
+ assert.ok(typeof res.signals.confidence === 'number', 'confidenceMedian must be a number');
432
+ });
433
+
434
+ test('v2 validateAndScore: malformed input → passed=false, parse_error in signals', () => {
435
+ const r = grader.validateAndScore('not even json');
436
+ assert.equal(r.passed, false);
437
+ assert.equal(r.signals.parse_error, true);
438
+ assert.equal(r.score, 0);
439
+ });
440
+
441
+ test('v2 validateAndScore: out-of-range scores → passed=false, schema_error', () => {
442
+ const r = grader.validateAndScore({
443
+ functional_fidelity: 15,
444
+ code_quality: 8,
445
+ notes_md: 'oops',
446
+ confidence: 0.9,
447
+ issues: [],
448
+ });
449
+ assert.equal(r.passed, false);
450
+ assert.equal(r.signals.schema_error, true);
451
+ });
452
+
453
+ test('v2 validateAndScore: accepts JSON-string input', () => {
454
+ const r = grader.validateAndScore(JSON.stringify({
455
+ functional_fidelity: 8,
456
+ code_quality: 8,
457
+ notes_md: 'great',
458
+ confidence: 0.9,
459
+ issues: [],
460
+ }));
461
+ assert.equal(r.passed, true);
462
+ });
463
+
464
+ test('v2 validateAndScore: issues array preserved + count in signals', () => {
465
+ const issues = [
466
+ { severity: 'minor', file: 'a.js', summary: 'nit' },
467
+ { severity: 'blocker', file: 'b.js', summary: 'broken' },
468
+ ];
469
+ const r = grader.validateAndScore({
470
+ functional_fidelity: 8,
471
+ code_quality: 8,
472
+ notes_md: 'see issues',
473
+ confidence: 0.9,
474
+ issues,
475
+ });
476
+ assert.equal(r.issues.length, 2);
477
+ assert.equal(r.signals.issue_count, 2);
478
+ assert.equal(r.issues[0].severity, 'minor');
479
+ });
480
+
481
+ test('v2 validateAndScore: just-at-threshold passes', () => {
482
+ const r = grader.validateAndScore({
483
+ functional_fidelity: 6,
484
+ code_quality: 6,
485
+ notes_md: 'borderline',
486
+ confidence: 0.7,
487
+ issues: [],
488
+ });
489
+ assert.equal(r.passed, true);
490
+ assert.equal(r.score, 6);
491
+ });
492
+
493
+ test('v2 validateAndScore: just-below-threshold fails', () => {
494
+ const r = grader.validateAndScore({
495
+ functional_fidelity: 5.9,
496
+ code_quality: 9,
497
+ notes_md: 'near miss',
498
+ confidence: 0.9,
499
+ issues: [],
500
+ });
501
+ assert.equal(r.passed, false);
502
+ });
503
+
504
+ test('v2 validateAndScore: rubric_scores records the two gated axes plus advisory fields', () => {
505
+ // Phase 2.5 (#423) added security_score + efficiency_score to the
506
+ // rubric_scores shape (both null when not provided — advisory, additive).
507
+ const r = grader.validateAndScore({
508
+ functional_fidelity: 8,
509
+ code_quality: 7,
510
+ economy: 9,
511
+ notes_md: '',
512
+ confidence: 0.8,
513
+ issues: [],
514
+ });
515
+ assert.deepEqual(
516
+ Object.keys(r.rubric_scores).sort(),
517
+ ['code_quality', 'code_quality_effective', 'economy', 'efficiency_score', 'functional_fidelity', 'security_score']
518
+ );
519
+ assert.equal(r.rubric_scores.functional_fidelity, 8);
520
+ assert.equal(r.rubric_scores.code_quality, 7);
521
+ assert.equal(r.rubric_scores.economy, 9);
522
+ assert.equal(r.rubric_scores.security_score, null);
523
+ assert.equal(r.rubric_scores.efficiency_score, null);
524
+ });
525
+
526
+ // ---------- economy axis (V3.R92 #353) — ADVISORY ----------
527
+
528
+ test('economy: every grade records an economy sub-score', () => {
529
+ const r = grader.validateAndScore({
530
+ functional_fidelity: 8, code_quality: 8, economy: 5,
531
+ notes_md: 'ok', confidence: 0.9, issues: [],
532
+ });
533
+ assert.equal(r.rubric_scores.economy, 5);
534
+ });
535
+
536
+ test('economy: missing economy field → recorded as null, no penalty (back-compat)', () => {
537
+ // An older subagent output without an economy field must grade exactly as before.
538
+ const r = grader.validateAndScore({
539
+ functional_fidelity: 8, code_quality: 7,
540
+ notes_md: 'legacy output', confidence: 0.9, issues: [],
541
+ });
542
+ assert.equal(r.rubric_scores.economy, null);
543
+ // No penalty: effective quality equals raw code_quality.
544
+ assert.equal(r.rubric_scores.code_quality_effective, 7);
545
+ assert.equal(r.passed, true); // unchanged gate behaviour
546
+ });
547
+
548
+ test('economy: out-of-range economy → recorded as null, no penalty', () => {
549
+ const r = grader.validateAndScore({
550
+ functional_fidelity: 8, code_quality: 7, economy: 42,
551
+ notes_md: 'bad economy', confidence: 0.9, issues: [],
552
+ });
553
+ assert.equal(r.rubric_scores.economy, null);
554
+ assert.equal(r.rubric_scores.code_quality_effective, 7);
555
+ });
556
+
557
+ test('economy: high economy (>= threshold) applies no quality nudge', () => {
558
+ const r = grader.validateAndScore({
559
+ functional_fidelity: 8, code_quality: 8, economy: 9,
560
+ notes_md: 'tight diff', confidence: 0.9, issues: [],
561
+ });
562
+ assert.equal(r.rubric_scores.code_quality_effective, 8); // no penalty
563
+ assert.equal(r.passed, true);
564
+ });
565
+
566
+ test('economy: low economy blends DOWN into code_quality_effective (advisory)', () => {
567
+ // economy 2, threshold 6 → penalty = (6-2)/2 = 2. cqEffective = 7-2 = 5.
568
+ const r = grader.validateAndScore({
569
+ functional_fidelity: 8, code_quality: 7, economy: 2,
570
+ notes_md: 'over-written', confidence: 0.9, issues: [],
571
+ });
572
+ assert.equal(r.rubric_scores.economy, 2);
573
+ assert.equal(r.rubric_scores.code_quality_effective, 5);
574
+ // ...but the effective value is the *only* thing that moves. Raw cq unchanged.
575
+ assert.equal(r.rubric_scores.code_quality, 7);
576
+ });
577
+
578
+ test('economy: a terrible economy does NOT hard-block when both gated axes pass', () => {
579
+ // This is the load-bearing advisory-first guarantee: economy alone can never
580
+ // fail a ship today. Both gated axes are >= threshold, economy is rock-bottom.
581
+ const r = grader.validateAndScore({
582
+ functional_fidelity: 8, code_quality: 8, economy: 0,
583
+ notes_md: 'wildly over-abstracted but functionally correct',
584
+ confidence: 0.9, issues: [],
585
+ });
586
+ assert.equal(r.rubric_scores.economy, 0);
587
+ assert.equal(r.passed, true, 'economy must not independently hard-block (advisory first)');
588
+ });
589
+
590
+ test('economy: a great economy cannot rescue a failing gated axis', () => {
591
+ // economy never lifts quality; a sub-threshold code_quality still fails.
592
+ const r = grader.validateAndScore({
593
+ functional_fidelity: 8, code_quality: 4, economy: 10,
594
+ notes_md: 'tiny but broken', confidence: 0.9, issues: [],
595
+ });
596
+ assert.equal(r.passed, false);
597
+ assert.match(r.notes_md, /code_quality below/);
598
+ });
599
+
600
+ test('economy: gate still reads raw axes — identical to pre-economy behaviour', () => {
601
+ // Same inputs as the canonical "both axes above threshold → PASS" test, now
602
+ // with an economy field present. Pass/blended/score must be unchanged.
603
+ const r = grader.validateAndScore({
604
+ functional_fidelity: 8, code_quality: 7, economy: 6,
605
+ notes_md: 'looks fine', confidence: 0.85, issues: [],
606
+ });
607
+ assert.equal(r.passed, true);
608
+ assert.equal(r.score, 7.5); // blended (8+7)/2 — economy does NOT enter the score
609
+ });
610
+
611
+ test('economy: buildPrompt asks for the economy axis and renders diff stats + est_minutes', () => {
612
+ const prompt = grader.buildPrompt({
613
+ task: { id: 7, title: 't', description: 'd', kind: 'feature', est_minutes: 90 },
614
+ valueSummary: 's',
615
+ diff: 'diff --git a/x b/x\n+1\n',
616
+ changedFiles: ['x'],
617
+ diffStats: { files_changed: 3, lines_added: 120, lines_removed: 40 },
618
+ });
619
+ assert.match(prompt, /economy/i);
620
+ assert.match(prompt, /"economy":\s+<number 0-10>/);
621
+ assert.match(prompt, /lines added: 120/);
622
+ assert.match(prompt, /lines removed: 40/);
623
+ assert.match(prompt, /files changed: 3/);
624
+ assert.match(prompt, /90 min/);
625
+ });
626
+
627
+ test('economy: buildPrompt degrades gracefully when diff stats are absent', () => {
628
+ const prompt = grader.buildPrompt({
629
+ task: { id: 7, title: 't', description: 'd' },
630
+ valueSummary: 's',
631
+ diff: 'd',
632
+ changedFiles: ['x'],
633
+ });
634
+ assert.match(prompt, /diff stats unavailable/i);
635
+ assert.match(prompt, /not estimated/i);
636
+ });
637
+
638
+ test('v2 extractAssistantResult: unwraps claude -p JSON envelope', () => {
639
+ // What `claude -p --output-format json` returns.
640
+ const env = JSON.stringify({
641
+ type: 'result',
642
+ subtype: 'success',
643
+ result: '{"functional_fidelity": 8, "code_quality": 7, "notes_md": "ok", "confidence": 0.8, "issues": []}',
644
+ is_error: false,
645
+ });
646
+ const inner = grader.extractAssistantResult(env);
647
+ assert.equal(typeof inner, 'string');
648
+ const r = grader.validateAndScore(inner);
649
+ assert.equal(r.passed, true);
650
+ });
651
+
652
+ test('v2 extractAssistantResult: passes-through non-envelope output', () => {
653
+ // If the subagent didn't return a wrapper, just hand the raw text back.
654
+ const out = grader.extractAssistantResult('{"functional_fidelity":8,"code_quality":7,"notes_md":"","confidence":0.8,"issues":[]}');
655
+ const r = grader.validateAndScore(out);
656
+ assert.equal(r.passed, true);
657
+ });
658
+
659
+ // ========================================================================
660
+ // v3 — cross-family selection + multi-shot aggregation (MAS Phase 1 / #395)
661
+ // ========================================================================
662
+
663
+ test('v3 selectGraderModel: builder Opus → grader Sonnet (cross-family)', () => {
664
+ assert.equal(grader.selectGraderModel({ builderModel: 'claude-opus-4-7' }), 'claude-sonnet-4-6');
665
+ assert.equal(grader.selectGraderModel({ builderModel: 'claude-opus-4-8' }), 'claude-sonnet-4-6');
666
+ assert.equal(grader.selectGraderModel({ builderModel: 'opus' }), 'claude-sonnet-4-6');
667
+ });
668
+
669
+ test('v3 selectGraderModel: builder Sonnet → grader Opus (cross-family)', () => {
670
+ assert.equal(grader.selectGraderModel({ builderModel: 'claude-sonnet-4-6' }), 'claude-opus-4-7');
671
+ assert.equal(grader.selectGraderModel({ builderModel: 'sonnet' }), 'claude-opus-4-7');
672
+ });
673
+
674
+ test('v3 selectGraderModel: builder Haiku → grader Opus', () => {
675
+ assert.equal(grader.selectGraderModel({ builderModel: 'claude-haiku-4-5-20251001' }), 'claude-opus-4-7');
676
+ assert.equal(grader.selectGraderModel({ builderModel: 'haiku' }), 'claude-opus-4-7');
677
+ });
678
+
679
+ test('selectGraderModel: missing builder → default Sonnet (assumed-Opus builder); explicit unknown THROWS (#117)', () => {
680
+ // No builderModel is the legitimate default path (the caller assumes the Opus
681
+ // builder per CLAUDE.md §2), so it resolves through the opus branch to Sonnet.
682
+ // This is the path the live callers — selectGraderModel({}) — take.
683
+ assert.equal(grader.selectGraderModel({}), 'claude-sonnet-4-6');
684
+ assert.equal(grader.selectGraderModel(), 'claude-sonnet-4-6');
685
+ // An explicitly-supplied unrecognized model is a misconfiguration: loud-throw
686
+ // instead of silently grading on the wrong cross-family assumption (#117).
687
+ assert.throws(() => grader.selectGraderModel({ builderModel: 'gpt-4' }), /unrecognized builder model/);
688
+ assert.throws(() => grader.selectGraderModel({ builderModel: 'llama-3' }), /unrecognized builder model/);
689
+ });
690
+
691
+ // --- aggregateShots: helpers ---
692
+
693
+ function mkPassShot(ff, cq, conf, idx) {
694
+ return grader.validateAndScore({
695
+ functional_fidelity: ff,
696
+ code_quality: cq,
697
+ economy: 7,
698
+ notes_md: 'ok',
699
+ confidence: conf,
700
+ issues: [],
701
+ }, { model: 'claude-sonnet-4-6', shot_index: idx });
702
+ }
703
+
704
+ function mkFailShot(ff, cq, conf, idx, issues = []) {
705
+ return grader.validateAndScore({
706
+ functional_fidelity: ff,
707
+ code_quality: cq,
708
+ economy: 5,
709
+ notes_md: 'problems',
710
+ confidence: conf,
711
+ issues,
712
+ }, { model: 'claude-sonnet-4-6', shot_index: idx });
713
+ }
714
+
715
+ function mkErroredShot(idx) {
716
+ return {
717
+ score: 0,
718
+ passed: false,
719
+ threshold: 6,
720
+ grader_kind: 'subagent',
721
+ grader_version: 'v3.0',
722
+ rubric_scores: {},
723
+ signals: {
724
+ shot_index: idx,
725
+ model: 'claude-sonnet-4-6',
726
+ subprocess_error: 'SUBAGENT_TIMEOUT',
727
+ },
728
+ notes_md: 'timeout',
729
+ issues: [],
730
+ };
731
+ }
732
+
733
+ test('v3 aggregateShots: 3 unanimous PASS → passed=true, shot_aggregation=unanimous_pass', () => {
734
+ const r = grader.aggregateShots([
735
+ mkPassShot(8, 8, 0.9, 0),
736
+ mkPassShot(8, 8, 0.85, 1),
737
+ mkPassShot(8, 8, 0.88, 2),
738
+ ]);
739
+ assert.equal(r.passed, true);
740
+ assert.equal(r.signals.shot_aggregation, 'unanimous_pass');
741
+ assert.equal(r.grader_kind, 'subagent-multishot');
742
+ // aggregateShots is a legacy diagnostic path; it stamps grader_version
743
+ // with whatever the current rubric VERSION is (v4.0 today). Asserting
744
+ // against grader.VERSION rather than a hardcoded literal keeps this test
745
+ // valid through future bumps.
746
+ assert.equal(r.grader_version, grader.VERSION);
747
+ assert.equal(r.signals.pass_count, 3);
748
+ assert.equal(r.signals.fail_count, 0);
749
+ assert.equal(r.signals.error_count, 0);
750
+ assert.equal(r.signals.shot_count, 3);
751
+ });
752
+
753
+ test('v3 aggregateShots: 2 PASS + 1 FAIL → passed=false, disagreement', () => {
754
+ const r = grader.aggregateShots([
755
+ mkPassShot(8, 8, 0.9, 0),
756
+ mkPassShot(8, 8, 0.85, 1),
757
+ mkFailShot(4, 7, 0.7, 2, [{ severity: 'blocker', file: 'foo', summary: 'broken' }]),
758
+ ]);
759
+ assert.equal(r.passed, false);
760
+ assert.equal(r.signals.shot_aggregation, 'disagreement');
761
+ assert.equal(r.signals.pass_count, 2);
762
+ assert.equal(r.signals.fail_count, 1);
763
+ });
764
+
765
+ test('v3 aggregateShots: 2 PASS + 1 low-confidence → passed=false (low-conf is FAIL per-shot)', () => {
766
+ const r = grader.aggregateShots([
767
+ mkPassShot(8, 8, 0.9, 0),
768
+ mkPassShot(8, 8, 0.85, 1),
769
+ mkPassShot(8, 8, 0.2, 2), // confidence below 0.4 → per-shot FAIL
770
+ ]);
771
+ assert.equal(r.passed, false);
772
+ // The third "shot" failed on confidence — counted as fail by per-shot validateAndScore.
773
+ assert.equal(r.signals.pass_count, 2);
774
+ assert.equal(r.signals.fail_count, 1);
775
+ });
776
+
777
+ test('v3 aggregateShots: all 3 FAIL → passed=false, unanimous_fail', () => {
778
+ const r = grader.aggregateShots([
779
+ mkFailShot(4, 5, 0.9, 0, [{ severity: 'blocker', file: 'a', summary: 'X' }]),
780
+ mkFailShot(5, 4, 0.85, 1, [{ severity: 'major', file: 'b', summary: 'Y' }]),
781
+ mkFailShot(3, 5, 0.88, 2, [{ severity: 'blocker', file: 'a', summary: 'X' }]),
782
+ ]);
783
+ assert.equal(r.passed, false);
784
+ assert.equal(r.signals.shot_aggregation, 'unanimous_fail');
785
+ // Issues deduped across shots by (file, summary).
786
+ assert.equal(r.issues.length, 2);
787
+ });
788
+
789
+ test('v3 aggregateShots: any errored shot → passed=false, errored', () => {
790
+ const r = grader.aggregateShots([
791
+ mkPassShot(8, 8, 0.9, 0),
792
+ mkPassShot(8, 8, 0.85, 1),
793
+ mkErroredShot(2),
794
+ ]);
795
+ assert.equal(r.passed, false);
796
+ assert.equal(r.signals.shot_aggregation, 'errored');
797
+ assert.equal(r.signals.error_count, 1);
798
+ });
799
+
800
+ test('v3 aggregateShots: shots=1 PASS → passes (back-compat with single-shot)', () => {
801
+ const r = grader.aggregateShots([mkPassShot(8, 8, 0.9, 0)]);
802
+ assert.equal(r.passed, true);
803
+ assert.equal(r.signals.shot_aggregation, 'unanimous_pass');
804
+ assert.equal(r.signals.shot_count, 1);
805
+ });
806
+
807
+ test('v3 aggregateShots: median across shots for displayed rubric_scores', () => {
808
+ const r = grader.aggregateShots([
809
+ mkPassShot(7, 6, 0.9, 0),
810
+ mkPassShot(9, 8, 0.9, 1),
811
+ mkPassShot(8, 7, 0.9, 2),
812
+ ]);
813
+ // Sorted ff = [7,8,9], median 8. Sorted cq = [6,7,8], median 7.
814
+ assert.equal(r.rubric_scores.functional_fidelity, 8);
815
+ assert.equal(r.rubric_scores.code_quality, 7);
816
+ // Blended median = (8+7)/2 = 7.5.
817
+ assert.equal(r.score, 7.5);
818
+ });
819
+
820
+ test('v3 aggregateShots: signals.shots[] preserves per-shot detail', () => {
821
+ const r = grader.aggregateShots([
822
+ mkPassShot(8, 8, 0.9, 0),
823
+ mkPassShot(7, 9, 0.85, 1),
824
+ mkPassShot(9, 7, 0.88, 2),
825
+ ]);
826
+ assert.ok(Array.isArray(r.signals.shots));
827
+ assert.equal(r.signals.shots.length, 3);
828
+ assert.equal(r.signals.shots[0].ff, 8);
829
+ assert.equal(r.signals.shots[1].ff, 7);
830
+ assert.equal(r.signals.shots[2].ff, 9);
831
+ assert.equal(r.signals.shots[0].model, 'claude-sonnet-4-6');
832
+ });
833
+
834
+ test('v3 aggregateShots: signals.confidence populated as median across shots (regression for null→server-FAIL)', () => {
835
+ // Caught on the MAS Phase 1 self-ship (#395): aggregateShots did not set a
836
+ // top-level confidence, ship.js forwarded null to the server, and the
837
+ // server's validateAndScore hit Number(null)=0 → lowConfidence → FAIL
838
+ // despite a unanimous-PASS verdict. The fix populates signals.confidence
839
+ // as the median of per-shot confidences.
840
+ const r = grader.aggregateShots([
841
+ mkPassShot(8, 8, 0.72, 0),
842
+ mkPassShot(8, 8, 0.85, 1),
843
+ mkPassShot(8, 8, 0.72, 2),
844
+ ]);
845
+ assert.equal(r.signals.confidence, 0.72, 'median of [0.72, 0.85, 0.72] is 0.72');
846
+ assert.notEqual(r.signals.confidence, null, 'must not be null — ship.js forwards this to the server');
847
+ });
848
+
849
+ test('v3 validateAndScore: null confidence treated as ABSENT, not as low-confidence (regression for Number(null)=0)', () => {
850
+ // Original #395 fix: Number(null)===0 must NOT trigger the low-confidence gate.
851
+ // #489 update: absent confidence still fails, but via a distinct "confidence absent"
852
+ // path — NOT the "low confidence" path. The distinction matters: the #395 invariant
853
+ // (null ≠ numeric-zero) is preserved; the #489 invariant (absent → gate failure) is added.
854
+ const r = grader.validateAndScore({
855
+ functional_fidelity: 8,
856
+ code_quality: 8,
857
+ notes_md: 'no confidence field set',
858
+ confidence: null,
859
+ issues: [],
860
+ });
861
+ // #489: absent confidence now fails, but NOT via the low-confidence message.
862
+ assert.equal(r.passed, false, 'absent confidence must now fail (#489)');
863
+ assert.match(r.notes_md, /confidence absent/i, 'must say "confidence absent", not "low confidence"');
864
+ assert.doesNotMatch(r.notes_md, /low confidence/i, '#395 invariant: null must not alias to numeric 0');
865
+ assert.equal(r.signals.confidence, null, 'confidence stays null in signals (truly absent)');
866
+ });
867
+
868
+ test('v3 validateAndScore: undefined confidence treated as ABSENT (same as null)', () => {
869
+ const r = grader.validateAndScore({
870
+ functional_fidelity: 8,
871
+ code_quality: 8,
872
+ notes_md: 'no confidence field at all',
873
+ issues: [],
874
+ // confidence omitted entirely
875
+ });
876
+ // #489: absent (omitted) confidence fails with "confidence absent", not "low confidence".
877
+ assert.equal(r.passed, false);
878
+ assert.match(r.notes_md, /confidence absent/i);
879
+ assert.doesNotMatch(r.notes_md, /low confidence/i);
880
+ assert.equal(r.signals.confidence, null);
881
+ });
882
+
883
+ test('v3 validateAndScore: explicit 0 confidence STILL FAILS (real low-confidence signal)', () => {
884
+ // Belt-and-braces: an explicit confidence: 0 from a real subagent IS a
885
+ // low-confidence signal and MUST still fail. The null fix doesn't change
886
+ // this — only null/undefined are treated as absent.
887
+ const r = grader.validateAndScore({
888
+ functional_fidelity: 8,
889
+ code_quality: 8,
890
+ notes_md: 'totally unsure',
891
+ confidence: 0,
892
+ issues: [],
893
+ });
894
+ assert.equal(r.passed, false);
895
+ assert.equal(r.signals.confidence, 0);
896
+ assert.match(r.notes_md, /low confidence/i);
897
+ });
898
+
899
+ test('v3 buildPrompt: includes default-fail framing', () => {
900
+ const prompt = grader.buildPrompt({
901
+ task: { id: 1, title: 't', description: 'd' },
902
+ valueSummary: 's',
903
+ diff: 'd',
904
+ changedFiles: ['x'],
905
+ });
906
+ // Adversarial framing markers.
907
+ assert.match(prompt, /default verdict is FAIL/i);
908
+ assert.match(prompt, /FIND WHAT IS WRONG/i);
909
+ // Failure-mode checklist markers.
910
+ assert.match(prompt, /TODOs.*FIXMEs/i);
911
+ assert.match(prompt, /Hardcoded shortcuts/i);
912
+ assert.match(prompt, /Scope gaps/i);
913
+ assert.match(prompt, /Demo \/ dummy/i);
914
+ // Sanity-check requirements.
915
+ assert.match(prompt, /PASS with no issues.*is suspicious/i);
916
+ assert.match(prompt, /FAIL with no issues.*is invalid/i);
917
+ // Default-fail reminder at the end.
918
+ assert.match(prompt, /Default-fail unless/i);
919
+ });
920
+
921
+ // ========================================================================
922
+ // v4 — worker panel (MAS Phase 2.0 / #410)
923
+ // ========================================================================
924
+
925
+ // --- helpers ---
926
+
927
+ function mkWorkerResult(kind, verdict, scores, confidence, issues = [], error = null) {
928
+ return {
929
+ kind,
930
+ name: kind,
931
+ specialty: 'x',
932
+ verdict,
933
+ scores,
934
+ issues,
935
+ notes_md: `${kind} ${verdict}`,
936
+ confidence,
937
+ model: 'claude-sonnet-4-6',
938
+ run_ms: 1000,
939
+ error,
940
+ };
941
+ }
942
+
943
+ // --- worker module surface ---
944
+
945
+ test('v4 narc worker: exposes the expected contract', () => {
946
+ const narc = grader.WORKERS.narc;
947
+ assert.equal(narc.KIND, 'narc');
948
+ assert.equal(typeof narc.buildPrompt, 'function');
949
+ assert.equal(typeof narc.extractJson, 'function');
950
+ assert.equal(typeof narc.computeVerdict, 'function');
951
+ assert.match(narc.SPECIALTY, /semantic completion|completion/i);
952
+ });
953
+
954
+ test('v4 quality worker: exposes the expected contract', () => {
955
+ const q = grader.WORKERS.quality;
956
+ assert.equal(q.KIND, 'quality');
957
+ assert.equal(typeof q.buildPrompt, 'function');
958
+ assert.equal(typeof q.extractJson, 'function');
959
+ assert.equal(typeof q.computeVerdict, 'function');
960
+ assert.match(q.SPECIALTY, /code quality|quality/i);
961
+ });
962
+
963
+ test('v4 narc prompt: includes default-fail framing + AC walking instructions', () => {
964
+ const prompt = grader.WORKERS.narc.buildPrompt({
965
+ task: { id: 1, title: 't', description: 'd' },
966
+ valueSummary: 's',
967
+ diff: 'd',
968
+ changedFiles: ['x'],
969
+ });
970
+ assert.match(prompt, /Narc/);
971
+ assert.match(prompt, /default verdict is FAIL/i);
972
+ assert.match(prompt, /walk(ing)? .*acceptance criter/i);
973
+ assert.match(prompt, /ac_checklist/);
974
+ });
975
+
976
+ test('v4 quality prompt: includes default-fail framing + cq + economy', () => {
977
+ const prompt = grader.WORKERS.quality.buildPrompt({
978
+ task: { id: 1, title: 't', description: 'd', est_minutes: 60 },
979
+ valueSummary: 's',
980
+ diff: 'd',
981
+ changedFiles: ['x'],
982
+ diffStats: { files_changed: 1, lines_added: 10, lines_removed: 2 },
983
+ });
984
+ assert.match(prompt, /Quality/);
985
+ assert.match(prompt, /default verdict is FAIL/i);
986
+ assert.match(prompt, /code_quality/);
987
+ assert.match(prompt, /economy/);
988
+ });
989
+
990
+ // --- narc.computeVerdict ---
991
+
992
+ test('v4 narc: all ACs satisfied + ff>=6 + conf>=0.4 → pass', () => {
993
+ const v = grader.WORKERS.narc.computeVerdict({
994
+ functional_fidelity: 8,
995
+ confidence: 0.85,
996
+ ac_checklist: [{ ac: 'A', status: 'satisfied', evidence: 'foo:1' }],
997
+ issues: [],
998
+ notes_md: 'verified',
999
+ }, {});
1000
+ assert.equal(v.verdict, 'pass');
1001
+ assert.equal(v.scores.functional_fidelity, 8);
1002
+ });
1003
+
1004
+ test('v4 narc: any missing AC → fail (even if ff was scored above threshold)', () => {
1005
+ const v = grader.WORKERS.narc.computeVerdict({
1006
+ functional_fidelity: 7,
1007
+ confidence: 0.9,
1008
+ ac_checklist: [
1009
+ { ac: 'A', status: 'satisfied', evidence: 'foo' },
1010
+ { ac: 'B', status: 'missing', evidence: '' },
1011
+ ],
1012
+ issues: [{ severity: 'blocker', file: 'foo', summary: 'missing B' }],
1013
+ notes_md: 'B missing',
1014
+ }, {});
1015
+ assert.equal(v.verdict, 'fail');
1016
+ });
1017
+
1018
+ test('v4 narc Phase 2.0.5: diff-confirmed AC counts as satisfied (no Read needed) → pass', () => {
1019
+ const v = grader.WORKERS.narc.computeVerdict({
1020
+ functional_fidelity: 9,
1021
+ confidence: 0.9,
1022
+ ac_checklist: [
1023
+ { ac: 'add X', status: 'diff-confirmed', evidence: '+function X()' },
1024
+ { ac: 'add Y', status: 'diff-confirmed', evidence: '+function Y()' },
1025
+ ],
1026
+ issues: [],
1027
+ notes_md: 'all visible in diff',
1028
+ }, {});
1029
+ assert.equal(v.verdict, 'pass');
1030
+ assert.equal(v.scores.functional_fidelity, 9);
1031
+ });
1032
+
1033
+ test('v4 narc Phase 2.0.5: a few unverified-budget mixed with satisfied → still pass (neutral)', () => {
1034
+ const v = grader.WORKERS.narc.computeVerdict({
1035
+ functional_fidelity: 8,
1036
+ confidence: 0.85,
1037
+ ac_checklist: [
1038
+ { ac: 'A', status: 'satisfied', evidence: 'foo:1' },
1039
+ { ac: 'B', status: 'diff-confirmed', evidence: '+B' },
1040
+ { ac: 'C', status: 'satisfied', evidence: 'bar:2' },
1041
+ { ac: 'D', status: 'unverified-budget', evidence: '' },
1042
+ ],
1043
+ issues: [],
1044
+ notes_md: 'mixed',
1045
+ }, {});
1046
+ assert.equal(v.verdict, 'pass', '1 of 4 unverified-budget should not dominate');
1047
+ });
1048
+
1049
+ test('v4 narc Phase 2.0.5: majority unverified-budget → concerns (dominates)', () => {
1050
+ const v = grader.WORKERS.narc.computeVerdict({
1051
+ functional_fidelity: 7,
1052
+ confidence: 0.85,
1053
+ ac_checklist: [
1054
+ { ac: 'A', status: 'satisfied', evidence: 'foo' },
1055
+ { ac: 'B', status: 'unverified-budget', evidence: '' },
1056
+ { ac: 'C', status: 'unverified-budget', evidence: '' },
1057
+ { ac: 'D', status: 'unverified-budget', evidence: '' },
1058
+ ],
1059
+ issues: [],
1060
+ notes_md: '3 of 4 unverified',
1061
+ }, {});
1062
+ assert.equal(v.verdict, 'concerns');
1063
+ });
1064
+
1065
+ test('v4 narc Phase 2.0.5: out-of-scope ACs excluded from the verifiable-count denominator', () => {
1066
+ // 1 of 2 verifiable (the 2 'out-of-scope' don't count) unverified-budget = 50%,
1067
+ // NOT majority. Should stay pass.
1068
+ const v = grader.WORKERS.narc.computeVerdict({
1069
+ functional_fidelity: 8,
1070
+ confidence: 0.85,
1071
+ ac_checklist: [
1072
+ { ac: 'A', status: 'satisfied', evidence: 'foo' },
1073
+ { ac: 'B', status: 'unverified-budget', evidence: '' },
1074
+ { ac: 'C', status: 'out-of-scope', evidence: 'description says X' },
1075
+ { ac: 'D', status: 'out-of-scope', evidence: 'description says Y' },
1076
+ ],
1077
+ issues: [],
1078
+ notes_md: '',
1079
+ }, {});
1080
+ assert.equal(v.verdict, 'pass');
1081
+ });
1082
+
1083
+ test('v4 narc Phase 2.0.5: prompt mentions Read budget + prefers diff', () => {
1084
+ const prompt = grader.WORKERS.narc.buildPrompt({
1085
+ task: { id: 1, title: 't', description: 'd' },
1086
+ valueSummary: 's',
1087
+ diff: 'd',
1088
+ changedFiles: ['x'],
1089
+ });
1090
+ // Budget instructions visible
1091
+ assert.match(prompt, /Read budget/i);
1092
+ assert.match(prompt, /5 Read calls/);
1093
+ // Prefer-diff guidance visible
1094
+ assert.match(prompt, /Prefer the diff/i);
1095
+ // New statuses listed
1096
+ assert.match(prompt, /diff-confirmed/);
1097
+ assert.match(prompt, /unverified-budget/);
1098
+ });
1099
+
1100
+ test('v4 narc: any partial AC → concerns', () => {
1101
+ const v = grader.WORKERS.narc.computeVerdict({
1102
+ functional_fidelity: 7,
1103
+ confidence: 0.85,
1104
+ ac_checklist: [{ ac: 'A', status: 'partial', evidence: 'foo' }],
1105
+ issues: [],
1106
+ notes_md: 'partial',
1107
+ }, {});
1108
+ assert.equal(v.verdict, 'concerns');
1109
+ });
1110
+
1111
+ test('v4 narc: ff < threshold → fail', () => {
1112
+ const v = grader.WORKERS.narc.computeVerdict({
1113
+ functional_fidelity: 4,
1114
+ confidence: 0.9,
1115
+ ac_checklist: [{ ac: 'A', status: 'satisfied' }],
1116
+ issues: [],
1117
+ notes_md: 'low score',
1118
+ }, {});
1119
+ assert.equal(v.verdict, 'fail');
1120
+ });
1121
+
1122
+ test('v4 narc: parse error → fail with error tag', () => {
1123
+ const v = grader.WORKERS.narc.computeVerdict(null, {});
1124
+ assert.equal(v.verdict, 'fail');
1125
+ assert.equal(v.error, 'parse_error');
1126
+ });
1127
+
1128
+ test('v4 narc: subprocess error → fail with error tag', () => {
1129
+ const v = grader.WORKERS.narc.computeVerdict(null, { subprocess_error: 'SUBAGENT_TIMEOUT' });
1130
+ assert.equal(v.verdict, 'fail');
1131
+ assert.equal(v.error, 'SUBAGENT_TIMEOUT');
1132
+ });
1133
+
1134
+ // --- quality.computeVerdict ---
1135
+
1136
+ test('v4 quality: cq>=6 + economy>=6 + conf>=0.4 + no major+blocker → pass', () => {
1137
+ const v = grader.WORKERS.quality.computeVerdict({
1138
+ code_quality: 8,
1139
+ economy: 7,
1140
+ confidence: 0.85,
1141
+ issues: [],
1142
+ notes_md: 'clean',
1143
+ }, {});
1144
+ assert.equal(v.verdict, 'pass');
1145
+ assert.equal(v.scores.code_quality, 8);
1146
+ assert.equal(v.scores.economy, 7);
1147
+ });
1148
+
1149
+ test('v4 quality: blocker issue → fail', () => {
1150
+ const v = grader.WORKERS.quality.computeVerdict({
1151
+ code_quality: 8,
1152
+ economy: 7,
1153
+ confidence: 0.85,
1154
+ issues: [{ severity: 'blocker', file: 'foo', summary: 'broken' }],
1155
+ notes_md: 'has blocker',
1156
+ }, {});
1157
+ assert.equal(v.verdict, 'fail');
1158
+ });
1159
+
1160
+ test('v4 quality: cq < threshold → fail', () => {
1161
+ const v = grader.WORKERS.quality.computeVerdict({
1162
+ code_quality: 4, economy: 7, confidence: 0.9, issues: [], notes_md: '',
1163
+ }, {});
1164
+ assert.equal(v.verdict, 'fail');
1165
+ });
1166
+
1167
+ test('v4 quality: major issue → concerns', () => {
1168
+ const v = grader.WORKERS.quality.computeVerdict({
1169
+ code_quality: 7, economy: 7, confidence: 0.85,
1170
+ issues: [{ severity: 'major', file: 'foo', summary: 'rough' }],
1171
+ notes_md: 'major found',
1172
+ }, {});
1173
+ assert.equal(v.verdict, 'concerns');
1174
+ });
1175
+
1176
+ test('v4 quality: low economy (advisory) → concerns even if cq passes', () => {
1177
+ const v = grader.WORKERS.quality.computeVerdict({
1178
+ code_quality: 8, economy: 3, confidence: 0.85, issues: [], notes_md: '',
1179
+ }, {});
1180
+ assert.equal(v.verdict, 'concerns');
1181
+ });
1182
+
1183
+ // --- aggregatePanel ---
1184
+
1185
+ test('v4 aggregatePanel: all PASS → panel pass, forwarded confidence = median', () => {
1186
+ const r = grader.aggregatePanel([
1187
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1188
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1189
+ ]);
1190
+ assert.equal(r.passed, true);
1191
+ assert.equal(r.signals.panel_outcome, 'pass');
1192
+ assert.equal(r.grader_kind, 'subagent-panel');
1193
+ assert.equal(r.grader_version, grader.VERSION);
1194
+ // median confidence of [0.80, 0.85] is 0.825
1195
+ assert.equal(r.signals.confidence, 0.825);
1196
+ });
1197
+
1198
+ test('v4 aggregatePanel: any CONCERNS → panel concerns, confidence = FALLTHROUGH_CONFIDENCE', () => {
1199
+ const r = grader.aggregatePanel([
1200
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1201
+ mkWorkerResult('quality', 'concerns', { code_quality: 7, economy: 4 }, 0.75,
1202
+ [{ severity: 'major', file: 'x', summary: 'rough' }]),
1203
+ ]);
1204
+ assert.equal(r.passed, false);
1205
+ assert.equal(r.signals.panel_outcome, 'concerns');
1206
+ assert.equal(r.signals.confidence, grader.FALLTHROUGH_CONFIDENCE);
1207
+ });
1208
+
1209
+ test('v4 aggregatePanel: any FAIL → panel fail, confidence = 0', () => {
1210
+ const r = grader.aggregatePanel([
1211
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1212
+ mkWorkerResult('quality', 'fail', { code_quality: 4 }, 0.75,
1213
+ [{ severity: 'blocker', file: 'x', summary: 'broken' }]),
1214
+ ]);
1215
+ assert.equal(r.passed, false);
1216
+ assert.equal(r.signals.panel_outcome, 'fail');
1217
+ assert.equal(r.signals.confidence, 0);
1218
+ });
1219
+
1220
+ test('v4 aggregatePanel: an errored GATING worker → panel concerns, manual-confirm (#126; not a hard fail)', () => {
1221
+ // Updated from the pre-#126 expectation of 'fail': a crashed worker is
1222
+ // MISSING SIGNAL, not a finding, so with no substantive failure it falls
1223
+ // through to manual-confirm. (narc here is advisory per #481; quality is the
1224
+ // gating worker that errored.)
1225
+ // #943: this must be a PARTIAL outage — another gating worker (hacker) DID
1226
+ // report — so it stays 'concerns'. If EVERY gating worker errored it would be
1227
+ // the distinct 'unavailable' total-outage outcome instead (covered in
1228
+ // tests/grader_panel.mjs). The earlier 2-worker panel had quality as the lone
1229
+ // gating worker, which is by definition a total gating outage.
1230
+ const r = grader.aggregatePanel([
1231
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1232
+ mkWorkerResult('quality', 'fail', {}, 0, [], 'parse_error'),
1233
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1234
+ ]);
1235
+ assert.equal(r.passed, false);
1236
+ assert.equal(r.signals.panel_outcome, 'concerns');
1237
+ assert.equal(r.signals.error_count, 1);
1238
+ });
1239
+
1240
+ test('v4 aggregatePanel: rubric_scores = median across workers that report each axis', () => {
1241
+ const r = grader.aggregatePanel([
1242
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 9 }, 0.9),
1243
+ mkWorkerResult('quality', 'pass', { code_quality: 7, economy: 8 }, 0.85),
1244
+ ]);
1245
+ assert.equal(r.rubric_scores.functional_fidelity, 9);
1246
+ assert.equal(r.rubric_scores.code_quality, 7);
1247
+ assert.equal(r.rubric_scores.economy, 8);
1248
+ assert.equal(r.score, 8); // blended = (9 + 7) / 2 = 8
1249
+ });
1250
+
1251
+ test('v4 aggregatePanel: signals.workers[] preserves the full per-worker audit trail', () => {
1252
+ const workers = [
1253
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1254
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1255
+ ];
1256
+ const r = grader.aggregatePanel(workers);
1257
+ assert.ok(Array.isArray(r.signals.workers));
1258
+ assert.equal(r.signals.workers.length, 2);
1259
+ assert.equal(r.signals.workers[0].kind, 'narc');
1260
+ assert.equal(r.signals.workers[1].kind, 'quality');
1261
+ // pass/concerns/fail counts surface for dashboards
1262
+ assert.equal(r.signals.pass_count, 2);
1263
+ assert.equal(r.signals.fail_count, 0);
1264
+ assert.equal(r.signals.concerns_count, 0);
1265
+ assert.equal(r.signals.error_count, 0);
1266
+ });
1267
+
1268
+ test('v4 aggregatePanel: issues deduped across workers by (kind, file, summary)', () => {
1269
+ const r = grader.aggregatePanel([
1270
+ mkWorkerResult('narc', 'fail', { functional_fidelity: 4 }, 0.85,
1271
+ [{ severity: 'blocker', file: 'foo.js', summary: 'X' }]),
1272
+ mkWorkerResult('quality', 'fail', { code_quality: 4 }, 0.80,
1273
+ [{ severity: 'blocker', file: 'foo.js', summary: 'X' }]),
1274
+ ]);
1275
+ // Same (file, summary) from two different workers should appear twice
1276
+ // (different worker namespaces — issue origin matters)
1277
+ assert.equal(r.issues.length, 2);
1278
+ assert.equal(r.issues[0].worker, 'narc');
1279
+ assert.equal(r.issues[1].worker, 'quality');
1280
+ });
1281
+
1282
+ // --- #481: Narc is advisory in the gate ---
1283
+
1284
+ test('v4 aggregatePanel (#481): a Narc CONCERNS is advisory — panel PASSES when gating workers pass', () => {
1285
+ const r = grader.aggregatePanel([
1286
+ mkWorkerResult('narc', 'concerns', { functional_fidelity: 6 }, 0.7,
1287
+ [{ severity: 'major', file: 'x', summary: 'AC quibble' }]),
1288
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 8 }, 0.85),
1289
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1290
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 9 }, 0.9),
1291
+ ]);
1292
+ assert.equal(r.passed, true);
1293
+ assert.equal(r.signals.panel_outcome, 'pass');
1294
+ // the Narc's lens still surfaces in the audit trail even though it didn't gate
1295
+ assert.ok(r.issues.some((i) => i.worker === 'narc'));
1296
+ });
1297
+
1298
+ test('v4 aggregatePanel (#481): a Narc FAIL downgrades to manual-confirm (CONCERNS), not a hard FAIL', () => {
1299
+ const r = grader.aggregatePanel([
1300
+ mkWorkerResult('narc', 'fail', { functional_fidelity: 4 }, 0.8,
1301
+ [{ severity: 'major', file: 'x', summary: 'scope gap' }]),
1302
+ mkWorkerResult('quality', 'pass', { code_quality: 8 }, 0.85),
1303
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1304
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 9 }, 0.9),
1305
+ ]);
1306
+ assert.equal(r.passed, false);
1307
+ assert.equal(r.signals.panel_outcome, 'concerns'); // manual-confirm, NOT a hard fail
1308
+ });
1309
+
1310
+ test('v4 aggregatePanel (#481): a GATING worker FAIL still hard-FAILs regardless of the Narc', () => {
1311
+ const r = grader.aggregatePanel([
1312
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 9 }, 0.9),
1313
+ mkWorkerResult('quality', 'fail', { code_quality: 3 }, 0.8,
1314
+ [{ severity: 'blocker', file: 'x', summary: 'broken' }]),
1315
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1316
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 9 }, 0.9),
1317
+ ]);
1318
+ assert.equal(r.passed, false);
1319
+ assert.equal(r.signals.panel_outcome, 'fail');
1320
+ });
1321
+
1322
+ test('v4 aggregatePanel (#481): an errored (missing-signal) Narc does not gate — gating workers govern', () => {
1323
+ const r = grader.aggregatePanel([
1324
+ mkWorkerResult('narc', 'fail', {}, 0, [], 'parse_error'), // missing signal, advisory kind
1325
+ mkWorkerResult('quality', 'pass', { code_quality: 8 }, 0.85),
1326
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1327
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 9 }, 0.9),
1328
+ ]);
1329
+ assert.equal(r.passed, true); // a crashed advisory lens doesn't block a clean panel
1330
+ assert.equal(r.signals.panel_outcome, 'pass');
1331
+ });
1332
+
1333
+ test('v4 aggregatePanel (#481 defense-in-depth): a Narc carrying a BLOCKER issue is NOT advisory — it gates', () => {
1334
+ // Guards the Hacker's #481 concern: a blocker-severity finding can never be
1335
+ // silently downgraded to manual-confirm just because it came through an
1336
+ // advisory-kind worker.
1337
+ const r = grader.aggregatePanel([
1338
+ mkWorkerResult('narc', 'fail', { functional_fidelity: 2 }, 0.9,
1339
+ [{ severity: 'blocker', file: 'auth.js', summary: 'ungated write route' }]),
1340
+ mkWorkerResult('quality', 'pass', { code_quality: 8 }, 0.85),
1341
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.9),
1342
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 9 }, 0.9),
1343
+ ]);
1344
+ assert.equal(r.passed, false);
1345
+ assert.equal(r.signals.panel_outcome, 'fail'); // blocker hard-FAILs even from the Narc
1346
+ });
1347
+
1348
+ // --- #465: runWorker auto-retries transient errors once ---
1349
+
1350
+ // A minimal worker module: extractJson parses JSON; computeVerdict maps a
1351
+ // subprocess error / bad parse to the matching error tag, else echoes the
1352
+ // parsed verdict. Mirrors the real workers' error contract closely enough to
1353
+ // exercise runWorker's retry branch.
1354
+ function mkFakeWorker(kind = 'narc') {
1355
+ return {
1356
+ KIND: kind, NAME: kind, SPECIALTY: 'x',
1357
+ buildPrompt: () => 'p',
1358
+ extractJson: (raw) => { try { return JSON.parse(raw); } catch { return null; } },
1359
+ computeVerdict: (parsed, meta) => {
1360
+ if (meta.subprocess_error) return { verdict: 'fail', error: meta.subprocess_error, scores: {}, issues: [], confidence: 0 };
1361
+ if (!parsed) return { verdict: 'fail', error: 'parse_error', scores: {}, issues: [], confidence: 0 };
1362
+ return { verdict: parsed.verdict || 'pass', scores: parsed.scores || {}, issues: parsed.issues || [], confidence: parsed.confidence ?? 0.9, error: null };
1363
+ },
1364
+ };
1365
+ }
1366
+ const okJson = JSON.stringify({ verdict: 'pass', scores: { functional_fidelity: 8 }, confidence: 0.9 });
1367
+
1368
+ await testAsync('v4 runWorker (#465): retries ONCE on parse_error, second (good) attempt wins', async () => {
1369
+ let calls = 0;
1370
+ const runSubagentFn = async () => { calls++; return { stdout: calls === 1 ? 'not-json' : okJson, run_ms: 10 }; };
1371
+ const r = await grader.WORKERS.runWorker(mkFakeWorker(), { runSubagentFn });
1372
+ assert.equal(calls, 2);
1373
+ assert.equal(r.retried, true);
1374
+ assert.equal(r.error, null);
1375
+ assert.equal(r.verdict, 'pass');
1376
+ });
1377
+
1378
+ await testAsync('v4 runWorker (#465): does NOT retry SUBAGENT_TIMEOUT (real budget signal)', async () => {
1379
+ let calls = 0;
1380
+ const runSubagentFn = async () => { calls++; throw Object.assign(new Error('t/o'), { code: 'SUBAGENT_TIMEOUT', run_ms: 240000 }); };
1381
+ const r = await grader.WORKERS.runWorker(mkFakeWorker(), { runSubagentFn });
1382
+ assert.equal(calls, 1); // no retry
1383
+ assert.equal(r.retried, false);
1384
+ assert.equal(r.error, 'SUBAGENT_TIMEOUT');
1385
+ });
1386
+
1387
+ await testAsync('v4 runWorker (#465): does NOT retry a real (non-error) verdict', async () => {
1388
+ let calls = 0;
1389
+ const failJson = JSON.stringify({ verdict: 'fail', scores: { functional_fidelity: 3 }, confidence: 0.9 });
1390
+ const runSubagentFn = async () => { calls++; return { stdout: failJson, run_ms: 10 }; };
1391
+ const r = await grader.WORKERS.runWorker(mkFakeWorker(), { runSubagentFn });
1392
+ assert.equal(calls, 1); // a real low-quality verdict is signal, not retried
1393
+ assert.equal(r.retried, false);
1394
+ assert.equal(r.verdict, 'fail');
1395
+ assert.equal(r.error, null);
1396
+ });
1397
+
1398
+ await testAsync('v4 runWorker (#465): one retry only — if the retry also errors, that error stands', async () => {
1399
+ let calls = 0;
1400
+ const runSubagentFn = async () => {
1401
+ calls++;
1402
+ if (calls === 1) return { stdout: 'not-json', run_ms: 10 }; // parse_error
1403
+ throw Object.assign(new Error('exit 1'), { code: 'SUBAGENT_NONZERO_EXIT', run_ms: 10 }); // retry also fails
1404
+ };
1405
+ const r = await grader.WORKERS.runWorker(mkFakeWorker(), { runSubagentFn });
1406
+ assert.equal(calls, 2); // exactly one retry, never more
1407
+ assert.equal(r.retried, true);
1408
+ assert.equal(r.error, 'SUBAGENT_NONZERO_EXIT');
1409
+ });
1410
+
1411
+ // --- #467: per-worker timeouts + transient-error set + audit-exports core ---
1412
+
1413
+ test('v4 #467: timeoutForWorker gives Read-heavy workers a longer budget', () => {
1414
+ const tfw = grader.WORKERS.timeoutForWorker;
1415
+ assert.equal(tfw({ KIND: 'narc' }, {}), 360000);
1416
+ assert.equal(tfw({ KIND: 'quality' }, {}), 360000);
1417
+ assert.equal(tfw({ KIND: 'hacker' }, {}), 240000);
1418
+ assert.equal(tfw({ KIND: 'efficiency' }, {}), 240000);
1419
+ });
1420
+
1421
+ test('v4 #467: an explicit module MAX_TIMEOUT_MS overrides the KIND map', () => {
1422
+ const tfw = grader.WORKERS.timeoutForWorker;
1423
+ assert.equal(tfw({ KIND: 'narc', MAX_TIMEOUT_MS: 120000 }, {}), 120000);
1424
+ });
1425
+
1426
+ test('v4 #467: unlisted worker falls back to runOpts.timeoutMs', () => {
1427
+ const tfw = grader.WORKERS.timeoutForWorker;
1428
+ assert.equal(tfw({ KIND: 'mystery' }, { timeoutMs: 99000 }), 99000);
1429
+ });
1430
+
1431
+ test('v4 #465/#467: TRANSIENT_WORKER_ERRORS covers infra noise but NOT timeout', () => {
1432
+ const t = grader.WORKERS.TRANSIENT_WORKER_ERRORS;
1433
+ for (const code of ['parse_error', 'schema_error', 'SUBAGENT_SPAWN_FAILED', 'SUBAGENT_NONZERO_EXIT']) {
1434
+ assert.ok(t.has(code), `${code} should be retryable`);
1435
+ }
1436
+ assert.ok(!t.has('SUBAGENT_TIMEOUT'), 'timeout is a real budget signal, never retried');
1437
+ });
1438
+
1439
+ test('v4 #467: audit-exports classifyExports — consumed / test-only / DEAD', () => {
1440
+ const auditExports = require('../scripts/gds/audit-exports.js');
1441
+ const target = '/repo/src/bongos/grader.js';
1442
+ const corpus = {
1443
+ [target]: 'function usedSym(){} function testSym(){} function deadSym(){}', // self — excluded
1444
+ '/repo/src/server.js': 'grader.usedSym()', // consumer in src/
1445
+ '/repo/tests/grader.mjs': 'grader.testSym === 1', // consumer only in tests/
1446
+ // deadSym appears nowhere outside the defining file
1447
+ };
1448
+ const out = auditExports.classifyExports(target, ['usedSym', 'testSym', 'deadSym'], corpus);
1449
+ const by = Object.fromEntries(out.map((r) => [r.name, r.status]));
1450
+ assert.equal(by.usedSym, 'consumed');
1451
+ assert.equal(by.testSym, 'test-only');
1452
+ assert.equal(by.deadSym, 'DEAD');
1453
+ });
1454
+
1455
+ test('v4 WORKERS.resolvePanel: unknown kind throws (typo catches don\'t silently skip)', () => {
1456
+ assert.throws(() => grader.WORKERS.resolvePanel(['narc', 'nopenotreal']), /unknown worker kind/i);
1457
+ });
1458
+
1459
+ // ========================================================================
1460
+ // v4 Phase 2.1 — Hacker worker (#416)
1461
+ // ========================================================================
1462
+
1463
+ test('v4 hacker worker: exposes the expected contract', () => {
1464
+ const h = grader.WORKERS.hacker;
1465
+ assert.equal(h.KIND, 'hacker');
1466
+ assert.equal(typeof h.buildPrompt, 'function');
1467
+ assert.equal(typeof h.extractJson, 'function');
1468
+ assert.equal(typeof h.computeVerdict, 'function');
1469
+ assert.match(h.SPECIALTY, /security|red[- ]?team/i);
1470
+ });
1471
+
1472
+ test('v4 hacker prompt: includes default-fail framing + security failure modes', () => {
1473
+ const prompt = grader.WORKERS.hacker.buildPrompt({
1474
+ task: { id: 1, title: 't', description: 'd' },
1475
+ valueSummary: 's',
1476
+ diff: 'd',
1477
+ changedFiles: ['x'],
1478
+ });
1479
+ assert.match(prompt, /Hacker/);
1480
+ assert.match(prompt, /default verdict is FAIL/i);
1481
+ // Failure modes by class
1482
+ assert.match(prompt, /SQL.*injection/i);
1483
+ assert.match(prompt, /[Cc]ommand injection/);
1484
+ assert.match(prompt, /[Aa]uthn? \/ authz|requireRank|auth.*bypass/i);
1485
+ assert.match(prompt, /[Hh]ardcoded secret/);
1486
+ assert.match(prompt, /[Tt]rust[- ]boundary/);
1487
+ // Severity ladder
1488
+ assert.match(prompt, /critical/i);
1489
+ assert.match(prompt, /[Hh]igh-severity/);
1490
+ assert.match(prompt, /[Mm]edium-severity/);
1491
+ // Read budget
1492
+ assert.match(prompt, /5 Read calls/);
1493
+ // New JSON fields
1494
+ assert.match(prompt, /security_score/);
1495
+ assert.match(prompt, /surfaces_checked/);
1496
+ });
1497
+
1498
+ // --- hacker.computeVerdict ---
1499
+
1500
+ test('v4 hacker: clean security (sec>=6, no blocker, conf>=0.4) → pass', () => {
1501
+ const v = grader.WORKERS.hacker.computeVerdict({
1502
+ security_score: 9,
1503
+ confidence: 0.9,
1504
+ surfaces_checked: [
1505
+ { kind: 'route', where: 'src/bongos/routes/x.js:42', verdict: 'ok' },
1506
+ ],
1507
+ issues: [],
1508
+ notes_md: 'auth wrapper present on the new route',
1509
+ }, {});
1510
+ assert.equal(v.verdict, 'pass');
1511
+ assert.equal(v.scores.security_score, 9);
1512
+ });
1513
+
1514
+ test('v4 hacker: blocker (critical/high) issue → fail', () => {
1515
+ const v = grader.WORKERS.hacker.computeVerdict({
1516
+ security_score: 3,
1517
+ confidence: 0.9,
1518
+ surfaces_checked: [{ kind: 'db_write', where: 'foo:1', verdict: 'flagged' }],
1519
+ issues: [{ severity: 'blocker', file: 'foo.js', summary: 'unparameterized SQL — concatenates req.body.id' }],
1520
+ notes_md: 'sql injection in new route',
1521
+ }, {});
1522
+ assert.equal(v.verdict, 'fail');
1523
+ });
1524
+
1525
+ test('v4 hacker: security_score below threshold → fail', () => {
1526
+ const v = grader.WORKERS.hacker.computeVerdict({
1527
+ security_score: 4, confidence: 0.9, surfaces_checked: [], issues: [], notes_md: '',
1528
+ }, {});
1529
+ assert.equal(v.verdict, 'fail');
1530
+ });
1531
+
1532
+ test('v4 hacker: medium-severity issue (major) → concerns', () => {
1533
+ const v = grader.WORKERS.hacker.computeVerdict({
1534
+ security_score: 7,
1535
+ confidence: 0.85,
1536
+ surfaces_checked: [],
1537
+ issues: [{ severity: 'major', file: 'auth.js', summary: 'MD5 used for non-password hashing — should be SHA-256' }],
1538
+ notes_md: 'defense-in-depth gap',
1539
+ }, {});
1540
+ assert.equal(v.verdict, 'concerns');
1541
+ });
1542
+
1543
+ test('v4 hacker: low-severity (minor) → pass (hardening note only)', () => {
1544
+ const v = grader.WORKERS.hacker.computeVerdict({
1545
+ security_score: 8,
1546
+ confidence: 0.9,
1547
+ surfaces_checked: [{ kind: 'route', where: 'foo:1', verdict: 'ok' }],
1548
+ issues: [{ severity: 'minor', file: 'foo.js', summary: 'consider rate-limiting on this public endpoint' }],
1549
+ notes_md: 'small hardening recommendation',
1550
+ }, {});
1551
+ assert.equal(v.verdict, 'pass');
1552
+ });
1553
+
1554
+ test('v4 hacker: low confidence → concerns even with clean scores', () => {
1555
+ const v = grader.WORKERS.hacker.computeVerdict({
1556
+ security_score: 8,
1557
+ confidence: 0.2,
1558
+ surfaces_checked: [],
1559
+ issues: [],
1560
+ notes_md: 'large diff touched auth.js — couldn\'t fully evaluate within budget',
1561
+ }, {});
1562
+ assert.equal(v.verdict, 'concerns');
1563
+ });
1564
+
1565
+ test('v4 hacker: subprocess error → fail with error tag', () => {
1566
+ const v = grader.WORKERS.hacker.computeVerdict(null, { subprocess_error: 'SUBAGENT_TIMEOUT' });
1567
+ assert.equal(v.verdict, 'fail');
1568
+ assert.equal(v.error, 'SUBAGENT_TIMEOUT');
1569
+ });
1570
+
1571
+ // --- 3-worker panel ---
1572
+
1573
+ test('v4 aggregatePanel: 3-worker unanimous PASS → panel pass + security_score recorded', () => {
1574
+ const r = grader.aggregatePanel([
1575
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1576
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1577
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.90),
1578
+ ]);
1579
+ assert.equal(r.passed, true);
1580
+ assert.equal(r.signals.panel_outcome, 'pass');
1581
+ assert.equal(r.signals.worker_count, 3);
1582
+ // security_score appears in rubric_scores for the audit trail
1583
+ assert.equal(r.rubric_scores.security_score, 9);
1584
+ });
1585
+
1586
+ test('v4 aggregatePanel: 3-worker — Hacker fails, blocks the ship', () => {
1587
+ const r = grader.aggregatePanel([
1588
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1589
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1590
+ mkWorkerResult('hacker', 'fail', { security_score: 3 }, 0.90,
1591
+ [{ severity: 'blocker', file: 'auth.js', summary: 'authn bypass' }]),
1592
+ ]);
1593
+ assert.equal(r.passed, false);
1594
+ assert.equal(r.signals.panel_outcome, 'fail');
1595
+ assert.equal(r.signals.fail_count, 1);
1596
+ });
1597
+
1598
+ // ========================================================================
1599
+ // v4 Phase 2.2 — Efficiency Monger worker (#421)
1600
+ // ========================================================================
1601
+
1602
+ test('v4 efficiency worker: exposes the expected contract', () => {
1603
+ const e = grader.WORKERS.efficiency;
1604
+ assert.equal(e.KIND, 'efficiency');
1605
+ assert.equal(typeof e.buildPrompt, 'function');
1606
+ assert.equal(typeof e.extractJson, 'function');
1607
+ assert.equal(typeof e.computeVerdict, 'function');
1608
+ assert.match(e.SPECIALTY, /runtime|performance|hot[- ]?path/i);
1609
+ });
1610
+
1611
+ test('v4 efficiency prompt: includes default-fail framing + runtime-cost failure modes', () => {
1612
+ const prompt = grader.WORKERS.efficiency.buildPrompt({
1613
+ task: { id: 1, title: 't', description: 'd', est_minutes: 60 },
1614
+ valueSummary: 's',
1615
+ diff: 'd',
1616
+ changedFiles: ['x'],
1617
+ });
1618
+ assert.match(prompt, /Efficiency Monger/);
1619
+ assert.match(prompt, /default verdict is FAIL/i);
1620
+ // Failure mode classes
1621
+ assert.match(prompt, /N\+1/);
1622
+ assert.match(prompt, /O\(n²\)|O\(n\^?2\)/);
1623
+ assert.match(prompt, /[Ss]ync I\/O/);
1624
+ // Distinct-from-economy framing
1625
+ assert.match(prompt, /diff size/i);
1626
+ assert.match(prompt, /runtime cost/i);
1627
+ // Read budget
1628
+ assert.match(prompt, /5 Read calls/);
1629
+ // New JSON fields
1630
+ assert.match(prompt, /efficiency_score/);
1631
+ assert.match(prompt, /expensive_patterns_checked/);
1632
+ });
1633
+
1634
+ // --- efficiency.computeVerdict ---
1635
+
1636
+ test('v4 efficiency: clean runtime (eff>=6, no blocker, conf>=0.4) → pass', () => {
1637
+ const v = grader.WORKERS.efficiency.computeVerdict({
1638
+ efficiency_score: 9,
1639
+ confidence: 0.9,
1640
+ expensive_patterns_checked: [
1641
+ { kind: 'loop', where: 'foo:1', verdict: 'ok', note: 'bounded by config' },
1642
+ ],
1643
+ issues: [],
1644
+ notes_md: 'hot path checked',
1645
+ }, {});
1646
+ assert.equal(v.verdict, 'pass');
1647
+ assert.equal(v.scores.efficiency_score, 9);
1648
+ });
1649
+
1650
+ test('v4 efficiency: blocker (N+1, O(n²), sync-I/O-on-hot-path) → fail', () => {
1651
+ const v = grader.WORKERS.efficiency.computeVerdict({
1652
+ efficiency_score: 3,
1653
+ confidence: 0.9,
1654
+ expensive_patterns_checked: [{ kind: 'query', where: 'foo:1', verdict: 'flagged', note: 'N+1' }],
1655
+ issues: [{ severity: 'blocker', file: 'foo.js', summary: 'N+1 query in request handler loop' }],
1656
+ notes_md: 'per-request N+1',
1657
+ }, {});
1658
+ assert.equal(v.verdict, 'fail');
1659
+ });
1660
+
1661
+ test('v4 efficiency: efficiency_score below threshold → fail', () => {
1662
+ const v = grader.WORKERS.efficiency.computeVerdict({
1663
+ efficiency_score: 4, confidence: 0.9, expensive_patterns_checked: [], issues: [], notes_md: '',
1664
+ }, {});
1665
+ assert.equal(v.verdict, 'fail');
1666
+ });
1667
+
1668
+ test('v4 efficiency: medium-severity (major) → concerns', () => {
1669
+ const v = grader.WORKERS.efficiency.computeVerdict({
1670
+ efficiency_score: 7,
1671
+ confidence: 0.85,
1672
+ expensive_patterns_checked: [],
1673
+ issues: [{ severity: 'major', file: 'q.js', summary: 'missing index on WHERE column' }],
1674
+ notes_md: 'index gap',
1675
+ }, {});
1676
+ assert.equal(v.verdict, 'concerns');
1677
+ });
1678
+
1679
+ test('v4 efficiency: low confidence → concerns', () => {
1680
+ const v = grader.WORKERS.efficiency.computeVerdict({
1681
+ efficiency_score: 8,
1682
+ confidence: 0.2,
1683
+ expensive_patterns_checked: [],
1684
+ issues: [],
1685
+ notes_md: 'large diff — could not establish call frequency for new function within budget',
1686
+ }, {});
1687
+ assert.equal(v.verdict, 'concerns');
1688
+ });
1689
+
1690
+ test('v4 efficiency: subprocess error → fail with error tag', () => {
1691
+ const v = grader.WORKERS.efficiency.computeVerdict(null, { subprocess_error: 'SUBAGENT_TIMEOUT' });
1692
+ assert.equal(v.verdict, 'fail');
1693
+ assert.equal(v.error, 'SUBAGENT_TIMEOUT');
1694
+ });
1695
+
1696
+ // --- 4-worker panel ---
1697
+
1698
+ test('v4 aggregatePanel: 4-worker unanimous PASS → panel pass + all 4 score axes recorded', () => {
1699
+ const r = grader.aggregatePanel([
1700
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1701
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1702
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.90),
1703
+ mkWorkerResult('efficiency', 'pass', { efficiency_score: 8 }, 0.85),
1704
+ ]);
1705
+ assert.equal(r.passed, true);
1706
+ assert.equal(r.signals.panel_outcome, 'pass');
1707
+ assert.equal(r.signals.worker_count, 4);
1708
+ // All 4 score axes appear in rubric_scores
1709
+ assert.equal(r.rubric_scores.functional_fidelity, 8);
1710
+ assert.equal(r.rubric_scores.code_quality, 8);
1711
+ assert.equal(r.rubric_scores.economy, 7);
1712
+ assert.equal(r.rubric_scores.security_score, 9);
1713
+ assert.equal(r.rubric_scores.efficiency_score, 8);
1714
+ });
1715
+
1716
+ test('v4 aggregatePanel: 4-worker — Efficiency Monger fails, blocks the ship', () => {
1717
+ const r = grader.aggregatePanel([
1718
+ mkWorkerResult('narc', 'pass', { functional_fidelity: 8 }, 0.85),
1719
+ mkWorkerResult('quality', 'pass', { code_quality: 8, economy: 7 }, 0.80),
1720
+ mkWorkerResult('hacker', 'pass', { security_score: 9 }, 0.90),
1721
+ mkWorkerResult('efficiency', 'fail', { efficiency_score: 3 }, 0.90,
1722
+ [{ severity: 'blocker', file: 'q.js', summary: 'N+1 in hot path' }]),
1723
+ ]);
1724
+ assert.equal(r.passed, false);
1725
+ assert.equal(r.signals.panel_outcome, 'fail');
1726
+ assert.equal(r.signals.fail_count, 1);
1727
+ });
1728
+
1729
+ // ========================================================================
1730
+ // v4 Phase 2.5 — server audit gap fix (#423)
1731
+ // ========================================================================
1732
+ // validateAndScore is what the server runs on the POST body. After Phases
1733
+ // 2.1 + 2.2, the local aggregatePanel computed security_score +
1734
+ // efficiency_score but the server-recorded rubric_scores didn't include
1735
+ // them because the POST body didn't forward them AND validateAndScore
1736
+ // didn't read them. Phase 2.5 closes both halves.
1737
+
1738
+ test('v4 Phase 2.5: validateAndScore accepts security_score → recorded in rubric_scores', () => {
1739
+ const r = grader.validateAndScore({
1740
+ functional_fidelity: 8,
1741
+ code_quality: 8,
1742
+ economy: 7,
1743
+ security_score: 9,
1744
+ notes_md: 'panel forwarded by ship.js',
1745
+ confidence: 0.85,
1746
+ issues: [],
1747
+ });
1748
+ assert.equal(r.passed, true);
1749
+ assert.equal(r.rubric_scores.security_score, 9);
1750
+ // The gate is UNCHANGED — only ff + cq drive pass/fail.
1751
+ assert.equal(r.score, 8); // blended = (8+8)/2
1752
+ });
1753
+
1754
+ test('v4 Phase 2.5: validateAndScore accepts efficiency_score → recorded in rubric_scores', () => {
1755
+ const r = grader.validateAndScore({
1756
+ functional_fidelity: 8,
1757
+ code_quality: 8,
1758
+ economy: 7,
1759
+ efficiency_score: 7,
1760
+ notes_md: 'panel forwarded',
1761
+ confidence: 0.85,
1762
+ issues: [],
1763
+ });
1764
+ assert.equal(r.passed, true);
1765
+ assert.equal(r.rubric_scores.efficiency_score, 7);
1766
+ });
1767
+
1768
+ test('v4 Phase 2.5: validateAndScore accepts both new fields together', () => {
1769
+ const r = grader.validateAndScore({
1770
+ functional_fidelity: 8,
1771
+ code_quality: 8,
1772
+ economy: 7,
1773
+ security_score: 9,
1774
+ efficiency_score: 8,
1775
+ notes_md: 'full panel',
1776
+ confidence: 0.85,
1777
+ issues: [],
1778
+ });
1779
+ assert.equal(r.passed, true);
1780
+ assert.equal(r.rubric_scores.security_score, 9);
1781
+ assert.equal(r.rubric_scores.efficiency_score, 8);
1782
+ });
1783
+
1784
+ test('v4 Phase 2.5: missing security_score + efficiency_score → recorded as null (back-compat)', () => {
1785
+ // Legacy callers (Phase 2.0 era or v3 single-shot diagnostics) that
1786
+ // don't include the new fields must grade EXACTLY as before.
1787
+ const r = grader.validateAndScore({
1788
+ functional_fidelity: 8,
1789
+ code_quality: 8,
1790
+ notes_md: 'no new fields',
1791
+ confidence: 0.85,
1792
+ issues: [],
1793
+ });
1794
+ assert.equal(r.passed, true);
1795
+ assert.equal(r.rubric_scores.security_score, null);
1796
+ assert.equal(r.rubric_scores.efficiency_score, null);
1797
+ });
1798
+
1799
+ test('v4 Phase 2.5: out-of-range security_score → recorded as null, no penalty', () => {
1800
+ const r = grader.validateAndScore({
1801
+ functional_fidelity: 8, code_quality: 8,
1802
+ security_score: 42, // out of range
1803
+ notes_md: 'bad input', confidence: 0.85, issues: [],
1804
+ });
1805
+ assert.equal(r.passed, true);
1806
+ assert.equal(r.rubric_scores.security_score, null);
1807
+ });
1808
+
1809
+ test('v4 Phase 2.5: low security_score does NOT independently fail (advisory only)', () => {
1810
+ // The gate is unchanged: only ff + cq drive pass/fail. A low security_score
1811
+ // is recorded for trending but doesn't gate the ship by itself. Hacker's
1812
+ // per-worker verdict drives the panel gate via confidence-forwarding —
1813
+ // that path is exercised in the aggregatePanel tests, not here.
1814
+ const r = grader.validateAndScore({
1815
+ functional_fidelity: 8, code_quality: 8, security_score: 2,
1816
+ notes_md: '', confidence: 0.85, issues: [],
1817
+ });
1818
+ assert.equal(r.passed, true, 'security_score is advisory at validateAndScore — does NOT hard-block');
1819
+ assert.equal(r.rubric_scores.security_score, 2);
1820
+ });
1821
+
1822
+ test('v4 Phase 2.5: low efficiency_score does NOT independently fail (advisory only)', () => {
1823
+ const r = grader.validateAndScore({
1824
+ functional_fidelity: 8, code_quality: 8, efficiency_score: 2,
1825
+ notes_md: '', confidence: 0.85, issues: [],
1826
+ });
1827
+ assert.equal(r.passed, true);
1828
+ assert.equal(r.rubric_scores.efficiency_score, 2);
1829
+ });
1830
+
1831
+ // ---------- v4.1 trivial-diff fast-path (#513) ----------
1832
+
1833
+ test('isTrivialDiff: small static front-end diff (CSS) → true', () => {
1834
+ assert.equal(
1835
+ grader.isTrivialDiff({
1836
+ // the hall surface carved to modules/hall-ui/public/ in BV1.R85
1837
+ changedFiles: ['modules/hall-ui/public/styles.css'],
1838
+ diffStats: { files_changed: 1, lines_added: 2, lines_removed: 1 },
1839
+ }),
1840
+ true
1841
+ );
1842
+ });
1843
+
1844
+ test('isTrivialDiff: executable front-end (JS, HTML, SVG) is NEVER trivial (#513 Hacker)', () => {
1845
+ // The #506 case (a copy string inside modules/hall-ui/public/builders.js) now
1846
+ // correctly takes the full panel: a path+size heuristic can't tell a copy
1847
+ // tweak from an XSS / client-authz regression. HTML and SVG are executable
1848
+ // too (inline <script>, on* handlers), so they get the panel as well.
1849
+ for (const f of [
1850
+ 'modules/hall-ui/public/builders.js', 'public/game/main.js', 'modules/status-ui/public/app.mjs',
1851
+ 'public/index.html', 'modules/hall-ui/public/welcome.htm', 'public/assets/icon.svg',
1852
+ ]) {
1853
+ assert.equal(
1854
+ grader.isTrivialDiff({ changedFiles: [f], diffStats: { files_changed: 1, lines_added: 1, lines_removed: 1 } }),
1855
+ false,
1856
+ `${f} must not be trivial (executable front-end)`
1857
+ );
1858
+ }
1859
+ });
1860
+
1861
+ test('isTrivialDiff: docs/markdown-only diff → true', () => {
1862
+ assert.equal(
1863
+ grader.isTrivialDiff({
1864
+ changedFiles: ['docs/architecture.md', 'README.md'],
1865
+ diffStats: { files_changed: 2, lines_added: 6, lines_removed: 2 },
1866
+ }),
1867
+ true
1868
+ );
1869
+ });
1870
+
1871
+ test('isTrivialDiff: a logic-path file (src/) → false even when small', () => {
1872
+ assert.equal(
1873
+ grader.isTrivialDiff({
1874
+ changedFiles: ['src/bongos/routes/tasks.js'],
1875
+ diffStats: { files_changed: 1, lines_added: 2, lines_removed: 1 },
1876
+ }),
1877
+ false
1878
+ );
1879
+ });
1880
+
1881
+ test('isTrivialDiff: scripts/ and tests/ are logic paths → false', () => {
1882
+ assert.equal(
1883
+ grader.isTrivialDiff({
1884
+ changedFiles: ['scripts/gds/smoke-write.sh'],
1885
+ diffStats: { files_changed: 1, lines_added: 2, lines_removed: 1 },
1886
+ }),
1887
+ false
1888
+ );
1889
+ assert.equal(
1890
+ grader.isTrivialDiff({
1891
+ changedFiles: ['tests/grader.mjs'],
1892
+ diffStats: { files_changed: 1, lines_added: 3, lines_removed: 0 },
1893
+ }),
1894
+ false
1895
+ );
1896
+ });
1897
+
1898
+ test('isTrivialDiff: config/data files (.json/.yaml/.env) are NOT trivial (#513 Hacker)', () => {
1899
+ for (const f of ['public/config.json', 'modules/hall-ui/public/settings.yaml', 'public/.env']) {
1900
+ assert.equal(
1901
+ grader.isTrivialDiff({ changedFiles: [f], diffStats: { files_changed: 1, lines_added: 2, lines_removed: 0 } }),
1902
+ false,
1903
+ `${f} must not be trivial (config/data)`
1904
+ );
1905
+ }
1906
+ });
1907
+
1908
+ test('isTrivialDiff: oversized diff (too many lines) → false', () => {
1909
+ // Use a trivial-eligible extension so this exercises the SIZE gate, not the
1910
+ // extension gate.
1911
+ assert.equal(
1912
+ grader.isTrivialDiff({
1913
+ changedFiles: ['public/styles.css'],
1914
+ diffStats: { files_changed: 1, lines_added: 40, lines_removed: 0 },
1915
+ }),
1916
+ false
1917
+ );
1918
+ });
1919
+
1920
+ test('isTrivialDiff: too many files → false', () => {
1921
+ assert.equal(
1922
+ grader.isTrivialDiff({
1923
+ changedFiles: ['docs/a.md', 'docs/b.md', 'docs/c.md', 'docs/d.md'],
1924
+ diffStats: { files_changed: 4, lines_added: 4, lines_removed: 0 },
1925
+ }),
1926
+ false
1927
+ );
1928
+ });
1929
+
1930
+ test('isTrivialDiff: mix of copy + logic path → false (any logic path disqualifies)', () => {
1931
+ assert.equal(
1932
+ grader.isTrivialDiff({
1933
+ changedFiles: ['docs/x.md', 'src/a.js'],
1934
+ diffStats: { files_changed: 2, lines_added: 4, lines_removed: 0 },
1935
+ }),
1936
+ false
1937
+ );
1938
+ });
1939
+
1940
+ test('isTrivialDiff: missing diffStats → false (fail-safe to full panel)', () => {
1941
+ assert.equal(grader.isTrivialDiff({ changedFiles: ['docs/x.md'], diffStats: null }), false);
1942
+ assert.equal(grader.isTrivialDiff({ changedFiles: ['docs/x.md'] }), false);
1943
+ });
1944
+
1945
+ test('isTrivialDiff: empty changed-files or zero-line diff → false', () => {
1946
+ assert.equal(
1947
+ grader.isTrivialDiff({ changedFiles: [], diffStats: { files_changed: 0, lines_added: 0, lines_removed: 0 } }),
1948
+ false
1949
+ );
1950
+ assert.equal(
1951
+ grader.isTrivialDiff({ changedFiles: ['docs/x.md'], diffStats: { files_changed: 1, lines_added: 0, lines_removed: 0 } }),
1952
+ false
1953
+ );
1954
+ });
1955
+
1956
+ // ---------- Summary ----------
1957
+
1958
+ console.log('');
1959
+ console.log(`Grader tests: ${passed} passed, ${failed} failed`);
1960
+ if (failed > 0) process.exit(1);