openlanes 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +62 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +24 -3
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +32 -10
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +26 -2
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -454
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -64
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +94 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1229 -116
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +38 -105
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +236 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +39 -11
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +665 -0
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +221 -31
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +290 -0
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +114 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +36 -1
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +38 -4
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +12 -8
- data/lib/openlanes/live/store.rb +36 -1
- data/lib/openlanes/live/subscriber.rb +93 -0
- data/lib/openlanes/live.rb +71 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +66 -7
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +110 -21
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +287 -7
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +210 -14
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +213 -15
- data/lib/openlanes/web/context.rb +33 -9
- data/lib/openlanes/web/demo.rb +39 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +137 -9
- data/lib/openlanes/web/launcher.rb +70 -8
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +31 -10
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +19 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -881
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-CI74IkWP.js +0 -198
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
3
|
module OpenLanes
|
|
6
4
|
module CLI
|
|
7
5
|
# `openlanes create <words…>` — backs /spec-create.
|
|
@@ -57,12 +55,12 @@ module OpenLanes
|
|
|
57
55
|
|
|
58
56
|
# noinspection RubyMismatchedArgumentType
|
|
59
57
|
example [
|
|
60
|
-
"tax rule dsl # 003.00
|
|
58
|
+
"tax rule dsl # 003.00━━⚪━━❯tax-rule-dsl, spec.md scaffolded and drafted",
|
|
61
59
|
"tax rule dsl --no-draft --no-open # scaffold only, nothing shelled out, nothing opened",
|
|
62
|
-
"--after 002 schedule k1 # 002.01
|
|
60
|
+
"--after 002 schedule k1 # 002.01━━⏪━━❯schedule-k1 (documented after the fact)",
|
|
63
61
|
"--after 018 --prs 12,15 verify # …and write spec.md from what those PRs did",
|
|
64
62
|
"--after 018 --pr https://…/pull/12 verify # verify the pull request by number or URL",
|
|
65
|
-
"--status ready billing sync # opens at
|
|
63
|
+
"--status ready billing sync # opens at ⭐ instead of ⚪",
|
|
66
64
|
"--from notes/tax-dsl.md # title and opening prose from the file's frontmatter and body",
|
|
67
65
|
"--lane quick split big migration # r2d2-mechanic does it and opens the PR; no research, spec or plan"
|
|
68
66
|
]
|
|
@@ -71,222 +69,18 @@ module OpenLanes
|
|
|
71
69
|
# @param options [Hash]
|
|
72
70
|
# @return [void]
|
|
73
71
|
def call(words:, **options)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
)
|
|
72
|
+
workflow = Lifecycle.create_plan(dir: options.fetch(:dir, OpenLanes::PLANS_DIR),
|
|
73
|
+
**options.slice(:root, :after, :status, :prs, :spec, :draft, :lane, :depth),
|
|
74
|
+
from: typed_path(options[:from]))
|
|
75
|
+
created = unwrap(workflow.call(words))
|
|
76
|
+
|
|
77
|
+
open_spec(created.spec) if created.spec && options.fetch(:open, true)
|
|
78
|
+
puts created.path
|
|
79
|
+
success(created.message) unless quiet?(options)
|
|
90
80
|
end
|
|
91
81
|
|
|
92
82
|
private
|
|
93
83
|
|
|
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
84
|
# @param spec_path [String]
|
|
291
85
|
# @return [void]
|
|
292
86
|
def open_spec(spec_path)
|
|
@@ -301,54 +95,6 @@ module OpenLanes
|
|
|
301
95
|
#
|
|
302
96
|
# @return [Boolean]
|
|
303
97
|
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
98
|
end
|
|
353
99
|
end
|
|
354
100
|
end
|
|
@@ -11,6 +11,7 @@ module OpenLanes
|
|
|
11
11
|
standalone!
|
|
12
12
|
desc "Score agents against evals/cases: recorded folders by default, real runs with --live"
|
|
13
13
|
|
|
14
|
+
argument :name, required: false, desc: "Only this agent's cases, as `--agent` says it"
|
|
14
15
|
option :agent, aliases: ["-a"], desc: "Only this agent's cases"
|
|
15
16
|
option :case, aliases: ["-c"], desc: "Only the case with this id"
|
|
16
17
|
option :depth,
|
|
@@ -36,9 +37,11 @@ module OpenLanes
|
|
|
36
37
|
"--live -c research-fast --max-tokens 100000"
|
|
37
38
|
]
|
|
38
39
|
|
|
40
|
+
# @param name [String, nil] the positional spelling of `--agent`
|
|
39
41
|
# @param options [Hash]
|
|
40
42
|
# @return [void]
|
|
41
|
-
def call(**options)
|
|
43
|
+
def call(name: nil, **options)
|
|
44
|
+
options = options.merge(agent: name) if name && options[:agent].nil?
|
|
42
45
|
quiet?(options)
|
|
43
46
|
refuse("--max-tokens only applies to --live runs.", 64) if options[:max_tokens] && !options[:live]
|
|
44
47
|
cases = selected(options)
|
|
@@ -21,6 +21,9 @@ module OpenLanes
|
|
|
21
21
|
type: :boolean,
|
|
22
22
|
desc: "Whether plans have a front end; leave it out to let each plan decide"
|
|
23
23
|
option :jobs, aliases: ["-j"], desc: "How many plans `openlanes run` works on at once (default: cores - 2)"
|
|
24
|
+
option :quality,
|
|
25
|
+
values: Config::QUALITIES.map(&:to_s),
|
|
26
|
+
desc: "desired_codebase_quality: high or mvp (R4); leave it out to be asked on the first ok-not-perfect review"
|
|
24
27
|
option :yes,
|
|
25
28
|
type: :boolean,
|
|
26
29
|
default: false,
|
|
@@ -43,6 +46,7 @@ module OpenLanes
|
|
|
43
46
|
# @return [void]
|
|
44
47
|
def call(**options)
|
|
45
48
|
quiet?(options)
|
|
49
|
+
prepare(options)
|
|
46
50
|
root = Project.init_root
|
|
47
51
|
path = File.join(root, Config::FILE)
|
|
48
52
|
plans = File.expand_path(options.fetch(:dir, OpenLanes::PLANS_DIR), root)
|
|
@@ -62,7 +66,28 @@ module OpenLanes
|
|
|
62
66
|
|
|
63
67
|
private
|
|
64
68
|
|
|
65
|
-
#
|
|
69
|
+
# `openlanes setup`, but only when the machine is not ready, so a new
|
|
70
|
+
# project goes setup, init, start in one go and a ready machine is
|
|
71
|
+
# not made to wait on brew. A check that still fails is reported and
|
|
72
|
+
# init carries on: the file it writes needs none of them.
|
|
73
|
+
#
|
|
74
|
+
# @param options [Hash]
|
|
75
|
+
# @return [void]
|
|
76
|
+
def prepare(options)
|
|
77
|
+
return if machine.ready?
|
|
78
|
+
|
|
79
|
+
UI.line("This machine is missing something openlanes needs; running openlanes setup first.")
|
|
80
|
+
outcome = machine.call(dry_run: dry_run?(options))
|
|
81
|
+
$stderr.write(OpenLanes::Setup.render(outcome.checks))
|
|
82
|
+
warn("Still not ready: run each fix above, then openlanes setup.") unless outcome.ok?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A named seam, so a spec can stand in for brew, gh and Redis.
|
|
86
|
+
#
|
|
87
|
+
# @return [OpenLanes::Setup::Machine]
|
|
88
|
+
def machine = @machine ||= OpenLanes::Setup.machine
|
|
89
|
+
|
|
90
|
+
# The five values, each from its flag, else asked on a terminal, else
|
|
66
91
|
# its default. The result is parsed back as the file would be, so
|
|
67
92
|
# `init` cannot write a file every other command then refuses.
|
|
68
93
|
#
|
|
@@ -75,8 +100,9 @@ module OpenLanes
|
|
|
75
100
|
lane = options[:lane] || (ask ? prompt.select("Default lane for new plans:", Plans::Spec::LANES.map(&:to_s)) : "full")
|
|
76
101
|
frontend = options.key?(:frontend) ? options[:frontend] : (ask_frontend if ask)
|
|
77
102
|
jobs = options[:jobs] || (prompt.ask("Plans to run at once (blank: cores - 2):") if ask)
|
|
103
|
+
quality = options[:quality] || (ask_quality if ask)
|
|
78
104
|
|
|
79
|
-
draft = Config.new(project:, lane: lane.to_sym, frontend:, jobs: count(jobs))
|
|
105
|
+
draft = Config.new(project:, lane: lane.to_sym, frontend:, jobs: count(jobs), desired_codebase_quality: quality&.to_sym)
|
|
80
106
|
Config.parse(draft.to_yaml, path: File.join(root, Config::FILE))
|
|
81
107
|
end
|
|
82
108
|
|
|
@@ -86,6 +112,17 @@ module OpenLanes
|
|
|
86
112
|
{ "YES" => true, "NO" => false }[choice]
|
|
87
113
|
end
|
|
88
114
|
|
|
115
|
+
# R4's question. Left unanswered under -y, so the first
|
|
116
|
+
# `review-ok-not-perfect` pull request asks it instead.
|
|
117
|
+
#
|
|
118
|
+
# @return [String]
|
|
119
|
+
def ask_quality
|
|
120
|
+
prompt.select("Please indicate the desired quality of the codebase (this affects the severity of items " \
|
|
121
|
+
"found during review that will require rework):",
|
|
122
|
+
{ "High quality, but slower and more expensive: a review-ok-not-perfect PR returns to developers" => "high",
|
|
123
|
+
"Good enough, but faster and cheaper, think MVP: review-ok-not-perfect is accepted" => "mvp" })
|
|
124
|
+
end
|
|
125
|
+
|
|
89
126
|
# @param value [String, Integer, nil]
|
|
90
127
|
# @return [Integer, nil]
|
|
91
128
|
def count(value)
|
|
@@ -39,7 +39,7 @@ module OpenLanes
|
|
|
39
39
|
# @param options [Hash]
|
|
40
40
|
# @return [void]
|
|
41
41
|
def call(team:, **options)
|
|
42
|
-
tree = tree_for(options)
|
|
42
|
+
tree = tree_for(options).everything
|
|
43
43
|
require_project!(options)
|
|
44
44
|
import = build(tree, team, resolve_project(team, tree, options), options)
|
|
45
45
|
|
|
@@ -16,7 +16,7 @@ module OpenLanes
|
|
|
16
16
|
# @param options [Hash]
|
|
17
17
|
# @return [void]
|
|
18
18
|
def call(team:, **options)
|
|
19
|
-
tree = tree_for(options)
|
|
19
|
+
tree = tree_for(options).everything
|
|
20
20
|
_api, survey = survey_for(team, tree)
|
|
21
21
|
|
|
22
22
|
survey.projects.each { |project| puts row(project) }
|
|
@@ -6,12 +6,18 @@ module OpenLanes
|
|
|
6
6
|
class ListPlans < Base
|
|
7
7
|
desc "Print every plan, its state, and its pull requests"
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
option :all,
|
|
10
|
+
type: :boolean,
|
|
11
|
+
default: false,
|
|
12
|
+
desc: "Also list the plans in archive/, deferred/ and abandoned/"
|
|
13
|
+
|
|
14
|
+
example ["", "--all", "-D .plans", "| less -R"]
|
|
10
15
|
|
|
11
16
|
# @param options [Hash]
|
|
12
17
|
# @return [void]
|
|
13
18
|
def call(**options)
|
|
14
|
-
|
|
19
|
+
tree = tree_for(options)
|
|
20
|
+
reporter = Presentation.reporter(tree: options[:all] ? tree.everything : tree)
|
|
15
21
|
$stdout.write(reporter.render)
|
|
16
22
|
|
|
17
23
|
exit 1 unless reporter.inconsistent.empty?
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module Mail
|
|
6
|
+
# `openlanes mail ack` — say a message was read.
|
|
7
|
+
#
|
|
8
|
+
# Delivery cannot say it: a message that reached `state.json` may sit
|
|
9
|
+
# there for a whole round while its reader never polls. Only the reader
|
|
10
|
+
# knows, so only the reader writes `read: "yes"`.
|
|
11
|
+
class Ack < Base
|
|
12
|
+
include Locating
|
|
13
|
+
|
|
14
|
+
desc "Mark a message in a plan's mailbox as read"
|
|
15
|
+
|
|
16
|
+
argument :number, type: :string, required: true, desc: "The message's number, as `mail read` prints it"
|
|
17
|
+
|
|
18
|
+
option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
|
|
19
|
+
option :for, required: true, desc: "The agent that read it, who must be its recipient"
|
|
20
|
+
|
|
21
|
+
example [
|
|
22
|
+
"--plan 003 --for rey-frontend 4 # rey read #4"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# @param number [String]
|
|
26
|
+
# @param options [Hash]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def call(number:, **options)
|
|
29
|
+
quiet?(options)
|
|
30
|
+
mailbox = mailbox_for(options)
|
|
31
|
+
message = begin
|
|
32
|
+
mailbox.ack(number, by: options[:for])
|
|
33
|
+
rescue OpenLanes::Error => e
|
|
34
|
+
refuse(e.message, 65)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return if quiet?(options)
|
|
38
|
+
|
|
39
|
+
# A mailbox.md message is history and already counts as read, so
|
|
40
|
+
# nothing was written; saying "read by" would claim a record that
|
|
41
|
+
# does not exist.
|
|
42
|
+
if mailbox.legacy?(message.number)
|
|
43
|
+
return UI.line("##{message.number} is a legacy message in #{File.basename(mailbox.dir)}/#{Plans::Mailbox::FILENAME}, " \
|
|
44
|
+
"already counted as read; nothing was written")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
UI.line("##{message.number} read by #{message.to}, in #{File.basename(mailbox.dir)}/#{Plans::PlanState::FILENAME}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module CLI
|
|
5
|
+
module Mail
|
|
6
|
+
# `openlanes mail render` — the whole exchange, as Markdown, on STDOUT.
|
|
7
|
+
#
|
|
8
|
+
# The JSON is the record. This prints a view of it and writes no file,
|
|
9
|
+
# so there is never a second copy in the plan folder to disagree with
|
|
10
|
+
# the first.
|
|
11
|
+
class Render < Base
|
|
12
|
+
include Locating
|
|
13
|
+
|
|
14
|
+
desc "Print a plan's whole mailbox as Markdown"
|
|
15
|
+
|
|
16
|
+
option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
|
|
17
|
+
|
|
18
|
+
example [
|
|
19
|
+
"--plan 003 # every message, with read or unread",
|
|
20
|
+
"--plan 003 > mail.md # keep a copy wherever you like"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# @param options [Hash]
|
|
24
|
+
# @return [void]
|
|
25
|
+
def call(**options)
|
|
26
|
+
puts mailbox_for(options).render
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -30,6 +30,7 @@ module OpenLanes
|
|
|
30
30
|
rescue OpenLanes::Error => e
|
|
31
31
|
refuse(e.message, 64)
|
|
32
32
|
end
|
|
33
|
+
publish(mailbox, message)
|
|
33
34
|
|
|
34
35
|
return if quiet?(options)
|
|
35
36
|
|
|
@@ -37,6 +38,31 @@ module OpenLanes
|
|
|
37
38
|
# four-line frame around "#7 → rey-frontend" is noise it reads.
|
|
38
39
|
UI.line("##{message.number} → #{message.to}, in #{File.basename(mailbox.dir)}/#{Plans::PlanState::FILENAME}")
|
|
39
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Hand the message to the harness over Redis, when there is one.
|
|
45
|
+
# `state.json` already has it, so no Redis is not a failure and says
|
|
46
|
+
# nothing: an agent that sees a warning on every send learns to
|
|
47
|
+
# ignore warnings. The keys are the plans directory's checkout, not
|
|
48
|
+
# this process's, because an agent in a worktree sends to the run in
|
|
49
|
+
# the main checkout.
|
|
50
|
+
#
|
|
51
|
+
# @param mailbox [OpenLanes::Plans::Mailbox]
|
|
52
|
+
# @param message [OpenLanes::Plans::Mailbox::Message]
|
|
53
|
+
# @return [void]
|
|
54
|
+
def publish(mailbox, message)
|
|
55
|
+
feature = Plans::Feature.parse(mailbox.dir) or return
|
|
56
|
+
root = File.dirname(mailbox.dir, 2)
|
|
57
|
+
store = Live.connect(Live.redis_url(config: Config.current))
|
|
58
|
+
begin
|
|
59
|
+
Live::Mail.new(store:, keys: Live.keys(root:)).publish(plan: feature.ordinal, message:)
|
|
60
|
+
ensure
|
|
61
|
+
store.close
|
|
62
|
+
end
|
|
63
|
+
rescue Live::Unavailable
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
40
66
|
end
|
|
41
67
|
end
|
|
42
68
|
end
|
|
@@ -4,42 +4,83 @@ module OpenLanes
|
|
|
4
4
|
module CLI
|
|
5
5
|
# `openlanes resync …`
|
|
6
6
|
module Resync
|
|
7
|
-
# `resync dirs` — reconcile folder names with folder contents.
|
|
7
|
+
# `resync dirs` — reconcile folder names and places with folder contents.
|
|
8
8
|
class Dirs < Base
|
|
9
|
-
desc "Rename plan folders so the name
|
|
9
|
+
desc "Rename and move plan folders so the name, the place and the contents agree"
|
|
10
10
|
|
|
11
|
-
dry_run_option "Rename nothing; print what would be renamed"
|
|
11
|
+
dry_run_option "Rename nothing; print what would be renamed or moved"
|
|
12
12
|
|
|
13
13
|
example [
|
|
14
|
-
"-n # show what would be renamed",
|
|
14
|
+
"-n # show what would be renamed or moved",
|
|
15
15
|
" # do it"
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
# @param options [Hash]
|
|
19
19
|
# @return [void]
|
|
20
20
|
def call(**options)
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
resync = OpenLanes::Lifecycle.resync_dirs(tree: tree_for(options))
|
|
22
|
+
locks = resync.locks
|
|
23
|
+
rewrites = resync.rewrites
|
|
24
|
+
changes = resync.call(commit: !dry_run?(options))
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
report_locks(locks, options)
|
|
27
|
+
report_rewrites(rewrites, options)
|
|
28
|
+
|
|
29
|
+
if changes.empty? && rewrites.empty?
|
|
30
|
+
unless quiet?(options)
|
|
31
|
+
success("Every folder is already named NNN.MM━━<emoji>━━❯<slug>, in its place, and the emoji matches.")
|
|
32
|
+
end
|
|
26
33
|
return
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
changes.each do |change|
|
|
30
|
-
puts "#{change.dirname}\t#{
|
|
37
|
+
puts "#{change.dirname}\t#{change.target_name}\t#{change.reason}"
|
|
31
38
|
next if quiet?(options)
|
|
32
39
|
|
|
33
|
-
say("#{paint(change.dirname, :bright_black)} → #{
|
|
40
|
+
say("#{paint(change.dirname, :bright_black)} → #{change.target_name}")
|
|
34
41
|
say(" #{paint(change.reason, :yellow)}", bullet: " ")
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
return if quiet?(options)
|
|
38
45
|
|
|
46
|
+
total = changes.size + rewrites.size
|
|
39
47
|
if dry_run?(options)
|
|
40
|
-
dry_run_footer(
|
|
48
|
+
dry_run_footer(total, "change#{"s" unless total == 1}")
|
|
41
49
|
else
|
|
42
|
-
success("Renamed #{changes.size} folder#{"s" unless changes.size == 1}
|
|
50
|
+
success("Renamed #{changes.size} folder#{"s" unless changes.size == 1}, " \
|
|
51
|
+
"rewrote #{rewrites.size} spec#{"s" unless rewrites.size == 1}.")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @param rewrites [Array<OpenLanes::Lifecycle::Resync::Dirs::Rewrite>]
|
|
58
|
+
# @param options [Hash]
|
|
59
|
+
# @return [void]
|
|
60
|
+
def report_rewrites(rewrites, options)
|
|
61
|
+
rewrites.each do |rewrite|
|
|
62
|
+
puts "#{rewrite.dirname}/spec.md\t#{rewrite.dirname}/spec.md\t#{rewrite.reason}"
|
|
63
|
+
say("#{rewrite.dirname}/spec.md: #{paint(rewrite.reason, :yellow)}") unless quiet?(options)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A lock keeps its folder's name, so a folder that did not move needs
|
|
68
|
+
# a word about why. A dead local lock is cleared by a real run; one
|
|
69
|
+
# from another machine is only ever reported.
|
|
70
|
+
#
|
|
71
|
+
# @param locks [Array<OpenLanes::Lifecycle::Resync::Dirs::HeldLock>]
|
|
72
|
+
# @param options [Hash]
|
|
73
|
+
# @return [void]
|
|
74
|
+
def report_locks(locks, options)
|
|
75
|
+
return if quiet?(options)
|
|
76
|
+
|
|
77
|
+
locks.each do |held|
|
|
78
|
+
verdict = if held.foreign? then "another machine's lock; left alone"
|
|
79
|
+
elsif held.stale? then dry_run?(options) ? "its process is gone; would clear it" : "its process is gone; cleared"
|
|
80
|
+
else "an agent is working in it; not renamed"
|
|
81
|
+
end
|
|
82
|
+
say("🔑 #{held}")
|
|
83
|
+
say(" #{paint(verdict, :yellow)}", bullet: " ")
|
|
43
84
|
end
|
|
44
85
|
end
|
|
45
86
|
end
|