openlanes 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +62 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +24 -3
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +32 -10
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +26 -2
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -454
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -64
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +94 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1229 -116
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +38 -105
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +236 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +39 -11
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +665 -0
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +221 -31
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +290 -0
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +114 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +36 -1
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +38 -4
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +12 -8
- data/lib/openlanes/live/store.rb +36 -1
- data/lib/openlanes/live/subscriber.rb +93 -0
- data/lib/openlanes/live.rb +71 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +66 -7
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +110 -21
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +287 -7
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +210 -14
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +213 -15
- data/lib/openlanes/web/context.rb +33 -9
- data/lib/openlanes/web/demo.rb +39 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +137 -9
- data/lib/openlanes/web/launcher.rb +70 -8
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +31 -10
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +19 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -881
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-CI74IkWP.js +0 -198
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Presentation
|
|
7
|
+
# The OpenAPI documents plans carry, one `openapi.yaml` per plan folder,
|
|
8
|
+
# gathered so a person can find and render them.
|
|
9
|
+
#
|
|
10
|
+
# This is a smoke check, not a validator: a file is a problem when it is
|
|
11
|
+
# not YAML, or lacks the two top-level keys every OpenAPI document has.
|
|
12
|
+
# A real validator would pull in a JSON Schema stack for a check whose
|
|
13
|
+
# job is to catch the file an agent half-wrote, which these two do.
|
|
14
|
+
class ApiDocs
|
|
15
|
+
# The name an agent writes its contract under, in its plan's folder.
|
|
16
|
+
FILE = "openapi.yaml"
|
|
17
|
+
|
|
18
|
+
# Keys whose absence means the file is not an OpenAPI document at all.
|
|
19
|
+
REQUIRED_KEYS = %w[openapi paths].freeze
|
|
20
|
+
|
|
21
|
+
# One document that passed the check.
|
|
22
|
+
#
|
|
23
|
+
# @!attribute path [String] absolute path to the file
|
|
24
|
+
# @!attribute title [String, nil] `info.title`, when it has one
|
|
25
|
+
# @!attribute version [String] the `openapi:` value, such as "3.1.0"
|
|
26
|
+
Found = Data.define(:path, :title, :version)
|
|
27
|
+
|
|
28
|
+
# One document that did not.
|
|
29
|
+
#
|
|
30
|
+
# @!attribute path [String] absolute path to the file
|
|
31
|
+
# @!attribute reason [String] what is wrong, in a sentence
|
|
32
|
+
Problem = Data.define(:path, :reason)
|
|
33
|
+
|
|
34
|
+
# Everything one look at a tree found.
|
|
35
|
+
#
|
|
36
|
+
# @!attribute found [Array<Found>]
|
|
37
|
+
# @!attribute problems [Array<Problem>]
|
|
38
|
+
Result = Data.define(:found, :problems) do
|
|
39
|
+
# @return [Boolean] true when no file failed the check
|
|
40
|
+
def ok? = problems.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
44
|
+
# @return [Result] files ordered by path, so plan order
|
|
45
|
+
def call(tree)
|
|
46
|
+
found = []
|
|
47
|
+
problems = []
|
|
48
|
+
Dir.glob(File.join(tree.dir, "*", FILE)).each do |path|
|
|
49
|
+
outcome = inspect_file(path)
|
|
50
|
+
(outcome.is_a?(Problem) ? problems : found) << outcome
|
|
51
|
+
end
|
|
52
|
+
Result.new(found:, problems:)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @param path [String]
|
|
58
|
+
# @return [Found, Problem]
|
|
59
|
+
def inspect_file(path)
|
|
60
|
+
document = YAML.safe_load_file(path, permitted_classes: Frontmatter::PERMITTED_CLASSES, aliases: true)
|
|
61
|
+
return Problem.new(path:, reason: "is not a YAML mapping") unless document.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
missing = REQUIRED_KEYS.reject { document.key?(it) }
|
|
64
|
+
unless missing.empty?
|
|
65
|
+
return Problem.new(path:, reason: "has no top-level #{missing.map { "#{it}:" }.join(" or ")}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Found.new(path:, title: document.dig("info", "title"), version: document["openapi"].to_s)
|
|
69
|
+
rescue Psych::Exception => e
|
|
70
|
+
# The parser's own message carries the line and column, which is
|
|
71
|
+
# what a person needs to find the bad indent.
|
|
72
|
+
Problem.new(path:, reason: "does not parse as YAML: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -19,6 +19,7 @@ module OpenLanes
|
|
|
19
19
|
[
|
|
20
20
|
header,
|
|
21
21
|
numbering,
|
|
22
|
+
places,
|
|
22
23
|
starting,
|
|
23
24
|
status_table,
|
|
24
25
|
blocking,
|
|
@@ -26,6 +27,7 @@ module OpenLanes
|
|
|
26
27
|
diagram,
|
|
27
28
|
files_section,
|
|
28
29
|
pull_requests,
|
|
30
|
+
review_labels,
|
|
29
31
|
footer
|
|
30
32
|
].join("\n")
|
|
31
33
|
end
|
|
@@ -58,7 +60,7 @@ module OpenLanes
|
|
|
58
60
|
| :---- | :---- | :---------------------- |
|
|
59
61
|
| **spec** | #{status(:new).emoji} #{status(:new).label} | `spec.md` |
|
|
60
62
|
| **plan** | #{status(:planned).emoji} #{status(:planned).label} | `plan.md` |
|
|
61
|
-
| **build** | #{status(:building).emoji} → #{status(:ready_for_review).emoji} → #{status(:in_review).emoji} → #{status(:approved).emoji} | `pull-requests.md` |
|
|
63
|
+
| **build** | #{status(:building).emoji} → #{status(:ready_for_review).emoji} → #{status(:in_review).emoji} → #{status(:verifying).emoji} → #{status(:completed).emoji} → #{status(:approved).emoji} | `pull-requests.md` |
|
|
62
64
|
|
|
63
65
|
Those files are not paperwork. They are what the tool checks: a folder may not
|
|
64
66
|
claim a phase whose file is missing, and `openlanes resync dirs` renames
|
|
@@ -79,18 +81,33 @@ module OpenLanes
|
|
|
79
81
|
The shape is always `NNN.MM`:
|
|
80
82
|
|
|
81
83
|
```
|
|
82
|
-
#{OpenLanes::PLANS_DIR}
|
|
83
|
-
#{OpenLanes::PLANS_DIR}
|
|
84
|
-
#{OpenLanes::PLANS_DIR}
|
|
85
|
-
#{OpenLanes::PLANS_DIR}
|
|
84
|
+
#{OpenLanes::PLANS_DIR}/#{name("000.00", :new, "initial-spec")}
|
|
85
|
+
#{OpenLanes::PLANS_DIR}/#{name("001.00", :building, "dev-foundation")}
|
|
86
|
+
#{OpenLanes::PLANS_DIR}/#{name("001.01", :retroactive, "schedule-k1-backfill")} <- shipped between 001 and 002,
|
|
87
|
+
#{OpenLanes::PLANS_DIR}/#{name("002.00", :planned, "tenancy-households")} specified afterwards
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
A folder name holds no space, so it needs no quoting in a shell, and every
|
|
91
|
+
state emoji is a single code point, so it measures the same in every terminal.
|
|
92
|
+
Older spellings (`NNN.MM-<emoji> → <slug>`, `NNN-<emoji>--<slug>`) and the
|
|
93
|
+
emoji they used are still read; `openlanes resync dirs` renames them.
|
|
94
|
+
|
|
88
95
|
- **`NNN` counts from `000`.** The first plan of a project is `000.00`; after
|
|
89
96
|
that it is the highest major plus one, zero-padded to three digits.
|
|
90
97
|
- **`MM` is `00` for an ordinary plan** — one specified before it was built.
|
|
91
98
|
- **`MM` from `01` to `99` marks a retroactive plan**: work that shipped with no
|
|
92
99
|
specification, documented after the fact. `openlanes create --after 001`
|
|
93
100
|
takes the next free slot in the gap after 001.
|
|
101
|
+
- **A number is taken if any branch holds it.** "Highest" counts the primary
|
|
102
|
+
checkout's `#{OpenLanes::PLANS_DIR}` (archived plans included) and `#{OpenLanes::PLANS_DIR}` on every local
|
|
103
|
+
branch and every `origin/*` branch, read without a fetch, so a plan on an
|
|
104
|
+
unmerged branch keeps its number.
|
|
105
|
+
- **Plans are created only in the primary checkout, on its default branch**
|
|
106
|
+
(origin's HEAD, else `main`). In a linked worktree, or on any other branch,
|
|
107
|
+
`openlanes create`, the page's new plan and `resync prs` adoption refuse and
|
|
108
|
+
say why. The same holds for an opening `spec.md` and for `depends_on:`;
|
|
109
|
+
every other file in a plan folder may be written from a worktree. Numbers are handed out under a lock in the shared git directory,
|
|
110
|
+
so two creates at once cannot take the same one.
|
|
94
111
|
|
|
95
112
|
`001.01` is a **sibling of 001 that arrived later, not a part of 001**. The dot
|
|
96
113
|
reads as containment in almost every other numbering scheme, and here it does
|
|
@@ -120,6 +137,53 @@ module OpenLanes
|
|
|
120
137
|
MARKDOWN
|
|
121
138
|
end
|
|
122
139
|
|
|
140
|
+
# Where a plan lives, derived from {Plans::Tree::INACTIVE_DIRS}.
|
|
141
|
+
#
|
|
142
|
+
# @return [String]
|
|
143
|
+
def places
|
|
144
|
+
rows = OpenLanes::Plans::Tree::INACTIVE_DIRS.group_by { |_, dir| dir }.map do |dir, pairs|
|
|
145
|
+
states = pairs.map { |key, _| "#{status(key).emoji} #{status(key).label}" }.join(", ")
|
|
146
|
+
"| `#{OpenLanes::PLANS_DIR}/#{dir}/` | #{states} |"
|
|
147
|
+
end
|
|
148
|
+
dated = name("001.00", :approved, "dev-foundation", archived_on: Date.new(2026, 9, 1))
|
|
149
|
+
|
|
150
|
+
<<~MARKDOWN
|
|
151
|
+
## Where a plan lives
|
|
152
|
+
|
|
153
|
+
The top of `#{OpenLanes::PLANS_DIR}` holds only plans somebody may still work on.
|
|
154
|
+
A plan in one of these states moves into a subfolder, and its name gains the
|
|
155
|
+
date it got there:
|
|
156
|
+
|
|
157
|
+
| Folder | States |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
#{rows.join("\n")}
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
#{OpenLanes::PLANS_DIR}/#{OpenLanes::Plans::Tree.subfolder_for(status(:approved))}/#{dated}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`openlanes resync dirs` does the moving, with `git mv`, and moves a plan back
|
|
166
|
+
to the top, undated, once its state is open again. The date is the folder's
|
|
167
|
+
last rename in git, else `state.json`'s `updated_at`, else today. Numbers span
|
|
168
|
+
the subfolders: a number in `archive/` is never handed out again.
|
|
169
|
+
`openlanes list-plans` shows the open plans; `--all` adds the rest.
|
|
170
|
+
|
|
171
|
+
While an agent works in a plan, its lock (pid, host, start time) sits in the
|
|
172
|
+
runtime state directory outside the repository (a legacy
|
|
173
|
+
`#{OpenLanes::Plans::Lock::FILENAME}` in the folder is still read), and `list-plans`
|
|
174
|
+
shows 🔑. The folder is never renamed while it is locked. `resync dirs` clears a
|
|
175
|
+
lock whose process is gone from this machine, and only reports one from another.
|
|
176
|
+
|
|
177
|
+
MARKDOWN
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @param ordinal [String]
|
|
181
|
+
# @param key [Symbol]
|
|
182
|
+
# @param slug [String]
|
|
183
|
+
# @param archived_on [Date, nil]
|
|
184
|
+
# @return [String] a folder name, spelled the one way {Plans.plan_dirname} spells it
|
|
185
|
+
def name(ordinal, key, slug, archived_on: nil) = OpenLanes::Plans.plan_dirname(ordinal, status(key), slug, archived_on:)
|
|
186
|
+
|
|
123
187
|
# @return [String]
|
|
124
188
|
def starting
|
|
125
189
|
headings = OpenLanes::Lifecycle::Brief::HEADINGS.map { |h| "- `## #{h}`" }.join("\n")
|
|
@@ -320,15 +384,22 @@ module OpenLanes
|
|
|
320
384
|
| :--- | :---------- |
|
|
321
385
|
| `spec.md` | the specification; written first |
|
|
322
386
|
| `plan.md` | the execution plan; written from the spec |
|
|
323
|
-
| `
|
|
387
|
+
| `contract.md` | the contract between the two halves of building |
|
|
388
|
+
| `openapi.yaml` | the HTTP surface, when the plan has one |
|
|
324
389
|
#{known.reject { |r| r.start_with?("| `spec.md`", "| `plan.md`") }.join("\n")}
|
|
325
390
|
|
|
326
|
-
`
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
391
|
+
`contract.md` is the one file no state requires. `luke-backend` writes it
|
|
392
|
+
before it writes code, and `rey-frontend` builds from it a round later: the
|
|
393
|
+
interfaces the back end landed, their shapes and their errors, which files
|
|
394
|
+
each half owns, which units may run concurrently, and the one test that
|
|
395
|
+
proves the halves are joined rather than merely both present. No state
|
|
396
|
+
requires it because a plan with no back-end work never needs one. When
|
|
397
|
+
`plan.md` says the plan has an HTTP surface, `openapi.yaml` sits beside it,
|
|
398
|
+
linked from it, and `openlanes api docs` lists every one in the project.
|
|
399
|
+
|
|
400
|
+
The units themselves live in `plan.md`, under `## Backend` and
|
|
401
|
+
`## Frontend`. `rey-frontend` is started only when `## Frontend` holds a
|
|
402
|
+
unit heading.
|
|
332
403
|
|
|
333
404
|
Nothing else belongs there. A folder holding notes, diagrams or scratch files
|
|
334
405
|
is a folder nobody can audit at a glance.
|
|
@@ -345,6 +416,28 @@ module OpenLanes
|
|
|
345
416
|
.join(", ")
|
|
346
417
|
end
|
|
347
418
|
|
|
419
|
+
# Derived from {Plans::ReviewLabel::ALL}, the only place a label is named.
|
|
420
|
+
#
|
|
421
|
+
# @return [String]
|
|
422
|
+
def review_labels
|
|
423
|
+
rows = OpenLanes::Plans::ReviewLabel::ALL.map do |l|
|
|
424
|
+
"| `#{l.name}` | `#{l.colour}` | #{l.passing? ? "yes" : "no"} | #{l.meaning} |"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
<<~MARKDOWN
|
|
428
|
+
## Review labels
|
|
429
|
+
|
|
430
|
+
A pull request carries one of these at a time, set through `openlanes review <pr> <label>`.
|
|
431
|
+
A new label replaces the previous one, except that `#{OpenLanes::Plans::ReviewLabel::VULNERABILITY}`
|
|
432
|
+
sits beside `#{OpenLanes::Plans::ReviewLabel::REJECTED}`. No passing verdict is given while CI is red.
|
|
433
|
+
The engine creates or recolours every one of them on the repository when a run starts.
|
|
434
|
+
|
|
435
|
+
| Label | Colour | Passing | Meaning |
|
|
436
|
+
| --- | --- | --- | --- |
|
|
437
|
+
#{rows.join("\n")}
|
|
438
|
+
MARKDOWN
|
|
439
|
+
end
|
|
440
|
+
|
|
348
441
|
# @return [String]
|
|
349
442
|
def pull_requests
|
|
350
443
|
<<~MARKDOWN
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Presentation
|
|
5
|
+
# What `resync prs` did, or would do, to pull request titles: one tab-
|
|
6
|
+
# separated row per change for STDOUT, and the retitles, the new plan
|
|
7
|
+
# folders, the skipped pull requests and the assumed-prefix warning for
|
|
8
|
+
# a person on STDERR.
|
|
9
|
+
class PrChanges
|
|
10
|
+
include UI
|
|
11
|
+
|
|
12
|
+
# @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
13
|
+
# @param dry_run [Boolean] whether nothing was actually changed
|
|
14
|
+
def initialize(changes, dry_run:)
|
|
15
|
+
@changes = changes
|
|
16
|
+
@dry_run = dry_run
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<String>] the deliverable rows: number, new title, reason
|
|
20
|
+
def lines = @changes.map { |change| "#{change.number}\t#{change.new_title || "-"}\t#{change.reason}" }
|
|
21
|
+
|
|
22
|
+
# Draws the report on STDERR.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def report
|
|
26
|
+
applicable, flagged = @changes.partition(&:applicable?)
|
|
27
|
+
assumed = applicable.select(&:assumed?)
|
|
28
|
+
|
|
29
|
+
applicable.each { |change| say("##{change.number} #{change.new_title}#{note(change)}") }
|
|
30
|
+
adoptions(applicable.select(&:adopted?))
|
|
31
|
+
flagged.each { |change| say("##{change.number} #{paint("SKIPPED", :red)} — #{change.reason}", bullet: "!") }
|
|
32
|
+
footer(applicable.size)
|
|
33
|
+
assumptions(assumed)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @param change [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
39
|
+
# @return [String, nil]
|
|
40
|
+
def note(change)
|
|
41
|
+
if change.adopted?
|
|
42
|
+
paint(" (new plan)", :magenta)
|
|
43
|
+
elsif change.assumed?
|
|
44
|
+
paint(" (assumed)", :yellow)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Creating folders is a bigger act than editing a title, so it is
|
|
49
|
+
# reported separately rather than buried in the retitle list.
|
|
50
|
+
#
|
|
51
|
+
# @param adopted [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
52
|
+
# @return [void]
|
|
53
|
+
def adoptions(adopted)
|
|
54
|
+
return if adopted.empty?
|
|
55
|
+
|
|
56
|
+
verb = @dry_run ? "Would create" : "Created"
|
|
57
|
+
info("#{verb} #{adopted.size} plan folder#{"s" unless adopted.size == 1} for pull " \
|
|
58
|
+
"requests that resolved to no plan:\n\n" \
|
|
59
|
+
"#{adopted.map { |c| " #{c.ordinal} ##{c.number} #{c.title}" }.join("\n")}\n\n" \
|
|
60
|
+
"Each holds its pull request and nothing else. Run `openlanes run`\n" \
|
|
61
|
+
"to have the specification written from what the pull request actually did.")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param count [Integer] titles retitled, or that would be
|
|
65
|
+
# @return [void]
|
|
66
|
+
def footer(count)
|
|
67
|
+
if @dry_run
|
|
68
|
+
return if count.zero?
|
|
69
|
+
|
|
70
|
+
warn("#{count} retitle#{"s" unless count == 1} pending — nothing was changed.\n" \
|
|
71
|
+
"Re-run without --dry-run to apply.")
|
|
72
|
+
else
|
|
73
|
+
success("Retitled #{count} pull request#{"s" unless count == 1}.")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param assumed [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
78
|
+
# @return [void]
|
|
79
|
+
def assumptions(assumed)
|
|
80
|
+
return if assumed.empty?
|
|
81
|
+
|
|
82
|
+
warn("#{assumed.size} title#{"s" unless assumed.size == 1} would get " \
|
|
83
|
+
"[#{OpenLanes::NO_PLAN_PREFIX}] because no plan resolved.\n\n" \
|
|
84
|
+
"That is an assertion about intent, and it is yours to make: check each one\n" \
|
|
85
|
+
"before committing. A pull request that writes a plan's spec belongs to that\n" \
|
|
86
|
+
"plan however its branch was named.")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -24,8 +24,11 @@ module OpenLanes
|
|
|
24
24
|
STATE_WIDTH = Plans::PullRequests::STATES.map { |_, label| UI.display_width(label) }.max
|
|
25
25
|
|
|
26
26
|
# @param tree [OpenLanes::Plans::Tree]
|
|
27
|
-
|
|
27
|
+
# @param waiting_on [Hash{String => Array<Integer>}] plan number to the
|
|
28
|
+
# pull requests open and awaiting a human, from the board snapshot
|
|
29
|
+
def initialize(tree:, waiting_on: {})
|
|
28
30
|
@tree = tree
|
|
31
|
+
@waiting_on = waiting_on.transform_keys(&:to_s)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# @return [OpenLanes::Plans::Tree]
|
|
@@ -54,7 +57,7 @@ module OpenLanes
|
|
|
54
57
|
def label_width = @label_width ||= tree.subjects.map { |s| UI.display_width(s.status.label) }.max.to_i + 4
|
|
55
58
|
|
|
56
59
|
# @return [Integer] widest feature name
|
|
57
|
-
def title_width = @title_width ||= tree.subjects.map { |s| UI.display_width(s
|
|
60
|
+
def title_width = @title_width ||= tree.subjects.map { |s| UI.display_width(title_for(s)) }.max.to_i + 2
|
|
58
61
|
|
|
59
62
|
# The one place the table's shape is written down. The heading, the plan
|
|
60
63
|
# rows, the continuation lines and the violations all come through here, so
|
|
@@ -67,7 +70,7 @@ module OpenLanes
|
|
|
67
70
|
#
|
|
68
71
|
# @return [String]
|
|
69
72
|
def row(flag, ordinal, emoji, label, title, tail)
|
|
70
|
-
format("%s %s %s
|
|
73
|
+
format("%s %s %s %s %s %s", flag, ordinal, emoji, label, title, tail)
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# Cell at which the pull requests start, so a plan's second and third pull
|
|
@@ -102,6 +105,25 @@ module OpenLanes
|
|
|
102
105
|
[UI.paint(text, :bold), UI.paint(rule, :bright_yellow)].join("\n")
|
|
103
106
|
end
|
|
104
107
|
|
|
108
|
+
# A plan an agent is working on wears 🔑 before its name (a lock held by
|
|
109
|
+
# another machine adds where and since when), and a plan
|
|
110
|
+
# moved aside carries the date it was moved. In the title cell rather
|
|
111
|
+
# than columns of their own, so the pull requests stay where every other
|
|
112
|
+
# row puts them.
|
|
113
|
+
#
|
|
114
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
115
|
+
# @return [String]
|
|
116
|
+
def title_for(subject)
|
|
117
|
+
feature = subject.feature
|
|
118
|
+
title = feature.archived_on ? "#{feature.title} [#{feature.archived_on.strftime("%F")}]" : feature.title
|
|
119
|
+
lock = Plans::Lock.read(feature.path)
|
|
120
|
+
return title unless lock
|
|
121
|
+
|
|
122
|
+
# A lock from another machine is never cleared, so say whose it is.
|
|
123
|
+
note = lock.foreign? ? " (held on #{lock.host} since #{lock.started_at.utc.iso8601})" : ""
|
|
124
|
+
"🔑 #{title}#{note}"
|
|
125
|
+
end
|
|
126
|
+
|
|
105
127
|
# One line per plan, plus a continuation line for each extra pull request
|
|
106
128
|
# so that every URL is visible and clickable rather than truncated.
|
|
107
129
|
#
|
|
@@ -116,13 +138,40 @@ module OpenLanes
|
|
|
116
138
|
UI.paint(UI.fit(feature.ordinal.to_s, ORDINAL_WIDTH), :bold),
|
|
117
139
|
UI.fit(feature.status.emoji, EMOJI_WIDTH),
|
|
118
140
|
UI.paint(UI.fit(feature.status.label, label_width), :yellow),
|
|
119
|
-
UI.paint(UI.fit(
|
|
120
|
-
prs.empty? ? UI.paint("[none yet]", :bright_red) : link(prs.first)
|
|
141
|
+
UI.paint(UI.fit(title_for(subject), title_width), :blue, :bold),
|
|
142
|
+
[prs.empty? ? UI.paint("[none yet]", :bright_red) : link(prs.first), *blocks_note(feature.ordinal)].join(" ")
|
|
121
143
|
)
|
|
122
144
|
|
|
123
145
|
rest = prs.drop(1).map { |pr| (" " * pr_column) + link(pr) }
|
|
124
146
|
|
|
125
|
-
[first, *rest, *violation_line(subject)]
|
|
147
|
+
[first, *rest, *waiting_lines(subject), *violation_line(subject), *dependency_lines(subject)]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# What this plan holds up, said in the row's tail so the one line that
|
|
151
|
+
# answers "why is 050 not moving" is on the plan that causes it.
|
|
152
|
+
#
|
|
153
|
+
# @param ordinal [OpenLanes::Plans::Ordinal]
|
|
154
|
+
# @return [Array<String>]
|
|
155
|
+
def blocks_note(ordinal)
|
|
156
|
+
blocked = tree.dependencies.blocks_of(ordinal)
|
|
157
|
+
blocked.empty? ? [] : [UI.paint("blocks #{blocked.join(", ")}", :cyan)]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Dependency trouble under the plan's row. Every answer comes from
|
|
161
|
+
# {Plans::Dependencies}; nothing about the graph is computed here.
|
|
162
|
+
#
|
|
163
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
164
|
+
# @return [Array<String>]
|
|
165
|
+
def dependency_lines(subject)
|
|
166
|
+
ordinal = subject.feature.ordinal
|
|
167
|
+
deps = tree.dependencies
|
|
168
|
+
notes = Plans::Spec.for(subject).problems.grep(/\Adepends_on/).map { |p| [p, :red] }
|
|
169
|
+
deps.unknown(ordinal).each { |o| notes << ["depends_on: #{o} is not a plan in this tree", :red] }
|
|
170
|
+
deps.closed(ordinal).each { |o| notes << ["depends_on: #{o} is already done, so the entry is ignored", :yellow] }
|
|
171
|
+
cycle = deps.cycle_of(ordinal)
|
|
172
|
+
notes << ["cycle: #{cycle.join(" → ")}", :red] if cycle
|
|
173
|
+
|
|
174
|
+
notes.map { |text, colour| (" " * title_column) + UI.paint("↳ #{text}", colour) }
|
|
126
175
|
end
|
|
127
176
|
|
|
128
177
|
# The URL is left bare rather than wrapped in an OSC 8 hyperlink: the table
|
|
@@ -152,6 +201,16 @@ module OpenLanes
|
|
|
152
201
|
end
|
|
153
202
|
end
|
|
154
203
|
|
|
204
|
+
# One line per pull request a human has yet to act on, under the plan.
|
|
205
|
+
#
|
|
206
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
207
|
+
# @return [Array<String>]
|
|
208
|
+
def waiting_lines(subject)
|
|
209
|
+
Array(@waiting_on[subject.feature.ordinal.to_s]).map do |number|
|
|
210
|
+
(" " * title_column) + UI.paint("↳ waiting on you: PR ##{number}", :bright_yellow)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
155
214
|
# @param subject [OpenLanes::Plans::Subject]
|
|
156
215
|
# @return [Array<String>]
|
|
157
216
|
def violation_line(subject)
|
|
@@ -162,7 +221,7 @@ module OpenLanes
|
|
|
162
221
|
|
|
163
222
|
# @return [String]
|
|
164
223
|
def footer
|
|
165
|
-
counts = totals.map { |key, n| "#{Plans::STATUS_BY_KEY.fetch(key).emoji}
|
|
224
|
+
counts = totals.map { |key, n| "#{Plans::STATUS_BY_KEY.fetch(key).emoji} #{n}" }.join(" ")
|
|
166
225
|
lines = [" #{tree.subjects.size} #{tree.subjects.size == 1 ? "plan" : "plans"} #{counts}"]
|
|
167
226
|
|
|
168
227
|
unless inconsistent.empty?
|
|
@@ -15,9 +15,28 @@ module OpenLanes
|
|
|
15
15
|
|
|
16
16
|
# @param tree [OpenLanes::Plans::Tree]
|
|
17
17
|
# @return [OpenLanes::Presentation::Reporter]
|
|
18
|
-
def reporter(tree:) = Reporter.new(tree:)
|
|
18
|
+
def reporter(tree:) = Reporter.new(tree:, waiting_on: waiting_on(tree))
|
|
19
|
+
|
|
20
|
+
# What the engine's watch last saw a person owe, read only while that
|
|
21
|
+
# engine is up: once it stops, nobody is watching, and a pull request
|
|
22
|
+
# merged since would still be listed as waiting.
|
|
23
|
+
#
|
|
24
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
25
|
+
# @return [Hash{String => Array<Integer>}]
|
|
26
|
+
def waiting_on(tree)
|
|
27
|
+
state = Engine.state_file(tree).load
|
|
28
|
+
state.running? ? state.waiting_on : {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
32
|
+
# @param dry_run [Boolean]
|
|
33
|
+
# @return [OpenLanes::Presentation::PrChanges]
|
|
34
|
+
def pr_changes(changes, dry_run:) = PrChanges.new(changes, dry_run:)
|
|
19
35
|
|
|
20
36
|
# @return [OpenLanes::Presentation::Viewer]
|
|
21
37
|
def viewer = Viewer.new
|
|
38
|
+
|
|
39
|
+
# @return [OpenLanes::Presentation::ApiDocs] the openapi.yaml files plans carry
|
|
40
|
+
def api_docs = ApiDocs.new
|
|
22
41
|
end
|
|
23
42
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Setup
|
|
5
|
+
# One question asked of the machine, and its answer.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute name [String] what was checked, as the report labels it
|
|
8
|
+
# @!attribute ok [Boolean]
|
|
9
|
+
# @!attribute detail [String] what was found
|
|
10
|
+
# @!attribute fix [String, nil] the command that turns a ✗ into a ✓
|
|
11
|
+
Check = Data.define(:name, :ok, :detail, :fix) do
|
|
12
|
+
# @return [Boolean]
|
|
13
|
+
def ok? = ok == true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|