openlanes 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +4 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +18 -4
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +14 -11
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +7 -0
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -486
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -51
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +84 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1175 -117
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +6 -6
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +12 -120
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +223 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +17 -13
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +60 -11
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +217 -30
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +38 -4
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +56 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +12 -1
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +31 -3
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +4 -4
- data/lib/openlanes/live/store.rb +18 -1
- data/lib/openlanes/live/subscriber.rb +22 -2
- data/lib/openlanes/live.rb +66 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +64 -5
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +39 -24
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +226 -8
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +167 -6
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +115 -16
- data/lib/openlanes/web/context.rb +15 -9
- data/lib/openlanes/web/demo.rb +29 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +86 -7
- data/lib/openlanes/web/launcher.rb +69 -7
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +29 -10
- data/lib/openlanes/web.rb +6 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/{index-BFPI-zXI.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-bpsFIpXE.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-DtA46i32.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-CnJfB0l2.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-DXgeajd6.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-DH0gDlee.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-DaSjQ-H4.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-Dbxvk-EC.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-6MZkmh3k.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-B5CKJPm-.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-CnvkZu5Y.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-IqD8mBos.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-C3zc4gFe.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-C2jwoWVV.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-pvJi-JJO.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-BF6QYroq.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-DbueqOK5.js → index-_mptZjlC.js} +2 -2
- data/web/dist/index.html +2 -2
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -889
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/index-BYJCWlk0.js +0 -198
- data/web/dist/assets/index-CjInBi5X.css +0 -1
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes app` — the app a plan builds, run on this machine for the
|
|
6
|
+
# control room's Preview tab (plan 012). See {OpenLanes::Apps}.
|
|
7
|
+
module App
|
|
8
|
+
# Branch names carry the plan: `<user>/NNN.MM-slug` (see {Vcs::Worktree}).
|
|
9
|
+
BRANCH_PLAN = %r{\A[^/]+/(\d{3}\.\d{2})-}
|
|
10
|
+
|
|
11
|
+
# The ordinal a run without one is filed under.
|
|
12
|
+
NO_PLAN = "000.00"
|
|
13
|
+
|
|
14
|
+
# What every subcommand needs from the registry.
|
|
15
|
+
module Registry
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# @return [OpenLanes::Apps::Ports]
|
|
19
|
+
def registry = OpenLanes::Apps.registry
|
|
20
|
+
|
|
21
|
+
# @param plan [String, nil] as typed
|
|
22
|
+
# @return [String, nil] canonical, nil when nothing was typed
|
|
23
|
+
def ordinal_of(plan)
|
|
24
|
+
return nil if plan.nil?
|
|
25
|
+
|
|
26
|
+
Plans::Ordinal.parse(plan.to_s)&.to_s || refuse("#{plan} is not a plan number like 012 or 012.00", 64)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/atomic/atomic_boolean"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module CLI
|
|
8
|
+
module App
|
|
9
|
+
# `openlanes app start` — run a plan's app in the foreground, as
|
|
10
|
+
# `.openlanes.yml` `app:` declares it, until Ctrl-C, `openlanes app
|
|
11
|
+
# stop`, or Stop in the Preview tab.
|
|
12
|
+
class Start < Base
|
|
13
|
+
include Registry
|
|
14
|
+
|
|
15
|
+
desc "Run a plan's app from .openlanes.yml app:, for the Preview tab"
|
|
16
|
+
|
|
17
|
+
argument :plan, required: false, desc: "Which plan: NNN or NNN.MM; its worktree when it has one"
|
|
18
|
+
|
|
19
|
+
option :redis_url, desc: "Redis the control room reads (default: .openlanes.yml redis_url, else $REDIS_URL, else #{Live::DEFAULT_URL})"
|
|
20
|
+
option :demo,
|
|
21
|
+
type: :boolean,
|
|
22
|
+
default: false,
|
|
23
|
+
desc: "Publish where `openlanes web --demo` reads, beside its sample board"
|
|
24
|
+
option :ready_timeout, default: Apps::Supervisor::READY_TIMEOUT.to_s, desc: "Seconds each process has to become ready"
|
|
25
|
+
option :control_room, default: Apps::Framing::ORIGIN, desc: "The control room's origin, which the app must allow to frame it"
|
|
26
|
+
|
|
27
|
+
example [
|
|
28
|
+
" # this checkout's app, filed under the plan its branch names",
|
|
29
|
+
"012 # plan 012's app, in its worktree when it has one",
|
|
30
|
+
"--demo --no-quiet # beside `openlanes web --demo`"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# @param plan [String, nil]
|
|
34
|
+
# @param options [Hash]
|
|
35
|
+
# @return [void]
|
|
36
|
+
def call(plan: nil, **options)
|
|
37
|
+
quiet?(options)
|
|
38
|
+
root = checkout(ordinal_of(plan), options)
|
|
39
|
+
definition = definition_in(root)
|
|
40
|
+
branch = branch_of(root)
|
|
41
|
+
ordinal = ordinal_of(plan) || branch[BRANCH_PLAN, 1] || NO_PLAN
|
|
42
|
+
store, keys = board(options)
|
|
43
|
+
runner = Apps.runner(definition:,
|
|
44
|
+
root:,
|
|
45
|
+
plan: ordinal,
|
|
46
|
+
branch:,
|
|
47
|
+
store:,
|
|
48
|
+
keys:,
|
|
49
|
+
say: ->(line) { UI.line("#{ordinal} #{line}") },
|
|
50
|
+
supervisor: { ready_timeout: Float(options[:ready_timeout]), origin: options[:control_room],
|
|
51
|
+
output: ->(name, line) { UI.line("#{name}: #{line}", bullet: "│") } })
|
|
52
|
+
# A trap may not take a lock, and the flag takes one.
|
|
53
|
+
%w[INT TERM].each { |name| Signal.trap(name) { Thread.new { runner.stopping.make_true } } }
|
|
54
|
+
UI.line("Starting #{ordinal}'s app in #{root} (#{branch}); Ctrl-C stops it")
|
|
55
|
+
runner.call
|
|
56
|
+
rescue Apps::Supervisor::StartFailed => e
|
|
57
|
+
refuse("#{ordinal}'s app did not start: #{e.message}", 70)
|
|
58
|
+
ensure
|
|
59
|
+
store&.close
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# The plan's worktree when it has one, else the project root.
|
|
65
|
+
#
|
|
66
|
+
# @return [String]
|
|
67
|
+
def checkout(ordinal, options)
|
|
68
|
+
return Dir.pwd unless ordinal
|
|
69
|
+
|
|
70
|
+
subject = tree_for(options).find(Plans::Ordinal.parse(ordinal))
|
|
71
|
+
refuse("No plan #{ordinal} in #{OpenLanes::PLANS_DIR}", 66) unless subject
|
|
72
|
+
path = Vcs.worktree(root: Dir.pwd).path_for(subject.feature)
|
|
73
|
+
File.directory?(path) ? path : Dir.pwd
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The checkout's own `.openlanes.yml`: on a plan's branch, luke may
|
|
77
|
+
# have written `app:` there and nowhere else yet (M12).
|
|
78
|
+
#
|
|
79
|
+
# @return [OpenLanes::Apps::Definition]
|
|
80
|
+
def definition_in(root)
|
|
81
|
+
definition = Apps.definition(OpenLanes::Config.load(root))
|
|
82
|
+
return definition if definition
|
|
83
|
+
|
|
84
|
+
refuse("#{File.join(root, Project::FILE)} declares no app:, so there is nothing to start.", 65)
|
|
85
|
+
rescue OpenLanes::Error => e
|
|
86
|
+
refuse(e.message, 65)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `symbolic-ref` rather than `rev-parse`, so a branch with no commit
|
|
90
|
+
# yet still has its name.
|
|
91
|
+
#
|
|
92
|
+
# @return [String]
|
|
93
|
+
def branch_of(root)
|
|
94
|
+
out, status = Open3.capture2("git", "-C", root, "symbolic-ref", "--short", "HEAD", err: File::NULL)
|
|
95
|
+
status.success? ? out.strip : "(no branch)"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The store and keys the control room reads, or neither when Redis
|
|
99
|
+
# is not there: the app runs either way.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array(OpenLanes::Live::Store, OpenLanes::Live::Keys), Array(nil, nil)]
|
|
102
|
+
def board(options)
|
|
103
|
+
url = Live.redis_url(option: options[:redis_url], config: OpenLanes::Config.current)
|
|
104
|
+
store = Live.connect(url)
|
|
105
|
+
keys = Live.keys(root: Dir.pwd)
|
|
106
|
+
[store, options[:demo] ? keys.with(repo: "demo/#{keys.repo}") : keys]
|
|
107
|
+
rescue Live::Unavailable => e
|
|
108
|
+
UI.line("#{e.message}: the app runs, but the Preview tab will not hear of it")
|
|
109
|
+
[nil, nil]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module App
|
|
6
|
+
# `openlanes app status` — every port an app holds on this machine,
|
|
7
|
+
# read from the registry, so it answers with or without Redis.
|
|
8
|
+
class Status < Base
|
|
9
|
+
include Registry
|
|
10
|
+
|
|
11
|
+
desc "List the apps running on this machine: plan, process, port, pid, checkout"
|
|
12
|
+
|
|
13
|
+
# @param options [Hash]
|
|
14
|
+
# @return [void]
|
|
15
|
+
def call(**options)
|
|
16
|
+
quiet?(options)
|
|
17
|
+
entries = registry.entries
|
|
18
|
+
if entries.empty?
|
|
19
|
+
UI.line("No app is running on this machine.") unless quiet?(options)
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
rows = entries.map { |port, entry| [entry["plan"], entry["process"], port.to_s, entry["owner"].to_s, entry["root"]] }
|
|
24
|
+
puts table([%w[PLAN PROCESS PORT PID CHECKOUT], *rows.sort])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# @param rows [Array<Array<String>>]
|
|
30
|
+
# @return [String]
|
|
31
|
+
def table(rows)
|
|
32
|
+
widths = rows.transpose.map { |column| column.map { it.to_s.length }.max }
|
|
33
|
+
rows.map { |row| row.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" ").rstrip }.join("\n")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module App
|
|
6
|
+
# `openlanes app stop` — TERM to the `openlanes app start` holding a
|
|
7
|
+
# plan's ports, which stops its processes and frees them.
|
|
8
|
+
class Stop < Base
|
|
9
|
+
include Registry
|
|
10
|
+
|
|
11
|
+
desc "Stop a plan's app (every app on this machine when no plan is given)"
|
|
12
|
+
|
|
13
|
+
argument :plan, required: false, desc: "Which plan: NNN or NNN.MM"
|
|
14
|
+
|
|
15
|
+
# @param plan [String, nil]
|
|
16
|
+
# @param options [Hash]
|
|
17
|
+
# @return [void]
|
|
18
|
+
def call(plan: nil, **options)
|
|
19
|
+
quiet?(options)
|
|
20
|
+
ordinal = ordinal_of(plan)
|
|
21
|
+
owners = registry.entries.values.select { ordinal.nil? || it["plan"] == ordinal }.map { it["owner"] }.uniq
|
|
22
|
+
if owners.empty?
|
|
23
|
+
UI.line("No app is running#{" for #{ordinal}" if ordinal} on this machine.") unless quiet?(options)
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
owners.each { signal(it) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# @return [void]
|
|
33
|
+
def signal(pid)
|
|
34
|
+
Process.kill("TERM", pid)
|
|
35
|
+
UI.line("Asked #{pid} to stop")
|
|
36
|
+
rescue Errno::ESRCH
|
|
37
|
+
UI.line("#{pid} had already stopped")
|
|
38
|
+
rescue Errno::EPERM
|
|
39
|
+
refuse("#{pid} is not yours to stop", 77)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/openlanes/cli/base.rb
CHANGED
|
@@ -144,8 +144,85 @@ module OpenLanes
|
|
|
144
144
|
desc:
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
# `-l`, `-L` and `--log-format`, for the commands that run something
|
|
148
|
+
# long enough to be worth a log. No defaults here: a flag left out must
|
|
149
|
+
# stay nil, so {Log::Settings.from} can fall back to what the engine
|
|
150
|
+
# handed its server through the environment.
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
def self.log_options
|
|
154
|
+
option :log,
|
|
155
|
+
aliases: ["-l"],
|
|
156
|
+
desc: "Write the log to FILE, appending; - for STDOUT (default: #{Log::DEFAULT_FILE} under the project root)"
|
|
157
|
+
option :log_level,
|
|
158
|
+
aliases: ["-L"],
|
|
159
|
+
values: Log::LEVELS,
|
|
160
|
+
desc: "The lowest level written (default: #{Log::DEFAULT_LEVEL})"
|
|
161
|
+
option :log_format,
|
|
162
|
+
values: Log::FORMATS.keys,
|
|
163
|
+
desc: "color or standard (one line per entry, with or without colour) or json (default: #{Log::DEFAULT_FORMAT})"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# `--ips` (alias `--bind`), `-I` and `--admin-credentials`, for the
|
|
167
|
+
# commands that serve the control room: `web`, and `run` and `start`
|
|
168
|
+
# through the server they launch.
|
|
169
|
+
#
|
|
170
|
+
# @return [void]
|
|
171
|
+
def self.listen_options
|
|
172
|
+
option :ips,
|
|
173
|
+
aliases: ["-i", "--bind"],
|
|
174
|
+
desc: "Comma-separated addresses the control room listens on, e.g. 127.0.0.1,192.168.1.20 " \
|
|
175
|
+
"(default: #{OpenLanes::Web::HOST}). Any address past loopback needs --admin-credentials"
|
|
176
|
+
option :all_interfaces,
|
|
177
|
+
aliases: ["-I"],
|
|
178
|
+
type: :boolean,
|
|
179
|
+
default: false,
|
|
180
|
+
desc: "Listen on every interface, the same as --ips #{OpenLanes::Web::Listen::EVERYWHERE}"
|
|
181
|
+
option :admin_credentials,
|
|
182
|
+
desc: "USER:PASSWORD, the Basic auth login asked of every request from off this machine; required " \
|
|
183
|
+
"when listening past loopback (default: $#{OpenLanes::Web::AdminAuth::ENV_NAME})"
|
|
184
|
+
end
|
|
185
|
+
|
|
147
186
|
private
|
|
148
187
|
|
|
188
|
+
# `--ips`, or `-I` for every interface. Both at once is refused rather
|
|
189
|
+
# than one quietly winning, since they disagree about who can reach it.
|
|
190
|
+
#
|
|
191
|
+
# @param options [Hash]
|
|
192
|
+
# @return [OpenLanes::Web::Listen]
|
|
193
|
+
def listen_for(options)
|
|
194
|
+
if options[:all_interfaces]
|
|
195
|
+
refuse("Pass --ips or -I, not both.", 64) unless options[:ips].to_s.strip.empty?
|
|
196
|
+
return OpenLanes::Web::Listen.parse(OpenLanes::Web::Listen::EVERYWHERE)
|
|
197
|
+
end
|
|
198
|
+
OpenLanes::Web::Listen.parse(options[:ips])
|
|
199
|
+
rescue OpenLanes::Web::Listen::Invalid => e
|
|
200
|
+
refuse("--ips: #{e.message}", 64)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `--admin-credentials`, else the ones `openlanes run` hands its server
|
|
204
|
+
# through the environment. Listening past loopback without them is
|
|
205
|
+
# refused: the page can stop agents and answer for a person, and a
|
|
206
|
+
# server anyone on the network can drive is never a default.
|
|
207
|
+
#
|
|
208
|
+
# @param options [Hash]
|
|
209
|
+
# @param listen [OpenLanes::Web::Listen]
|
|
210
|
+
# @param env [Hash]
|
|
211
|
+
# @return [OpenLanes::Web::AdminAuth::Credentials, nil]
|
|
212
|
+
def credentials_for(options, listen, env: ENV)
|
|
213
|
+
text = [options[:admin_credentials], env[OpenLanes::Web::AdminAuth::ENV_NAME]].find { !it.to_s.empty? }
|
|
214
|
+
if text.nil?
|
|
215
|
+
return unless listen.public?
|
|
216
|
+
|
|
217
|
+
refuse("Listening on #{listen.public_ips.join(", ")} needs a login: pass --admin-credentials USER:PASSWORD, " \
|
|
218
|
+
"or set $#{OpenLanes::Web::AdminAuth::ENV_NAME}.",
|
|
219
|
+
64)
|
|
220
|
+
end
|
|
221
|
+
OpenLanes::Web::AdminAuth.parse(text)
|
|
222
|
+
rescue OpenLanes::Web::AdminAuth::Invalid => e
|
|
223
|
+
refuse("--admin-credentials #{e.message}.", 64)
|
|
224
|
+
end
|
|
225
|
+
|
|
149
226
|
# @param options [Hash]
|
|
150
227
|
# @return [OpenLanes::Plans::Tree]
|
|
151
228
|
def tree_for(options)
|
|
@@ -226,27 +303,23 @@ module OpenLanes
|
|
|
226
303
|
# @return [Boolean]
|
|
227
304
|
def dry_run?(options) = options.fetch(:dry_run, false)
|
|
228
305
|
|
|
229
|
-
#
|
|
230
|
-
#
|
|
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.
|
|
306
|
+
# The credential warning {Execution.credentials_notice} words, drawn
|
|
307
|
+
# when there is one to give.
|
|
236
308
|
#
|
|
237
309
|
# @return [void]
|
|
238
310
|
def credentials_warning
|
|
239
|
-
|
|
240
|
-
|
|
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.")
|
|
311
|
+
notice = Execution.credentials_notice
|
|
312
|
+
warn(notice) if notice
|
|
248
313
|
end
|
|
249
314
|
|
|
315
|
+
# The success value of a workflow's result, or the refusal it failed
|
|
316
|
+
# with, reported and exited on. One shape for every workflow, so a
|
|
317
|
+
# command reads the same whichever one it calls.
|
|
318
|
+
#
|
|
319
|
+
# @param result [Dry::Monads::Result] Success(value) or Failure({Refusal})
|
|
320
|
+
# @return [Object] the success value; a failure never returns
|
|
321
|
+
def unwrap(result) = result.value_or { |refusal| refuse(refusal.message, refusal.status) }
|
|
322
|
+
|
|
250
323
|
# The line every dry run ends with, so nobody mistakes a preview for the
|
|
251
324
|
# thing having happened.
|
|
252
325
|
#
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
# `openlanes coverage PLAN` — the coverage gate (M15), by hand.
|
|
6
|
+
#
|
|
7
|
+
# Runs the declared test command in the plan's worktree, never in the main
|
|
8
|
+
# tree, and prints the verdict `ready-to-merge` will wait on. The verdict is
|
|
9
|
+
# the deliverable and goes to STDOUT; the command's own output, when the
|
|
10
|
+
# gate withholds, goes to STDERR. Exits 1 when the gate withholds, so a
|
|
11
|
+
# script or CI step can stop on it.
|
|
12
|
+
#
|
|
13
|
+
# `CLI::Coverage` shadows the library's namespace, so this file names
|
|
14
|
+
# `OpenLanes::Coverage` in full.
|
|
15
|
+
class Coverage < Base
|
|
16
|
+
desc "Run a plan's declared test command in its worktree and check line coverage"
|
|
17
|
+
|
|
18
|
+
argument :plan, required: true, desc: "Plan ordinal: NNN or NNN.MM"
|
|
19
|
+
|
|
20
|
+
dry_run_option "Print the declaration and where the command would run; run nothing"
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
"013 # run .openlanes.yml's coverage.command in plan 013's worktree",
|
|
24
|
+
"013 --dry-run # show what would run, and where"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# @param plan [String]
|
|
28
|
+
# @param options [Hash]
|
|
29
|
+
# @return [void]
|
|
30
|
+
def call(plan:, **options)
|
|
31
|
+
tree = tree_for(options)
|
|
32
|
+
ordinal = Plans::Ordinal.parse(plan.to_s)
|
|
33
|
+
subject = ordinal && tree.find(ordinal)
|
|
34
|
+
refuse("No plan #{plan} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66) unless subject
|
|
35
|
+
|
|
36
|
+
worktree = OpenLanes::Vcs.worktree(root: File.dirname(tree.dir))
|
|
37
|
+
refuse("#{worktree.root} is not a git repository; the gate runs in a plan's worktree.", 66) unless worktree.repository?
|
|
38
|
+
|
|
39
|
+
return preview(worktree, subject.feature) if dry_run?(options)
|
|
40
|
+
|
|
41
|
+
verdict = OpenLanes::Engine.coverage_gate.call(checkout: worktree.checkout_for(subject.feature))
|
|
42
|
+
$stdout.puts(verdict.to_s)
|
|
43
|
+
return if verdict.passed?
|
|
44
|
+
|
|
45
|
+
warn(verdict.output.join("\n")) unless verdict.output.empty?
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [void]
|
|
52
|
+
def preview(worktree, feature)
|
|
53
|
+
path = worktree.path_for(feature)
|
|
54
|
+
config = File.directory?(path) && OpenLanes::Coverage.config(path)
|
|
55
|
+
$stdout.puts("would run in #{path}")
|
|
56
|
+
if config
|
|
57
|
+
$stdout.puts(" #{config.command}", " then read #{config.source}, minimum #{format("%g%%", config.minimum)}")
|
|
58
|
+
else
|
|
59
|
+
$stdout.puts(" the declaration is read from the #{OpenLanes::Coverage::Config::SECTION}: section of " \
|
|
60
|
+
"#{OpenLanes::Config::FILE} there, once the worktree exists and has one")
|
|
61
|
+
end
|
|
62
|
+
dry_run_footer(1, "coverage run")
|
|
63
|
+
rescue OpenLanes::Coverage::Invalid => e
|
|
64
|
+
refuse(e.message, 65)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|