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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes start` — the factory as a long-lived service: the engine and
|
|
6
|
+
# the control room together, with or without any plans.
|
|
7
|
+
#
|
|
8
|
+
# It keeps the board published and the commands read, rescans the tree,
|
|
9
|
+
# and starts agents as soon as an answer, a new plan or a hand edit makes
|
|
10
|
+
# something dispatchable. It never ends on its own, not even once every
|
|
11
|
+
# plan is finished: the page creates the next plan. It ends on Stop All
|
|
12
|
+
# Local in the page, Ctrl-C or SIGTERM.
|
|
13
|
+
#
|
|
14
|
+
# The page is the only way in: every decision a person makes goes through
|
|
15
|
+
# it, and the terminal is output only. So there is no headless fallback
|
|
16
|
+
# (Redis and the control room are required, not hoped for), no dry run,
|
|
17
|
+
# no `--no-web` and no `--until-settled`.
|
|
18
|
+
class Start < Run
|
|
19
|
+
desc "web + run in one: the engine and its control room, with or without plans, until Stop All Local"
|
|
20
|
+
|
|
21
|
+
# Inherited from `run`, and meaningless here: a dry run publishes no
|
|
22
|
+
# board, and a run without its page cannot be answered.
|
|
23
|
+
@options = options.reject { %i[dry_run web until_settled].include?(it.name) }
|
|
24
|
+
|
|
25
|
+
option :rescan,
|
|
26
|
+
default: Engine::Dispatcher::RESCAN.to_s,
|
|
27
|
+
desc: "Seconds between rescans of .plans while idle; a command from the page rescans at once"
|
|
28
|
+
|
|
29
|
+
example [
|
|
30
|
+
" # engine and control room, until Stop All Local or Ctrl-C",
|
|
31
|
+
"--no-open # print the control room's URL, open no browser",
|
|
32
|
+
"--port 4848 -j 4 # somewhere other than 11000, four agents at a time",
|
|
33
|
+
"--plan 005 # only plan 005, until stopped"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Exit status when Redis or the control room is not there to serve.
|
|
37
|
+
UNAVAILABLE = 69
|
|
38
|
+
|
|
39
|
+
# The progress log, echoed to the terminal: the terminal is where a
|
|
40
|
+
# person watches `start` from when the page is closed, and it takes
|
|
41
|
+
# no input.
|
|
42
|
+
#
|
|
43
|
+
# @param options [Hash]
|
|
44
|
+
# @return [void]
|
|
45
|
+
def call(**options)
|
|
46
|
+
@rescan = Integer(options.fetch(:rescan, Engine::Dispatcher::RESCAN), exception: false)
|
|
47
|
+
refuse("--rescan takes a whole number of seconds, at least 1.", 64) unless @rescan&.positive?
|
|
48
|
+
|
|
49
|
+
tap = UI.tap_log(lambda { |message, fields|
|
|
50
|
+
next if UI.quiet
|
|
51
|
+
|
|
52
|
+
Kernel.warn(Engine::ProgressLog.render(message, **fields.slice(:plan, :status, :agent, :seconds, :round)))
|
|
53
|
+
})
|
|
54
|
+
super
|
|
55
|
+
ensure
|
|
56
|
+
UI.untap_log(tap) if tap
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# @param _options [Hash]
|
|
62
|
+
# @param _web [OpenLanes::Web::Launcher]
|
|
63
|
+
# @return [Hash]
|
|
64
|
+
def staying(_options, _web) = { stay: true, rescan: @rescan }
|
|
65
|
+
|
|
66
|
+
# @return [void]
|
|
67
|
+
def without_live(message, _options)
|
|
68
|
+
refuse("#{message}\n\nopenlanes start needs Redis: its control room is the only way to answer the " \
|
|
69
|
+
"agents' questions. Start redis-server, or pass --redis-url. " \
|
|
70
|
+
"`openlanes run` goes on without it, headless.",
|
|
71
|
+
UNAVAILABLE)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [void]
|
|
75
|
+
def without_web(web, live, _options)
|
|
76
|
+
live.close
|
|
77
|
+
refuse("The control room did not answer on #{web.url}, and openlanes start is no use without it. " \
|
|
78
|
+
"Its log: #{web.log}",
|
|
79
|
+
UNAVAILABLE)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A project with nothing left to do opens on the idea dialog (W19).
|
|
83
|
+
#
|
|
84
|
+
# @return [String]
|
|
85
|
+
def browser_url(web, tree)
|
|
86
|
+
open = tree.subjects.reject { Engine::Dispatcher::FINISHED.include?(it.status.key) }
|
|
87
|
+
open.empty? ? "#{web.url}#start" : web.url
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# What the service did, and exit 0: stopping it on purpose is not a
|
|
91
|
+
# failure, and the failed attempts it lived through are listed above
|
|
92
|
+
# the box.
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
def finish(runner, attempts, options, seconds:)
|
|
96
|
+
puts ""
|
|
97
|
+
puts attempt_lines(attempts)
|
|
98
|
+
puts("", Engine.tally(attempts:, seconds:).render)
|
|
99
|
+
unless quiet?(options)
|
|
100
|
+
blocked = runner.blocked.map { it.feature.ordinal }
|
|
101
|
+
lines = ["Stopped. Unfinished plans keep their state; openlanes start picks them up again."]
|
|
102
|
+
lines << "Still waiting for answers: #{blocked.join(", ")}" unless blocked.empty?
|
|
103
|
+
success("#{attempts.size} invocation#{"s" unless attempts.size == 1}, " \
|
|
104
|
+
"#{attempts.count(&:advanced?)} advanced\n\n#{lines.join("\n")}")
|
|
105
|
+
end
|
|
106
|
+
exit(0)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes state` — a plan's `state.json`, read and signed. Agents sign
|
|
6
|
+
# through this rather than editing the file, because the command takes the
|
|
7
|
+
# lock that keeps two agents on one plan from overwriting each other.
|
|
8
|
+
module State
|
|
9
|
+
# Both subcommands name a plan and need its folder.
|
|
10
|
+
module Locating
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @param options [Hash]
|
|
14
|
+
# @return [OpenLanes::Plans::PlanState]
|
|
15
|
+
def plan_state_for(options)
|
|
16
|
+
tree = tree_for(options)
|
|
17
|
+
ordinal = Plans::Ordinal.parse(options[:plan].to_s)
|
|
18
|
+
subject = ordinal && tree.find(ordinal)
|
|
19
|
+
refuse("No plan #{options[:plan]} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66) unless subject
|
|
20
|
+
|
|
21
|
+
Plans::PlanState.for(subject.feature.path)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module CLI
|
|
7
|
+
module State
|
|
8
|
+
# `openlanes state show` — the plan's `state.json`, on STDOUT, so it
|
|
9
|
+
# pipes into `jq`.
|
|
10
|
+
class Show < Base
|
|
11
|
+
include Locating
|
|
12
|
+
|
|
13
|
+
desc "Print a plan's state.json"
|
|
14
|
+
|
|
15
|
+
option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
|
|
16
|
+
|
|
17
|
+
example [
|
|
18
|
+
"--plan 003",
|
|
19
|
+
"--plan 003 | jq '.stages[] | {agent, round, status, model, seconds}'"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# @param options [Hash]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def call(**options)
|
|
25
|
+
$stdout.puts(JSON.pretty_generate(plan_state_for(options).read))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module State
|
|
6
|
+
# `openlanes state sign` — an agent's ledger line, written into the
|
|
7
|
+
# plan's `state.json`.
|
|
8
|
+
class Sign < Base
|
|
9
|
+
include Locating
|
|
10
|
+
|
|
11
|
+
desc "Sign a plan's ledger: Started, Completed, Almost completed, Interrupted or Blocked"
|
|
12
|
+
|
|
13
|
+
option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
|
|
14
|
+
option :agent, required: true, desc: "The agent signing"
|
|
15
|
+
option :round, required: true, desc: "Which round this is, as the prompt states it"
|
|
16
|
+
option :status, required: true, values: Plans::Ledger::STATUSES, desc: "Where the agent stands"
|
|
17
|
+
option :note, desc: "A word or two the harness reads, e.g. approved, rejected, technical, product"
|
|
18
|
+
option :next, desc: "Only with Completed: the agent that should go next"
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
"--plan 003 --agent luke-backend --round 1 --status Started",
|
|
22
|
+
"--plan 003 --agent hansolo-reviewer --round 1 --status Completed --note approved",
|
|
23
|
+
"--plan 003 --agent yoda-writer --round 1 --status Completed --next palpatine-planner"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# @param options [Hash]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def call(**options)
|
|
29
|
+
state = plan_state_for(options)
|
|
30
|
+
signature = begin
|
|
31
|
+
state.sign!(agent: options[:agent],
|
|
32
|
+
round: Integer(options[:round].to_s, exception: false) || refuse("--round must be a number", 64),
|
|
33
|
+
status: options[:status],
|
|
34
|
+
note: options[:note],
|
|
35
|
+
next_agent: options[:next])
|
|
36
|
+
rescue OpenLanes::Error => e
|
|
37
|
+
refuse(e.message, 64)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return if quiet?(options)
|
|
41
|
+
|
|
42
|
+
UI.line("#{signature["agent"]}: #{signature["status"]}, round #{signature["round"]} " \
|
|
43
|
+
"in #{File.basename(state.dir)}/#{Plans::PlanState::FILENAME}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes states` — the state machine, as a terminal diagram.
|
|
6
|
+
class States < Dry::CLI::Command
|
|
7
|
+
desc "Print a diagram of every state and the transitions between them"
|
|
8
|
+
|
|
9
|
+
example [""]
|
|
10
|
+
|
|
11
|
+
# @return [void]
|
|
12
|
+
def call(**) = $stdout.write(Presentation.diagram)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes unblock NNN…` — hand a stopped plan to the agent that
|
|
6
|
+
# drains its `blocked.md`.
|
|
7
|
+
#
|
|
8
|
+
# Deliberately not part of `run`. ⭕️ and 🅱️ are {StateMachine::SETTLED}, so
|
|
9
|
+
# the loop never offers a blocked plan to anybody, and that is the property
|
|
10
|
+
# that makes the state mean anything: the plan waits for a human, and no
|
|
11
|
+
# agent quietly decides otherwise. Answers arriving is not a fact the tool
|
|
12
|
+
# can observe, so a human typing this command *is* the signal, and there is
|
|
13
|
+
# nothing else that could produce it.
|
|
14
|
+
class Unblock < Base
|
|
15
|
+
# The agent that knows the shape of `blocked.md`.
|
|
16
|
+
DEFAULT_AGENT = "lando-broker"
|
|
17
|
+
|
|
18
|
+
# What each token turned out to be, for whoever has to fix it.
|
|
19
|
+
PROBLEMS = {
|
|
20
|
+
missing: "no plan of that number",
|
|
21
|
+
not_blocked: "no blocked.md, so there is nothing to drain"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
desc "Fold answered blocks into a plan's documents and retire blocked.md"
|
|
25
|
+
|
|
26
|
+
argument :plans,
|
|
27
|
+
type: :array,
|
|
28
|
+
required: true,
|
|
29
|
+
desc: "Which plans to drain: NNN or NNN.MM, e.g. 003 005.01"
|
|
30
|
+
|
|
31
|
+
dry_run_option "Invoke no agent; print the questions still open"
|
|
32
|
+
option :agent, default: DEFAULT_AGENT, desc: "Hand the folder to a different agent"
|
|
33
|
+
option :root, desc: "Repository root the agent works in (default: the .plans parent)"
|
|
34
|
+
|
|
35
|
+
example [
|
|
36
|
+
"003 -n # what 003 is still waiting on",
|
|
37
|
+
"003 # fold in whatever has been answered",
|
|
38
|
+
"003,005 # both"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# @param plans [Array<String>]
|
|
42
|
+
# @param options [Hash]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def call(plans:, **options)
|
|
45
|
+
tree = tree_for(options)
|
|
46
|
+
quiet?(options)
|
|
47
|
+
unblocker = Lifecycle.unblocker(tree:,
|
|
48
|
+
agent: agent_for(options),
|
|
49
|
+
root: options[:root],
|
|
50
|
+
commit: !dry_run?(options),
|
|
51
|
+
executor: Execution.executor(root: options[:root] || File.dirname(tree.dir),
|
|
52
|
+
dry_run: dry_run?(options)))
|
|
53
|
+
|
|
54
|
+
targets = unblocker.resolve(plans)
|
|
55
|
+
refused(targets, tree, options)
|
|
56
|
+
subjects = targets.select(&:drainable?).map(&:subject)
|
|
57
|
+
exit(worst(targets, [])) if subjects.empty?
|
|
58
|
+
|
|
59
|
+
credentials_warning if !dry_run?(options) && !quiet?(options)
|
|
60
|
+
preflight(subjects, unblocker, options)
|
|
61
|
+
|
|
62
|
+
outcomes = Execution.on_interrupt { unblocker.call(subjects) }
|
|
63
|
+
outcomes.each { |outcome| report(outcome, options) }
|
|
64
|
+
footer(outcomes, options) unless quiet?(options)
|
|
65
|
+
exit(worst(targets, outcomes))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# @param options [Hash]
|
|
71
|
+
# @return [OpenLanes::Agents::Agent]
|
|
72
|
+
def agent_for(options)
|
|
73
|
+
name = options.fetch(:agent, DEFAULT_AGENT)
|
|
74
|
+
agents = OpenLanes::Agents.registry
|
|
75
|
+
agents.find(name) or
|
|
76
|
+
refuse("No agent called #{name}.\n\nKnown: #{agents.all.map(&:name).join(", ")}", 65)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A number that names no folder, or a folder that was never stopped, is
|
|
80
|
+
# refused rather than skipped. Whoever typed this believes an answer has
|
|
81
|
+
# arrived; running against a folder nobody blocked and saying nothing is
|
|
82
|
+
# how you come back later to a plan nobody touched and no record of why.
|
|
83
|
+
#
|
|
84
|
+
# Every bad token is reported, not just the first, because stopping at
|
|
85
|
+
# the first one hides the rest of the answer.
|
|
86
|
+
#
|
|
87
|
+
# @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
|
|
88
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
89
|
+
# @param options [Hash]
|
|
90
|
+
# @return [void]
|
|
91
|
+
def refused(targets, tree, options)
|
|
92
|
+
problems = targets.reject(&:drainable?)
|
|
93
|
+
return if problems.empty?
|
|
94
|
+
|
|
95
|
+
problems.each do |target|
|
|
96
|
+
state = target.subject ? target.subject.status.key : "unknown"
|
|
97
|
+
puts "#{target.token}\t#{state}\t0 open\t#{PROBLEMS.fetch(target.problem)}"
|
|
98
|
+
end
|
|
99
|
+
return if quiet?(options)
|
|
100
|
+
|
|
101
|
+
error("#{problems.size} of the plans named cannot be drained:\n\n" +
|
|
102
|
+
problems.map { |t| " #{t.token} — #{PROBLEMS.fetch(t.problem)}" }.join("\n") +
|
|
103
|
+
"\n\nThe tree holds: #{tree.ordinals.join(", ")}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# What each folder is waiting on, said BEFORE the agent is invoked.
|
|
107
|
+
#
|
|
108
|
+
# This is the half that was missing. A real run hands the folder to an
|
|
109
|
+
# agent that can take a quarter of an hour, and until it came back the
|
|
110
|
+
# terminal showed nothing at all — a run that had found nothing to do and
|
|
111
|
+
# a run still working looked exactly alike.
|
|
112
|
+
#
|
|
113
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>]
|
|
114
|
+
# @param unblocker [OpenLanes::Lifecycle::Unblocker]
|
|
115
|
+
# @param options [Hash]
|
|
116
|
+
# @return [void]
|
|
117
|
+
def preflight(subjects, _unblocker, options)
|
|
118
|
+
return if quiet?(options)
|
|
119
|
+
|
|
120
|
+
subjects.each do |subject|
|
|
121
|
+
questions = Lifecycle::Unblocker.questions(subject)
|
|
122
|
+
answered = questions.count(&:answered)
|
|
123
|
+
say("#{paint(subject.feature.ordinal.to_s, :bright_black)} #{subject.status.emoji} " \
|
|
124
|
+
"#{subject.feature.title} — #{summary(questions.size, answered)}")
|
|
125
|
+
detail(subject, questions)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param open [Integer]
|
|
130
|
+
# @param answered [Integer]
|
|
131
|
+
# @return [String]
|
|
132
|
+
def summary(open, answered)
|
|
133
|
+
return "nothing open" if open.zero?
|
|
134
|
+
|
|
135
|
+
"#{open} open, #{answered.zero? ? "none answered yet" : "#{answered} with an answer waiting"}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
139
|
+
# @param questions [Array<OpenLanes::Lifecycle::Unblocker::Question>]
|
|
140
|
+
# @return [void]
|
|
141
|
+
def detail(subject, questions)
|
|
142
|
+
if subject.unreadable_block?
|
|
143
|
+
say(" #{paint("blocked.md names no `## B<n>` question, so nothing here can be drained", :red)}",
|
|
144
|
+
bullet: " ")
|
|
145
|
+
say(" #{paint("Number each open question `## B1`, `## B2`, and each answer `## A1`, `## A2`.", :yellow)}",
|
|
146
|
+
bullet: " ")
|
|
147
|
+
elsif questions.empty?
|
|
148
|
+
say(" #{paint("nothing left open", :green)}", bullet: " ")
|
|
149
|
+
else
|
|
150
|
+
questions.each { |question| say(" #{paint(question.to_s, :yellow)}", bullet: " ") }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# One plan, after the fact: the deliverable line on STDOUT, and what
|
|
155
|
+
# changed on STDERR.
|
|
156
|
+
#
|
|
157
|
+
# @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
|
|
158
|
+
# @param options [Hash]
|
|
159
|
+
# @return [void]
|
|
160
|
+
def report(outcome, options)
|
|
161
|
+
moved = movement(outcome, !dry_run?(options))
|
|
162
|
+
puts "#{outcome.ordinal}\t#{outcome.ok ? outcome.subject.status.key : "failed"}\t" \
|
|
163
|
+
"#{outcome.after.size} open\t#{moved}\t#{outcome.note}"
|
|
164
|
+
# A dry run changed nothing, so the preflight above it is still the
|
|
165
|
+
# whole truth. Printing the same three lines again reads as a second
|
|
166
|
+
# pass that found the same thing, which is not what happened.
|
|
167
|
+
return if quiet?(options) || dry_run?(options)
|
|
168
|
+
|
|
169
|
+
say("#{paint(outcome.ordinal.to_s, :bright_black)} #{outcome.subject.status.emoji} " \
|
|
170
|
+
"#{outcome.subject.feature.title} — #{paint(moved, outcome.ok ? :green : :red)}")
|
|
171
|
+
detail(outcome.subject, outcome.after)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# What the file says happened, rather than what the agent claims. The
|
|
175
|
+
# questions are counted off disk on both sides of the run.
|
|
176
|
+
#
|
|
177
|
+
# @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
|
|
178
|
+
# @param commit [Boolean]
|
|
179
|
+
# @return [String]
|
|
180
|
+
def movement(outcome, commit)
|
|
181
|
+
return "failed" unless outcome.ok
|
|
182
|
+
return "not attempted" unless commit
|
|
183
|
+
return "blocked.md retired" if outcome.cleared?
|
|
184
|
+
return "nothing folded" if outcome.folded.empty?
|
|
185
|
+
|
|
186
|
+
"folded #{outcome.folded.map { |n| "B#{n}" }.join(", ")}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
|
|
190
|
+
# @param options [Hash]
|
|
191
|
+
# @return [void]
|
|
192
|
+
def footer(outcomes, options)
|
|
193
|
+
failed = outcomes.reject(&:ok)
|
|
194
|
+
|
|
195
|
+
if dry_run?(options)
|
|
196
|
+
warn("Dry run: no agent was invoked, and #{outcomes.size} " \
|
|
197
|
+
"plan#{"s" unless outcomes.size == 1} #{outcomes.size == 1 ? "is" : "are"} unchanged.\n" \
|
|
198
|
+
"Re-run without --dry-run to fold in whatever has been answered.")
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return error(failed.map { |o| "#{o.ordinal}: #{o.note}" }.join("\n")) unless failed.empty?
|
|
203
|
+
|
|
204
|
+
cleared = outcomes.count(&:cleared?)
|
|
205
|
+
folded = outcomes.sum { |o| o.folded.size }
|
|
206
|
+
waiting = outcomes.sum { |o| o.after.size }
|
|
207
|
+
success("#{folded} question#{"s" unless folded == 1} folded in · " \
|
|
208
|
+
"#{cleared} plan#{"s" unless cleared == 1} out of the block · " \
|
|
209
|
+
"#{waiting} still waiting on a human." +
|
|
210
|
+
(if folded.zero?
|
|
211
|
+
"\n\nNothing moved. An answer has to be written into blocked.md as its own " \
|
|
212
|
+
"`## A<n>` section, answering the `## B<n>` of the same number, before " \
|
|
213
|
+
"there is anything to fold."
|
|
214
|
+
else
|
|
215
|
+
""
|
|
216
|
+
end))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The most serious thing that happened, as an exit status. A tree that
|
|
220
|
+
# was partly drained still exits non-zero when part of it could not be.
|
|
221
|
+
#
|
|
222
|
+
# @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
|
|
223
|
+
# @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
|
|
224
|
+
# @return [Integer]
|
|
225
|
+
def worst(targets, outcomes)
|
|
226
|
+
problems = targets.map(&:problem)
|
|
227
|
+
return 66 if problems.include?(:missing)
|
|
228
|
+
return 65 if problems.include?(:not_blocked)
|
|
229
|
+
return 1 unless outcomes.all?(&:ok)
|
|
230
|
+
|
|
231
|
+
0
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes version`
|
|
6
|
+
class Version < Dry::CLI::Command
|
|
7
|
+
desc "Print the version and exit"
|
|
8
|
+
|
|
9
|
+
# @return [void]
|
|
10
|
+
def call(**) = puts("openlanes #{OpenLanes::VERSION}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes web` — the control room's API server on its own. `openlanes run`
|
|
6
|
+
# starts one beside the engine; this is that server, and with `--demo`
|
|
7
|
+
# the same server over a pretend engine, for Cypress and front-end work.
|
|
8
|
+
class Web < Base
|
|
9
|
+
desc "Serve the control room page and its API on 127.0.0.1"
|
|
10
|
+
|
|
11
|
+
option :port,
|
|
12
|
+
default: OpenLanes::Web::DEFAULT_PORT.to_s,
|
|
13
|
+
desc: "Port to listen on"
|
|
14
|
+
option :redis_url,
|
|
15
|
+
desc: "Redis the engine publishes to (default: $OPENLANES_REDIS_URL, else .openlanes.yml redis_url, else $REDIS_URL, " \
|
|
16
|
+
"else #{Live::DEFAULT_URL})"
|
|
17
|
+
option :open,
|
|
18
|
+
type: :boolean,
|
|
19
|
+
default: true,
|
|
20
|
+
desc: "Open the page in the browser; --no-open prints the URL only"
|
|
21
|
+
option :demo,
|
|
22
|
+
type: :boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
desc: "Serve a sample run that moves and answers commands, with no agents. " \
|
|
25
|
+
"Uses Redis when it answers, an in-memory store when it does not"
|
|
26
|
+
|
|
27
|
+
example [
|
|
28
|
+
" # serve the live run of this checkout",
|
|
29
|
+
"--demo --no-open # the sample run, for Cypress and npm run dev",
|
|
30
|
+
"--port 4848 # somewhere other than 11000"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# @param options [Hash]
|
|
34
|
+
# @return [void]
|
|
35
|
+
def call(**options)
|
|
36
|
+
tree = tree_for(options)
|
|
37
|
+
root = File.dirname(tree.dir)
|
|
38
|
+
url = Live.redis_url(option: redis_url_option(options), config: Config.current)
|
|
39
|
+
stores = stores_for(url, options)
|
|
40
|
+
node = Live::Node.current(root:)
|
|
41
|
+
keys = Live.keys(root:, node:)
|
|
42
|
+
keys = keys.with(repo: "demo/#{keys.repo}") if options[:demo]
|
|
43
|
+
demo = OpenLanes::Web.demo(store: stores.current, keys:).start if options[:demo]
|
|
44
|
+
context = OpenLanes::Web.context(stores:, keys:, plans_dir: tree.dir, url: stores.current.url, node:, demo:)
|
|
45
|
+
server = OpenLanes::Web.server(app: OpenLanes::Web.app(context), context:, port: options[:port])
|
|
46
|
+
serve(server, options, keys:, node:, url: stores.current.url)
|
|
47
|
+
ensure
|
|
48
|
+
demo&.stop
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# `--redis-url`, else the URL `openlanes run` hands its server through the
|
|
54
|
+
# environment, so a password never shows in `ps`. Both are passed to
|
|
55
|
+
# {Live.redis_url} as the option, which keeps the rest of its order.
|
|
56
|
+
#
|
|
57
|
+
# @param options [Hash]
|
|
58
|
+
# @param env [Hash]
|
|
59
|
+
# @return [String, nil]
|
|
60
|
+
def redis_url_option(options, env: ENV)
|
|
61
|
+
[options[:redis_url], env[OpenLanes::Web::Launcher::REDIS_URL_ENV]].find { !it.to_s.strip.empty? }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [OpenLanes::Web::Stores]
|
|
65
|
+
def stores_for(url, options)
|
|
66
|
+
store = Live.connect(url)
|
|
67
|
+
store.close
|
|
68
|
+
OpenLanes::Web::Stores.new { Live.store(url) }
|
|
69
|
+
rescue Live::Unavailable => e
|
|
70
|
+
unless options[:demo]
|
|
71
|
+
refuse("#{e.message}\n\nThe page reads the run from Redis. Start redis-server, pass --redis-url, " \
|
|
72
|
+
"or try --demo.",
|
|
73
|
+
69)
|
|
74
|
+
end
|
|
75
|
+
warn("#{e.message}\n\nThe demo keeps its board in memory instead: this page only, no other machine.") unless quiet?(options)
|
|
76
|
+
OpenLanes::Web::Stores.shared(Live.memory_store)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Listen until INT or TERM.
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
def serve(server, options, keys:, node:, url:)
|
|
83
|
+
begin
|
|
84
|
+
server.start
|
|
85
|
+
rescue Errno::EADDRINUSE
|
|
86
|
+
refuse("Port #{options[:port]} is taken: another `openlanes web` may already be serving. " \
|
|
87
|
+
"Pass --port to use another.",
|
|
88
|
+
69)
|
|
89
|
+
end
|
|
90
|
+
%w[INT TERM].each { |name| Signal.trap(name) { Thread.new { server.stop } } }
|
|
91
|
+
unless quiet?(options)
|
|
92
|
+
info("Control room: #{server.url}#{" (demo)" if options[:demo]}\n" \
|
|
93
|
+
"Redis: #{Live.mask(url)} as #{node.label} (#{keys.node}), repository #{keys.repo}")
|
|
94
|
+
end
|
|
95
|
+
OpenLanes::Web.open(server.url) if options[:open]
|
|
96
|
+
server.wait
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes worktree` — create and seed worktrees for plans.
|
|
6
|
+
class Worktree < Base
|
|
7
|
+
desc "Create and seed a worktree for a plan"
|
|
8
|
+
|
|
9
|
+
argument :plan,
|
|
10
|
+
required: true,
|
|
11
|
+
desc: "Plan ordinal (e.g. 002, 002.00, or a plan folder name)"
|
|
12
|
+
|
|
13
|
+
option :skip_seed,
|
|
14
|
+
type: :boolean,
|
|
15
|
+
default: false,
|
|
16
|
+
desc: "Create the worktree but skip seeding it with .env and credential files"
|
|
17
|
+
|
|
18
|
+
example [
|
|
19
|
+
"002 # create/seed the worktree for plan 002.00",
|
|
20
|
+
"002.01 # create/seed the worktree for plan 002.01",
|
|
21
|
+
"002.00-⚪️ → update-dsl # find the plan by its folder name",
|
|
22
|
+
"--skip-seed 003 # create the worktree but don't seed it"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# @param plan [String] plan identifier
|
|
26
|
+
# @param options [Hash]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def call(plan:, **options)
|
|
29
|
+
tree = tree_for(options)
|
|
30
|
+
is_quiet = quiet?(options)
|
|
31
|
+
|
|
32
|
+
feature = resolve_plan(tree, plan)
|
|
33
|
+
unless feature
|
|
34
|
+
refuse("Could not find plan: #{plan}", 66)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
root = File.dirname(tree.dir)
|
|
38
|
+
worktree = OpenLanes::Vcs.worktree(root:)
|
|
39
|
+
|
|
40
|
+
checkout = worktree.checkout_for(feature)
|
|
41
|
+
|
|
42
|
+
return if is_quiet
|
|
43
|
+
|
|
44
|
+
if checkout.created?
|
|
45
|
+
puts("✓ Created worktree for #{feature.ordinal}: #{checkout.path}")
|
|
46
|
+
else
|
|
47
|
+
puts("✓ Using existing worktree for #{feature.ordinal}: #{checkout.path}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless options[:skip_seed]
|
|
51
|
+
seeded = worktree.seed(checkout.path)
|
|
52
|
+
if seeded
|
|
53
|
+
puts("✓ Seeded with .env and credential files")
|
|
54
|
+
else
|
|
55
|
+
warn("⚠ Could not seed worktree, see above for details")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts("")
|
|
60
|
+
success("Worktree ready for #{feature.ordinal}")
|
|
61
|
+
puts("Path: #{checkout.path}")
|
|
62
|
+
puts("Branch: #{checkout.branch}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Find the plan that matches the given identifier.
|
|
68
|
+
#
|
|
69
|
+
# @param tree [OpenLanes::Plans::Tree] plans directory wrapper
|
|
70
|
+
# @param plan [String] ordinal, folder name, or slug
|
|
71
|
+
# @return [OpenLanes::Plans::Feature, nil]
|
|
72
|
+
def resolve_plan(tree, plan)
|
|
73
|
+
# Try parsing as an ordinal first
|
|
74
|
+
ordinal = ::OpenLanes::Plans::Ordinal.parse(plan)
|
|
75
|
+
if ordinal
|
|
76
|
+
found = tree.features.find { |f| f.ordinal == ordinal }
|
|
77
|
+
return found if found
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Try matching by folder name
|
|
81
|
+
tree.features.find { |f| f.dirname == plan }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|