openlanes 3.0.1 → 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 +4 -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 +18 -4
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +14 -11
- 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 +7 -0
- 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 -486
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -51
- 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 +84 -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 +1175 -117
- 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 +6 -6
- 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 +12 -120
- 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 +223 -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 +17 -13
- 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 +60 -11
- 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 +217 -30
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +38 -4
- 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 +56 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +12 -1
- 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 +31 -3
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +4 -4
- data/lib/openlanes/live/store.rb +18 -1
- data/lib/openlanes/live/subscriber.rb +22 -2
- data/lib/openlanes/live.rb +66 -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 +64 -5
- 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 +39 -24
- 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 +226 -8
- 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 +167 -6
- 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 +115 -16
- data/lib/openlanes/web/context.rb +15 -9
- data/lib/openlanes/web/demo.rb +29 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +86 -7
- data/lib/openlanes/web/launcher.rb +69 -7
- 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 +29 -10
- data/lib/openlanes/web.rb +6 -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/{index-BFPI-zXI.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-bpsFIpXE.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-DtA46i32.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-CnJfB0l2.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-DXgeajd6.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-DH0gDlee.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-DaSjQ-H4.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-Dbxvk-EC.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-6MZkmh3k.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-B5CKJPm-.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-CnvkZu5Y.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-IqD8mBos.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-C3zc4gFe.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-C2jwoWVV.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-pvJi-JJO.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-BF6QYroq.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-DbueqOK5.js → index-_mptZjlC.js} +2 -2
- data/web/dist/index.html +2 -2
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -889
- 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/index-BYJCWlk0.js +0 -198
- data/web/dist/assets/index-CjInBi5X.css +0 -1
- /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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
module Verification
|
|
6
|
+
# The plan's app, up for exactly as long as chewie runs (M1, M12):
|
|
7
|
+
# started in the plan's checkout through {Apps.supervisor}, and stopped
|
|
8
|
+
# in `ensure`, whatever the agent did, so no verification leaves a dev
|
|
9
|
+
# server holding a port from the shared registry.
|
|
10
|
+
#
|
|
11
|
+
# A project with no `app:` starts nothing and chewie verifies through the
|
|
12
|
+
# shell (M11), which is how openlanes itself, a CLI gem, is verified.
|
|
13
|
+
class AppSession
|
|
14
|
+
# The app did not come up, so chewie is never started: a verifier
|
|
15
|
+
# pointed at a dead port can only report what the engine already
|
|
16
|
+
# knows. The rejection quotes {#tail} (M1: the last 20 lines).
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [r] process
|
|
19
|
+
# @return [String, nil] the process that failed
|
|
20
|
+
# @!attribute [r] reason
|
|
21
|
+
# @return [String]
|
|
22
|
+
# @!attribute [r] tail
|
|
23
|
+
# @return [Array<String>] that process's last lines
|
|
24
|
+
Failed = Data.define(:process, :reason, :tail) do
|
|
25
|
+
# @return [String] the opening of every rejection it causes
|
|
26
|
+
def headline = "the app does not start"
|
|
27
|
+
|
|
28
|
+
# @return [String] Markdown, for the report and the comment
|
|
29
|
+
def to_markdown
|
|
30
|
+
first = reason.to_s.lines.first.to_s.strip
|
|
31
|
+
lines = ["#{headline}: #{first}"]
|
|
32
|
+
unless tail.empty?
|
|
33
|
+
lines << "" << "The last #{tail.size} lines #{process || "it"} printed:" << "" << "```text"
|
|
34
|
+
lines.concat(tail) << "```"
|
|
35
|
+
end
|
|
36
|
+
lines.join("\n")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param definition [OpenLanes::Apps::Definition, nil] nil when the
|
|
41
|
+
# project declares no app
|
|
42
|
+
# @param root [String] the plan's checkout
|
|
43
|
+
# @param plan [String] the ordinal
|
|
44
|
+
# @param branch [String]
|
|
45
|
+
# @param supervisor_factory [#call] keywords to a supervisor; the
|
|
46
|
+
# suite passes one with its own port registry
|
|
47
|
+
def initialize(definition:, root:, plan:, branch:, supervisor_factory: Apps.method(:supervisor))
|
|
48
|
+
@definition = definition
|
|
49
|
+
@root = root
|
|
50
|
+
@plan = plan
|
|
51
|
+
@branch = branch
|
|
52
|
+
@factory = supervisor_factory
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @yieldparam statuses [Array<OpenLanes::Apps::Status>] every process,
|
|
56
|
+
# ready; empty when there is no app
|
|
57
|
+
# @return [Object, Failed] the block's value, or {Failed} without
|
|
58
|
+
# calling the block
|
|
59
|
+
def around
|
|
60
|
+
return yield([]) if @definition.nil?
|
|
61
|
+
|
|
62
|
+
supervisor = @factory.call(definition: @definition, root: @root, plan: @plan, branch: @branch)
|
|
63
|
+
begin
|
|
64
|
+
supervisor.start
|
|
65
|
+
rescue Apps::Supervisor::StartFailed => e
|
|
66
|
+
return Failed.new(process: e.process, reason: e.message, tail: e.process ? supervisor.log_tail(e.process) : [])
|
|
67
|
+
end
|
|
68
|
+
yield supervisor.statuses
|
|
69
|
+
ensure
|
|
70
|
+
supervisor&.stop
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# What chewie is told about the app, written into its round's folder
|
|
74
|
+
# before it starts, so the ports reach it without the prompt (which
|
|
75
|
+
# is the executor's) having to know apps exist.
|
|
76
|
+
#
|
|
77
|
+
# @param statuses [Array<OpenLanes::Apps::Status>]
|
|
78
|
+
# @param definition [OpenLanes::Apps::Definition, nil]
|
|
79
|
+
# @return [String] Markdown
|
|
80
|
+
def self.note(statuses, definition)
|
|
81
|
+
return <<~MARKDOWN if statuses.empty?
|
|
82
|
+
# The app
|
|
83
|
+
|
|
84
|
+
This project declares no `app:` in `.openlanes.yml`, so nothing was started.
|
|
85
|
+
Follow `## How to test` through the shell (M11).
|
|
86
|
+
MARKDOWN
|
|
87
|
+
|
|
88
|
+
envs = definition.processes.to_h { [it.name, it.port_env] }
|
|
89
|
+
rows = statuses.map { "| #{it.process} | `#{envs[it.process]}=#{it.port}` | #{it.url} | #{"yes" if it.preview} |" }
|
|
90
|
+
<<~MARKDOWN
|
|
91
|
+
# The app
|
|
92
|
+
|
|
93
|
+
The engine started every process below, ready, and stops them when you finish.
|
|
94
|
+
Do not start or stop them yourself.
|
|
95
|
+
|
|
96
|
+
| Process | Port | URL | Preview |
|
|
97
|
+
| :------ | :--- | :-- | :------ |
|
|
98
|
+
#{rows.join("\n")}
|
|
99
|
+
MARKDOWN
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
module Verification
|
|
6
|
+
# Everything the dispatcher asks about verification, in one object, so
|
|
7
|
+
# the loop gains a few calls rather than a second job: the hand-off
|
|
8
|
+
# from hansolo, the app around chewie's run, the verdict, and the CI
|
|
9
|
+
# wait after a pass.
|
|
10
|
+
#
|
|
11
|
+
# The wait is read from disk, not remembered: a 🧪 plan whose newest
|
|
12
|
+
# round says `verified` is waiting on CI. A run that ends while it
|
|
13
|
+
# waits leaves nothing behind to clean up, and the next run, or a
|
|
14
|
+
# staying one, picks the wait up where it stood.
|
|
15
|
+
class Driver
|
|
16
|
+
# One plan's wait, settled.
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [r] plan
|
|
19
|
+
# @return [String] the ordinal
|
|
20
|
+
# @!attribute [r] result
|
|
21
|
+
# @return [Outcome::Result]
|
|
22
|
+
# @!attribute [r] round
|
|
23
|
+
# @return [Integer, nil] the round it settled
|
|
24
|
+
Settled = Data.define(:plan, :result, :round) do
|
|
25
|
+
def initialize(round: nil, **rest) = super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Seconds between looks at CI, as {PrWatch} paces its own.
|
|
29
|
+
INTERVAL = 15
|
|
30
|
+
|
|
31
|
+
# @param outcome [Outcome]
|
|
32
|
+
# @param worktree [OpenLanes::Vcs::Worktree] where a waiting plan's checkout is
|
|
33
|
+
# @param app [OpenLanes::Apps::Definition, nil] the project's `app:`
|
|
34
|
+
# @param supervisor_factory [#call] see {AppSession#initialize}
|
|
35
|
+
# @param clock [#call] monotonic seconds
|
|
36
|
+
# @param interval [Numeric] seconds between looks at CI
|
|
37
|
+
# @param quality [Symbol, nil, #call] {Config#desired_codebase_quality},
|
|
38
|
+
# nil while the project is still to be asked (R4); a callable is
|
|
39
|
+
# asked each time, so an answer given mid-run counts at once
|
|
40
|
+
def initialize(outcome:, worktree:, app: nil, supervisor_factory: Apps.method(:supervisor),
|
|
41
|
+
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, interval: INTERVAL, quality: nil)
|
|
42
|
+
@outcome = outcome
|
|
43
|
+
@quality = quality
|
|
44
|
+
@worktree = worktree
|
|
45
|
+
@app = app
|
|
46
|
+
@factory = supervisor_factory
|
|
47
|
+
@clock = clock
|
|
48
|
+
@interval = interval
|
|
49
|
+
@last = nil
|
|
50
|
+
@thread = nil
|
|
51
|
+
@ready = Set.new
|
|
52
|
+
@looking = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Outcome]
|
|
56
|
+
attr_reader :outcome
|
|
57
|
+
|
|
58
|
+
# hansolo approved: on to 🧪, when the pull request carries a verdict
|
|
59
|
+
# that goes on to verification ({Verification.from_review?}) and is
|
|
60
|
+
# built from the branch chewie would be run in. A
|
|
61
|
+
# `review-ok-not-perfect` one goes back to 🔴 under `high` (R4), and
|
|
62
|
+
# stays 👀 while the project has not said which quality it wants. The checkout's branch
|
|
63
|
+
# is derived from the plan, and an adopted pull request may be built
|
|
64
|
+
# from any other; verifying it there would test the wrong code.
|
|
65
|
+
#
|
|
66
|
+
# @param subject [OpenLanes::Plans::Subject] read fresh
|
|
67
|
+
# @return [String, nil] what happened, for the attempt; nil when the
|
|
68
|
+
# folder cannot be 🧪 at all, and the plan finishes at 👀 as before
|
|
69
|
+
def hand_off(subject)
|
|
70
|
+
return nil unless subject.machine.may?(:verifying)
|
|
71
|
+
|
|
72
|
+
pull = open_pull(subject) or return nil
|
|
73
|
+
labels = @outcome.labels(pull)
|
|
74
|
+
sent = quality_says(subject, pull, labels)
|
|
75
|
+
return sent if sent
|
|
76
|
+
|
|
77
|
+
unless Verification.from_review?(labels, quality:)
|
|
78
|
+
verdict = (labels & Plans::ReviewLabel.names).join(", ")
|
|
79
|
+
return "stays 👀: PR ##{pull.number.to_s.delete("#")} is labelled #{verdict.empty? ? "nothing" : verdict}, " \
|
|
80
|
+
"and only #{FROM_REVIEW.map(&:name).join(", ")} goes on to verification"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
mismatch = @outcome.mismatch(checkout: @worktree.checkout_for(subject.feature), pull:)
|
|
84
|
+
return "stays 👀: #{mismatch}; verify it by hand" if mismatch
|
|
85
|
+
|
|
86
|
+
# A round of its own for this hand-off, before the rename carries
|
|
87
|
+
# it into 🧪: the newest round otherwise may be one verified before
|
|
88
|
+
# a rejection and a rebuild, and would be taken for this one,
|
|
89
|
+
# waiting on CI, with the fixes never verified.
|
|
90
|
+
Verification.round_for(subject.feature.path).make!
|
|
91
|
+
subject.machine.promote!(:verifying)
|
|
92
|
+
"on to 🧪 verification"
|
|
93
|
+
rescue OpenLanes::Error => e
|
|
94
|
+
"stays 👀: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The app up around chewie's run, and chewie told where it is.
|
|
98
|
+
#
|
|
99
|
+
# @param task [OpenLanes::Engine::Runner::Task]
|
|
100
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
101
|
+
# @yieldreturn [Object] what the executor returned
|
|
102
|
+
# @return [Object, AppSession::Failed]
|
|
103
|
+
def around(task, subject)
|
|
104
|
+
round = Verification.round_for(subject.feature.path).make!
|
|
105
|
+
session = AppSession.new(definition: @app,
|
|
106
|
+
root: task.root,
|
|
107
|
+
plan: subject.feature.ordinal.to_s,
|
|
108
|
+
branch: task.checkout&.branch.to_s,
|
|
109
|
+
supervisor_factory: @factory)
|
|
110
|
+
session.around do |statuses|
|
|
111
|
+
File.write(File.join(round.dir, Round::APP_NOTE), AppSession.note(statuses, @app))
|
|
112
|
+
yield
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @param task [OpenLanes::Engine::Runner::Task]
|
|
117
|
+
# @param subject [OpenLanes::Plans::Subject] read fresh
|
|
118
|
+
# @param why [String, nil] see {Outcome#reject}
|
|
119
|
+
# @return [Outcome::Result, nil] nil when the plan has no open pull request to reject
|
|
120
|
+
def reject(task, subject, why: nil)
|
|
121
|
+
pull = open_pull(subject) or return nil
|
|
122
|
+
|
|
123
|
+
@outcome.reject(checkout: checkout_of(task, subject), plan: subject, pull:, round: latest(subject), why:)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param task [OpenLanes::Engine::Runner::Task]
|
|
127
|
+
# @param subject [OpenLanes::Plans::Subject] read fresh
|
|
128
|
+
# @return [Outcome::Result, nil] nil when the plan has no open pull request
|
|
129
|
+
def pass(task, subject)
|
|
130
|
+
pull = open_pull(subject) or return nil
|
|
131
|
+
|
|
132
|
+
round = latest(subject)
|
|
133
|
+
result = @outcome.pass(checkout: checkout_of(task, subject), plan: subject, pull:, round:)
|
|
134
|
+
# Only now is the round waiting on CI ({#awaiting_ci?}).
|
|
135
|
+
round.publish!(result.sha) if result.kind == :committed
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
140
|
+
# A verified round counts only once the engine pushed its evidence
|
|
141
|
+
# ({Round#published?}); one chewie reported but the engine never
|
|
142
|
+
# published goes back to chewie, in a new round.
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean] whether the plan waits on CI rather than on chewie
|
|
145
|
+
def awaiting_ci?(subject)
|
|
146
|
+
return false unless subject.status.key == :verifying
|
|
147
|
+
|
|
148
|
+
round = Round.latest(subject.feature.path)
|
|
149
|
+
!round.nil? && round.published? && round.report&.verdict == :verified
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Look at CI for every plan waiting on it, on a thread of its own:
|
|
153
|
+
# the coverage gate runs the project's whole suite, and the tick may
|
|
154
|
+
# not wait for that. The thread only reads ({Outcome#look}). What it
|
|
155
|
+
# found is acted on here, on the dispatcher's thread, on the first
|
|
156
|
+
# call after it finished: labelling, rejecting, mailing and renaming
|
|
157
|
+
# are lifecycle changes, and doing them on the thread raced the PR
|
|
158
|
+
# watcher and the builders over the same folder.
|
|
159
|
+
#
|
|
160
|
+
# Each plan is checked again before its look is acted on, since the
|
|
161
|
+
# look may have taken minutes: still 🧪, still waiting on the same
|
|
162
|
+
# round, still the same open pull request. One that changed is left
|
|
163
|
+
# for the next look.
|
|
164
|
+
#
|
|
165
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>] plans the loop
|
|
166
|
+
# may touch, read fresh, the ones being looked at included
|
|
167
|
+
# @return [Array<Settled>] finished looks, often none
|
|
168
|
+
def poll(subjects)
|
|
169
|
+
if @thread && !@thread.alive?
|
|
170
|
+
looked = @thread.value || []
|
|
171
|
+
@thread = nil
|
|
172
|
+
@looking = []
|
|
173
|
+
return looked.filter_map { |pending, seen| apply(pending, seen, subjects) }
|
|
174
|
+
end
|
|
175
|
+
return [] if @thread || !due?
|
|
176
|
+
|
|
177
|
+
waiting = subjects.select { awaiting_ci?(it) && !@ready.include?(ready_key(it)) }.filter_map { pending_for(it) }
|
|
178
|
+
return [] if waiting.empty?
|
|
179
|
+
|
|
180
|
+
@looking = waiting.map(&:plan)
|
|
181
|
+
@thread = Thread.new { waiting.filter_map { look_at(it) } }
|
|
182
|
+
[]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [Boolean] whether a look is under way, or finished and not
|
|
186
|
+
# yet acted on: a run may not end before {#poll} has had it
|
|
187
|
+
def busy? = !@thread.nil?
|
|
188
|
+
|
|
189
|
+
# @return [Array<String>] ordinals being looked at: no agent may start
|
|
190
|
+
# on them and no rename may touch them until the look is acted on
|
|
191
|
+
def looking = @looking.dup
|
|
192
|
+
|
|
193
|
+
# @return [void] waits for a look under way
|
|
194
|
+
def join = @thread&.join
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
# What R4 does with a `review-ok-not-perfect` pull request, short of
|
|
199
|
+
# verifying it.
|
|
200
|
+
#
|
|
201
|
+
# @return [String, nil] what happened; nil when the quality lets it
|
|
202
|
+
# go on, or the verdict is not the one the quality decides
|
|
203
|
+
def quality_says(subject, pull, labels)
|
|
204
|
+
now = quality
|
|
205
|
+
return nil unless Verification.ok_not_perfect?(labels) && now != :mvp
|
|
206
|
+
|
|
207
|
+
number = pull.number.to_s.delete("#")
|
|
208
|
+
return unasked(subject, number) if now.nil?
|
|
209
|
+
|
|
210
|
+
return nil unless subject.machine.may?(:rejected)
|
|
211
|
+
|
|
212
|
+
# Counted, as any rejection is, so a PR labelled the same round
|
|
213
|
+
# after round reaches `merge.max_rejections` and the arbitrator.
|
|
214
|
+
result = @outcome.send_back_for_quality(plan: subject, pull:)
|
|
215
|
+
"back to 🔴: #{result.note}; luke and rey answer every comment that is not ✅"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [Symbol, nil]
|
|
219
|
+
def quality = @quality.respond_to?(:call) ? @quality.call : @quality
|
|
220
|
+
|
|
221
|
+
# The key is missing: R4's question goes to a person, once per
|
|
222
|
+
# project, and the plan waits on it at 👀.
|
|
223
|
+
#
|
|
224
|
+
# @return [String]
|
|
225
|
+
def unasked(subject, number)
|
|
226
|
+
asked = QualityQuestion.ask!(subject.feature.path) ? "asked in the drawer" : "already asked on another plan"
|
|
227
|
+
"stays 👀: PR ##{number} is labelled #{Verification::ON_QUALITY.name}, " \
|
|
228
|
+
"and desired_codebase_quality is not set in #{Config::FILE}; say high or mvp (#{asked})"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# One plan's look, as it stood when the look began.
|
|
232
|
+
Pending = Data.define(:plan, :subject, :round, :pull, :checkout)
|
|
233
|
+
|
|
234
|
+
# @return [Pending, nil]
|
|
235
|
+
def pending_for(subject)
|
|
236
|
+
pull = open_pull(subject) or return nil
|
|
237
|
+
|
|
238
|
+
Pending.new(plan: subject.feature.ordinal.to_s,
|
|
239
|
+
subject:,
|
|
240
|
+
round: Round.latest(subject.feature.path),
|
|
241
|
+
pull:,
|
|
242
|
+
checkout: @worktree.checkout_for(subject.feature))
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
not_settled(subject.feature.ordinal.to_s, e)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# On the look's thread: reads only.
|
|
248
|
+
#
|
|
249
|
+
# @return [Array(Pending, Outcome::Look), nil]
|
|
250
|
+
def look_at(pending)
|
|
251
|
+
[pending, @outcome.look(checkout: pending.checkout, plan: pending.subject, pull: pending.pull, round: pending.round)]
|
|
252
|
+
# StandardError, not only OpenLanes::Error: one plan's surprise from
|
|
253
|
+
# `gh` must not end the look for the others, nor the run.
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
not_settled(pending.plan, e)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# On the dispatcher's thread.
|
|
259
|
+
#
|
|
260
|
+
# @return [Settled, nil]
|
|
261
|
+
def apply(pending, seen, subjects)
|
|
262
|
+
subject = subjects.find { it.feature.ordinal.to_s == pending.plan } or return nil
|
|
263
|
+
return nil unless still?(pending, subject)
|
|
264
|
+
|
|
265
|
+
result = @outcome.settle(checkout: pending.checkout, plan: subject, pull: open_pull(subject), round: pending.round, seen:)
|
|
266
|
+
@ready << [pending.plan, pending.round.number] if result.kind == :ready
|
|
267
|
+
Settled.new(plan: pending.plan, result:, round: pending.round.number)
|
|
268
|
+
rescue StandardError => e
|
|
269
|
+
not_settled(pending.plan, e)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# @return [Boolean] whether the plan is as the look found it
|
|
273
|
+
def still?(pending, subject)
|
|
274
|
+
pull = open_pull(subject)
|
|
275
|
+
awaiting_ci?(subject) && Round.latest(subject.feature.path)&.number == pending.round.number &&
|
|
276
|
+
!pull.nil? && [pull.number, pull.url] == [pending.pull.number, pending.pull.url]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @return [nil]
|
|
280
|
+
def not_settled(plan, error)
|
|
281
|
+
UI.log("verification not settled: #{error.message.lines.first.to_s.strip}", level: :warn, plan:)
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# The first look comes at once; after that, one per interval.
|
|
286
|
+
#
|
|
287
|
+
# @return [Boolean]
|
|
288
|
+
def due?
|
|
289
|
+
now = @clock.call
|
|
290
|
+
return false if @last && now - @last < @interval
|
|
291
|
+
|
|
292
|
+
@last = now
|
|
293
|
+
true
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Ready is remembered per round, not per plan: a plan rejected after
|
|
297
|
+
# `ready-to-merge`, rebuilt and verified again is a new round, and
|
|
298
|
+
# must be looked at again.
|
|
299
|
+
#
|
|
300
|
+
# @return [Array(String, Integer)]
|
|
301
|
+
def ready_key(subject) = [subject.feature.ordinal.to_s, Round.latest(subject.feature.path)&.number]
|
|
302
|
+
|
|
303
|
+
# @return [OpenLanes::Plans::PullRequest, nil]
|
|
304
|
+
def open_pull(subject) = subject.pull_requests.select(&:open?).last
|
|
305
|
+
|
|
306
|
+
# @return [Round]
|
|
307
|
+
def latest(subject) = Round.latest(subject.feature.path) || Round.next(subject.feature.path)
|
|
308
|
+
|
|
309
|
+
# @return [OpenLanes::Vcs::Worktree::Checkout]
|
|
310
|
+
def checkout_of(task, subject) = task.checkout || @worktree.checkout_for(subject.feature)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|