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,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Lifecycle
|
|
5
|
+
# `openlanes unblock NNN…` without the command: resolve the plans named,
|
|
6
|
+
# refuse the ones that cannot be drained, say what each is waiting on,
|
|
7
|
+
# hand the rest to the draining agent, and report what moved.
|
|
8
|
+
#
|
|
9
|
+
# Deliberately not part of `run`. ⭕ and 🛑 are {Plans::StateMachine::SETTLED},
|
|
10
|
+
# so the loop never offers a blocked plan to anybody, and that is the
|
|
11
|
+
# property that makes the state mean anything: the plan waits for a human,
|
|
12
|
+
# and no agent quietly decides otherwise. Answers arriving is not a fact
|
|
13
|
+
# the tool can observe, so a human typing the command *is* the signal.
|
|
14
|
+
class UnblockWorkflow
|
|
15
|
+
include UI
|
|
16
|
+
include Dry::Monads[:result]
|
|
17
|
+
|
|
18
|
+
# The agent that knows the shape of `blocked.md`.
|
|
19
|
+
DEFAULT_AGENT = "lando-broker"
|
|
20
|
+
|
|
21
|
+
# What each token turned out to be, for whoever has to fix it.
|
|
22
|
+
PROBLEMS = {
|
|
23
|
+
missing: "no plan of that number",
|
|
24
|
+
not_blocked: "no blocked.md, so there is nothing to drain"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
28
|
+
# @param agent [String] the name of the agent that drains
|
|
29
|
+
# @param root [String, nil] repository root (default: the .plans parent)
|
|
30
|
+
# @param dry_run [Boolean] invoke no agent, change nothing
|
|
31
|
+
# @param quiet [Boolean] draw nothing on STDERR
|
|
32
|
+
def initialize(tree:, agent: DEFAULT_AGENT, root: nil, dry_run: false, quiet: false)
|
|
33
|
+
@tree = tree
|
|
34
|
+
@agent = agent
|
|
35
|
+
@root = root
|
|
36
|
+
@dry_run = dry_run
|
|
37
|
+
@quiet = quiet
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The deliverable rows go to STDOUT and the progress to STDERR as the
|
|
41
|
+
# work happens; the answer is the exit status the command ends with.
|
|
42
|
+
#
|
|
43
|
+
# @param tokens [Array<String>] the plans named: NNN or NNN.MM
|
|
44
|
+
# @return [Dry::Monads::Result] Success(Integer) the exit status, or
|
|
45
|
+
# Failure({Refusal}) when no agent has the name given
|
|
46
|
+
def call(tokens)
|
|
47
|
+
agent = OpenLanes::Agents.registry.find(@agent) or return Failure(unknown_agent)
|
|
48
|
+
|
|
49
|
+
unblocker = Lifecycle.unblocker(tree: @tree,
|
|
50
|
+
agent:,
|
|
51
|
+
root: @root,
|
|
52
|
+
commit: !@dry_run,
|
|
53
|
+
executor: Execution.executor(root: @root || File.dirname(@tree.dir), dry_run: @dry_run))
|
|
54
|
+
targets = unblocker.resolve(tokens)
|
|
55
|
+
refused(targets)
|
|
56
|
+
subjects = targets.select(&:drainable?).map(&:subject)
|
|
57
|
+
return Success(worst(targets, [])) if subjects.empty?
|
|
58
|
+
|
|
59
|
+
credentials unless @dry_run || @quiet
|
|
60
|
+
preflight(subjects)
|
|
61
|
+
|
|
62
|
+
outcomes = Execution.on_interrupt { unblocker.call(subjects) }
|
|
63
|
+
outcomes.each { |outcome| report(outcome) }
|
|
64
|
+
footer(outcomes) unless @quiet
|
|
65
|
+
Success(worst(targets, outcomes))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# @return [OpenLanes::Refusal]
|
|
71
|
+
def unknown_agent
|
|
72
|
+
known = OpenLanes::Agents.registry.all.map(&:name).join(", ")
|
|
73
|
+
Refusal.new(message: "No agent called #{@agent}.\n\nKnown: #{known}", status: 65)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [void]
|
|
77
|
+
def credentials
|
|
78
|
+
notice = Execution.credentials_notice
|
|
79
|
+
warn(notice) if notice
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A number that names no folder, or a folder that was never stopped, is
|
|
83
|
+
# refused rather than skipped. Whoever typed this believes an answer has
|
|
84
|
+
# arrived; running against a folder nobody blocked and saying nothing is
|
|
85
|
+
# how you come back later to a plan nobody touched and no record of why.
|
|
86
|
+
#
|
|
87
|
+
# Every bad token is reported, not just the first, because stopping at
|
|
88
|
+
# the first one hides the rest of the answer.
|
|
89
|
+
#
|
|
90
|
+
# @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
|
|
91
|
+
# @return [void]
|
|
92
|
+
def refused(targets)
|
|
93
|
+
problems = targets.reject(&:drainable?)
|
|
94
|
+
return if problems.empty?
|
|
95
|
+
|
|
96
|
+
problems.each do |target|
|
|
97
|
+
state = target.subject ? target.subject.status.key : "unknown"
|
|
98
|
+
puts "#{target.token}\t#{state}\t0 open\t#{PROBLEMS.fetch(target.problem)}"
|
|
99
|
+
end
|
|
100
|
+
return if @quiet
|
|
101
|
+
|
|
102
|
+
error("#{problems.size} of the plans named cannot be drained:\n\n" +
|
|
103
|
+
problems.map { |t| " #{t.token} — #{PROBLEMS.fetch(t.problem)}" }.join("\n") +
|
|
104
|
+
"\n\nThe tree holds: #{@tree.ordinals.join(", ")}")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# What each folder is waiting on, said BEFORE the agent is invoked.
|
|
108
|
+
#
|
|
109
|
+
# A real run hands the folder to an agent that can take a quarter of an
|
|
110
|
+
# hour, and until it came back the terminal used to show nothing at
|
|
111
|
+
# all: a run that had found nothing to do and a run still working
|
|
112
|
+
# looked exactly alike.
|
|
113
|
+
#
|
|
114
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>]
|
|
115
|
+
# @return [void]
|
|
116
|
+
def preflight(subjects)
|
|
117
|
+
return if @quiet
|
|
118
|
+
|
|
119
|
+
subjects.each do |subject|
|
|
120
|
+
questions = Unblocker.questions(subject)
|
|
121
|
+
answered = questions.count(&:answered)
|
|
122
|
+
say("#{paint(subject.feature.ordinal.to_s, :bright_black)} #{subject.status.emoji} " \
|
|
123
|
+
"#{subject.feature.title} — #{summary(questions.size, answered)}")
|
|
124
|
+
detail(subject, questions)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param open [Integer]
|
|
129
|
+
# @param answered [Integer]
|
|
130
|
+
# @return [String]
|
|
131
|
+
def summary(open, answered)
|
|
132
|
+
return "nothing open" if open.zero?
|
|
133
|
+
|
|
134
|
+
"#{open} open, #{answered.zero? ? "none answered yet" : "#{answered} with an answer waiting"}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
138
|
+
# @param questions [Array<OpenLanes::Lifecycle::Unblocker::Question>]
|
|
139
|
+
# @return [void]
|
|
140
|
+
def detail(subject, questions)
|
|
141
|
+
if subject.unreadable_block?
|
|
142
|
+
say(" #{paint("blocked.md names no `## B<n>` question, so nothing here can be drained", :red)}",
|
|
143
|
+
bullet: " ")
|
|
144
|
+
say(" #{paint("Number each open question `## B1`, `## B2`, and each answer `## A1`, `## A2`.", :yellow)}",
|
|
145
|
+
bullet: " ")
|
|
146
|
+
elsif questions.empty?
|
|
147
|
+
say(" #{paint("nothing left open", :green)}", bullet: " ")
|
|
148
|
+
else
|
|
149
|
+
questions.each { |question| say(" #{paint(question.to_s, :yellow)}", bullet: " ") }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# One plan, after the fact: the deliverable line on STDOUT, and what
|
|
154
|
+
# changed on STDERR.
|
|
155
|
+
#
|
|
156
|
+
# @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
|
|
157
|
+
# @return [void]
|
|
158
|
+
def report(outcome)
|
|
159
|
+
moved = movement(outcome)
|
|
160
|
+
puts "#{outcome.ordinal}\t#{outcome.ok ? outcome.subject.status.key : "failed"}\t" \
|
|
161
|
+
"#{outcome.after.size} open\t#{moved}\t#{outcome.note}"
|
|
162
|
+
# A dry run changed nothing, so the preflight above it is still the
|
|
163
|
+
# whole truth. Printing the same lines again reads as a second pass
|
|
164
|
+
# that found the same thing, which is not what happened.
|
|
165
|
+
return if @quiet || @dry_run
|
|
166
|
+
|
|
167
|
+
say("#{paint(outcome.ordinal.to_s, :bright_black)} #{outcome.subject.status.emoji} " \
|
|
168
|
+
"#{outcome.subject.feature.title} — #{paint(moved, outcome.ok ? :green : :red)}")
|
|
169
|
+
detail(outcome.subject, outcome.after)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# What the file says happened, rather than what the agent claims. The
|
|
173
|
+
# questions are counted off disk on both sides of the run.
|
|
174
|
+
#
|
|
175
|
+
# @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
|
|
176
|
+
# @return [String]
|
|
177
|
+
def movement(outcome)
|
|
178
|
+
return "failed" unless outcome.ok
|
|
179
|
+
return "not attempted" if @dry_run
|
|
180
|
+
return "blocked.md retired" if outcome.cleared?
|
|
181
|
+
return "nothing folded" if outcome.folded.empty?
|
|
182
|
+
|
|
183
|
+
"folded #{outcome.folded.map { |n| "B#{n}" }.join(", ")}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
|
|
187
|
+
# @return [void]
|
|
188
|
+
def footer(outcomes)
|
|
189
|
+
if @dry_run
|
|
190
|
+
warn("Dry run: no agent was invoked, and #{outcomes.size} " \
|
|
191
|
+
"plan#{"s" unless outcomes.size == 1} #{outcomes.size == 1 ? "is" : "are"} unchanged.\n" \
|
|
192
|
+
"Re-run without --dry-run to fold in whatever has been answered.")
|
|
193
|
+
return
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
failed = outcomes.reject(&:ok)
|
|
197
|
+
return error(failed.map { |o| "#{o.ordinal}: #{o.note}" }.join("\n")) unless failed.empty?
|
|
198
|
+
|
|
199
|
+
success(tally(outcomes))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
|
|
203
|
+
# @return [String]
|
|
204
|
+
def tally(outcomes)
|
|
205
|
+
cleared = outcomes.count(&:cleared?)
|
|
206
|
+
folded = outcomes.sum { |o| o.folded.size }
|
|
207
|
+
waiting = outcomes.sum { |o| o.after.size }
|
|
208
|
+
text = "#{folded} question#{"s" unless folded == 1} folded in · " \
|
|
209
|
+
"#{cleared} plan#{"s" unless cleared == 1} out of the block · " \
|
|
210
|
+
"#{waiting} still waiting on a human."
|
|
211
|
+
return text unless folded.zero?
|
|
212
|
+
|
|
213
|
+
"#{text}\n\nNothing moved. An answer has to be written into blocked.md as its own " \
|
|
214
|
+
"`## A<n>` section, answering the `## B<n>` of the same number, before " \
|
|
215
|
+
"there is anything to fold."
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The most serious thing that happened, as an exit status. A tree that
|
|
219
|
+
# was partly drained still exits non-zero when part of it could not be.
|
|
220
|
+
#
|
|
221
|
+
# @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
|
|
222
|
+
# @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
|
|
223
|
+
# @return [Integer]
|
|
224
|
+
def worst(targets, outcomes)
|
|
225
|
+
problems = targets.map(&:problem)
|
|
226
|
+
return 66 if problems.include?(:missing)
|
|
227
|
+
return 65 if problems.include?(:not_blocked)
|
|
228
|
+
return 1 unless outcomes.all?(&:ok)
|
|
229
|
+
|
|
230
|
+
0
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -118,7 +118,7 @@ module OpenLanes
|
|
|
118
118
|
# is as much a thing the user needs told about as a folder that drained, and
|
|
119
119
|
# stopping at the first bad one hides the rest of the report.
|
|
120
120
|
#
|
|
121
|
-
# The gate is `blocked.md` itself, not the folder's emoji.
|
|
121
|
+
# The gate is `blocked.md` itself, not the folder's emoji. ⭕ is *derived*
|
|
122
122
|
# from the file, so gating on the emoji made the tool circular: a
|
|
123
123
|
# `blocked.md` whose questions are not numbered `## B<n>` never earns the
|
|
124
124
|
# emoji, so `unblock` refused it, so the file could never drain and the
|
|
@@ -187,11 +187,85 @@ module OpenLanes
|
|
|
187
187
|
# @param progress [OpenLanes::UI::Line]
|
|
188
188
|
# @return [Array(Boolean, String)]
|
|
189
189
|
def invoke(subject, progress)
|
|
190
|
-
|
|
190
|
+
journal_answers(subject)
|
|
191
|
+
forward = progress.to_proc
|
|
192
|
+
kept = { written: 0, number: nil }
|
|
193
|
+
@executor.call(agent, subject, root:) do |update|
|
|
194
|
+
record_journal(subject, update, kept)
|
|
195
|
+
forward.call(update)
|
|
196
|
+
end
|
|
191
197
|
rescue StandardError => e
|
|
192
198
|
[false, "#{e.class}: #{e.message.lines.first.to_s.strip}"]
|
|
193
199
|
end
|
|
194
200
|
|
|
201
|
+
# The broker's own journal lines (M2), written as they arrive, the way
|
|
202
|
+
# {Engine::Dispatcher} writes every other agent's: its prompt asks it to
|
|
203
|
+
# keep one like any agent's, and neither the run loop nor `openlanes
|
|
204
|
+
# unblock` hands its progress to the dispatcher. As they arrive, so a
|
|
205
|
+
# broker that fails or is killed keeps what it said.
|
|
206
|
+
#
|
|
207
|
+
# A journal that cannot be written costs the record, never the drain;
|
|
208
|
+
# the lines stay unwritten for the next update to try again.
|
|
209
|
+
#
|
|
210
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
211
|
+
# @param update [Object] whatever the executor reports
|
|
212
|
+
# @param kept [Hash] how many lines are written, and the journal round
|
|
213
|
+
# @return [void]
|
|
214
|
+
def record_journal(subject, update, kept)
|
|
215
|
+
lines = update.respond_to?(:journal) ? Array(update.journal) : []
|
|
216
|
+
return if !applying? || lines.size <= kept[:written]
|
|
217
|
+
|
|
218
|
+
entries, rejected = lines[kept[:written]..].partition { it.is_a?(Plans::Journal::Entry) }
|
|
219
|
+
unless entries.empty?
|
|
220
|
+
kept[:number], refused = Plans::PlanState.for(subject.feature.path)
|
|
221
|
+
.journal!(agent: agent.name,
|
|
222
|
+
entries:,
|
|
223
|
+
number: kept[:number],
|
|
224
|
+
state: subject.feature.status)
|
|
225
|
+
rejected += refused
|
|
226
|
+
end
|
|
227
|
+
kept[:written] = lines.size
|
|
228
|
+
rejected.each { UI.log("journal line ignored, #{it.reason}: #{it.line[0, 160]}", level: :warn, **log_fields(subject)) }
|
|
229
|
+
rescue SystemCallError, OpenLanes::Error => e
|
|
230
|
+
UI.log("journal not written yet: #{e.message}", level: :warn, **log_fields(subject))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Every answered question goes into the journal of the agent that asked
|
|
234
|
+
# it (M3), B1 with A1, before the broker folds it in and deletes it:
|
|
235
|
+
# afterwards the words are gone from `blocked.md`, and the asker's next
|
|
236
|
+
# round would know only that the plan was unblocked, not what it was
|
|
237
|
+
# told. Here, rather than where the page writes an answer, because an
|
|
238
|
+
# answer typed into the file by hand is drained the same way.
|
|
239
|
+
#
|
|
240
|
+
# A journal that cannot be written costs the record, never the drain.
|
|
241
|
+
#
|
|
242
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
243
|
+
# @return [void]
|
|
244
|
+
def journal_answers(subject)
|
|
245
|
+
return unless applying?
|
|
246
|
+
|
|
247
|
+
state = Plans::PlanState.for(subject.feature.path)
|
|
248
|
+
asker = state.blocked_by or return
|
|
249
|
+
text = subject.read("blocked.md").to_s
|
|
250
|
+
bodies = Plans::BlockedFile.bodies(text)
|
|
251
|
+
answers = Plans::BlockedFile.answers(text).transform_values { plain(it) }
|
|
252
|
+
self.class.questions(subject).each do |question|
|
|
253
|
+
answer = answers[question.number]
|
|
254
|
+
next if answer.nil? || answer.empty?
|
|
255
|
+
|
|
256
|
+
state.answer!(agent: asker,
|
|
257
|
+
ref: "B#{question.number}",
|
|
258
|
+
question: plain("#{question.heading} #{bodies[question.number]}"),
|
|
259
|
+
answer:)
|
|
260
|
+
end
|
|
261
|
+
rescue SystemCallError, OpenLanes::Error => e
|
|
262
|
+
UI.log("journal not written: #{e.message}", level: :warn, **log_fields(subject))
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @param text [String] a block's body
|
|
266
|
+
# @return [String] one line, without the page's command markers
|
|
267
|
+
def plain(text) = Plans::Journal.clip(text.gsub(/<!--.*?-->/m, "").split.join(" "))
|
|
268
|
+
|
|
195
269
|
# The same pass the run loop makes after every round, for the same reason:
|
|
196
270
|
# a drained folder is only *named* differently once something reads the
|
|
197
271
|
# file the agent just deleted.
|
data/lib/openlanes/lifecycle.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module OpenLanes
|
|
4
4
|
# Operations that change which plans exist or what they are called:
|
|
5
5
|
# minting a folder, drafting its brief, adopting an orphan pull request,
|
|
6
|
-
# reconciling names with contents,
|
|
6
|
+
# reconciling names with contents, draining answered questions, and
|
|
7
|
+
# aging the ones nobody answers.
|
|
7
8
|
module Lifecycle
|
|
8
9
|
module_function
|
|
9
10
|
|
|
@@ -12,6 +13,10 @@ module OpenLanes
|
|
|
12
13
|
# @return [Dry::Monads::Result] Success(path) or Failure(message)
|
|
13
14
|
def create(dir:, **) = Creator.new(dir:).create(**)
|
|
14
15
|
|
|
16
|
+
# @param options [Hash] passed to {CreateWorkflow#initialize}
|
|
17
|
+
# @return [OpenLanes::Lifecycle::CreateWorkflow] `openlanes create`, without the command
|
|
18
|
+
def create_plan(**) = CreateWorkflow.new(**)
|
|
19
|
+
|
|
15
20
|
# @param tree [OpenLanes::Plans::Tree]
|
|
16
21
|
# @return [OpenLanes::Lifecycle::Resync::Dirs]
|
|
17
22
|
def resync_dirs(tree:, **) = Resync::Dirs.new(tree:, **)
|
|
@@ -19,10 +24,41 @@ module OpenLanes
|
|
|
19
24
|
# @return [OpenLanes::Lifecycle::Resync::Prs]
|
|
20
25
|
def resync_prs(**) = Resync::Prs.new(**)
|
|
21
26
|
|
|
27
|
+
# `resync prs` as one step: retitle (and adopt) on GitHub, or say why not.
|
|
28
|
+
#
|
|
29
|
+
# @param commit [Boolean] false previews and changes nothing
|
|
30
|
+
# @param options [Hash] passed to {Resync::Prs#initialize}
|
|
31
|
+
# @return [Dry::Monads::Result] Success(Array<Resync::Prs::Change>), or
|
|
32
|
+
# Failure({Refusal}) with status 69 when GitHub refused, or 73 when an
|
|
33
|
+
# orphan would be adopted in a linked worktree, where no plan is minted
|
|
34
|
+
def retitle_prs(commit:, **)
|
|
35
|
+
Dry::Monads::Result::Success.new(Resync::Prs.new(**).call(commit:))
|
|
36
|
+
rescue Allocator::Refused => e
|
|
37
|
+
Dry::Monads::Result::Failure.new(Refusal.new(message: e.message, status: 73))
|
|
38
|
+
rescue OpenLanes::Error => e
|
|
39
|
+
Dry::Monads::Result::Failure.new(Refusal.new(message: e.message, status: 69))
|
|
40
|
+
end
|
|
41
|
+
|
|
22
42
|
# @return [OpenLanes::Lifecycle::Brief]
|
|
23
43
|
def brief(**) = Brief.new(**)
|
|
24
44
|
|
|
45
|
+
# @param options [Hash] passed to {UnblockWorkflow#initialize}
|
|
46
|
+
# @return [OpenLanes::Lifecycle::UnblockWorkflow] `openlanes unblock`, without the command
|
|
47
|
+
def unblock(**) = UnblockWorkflow.new(**)
|
|
48
|
+
|
|
25
49
|
# @return [OpenLanes::Lifecycle::Unblocker]
|
|
26
50
|
def unblocker(**) = Unblocker.new(**)
|
|
51
|
+
|
|
52
|
+
# Bomb or defer the blocked plans past their limits (plan 009).
|
|
53
|
+
#
|
|
54
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
55
|
+
# @param subjects [Array<OpenLanes::Plans::Subject>, nil] nil for every plan
|
|
56
|
+
# @param since [Date, nil] `plans.aging_since`
|
|
57
|
+
# @param now [Time]
|
|
58
|
+
# @param commit [Boolean] false reports and changes nothing
|
|
59
|
+
# @return [OpenLanes::Lifecycle::Aging::Result]
|
|
60
|
+
def age(tree:, subjects: nil, since: nil, now: Time.now, commit: true)
|
|
61
|
+
Aging.new(tree:, since:, now:).call(subjects:, commit:)
|
|
62
|
+
end
|
|
27
63
|
end
|
|
28
64
|
end
|
|
@@ -12,7 +12,7 @@ module OpenLanes
|
|
|
12
12
|
# contract, and the name we would prefer if the team happens to have one.
|
|
13
13
|
# {Push} resolves the name first and falls back to the type.
|
|
14
14
|
#
|
|
15
|
-
# Several states have no Linear equivalent at all —
|
|
15
|
+
# Several states have no Linear equivalent at all — ⭕ Technical Block is
|
|
16
16
|
# a reason, not a position in a workflow — so they map to the nearest
|
|
17
17
|
# position and carry a label saying which reason.
|
|
18
18
|
#
|
|
@@ -50,11 +50,14 @@ module OpenLanes
|
|
|
50
50
|
building_ui: Placement.new(type: "started", name: "In Progress", labels: %w[frontend]),
|
|
51
51
|
ready_for_review: Placement.new(type: "started", name: "In Review", labels: []),
|
|
52
52
|
in_review: Placement.new(type: "started", name: "In Review", labels: []),
|
|
53
|
+
verifying: Placement.new(type: "started", name: "In Review", labels: []),
|
|
54
|
+
completed: Placement.new(type: "started", name: "In Review", labels: %w[ready-to-merge]),
|
|
53
55
|
rejected: Placement.new(type: "started", name: "In Review", labels: %w[changes-requested]),
|
|
54
56
|
approved: Placement.new(type: "completed", name: "Done", labels: []),
|
|
55
57
|
deployed: Placement.new(type: "completed", name: "Done", labels: %w[deployed]),
|
|
56
58
|
blocked: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked]),
|
|
57
59
|
product_blocked: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked-on-product]),
|
|
60
|
+
bomb: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked bomb]),
|
|
58
61
|
deferred: Placement.new(type: "backlog", name: "Backlog", labels: %w[deferred]),
|
|
59
62
|
retroactive: Placement.new(type: "completed", name: "Done", labels: %w[retroactive]),
|
|
60
63
|
discarded: Placement.new(type: "canceled", name: "Canceled", labels: [])
|
|
@@ -55,7 +55,7 @@ module OpenLanes
|
|
|
55
55
|
# the moment the work moves.
|
|
56
56
|
GLYPH_NOTE = /\s*\((?:[^\w\s(][^)]*)\)\s*\z/
|
|
57
57
|
|
|
58
|
-
# The same thing without the brackets: `… over the law plane
|
|
58
|
+
# The same thing without the brackets: `… over the law plane 💤 deferred`.
|
|
59
59
|
TRAILING_GLYPH = /\s*[\u{2190}-\u{2BFF}\u{1F000}-\u{1FAFF}][^\n]*\z/
|
|
60
60
|
|
|
61
61
|
# A plan number the author typed into the heading as well. The issue for
|