openlanes 3.0.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 +7 -0
- data/agents/hansolo-reviewer.md +59 -0
- data/agents/lando-broker.md +70 -0
- data/agents/leah-researcher.md +60 -0
- data/agents/luke-backend.md +73 -0
- data/agents/palpatine-planner.md +61 -0
- data/agents/r2d2-mechanic.md +57 -0
- data/agents/rey-frontend.md +68 -0
- data/agents/yoda-writer.md +67 -0
- data/bin/factory-e2e +157 -0
- data/bin/plan-number +164 -0
- data/bin/setup +26 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
- data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
- data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast.yml +44 -0
- data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
- data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
- data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep.yml +51 -0
- data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
- data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
- data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast.yml +55 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
- data/evals/cases/luke-backend/build-medium.yml +68 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium.yml +55 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
- data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
- data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium.yml +40 -0
- data/exe/depot +1 -0
- data/exe/lanes +1 -0
- data/exe/openlanes +32 -0
- data/lib/openlanes/adapters/base.rb +69 -0
- data/lib/openlanes/adapters/claude.rb +124 -0
- data/lib/openlanes/adapters/codex.rb +33 -0
- data/lib/openlanes/adapters/model.rb +37 -0
- data/lib/openlanes/adapters/pi.rb +22 -0
- data/lib/openlanes/adapters/stream.rb +196 -0
- data/lib/openlanes/adapters.rb +92 -0
- data/lib/openlanes/agents/agent.rb +103 -0
- data/lib/openlanes/agents/profile.rb +57 -0
- data/lib/openlanes/agents/registry.rb +151 -0
- data/lib/openlanes/agents/roster.rb +103 -0
- data/lib/openlanes/agents/routing.rb +88 -0
- data/lib/openlanes/agents.rb +20 -0
- data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
- data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
- data/lib/openlanes/cli/base.rb +263 -0
- data/lib/openlanes/cli/create/create.rb +354 -0
- data/lib/openlanes/cli/docs/docs.rb +40 -0
- data/lib/openlanes/cli/eval/eval.rb +110 -0
- data/lib/openlanes/cli/index/index.rb +40 -0
- data/lib/openlanes/cli/init/init.rb +145 -0
- data/lib/openlanes/cli/linear/linear.rb +36 -0
- data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
- data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
- data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
- data/lib/openlanes/cli/mail/mail.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
- data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
- data/lib/openlanes/cli/run/run.rb +544 -0
- data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
- data/lib/openlanes/cli/start/start.rb +110 -0
- data/lib/openlanes/cli/state/state.rb +26 -0
- data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
- data/lib/openlanes/cli/states/states.rb +15 -0
- data/lib/openlanes/cli/unblock/unblock.rb +235 -0
- data/lib/openlanes/cli/version/version.rb +13 -0
- data/lib/openlanes/cli/web/web.rb +100 -0
- data/lib/openlanes/cli/worktree/worktree.rb +85 -0
- data/lib/openlanes/cli.rb +162 -0
- data/lib/openlanes/engine/board.rb +104 -0
- data/lib/openlanes/engine/dispatcher.rb +1297 -0
- data/lib/openlanes/engine/progress_log.rb +125 -0
- data/lib/openlanes/engine/runner.rb +224 -0
- data/lib/openlanes/engine/state_file.rb +190 -0
- data/lib/openlanes/engine/tally.rb +177 -0
- data/lib/openlanes/engine.rb +19 -0
- data/lib/openlanes/evals/case.rb +191 -0
- data/lib/openlanes/evals/changes.rb +67 -0
- data/lib/openlanes/evals/check.rb +224 -0
- data/lib/openlanes/evals/evidence.rb +71 -0
- data/lib/openlanes/evals/live.rb +140 -0
- data/lib/openlanes/evals/recording.rb +38 -0
- data/lib/openlanes/evals/run.rb +34 -0
- data/lib/openlanes/evals/score.rb +24 -0
- data/lib/openlanes/evals/scorer.rb +42 -0
- data/lib/openlanes/evals/verdict.rb +18 -0
- data/lib/openlanes/evals.rb +56 -0
- data/lib/openlanes/execution/checks.rb +73 -0
- data/lib/openlanes/execution/child.rb +228 -0
- data/lib/openlanes/execution/clock.rb +121 -0
- data/lib/openlanes/execution/control.rb +194 -0
- data/lib/openlanes/execution/executor.rb +849 -0
- data/lib/openlanes/execution/prompt.rb +455 -0
- data/lib/openlanes/execution/transcript.rb +531 -0
- data/lib/openlanes/execution.rb +19 -0
- data/lib/openlanes/launcher.rb +69 -0
- data/lib/openlanes/lifecycle/adoption.rb +197 -0
- data/lib/openlanes/lifecycle/brief.rb +288 -0
- data/lib/openlanes/lifecycle/completion.rb +58 -0
- data/lib/openlanes/lifecycle/creator.rb +126 -0
- data/lib/openlanes/lifecycle/lanes.rb +74 -0
- data/lib/openlanes/lifecycle/resync.rb +428 -0
- data/lib/openlanes/lifecycle/unblocker.rb +236 -0
- data/lib/openlanes/lifecycle.rb +28 -0
- data/lib/openlanes/linear/api.rb +224 -0
- data/lib/openlanes/linear/attribution.rb +185 -0
- data/lib/openlanes/linear/fuzzy.rb +68 -0
- data/lib/openlanes/linear/import.rb +314 -0
- data/lib/openlanes/linear/issue.rb +187 -0
- data/lib/openlanes/linear/mapping.rb +116 -0
- data/lib/openlanes/linear/push.rb +197 -0
- data/lib/openlanes/linear/survey.rb +174 -0
- data/lib/openlanes/linear/unit.rb +274 -0
- data/lib/openlanes/linear.rb +41 -0
- data/lib/openlanes/live/claims.rb +71 -0
- data/lib/openlanes/live/command.rb +122 -0
- data/lib/openlanes/live/commands.rb +505 -0
- data/lib/openlanes/live/keys.rb +93 -0
- data/lib/openlanes/live/memory_store.rb +265 -0
- data/lib/openlanes/live/meter.rb +51 -0
- data/lib/openlanes/live/node.rb +88 -0
- data/lib/openlanes/live/publisher.rb +179 -0
- data/lib/openlanes/live/repo.rb +60 -0
- data/lib/openlanes/live/run_record.rb +133 -0
- data/lib/openlanes/live/run_state.rb +101 -0
- data/lib/openlanes/live/session.rb +107 -0
- data/lib/openlanes/live/snapshot.rb +322 -0
- data/lib/openlanes/live/stations.rb +67 -0
- data/lib/openlanes/live/store.rb +277 -0
- data/lib/openlanes/live.rb +109 -0
- data/lib/openlanes/plans/dev_work.rb +56 -0
- data/lib/openlanes/plans/feature.rb +131 -0
- data/lib/openlanes/plans/index.rb +208 -0
- data/lib/openlanes/plans/ledger.rb +255 -0
- data/lib/openlanes/plans/mailbox.rb +164 -0
- data/lib/openlanes/plans/ordinal.rb +92 -0
- data/lib/openlanes/plans/plan_state.rb +291 -0
- data/lib/openlanes/plans/pull_request.rb +29 -0
- data/lib/openlanes/plans/pull_requests.rb +229 -0
- data/lib/openlanes/plans/spec.rb +213 -0
- data/lib/openlanes/plans/state_machine.rb +433 -0
- data/lib/openlanes/plans/status.rb +365 -0
- data/lib/openlanes/plans/subject.rb +133 -0
- data/lib/openlanes/plans/tree.rb +79 -0
- data/lib/openlanes/plans.rb +39 -0
- data/lib/openlanes/presentation/diagram.rb +149 -0
- data/lib/openlanes/presentation/documentation.rb +438 -0
- data/lib/openlanes/presentation/reporter.rb +181 -0
- data/lib/openlanes/presentation/viewer.rb +62 -0
- data/lib/openlanes/presentation.rb +23 -0
- data/lib/openlanes/skills/installer.rb +68 -0
- data/lib/openlanes/skills/skill.rb +60 -0
- data/lib/openlanes/skills.rb +25 -0
- data/lib/openlanes/support/config.rb +217 -0
- data/lib/openlanes/support/frontmatter.rb +38 -0
- data/lib/openlanes/support/markdown.rb +56 -0
- data/lib/openlanes/support/project.rb +79 -0
- data/lib/openlanes/support/ui.rb +671 -0
- data/lib/openlanes/vcs/github.rb +171 -0
- data/lib/openlanes/vcs/publisher.rb +221 -0
- data/lib/openlanes/vcs/worktree.rb +248 -0
- data/lib/openlanes/vcs.rb +19 -0
- data/lib/openlanes/version.rb +10 -0
- data/lib/openlanes/web/app.rb +176 -0
- data/lib/openlanes/web/context.rb +66 -0
- data/lib/openlanes/web/demo.rb +169 -0
- data/lib/openlanes/web/demo_board.rb +286 -0
- data/lib/openlanes/web/events.rb +104 -0
- data/lib/openlanes/web/launcher.rb +143 -0
- data/lib/openlanes/web/server.rb +77 -0
- data/lib/openlanes/web/stores.rb +36 -0
- data/lib/openlanes/web.rb +92 -0
- data/lib/openlanes.rb +151 -0
- data/openlanes.gemspec +87 -0
- data/schemas/plan-state.schema.json +101 -0
- data/skills/openlanes/SKILL.md +48 -0
- data/skills/openlanes-documents/SKILL.md +61 -0
- data/skills/openlanes-documents/references/blocked-template.md +49 -0
- data/skills/openlanes-documents/references/closing-templates.md +59 -0
- data/skills/openlanes-documents/references/plan-template.md +61 -0
- data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
- data/skills/openlanes-documents/references/spec-template.md +45 -0
- data/skills/openlanes-run/SKILL.md +90 -0
- data/skills/openlanes-setup/SKILL.md +64 -0
- data/skills/openlanes-spec/SKILL.md +90 -0
- data/skills/openlanes-status/SKILL.md +32 -0
- data/skills/openlanes-sync/SKILL.md +22 -0
- data/web/dist/assets/apl-B4CMkyY2.js +2 -0
- data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
- data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
- data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
- data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
- data/web/dist/assets/clike-B9uivgTg.js +2 -0
- data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
- data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
- data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
- data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
- data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
- data/web/dist/assets/crystal-SjHAIU92.js +2 -0
- data/web/dist/assets/css-BnMrqG3P.js +2 -0
- data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
- data/web/dist/assets/d-pRatUO7H.js +2 -0
- data/web/dist/assets/diff-DbItnlRl.js +2 -0
- data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
- data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
- data/web/dist/assets/dylan-DwRh75JA.js +2 -0
- data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
- data/web/dist/assets/ecl-Cabwm37j.js +2 -0
- data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
- data/web/dist/assets/elm-vLlmbW-K.js +2 -0
- data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
- data/web/dist/assets/factor-kuTfRLto.js +2 -0
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
- data/web/dist/assets/forth-Ffai-XNe.js +2 -0
- data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
- data/web/dist/assets/gas-Bneqetm1.js +2 -0
- data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
- data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
- data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
- data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
- data/web/dist/assets/http-DBlCnlav.js +2 -0
- data/web/dist/assets/idl-BEugSyMb.js +2 -0
- data/web/dist/assets/index-2xPC-eHt.js +2 -0
- data/web/dist/assets/index-8dNhRmiO.js +8 -0
- data/web/dist/assets/index-B7-O9ujQ.js +2 -0
- data/web/dist/assets/index-BdR34cXC.js +2 -0
- data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
- data/web/dist/assets/index-Byyxbh_z.js +4 -0
- data/web/dist/assets/index-C0VmkrO-.js +2 -0
- data/web/dist/assets/index-CI74IkWP.js +198 -0
- data/web/dist/assets/index-CYxcbpRM.js +3 -0
- data/web/dist/assets/index-D0rmJN3h.js +2 -0
- data/web/dist/assets/index-DB4QwuYn.js +2 -0
- data/web/dist/assets/index-DDI7zoAS.js +2 -0
- data/web/dist/assets/index-DGLUuEXi.js +2 -0
- data/web/dist/assets/index-OR-odsl9.js +2 -0
- data/web/dist/assets/index-P3vSkiU-.js +2 -0
- data/web/dist/assets/index-a4EHR-u4.css +1 -0
- data/web/dist/assets/index-mRV4XhJZ.js +2 -0
- data/web/dist/assets/index-popk9wbW.js +2 -0
- data/web/dist/assets/index-uM88Njdj.js +2 -0
- data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
- data/web/dist/assets/julia-DuME0IfC.js +2 -0
- data/web/dist/assets/livescript-BwQOo05w.js +2 -0
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- data/web/dist/assets/lua-VAEuO923.js +2 -0
- data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
- data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
- data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
- data/web/dist/assets/mllike-CXdrOF99.js +2 -0
- data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
- data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
- data/web/dist/assets/mumps-BT43cFF4.js +2 -0
- data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
- data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
- data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
- data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
- data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
- data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
- data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
- data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
- data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
- data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
- data/web/dist/assets/oz-BzwKVEFT.js +2 -0
- data/web/dist/assets/pascal--L3eBynH.js +2 -0
- data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
- data/web/dist/assets/pig-CevX1Tat.js +2 -0
- data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
- data/web/dist/assets/properties-C78fOPTZ.js +2 -0
- data/web/dist/assets/protobuf-ChK-085T.js +2 -0
- data/web/dist/assets/pug-DeIclll2.js +2 -0
- data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
- data/web/dist/assets/python-BuPzkPfP.js +2 -0
- data/web/dist/assets/q-pXgVlZs6.js +2 -0
- data/web/dist/assets/r-B6wPVr8A.js +2 -0
- data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
- data/web/dist/assets/ruby-BK5aLJev.js +2 -0
- data/web/dist/assets/sas-B4kiWyti.js +2 -0
- data/web/dist/assets/scheme-C41bIUwD.js +2 -0
- data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
- data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
- data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
- data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
- data/web/dist/assets/solr-DehyRSwq.js +2 -0
- data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
- data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
- data/web/dist/assets/sql-D0XecflT.js +2 -0
- data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
- data/web/dist/assets/stylus-B533Al4x.js +2 -0
- data/web/dist/assets/swift-BzpIVaGY.js +2 -0
- data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
- data/web/dist/assets/textile-CnDTJFAw.js +2 -0
- data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- data/web/dist/assets/tiki-DGYXhP31.js +2 -0
- data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
- data/web/dist/assets/troff-wAsdV37c.js +2 -0
- data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
- data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
- data/web/dist/assets/vb-CmGdzxic.js +2 -0
- data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
- data/web/dist/assets/velocity-D8B20fx6.js +2 -0
- data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
- data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
- data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
- data/web/dist/assets/xquery-DzFWVndE.js +2 -0
- data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
- data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
- data/web/dist/index.html +14 -0
- data/web/dist/vite.svg +1 -0
- data/web/src/api/fixtures/board.json +512 -0
- metadata +907 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
# One line of `run --log FILE`, laid out as fixed-width columns.
|
|
6
|
+
#
|
|
7
|
+
# The log file is the only record of a headless run, and it is shared: two
|
|
8
|
+
# `openlanes run` invocations pointed at the same `--log` both append to
|
|
9
|
+
# it, interleaved, and so does every round within one run. That is what the
|
|
10
|
+
# process id column is for. Without it a reader cannot tell one run's agents
|
|
11
|
+
# from another's, and "started 003.00" twice looks like a bug rather than two
|
|
12
|
+
# machines doing the same work.
|
|
13
|
+
#
|
|
14
|
+
# Fixed columns are what make the file readable by eye and by `awk` alike. A
|
|
15
|
+
# value wider than its column is cut rather than allowed to push the columns
|
|
16
|
+
# out, and a value that is missing still occupies its width, so a round header
|
|
17
|
+
# with no plan or agent of its own lines up with the lines underneath it.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# ProgressLog.render("editing spec.md", plan: "003.00", status: "⭐️ Planned",
|
|
21
|
+
# agent: "yoda-writer", seconds: 42, round: "01", pid: 91_234)
|
|
22
|
+
|
|
23
|
+
# #=> "[16:22:14 | 003.00 | ⭐️ Planned | yoda-writer | 01 | 91234 | 42s] editing spec.md"
|
|
24
|
+
module ProgressLog
|
|
25
|
+
# Wall clock only. A log line answers "when", and the date is the file's.
|
|
26
|
+
TIME_FORMAT = "%H:%M:%S"
|
|
27
|
+
|
|
28
|
+
# Width of {TIME_FORMAT}'s output.
|
|
29
|
+
TIME_WIDTH = 8
|
|
30
|
+
|
|
31
|
+
# "003.00".
|
|
32
|
+
PLAN_WIDTH = 6
|
|
33
|
+
|
|
34
|
+
# Derived from the widest state as {Status#to_s} renders it, emoji
|
|
35
|
+
# included, so a state added to {STATUSES} with a longer label cannot
|
|
36
|
+
# silently knock the columns after it out of line — then trimmed by five
|
|
37
|
+
# cells: the emoji already names the state, so the longest labels can
|
|
38
|
+
# afford to lose their tails to keep the line short.
|
|
39
|
+
STATUS_WIDTH = Plans::STATUSES.map { |status| UI.display_width(status.to_s) }.max - 5
|
|
40
|
+
|
|
41
|
+
# The longest specialist name in `agents/` is `palpatine-planner` at 17,
|
|
42
|
+
# and the names are hyphenated words rather than a bounded vocabulary, so
|
|
43
|
+
# this leaves room for one more without a reflow.
|
|
44
|
+
AGENT_WIDTH = 20
|
|
45
|
+
|
|
46
|
+
# Enough for a 32-bit process id, right justified.
|
|
47
|
+
PID_WIDTH = 7
|
|
48
|
+
|
|
49
|
+
# "01". Rounds cap at two digits; a run that reaches a third has bigger
|
|
50
|
+
# problems than this column.
|
|
51
|
+
ROUND_WIDTH = 2
|
|
52
|
+
|
|
53
|
+
# " 907s". Anything longer than four digits of seconds is an agent nobody
|
|
54
|
+
# is still waiting on.
|
|
55
|
+
SECONDS_WIDTH = 6
|
|
56
|
+
|
|
57
|
+
# Between columns, inside the brackets.
|
|
58
|
+
SEPARATOR = " | "
|
|
59
|
+
|
|
60
|
+
# Cells from the opening bracket to the closing one, separators included.
|
|
61
|
+
# The message starts one space after this on every line, whatever fields
|
|
62
|
+
# that line happens to be missing, so a reader indenting a wrapped message
|
|
63
|
+
# has a number to indent by.
|
|
64
|
+
COLUMN_WIDTHS = [TIME_WIDTH, PLAN_WIDTH, STATUS_WIDTH, AGENT_WIDTH, ROUND_WIDTH, PID_WIDTH,
|
|
65
|
+
SECONDS_WIDTH].freeze
|
|
66
|
+
|
|
67
|
+
# @see COLUMN_WIDTHS
|
|
68
|
+
LINE_WIDTH = COLUMN_WIDTHS.sum + (SEPARATOR.length * (COLUMN_WIDTHS.size - 1)) + 2
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# Render one log line: bracketed fixed-width columns, then the message.
|
|
72
|
+
#
|
|
73
|
+
# No colour. The line goes to a file, where an escape sequence is neither
|
|
74
|
+
# readable nor the width it claims to be.
|
|
75
|
+
#
|
|
76
|
+
# @param message [String] the free-form tail, the only variable-width part
|
|
77
|
+
# @param plan [String, nil] the plan's ordinal, e.g. "003.00"
|
|
78
|
+
# @param status [String, nil] emoji and label, e.g. "⭐️ Planned"
|
|
79
|
+
# @param agent [String, nil] the specialist's name, e.g. "yoda-writer"
|
|
80
|
+
# @param seconds [Numeric, nil] how long this agent has been alive
|
|
81
|
+
# @param round [String, nil] which pass over the tree, e.g. "01"
|
|
82
|
+
# @param pid [Integer, nil] which run wrote the line
|
|
83
|
+
# @param at [Time] injectable, so a caller can render a fixed clock
|
|
84
|
+
# @return [String] one line, with no trailing newline
|
|
85
|
+
def render(message, plan: nil, status: nil, agent: nil, seconds: nil,
|
|
86
|
+
round: nil, pid: Process.pid, at: Time.now)
|
|
87
|
+
columns = [
|
|
88
|
+
left(at.strftime(TIME_FORMAT), TIME_WIDTH),
|
|
89
|
+
left(plan, PLAN_WIDTH),
|
|
90
|
+
left(status, STATUS_WIDTH),
|
|
91
|
+
left(agent, AGENT_WIDTH),
|
|
92
|
+
right(round, ROUND_WIDTH),
|
|
93
|
+
right(pid, PID_WIDTH),
|
|
94
|
+
right(duration(seconds), SECONDS_WIDTH)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
text = message.to_s
|
|
98
|
+
prefix = "[#{columns.join(SEPARATOR)}]"
|
|
99
|
+
text.empty? ? prefix : "#{prefix} #{text}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param seconds [Numeric, nil]
|
|
103
|
+
# @return [String] whole seconds with a trailing "s", or blank for nil
|
|
104
|
+
def duration(seconds) = seconds.nil? ? "" : "#{seconds.round}s"
|
|
105
|
+
|
|
106
|
+
# @param value [Object, nil]
|
|
107
|
+
# @param width [Integer] terminal cells
|
|
108
|
+
# @return [String] padded on the right to exactly +width+ cells
|
|
109
|
+
def left(value, width) = UI.fit(value, width)
|
|
110
|
+
|
|
111
|
+
# {UI.fit} pads on the right, which is the wrong end for a number. Fit
|
|
112
|
+
# first so an over-long value is still truncated to the column, then move
|
|
113
|
+
# the padding to the front.
|
|
114
|
+
#
|
|
115
|
+
# @param value [Object, nil]
|
|
116
|
+
# @param width [Integer] terminal cells
|
|
117
|
+
# @return [String] padded on the left to exactly +width+ cells
|
|
118
|
+
def right(value, width)
|
|
119
|
+
text = UI.fit(value, width).rstrip
|
|
120
|
+
UI.fit("", width - UI.display_width(text)) + text
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
# Drives specialist agents over a `.plans` tree until nothing is left to
|
|
6
|
+
# dispatch.
|
|
7
|
+
#
|
|
8
|
+
# The loop itself is {Dispatcher}. This holds what the loop needs about
|
|
9
|
+
# the run: the tree and its scope, the roster, the executor, how many run
|
|
10
|
+
# at once, where each plan is checked out, and how a finished plan is
|
|
11
|
+
# published. Blocked plans (⭕️ 🅱️) are stepped around, never assigned.
|
|
12
|
+
class Runner
|
|
13
|
+
# What one agent did to one plan.
|
|
14
|
+
Attempt = Data.define(:ordinal,
|
|
15
|
+
:agent,
|
|
16
|
+
:from,
|
|
17
|
+
:to,
|
|
18
|
+
:ok,
|
|
19
|
+
:note,
|
|
20
|
+
:up,
|
|
21
|
+
:down,
|
|
22
|
+
:subagents,
|
|
23
|
+
:delegated,
|
|
24
|
+
:seconds,
|
|
25
|
+
:round,
|
|
26
|
+
:file,
|
|
27
|
+
:model,
|
|
28
|
+
:status) do
|
|
29
|
+
def initialize(round: 1, file: "", model: nil, status: nil, **rest) = super
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] whether the plan actually moved
|
|
32
|
+
def advanced? = ok && from != to
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# One unit of work: an agent, a plan, and the checkout it happens in.
|
|
36
|
+
Task = Data.define(:agent, :subject, :root, :checkout, :round) do
|
|
37
|
+
# @return [Hash] columns for the log
|
|
38
|
+
def log_fields
|
|
39
|
+
{ plan: subject.feature.ordinal.to_s, status: subject.status.to_s,
|
|
40
|
+
agent: agent.name, round: format("%02d", round) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param rounds [Integer, nil] `--rounds`, capping each agent's own
|
|
45
|
+
# @param state [OpenLanes::Engine::StateFile, nil]
|
|
46
|
+
# @param sleeper [Proc]
|
|
47
|
+
# @param on_board [Proc, nil]
|
|
48
|
+
# @param claims [OpenLanes::Live::Claims, nil] plan claims shared with
|
|
49
|
+
# other machines; nil when the run is not live, and every plan is ours
|
|
50
|
+
# @param stay [Boolean] idle past fixed points until a person stops
|
|
51
|
+
# the run; see {Dispatcher#done?}
|
|
52
|
+
# @param rescan [Integer] seconds between rescans while idle
|
|
53
|
+
def initialize(tree:, executor:, agents: Agents::Registry.new, isolation: :shared, jobs: 1, worktree: nil,
|
|
54
|
+
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)
|
|
56
|
+
@tree = tree
|
|
57
|
+
@executor = executor
|
|
58
|
+
@agents = agents
|
|
59
|
+
@isolation = isolation
|
|
60
|
+
@worktree = worktree
|
|
61
|
+
@plans = plans
|
|
62
|
+
@publisher = publisher
|
|
63
|
+
@dry_run = dry_run
|
|
64
|
+
@rounds = rounds
|
|
65
|
+
@state = dry_run ? nil : state
|
|
66
|
+
@sleeper = sleeper
|
|
67
|
+
@on_board = on_board
|
|
68
|
+
@claims = claims
|
|
69
|
+
@stay = stay
|
|
70
|
+
@rescan = rescan
|
|
71
|
+
@attempts = []
|
|
72
|
+
@dropped = []
|
|
73
|
+
@jobs = isolated? ? jobs : 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attr_reader :jobs, :worktree, :tree, :attempts, :executor, :agents
|
|
77
|
+
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def isolated? = @isolation == :worktree
|
|
80
|
+
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def dry_run? = @dry_run
|
|
83
|
+
|
|
84
|
+
# Whether a finished build is pushed and its pull request opened. Not
|
|
85
|
+
# with `--isolation shared` or `--no-git-push`, the local-only runs, where
|
|
86
|
+
# a plan can never hold the open pull request 🟢 requires.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def publishes? = !@publisher.nil?
|
|
90
|
+
|
|
91
|
+
# @return [String] the repository root
|
|
92
|
+
def root = isolated? ? worktree.root : shared_root
|
|
93
|
+
|
|
94
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def in_scope?(subject)
|
|
97
|
+
ordinal = subject.feature.ordinal
|
|
98
|
+
!@dropped.include?(ordinal.to_s) && (@plans.nil? || @plans.include?(ordinal))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Take a plan out of this run, as when another machine now holds it.
|
|
102
|
+
#
|
|
103
|
+
# @param ordinal [#to_s]
|
|
104
|
+
# @return [void]
|
|
105
|
+
def drop_plan(ordinal) = @dropped |= [ordinal.to_s]
|
|
106
|
+
|
|
107
|
+
# @return [Array<OpenLanes::Plans::Subject>]
|
|
108
|
+
def in_scope = tree.subjects.select { |s| in_scope?(s) }
|
|
109
|
+
|
|
110
|
+
# Widen a run scoped by `--plan` to one more plan, from the page's
|
|
111
|
+
# "Choose an existing plan". An unscoped run already has every plan.
|
|
112
|
+
#
|
|
113
|
+
# @param ordinal [OpenLanes::Plans::Ordinal]
|
|
114
|
+
# @return [Boolean] whether the scope grew
|
|
115
|
+
def add_plan(ordinal)
|
|
116
|
+
@dropped.delete(ordinal.to_s)
|
|
117
|
+
return false if @plans.nil? || @plans.include?(ordinal)
|
|
118
|
+
|
|
119
|
+
@plans << ordinal
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Whether another machine holds the plan (W26). Such a plan is stepped
|
|
124
|
+
# around, as a blocked one is.
|
|
125
|
+
#
|
|
126
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def foreign?(subject)
|
|
129
|
+
!@claims.nil? && @claims.foreign?(subject.feature.ordinal)
|
|
130
|
+
rescue Live::Unavailable => e
|
|
131
|
+
claims_unavailable(e)
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Take the plan before an agent starts on it. With Redis gone the run
|
|
136
|
+
# carries on unclaimed rather than stalling: the claim is an agreement
|
|
137
|
+
# between machines, and a machine that cannot reach the others is
|
|
138
|
+
# working alone.
|
|
139
|
+
#
|
|
140
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
141
|
+
# @return [Boolean] whether this machine may work it
|
|
142
|
+
def claim(subject)
|
|
143
|
+
@claims.nil? || @claims.claim(subject.feature.ordinal)
|
|
144
|
+
rescue Live::Unavailable => e
|
|
145
|
+
claims_unavailable(e)
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Run until nothing is dispatchable, or, staying, until a person stops
|
|
150
|
+
# it.
|
|
151
|
+
#
|
|
152
|
+
# @yieldparam dispatcher [OpenLanes::Engine::Dispatcher] before it runs, so a
|
|
153
|
+
# console can attach itself and reach the running jobs
|
|
154
|
+
# @return [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
155
|
+
def call
|
|
156
|
+
dispatcher = Dispatcher.new(runner: self,
|
|
157
|
+
state: @state,
|
|
158
|
+
rounds: @rounds,
|
|
159
|
+
sleeper: @sleeper,
|
|
160
|
+
on_board: @on_board,
|
|
161
|
+
stay: @stay,
|
|
162
|
+
rescan: @rescan)
|
|
163
|
+
yield dispatcher if block_given?
|
|
164
|
+
@attempts = dispatcher.run
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [Array<OpenLanes::Plans::Subject>]
|
|
168
|
+
def blocked = in_scope.select { |s| %i[blocked product_blocked].include?(s.status.key) }
|
|
169
|
+
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
def settled? = in_scope.all? { |s| Plans::StateMachine::SETTLED.include?(s.status.key) }
|
|
172
|
+
|
|
173
|
+
# A dry run gets the shared root whatever the isolation says: nothing
|
|
174
|
+
# runs in the checkout, and `git worktree add` is a side effect a
|
|
175
|
+
# preview must not have. It also fails outright when the plan's branch
|
|
176
|
+
# is the one checked out where the run was typed, which is where a dry
|
|
177
|
+
# run of that plan is typed.
|
|
178
|
+
#
|
|
179
|
+
# @return [OpenLanes::Engine::Runner::Task]
|
|
180
|
+
def prepare(agent, subject, round)
|
|
181
|
+
return Task.new(agent:, subject:, root: shared_root, checkout: nil, round:) if !isolated? || dry_run?
|
|
182
|
+
|
|
183
|
+
checkout = worktree.checkout_for(subject.feature)
|
|
184
|
+
Task.new(agent:, subject:, root: checkout.path, checkout:, round:)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Push a finished build and record the pull request it opened in the
|
|
188
|
+
# plan's `pull-requests.md`, the row 🟢 is judged by. A push to a pull
|
|
189
|
+
# request the plan already had records nothing: the row is there, and a
|
|
190
|
+
# second copy of it would read as a second pull request.
|
|
191
|
+
#
|
|
192
|
+
# @param task [OpenLanes::Engine::Runner::Task]
|
|
193
|
+
# @param subject [OpenLanes::Plans::Subject] the plan in the main tree
|
|
194
|
+
# @return [OpenLanes::Vcs::Publisher::Publication, nil] nil when the run
|
|
195
|
+
# publishes nothing or the checkout holds no change
|
|
196
|
+
def publish(task, subject)
|
|
197
|
+
return nil unless @publisher && task.checkout&.dirty?
|
|
198
|
+
|
|
199
|
+
publication = @publisher.publish(checkout: task.checkout, subject:)
|
|
200
|
+
record_pull_request(subject.feature.path, publication) if publication.published? && !publication.updated
|
|
201
|
+
publication
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
# @return [String]
|
|
207
|
+
def shared_root = File.dirname(tree.dir)
|
|
208
|
+
|
|
209
|
+
# @param error [OpenLanes::Live::Unavailable]
|
|
210
|
+
# @return [void]
|
|
211
|
+
def claims_unavailable(error) = UI.log("claims unchecked: #{error.message}")
|
|
212
|
+
|
|
213
|
+
# @return [void]
|
|
214
|
+
def record_pull_request(path, publication)
|
|
215
|
+
number = publication.url.to_s[%r{/pull/(\d+)}, 1]
|
|
216
|
+
rows = Plans::PullRequests.new(dir: path).all.map { |pr|
|
|
217
|
+
{ number: pr.number, title: pr.title, url: pr.url, state: pr.state, body: "" }
|
|
218
|
+
}
|
|
219
|
+
rows << { number:, title: publication.title, url: publication.url, state: "Open 🟡", body: "" }
|
|
220
|
+
Plans::PullRequests.upsert(File.join(path, Plans::PullRequests::FILENAME), rows)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module Engine
|
|
8
|
+
# Where a run stands, written every second, so a harness that dies leaves
|
|
9
|
+
# the next one something to restart from.
|
|
10
|
+
#
|
|
11
|
+
# The ledger inside each document is the record humans read and the
|
|
12
|
+
# dispatcher acts on; this is the index of it, plus what the ledger cannot
|
|
13
|
+
# hold: which run wrote what, process ids, tokens, and whether the run
|
|
14
|
+
# that wrote a `Started` is still alive. It sits in `.plans/` rather than
|
|
15
|
+
# the system temp dir because a reboot must not lose it, and it is
|
|
16
|
+
# gitignored because it is about one machine's run, not the plan.
|
|
17
|
+
class StateFile
|
|
18
|
+
FILENAME = "openlanes-state.json"
|
|
19
|
+
|
|
20
|
+
# Where it used to live, one directory down. Read by {#load} when the
|
|
21
|
+
# current path holds nothing, so a run started under the old layout is
|
|
22
|
+
# picked up rather than silently restarted from zero. Nothing writes
|
|
23
|
+
# here any more and nothing deletes it: the file left behind is the
|
|
24
|
+
# only record a half-finished run has.
|
|
25
|
+
LEGACY_DIRNAME = "tmp"
|
|
26
|
+
|
|
27
|
+
# The lines {.ensure_ignored!} adds. The second covers the sibling
|
|
28
|
+
# {#save} writes and renames over the first — brief, but `.plans/` is a
|
|
29
|
+
# directory whose other contents are committed, so a `git status` run
|
|
30
|
+
# during that instant must not offer it.
|
|
31
|
+
IGNORE = ["#{OpenLanes::PLANS_DIR}/#{FILENAME}", "#{OpenLanes::PLANS_DIR}/#{FILENAME}.*.tmp"].freeze
|
|
32
|
+
|
|
33
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
34
|
+
# @return [String]
|
|
35
|
+
def self.for(tree) = File.join(tree.dir, FILENAME)
|
|
36
|
+
|
|
37
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
38
|
+
# @return [String] where {LEGACY_DIRNAME} put it
|
|
39
|
+
def self.legacy_for(tree) = File.join(tree.dir, LEGACY_DIRNAME, FILENAME)
|
|
40
|
+
|
|
41
|
+
# Add the ignore lines once. Editing somebody's `.gitignore` is a thing
|
|
42
|
+
# to announce, which is why this returns whether it did.
|
|
43
|
+
#
|
|
44
|
+
# Only the first line is asked about: git answers for a literal path,
|
|
45
|
+
# and the second is a glob no path equals. They are written together,
|
|
46
|
+
# so one being ignored means both are.
|
|
47
|
+
#
|
|
48
|
+
# @param root [String] repository root
|
|
49
|
+
# @return [Boolean] true when lines were added
|
|
50
|
+
def self.ensure_ignored!(root)
|
|
51
|
+
return false unless system("git", "-C", root, "rev-parse", "--git-dir", out: File::NULL, err: File::NULL)
|
|
52
|
+
return false if system("git", "-C", root, "check-ignore", "-q", IGNORE.first, out: File::NULL, err: File::NULL)
|
|
53
|
+
|
|
54
|
+
path = File.join(root, ".gitignore")
|
|
55
|
+
existing = File.file?(path) ? File.read(path) : ""
|
|
56
|
+
glue = existing.empty? || existing.end_with?("\n") ? "" : "\n"
|
|
57
|
+
File.write(path, "#{existing}#{glue}#{IGNORE.join("\n")}\n")
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param path [String]
|
|
62
|
+
# @param pid [Integer] this run's process id
|
|
63
|
+
def initialize(path:, pid: Process.pid)
|
|
64
|
+
@path = path
|
|
65
|
+
@pid = pid
|
|
66
|
+
@data = { "run" => {}, "plans" => {} }
|
|
67
|
+
@mutex = Mutex.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [String]
|
|
71
|
+
attr_reader :path
|
|
72
|
+
|
|
73
|
+
# Where a run started before the file moved up out of {LEGACY_DIRNAME}
|
|
74
|
+
# would have written. {#load} falls back to it; {#save} never uses it.
|
|
75
|
+
#
|
|
76
|
+
# @return [String]
|
|
77
|
+
def legacy_path = File.join(File.dirname(path), LEGACY_DIRNAME, File.basename(path))
|
|
78
|
+
|
|
79
|
+
# Reads the current path, or the legacy one when the current path holds
|
|
80
|
+
# nothing. A run interrupted under the old layout is otherwise invisible
|
|
81
|
+
# to the run that picks it up, and {#stranded} would report no stranded
|
|
82
|
+
# stages for a harness that died with several.
|
|
83
|
+
#
|
|
84
|
+
# @return [self]
|
|
85
|
+
def load
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
source = [path, legacy_path].find { |candidate| File.file?(candidate) }
|
|
88
|
+
@data = JSON.parse(File.read(source)) if source
|
|
89
|
+
@data = { "run" => {}, "plans" => {} } unless @data.is_a?(Hash) && @data["plans"].is_a?(Hash)
|
|
90
|
+
end
|
|
91
|
+
self
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Written whole to a sibling and renamed over, so a reader never sees
|
|
97
|
+
# half a file.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def save
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
103
|
+
temp = "#{path}.#{@pid}.tmp"
|
|
104
|
+
File.write(temp, JSON.pretty_generate(@data))
|
|
105
|
+
File.rename(temp, path)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param root [String]
|
|
110
|
+
# @return [void]
|
|
111
|
+
def begin_run!(root:)
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
@data["run"] = { "pid" => @pid, "root" => root, "started_at" => Time.now.iso8601,
|
|
114
|
+
"heartbeat_at" => Time.now.iso8601 }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return [void]
|
|
119
|
+
def heartbeat!
|
|
120
|
+
@mutex.synchronize { @data["run"]["heartbeat_at"] = Time.now.iso8601 }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param ordinal [String]
|
|
124
|
+
# @param agent [String]
|
|
125
|
+
# @param round [Integer]
|
|
126
|
+
# @param fields [Hash] anything else worth keeping: status, model, file,
|
|
127
|
+
# pid, up, down, state, next, exit
|
|
128
|
+
# @return [void]
|
|
129
|
+
def record(ordinal, agent:, round:, **fields)
|
|
130
|
+
@mutex.synchronize do
|
|
131
|
+
plan = (@data["plans"][ordinal.to_s] ||= { "stages" => [] })
|
|
132
|
+
stage = plan["stages"].find { |s| s["agent"] == agent && s["round"] == round }
|
|
133
|
+
unless stage
|
|
134
|
+
stage = { "agent" => agent, "round" => round, "run_pid" => @pid }
|
|
135
|
+
plan["stages"] << stage
|
|
136
|
+
end
|
|
137
|
+
fields.each { |key, value| stage[key.to_s] = value }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The live run's record (W6): what a person said about the run that no
|
|
142
|
+
# plan folder holds, kept here as the copy of Redis that survives Redis
|
|
143
|
+
# losing it. {Live::RunRecord} writes it and reads it back on start;
|
|
144
|
+
# it carries its own `seq`, so the newer of the two copies wins.
|
|
145
|
+
#
|
|
146
|
+
# @return [Hash, nil]
|
|
147
|
+
def live = @mutex.synchronize { @data["live"]&.dup }
|
|
148
|
+
|
|
149
|
+
# @param data [Hash] written by the next {#save}
|
|
150
|
+
# @return [void]
|
|
151
|
+
def live=(data)
|
|
152
|
+
@mutex.synchronize { @data["live"] = data }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @param ordinal [String]
|
|
156
|
+
# @return [Array<Hash>]
|
|
157
|
+
def stages(ordinal) = @mutex.synchronize { (@data.dig("plans", ordinal.to_s, "stages") || []).map(&:dup) }
|
|
158
|
+
|
|
159
|
+
# @return [Array<String>] every ordinal with a stage recorded
|
|
160
|
+
def plans = @mutex.synchronize { @data["plans"].keys }
|
|
161
|
+
|
|
162
|
+
# @return [Boolean] whether the run that last wrote this file is gone
|
|
163
|
+
def previous_run_dead?
|
|
164
|
+
previous = @data.dig("run", "pid")
|
|
165
|
+
return false if previous.nil? || previous == @pid
|
|
166
|
+
|
|
167
|
+
Process.kill(0, previous)
|
|
168
|
+
false
|
|
169
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Stages a dead run left `Started`. Each is an agent that was cut off
|
|
174
|
+
# mid-flight and has to be re-run, with the harness's own Interrupted
|
|
175
|
+
# line written so the document agrees with this file.
|
|
176
|
+
#
|
|
177
|
+
# @return [Array<Array(String, Hash)>] ordinal and stage
|
|
178
|
+
def stranded
|
|
179
|
+
return [] unless previous_run_dead?
|
|
180
|
+
|
|
181
|
+
@mutex.synchronize do
|
|
182
|
+
@data["plans"].flat_map { |ordinal, plan|
|
|
183
|
+
plan["stages"].select { |s| s["status"] == "Started" && s["run_pid"] != @pid }
|
|
184
|
+
.map { |s| [ordinal, s.dup] }
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|