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,544 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module CLI
|
|
7
|
+
# `openlanes run` — drive the specialist agents until the tree settles.
|
|
8
|
+
class Run < Base
|
|
9
|
+
desc "Run specialist agents over the plans; with the control room up it stays, else it ends when nothing changes"
|
|
10
|
+
|
|
11
|
+
dry_run_option "Invoke no agent; print which agent would take which plan"
|
|
12
|
+
option :rounds,
|
|
13
|
+
desc: "Cap on rounds per agent per plan; each agent declares its own, at most 5 " \
|
|
14
|
+
"(default: the agent's own)"
|
|
15
|
+
option :timeout,
|
|
16
|
+
desc: "Seconds before one agent is abandoned; only tightens an agent's own clock " \
|
|
17
|
+
"(default: the agent's own, else 900)"
|
|
18
|
+
option :agent, desc: "Only run this one agent"
|
|
19
|
+
# `--prompt` shorter than this that names an existing file is read.
|
|
20
|
+
PROMPT_PATH_LIMIT = 80
|
|
21
|
+
|
|
22
|
+
option :prompt,
|
|
23
|
+
desc: "Extra instructions appended to the agent's prompt (only with --agent); " \
|
|
24
|
+
"a path to an existing text file under #{PROMPT_PATH_LIMIT} characters is read instead"
|
|
25
|
+
option :skip,
|
|
26
|
+
desc: "Never assign this agent; its plans wait, the rest of the pipeline runs. " \
|
|
27
|
+
"Comma separated for several"
|
|
28
|
+
option :model,
|
|
29
|
+
desc: "Model for every agent this run, overriding each agent's own frontmatter and spec.md " \
|
|
30
|
+
"(default: what the agent declares, else the CLI's default). Claude models above Opus run as Opus"
|
|
31
|
+
option :user_config,
|
|
32
|
+
type: :boolean,
|
|
33
|
+
default: false,
|
|
34
|
+
desc: "Start agents with your personal plugins, skills, hooks and MCP servers. " \
|
|
35
|
+
"Off by default: they cost every agent context and start-up time on every turn"
|
|
36
|
+
option :max_tokens,
|
|
37
|
+
desc: "Token budget per agent invocation, input plus output, sub-agents " \
|
|
38
|
+
"included. The agent is told the number so it can finish inside it; the meter aborts it past " \
|
|
39
|
+
"the number so the telling is true. (default: unmetered)"
|
|
40
|
+
option :plan,
|
|
41
|
+
aliases: ["--plans"],
|
|
42
|
+
desc: "Only these plans, comma separated: NNN or NNN.MM, e.g. --plan 003,005.01. Default: the whole tree"
|
|
43
|
+
option :root, desc: "Repository root the agents work in (default: the .plans parent)"
|
|
44
|
+
option :isolation,
|
|
45
|
+
default: "worktree",
|
|
46
|
+
values: %w[worktree shared],
|
|
47
|
+
desc: "worktree: a checkout and branch per plan, run in parallel. shared: one tree, serial"
|
|
48
|
+
option :jobs,
|
|
49
|
+
aliases: ["-j"],
|
|
50
|
+
desc: "Agents to run at once (default: cores - 2, capped at 12)"
|
|
51
|
+
option :git_push,
|
|
52
|
+
type: :boolean,
|
|
53
|
+
default: true,
|
|
54
|
+
desc: "With --isolation worktree, a finished branch is pushed and its pull request " \
|
|
55
|
+
"opened as soon as it lands, titled [NNN.MM](X). --no-git-push turns that off and leaves it uncommitted."
|
|
56
|
+
option :log, desc: "Append progress to this file (default: a per-project file under the system temp dir)"
|
|
57
|
+
option :redis_url,
|
|
58
|
+
desc: "Redis for the live board, its commands and plan claims shared with other machines " \
|
|
59
|
+
"(default: .openlanes.yml redis_url, else $REDIS_URL, else #{Live::DEFAULT_URL})"
|
|
60
|
+
option :web,
|
|
61
|
+
type: :boolean,
|
|
62
|
+
default: true,
|
|
63
|
+
desc: "Serve the control room beside the run, when Redis answers; --no-web runs headless"
|
|
64
|
+
option :open,
|
|
65
|
+
type: :boolean,
|
|
66
|
+
default: true,
|
|
67
|
+
desc: "Open the control room in the browser; --no-open only prints its URL"
|
|
68
|
+
option :port,
|
|
69
|
+
default: OpenLanes::Web::DEFAULT_PORT.to_s,
|
|
70
|
+
desc: "Port the control room listens on, on 127.0.0.1"
|
|
71
|
+
option :until_settled,
|
|
72
|
+
type: :boolean,
|
|
73
|
+
default: false,
|
|
74
|
+
desc: "End at the first fixed point even with the control room up, as CI wants; " \
|
|
75
|
+
"without it the engine and the page stay up until Stop All Local or Ctrl-C"
|
|
76
|
+
|
|
77
|
+
example [
|
|
78
|
+
"-n # show which agent would take which plan",
|
|
79
|
+
" # run them, one worktree per plan, in parallel",
|
|
80
|
+
"-j 4 # …with four at a time",
|
|
81
|
+
"--isolation shared # one tree, serial — no git required",
|
|
82
|
+
"--rounds 3 # …capping every agent at three rounds per plan",
|
|
83
|
+
"--plan 005,006,007 # only the plans a batch step just created",
|
|
84
|
+
"--no-web # headless: no control room, no browser; ends when the tree settles",
|
|
85
|
+
"--until-settled # with the control room, but ending when the tree settles"
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Option precedence, quietest to loudest: the built-in default, then
|
|
89
|
+
# `.openlanes.yml` (`jobs`, and the `commands.run` section), then the flag
|
|
90
|
+
# actually typed. {Base.default_params} has already folded the file in.
|
|
91
|
+
#
|
|
92
|
+
# @param options [Hash]
|
|
93
|
+
# @return [void]
|
|
94
|
+
def call(**options)
|
|
95
|
+
if options[:prompt] && !options[:agent]
|
|
96
|
+
refuse("--prompt only works with --agent: it speaks to one agent, and without " \
|
|
97
|
+
"that restriction every agent in the round would hear it.",
|
|
98
|
+
64)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
tree = tree_for(options)
|
|
102
|
+
root = options[:root] || File.dirname(tree.dir)
|
|
103
|
+
agents = OpenLanes::Agents.registry
|
|
104
|
+
agents = filtered(agents, options[:agent]) if options[:agent]
|
|
105
|
+
agents = skipped(agents, options[:skip], options[:agent]) if options[:skip]
|
|
106
|
+
plans = options[:plan] ? scoped(tree, options[:plan]) : nil
|
|
107
|
+
assignable!(agents, tree, plans, options[:agent]) if options[:agent]
|
|
108
|
+
|
|
109
|
+
isolation = options.fetch(:isolation, "worktree").to_sym
|
|
110
|
+
jobs = (options[:jobs] || UI.default_jobs).to_i
|
|
111
|
+
timeout = options[:timeout]&.to_i
|
|
112
|
+
|
|
113
|
+
if isolation == :worktree && !OpenLanes::Vcs.worktree(root:).repository?
|
|
114
|
+
refuse("#{root} is not a git repository, so plans cannot be isolated.\n\n" \
|
|
115
|
+
"Run with --isolation shared to work in one tree, serially.",
|
|
116
|
+
66)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Set before the loop starts, not after: `UI.animate?` (and therefore
|
|
120
|
+
# whether a round prints anything at all) is read the whole time the
|
|
121
|
+
# loop runs, not just when `report` prints its closing summary.
|
|
122
|
+
quiet?(options)
|
|
123
|
+
UI.log_path = options[:log] ||
|
|
124
|
+
File.join(Dir.tmpdir, "openlanes-#{File.basename(root)}.log")
|
|
125
|
+
info("Progress: #{UI.log_path}") unless quiet?(options)
|
|
126
|
+
credentials_warning if !dry_run?(options) && !quiet?(options)
|
|
127
|
+
spec_warning(tree, plans) unless quiet?(options)
|
|
128
|
+
|
|
129
|
+
# The state file is what a harness that dies leaves for the next one.
|
|
130
|
+
# It sits in .plans/ so a reboot keeps it, and it is ignored by name
|
|
131
|
+
# so it never rides along in a commit. Editing somebody's .gitignore
|
|
132
|
+
# is announced, the once it happens. The runner drops the file on a
|
|
133
|
+
# dry run, which is why it can be built unconditionally.
|
|
134
|
+
state = Engine.state_file(tree)
|
|
135
|
+
if !dry_run?(options) && Engine::StateFile.ensure_ignored!(root) && !quiet?(options)
|
|
136
|
+
info("Added #{Engine::StateFile::IGNORE.join(" and ")} to .gitignore: the run keeps its state there.")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Control files from a previous run would otherwise be read as orders
|
|
140
|
+
# to this one. Progress goes to STDERR as lines; the web view (plan 004)
|
|
141
|
+
# is what will draw the board and take kill, extend and stop.
|
|
142
|
+
Execution::Control.reset!
|
|
143
|
+
|
|
144
|
+
live = live_for(root:, tree:, agents:, options:)
|
|
145
|
+
web = web_for(live, root:, tree:, options:)
|
|
146
|
+
runner = Engine.runner(
|
|
147
|
+
tree:,
|
|
148
|
+
agents:,
|
|
149
|
+
isolation:,
|
|
150
|
+
jobs:,
|
|
151
|
+
plans:,
|
|
152
|
+
state:,
|
|
153
|
+
worktree: (OpenLanes::Vcs.worktree(root:) if isolation == :worktree),
|
|
154
|
+
rounds: options[:rounds]&.to_i,
|
|
155
|
+
executor: Execution.executor(root:,
|
|
156
|
+
timeout:,
|
|
157
|
+
dry_run: dry_run?(options),
|
|
158
|
+
instructions: instructions_from(options[:prompt]),
|
|
159
|
+
model: options[:model],
|
|
160
|
+
max_tokens: options[:max_tokens]&.to_i,
|
|
161
|
+
lean: !options[:user_config]),
|
|
162
|
+
dry_run: dry_run?(options),
|
|
163
|
+
publisher: publisher_for(root, isolation, options),
|
|
164
|
+
on_board: live&.on_board,
|
|
165
|
+
claims: live&.claims,
|
|
166
|
+
**staying(options, web)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
started = UI.monotonic
|
|
170
|
+
attempts = begin
|
|
171
|
+
Execution.on_interrupt(signals:) { runner.call { |dispatcher| live&.attach(dispatcher) } }
|
|
172
|
+
ensure
|
|
173
|
+
live&.close
|
|
174
|
+
web&.stop
|
|
175
|
+
end
|
|
176
|
+
finish(runner, attempts, options, seconds: UI.monotonic - started)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
# What the runner is told about idling past its fixed points. With its
|
|
182
|
+
# control room up the engine stays, since the page can answer a blocked
|
|
183
|
+
# plan or create the next one at any moment, and a run that had ended
|
|
184
|
+
# would leave the page talking to nobody. Headless, or with
|
|
185
|
+
# `--until-settled`, it ends at the first fixed point.
|
|
186
|
+
#
|
|
187
|
+
# @param options [Hash]
|
|
188
|
+
# @param web [OpenLanes::Web::Launcher, nil] the control room, when it came up
|
|
189
|
+
# @return [Hash] `stay:` and `rescan:` for {Engine::Runner}, or nothing
|
|
190
|
+
def staying(options, web)
|
|
191
|
+
return {} if web.nil? || options[:until_settled]
|
|
192
|
+
|
|
193
|
+
info("The engine stays up with its control room: Stop All Local or Ctrl-C ends it.") unless quiet?(options)
|
|
194
|
+
{ stay: true, rescan: Engine::Dispatcher::RESCAN }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Signals that stop the run gracefully, the way the first Ctrl-C does.
|
|
198
|
+
# TERM too: a run with its control room stays up like a service, and a
|
|
199
|
+
# service manager stops a service that way.
|
|
200
|
+
#
|
|
201
|
+
# @return [Array<String>]
|
|
202
|
+
def signals = %w[INT TERM]
|
|
203
|
+
|
|
204
|
+
# The closing report, and the exit status: 1 when any attempt failed.
|
|
205
|
+
#
|
|
206
|
+
# @param runner [OpenLanes::Engine::Runner]
|
|
207
|
+
# @param attempts [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
208
|
+
# @param options [Hash]
|
|
209
|
+
# @param seconds [Float]
|
|
210
|
+
# @return [void]
|
|
211
|
+
def finish(runner, attempts, options, seconds:)
|
|
212
|
+
report(runner, attempts, options, seconds:)
|
|
213
|
+
exit(failures(attempts).empty? ? 0 : 1)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Where the browser is sent once the control room answers.
|
|
217
|
+
#
|
|
218
|
+
# @param web [OpenLanes::Web::Launcher]
|
|
219
|
+
# @param _tree [OpenLanes::Plans::Tree]
|
|
220
|
+
# @return [String]
|
|
221
|
+
def browser_url(web, _tree) = web.url
|
|
222
|
+
|
|
223
|
+
# A live board this run cannot do without. `run` goes on headless, and
|
|
224
|
+
# {Start}, whose page is its only way in, refuses instead.
|
|
225
|
+
#
|
|
226
|
+
# @param message [String] why there is no board
|
|
227
|
+
# @param options [Hash]
|
|
228
|
+
# @return [nil]
|
|
229
|
+
def without_live(message, options)
|
|
230
|
+
return nil if quiet?(options)
|
|
231
|
+
|
|
232
|
+
warn("#{message}\n\nThe run goes on without the live board: no page, no commands from it, and no " \
|
|
233
|
+
"plan claims shared with other machines. Start redis-server, or pass --redis-url.")
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# @param web [OpenLanes::Web::Launcher] one that did not answer
|
|
238
|
+
# @param _live [OpenLanes::Live::Session]
|
|
239
|
+
# @param options [Hash]
|
|
240
|
+
# @return [nil]
|
|
241
|
+
def without_web(web, _live, options)
|
|
242
|
+
return nil if quiet?(options)
|
|
243
|
+
|
|
244
|
+
warn("The control room did not answer on #{web.url}, so the run goes on without it. Its log: #{web.log}")
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# The live board (W6, W13, W26), when this run has one: not on a dry
|
|
249
|
+
# run, which must leave nothing behind, and not without Redis, where
|
|
250
|
+
# the run goes on exactly as it did before there was a page. Either
|
|
251
|
+
# way it is said once, so a page that never updates is not a mystery.
|
|
252
|
+
#
|
|
253
|
+
# @param root [String]
|
|
254
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
255
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
256
|
+
# @param options [Hash]
|
|
257
|
+
# @param live [Boolean] false runs without Redis at all, for the
|
|
258
|
+
# headless flags a later command adds
|
|
259
|
+
# @return [OpenLanes::Live::Session, nil]
|
|
260
|
+
def live_for(root:, tree:, agents:, options:, live: true)
|
|
261
|
+
return nil unless live
|
|
262
|
+
|
|
263
|
+
if dry_run?(options)
|
|
264
|
+
warn("Dry run: the live board is off, so nothing is published to Redis and no plan is claimed.") unless quiet?(options)
|
|
265
|
+
return nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
config = Config.current
|
|
269
|
+
store = Live.connect(Live.redis_url(option: options[:redis_url], config:))
|
|
270
|
+
Live.session(store:, root:, tree:, agents:, project: config.project).tap do |session|
|
|
271
|
+
info("Live board: #{session.describe}") unless quiet?(options)
|
|
272
|
+
end
|
|
273
|
+
rescue Live::Unavailable => e
|
|
274
|
+
without_live(e.message, options)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The control room (W1, W7): `openlanes web` as its own process beside
|
|
278
|
+
# the engine, reading the board this run publishes. Only with a live
|
|
279
|
+
# board to read, so a dry run, a run without Redis and `--no-web` all
|
|
280
|
+
# stay headless. A server that does not come up is reported and left
|
|
281
|
+
# out; the run is the agents' work, and the page is a view of it.
|
|
282
|
+
#
|
|
283
|
+
# @param live [OpenLanes::Live::Session, nil]
|
|
284
|
+
# @param root [String]
|
|
285
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
286
|
+
# @param options [Hash]
|
|
287
|
+
# @return [OpenLanes::Web::Launcher, nil] running, for the run to stop
|
|
288
|
+
def web_for(live, root:, tree:, options:)
|
|
289
|
+
return nil if live.nil? || options[:web] == false
|
|
290
|
+
|
|
291
|
+
web = OpenLanes::Web.launcher(root:,
|
|
292
|
+
plans_dir: tree.dir,
|
|
293
|
+
port: options[:port] || OpenLanes::Web::DEFAULT_PORT,
|
|
294
|
+
redis_url: live.store.url).start
|
|
295
|
+
unless web.ready?
|
|
296
|
+
web.stop
|
|
297
|
+
return without_web(web, live, options)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
info("Control room: #{web.url}") unless quiet?(options)
|
|
301
|
+
OpenLanes::Web.open(browser_url(web, tree)) unless options[:open] == false
|
|
302
|
+
web
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The attempts list, padded into columns.
|
|
306
|
+
#
|
|
307
|
+
# Tabs used to separate these, which put every column on the next
|
|
308
|
+
# multiple of eight: one agent's name a character longer than the
|
|
309
|
+
# next one's pushed its round and mark a whole stop to the right, so
|
|
310
|
+
# nothing under `luke-backend` and `yoda-writer` lined up. Widths come
|
|
311
|
+
# from the rows themselves, so the columns are as narrow as the run
|
|
312
|
+
# allows and still square.
|
|
313
|
+
#
|
|
314
|
+
# @param attempts [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
315
|
+
# @return [Array<String>] one line per attempt, indented by two
|
|
316
|
+
def attempt_lines(attempts)
|
|
317
|
+
rows = attempts.map { |a| [a.ordinal.to_s, a.agent.to_s, "[R:#{a.round}]", mark(a), a.note.to_s] }
|
|
318
|
+
widths = rows.transpose.map { |column| column.map { |cell| UI.display_width(cell) }.max.to_i }
|
|
319
|
+
rows.map do |cells|
|
|
320
|
+
# The note is last and is left as it is: padding the final column
|
|
321
|
+
# only adds trailing whitespace a copy-paste then carries.
|
|
322
|
+
last = cells.size - 1
|
|
323
|
+
" #{cells.each_with_index.map { |cell, i| i == last ? cell : UI.fit(cell, widths[i] + 2) }.join}"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @param attempt [OpenLanes::Engine::Runner::Attempt]
|
|
328
|
+
# @return [String] what the attempt did to its plan's state
|
|
329
|
+
def mark(attempt)
|
|
330
|
+
return "FAIL" unless attempt.ok
|
|
331
|
+
return "#{attempt.from} -> #{attempt.to}" if attempt.advanced?
|
|
332
|
+
|
|
333
|
+
"no change"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# `--prompt` takes the instructions themselves or a file holding them.
|
|
337
|
+
# A short argument naming an existing file is read; anything else is
|
|
338
|
+
# the text, so a sentence that happens to match a filename in the
|
|
339
|
+
# project is not silently swapped for that file once it is long enough
|
|
340
|
+
# to be a sentence.
|
|
341
|
+
#
|
|
342
|
+
# @param prompt [String, nil]
|
|
343
|
+
# @return [String, nil]
|
|
344
|
+
def instructions_from(prompt)
|
|
345
|
+
return prompt unless prompt && prompt.length < PROMPT_PATH_LIMIT && File.file?(typed_path(prompt))
|
|
346
|
+
|
|
347
|
+
File.read(typed_path(prompt))
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
351
|
+
# @param name [String]
|
|
352
|
+
# @return [OpenLanes::Agents::Registry] holding only the agent named — the
|
|
353
|
+
# restriction has to reach assignments, not just chaining, or a flag
|
|
354
|
+
# like `--prompt` aimed at one agent leaks to the whole round
|
|
355
|
+
def filtered(agents, name)
|
|
356
|
+
agents.find(name) or
|
|
357
|
+
refuse("No agent called #{name}.\n\nKnown: #{agents.all.map(&:name).join(", ")}", 65)
|
|
358
|
+
agents.only(name)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Each name is checked against the full roster, not the possibly
|
|
362
|
+
# already-restricted one, so `--skip` misspelled is a refusal rather
|
|
363
|
+
# than a silent no-op — the same reasoning `--plan` applies to numbers.
|
|
364
|
+
#
|
|
365
|
+
# @param agents [OpenLanes::Agents::Registry]
|
|
366
|
+
# @param text [String] comma-separated agent names
|
|
367
|
+
# @param restricted [String, nil] what `--agent` asked for, if anything
|
|
368
|
+
# @return [OpenLanes::Agents::Registry] without the agents named
|
|
369
|
+
def skipped(agents, text, restricted)
|
|
370
|
+
roster = OpenLanes::Agents.registry
|
|
371
|
+
names = text.split(",").map(&:strip).reject(&:empty?)
|
|
372
|
+
names.each do |name|
|
|
373
|
+
roster.find(name) or
|
|
374
|
+
refuse("No agent called #{name}.\n\nKnown: #{roster.all.map(&:name).join(", ")}", 65)
|
|
375
|
+
next unless name == restricted
|
|
376
|
+
|
|
377
|
+
refuse("--agent #{name} and --skip #{name} contradict each other: " \
|
|
378
|
+
"one asks for only this agent, the other for anything but.",
|
|
379
|
+
64)
|
|
380
|
+
end
|
|
381
|
+
agents.without(*names)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# A spec.md whose frontmatter does not parse routes as if it had none:
|
|
385
|
+
# lane full, every override dropped. An eval agent's unquoted backtick
|
|
386
|
+
# sent a quick-lane rename to research that way, and said nothing.
|
|
387
|
+
# {Plans::Spec#problems} already knew; say it before agents are assigned.
|
|
388
|
+
#
|
|
389
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
390
|
+
# @param plans [Array<OpenLanes::Plans::Ordinal>, nil] the --plan ordinals, nil for all
|
|
391
|
+
# @return [void]
|
|
392
|
+
def spec_warning(tree, plans)
|
|
393
|
+
lines = tree.subjects.filter_map { |subject|
|
|
394
|
+
ordinal = subject.feature.ordinal
|
|
395
|
+
next if plans && !plans.include?(ordinal)
|
|
396
|
+
|
|
397
|
+
problems = Plans::Spec.for(subject).problems
|
|
398
|
+
"#{ordinal}: #{problems.join("; ")}" unless problems.empty?
|
|
399
|
+
}
|
|
400
|
+
return if lines.empty?
|
|
401
|
+
|
|
402
|
+
warn("spec.md frontmatter ignored in part, so these plans route on defaults:\n" \
|
|
403
|
+
"#{lines.join("\n")}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# `--plan` names the whole point of scoping: a skill that just minted
|
|
407
|
+
# N folders hands off exactly those, not the tree. A typo'd or already-
|
|
408
|
+
# settled number silently running the whole tree instead is the failure
|
|
409
|
+
# this exists to prevent, so an unknown one is refused rather than
|
|
410
|
+
# dropped.
|
|
411
|
+
#
|
|
412
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
413
|
+
# @param text [String]
|
|
414
|
+
# @return [Array<OpenLanes::Plans::Ordinal>]
|
|
415
|
+
def scoped(tree, text)
|
|
416
|
+
known = tree.subjects.map { |s| s.feature.ordinal }
|
|
417
|
+
text.split(",").map(&:strip).reject(&:empty?).map { |token|
|
|
418
|
+
ordinal = Plans::Ordinal.parse(token)
|
|
419
|
+
unless ordinal && known.include?(ordinal)
|
|
420
|
+
refuse("No plan #{token} in #{tree.dir}.\n\n" \
|
|
421
|
+
"Known: #{known.join(", ")}",
|
|
422
|
+
66)
|
|
423
|
+
end
|
|
424
|
+
ordinal
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# `--agent` narrowing the round to nobody used to exit 0 in silence —
|
|
429
|
+
# "0 plans addressed", green box, no hint that the agent named simply
|
|
430
|
+
# does not handle any in-scope plan's state. Someone who typed both an
|
|
431
|
+
# agent and a plan meant them to meet, so a pairing that cannot happen
|
|
432
|
+
# is refused up front with the agent that would actually take each plan.
|
|
433
|
+
#
|
|
434
|
+
# @param agents [OpenLanes::Agents::Registry] already narrowed to the one agent
|
|
435
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
436
|
+
# @param plans [Array<OpenLanes::Plans::Ordinal>, nil]
|
|
437
|
+
# @param name [String]
|
|
438
|
+
# @return [void]
|
|
439
|
+
def assignable!(agents, tree, plans, name)
|
|
440
|
+
agent = agents.find(name)
|
|
441
|
+
active = tree.subjects.select { |s|
|
|
442
|
+
(plans.nil? || plans.include?(s.feature.ordinal)) &&
|
|
443
|
+
!Plans::StateMachine::SETTLED.include?(s.status.key)
|
|
444
|
+
}
|
|
445
|
+
return if active.empty? || active.any? { |s| agent.handles?(s.status) }
|
|
446
|
+
|
|
447
|
+
roster = OpenLanes::Agents.registry
|
|
448
|
+
lines = active.map { |s|
|
|
449
|
+
takers = OpenLanes::Agents::Routing.filter(roster.for_status(s.status), s).map(&:name)
|
|
450
|
+
verb = takers.size == 1 ? "takes" : "take"
|
|
451
|
+
" #{s.feature.ordinal} is #{s.status.emoji} #{s.status.label}" \
|
|
452
|
+
"#{" - #{takers.join(" and ")} #{verb} it" unless takers.empty?}"
|
|
453
|
+
}
|
|
454
|
+
handled = agent.handles.map { |k| OpenLanes::Plans::STATUS_BY_KEY[k]&.then { |st| "#{st.emoji} #{st.label}" } || k }
|
|
455
|
+
refuse("#{name} handles #{handled.join(", ")}, and no plan in scope is there:\n\n" \
|
|
456
|
+
"#{lines.join("\n")}\n\nName the agent that takes these states, or drop --agent.",
|
|
457
|
+
65)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# @param attempts [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
461
|
+
# @return [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
462
|
+
def failures(attempts) = attempts.reject(&:ok)
|
|
463
|
+
|
|
464
|
+
# The one thing this harness does that leaves the machine — pushing a
|
|
465
|
+
# branch and opening its pull request — so it takes one more thing to
|
|
466
|
+
# turn on than everything else here: not being a dry run is not enough,
|
|
467
|
+
# isolation has to actually give each plan a branch of its own too.
|
|
468
|
+
#
|
|
469
|
+
# `nil` is how {Runner} is told to leave a finished worktree alone; it
|
|
470
|
+
# is what `--no-git-push` asks for, and what a shared tree gets
|
|
471
|
+
# by construction, since there is no separate branch there to push.
|
|
472
|
+
#
|
|
473
|
+
# @param root [String]
|
|
474
|
+
# @param isolation [Symbol]
|
|
475
|
+
# @param options [Hash]
|
|
476
|
+
# @return [OpenLanes::Vcs::Publisher, nil]
|
|
477
|
+
def publisher_for(root, isolation, options)
|
|
478
|
+
if isolation != :worktree || options[:git_push] == false
|
|
479
|
+
unless quiet?(options)
|
|
480
|
+
warn("No pull request will be opened (#{isolation == :worktree ? "--no-git-push" : "--isolation shared"}), " \
|
|
481
|
+
"so a finished build stays at 🟡 Building:\n🟢 Ready for Review needs an open pull request, and hansolo-reviewer reviews one.")
|
|
482
|
+
end
|
|
483
|
+
return nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
Vcs.publisher(root:, dry_run: dry_run?(options))
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# One line per attempt: the plan, who ran, which of that agent's rounds
|
|
490
|
+
# it was, what moved, and the note. There is no round header any more
|
|
491
|
+
# because the loop has no rounds of its own: each agent counts its own
|
|
492
|
+
# per plan, so two agents on one plan can be on different numbers.
|
|
493
|
+
#
|
|
494
|
+
# @param runner [OpenLanes::Engine::Runner]
|
|
495
|
+
# @param attempts [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
496
|
+
# @param options [Hash]
|
|
497
|
+
# @param seconds [Float] wall clock for the whole loop
|
|
498
|
+
# @return [void]
|
|
499
|
+
def report(runner, attempts, options, seconds: 0.0)
|
|
500
|
+
# A leading blank line, because the spinners this replaces write to
|
|
501
|
+
# STDERR and the last of them may have left the cursor mid-line:
|
|
502
|
+
# without it the first attempt starts wherever that line stopped,
|
|
503
|
+
# indented by however much it had drawn.
|
|
504
|
+
puts ""
|
|
505
|
+
puts attempt_lines(attempts)
|
|
506
|
+
|
|
507
|
+
# What the run cost, on STDOUT with the attempts it belongs to, so a run
|
|
508
|
+
# redirected to a file keeps its bill. A dry run spent nothing and gets
|
|
509
|
+
# none of this.
|
|
510
|
+
tally = Engine.tally(attempts:, seconds:)
|
|
511
|
+
puts("", tally.render) unless dry_run?(options)
|
|
512
|
+
|
|
513
|
+
return if quiet?(options)
|
|
514
|
+
|
|
515
|
+
if Execution::Control.quit?
|
|
516
|
+
warn("Stopped from the keyboard — q. Unfinished plans keep their state; " \
|
|
517
|
+
"re-run to resume where they stand.")
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
advanced = attempts.count(&:advanced?)
|
|
521
|
+
blocked = runner.blocked
|
|
522
|
+
summary = ["#{attempts.size} invocation#{"s" unless attempts.size == 1}", "#{advanced} advanced"]
|
|
523
|
+
summary << (runner.isolated? ? "#{runner.jobs} at a time, one worktree each" : "serial, shared tree")
|
|
524
|
+
summary << "#{blocked.size} blocked" unless blocked.empty?
|
|
525
|
+
summary << "#{failures(attempts).size} failed" unless failures(attempts).empty?
|
|
526
|
+
|
|
527
|
+
if dry_run?(options)
|
|
528
|
+
warn("Dry run — no agent was invoked.\n#{summary.join(" · ")}\n\nRe-run without --dry-run.")
|
|
529
|
+
elsif failures(attempts).empty?
|
|
530
|
+
success("#{summary.join(" · ")}#{"\n\nEvery plan is done or deliberately parked." if runner.settled?}")
|
|
531
|
+
else
|
|
532
|
+
error("#{summary.join(" · ")}\n\n#{failures(attempts).map { |f| "#{f.ordinal} #{f.agent}: #{f.note}" }.join("\n")}")
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
return if blocked.empty?
|
|
536
|
+
|
|
537
|
+
warn("#{blocked.size} plan#{"s" unless blocked.size == 1} need a human decision:\n" +
|
|
538
|
+
blocked.map { |b| " #{b.feature.ordinal} #{b.status.emoji} #{b.feature.title} — see blocked.md" }.join("\n") +
|
|
539
|
+
"\n\nWrite the answers into blocked.md, then: openlanes unblock " \
|
|
540
|
+
"#{blocked.map { |b| b.feature.ordinal }.join(",")}")
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes skills …`
|
|
6
|
+
module Skills
|
|
7
|
+
# `skills install` — copy the bundled skills where agents look for them.
|
|
8
|
+
class Install < Base
|
|
9
|
+
standalone!
|
|
10
|
+
|
|
11
|
+
SUMMARY = "Copy the skills bundled with this gem into a skill directory"
|
|
12
|
+
|
|
13
|
+
# The help screen lists what is bundled, read from each SKILL.md when
|
|
14
|
+
# the help renders, so a skill added to `skills/` shows up here
|
|
15
|
+
# without anybody editing this command. One paragraph per skill,
|
|
16
|
+
# because dry-cli-help reflows paragraphs to the terminal's width.
|
|
17
|
+
#
|
|
18
|
+
# The `skills` group screen asks for the same description to fill one
|
|
19
|
+
# row of its command list, where five paragraphs would bury the row;
|
|
20
|
+
# it gets the summary alone.
|
|
21
|
+
#
|
|
22
|
+
# @return [String]
|
|
23
|
+
def self.description
|
|
24
|
+
skills = OpenLanes::Skills.bundled
|
|
25
|
+
return SUMMARY if skills.empty? || listing?
|
|
26
|
+
|
|
27
|
+
rows = skills.map do |skill|
|
|
28
|
+
mark = skill.installed?(OpenLanes::Skills::DESTINATION) ? "installed" : "not installed"
|
|
29
|
+
"#{skill.name} (#{mark}) — #{skill.summary}"
|
|
30
|
+
end
|
|
31
|
+
"#{SUMMARY}, #{OpenLanes::Skills::DESTINATION} by default.\n\n#{rows.join("\n\n")}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether the caller is a group screen's command list, which dry-cli-help
|
|
35
|
+
# fills through `describe_node` (and, in later releases, its
|
|
36
|
+
# `node_description` helper).
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def self.listing?
|
|
40
|
+
caller_locations(2, 3).any? { it.label.end_with?("describe_node", "node_description") }
|
|
41
|
+
end
|
|
42
|
+
private_class_method :listing?
|
|
43
|
+
|
|
44
|
+
option :into,
|
|
45
|
+
default: OpenLanes::Skills::DESTINATION,
|
|
46
|
+
desc: "The skill directory to copy into"
|
|
47
|
+
option :overwrite,
|
|
48
|
+
type: :boolean,
|
|
49
|
+
default: false,
|
|
50
|
+
desc: "Replace a skill already there, which is otherwise kept"
|
|
51
|
+
dry_run_option "Copy nothing; print what would be installed, kept and replaced"
|
|
52
|
+
|
|
53
|
+
example [
|
|
54
|
+
" # copy each skill not yet installed",
|
|
55
|
+
"--overwrite # …and replace the ones that are, after a gem upgrade",
|
|
56
|
+
"--into ~/.claude/skills # somewhere else",
|
|
57
|
+
"-n # show what would happen"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# @param options [Hash]
|
|
61
|
+
# @return [void]
|
|
62
|
+
def call(**options)
|
|
63
|
+
quiet?(options)
|
|
64
|
+
installer = OpenLanes::Skills.installer(into: options.fetch(:into, OpenLanes::Skills::DESTINATION))
|
|
65
|
+
outcomes = installer.call(overwrite: options.fetch(:overwrite, false), dry_run: dry_run?(options))
|
|
66
|
+
|
|
67
|
+
outcomes.each { |o| puts "#{o.action}\t#{o.skill.name}\t#{o.target}" }
|
|
68
|
+
report(outcomes, options) unless quiet?(options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @param outcomes [Array<OpenLanes::Skills::Installer::Outcome>]
|
|
74
|
+
# @param options [Hash]
|
|
75
|
+
# @return [void]
|
|
76
|
+
def report(outcomes, options)
|
|
77
|
+
counts = outcomes.group_by(&:action).transform_values(&:size)
|
|
78
|
+
summary = %i[installed replaced kept].filter_map { |action|
|
|
79
|
+
"#{counts[action]} #{action}" if counts[action]
|
|
80
|
+
}.join(" · ")
|
|
81
|
+
kept = counts.key?(:kept) ? "\n\nA kept skill is left as it was; --overwrite replaces it." : ""
|
|
82
|
+
|
|
83
|
+
if dry_run?(options)
|
|
84
|
+
warn("Dry run — nothing was copied.\nWould be: #{summary}#{kept}\n\nRe-run without --dry-run to apply.")
|
|
85
|
+
else
|
|
86
|
+
success("#{summary}#{kept}")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|