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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Live
|
|
7
|
+
# Keeps the {RunState} past an engine restart (W6). Redis holds the live
|
|
8
|
+
# copy under {Keys#run}; `.plans/openlanes-state.json` holds the one a
|
|
9
|
+
# person can read without `redis-cli` and the one that survives Redis
|
|
10
|
+
# being flushed. Both carry the state's `seq`.
|
|
11
|
+
#
|
|
12
|
+
# On the run's first tick, after the dispatcher has loaded its state
|
|
13
|
+
# file, the newer copy is taken: normally Redis, the file when Redis came
|
|
14
|
+
# back empty or behind. Queued models go back to the executor, which is
|
|
15
|
+
# what applies them. From then on each tick writes whichever copy is
|
|
16
|
+
# behind, and {Commands} writes Redis in the same `MULTI` as a command's
|
|
17
|
+
# reply, so an applied command and its effect on the run state land
|
|
18
|
+
# together.
|
|
19
|
+
class RunRecord
|
|
20
|
+
# @param store [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
21
|
+
# @param keys [OpenLanes::Live::Keys]
|
|
22
|
+
# @param run_state [OpenLanes::Live::RunState]
|
|
23
|
+
def initialize(store:, keys:, run_state:)
|
|
24
|
+
@store = store
|
|
25
|
+
@keys = keys
|
|
26
|
+
@run_state = run_state
|
|
27
|
+
@restored = false
|
|
28
|
+
@in_redis = nil
|
|
29
|
+
@in_file = nil
|
|
30
|
+
@state_file = nil
|
|
31
|
+
@warned = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Proc] for {Engine::Dispatcher#before_tick}
|
|
35
|
+
def to_proc = method(:call).to_proc
|
|
36
|
+
|
|
37
|
+
# @param dispatcher [OpenLanes::Engine::Dispatcher]
|
|
38
|
+
# @return [void]
|
|
39
|
+
def call(dispatcher)
|
|
40
|
+
restore(dispatcher) unless @restored
|
|
41
|
+
flush
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Take back the newer copy, and hand its models to the executor.
|
|
45
|
+
#
|
|
46
|
+
# @param dispatcher [OpenLanes::Engine::Dispatcher]
|
|
47
|
+
# @return [void]
|
|
48
|
+
def restore(dispatcher)
|
|
49
|
+
@restored = true
|
|
50
|
+
@state_file = dispatcher.state_file
|
|
51
|
+
redis = read_redis
|
|
52
|
+
file = @state_file&.live
|
|
53
|
+
newer = [redis, file].compact.max_by { it["seq"].to_i }
|
|
54
|
+
# A copy that is missing is behind, unless both are: then there is
|
|
55
|
+
# nothing to keep until a person changes something.
|
|
56
|
+
@in_redis = redis ? redis["seq"].to_i : (newer && -1) || 0
|
|
57
|
+
@in_file = file ? file["seq"].to_i : (newer && -1) || 0
|
|
58
|
+
return unless newer
|
|
59
|
+
|
|
60
|
+
@run_state.restore(newer)
|
|
61
|
+
@run_state.models.each do |key, model|
|
|
62
|
+
ordinal, agent = key.split("/", 2)
|
|
63
|
+
dispatcher.prefer_model(ordinal, agent, model)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Bring both copies up to the state's `seq`. The file is written by the
|
|
68
|
+
# dispatcher's own save at the end of the tick, and by {#close}.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def flush
|
|
72
|
+
seq = @run_state.seq
|
|
73
|
+
@store.multi { |tx| write(tx) } unless @in_redis == seq
|
|
74
|
+
mirror
|
|
75
|
+
rescue Unavailable => e
|
|
76
|
+
unavailable(e)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The Redis half, inside someone else's `MULTI`.
|
|
80
|
+
#
|
|
81
|
+
# @param transaction [#hset]
|
|
82
|
+
# @return [void]
|
|
83
|
+
def write(transaction)
|
|
84
|
+
data = @run_state.to_h
|
|
85
|
+
transaction.hset(@keys.run, "seq", data["seq"].to_s)
|
|
86
|
+
transaction.hset(@keys.run, "json", JSON.generate(data))
|
|
87
|
+
@in_redis = data["seq"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The run is over: the file gets the last word, saved now rather than
|
|
91
|
+
# at a tick that is not coming.
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
def close
|
|
95
|
+
flush
|
|
96
|
+
@state_file&.save
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# @return [void]
|
|
102
|
+
def mirror
|
|
103
|
+
return unless @state_file && @in_file != @run_state.seq
|
|
104
|
+
|
|
105
|
+
data = @run_state.to_h
|
|
106
|
+
@state_file.live = data
|
|
107
|
+
@in_file = data["seq"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Hash, nil]
|
|
111
|
+
def read_redis
|
|
112
|
+
seq, json = @store.hgetall(@keys.run).values_at("seq", "json")
|
|
113
|
+
return nil unless json
|
|
114
|
+
|
|
115
|
+
JSON.parse(json).merge("seq" => seq.to_i)
|
|
116
|
+
rescue JSON::ParserError
|
|
117
|
+
nil
|
|
118
|
+
rescue Unavailable => e
|
|
119
|
+
unavailable(e)
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param error [OpenLanes::Live::Unavailable]
|
|
124
|
+
# @return [void]
|
|
125
|
+
def unavailable(error)
|
|
126
|
+
return if @warned
|
|
127
|
+
|
|
128
|
+
@warned = true
|
|
129
|
+
UI.log("run state not kept in Redis: #{error.message}")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Live
|
|
5
|
+
# What a person has said about the run that the plan folders do not
|
|
6
|
+
# record: the questions they are holding, the agents they asked to hold,
|
|
7
|
+
# and the models chosen for agents' next rounds.
|
|
8
|
+
#
|
|
9
|
+
# Held in the engine, and kept by {RunRecord} in Redis and in
|
|
10
|
+
# `.plans/openlanes-state.json` (W6), so an engine that restarts still
|
|
11
|
+
# knows them. Every change moves {#seq} on by one, which is how the
|
|
12
|
+
# record tells which of its two copies is newer. A model choice also
|
|
13
|
+
# lives in the executor, which is what applies it; this copy is what the
|
|
14
|
+
# page is shown and what a restart hands back to the executor.
|
|
15
|
+
class RunState
|
|
16
|
+
def initialize
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@held_questions = []
|
|
19
|
+
@held_cars = []
|
|
20
|
+
@models = {}
|
|
21
|
+
@seq = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param id [String] `"<ordinal>/B<n>"`
|
|
25
|
+
# @param held [Boolean]
|
|
26
|
+
# @return [void]
|
|
27
|
+
def hold_question(id, held: true) = toggle(@held_questions, id, held)
|
|
28
|
+
|
|
29
|
+
# @param id [String]
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def question_held?(id) = @mutex.synchronize { @held_questions.include?(id) }
|
|
32
|
+
|
|
33
|
+
# @param key [String] `"<ordinal>/<agent>"`
|
|
34
|
+
# @param held [Boolean]
|
|
35
|
+
# @return [void]
|
|
36
|
+
def hold_car(key, held: true) = toggle(@held_cars, key, held)
|
|
37
|
+
|
|
38
|
+
# @param key [String]
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def car_held?(key) = @mutex.synchronize { @held_cars.include?(key) }
|
|
41
|
+
|
|
42
|
+
# @param key [String] `"<ordinal>/<agent>"`
|
|
43
|
+
# @param model [String, nil] nil forgets the choice
|
|
44
|
+
# @return [void]
|
|
45
|
+
def choose_model(key, model)
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
next if @models[key] == model
|
|
48
|
+
|
|
49
|
+
model ? @models[key] = model : @models.delete(key)
|
|
50
|
+
@seq += 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param key [String]
|
|
55
|
+
# @return [String, nil] the model queued for that agent's next round
|
|
56
|
+
def model_for(key) = @mutex.synchronize { @models[key] }
|
|
57
|
+
|
|
58
|
+
# @return [Hash{String => String}] `"<ordinal>/<agent>"` to model, a copy
|
|
59
|
+
def models = @mutex.synchronize { @models.dup }
|
|
60
|
+
|
|
61
|
+
# @return [Integer] changes made since the run's state was first recorded
|
|
62
|
+
def seq = @mutex.synchronize { @seq }
|
|
63
|
+
|
|
64
|
+
# As {RunRecord} keeps it.
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def to_h
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
{ "seq" => @seq, "held_questions" => @held_questions.dup, "held_cars" => @held_cars.dup,
|
|
70
|
+
"models" => @models.dup }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Take back what {#to_h} wrote, sequence number included.
|
|
75
|
+
#
|
|
76
|
+
# @param data [Hash]
|
|
77
|
+
# @return [self]
|
|
78
|
+
def restore(data)
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
@held_questions = Array(data["held_questions"]).map(&:to_s)
|
|
81
|
+
@held_cars = Array(data["held_cars"]).map(&:to_s)
|
|
82
|
+
@models = (data["models"].is_a?(Hash) ? data["models"] : {}).to_h { |k, v| [k.to_s, v.to_s] }
|
|
83
|
+
@seq = data["seq"].to_i
|
|
84
|
+
end
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# @return [void]
|
|
91
|
+
def toggle(list, item, on)
|
|
92
|
+
@mutex.synchronize do
|
|
93
|
+
next if list.include?(item) == on
|
|
94
|
+
|
|
95
|
+
on ? list.push(item) : list.delete(item)
|
|
96
|
+
@seq += 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Live
|
|
5
|
+
# One engine's live run: its node and keys, its claims, the publisher
|
|
6
|
+
# that writes its board and the reader that applies its commands, built
|
|
7
|
+
# once and handed to the runner.
|
|
8
|
+
#
|
|
9
|
+
# live = Live.session(store:, root:, tree:, agents:)
|
|
10
|
+
# runner = Engine.runner(..., on_board: live.on_board, claims: live.claims)
|
|
11
|
+
# runner.call { |dispatcher| live.attach(dispatcher) }
|
|
12
|
+
# live.close
|
|
13
|
+
class Session
|
|
14
|
+
# @param store [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
15
|
+
# @param root [String] the checkout
|
|
16
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
17
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
18
|
+
# @param project [String, nil] the header's name (W18); the root's name by default
|
|
19
|
+
# @param node [OpenLanes::Live::Node]
|
|
20
|
+
# @param repo [String]
|
|
21
|
+
# @param clock [Proc] wall time
|
|
22
|
+
def initialize(store:, root:, tree:, agents:, project: nil, node: Node.current(root:), repo: Repo.for(root),
|
|
23
|
+
clock: -> { Time.now })
|
|
24
|
+
@store = store
|
|
25
|
+
@node = node
|
|
26
|
+
@keys = Keys.new(repo:, node: node.name)
|
|
27
|
+
@run_state = RunState.new
|
|
28
|
+
@record = RunRecord.new(store:, keys: @keys, run_state: @run_state)
|
|
29
|
+
@claims = Claims.new(store:, keys: @keys)
|
|
30
|
+
@publisher = Publisher.new(store:,
|
|
31
|
+
keys: @keys,
|
|
32
|
+
node:,
|
|
33
|
+
tree:,
|
|
34
|
+
agents:,
|
|
35
|
+
claims: @claims,
|
|
36
|
+
run_state: @run_state,
|
|
37
|
+
project: project || File.basename(root),
|
|
38
|
+
clock:)
|
|
39
|
+
@commands = Commands.new(store:,
|
|
40
|
+
keys: @keys,
|
|
41
|
+
node:,
|
|
42
|
+
run_state: @run_state,
|
|
43
|
+
record: @record,
|
|
44
|
+
claims: @claims,
|
|
45
|
+
clock:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
49
|
+
attr_reader :store
|
|
50
|
+
|
|
51
|
+
# @return [OpenLanes::Live::Node]
|
|
52
|
+
attr_reader :node
|
|
53
|
+
|
|
54
|
+
# @return [OpenLanes::Live::Keys]
|
|
55
|
+
attr_reader :keys
|
|
56
|
+
|
|
57
|
+
# @return [OpenLanes::Live::Claims]
|
|
58
|
+
attr_reader :claims
|
|
59
|
+
|
|
60
|
+
# @return [OpenLanes::Live::Publisher]
|
|
61
|
+
attr_reader :publisher
|
|
62
|
+
|
|
63
|
+
# @return [OpenLanes::Live::Commands]
|
|
64
|
+
attr_reader :commands
|
|
65
|
+
|
|
66
|
+
# @return [OpenLanes::Live::RunState]
|
|
67
|
+
attr_reader :run_state
|
|
68
|
+
|
|
69
|
+
# @return [OpenLanes::Live::RunRecord]
|
|
70
|
+
attr_reader :record
|
|
71
|
+
|
|
72
|
+
# @return [Proc] the runner's `on_board:`
|
|
73
|
+
def on_board = @publisher.to_proc
|
|
74
|
+
|
|
75
|
+
# Wire the run in: the run state a previous engine left is taken back
|
|
76
|
+
# on the first tick, commands are applied at the top of each tick, and
|
|
77
|
+
# the log is heard from now on.
|
|
78
|
+
#
|
|
79
|
+
# @param dispatcher [OpenLanes::Engine::Dispatcher]
|
|
80
|
+
# @return [void]
|
|
81
|
+
def attach(dispatcher)
|
|
82
|
+
@publisher.listen
|
|
83
|
+
@publisher.on_lost { |ordinals| dispatcher.fence(ordinals) }
|
|
84
|
+
dispatcher.before_tick(&@record)
|
|
85
|
+
dispatcher.before_tick(&@commands)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The run is over: the run state is written down, claims go back, and
|
|
89
|
+
# this node's snapshot goes at once rather than when it expires, so
|
|
90
|
+
# other pages stop showing it.
|
|
91
|
+
#
|
|
92
|
+
# @return [void]
|
|
93
|
+
def close
|
|
94
|
+
@record.close
|
|
95
|
+
@publisher.close
|
|
96
|
+
@claims.release_all
|
|
97
|
+
@store.del(@keys.snapshot)
|
|
98
|
+
@store.hdel(@keys.nodes, @node.name)
|
|
99
|
+
rescue Unavailable
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [String] where, with the password masked, and as whom
|
|
104
|
+
def describe = "#{Live.mask(@store.url)} as #{@node.label} (#{@node.name})"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Live
|
|
5
|
+
# Everything the page shows about this node, as one hash in exactly the
|
|
6
|
+
# shape of `Snapshot` in `web/src/api/types.ts`.
|
|
7
|
+
#
|
|
8
|
+
# Built from what already says it: the dispatcher's {Engine::Board} for
|
|
9
|
+
# who is running and what they spent, the `.plans` tree for each plan's
|
|
10
|
+
# state, questions and pull requests, the registry for the stations, the
|
|
11
|
+
# adapter for the models and their prices. Nothing here is a second copy
|
|
12
|
+
# of any of them. `spec/openlanes/live/snapshot_spec.rb` holds the keys
|
|
13
|
+
# to the page's fixture, level by level, so a field renamed on one side
|
|
14
|
+
# fails the other's suite.
|
|
15
|
+
class Snapshot
|
|
16
|
+
# How an agent marks a question as answerable with Yes or No (W30), on
|
|
17
|
+
# its own line under the question's heading. The agents' prompt shows
|
|
18
|
+
# it, and {#questions} looks for it; neither spells it twice.
|
|
19
|
+
YES_NO_MARK = "answers: yes, no"
|
|
20
|
+
|
|
21
|
+
# {YES_NO_MARK}, read forgivingly: brackets, case and spacing vary.
|
|
22
|
+
YES_NO = /answers:\s*\[?\s*yes\s*,\s*no\s*\]?/i
|
|
23
|
+
|
|
24
|
+
# Any Markdown heading, which ends the body of the question above it.
|
|
25
|
+
HEADING = /^[ \t]{0,3}\#{1,6}[ \t]/
|
|
26
|
+
|
|
27
|
+
# Plan states finished with, kept off "Choose an existing plan".
|
|
28
|
+
FINISHED = %i[approved deployed discarded].freeze
|
|
29
|
+
|
|
30
|
+
# @param board [OpenLanes::Engine::Board]
|
|
31
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
32
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
33
|
+
# @param node [OpenLanes::Live::Node]
|
|
34
|
+
# @param repo [String]
|
|
35
|
+
# @param project [String]
|
|
36
|
+
# @param description [String, nil]
|
|
37
|
+
# @param seq [Integer]
|
|
38
|
+
# @param at [Time]
|
|
39
|
+
# @param started_at [Time, nil] the run's start, wall clock
|
|
40
|
+
# @param claims [OpenLanes::Live::Claims, nil]
|
|
41
|
+
# @param run_state [OpenLanes::Live::RunState]
|
|
42
|
+
# @param meter [OpenLanes::Live::Meter]
|
|
43
|
+
# @param log [Array<Hash>] `LogLine`s, newest last
|
|
44
|
+
# @param adapter [OpenLanes::Adapters::Base] whose models the page offers
|
|
45
|
+
def initialize(board:, tree:, agents:, node:, repo:, project:, seq:, description: nil, at: Time.now,
|
|
46
|
+
started_at: nil, claims: nil, run_state: RunState.new, meter: Meter.new, log: [],
|
|
47
|
+
adapter: Adapters.for(Adapters::DEFAULT))
|
|
48
|
+
@board = board
|
|
49
|
+
@tree = tree
|
|
50
|
+
@agents = agents
|
|
51
|
+
@node = node
|
|
52
|
+
@repo = repo
|
|
53
|
+
@project = project
|
|
54
|
+
@description = description
|
|
55
|
+
@seq = seq
|
|
56
|
+
@at = at
|
|
57
|
+
@started_at = started_at
|
|
58
|
+
@claims = claims
|
|
59
|
+
@run_state = run_state
|
|
60
|
+
@meter = meter
|
|
61
|
+
@log = log
|
|
62
|
+
@adapter = adapter
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
def to_h
|
|
67
|
+
subjects = @tree.reload.subjects
|
|
68
|
+
mine = ours(subjects)
|
|
69
|
+
{ seq: @seq,
|
|
70
|
+
at: iso(@at),
|
|
71
|
+
node: @node.wire(heartbeat_at: @at, me: true),
|
|
72
|
+
repo: @repo,
|
|
73
|
+
project: @project,
|
|
74
|
+
description: @description,
|
|
75
|
+
run: run(mine),
|
|
76
|
+
stations: Stations.all(@agents).map(&:wire),
|
|
77
|
+
plans: mine,
|
|
78
|
+
backlog: backlog(subjects, mine.map { it[:ordinal] }),
|
|
79
|
+
models: @adapter.models.map(&:wire),
|
|
80
|
+
autocompact: Adapters::AUTOCOMPACT / 100.0,
|
|
81
|
+
log: @log }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# ---- run --------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
# @param plans [Array<Hash>]
|
|
89
|
+
# @return [Hash]
|
|
90
|
+
def run(plans)
|
|
91
|
+
{ status: run_status,
|
|
92
|
+
started_at: @started_at && iso(@started_at),
|
|
93
|
+
up: @board.up.to_i,
|
|
94
|
+
down: @board.down.to_i,
|
|
95
|
+
cost: plans.sum { it[:cost] }.round(4),
|
|
96
|
+
agents_working: @board.running.to_i }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [String]
|
|
100
|
+
def run_status
|
|
101
|
+
return "idle" if @board.running.to_i.zero? && @board.rows.empty? && @board.status == :running
|
|
102
|
+
|
|
103
|
+
@board.status.to_s
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ---- plans ------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
# The plans this node is working on: the board's, and any it has
|
|
109
|
+
# claimed but not yet started.
|
|
110
|
+
#
|
|
111
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>]
|
|
112
|
+
# @return [Array<Hash>]
|
|
113
|
+
def ours(subjects)
|
|
114
|
+
ordinals = (@board.plans + Array(@claims&.held)).uniq
|
|
115
|
+
subjects.select { |s| ordinals.include?(s.feature.ordinal.to_s) }.map { |s| plan(s) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
119
|
+
# @return [Hash] `Plan`
|
|
120
|
+
def plan(subject)
|
|
121
|
+
ordinal = subject.feature.ordinal.to_s
|
|
122
|
+
state = Plans::PlanState.for(subject.feature.path)
|
|
123
|
+
stages = state.stages
|
|
124
|
+
rows = @board.rows.select { it.ordinal == ordinal }
|
|
125
|
+
spent = @meter.for(ordinal, method(:price))
|
|
126
|
+
started = @meter.started_at(ordinal)
|
|
127
|
+
{ ordinal:,
|
|
128
|
+
title: subject.feature.title,
|
|
129
|
+
slug: subject.feature.slug,
|
|
130
|
+
status: status(subject.status),
|
|
131
|
+
lane: Plans::Spec.for(subject).lane.to_s,
|
|
132
|
+
station: Stations.key_for(subject.status.key, before: stages.last&.fetch("from", nil)&.to_sym).to_s,
|
|
133
|
+
up: spent[:up],
|
|
134
|
+
down: spent[:down],
|
|
135
|
+
cost: spent[:cost],
|
|
136
|
+
started_at: started && iso(started),
|
|
137
|
+
elapsed: started ? (@at - started).round : 0,
|
|
138
|
+
cars: rows.map { car(it, subject, stages) },
|
|
139
|
+
pr: pull_request(subject),
|
|
140
|
+
questions: questions(subject, state),
|
|
141
|
+
interrupted: interrupted(stages, rows),
|
|
142
|
+
claimed_by: claimed_by(ordinal) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @param status [OpenLanes::Plans::Status]
|
|
146
|
+
# @return [Hash] `Status`
|
|
147
|
+
def status(status) = { key: status.key.to_s, emoji: status.emoji, label: status.label }
|
|
148
|
+
|
|
149
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>]
|
|
150
|
+
# @param taken [Array<String>] ordinals already on the board
|
|
151
|
+
# @return [Array<Hash>] plans a person could add to this run
|
|
152
|
+
def backlog(subjects, taken)
|
|
153
|
+
subjects.reject { |s| taken.include?(s.feature.ordinal.to_s) || FINISHED.include?(s.status.key) }
|
|
154
|
+
.reject { |s| foreign?(s.feature.ordinal.to_s) }
|
|
155
|
+
.map do |s|
|
|
156
|
+
{ ordinal: s.feature.ordinal.to_s, title: s.feature.title, status: status(s.status),
|
|
157
|
+
lane: Plans::Spec.for(s).lane.to_s }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# ---- cars -------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
# @param row [OpenLanes::Engine::Board::Row]
|
|
164
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
165
|
+
# @param stages [Array<Hash>]
|
|
166
|
+
# @return [Hash] `Car`
|
|
167
|
+
def car(row, subject, stages)
|
|
168
|
+
{ key: row.key,
|
|
169
|
+
agent: row.agent,
|
|
170
|
+
role: row.role.to_s,
|
|
171
|
+
round: row.round,
|
|
172
|
+
rounds: row.rounds,
|
|
173
|
+
model: model_id(row.model),
|
|
174
|
+
effort: stages.rfind { it["agent"] == row.agent }&.fetch("effort", nil),
|
|
175
|
+
state: car_state(row, subject),
|
|
176
|
+
phase: row.phase&.to_s,
|
|
177
|
+
message: row.message.to_s,
|
|
178
|
+
history: Array(row.history).first(Engine::Board::Row::HISTORY),
|
|
179
|
+
up: row.up.to_i,
|
|
180
|
+
down: row.down.to_i,
|
|
181
|
+
context: row.context,
|
|
182
|
+
context_limit: price(row.model)&.context,
|
|
183
|
+
elapsed: row.elapsed.to_i,
|
|
184
|
+
remaining: row.remaining,
|
|
185
|
+
held: @run_state.car_held?(row.key),
|
|
186
|
+
next_model: next_model(row) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The model a person queued for the agent's next round, until a round
|
|
190
|
+
# runs on it: from then on it is the model, not the next one.
|
|
191
|
+
#
|
|
192
|
+
# @param row [OpenLanes::Engine::Board::Row]
|
|
193
|
+
# @return [String, nil]
|
|
194
|
+
def next_model(row)
|
|
195
|
+
queued = @run_state.model_for(row.key)
|
|
196
|
+
queued unless queued == model_id(row.model)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# A running agent on a plan that has stopped for a question is stuck,
|
|
200
|
+
# not working: the page says so rather than spinning.
|
|
201
|
+
#
|
|
202
|
+
# @return [String]
|
|
203
|
+
def car_state(row, subject)
|
|
204
|
+
return "stuck" if row.running? && %i[blocked product_blocked].include?(subject.status.key)
|
|
205
|
+
|
|
206
|
+
row.state.to_s
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# @param label [String, nil] a {Agents::Profile#label}: `opus`, or `codex:gpt-5`
|
|
210
|
+
# @return [String] the full model id when the adapter knows it
|
|
211
|
+
def model_id(label)
|
|
212
|
+
name = label.to_s.split(":", 2).last
|
|
213
|
+
adapter_for(label)&.model_entry(name)&.id || name
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# The catalogue's entry for a model: its prices, and its context window.
|
|
217
|
+
#
|
|
218
|
+
# @param label [String, nil]
|
|
219
|
+
# @return [OpenLanes::Adapters::Model, nil]
|
|
220
|
+
def price(label) = adapter_for(label)&.model_entry(label.to_s.split(":", 2).last)
|
|
221
|
+
|
|
222
|
+
# @param label [String, nil]
|
|
223
|
+
# @return [OpenLanes::Adapters::Base, nil]
|
|
224
|
+
def adapter_for(label)
|
|
225
|
+
prefix = label.to_s.include?(":") ? label.to_s.split(":", 2).first : Adapters::DEFAULT
|
|
226
|
+
Adapters.known?(prefix) ? Adapters.for(prefix) : nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ---- pull request, interruption, claim --------------------------------
|
|
230
|
+
|
|
231
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
232
|
+
# @return [Hash, nil] `PullRequest`
|
|
233
|
+
def pull_request(subject)
|
|
234
|
+
pr = subject.pull_requests.find(&:open?) || subject.pull_requests.last or return nil
|
|
235
|
+
{ number: pr.number.to_s[/\d+/].to_i, url: pr.url, state: pr_state(pr, subject), ci: nil, merged_at: nil }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# @return [String]
|
|
239
|
+
def pr_state(pr, subject)
|
|
240
|
+
return "merged" if pr.merged?
|
|
241
|
+
return "closed" unless pr.open?
|
|
242
|
+
|
|
243
|
+
{ rejected: "rejected", approved: "approved" }.fetch(subject.status.key, "open")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The latest stage, when it is an interruption nobody has resumed.
|
|
247
|
+
#
|
|
248
|
+
# @param stages [Array<Hash>]
|
|
249
|
+
# @param rows [Array<OpenLanes::Engine::Board::Row>]
|
|
250
|
+
# @return [Hash, nil]
|
|
251
|
+
def interrupted(stages, rows)
|
|
252
|
+
last = stages.last
|
|
253
|
+
return nil unless last && last["status"] == "Interrupted" && rows.none? { it.agent == last["agent"] && it.running? }
|
|
254
|
+
|
|
255
|
+
{ agent: last["agent"], at: last["finished_at"] || last["started_at"], note: last["note"].to_s }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @param ordinal [String]
|
|
259
|
+
# @return [String, nil] the node id, without its checkout
|
|
260
|
+
def claimed_by(ordinal) = @claims&.held_by(ordinal)&.then { it.split(":").first }
|
|
261
|
+
|
|
262
|
+
# @param ordinal [String]
|
|
263
|
+
# @return [Boolean]
|
|
264
|
+
def foreign?(ordinal) = !@claims.nil? && @claims.foreign?(ordinal)
|
|
265
|
+
|
|
266
|
+
# ---- questions --------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
# The questions `blocked.md` still asks, each with its body, whether it
|
|
269
|
+
# is a yes/no question, and who asked.
|
|
270
|
+
#
|
|
271
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
272
|
+
# @param state [OpenLanes::Plans::PlanState]
|
|
273
|
+
# @return [Array<Hash>] `Question`
|
|
274
|
+
def questions(subject, state)
|
|
275
|
+
open = Lifecycle::Unblocker.questions(subject).reject(&:answered)
|
|
276
|
+
return [] if open.empty?
|
|
277
|
+
|
|
278
|
+
bodies = bodies(subject.read("blocked.md").to_s)
|
|
279
|
+
asker = state.signatures.rfind { it["status"] == "Blocked" }
|
|
280
|
+
asked_at = asker&.fetch("at", nil) || iso(File.mtime(File.join(subject.feature.path, "blocked.md")))
|
|
281
|
+
open.map { |q| question(subject, q, bodies.fetch(q.number, ""), asker, asked_at) }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# @return [Hash]
|
|
285
|
+
def question(subject, open, body, asker, asked_at)
|
|
286
|
+
id = "#{subject.feature.ordinal}/B#{open.number}"
|
|
287
|
+
yes_no = open.heading.match?(YES_NO) || body.match?(YES_NO)
|
|
288
|
+
{ id:,
|
|
289
|
+
number: open.number,
|
|
290
|
+
heading: open.heading.sub(YES_NO, "").strip,
|
|
291
|
+
body: body.lines.grep_v(/\A\s*#{YES_NO.source}\s*\z/i).join.strip,
|
|
292
|
+
agent: asker&.fetch("agent", nil),
|
|
293
|
+
owner: subject.status.key == :product_blocked ? "product" : "technical",
|
|
294
|
+
answers: yes_no ? %w[yes no] : nil,
|
|
295
|
+
held: @run_state.question_held?(id),
|
|
296
|
+
asked_at: }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# @param text [String] the whole of blocked.md
|
|
300
|
+
# @return [Hash{Integer => String}] each open question's number to the
|
|
301
|
+
# lines under its heading, up to the next heading
|
|
302
|
+
def bodies(text)
|
|
303
|
+
found = {}
|
|
304
|
+
current = nil
|
|
305
|
+
text.each_line do |line|
|
|
306
|
+
if (number = line[Plans::OPEN_BLOCK, 1])
|
|
307
|
+
current = found[number.to_i] = +""
|
|
308
|
+
elsif line.match?(HEADING) || line.match?(Plans::ANSWER_BLOCK)
|
|
309
|
+
current = nil
|
|
310
|
+
elsif current
|
|
311
|
+
current << line
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
found
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @param time [Time]
|
|
318
|
+
# @return [String]
|
|
319
|
+
def iso(time) = time.utc.iso8601
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|