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,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes agents …` — the specialist roster.
|
|
6
|
+
module Agents
|
|
7
|
+
# `openlanes agents list`
|
|
8
|
+
class List < Dry::CLI::Command
|
|
9
|
+
include UI
|
|
10
|
+
|
|
11
|
+
desc "List every specialist, what it handles, and what it advances to"
|
|
12
|
+
|
|
13
|
+
example ["", "| less -R"]
|
|
14
|
+
|
|
15
|
+
# @return [void]
|
|
16
|
+
def call(**) = $stdout.write(OpenLanes::Agents.roster.list)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module CLI
|
|
7
|
+
# Shared flags and the plumbing every command needs.
|
|
8
|
+
class Base < Dry::CLI::Command
|
|
9
|
+
include UI
|
|
10
|
+
|
|
11
|
+
# dry-cli gathers every token a command did not declare an argument for
|
|
12
|
+
# into :args, and hands it over without a word. `openlanes docs
|
|
13
|
+
# .plans/README.md` — the `-o` forgotten — therefore ran with every
|
|
14
|
+
# default, wrote the conventions to the default path, and printed
|
|
15
|
+
# success for a file the caller never named.
|
|
16
|
+
#
|
|
17
|
+
# Prepended rather than called from each `call`, because the command
|
|
18
|
+
# that forgets this check is the one that needed it.
|
|
19
|
+
module RefuseStrayArguments
|
|
20
|
+
# @param options [Hash]
|
|
21
|
+
# @return [void]
|
|
22
|
+
def call(**options)
|
|
23
|
+
stray = Array(options[:args]) - self.class.arguments.flat_map { Array(options[it.name.to_sym]) }
|
|
24
|
+
unless stray.empty?
|
|
25
|
+
refuse("This command takes no argument like #{stray.join(" ")}.\n" \
|
|
26
|
+
"An option it does take may be the one you meant: run it with -h.",
|
|
27
|
+
64)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Every project command starts at the project root, wherever it was
|
|
35
|
+
# typed: a `.plans` found relative to a subdirectory is a `.plans` that
|
|
36
|
+
# does not exist, and the refusal that follows names the wrong problem.
|
|
37
|
+
#
|
|
38
|
+
# Prepended for the same reason as {RefuseStrayArguments}: the command
|
|
39
|
+
# that forgets to look is the one that runs against the wrong tree.
|
|
40
|
+
module EnterProject
|
|
41
|
+
# @param options [Hash]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def call(**options)
|
|
44
|
+
enter_project(options) unless self.class.standalone?
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Marks a command that needs no project — reference output, the agent
|
|
50
|
+
# roster, completion — so it runs from anywhere without the search.
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
53
|
+
def self.standalone! = @standalone = true
|
|
54
|
+
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def self.standalone? = @standalone == true
|
|
57
|
+
|
|
58
|
+
# The name this command is typed as, and the key its section takes
|
|
59
|
+
# under `commands:` in `.openlanes.yml`: "run", "resync dirs", "list-plans".
|
|
60
|
+
#
|
|
61
|
+
# @return [String, nil] nil for a class with no name, as in a spec
|
|
62
|
+
def self.command_name
|
|
63
|
+
return nil unless name
|
|
64
|
+
|
|
65
|
+
name.delete_prefix("OpenLanes::CLI::").split("::")
|
|
66
|
+
.map { OpenLanes.inflector.underscore(it).tr("_", "-") }.join(" ")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# dry-cli merges what was typed over these, so a `.openlanes.yml` value
|
|
70
|
+
# placed here sits exactly between the flag and the built-in default.
|
|
71
|
+
# A file that cannot be read adds nothing at this point; {#enter_project}
|
|
72
|
+
# reads it again and refuses with the reason.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash{Symbol => Object}]
|
|
75
|
+
def self.default_params
|
|
76
|
+
return super if standalone? || command_name.nil?
|
|
77
|
+
|
|
78
|
+
root = Project.root or return super
|
|
79
|
+
known = options.map(&:name)
|
|
80
|
+
super.merge(OpenLanes::Config.load(root).for(command_name).slice(*known))
|
|
81
|
+
rescue OpenLanes::Error
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Every `commands:` section must name a command that exists, and every
|
|
86
|
+
# key in it an option that command takes, with a value it accepts. Each
|
|
87
|
+
# project command checks the whole file, not only its own section: a
|
|
88
|
+
# typo under a command you rarely run is still a typo.
|
|
89
|
+
#
|
|
90
|
+
# @param config [OpenLanes::Config]
|
|
91
|
+
# @return [void]
|
|
92
|
+
# @raise [OpenLanes::Error]
|
|
93
|
+
def self.check_config!(config)
|
|
94
|
+
config.commands.each do |name, settings|
|
|
95
|
+
command = OpenLanes::CLI.get(name.split).then { it.found? ? it.command : nil }
|
|
96
|
+
unless command.respond_to?(:command_name) && command.command_name == name
|
|
97
|
+
raise OpenLanes::Error, "#{config.path}: commands.#{name}: no such command"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
settings.each { |key, value| check_option!(config, command, name, key, value) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [void]
|
|
105
|
+
def self.check_option!(config, command, name, key, value)
|
|
106
|
+
where = "#{config.path}: commands.#{name}.#{key}"
|
|
107
|
+
option = command.options.find { it.name == key }
|
|
108
|
+
raise OpenLanes::Error, "#{where}: #{name} takes no such option" unless option
|
|
109
|
+
raise OpenLanes::Error, "#{where}: a preview is asked for each time, never set as a default" if key == :dry_run
|
|
110
|
+
|
|
111
|
+
if option.boolean? && ![true, false].include?(value)
|
|
112
|
+
raise OpenLanes::Error, "#{where}: #{value.inspect} is not true or false"
|
|
113
|
+
end
|
|
114
|
+
return unless option.values && !option.values.map(&:to_s).include?(value.to_s)
|
|
115
|
+
|
|
116
|
+
raise OpenLanes::Error, "#{where}: #{value.inspect} is not one of #{option.values.join(", ")}"
|
|
117
|
+
end
|
|
118
|
+
private_class_method :check_option!
|
|
119
|
+
|
|
120
|
+
def self.inherited(klass)
|
|
121
|
+
super
|
|
122
|
+
klass.prepend(EnterProject)
|
|
123
|
+
klass.prepend(RefuseStrayArguments)
|
|
124
|
+
klass.option :dir,
|
|
125
|
+
default: OpenLanes::PLANS_DIR,
|
|
126
|
+
aliases: ["-D"],
|
|
127
|
+
desc: "The .plans directory"
|
|
128
|
+
klass.option :quiet,
|
|
129
|
+
type: :boolean,
|
|
130
|
+
default: false,
|
|
131
|
+
aliases: ["-q"],
|
|
132
|
+
desc: "Suppress progress output on STDERR"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Declares `-n` / `--dry-run` on a command that changes something.
|
|
136
|
+
#
|
|
137
|
+
# @param desc [String] what the preview prints instead of doing
|
|
138
|
+
# @return [void]
|
|
139
|
+
def self.dry_run_option(desc)
|
|
140
|
+
option :dry_run,
|
|
141
|
+
type: :boolean,
|
|
142
|
+
default: false,
|
|
143
|
+
aliases: ["-n"],
|
|
144
|
+
desc:
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# @param options [Hash]
|
|
150
|
+
# @return [OpenLanes::Plans::Tree]
|
|
151
|
+
def tree_for(options)
|
|
152
|
+
tree = Plans.tree(options.fetch(:dir, OpenLanes::PLANS_DIR))
|
|
153
|
+
unless tree.exist?
|
|
154
|
+
refuse("No #{OpenLanes::PLANS_DIR} directory at\n#{tree.dir}\n\n" \
|
|
155
|
+
"Run `openlanes init` to create it, or pass -D.",
|
|
156
|
+
66)
|
|
157
|
+
end
|
|
158
|
+
tree
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The directory the command was typed in, before {EnterProject} moved
|
|
162
|
+
# to the root. A path the user typed (`--prompt notes.md`) means a file
|
|
163
|
+
# beside them, not one beside `.plans`.
|
|
164
|
+
#
|
|
165
|
+
# @return [String]
|
|
166
|
+
def origin = @origin || Dir.pwd
|
|
167
|
+
|
|
168
|
+
# @param path [String, nil]
|
|
169
|
+
# @return [String, nil] +path+ as the user meant it, from {#origin}
|
|
170
|
+
def typed_path(path) = path && File.expand_path(path, origin)
|
|
171
|
+
|
|
172
|
+
# Finds the root and moves there, saying so when that is somewhere
|
|
173
|
+
# else. `-D` and `PLANS_DIR` are relative, so they resolve against the
|
|
174
|
+
# root from here on.
|
|
175
|
+
#
|
|
176
|
+
# @param options [Hash]
|
|
177
|
+
# @return [void]
|
|
178
|
+
def enter_project(options)
|
|
179
|
+
@origin = Dir.pwd
|
|
180
|
+
root = Project.root(from: @origin)
|
|
181
|
+
unless root
|
|
182
|
+
refuse("No project here: nothing in #{@origin} or above it holds " \
|
|
183
|
+
"#{Project::FILE}, #{OpenLanes::PLANS_DIR}/ or .git.\n\n" \
|
|
184
|
+
"Run `openlanes init` to make this directory one.",
|
|
185
|
+
66)
|
|
186
|
+
end
|
|
187
|
+
check_config(root)
|
|
188
|
+
return if root == Project.expand(@origin)
|
|
189
|
+
|
|
190
|
+
Dir.chdir(root)
|
|
191
|
+
UI.line("Project root: #{root}") unless options.fetch(:quiet, false)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @param root [String]
|
|
195
|
+
# @return [void]
|
|
196
|
+
def check_config(root)
|
|
197
|
+
self.class.check_config!(OpenLanes::Config.load(root))
|
|
198
|
+
rescue OpenLanes::Error => e
|
|
199
|
+
refuse(e.message, 65)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Report and stop, in the shape every refusal here takes. The statuses
|
|
203
|
+
# follow sysexits where one fits: 64 usage, 65 bad data, 66 no input,
|
|
204
|
+
# 69 a service (GitHub, Linear) refused us.
|
|
205
|
+
#
|
|
206
|
+
# @param message [String]
|
|
207
|
+
# @param status [Integer]
|
|
208
|
+
# @return [void] never returns
|
|
209
|
+
def refuse(message, status)
|
|
210
|
+
error(message)
|
|
211
|
+
exit status
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @param options [Hash]
|
|
215
|
+
# @return [Boolean]
|
|
216
|
+
def quiet?(options)
|
|
217
|
+
quiet = options.fetch(:quiet, false)
|
|
218
|
+
UI.quiet = quiet
|
|
219
|
+
quiet
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# `-n` / `--dry-run`. A command does the work it is named for, so the
|
|
223
|
+
# preview is what has to be asked for, never the work itself.
|
|
224
|
+
#
|
|
225
|
+
# @param options [Hash]
|
|
226
|
+
# @return [Boolean]
|
|
227
|
+
def dry_run?(options) = options.fetch(:dry_run, false)
|
|
228
|
+
|
|
229
|
+
# `claude` prefers a credential in the environment to a claude.ai login,
|
|
230
|
+
# so a project `.env` that direnv loads on `cd` can redirect every agent
|
|
231
|
+
# a run spawns to a key meant for the application itself.
|
|
232
|
+
#
|
|
233
|
+
# Say it once, before the run starts, because the failure arrives three
|
|
234
|
+
# minutes later, once per agent, and reads like an agent problem rather
|
|
235
|
+
# than an environment one.
|
|
236
|
+
#
|
|
237
|
+
# @return [void]
|
|
238
|
+
def credentials_warning
|
|
239
|
+
names = Execution::Executor.foreign_credentials
|
|
240
|
+
return if names.empty?
|
|
241
|
+
|
|
242
|
+
one = names.size == 1
|
|
243
|
+
warn("#{names.join(" and ")} #{one ? "is" : "are"} set in this shell, so every agent authenticates " \
|
|
244
|
+
"with #{one ? "it" : "them"} rather than with your claude.ai login.\n" \
|
|
245
|
+
"A stale one fails every agent in the run with `401 API key is invalid`, " \
|
|
246
|
+
"three minutes at a time.\n" \
|
|
247
|
+
"If it came from a project .env, run `unset #{names.join(" ")}` first.")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The line every dry run ends with, so nobody mistakes a preview for the
|
|
251
|
+
# thing having happened.
|
|
252
|
+
#
|
|
253
|
+
# @param count [Integer] how much work is pending
|
|
254
|
+
# @param what [String]
|
|
255
|
+
# @return [void]
|
|
256
|
+
def dry_run_footer(count, what)
|
|
257
|
+
return if count.zero?
|
|
258
|
+
|
|
259
|
+
warn("#{count} #{what} pending — nothing was changed.\nRe-run without --dry-run to apply.")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module CLI
|
|
7
|
+
# `openlanes create <words…>` — backs /spec-create.
|
|
8
|
+
class Create < Base
|
|
9
|
+
desc "Create the next numbered plan folder"
|
|
10
|
+
|
|
11
|
+
argument :words,
|
|
12
|
+
type: :array,
|
|
13
|
+
required: false,
|
|
14
|
+
default: [],
|
|
15
|
+
desc:
|
|
16
|
+
"The topic, two to five words; becomes the folder slug. Omit when --from names it"
|
|
17
|
+
|
|
18
|
+
option :from,
|
|
19
|
+
aliases: ["-f"],
|
|
20
|
+
desc:
|
|
21
|
+
"Seed from a markdown file: its frontmatter `title:` names the folder, and its body fills the spec."
|
|
22
|
+
option :after,
|
|
23
|
+
aliases: ["-a"],
|
|
24
|
+
desc:
|
|
25
|
+
"Create a retroactive plan in the gap after this plan, e.g. 002"
|
|
26
|
+
option :status,
|
|
27
|
+
aliases: ["-s"],
|
|
28
|
+
desc: "Open in a state other than the default"
|
|
29
|
+
option :prs,
|
|
30
|
+
aliases: ["--pr"],
|
|
31
|
+
desc:
|
|
32
|
+
"Document work that already shipped: pull request numbers or URLs, comma separated. Requires --after"
|
|
33
|
+
option :spec,
|
|
34
|
+
type: :boolean,
|
|
35
|
+
default: true,
|
|
36
|
+
desc:
|
|
37
|
+
"With --prs, write spec.md from what the pull requests did. --no-spec records them and stops, which is fast and offline"
|
|
38
|
+
option :draft,
|
|
39
|
+
type: :boolean,
|
|
40
|
+
default: true,
|
|
41
|
+
desc:
|
|
42
|
+
"For a new feature (no --prs), attempt spec.md's four headings from project context via `claude`. --no-draft leaves them bare"
|
|
43
|
+
option :lane,
|
|
44
|
+
aliases: ["-l"],
|
|
45
|
+
values: Plans::Spec::LANES.map(&:to_s),
|
|
46
|
+
desc:
|
|
47
|
+
"Route the plan: full (research, spec, plan, build), plan (skip research and rewriting), " \
|
|
48
|
+
"quick (one agent does a short mechanical task). Written into spec.md's frontmatter"
|
|
49
|
+
option :depth,
|
|
50
|
+
values: Plans::Spec::DEPTHS.map(&:to_s),
|
|
51
|
+
desc: "How hard every phase should think: fast, medium or deep. Written into spec.md's frontmatter"
|
|
52
|
+
option :open,
|
|
53
|
+
type: :boolean,
|
|
54
|
+
default: true,
|
|
55
|
+
desc:
|
|
56
|
+
"Open the new spec.md in the system editor when done (macOS `open`). --no-open leaves it for you to open"
|
|
57
|
+
|
|
58
|
+
# noinspection RubyMismatchedArgumentType
|
|
59
|
+
example [
|
|
60
|
+
"tax rule dsl # 003.00-⚪️ → tax-rule-dsl, spec.md scaffolded and drafted",
|
|
61
|
+
"tax rule dsl --no-draft --no-open # scaffold only, nothing shelled out, nothing opened",
|
|
62
|
+
"--after 002 schedule k1 # 002.01-🕰️-schedule-k1 (documented after the fact)",
|
|
63
|
+
"--after 018 --prs 12,15 verify # …and write spec.md from what those PRs did",
|
|
64
|
+
"--after 018 --pr https://…/pull/12 verify # verify the pull request by number or URL",
|
|
65
|
+
"--status ready billing sync # opens at ⭐️ instead of ⚪️",
|
|
66
|
+
"--from notes/tax-dsl.md # title and opening prose from the file's frontmatter and body",
|
|
67
|
+
"--lane quick split big migration # r2d2-mechanic does it and opens the PR; no research, spec or plan"
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# @param words [Array<String>]
|
|
71
|
+
# @param options [Hash]
|
|
72
|
+
# @return [void]
|
|
73
|
+
def call(words:, **options)
|
|
74
|
+
dir = options.fetch(:dir, OpenLanes::PLANS_DIR)
|
|
75
|
+
FileUtils.mkdir_p(dir)
|
|
76
|
+
|
|
77
|
+
words, seed = resolve_seed(words, options)
|
|
78
|
+
prs = fetch_prs(options)
|
|
79
|
+
result =
|
|
80
|
+
Lifecycle.create(dir:,
|
|
81
|
+
words:,
|
|
82
|
+
after: options[:after],
|
|
83
|
+
status: options[:status],
|
|
84
|
+
prs:)
|
|
85
|
+
|
|
86
|
+
result.either(
|
|
87
|
+
->(path) { created(path, prs, options, seed:) },
|
|
88
|
+
->(message) { refuse("Could not create the plan:\n#{message}", 65) }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# `--from FILE` trades the words argument for a markdown file: the
|
|
95
|
+
# frontmatter's `title:` names the folder, and the body travels on to
|
|
96
|
+
# {Brief} as the author's own opening statement. Words and a file are
|
|
97
|
+
# two answers to one question — the folder's name — so holding both is
|
|
98
|
+
# refused rather than silently ranked.
|
|
99
|
+
#
|
|
100
|
+
# @param words [Array<String>]
|
|
101
|
+
# @param options [Hash]
|
|
102
|
+
# @return [Array(Array<String>, String, nil)] the slug words, and the
|
|
103
|
+
# seed body when a file supplied them
|
|
104
|
+
def resolve_seed(words, options)
|
|
105
|
+
file = typed_path(options[:from])
|
|
106
|
+
return words, nil unless file
|
|
107
|
+
|
|
108
|
+
unless words.empty?
|
|
109
|
+
refuse(
|
|
110
|
+
"--from names the topic by its frontmatter title;\n" \
|
|
111
|
+
"drop the words, or drop --from",
|
|
112
|
+
64
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
if options[:prs]
|
|
116
|
+
refuse(
|
|
117
|
+
"--prs reconstructs spec.md from the pull requests, so a seed file\n" \
|
|
118
|
+
"would be ignored; use one or the other",
|
|
119
|
+
64
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
unless File.file?(file)
|
|
123
|
+
refuse("Could not read the seed file:\n#{file} does not exist", 66)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
title, body = parse_seed(File.read(file, encoding: "UTF-8"))
|
|
127
|
+
if title.to_s.strip.empty?
|
|
128
|
+
refuse(
|
|
129
|
+
"The seed file needs a frontmatter title, e.g.\n" \
|
|
130
|
+
"---\ntitle: Tax Rule DSL\n---",
|
|
131
|
+
65
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[title.split, body]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Frontmatter that will not parse used to be reported as a missing
|
|
139
|
+
# title, which sends the author looking for a `title:` line that is
|
|
140
|
+
# sitting right there. Say which of the two actually went wrong.
|
|
141
|
+
#
|
|
142
|
+
# @param content [String]
|
|
143
|
+
# @return [Array(String, String)] title and body
|
|
144
|
+
def parse_seed(content)
|
|
145
|
+
meta, body = Frontmatter.split(content)
|
|
146
|
+
[meta["title"].to_s, body.strip]
|
|
147
|
+
rescue Psych::Exception => e
|
|
148
|
+
refuse(
|
|
149
|
+
"The seed file's frontmatter is not valid YAML:\n#{e.message}",
|
|
150
|
+
65
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A retroactive plan documents work that landed *somewhere* in the
|
|
155
|
+
# sequence, and only its author knows where. Guessing would put the
|
|
156
|
+
# number — the one thing that never changes — in the wrong place.
|
|
157
|
+
#
|
|
158
|
+
# @param options [Hash]
|
|
159
|
+
# @return [Array<Hash>, nil]
|
|
160
|
+
def fetch_prs(options)
|
|
161
|
+
return nil unless options[:prs]
|
|
162
|
+
|
|
163
|
+
unless options[:after]
|
|
164
|
+
refuse(
|
|
165
|
+
"--prs documents work that already shipped;\n" \
|
|
166
|
+
"name the plan it landed after with --after, e.g. --after 018",
|
|
167
|
+
64
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
Vcs.github.pull_requests(Vcs::GitHub.parse_refs(options[:prs]))
|
|
172
|
+
rescue OpenLanes::Error => e
|
|
173
|
+
refuse("Could not read the pull requests:\n#{e.message}", 65)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# A plan with recorded pull requests already has its facts — hand it to
|
|
177
|
+
# the writer that reconstructs a specification from a diff. One without
|
|
178
|
+
# them does not exist yet, and reconstructing is not the job; {#brief} is.
|
|
179
|
+
#
|
|
180
|
+
# @param path [String]
|
|
181
|
+
# @param prs [Array<Hash>, nil]
|
|
182
|
+
# @param options [Hash]
|
|
183
|
+
# @return [void]
|
|
184
|
+
def created(path, prs, options, seed: nil)
|
|
185
|
+
from_prs = prs && !prs.empty?
|
|
186
|
+
path =
|
|
187
|
+
if from_prs
|
|
188
|
+
synthesize(path, options) if options.fetch(:spec, true)
|
|
189
|
+
else
|
|
190
|
+
brief(path, options, seed:)
|
|
191
|
+
end || path
|
|
192
|
+
puts path
|
|
193
|
+
return if quiet?(options)
|
|
194
|
+
|
|
195
|
+
feature = Plans::Feature.parse(path)
|
|
196
|
+
success(
|
|
197
|
+
"Created #{File.basename(path)}\n\n" \
|
|
198
|
+
"#{feature.status.emoji} #{feature.status.label} — #{feature.status.note}\n" \
|
|
199
|
+
"#{next_step(path, feature, from_prs:)}"
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Hand the folder to the writer that already knows how to write a
|
|
204
|
+
# retroactive specification, then let `resync dirs` decide what the
|
|
205
|
+
# folder has become — 🕰️ is only true while there is no `spec.md`.
|
|
206
|
+
#
|
|
207
|
+
# @param path [String]
|
|
208
|
+
# @param options [Hash]
|
|
209
|
+
# @return [String] the folder's path, which the resync may have renamed
|
|
210
|
+
def synthesize(path, options)
|
|
211
|
+
agent = OpenLanes::Agents.registry.find(OpenLanes::RETROACTIVE_WRITER) or
|
|
212
|
+
return path
|
|
213
|
+
root = options[:root] || File.dirname(path, 2)
|
|
214
|
+
|
|
215
|
+
feature = Plans::Feature.parse(path)
|
|
216
|
+
ok, note = on_agent_row("Writing spec.md from #{UI.paint(File.basename(path).to_s, :yellow)}",
|
|
217
|
+
agent: agent.name,
|
|
218
|
+
plan: feature.ordinal.to_s) do |line|
|
|
219
|
+
Execution.executor(root:).call(agent, Plans.subject(feature), &line)
|
|
220
|
+
end
|
|
221
|
+
warn_about(note) unless ok
|
|
222
|
+
|
|
223
|
+
settle(path)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# A folder with no pull requests to reconstruct from is a feature that
|
|
227
|
+
# does not exist yet. {Brief} writes the four headings a human still has
|
|
228
|
+
# to answer, makes a best-effort pass at them from what the project
|
|
229
|
+
# already has on disk, and — unless told not to — opens the result for
|
|
230
|
+
# a human to finish. The folder's state never moves: ⚪️ New only ever
|
|
231
|
+
# claimed that a specification exists, not that it is complete.
|
|
232
|
+
#
|
|
233
|
+
# @param path [String] the path to the folder
|
|
234
|
+
# @param seed [String, nil] the seed body from the `--from` file
|
|
235
|
+
# @param options [Hash] the options hash
|
|
236
|
+
# @return [String] +path+, unchanged
|
|
237
|
+
def brief(path, options, seed: nil)
|
|
238
|
+
feature = Plans::Feature.parse(path)
|
|
239
|
+
return path if feature.status.key == :retroactive
|
|
240
|
+
|
|
241
|
+
root = options[:root] || File.dirname(path, 2)
|
|
242
|
+
brief = Lifecycle.brief(path:, title: feature.title, root:, seed:)
|
|
243
|
+
brief.write_scaffold!
|
|
244
|
+
|
|
245
|
+
if options.fetch(:draft, true)
|
|
246
|
+
# Painted the way Runner::Task#label paints a roster agent, so the
|
|
247
|
+
# half-agent reads as one of them on the terminal.
|
|
248
|
+
label =
|
|
249
|
+
"#{UI.paint(Lifecycle::Brief::AGENT_NAME, :yellow, :bold)} drafting spec.md from #{seed}"
|
|
250
|
+
ok, note = on_agent_row(label, agent: Lifecycle::Brief::AGENT_NAME, plan: feature.ordinal.to_s) { brief.attempt! }
|
|
251
|
+
warn_about_draft(note) unless ok
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
stamp(brief.spec_path, options)
|
|
255
|
+
open_spec(brief.spec_path) if options.fetch(:open, true)
|
|
256
|
+
path
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# One agent writing spec.md, drawn as a spinner line.
|
|
260
|
+
#
|
|
261
|
+
# @param label [String] the spinner line's text
|
|
262
|
+
# @param agent [String]
|
|
263
|
+
# @param plan [String]
|
|
264
|
+
# @yieldparam line [OpenLanes::UI::Line]
|
|
265
|
+
# @return [Object] the block's value
|
|
266
|
+
def on_agent_row(label, agent:, plan:, &block)
|
|
267
|
+
Execution.on_interrupt do
|
|
268
|
+
UI.concurrently([plan],
|
|
269
|
+
label,
|
|
270
|
+
jobs: 1,
|
|
271
|
+
label: ->(_) { label },
|
|
272
|
+
fields: ->(_) { { plan:, agent: } }) { |_, line| block.call(line) }.first
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Writes `--lane` and `--depth` into spec.md's frontmatter, after the
|
|
277
|
+
# draft, so a drafting agent that rewrites the file cannot drop them.
|
|
278
|
+
#
|
|
279
|
+
# @param spec_path [String]
|
|
280
|
+
# @param options [Hash]
|
|
281
|
+
# @return [void]
|
|
282
|
+
def stamp(spec_path, options)
|
|
283
|
+
settings = { "lane" => options[:lane], "depth" => options[:depth] }.compact
|
|
284
|
+
return if settings.empty? || !File.file?(spec_path)
|
|
285
|
+
|
|
286
|
+
meta, body = Frontmatter.split(File.read(spec_path, encoding: "UTF-8"))
|
|
287
|
+
File.write(spec_path, "#{YAML.dump(meta.merge(settings))}---\n#{body}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @param spec_path [String]
|
|
291
|
+
# @return [void]
|
|
292
|
+
def open_spec(spec_path)
|
|
293
|
+
return unless macos?
|
|
294
|
+
|
|
295
|
+
system("open", spec_path, out: File::NULL, err: File::NULL)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# `open` is a macOS command; anywhere else the viewer step is skipped
|
|
299
|
+
# rather than failed. A named seam so a spec can assert the viewer was
|
|
300
|
+
# asked for without inheriting the platform CI happens to run on.
|
|
301
|
+
#
|
|
302
|
+
# @return [Boolean]
|
|
303
|
+
def macos? = RbConfig::CONFIG["host_os"].to_s.match?(/darwin/)
|
|
304
|
+
|
|
305
|
+
# @param path [String]
|
|
306
|
+
# @return [String] where the folder ended up
|
|
307
|
+
def settle(path)
|
|
308
|
+
tree = Plans.tree(File.dirname(path))
|
|
309
|
+
change =
|
|
310
|
+
OpenLanes::Lifecycle::Resync::Dirs
|
|
311
|
+
.new(tree:)
|
|
312
|
+
.call(commit: true)
|
|
313
|
+
.find { |c| c.source == path }
|
|
314
|
+
change ? change.target : path
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @param note [String]
|
|
318
|
+
# @return [void]
|
|
319
|
+
def warn_about(note)
|
|
320
|
+
error(
|
|
321
|
+
"The folder was created, but spec.md was not written:\n#{note}\n\n" \
|
|
322
|
+
"The pull requests are recorded. Run `openlanes run` to retry."
|
|
323
|
+
)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# @param note [String]
|
|
327
|
+
# @return [void]
|
|
328
|
+
def warn_about_draft(note)
|
|
329
|
+
error(
|
|
330
|
+
"spec.md was scaffolded, but the drafting attempt did not finish:\n#{note}\n\n" \
|
|
331
|
+
"The four headings are there, empty. Fill them in by hand, or hand off to leah-researcher."
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# @param path [String]
|
|
336
|
+
# @param feature [OpenLanes::Plans::Feature]
|
|
337
|
+
# @return [String]
|
|
338
|
+
def next_step(path, feature, from_prs:)
|
|
339
|
+
spec = File.join(File.basename(path), "spec.md")
|
|
340
|
+
unless feature.status.key == :new &&
|
|
341
|
+
File.file?(File.join(path, "spec.md"))
|
|
342
|
+
return "Next: write #{spec}"
|
|
343
|
+
end
|
|
344
|
+
if from_prs
|
|
345
|
+
return (
|
|
346
|
+
"Next: read #{spec} — it was written from the pull requests, so check it against what actually shipped"
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
"Next: fill in the four headings in #{spec}, then hand off to leah-researcher"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module CLI
|
|
7
|
+
# `openlanes docs` — regenerate the conventions document.
|
|
8
|
+
class Docs < Base
|
|
9
|
+
standalone!
|
|
10
|
+
desc "Generate the conventions document from the state machine itself"
|
|
11
|
+
|
|
12
|
+
# STDOUT unless a file is named. The default used to be a file in the
|
|
13
|
+
# user's home, so an agent running `openlanes docs` to *read* the
|
|
14
|
+
# conventions overwrote ~/.agents/context/workflow.md, untracked and
|
|
15
|
+
# unrecoverable, while the help promised STDOUT.
|
|
16
|
+
option :output,
|
|
17
|
+
aliases: ["-o"],
|
|
18
|
+
desc: "Write here instead of STDOUT; - for STDOUT"
|
|
19
|
+
|
|
20
|
+
example [" # print it", "-o ~/.agents/context/workflow.md # write it"]
|
|
21
|
+
|
|
22
|
+
# @param options [Hash]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def call(**options)
|
|
25
|
+
document = Presentation.documentation
|
|
26
|
+
|
|
27
|
+
path = options[:output]
|
|
28
|
+
if path && path != "-"
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
30
|
+
File.write(path, document)
|
|
31
|
+
# Argument form, so a path with a space or a quote reaches mdformat whole.
|
|
32
|
+
system("mdformat", "--wrap", "no", path, err: File::NULL, out: File::NULL)
|
|
33
|
+
success("created workflow description in [#{path}]") unless quiet?(options)
|
|
34
|
+
else
|
|
35
|
+
$stdout.write(document)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|