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,45 @@
|
|
|
1
|
+
# Cart Quantity Limits
|
|
2
|
+
|
|
3
|
+
Customers of Example Corp's shop can add negative or absurd quantities to a cart.
|
|
4
|
+
|
|
5
|
+
## What research needs to settle
|
|
6
|
+
|
|
7
|
+
1. Where is quantity validated today?
|
|
8
|
+
|
|
9
|
+
## Research
|
|
10
|
+
|
|
11
|
+
1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
|
|
12
|
+
2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
|
|
13
|
+
|
|
14
|
+
### Could not settle
|
|
15
|
+
|
|
16
|
+
1. Whether zero should remove the line or be rejected.
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Validate quantities in the cart.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Changing the checkout limit.
|
|
25
|
+
|
|
26
|
+
## In scope
|
|
27
|
+
|
|
28
|
+
- Handles errors.
|
|
29
|
+
|
|
30
|
+
## Out of scope
|
|
31
|
+
|
|
32
|
+
- Bulk orders.
|
|
33
|
+
|
|
34
|
+
## Back end / front end
|
|
35
|
+
|
|
36
|
+
All back end.
|
|
37
|
+
|
|
38
|
+
## Open questions
|
|
39
|
+
|
|
40
|
+
None.
|
|
41
|
+
|
|
42
|
+
> [!NOTE]
|
|
43
|
+
>
|
|
44
|
+
> [2026-09-04 12:02:11 PM PDT] [ agent: yoda-writer status: Started, round 1 ]
|
|
45
|
+
> [2026-09-04 12:04:02 PM PDT] [ agent: yoda-writer status: Completed, round 1 ]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"seconds": 111, "tokens": 21900, "state": "ready_for_planning"}
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Cart Quantity Limits
|
|
2
|
+
|
|
3
|
+
Customers of Example Corp's shop can add negative or absurd quantities to a cart.
|
|
4
|
+
|
|
5
|
+
## What research needs to settle
|
|
6
|
+
|
|
7
|
+
1. Where is quantity validated today?
|
|
8
|
+
|
|
9
|
+
## Research
|
|
10
|
+
|
|
11
|
+
1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
|
|
12
|
+
2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
|
|
13
|
+
|
|
14
|
+
### Could not settle
|
|
15
|
+
|
|
16
|
+
1. Whether zero should remove the line or be rejected.
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
A cart can only ever hold quantities the checkout will accept, so a customer learns about a bad quantity when they type it rather than at payment. Framed two ways, as a validation problem in `Cart#add` or as a contract shared with checkout, the second wins: the limit already lives in `lib/checkout.rb:12`, and copying it would let the two drift.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Changing the checkout limit of 99.
|
|
25
|
+
- Per-product stock limits; stock is a separate system.
|
|
26
|
+
- Changing how the cart is persisted.
|
|
27
|
+
- Localising the error message beyond English.
|
|
28
|
+
|
|
29
|
+
## In scope
|
|
30
|
+
|
|
31
|
+
- `Cart#add` with a quantity below 1 raises `Cart::InvalidQuantity` and leaves the cart unchanged; a spec proves it.
|
|
32
|
+
- `Cart#add` with a quantity above the checkout limit raises the same error; the limit is read from `Checkout::MAX_QUANTITY`.
|
|
33
|
+
- The cart page shows the error next to the quantity field, and the field keeps the rejected value.
|
|
34
|
+
|
|
35
|
+
## Out of scope
|
|
36
|
+
|
|
37
|
+
- Bulk orders above 99: they need a sales contact flow, which is a product decision.
|
|
38
|
+
- Backfilling existing carts that already hold bad quantities: none exist in production per the research.
|
|
39
|
+
|
|
40
|
+
## Back end / front end
|
|
41
|
+
|
|
42
|
+
| Item | Half |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `Cart#add` validation and the shared constant | luke-backend |
|
|
45
|
+
| Error display on the cart page | rey-frontend |
|
|
46
|
+
|
|
47
|
+
## Open questions
|
|
48
|
+
|
|
49
|
+
1. Whether zero removes the line or is rejected (leah could not settle it). Assumed: rejected.
|
|
50
|
+
|
|
51
|
+
## Risks
|
|
52
|
+
|
|
53
|
+
- The checkout constant is private today; making it public touches a file another team owns.
|
|
54
|
+
|
|
55
|
+
## Conclusion
|
|
56
|
+
|
|
57
|
+
When this ships, a cart cannot hold a quantity checkout would refuse, the limit is defined once, and the customer sees the problem on the cart page.
|
|
58
|
+
|
|
59
|
+
> [!NOTE]
|
|
60
|
+
>
|
|
61
|
+
> [2026-09-04 12:02:11 PM PDT] [ agent: yoda-writer status: Started, round 1 ]
|
|
62
|
+
> [2026-09-04 12:09:40 PM PDT] [ agent: yoda-writer status: Completed, round 1 ]
|
|
63
|
+
> [2026-09-04 12:09:41 PM PDT] [ next: palpatine-planner ]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"seconds": 449, "tokens": 96200, "state": "ready_for_planning"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
id: spec-medium
|
|
2
|
+
agent: yoda-writer
|
|
3
|
+
depth: medium
|
|
4
|
+
description: >
|
|
5
|
+
A researched spec. yoda must write every section of a full specification,
|
|
6
|
+
at least three Non-Goals, and leave plan.md blank for the planner; a
|
|
7
|
+
heading in plan.md would claim the plan was already written.
|
|
8
|
+
fixture:
|
|
9
|
+
state: researched
|
|
10
|
+
slug: cart-quantity-limits
|
|
11
|
+
files:
|
|
12
|
+
spec.md: |
|
|
13
|
+
# Cart Quantity Limits
|
|
14
|
+
|
|
15
|
+
Customers of Example Corp's shop can add negative or absurd quantities to a cart.
|
|
16
|
+
|
|
17
|
+
## What research needs to settle
|
|
18
|
+
|
|
19
|
+
1. Where is quantity validated today?
|
|
20
|
+
|
|
21
|
+
## Research
|
|
22
|
+
|
|
23
|
+
1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
|
|
24
|
+
2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
|
|
25
|
+
|
|
26
|
+
### Could not settle
|
|
27
|
+
|
|
28
|
+
1. Whether zero should remove the line or be rejected.
|
|
29
|
+
expect:
|
|
30
|
+
state: ready_for_planning
|
|
31
|
+
signed: true
|
|
32
|
+
files_exist: [plan.md]
|
|
33
|
+
sections:
|
|
34
|
+
spec.md: [Goal, Non-Goals, In scope, Out of scope, Back end / front end, Open questions, Risks, Conclusion]
|
|
35
|
+
depth:
|
|
36
|
+
medium:
|
|
37
|
+
words: { file: spec.md, min: 250, max: 2500 }
|
|
38
|
+
count: { file: spec.md, section: Non-Goals, pattern: '^[ \t]*(?:[-*]|\d+\.)[ \t]', min: 3, max: 8 }
|
|
39
|
+
max_seconds: 900
|
|
40
|
+
max_tokens: 150000
|
data/exe/depot
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openlanes
|
data/exe/lanes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openlanes
|
data/exe/openlanes
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# openlanes — spec → plan → build.
|
|
5
|
+
#
|
|
6
|
+
# Every command does the work it is named for; -n / --dry-run previews it.
|
|
7
|
+
#
|
|
8
|
+
# © 2026 Konstantin Gredeskoul
|
|
9
|
+
|
|
10
|
+
# Resolve the dependencies from this repository without the bundler, thank you very much.
|
|
11
|
+
module OpenLanes
|
|
12
|
+
PROJECT_ROOT = File.expand_path("..", __dir__)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if OpenLanes::PROJECT_ROOT.include?("lib/ruby/gems")
|
|
16
|
+
require "rubygems"
|
|
17
|
+
gem "openlanes"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# lib/ goes first on the load path: several files `require "openlanes/..."`,
|
|
21
|
+
# and without this those resolve to whichever openlanes gem is installed,
|
|
22
|
+
# not to this checkout, and one class ends up defined twice.
|
|
23
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
24
|
+
|
|
25
|
+
require_relative "../lib/openlanes"
|
|
26
|
+
require_relative "../lib/openlanes/launcher"
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
OpenLanes::Launcher.new(ARGV.dup).execute!
|
|
30
|
+
rescue SystemExit => e
|
|
31
|
+
exit(e.status)
|
|
32
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Adapters
|
|
5
|
+
# What every adapter answers. A subclass overrides {#argv}; the rest has a
|
|
6
|
+
# default that is right for a CLI with no opinions of its own.
|
|
7
|
+
class Base
|
|
8
|
+
# @return [String] the name agent frontmatter uses
|
|
9
|
+
def name = self.class.name.split("::").last.downcase
|
|
10
|
+
|
|
11
|
+
# @return [String] the executable, for messages
|
|
12
|
+
def executable = name
|
|
13
|
+
|
|
14
|
+
# @param invocation [OpenLanes::Adapters::Invocation]
|
|
15
|
+
# @return [Array<String>]
|
|
16
|
+
def argv(invocation) = raise NotImplementedError, "#{self.class} must build an argv for #{invocation.class}"
|
|
17
|
+
|
|
18
|
+
# A parser for what this CLI prints, with the same interface as
|
|
19
|
+
# {OpenLanes::Execution::Transcript}.
|
|
20
|
+
#
|
|
21
|
+
# @param trace [String, nil] where to keep every line verbatim
|
|
22
|
+
# @return [#push, #finish, #up, #down, #failed?]
|
|
23
|
+
def transcript(trace: nil, &) = Stream.new(trace:, &)
|
|
24
|
+
|
|
25
|
+
# The model to ask for, after any ceiling this CLI imposes.
|
|
26
|
+
#
|
|
27
|
+
# @param model [String, nil]
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
def model(model) = model
|
|
30
|
+
|
|
31
|
+
# @param effort [String, nil] one of {EFFORTS}
|
|
32
|
+
# @return [String, nil] this CLI's word for it
|
|
33
|
+
def effort(effort) = effort
|
|
34
|
+
|
|
35
|
+
# What the child process gets on top of the harness's own environment.
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash{String => String}]
|
|
38
|
+
def environment = {}
|
|
39
|
+
|
|
40
|
+
# The models this CLI offers, most capable first, for the page's
|
|
41
|
+
# dropdowns and the cost meter. Empty for a CLI nobody has priced.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<OpenLanes::Adapters::Model>]
|
|
44
|
+
def models = []
|
|
45
|
+
|
|
46
|
+
# @param name [String, nil] an alias or a full id
|
|
47
|
+
# @return [OpenLanes::Adapters::Model, nil]
|
|
48
|
+
def model_entry(name) = models.find { |m| m.id == name.to_s }
|
|
49
|
+
|
|
50
|
+
# What, sent into a resumed session, makes this CLI compact it (W28);
|
|
51
|
+
# nil for a CLI that cannot resume one, whose agents are therefore never
|
|
52
|
+
# compacted mid-round.
|
|
53
|
+
#
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
def compact_prompt = nil
|
|
56
|
+
|
|
57
|
+
# @return [Boolean] whether an agent on this CLI can be compacted and
|
|
58
|
+
# carried on in the same session
|
|
59
|
+
def compacts? = !compact_prompt.nil?
|
|
60
|
+
|
|
61
|
+
# Whether the CLI itself refuses the denied tools, rather than only being
|
|
62
|
+
# asked not to use them. The prompt says which, because telling an agent
|
|
63
|
+
# a guard exists when it does not is how it stops being careful.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def enforces_tool_denial? = false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Adapters
|
|
5
|
+
# `claude -p`, streaming JSON.
|
|
6
|
+
class Claude < Base
|
|
7
|
+
# Models by capability, cheapest first. Anything ranked above
|
|
8
|
+
# {CEILING} is asked for as the ceiling instead: a run that quietly
|
|
9
|
+
# spends Fable money on a task Opus does fine is a run nobody budgeted.
|
|
10
|
+
LADDER = %w[haiku sonnet opus fable].freeze
|
|
11
|
+
|
|
12
|
+
# The most expensive model any agent may run on.
|
|
13
|
+
CEILING = "opus"
|
|
14
|
+
|
|
15
|
+
# Every Claude model priced, most capable first, newest first within a
|
|
16
|
+
# tier. The one place a model id, its label and its rates are written:
|
|
17
|
+
# the dropdown, the cost meter and alias resolution all read it, through
|
|
18
|
+
# {#models}, which offers only what {CEILING} lets run. Rates are base
|
|
19
|
+
# dollars per million tokens, in and out, from
|
|
20
|
+
# https://platform.claude.com/docs/en/about-claude/pricing as read on
|
|
21
|
+
# 2026-10-13. Haiku 5.5 is priced for prompts up to 100k tokens; above
|
|
22
|
+
# that it is $0.50 in and $2.50 out, which this table does not model.
|
|
23
|
+
CATALOGUE = [
|
|
24
|
+
Model.new(id: "claude-fable-5-1", label: "Fable 5.1", rate_in: 10, rate_out: 50),
|
|
25
|
+
Model.new(id: "claude-opus-5-5", label: "Opus 5.5", rate_in: 4, rate_out: 20),
|
|
26
|
+
Model.new(id: "claude-opus-5", label: "Opus 5", rate_in: 5, rate_out: 25),
|
|
27
|
+
Model.new(id: "claude-sonnet-5-5", label: "Sonnet 5.5", rate_in: 2, rate_out: 10),
|
|
28
|
+
Model.new(id: "claude-sonnet-5", label: "Sonnet 5", rate_in: 2, rate_out: 10),
|
|
29
|
+
Model.new(id: "claude-haiku-5-5", label: "Haiku 5.5", rate_in: 0.10, rate_out: 0.50)
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# What an agent may actually run on: the catalogue below {CEILING}. A
|
|
33
|
+
# model the CLI would be asked for as something cheaper is not offered,
|
|
34
|
+
# so the page never shows a choice that will not happen.
|
|
35
|
+
MODELS = CATALOGUE.select { |m| LADDER.index { |tier| m.id.include?(tier) } <= LADDER.index(CEILING) }.freeze
|
|
36
|
+
|
|
37
|
+
# The tier `claude` runs when an agent names no model, so an unnamed
|
|
38
|
+
# model is still priced rather than shown as free.
|
|
39
|
+
UNNAMED = "sonnet"
|
|
40
|
+
|
|
41
|
+
# The variable `claude` reads its autocompact threshold from, as a
|
|
42
|
+
# percentage. Found in the 2.1.x binary, where `Vle()` reads it with
|
|
43
|
+
# `parseFloat(process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE)`.
|
|
44
|
+
AUTOCOMPACT_VARIABLE = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"
|
|
45
|
+
|
|
46
|
+
# Flags that keep the operator's personal setup out of the agent. The
|
|
47
|
+
# project's CLAUDE.md and settings still load. Measured on a one-word
|
|
48
|
+
# prompt: 143 tools, 60 MCP servers, 358 skills and 17 hook events
|
|
49
|
+
# without these; 28 tools and none of the rest with them, at three
|
|
50
|
+
# quarters of the context and half the start-up time.
|
|
51
|
+
LEAN = ["--setting-sources", "project,local",
|
|
52
|
+
"--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
53
|
+
"--disable-slash-commands"].freeze
|
|
54
|
+
|
|
55
|
+
# The one flag of {LEAN} a compaction drops: with it, `claude -p
|
|
56
|
+
# --resume <id> "/compact"` answers "/compact isn't available in this
|
|
57
|
+
# environment" and compacts nothing (measured on 2.1.x).
|
|
58
|
+
SLASH_COMMANDS_OFF = "--disable-slash-commands"
|
|
59
|
+
|
|
60
|
+
# @param invocation [OpenLanes::Adapters::Invocation]
|
|
61
|
+
# @return [Array<String>]
|
|
62
|
+
def argv(invocation)
|
|
63
|
+
# `--include-partial-messages` is what the token meter runs on. Without
|
|
64
|
+
# it the stream reports a settled input count and a placeholder output
|
|
65
|
+
# count — 2 for a four-thousand-token answer — and a spinner counting
|
|
66
|
+
# what came back would read zero all run.
|
|
67
|
+
argv = ["claude", "-p", invocation.prompt, "--add-dir", invocation.root,
|
|
68
|
+
"--output-format", "stream-json", "--verbose", "--include-partial-messages", "--brief"]
|
|
69
|
+
argv += ["--resume", invocation.resume] if invocation.resume
|
|
70
|
+
argv += LEAN.reject { invocation.slash_commands && it == SLASH_COMMANDS_OFF } if invocation.lean
|
|
71
|
+
argv += ["--disallowedTools", invocation.denied_tools.join(",")] unless invocation.denied_tools.empty?
|
|
72
|
+
argv += ["--allowedTools", invocation.allowed_tools.join(",")] unless invocation.allowed_tools.empty?
|
|
73
|
+
model = model(invocation.model)
|
|
74
|
+
argv += ["--model", model] if model
|
|
75
|
+
argv += ["--effort", invocation.effort] if invocation.effort
|
|
76
|
+
argv
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [OpenLanes::Execution::Transcript]
|
|
80
|
+
def transcript(trace: nil, &) = Execution::Transcript.new(trace:, &)
|
|
81
|
+
|
|
82
|
+
# @param model [String, nil] an alias (`sonnet`) or a full id
|
|
83
|
+
# (`claude-fable-5-1`)
|
|
84
|
+
# @return [String, nil]
|
|
85
|
+
def model(model)
|
|
86
|
+
return model if model.nil? || model.to_s.empty?
|
|
87
|
+
|
|
88
|
+
rank = LADDER.index { |tier| model.to_s.include?(tier) }
|
|
89
|
+
rank && rank > LADDER.index(CEILING) ? CEILING : model.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def enforces_tool_denial? = true
|
|
94
|
+
|
|
95
|
+
# `claude -p --resume <id> "/compact"` compacts the session and prints a
|
|
96
|
+
# `compact_boundary` event; the agent cannot run it on itself, because
|
|
97
|
+
# `/compact` is the caller's command, not a tool.
|
|
98
|
+
#
|
|
99
|
+
# @return [String]
|
|
100
|
+
def compact_prompt = "/compact"
|
|
101
|
+
|
|
102
|
+
# @return [Hash{String => String}]
|
|
103
|
+
def environment = { AUTOCOMPACT_VARIABLE => AUTOCOMPACT.to_s }
|
|
104
|
+
|
|
105
|
+
# @return [Array<OpenLanes::Adapters::Model>]
|
|
106
|
+
def models = MODELS
|
|
107
|
+
|
|
108
|
+
# A full id as itself; an alias that is exactly a tier (`opus`) as the
|
|
109
|
+
# newest model of that tier, the first in {MODELS} to carry its name;
|
|
110
|
+
# no name as {UNNAMED}. Anything else, `not-opus` or `sonnet-old`
|
|
111
|
+
# included, is no model at all.
|
|
112
|
+
#
|
|
113
|
+
# @param name [String, nil]
|
|
114
|
+
# @return [OpenLanes::Adapters::Model, nil]
|
|
115
|
+
def model_entry(name)
|
|
116
|
+
text = name.to_s
|
|
117
|
+
text = UNNAMED if text.empty? || text == "default"
|
|
118
|
+
return MODELS.find { |m| m.id.match?(/\Aclaude-#{text}-/) } if LADDER.include?(text)
|
|
119
|
+
|
|
120
|
+
MODELS.find { |m| m.id == text }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Adapters
|
|
5
|
+
# `codex exec --json`, inside codex's own workspace-write sandbox.
|
|
6
|
+
#
|
|
7
|
+
# The sandbox has no network unless the agent declares `network: true`,
|
|
8
|
+
# which is the same default the Claude adapter gets from withholding
|
|
9
|
+
# WebFetch and WebSearch. Codex has no per-command denylist, so the
|
|
10
|
+
# forbidden commands are stated in the prompt and the after-check (HEAD
|
|
11
|
+
# unmoved, no new remote ref) is what actually holds the line.
|
|
12
|
+
class Codex < Base
|
|
13
|
+
# Codex says `xhigh` for the top of its scale and has no `max`.
|
|
14
|
+
EFFORT = { "low" => "low", "medium" => "medium", "high" => "high", "xhigh" => "xhigh", "max" => "xhigh" }.freeze
|
|
15
|
+
|
|
16
|
+
# @param invocation [OpenLanes::Adapters::Invocation]
|
|
17
|
+
# @return [Array<String>]
|
|
18
|
+
def argv(invocation)
|
|
19
|
+
argv = ["codex", "exec", "--json", "--sandbox", "workspace-write", "--cd", invocation.root]
|
|
20
|
+
argv << "--ignore-user-config" if invocation.lean
|
|
21
|
+
argv += ["--model", invocation.model] if invocation.model
|
|
22
|
+
effort = effort(invocation.effort)
|
|
23
|
+
argv += ["--config", %(model_reasoning_effort="#{effort}")] if effort
|
|
24
|
+
argv += ["--config", "sandbox_workspace_write.network_access=true"] if invocation.network
|
|
25
|
+
argv << invocation.prompt
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param effort [String, nil]
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
def effort(effort) = effort && EFFORT[effort.to_s]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Adapters
|
|
5
|
+
# One model an adapter offers: what the page lists in a car's dropdown,
|
|
6
|
+
# and what a token costs on it.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [r] id
|
|
9
|
+
# @return [String] the full model id, e.g. `claude-sonnet-5-5`
|
|
10
|
+
# @!attribute [r] label
|
|
11
|
+
# @return [String] e.g. `Sonnet 5.5`
|
|
12
|
+
# @!attribute [r] rate_in
|
|
13
|
+
# @return [Numeric] dollars per million tokens sent
|
|
14
|
+
# @!attribute [r] rate_out
|
|
15
|
+
# @return [Numeric] dollars per million tokens generated
|
|
16
|
+
# @!attribute [r] context
|
|
17
|
+
# @return [Integer] tokens the model's context window holds, the
|
|
18
|
+
# ceiling a car's context meter is drawn against
|
|
19
|
+
Model = Data.define(:id, :label, :rate_in, :rate_out, :context) do
|
|
20
|
+
def initialize(context: Model::WINDOW, **rest) = super
|
|
21
|
+
|
|
22
|
+
# @param up [Integer] tokens in
|
|
23
|
+
# @param down [Integer] tokens out
|
|
24
|
+
# @return [Float] dollars
|
|
25
|
+
def cost(up, down) = ((up.to_i * rate_in) + (down.to_i * rate_out)) / 1_000_000.0
|
|
26
|
+
|
|
27
|
+
# The `ModelChoice` of `web/src/api/types.ts`.
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def wire = { id:, label:, rates: { in: rate_in, out: rate_out } }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The context window a model gets when its entry names none: every
|
|
34
|
+
# model the adapters price today holds this many tokens.
|
|
35
|
+
Model::WINDOW = 200_000
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Adapters
|
|
5
|
+
# `pi -p --mode json`. Pi has no sandbox and no per-command denylist, so
|
|
6
|
+
# the after-check is the only guard, and the prompt says as much.
|
|
7
|
+
class Pi < Base
|
|
8
|
+
# Pi's `--thinking` scale already uses these words.
|
|
9
|
+
THINKING = Adapters::EFFORTS
|
|
10
|
+
|
|
11
|
+
# @param invocation [OpenLanes::Adapters::Invocation]
|
|
12
|
+
# @return [Array<String>]
|
|
13
|
+
def argv(invocation)
|
|
14
|
+
argv = ["pi", "--print", "--mode", "json", "--no-session"]
|
|
15
|
+
argv += ["--no-extensions", "--no-skills", "--no-prompt-templates"] if invocation.lean
|
|
16
|
+
argv += ["--model", invocation.model] if invocation.model
|
|
17
|
+
argv += ["--thinking", invocation.effort] if THINKING.include?(invocation.effort.to_s)
|
|
18
|
+
argv << invocation.prompt
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Adapters
|
|
7
|
+
# A transcript for a CLI that prints JSON lines in a schema this harness
|
|
8
|
+
# was not written against: `codex exec --json`, `pi --mode json`.
|
|
9
|
+
#
|
|
10
|
+
# It answers everything {OpenLanes::Execution::Transcript} answers, but
|
|
11
|
+
# it reads events by shape rather than by name. Any `usage` object with
|
|
12
|
+
# `input_tokens` and `output_tokens` moves the meter; any event whose
|
|
13
|
+
# type mentions a tool, a command or an exec counts as a tool call; any
|
|
14
|
+
# event typed as an error or a failure is the error. A CLI that renames
|
|
15
|
+
# its events keeps working, and one that stops reporting usage shows a
|
|
16
|
+
# meter at zero rather than a crash.
|
|
17
|
+
class Stream
|
|
18
|
+
# @param trace [String, nil] where every line is kept verbatim
|
|
19
|
+
# @yieldparam progress [OpenLanes::Execution::Transcript::Progress]
|
|
20
|
+
def initialize(trace: nil, &on_progress)
|
|
21
|
+
@on_progress = on_progress
|
|
22
|
+
@buffer = +""
|
|
23
|
+
@plain = []
|
|
24
|
+
@trace_path = trace
|
|
25
|
+
@trace = trace && File.open(trace, "a")
|
|
26
|
+
@up = 0
|
|
27
|
+
@down = 0
|
|
28
|
+
@cached = 0
|
|
29
|
+
@tools = 0
|
|
30
|
+
@activity = nil
|
|
31
|
+
@result = nil
|
|
32
|
+
@error = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Integer] tokens sent, cached input included
|
|
36
|
+
attr_reader :up
|
|
37
|
+
|
|
38
|
+
# @return [Integer] tokens generated
|
|
39
|
+
attr_reader :down
|
|
40
|
+
|
|
41
|
+
# @return [Integer] of {#up}, input the CLI reports as served from cache
|
|
42
|
+
attr_reader :cached
|
|
43
|
+
|
|
44
|
+
# @return [Integer]
|
|
45
|
+
attr_reader :tools
|
|
46
|
+
|
|
47
|
+
# @return [String, nil]
|
|
48
|
+
attr_reader :activity
|
|
49
|
+
|
|
50
|
+
# @return [String, nil] the last thing the agent said
|
|
51
|
+
attr_reader :result
|
|
52
|
+
|
|
53
|
+
# @return [Array<String>] lines that were not JSON
|
|
54
|
+
attr_reader :plain
|
|
55
|
+
|
|
56
|
+
# @return [String, nil]
|
|
57
|
+
attr_reader :error
|
|
58
|
+
|
|
59
|
+
# @return [String, nil]
|
|
60
|
+
attr_reader :trace_path
|
|
61
|
+
|
|
62
|
+
# @return [Integer, nil]
|
|
63
|
+
attr_accessor :pid
|
|
64
|
+
|
|
65
|
+
# These CLIs do not report sub-agents.
|
|
66
|
+
#
|
|
67
|
+
# @return [Integer]
|
|
68
|
+
def spawned = 0
|
|
69
|
+
|
|
70
|
+
# @return [Integer]
|
|
71
|
+
def delegated = 0
|
|
72
|
+
|
|
73
|
+
# @return [String, nil] nothing is sent through a side channel
|
|
74
|
+
def message = nil
|
|
75
|
+
|
|
76
|
+
# @return [Integer] new input plus output
|
|
77
|
+
def fresh = up - cached + down
|
|
78
|
+
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def failed? = !@error.nil?
|
|
81
|
+
|
|
82
|
+
# @return [OpenLanes::Execution::Transcript::Progress]
|
|
83
|
+
def progress = Execution::Transcript::Progress.new(activity:, up:, down:, subagents: 0, pid:, message: nil)
|
|
84
|
+
|
|
85
|
+
# @param chunk [String, nil]
|
|
86
|
+
# @return [void]
|
|
87
|
+
def push(chunk)
|
|
88
|
+
return if chunk.nil?
|
|
89
|
+
|
|
90
|
+
@buffer << chunk
|
|
91
|
+
while (index = @buffer.index("\n"))
|
|
92
|
+
consume(@buffer.slice!(0..index).chomp)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [void]
|
|
97
|
+
def finish
|
|
98
|
+
consume(@buffer.slice!(0..-1).to_s)
|
|
99
|
+
@trace&.close
|
|
100
|
+
@trace = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# @param line [String]
|
|
106
|
+
# @return [void]
|
|
107
|
+
def consume(line)
|
|
108
|
+
text = line.strip
|
|
109
|
+
return if text.empty?
|
|
110
|
+
|
|
111
|
+
@trace&.write("#{text}\n")
|
|
112
|
+
@trace&.flush
|
|
113
|
+
event = text.start_with?("{") ? parse(text) : nil
|
|
114
|
+
return @plain << text if event.nil?
|
|
115
|
+
|
|
116
|
+
handle(event)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param text [String]
|
|
120
|
+
# @return [Hash, nil]
|
|
121
|
+
def parse(text)
|
|
122
|
+
value = JSON.parse(text)
|
|
123
|
+
value.is_a?(Hash) ? value : nil
|
|
124
|
+
rescue JSON::ParserError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param event [Hash]
|
|
129
|
+
# @return [void]
|
|
130
|
+
def handle(event)
|
|
131
|
+
type = event["type"].to_s
|
|
132
|
+
meter(event)
|
|
133
|
+
@tools += 1 if type.match?(/tool|command|exec/i) && type.match?(/start|call|begin/i)
|
|
134
|
+
@error = describe_error(event) if type.match?(/error|fail/i)
|
|
135
|
+
said = text_in(event)
|
|
136
|
+
if said
|
|
137
|
+
@activity = said.lines.first.to_s.strip[0, Execution::Transcript::LIMIT]
|
|
138
|
+
@result = said
|
|
139
|
+
end
|
|
140
|
+
@on_progress&.call(progress)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Usage counts arrive per turn, so they are added rather than replaced.
|
|
144
|
+
#
|
|
145
|
+
# @param event [Hash]
|
|
146
|
+
# @return [void]
|
|
147
|
+
def meter(event)
|
|
148
|
+
usage = find(event, "usage")
|
|
149
|
+
return unless usage.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
@up += usage["input_tokens"].to_i
|
|
152
|
+
@down += usage["output_tokens"].to_i
|
|
153
|
+
@cached += usage["cached_input_tokens"].to_i
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @param event [Hash]
|
|
157
|
+
# @return [String]
|
|
158
|
+
def describe_error(event)
|
|
159
|
+
(find(event, "message") || find(event, "error") || event["type"]).to_s
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The agent's own words, wherever this CLI put them.
|
|
163
|
+
#
|
|
164
|
+
# @param event [Hash]
|
|
165
|
+
# @return [String, nil]
|
|
166
|
+
def text_in(event)
|
|
167
|
+
text = find(event, "text")
|
|
168
|
+
text.is_a?(String) && !text.strip.empty? ? text : nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Depth-first search for the first value under +key+.
|
|
172
|
+
#
|
|
173
|
+
# @param node [Object]
|
|
174
|
+
# @param key [String]
|
|
175
|
+
# @return [Object, nil]
|
|
176
|
+
def find(node, key)
|
|
177
|
+
case node
|
|
178
|
+
when Hash
|
|
179
|
+
return node[key] if node.key?(key)
|
|
180
|
+
|
|
181
|
+
node.each_value do |value|
|
|
182
|
+
found = find(value, key)
|
|
183
|
+
return found unless found.nil?
|
|
184
|
+
end
|
|
185
|
+
nil
|
|
186
|
+
when Array
|
|
187
|
+
node.each do |value|
|
|
188
|
+
found = find(value, key)
|
|
189
|
+
return found unless found.nil?
|
|
190
|
+
end
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|