limn-review 0.1.123

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 (474) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +133 -0
  3. package/dist/ai/runtime.js +535 -0
  4. package/dist/api/plugins/auth.js +169 -0
  5. package/dist/api/plugins/error-handler.js +114 -0
  6. package/dist/api/plugins/rate-limit.js +855 -0
  7. package/dist/api/plugins/security.js +321 -0
  8. package/dist/api/routes/activity.js +103 -0
  9. package/dist/api/routes/ai-runtime.js +67 -0
  10. package/dist/api/routes/auth.js +194 -0
  11. package/dist/api/routes/billing.js +209 -0
  12. package/dist/api/routes/bot-triage.js +1289 -0
  13. package/dist/api/routes/conflicts.js +730 -0
  14. package/dist/api/routes/contact.js +318 -0
  15. package/dist/api/routes/daily-brief.js +58 -0
  16. package/dist/api/routes/flow.js +71 -0
  17. package/dist/api/routes/health.js +7 -0
  18. package/dist/api/routes/insights.js +200 -0
  19. package/dist/api/routes/me.js +458 -0
  20. package/dist/api/routes/mentions.js +22 -0
  21. package/dist/api/routes/mergers.js +8 -0
  22. package/dist/api/routes/ml-labels.js +109 -0
  23. package/dist/api/routes/open-prs.js +35 -0
  24. package/dist/api/routes/prs.js +1633 -0
  25. package/dist/api/routes/reactions.js +217 -0
  26. package/dist/api/routes/repos.js +563 -0
  27. package/dist/api/routes/search.js +41 -0
  28. package/dist/api/routes/threads.js +118 -0
  29. package/dist/api/routes/timeline.js +150 -0
  30. package/dist/api/routes/users.js +67 -0
  31. package/dist/api/routes/webhooks.js +181 -0
  32. package/dist/api/routes/workspaces.js +322 -0
  33. package/dist/app.js +274 -0
  34. package/dist/ascii.js +19 -0
  35. package/dist/auth/account.js +482 -0
  36. package/dist/auth/crypto.js +46 -0
  37. package/dist/bot-triage/resolve.js +59 -0
  38. package/dist/cli.js +388 -0
  39. package/dist/coding/activity.js +80 -0
  40. package/dist/coding/agent.js +288 -0
  41. package/dist/coding/ai-fix/auto-fix.js +212 -0
  42. package/dist/coding/ai-fix/manager.js +370 -0
  43. package/dist/coding/ai-fix/persist.js +213 -0
  44. package/dist/coding/ai-fix/pr-context.js +58 -0
  45. package/dist/coding/ai-fix/prompts.js +138 -0
  46. package/dist/coding/ai-fix/review-seed.js +357 -0
  47. package/dist/coding/ai-fix/routes.js +425 -0
  48. package/dist/coding/git-ops.js +321 -0
  49. package/dist/coding/git.js +184 -0
  50. package/dist/coding/merge.js +262 -0
  51. package/dist/coding/safe-path.js +75 -0
  52. package/dist/coding/schema.js +43 -0
  53. package/dist/config.js +648 -0
  54. package/dist/conflict/diff.js +304 -0
  55. package/dist/conflict/git.js +118 -0
  56. package/dist/conflict/hash.js +73 -0
  57. package/dist/conflict/janitor.js +218 -0
  58. package/dist/conflict/land.js +675 -0
  59. package/dist/conflict/model-types.js +2 -0
  60. package/dist/conflict/model.js +1081 -0
  61. package/dist/conflict/parse.js +142 -0
  62. package/dist/conflict/seam.js +136 -0
  63. package/dist/conflict/session.js +532 -0
  64. package/dist/conflict/suggestion.js +398 -0
  65. package/dist/conflict/tokens.js +72 -0
  66. package/dist/data-dir.js +112 -0
  67. package/dist/db/actor-lanes.js +213 -0
  68. package/dist/db/automation-ids.js +97 -0
  69. package/dist/db/automation-output.js +349 -0
  70. package/dist/db/blast-radius-query.js +36 -0
  71. package/dist/db/blast-radius.js +226 -0
  72. package/dist/db/bot-dormancy.js +115 -0
  73. package/dist/db/bot-overlap.js +579 -0
  74. package/dist/db/bot-volume.js +722 -0
  75. package/dist/db/bot-window.js +35 -0
  76. package/dist/db/branch-queries.js +310 -0
  77. package/dist/db/change-shape.js +243 -0
  78. package/dist/db/changepoint.js +75 -0
  79. package/dist/db/cleanup.js +36 -0
  80. package/dist/db/client.js +102 -0
  81. package/dist/db/code-loc.js +388 -0
  82. package/dist/db/credits.js +79 -0
  83. package/dist/db/daily-brief.js +406 -0
  84. package/dist/db/dependency-cards.js +145 -0
  85. package/dist/db/erase-account.js +255 -0
  86. package/dist/db/export-account.js +189 -0
  87. package/dist/db/failing-checks.js +124 -0
  88. package/dist/db/file-coupling.js +197 -0
  89. package/dist/db/flow-detail.js +661 -0
  90. package/dist/db/flow-pointers.js +197 -0
  91. package/dist/db/flow-settings.js +67 -0
  92. package/dist/db/forecast.js +114 -0
  93. package/dist/db/line-overlap.js +81 -0
  94. package/dist/db/merge-queue.js +189 -0
  95. package/dist/db/migrate.js +17 -0
  96. package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
  97. package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
  98. package/dist/db/migrations/0002_famous_sersi.sql +9 -0
  99. package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
  100. package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
  101. package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
  102. package/dist/db/migrations/0006_cute_violations.sql +45 -0
  103. package/dist/db/migrations/0007_dark_anthem.sql +2 -0
  104. package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
  105. package/dist/db/migrations/0009_lean_bodies.sql +13 -0
  106. package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
  107. package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
  108. package/dist/db/migrations/0012_findings_included_default.sql +7 -0
  109. package/dist/db/migrations/0013_review_routing.sql +9 -0
  110. package/dist/db/migrations/0014_account_last_active.sql +6 -0
  111. package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
  112. package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
  113. package/dist/db/migrations/0017_account_display_name.sql +7 -0
  114. package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
  115. package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
  116. package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
  117. package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
  118. package/dist/db/migrations/0022_ci_status_events.sql +21 -0
  119. package/dist/db/migrations/0023_ai_usage.sql +21 -0
  120. package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
  121. package/dist/db/migrations/0025_account_billing.sql +7 -0
  122. package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
  123. package/dist/db/migrations/0027_user_github_type.sql +6 -0
  124. package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
  125. package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
  126. package/dist/db/migrations/0030_teams.sql +32 -0
  127. package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
  128. package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
  129. package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
  130. package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
  131. package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
  132. package/dist/db/migrations/0036_search_index.sql +25 -0
  133. package/dist/db/migrations/0037_author_indexes.sql +15 -0
  134. package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
  135. package/dist/db/migrations/0039_branch_status.sql +44 -0
  136. package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
  137. package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
  138. package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
  139. package/dist/db/migrations/0043_account_reviewers.sql +131 -0
  140. package/dist/db/migrations/0044_workspaces.sql +138 -0
  141. package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
  142. package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
  143. package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
  144. package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
  145. package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
  146. package/dist/db/migrations/0050_users_app_slug.sql +12 -0
  147. package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
  148. package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
  149. package/dist/db/migrations/0053_automation_roles.sql +79 -0
  150. package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
  151. package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
  152. package/dist/db/migrations/0056_pr_mentions.sql +22 -0
  153. package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
  154. package/dist/db/migrations/0058_pending_mute.sql +53 -0
  155. package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
  156. package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
  157. package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
  158. package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
  159. package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
  160. package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
  161. package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
  162. package/dist/db/migrations/0066_review_request_events.sql +31 -0
  163. package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
  164. package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
  165. package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
  166. package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
  167. package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
  168. package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
  169. package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
  170. package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
  171. package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
  172. package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
  173. package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
  174. package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
  175. package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
  176. package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
  177. package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
  178. package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
  179. package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
  180. package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
  181. package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
  182. package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
  183. package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
  184. package/dist/db/migrations/meta/_journal.json +566 -0
  185. package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
  186. package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
  187. package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
  188. package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
  189. package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
  190. package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
  191. package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
  192. package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
  193. package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
  194. package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
  195. package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
  196. package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
  197. package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
  198. package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
  199. package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
  200. package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
  201. package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
  202. package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
  203. package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
  204. package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
  205. package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
  206. package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
  207. package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
  208. package/dist/db/migrations-pg/0023_search_index.sql +19 -0
  209. package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
  210. package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
  211. package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
  212. package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
  213. package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
  214. package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
  215. package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
  216. package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
  217. package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
  218. package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
  219. package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
  220. package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
  221. package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
  222. package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
  223. package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
  224. package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
  225. package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
  226. package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
  227. package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
  228. package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
  229. package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
  230. package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
  231. package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
  232. package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
  233. package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
  234. package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
  235. package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
  236. package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
  237. package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
  238. package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
  239. package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
  240. package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
  241. package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
  242. package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
  243. package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
  244. package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
  245. package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
  246. package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
  247. package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
  248. package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
  249. package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
  250. package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
  251. package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
  252. package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
  253. package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
  254. package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
  255. package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
  256. package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
  257. package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
  258. package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
  259. package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
  260. package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
  261. package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
  262. package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
  263. package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
  264. package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
  265. package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
  266. package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
  267. package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
  268. package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
  269. package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
  270. package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
  271. package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
  272. package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
  273. package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
  274. package/dist/db/migrations-pg/meta/_journal.json +475 -0
  275. package/dist/db/ml-labels.js +1666 -0
  276. package/dist/db/my-turn-dismissals.js +343 -0
  277. package/dist/db/my-turn-settings.js +34 -0
  278. package/dist/db/pending-classify.js +46 -0
  279. package/dist/db/pending-mute.js +171 -0
  280. package/dist/db/pending-tabs.js +238 -0
  281. package/dist/db/period-metrics.js +820 -0
  282. package/dist/db/person-period.js +763 -0
  283. package/dist/db/pr-intervals.js +930 -0
  284. package/dist/db/pr-liveness.js +240 -0
  285. package/dist/db/pr-mentions.js +279 -0
  286. package/dist/db/pr-merge-queue-stamp.js +91 -0
  287. package/dist/db/queries.js +14631 -0
  288. package/dist/db/reaction-targets.js +94 -0
  289. package/dist/db/repo-activity.js +171 -0
  290. package/dist/db/retention.js +138 -0
  291. package/dist/db/review-threads-for-review.js +240 -0
  292. package/dist/db/run-migrations.js +24 -0
  293. package/dist/db/schema.pg.js +1220 -0
  294. package/dist/db/schema.sqlite.js +1801 -0
  295. package/dist/db/search.js +317 -0
  296. package/dist/db/security-alerts.js +218 -0
  297. package/dist/db/synthesis-input.js +565 -0
  298. package/dist/db/triage.js +320 -0
  299. package/dist/db/usage.js +63 -0
  300. package/dist/db/work-plan.js +1086 -0
  301. package/dist/db/working-hours.js +161 -0
  302. package/dist/feed/my-turn.js +122 -0
  303. package/dist/github/actions-logs.js +397 -0
  304. package/dist/github/auth.js +86 -0
  305. package/dist/github/branch-queries.js +229 -0
  306. package/dist/github/client.js +454 -0
  307. package/dist/github/codeowners.js +221 -0
  308. package/dist/github/commit-checks.js +105 -0
  309. package/dist/github/compare.js +110 -0
  310. package/dist/github/diff-anchor.js +227 -0
  311. package/dist/github/mutations.js +706 -0
  312. package/dist/github/pr-file-content.js +254 -0
  313. package/dist/github/pr-liveness.js +161 -0
  314. package/dist/github/queries.js +875 -0
  315. package/dist/github/rate-budget.js +129 -0
  316. package/dist/github/reactions.js +161 -0
  317. package/dist/github/reviewer-suggest.js +104 -0
  318. package/dist/github/team-reviewers.js +96 -0
  319. package/dist/index.js +184 -0
  320. package/dist/merge/auto-merge-runner.js +960 -0
  321. package/dist/ml/severity-client.js +218 -0
  322. package/dist/pr/detail-enricher.js +26 -0
  323. package/dist/pro/bind.js +302 -0
  324. package/dist/pro/contract.js +37 -0
  325. package/dist/pro/migrate.js +64 -0
  326. package/dist/review/agent-context.js +127 -0
  327. package/dist/review/agent.js +385 -0
  328. package/dist/review/agentic.js +59 -0
  329. package/dist/review/auth.js +94 -0
  330. package/dist/review/chat-agent.js +179 -0
  331. package/dist/review/claude-review/ai-ready.js +16 -0
  332. package/dist/review/claude-review/auto-settings.js +139 -0
  333. package/dist/review/claude-review/auto.js +244 -0
  334. package/dist/review/claude-review/chat.js +489 -0
  335. package/dist/review/claude-review/ci-failures.js +350 -0
  336. package/dist/review/claude-review/follow-up.js +310 -0
  337. package/dist/review/claude-review/manager.js +686 -0
  338. package/dist/review/claude-review/persist.js +996 -0
  339. package/dist/review/claude-review/prompts.js +357 -0
  340. package/dist/review/claude-review/routes.js +518 -0
  341. package/dist/review/claude-review/routing.js +61 -0
  342. package/dist/review/claude-review/specialists.js +190 -0
  343. package/dist/review/claude-review/threads.js +265 -0
  344. package/dist/review/claude-review/ticket.js +282 -0
  345. package/dist/review/clone-hygiene.js +380 -0
  346. package/dist/review/clone-manager.js +367 -0
  347. package/dist/review/llm.js +113 -0
  348. package/dist/review/model-options.js +48 -0
  349. package/dist/review/plugin-providers.js +12 -0
  350. package/dist/review/post-review.js +216 -0
  351. package/dist/review/post-seam.js +161 -0
  352. package/dist/review/prepare.js +168 -0
  353. package/dist/review/pricing.js +17 -0
  354. package/dist/review/schema.js +156 -0
  355. package/dist/review/submit-map.js +41 -0
  356. package/dist/review/usage.js +90 -0
  357. package/dist/shared/claude-review.js +335 -0
  358. package/dist/shared/conflict-fold.js +151 -0
  359. package/dist/shared/conflicts.js +30 -0
  360. package/dist/shared/flow-settings.js +191 -0
  361. package/dist/shared/index.js +8 -0
  362. package/dist/shared/my-turn-settings.js +238 -0
  363. package/dist/shared/pending-rules.js +281 -0
  364. package/dist/shared/product.js +54 -0
  365. package/dist/shared/types.js +896 -0
  366. package/dist/status.js +386 -0
  367. package/dist/sync/adaptive.js +185 -0
  368. package/dist/sync/app-attribution.js +66 -0
  369. package/dist/sync/auth-notices.js +36 -0
  370. package/dist/sync/backfill-ci-history.js +315 -0
  371. package/dist/sync/backfill-commit-headlines.js +136 -0
  372. package/dist/sync/backfill-pr-security.js +175 -0
  373. package/dist/sync/backfill-review-requests.js +97 -0
  374. package/dist/sync/benchmark-rollup.js +50 -0
  375. package/dist/sync/bot-detection.js +309 -0
  376. package/dist/sync/bot-resolution-markers.js +30 -0
  377. package/dist/sync/branch-status.js +634 -0
  378. package/dist/sync/classify-change-shape.js +150 -0
  379. package/dist/sync/commit-files.js +74 -0
  380. package/dist/sync/derive-thread-state.js +99 -0
  381. package/dist/sync/drain-review-threads.js +212 -0
  382. package/dist/sync/hydrate-detail.js +482 -0
  383. package/dist/sync/mention-scan.js +89 -0
  384. package/dist/sync/ml-enrichment.js +478 -0
  385. package/dist/sync/pr-change-signal.js +96 -0
  386. package/dist/sync/pr-liveness-sweep.js +139 -0
  387. package/dist/sync/pr-settle.js +381 -0
  388. package/dist/sync/refresh-pr.js +163 -0
  389. package/dist/sync/reparse-vendor-badges.js +495 -0
  390. package/dist/sync/resync-after-write.js +280 -0
  391. package/dist/sync/review-fingerprint.js +117 -0
  392. package/dist/sync/reviewer-behavior.js +103 -0
  393. package/dist/sync/reviewer-classify.js +503 -0
  394. package/dist/sync/routing-files.js +97 -0
  395. package/dist/sync/scheduled-jobs.js +12 -0
  396. package/dist/sync/scheduler.js +159 -0
  397. package/dist/sync/security-detect.js +521 -0
  398. package/dist/sync/sync-manager.js +750 -0
  399. package/dist/sync/sync-one-pr.js +205 -0
  400. package/dist/sync/sync-repo.js +400 -0
  401. package/dist/sync/unsettled-prs.js +506 -0
  402. package/dist/sync/upsert.js +1190 -0
  403. package/package.json +56 -0
  404. package/public/apple-touch-icon.png +0 -0
  405. package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
  406. package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
  407. package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
  408. package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  409. package/public/assets/highlight-BEHUn5zE.css +10 -0
  410. package/public/assets/highlight-CIIZLa6Z.js +12 -0
  411. package/public/assets/index-BDnYs6zg.js +73 -0
  412. package/public/assets/index-mCuI--RC.css +1 -0
  413. package/public/assets/markdown-I_JLLVAZ.js +32 -0
  414. package/public/assets/perfProbe-Da13EhTE.js +1 -0
  415. package/public/assets/tanstack-5qsxxaLe.js +17 -0
  416. package/public/assets/vis-C3BCkP5U.js +1280 -0
  417. package/public/assets/vis-ziCVehMy.css +1 -0
  418. package/public/favicon-16.png +0 -0
  419. package/public/favicon-32.png +0 -0
  420. package/public/favicon-48.png +0 -0
  421. package/public/favicon.svg +5 -0
  422. package/public/index.html +41 -0
  423. package/public-landing/apple-touch-icon.png +0 -0
  424. package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
  425. package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  426. package/public-landing/assets/index-CA3eEGXO.js +40 -0
  427. package/public-landing/assets/index-naQrQ6Fe.css +1 -0
  428. package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
  429. package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
  430. package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
  431. package/public-landing/bots/index.html +156 -0
  432. package/public-landing/contact/index.html +156 -0
  433. package/public-landing/cookies/index.html +156 -0
  434. package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
  435. package/public-landing/demo/limn-walkthrough.cues.json +44 -0
  436. package/public-landing/demo/limn-walkthrough.mp4 +0 -0
  437. package/public-landing/favicon-16.png +0 -0
  438. package/public-landing/favicon-32.png +0 -0
  439. package/public-landing/favicon-48.png +0 -0
  440. package/public-landing/favicon.svg +5 -0
  441. package/public-landing/features/index.html +156 -0
  442. package/public-landing/for-developers/index.html +156 -0
  443. package/public-landing/for-managers/index.html +156 -0
  444. package/public-landing/how-it-works/index.html +156 -0
  445. package/public-landing/how-we-measure/index.html +156 -0
  446. package/public-landing/icon-192.png +0 -0
  447. package/public-landing/icon-512.png +0 -0
  448. package/public-landing/index.html +156 -0
  449. package/public-landing/insights/index.html +156 -0
  450. package/public-landing/og-image.png +0 -0
  451. package/public-landing/pricing/index.html +156 -0
  452. package/public-landing/privacy/index.html +156 -0
  453. package/public-landing/pro/index.html +156 -0
  454. package/public-landing/reviews/index.html +156 -0
  455. package/public-landing/robots.txt +8 -0
  456. package/public-landing/shots/benchmark.png +0 -0
  457. package/public-landing/shots/bot-roi.png +0 -0
  458. package/public-landing/shots/bot-settings.png +0 -0
  459. package/public-landing/shots/chronology.png +0 -0
  460. package/public-landing/shots/feed.png +0 -0
  461. package/public-landing/shots/flow-metrics.png +0 -0
  462. package/public-landing/shots/free-reports.png +0 -0
  463. package/public-landing/shots/pending-board.png +0 -0
  464. package/public-landing/shots/pending-card.png +0 -0
  465. package/public-landing/shots/period-report.png +0 -0
  466. package/public-landing/shots/pr-detail.png +0 -0
  467. package/public-landing/shots/pr-threads.png +0 -0
  468. package/public-landing/shots/reach.png +0 -0
  469. package/public-landing/shots/repo-rows.png +0 -0
  470. package/public-landing/shots/severity-findings.png +0 -0
  471. package/public-landing/shots/severity-strip.png +0 -0
  472. package/public-landing/site.webmanifest +14 -0
  473. package/public-landing/sitemap.xml +51 -0
  474. package/public-landing/terms/index.html +156 -0
package/dist/config.js ADDED
@@ -0,0 +1,648 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ import { migrateLegacyDataDir, resolveDataDir } from './data-dir.js';
4
+ // apps/backend as the base for relative paths regardless of cwd.
5
+ const backendRoot = resolve(import.meta.dirname, '..');
6
+ // Load .env from the repo root and (optionally) apps/backend before reading
7
+ // any config. Node's loader no-ops politely if a file is missing.
8
+ for (const envPath of [
9
+ resolve(backendRoot, '../../.env'),
10
+ resolve(backendRoot, '.env'),
11
+ ]) {
12
+ try {
13
+ process.loadEnvFile(envPath);
14
+ }
15
+ catch {
16
+ /* no .env at this location */
17
+ }
18
+ }
19
+ // Exported so the Fastify factory (app.ts) can read its own numeric knobs
20
+ // (bodyLimit / requestTimeout) with the same "unset or unparseable ⇒ fallback"
21
+ // semantics as everything in `config`, without a second copy of the parser.
22
+ export function intFromEnv(key, fallback) {
23
+ const raw = process.env[key];
24
+ if (!raw)
25
+ return fallback;
26
+ const n = Number.parseInt(raw, 10);
27
+ return Number.isFinite(n) ? n : fallback;
28
+ }
29
+ function floatFromEnv(key, fallback) {
30
+ const raw = process.env[key];
31
+ if (!raw)
32
+ return fallback;
33
+ const n = Number.parseFloat(raw);
34
+ return Number.isFinite(n) ? n : fallback;
35
+ }
36
+ // The Claude Agent SDK `effort` levels (guides thinking depth + overall token
37
+ // spend). Lower effort → fewer/cheaper thinking tokens + terser output. NOTE:
38
+ // `effort` is rejected by Haiku 4.5 — only models that accept it get it (see
39
+ // review/model-options.ts EFFORT_CAPABLE_MODELS).
40
+ const REVIEW_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
41
+ function effortFromEnv(key, fallback) {
42
+ const raw = process.env[key];
43
+ return REVIEW_EFFORTS.includes(raw ?? '')
44
+ ? raw
45
+ : fallback;
46
+ }
47
+ // ---- Deployment mode (the master switch) ----
48
+ // `local` (default): SQLite via better-sqlite3, `gh auth token` auth, one
49
+ // implicit account, no landing page — the unchanged zero-config experience.
50
+ // `cloud`: Postgres, GitHub sign-in (OAuth App and/or GitHub App), many accounts, landing page.
51
+ // Everything dialect/tenancy-specific branches off `deploymentMode`.
52
+ const deploymentMode = process.env.DEPLOYMENT_MODE === 'cloud' ? 'cloud' : 'local';
53
+ const isCloud = deploymentMode === 'cloud';
54
+ // Local: move a pre-rename ~/.pierre-review to ~/.limn, once, BEFORE anything below resolves a path
55
+ // inside the data directory (data-dir.ts). Here, not in index.ts's start(): `pnpm dev` imports this
56
+ // module first, so a later call would move the directory out from under `dataDir`/`cloneDir`. The
57
+ // .env files are loaded by now, so a DATABASE_URL/CLONE_DIR pinned inside the old directory is seen.
58
+ // The CLI already ran it before choosing the DB path; a second call is a no-op. Never under test.
59
+ if (!isCloud && !process.env.VITEST) {
60
+ migrateLegacyDataDir({
61
+ log: (msg) => console.log(msg),
62
+ resolvePath: (p) => (isAbsolute(p) ? p : resolve(backendRoot, p)),
63
+ });
64
+ }
65
+ // Default the SQLite DB under apps/backend/data for local dev. The INSTALLED CLI
66
+ // instead points DATABASE_URL at a user-writable ~/.limn path (see
67
+ // cli.ts) BEFORE config loads — so `pnpm dev` and a globally-installed `pierre`
68
+ // never share a database. Sharing one would be a trap: each process has its own
69
+ // in-memory "is-syncing" guard, so two of them double-sync every repo against the
70
+ // GitHub rate limit and contend on the SQLite write lock. DATABASE_URL / --db
71
+ // override this; a relative override resolves under apps/backend.
72
+ //
73
+ // In cloud mode DATABASE_URL is a `postgres://…` connection string (not a path),
74
+ // so dbPath is irrelevant there and left as the connection string verbatim.
75
+ const rawDbUrl = process.env.DATABASE_URL ?? (isCloud ? '' : './data/pierre-review.sqlite');
76
+ // ---- Phase 2 real-time sync: adaptive polling (see docs/REALTIME-SYNC.md) ----
77
+ // DEFAULT ON IN BOTH MODES. This is the PRIMARY sync strategy everywhere, because it's the
78
+ // only one that needs no cooperation from the repo's owner: webhooks require the GitHub App
79
+ // to be INSTALLED on each repo, and most watched repos are third-party public ones nobody
80
+ // here can install on. Adaptive works on all of them — cadence by activity bucket, plus a
81
+ // conditional REST probe whose 304 costs no rate limit, so it's both fresher on active repos
82
+ // and cheaper on quiet ones than the fixed-clock re-walk.
83
+ //
84
+ // Webhooks stay strictly ADDITIVE on top (cloud, installed repos only): a delivery fires a
85
+ // targeted syncOnePr in seconds, and adaptive keeps reconciling everything else. The two
86
+ // compose — nothing double-syncs, since both funnel into the idempotent persistPr.
87
+ // `SYNC_ADAPTIVE=false` restores the classic fixed-clock re-walk in either mode.
88
+ // Hoisted above `config` because the syncCron default below keys off it.
89
+ const syncAdaptive = process.env.SYNC_ADAPTIVE
90
+ ? process.env.SYNC_ADAPTIVE === 'true'
91
+ : true;
92
+ // ---- Does THIS process serve the built SPA? ----
93
+ //
94
+ // The single source of truth for a question two places need: app.ts, to decide whether to
95
+ // register the @fastify/static instance at `/app/`, and `appWebUrl` below, to decide whether a
96
+ // deep link handed to a person should point here at all.
97
+ //
98
+ // The SPA ships next to the compiled server (release/dist → release/public), so its presence on
99
+ // disk IS the answer: true for the packaged release and for cloud on Railway, false under
100
+ // `pnpm dev`, where there is no sibling public/ dir and Vite serves the UI while proxying /api
101
+ // back to this process.
102
+ //
103
+ // ⚠ SPELLED ONCE, ON PURPOSE. app.ts used to compute this inline; two copies of an existsSync
104
+ // against a hand-written relative path is exactly the "TWO SPELLINGS MUST AGREE" hazard, and the
105
+ // failure mode if they ever drifted is silent — a deep link that 404s, or a static root that
106
+ // never registers.
107
+ // ---- Where git clones live ----
108
+ //
109
+ // ⚠ TWO DEFAULTS, ONE CODE PATH. The clone cache is the same shared, LRU-evicted,
110
+ // credential-free cache in both modes (review/clone-manager.ts); only its ROOT differs.
111
+ //
112
+ // local — under the user's home, so a clone survives a restart and a cold re-clone of a
113
+ // large repository (measured 3.6s to 13.7s) is paid once.
114
+ // cloud — the container's EPHEMERAL filesystem. Deliberately NOT a mounted volume: a
115
+ // volume buys only that cold clone, and costs replicas (a Railway volume attaches
116
+ // to exactly one), downtime on every unattended deploy, and running as root, which
117
+ // reverses the Dockerfile's drop-root hardening. A clone is a rebuildable cache —
118
+ // losing one on a redeploy costs seconds, so it is the right thing to lose.
119
+ //
120
+ // CLONE_DIR overrides either. The janitor (conflict/janitor.ts) is what keeps the cloud disk
121
+ // bounded between deploys; nothing here assumes the directory survives one.
122
+ function defaultCloneDir() {
123
+ // ⚠ TWO LEVELS, NOT ONE. `tightenCloneRoot()` chmods the clone root AND ITS PARENT to 0700, so a
124
+ // bare `/tmp/pierre-clones` would aim that at `/tmp` itself — a chmod this process does not own,
125
+ // which fails silently and leaves the root un-tightened. `/tmp/pierre-review` is ours, created by
126
+ // the same `mkdirSync(..., {recursive: true})`, and mirrors the local `~/.limn/clones`.
127
+ if (isCloud)
128
+ return '/tmp/pierre-review/clones';
129
+ return join(resolveDataDir(), 'clones');
130
+ }
131
+ export const spaPublicDir = resolve(backendRoot, 'public');
132
+ export const spaLandingDir = resolve(backendRoot, 'public-landing');
133
+ export const serverServesSpa = existsSync(resolve(spaPublicDir, 'index.html'));
134
+ // The browser-facing base for deep links. See `appWebUrl` on the config object for the full
135
+ // contract; this is the resolution it names.
136
+ function resolveAppWebUrl() {
137
+ const explicit = process.env.APP_WEB_URL?.trim().replace(/\/+$/, '');
138
+ if (explicit)
139
+ return explicit;
140
+ const apiOrigin = process.env.APP_BASE_URL?.replace(/\/$/, '') ??
141
+ `http://localhost:${intFromEnv('PORT', 4000)}`;
142
+ if (serverServesSpa)
143
+ return apiOrigin;
144
+ // Nothing here serves the SPA, so a link to this origin would 404. The dev server does.
145
+ // Its port is `server.port` in apps/frontend/vite.config.ts; FRONTEND_PORT covers the demo
146
+ // stack (:5273) and anyone who moved it.
147
+ return `http://localhost:${intFromEnv('FRONTEND_PORT', 5173)}`;
148
+ }
149
+ // Chronology's working hours need a time zone before anyone has set one for a workspace. Locally
150
+ // the machine's own zone is the honest answer (it is the reader's); in the cloud the server's zone
151
+ // is an accident of hosting, so it is UTC and the Settings screen says "default" beside it.
152
+ // WORK_TIMEZONE overrides both. An unrecognised value falls back rather than throwing at boot.
153
+ function resolveDefaultWorkTimezone() {
154
+ const valid = (tz) => {
155
+ if (!tz)
156
+ return false;
157
+ try {
158
+ new Intl.DateTimeFormat('en-GB', { timeZone: tz });
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ };
165
+ const explicit = process.env.WORK_TIMEZONE?.trim();
166
+ if (valid(explicit))
167
+ return explicit;
168
+ if (isCloud)
169
+ return 'UTC';
170
+ const machine = Intl.DateTimeFormat().resolvedOptions().timeZone;
171
+ return valid(machine) ? machine : 'UTC';
172
+ }
173
+ export const config = {
174
+ deploymentMode,
175
+ isCloud,
176
+ // Drives the DB driver + schema selection in client.ts.
177
+ dbDialect: (isCloud ? 'postgres' : 'sqlite'),
178
+ // ---- Lean storage (both modes by default) ----
179
+ // When false (the default), sync does NOT persist bulky user-authored text —
180
+ // comment / review / PR bodies, review-comment diff hunks, commit messages, and
181
+ // the per-job checkRuns JSON (the ci_status summary enum is kept). That text is
182
+ // regenerable from GitHub (and duplicated per tenant in cloud), so it's the
183
+ // dominant storage cost; it's hydrated on demand when a PR/thread is opened
184
+ // (sync/hydrate-detail.ts, via the gh-CLI token locally or the OAuth token in
185
+ // cloud) and cached in the browser. Not storing it also shrinks the DB and the
186
+ // sync payload, speeding up an initial backfill. Set PERSIST_BODIES=true to store
187
+ // full bodies instead — instant, fully-offline detail at the cost of a larger DB
188
+ // (e.g. a local instance used without network).
189
+ persistBodies: process.env.PERSIST_BODIES === 'true',
190
+ // Postgres connection string (cloud only). Empty in local mode.
191
+ databaseUrl: process.env.DATABASE_URL ?? '',
192
+ port: intFromEnv('PORT', 4000),
193
+ host: process.env.HOST ?? (isCloud ? '0.0.0.0' : '127.0.0.1'),
194
+ dbPath: isCloud || isAbsolute(rawDbUrl) ? rawDbUrl : resolve(backendRoot, rawDbUrl),
195
+ backfillDays: intFromEnv('BACKFILL_DAYS', 90),
196
+ // First sync runs in two phases: a fast "foreground" window (matching the
197
+ // default timeline range) so the board is usable in seconds, then the rest of
198
+ // backfillDays is fetched in the background. Two-phase only kicks in when
199
+ // backfillDays exceeds this.
200
+ foregroundSyncDays: intFromEnv('FOREGROUND_SYNC_DAYS', 14),
201
+ // After a repo's first full sync (and any forced deep re-sync), backfill CI HISTORY so the
202
+ // Activity tab's CI charts aren't blank on a fresh repo: trunk commits back to the 90-day
203
+ // trend window, plus per-PR CI transition events synthesized from GitHub's retained check
204
+ // rollups (sync/backfill-ci-history.ts). One-time, bounded GraphQL spend per repo;
205
+ // CI_HISTORY_BACKFILL=false disables it.
206
+ ciHistoryBackfill: process.env.CI_HISTORY_BACKFILL
207
+ ? process.env.CI_HISTORY_BACKFILL === 'true'
208
+ : true,
209
+ // How many commit-file REST fetches to keep in flight at once (one pool per
210
+ // page). These draw from the REST quota (disjoint from the GraphQL points
211
+ // pool), so a modest pool safely cuts the dominant sync stage.
212
+ commitFileConcurrency: intFromEnv('COMMIT_FILE_CONCURRENCY', 10),
213
+ // The scheduler TICK, not the per-repo cadence. Under adaptive polling the tick must be
214
+ // faster than the hot bucket (syncHotIntervalSec, 120s) or the due-check can never grant
215
+ // it — a */5 tick would pin every repo to 5 min and throw away the freshness half of
216
+ // Phase 2 — so adaptive defaults the tick to every minute and lets isDue() decide what
217
+ // actually syncs. Non-adaptive keeps the classic fixed */5 re-walk. SYNC_CRON overrides.
218
+ syncCron: process.env.SYNC_CRON ?? (syncAdaptive ? '*/1 * * * *' : '*/5 * * * *'),
219
+ syncOverlapMinutes: intFromEnv('SYNC_OVERLAP_MINUTES', 20),
220
+ // The time zone Chronology counts working hours in for a workspace that never set one
221
+ // (packages/shared/src/flow-settings.ts). See resolveDefaultWorkTimezone.
222
+ defaultWorkTimezone: resolveDefaultWorkTimezone(),
223
+ // Phase 0 real-time sync (see docs/REALTIME-SYNC.md). enqueuePrSync coalesces a burst
224
+ // of change signals for the SAME PR — a push emits push + synchronize + check_run
225
+ // within seconds — into ONE targeted syncOnePr fired this many ms after the burst
226
+ // settles. Fed by webhooks (cloud) / the adaptive scheduler (local); unused until
227
+ // those land. WEBHOOK_DEBOUNCE_MS overrides.
228
+ webhookDebounceMs: intFromEnv('WEBHOOK_DEBOUNCE_MS', 4000),
229
+ // Resolved above (default ON in BOTH modes). When on, the scheduler's per-repo pass
230
+ // becomes adaptive: the tick runs every minute but a repo only syncs when it's DUE for
231
+ // its activity bucket, and — for incremental syncs — a cheap conditional REST probe runs
232
+ // first, skipping the fat GraphQL walk when nothing changed (a 304 costs no rate limit).
233
+ // In cloud it composes with the activity-gate (idle tenants are skipped first) and with
234
+ // webhooks (which bypass the cadence entirely for installed repos).
235
+ syncAdaptive,
236
+ // Per-bucket minimum seconds between sync attempts. A repo is "hot" when a PR changed
237
+ // within the last hour, "warm" within 6h, else "cold" (windows are constants in
238
+ // sync/adaptive.ts). Fresher where activity is; backs off when quiet.
239
+ syncHotIntervalSec: intFromEnv('SYNC_HOT_INTERVAL_SEC', 120),
240
+ syncWarmIntervalSec: intFromEnv('SYNC_WARM_INTERVAL_SEC', 300),
241
+ syncColdIntervalSec: intFromEnv('SYNC_COLD_INTERVAL_SEC', 900),
242
+ // The conditional probe can't see CI-finish / thread-resolve (they don't bump a PR's
243
+ // updatedAt), so force a full re-walk at least this often even when the probe says
244
+ // "unchanged" — the floor that keeps those signals fresh. SYNC_FLOOR_INTERVAL_SEC.
245
+ syncFloorIntervalSec: intFromEnv('SYNC_FLOOR_INTERVAL_SEC', 1800),
246
+ // CLOUD ONLY: the scheduled sync skips any account whose loaded frontend hasn't
247
+ // been seen within this many minutes (accounts.lastActiveAt), so a tenant with no
248
+ // open tab is not re-synced every 5 min. Comfortably exceeds the cron period so a
249
+ // user active a few minutes ago isn't dropped between ticks. Local mode ignores
250
+ // this entirely (one always-on account). SYNC_ACTIVE_WINDOW_MINUTES overrides.
251
+ syncActiveWindowMinutes: intFromEnv('SYNC_ACTIVE_WINDOW_MINUTES', 15),
252
+ stallThresholdDays: intFromEnv('STALL_THRESHOLD_DAYS', 3),
253
+ // ---- Retention / TTL ----
254
+ // Per-account server data doesn't grow forever: a periodic sweep prunes PRs (and their
255
+ // whole subtree) whose `updatedAt` is older than this many days. 0 disables it. The
256
+ // effective cutoff is clamped to at least `backfillDays`, so a forced full sync (which
257
+ // re-walks `now − backfillDays`) can never re-fetch (resurrect) a just-deleted PR.
258
+ // Runs in BOTH modes (local + cloud). RETENTION_DAYS overrides.
259
+ retentionDays: intFromEnv('RETENTION_DAYS', 180),
260
+ // When the retention sweep runs (node-cron). Daily at 03:00 by default; off-peak so a
261
+ // large delete doesn't contend with the 5-minute sync. RETENTION_CRON overrides.
262
+ retentionCron: process.env.RETENTION_CRON ?? '0 3 * * *',
263
+ // Cross-org benchmark rollup (CLOUD-ONLY; inert unless an account has opted in). Weekly,
264
+ // Monday 04:00 UTC — after a fresh ISO week starts, off-peak. BENCHMARK_ROLLUP_CRON overrides.
265
+ benchmarkRollupCron: process.env.BENCHMARK_ROLLUP_CRON ?? '0 4 * * 1',
266
+ // Disable the periodic scheduler (used by scripts/tests).
267
+ disableScheduler: process.env.DISABLE_SCHEDULER === 'true',
268
+ // ---- ML severity/category enrichment of bot comments (CORE, free tier) ----
269
+ // Classifies BOT-authored review comments, PR comments and review bodies against the
270
+ // `severity-api` microservice from the `packages/ml` submodule (a fine-tuned ModernBERT-ONNX
271
+ // severity model + a deterministic category parser). No LLM, nothing billed. Full contract +
272
+ // the local run recipe: docs/ML-SEVERITY.md.
273
+ //
274
+ // THE GATE IS THE URL. `SEVERITY_API_URL` unset ⇒ the whole feature is inert: no worker, no
275
+ // routes registered, `/api/me` reports mlSeverity:false and the SPA issues zero ML queries.
276
+ // That is deliberately the same switch that makes the feature UNAVAILABLE under
277
+ // `npx pierre-review` — the published package ships no model and config.ts's .env search
278
+ // paths resolve against the INSTALL directory, so an npx user has nowhere to put the var.
279
+ // Cloud sets it to the Railway private-DNS name; local dev to the sibling repo's port.
280
+ // ML_SEVERITY_DISABLED=true is the kill switch for a deployment that has the URL set.
281
+ //
282
+ // SEVERITY_API_DEFAULT_URL is a DEV-LOOP FALLBACK, never a deployment knob: `pnpm dev`
283
+ // (scripts/dev.mjs) exports it only after confirming it can actually start the sibling repo's
284
+ // service, so the backend points at a service that exists without anyone editing `.env`. It is
285
+ // deliberately a SECOND variable rather than a default assigned to SEVERITY_API_URL, because
286
+ // `process.loadEnvFile` does NOT overwrite an already-set variable — a command-line
287
+ // SEVERITY_API_URL would therefore have BEATEN the developer's own `.env`, inverting the
288
+ // precedence a default is supposed to have. Nothing but the dev script ever sets it.
289
+ severityApiUrl: process.env.ML_SEVERITY_DISABLED === 'true'
290
+ ? ''
291
+ : (process.env.SEVERITY_API_URL ?? process.env.SEVERITY_API_DEFAULT_URL ?? '').replace(/\/$/, ''),
292
+ // Extra headers for every severity-api call, as a JSON object string — e.g.
293
+ // `SEVERITY_API_HEADERS='{"X-Severity-Token":"…"}'`. Empty in both shipped deployments, which
294
+ // reach the service over private DNS (cloud) or loopback (local) and need no credential. It
295
+ // exists so a RENTED endpoint can be used without a code change — draining a large backlog on
296
+ // hired CPUs is far faster than a dev box, and that endpoint must not be open to the internet.
297
+ // Parsing is deliberately forgiving; see `extraHeaders()` in ml/severity-client.ts for why a
298
+ // malformed value must not throw.
299
+ severityApiHeaders: process.env.SEVERITY_API_HEADERS ?? '',
300
+ // The enrichment worker's own cron — a TICK, not a cadence: each tick drains as much of the
301
+ // unlabelled backlog as its wall-clock budget allows, newest-first. Separate from the sync
302
+ // cron on purpose (the two are independent, and enrichment must never delay a sync).
303
+ mlEnrichmentCron: process.env.ML_ENRICHMENT_CRON ?? '*/2 * * * *',
304
+ // Wall-clock ceiling for one tick. Sized under the cron period so ticks never overlap in
305
+ // practice (a re-entrancy guard covers the case where they would anyway).
306
+ mlTickBudgetMs: intFromEnv('ML_TICK_BUDGET_MS', 90_000),
307
+ // Batch shape. The service caps a request at 256 items (422 beyond), but ITEM COUNT is the
308
+ // wrong budget: inference cost tracks TOTAL TEXT, and a batch pads to its longest member, so
309
+ // one 8k-char walkthrough in a batch of 128 makes every slot cost like the walkthrough.
310
+ // Measured on an Intel i9: 32 short bodies 2.7s vs 32 long bodies 28.4s. The worker therefore
311
+ // sorts candidates by length and fills batches to a CHARACTER budget, capping the count too.
312
+ mlBatchMaxItems: intFromEnv('ML_BATCH_MAX_ITEMS', 128),
313
+ mlBatchMaxChars: intFromEnv('ML_BATCH_MAX_CHARS', 24_000),
314
+ // How many batch requests are in flight at once. The deployed service runs 2 uvicorn workers,
315
+ // so 2 saturates it; more just queues.
316
+ mlConcurrency: intFromEnv('ML_CONCURRENCY', 2),
317
+ // Per-request timeout. A cold start loads a ~150 MB model, and a long batch is legitimately
318
+ // slow on CPU, so this is generous by design.
319
+ mlRequestTimeoutMs: intFromEnv('ML_REQUEST_TIMEOUT_MS', 120_000),
320
+ // The model truncates at 512 tokens, so anything past ~2.5k chars is discarded server-side.
321
+ // Trimming client-side bounds the request payload without changing what the model sees.
322
+ mlBodyMaxChars: intFromEnv('ML_BODY_MAX_CHARS', 6_000),
323
+ // ---- Cloud (Railway) — GitHub sign-in + sessions + token encryption ----
324
+ // Public base URL of the deployment (no trailing slash); used to build the
325
+ // OAuth redirect_uri and absolute links. Locally http://localhost:<port>.
326
+ appBaseUrl: process.env.APP_BASE_URL?.replace(/\/$/, '') ??
327
+ `http://localhost:${intFromEnv('PORT', 4000)}`,
328
+ // Where a HUMAN'S BROWSER reaches the SPA — the base for deep links we hand to a person
329
+ // (today: the workspace + per-repo links in the Slack digest).
330
+ //
331
+ // ⚠ THIS IS NOT `appBaseUrl` AND THE TWO MUST NOT BE MERGED. `appBaseUrl` is THIS SERVER's own
332
+ // public origin: it builds the OAuth `redirect_uri` and is the EXACT CORS allowlist entry in
333
+ // cloud, so it must keep naming the API origin whatever the SPA is served from. `appWebUrl`
334
+ // answers a different question — "where do I send someone to open the app?" — and the two
335
+ // diverge in exactly one deployment: `pnpm dev`, where this server runs on :4000 and NEVER
336
+ // serves the SPA (see `serverServesSpa` below), while Vite serves it on :5173. A deep link
337
+ // built on `appBaseUrl` 404s there. That is the bug this exists to fix.
338
+ //
339
+ // Resolution, in order:
340
+ // 1. APP_WEB_URL — an explicit override, for any split deployment (SPA on a CDN or a
341
+ // separate host). Always wins.
342
+ // 2. This server serves the SPA itself ⇒ `appBaseUrl`. True for the packaged release
343
+ // (`npx pierre-review`) and for cloud on Railway — one origin, one answer.
344
+ // 3. Otherwise this server does NOT serve the SPA, so something else does: the Vite dev
345
+ // origin, `http://localhost:${FRONTEND_PORT ?? 5173}`.
346
+ //
347
+ // ⚠ STEP 2 KEYS ON WHETHER THE SPA IS ACTUALLY ON DISK, NOT ON `isCloud` AND NOT ON WHETHER
348
+ // APP_BASE_URL IS SET. docs/LOCAL-CLOUD-TESTING.md § Option A runs `DEPLOYMENT_MODE=cloud
349
+ // pnpm dev` WITH `APP_BASE_URL=http://localhost:4000` (the OAuth callback needs it) while Vite
350
+ // still serves the SPA on :5173 — so keying on either of those would put that mode back on the
351
+ // 404. Serving the files is the only ground truth.
352
+ appWebUrl: resolveAppWebUrl(),
353
+ // Cloud supports TWO sign-in providers side by side; a deployment configures either or both
354
+ // and the SignInGate offers whatever's set. They are SEPARATE GitHub registrations (distinct
355
+ // client id/secret) but share the same authorize/token endpoints — the flow only differs in
356
+ // which credential it uses and whether it requests `oauthScope` (see api/routes/auth.ts).
357
+ //
358
+ // • OAuth App — user-scoped token, NO installation. `public_repo` reads public repos incl.
359
+ // CI checks/statuses (what an unscoped token can't) + enables the interactive PR actions.
360
+ // • GitHub App — user-to-server token; for PRIVATE repos the App must be INSTALLED where they
361
+ // live (public repos work without install, but reading CI Checks/Actions needs install).
362
+ githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID ?? '',
363
+ githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET ?? '',
364
+ // Scopes requested by the OAuth App at authorize time. Default = public repos only (widen to
365
+ // `repo read:org` for private-repo access via OAuth).
366
+ oauthScope: process.env.GITHUB_OAUTH_SCOPE ?? 'public_repo read:org',
367
+ githubAppClientId: process.env.GITHUB_APP_CLIENT_ID ?? '',
368
+ githubAppClientSecret: process.env.GITHUB_APP_CLIENT_SECRET ?? '',
369
+ // The GitHub App's URL slug — builds the private-repo install link
370
+ // (github.com/apps/<slug>/installations/new), shown on the sign-in gate.
371
+ githubAppSlug: process.env.GITHUB_APP_SLUG ?? '',
372
+ // GitHub App webhook shared secret (real-time sync Phase 1 — see docs/REALTIME-SYNC.md).
373
+ // The App POSTs events to /api/webhooks/github; we verify X-Hub-Signature-256
374
+ // (HMAC-SHA256 over the raw body) against this. Empty = the route replies 501
375
+ // (unconfigured), so webhook-driven sync is INERT until it's set — a deployment opts
376
+ // in additively without weakening the periodic poll. Optional (NOT required by
377
+ // assertCloudConfig). GITHUB_APP_WEBHOOK_SECRET.
378
+ githubAppWebhookSecret: process.env.GITHUB_APP_WEBHOOK_SECRET ?? '',
379
+ // Derived: is each provider fully configured? (Both are optional; assertCloudConfig requires
380
+ // at least one.) The auth routes + SignInGate gate on these.
381
+ oauthProviderEnabled: !!(process.env.GITHUB_OAUTH_CLIENT_ID && process.env.GITHUB_OAUTH_CLIENT_SECRET),
382
+ appProviderEnabled: !!(process.env.GITHUB_APP_CLIENT_ID && process.env.GITHUB_APP_CLIENT_SECRET),
383
+ // Seals the session cookie ({accountId}). Rotate to invalidate all sessions.
384
+ sessionSecret: process.env.SESSION_SECRET ?? '',
385
+ // 32-byte (64-hex) key for AES-256-GCM encryption of stored access tokens.
386
+ encryptionKey: process.env.ENCRYPTION_KEY ?? '',
387
+ // HSTS max-age (seconds) for the cloud public origin. Sent only in cloud mode
388
+ // and honored only over HTTPS (Railway terminates TLS). Default 1 year. Set
389
+ // HSTS_MAX_AGE=0 as a kill switch. `preload` is intentionally NOT sent — it is
390
+ // hard to undo; opt in manually once the domain is proven.
391
+ hstsMaxAge: intFromEnv('HSTS_MAX_AGE', 31536000),
392
+ // ---- Stripe billing (optional; NOT required by assertCloudConfig) ----
393
+ // Payment Link URL for the Pro plan — GET /api/billing/checkout 302s to it with
394
+ // client_reference_id=<accountId> appended. Empty = checkout unavailable.
395
+ stripePaymentLinkUrl: process.env.STRIPE_PAYMENT_LINK_URL ?? '',
396
+ // Webhook signing secret (whsec_…) for POST /api/billing/webhook. Empty = the
397
+ // webhook replies 501 and no billing state ever changes.
398
+ stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? '',
399
+ // ---- The public contact form (cloud only in practice; the landing is never served
400
+ // in local mode) ----
401
+ // A Slack incoming-webhook URL. Empty = GET /api/contact/ticket and POST /api/contact
402
+ // both 503, and the form on the landing page says so instead of accepting a message
403
+ // it cannot deliver. Validated at the point of use (api/routes/contact.ts) rather
404
+ // than here, so a bad value is a failed request with a log line rather than a boot
405
+ // failure on an otherwise healthy deploy.
406
+ contactSlackWebhookUrl: process.env.CONTACT_SLACK_WEBHOOK_URL ?? '',
407
+ // Pro plugin master gate. bind.ts skips the dynamic import entirely when false.
408
+ // PRO_DISABLED=true forces pure-OSS mode even when the submodule is checked out (used to
409
+ // exercise/capture the free tier). Local defaults ON. In CLOUD it stays OFF unless
410
+ // PRO_CLOUD_ENABLED=true (the paid summary-AI tier — set on the Railway image), so the
411
+ // public Dockerfile / OSS npm path is byte-identical to before. Even when true in cloud,
412
+ // per-account entitlement (plan !== 'free') + the /api/pro/* 402 gate decide who actually
413
+ // gets Pro. The agentic features are core and gated separately (`aiEnabled` below; off in cloud).
414
+ proEnabled: process.env.PRO_DISABLED !== 'true' &&
415
+ (!isCloud || process.env.PRO_CLOUD_ENABLED === 'true'),
416
+ // Per-repo digest (Pro, Workstream 2) config — consumed by @pierre/pro, kept in
417
+ // core so the model id / budgets live in one place and aren't hardcoded at call
418
+ // sites. Inert until the plugin is present AND digestEnabled.
419
+ pro: {
420
+ digestModel: process.env.PRO_DIGEST_MODEL ?? 'claude-haiku-4-5',
421
+ // The period-report narration's default model. Reaches the plugin the same way digestModel
422
+ // does — the env var itself (@pierre/pro reads PRO_REPORT_MODEL in llm/seam.ts's
423
+ // DEFAULT_REPORT_MODEL); this entry keeps the model ids in the one place they are all
424
+ // documented. The plugin validates the value against its REPORT_MODELS allowlist — an
425
+ // unselectable id here silently falls back to the plugin's default rather than reaching a
426
+ // meter. Per-account override: pro_settings.report_model; per-run override: the generate
427
+ // call's `model` body field.
428
+ //
429
+ // ⚠ THE TWO SPELLINGS MUST AGREE. This literal and `DEFAULT_REPORT_MODEL`'s fallback in
430
+ // packages/pro/src/llm/seam.ts are the same default written twice (the plugin cannot import
431
+ // host config, and this line is where the model ids are documented); a half-edit makes this
432
+ // comment lie about what actually narrates. Both now say claude-haiku-4-5.
433
+ //
434
+ // It was claude-sonnet-5 — plan P4.1's "a forwarded sprint retro is worth more prose care
435
+ // than a per-repo digest". Retired: Haiku narrates every summary surface now, so a report
436
+ // cannot differ from the one a colleague forwarded by model. The model id is folded into the
437
+ // narration's payload hash (and the stored row's key), so this flip is a deliberate one-off
438
+ // re-bill: the next generate of each period is a cache miss.
439
+ reportModel: process.env.PRO_REPORT_MODEL ?? 'claude-haiku-4-5',
440
+ digestEnabled: process.env.PRO_DIGEST_ENABLED === 'true',
441
+ digestMaxUsdPerRefresh: floatFromEnv('PRO_DIGEST_MAX_USD', 0.5),
442
+ digestMaxReposPerRefresh: intFromEnv('PRO_DIGEST_MAX_REPOS', 30),
443
+ digestMinIntervalSec: intFromEnv('PRO_DIGEST_MIN_INTERVAL_SEC', 60),
444
+ },
445
+ // ---- The agentic features: Claude Review, review memory, AI Fix (CORE, free, LOCAL ONLY) ----
446
+ // ONE switch, and it is NOT an opt-in: locally they are on and always visible (a missing Claude
447
+ // credential replaces the Run button with one line; it never hides the feature). They run on
448
+ // the user's own Claude Code session or ANTHROPIC_API_KEY (review/auth.ts).
449
+ // • CLOUD: off, STRUCTURALLY — `registerAgenticRoutes` refuses to register anything when
450
+ // `isCloud`, so the guarantee never rests on an env var being unset (it used to: the plugin's
451
+ // PRO_ADVANCED_AI_ENABLED, now deleted along with ENABLE_CLAUDE_REVIEW / CLAUDE_REVIEW_ENABLED).
452
+ // • LIMN_AI_DISABLED=true: the kill switch for a team that forbids AI tooling — hides every
453
+ // agentic surface (`MeResponse.ai.enabled` false) and registers no agentic route.
454
+ aiEnabled: !isCloud && process.env.LIMN_AI_DISABLED !== 'true',
455
+ // ---- Claude Review — infra knobs ----
456
+ // The routing thresholds, concurrency + queue caps keep their historical PRO_REVIEW_* env names.
457
+ // Partial clones + ephemeral worktrees live here (a user-writable home path,
458
+ // never the read-only install dir). CLONE_DIR overrides. See `defaultCloneDir`
459
+ // above for why cloud puts them somewhere else.
460
+ cloneDir: process.env.CLONE_DIR ?? defaultCloneDir(),
461
+ // The local data directory: `~/.limn` (LIMN_DATA_DIR overrides). Holds the installed CLI's
462
+ // database, the clone cache, config.json and the AI runtime downloaded on first use. An existing
463
+ // `~/.pierre-review` (the pre-rename name) is moved here ONCE on first boot — data-dir.ts.
464
+ dataDir: resolveDataDir(),
465
+ // Soft cap on the clone cache before LRU cleanup evicts idle repos.
466
+ //
467
+ // 2 GiB locally, where the disk is the developer's own and a clone kept warm saves a cold
468
+ // re-clone. 1 GiB in cloud: that disk is the container's ephemeral filesystem, shared with
469
+ // everything else the process writes, and the measured worst case for one clone of a large
470
+ // repository is 111 MB — so 1 GiB still holds roughly nine of the worst ones at once, and
471
+ // the janitor evicts by least-recently-used before it is reached.
472
+ cloneCacheMaxBytes: intFromEnv('CLONE_CACHE_MAX_BYTES', (isCloud ? 1 : 2) * 1024 * 1024 * 1024),
473
+ // ---- Merge conflict resolver (CORE / free, BOTH MODES) ----
474
+ // Bounds on ONE resolver session. Every one of these is a REFUSAL boundary, not a
475
+ // truncation: past the cap the file is listed as unsupported with a reason, never
476
+ // silently shortened. A shortened file would commit bytes nobody saw.
477
+ conflictMaxFiles: intFromEnv('CONFLICT_MAX_FILES', 40),
478
+ conflictMaxFileBytes: intFromEnv('CONFLICT_MAX_FILE_BYTES', 512 * 1024),
479
+ // ⚠ Must stay < 127: `git merge-file` reports the conflict count in its EXIT STATUS,
480
+ // which saturates there.
481
+ conflictMaxRegions: intFromEnv('CONFLICT_MAX_REGIONS', 120),
482
+ conflictMaxTotalBytes: intFromEnv('CONFLICT_MAX_TOTAL_BYTES', 8 * 1024 * 1024),
483
+ // Myers diff `d` ceilings. Past these the diff degrades to a single whole-file conflict
484
+ // region rather than running quadratically on a pathological pair.
485
+ conflictDiffMaxD: intFromEnv('CONFLICT_DIFF_MAX_D', 4000),
486
+ conflictWordDiffMaxD: intFromEnv('CONFLICT_WORD_DIFF_MAX_D', 2000),
487
+ conflictWordTokenCap: intFromEnv('CONFLICT_WORD_TOKEN_CAP', 20_000),
488
+ conflictPrefetchMaxBlobs: intFromEnv('CONFLICT_PREFETCH_MAX_BLOBS', 2000),
489
+ conflictModelTimeoutMs: intFromEnv('CONFLICT_MODEL_TIMEOUT_MS', 45_000),
490
+ conflictSessionTtlMs: intFromEnv('CONFLICT_SESSION_TTL_MIN', 30) * 60_000,
491
+ conflictSuggestMaxChars: intFromEnv('CONFLICT_SUGGEST_MAX_CHARS', 4000),
492
+ // A worktree older than this is an ORPHAN — its run died without its `finally`. The
493
+ // startup sweep prunes it, and the LRU may evict its repo despite `.worktrees/` being
494
+ // non-empty. Without this one crashed run pins a repo in the cache permanently.
495
+ worktreeTtlMs: intFromEnv('WORKTREE_TTL_HOURS', 6) * 3_600_000,
496
+ cloneSweepMaxMs: intFromEnv('CLONE_SWEEP_MAX_MS', 30_000),
497
+ // ---- The clone-cache janitor (conflict/janitor.ts, scheduled in sync/scheduler.ts) ----
498
+ // The boot sweep runs ONCE; a long-lived process accumulates a session's two fetch refs per
499
+ // resolver opened and never closed, and each ref pins objects the repack cannot drop. This is
500
+ // the same work on a clock. Quarter-hourly is well inside the 30-minute session TTL, so a ref
501
+ // is collected in the same order of time as the session that made it.
502
+ conflictJanitorCron: process.env.CONFLICT_JANITOR_CRON ?? '*/15 * * * *',
503
+ // Loose objects in one clone past which the janitor runs `git gc --prune=now`. Below it, gc
504
+ // is a ~120ms spawn that repacks nothing. 5,000 is git's own `gc.auto` default.
505
+ cloneGcLooseObjects: intFromEnv('CLONE_GC_LOOSE_OBJECTS', 5_000),
506
+ // How long SIGTERM waits for in-flight resolver jobs before closing the server anyway.
507
+ // Matched to the 120s timeout on a single git subprocess (review/clone-manager.ts), which is
508
+ // the longest single step a job can be inside.
509
+ shutdownGraceMs: intFromEnv('SHUTDOWN_GRACE_MS', 120_000),
510
+ // Per-run caps (cost/disk/time runaway guards). The diff is inlined in full, so
511
+ // reviews need far fewer turns than the old default; 30 is still generous.
512
+ reviewMaxTurns: intFromEnv('REVIEW_MAX_TURNS', 30),
513
+ // Hard USD ceiling per run. When the SDK trips this it returns an
514
+ // `error_max_budget_usd` result BEFORE the agent calls submit_review — so a run
515
+ // that hits the cap is recorded FAILED and still bills (you pay for no review).
516
+ // The cap must therefore sit ABOVE the cost of a normal completed review, not at
517
+ // it; `reviewEffort` below is the real cost lever (a large diff at default-high
518
+ // effort is what blew the old $1 cap). Lower REVIEW_BUDGET_USD only if you'd
519
+ // rather a borderline review fail than complete. It is the WHOLE limit for one run,
520
+ // deep-review specialists included (up to CLAUDE_REVIEW_MAX_SPECIALISTS, each on the review's
521
+ // model) — there is no per-user override and no route that sets it.
522
+ reviewBudgetUsd: floatFromEnv('REVIEW_BUDGET_USD', 6.75),
523
+ // Turn cap for a diff-only run. These are TOOL-LESS (only submit_review), so they
524
+ // should finish in ~2 turns; a tight cap is a cheap runaway guard.
525
+ reviewDiffOnlyMaxTurns: intFromEnv('REVIEW_DIFF_ONLY_MAX_TURNS', 6),
526
+ // ---- Claude Review chat (one answered question about a finished review) ----
527
+ // Each answer is its own agent run over the whole review + diff, so it gets its OWN small spend
528
+ // cap rather than the review's. Turn caps are low on purpose: a worktree answer may look up a
529
+ // few files (Read/Glob/Grep), a diff-only answer has no tools and needs one reply.
530
+ reviewChatBudgetUsd: floatFromEnv('REVIEW_CHAT_BUDGET_USD', 1),
531
+ reviewChatMaxTurns: intFromEnv('REVIEW_CHAT_MAX_TURNS', 8),
532
+ reviewChatDiffOnlyMaxTurns: intFromEnv('REVIEW_CHAT_DIFF_ONLY_MAX_TURNS', 2),
533
+ // ---- Diff-size cap ----
534
+ // A very large inlined diff is the dominant cost on a big PR (it's the cached
535
+ // prefix re-read every turn). The diff shown IN THE PROMPT is truncated at a
536
+ // whole-file boundary to this many characters; routing + line anchoring still use
537
+ // the FULL diff, and the changed-file LIST stays complete (so a worktree run can
538
+ // Read the omitted files). ON by default — it proved its worth on a large PR that
539
+ // failed without it. ~60k chars ≈ ~15k tokens, so only outlier PRs are truncated
540
+ // and a normal review is unaffected; each run records diffCapped + the full diff
541
+ // size, so you can still A/B by setting REVIEW_DIFF_CAP_ENABLED=false for a
542
+ // baseline run and comparing the recorded cost.
543
+ reviewDiffCapEnabled: process.env.REVIEW_DIFF_CAP_ENABLED !== 'false',
544
+ reviewDiffCapChars: intFromEnv('REVIEW_DIFF_CAP_CHARS', 60000),
545
+ // Agent `effort` per mode — the dominant cost knob (unset ⇒ the SDK default
546
+ // `high`, which over-thinks bounded reviews). A diff-only run just hunts bugs in a
547
+ // small inlined diff, so `low` is plenty; a worktree run reasons across files, so
548
+ // `medium` keeps that while trimming the over-exploration that ran up the bill.
549
+ // Applied only to effort-capable models (Sonnet/Opus); Haiku ignores it.
550
+ reviewEffort: effortFromEnv('REVIEW_EFFORT', 'medium'),
551
+ reviewDiffOnlyEffort: effortFromEnv('REVIEW_DIFF_ONLY_EFFORT', 'low'),
552
+ // (Review concurrency + queue caps + the routing thresholds moved to the plugin's
553
+ // PRO_REVIEW_* env — the plugin owns the queue/manager + the mode-routing decision.)
554
+ // ---- AI Fix (Pro: agentic code fixer) — core infra knobs ----
555
+ // The write-capable fixer (packages/pro/ai-fix) reuses the review clone/agent
556
+ // machinery. A runaway fixer is pricier than a review (it edits + re-reads files),
557
+ // so the caps are a touch higher on turns but the USD ceiling is the real guard.
558
+ // Concurrency is 1: the fixer relies on ambient Claude auth WITHOUT mutating
559
+ // process.env (which would race the review manager), so only one fix runs at once.
560
+ aiFixMaxTurns: intFromEnv('AI_FIX_MAX_TURNS', 40),
561
+ aiFixBudgetUsd: floatFromEnv('AI_FIX_BUDGET_USD', 5),
562
+ aiFixConcurrency: intFromEnv('AI_FIX_CONCURRENCY', 1),
563
+ // Refuse a fix whose captured patch exceeds this (a runaway diff shouldn't bloat a
564
+ // DB row). ~1 MiB of unified diff is already a very large change.
565
+ aiFixPatchMaxBytes: intFromEnv('AI_FIX_PATCH_MAX_BYTES', 1024 * 1024),
566
+ // Cap the continue/skip loop of CORE "Update branch from trunk"'s rebase
567
+ // (coding/merge.ts): a stop with no conflicted file (a commit that became empty) is
568
+ // continued or skipped up to this many times, then the rebase is aborted rather than
569
+ // looping forever on a pathological history. Any real conflict aborts at once. The
570
+ // AI_FIX_ name is historical — it once also bounded AI Fix's (removed) resolver.
571
+ aiFixRebaseMaxSteps: intFromEnv('AI_FIX_REBASE_MAX_STEPS', 10),
572
+ };
573
+ // Fail loud at startup if a required cloud var is missing — mirrors the
574
+ // gh-auth loud failure. Called from index.ts only when deploymentMode==='cloud'.
575
+ export function assertCloudConfig() {
576
+ const required = [
577
+ ['DATABASE_URL', config.databaseUrl],
578
+ ['APP_BASE_URL', process.env.APP_BASE_URL ?? ''],
579
+ ['SESSION_SECRET', config.sessionSecret],
580
+ ['ENCRYPTION_KEY', config.encryptionKey],
581
+ ];
582
+ // At least ONE sign-in provider must be fully configured; a deployment may enable either or
583
+ // both. A GitHub App with no OAuth App (or vice-versa) is fine.
584
+ if (!config.oauthProviderEnabled && !config.appProviderEnabled) {
585
+ throw new Error('Cloud mode needs at least one GitHub sign-in method configured: set ' +
586
+ 'GITHUB_OAUTH_CLIENT_ID + GITHUB_OAUTH_CLIENT_SECRET (OAuth App, public repos) and/or ' +
587
+ 'GITHUB_APP_CLIENT_ID + GITHUB_APP_CLIENT_SECRET (GitHub App, adds private org repos via ' +
588
+ 'install). See docs/GITHUB-AUTH-SETUP.md.');
589
+ }
590
+ const missing = required.filter(([, v]) => !v).map(([k]) => k);
591
+ if (missing.length > 0) {
592
+ throw new Error(`Cloud mode (DEPLOYMENT_MODE=cloud) requires these env vars: ${missing.join(', ')}. See .env.cloud.example and docs/DEPLOY-RAILWAY.md.`);
593
+ }
594
+ if (Buffer.from(config.encryptionKey, 'hex').length !== 32) {
595
+ throw new Error('ENCRYPTION_KEY must be 32 bytes as 64 hex chars (generate with `openssl rand -hex 32`).');
596
+ }
597
+ // SESSION_SECRET needs the SAME rigour as ENCRYPTION_KEY, and previously got none — it was
598
+ // only checked for being non-empty. It is not a "secret" in the guessable-password sense:
599
+ // api/plugins/auth.ts derives the sealing key as sha256(SESSION_SECRET) and hands that to
600
+ // @fastify/secure-session as a raw `key`, which BYPASSES that library's own 32-byte
601
+ // minimum and its deliberately-slow crypto_pwhash KDF. A single SHA-256 per candidate makes
602
+ // a short secret brute-forceable offline, and whoever recovers it can mint a valid
603
+ // `pierre_session` cookie for accountId = 1..N — i.e. read and write every tenant's data
604
+ // and spend every tenant's stored GitHub token. That makes it the single highest-value
605
+ // secret in the deployment, so a weak one must fail the boot, not merely be discouraged.
606
+ if (Buffer.byteLength(config.sessionSecret, 'utf8') < 32) {
607
+ throw new Error('SESSION_SECRET must be at least 32 bytes. It is stretched with a single SHA-256 into ' +
608
+ 'the session sealing key, so a short or guessable value is brute-forceable offline ' +
609
+ 'and lets an attacker forge a session for ANY account. Generate one with ' +
610
+ '`openssl rand -hex 32`.');
611
+ }
612
+ // Reject the obvious placeholders outright — length alone would pass 'changeme-changeme-…'.
613
+ const weakSecrets = ['changeme', 'change-me', 'secret', 'password', 'pierre', 'test'];
614
+ const lowered = config.sessionSecret.toLowerCase();
615
+ if (weakSecrets.some((w) => lowered.startsWith(w) || lowered === w.repeat(4))) {
616
+ throw new Error('SESSION_SECRET looks like a placeholder. Generate a real one: `openssl rand -hex 32`.');
617
+ }
618
+ // A cookie marked `secure` is only sent over HTTPS, and that flag is derived from
619
+ // APP_BASE_URL's scheme (api/plugins/auth.ts). An https deployment configured with an
620
+ // http:// base URL therefore ships its session cookie in the clear over the public
621
+ // internet — a silent, total session-hijack exposure from a one-character typo. Refuse to
622
+ // boot a non-localhost cloud deployment on plain http.
623
+ if (!config.appBaseUrl.startsWith('https://')) {
624
+ const host = (() => {
625
+ try {
626
+ return new URL(config.appBaseUrl).hostname;
627
+ }
628
+ catch {
629
+ return '';
630
+ }
631
+ })();
632
+ const isLocalTest = host === 'localhost' || host === '127.0.0.1' || host === '::1';
633
+ if (!isLocalTest) {
634
+ throw new Error(`APP_BASE_URL must be https:// in cloud mode (got "${config.appBaseUrl}"). The ` +
635
+ 'session cookie\'s `secure` flag is derived from this scheme, so an http:// value ' +
636
+ 'would send the session cookie unencrypted. Use http:// only for localhost testing.');
637
+ }
638
+ }
639
+ // When Pro is cloud-enabled the SUMMARY seam needs its OWN metered credential: there is no
640
+ // ambient logged-in `claude` session on Railway, so without SUMMARY_ANTHROPIC_API_KEY every
641
+ // summary completion has no credential and silently fails. Fail loud instead.
642
+ if (config.proEnabled && !process.env.SUMMARY_ANTHROPIC_API_KEY) {
643
+ throw new Error('Cloud Pro (PRO_CLOUD_ENABLED=true) requires SUMMARY_ANTHROPIC_API_KEY — the metered ' +
644
+ 'Anthropic key the summary seam spends against (no ambient Claude session exists in cloud). ' +
645
+ 'Set it or unset PRO_CLOUD_ENABLED. See docs/DEPLOY-RAILWAY.md.');
646
+ }
647
+ }
648
+ //# sourceMappingURL=config.js.map