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.
- package/LICENSE +110 -0
- package/README.md +133 -0
- package/dist/ai/runtime.js +535 -0
- package/dist/api/plugins/auth.js +169 -0
- package/dist/api/plugins/error-handler.js +114 -0
- package/dist/api/plugins/rate-limit.js +855 -0
- package/dist/api/plugins/security.js +321 -0
- package/dist/api/routes/activity.js +103 -0
- package/dist/api/routes/ai-runtime.js +67 -0
- package/dist/api/routes/auth.js +194 -0
- package/dist/api/routes/billing.js +209 -0
- package/dist/api/routes/bot-triage.js +1289 -0
- package/dist/api/routes/conflicts.js +730 -0
- package/dist/api/routes/contact.js +318 -0
- package/dist/api/routes/daily-brief.js +58 -0
- package/dist/api/routes/flow.js +71 -0
- package/dist/api/routes/health.js +7 -0
- package/dist/api/routes/insights.js +200 -0
- package/dist/api/routes/me.js +458 -0
- package/dist/api/routes/mentions.js +22 -0
- package/dist/api/routes/mergers.js +8 -0
- package/dist/api/routes/ml-labels.js +109 -0
- package/dist/api/routes/open-prs.js +35 -0
- package/dist/api/routes/prs.js +1633 -0
- package/dist/api/routes/reactions.js +217 -0
- package/dist/api/routes/repos.js +563 -0
- package/dist/api/routes/search.js +41 -0
- package/dist/api/routes/threads.js +118 -0
- package/dist/api/routes/timeline.js +150 -0
- package/dist/api/routes/users.js +67 -0
- package/dist/api/routes/webhooks.js +181 -0
- package/dist/api/routes/workspaces.js +322 -0
- package/dist/app.js +274 -0
- package/dist/ascii.js +19 -0
- package/dist/auth/account.js +482 -0
- package/dist/auth/crypto.js +46 -0
- package/dist/bot-triage/resolve.js +59 -0
- package/dist/cli.js +388 -0
- package/dist/coding/activity.js +80 -0
- package/dist/coding/agent.js +288 -0
- package/dist/coding/ai-fix/auto-fix.js +212 -0
- package/dist/coding/ai-fix/manager.js +370 -0
- package/dist/coding/ai-fix/persist.js +213 -0
- package/dist/coding/ai-fix/pr-context.js +58 -0
- package/dist/coding/ai-fix/prompts.js +138 -0
- package/dist/coding/ai-fix/review-seed.js +357 -0
- package/dist/coding/ai-fix/routes.js +425 -0
- package/dist/coding/git-ops.js +321 -0
- package/dist/coding/git.js +184 -0
- package/dist/coding/merge.js +262 -0
- package/dist/coding/safe-path.js +75 -0
- package/dist/coding/schema.js +43 -0
- package/dist/config.js +648 -0
- package/dist/conflict/diff.js +304 -0
- package/dist/conflict/git.js +118 -0
- package/dist/conflict/hash.js +73 -0
- package/dist/conflict/janitor.js +218 -0
- package/dist/conflict/land.js +675 -0
- package/dist/conflict/model-types.js +2 -0
- package/dist/conflict/model.js +1081 -0
- package/dist/conflict/parse.js +142 -0
- package/dist/conflict/seam.js +136 -0
- package/dist/conflict/session.js +532 -0
- package/dist/conflict/suggestion.js +398 -0
- package/dist/conflict/tokens.js +72 -0
- package/dist/data-dir.js +112 -0
- package/dist/db/actor-lanes.js +213 -0
- package/dist/db/automation-ids.js +97 -0
- package/dist/db/automation-output.js +349 -0
- package/dist/db/blast-radius-query.js +36 -0
- package/dist/db/blast-radius.js +226 -0
- package/dist/db/bot-dormancy.js +115 -0
- package/dist/db/bot-overlap.js +579 -0
- package/dist/db/bot-volume.js +722 -0
- package/dist/db/bot-window.js +35 -0
- package/dist/db/branch-queries.js +310 -0
- package/dist/db/change-shape.js +243 -0
- package/dist/db/changepoint.js +75 -0
- package/dist/db/cleanup.js +36 -0
- package/dist/db/client.js +102 -0
- package/dist/db/code-loc.js +388 -0
- package/dist/db/credits.js +79 -0
- package/dist/db/daily-brief.js +406 -0
- package/dist/db/dependency-cards.js +145 -0
- package/dist/db/erase-account.js +255 -0
- package/dist/db/export-account.js +189 -0
- package/dist/db/failing-checks.js +124 -0
- package/dist/db/file-coupling.js +197 -0
- package/dist/db/flow-detail.js +661 -0
- package/dist/db/flow-pointers.js +197 -0
- package/dist/db/flow-settings.js +67 -0
- package/dist/db/forecast.js +114 -0
- package/dist/db/line-overlap.js +81 -0
- package/dist/db/merge-queue.js +189 -0
- package/dist/db/migrate.js +17 -0
- package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
- package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
- package/dist/db/migrations/0002_famous_sersi.sql +9 -0
- package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
- package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
- package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
- package/dist/db/migrations/0006_cute_violations.sql +45 -0
- package/dist/db/migrations/0007_dark_anthem.sql +2 -0
- package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
- package/dist/db/migrations/0009_lean_bodies.sql +13 -0
- package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
- package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
- package/dist/db/migrations/0012_findings_included_default.sql +7 -0
- package/dist/db/migrations/0013_review_routing.sql +9 -0
- package/dist/db/migrations/0014_account_last_active.sql +6 -0
- package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
- package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
- package/dist/db/migrations/0017_account_display_name.sql +7 -0
- package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
- package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
- package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
- package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
- package/dist/db/migrations/0022_ci_status_events.sql +21 -0
- package/dist/db/migrations/0023_ai_usage.sql +21 -0
- package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
- package/dist/db/migrations/0025_account_billing.sql +7 -0
- package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
- package/dist/db/migrations/0027_user_github_type.sql +6 -0
- package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
- package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
- package/dist/db/migrations/0030_teams.sql +32 -0
- package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
- package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
- package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
- package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
- package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
- package/dist/db/migrations/0036_search_index.sql +25 -0
- package/dist/db/migrations/0037_author_indexes.sql +15 -0
- package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
- package/dist/db/migrations/0039_branch_status.sql +44 -0
- package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
- package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
- package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
- package/dist/db/migrations/0043_account_reviewers.sql +131 -0
- package/dist/db/migrations/0044_workspaces.sql +138 -0
- package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
- package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
- package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
- package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
- package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
- package/dist/db/migrations/0050_users_app_slug.sql +12 -0
- package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
- package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
- package/dist/db/migrations/0053_automation_roles.sql +79 -0
- package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
- package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
- package/dist/db/migrations/0056_pr_mentions.sql +22 -0
- package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
- package/dist/db/migrations/0058_pending_mute.sql +53 -0
- package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
- package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
- package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
- package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
- package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
- package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
- package/dist/db/migrations/0066_review_request_events.sql +31 -0
- package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
- package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
- package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
- package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
- package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
- package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
- package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
- package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
- package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
- package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
- package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
- package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
- package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
- package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
- package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
- package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
- package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
- package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
- package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
- package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
- package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
- package/dist/db/migrations/meta/_journal.json +566 -0
- package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
- package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
- package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
- package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
- package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
- package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
- package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
- package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
- package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
- package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
- package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
- package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
- package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
- package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
- package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
- package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
- package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
- package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
- package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
- package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
- package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
- package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
- package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
- package/dist/db/migrations-pg/0023_search_index.sql +19 -0
- package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
- package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
- package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
- package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
- package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
- package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
- package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
- package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
- package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
- package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
- package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
- package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
- package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
- package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
- package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
- package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
- package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
- package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
- package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
- package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
- package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
- package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
- package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
- package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
- package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
- package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
- package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
- package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
- package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
- package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
- package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
- package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
- package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
- package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
- package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
- package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
- package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
- package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
- package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
- package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
- package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
- package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
- package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
- package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
- package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
- package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
- package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
- package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
- package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
- package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
- package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
- package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
- package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
- package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
- package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
- package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
- package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
- package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
- package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
- package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
- package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
- package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
- package/dist/db/migrations-pg/meta/_journal.json +475 -0
- package/dist/db/ml-labels.js +1666 -0
- package/dist/db/my-turn-dismissals.js +343 -0
- package/dist/db/my-turn-settings.js +34 -0
- package/dist/db/pending-classify.js +46 -0
- package/dist/db/pending-mute.js +171 -0
- package/dist/db/pending-tabs.js +238 -0
- package/dist/db/period-metrics.js +820 -0
- package/dist/db/person-period.js +763 -0
- package/dist/db/pr-intervals.js +930 -0
- package/dist/db/pr-liveness.js +240 -0
- package/dist/db/pr-mentions.js +279 -0
- package/dist/db/pr-merge-queue-stamp.js +91 -0
- package/dist/db/queries.js +14631 -0
- package/dist/db/reaction-targets.js +94 -0
- package/dist/db/repo-activity.js +171 -0
- package/dist/db/retention.js +138 -0
- package/dist/db/review-threads-for-review.js +240 -0
- package/dist/db/run-migrations.js +24 -0
- package/dist/db/schema.pg.js +1220 -0
- package/dist/db/schema.sqlite.js +1801 -0
- package/dist/db/search.js +317 -0
- package/dist/db/security-alerts.js +218 -0
- package/dist/db/synthesis-input.js +565 -0
- package/dist/db/triage.js +320 -0
- package/dist/db/usage.js +63 -0
- package/dist/db/work-plan.js +1086 -0
- package/dist/db/working-hours.js +161 -0
- package/dist/feed/my-turn.js +122 -0
- package/dist/github/actions-logs.js +397 -0
- package/dist/github/auth.js +86 -0
- package/dist/github/branch-queries.js +229 -0
- package/dist/github/client.js +454 -0
- package/dist/github/codeowners.js +221 -0
- package/dist/github/commit-checks.js +105 -0
- package/dist/github/compare.js +110 -0
- package/dist/github/diff-anchor.js +227 -0
- package/dist/github/mutations.js +706 -0
- package/dist/github/pr-file-content.js +254 -0
- package/dist/github/pr-liveness.js +161 -0
- package/dist/github/queries.js +875 -0
- package/dist/github/rate-budget.js +129 -0
- package/dist/github/reactions.js +161 -0
- package/dist/github/reviewer-suggest.js +104 -0
- package/dist/github/team-reviewers.js +96 -0
- package/dist/index.js +184 -0
- package/dist/merge/auto-merge-runner.js +960 -0
- package/dist/ml/severity-client.js +218 -0
- package/dist/pr/detail-enricher.js +26 -0
- package/dist/pro/bind.js +302 -0
- package/dist/pro/contract.js +37 -0
- package/dist/pro/migrate.js +64 -0
- package/dist/review/agent-context.js +127 -0
- package/dist/review/agent.js +385 -0
- package/dist/review/agentic.js +59 -0
- package/dist/review/auth.js +94 -0
- package/dist/review/chat-agent.js +179 -0
- package/dist/review/claude-review/ai-ready.js +16 -0
- package/dist/review/claude-review/auto-settings.js +139 -0
- package/dist/review/claude-review/auto.js +244 -0
- package/dist/review/claude-review/chat.js +489 -0
- package/dist/review/claude-review/ci-failures.js +350 -0
- package/dist/review/claude-review/follow-up.js +310 -0
- package/dist/review/claude-review/manager.js +686 -0
- package/dist/review/claude-review/persist.js +996 -0
- package/dist/review/claude-review/prompts.js +357 -0
- package/dist/review/claude-review/routes.js +518 -0
- package/dist/review/claude-review/routing.js +61 -0
- package/dist/review/claude-review/specialists.js +190 -0
- package/dist/review/claude-review/threads.js +265 -0
- package/dist/review/claude-review/ticket.js +282 -0
- package/dist/review/clone-hygiene.js +380 -0
- package/dist/review/clone-manager.js +367 -0
- package/dist/review/llm.js +113 -0
- package/dist/review/model-options.js +48 -0
- package/dist/review/plugin-providers.js +12 -0
- package/dist/review/post-review.js +216 -0
- package/dist/review/post-seam.js +161 -0
- package/dist/review/prepare.js +168 -0
- package/dist/review/pricing.js +17 -0
- package/dist/review/schema.js +156 -0
- package/dist/review/submit-map.js +41 -0
- package/dist/review/usage.js +90 -0
- package/dist/shared/claude-review.js +335 -0
- package/dist/shared/conflict-fold.js +151 -0
- package/dist/shared/conflicts.js +30 -0
- package/dist/shared/flow-settings.js +191 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/my-turn-settings.js +238 -0
- package/dist/shared/pending-rules.js +281 -0
- package/dist/shared/product.js +54 -0
- package/dist/shared/types.js +896 -0
- package/dist/status.js +386 -0
- package/dist/sync/adaptive.js +185 -0
- package/dist/sync/app-attribution.js +66 -0
- package/dist/sync/auth-notices.js +36 -0
- package/dist/sync/backfill-ci-history.js +315 -0
- package/dist/sync/backfill-commit-headlines.js +136 -0
- package/dist/sync/backfill-pr-security.js +175 -0
- package/dist/sync/backfill-review-requests.js +97 -0
- package/dist/sync/benchmark-rollup.js +50 -0
- package/dist/sync/bot-detection.js +309 -0
- package/dist/sync/bot-resolution-markers.js +30 -0
- package/dist/sync/branch-status.js +634 -0
- package/dist/sync/classify-change-shape.js +150 -0
- package/dist/sync/commit-files.js +74 -0
- package/dist/sync/derive-thread-state.js +99 -0
- package/dist/sync/drain-review-threads.js +212 -0
- package/dist/sync/hydrate-detail.js +482 -0
- package/dist/sync/mention-scan.js +89 -0
- package/dist/sync/ml-enrichment.js +478 -0
- package/dist/sync/pr-change-signal.js +96 -0
- package/dist/sync/pr-liveness-sweep.js +139 -0
- package/dist/sync/pr-settle.js +381 -0
- package/dist/sync/refresh-pr.js +163 -0
- package/dist/sync/reparse-vendor-badges.js +495 -0
- package/dist/sync/resync-after-write.js +280 -0
- package/dist/sync/review-fingerprint.js +117 -0
- package/dist/sync/reviewer-behavior.js +103 -0
- package/dist/sync/reviewer-classify.js +503 -0
- package/dist/sync/routing-files.js +97 -0
- package/dist/sync/scheduled-jobs.js +12 -0
- package/dist/sync/scheduler.js +159 -0
- package/dist/sync/security-detect.js +521 -0
- package/dist/sync/sync-manager.js +750 -0
- package/dist/sync/sync-one-pr.js +205 -0
- package/dist/sync/sync-repo.js +400 -0
- package/dist/sync/unsettled-prs.js +506 -0
- package/dist/sync/upsert.js +1190 -0
- package/package.json +56 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
- package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
- package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
- package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public/assets/highlight-BEHUn5zE.css +10 -0
- package/public/assets/highlight-CIIZLa6Z.js +12 -0
- package/public/assets/index-BDnYs6zg.js +73 -0
- package/public/assets/index-mCuI--RC.css +1 -0
- package/public/assets/markdown-I_JLLVAZ.js +32 -0
- package/public/assets/perfProbe-Da13EhTE.js +1 -0
- package/public/assets/tanstack-5qsxxaLe.js +17 -0
- package/public/assets/vis-C3BCkP5U.js +1280 -0
- package/public/assets/vis-ziCVehMy.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-48.png +0 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +41 -0
- package/public-landing/apple-touch-icon.png +0 -0
- package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
- package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public-landing/assets/index-CA3eEGXO.js +40 -0
- package/public-landing/assets/index-naQrQ6Fe.css +1 -0
- package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
- package/public-landing/bots/index.html +156 -0
- package/public-landing/contact/index.html +156 -0
- package/public-landing/cookies/index.html +156 -0
- package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
- package/public-landing/demo/limn-walkthrough.cues.json +44 -0
- package/public-landing/demo/limn-walkthrough.mp4 +0 -0
- package/public-landing/favicon-16.png +0 -0
- package/public-landing/favicon-32.png +0 -0
- package/public-landing/favicon-48.png +0 -0
- package/public-landing/favicon.svg +5 -0
- package/public-landing/features/index.html +156 -0
- package/public-landing/for-developers/index.html +156 -0
- package/public-landing/for-managers/index.html +156 -0
- package/public-landing/how-it-works/index.html +156 -0
- package/public-landing/how-we-measure/index.html +156 -0
- package/public-landing/icon-192.png +0 -0
- package/public-landing/icon-512.png +0 -0
- package/public-landing/index.html +156 -0
- package/public-landing/insights/index.html +156 -0
- package/public-landing/og-image.png +0 -0
- package/public-landing/pricing/index.html +156 -0
- package/public-landing/privacy/index.html +156 -0
- package/public-landing/pro/index.html +156 -0
- package/public-landing/reviews/index.html +156 -0
- package/public-landing/robots.txt +8 -0
- package/public-landing/shots/benchmark.png +0 -0
- package/public-landing/shots/bot-roi.png +0 -0
- package/public-landing/shots/bot-settings.png +0 -0
- package/public-landing/shots/chronology.png +0 -0
- package/public-landing/shots/feed.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/free-reports.png +0 -0
- package/public-landing/shots/pending-board.png +0 -0
- package/public-landing/shots/pending-card.png +0 -0
- package/public-landing/shots/period-report.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/pr-threads.png +0 -0
- package/public-landing/shots/reach.png +0 -0
- package/public-landing/shots/repo-rows.png +0 -0
- package/public-landing/shots/severity-findings.png +0 -0
- package/public-landing/shots/severity-strip.png +0 -0
- package/public-landing/site.webmanifest +14 -0
- package/public-landing/sitemap.xml +51 -0
- 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
|