openlanes 3.0.0 → 3.1.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 +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +62 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +24 -3
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +32 -10
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +26 -2
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -454
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -64
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +94 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1229 -116
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +38 -105
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +236 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +39 -11
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +665 -0
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +221 -31
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +290 -0
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +114 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +36 -1
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +38 -4
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +12 -8
- data/lib/openlanes/live/store.rb +36 -1
- data/lib/openlanes/live/subscriber.rb +93 -0
- data/lib/openlanes/live.rb +71 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +66 -7
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +110 -21
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +287 -7
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +210 -14
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +213 -15
- data/lib/openlanes/web/context.rb +33 -9
- data/lib/openlanes/web/demo.rb +39 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +137 -9
- data/lib/openlanes/web/launcher.rb +70 -8
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +31 -10
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +19 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -881
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-CI74IkWP.js +0 -198
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Live
|
|
5
|
+
# Rewrites a question an agent wrote as prose into D3's shape (D9): a
|
|
6
|
+
# short context paragraph and lettered options, exactly one recommended.
|
|
7
|
+
# The page draws a button per option and a text box under them, so a
|
|
8
|
+
# person can answer with one click instead of composing a reply to a
|
|
9
|
+
# paragraph.
|
|
10
|
+
#
|
|
11
|
+
# One call to a small model on OpenRouter, through `inquirex-llm`'s
|
|
12
|
+
# `OpenRouterAdapter`, which asks for a JSON object and checks that both
|
|
13
|
+
# fields came back. What the model returns is not trusted to be D3: it is
|
|
14
|
+
# cleaned, written out with {Plans::Choices.write}, and read back with
|
|
15
|
+
# {Plans::Choices.parse}, and anything that does not survive the trip is
|
|
16
|
+
# {Invalid}.
|
|
17
|
+
#
|
|
18
|
+
# It neither reads nor writes `blocked.md`; {Engine::Conversions} does,
|
|
19
|
+
# so this stays a function of text, and a seam: the suite sets
|
|
20
|
+
# {Live.choice_converter} to nil, and examples about it hand it an
|
|
21
|
+
# adapter double, so nothing in the tests reaches OpenRouter.
|
|
22
|
+
class ChoiceConverter
|
|
23
|
+
# Small and quick, and listed by OpenRouter and by inquirex-llm's own
|
|
24
|
+
# catalogue (`Models::OPENROUTER_ALIASES[:claude_haiku]`).
|
|
25
|
+
# `questions.model` in `.openlanes.yml` replaces it.
|
|
26
|
+
DEFAULT_MODEL = "anthropic/claude-haiku-5.5"
|
|
27
|
+
|
|
28
|
+
# What the model reads with the question.
|
|
29
|
+
PROMPT = <<~PROMPT
|
|
30
|
+
A software agent stopped and wrote the question below for a person. The person answers it in a
|
|
31
|
+
web page that shows one button per option and a free-text box under them. Rewrite the question as
|
|
32
|
+
multiple choice:
|
|
33
|
+
|
|
34
|
+
- `context`: one short paragraph with what the person needs to know to choose. Keep every fact,
|
|
35
|
+
number and name that matters; drop repetition. Do not repeat the heading.
|
|
36
|
+
- `options`: an array of 2 to 5 objects `{"label": String, "recommended": Boolean}`, each a distinct
|
|
37
|
+
answer the question already implies, written as one short line in the agent's own terms.
|
|
38
|
+
Exactly one has `"recommended": true`: the one the question itself prefers, or, if it prefers
|
|
39
|
+
none, the safest one.
|
|
40
|
+
|
|
41
|
+
Never change what is being asked, never add facts the question does not state, never answer it,
|
|
42
|
+
and never add an option for "something else" or "type something instead": the page has a text box.
|
|
43
|
+
PROMPT
|
|
44
|
+
|
|
45
|
+
# What the model answered could not be made into a D3 question.
|
|
46
|
+
class Invalid < Error; end
|
|
47
|
+
|
|
48
|
+
# @!attribute [r] body
|
|
49
|
+
# @return [String] the question's new body, D3-shaped Markdown
|
|
50
|
+
# @!attribute [r] count
|
|
51
|
+
# @return [Integer] how many options it offers
|
|
52
|
+
# @!attribute [r] model
|
|
53
|
+
# @return [String] the model that wrote it
|
|
54
|
+
Result = Data.define(:body, :count, :model)
|
|
55
|
+
|
|
56
|
+
# A converter when OPENROUTER_API_KEY is set, nil otherwise.
|
|
57
|
+
#
|
|
58
|
+
# @param env [#[]]
|
|
59
|
+
# @param config [OpenLanes::Config]
|
|
60
|
+
# @return [OpenLanes::Live::ChoiceConverter, nil]
|
|
61
|
+
def self.from_env(env = ENV, config: Config.current)
|
|
62
|
+
key = env["OPENROUTER_API_KEY"].to_s.strip
|
|
63
|
+
key.empty? ? nil : new(api_key: key, model: config.questions_model || DEFAULT_MODEL)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The model's answer, cleaned and written in D3's shape.
|
|
67
|
+
#
|
|
68
|
+
# @param output [Hash] `context` and `options`, as the adapter parsed them
|
|
69
|
+
# @return [String] the new body
|
|
70
|
+
# @raise [Invalid] when it cannot be one
|
|
71
|
+
def self.render(output)
|
|
72
|
+
output = output.to_h.transform_keys(&:to_sym)
|
|
73
|
+
context = output[:context].to_s.strip
|
|
74
|
+
raise Invalid, "no context" if context.empty?
|
|
75
|
+
# A heading inside the body would end the question there, and a
|
|
76
|
+
# `Bn` or `An` line would start another one.
|
|
77
|
+
if [Plans::BlockedFile::HEADING, Plans::OPEN_BLOCK, Plans::ANSWER_BLOCK].any? { context.match?(it) }
|
|
78
|
+
raise Invalid, "the context holds a heading"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
options = Array(output[:options]).map { option(it) }
|
|
82
|
+
.reject { |label, _| label.match?(Plans::Choices::SOMETHING_ELSE) }
|
|
83
|
+
raise Invalid, "#{options.size} option#{"s" unless options.size == 1}, not 2 or more" if options.size < 2
|
|
84
|
+
|
|
85
|
+
recommended = options.each_index.select { options[it].last }
|
|
86
|
+
raise Invalid, "#{recommended.size} options recommended, not one" unless recommended.size == 1
|
|
87
|
+
|
|
88
|
+
body = Plans::Choices.write(context:, labels: options.map(&:first), recommended: recommended.first)
|
|
89
|
+
back = Plans::Choices.parse(body)
|
|
90
|
+
raise Invalid, "the rewrite does not read back as #{options.size} choices" unless back&.options&.size == options.size
|
|
91
|
+
|
|
92
|
+
body
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# One option as a label on one line and whether it is recommended; a
|
|
96
|
+
# recommendation the model wrote into the label is taken out of it, since
|
|
97
|
+
# the flag already says so.
|
|
98
|
+
#
|
|
99
|
+
# @param raw [Hash, Object]
|
|
100
|
+
# @return [Array(String, Boolean)]
|
|
101
|
+
# @raise [Invalid]
|
|
102
|
+
def self.option(raw)
|
|
103
|
+
raise Invalid, "an option is not an object" unless raw.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
raw = raw.transform_keys(&:to_sym)
|
|
106
|
+
label = raw[:label].to_s.gsub(/\s+/, " ").sub(Plans::Choices::RECOMMENDED, " ").strip
|
|
107
|
+
raise Invalid, "an option has no label" if label.empty?
|
|
108
|
+
|
|
109
|
+
[label, [true, "true"].include?(raw[:recommended])]
|
|
110
|
+
end
|
|
111
|
+
private_class_method :option
|
|
112
|
+
|
|
113
|
+
# @param api_key [String, nil] OpenRouter's; only read to build the default adapter
|
|
114
|
+
# @param model [String] an OpenRouter model id
|
|
115
|
+
# @param adapter [#call, nil] answers `call(node, answers)` with a Hash
|
|
116
|
+
# holding `context` and `options`, as `Inquirex::LLM::OpenRouterAdapter`
|
|
117
|
+
# does; built on first use when nil, so a process that never converts
|
|
118
|
+
# never loads inquirex
|
|
119
|
+
def initialize(api_key: nil, model: DEFAULT_MODEL, adapter: nil)
|
|
120
|
+
@api_key = api_key
|
|
121
|
+
@model = model
|
|
122
|
+
@adapter = adapter
|
|
123
|
+
@mutex = Mutex.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [String]
|
|
127
|
+
attr_reader :model
|
|
128
|
+
|
|
129
|
+
# Blocks for as long as the request takes (the adapter gives up after
|
|
130
|
+
# 30 seconds without an answer), so the engine calls it on a thread.
|
|
131
|
+
#
|
|
132
|
+
# @param heading [String] the question's heading, for context; it is kept
|
|
133
|
+
# @param body [String] the question as the agent wrote it
|
|
134
|
+
# @return [OpenLanes::Live::ChoiceConverter::Result]
|
|
135
|
+
# @raise [Invalid] when the answer is not a D3 question
|
|
136
|
+
# @raise [StandardError] whatever the adapter raises: no network, a 4xx, bad JSON
|
|
137
|
+
def call(heading:, body:)
|
|
138
|
+
output = adapter.call(node, { heading: heading.to_s, question: body.to_s })
|
|
139
|
+
written = self.class.render(output)
|
|
140
|
+
Result.new(body: written, count: Plans::Choices.parse(written).options.size, model:)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def adapter
|
|
146
|
+
@mutex.synchronize do
|
|
147
|
+
@adapter ||= begin
|
|
148
|
+
require "inquirex-llm"
|
|
149
|
+
Inquirex::LLM::OpenRouterAdapter.new(api_key: @api_key, model:)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def node
|
|
155
|
+
@node ||= begin
|
|
156
|
+
require "inquirex-llm"
|
|
157
|
+
Inquirex::LLM::Node.new(id: :choices,
|
|
158
|
+
verb: :extract,
|
|
159
|
+
prompt: PROMPT,
|
|
160
|
+
schema: Inquirex::LLM::Schema.new(context: :string, options: :array),
|
|
161
|
+
from_steps: %i[heading question])
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Live
|
|
7
|
+
# A second opinion on {Plans::Choices}: does this message ask its reader
|
|
8
|
+
# to choose one of the options it lists? Asked of Jev, TypeSafe's System
|
|
9
|
+
# One model, through `inquirex-llm`'s `JevAdapter`, as one boolean.
|
|
10
|
+
#
|
|
11
|
+
# The parser cannot tell "Done so far: 1. … 2. …" from a numbered choice;
|
|
12
|
+
# Jev can. Its answer only ever narrows: `false` takes the buttons away and
|
|
13
|
+
# leaves the text box, `true` keeps them, and `nil` (no answer, an error, a
|
|
14
|
+
# timeout, an answer too unsure to count) leaves the parser's verdict as it
|
|
15
|
+
# was. A missing key therefore costs nothing but the second opinion.
|
|
16
|
+
#
|
|
17
|
+
# The snapshot is rebuilt every time the board moves, so every answer is
|
|
18
|
+
# kept, in this process, by the SHA256 of the text: one request per
|
|
19
|
+
# distinct question, ever. A request still in flight when {#call} gives up
|
|
20
|
+
# waiting goes on in its thread, and the next snapshot finds its answer.
|
|
21
|
+
#
|
|
22
|
+
# A seam, like {Vcs::GitHub} and {Linear::API}: the suite sets
|
|
23
|
+
# {Live.choice_judge} to nil, and examples about the judge hand it an
|
|
24
|
+
# adapter double, so nothing in the tests reaches TypeSafe.
|
|
25
|
+
class ChoiceJudge
|
|
26
|
+
# What Jev reads with the message. Its boolean question is whether
|
|
27
|
+
# `asks_to_choose` is true according to the message.
|
|
28
|
+
PROMPT = <<~PROMPT
|
|
29
|
+
The message below was written by a software agent to a person, who answers it in a web page.
|
|
30
|
+
`asks_to_choose` is true when the message asks the person to pick one of the options it lists
|
|
31
|
+
(lettered or numbered alternatives, or a plain yes/no question), and false when the list is
|
|
32
|
+
something else: steps already done, findings, a plan, or a question that wants a written answer.
|
|
33
|
+
PROMPT
|
|
34
|
+
|
|
35
|
+
# How long a snapshot waits for an answer it does not have yet.
|
|
36
|
+
TIMEOUT = 3.0
|
|
37
|
+
|
|
38
|
+
# Below this Jev is guessing, and the parser's verdict stands.
|
|
39
|
+
MIN_CONFIDENCE = 0.6
|
|
40
|
+
|
|
41
|
+
# The schema field Jev answers.
|
|
42
|
+
FIELD = :asks_to_choose
|
|
43
|
+
|
|
44
|
+
# A judge when TYPESAFE_API_KEY is set, nil otherwise.
|
|
45
|
+
#
|
|
46
|
+
# @param env [#[]]
|
|
47
|
+
# @return [OpenLanes::Live::ChoiceJudge, nil]
|
|
48
|
+
def self.from_env(env = ENV)
|
|
49
|
+
key = env["TYPESAFE_API_KEY"].to_s.strip
|
|
50
|
+
key.empty? ? nil : new(api_key: key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param api_key [String, nil] TypeSafe's; only read to build the default adapter
|
|
54
|
+
# @param adapter [#call, nil] answers `call(node, answers)` with a Hash
|
|
55
|
+
# holding {FIELD}, as `Inquirex::LLM::JevAdapter` does; built on first
|
|
56
|
+
# use when nil, so a process that never judges never loads inquirex
|
|
57
|
+
# @param timeout [Float] seconds {#call} waits for an answer it lacks
|
|
58
|
+
def initialize(api_key: nil, adapter: nil, timeout: TIMEOUT)
|
|
59
|
+
@api_key = api_key
|
|
60
|
+
@adapter = adapter
|
|
61
|
+
@timeout = timeout
|
|
62
|
+
@answers = {}
|
|
63
|
+
@mutex = Mutex.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param text [String] the question, as the person would read it
|
|
67
|
+
# @return [Boolean, nil] whether it asks to choose; nil when Jev has not
|
|
68
|
+
# said, or could not
|
|
69
|
+
def call(text)
|
|
70
|
+
key = Digest::SHA256.hexdigest(text.to_s)
|
|
71
|
+
asking = @mutex.synchronize { @answers[key] ||= ask(text.to_s) }
|
|
72
|
+
asking.join(@timeout) ? asking.value : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @return [Thread] whose value is true, false or nil
|
|
78
|
+
def ask(text)
|
|
79
|
+
Thread.new do
|
|
80
|
+
Thread.current.report_on_exception = false
|
|
81
|
+
adapter.call(node, { message: text })[FIELD]
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
UI.log("choice judge: no verdict (#{e.class})")
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def adapter
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@adapter ||= begin
|
|
91
|
+
require "inquirex-llm"
|
|
92
|
+
Inquirex::LLM::JevAdapter.new(api_key: @api_key, min_confidence: MIN_CONFIDENCE)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def node
|
|
98
|
+
@node ||= begin
|
|
99
|
+
require "inquirex-llm"
|
|
100
|
+
Inquirex::LLM::Node.new(id: :choice,
|
|
101
|
+
verb: :extract,
|
|
102
|
+
prompt: PROMPT,
|
|
103
|
+
schema: Inquirex::LLM::Schema.new(FIELD => :boolean),
|
|
104
|
+
from_steps: [:message])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -39,9 +39,17 @@ module OpenLanes
|
|
|
39
39
|
# Extend every claim this node holds. One that lapsed and was taken by
|
|
40
40
|
# another node in the meantime is dropped from {#held}, not fought for.
|
|
41
41
|
#
|
|
42
|
+
# One that is simply gone is taken back: a Redis that restarted empty
|
|
43
|
+
# (W32) has lost every claim, and reading that as "another machine
|
|
44
|
+
# took it" stopped this machine's own agents for plans nobody else
|
|
45
|
+
# wanted. `SET NX` still loses to a node that got there first.
|
|
46
|
+
#
|
|
42
47
|
# @return [Array<String>] the ordinals lost
|
|
43
48
|
def renew
|
|
44
|
-
lost = held.reject
|
|
49
|
+
lost = held.reject do |ordinal|
|
|
50
|
+
key = @keys.claim(ordinal)
|
|
51
|
+
@store.compare_and_expire(key, @keys.node, @ttl) || @store.set(key, @keys.node, ex: @ttl, nx: true)
|
|
52
|
+
end
|
|
45
53
|
@mutex.synchronize { @held -= lost }
|
|
46
54
|
lost
|
|
47
55
|
end
|
|
@@ -42,25 +42,28 @@ module OpenLanes
|
|
|
42
42
|
|
|
43
43
|
# Every kind the page may send.
|
|
44
44
|
KINDS = %w[answer hold_blocked release_hold resume discard message hold wrap_up compact stop stop_all
|
|
45
|
-
set_model switch_model add_plan create_plans
|
|
45
|
+
set_model switch_model add_plan create_plans respond pause_discuss resume_anyway
|
|
46
|
+
stop_plan restart_plan set_concurrency].freeze
|
|
46
47
|
|
|
47
48
|
# Kinds that act on one plan, and so must name it.
|
|
48
49
|
PLAN_KINDS = %w[answer hold_blocked release_hold resume discard message hold wrap_up compact stop
|
|
49
|
-
set_model switch_model].freeze
|
|
50
|
+
set_model switch_model respond pause_discuss resume_anyway stop_plan restart_plan].freeze
|
|
50
51
|
|
|
51
52
|
# Kinds that act on one agent of that plan, and so must name it too.
|
|
52
53
|
AGENT_KINDS = %w[hold wrap_up compact stop set_model switch_model].freeze
|
|
53
54
|
|
|
54
55
|
# Payload fields each kind cannot do without.
|
|
55
56
|
PAYLOAD = {
|
|
56
|
-
"answer"
|
|
57
|
-
"hold_blocked"
|
|
58
|
-
"release_hold"
|
|
59
|
-
"message"
|
|
60
|
-
"set_model"
|
|
61
|
-
"switch_model"
|
|
62
|
-
"add_plan"
|
|
63
|
-
"create_plans"
|
|
57
|
+
"answer" => %w[question text],
|
|
58
|
+
"hold_blocked" => %w[question],
|
|
59
|
+
"release_hold" => %w[question],
|
|
60
|
+
"message" => %w[text],
|
|
61
|
+
"set_model" => %w[model],
|
|
62
|
+
"switch_model" => %w[model],
|
|
63
|
+
"add_plan" => %w[ordinal],
|
|
64
|
+
"create_plans" => %w[idea],
|
|
65
|
+
"respond" => %w[text],
|
|
66
|
+
"set_concurrency" => %w[limit]
|
|
64
67
|
}.freeze
|
|
65
68
|
|
|
66
69
|
# Ids are ULIDs; this only keeps an absurd one out of a hash field.
|