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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# Everything a check may look at: the folder, where it ended, who was
|
|
6
|
+
# asked to write it, what the run spent and what it changed.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [r] folder
|
|
9
|
+
# @return [String] the plan folder, which may not exist
|
|
10
|
+
# @!attribute [r] state
|
|
11
|
+
# @return [Symbol, nil]
|
|
12
|
+
# @!attribute [r] agent
|
|
13
|
+
# @return [OpenLanes::Agents::Agent]
|
|
14
|
+
# @!attribute [r] run
|
|
15
|
+
# @return [OpenLanes::Evals::Run]
|
|
16
|
+
# @!attribute [r] changes
|
|
17
|
+
# @return [OpenLanes::Evals::Changes, nil] nil when no repository was recorded
|
|
18
|
+
Evidence = Data.define(:folder, :state, :agent, :run, :changes) do
|
|
19
|
+
# @param name [String] a file in the plan folder
|
|
20
|
+
# @return [String, nil]
|
|
21
|
+
def read(name)
|
|
22
|
+
path = File.join(folder, name)
|
|
23
|
+
File.file?(path) ? File.read(path, encoding: "UTF-8") : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A document without its ledger blocks, narrowed to one section when
|
|
27
|
+
# asked. The ledger is the harness's bookkeeping, not the agent's prose,
|
|
28
|
+
# and counting it would let a run pad itself by signing.
|
|
29
|
+
#
|
|
30
|
+
# @param name [String]
|
|
31
|
+
# @param section [String, nil] a heading's text
|
|
32
|
+
# @return [String, nil] nil when the file or the section is missing
|
|
33
|
+
def text(name, section = nil)
|
|
34
|
+
body = read(name) or return nil
|
|
35
|
+
body = Plans::Ledger.stripped(body)
|
|
36
|
+
section ? Evidence.section(body, section) : body
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The folder as the state machine would see it under +status+'s name,
|
|
40
|
+
# so the status's own invariant can be asked of a folder whose name
|
|
41
|
+
# carries no state, such as a recording's `plan/`.
|
|
42
|
+
#
|
|
43
|
+
# @param status [OpenLanes::Plans::Status]
|
|
44
|
+
# @return [OpenLanes::Plans::Subject]
|
|
45
|
+
def subject_as(status)
|
|
46
|
+
Plans.subject(Plans::Feature.new(ordinal: Plans.ordinal("1"),
|
|
47
|
+
status:,
|
|
48
|
+
slug: "eval",
|
|
49
|
+
dirname: File.basename(folder),
|
|
50
|
+
path: folder))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param name [String]
|
|
54
|
+
# @return [Regexp] a Markdown heading with that text, any level
|
|
55
|
+
def self.heading(name) = /\A[ \t]{0,3}\#{1,6}[ \t]+#{Regexp.escape(name)}(?!\w)/i
|
|
56
|
+
|
|
57
|
+
# The body under a heading, up to the next heading of the same or a
|
|
58
|
+
# higher level.
|
|
59
|
+
#
|
|
60
|
+
# @param text [String]
|
|
61
|
+
# @param name [String]
|
|
62
|
+
# @return [String, nil]
|
|
63
|
+
def self.section(text, name)
|
|
64
|
+
lines = text.lines
|
|
65
|
+
start = lines.index { |l| l.match?(heading(name)) } or return nil
|
|
66
|
+
level = lines[start][/\A[ \t]*(#+)/, 1].size
|
|
67
|
+
lines.drop(start + 1).take_while { |l| (l[/\A[ \t]{0,3}(#+)[ \t]/, 1]&.size || 7) > level }.join
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module OpenLanes
|
|
8
|
+
module Evals
|
|
9
|
+
# Runs the real agent on a case's fixture, in a repository nobody else
|
|
10
|
+
# uses, under a token cap, and scores what it leaves behind.
|
|
11
|
+
#
|
|
12
|
+
# The steps mirror what the runner does around one agent, and reuse its
|
|
13
|
+
# pieces rather than imitating them: {Execution.executor} runs the agent
|
|
14
|
+
# with the same autonomy boundary, and `resync dirs` renames the folder to
|
|
15
|
+
# the state its contents justify, as the runner does after every round.
|
|
16
|
+
# What it cannot reproduce is publishing. A build agent's 🟢 needs a pull
|
|
17
|
+
# request on GitHub, so build cases check signatures and diffs, not state.
|
|
18
|
+
class Live
|
|
19
|
+
# The default ceiling for one live case. Cheap enough to run by hand,
|
|
20
|
+
# high enough that a deep research case is not killed for doing its job.
|
|
21
|
+
DEFAULT_MAX_TOKENS = 300_000
|
|
22
|
+
|
|
23
|
+
# The one plan in the fixture repository.
|
|
24
|
+
ORDINAL = "001.00"
|
|
25
|
+
|
|
26
|
+
# Who the fixture commit is from: a made-up identity, so a commit in a
|
|
27
|
+
# temp repository can never be mistaken for a person's.
|
|
28
|
+
AUTHOR = ["-c", "user.name=OpenLanes Eval", "-c", "user.email=eval@example.com",
|
|
29
|
+
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null"].freeze
|
|
30
|
+
|
|
31
|
+
# @!attribute [r] score
|
|
32
|
+
# @return [OpenLanes::Evals::Score]
|
|
33
|
+
# @!attribute [r] result
|
|
34
|
+
# @return [OpenLanes::Execution::Executor::Result]
|
|
35
|
+
# @!attribute [r] root
|
|
36
|
+
# @return [String, nil] the repository, when it was kept
|
|
37
|
+
Outcome = Data.define(:score, :result, :root)
|
|
38
|
+
|
|
39
|
+
# @param kase [OpenLanes::Evals::Case] at the depth to run it
|
|
40
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
41
|
+
# @param max_tokens [Integer] enforced by the executor's meter
|
|
42
|
+
# @param timeout [Integer, nil] seconds; nil leaves the agent's own clock
|
|
43
|
+
# @param spawn [#call, nil] the executor's seam; nil starts the real CLI
|
|
44
|
+
# @param trace_dir [String, nil] where the NDJSON trace goes
|
|
45
|
+
# @param keep [Boolean] leave the repository on disk for inspection
|
|
46
|
+
def initialize(kase, registry: Agents.registry, max_tokens: DEFAULT_MAX_TOKENS, timeout: nil, spawn: nil,
|
|
47
|
+
trace_dir: nil, keep: false)
|
|
48
|
+
@kase = kase
|
|
49
|
+
@registry = registry
|
|
50
|
+
@agent = registry.find(kase.agent) or raise Invalid, "#{kase.path}: names no agent called #{kase.agent}"
|
|
51
|
+
@max_tokens = max_tokens
|
|
52
|
+
@timeout = timeout
|
|
53
|
+
@spawn = spawn
|
|
54
|
+
@trace_dir = trace_dir
|
|
55
|
+
@keep = keep
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [OpenLanes::Evals::Live::Outcome]
|
|
59
|
+
def call
|
|
60
|
+
root = Dir.mktmpdir("openlanes-eval-")
|
|
61
|
+
begin
|
|
62
|
+
plans = build(root)
|
|
63
|
+
result = executor(root).call(@agent, start(plans), root:)
|
|
64
|
+
Lifecycle.resync_dirs(tree: Plans.tree(plans)).call(commit: true)
|
|
65
|
+
found = Plans.tree(plans).find(ORDINAL)
|
|
66
|
+
run = Run.new(seconds: result.seconds, tokens: result.fresh)
|
|
67
|
+
score = Scorer.new(@kase, registry: @registry)
|
|
68
|
+
.call(folder: found&.feature&.path || File.join(plans, "missing"), run:, repo: root)
|
|
69
|
+
Outcome.new(score:, result:, root: @keep ? root : nil)
|
|
70
|
+
ensure
|
|
71
|
+
FileUtils.rm_rf(root) unless @keep
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# A repository holding the fixture, committed, so the executor's
|
|
78
|
+
# after-check has a HEAD to hold the agent to.
|
|
79
|
+
#
|
|
80
|
+
# @return [String] the `.plans` directory
|
|
81
|
+
def build(root)
|
|
82
|
+
git(root, "init", "-q")
|
|
83
|
+
@kase.fixture.repo.each { |name, body| write(File.join(root, name), body) }
|
|
84
|
+
plans = File.join(root, OpenLanes::PLANS_DIR)
|
|
85
|
+
status = Plans.status(@kase.fixture.state)
|
|
86
|
+
folder = File.join(plans, Plans.plan_dirname(ORDINAL, status, @kase.fixture.slug))
|
|
87
|
+
@kase.fixture.files.each { |name, body| write(File.join(folder, name), body) }
|
|
88
|
+
stamp_depth(File.join(folder, Plans::Spec::FILENAME))
|
|
89
|
+
git(root, "add", "-A")
|
|
90
|
+
git(root, *AUTHOR, "commit", "-q", "-m", "Eval fixture #{@kase}")
|
|
91
|
+
plans
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The depth reaches the agent the way it would in a real plan: as
|
|
95
|
+
# `depth:` in spec.md's frontmatter, which the profile turns into effort.
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
def stamp_depth(path)
|
|
99
|
+
meta, body = Frontmatter.split(File.read(path, encoding: "UTF-8"))
|
|
100
|
+
meta = meta.merge("depth" => @kase.depth.to_s)
|
|
101
|
+
File.write(path, "#{YAML.dump(meta)}---\n#{body}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The runner moves a folder to the agent's `starts_as` the moment it is
|
|
105
|
+
# dispatched, so r2d2 works a 🟡 plan and hansolo a 👀 one. Same here.
|
|
106
|
+
#
|
|
107
|
+
# @return [OpenLanes::Plans::Subject]
|
|
108
|
+
def start(plans)
|
|
109
|
+
subject = Plans.tree(plans).find(ORDINAL)
|
|
110
|
+
target = @agent.starts_as
|
|
111
|
+
return subject unless target && subject.machine.may?(target)
|
|
112
|
+
|
|
113
|
+
subject.machine.promote!(target)
|
|
114
|
+
Plans.tree(plans).find(ORDINAL)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [OpenLanes::Execution::Executor]
|
|
118
|
+
def executor(root)
|
|
119
|
+
options = { root:, max_tokens: @max_tokens, fresh_budget: true, timeout: @timeout }
|
|
120
|
+
options[:spawn] = @spawn if @spawn
|
|
121
|
+
options[:trace_dir] = @trace_dir if @trace_dir
|
|
122
|
+
Execution.executor(**options)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [void]
|
|
126
|
+
def write(path, body)
|
|
127
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
128
|
+
File.write(path, body)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [void]
|
|
132
|
+
# @raise [OpenLanes::Error] when git refuses
|
|
133
|
+
def git(root, *args)
|
|
134
|
+
return if system("git", "-C", root, *args, out: File::NULL, err: File::NULL)
|
|
135
|
+
|
|
136
|
+
raise Error, "git #{args.last(2).join(" ")} failed in #{root}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# A plan folder as some run of the agent left it, kept so the scorers can
|
|
6
|
+
# be checked without running anything.
|
|
7
|
+
#
|
|
8
|
+
# <case>/pass/plan/ the plan folder
|
|
9
|
+
# <case>/pass/repo/ the repository around it, for coding agents
|
|
10
|
+
# <case>/pass/run.json {"seconds": .., "tokens": .., "state": ..}
|
|
11
|
+
#
|
|
12
|
+
# The folder sits in `plan/` rather than at the top so that `run.json` and
|
|
13
|
+
# `repo/` are never mistaken for files the agent wrote.
|
|
14
|
+
#
|
|
15
|
+
# @!attribute [r] dir
|
|
16
|
+
# @return [String]
|
|
17
|
+
Recording = Data.define(:dir) do
|
|
18
|
+
# @param dir [String]
|
|
19
|
+
# @return [OpenLanes::Evals::Recording, nil] nil when there is no plan folder
|
|
20
|
+
def self.at(dir) = File.directory?(File.join(dir, "plan")) ? new(dir:) : nil
|
|
21
|
+
|
|
22
|
+
# @return [String] the plan folder
|
|
23
|
+
def plan = File.join(dir, "plan")
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] the repository, when one was recorded
|
|
26
|
+
def repo
|
|
27
|
+
path = File.join(dir, "repo")
|
|
28
|
+
File.directory?(path) ? path : nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [OpenLanes::Evals::Run]
|
|
32
|
+
def run = Run.load(File.join(dir, "run.json"))
|
|
33
|
+
|
|
34
|
+
# @return [Symbol] :pass or :fail
|
|
35
|
+
def kind = File.basename(dir).to_sym
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Evals
|
|
7
|
+
# What one run spent, and where it left the folder.
|
|
8
|
+
#
|
|
9
|
+
# A live run measures all three. A recording carries them in `run.json`,
|
|
10
|
+
# because a directory on disk cannot say how long it took to write.
|
|
11
|
+
#
|
|
12
|
+
# @!attribute [r] seconds
|
|
13
|
+
# @return [Float, nil] wall clock
|
|
14
|
+
# @!attribute [r] tokens
|
|
15
|
+
# @return [Integer, nil] sent plus generated
|
|
16
|
+
# @!attribute [r] state
|
|
17
|
+
# @return [Symbol, nil] the status key the folder ended in, when its
|
|
18
|
+
# name cannot say so itself
|
|
19
|
+
Run = Data.define(:seconds, :tokens, :state) do
|
|
20
|
+
def initialize(seconds: nil, tokens: nil, state: nil)
|
|
21
|
+
super(seconds: seconds&.to_f, tokens: tokens&.to_i, state: state&.to_sym)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param path [String] a `run.json`
|
|
25
|
+
# @return [OpenLanes::Evals::Run] empty when the file is absent
|
|
26
|
+
def self.load(path)
|
|
27
|
+
return new unless File.file?(path)
|
|
28
|
+
|
|
29
|
+
data = JSON.parse(File.read(path))
|
|
30
|
+
new(seconds: data["seconds"], tokens: data["tokens"], state: data["state"])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# Every verdict for one case against one folder.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] kase
|
|
8
|
+
# @return [OpenLanes::Evals::Case] at the depth it was scored
|
|
9
|
+
# @!attribute [r] verdicts
|
|
10
|
+
# @return [Array<OpenLanes::Evals::Verdict>]
|
|
11
|
+
Score = Data.define(:kase, :verdicts) do
|
|
12
|
+
# A case with no checks has proved nothing, so it does not pass.
|
|
13
|
+
#
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def passed? = !verdicts.empty? && verdicts.all?(&:ok?)
|
|
16
|
+
|
|
17
|
+
# @return [Array<OpenLanes::Evals::Verdict>]
|
|
18
|
+
def failures = verdicts.reject(&:ok?)
|
|
19
|
+
|
|
20
|
+
# @return [String] `7/8`
|
|
21
|
+
def tally = "#{verdicts.count(&:ok?)}/#{verdicts.size}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# Applies a case's checks to a folder.
|
|
6
|
+
#
|
|
7
|
+
# The scorer does not care whether the folder was recorded months ago or
|
|
8
|
+
# written a second ago by a live run; both reach it as a path plus a
|
|
9
|
+
# {Run}, which is what lets the recordings test the scorer that the live
|
|
10
|
+
# runs depend on.
|
|
11
|
+
class Scorer
|
|
12
|
+
# @param kase [OpenLanes::Evals::Case]
|
|
13
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
14
|
+
def initialize(kase, registry: Agents.registry)
|
|
15
|
+
@kase = kase
|
|
16
|
+
@agent = registry.find(kase.agent) or raise Invalid, "#{kase.path}: names no agent called #{kase.agent}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param folder [String] the plan folder
|
|
20
|
+
# @param run [OpenLanes::Evals::Run, nil]
|
|
21
|
+
# @param repo [String, nil] the repository afterwards
|
|
22
|
+
# @return [OpenLanes::Evals::Score]
|
|
23
|
+
def call(folder:, run: nil, repo: nil)
|
|
24
|
+
run ||= Run.new
|
|
25
|
+
evidence = Evidence.new(folder: File.expand_path(folder),
|
|
26
|
+
state: state_of(folder, run),
|
|
27
|
+
agent: @agent,
|
|
28
|
+
run:,
|
|
29
|
+
changes: repo && Changes.between(@kase.fixture.repo, repo))
|
|
30
|
+
Score.new(kase: @kase, verdicts: @kase.checks.map { |check| check.call(evidence) })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A folder named like a plan says where it is; a recording's `plan/`
|
|
36
|
+
# cannot, and leans on what `run.json` says instead.
|
|
37
|
+
#
|
|
38
|
+
# @return [Symbol, nil]
|
|
39
|
+
def state_of(folder, run) = Plans.feature(File.expand_path(folder))&.status&.key || run.state
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# One check's answer.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] name
|
|
8
|
+
# @return [String] the check, as a reader would name it
|
|
9
|
+
# @!attribute [r] ok
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
# @!attribute [r] detail
|
|
12
|
+
# @return [String] what was found, pass or fail
|
|
13
|
+
Verdict = Data.define(:name, :ok, :detail) do
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def ok? = ok
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
# Per-agent evals: did the agent finish the task, and did it work at the
|
|
5
|
+
# depth it was asked for?
|
|
6
|
+
#
|
|
7
|
+
# A case (`evals/cases/<agent>/<id>.yml`) describes a fixture plan, the depth
|
|
8
|
+
# requested and the deterministic checks that hold an agent to both. Offline,
|
|
9
|
+
# the checks score recorded plan folders, a passing and a failing one per
|
|
10
|
+
# case, so the scorers are tested by the same corpus they score. Live, the
|
|
11
|
+
# agent runs in a throwaway repository under a token cap, and the folder it
|
|
12
|
+
# leaves behind is scored the same way.
|
|
13
|
+
module Evals
|
|
14
|
+
# Where the shipped cases live: beside the gem, so `openlanes eval` finds
|
|
15
|
+
# them from any project root.
|
|
16
|
+
DEFAULT_DIR = File.expand_path("../../evals/cases", __dir__)
|
|
17
|
+
|
|
18
|
+
# A case file that cannot be scored as written.
|
|
19
|
+
class Invalid < OpenLanes::Error; end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @param dir [String] a directory of `<agent>/<id>.yml` files
|
|
24
|
+
# @param registry [OpenLanes::Agents::Registry] the agents a case may name
|
|
25
|
+
# @return [Array<OpenLanes::Evals::Case>] ordered by agent, then id
|
|
26
|
+
# @raise [OpenLanes::Evals::Invalid] when any case fails validation
|
|
27
|
+
def cases(dir = DEFAULT_DIR, registry: Agents.registry) = Case.all(dir, registry:)
|
|
28
|
+
|
|
29
|
+
# @param kase [OpenLanes::Evals::Case]
|
|
30
|
+
# @param folder [String] the plan folder as the agent left it
|
|
31
|
+
# @param run [OpenLanes::Evals::Run, nil] what the run spent, and where it ended
|
|
32
|
+
# @param repo [String, nil] the repository as the agent left it, for the
|
|
33
|
+
# changed-path checks
|
|
34
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
35
|
+
# @return [OpenLanes::Evals::Score]
|
|
36
|
+
def score(kase, folder:, run: nil, repo: nil, registry: Agents.registry)
|
|
37
|
+
Scorer.new(kase, registry:).call(folder:, run:, repo:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param kase [OpenLanes::Evals::Case]
|
|
41
|
+
# @param kind [Symbol] :pass or :fail
|
|
42
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
43
|
+
# @return [OpenLanes::Evals::Score, nil] nil when the case ships no such recording
|
|
44
|
+
def score_recording(kase, kind = :pass, registry: Agents.registry)
|
|
45
|
+
recording = kase.recording(kind) or return nil
|
|
46
|
+
|
|
47
|
+
score(kase, folder: recording.plan, run: recording.run, repo: recording.repo, registry:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Runs the real agent. Nothing calls this unless `--live` was typed.
|
|
51
|
+
#
|
|
52
|
+
# @param kase [OpenLanes::Evals::Case]
|
|
53
|
+
# @return [OpenLanes::Evals::Live::Outcome]
|
|
54
|
+
def run_live(kase, **) = Live.new(kase, **).call
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Execution
|
|
5
|
+
# The requirement every agent that changes code carries whether or not the
|
|
6
|
+
# spec says it: after the changes, every test suite and linter passes.
|
|
7
|
+
#
|
|
8
|
+
# A spec author writes the things that are particular to a plan. That the
|
|
9
|
+
# suite stays green is particular to none of them, so it was written in
|
|
10
|
+
# none of them, and a builder that never ran the linter shipped work the
|
|
11
|
+
# reviewer then had to reject for it. Deriving the command from the
|
|
12
|
+
# checkout, rather than asking each spec to name it, keeps it one fact in
|
|
13
|
+
# one place: the project's own justfile or Gemfile.
|
|
14
|
+
module Checks
|
|
15
|
+
# Agents whose work is code, or the judging of code. Research, planning
|
|
16
|
+
# and unblocking write documents, and running a suite for a document is
|
|
17
|
+
# minutes spent proving nothing.
|
|
18
|
+
PHASES = %w[build frontend review].freeze
|
|
19
|
+
|
|
20
|
+
# The recipe a project names its everything-gate with.
|
|
21
|
+
RECIPE = "check-all"
|
|
22
|
+
|
|
23
|
+
JUSTFILES = %w[justfile Justfile .justfile].freeze
|
|
24
|
+
|
|
25
|
+
# A recipe header: optional `@`, the name, optional parameters, a colon
|
|
26
|
+
# that is not the start of `:=` (an assignment).
|
|
27
|
+
RECIPE_LINE = /^@?#{Regexp.escape(RECIPE)}(?:\s[^:\n]*)?:(?!=)/
|
|
28
|
+
|
|
29
|
+
RUBY = ["bundle exec rspec", "bundle exec rubocop"].freeze
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
33
|
+
# @return [Boolean] whether the agent carries the requirement
|
|
34
|
+
def apply_to?(agent) = PHASES.include?(agent.phase.to_s)
|
|
35
|
+
|
|
36
|
+
# @param root [String] the checkout the agent works in
|
|
37
|
+
# @return [Array<String>] the commands that must pass, empty when the
|
|
38
|
+
# project names none this can recognise
|
|
39
|
+
def commands(root)
|
|
40
|
+
return ["just #{RECIPE}"] if recipe?(root)
|
|
41
|
+
return RUBY if File.file?(File.join(root, "Gemfile"))
|
|
42
|
+
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param root [String]
|
|
47
|
+
# @return [String] the requirement, one line
|
|
48
|
+
def requirement(root)
|
|
49
|
+
commands = commands(root)
|
|
50
|
+
if commands.empty?
|
|
51
|
+
"After your changes, every test suite and linter the project defines passes. " \
|
|
52
|
+
"Find them in its README, AGENTS.md or CLAUDE.md, run them all, and fix what they report."
|
|
53
|
+
else
|
|
54
|
+
"After your changes, all tests and linters pass: #{commands.map { "`#{it}`" }.join(" and ")} " \
|
|
55
|
+
"#{commands.size == 1 ? "exits" : "each exit"} 0. Run #{commands.size == 1 ? "it" : "them"} " \
|
|
56
|
+
"before you sign Completed, and fix what they report rather than reporting it."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# @param root [String]
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def recipe?(root)
|
|
65
|
+
JUSTFILES.any? do |name|
|
|
66
|
+
path = File.join(root, name)
|
|
67
|
+
File.file?(path) && File.read(path, encoding: "UTF-8").match?(RECIPE_LINE)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|