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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
3
5
|
require "tempfile"
|
|
4
6
|
require "tty/command"
|
|
5
7
|
|
|
@@ -34,13 +36,23 @@ module OpenLanes
|
|
|
34
36
|
# @!attribute [r] updated
|
|
35
37
|
# @return [Boolean] whether the push went to a pull request the plan
|
|
36
38
|
# already had open, rather than opening a new one
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
# @!attribute [r] label_error
|
|
40
|
+
# @return [String, nil] why `ready-for-review` could not be applied; the
|
|
41
|
+
# pull request stays open regardless (open question 4)
|
|
42
|
+
Publication = Data.define(:ordinal, :branch, :title, :url, :refusal, :updated, :label_error) do
|
|
43
|
+
def initialize(updated: false, label_error: nil, **rest) = super
|
|
39
44
|
|
|
40
45
|
# @return [Boolean]
|
|
41
46
|
def published? = !url.nil?
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] whether it was refused for want of `## How to test`,
|
|
49
|
+
# which an agent can write, unlike a rejected push
|
|
50
|
+
def untestable? = refusal == UNTESTABLE
|
|
42
51
|
end
|
|
43
52
|
|
|
53
|
+
# Why a pull request nobody could test is not opened.
|
|
54
|
+
UNTESTABLE = "publish refused — plan.md has no `#{Lifecycle::Completion::HOW_TO_TEST}` section".freeze
|
|
55
|
+
|
|
44
56
|
# A..Z, which is more parts than any feature should be split into.
|
|
45
57
|
LETTERS = ("A".."Z").to_a.freeze
|
|
46
58
|
|
|
@@ -88,20 +100,94 @@ module OpenLanes
|
|
|
88
100
|
return refuse(subject, checkout, title, "nothing to publish — the worktree is unchanged")
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
untestable = open.nil? && missing_how_to_test(subject)
|
|
104
|
+
return refuse(subject, checkout, title, untestable) if untestable
|
|
105
|
+
|
|
91
106
|
return Publication.new(**base_fields(subject, checkout, title), url: nil, refusal: nil) if @dry_run
|
|
92
107
|
|
|
93
108
|
mismatch = open && head_mismatch(open, checkout)
|
|
94
109
|
return refuse(subject, checkout, title, mismatch) if mismatch
|
|
95
110
|
|
|
111
|
+
carry_plan(checkout, subject)
|
|
96
112
|
commit(checkout, title)
|
|
97
113
|
push(checkout)
|
|
98
114
|
url = open ? open.url : create_pull_request(checkout, subject, title)
|
|
115
|
+
label_error = url.nil? ? nil : label_for_review(url, resubmitted: !open.nil?)
|
|
99
116
|
|
|
100
|
-
Publication.new(**base_fields(subject, checkout, title), url:, refusal: nil, updated: !open.nil
|
|
117
|
+
Publication.new(**base_fields(subject, checkout, title), url:, refusal: nil, updated: !open.nil?, label_error:)
|
|
101
118
|
rescue TTY::Command::ExitError => e
|
|
102
119
|
refuse(subject, checkout, title, git_complaint(e))
|
|
103
120
|
end
|
|
104
121
|
|
|
122
|
+
# Why a push from this checkout would not reach the pull request, as
|
|
123
|
+
# {#publish} asks before pushing. `pull-requests.md` does not record a
|
|
124
|
+
# head branch, and an adopted or hand-opened pull request can come from
|
|
125
|
+
# any branch, while the checkout's branch is derived from the plan.
|
|
126
|
+
# Verification asks it too, before chewie runs and before its commit:
|
|
127
|
+
# otherwise it verifies one branch's code, pushes screenshots to it,
|
|
128
|
+
# and labels a pull request built from another.
|
|
129
|
+
#
|
|
130
|
+
# @param pull [OpenLanes::Plans::PullRequest] an open pull request
|
|
131
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
132
|
+
# @param refused [String] how the sentence starts
|
|
133
|
+
# @return [String, nil] nil when the pull request is built from the checkout's branch
|
|
134
|
+
def branch_mismatch(pull, checkout, refused: "refused") = head_mismatch(pull, checkout, refused:)
|
|
135
|
+
|
|
136
|
+
# Paths named in a {#commit_plan} refusal before the rest are counted.
|
|
137
|
+
STRAY_SHOWN = 10
|
|
138
|
+
|
|
139
|
+
# Commit the plan's folder, and nothing else, to the checkout's branch and
|
|
140
|
+
# push it. Verification uses it to land chewie's screenshots on the pull
|
|
141
|
+
# request before the engine links them by SHA.
|
|
142
|
+
#
|
|
143
|
+
# It refuses a checkout with changes outside the plan folder: chewie
|
|
144
|
+
# writes `verification/**` and nothing more, so a stray code edit is
|
|
145
|
+
# something it should not have made, and committing it here would slip
|
|
146
|
+
# unreviewed code into a pull request that already passed review. Nor is
|
|
147
|
+
# it a resubmit, so the review labels are left exactly as they are.
|
|
148
|
+
#
|
|
149
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
150
|
+
# @param plan [OpenLanes::Plans::Subject] the plan in the main checkout
|
|
151
|
+
# @param message [String] the commit subject
|
|
152
|
+
# @return [String, nil] the new commit's SHA, nil on a dry run
|
|
153
|
+
# @raise [OpenLanes::Error] on a stray change, a pull request built from
|
|
154
|
+
# another branch, or nothing to commit
|
|
155
|
+
def commit_plan(checkout:, plan:, message:)
|
|
156
|
+
folder = plan_parent(plan)
|
|
157
|
+
raise Error, "the plans live outside the repository, so the branch has no place for them" if folder.nil?
|
|
158
|
+
|
|
159
|
+
stray = changed_paths(checkout).reject { |path, _| inside_plan?(path, folder, plan) }
|
|
160
|
+
raise Error, stray_complaint(plan, stray) if stray.any?
|
|
161
|
+
return nil if @dry_run
|
|
162
|
+
|
|
163
|
+
open = plan.pull_requests.select(&:open?).last
|
|
164
|
+
mismatch = open && branch_mismatch(open, checkout, refused: "refused to commit #{plan.feature.ordinal}")
|
|
165
|
+
raise Error, mismatch if mismatch
|
|
166
|
+
|
|
167
|
+
carry_plan(checkout, plan)
|
|
168
|
+
run(checkout, "git", "add", "-A", "--", folder)
|
|
169
|
+
raise Error, "nothing to commit for #{plan.feature.ordinal}" if run!(checkout, "git", "diff", "--cached", "--quiet").success?
|
|
170
|
+
|
|
171
|
+
run(checkout, "git", "commit", "-m", message)
|
|
172
|
+
sha = run(checkout, "git", "rev-parse", "HEAD").out.strip
|
|
173
|
+
pushed!(checkout, sha)
|
|
174
|
+
sha
|
|
175
|
+
rescue TTY::Command::ExitError => e
|
|
176
|
+
raise Error, git_complaint(e)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A commit that stayed on this machine is said as one: "not committed"
|
|
180
|
+
# would send someone looking for a change that is right there in the
|
|
181
|
+
# checkout, waiting for the next push to carry it.
|
|
182
|
+
#
|
|
183
|
+
# @return [void]
|
|
184
|
+
# @raise [OpenLanes::Error] when the push fails
|
|
185
|
+
def pushed!(checkout, sha)
|
|
186
|
+
push(checkout)
|
|
187
|
+
rescue TTY::Command::ExitError => e
|
|
188
|
+
raise Error, "committed #{sha[0, 7]} locally, but the push failed: #{git_complaint(e)}"
|
|
189
|
+
end
|
|
190
|
+
|
|
105
191
|
# git and gh put the useful sentence in stderr, several lines below the
|
|
106
192
|
# command they echo. Reporting only the first line reports "it failed".
|
|
107
193
|
#
|
|
@@ -132,22 +218,140 @@ module OpenLanes
|
|
|
132
218
|
# @return [String]
|
|
133
219
|
def auto_letter(subject) = LETTERS.fetch(subject.pull_requests.size, LETTERS.last)
|
|
134
220
|
|
|
221
|
+
# Left out of the copy: the half-written temporaries an atomic write
|
|
222
|
+
# renames into place (`state.json.<pid>.<thread>.tmp`).
|
|
223
|
+
CARRY_EXCLUDED = ["*.tmp"].freeze
|
|
224
|
+
|
|
225
|
+
# Copy the plan's folder from the main checkout into the worktree, so
|
|
226
|
+
# the pull request carries its spec, plan and state next to the code.
|
|
227
|
+
#
|
|
228
|
+
# The main checkout stays the only writer. The engine reads, signs and
|
|
229
|
+
# renames the plan there and nowhere else; the worktree gets a snapshot,
|
|
230
|
+
# refreshed on every publish. Every copy of the same plan number is
|
|
231
|
+
# removed first, so a folder renamed since the last publish (⭐ to 🟢,
|
|
232
|
+
# say) does not leave its old name behind in the branch, and a file
|
|
233
|
+
# deleted in the main checkout is deleted in the branch too.
|
|
234
|
+
#
|
|
235
|
+
# Nothing is copied when the worktree is the main checkout itself, which
|
|
236
|
+
# would delete the source, or when the plans live outside the
|
|
237
|
+
# repository, where the branch has no place for them.
|
|
238
|
+
#
|
|
239
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
240
|
+
# @param subject [OpenLanes::Plans::Subject] the plan in the main checkout
|
|
241
|
+
# @return [String, nil] the copy's path, nil when nothing was copied
|
|
242
|
+
def carry_plan(checkout, subject)
|
|
243
|
+
root = real(@root)
|
|
244
|
+
source = real(subject.feature.path)
|
|
245
|
+
return nil if real(checkout.path) == root || !source.start_with?("#{root}/")
|
|
246
|
+
|
|
247
|
+
parent = File.join(checkout.path, File.dirname(source.delete_prefix("#{root}/")))
|
|
248
|
+
FileUtils.mkdir_p(parent)
|
|
249
|
+
Dir.children(parent).each do |name|
|
|
250
|
+
next unless Plans::Ordinal.from_dirname(name) == subject.feature.ordinal
|
|
251
|
+
|
|
252
|
+
FileUtils.rm_rf(File.join(parent, name))
|
|
253
|
+
end
|
|
254
|
+
copy_tree(source, File.join(parent, File.basename(source)))
|
|
255
|
+
end
|
|
256
|
+
|
|
135
257
|
private
|
|
136
258
|
|
|
259
|
+
# @param plan [OpenLanes::Plans::Subject]
|
|
260
|
+
# @return [String, nil] the plans directory relative to the repository,
|
|
261
|
+
# nil when the plans live outside it
|
|
262
|
+
def plan_parent(plan)
|
|
263
|
+
root = real(@root)
|
|
264
|
+
source = real(plan.feature.path)
|
|
265
|
+
source.start_with?("#{root}/") ? File.dirname(source.delete_prefix("#{root}/")) : nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Every path git sees changed, untracked files included. `-z` keeps an
|
|
269
|
+
# emoji folder name unquoted; a rename's second entry is its old path.
|
|
270
|
+
#
|
|
271
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
272
|
+
# @return [Hash{String => Boolean}] each path, and whether git does not
|
|
273
|
+
# track it
|
|
274
|
+
def changed_paths(checkout)
|
|
275
|
+
entries = run(checkout, "git", "status", "--porcelain", "-z", "--untracked-files=all").out.split("\0")
|
|
276
|
+
paths = {}
|
|
277
|
+
until entries.empty?
|
|
278
|
+
entry = entries.shift
|
|
279
|
+
paths[entry[3..]] = entry.start_with?("??")
|
|
280
|
+
paths[entries.shift] = false if entry.start_with?("R", "C")
|
|
281
|
+
end
|
|
282
|
+
paths.reject { |path, _| path.nil? }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Running the app in the checkout leaves files behind that a project
|
|
286
|
+
# may never have ignored (logs, `tmp/`, a SQLite file, `coverage/`),
|
|
287
|
+
# and each one refuses every verification commit until it is. So the
|
|
288
|
+
# refusal names them, apart from edits to tracked files, and says what
|
|
289
|
+
# to do about them; "changes outside its plan folder" alone reads as a
|
|
290
|
+
# stray code edit and sends someone looking for one.
|
|
291
|
+
#
|
|
292
|
+
# @param plan [OpenLanes::Plans::Subject]
|
|
293
|
+
# @param stray [Hash{String => Boolean}] see {#changed_paths}
|
|
294
|
+
# @return [String]
|
|
295
|
+
def stray_complaint(plan, stray)
|
|
296
|
+
untracked, edited = stray.partition { |_, new| new }.map { |pairs| pairs.map(&:first) }
|
|
297
|
+
parts = []
|
|
298
|
+
parts << "edited #{shown(edited)}" if edited.any?
|
|
299
|
+
parts << "untracked #{shown(untracked)} (left by the app or its tests? add them to .gitignore)" if untracked.any?
|
|
300
|
+
"refused to commit #{plan.feature.ordinal}: changes outside its plan folder: #{parts.join("; ")}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# @param paths [Array<String>]
|
|
304
|
+
# @return [String]
|
|
305
|
+
def shown(paths)
|
|
306
|
+
more = paths.size - STRAY_SHOWN
|
|
307
|
+
"#{paths.first(STRAY_SHOWN).join(", ")}#{" and #{more} more" if more.positive?}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# @return [Boolean] whether `path` lies in any copy of this plan's folder
|
|
311
|
+
def inside_plan?(path, folder, plan)
|
|
312
|
+
return false unless path.start_with?("#{folder}/")
|
|
313
|
+
|
|
314
|
+
Plans::Ordinal.from_dirname(path.delete_prefix("#{folder}/").split("/").first) == plan.feature.ordinal
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @param path [String]
|
|
318
|
+
# @return [String] symlinks resolved, so /var and /private/var compare equal
|
|
319
|
+
def real(path)
|
|
320
|
+
File.realpath(path)
|
|
321
|
+
rescue Errno::ENOENT
|
|
322
|
+
File.expand_path(path)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# @param source [String]
|
|
326
|
+
# @param target [String]
|
|
327
|
+
# @return [String] target
|
|
328
|
+
def copy_tree(source, target)
|
|
329
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: source).each do |relative|
|
|
330
|
+
next if relative == "." || CARRY_EXCLUDED.any? { File.fnmatch?(it, File.basename(relative)) }
|
|
331
|
+
|
|
332
|
+
from = File.join(source, relative)
|
|
333
|
+
to = File.join(target, relative)
|
|
334
|
+
FileUtils.mkdir_p(File.directory?(from) ? to : File.dirname(to))
|
|
335
|
+
FileUtils.cp(from, to) if File.file?(from)
|
|
336
|
+
end
|
|
337
|
+
FileUtils.mkdir_p(target)
|
|
338
|
+
target
|
|
339
|
+
end
|
|
340
|
+
|
|
137
341
|
# Why the open pull request cannot receive this checkout's push, if it
|
|
138
342
|
# cannot.
|
|
139
343
|
#
|
|
140
344
|
# @param open [OpenLanes::Plans::PullRequest]
|
|
141
345
|
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
142
346
|
# @return [String, nil] nil when the pull request is built from the checkout's branch
|
|
143
|
-
def head_mismatch(open, checkout)
|
|
347
|
+
def head_mismatch(open, checkout, refused: "publish refused")
|
|
144
348
|
head = @github.pull_request(open.number || open.url).fetch(:branch)
|
|
145
349
|
return if head == checkout.branch
|
|
146
350
|
|
|
147
|
-
"
|
|
351
|
+
"#{refused} — ##{open.number} is built from #{head.empty? ? "an unknown branch" : head}, " \
|
|
148
352
|
"not #{checkout.branch}, so a push would not reach it"
|
|
149
353
|
rescue Error => e
|
|
150
|
-
"
|
|
354
|
+
"#{refused} — could not tell which branch ##{open.number} is built from: #{e.message}"
|
|
151
355
|
end
|
|
152
356
|
|
|
153
357
|
# @return [Hash]
|
|
@@ -170,9 +374,42 @@ module OpenLanes
|
|
|
170
374
|
run(checkout, "git", "commit", "-m", title.sub(/\A\[[^\]]+\](?:\([A-Z]\))?\s*/, ""))
|
|
171
375
|
end
|
|
172
376
|
|
|
377
|
+
# The branch is the checkout's own, named by {Worktree#branch_for} and
|
|
378
|
+
# written only by this harness, so after the harness has rebased it
|
|
379
|
+
# (see {Refresh}) it may replace what `origin` holds. The lease alone
|
|
380
|
+
# is not enough: it trusts the tracking ref, and worktrees share
|
|
381
|
+
# `refs/remotes` with the main checkout, so a fetch there (an editor's
|
|
382
|
+
# auto-fetch, `gh pr checkout`) moves it onto somebody else's push and
|
|
383
|
+
# the lease would then let us overwrite it. `--force-if-includes` adds
|
|
384
|
+
# that the tracking ref's tip must be in this branch's own reflog,
|
|
385
|
+
# i.e. a commit the harness had here. It is a no-op beside an explicit
|
|
386
|
+
# `:<expect>`, so the lease names only the ref. A branch with no
|
|
387
|
+
# tracking ref yet gets a plain push, which can only fast-forward.
|
|
388
|
+
#
|
|
173
389
|
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
174
390
|
# @return [void]
|
|
175
|
-
def push(checkout)
|
|
391
|
+
def push(checkout)
|
|
392
|
+
# Declared first, so an agent still running in another worktree is
|
|
393
|
+
# not blamed for the tracking ref this push moves under it.
|
|
394
|
+
Execution::Boundary.sanction(checkout.path, "origin", checkout.branch)
|
|
395
|
+
run(checkout, "git", "push", *lease(checkout), "-u", "origin", checkout.branch)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# @param checkout [OpenLanes::Vcs::Worktree::Checkout]
|
|
399
|
+
# @return [Array<String>] the lease flags, or nothing
|
|
400
|
+
def lease(checkout)
|
|
401
|
+
# Read directly rather than through {#run}: it is a local lookup that
|
|
402
|
+
# changes nothing, and the command seam is for what leaves the machine.
|
|
403
|
+
seen, = Open3.capture2("git",
|
|
404
|
+
"-C",
|
|
405
|
+
checkout.path,
|
|
406
|
+
"rev-parse",
|
|
407
|
+
"--verify",
|
|
408
|
+
"--quiet",
|
|
409
|
+
"refs/remotes/origin/#{checkout.branch}",
|
|
410
|
+
err: File::NULL)
|
|
411
|
+
seen.strip.empty? ? [] : ["--force-with-lease=refs/heads/#{checkout.branch}", "--force-if-includes"]
|
|
412
|
+
end
|
|
176
413
|
|
|
177
414
|
# @return [String, nil] the pull request URL
|
|
178
415
|
def create_pull_request(checkout, subject, title)
|
|
@@ -201,9 +438,49 @@ module OpenLanes
|
|
|
201
438
|
|
|
202
439
|
Implements `#{subject.feature.dirname}`. The specification and the plan
|
|
203
440
|
for this work live in that folder.
|
|
441
|
+
|
|
442
|
+
#{Lifecycle::Completion::HOW_TO_TEST}
|
|
443
|
+
|
|
444
|
+
#{how_to_test(subject)}
|
|
204
445
|
MARKDOWN
|
|
205
446
|
end
|
|
206
447
|
|
|
448
|
+
# A pull request a person cannot test is not ready for one to review.
|
|
449
|
+
#
|
|
450
|
+
# @return [String, nil] the refusal, or nil when plan.md has the section
|
|
451
|
+
def missing_how_to_test(subject)
|
|
452
|
+
UNTESTABLE if how_to_test(subject).empty?
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# A plan.md written before the rename says `## How to verify`; the
|
|
456
|
+
# harness no longer appends a duplicate to it, so its steps are read
|
|
457
|
+
# here under either heading rather than refused.
|
|
458
|
+
#
|
|
459
|
+
# @return [String] the section's body, without its heading
|
|
460
|
+
def how_to_test(subject)
|
|
461
|
+
headings = [Lifecycle::Completion::HOW_TO_TEST, Lifecycle::Completion::FORMER_HOW_TO_TEST]
|
|
462
|
+
lines = subject.read("plan.md").to_s.lines
|
|
463
|
+
start = lines.index { |l| headings.any? { l.strip.casecmp?(it) } } or return ""
|
|
464
|
+
lines[(start + 1)..].take_while { |l| !l.start_with?("## ", "# ") }.join.strip
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Put `ready-for-review` on the pull request. A resubmission also takes
|
|
468
|
+
# off whatever verdict the last review left: a `review-rejected` still
|
|
469
|
+
# there after the fixes are pushed reads to the watch as a fresh
|
|
470
|
+
# rejection, and the 🟢 plan would be sent straight back to 🔴.
|
|
471
|
+
#
|
|
472
|
+
# @param url [String]
|
|
473
|
+
# @param resubmitted [Boolean] whether the pull request was already open
|
|
474
|
+
# @return [String, nil] the error, when labelling failed
|
|
475
|
+
def label_for_review(url, resubmitted: false)
|
|
476
|
+
number = GitHub.number_of(url)
|
|
477
|
+
remove = resubmitted ? Plans::ReviewLabel.known : []
|
|
478
|
+
@github.swap_label(number:, repo: GitHub.repo_of(url), add: Plans::ReviewLabel::INITIAL, remove:)
|
|
479
|
+
nil
|
|
480
|
+
rescue Error => e
|
|
481
|
+
e.message
|
|
482
|
+
end
|
|
483
|
+
|
|
207
484
|
# The specification's own Goal section, verbatim. A pull request body that
|
|
208
485
|
# paraphrases the spec is a second copy that drifts; quoting it is not.
|
|
209
486
|
#
|
|
@@ -216,6 +493,9 @@ module OpenLanes
|
|
|
216
493
|
|
|
217
494
|
# @return [TTY::Command::Result]
|
|
218
495
|
def run(checkout, *argv) = @command.run(*argv, chdir: checkout.path)
|
|
496
|
+
|
|
497
|
+
# @return [TTY::Command::Result] whatever the exit status
|
|
498
|
+
def run!(checkout, *argv) = @command.run!(*argv, chdir: checkout.path)
|
|
219
499
|
end
|
|
220
500
|
end
|
|
221
501
|
end
|