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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Web
|
|
7
|
+
# One page's event stream (contract.md, `GET /api/events`): a `board`
|
|
8
|
+
# event whenever any node's snapshot `seq` changes, a `reply` event for
|
|
9
|
+
# each reply the engine writes, and a `: ping` comment so proxies and the
|
|
10
|
+
# browser keep the connection open.
|
|
11
|
+
#
|
|
12
|
+
# Polled rather than blocked on: a blocking read would hold the thread's
|
|
13
|
+
# connection for the length of the wait, and the board has to be looked
|
|
14
|
+
# at every tick anyway.
|
|
15
|
+
#
|
|
16
|
+
# A reply carries its stream id as the event's `id:`, so a browser that
|
|
17
|
+
# reconnects sends `Last-Event-ID` and gets what it missed. The stream
|
|
18
|
+
# opens with a `cursor` event carrying the id it starts from, so a
|
|
19
|
+
# browser that drops before the first reply still has one to send;
|
|
20
|
+
# without it, replies written during that gap were skipped.
|
|
21
|
+
class Events
|
|
22
|
+
# @param context [OpenLanes::Web::Context]
|
|
23
|
+
# @param store [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
24
|
+
# @param last_id [String, nil] the `Last-Event-ID` the browser sent
|
|
25
|
+
# @param clock [Proc] seconds, monotonic
|
|
26
|
+
# @param sleeper [Proc] waits the given seconds between ticks
|
|
27
|
+
def initialize(context:, store:, last_id: nil, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
28
|
+
sleeper: ->(seconds) { sleep seconds })
|
|
29
|
+
@context = context
|
|
30
|
+
@store = store
|
|
31
|
+
@keys = context.keys
|
|
32
|
+
@last_id = last_id.to_s.match?(/\A\d+-\d+\z/) ? last_id : nil
|
|
33
|
+
@clock = clock
|
|
34
|
+
@sleeper = sleeper
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write events to +out+ until the browser leaves, the server stops, or
|
|
38
|
+
# the context's lifetime runs out.
|
|
39
|
+
#
|
|
40
|
+
# @param out [#<<, #closed?]
|
|
41
|
+
# @return [void]
|
|
42
|
+
def run(out)
|
|
43
|
+
started = @clock.call
|
|
44
|
+
pinged = started
|
|
45
|
+
cursor = @last_id || newest_reply
|
|
46
|
+
out << self.class.frame("cursor", { id: cursor }, id: cursor)
|
|
47
|
+
seqs = nil
|
|
48
|
+
until over?(out, started)
|
|
49
|
+
seqs = board(out, seqs)
|
|
50
|
+
cursor = replies(out, cursor)
|
|
51
|
+
pinged = ping(out) if @clock.call - pinged >= @context.ping_every
|
|
52
|
+
@sleeper.call(@context.tick)
|
|
53
|
+
end
|
|
54
|
+
rescue Live::Unavailable => e
|
|
55
|
+
# Ending the stream is the right answer: the page reconnects with
|
|
56
|
+
# backoff and shows itself as reconnecting meanwhile.
|
|
57
|
+
out << ": #{e.message.tr("\n", " ")}\n\n"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param event [String]
|
|
61
|
+
# @param data [Object]
|
|
62
|
+
# @param id [String, nil]
|
|
63
|
+
# @return [String] one server-sent event
|
|
64
|
+
def self.frame(event, data, id: nil) = "#{"id: #{id}\n" if id}event: #{event}\ndata: #{JSON.generate(data)}\n\n"
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def over?(out, started)
|
|
70
|
+
out.closed? || @context.stopping.true? || (@context.lifetime && @clock.call - started >= @context.lifetime)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Array] the seqs now, after sending the board if they changed
|
|
74
|
+
def board(out, seqs)
|
|
75
|
+
board = Live.board(store: @store, keys: @keys)
|
|
76
|
+
now = [board[:me], *board[:others]].compact.map { [it.dig("node", "id"), it.dig("node", "checkout"), it["seq"]] }
|
|
77
|
+
out << self.class.frame("board", board) unless now == seqs
|
|
78
|
+
now
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [String] the last reply id sent
|
|
82
|
+
def replies(out, cursor)
|
|
83
|
+
Live.replies(store: @store, keys: @keys, after: cursor).each do |id, reply|
|
|
84
|
+
out << self.class.frame("reply", reply, id:)
|
|
85
|
+
cursor = id
|
|
86
|
+
end
|
|
87
|
+
cursor
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Float] when it pinged
|
|
91
|
+
def ping(out)
|
|
92
|
+
out << ": ping\n\n"
|
|
93
|
+
@clock.call
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Replies already written before this page connected are not news.
|
|
97
|
+
# An empty stream starts from `0-0` rather than `0`: the browser echoes
|
|
98
|
+
# it back as `Last-Event-ID`, and only a full stream id is accepted.
|
|
99
|
+
#
|
|
100
|
+
# @return [String]
|
|
101
|
+
def newest_reply = @store.xrevrange(@keys.replies, count: 1).first&.id || "0-0"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module Web
|
|
8
|
+
# The API server beside a `openlanes run` (W7): `openlanes web` as a process
|
|
9
|
+
# of its own, in its own process group, so a crash in one never takes the
|
|
10
|
+
# other with it and stopping the run stops the server and nothing else.
|
|
11
|
+
#
|
|
12
|
+
# web = Web.launcher(root:, plans_dir:, port: 11000, redis_url:).start
|
|
13
|
+
# web.ready? # => true once /healthz answers
|
|
14
|
+
# web.stop
|
|
15
|
+
class Launcher
|
|
16
|
+
# The executable, from this gem rather than whatever is on PATH.
|
|
17
|
+
EXE = File.join(GEM_ROOT, "exe", "openlanes")
|
|
18
|
+
|
|
19
|
+
# Where the server finds this run's Redis. The URL can carry a password,
|
|
20
|
+
# and argv is world-readable through `ps`; a child's environment is not.
|
|
21
|
+
REDIS_URL_ENV = "OPENLANES_REDIS_URL"
|
|
22
|
+
|
|
23
|
+
# Seconds {#start} waits for `/healthz`.
|
|
24
|
+
READY_WITHIN = 10.0
|
|
25
|
+
|
|
26
|
+
# Seconds {#stop} gives the server after TERM before KILL.
|
|
27
|
+
STOP_WITHIN = 5.0
|
|
28
|
+
|
|
29
|
+
# @param root [String] the checkout the run works in
|
|
30
|
+
# @param plans_dir [String]
|
|
31
|
+
# @param port [Integer]
|
|
32
|
+
# @param redis_url [String]
|
|
33
|
+
# @param log [String] where the server's output goes
|
|
34
|
+
# @param ready_within [Float]
|
|
35
|
+
# @param probe [Proc, nil] answers whether the server is up; `/healthz` by default
|
|
36
|
+
def initialize(root:, plans_dir:, port:, redis_url:, log: default_log(root), ready_within: READY_WITHIN, probe: nil)
|
|
37
|
+
@root = root
|
|
38
|
+
@plans_dir = plans_dir
|
|
39
|
+
@port = Integer(port)
|
|
40
|
+
@redis_url = redis_url
|
|
41
|
+
@log = log
|
|
42
|
+
@ready_within = ready_within
|
|
43
|
+
@probe = probe || method(:healthy?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [String]
|
|
47
|
+
attr_reader :log
|
|
48
|
+
|
|
49
|
+
# @return [Integer, nil]
|
|
50
|
+
attr_reader :pid
|
|
51
|
+
|
|
52
|
+
# @return [String]
|
|
53
|
+
def url = "http://#{HOST}:#{@port}"
|
|
54
|
+
|
|
55
|
+
# @return [Array<String>] what is spawned; the Redis URL is in {#env}, never here
|
|
56
|
+
def argv
|
|
57
|
+
[EXE, "web", "--port", @port.to_s, "--no-open", "--dir", @plans_dir]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Hash{String => String}] added to the server's environment
|
|
61
|
+
def env = { REDIS_URL_ENV => @redis_url }
|
|
62
|
+
|
|
63
|
+
# Spawn the server and wait for it to answer.
|
|
64
|
+
#
|
|
65
|
+
# @return [self]
|
|
66
|
+
def start
|
|
67
|
+
@pid = Process.spawn(env, *argv, chdir: @root, pgroup: true, in: File::NULL, %i[out err] => [@log, "a"])
|
|
68
|
+
@ready = wait_until_ready
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Boolean] whether `/healthz` answered within the wait
|
|
73
|
+
def ready? = @ready == true
|
|
74
|
+
|
|
75
|
+
# TERM to the server's process group, then KILL if it lingers.
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def stop
|
|
79
|
+
return unless @pid
|
|
80
|
+
|
|
81
|
+
signal("TERM")
|
|
82
|
+
deadline = monotonic + STOP_WITHIN
|
|
83
|
+
sleep 0.05 while alive? && monotonic < deadline
|
|
84
|
+
signal("KILL") if alive?
|
|
85
|
+
reap
|
|
86
|
+
@pid = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def wait_until_ready
|
|
93
|
+
deadline = monotonic + @ready_within
|
|
94
|
+
until monotonic >= deadline
|
|
95
|
+
return true if @probe.call
|
|
96
|
+
return false unless alive?
|
|
97
|
+
|
|
98
|
+
sleep 0.1
|
|
99
|
+
end
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def healthy?
|
|
105
|
+
Net::HTTP.start(HOST, @port, open_timeout: 0.5, read_timeout: 1) { it.get("/healthz").code == "200" }
|
|
106
|
+
rescue SystemCallError, IOError, Net::OpenTimeout, Net::ReadTimeout
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def alive?
|
|
112
|
+
Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
113
|
+
rescue Errno::ECHILD
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [void]
|
|
118
|
+
def signal(name)
|
|
119
|
+
Process.kill(name, -@pid)
|
|
120
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Blocks: after KILL the child is gone or about to be, and a
|
|
125
|
+
# non-blocking wait that runs before the kernel has finished with it
|
|
126
|
+
# returns nil and leaves a zombie behind. ECHILD means {#alive?}
|
|
127
|
+
# already reaped it.
|
|
128
|
+
#
|
|
129
|
+
# @return [void]
|
|
130
|
+
def reap
|
|
131
|
+
Process.waitpid(@pid)
|
|
132
|
+
rescue Errno::ECHILD
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Float]
|
|
137
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
138
|
+
|
|
139
|
+
# @return [String]
|
|
140
|
+
def default_log(root) = File.join(Dir.tmpdir, "openlanes-web-#{File.basename(root)}.log")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma"
|
|
4
|
+
require "puma/log_writer"
|
|
5
|
+
require "puma/server"
|
|
6
|
+
|
|
7
|
+
module OpenLanes
|
|
8
|
+
module Web
|
|
9
|
+
# The app on Puma, multi-threaded because every open page holds one
|
|
10
|
+
# thread for its event stream, and bound to {HOST} only.
|
|
11
|
+
#
|
|
12
|
+
# server = Web.server(app:, context:, port: 11000).start
|
|
13
|
+
# server.url # => "http://127.0.0.1:11000"
|
|
14
|
+
# server.wait # until #stop
|
|
15
|
+
class Server
|
|
16
|
+
# Threads: one per open event stream plus the requests beside them.
|
|
17
|
+
MIN_THREADS = 2
|
|
18
|
+
MAX_THREADS = 64
|
|
19
|
+
|
|
20
|
+
# Seconds Puma waits for open streams after {#stop}. They notice the
|
|
21
|
+
# context's `stopping` within a tick; this is the backstop.
|
|
22
|
+
SHUTDOWN_AFTER = 2
|
|
23
|
+
|
|
24
|
+
# @param app [#call]
|
|
25
|
+
# @param context [OpenLanes::Web::Context]
|
|
26
|
+
# @param port [Integer] 0 for any free port
|
|
27
|
+
# @param host [String]
|
|
28
|
+
# @param log [IO] where Puma writes its errors
|
|
29
|
+
def initialize(app:, context:, port: DEFAULT_PORT, host: HOST, log: $stderr)
|
|
30
|
+
@app = app
|
|
31
|
+
@context = context
|
|
32
|
+
@port = Integer(port)
|
|
33
|
+
@host = host
|
|
34
|
+
@log = log
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [String]
|
|
38
|
+
attr_reader :host
|
|
39
|
+
|
|
40
|
+
# Listen, and serve on Puma's own threads.
|
|
41
|
+
#
|
|
42
|
+
# @return [self]
|
|
43
|
+
# @raise [Errno::EADDRINUSE] when the port is taken
|
|
44
|
+
def start
|
|
45
|
+
@puma = Puma::Server.new(@app,
|
|
46
|
+
nil,
|
|
47
|
+
min_threads: MIN_THREADS,
|
|
48
|
+
max_threads: MAX_THREADS,
|
|
49
|
+
force_shutdown_after: SHUTDOWN_AFTER,
|
|
50
|
+
log_writer: Puma::LogWriter.new(File.open(File::NULL, "w"), @log))
|
|
51
|
+
@puma.add_tcp_listener(@host, @port)
|
|
52
|
+
@thread = @puma.run
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Integer] the port listened on, the real one when 0 was asked
|
|
57
|
+
def port = @puma&.connected_ports&.first || @port
|
|
58
|
+
|
|
59
|
+
# @return [String]
|
|
60
|
+
def url = "http://#{@host}:#{port}"
|
|
61
|
+
|
|
62
|
+
# Block until the server stops.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def wait = @thread&.join
|
|
66
|
+
|
|
67
|
+
# Stop listening, end every open stream, and wait for Puma.
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
def stop
|
|
71
|
+
@context.stopping.make_true
|
|
72
|
+
@puma&.stop(true)
|
|
73
|
+
@context.stores.close
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Web
|
|
5
|
+
# One store per Puma thread. {Live::Store} holds a single Redis
|
|
6
|
+
# connection, which two threads must not share: an event stream polling
|
|
7
|
+
# on one thread and a command posted on another would interleave their
|
|
8
|
+
# replies on the wire.
|
|
9
|
+
#
|
|
10
|
+
# Puma's threads are pooled, so the map is bounded by the pool's size.
|
|
11
|
+
class Stores
|
|
12
|
+
# Every thread gets the same store: {Live::MemoryStore}, which lives in
|
|
13
|
+
# this process and is already synchronised.
|
|
14
|
+
#
|
|
15
|
+
# @param store [OpenLanes::Live::MemoryStore]
|
|
16
|
+
# @return [OpenLanes::Web::Stores]
|
|
17
|
+
def self.shared(store) = new { store }
|
|
18
|
+
|
|
19
|
+
# @yieldreturn [OpenLanes::Live::Store] a new store
|
|
20
|
+
def initialize(&factory)
|
|
21
|
+
@factory = factory
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
@by_thread = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
27
|
+
def current = @mutex.synchronize { @by_thread[Thread.current] ||= @factory.call }
|
|
28
|
+
|
|
29
|
+
# @return [void]
|
|
30
|
+
def close
|
|
31
|
+
stores = @mutex.synchronize { @by_thread.values.uniq.tap { @by_thread.clear } }
|
|
32
|
+
stores.each(&:close)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
# The control room's API server (plan 004, W3, W7): Sinatra on a threaded
|
|
8
|
+
# Puma, bound to 127.0.0.1. It holds no run state. Everything it shows is
|
|
9
|
+
# read from Redis through {Live}, and everything it is told goes back
|
|
10
|
+
# there as a command for the engine, so a restarted server loses nothing.
|
|
11
|
+
#
|
|
12
|
+
# context = Web.context(stores:, keys:, plans_dir:, url:)
|
|
13
|
+
# server = Web.server(app: Web.app(context), context:, port: 11000).start
|
|
14
|
+
# server.wait
|
|
15
|
+
module Web
|
|
16
|
+
# Where the gem is, whether run from a checkout or installed.
|
|
17
|
+
GEM_ROOT = File.expand_path("../..", __dir__)
|
|
18
|
+
|
|
19
|
+
# The built page, which the gem ships (W3).
|
|
20
|
+
DIST = File.join(GEM_ROOT, "web", "dist")
|
|
21
|
+
|
|
22
|
+
# The sample board the page's own tests type-check, and {Demo} replays.
|
|
23
|
+
FIXTURE = File.join(GEM_ROOT, "web", "src", "api", "fixtures", "board.json")
|
|
24
|
+
|
|
25
|
+
# Where the server listens unless told otherwise (contract.md).
|
|
26
|
+
DEFAULT_PORT = 11_000
|
|
27
|
+
|
|
28
|
+
# Loopback only: the page can stop agents, and nothing off this machine
|
|
29
|
+
# may reach it (W3).
|
|
30
|
+
HOST = "127.0.0.1"
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param stores [OpenLanes::Web::Stores]
|
|
35
|
+
# @param keys [OpenLanes::Live::Keys]
|
|
36
|
+
# @param plans_dir [String] absolute path of the `.plans` directory
|
|
37
|
+
# @param url [String] the Redis address, shown masked by `/healthz`
|
|
38
|
+
# @param rest [Hash] {Context}'s other fields
|
|
39
|
+
# @return [OpenLanes::Web::Context]
|
|
40
|
+
def context(stores:, keys:, plans_dir:, url:, **rest)
|
|
41
|
+
Context.new(stores:, keys:, plans_dir:, url:, csrf: SecureRandom.hex(32), **rest)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param context [OpenLanes::Web::Context]
|
|
45
|
+
# @return [Class] a Rack application
|
|
46
|
+
def app(context) = App.serving(context)
|
|
47
|
+
|
|
48
|
+
# @param app [#call]
|
|
49
|
+
# @param context [OpenLanes::Web::Context]
|
|
50
|
+
# @param port [Integer]
|
|
51
|
+
# @return [OpenLanes::Web::Server]
|
|
52
|
+
def server(app:, context:, port: DEFAULT_PORT, **) = Server.new(app:, context:, port:, **)
|
|
53
|
+
|
|
54
|
+
# @return [OpenLanes::Web::Demo]
|
|
55
|
+
def demo(**) = Demo.new(**)
|
|
56
|
+
|
|
57
|
+
# @return [OpenLanes::Web::Launcher]
|
|
58
|
+
def launcher(**) = Launcher.new(**)
|
|
59
|
+
|
|
60
|
+
# Whether a browser can be opened here (W1): not under CI, not over
|
|
61
|
+
# SSH, not without a terminal, and on Linux only with a display.
|
|
62
|
+
#
|
|
63
|
+
# @param env [Hash]
|
|
64
|
+
# @param tty [Boolean] whether a person is at the terminal
|
|
65
|
+
# @param os [String] `RbConfig::CONFIG["host_os"]`
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def display?(env: ENV, tty: $stderr.tty?, os: RbConfig::CONFIG["host_os"])
|
|
68
|
+
return false if !tty || present?(env["CI"]) || present?(env["SSH_CONNECTION"]) || present?(env["SSH_TTY"])
|
|
69
|
+
return true if os.match?(/darwin|mswin|mingw/)
|
|
70
|
+
|
|
71
|
+
present?(env["DISPLAY"]) || present?(env["WAYLAND_DISPLAY"])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Open +url+ in the browser, when there is one to open.
|
|
75
|
+
#
|
|
76
|
+
# @param url [String]
|
|
77
|
+
# @param display [Boolean]
|
|
78
|
+
# @return [Boolean] whether a browser was asked to open it
|
|
79
|
+
def open(url, display: display?)
|
|
80
|
+
return false unless display
|
|
81
|
+
|
|
82
|
+
require "launchy"
|
|
83
|
+
Launchy.open(url)
|
|
84
|
+
true
|
|
85
|
+
rescue Launchy::Error, SystemCallError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def present?(value) = !value.to_s.strip.empty?
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/openlanes.rb
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Help screens come from dry-cli-help, which prepends itself to Dry::CLI the
|
|
4
|
+
# moment it loads, so it has to load before the registry's `help` block runs.
|
|
5
|
+
require "dry/cli"
|
|
6
|
+
require "dry/cli/help"
|
|
7
|
+
|
|
8
|
+
require "dry/inflector"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "shellwords"
|
|
11
|
+
require "zeitwerk"
|
|
12
|
+
loader = Zeitwerk::Loader.for_gem
|
|
13
|
+
loader.inflector.inflect(
|
|
14
|
+
"openlanes" => "OpenLanes",
|
|
15
|
+
"api" => "API",
|
|
16
|
+
"cli" => "CLI",
|
|
17
|
+
"github" => "GitHub",
|
|
18
|
+
"issue" => "Issues",
|
|
19
|
+
"list_plans" => "ListPlans",
|
|
20
|
+
"unit" => "Units",
|
|
21
|
+
"ui" => "UI"
|
|
22
|
+
)
|
|
23
|
+
loader.collapse("#{__dir__}/openlanes/support")
|
|
24
|
+
loader.collapse("#{__dir__}/openlanes/cli/create")
|
|
25
|
+
loader.collapse("#{__dir__}/openlanes/cli/docs")
|
|
26
|
+
loader.collapse("#{__dir__}/openlanes/cli/eval")
|
|
27
|
+
loader.collapse("#{__dir__}/openlanes/cli/index")
|
|
28
|
+
loader.collapse("#{__dir__}/openlanes/cli/init")
|
|
29
|
+
loader.collapse("#{__dir__}/openlanes/cli/list_plans")
|
|
30
|
+
loader.collapse("#{__dir__}/openlanes/cli/run")
|
|
31
|
+
loader.collapse("#{__dir__}/openlanes/cli/start")
|
|
32
|
+
loader.collapse("#{__dir__}/openlanes/cli/states")
|
|
33
|
+
loader.collapse("#{__dir__}/openlanes/cli/unblock")
|
|
34
|
+
loader.collapse("#{__dir__}/openlanes/cli/version")
|
|
35
|
+
loader.collapse("#{__dir__}/openlanes/cli/web")
|
|
36
|
+
loader.collapse("#{__dir__}/openlanes/cli/worktree")
|
|
37
|
+
loader.collapse("#{__dir__}/openlanes/cli/agents/subcommands")
|
|
38
|
+
loader.collapse("#{__dir__}/openlanes/cli/linear/subcommands")
|
|
39
|
+
loader.collapse("#{__dir__}/openlanes/cli/mail/subcommands")
|
|
40
|
+
loader.collapse("#{__dir__}/openlanes/cli/state/subcommands")
|
|
41
|
+
loader.collapse("#{__dir__}/openlanes/cli/resync/subcommands")
|
|
42
|
+
loader.collapse("#{__dir__}/openlanes/cli/skills/subcommands")
|
|
43
|
+
loader.ignore("#{__dir__}/openlanes/cli/linear/linear.rb")
|
|
44
|
+
loader.ignore("#{__dir__}/openlanes/cli/mail/mail.rb")
|
|
45
|
+
loader.ignore("#{__dir__}/openlanes/cli/state/state.rb")
|
|
46
|
+
loader.ignore("#{__dir__}/openlanes/linear/mapping.rb")
|
|
47
|
+
loader.setup
|
|
48
|
+
|
|
49
|
+
# Spec → Plan → Build.
|
|
50
|
+
#
|
|
51
|
+
# Three phases, each with a file that proves it happened, and a guarded state
|
|
52
|
+
# machine that refuses to let a plan folder claim a phase it has not reached.
|
|
53
|
+
#
|
|
54
|
+
# spec ⚪️ New spec.md
|
|
55
|
+
# plan ⭐️ Ready plan.md
|
|
56
|
+
# build 🟡 → ✅ pull-requests.md
|
|
57
|
+
#
|
|
58
|
+
# The conventions are not documented anywhere else by hand: the status
|
|
59
|
+
# vocabulary, the numbering rules and the transition table live here and are
|
|
60
|
+
# emitted by `openlanes docs`. Three hand-maintained copies of that table
|
|
61
|
+
# have already drifted apart, which is why there is now exactly one.
|
|
62
|
+
|
|
63
|
+
# © 2026 Konstantin Gredeskoul
|
|
64
|
+
module OpenLanes
|
|
65
|
+
# The folder every project keeps its plans in.
|
|
66
|
+
PLANS_DIR = ENV.fetch("PLANS_DIR", ".plans")
|
|
67
|
+
|
|
68
|
+
# Prefix for a pull request that deliberately implements no plan —
|
|
69
|
+
# dependency bumps, CI work, hotfixes, developer tooling.
|
|
70
|
+
#
|
|
71
|
+
# Lower case, and not a number, so it cannot be mistaken for one. It used to
|
|
72
|
+
# be `DEV.00`, which read as a plan number and sorted among them.
|
|
73
|
+
NO_PLAN_PREFIX = "dev"
|
|
74
|
+
|
|
75
|
+
# What it used to be. Titles still wearing it are rewritten rather than
|
|
76
|
+
# skipped as already-prefixed.
|
|
77
|
+
STALE_NO_PLAN_PREFIX = "DEV.00"
|
|
78
|
+
|
|
79
|
+
# Prefix for a pull request that implements *something* nothing could name.
|
|
80
|
+
# Unlike `dev` this asserts nothing; it marks a question left open.
|
|
81
|
+
NONE_PREFIX = "none"
|
|
82
|
+
|
|
83
|
+
# @return [Dry::Inflector] shared Inflector
|
|
84
|
+
def self.inflector = @inflector ||= ::Dry::Inflector.new
|
|
85
|
+
|
|
86
|
+
# Move a directory, preferring `git mv` so its history follows it.
|
|
87
|
+
#
|
|
88
|
+
# Both the state machine and `resync dirs` move plan folders, and a folder
|
|
89
|
+
# that loses its history because one of them used `FileUtils` is a folder
|
|
90
|
+
# nobody can `git log`.
|
|
91
|
+
#
|
|
92
|
+
# @param source [String] absolute
|
|
93
|
+
# @param target [String] absolute
|
|
94
|
+
# @return [Boolean] false when the target was already occupied
|
|
95
|
+
def self.move_directory(source, target)
|
|
96
|
+
return false if File.exist?(target)
|
|
97
|
+
|
|
98
|
+
parent = File.dirname(source)
|
|
99
|
+
tracked = system("git",
|
|
100
|
+
"-C",
|
|
101
|
+
parent,
|
|
102
|
+
"ls-files",
|
|
103
|
+
"--error-unmatch",
|
|
104
|
+
source,
|
|
105
|
+
out: File::NULL,
|
|
106
|
+
err: File::NULL)
|
|
107
|
+
moved = tracked && system("git",
|
|
108
|
+
"-C",
|
|
109
|
+
parent,
|
|
110
|
+
"mv",
|
|
111
|
+
source,
|
|
112
|
+
target,
|
|
113
|
+
out: File::NULL,
|
|
114
|
+
err: File::NULL)
|
|
115
|
+
FileUtils.mv(source, target) unless moved
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Plan numbers visible in `.plans` on a git ref, without checking it out.
|
|
120
|
+
#
|
|
121
|
+
# A pull request's branch is the only place that knows how far the sequence
|
|
122
|
+
# had got when the work started, and branches are not all rebased onto the
|
|
123
|
+
# same main. Reading the ref directly costs one `ls-tree` and no worktree.
|
|
124
|
+
#
|
|
125
|
+
# @param root [String] repository root
|
|
126
|
+
# @param ref [String] a branch name; `origin/` is tried first
|
|
127
|
+
# @return [Array<OpenLanes::Plans::Ordinal>] possibly empty
|
|
128
|
+
def self.plans_on_ref(root, ref)
|
|
129
|
+
["origin/#{ref}", ref].each do |candidate|
|
|
130
|
+
out = `git -C #{root.shellescape} ls-tree -d --name-only #{candidate.shellescape} #{PLANS_DIR}/ 2>/dev/null`
|
|
131
|
+
next if out.to_s.strip.empty?
|
|
132
|
+
|
|
133
|
+
return out.lines.filter_map { |line| Plans::Ordinal.from_dirname(File.basename(line.strip)) }
|
|
134
|
+
end
|
|
135
|
+
[]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The agent that writes a specification for work that already shipped.
|
|
139
|
+
# Named here rather than in the CLI so the definition file stays the single
|
|
140
|
+
# source of truth about who does what.
|
|
141
|
+
RETROACTIVE_WRITER = "yoda-writer"
|
|
142
|
+
|
|
143
|
+
class Error < StandardError; end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# plans/status.rb and plans/feature.rb define methods on OpenLanes::Plans (`status`,
|
|
147
|
+
# `plan_dirname`, `titleize` and friends) beside the class each is named for.
|
|
148
|
+
# Zeitwerk loads a file when its constant is first named, and a caller of
|
|
149
|
+
# `Plans.status` names no constant, so without this those methods exist
|
|
150
|
+
# or not depending on what happened to load first.
|
|
151
|
+
%w[status feature].each { |name| loader.load_file("#{__dir__}/openlanes/plans/#{name}.rb") }
|
data/openlanes.gemspec
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/openlanes/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "openlanes"
|
|
7
|
+
spec.version = OpenLanes::VERSION
|
|
8
|
+
spec.authors = ["Konstantin Gredeskoul"]
|
|
9
|
+
spec.email = ["kigster@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Agentic specification-driven development: spec, plan, build, review"
|
|
12
|
+
spec.description = <<~TEXT
|
|
13
|
+
Keeps a project's specifications, plans and pull requests joined up, and
|
|
14
|
+
drives specialist agents over them in parallel. A feature's state is its
|
|
15
|
+
folder name under .plans, so a transition renames a directory rather than
|
|
16
|
+
updating a row, and nothing can claim a phase whose document is missing.
|
|
17
|
+
TEXT
|
|
18
|
+
spec.homepage = "https://github.com/kigster/openlanes"
|
|
19
|
+
|
|
20
|
+
spec.required_ruby_version = ">= 3" # rubocop:disable Gemspec/RequiredRubyVersion -- the floor users get, not the Ruby the cops lint for
|
|
21
|
+
|
|
22
|
+
spec.metadata = {
|
|
23
|
+
"homepage_uri" => spec.homepage,
|
|
24
|
+
"source_code_uri" => spec.homepage,
|
|
25
|
+
"rubygems_mfa_required" => "true"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Globbed rather than taken from `git ls-files`, so building the gem does not
|
|
29
|
+
# require a git checkout — the same reason the executables resolve their own
|
|
30
|
+
# bundle rather than trusting the caller's working directory.
|
|
31
|
+
spec.files = Dir.chdir(__dir__) do
|
|
32
|
+
# web/dist is the built page the API server serves; board.json is what
|
|
33
|
+
# `openlanes web --demo` replays.
|
|
34
|
+
# Source maps stay out: they double the page's size for nobody who installs it.
|
|
35
|
+
globbed = Dir.glob(["*.gemspec", "lib/**/*.rb", "agents/*.md", "schemas/*.json", "evals/cases/**/*", "skills/**/*",
|
|
36
|
+
"exe/*", "bin/*", "web/dist/**/*", "web/src/api/fixtures/board.json"])
|
|
37
|
+
globbed.select { |f| File.file?(f) && !f.end_with?(".map") }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A gem built without the page installs a control room that serves a 404.
|
|
41
|
+
# Only `gem build` is stopped (which `rake build` and `just publish` run):
|
|
42
|
+
# Bundler, rspec and rubocop load this file too, in checkouts that have
|
|
43
|
+
# never run npm. `abort` rather than `raise`, because Gem::Specification.load
|
|
44
|
+
# swallows a StandardError into a warning and a puzzling "not found".
|
|
45
|
+
building = File.basename($PROGRAM_NAME) == "gem" && ARGV.first == "build"
|
|
46
|
+
if building && !File.file?(File.join(__dir__, "web", "dist", "index.html"))
|
|
47
|
+
abort "openlanes.gemspec: web/dist/index.html is missing, so the gem would ship without its control room.\n" \
|
|
48
|
+
"Build the page first: just web-build (or: cd web && npm ci && npm run build)."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
spec.bindir = "exe"
|
|
52
|
+
# `depot` and `lanes` are symlinks to `openlanes` in the checkout; named here
|
|
53
|
+
# so that installing the gem writes all three, rather than only the one git
|
|
54
|
+
# recorded.
|
|
55
|
+
spec.executables = %w[openlanes depot lanes]
|
|
56
|
+
spec.require_paths = ["lib"]
|
|
57
|
+
|
|
58
|
+
spec.add_dependency "aasm"
|
|
59
|
+
spec.add_dependency "concurrent-ruby"
|
|
60
|
+
spec.add_dependency "dry-cli"
|
|
61
|
+
spec.add_dependency "dry-cli-autocomplete", "~> 0.5"
|
|
62
|
+
# 0.5.1 is the first release whose help screen survives `example [""]`,
|
|
63
|
+
# which four commands here declare.
|
|
64
|
+
spec.add_dependency "dry-cli-help", ">= 0.5.1", "< 0.6"
|
|
65
|
+
spec.add_dependency "dry-cli-ui", "~> 0.5"
|
|
66
|
+
spec.add_dependency "dry-inflector"
|
|
67
|
+
spec.add_dependency "dry-monads"
|
|
68
|
+
spec.add_dependency "fuzzy-string-match"
|
|
69
|
+
# Opens the control room in the browser when `openlanes run` starts the API server.
|
|
70
|
+
spec.add_dependency "launchy", "~> 3.1"
|
|
71
|
+
spec.add_dependency "parallel"
|
|
72
|
+
spec.add_dependency "pastel"
|
|
73
|
+
# The API server (plan 004, W3, W7): Sinatra on a threaded Puma, so SSE streams hold a thread each.
|
|
74
|
+
spec.add_dependency "puma", ">= 6.4"
|
|
75
|
+
spec.add_dependency "rack-protection", "~> 4.2"
|
|
76
|
+
spec.add_dependency "rackup", "~> 2.1"
|
|
77
|
+
# The live run (plan 004) lives in Redis: board and command streams, plan claims.
|
|
78
|
+
spec.add_dependency "redis-client", "~> 0.31"
|
|
79
|
+
spec.add_dependency "sinatra", "~> 4.2"
|
|
80
|
+
spec.add_dependency "tty-box"
|
|
81
|
+
spec.add_dependency "tty-command"
|
|
82
|
+
spec.add_dependency "tty-cursor"
|
|
83
|
+
spec.add_dependency "tty-prompt"
|
|
84
|
+
spec.add_dependency "tty-screen"
|
|
85
|
+
spec.add_dependency "unicode-display_width"
|
|
86
|
+
spec.add_dependency "zeitwerk"
|
|
87
|
+
end
|