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
|
@@ -8,7 +8,8 @@ module OpenLanes
|
|
|
8
8
|
# The loop itself is {Dispatcher}. This holds what the loop needs about
|
|
9
9
|
# the run: the tree and its scope, the roster, the executor, how many run
|
|
10
10
|
# at once, where each plan is checked out, and how a finished plan is
|
|
11
|
-
# published. Blocked plans (
|
|
11
|
+
# published. Blocked plans (⭕ 🛑 💣) are stepped around, never assigned,
|
|
12
|
+
# and while a 💣 stands nothing new is assigned at all (plan 009).
|
|
12
13
|
class Runner
|
|
13
14
|
# What one agent did to one plan.
|
|
14
15
|
Attempt = Data.define(:ordinal,
|
|
@@ -33,7 +34,11 @@ module OpenLanes
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# One unit of work: an agent, a plan, and the checkout it happens in.
|
|
36
|
-
|
|
37
|
+
# `refresh` is what bringing the checkout up to date did, nil when
|
|
38
|
+
# nothing was tried (shared isolation, a dry run, a partner already in it).
|
|
39
|
+
Task = Data.define(:agent, :subject, :root, :checkout, :round, :refresh) do
|
|
40
|
+
def initialize(refresh: nil, **rest) = super
|
|
41
|
+
|
|
37
42
|
# @return [Hash] columns for the log
|
|
38
43
|
def log_fields
|
|
39
44
|
{ plan: subject.feature.ordinal.to_s, status: subject.status.to_s,
|
|
@@ -49,10 +54,24 @@ module OpenLanes
|
|
|
49
54
|
# other machines; nil when the run is not live, and every plan is ours
|
|
50
55
|
# @param stay [Boolean] idle past fixed points until a person stops
|
|
51
56
|
# the run; see {Dispatcher#done?}
|
|
52
|
-
# @param rescan [Integer]
|
|
57
|
+
# @param rescan [Integer] ticks between rescans while idle
|
|
58
|
+
# @param watch [OpenLanes::Engine::PrWatch, nil] see {Dispatcher#initialize}
|
|
59
|
+
# @param ignore_bombs [Boolean] `--ignore-bombs`: a 💣 halts nothing
|
|
60
|
+
# @param aging_since [Date, nil] `plans.aging_since`, the floor of every
|
|
61
|
+
# blocked plan's clock
|
|
62
|
+
# @param verification [OpenLanes::Engine::Verification::Driver, nil]
|
|
63
|
+
# what a 🧪 plan is run through (plan 014); nil when the run publishes
|
|
64
|
+
# nothing, and an approved plan then finishes at 👀
|
|
65
|
+
# @param journal [OpenLanes::Engine::EventJournal, nil] the progress
|
|
66
|
+
# journal each engine event is appended to; nil keeps none
|
|
67
|
+
# @param merging [OpenLanes::Engine::Merging, nil] what a 🏁 plan's
|
|
68
|
+
# pull requests go through once palpatine has ruled; nil when the
|
|
69
|
+
# run publishes nothing
|
|
53
70
|
def initialize(tree:, executor:, agents: Agents::Registry.new, isolation: :shared, jobs: 1, worktree: nil,
|
|
54
71
|
plans: nil, publisher: nil, dry_run: false, rounds: nil, state: nil,
|
|
55
|
-
sleeper: ->(seconds) { sleep(seconds) }, on_board: nil, claims: nil, stay: false, rescan: Dispatcher::RESCAN
|
|
72
|
+
sleeper: ->(seconds) { sleep(seconds) }, on_board: nil, claims: nil, stay: false, rescan: Dispatcher::RESCAN,
|
|
73
|
+
watch: nil, ignore_bombs: false, aging_since: nil, extensions: nil, verification: nil, journal: nil,
|
|
74
|
+
merging: nil)
|
|
56
75
|
@tree = tree
|
|
57
76
|
@executor = executor
|
|
58
77
|
@agents = agents
|
|
@@ -68,6 +87,14 @@ module OpenLanes
|
|
|
68
87
|
@claims = claims
|
|
69
88
|
@stay = stay
|
|
70
89
|
@rescan = rescan
|
|
90
|
+
@watch = watch
|
|
91
|
+
@ignore_bombs = ignore_bombs
|
|
92
|
+
@aging_since = aging_since
|
|
93
|
+
@extensions = extensions
|
|
94
|
+
@verification = verification
|
|
95
|
+
@journal = dry_run ? nil : journal
|
|
96
|
+
@merging = merging
|
|
97
|
+
@would_bomb = []
|
|
71
98
|
@attempts = []
|
|
72
99
|
@dropped = []
|
|
73
100
|
@jobs = isolated? ? jobs : 1
|
|
@@ -75,6 +102,29 @@ module OpenLanes
|
|
|
75
102
|
|
|
76
103
|
attr_reader :jobs, :worktree, :tree, :attempts, :executor, :agents
|
|
77
104
|
|
|
105
|
+
# @return [OpenLanes::Engine::Verification::Driver, nil]
|
|
106
|
+
attr_reader :verification
|
|
107
|
+
|
|
108
|
+
# @return [OpenLanes::Engine::Merging, nil]
|
|
109
|
+
attr_reader :merging
|
|
110
|
+
|
|
111
|
+
# A new concurrency limit, from the copilot's `/concurrency N`. Nothing
|
|
112
|
+
# running is stopped: the dispatcher fills only while fewer agents run
|
|
113
|
+
# than the limit, so a lower one takes effect as agents finish.
|
|
114
|
+
#
|
|
115
|
+
# @param count [Integer] at least 1
|
|
116
|
+
# @return [void]
|
|
117
|
+
# @raise [OpenLanes::Error] under `--isolation shared`, where every agent
|
|
118
|
+
# works in one checkout and a second would write over the first
|
|
119
|
+
def jobs=(count)
|
|
120
|
+
raise OpenLanes::Error, "concurrency must be a whole number of at least 1, not #{count.inspect}" unless count.is_a?(Integer) && count >= 1
|
|
121
|
+
if !isolated? && count != 1
|
|
122
|
+
raise OpenLanes::Error, "this run is --isolation shared: one checkout, one agent at a time, so concurrency stays 1"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@jobs = count
|
|
126
|
+
end
|
|
127
|
+
|
|
78
128
|
# @return [Boolean]
|
|
79
129
|
def isolated? = @isolation == :worktree
|
|
80
130
|
|
|
@@ -104,6 +154,16 @@ module OpenLanes
|
|
|
104
154
|
# @return [void]
|
|
105
155
|
def drop_plan(ordinal) = @dropped |= [ordinal.to_s]
|
|
106
156
|
|
|
157
|
+
# @param ordinal [#to_s]
|
|
158
|
+
# @return [Boolean] whether {#drop_plan} took it out of this run
|
|
159
|
+
def dropped?(ordinal) = @dropped.include?(ordinal.to_s)
|
|
160
|
+
|
|
161
|
+
# Let another machine take the plan: a person stopped it here.
|
|
162
|
+
#
|
|
163
|
+
# @param ordinal [#to_s]
|
|
164
|
+
# @return [void]
|
|
165
|
+
def release(ordinal) = @claims&.release(ordinal)
|
|
166
|
+
|
|
107
167
|
# @return [Array<OpenLanes::Plans::Subject>]
|
|
108
168
|
def in_scope = tree.subjects.select { |s| in_scope?(s) }
|
|
109
169
|
|
|
@@ -132,20 +192,30 @@ module OpenLanes
|
|
|
132
192
|
false
|
|
133
193
|
end
|
|
134
194
|
|
|
135
|
-
# Take the plan before an agent starts on it. With Redis gone
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
195
|
+
# Take the plan before an agent starts on it. With Redis gone nothing
|
|
196
|
+
# new starts (W31): this machine cannot tell whether another one sharing
|
|
197
|
+
# the Redis already took the plan, and two machines building one spec is
|
|
198
|
+
# the collision the claim exists to prevent. Agents already running keep
|
|
199
|
+
# going. A run with no Redis at all has no claims and works alone.
|
|
139
200
|
#
|
|
140
201
|
# @param subject [OpenLanes::Plans::Subject]
|
|
141
202
|
# @return [Boolean] whether this machine may work it
|
|
142
203
|
def claim(subject)
|
|
143
|
-
@claims.nil? || @claims.claim(subject.feature.ordinal)
|
|
204
|
+
claimed = @claims.nil? || @claims.claim(subject.feature.ordinal)
|
|
205
|
+
@claims_down = nil
|
|
206
|
+
claimed
|
|
144
207
|
rescue Live::Unavailable => e
|
|
145
208
|
claims_unavailable(e)
|
|
146
|
-
|
|
209
|
+
false
|
|
147
210
|
end
|
|
148
211
|
|
|
212
|
+
# Where the plan's checkout lives when the run isolates, whether or not
|
|
213
|
+
# it has been made.
|
|
214
|
+
#
|
|
215
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
216
|
+
# @return [String, nil]
|
|
217
|
+
def checkout_path(subject) = isolated? ? worktree.path_for(subject.feature) : nil
|
|
218
|
+
|
|
149
219
|
# Run until nothing is dispatchable, or, staying, until a person stops
|
|
150
220
|
# it.
|
|
151
221
|
#
|
|
@@ -159,13 +229,99 @@ module OpenLanes
|
|
|
159
229
|
sleeper: @sleeper,
|
|
160
230
|
on_board: @on_board,
|
|
161
231
|
stay: @stay,
|
|
162
|
-
rescan: @rescan
|
|
232
|
+
rescan: @rescan,
|
|
233
|
+
watch: @watch,
|
|
234
|
+
extensions: @extensions,
|
|
235
|
+
journal: @journal)
|
|
163
236
|
yield dispatcher if block_given?
|
|
164
237
|
@attempts = dispatcher.run
|
|
165
238
|
end
|
|
166
239
|
|
|
167
240
|
# @return [Array<OpenLanes::Plans::Subject>]
|
|
168
|
-
def blocked = in_scope.select { |s|
|
|
241
|
+
def blocked = in_scope.select { |s| Dispatcher::BLOCKED.include?(s.status.key) }
|
|
242
|
+
|
|
243
|
+
# Age the blocked plans in scope (plan 009): bomb the ones others wait
|
|
244
|
+
# on, defer the ones nobody does. A dry run fires nothing, and
|
|
245
|
+
# remembers what it would have bombed so it halts as a real run would.
|
|
246
|
+
#
|
|
247
|
+
# @param except [Array<String>] ordinals an agent or a drain is in
|
|
248
|
+
# @param now [Time]
|
|
249
|
+
# @return [OpenLanes::Lifecycle::Aging::Result]
|
|
250
|
+
def age!(except: [], now: Clock::System.now)
|
|
251
|
+
subjects = in_scope.reject { except.include?(it.feature.ordinal.to_s) || foreign?(it) }
|
|
252
|
+
result = Lifecycle.age(tree:, subjects:, since: @aging_since, now:, commit: !dry_run?)
|
|
253
|
+
@would_bomb = result.bombed if dry_run?
|
|
254
|
+
result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Plans in scope that are 💣, or on a dry run would have become one.
|
|
258
|
+
#
|
|
259
|
+
# @return [Array<OpenLanes::Plans::Subject>]
|
|
260
|
+
def bombs
|
|
261
|
+
in_scope.select { it.status.key == :bomb || @would_bomb.include?(it.feature.ordinal.to_s) }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The bombs that stop new work right now: none under `--ignore-bombs`,
|
|
265
|
+
# and none whose halt a person lifted from the page until it lapses.
|
|
266
|
+
#
|
|
267
|
+
# @param now [Time]
|
|
268
|
+
# @return [Array<OpenLanes::Plans::Subject>]
|
|
269
|
+
def halted_by(now: Clock::System.now)
|
|
270
|
+
return [] if @ignore_bombs
|
|
271
|
+
|
|
272
|
+
bombs.reject { Plans::PlanState.for(it.feature.path).resumed?(now:) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @return [Boolean] whether a 💣 keeps every new agent from starting
|
|
276
|
+
def halted? = !halted_by.empty?
|
|
277
|
+
|
|
278
|
+
# Plans in scope the dependency gate holds back, with the reason: a
|
|
279
|
+
# dependency that is not done yet, or a cycle that no order can break.
|
|
280
|
+
# A plan with no `depends_on:` key is never held, nor is one already
|
|
281
|
+
# parked, whose own state says more than its dependencies do.
|
|
282
|
+
#
|
|
283
|
+
# @param deps [OpenLanes::Plans::Dependencies] the graph to ask; the
|
|
284
|
+
# dispatcher passes the one it gated with, so both say the same thing
|
|
285
|
+
# @return [Hash{String => String}] ordinal => "waiting on 034.00" or "cycle: ..."
|
|
286
|
+
def held(deps = dependencies)
|
|
287
|
+
in_scope.each_with_object({}) do |subject, out|
|
|
288
|
+
next if Plans::StateMachine::SETTLED.include?(subject.status.key)
|
|
289
|
+
|
|
290
|
+
ordinal = subject.feature.ordinal.to_s
|
|
291
|
+
reason = deps.reason(ordinal) and out[ordinal] = reason
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# The dependency graph judged by the states this run acts on. A real run
|
|
296
|
+
# has reconciled every folder name before it asks, so the names are the
|
|
297
|
+
# states. A dry run renames nothing, so it judges each plan by the state
|
|
298
|
+
# reconciliation would give it, as {Dispatcher} does when it picks an
|
|
299
|
+
# agent; otherwise an In Review prerequisite whose pull requests have
|
|
300
|
+
# all merged would read as done to one and open to the other.
|
|
301
|
+
#
|
|
302
|
+
# @param except [Array<String>] ordinals reconciliation leaves alone
|
|
303
|
+
# (an agent is in them), judged by their folder's name; plans another
|
|
304
|
+
# machine holds are always left alone
|
|
305
|
+
# @return [OpenLanes::Plans::Dependencies]
|
|
306
|
+
def dependencies(except: [])
|
|
307
|
+
return tree.dependencies unless dry_run?
|
|
308
|
+
|
|
309
|
+
leave = except.map(&:to_s)
|
|
310
|
+
# Every plan, archived ones included, as {Plans::Tree#dependencies}
|
|
311
|
+
# does: a shipped prerequisite sits in `archive/`.
|
|
312
|
+
Plans::Dependencies.new(tree.everything,
|
|
313
|
+
status_of: lambda { |subject|
|
|
314
|
+
untouched = leave.include?(subject.feature.ordinal.to_s) || foreign?(subject)
|
|
315
|
+
untouched ? subject.status : effective_status(subject)
|
|
316
|
+
})
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The state a plan's contents justify, which reconciliation would rename
|
|
320
|
+
# its folder to: its lane's skip first, then what `resync dirs` reads.
|
|
321
|
+
#
|
|
322
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
323
|
+
# @return [OpenLanes::Plans::Status]
|
|
324
|
+
def effective_status(subject) = Lifecycle::Lanes.target(subject) || Lifecycle::Resync::Dirs.target(subject)
|
|
169
325
|
|
|
170
326
|
# @return [Boolean]
|
|
171
327
|
def settled? = in_scope.all? { |s| Plans::StateMachine::SETTLED.include?(s.status.key) }
|
|
@@ -176,12 +332,20 @@ module OpenLanes
|
|
|
176
332
|
# is the one checked out where the run was typed, which is where a dry
|
|
177
333
|
# run of that plan is typed.
|
|
178
334
|
#
|
|
335
|
+
# A worktree is brought up to date with the default branch first (see
|
|
336
|
+
# {Vcs::Refresh}), here rather than on the agent's thread, so no partner
|
|
337
|
+
# can start in the same checkout while git is rewriting it.
|
|
338
|
+
#
|
|
339
|
+
# @param refresh [Boolean] false when another agent is already working
|
|
340
|
+
# in the plan's checkout, which must not move under it
|
|
341
|
+
# @param hold [String, nil] see {Vcs::Refresh#call}
|
|
179
342
|
# @return [OpenLanes::Engine::Runner::Task]
|
|
180
|
-
def prepare(agent, subject, round)
|
|
343
|
+
def prepare(agent, subject, round, refresh: true, hold: nil)
|
|
181
344
|
return Task.new(agent:, subject:, root: shared_root, checkout: nil, round:) if !isolated? || dry_run?
|
|
182
345
|
|
|
183
346
|
checkout = worktree.checkout_for(subject.feature)
|
|
184
|
-
|
|
347
|
+
outcome = worktree.refresh(checkout, hold:) if refresh
|
|
348
|
+
Task.new(agent:, subject:, root: checkout.path, checkout:, round:, refresh: outcome)
|
|
185
349
|
end
|
|
186
350
|
|
|
187
351
|
# Push a finished build and record the pull request it opened in the
|
|
@@ -208,7 +372,14 @@ module OpenLanes
|
|
|
208
372
|
|
|
209
373
|
# @param error [OpenLanes::Live::Unavailable]
|
|
210
374
|
# @return [void]
|
|
211
|
-
|
|
375
|
+
# Said once per outage: every plan asks on every tick, and the same
|
|
376
|
+
# line a hundred times buries the moment Redis went away.
|
|
377
|
+
def claims_unavailable(error)
|
|
378
|
+
return if @claims_down == error.message
|
|
379
|
+
|
|
380
|
+
@claims_down = error.message
|
|
381
|
+
UI.log("claims unchecked, starting nothing new until Redis answers: #{error.message}", level: :warn)
|
|
382
|
+
end
|
|
212
383
|
|
|
213
384
|
# @return [void]
|
|
214
385
|
def record_pull_request(path, publication)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
# `openlanes start` without the command: {RunWorkflow} as a long-lived
|
|
6
|
+
# service. It stays up past every fixed point, rescanning the tree, and
|
|
7
|
+
# treats a missing live board or control room as a refusal rather than a
|
|
8
|
+
# reason to go on headless, since the page is the only way in.
|
|
9
|
+
class StartWorkflow < RunWorkflow
|
|
10
|
+
# Exit status when Redis or the control room is not there to serve.
|
|
11
|
+
UNAVAILABLE = 69
|
|
12
|
+
|
|
13
|
+
# @param rescan [Integer] seconds between rescans of .plans while idle
|
|
14
|
+
# @param options [Hash] as for {RunWorkflow#initialize}
|
|
15
|
+
def initialize(rescan:, **)
|
|
16
|
+
super(**)
|
|
17
|
+
@rescan = rescan
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Always stays, at the rescan asked for, and watches the pull requests
|
|
23
|
+
# a person merges the way a staying `run` does. `--rescan` is seconds
|
|
24
|
+
# and the dispatcher counts ticks, so a 5 passed through unconverted
|
|
25
|
+
# would rescan every 2.5 s.
|
|
26
|
+
#
|
|
27
|
+
# @param _web [OpenLanes::Web::Launcher]
|
|
28
|
+
# @return [Hash]
|
|
29
|
+
def staying(_web, _live = nil) = { stay: true, rescan: Dispatcher.ticks(@rescan), watch: watch }
|
|
30
|
+
|
|
31
|
+
# @param message [String]
|
|
32
|
+
# @return [Dry::Monads::Result::Failure]
|
|
33
|
+
def without_live(message)
|
|
34
|
+
refusal("#{message}\n\nopenlanes start needs Redis: its control room is the only way to answer the " \
|
|
35
|
+
"agents' questions. Start redis-server, or pass --redis-url. " \
|
|
36
|
+
"`openlanes run` goes on without it, headless.",
|
|
37
|
+
UNAVAILABLE)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param web [OpenLanes::Web::Launcher]
|
|
41
|
+
# @param live [OpenLanes::Live::Session]
|
|
42
|
+
# @return [Dry::Monads::Result::Failure]
|
|
43
|
+
def without_web(web, live)
|
|
44
|
+
live.close
|
|
45
|
+
refusal("The control room did not answer on #{web.url}, and openlanes start is no use without it. " \
|
|
46
|
+
"Its log: #{web.log}",
|
|
47
|
+
UNAVAILABLE)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A project with nothing left to do opens on the idea dialog (W19).
|
|
51
|
+
#
|
|
52
|
+
# @param web [OpenLanes::Web::Launcher]
|
|
53
|
+
# @return [String]
|
|
54
|
+
def browser_url(web)
|
|
55
|
+
open = @tree.subjects.reject { Dispatcher::FINISHED.include?(it.status.key) }
|
|
56
|
+
open.empty? ? "#{web.url}#start" : web.url
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# What the service did, and exit 0: stopping it on purpose is not a
|
|
60
|
+
# failure, and the failed attempts it lived through are listed above
|
|
61
|
+
# the box.
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer] 0
|
|
64
|
+
def finish(runner, attempts, seconds:)
|
|
65
|
+
puts_report(attempts, Engine.tally(attempts:, seconds:).render)
|
|
66
|
+
unless @quiet
|
|
67
|
+
blocked = runner.blocked.map { it.feature.ordinal }
|
|
68
|
+
lines = ["Stopped. Unfinished plans keep their state; openlanes start picks them up again."]
|
|
69
|
+
lines << "Still waiting for answers: #{blocked.join(", ")}" unless blocked.empty?
|
|
70
|
+
success("#{attempts.size} invocation#{"s" unless attempts.size == 1}, " \
|
|
71
|
+
"#{attempts.count(&:advanced?)} advanced\n\n#{lines.join("\n")}")
|
|
72
|
+
end
|
|
73
|
+
0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
|
+
require "socket"
|
|
5
6
|
|
|
6
7
|
module OpenLanes
|
|
7
8
|
module Engine
|
|
@@ -11,58 +12,72 @@ module OpenLanes
|
|
|
11
12
|
# The ledger inside each document is the record humans read and the
|
|
12
13
|
# dispatcher acts on; this is the index of it, plus what the ledger cannot
|
|
13
14
|
# hold: which run wrote what, process ids, tokens, and whether the run
|
|
14
|
-
# that wrote a `Started` is still alive. It sits in
|
|
15
|
-
# the system temp dir because
|
|
16
|
-
#
|
|
15
|
+
# that wrote a `Started` is still alive. It sits in the runtime state
|
|
16
|
+
# directory ({OpenLanes::Runtime}) rather than the system temp dir because
|
|
17
|
+
# a reboot must not lose it, and outside `.plans/` because it is about one
|
|
18
|
+
# machine's run, not the plan: in `.plans/` every `git status` and every
|
|
19
|
+
# snapshot of the tree had to learn to ignore it.
|
|
17
20
|
class StateFile
|
|
18
21
|
FILENAME = "openlanes-state.json"
|
|
19
22
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
23
|
+
# The oldest place it lived, under `.plans/tmp/`. Read by {#load} after
|
|
24
|
+
# `.plans/` itself, so a run started under either old layout is picked
|
|
25
|
+
# up rather than silently restarted from zero. Nothing writes to either
|
|
26
|
+
# any more and nothing deletes them: the file left behind is the only
|
|
27
|
+
# record a half-finished run has.
|
|
25
28
|
LEGACY_DIRNAME = "tmp"
|
|
26
29
|
|
|
27
|
-
# The
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# during that instant must not offer
|
|
31
|
-
IGNORE = ["#{OpenLanes::PLANS_DIR}
|
|
30
|
+
# The line {.ensure_ignored!} adds: the `<name>.<pid>.tmp` siblings
|
|
31
|
+
# `state.json` and `blocked.md` are written to and renamed over, inside
|
|
32
|
+
# plan folders whose other contents are committed. Brief, but a
|
|
33
|
+
# `git status` during that instant must not offer one.
|
|
34
|
+
IGNORE = ["#{OpenLanes::PLANS_DIR}/**/*.tmp"].freeze
|
|
35
|
+
|
|
36
|
+
# A literal path git can be asked about for the {IGNORE} line.
|
|
37
|
+
PROBE = "#{OpenLanes::PLANS_DIR}/000.00/state.json.1.2.tmp".freeze
|
|
32
38
|
|
|
33
39
|
# @param tree [OpenLanes::Plans::Tree]
|
|
34
|
-
# @return [String]
|
|
35
|
-
def self.for(tree) = File.
|
|
40
|
+
# @return [String] in the runtime state directory, keyed by the tree's root
|
|
41
|
+
def self.for(tree) = Runtime.state_file(root: File.dirname(File.expand_path(tree.dir)))
|
|
36
42
|
|
|
37
43
|
# @param tree [OpenLanes::Plans::Tree]
|
|
38
|
-
# @return [String] where
|
|
39
|
-
def self.legacy_for(tree) = File.join(tree.dir, LEGACY_DIRNAME, FILENAME)
|
|
44
|
+
# @return [Array<String>] where older releases wrote it, newest first
|
|
45
|
+
def self.legacy_for(tree) = [File.join(tree.dir, FILENAME), File.join(tree.dir, LEGACY_DIRNAME, FILENAME)]
|
|
40
46
|
|
|
41
|
-
# Add the ignore
|
|
42
|
-
# to announce, which is why this returns whether it did.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# and the second is a glob no path equals. They are written together,
|
|
46
|
-
# so one being ignored means both are.
|
|
47
|
+
# Add the ignore line once. Editing somebody's `.gitignore` is a thing
|
|
48
|
+
# to announce, which is why this returns whether it did. Lines an older
|
|
49
|
+
# release wrote are left where they are: removing a line is editing
|
|
50
|
+
# something this method did not write.
|
|
47
51
|
#
|
|
48
52
|
# @param root [String] repository root
|
|
49
53
|
# @return [Boolean] true when lines were added
|
|
50
54
|
def self.ensure_ignored!(root)
|
|
51
55
|
return false unless system("git", "-C", root, "rev-parse", "--git-dir", out: File::NULL, err: File::NULL)
|
|
52
|
-
|
|
56
|
+
|
|
57
|
+
missing = ignored?(root, PROBE) ? [] : IGNORE
|
|
58
|
+
return false if missing.empty?
|
|
53
59
|
|
|
54
60
|
path = File.join(root, ".gitignore")
|
|
55
61
|
existing = File.file?(path) ? File.read(path) : ""
|
|
56
62
|
glue = existing.empty? || existing.end_with?("\n") ? "" : "\n"
|
|
57
|
-
File.write(path, "#{existing}#{glue}#{
|
|
63
|
+
File.write(path, "#{existing}#{glue}#{missing.join("\n")}\n")
|
|
58
64
|
true
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
# @param root [String]
|
|
68
|
+
# @param path [String] relative to +root+
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def self.ignored?(root, path) = system("git", "-C", root, "check-ignore", "-q", path, out: File::NULL, err: File::NULL)
|
|
71
|
+
private_class_method :ignored?
|
|
72
|
+
|
|
61
73
|
# @param path [String]
|
|
62
74
|
# @param pid [Integer] this run's process id
|
|
63
|
-
|
|
75
|
+
# @param legacy [Array<String>, nil] older locations {#load} falls back
|
|
76
|
+
# to; by default the two beside and under +path+'s directory
|
|
77
|
+
def initialize(path:, pid: Process.pid, legacy: nil)
|
|
64
78
|
@path = path
|
|
65
79
|
@pid = pid
|
|
80
|
+
@legacy = legacy
|
|
66
81
|
@data = { "run" => {}, "plans" => {} }
|
|
67
82
|
@mutex = Mutex.new
|
|
68
83
|
end
|
|
@@ -70,11 +85,11 @@ module OpenLanes
|
|
|
70
85
|
# @return [String]
|
|
71
86
|
attr_reader :path
|
|
72
87
|
|
|
73
|
-
# Where a run started
|
|
74
|
-
#
|
|
88
|
+
# Where a run started under an older layout would have written.
|
|
89
|
+
# {#load} falls back to them in order; {#save} never uses them.
|
|
75
90
|
#
|
|
76
|
-
# @return [String]
|
|
77
|
-
def
|
|
91
|
+
# @return [Array<String>]
|
|
92
|
+
def legacy_paths = @legacy || [File.join(File.dirname(path), LEGACY_DIRNAME, File.basename(path))]
|
|
78
93
|
|
|
79
94
|
# Reads the current path, or the legacy one when the current path holds
|
|
80
95
|
# nothing. A run interrupted under the old layout is otherwise invisible
|
|
@@ -84,7 +99,7 @@ module OpenLanes
|
|
|
84
99
|
# @return [self]
|
|
85
100
|
def load
|
|
86
101
|
@mutex.synchronize do
|
|
87
|
-
source = [path,
|
|
102
|
+
source = [path, *legacy_paths].find { |candidate| File.file?(candidate) }
|
|
88
103
|
@data = JSON.parse(File.read(source)) if source
|
|
89
104
|
@data = { "run" => {}, "plans" => {} } unless @data.is_a?(Hash) && @data["plans"].is_a?(Hash)
|
|
90
105
|
end
|
|
@@ -106,18 +121,24 @@ module OpenLanes
|
|
|
106
121
|
end
|
|
107
122
|
end
|
|
108
123
|
|
|
124
|
+
# Also forgets the previous run's `waiting_on`. A live pid only says some
|
|
125
|
+
# run is up, not that it watches: a plain `run`, or one with `merge.by:
|
|
126
|
+
# auto`, never writes the map, so the last watch's entries would read as
|
|
127
|
+
# current for as long as this run lasted.
|
|
128
|
+
#
|
|
109
129
|
# @param root [String]
|
|
110
130
|
# @return [void]
|
|
111
131
|
def begin_run!(root:)
|
|
112
132
|
@mutex.synchronize do
|
|
113
|
-
@data
|
|
114
|
-
|
|
133
|
+
@data.delete("waiting_on")
|
|
134
|
+
@data["run"] = { "pid" => @pid, "host" => Socket.gethostname, "root" => root,
|
|
135
|
+
"started_at" => Clock::System.now.iso8601, "heartbeat_at" => Clock::System.now.iso8601 }
|
|
115
136
|
end
|
|
116
137
|
end
|
|
117
138
|
|
|
118
139
|
# @return [void]
|
|
119
140
|
def heartbeat!
|
|
120
|
-
@mutex.synchronize { @data["run"]["heartbeat_at"] =
|
|
141
|
+
@mutex.synchronize { @data["run"]["heartbeat_at"] = Clock::System.now.iso8601 }
|
|
121
142
|
end
|
|
122
143
|
|
|
123
144
|
# @param ordinal [String]
|
|
@@ -152,6 +173,42 @@ module OpenLanes
|
|
|
152
173
|
@mutex.synchronize { @data["live"] = data }
|
|
153
174
|
end
|
|
154
175
|
|
|
176
|
+
# The Redis outage under way (W32): when it began, when the agents were
|
|
177
|
+
# paused, how many times Redis was asked since. Nil when there is none.
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash, nil]
|
|
180
|
+
def outage = @mutex.synchronize { @data["outage"]&.dup }
|
|
181
|
+
|
|
182
|
+
# @param record [Hash, nil] written by the next {#save}
|
|
183
|
+
# @return [void]
|
|
184
|
+
def outage=(record)
|
|
185
|
+
@mutex.synchronize { record ? @data["outage"] = record : @data.delete("outage") }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Outages that ended, resumed or given up on, oldest first.
|
|
189
|
+
#
|
|
190
|
+
# @return [Array<Hash>]
|
|
191
|
+
def outages = @mutex.synchronize { (@data["outages"] || []).map(&:dup) }
|
|
192
|
+
|
|
193
|
+
# @param records [Array<Hash>]
|
|
194
|
+
# @return [void]
|
|
195
|
+
def outages=(records)
|
|
196
|
+
@mutex.synchronize { @data["outages"] = records }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Pull requests open and waiting on a person, by plan, as the
|
|
200
|
+
# human-mode watch last saw them. Here so `openlanes list-plans`, a
|
|
201
|
+
# process of its own, can say who owes what while the engine runs.
|
|
202
|
+
#
|
|
203
|
+
# @return [Hash{String => Array<Integer>}]
|
|
204
|
+
def waiting_on = @mutex.synchronize { (@data["waiting_on"] || {}).transform_values(&:dup) }
|
|
205
|
+
|
|
206
|
+
# @param data [Hash{String => Array<Integer>}] written by the next {#save}
|
|
207
|
+
# @return [void]
|
|
208
|
+
def waiting_on=(data)
|
|
209
|
+
@mutex.synchronize { @data["waiting_on"] = data.transform_keys(&:to_s) }
|
|
210
|
+
end
|
|
211
|
+
|
|
155
212
|
# @param ordinal [String]
|
|
156
213
|
# @return [Array<Hash>]
|
|
157
214
|
def stages(ordinal) = @mutex.synchronize { (@data.dig("plans", ordinal.to_s, "stages") || []).map(&:dup) }
|
|
@@ -159,11 +216,21 @@ module OpenLanes
|
|
|
159
216
|
# @return [Array<String>] every ordinal with a stage recorded
|
|
160
217
|
def plans = @mutex.synchronize { @data["plans"].keys }
|
|
161
218
|
|
|
219
|
+
# @return [Boolean] whether a run wrote this file and is still alive
|
|
220
|
+
def running? = !@data.dig("run", "pid").nil? && !previous_run_dead?
|
|
221
|
+
|
|
222
|
+
# A run recorded on another host is never provably dead: its pid names
|
|
223
|
+
# a process on a machine this one cannot see. A record with no `host`
|
|
224
|
+
# predates the field and keeps the local check.
|
|
225
|
+
#
|
|
162
226
|
# @return [Boolean] whether the run that last wrote this file is gone
|
|
163
227
|
def previous_run_dead?
|
|
164
228
|
previous = @data.dig("run", "pid")
|
|
165
229
|
return false if previous.nil? || previous == @pid
|
|
166
230
|
|
|
231
|
+
host = @data.dig("run", "host")
|
|
232
|
+
return false if host && host != Socket.gethostname
|
|
233
|
+
|
|
167
234
|
Process.kill(0, previous)
|
|
168
235
|
false
|
|
169
236
|
rescue Errno::ESRCH, Errno::EPERM
|
|
@@ -4,7 +4,7 @@ module OpenLanes
|
|
|
4
4
|
module Engine
|
|
5
5
|
# What a `run` cost, once it is over.
|
|
6
6
|
#
|
|
7
|
-
# The loop reports movement — which plan went from
|
|
7
|
+
# The loop reports movement — which plan went from ⭐ to 🟡, which agent
|
|
8
8
|
# failed and why — and says nothing about the bill. A round of five agents
|
|
9
9
|
# against a large repository spends several million tokens and the better
|
|
10
10
|
# part of an hour, and until this existed the only record of that was a
|