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,671 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/hash"
|
|
4
|
+
require "etc"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "dry/cli/ui"
|
|
7
|
+
require "pastel"
|
|
8
|
+
require "stringio"
|
|
9
|
+
require "tty/screen"
|
|
10
|
+
require "unicode/display_width"
|
|
11
|
+
|
|
12
|
+
module OpenLanes
|
|
13
|
+
# Everything the user sees that is not the deliverable itself.
|
|
14
|
+
#
|
|
15
|
+
# Include it and you get `info`, `warn`, `error` and `success` as instance
|
|
16
|
+
# methods, each drawing a dry-cli-ui box on **STDERR**. STDERR is deliberate:
|
|
17
|
+
# the documents and tables these commands produce own STDOUT, so every
|
|
18
|
+
# command composes in a pipe.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# class Thing
|
|
22
|
+
# include OpenLanes::UI
|
|
23
|
+
# def run = success("Linked 4 skills")
|
|
24
|
+
# end
|
|
25
|
+
module UI
|
|
26
|
+
# Widest a box may be drawn, regardless of how wide the terminal is.
|
|
27
|
+
MAX_WIDTH = 100
|
|
28
|
+
|
|
29
|
+
# Narrowest, so a small terminal still produces readable boxes.
|
|
30
|
+
MIN_WIDTH = 60
|
|
31
|
+
|
|
32
|
+
# Below this many items a progress bar is noise: it appears and vanishes
|
|
33
|
+
# before the eye resolves it, and the line it prints is longer than the work.
|
|
34
|
+
PROGRESS_THRESHOLD = 3
|
|
35
|
+
|
|
36
|
+
# What an item contributes to its log line's columns when its caller has
|
|
37
|
+
# nothing to say about it. A round header is such an item, and it still
|
|
38
|
+
# has to line up with the agent lines under it.
|
|
39
|
+
NO_FIELDS = ->(_item) { {} }
|
|
40
|
+
|
|
41
|
+
# What an item's countdown starts from when its caller has no opinion:
|
|
42
|
+
# nothing, so no timer is drawn. A line without a deadline showing 0:00
|
|
43
|
+
# forever would read as an agent perpetually out of time.
|
|
44
|
+
NO_TIMEOUT = ->(_item) {}
|
|
45
|
+
|
|
46
|
+
# The countdown turns red with this many seconds left — late enough to
|
|
47
|
+
# stay calm through a normal run, early enough to look up before the
|
|
48
|
+
# executor pulls the plug.
|
|
49
|
+
TIMER_WARNING = 60
|
|
50
|
+
|
|
51
|
+
# How a block's RETURN VALUE is judged when its caller has no opinion:
|
|
52
|
+
# nothing is ever a failure. The distinction exists because {Runner}'s
|
|
53
|
+
# executor reports failure by returning a not-ok result rather than by
|
|
54
|
+
# raising — and a line that drew ✓ "done" over a timed-out agent, while
|
|
55
|
+
# the round table under it said FAIL, was the contradiction this closes.
|
|
56
|
+
NO_FAILURE = ->(_result) {}
|
|
57
|
+
|
|
58
|
+
# Longest phrase a spinner line can carry without wrapping into the next.
|
|
59
|
+
SAID_LIMIT = 56
|
|
60
|
+
|
|
61
|
+
# One item's line on the screen, and the same news written to the log.
|
|
62
|
+
#
|
|
63
|
+
# These are two readers of one story and used to be told it separately:
|
|
64
|
+
# the spinner got the phrase, the log got a start and a finish, and an
|
|
65
|
+
# animated run wrote nothing about what any agent was actually doing. A
|
|
66
|
+
# {OpenLanes::Execution::Transcript::Progress} arrives here several times a
|
|
67
|
+
# second; the line is redrawn every time, and the log takes a line only
|
|
68
|
+
# when the phrase itself changes, which is a few dozen times an agent.
|
|
69
|
+
#
|
|
70
|
+
# The drawing belongs to dry-cli-ui. This keeps the parts the text after
|
|
71
|
+
# the agent's name is made of — identity, countdown, meter, phrase — and
|
|
72
|
+
# hands the whole of it to the {Dry::CLI::UI::Line} whenever one changes.
|
|
73
|
+
class Line
|
|
74
|
+
# @param fields [Hash] plan, status and agent, for the log's columns
|
|
75
|
+
# @param handle [Dry::CLI::UI::Line, nil] what dry-cli-ui draws this
|
|
76
|
+
# item's line from; nil where nothing is being drawn
|
|
77
|
+
# @param timeout [Integer, nil] seconds until the executor abandons
|
|
78
|
+
# this agent; drawn as a countdown on the line, nil draws nothing
|
|
79
|
+
def initialize(fields: {}, handle: nil, timeout: nil)
|
|
80
|
+
@fields = fields
|
|
81
|
+
@handle = handle
|
|
82
|
+
@timeout = timeout
|
|
83
|
+
@started = UI.monotonic
|
|
84
|
+
@phrase = nil
|
|
85
|
+
@pid = nil
|
|
86
|
+
@ticker = nil
|
|
87
|
+
# The meter starts at zero rather than appearing with the first
|
|
88
|
+
# number, which would shift everything after it sideways.
|
|
89
|
+
@parts = { pid: "", timer: "", meter: UI.meter(nil), activity: "" }
|
|
90
|
+
@lock = Mutex.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Float] seconds this agent has been alive
|
|
94
|
+
def seconds = UI.monotonic - @started
|
|
95
|
+
|
|
96
|
+
# @return [String] the same, as the log and the report write it
|
|
97
|
+
def alive = "#{seconds.round}s"
|
|
98
|
+
|
|
99
|
+
# @param message [String]
|
|
100
|
+
# @return [void]
|
|
101
|
+
def note(message) = UI.log(message, **@fields, seconds:)
|
|
102
|
+
|
|
103
|
+
# @return [void]
|
|
104
|
+
def start
|
|
105
|
+
redraw(pid: identity)
|
|
106
|
+
tick
|
|
107
|
+
note("started")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# What remains on this agent's clock, or nil when it has none. Floored
|
|
111
|
+
# at zero: the executor's kill and this thread's wake-up race by up to
|
|
112
|
+
# a second, and a line reading `-0:01` accuses the wrong party.
|
|
113
|
+
#
|
|
114
|
+
# @return [Integer, nil]
|
|
115
|
+
def remaining
|
|
116
|
+
return nil unless @timeout
|
|
117
|
+
|
|
118
|
+
[@timeout - seconds, 0].max.round
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The countdown, redrawn once a second on its own thread. Progress
|
|
122
|
+
# updates cannot drive it — they arrive only while the agent is
|
|
123
|
+
# talking, and a stalled agent is exactly when the clock matters most.
|
|
124
|
+
#
|
|
125
|
+
# @return [void]
|
|
126
|
+
def tick
|
|
127
|
+
return unless @handle && @timeout
|
|
128
|
+
|
|
129
|
+
redraw(timer: UI.countdown(remaining))
|
|
130
|
+
@ticker ||= Thread.new do
|
|
131
|
+
loop do
|
|
132
|
+
sleep(1)
|
|
133
|
+
left = remaining
|
|
134
|
+
redraw(timer: UI.countdown(left))
|
|
135
|
+
break unless left.positive?
|
|
136
|
+
end
|
|
137
|
+
rescue StandardError
|
|
138
|
+
# A dying line must not take the round down with it.
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @return [void]
|
|
143
|
+
def stop_ticker
|
|
144
|
+
@ticker&.kill
|
|
145
|
+
@ticker = nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# What sits between the agent's name and its activity: the `claude`
|
|
149
|
+
# process and the round, `[36123, round 01]`, so a line on the screen
|
|
150
|
+
# can be matched to a process in `ps` and a row in the report. The pid
|
|
151
|
+
# is unknowable until the child has been spawned and found, so it reads
|
|
152
|
+
# `…` until then; a caller with neither fact gets nothing at all.
|
|
153
|
+
#
|
|
154
|
+
# @return [String]
|
|
155
|
+
def identity
|
|
156
|
+
round = @fields[:round]
|
|
157
|
+
return "" if round.nil? && @pid.nil?
|
|
158
|
+
|
|
159
|
+
inner = [(@pid || "…").to_s, ("round #{round}" if round)].compact.join(", ")
|
|
160
|
+
UI.paint("[#{inner}]", :bright_black)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# What the line shows after the agent's name.
|
|
164
|
+
#
|
|
165
|
+
# @return [String] e.g. `[36123, round 01] 14:59 ↑4.9k ↓512: reading spec.md`
|
|
166
|
+
def detail
|
|
167
|
+
parts = @lock.synchronize { @parts.dup }
|
|
168
|
+
[parts[:pid], parts[:timer] + parts[:meter]].map(&:rstrip).reject(&:empty?).join(" ") + parts[:activity]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @return [void]
|
|
172
|
+
def done
|
|
173
|
+
stop_ticker
|
|
174
|
+
note("finished after #{alive}")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Ends the line as a failure without raising: the executor reports a
|
|
178
|
+
# timed-out agent by returning, and the line must still say 𝘅.
|
|
179
|
+
#
|
|
180
|
+
# @param reason [String]
|
|
181
|
+
# @return [void]
|
|
182
|
+
def failed(reason)
|
|
183
|
+
stop_ticker
|
|
184
|
+
@handle&.fail(reason)
|
|
185
|
+
note("failed after #{alive}: #{reason}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @param update [OpenLanes::Execution::Transcript::Progress]
|
|
189
|
+
# @return [void]
|
|
190
|
+
def call(update)
|
|
191
|
+
if update.respond_to?(:pid) && update.pid && update.pid != @pid
|
|
192
|
+
@pid = update.pid
|
|
193
|
+
redraw(pid: identity)
|
|
194
|
+
note("claude is pid #{@pid}")
|
|
195
|
+
end
|
|
196
|
+
redraw(meter: UI.meter(update), activity: UI.said(update.activity))
|
|
197
|
+
return if update.activity.nil? || update.activity == @phrase
|
|
198
|
+
|
|
199
|
+
@phrase = update.activity
|
|
200
|
+
note(update.activity)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# So a caller can pass this straight on as a block: {Executor} yields
|
|
204
|
+
# to it from the thread it reads the agent's stream on.
|
|
205
|
+
#
|
|
206
|
+
# @return [Proc]
|
|
207
|
+
def to_proc = method(:call).to_proc
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
# Called from the reader thread and the ticker at once, which is why
|
|
212
|
+
# the parts are replaced under a lock rather than assembled in place.
|
|
213
|
+
#
|
|
214
|
+
# @param parts [Hash{Symbol => String}]
|
|
215
|
+
# @return [void]
|
|
216
|
+
def redraw(**parts)
|
|
217
|
+
@lock.synchronize { @parts.merge!(parts) }
|
|
218
|
+
@handle&.detail = detail
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
class << self
|
|
223
|
+
# Set by the CLI's --quiet. Silences spinners and bars along with
|
|
224
|
+
# everything else, so a quiet run really is quiet.
|
|
225
|
+
#
|
|
226
|
+
# @return [Boolean]
|
|
227
|
+
attr_accessor :quiet
|
|
228
|
+
|
|
229
|
+
# @return [Pastel] colour engine, disabled when STDERR is not a terminal
|
|
230
|
+
def pastel = @pastel ||= Pastel.new(enabled: color?)
|
|
231
|
+
|
|
232
|
+
# Forget everything memoized here.
|
|
233
|
+
#
|
|
234
|
+
# Pastel captures `enabled:` once, at construction. Anything that changes
|
|
235
|
+
# the answer afterwards — a test stubbing `tty?`, a caller setting
|
|
236
|
+
# NO_COLOR late — would otherwise be ignored for the rest of the process,
|
|
237
|
+
# and the first answer would leak into every later call.
|
|
238
|
+
#
|
|
239
|
+
# @return [void]
|
|
240
|
+
def reset!
|
|
241
|
+
remove_instance_variable(:@pastel) if instance_variable_defined?(:@pastel)
|
|
242
|
+
self.quiet = false
|
|
243
|
+
self.log_path = nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Where {.log} appends to, if anywhere. nil (the default) means nowhere;
|
|
247
|
+
# `openlanes run` sets this before the loop starts, so a round
|
|
248
|
+
# started under a tool that discards STDERR — an agent's own Bash call,
|
|
249
|
+
# for instance — still leaves something to `tail -f`.
|
|
250
|
+
#
|
|
251
|
+
# @return [String, nil]
|
|
252
|
+
attr_accessor :log_path
|
|
253
|
+
|
|
254
|
+
# Something else that hears every {.log} line, as it is logged: the
|
|
255
|
+
# live board keeps the latest few for the page. Taps are called on the
|
|
256
|
+
# logging thread and must not raise.
|
|
257
|
+
#
|
|
258
|
+
# @param tap [#call] receives the message and its fields
|
|
259
|
+
# @return [#call] the tap, to hand back to {.untap_log}
|
|
260
|
+
def tap_log(tap)
|
|
261
|
+
@log_taps = [*log_taps, tap]
|
|
262
|
+
tap
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @param tap [#call]
|
|
266
|
+
# @return [void]
|
|
267
|
+
def untap_log(tap)
|
|
268
|
+
@log_taps = log_taps - [tap]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# @return [Array<#call>]
|
|
272
|
+
def log_taps = @log_taps || []
|
|
273
|
+
|
|
274
|
+
# Append one timestamped line to {.log_path}. A no-op with nothing set.
|
|
275
|
+
# Safe to call from several threads at once.
|
|
276
|
+
#
|
|
277
|
+
# @param message [String]
|
|
278
|
+
# @return [void]
|
|
279
|
+
def log(message, **fields)
|
|
280
|
+
log_taps.each { |tap| tap.call(message, fields) }
|
|
281
|
+
path = log_path or return
|
|
282
|
+
|
|
283
|
+
line = Engine::ProgressLog.render(message, **fields)
|
|
284
|
+
(@log_mutex ||= Mutex.new).synchronize do
|
|
285
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
286
|
+
File.open(path, "a") { |f| f.puts(line) }
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Cells the meter takes on a spinner line, per direction.
|
|
291
|
+
#
|
|
292
|
+
# Fixed, and padded to it, because the numbers grow as the agent works and
|
|
293
|
+
# a column that sizes itself to them drags the whole line sideways every
|
|
294
|
+
# few seconds.
|
|
295
|
+
METER_WIDTH = 7
|
|
296
|
+
|
|
297
|
+
# The token counter that sits between the spinner and the agent's name.
|
|
298
|
+
#
|
|
299
|
+
# Up is everything sent, cache reads included, which is most of it. Down
|
|
300
|
+
# is what the model generated. Sub-agent spend is folded into up, since
|
|
301
|
+
# `claude` reports a sub-agent's total without splitting it.
|
|
302
|
+
#
|
|
303
|
+
# @param update [OpenLanes::Execution::Transcript::Progress, nil]
|
|
304
|
+
# @return [String]
|
|
305
|
+
def meter(update)
|
|
306
|
+
paint(fit("↑#{abbreviate(update&.up)}", METER_WIDTH), :bright_blue) +
|
|
307
|
+
paint(fit("↓#{abbreviate(update&.down)}", METER_WIDTH), :bright_magenta)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Cells the countdown takes, `MM:SS ` included — the run default of
|
|
311
|
+
# 900s reads `15:00`, and padding to a fixed width keeps the columns
|
|
312
|
+
# to its right from stepping sideways once `9:59` loses a digit.
|
|
313
|
+
TIMER_WIDTH = 6
|
|
314
|
+
|
|
315
|
+
# The countdown that sits between the spinner and the meter: what is
|
|
316
|
+
# left of the agent's timeout, quiet grey until the last
|
|
317
|
+
# {TIMER_WARNING} seconds, red from there down.
|
|
318
|
+
#
|
|
319
|
+
# @param left [Integer, nil] seconds remaining; nil draws nothing
|
|
320
|
+
# @return [String]
|
|
321
|
+
def countdown(left)
|
|
322
|
+
return "" if left.nil?
|
|
323
|
+
|
|
324
|
+
text = fit(format("%d:%02d", left / 60, left % 60), TIMER_WIDTH)
|
|
325
|
+
paint(text, left <= TIMER_WARNING ? :red : :bright_black)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Token counts run to seven figures, and seven figures on a spinner line
|
|
329
|
+
# is four cells of noise about a number nobody reads to the digit.
|
|
330
|
+
#
|
|
331
|
+
# @param count [Integer]
|
|
332
|
+
# @return [String] e.g. "512", "4.9k", "121k", "1.6M"
|
|
333
|
+
def abbreviate(count)
|
|
334
|
+
count = count.to_i
|
|
335
|
+
case count
|
|
336
|
+
when 0...1_000 then count.to_s
|
|
337
|
+
when 1_000...10_000 then "#{(count / 1000.0).round(1)}k"
|
|
338
|
+
when 10_000...1_000_000 then "#{(count / 1000.0).round}k"
|
|
339
|
+
when 1_000_000...10_000_000 then "#{(count / 1_000_000.0).round(1)}M"
|
|
340
|
+
else "#{(count / 1_000_000.0).round}M"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @param phrase [String, nil]
|
|
345
|
+
# @return [String] the phrase as a spinner line carries it, cut to
|
|
346
|
+
# {SAID_LIMIT}
|
|
347
|
+
def said(phrase)
|
|
348
|
+
text = phrase.to_s
|
|
349
|
+
return "" if text.empty?
|
|
350
|
+
|
|
351
|
+
text = "#{text[0, SAID_LIMIT - 1]}…" if text.length > SAID_LIMIT
|
|
352
|
+
paint(": #{text}", :green, :bold)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# @return [Boolean] whether STDERR is an interactive terminal
|
|
356
|
+
def tty? = $stderr.tty?
|
|
357
|
+
|
|
358
|
+
# Whether animated output is worth drawing at all. A pipe, a CI log or a
|
|
359
|
+
# --quiet run gets none: spinner frames written to a file are line noise.
|
|
360
|
+
#
|
|
361
|
+
# @return [Boolean]
|
|
362
|
+
def animate? = tty? && !quiet
|
|
363
|
+
|
|
364
|
+
# Indeterminate work — one call whose duration cannot be predicted, such
|
|
365
|
+
# as a network round trip. The spinner runs until the block returns.
|
|
366
|
+
#
|
|
367
|
+
# @param message [String] what is being waited on
|
|
368
|
+
# @yieldparam activity [Proc] phrase -> void, for news about the work
|
|
369
|
+
# @yieldreturn [Object] whatever the work produces
|
|
370
|
+
# @return [Object] the block's value, untouched
|
|
371
|
+
def spinning(message)
|
|
372
|
+
return yield(logging_activity(message)) unless animate?
|
|
373
|
+
|
|
374
|
+
console.spinner(message) { |line| yield(activity_for(line)) }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Determinate work — N items of roughly equal cost. Yields each item and
|
|
378
|
+
# advances the bar; returns the collection so it can be chained.
|
|
379
|
+
#
|
|
380
|
+
# @param items [Array]
|
|
381
|
+
# @param message [String]
|
|
382
|
+
# @yieldparam item [Object]
|
|
383
|
+
# @return [Array] +items+
|
|
384
|
+
def stepping(items, message, &)
|
|
385
|
+
list = items.to_a
|
|
386
|
+
return list.each(&) unless animate? && list.size >= PROGRESS_THRESHOLD
|
|
387
|
+
|
|
388
|
+
console.progress(message, total: list.size) do |bar|
|
|
389
|
+
list.each do |item|
|
|
390
|
+
yield item
|
|
391
|
+
bar.advance
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
list
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# The dry-cli-ui console every box, spinner and bar here draws through.
|
|
398
|
+
#
|
|
399
|
+
# Both of its streams are STDERR, because dry-cli-ui sends `info` and
|
|
400
|
+
# `success` to its `out`, and STDOUT here belongs to the deliverable.
|
|
401
|
+
# Colour and animation are decided once, by {.color?} and {.animate?},
|
|
402
|
+
# rather than by the gem looking at the stream a second time and
|
|
403
|
+
# possibly disagreeing about --quiet.
|
|
404
|
+
#
|
|
405
|
+
# Built on every call rather than memoized: `$stderr` is swapped out by
|
|
406
|
+
# the specs and by `output(...).to_stderr`, and a console holding on to
|
|
407
|
+
# the stream it was born with would write past every one of them.
|
|
408
|
+
#
|
|
409
|
+
# @return [Dry::CLI::UI::Console]
|
|
410
|
+
def console
|
|
411
|
+
Dry::CLI::UI::Console.new(out: $stderr, err: $stderr, color: color?, animate: animate?, box_width: width)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Run a block over many items at once, one line each.
|
|
415
|
+
#
|
|
416
|
+
# This is the shape for work that is independent and slow: each item gets
|
|
417
|
+
# its own line, its own thread and its own success or failure mark, so a
|
|
418
|
+
# long round reads as progress rather than as a hang.
|
|
419
|
+
#
|
|
420
|
+
# Results come back in the order the items were given, not the order they
|
|
421
|
+
# finished — a caller that had to re-sort them would be a caller that
|
|
422
|
+
# eventually forgets to.
|
|
423
|
+
#
|
|
424
|
+
# Every path here — one item, several without a terminal, several with
|
|
425
|
+
# one — reports something. `jobs <= 1 || list.size <= 1` used to bypass
|
|
426
|
+
# all of it and run silently, which is exactly the shape a `--plan
|
|
427
|
+
# NNN.MM` round takes: one plan, one agent, nothing printed until the
|
|
428
|
+
# whole thing finished and it was too late to tell "working" from "hung."
|
|
429
|
+
#
|
|
430
|
+
# Several items are a dry-cli-ui task list, capped at `jobs` at once. A
|
|
431
|
+
# failing item is caught inside its own task, so its line reads 𝘅 and
|
|
432
|
+
# its siblings carry on.
|
|
433
|
+
#
|
|
434
|
+
# @param items [Array]
|
|
435
|
+
# @param message [String] the header line
|
|
436
|
+
# @param jobs [Integer] how many run at once
|
|
437
|
+
# @param label [Proc] item -> the text on its line
|
|
438
|
+
# @param failure [Proc] the block's return value -> a reason when that
|
|
439
|
+
# value reports a failure, nil when it reports success. The block
|
|
440
|
+
# returning normally is not the same fact as the work having worked.
|
|
441
|
+
# @param header [Hash] log columns for the header line itself, so the
|
|
442
|
+
# line announcing a round carries the same round number as the agent
|
|
443
|
+
# lines under it rather than a blank cell
|
|
444
|
+
# @yieldparam item [Object]
|
|
445
|
+
# @yieldparam line [Line]
|
|
446
|
+
# @return [Array] one result per item, in input order
|
|
447
|
+
def concurrently(items, message, jobs:, label: :to_s.to_proc, fields: NO_FIELDS,
|
|
448
|
+
failure: NO_FAILURE, header: {}, timeout: NO_TIMEOUT, &)
|
|
449
|
+
list = items.to_a
|
|
450
|
+
return [] if list.empty?
|
|
451
|
+
|
|
452
|
+
log(message, **header)
|
|
453
|
+
|
|
454
|
+
if jobs <= 1 || list.size <= 1
|
|
455
|
+
report_line(message) unless animate?
|
|
456
|
+
return list.map { |item| once(item, label, fields, failure:, timeout:, &) }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
results = Concurrent::Hash.new
|
|
460
|
+
progress.tasks(message, concurrent: jobs) do |tasks|
|
|
461
|
+
list.each_with_index do |item, index|
|
|
462
|
+
tasks.task(label.call(item)) do |handle|
|
|
463
|
+
line = Line.new(fields: fields.call(item), handle:, timeout: timeout.call(item))
|
|
464
|
+
results[index] = attempt(item, line, failure, &)
|
|
465
|
+
rescue StandardError => e
|
|
466
|
+
results[index] = e
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
list.each_index.map { |i| results[i] }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# The same news, with no spinner to put it on. A piped or CI run still
|
|
474
|
+
# wants it, in the log where the rest of that run's progress goes.
|
|
475
|
+
#
|
|
476
|
+
# @param text [String] the item's label
|
|
477
|
+
# @return [Proc] phrase -> void
|
|
478
|
+
def logging_activity(text) = ->(phrase) { log("#{text}: #{phrase}") }
|
|
479
|
+
|
|
480
|
+
# A callable that writes what the work is doing after its spinner's label.
|
|
481
|
+
#
|
|
482
|
+
# @param line [Dry::CLI::UI::Line]
|
|
483
|
+
# @return [Proc] phrase -> void
|
|
484
|
+
def activity_for(line)
|
|
485
|
+
->(phrase) { line.detail = phrase.to_s.empty? ? "" : paint("— #{phrase}", :green, :bold) }
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# One item, no concurrency to speak of: a serial round (`--isolation
|
|
489
|
+
# shared`), or the last plan left in a parallel one. A live spinner on a
|
|
490
|
+
# terminal; a start line and a finish line with an elapsed time otherwise.
|
|
491
|
+
#
|
|
492
|
+
# @param item [Object]
|
|
493
|
+
# @param label [Proc]
|
|
494
|
+
# @yieldparam item [Object]
|
|
495
|
+
# @return [Object]
|
|
496
|
+
# @raise [StandardError] whatever the block raised, once its line says so
|
|
497
|
+
def once(item, label, fields = NO_FIELDS, failure: NO_FAILURE, timeout: NO_TIMEOUT, &)
|
|
498
|
+
progress.spinner(label.call(item)) do |handle|
|
|
499
|
+
attempt(item, Line.new(fields: fields.call(item), handle:, timeout: timeout.call(item)), failure, &)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Runs one item and says how it went, on its line and in the log.
|
|
504
|
+
#
|
|
505
|
+
# @param item [Object]
|
|
506
|
+
# @param line [Line]
|
|
507
|
+
# @param failure [Proc]
|
|
508
|
+
# @return [Object] the block's value
|
|
509
|
+
def attempt(item, line, failure)
|
|
510
|
+
line.start
|
|
511
|
+
begin
|
|
512
|
+
result = yield(item, line)
|
|
513
|
+
rescue StandardError => e
|
|
514
|
+
line.failed(e.message.lines.first.to_s.strip)
|
|
515
|
+
raise
|
|
516
|
+
end
|
|
517
|
+
if (reason = failure.call(result))
|
|
518
|
+
line.failed(reason)
|
|
519
|
+
else
|
|
520
|
+
line.done
|
|
521
|
+
end
|
|
522
|
+
result
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# The console per-item lines go through: {.console}, or one that
|
|
526
|
+
# writes nowhere under --quiet, so a quiet run really is quiet while
|
|
527
|
+
# the work, the log and the results stay exactly the same.
|
|
528
|
+
#
|
|
529
|
+
# @return [Dry::CLI::UI::Console]
|
|
530
|
+
def progress
|
|
531
|
+
return console unless quiet
|
|
532
|
+
|
|
533
|
+
Dry::CLI::UI::Console.new(out: StringIO.new, err: StringIO.new, color: false, animate: false)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# @return [Float] a monotonic clock reading, immune to wall-clock changes
|
|
537
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
538
|
+
|
|
539
|
+
# @param started [Float] a {#monotonic} reading taken before the work began
|
|
540
|
+
# @return [String] e.g. "42s"
|
|
541
|
+
def elapsed(started) = "#{(monotonic - started).round}s"
|
|
542
|
+
|
|
543
|
+
# A progress line, thread-safe and quiet-aware — the one thing every
|
|
544
|
+
# non-spinner path above needs and would otherwise have to reimplement.
|
|
545
|
+
#
|
|
546
|
+
# @param text [String]
|
|
547
|
+
# @param bullet [String]
|
|
548
|
+
# @return [void]
|
|
549
|
+
def report_line(text, bullet: "·")
|
|
550
|
+
return if quiet
|
|
551
|
+
|
|
552
|
+
(@print_mutex ||= Mutex.new).synchronize { line(text, bullet:) }
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# A sensible worker count: agents are mostly waiting on a model rather
|
|
556
|
+
# than burning CPU, so this is deliberately close to the core count. Two
|
|
557
|
+
# are left for the machine, and the cap keeps a very large tree from
|
|
558
|
+
# opening fifty subprocesses at once.
|
|
559
|
+
#
|
|
560
|
+
# @return [Integer]
|
|
561
|
+
def default_jobs = (Etc.nprocessors - 2).clamp(1, 12)
|
|
562
|
+
|
|
563
|
+
# Honours NO_COLOR — https://no-color.org
|
|
564
|
+
#
|
|
565
|
+
# @return [Boolean]
|
|
566
|
+
def color? = tty? && !ENV.key?("NO_COLOR")
|
|
567
|
+
|
|
568
|
+
# @return [Integer] a box width that fits the terminal but stays readable
|
|
569
|
+
def width = (TTY::Screen.width - 4).clamp(MIN_WIDTH, MAX_WIDTH)
|
|
570
|
+
|
|
571
|
+
# @param text [String]
|
|
572
|
+
# @param styles [Array<Symbol>] pastel style names
|
|
573
|
+
# @return [String] decorated when colour is on, bare otherwise
|
|
574
|
+
def paint(text, *styles) = color? ? pastel.decorate(text.to_s, *styles) : text.to_s
|
|
575
|
+
|
|
576
|
+
# @param text [String]
|
|
577
|
+
# @return [Integer] how many terminal cells the text occupies
|
|
578
|
+
def display_width(text) = ::Unicode::DisplayWidth.of(text.to_s)
|
|
579
|
+
|
|
580
|
+
# Pad or truncate to an exact number of terminal *cells*.
|
|
581
|
+
#
|
|
582
|
+
# `format`'s "%-20.20s" counts characters, and a character is not a cell.
|
|
583
|
+
# "✅" is one character two cells wide; "🅱️" is two characters one cell
|
|
584
|
+
# wide. Any column laid out with %s therefore drifts by one for every
|
|
585
|
+
# emoji whose two counts disagree — which is every emoji, in one
|
|
586
|
+
# direction or the other.
|
|
587
|
+
#
|
|
588
|
+
# @param text [String] unpainted; escape codes count as characters and
|
|
589
|
+
# would be padded like any other
|
|
590
|
+
# @param width [Integer] terminal cells
|
|
591
|
+
# @return [String]
|
|
592
|
+
def fit(text, width)
|
|
593
|
+
text = text.to_s
|
|
594
|
+
text = text[0..-2] while display_width(text) > width
|
|
595
|
+
text + (" " * (width - display_width(text)))
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Every box is drawn by dry-cli-ui, on STDERR, through {.console}.
|
|
599
|
+
#
|
|
600
|
+
# Not through `Kernel.warn`, which is a **no-op** when `$VERBOSE` is
|
|
601
|
+
# nil, which is what `-W0` sets — and `RUBYOPT=-W0` is common in CI
|
|
602
|
+
# images and agent harnesses. Routed through `Kernel.warn`, every box
|
|
603
|
+
# this tool draws silently disappears in exactly the environments where
|
|
604
|
+
# a failure most needs explaining. The console writes to its stream.
|
|
605
|
+
#
|
|
606
|
+
# dry-cli-ui wraps before it frames, so a long line no longer costs the
|
|
607
|
+
# box its last row, which is where the instruction lives. One run
|
|
608
|
+
# reported four of its ten failures and cut the fifth mid-sentence.
|
|
609
|
+
#
|
|
610
|
+
# @param kind [Symbol] :info, :warn, :error or :success
|
|
611
|
+
# @param message [String]
|
|
612
|
+
# @return [void]
|
|
613
|
+
def box(kind, message) = console.public_send(kind, message.to_s)
|
|
614
|
+
|
|
615
|
+
# A framed panel centered on the screen, for the keyboard help. Unlike
|
|
616
|
+
# {.box} it positions itself absolutely, so it overlays whatever the
|
|
617
|
+
# spinners are drawing rather than scrolling in below them — the next
|
|
618
|
+
# repaint draws over it, which is all the dismissal a help screen needs.
|
|
619
|
+
#
|
|
620
|
+
# @param title [String]
|
|
621
|
+
# @param text [String]
|
|
622
|
+
# @return [void]
|
|
623
|
+
def popup(title, text) = console.popup(text.to_s, title:)
|
|
624
|
+
|
|
625
|
+
# A framed table, returned rather than printed: a table is a
|
|
626
|
+
# deliverable, so it belongs on STDOUT and the caller picks the stream.
|
|
627
|
+
#
|
|
628
|
+
# @param rows [Array<Array<#to_s>>]
|
|
629
|
+
# @param header [Array<#to_s>, nil]
|
|
630
|
+
# @return [String] the table ending in a newline, or "" when there are no rows
|
|
631
|
+
def table(rows, header: nil)
|
|
632
|
+
Dry::CLI::UI::Widgets::Table.new(Dry::CLI::UI::Terminal.new($stdout, color: color?)).render(rows, header:)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# A single unadorned line, for per-item progress that does not deserve
|
|
636
|
+
# a box of its own.
|
|
637
|
+
#
|
|
638
|
+
# @param message [String]
|
|
639
|
+
# @param bullet [String]
|
|
640
|
+
# @return [void]
|
|
641
|
+
# rubocop:disable-next Style/StderrPuts -- see {.box}: `warn` is a no-op under -W0.
|
|
642
|
+
def line(message, bullet: "·") = $stderr.puts(" #{paint(bullet, :bright_black)} #{message}")
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# @param message [String]
|
|
646
|
+
# @return [void]
|
|
647
|
+
def info(message) = UI.box(:info, message)
|
|
648
|
+
|
|
649
|
+
# @param message [String]
|
|
650
|
+
# @return [void]
|
|
651
|
+
def warn(message) = UI.box(:warn, message)
|
|
652
|
+
|
|
653
|
+
# @param message [String]
|
|
654
|
+
# @return [void]
|
|
655
|
+
def error(message) = UI.box(:error, message)
|
|
656
|
+
|
|
657
|
+
# @param message [String]
|
|
658
|
+
# @return [void]
|
|
659
|
+
def success(message) = UI.box(:success, message)
|
|
660
|
+
|
|
661
|
+
# @param message [String]
|
|
662
|
+
# @param bullet [String]
|
|
663
|
+
# @return [void]
|
|
664
|
+
def say(message, bullet: "·") = UI.line(message, bullet:)
|
|
665
|
+
|
|
666
|
+
# @param text [String]
|
|
667
|
+
# @param styles [Array<Symbol>]
|
|
668
|
+
# @return [String]
|
|
669
|
+
def paint(text, *styles) = UI.paint(text, *styles)
|
|
670
|
+
end
|
|
671
|
+
end
|