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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes eval` — score the agents against their cases.
|
|
6
|
+
#
|
|
7
|
+
# Offline by default: every case's recorded folder is scored, which costs
|
|
8
|
+
# nothing and runs in CI. `--live` runs the real agents and is never the
|
|
9
|
+
# default, because it spends tokens.
|
|
10
|
+
class Eval < Base
|
|
11
|
+
standalone!
|
|
12
|
+
desc "Score agents against evals/cases: recorded folders by default, real runs with --live"
|
|
13
|
+
|
|
14
|
+
option :agent, aliases: ["-a"], desc: "Only this agent's cases"
|
|
15
|
+
option :case, aliases: ["-c"], desc: "Only the case with this id"
|
|
16
|
+
option :depth,
|
|
17
|
+
values: Plans::Spec::DEPTHS.map(&:to_s),
|
|
18
|
+
desc: "Hold every case to this depth's bounds instead of its own"
|
|
19
|
+
option :recording,
|
|
20
|
+
values: %w[pass fail],
|
|
21
|
+
default: "pass",
|
|
22
|
+
desc: "Which recording to score offline"
|
|
23
|
+
option :live,
|
|
24
|
+
type: :boolean,
|
|
25
|
+
default: false,
|
|
26
|
+
desc: "Run the real agent in a temp repository (spends tokens)"
|
|
27
|
+
option :max_tokens,
|
|
28
|
+
type: :integer,
|
|
29
|
+
desc: "Token cap per live case (default #{Evals::Live::DEFAULT_MAX_TOKENS})"
|
|
30
|
+
option :cases, desc: "Directory of <agent>/<id>.yml cases (default: the gem's evals/cases)"
|
|
31
|
+
|
|
32
|
+
example [
|
|
33
|
+
" # score every pass/ recording",
|
|
34
|
+
"--recording fail # the fail/ ones, which should all FAIL",
|
|
35
|
+
"-a leah-researcher --depth deep",
|
|
36
|
+
"--live -c research-fast --max-tokens 100000"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# @param options [Hash]
|
|
40
|
+
# @return [void]
|
|
41
|
+
def call(**options)
|
|
42
|
+
quiet?(options)
|
|
43
|
+
refuse("--max-tokens only applies to --live runs.", 64) if options[:max_tokens] && !options[:live]
|
|
44
|
+
cases = selected(options)
|
|
45
|
+
scores = options[:live] ? live(cases, options) : offline(cases, options.fetch(:recording, "pass"))
|
|
46
|
+
$stdout.write(report(scores))
|
|
47
|
+
exit 1 unless scores.all? { |s, _| s.passed? }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @return [Array<OpenLanes::Evals::Case>]
|
|
53
|
+
def selected(options)
|
|
54
|
+
all = Evals.cases(options[:cases] || Evals::DEFAULT_DIR)
|
|
55
|
+
picked = all.select do |kase|
|
|
56
|
+
(options[:agent].nil? || kase.agent == options[:agent]) && (options[:case].nil? || kase.id == options[:case])
|
|
57
|
+
end
|
|
58
|
+
refuse("No eval case matches #{filters(options)}.", 66) if picked.empty?
|
|
59
|
+
picked.map { |kase| kase.at(options[:depth]) }
|
|
60
|
+
rescue Evals::Invalid => e
|
|
61
|
+
refuse(e.message, 65)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [String]
|
|
65
|
+
def filters(options) = options.slice(:agent, :case).map { |k, v| "--#{k} #{v}" }.join(" ").then { it.empty? ? "(none)" : it }
|
|
66
|
+
|
|
67
|
+
# @return [Array<Array(OpenLanes::Evals::Score, OpenLanes::Evals::Run)>]
|
|
68
|
+
def offline(cases, kind)
|
|
69
|
+
cases.filter_map do |kase|
|
|
70
|
+
recording = kase.recording(kind)
|
|
71
|
+
unless recording
|
|
72
|
+
UI.line("#{kase} has no #{kind}/ recording, skipped")
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
[Evals.score_recording(kase, kind.to_sym), recording.run]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Array<Array(OpenLanes::Evals::Score, OpenLanes::Evals::Run)>]
|
|
80
|
+
def live(cases, options)
|
|
81
|
+
credentials_warning
|
|
82
|
+
cap = options[:max_tokens] || Evals::Live::DEFAULT_MAX_TOKENS
|
|
83
|
+
cases.map do |kase|
|
|
84
|
+
UI.line("running #{kase} at #{kase.depth}, capped at #{cap} tokens")
|
|
85
|
+
outcome = Evals.run_live(kase, max_tokens: cap)
|
|
86
|
+
result = outcome.result
|
|
87
|
+
[outcome.score, Evals::Run.new(seconds: result.seconds, tokens: result.fresh)]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The table, then what failed and why: the deliverable, on STDOUT.
|
|
92
|
+
#
|
|
93
|
+
# @return [String]
|
|
94
|
+
def report(scores)
|
|
95
|
+
rows = scores.map do |score, run|
|
|
96
|
+
kase = score.kase
|
|
97
|
+
[kase.id, kase.agent, kase.depth, score.tally, run.seconds&.round || "-", run.tokens || "-",
|
|
98
|
+
score.passed? ? "PASS" : "FAIL"]
|
|
99
|
+
end
|
|
100
|
+
table = UI.table(rows, header: %w[Case Agent Depth Checks Seconds Tokens Result])
|
|
101
|
+
details = scores.reject { |s, _| s.passed? }.map do |score, _|
|
|
102
|
+
lines = score.failures.map { |v| " - #{v.name}: #{v.detail}" }
|
|
103
|
+
lines = [" - no checks apply at #{score.kase.depth}"] if lines.empty?
|
|
104
|
+
"#{score.kase} FAIL\n#{lines.join("\n")}\n"
|
|
105
|
+
end
|
|
106
|
+
[table, *details].join("\n")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes index` — the plans as one browsable page.
|
|
6
|
+
class Index < Base
|
|
7
|
+
desc "Create or update .plans/INDEX.md — a global index of every plan."
|
|
8
|
+
|
|
9
|
+
option :output,
|
|
10
|
+
aliases: ["-o"],
|
|
11
|
+
desc: "Write somewhere other than <plans>/INDEX.md; - for STDOUT"
|
|
12
|
+
option :project,
|
|
13
|
+
aliases: ["-p"],
|
|
14
|
+
desc: "Heading for the page, default: the repository's directory name"
|
|
15
|
+
|
|
16
|
+
example [
|
|
17
|
+
" # write .plans/INDEX.md",
|
|
18
|
+
"-o - # print it instead",
|
|
19
|
+
"-p 'Equilibris App' # override the heading"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# @param options [Hash]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def call(**options)
|
|
25
|
+
index = OpenLanes::Plans.index(tree: tree_for(options), project: options[:project])
|
|
26
|
+
|
|
27
|
+
if options[:output] == "-"
|
|
28
|
+
$stdout.write(index.render)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
path = index.write(typed_path(options[:output]))
|
|
33
|
+
puts path
|
|
34
|
+
return if quiet?(options)
|
|
35
|
+
|
|
36
|
+
success("Wrote #{path}\n\nRegenerate it after any `resync dirs`, which renames folders\nand would otherwise leave every link here pointing at nothing.")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module CLI
|
|
8
|
+
# `openlanes init` — make a directory a project: `.openlanes.yml` and `.plans/`.
|
|
9
|
+
#
|
|
10
|
+
# It decides its own root rather than searching for one, because it is
|
|
11
|
+
# the command that creates what the search looks for.
|
|
12
|
+
class Init < Base
|
|
13
|
+
standalone!
|
|
14
|
+
desc "Write .openlanes.yml at the project root and create the plans directory"
|
|
15
|
+
|
|
16
|
+
option :project, desc: "Heading for INDEX.md and similar (default: the root's directory name)"
|
|
17
|
+
option :lane,
|
|
18
|
+
values: Plans::Spec::LANES.map(&:to_s),
|
|
19
|
+
desc: "Default lane for `openlanes create`: full, plan or quick (default: full)"
|
|
20
|
+
option :frontend,
|
|
21
|
+
type: :boolean,
|
|
22
|
+
desc: "Whether plans have a front end; leave it out to let each plan decide"
|
|
23
|
+
option :jobs, aliases: ["-j"], desc: "How many plans `openlanes run` works on at once (default: cores - 2)"
|
|
24
|
+
option :yes,
|
|
25
|
+
type: :boolean,
|
|
26
|
+
default: false,
|
|
27
|
+
aliases: ["-y"],
|
|
28
|
+
desc: "Ask nothing; take the flags and the defaults"
|
|
29
|
+
dry_run_option "Write nothing; print the file that would be written"
|
|
30
|
+
option :overwrite,
|
|
31
|
+
type: :boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
desc: "Replace an existing .openlanes.yml, which is otherwise kept"
|
|
34
|
+
|
|
35
|
+
example [
|
|
36
|
+
" # ask for each value on a terminal",
|
|
37
|
+
"-y --project 'Equilibris App' # ask nothing",
|
|
38
|
+
"-y --lane plan --no-frontend -j 4 # a command-line tool, four plans at once",
|
|
39
|
+
"-n # show the file, write nothing"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# @param options [Hash]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def call(**options)
|
|
45
|
+
quiet?(options)
|
|
46
|
+
root = Project.init_root
|
|
47
|
+
path = File.join(root, Config::FILE)
|
|
48
|
+
plans = File.expand_path(options.fetch(:dir, OpenLanes::PLANS_DIR), root)
|
|
49
|
+
keep = File.file?(path) && !options[:overwrite]
|
|
50
|
+
|
|
51
|
+
config = keep ? Config.load(root) : settings(root, options)
|
|
52
|
+
return preview(path, plans, config, keep) if dry_run?(options)
|
|
53
|
+
return unless keep || confirmed?(path, options)
|
|
54
|
+
|
|
55
|
+
File.write(path, config.to_yaml) unless keep
|
|
56
|
+
FileUtils.mkdir_p(plans)
|
|
57
|
+
puts path
|
|
58
|
+
report(path, plans, keep, options)
|
|
59
|
+
rescue OpenLanes::Error => e
|
|
60
|
+
refuse(e.message, 65)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# The four values, each from its flag, else asked on a terminal, else
|
|
66
|
+
# its default. The result is parsed back as the file would be, so
|
|
67
|
+
# `init` cannot write a file every other command then refuses.
|
|
68
|
+
#
|
|
69
|
+
# @param root [String]
|
|
70
|
+
# @param options [Hash]
|
|
71
|
+
# @return [OpenLanes::Config]
|
|
72
|
+
def settings(root, options)
|
|
73
|
+
ask = interactive?(options)
|
|
74
|
+
project = options[:project] || (ask ? prompt.ask("Project name:", default: File.basename(root)) : File.basename(root))
|
|
75
|
+
lane = options[:lane] || (ask ? prompt.select("Default lane for new plans:", Plans::Spec::LANES.map(&:to_s)) : "full")
|
|
76
|
+
frontend = options.key?(:frontend) ? options[:frontend] : (ask_frontend if ask)
|
|
77
|
+
jobs = options[:jobs] || (prompt.ask("Plans to run at once (blank: cores - 2):") if ask)
|
|
78
|
+
|
|
79
|
+
draft = Config.new(project:, lane: lane.to_sym, frontend:, jobs: count(jobs))
|
|
80
|
+
Config.parse(draft.to_yaml, path: File.join(root, Config::FILE))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean, nil]
|
|
84
|
+
def ask_frontend
|
|
85
|
+
choice = prompt.select("Do plans have a front end?", ["each plan decides", "YES", "NO"])
|
|
86
|
+
{ "YES" => true, "NO" => false }[choice]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param value [String, Integer, nil]
|
|
90
|
+
# @return [Integer, nil]
|
|
91
|
+
def count(value)
|
|
92
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
93
|
+
return value.to_s.to_i if value.to_s.match?(/\A[1-9]\d*\z/)
|
|
94
|
+
|
|
95
|
+
refuse("--jobs must be a whole number above zero, not #{value.inspect}.", 64)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @param path [String]
|
|
99
|
+
# @param options [Hash]
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def confirmed?(path, options)
|
|
102
|
+
return true unless interactive?(options)
|
|
103
|
+
return true if prompt.select("Write #{path}?", %w[YES NO]) == "YES"
|
|
104
|
+
|
|
105
|
+
warn("Nothing was written.") unless quiet?(options)
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Prompts only for a person at a terminal who did not say -y: a script
|
|
110
|
+
# or a pipe gets the flags and the defaults, never a question it
|
|
111
|
+
# cannot answer.
|
|
112
|
+
#
|
|
113
|
+
# @param options [Hash]
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def interactive?(options) = !options[:yes] && tty?
|
|
116
|
+
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
def tty? = $stdin.tty? && $stderr.tty?
|
|
119
|
+
|
|
120
|
+
# A named seam, so a spec can answer the questions.
|
|
121
|
+
#
|
|
122
|
+
# @return [TTY::Prompt]
|
|
123
|
+
def prompt
|
|
124
|
+
@prompt ||= TTY::Prompt.new(output: $stderr)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [void]
|
|
128
|
+
def preview(path, plans, config, keep)
|
|
129
|
+
$stdout.write(config.to_yaml)
|
|
130
|
+
warn("Dry run — nothing was written.\n" \
|
|
131
|
+
"#{keep ? "Would keep" : "Would write"} #{path}" \
|
|
132
|
+
"#{"\nWould create #{plans}" unless File.directory?(plans)}\n\n" \
|
|
133
|
+
"Re-run without --dry-run to apply.")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [void]
|
|
137
|
+
def report(path, plans, keep, options)
|
|
138
|
+
return if quiet?(options)
|
|
139
|
+
|
|
140
|
+
success("#{keep ? "Kept" : "Wrote"} #{path}#{" (--overwrite replaces it)" if keep}\n" \
|
|
141
|
+
"Plans live in #{plans}\n\nNext: openlanes create <a few words>")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes linear …`
|
|
6
|
+
module Linear
|
|
7
|
+
# Shared by both linear commands: the team is which workspace this is
|
|
8
|
+
# about, so it is an argument rather than a flag. There is no default —
|
|
9
|
+
# a tool that picks a team for you when you forget to say which is a
|
|
10
|
+
# tool that files a quarter of somebody's work in the wrong place.
|
|
11
|
+
class Team < Base
|
|
12
|
+
def self.inherited(klass)
|
|
13
|
+
super
|
|
14
|
+
klass.argument :team,
|
|
15
|
+
required: true,
|
|
16
|
+
desc: "The Linear team key that prefixes its issues, e.g. TAX"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# The key is checked before anything reaches for a token, so a typo in
|
|
22
|
+
# the team name reports the typo rather than an authentication
|
|
23
|
+
# problem the user does not have.
|
|
24
|
+
#
|
|
25
|
+
# @param team [String]
|
|
26
|
+
# @return [OpenLanes::Linear::Survey]
|
|
27
|
+
def survey_for(team, tree)
|
|
28
|
+
key = OpenLanes::Linear.key!(team)
|
|
29
|
+
api = OpenLanes::Linear::API.new(token: OpenLanes::Linear::API.token_from_env)
|
|
30
|
+
projects = UI.spinning("Listing #{key} projects") { api.projects(api.team(key)[:id]) }
|
|
31
|
+
[api, OpenLanes::Linear::Survey.new(tree:, projects:)]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes linear …`
|
|
6
|
+
module Linear
|
|
7
|
+
# `linear import` — the plans, as Linear issues under one of your projects.
|
|
8
|
+
class Import < Team
|
|
9
|
+
desc "Create Linear issues from the plans: one per folder, one child per work unit"
|
|
10
|
+
|
|
11
|
+
option :project,
|
|
12
|
+
aliases: ["-p", "--project-url", "--project-id"],
|
|
13
|
+
desc: "The project to file everything under: its URL, its name, or its id"
|
|
14
|
+
dry_run_option "Send nothing to Linear; print what would be created and updated"
|
|
15
|
+
option :format,
|
|
16
|
+
default: "text",
|
|
17
|
+
values: %w[text json],
|
|
18
|
+
desc: "json emits the exact arguments the Linear MCP tools take"
|
|
19
|
+
option :since,
|
|
20
|
+
aliases: ["-s"],
|
|
21
|
+
desc: "Skip plans numbered below this, e.g. 010.00"
|
|
22
|
+
option :status,
|
|
23
|
+
desc: "Only plans in these states: a comma-separated list of status keys"
|
|
24
|
+
option :update_unchanged,
|
|
25
|
+
type: :boolean,
|
|
26
|
+
default: false,
|
|
27
|
+
desc: "Update everything, whether the plan has changed or not"
|
|
28
|
+
|
|
29
|
+
example [
|
|
30
|
+
"TAX -p 'US Tax Law: Self Contained Ruby Gem' # list what would be created",
|
|
31
|
+
"TAX -p https://linear.app/acme/project/… # a URL works too",
|
|
32
|
+
"TAX -p 'Ruby Gem' -n # preview, sending nothing",
|
|
33
|
+
"TAX -p 'Ruby Gem' --format json # hand it to the MCP transport instead",
|
|
34
|
+
"TAX -p 'Ruby Gem' --since 010.00 # only the recent plans",
|
|
35
|
+
"TAX -p 'Ruby Gem' --status building,in_review"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# @param team [String]
|
|
39
|
+
# @param options [Hash]
|
|
40
|
+
# @return [void]
|
|
41
|
+
def call(team:, **options)
|
|
42
|
+
tree = tree_for(options)
|
|
43
|
+
require_project!(options)
|
|
44
|
+
import = build(tree, team, resolve_project(team, tree, options), options)
|
|
45
|
+
|
|
46
|
+
return $stdout.puts(import.to_json) if options[:format] == "json"
|
|
47
|
+
|
|
48
|
+
if import.pending.empty?
|
|
49
|
+
success("Linear is already in step with #{tree.dir}.") unless quiet?(options)
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
preview(import, options)
|
|
54
|
+
rescue OpenLanes::Error => e
|
|
55
|
+
refuse(e.message, 69)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Looking the project up costs a token, and the whole point of
|
|
61
|
+
# `--format json` is to work without one. A name needs no lookup — the
|
|
62
|
+
# MCP server resolves a project by name itself — so only a URL or an
|
|
63
|
+
# id, which do not carry a name, force the network.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash] `{"id", "name", "url"}`
|
|
66
|
+
def resolve_project(team, tree, options)
|
|
67
|
+
reference = options[:project].to_s
|
|
68
|
+
looks_up = reference.match?(%r{\Ahttps?://}) || reference.match?(/\A[0-9a-f-]{32,}\z/)
|
|
69
|
+
return { "id" => nil, "name" => reference, "url" => nil } if !looks_up && offline?
|
|
70
|
+
|
|
71
|
+
_api, survey = survey_for(team, tree)
|
|
72
|
+
survey.project(reference)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def offline? = OpenLanes::Linear::API.token_from_env.nil?
|
|
77
|
+
|
|
78
|
+
# @param options [Hash]
|
|
79
|
+
# @return [void]
|
|
80
|
+
def require_project!(options)
|
|
81
|
+
return if options[:project]
|
|
82
|
+
|
|
83
|
+
raise OpenLanes::Error,
|
|
84
|
+
"which project? Pass -p with a project's URL, name or id.\n\n" \
|
|
85
|
+
"This never creates one: a team's project list is something you curated, and " \
|
|
86
|
+
"every plan is filed under one you named.\n\n " \
|
|
87
|
+
"openlanes linear projects <TEAM> lists them"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [OpenLanes::Linear::Import]
|
|
91
|
+
def build(tree, team, project, options)
|
|
92
|
+
OpenLanes::Linear::Import.new(tree:,
|
|
93
|
+
team: OpenLanes::Linear.key!(team),
|
|
94
|
+
project:,
|
|
95
|
+
since: options[:since],
|
|
96
|
+
statuses: statuses(options),
|
|
97
|
+
update_unchanged: options.fetch(:update_unchanged, false))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @param options [Hash]
|
|
101
|
+
# @return [Array<Symbol>, nil]
|
|
102
|
+
def statuses(options)
|
|
103
|
+
return nil unless options[:status]
|
|
104
|
+
|
|
105
|
+
options[:status].to_s.split(",").map { |word|
|
|
106
|
+
OpenLanes::Plans.status(word.strip)&.key ||
|
|
107
|
+
raise(OpenLanes::Error, "no such state: #{word.strip}")
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param import [OpenLanes::Linear::Import]
|
|
112
|
+
# @param options [Hash]
|
|
113
|
+
# @return [void]
|
|
114
|
+
def preview(import, options)
|
|
115
|
+
import.pending.each { |action| puts row(action) }
|
|
116
|
+
return if quiet?(options)
|
|
117
|
+
|
|
118
|
+
import.pending.each { |action| say(line(action)) }
|
|
119
|
+
report_unplaced(import)
|
|
120
|
+
what = "Linear change#{"s" unless import.pending.size == 1}"
|
|
121
|
+
return dry_run_footer(import.pending.size, what) if dry_run?(options)
|
|
122
|
+
|
|
123
|
+
# There is no push of its own here yet: the MCP transport is what
|
|
124
|
+
# applies these. Saying so beats implying something was sent.
|
|
125
|
+
warn("#{import.pending.size} #{what} pending — nothing was sent.\n" \
|
|
126
|
+
"This command does not write to Linear itself: pipe --format json to the Linear MCP tools.")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param action [OpenLanes::Linear::Action]
|
|
130
|
+
# @return [String]
|
|
131
|
+
def row(action)
|
|
132
|
+
[action.op, action.kind, action.ordinal, action.unit || "-",
|
|
133
|
+
action.identifier || "-", action.title].join("\t")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @param action [OpenLanes::Linear::Action]
|
|
137
|
+
# @return [String]
|
|
138
|
+
def line(action)
|
|
139
|
+
verb = paint(action.op.to_s.ljust(6), action.op == :create ? :green : :yellow)
|
|
140
|
+
indent = action.child? ? " " : ""
|
|
141
|
+
"#{verb} #{indent}#{action.title} #{paint("(#{action.reason})", :bright_black)}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @param import [OpenLanes::Linear::Import]
|
|
145
|
+
# @return [void]
|
|
146
|
+
def report_unplaced(import)
|
|
147
|
+
return if import.unplaced.empty?
|
|
148
|
+
|
|
149
|
+
listed = import.unplaced.map { |status, ordinals|
|
|
150
|
+
" #{status} — #{ordinals.join(", ")}\n#{wrapped(OpenLanes::Linear.reason_unplaced(status))}"
|
|
151
|
+
}
|
|
152
|
+
warn("Not imported, because nothing here knows where they belong:\n\n" \
|
|
153
|
+
"#{listed.join("\n\n")}\n\n" \
|
|
154
|
+
"Decide where they go on your board and add it to Linear::PLACEMENTS.")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A box re-wraps a line that overruns it, and the wrapped remainder
|
|
158
|
+
# comes back at column zero — which reads as a new entry rather than
|
|
159
|
+
# the continuation of one. Wrapping it here keeps the indent.
|
|
160
|
+
#
|
|
161
|
+
# @param text [String]
|
|
162
|
+
# @param width [Integer] narrower than the narrowest box
|
|
163
|
+
# @return [String]
|
|
164
|
+
def wrapped(text, width: 58)
|
|
165
|
+
text.split.each_with_object([+""]) { |word, lines|
|
|
166
|
+
lines << +"" if lines.last.length + word.length + 1 > width
|
|
167
|
+
lines.last << " " unless lines.last.empty?
|
|
168
|
+
lines.last << word
|
|
169
|
+
}.map { |line| " #{line}" }.join("\n")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes linear …`
|
|
6
|
+
module Linear
|
|
7
|
+
# `linear projects` — what the team already has.
|
|
8
|
+
class Projects < Team
|
|
9
|
+
desc "List the Linear projects a team owns, and which plans they already cover"
|
|
10
|
+
|
|
11
|
+
example [
|
|
12
|
+
"TAX # every project the team owns, matched against .plans"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# @param team [String]
|
|
16
|
+
# @param options [Hash]
|
|
17
|
+
# @return [void]
|
|
18
|
+
def call(team:, **options)
|
|
19
|
+
tree = tree_for(options)
|
|
20
|
+
_api, survey = survey_for(team, tree)
|
|
21
|
+
|
|
22
|
+
survey.projects.each { |project| puts row(project) }
|
|
23
|
+
return if quiet?(options)
|
|
24
|
+
|
|
25
|
+
survey.projects.each do |project|
|
|
26
|
+
say("#{paint(project["name"], :cyan)}\n #{paint(project["url"].to_s, :bright_black)}")
|
|
27
|
+
end
|
|
28
|
+
report_descriptions(survey)
|
|
29
|
+
rescue OpenLanes::Error => e
|
|
30
|
+
refuse(e.message, 69)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# @param project [Hash]
|
|
36
|
+
# @return [String]
|
|
37
|
+
def row(project)
|
|
38
|
+
[project["name"], project.dig("status", "name") || "-", project["url"]].join("\t")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param survey [OpenLanes::Linear::Survey]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def report_descriptions(survey)
|
|
44
|
+
return if survey.undescribed.empty?
|
|
45
|
+
|
|
46
|
+
warn("#{survey.undescribed.size} of these say nothing about themselves:\n\n" \
|
|
47
|
+
"#{survey.undescribed.map { |p| " #{p["name"]}" }.join("\n")}\n\n" \
|
|
48
|
+
"A name is three or four words and half of them are the company's. If you " \
|
|
49
|
+
"want anything here to reason about which project a plan belongs to, that " \
|
|
50
|
+
"reasoning has to have a sentence to read.")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes list-plans` — backs /plan-status.
|
|
6
|
+
class ListPlans < Base
|
|
7
|
+
desc "Print every plan, its state, and its pull requests"
|
|
8
|
+
|
|
9
|
+
example ["", "-D .plans", "| less -R"]
|
|
10
|
+
|
|
11
|
+
# @param options [Hash]
|
|
12
|
+
# @return [void]
|
|
13
|
+
def call(**options)
|
|
14
|
+
reporter = Presentation.reporter(tree: tree_for(options))
|
|
15
|
+
$stdout.write(reporter.render)
|
|
16
|
+
|
|
17
|
+
exit 1 unless reporter.inconsistent.empty?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes mail` — the two halves of a pair talking through the plan
|
|
6
|
+
# folder. See {OpenLanes::Plans::Mailbox} for why a file rather than a session.
|
|
7
|
+
module Mail
|
|
8
|
+
# Both subcommands name a plan and need its folder.
|
|
9
|
+
module Locating
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# An unknown number is refused rather than given an empty mailbox:
|
|
13
|
+
# an agent polling the wrong plan for a whole round would read
|
|
14
|
+
# silence as "my partner has nothing to say".
|
|
15
|
+
#
|
|
16
|
+
# @param options [Hash]
|
|
17
|
+
# @return [OpenLanes::Plans::Mailbox]
|
|
18
|
+
def mailbox_for(options)
|
|
19
|
+
tree = tree_for(options)
|
|
20
|
+
ordinal = Plans::Ordinal.parse(options[:plan].to_s)
|
|
21
|
+
subject = ordinal && tree.find(ordinal)
|
|
22
|
+
unless subject
|
|
23
|
+
refuse("No plan #{options[:plan]} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Plans.mailbox(subject.feature.path)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module Mail
|
|
6
|
+
# `openlanes mail read` — what is waiting for one agent.
|
|
7
|
+
#
|
|
8
|
+
# Nothing waiting is the ordinary result of a poll, so it is not an
|
|
9
|
+
# error and not even STDOUT: the messages are the deliverable, and an
|
|
10
|
+
# empty one is empty.
|
|
11
|
+
class Read < Base
|
|
12
|
+
include Locating
|
|
13
|
+
|
|
14
|
+
desc "Print the messages waiting for an agent in a plan's mailbox"
|
|
15
|
+
|
|
16
|
+
option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
|
|
17
|
+
option :for, required: true, desc: "The agent reading"
|
|
18
|
+
option :after, default: "0", desc: "Only messages numbered above this, so a poll shows what is new"
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
"--plan 003 --for rey-frontend # everything addressed to rey",
|
|
22
|
+
"--plan 003 --for rey-frontend --after 4 # only what arrived since #4"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# @param options [Hash]
|
|
26
|
+
# @return [void]
|
|
27
|
+
def call(**options)
|
|
28
|
+
quiet?(options)
|
|
29
|
+
mailbox = mailbox_for(options)
|
|
30
|
+
after = options[:after].to_i
|
|
31
|
+
waiting = mailbox.for(options[:for], after:)
|
|
32
|
+
|
|
33
|
+
if waiting.empty?
|
|
34
|
+
UI.line("Nothing for #{options[:for]}#{" after ##{after}" if after.positive?}") unless quiet?(options)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts waiting.join("\n")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|