openlanes 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +62 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +24 -3
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +32 -10
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +26 -2
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -454
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -64
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +94 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1229 -116
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +38 -105
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +236 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +39 -11
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +665 -0
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +221 -31
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +290 -0
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +114 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +36 -1
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +38 -4
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +12 -8
- data/lib/openlanes/live/store.rb +36 -1
- data/lib/openlanes/live/subscriber.rb +93 -0
- data/lib/openlanes/live.rb +71 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +66 -7
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +110 -21
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +287 -7
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +210 -14
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +213 -15
- data/lib/openlanes/web/context.rb +33 -9
- data/lib/openlanes/web/demo.rb +39 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +137 -9
- data/lib/openlanes/web/launcher.rb +70 -8
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +31 -10
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +19 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -881
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-CI74IkWP.js +0 -198
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -20,11 +20,55 @@ module OpenLanes
|
|
|
20
20
|
# carries itself from worktree creation through to a merged pull request with
|
|
21
21
|
# nobody having to remember it.
|
|
22
22
|
class Worktree
|
|
23
|
-
# `
|
|
24
|
-
# checkout does not get.
|
|
25
|
-
#
|
|
26
|
-
# usable from a shell on a machine that
|
|
27
|
-
|
|
23
|
+
# `setup-worktree`, which copies in the ignored-but-required files a new
|
|
24
|
+
# checkout does not get. It is the user's own script, not this gem's:
|
|
25
|
+
# shelling out rather than reimplementing the rules here keeps one answer
|
|
26
|
+
# to "what does a worktree need", usable from a shell on a machine that
|
|
27
|
+
# has never run `bundle install`.
|
|
28
|
+
SEEDER_NAME = "setup-worktree"
|
|
29
|
+
|
|
30
|
+
# Names the seeder outright, ahead of every place {.seeder} searches.
|
|
31
|
+
SEEDER_ENV = "OPENLANES_SETUP_WORKTREE"
|
|
32
|
+
|
|
33
|
+
# Searched after `PATH`, where the user's own tooling keeps it.
|
|
34
|
+
SEEDER_HOMES = %w[~/.claude/bin ~/.agents/bin].freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# The seeder to run, or nil when none is installed.
|
|
38
|
+
#
|
|
39
|
+
# It used to be `bin/setup-worktree` beside this file, which the gem
|
|
40
|
+
# never shipped, so every installed run seeded nothing and said so
|
|
41
|
+
# once per agent.
|
|
42
|
+
#
|
|
43
|
+
# @param env [Hash] the environment, for `PATH` and {SEEDER_ENV}
|
|
44
|
+
# @return [String, nil] an executable path
|
|
45
|
+
def seeder(env = ENV) = seeder_candidates(env).find { File.file?(it) && File.executable?(it) }
|
|
46
|
+
|
|
47
|
+
# Every place {.seeder} looks, in order.
|
|
48
|
+
#
|
|
49
|
+
# @param env [Hash]
|
|
50
|
+
# @return [Array<String>]
|
|
51
|
+
def seeder_candidates(env = ENV)
|
|
52
|
+
on_path = env.fetch("PATH", "").split(File::PATH_SEPARATOR).reject(&:empty?).map { File.join(it, SEEDER_NAME) }
|
|
53
|
+
homes = SEEDER_HOMES.map { File.join(File.expand_path(it), SEEDER_NAME) }
|
|
54
|
+
[env[SEEDER_ENV].to_s, *on_path, *homes].reject(&:empty?).uniq
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [String] where {.seeder} looks, for a person
|
|
58
|
+
def seeder_search = "$#{SEEDER_ENV}, #{SEEDER_NAME} on PATH, #{SEEDER_HOMES.join(", ")}"
|
|
59
|
+
|
|
60
|
+
# The main checkout of the repository +path+ is in, even when +path+
|
|
61
|
+
# is itself a worktree. Worktrees live beside the main checkout, so a
|
|
62
|
+
# command run from inside one and rooted there looks for
|
|
63
|
+
# `<worktree>.worktrees` and finds nothing.
|
|
64
|
+
#
|
|
65
|
+
# @param path [String] any checkout of the repository
|
|
66
|
+
# @return [String] the main checkout; +path+ when git cannot say
|
|
67
|
+
def main_root(path)
|
|
68
|
+
common = `git -C #{path.shellescape} rev-parse --path-format=absolute --git-common-dir 2>/dev/null`.strip
|
|
69
|
+
common.empty? ? path : File.dirname(common)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
28
72
|
|
|
29
73
|
# One plan's isolated checkout.
|
|
30
74
|
#
|
|
@@ -50,9 +94,11 @@ module OpenLanes
|
|
|
50
94
|
# @param root [String] the main repository
|
|
51
95
|
# @param dir [String, nil] where worktrees go; defaults to a sibling
|
|
52
96
|
# @param user [String] branch namespace
|
|
53
|
-
|
|
97
|
+
# @param seeder [String, nil] the seeder to run; nil runs none
|
|
98
|
+
def initialize(root:, dir: nil, user: ENV["USER"] || "agent", seeder: self.class.seeder)
|
|
54
99
|
@root = File.expand_path(root)
|
|
55
100
|
@user = user
|
|
101
|
+
@seeder = seeder
|
|
56
102
|
@dir = File.expand_path(dir || default_dir)
|
|
57
103
|
end
|
|
58
104
|
|
|
@@ -103,6 +149,18 @@ module OpenLanes
|
|
|
103
149
|
Checkout.new(ordinal: feature.ordinal, branch:, path:, created: true)
|
|
104
150
|
end
|
|
105
151
|
|
|
152
|
+
# Bring a plan's checkout up to date with the default branch before an
|
|
153
|
+
# agent runs in it, and say what happened in the progress log.
|
|
154
|
+
#
|
|
155
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
156
|
+
# @param hold [String, nil] see {Refresh#call}
|
|
157
|
+
# @return [OpenLanes::Vcs::Refresh::Outcome]
|
|
158
|
+
def refresh(checkout, hold: nil)
|
|
159
|
+
outcome = Refresh.new(path: checkout.path).call(hold:)
|
|
160
|
+
UI.log(outcome.summary, plan: checkout.ordinal.to_s) unless outcome.kind == :current
|
|
161
|
+
outcome
|
|
162
|
+
end
|
|
163
|
+
|
|
106
164
|
# Copy in the files git ignores and a new checkout therefore lacks: `.env`
|
|
107
165
|
# and friends, and credential keys.
|
|
108
166
|
#
|
|
@@ -118,14 +176,16 @@ module OpenLanes
|
|
|
118
176
|
# @param path [String] the worktree to seed
|
|
119
177
|
# @return [Boolean] whether the seeder ran and succeeded
|
|
120
178
|
def seed(path)
|
|
121
|
-
unless File.executable?(
|
|
122
|
-
UI.log("worktree: #{
|
|
179
|
+
unless @seeder && File.executable?(@seeder)
|
|
180
|
+
UI.log("worktree: no #{SEEDER_NAME} found (searched #{self.class.seeder_search}), " \
|
|
181
|
+
"so #{path} has no .env or credential keys",
|
|
182
|
+
level: :warn)
|
|
123
183
|
return false
|
|
124
184
|
end
|
|
125
185
|
|
|
126
|
-
return true if system(
|
|
186
|
+
return true if system(@seeder, "--quiet", path, out: File::NULL, err: File::NULL)
|
|
127
187
|
|
|
128
|
-
UI.log("worktree: could not seed #{path}; its suite may fail on a missing key")
|
|
188
|
+
UI.log("worktree: could not seed #{path}; its suite may fail on a missing key", level: :warn)
|
|
129
189
|
false
|
|
130
190
|
end
|
|
131
191
|
|
|
@@ -164,7 +224,82 @@ module OpenLanes
|
|
|
164
224
|
#
|
|
165
225
|
# @return [Array<String>] paths removed
|
|
166
226
|
def prune
|
|
167
|
-
list.select { |path| clean?(path) }.
|
|
227
|
+
list.select { |path| clean?(path) }.select { |path| remove(path) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @param path [String] a checkout
|
|
231
|
+
# @return [String] the branch it has checked out; empty when detached
|
|
232
|
+
def branch_of(path) = `git -C #{path.shellescape} branch --show-current 2>/dev/null`.strip
|
|
233
|
+
|
|
234
|
+
# Every checkout of one plan: the one at {#path_for}, and any other
|
|
235
|
+
# whose folder starts with the same ordinal. A folder-name parse that
|
|
236
|
+
# once leaked the status into the slug (`018.00-018.00━━🟡━━❯x`) gave a
|
|
237
|
+
# plan three checkouts, and only the ordinal finds all of them.
|
|
238
|
+
#
|
|
239
|
+
# @param feature [OpenLanes::Plans::Feature]
|
|
240
|
+
# @return [Array<String>] absolute paths, as git lists them
|
|
241
|
+
def checkouts_of(feature)
|
|
242
|
+
prefix = "#{feature.ordinal}-"
|
|
243
|
+
list.select { File.basename(it).start_with?(prefix) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Remove a checkout whose work is on the default branch, and its local
|
|
247
|
+
# branch with it.
|
|
248
|
+
#
|
|
249
|
+
# A merged pull request ends a plan's worktree; left behind, they piled
|
|
250
|
+
# up to forty-eight in one repository, each looking like work in
|
|
251
|
+
# progress. Removal cannot be undone for anything that never left the
|
|
252
|
+
# machine, so every condition below has to hold first, and a refusal
|
|
253
|
+
# says which one failed and leaves the checkout exactly as it was.
|
|
254
|
+
#
|
|
255
|
+
# "On the default branch" is a tree comparison, never `git log
|
|
256
|
+
# main..branch`: a squash merge rewrites every hash, so that log lists
|
|
257
|
+
# every commit of a branch that landed whole. A stacked pull request
|
|
258
|
+
# merged into its parent's branch reports MERGED too, which is why the
|
|
259
|
+
# base is checked.
|
|
260
|
+
#
|
|
261
|
+
# @param path [String] a checkout from {#list}
|
|
262
|
+
# @param merge_commit [String] the commit the pull request merged as
|
|
263
|
+
# @param base [String] the branch the pull request merged into
|
|
264
|
+
# @param default_branch [String]
|
|
265
|
+
# @param held [#call, nil] path -> who holds a lock in it, or nil
|
|
266
|
+
# @param dry_run [Boolean] check everything, remove nothing
|
|
267
|
+
# @return [Retirement]
|
|
268
|
+
def retire(path, merge_commit:, base:, default_branch: Refresh.default_branch(root), held: nil, dry_run: false)
|
|
269
|
+
branch = branch_of(path)
|
|
270
|
+
sha = `git -C #{path.shellescape} rev-parse HEAD 2>/dev/null`.strip
|
|
271
|
+
refusal = retire_refusal(path, sha, merge_commit, base, default_branch, held)
|
|
272
|
+
return Retirement.new(path:, branch:, sha:, removed: false, reason: refusal) if refusal
|
|
273
|
+
return Retirement.new(path:, branch:, sha:, removed: false, reason: "would be removed: landed in #{merge_commit[0, 8]}") if dry_run
|
|
274
|
+
return Retirement.new(path:, branch:, sha:, removed: false, reason: "git refused to remove it") unless remove(path)
|
|
275
|
+
|
|
276
|
+
# -D, not -d: a squash-merged branch looks unmerged to -d. The tree
|
|
277
|
+
# comparison above is the check that makes this safe.
|
|
278
|
+
system("git", "-C", root, "branch", "-D", branch, out: File::NULL, err: File::NULL) unless branch.empty?
|
|
279
|
+
Retirement.new(path:, branch:, sha:, removed: true, reason: "landed in #{merge_commit[0, 8]}")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# What {#retire} did with one checkout.
|
|
283
|
+
#
|
|
284
|
+
# @!attribute [r] path
|
|
285
|
+
# @return [String]
|
|
286
|
+
# @!attribute [r] branch
|
|
287
|
+
# @return [String] empty for a detached checkout
|
|
288
|
+
# @!attribute [r] sha
|
|
289
|
+
# @return [String] the branch's last commit, so it can be restored
|
|
290
|
+
# @!attribute [r] removed
|
|
291
|
+
# @return [Boolean]
|
|
292
|
+
# @!attribute [r] reason
|
|
293
|
+
# @return [String] why it was removed, or why not
|
|
294
|
+
Retirement = Data.define(:path, :branch, :sha, :removed, :reason) do
|
|
295
|
+
# @return [Boolean]
|
|
296
|
+
def removed? = removed
|
|
297
|
+
|
|
298
|
+
# @return [String] one line for the progress log
|
|
299
|
+
def to_s
|
|
300
|
+
name = File.basename(path)
|
|
301
|
+
removed? ? "worktree #{name} removed (#{branch} was #{sha[0, 8]}): #{reason}" : "worktree #{name} kept: #{reason}"
|
|
302
|
+
end
|
|
168
303
|
end
|
|
169
304
|
|
|
170
305
|
# Where one plan's checkout lives, whether or not it has been made.
|
|
@@ -207,10 +342,14 @@ module OpenLanes
|
|
|
207
342
|
true
|
|
208
343
|
end
|
|
209
344
|
|
|
345
|
+
# Never `--force`: git refuses a checkout with uncommitted or untracked
|
|
346
|
+
# work, and that refusal is the last guard against deleting it.
|
|
347
|
+
# Ignored files, the seeded `.env` among them, do not count.
|
|
348
|
+
#
|
|
210
349
|
# @param path [String]
|
|
211
|
-
# @return [
|
|
350
|
+
# @return [Boolean] whether git removed it
|
|
212
351
|
def remove(path)
|
|
213
|
-
system("git", "-C", root, "worktree", "remove",
|
|
352
|
+
system("git", "-C", root, "worktree", "remove", path, out: File::NULL, err: File::NULL)
|
|
214
353
|
end
|
|
215
354
|
|
|
216
355
|
private
|
|
@@ -232,12 +371,69 @@ module OpenLanes
|
|
|
232
371
|
# @return [Boolean]
|
|
233
372
|
def clean?(path) = `git -C #{path.shellescape} status --porcelain 2>/dev/null`.strip.empty?
|
|
234
373
|
|
|
374
|
+
# The first reason {#retire} must not remove a checkout, or nil.
|
|
375
|
+
#
|
|
376
|
+
# @return [String, nil]
|
|
377
|
+
def retire_refusal(path, sha, merge_commit, base, default_branch, held)
|
|
378
|
+
return "not a worktree openlanes manages" unless managed?(path)
|
|
379
|
+
return "its pull request merged into #{base}, not #{default_branch}" unless base == default_branch
|
|
380
|
+
return "it has uncommitted changes" unless clean?(path)
|
|
381
|
+
|
|
382
|
+
holder = held&.call(path)
|
|
383
|
+
return "#{holder} holds a lock in it" if holder
|
|
384
|
+
return "merge commit #{merge_commit[0, 8]} is not in this repository, even after a fetch" unless commit?(merge_commit)
|
|
385
|
+
|
|
386
|
+
"its branch has work that is not in #{merge_commit[0, 8]}" unless landed?(sha, merge_commit)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# @param path [String]
|
|
390
|
+
# @return [Boolean] one of {#list}, and not the main checkout
|
|
391
|
+
def managed?(path)
|
|
392
|
+
here = canonical(path)
|
|
393
|
+
here != canonical(root) && list.any? { canonical(it) == here }
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Present locally, fetching once if not: the engine merged it a moment
|
|
397
|
+
# ago on GitHub, and nothing has fetched since.
|
|
398
|
+
#
|
|
399
|
+
# @param sha [String]
|
|
400
|
+
# @return [Boolean]
|
|
401
|
+
def commit?(sha)
|
|
402
|
+
known = -> { system("git", "-C", root, "cat-file", "-e", "#{sha}^{commit}", out: File::NULL, err: File::NULL) }
|
|
403
|
+
known.call || (system("git", "-C", root, "fetch", "--quiet", "origin", out: File::NULL, err: File::NULL) && known.call)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# The branch tip's tree is the merge commit's, or every file the branch
|
|
407
|
+
# changed reads the same in the merge commit. The second covers a
|
|
408
|
+
# default branch that moved on before the merge.
|
|
409
|
+
#
|
|
410
|
+
# @param sha [String] the branch tip
|
|
411
|
+
# @param merge_commit [String]
|
|
412
|
+
# @return [Boolean]
|
|
413
|
+
def landed?(sha, merge_commit)
|
|
414
|
+
tree = ->(ref) { `git -C #{root.shellescape} rev-parse #{"#{ref}^{tree}".shellescape} 2>/dev/null`.strip }
|
|
415
|
+
return true if tree.call(sha) == tree.call(merge_commit)
|
|
416
|
+
|
|
417
|
+
fork = `git -C #{root.shellescape} merge-base #{sha.shellescape} #{merge_commit.shellescape} 2>/dev/null`.strip
|
|
418
|
+
return false if fork.empty?
|
|
419
|
+
|
|
420
|
+
files = `git -C #{root.shellescape} diff --name-only #{fork.shellescape} #{sha.shellescape} 2>/dev/null`.lines(chomp: true)
|
|
421
|
+
files.empty? || system("git", "-C", root, "diff", "--quiet", sha, merge_commit, "--", *files, out: File::NULL, err: File::NULL)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# A new branch starts from the freshly fetched default branch on
|
|
425
|
+
# `origin`, not from whatever the main checkout has checked out: that
|
|
426
|
+
# is often a branch of its own, or a `main` nobody has pulled in weeks,
|
|
427
|
+
# and the plan's agent then builds against code that is not there.
|
|
428
|
+
# Without such a remote branch it falls back to the main checkout's
|
|
429
|
+
# HEAD, as it always did.
|
|
430
|
+
#
|
|
235
431
|
# @param branch [String]
|
|
236
432
|
# @param path [String]
|
|
237
433
|
# @return [void]
|
|
238
434
|
def add(branch, path)
|
|
239
435
|
exists = system("git", "-C", root, "show-ref", "--verify", "--quiet", "refs/heads/#{branch}")
|
|
240
|
-
args = exists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path]
|
|
436
|
+
args = exists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, *Refresh.fetch(root)]
|
|
241
437
|
|
|
242
438
|
return if system("git", "-C", root, *args, out: File::NULL, err: File::NULL)
|
|
243
439
|
|
data/lib/openlanes/vcs.rb
CHANGED
|
@@ -15,5 +15,23 @@ module OpenLanes
|
|
|
15
15
|
|
|
16
16
|
# @return [OpenLanes::Vcs::Publisher]
|
|
17
17
|
def publisher(**) = Publisher.new(**)
|
|
18
|
+
|
|
19
|
+
# @param github [OpenLanes::Vcs::GitHub]
|
|
20
|
+
# @return [OpenLanes::Vcs::Review]
|
|
21
|
+
def review(github: GitHub.new) = Review.new(github:)
|
|
22
|
+
|
|
23
|
+
# @param root [String, nil] the checkout whose repository is synced; nil
|
|
24
|
+
# means the current directory's
|
|
25
|
+
# @param github [OpenLanes::Vcs::GitHub]
|
|
26
|
+
# @return [OpenLanes::Vcs::LabelSync]
|
|
27
|
+
def label_sync(root: nil, github: GitHub.new(chdir: root)) = LabelSync.new(github:)
|
|
28
|
+
|
|
29
|
+
# Run before {label_sync}, once per repository: the repository's
|
|
30
|
+
# {Plans::ReviewLabel::MIGRATED} label marks it done.
|
|
31
|
+
#
|
|
32
|
+
# @param root [String, nil] as for {label_sync}
|
|
33
|
+
# @param github [OpenLanes::Vcs::GitHub]
|
|
34
|
+
# @return [OpenLanes::Vcs::LabelMigration]
|
|
35
|
+
def label_migration(root: nil, github: GitHub.new(chdir: root)) = LabelMigration.new(github:)
|
|
18
36
|
end
|
|
19
37
|
end
|
data/lib/openlanes/version.rb
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "rack/auth/basic"
|
|
5
|
+
require "rack/utils"
|
|
6
|
+
|
|
7
|
+
module OpenLanes
|
|
8
|
+
module Web
|
|
9
|
+
# Basic auth for every request from off this machine. The page can stop
|
|
10
|
+
# agents, answer their questions and message them, so a server listening
|
|
11
|
+
# past loopback is refused unless it is given credentials
|
|
12
|
+
# (`--admin-credentials`), and this asks for them.
|
|
13
|
+
#
|
|
14
|
+
# A request from loopback passes untouched: it already reached the
|
|
15
|
+
# machine, the browser `openlanes start` opens is on it, and so is the
|
|
16
|
+
# launcher's `/healthz` probe.
|
|
17
|
+
#
|
|
18
|
+
# use AdminAuth, credentials: AdminAuth::Credentials.parse("admin:s3cret")
|
|
19
|
+
class AdminAuth
|
|
20
|
+
# Where `openlanes run` hands its server the credentials. argv is
|
|
21
|
+
# world-readable through `ps`; a child's environment is not.
|
|
22
|
+
ENV_NAME = "OPENLANES_ADMIN_CREDENTIALS"
|
|
23
|
+
|
|
24
|
+
# What the browser's login prompt names.
|
|
25
|
+
REALM = "openlanes control room"
|
|
26
|
+
|
|
27
|
+
# Raised for credentials that are not `USER:PASSWORD`.
|
|
28
|
+
class Invalid < OpenLanes::Error; end
|
|
29
|
+
|
|
30
|
+
# One user and one password. The password may itself hold a colon;
|
|
31
|
+
# the user may not, since Basic auth splits at the first one.
|
|
32
|
+
Credentials = Data.define(:user, :password) do
|
|
33
|
+
# @param user [String]
|
|
34
|
+
# @param password [String]
|
|
35
|
+
# @return [Boolean] whether both match, compared in constant time
|
|
36
|
+
def match?(user, password)
|
|
37
|
+
# Both compared, always, so the time taken says nothing about
|
|
38
|
+
# which of the two was wrong.
|
|
39
|
+
[Rack::Utils.secure_compare(self.user, user.to_s),
|
|
40
|
+
Rack::Utils.secure_compare(self.password, password.to_s)].all?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Never the password, so a credential that reaches a log or an
|
|
44
|
+
# exception message leaks nothing.
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
47
|
+
def inspect = "#<#{self.class.name} user=#{user.inspect}>"
|
|
48
|
+
alias_method :to_s, :inspect
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# @param text [String] `USER:PASSWORD`
|
|
53
|
+
# @return [Credentials]
|
|
54
|
+
# @raise [Invalid] when either half is missing
|
|
55
|
+
def parse(text)
|
|
56
|
+
user, password = text.to_s.split(":", 2)
|
|
57
|
+
unless user.to_s.strip.empty? || password.to_s.empty?
|
|
58
|
+
return Credentials.new(user: user.strip, password:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
raise Invalid, "takes USER:PASSWORD, both non-empty"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param address [String, nil] `REMOTE_ADDR`
|
|
65
|
+
# @return [Boolean] whether it is this machine, IPv4-mapped IPv6 included
|
|
66
|
+
def loopback?(address)
|
|
67
|
+
ip = IPAddr.new(address.to_s)
|
|
68
|
+
ip = ip.native if ip.ipv4_mapped?
|
|
69
|
+
ip.loopback?
|
|
70
|
+
rescue IPAddr::Error
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param app [#call]
|
|
76
|
+
# @param credentials [Credentials]
|
|
77
|
+
def initialize(app, credentials:)
|
|
78
|
+
@app = app
|
|
79
|
+
@credentials = credentials
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param env [Hash] the Rack environment
|
|
83
|
+
# @return [Array] the Rack response
|
|
84
|
+
def call(env)
|
|
85
|
+
return @app.call(env) if self.class.loopback?(env["REMOTE_ADDR"])
|
|
86
|
+
|
|
87
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
|
88
|
+
return challenge unless auth.provided? && auth.basic? && @credentials.match?(*auth.credentials)
|
|
89
|
+
|
|
90
|
+
env["REMOTE_USER"] = auth.username
|
|
91
|
+
@app.call(env)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# @return [Array] a 401 the browser answers with its login prompt
|
|
97
|
+
def challenge
|
|
98
|
+
[401,
|
|
99
|
+
{ "content-type" => "text/plain", "content-length" => "0", "www-authenticate" => %(Basic realm="#{REALM}") },
|
|
100
|
+
[]]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|