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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
# The coding agents a plan can be handed to: `claude`, `codex` and `pi`.
|
|
5
|
+
#
|
|
6
|
+
# The harness decides who works which plan and checks what they did; an
|
|
7
|
+
# adapter only knows how to start one particular CLI and how to read what
|
|
8
|
+
# it prints. That is the whole seam, and it is what lets an agent
|
|
9
|
+
# definition say `adapter: codex` without the run loop noticing.
|
|
10
|
+
module Adapters
|
|
11
|
+
# Reasoning effort, in the vocabulary agent frontmatter and spec.md use.
|
|
12
|
+
# Each adapter translates it into its own CLI's words.
|
|
13
|
+
EFFORTS = %w[low medium high xhigh max].freeze
|
|
14
|
+
|
|
15
|
+
# Percent of the context window at which every agent compacts itself
|
|
16
|
+
# (W28), instead of the CLI's default near the limit: a long session's
|
|
17
|
+
# quality drops well before its window is full. Each adapter maps it to
|
|
18
|
+
# its own CLI's setting in {Base#environment}.
|
|
19
|
+
AUTOCOMPACT = 40
|
|
20
|
+
|
|
21
|
+
# Everything an adapter needs to start one agent, and nothing about why.
|
|
22
|
+
#
|
|
23
|
+
# @!attribute [r] prompt
|
|
24
|
+
# @return [String]
|
|
25
|
+
# @!attribute [r] root
|
|
26
|
+
# @return [String] the checkout the agent works in
|
|
27
|
+
# @!attribute [r] model
|
|
28
|
+
# @return [String, nil] nil lets the CLI choose
|
|
29
|
+
# @!attribute [r] effort
|
|
30
|
+
# @return [String, nil] one of {EFFORTS}
|
|
31
|
+
# @!attribute [r] allowed_tools
|
|
32
|
+
# @return [Array<String>]
|
|
33
|
+
# @!attribute [r] denied_tools
|
|
34
|
+
# @return [Array<String>] tool names, plus `Bash(<command>:*)` specifiers
|
|
35
|
+
# @!attribute [r] denied_commands
|
|
36
|
+
# @return [Array<String>] the bare commands behind those specifiers
|
|
37
|
+
# @!attribute [r] network
|
|
38
|
+
# @return [Boolean] whether the agent may reach the internet
|
|
39
|
+
# @!attribute [r] lean
|
|
40
|
+
# @return [Boolean] start without the operator's personal plugins,
|
|
41
|
+
# skills, hooks and MCP servers
|
|
42
|
+
# @!attribute [r] resume
|
|
43
|
+
# @return [String, nil] a session to carry on rather than start afresh
|
|
44
|
+
# @!attribute [r] slash_commands
|
|
45
|
+
# @return [Boolean] keep the CLI's slash commands even when lean, which
|
|
46
|
+
# only the `/compact` sent into a resumed session needs
|
|
47
|
+
Invocation = Data.define(:prompt,
|
|
48
|
+
:root,
|
|
49
|
+
:model,
|
|
50
|
+
:effort,
|
|
51
|
+
:allowed_tools,
|
|
52
|
+
:denied_tools,
|
|
53
|
+
:denied_commands,
|
|
54
|
+
:network,
|
|
55
|
+
:lean,
|
|
56
|
+
:resume,
|
|
57
|
+
:slash_commands) do
|
|
58
|
+
def initialize(allowed_tools: [], denied_tools: [], denied_commands: [], network: false, lean: true,
|
|
59
|
+
model: nil, effort: nil, resume: nil, slash_commands: false, **rest)
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The adapter every agent gets unless its definition names another.
|
|
65
|
+
DEFAULT = "claude"
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
# @return [Hash{String => Class}]
|
|
70
|
+
def registry = { "claude" => Claude, "codex" => Codex, "pi" => Pi }
|
|
71
|
+
|
|
72
|
+
# @return [Array<String>]
|
|
73
|
+
def names = registry.keys
|
|
74
|
+
|
|
75
|
+
# @param name [String, Symbol, nil]
|
|
76
|
+
# @return [OpenLanes::Adapters::Base]
|
|
77
|
+
# @raise [OpenLanes::Error] for a name nothing answers to
|
|
78
|
+
def for(name)
|
|
79
|
+
key = (name || DEFAULT).to_s
|
|
80
|
+
klass = registry[key] or raise OpenLanes::Error, "unknown adapter #{key.inspect}; known: #{names.join(", ")}"
|
|
81
|
+
klass.new
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param name [String, Symbol, nil]
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def known?(name) = registry.key?((name || DEFAULT).to_s)
|
|
87
|
+
|
|
88
|
+
# @param level [String, Symbol, nil]
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def effort?(level) = EFFORTS.include?(level.to_s)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Agents
|
|
5
|
+
# One specialist, loaded from `~/.agents/agents/<name>.md`.
|
|
6
|
+
#
|
|
7
|
+
# The definition files are the single source of truth for who does what: the
|
|
8
|
+
# frontmatter says which plan states this agent handles and what it may write,
|
|
9
|
+
# and the body is the prompt. Nothing about a specialty is duplicated in Ruby.
|
|
10
|
+
#
|
|
11
|
+
# @!attribute [r] name
|
|
12
|
+
# @return [String]
|
|
13
|
+
# @!attribute [r] description
|
|
14
|
+
# @return [String]
|
|
15
|
+
# @!attribute [r] handles
|
|
16
|
+
# @return [Array<Symbol>] plan states this agent is offered work from
|
|
17
|
+
# @!attribute [r] advances_to
|
|
18
|
+
# @return [Symbol, nil] the state it is expected to reach; nil = read-only
|
|
19
|
+
# @!attribute [r] model
|
|
20
|
+
# @return [String, nil]
|
|
21
|
+
# @!attribute [r] allowed_tools
|
|
22
|
+
# @return [Array<String>]
|
|
23
|
+
# @!attribute [r] may
|
|
24
|
+
# @return [Array<String>] commands lifted from {Executor::FORBIDDEN_COMMANDS}
|
|
25
|
+
# for this agent alone. Nothing in {Executor::UNGRANTABLE} can be lifted.
|
|
26
|
+
# @!attribute [r] timeout
|
|
27
|
+
# @return [Integer, nil] seconds before this agent is abandoned; nil
|
|
28
|
+
# defers to the executor's run-wide default
|
|
29
|
+
# @!attribute [r] ledger
|
|
30
|
+
# @return [Array<String>] the documents this agent signs, in the order
|
|
31
|
+
# it reaches them; the last entry it wrote is where it stands
|
|
32
|
+
# @!attribute [r] rounds
|
|
33
|
+
# @return [Integer] how many times it may be invoked on one plan in one
|
|
34
|
+
# state before the plan parks; a Completed never earns another
|
|
35
|
+
# @!attribute [r] effort
|
|
36
|
+
# @return [String, nil] passed to `claude --effort`
|
|
37
|
+
# @!attribute [r] starts_as
|
|
38
|
+
# @return [Symbol, nil] the state the harness renames the plan into the
|
|
39
|
+
# moment this agent is dispatched, where the topology permits it
|
|
40
|
+
# @!attribute [r] holds_at
|
|
41
|
+
# @return [Symbol, nil] the state the plan takes when this agent completes
|
|
42
|
+
# while its partner on the same plan is still running
|
|
43
|
+
# @!attribute [r] adapter
|
|
44
|
+
# @return [String] which coding agent runs it: `claude`, `codex` or `pi`
|
|
45
|
+
# @!attribute [r] phase
|
|
46
|
+
# @return [String, nil] the word a spec's `phases:` map uses to reach it
|
|
47
|
+
# @!attribute [r] lanes
|
|
48
|
+
# @return [Array<Symbol>] the lanes it works in; empty means every lane
|
|
49
|
+
# @!attribute [r] needs
|
|
50
|
+
# @return [Array<String>] plan files that must hold at least one work-unit
|
|
51
|
+
# heading before it is dispatched
|
|
52
|
+
# @!attribute [r] toggle
|
|
53
|
+
# @return [String, nil] a spec.md frontmatter key that switches it off
|
|
54
|
+
# when false and on when true
|
|
55
|
+
Agent = Data.define(:name,
|
|
56
|
+
:description,
|
|
57
|
+
:handles,
|
|
58
|
+
:advances_to,
|
|
59
|
+
:model,
|
|
60
|
+
:allowed_tools,
|
|
61
|
+
:may,
|
|
62
|
+
:network,
|
|
63
|
+
:timeout,
|
|
64
|
+
:prompt,
|
|
65
|
+
:path,
|
|
66
|
+
:ledger,
|
|
67
|
+
:rounds,
|
|
68
|
+
:effort,
|
|
69
|
+
:starts_as,
|
|
70
|
+
:holds_at,
|
|
71
|
+
:adapter,
|
|
72
|
+
:phase,
|
|
73
|
+
:lanes,
|
|
74
|
+
:needs,
|
|
75
|
+
:toggle) do
|
|
76
|
+
def initialize(ledger: [], rounds: 1, effort: nil, starts_as: nil, holds_at: nil, adapter: Adapters::DEFAULT,
|
|
77
|
+
phase: nil, lanes: [], needs: [], toggle: nil, **rest)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Boolean] whether this agent changes anything on disk
|
|
82
|
+
def read_only? = advances_to.nil?
|
|
83
|
+
|
|
84
|
+
# @param status [OpenLanes::Plans::Status]
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def handles?(status) = handles.include?(status.key)
|
|
87
|
+
|
|
88
|
+
# @param lane [Symbol]
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def serves?(lane) = lanes.empty? || lanes.include?(lane.to_sym)
|
|
91
|
+
|
|
92
|
+
# The word after the hyphen: `researcher`, `backend`. What the screen's
|
|
93
|
+
# agent column shows, the name being too long for it.
|
|
94
|
+
#
|
|
95
|
+
# @return [String]
|
|
96
|
+
def role = name.split("-", 2).last.to_s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# More rounds than this costs tokens and buys nothing: an agent that has
|
|
100
|
+
# not finished in five tries is not going to on the sixth.
|
|
101
|
+
Agent::MAX_ROUNDS = 5
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Agents
|
|
5
|
+
# Which coding agent runs an agent on a particular plan, on which model,
|
|
6
|
+
# thinking how hard: the agent's own frontmatter, overridden by what the
|
|
7
|
+
# plan's author asked for, overridden by what the operator typed.
|
|
8
|
+
#
|
|
9
|
+
# Precedence, highest first:
|
|
10
|
+
#
|
|
11
|
+
# 1. `run --model` (model only)
|
|
12
|
+
# 2. spec.md `phases.<phase>` (adapter, model, effort)
|
|
13
|
+
# 3. spec.md `depth` (effort only)
|
|
14
|
+
# 4. the agent's frontmatter
|
|
15
|
+
#
|
|
16
|
+
# The adapter has the last word on the model, because it is the one that
|
|
17
|
+
# knows the ceiling: a Claude agent asked for Fable runs on Opus.
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [r] adapter
|
|
20
|
+
# @return [OpenLanes::Adapters::Base]
|
|
21
|
+
# @!attribute [r] model
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
# @!attribute [r] effort
|
|
24
|
+
# @return [String, nil]
|
|
25
|
+
Profile = Data.define(:adapter, :model, :effort) do
|
|
26
|
+
# @return [String] e.g. `claude:opus/high`
|
|
27
|
+
def to_s = "#{adapter.name}:#{model || "default"}#{"/#{effort}" if effort}"
|
|
28
|
+
|
|
29
|
+
# What a narrow column shows: the model, prefixed by the adapter only
|
|
30
|
+
# when it is not the default one.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] e.g. `opus`, `codex:gpt-5-codex`
|
|
33
|
+
def label
|
|
34
|
+
name = model || "default"
|
|
35
|
+
adapter.name == Adapters::DEFAULT ? name : "#{adapter.name}:#{name}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Profile
|
|
40
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
41
|
+
# @param spec [OpenLanes::Plans::Spec, nil]
|
|
42
|
+
# @param model [String, nil] what `run --model` typed
|
|
43
|
+
# @return [OpenLanes::Agents::Profile]
|
|
44
|
+
def self.resolve(agent, spec: nil, model: nil)
|
|
45
|
+
override = spec ? spec.override_for(agent.phase) : {}
|
|
46
|
+
adapter = Adapters.for(override["adapter"] || agent.adapter)
|
|
47
|
+
# A model named for one CLI means nothing to another, so switching
|
|
48
|
+
# adapters without naming a model drops the agent's own.
|
|
49
|
+
own_model = override.key?("adapter") && override["adapter"] != agent.adapter ? nil : agent.model
|
|
50
|
+
depth_effort = spec&.depth && Plans::Spec::EFFORT_FOR_DEPTH[spec.depth]
|
|
51
|
+
new(adapter:,
|
|
52
|
+
model: adapter.model(model || override["model"] || own_model),
|
|
53
|
+
effort: override["effort"] || depth_effort || agent.effort)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Agents
|
|
5
|
+
# Loads and indexes the agent definitions.
|
|
6
|
+
class Registry
|
|
7
|
+
# Where definitions live, unless told otherwise.
|
|
8
|
+
DEFAULT_DIR = File.expand_path("../../../agents", __dir__)
|
|
9
|
+
|
|
10
|
+
# @param dir [String]
|
|
11
|
+
# @param roster [Array<OpenLanes::Agents::Agent>, nil] a pre-selected list, used
|
|
12
|
+
# by {#only} and {#without} to derive a narrower roster; nil (the
|
|
13
|
+
# default) loads every definition in `dir`
|
|
14
|
+
def initialize(dir: DEFAULT_DIR, roster: nil)
|
|
15
|
+
@dir = File.expand_path(dir)
|
|
16
|
+
@all = roster
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [String]
|
|
20
|
+
attr_reader :dir
|
|
21
|
+
|
|
22
|
+
# @return [Array<OpenLanes::Agents::Agent>] in name order
|
|
23
|
+
def all
|
|
24
|
+
@all ||= Dir.glob(File.join(dir, "*.md")).sort.filter_map { |path| parse(path) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param name [String]
|
|
28
|
+
# @return [OpenLanes::Agents::Agent, nil]
|
|
29
|
+
def find(name) = all.find { |a| a.name == name.to_s }
|
|
30
|
+
|
|
31
|
+
# A roster holding only the agents named — what `run --agent` hands the
|
|
32
|
+
# loop, so a restriction typed at the command line restricts assignments
|
|
33
|
+
# and not merely chaining.
|
|
34
|
+
#
|
|
35
|
+
# @param names [Array<String>]
|
|
36
|
+
# @return [OpenLanes::Agents::Registry]
|
|
37
|
+
def only(*names)
|
|
38
|
+
wanted = names.flatten.map(&:to_s)
|
|
39
|
+
self.class.new(dir:, roster: all.select { |a| wanted.include?(a.name) })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A roster without the agents named — what `run --skip` hands the loop.
|
|
43
|
+
# A plan sitting in a skipped agent's state is simply never assigned, the
|
|
44
|
+
# same way a state no agent handles is stepped around.
|
|
45
|
+
#
|
|
46
|
+
# @param names [Array<String>]
|
|
47
|
+
# @return [OpenLanes::Agents::Registry]
|
|
48
|
+
def without(*names)
|
|
49
|
+
unwanted = names.flatten.map(&:to_s)
|
|
50
|
+
self.class.new(dir:, roster: all.reject { |a| unwanted.include?(a.name) })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Every agent the query could mean. An exact name wins outright; failing
|
|
54
|
+
# that the query matches as a prefix, and failing that anywhere in the
|
|
55
|
+
# name, so `leah` finds leah-researcher and `review` finds
|
|
56
|
+
# hansolo-reviewer. A directory or a trailing `.md` is stripped first,
|
|
57
|
+
# because tab completion hands those in.
|
|
58
|
+
#
|
|
59
|
+
# @param query [String]
|
|
60
|
+
# @return [Array<OpenLanes::Agents::Agent>]
|
|
61
|
+
def match(query)
|
|
62
|
+
wanted = File.basename(query.to_s, ".md")
|
|
63
|
+
exact = all.select { |a| a.name == wanted }
|
|
64
|
+
return exact unless exact.empty?
|
|
65
|
+
|
|
66
|
+
prefixed = all.select { |a| a.name.start_with?(wanted) }
|
|
67
|
+
return prefixed unless prefixed.empty?
|
|
68
|
+
|
|
69
|
+
all.select { |a| a.name.include?(wanted) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Every agent that will act on a plan in this state, in definition order.
|
|
73
|
+
# A read-only agent is never offered work by the loop — it has nothing to
|
|
74
|
+
# advance, so including it would make every round look productive.
|
|
75
|
+
#
|
|
76
|
+
# @param status [OpenLanes::Plans::Status]
|
|
77
|
+
# @return [Array<OpenLanes::Agents::Agent>]
|
|
78
|
+
def for_status(status) = all.select { |a| a.handles?(status) && !a.read_only? }
|
|
79
|
+
|
|
80
|
+
# Everyone on a plan in this state: the agents that work it, plus any
|
|
81
|
+
# agent whose `holds_at` parks the plan here after it finished its half.
|
|
82
|
+
# Rey resuming a plan at 🎨 still needs luke's messages in the mailbox.
|
|
83
|
+
#
|
|
84
|
+
# @param status [OpenLanes::Plans::Status]
|
|
85
|
+
# @return [Array<OpenLanes::Agents::Agent>]
|
|
86
|
+
def team_for(status) = all.select { |a| !a.read_only? && (a.handles?(status) || a.holds_at == status.key) }
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# @param path [String]
|
|
91
|
+
# @return [OpenLanes::Agents::Agent, nil]
|
|
92
|
+
def parse(path)
|
|
93
|
+
meta, body = Frontmatter.split(File.read(path, encoding: "UTF-8"))
|
|
94
|
+
return nil if meta["name"].to_s.empty?
|
|
95
|
+
|
|
96
|
+
Agent.new(
|
|
97
|
+
name: meta["name"].to_s,
|
|
98
|
+
description: meta["description"].to_s,
|
|
99
|
+
handles: Array(meta["handles"]).map { |s| s.to_s.to_sym },
|
|
100
|
+
advances_to: meta["advances_to"]&.to_s&.then { |s| s.empty? ? nil : s.to_sym },
|
|
101
|
+
model: meta["model"],
|
|
102
|
+
allowed_tools: Array(meta["allowed_tools"]).map(&:to_s),
|
|
103
|
+
may: Array(meta["may"]).map { |c| c.to_s.strip.squeeze(" ") },
|
|
104
|
+
network: meta["network"] == true,
|
|
105
|
+
timeout: meta["timeout"].to_i.then { |s| s.positive? ? s : nil },
|
|
106
|
+
prompt: body.strip,
|
|
107
|
+
path: path,
|
|
108
|
+
ledger: Array(meta["ledger"]).map(&:to_s),
|
|
109
|
+
rounds: meta["rounds"].to_i.clamp(1, Agent::MAX_ROUNDS),
|
|
110
|
+
effort: effort(meta["effort"], path),
|
|
111
|
+
starts_as: symbol_or_nil(meta["starts_as"]),
|
|
112
|
+
holds_at: symbol_or_nil(meta["holds_at"]),
|
|
113
|
+
adapter: adapter(meta["adapter"], path),
|
|
114
|
+
phase: meta["phase"]&.to_s,
|
|
115
|
+
lanes: Array(meta["lanes"]).map { |l| l.to_s.to_sym },
|
|
116
|
+
needs: Array(meta["needs"]).map(&:to_s),
|
|
117
|
+
toggle: meta["toggle"]&.to_s
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A definition is checked in with the code, so a bad value in one is a
|
|
122
|
+
# bug to stop on, not a preference to work around.
|
|
123
|
+
#
|
|
124
|
+
# @param value [Object, nil]
|
|
125
|
+
# @param path [String]
|
|
126
|
+
# @return [String, nil]
|
|
127
|
+
# @raise [OpenLanes::Error]
|
|
128
|
+
def effort(value, path)
|
|
129
|
+
return nil if value.nil?
|
|
130
|
+
return value.to_s if Adapters.effort?(value)
|
|
131
|
+
|
|
132
|
+
raise OpenLanes::Error, "#{File.basename(path)}: effort #{value.inspect} is not one of #{Adapters::EFFORTS.join(", ")}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param value [Object, nil]
|
|
136
|
+
# @param path [String]
|
|
137
|
+
# @return [String]
|
|
138
|
+
# @raise [OpenLanes::Error]
|
|
139
|
+
def adapter(value, path)
|
|
140
|
+
return Adapters::DEFAULT if value.nil?
|
|
141
|
+
return value.to_s if Adapters.known?(value)
|
|
142
|
+
|
|
143
|
+
raise OpenLanes::Error, "#{File.basename(path)}: adapter #{value.inspect} is not one of #{Adapters.names.join(", ")}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @param value [Object, nil]
|
|
147
|
+
# @return [Symbol, nil]
|
|
148
|
+
def symbol_or_nil(value) = value.to_s.empty? ? nil : value.to_s.to_sym
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Agents
|
|
5
|
+
# `openlanes agents` — who the specialists are and what each is offered
|
|
6
|
+
# work from. Reading one in full is {Viewer}'s job: the definition file is
|
|
7
|
+
# already the whole truth, so `agents describe` opens it rather than
|
|
8
|
+
# restating it.
|
|
9
|
+
#
|
|
10
|
+
# {#list} returns a string and prints nothing, the same bargain {Reporter}
|
|
11
|
+
# makes, so the caller decides where the text goes and the table stays
|
|
12
|
+
# pipeable.
|
|
13
|
+
#
|
|
14
|
+
# Everything here is read from the definition files. Nothing about a
|
|
15
|
+
# specialty is restated in Ruby, because a second copy of "who does what" is
|
|
16
|
+
# a copy that drifts from the frontmatter the loop actually routes on.
|
|
17
|
+
class Roster
|
|
18
|
+
# Columns, so the header and the rows cannot drift apart.
|
|
19
|
+
HEADINGS = ["Agent", "Handles", "Advances to", "Runs on", "Lanes"].freeze
|
|
20
|
+
|
|
21
|
+
# What an agent with no `advances_to` is, in the one word that explains why
|
|
22
|
+
# `run` never offers it work.
|
|
23
|
+
READ_ONLY = "read-only"
|
|
24
|
+
|
|
25
|
+
# What an agent that handles only {StateMachine::SETTLED} states advances
|
|
26
|
+
# to. `run` never starts it; a command does, and `resync` then names the
|
|
27
|
+
# folder by what its files justify.
|
|
28
|
+
BY_CONTENTS = "by contents"
|
|
29
|
+
|
|
30
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
31
|
+
def initialize(agents: Registry.new)
|
|
32
|
+
@agents = agents
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [OpenLanes::Agents::Registry]
|
|
36
|
+
attr_reader :agents
|
|
37
|
+
|
|
38
|
+
# One row per agent, in name order, framed by {UI.table}.
|
|
39
|
+
#
|
|
40
|
+
# @return [String] newline-terminated
|
|
41
|
+
def list
|
|
42
|
+
all = agents.all
|
|
43
|
+
return "No agent definitions in #{agents.dir}\n" if all.empty?
|
|
44
|
+
|
|
45
|
+
UI.table(all.map { |agent| row(agent) }, header: HEADINGS)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# A state as its words alone, deliberately without the emoji that every
|
|
51
|
+
# other rendering of a state carries. The table widget pads a cell to a
|
|
52
|
+
# width it measures in characters, while the terminal draws a status emoji
|
|
53
|
+
# in two cells; the variation-selector ones (⭕️ 🅱️ ⚪️ ⭐️ 🕰️) are two
|
|
54
|
+
# characters drawn in two cells, so no single count is right for all of
|
|
55
|
+
# them. A row carrying one lands short or long and the frame stops lining
|
|
56
|
+
# up. The label says the same thing unambiguously, and is worth more than
|
|
57
|
+
# the emoji is. {Reporter} lays its own columns out with {UI.fit} and so
|
|
58
|
+
# keeps the emoji.
|
|
59
|
+
#
|
|
60
|
+
# @param key [Symbol]
|
|
61
|
+
# @return [String]
|
|
62
|
+
def state(key)
|
|
63
|
+
status = Plans::STATUS_BY_KEY[key] or return key.to_s
|
|
64
|
+
status.label
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
68
|
+
# @return [String]
|
|
69
|
+
def handles(agent) = agent.handles.map { |key| state(key) }.join(", ")
|
|
70
|
+
|
|
71
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
72
|
+
# @return [String]
|
|
73
|
+
def advances(agent)
|
|
74
|
+
return state(agent.advances_to) if agent.advances_to
|
|
75
|
+
return BY_CONTENTS if agent.handles.any? && agent.handles.all? { |key| Plans::StateMachine::SETTLED.include?(key) }
|
|
76
|
+
|
|
77
|
+
READ_ONLY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The widget measures and pads the cells, so they arrive painted only.
|
|
81
|
+
#
|
|
82
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
83
|
+
# @return [Array<String>] one cell per entry in {HEADINGS}
|
|
84
|
+
def row(agent)
|
|
85
|
+
[UI.paint(agent.name, :bright_cyan),
|
|
86
|
+
handles(agent),
|
|
87
|
+
UI.paint(advances(agent), agent.read_only? ? :bright_black : :green),
|
|
88
|
+
UI.paint(runs_on(agent), :bright_black),
|
|
89
|
+
UI.paint(agent.lanes.empty? ? "all" : agent.lanes.join(", "), :bright_black)]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The agent's own settings, before any spec.md overrides them: the
|
|
93
|
+
# model, the effort, and the adapter when it is not the default.
|
|
94
|
+
#
|
|
95
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
96
|
+
# @return [String] e.g. `opus/high`, `codex:gpt-5-codex/medium`
|
|
97
|
+
def runs_on(agent)
|
|
98
|
+
model = Agents::Profile.resolve(agent).label
|
|
99
|
+
agent.effort ? "#{model}/#{agent.effort}" : model
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Agents
|
|
5
|
+
# Whether an agent that handles a plan's state should actually be given
|
|
6
|
+
# the plan. Handling the state is necessary, not sufficient: the plan's
|
|
7
|
+
# lane may route around the agent, its spec.md may switch it off, or the
|
|
8
|
+
# files it builds from may hold nothing for it to do.
|
|
9
|
+
#
|
|
10
|
+
# The front end is the case that motivated this. rey-frontend used to be
|
|
11
|
+
# started on every planned folder, and spent ten minutes and five dollars
|
|
12
|
+
# on a plan whose interface was three lines of terminal output.
|
|
13
|
+
module Routing
|
|
14
|
+
# A work unit in a plan file: a second- or third-level heading. A title
|
|
15
|
+
# line does not count, so a `plan-frontend.md` that is only a heading
|
|
16
|
+
# and "no front-end units" starts nobody.
|
|
17
|
+
UNIT_HEADING = /^[ \t]{0,3}\#{2,3}[ \t]+\S/
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
22
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
23
|
+
# @param spec [OpenLanes::Plans::Spec]
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def allows?(agent, subject, spec = Plans::Spec.for(subject))
|
|
26
|
+
return false unless agent.serves?(spec.lane)
|
|
27
|
+
return false unless reviewable?(agent, subject)
|
|
28
|
+
|
|
29
|
+
switch = toggled(agent, spec)
|
|
30
|
+
return switch unless switch.nil?
|
|
31
|
+
|
|
32
|
+
agent.needs.all? { |file| units?(subject, file) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The agents in +agents+ this plan should get, in their given order.
|
|
36
|
+
#
|
|
37
|
+
# {Agent#needs} only ever chooses between agents. When lanes and
|
|
38
|
+
# switches leave one candidate, that one runs: a plan parked at 🎨 is
|
|
39
|
+
# rey-frontend's to finish whatever `plan-frontend.md` says, and an
|
|
40
|
+
# operator who typed `--agent rey-frontend` meant it.
|
|
41
|
+
#
|
|
42
|
+
# @param agents [Array<OpenLanes::Agents::Agent>]
|
|
43
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
44
|
+
# @return [Array<OpenLanes::Agents::Agent>]
|
|
45
|
+
def filter(agents, subject)
|
|
46
|
+
spec = Plans::Spec.for(subject)
|
|
47
|
+
routed = agents.select do |agent|
|
|
48
|
+
agent.serves?(spec.lane) && toggled(agent, spec) != false && reviewable?(agent, subject)
|
|
49
|
+
end
|
|
50
|
+
return routed if routed.size <= 1
|
|
51
|
+
|
|
52
|
+
routed.select { |agent| toggled(agent, spec) || agent.needs.all? { |file| units?(subject, file) } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# What the spec's switch for this agent says: true forces it, false
|
|
56
|
+
# forbids it, nil leaves the decision to {Agent#needs}. A spec.md that
|
|
57
|
+
# says nothing takes the project's answer from `.openlanes.yml`, so a
|
|
58
|
+
# command-line tool can say once that no plan of it has a front end.
|
|
59
|
+
#
|
|
60
|
+
# @param config [OpenLanes::Config] the project's settings
|
|
61
|
+
# @return [Boolean, nil]
|
|
62
|
+
def toggled(agent, spec, config = OpenLanes::Config.current)
|
|
63
|
+
return nil if agent.toggle.nil?
|
|
64
|
+
return nil unless spec.respond_to?(agent.toggle)
|
|
65
|
+
|
|
66
|
+
value = spec.public_send(agent.toggle)
|
|
67
|
+
return value unless value.nil?
|
|
68
|
+
|
|
69
|
+
config.public_send(agent.toggle) if config.respond_to?(agent.toggle)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A reviewer reviews a pull request, through `gh pr diff`. A plan that
|
|
73
|
+
# reached 🟢 with none open (a shared-tree run, or --no-git-push) would
|
|
74
|
+
# hand it nothing to read, so the plan waits at 🟢 until one exists;
|
|
75
|
+
# `resync prs` records one a human opened.
|
|
76
|
+
#
|
|
77
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
78
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def reviewable?(agent, subject) = agent.phase.to_s != "review" || subject.pull_requests.any?(&:open?)
|
|
81
|
+
|
|
82
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
83
|
+
# @param file [String]
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def units?(subject, file) = Plans::Ledger.stripped(subject.read(file)).match?(UNIT_HEADING)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
# The specialists: their definitions in `agents/*.md`, the registry that
|
|
5
|
+
# loads and filters them, and the roster that reports on them.
|
|
6
|
+
module Agents
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @param dir [String] where the definitions live
|
|
10
|
+
# @return [OpenLanes::Agents::Registry]
|
|
11
|
+
def registry(dir: Registry::DEFAULT_DIR) = Registry.new(dir:)
|
|
12
|
+
|
|
13
|
+
# @param name [String]
|
|
14
|
+
# @return [OpenLanes::Agents::Agent, nil]
|
|
15
|
+
def find(name, dir: Registry::DEFAULT_DIR) = registry(dir:).find(name)
|
|
16
|
+
|
|
17
|
+
# @return [OpenLanes::Agents::Roster]
|
|
18
|
+
def roster(**) = Roster.new(**)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes agents …` — the specialist roster.
|
|
6
|
+
module Agents
|
|
7
|
+
# `openlanes agents describe [NAME]`
|
|
8
|
+
class Describe < Dry::CLI::Command
|
|
9
|
+
include UI
|
|
10
|
+
|
|
11
|
+
desc "Open one specialist's definition in a Markdown viewer"
|
|
12
|
+
|
|
13
|
+
argument :name,
|
|
14
|
+
required: false,
|
|
15
|
+
desc: "Which specialist, full name or a fragment, e.g. leah (default: every one)"
|
|
16
|
+
|
|
17
|
+
option :mdfried,
|
|
18
|
+
type: :boolean,
|
|
19
|
+
default: false,
|
|
20
|
+
aliases: ["-m"],
|
|
21
|
+
desc: "Render in the terminal through mdfried instead of the system viewer"
|
|
22
|
+
|
|
23
|
+
example [
|
|
24
|
+
"leah # leah-researcher, in the system Markdown viewer",
|
|
25
|
+
"luke-backend -m # the same, rendered in this terminal by mdfried",
|
|
26
|
+
" # every agent, one viewer window each"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# The registry instantiates commands bare, so both collaborators
|
|
30
|
+
# default to the real thing; the suite hands in its own to keep
|
|
31
|
+
# viewers from opening on the box running it.
|
|
32
|
+
#
|
|
33
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
34
|
+
# @param viewer [OpenLanes::Presentation::Viewer]
|
|
35
|
+
def initialize(agents: OpenLanes::Agents.registry, viewer: Presentation.viewer)
|
|
36
|
+
super()
|
|
37
|
+
@agents = agents
|
|
38
|
+
@viewer = viewer
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param name [String, nil]
|
|
42
|
+
# @param mdfried [Boolean]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def call(name: nil, mdfried: false, **_options)
|
|
45
|
+
agents = @agents
|
|
46
|
+
found = name ? agents.match(name) : agents.all
|
|
47
|
+
|
|
48
|
+
if found.empty?
|
|
49
|
+
known = agents.all.map(&:name).join(", ")
|
|
50
|
+
raise Error, name ? "No agent matches #{name}. Known: #{known}" : "No agent definitions in #{agents.dir}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A fragment that fits several agents is a question, not an order:
|
|
54
|
+
# opening all of them would bury the one that was meant.
|
|
55
|
+
raise Error, "#{name} could be any of #{found.map(&:name).join(", ")} — say which" if name && found.size > 1
|
|
56
|
+
|
|
57
|
+
paths = found.map(&:path)
|
|
58
|
+
mdfried ? @viewer.mdfried(paths) : @viewer.open(paths)
|
|
59
|
+
rescue OpenLanes::Error => e
|
|
60
|
+
error(e.message)
|
|
61
|
+
exit 65
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|