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
|
@@ -31,26 +31,49 @@ module OpenLanes
|
|
|
31
31
|
|
|
32
32
|
# Command kind to the method that applies it.
|
|
33
33
|
HANDLERS = {
|
|
34
|
-
"answer"
|
|
35
|
-
"hold_blocked"
|
|
36
|
-
"release_hold"
|
|
37
|
-
"resume"
|
|
38
|
-
"discard"
|
|
39
|
-
"message"
|
|
40
|
-
"hold"
|
|
41
|
-
"wrap_up"
|
|
42
|
-
"compact"
|
|
43
|
-
"stop"
|
|
44
|
-
"stop_all"
|
|
45
|
-
"set_model"
|
|
46
|
-
"switch_model"
|
|
47
|
-
"add_plan"
|
|
48
|
-
"create_plans"
|
|
34
|
+
"answer" => :answer,
|
|
35
|
+
"hold_blocked" => :hold_blocked,
|
|
36
|
+
"release_hold" => :release_hold,
|
|
37
|
+
"resume" => :resume,
|
|
38
|
+
"discard" => :discard,
|
|
39
|
+
"message" => :message,
|
|
40
|
+
"hold" => :signal,
|
|
41
|
+
"wrap_up" => :signal,
|
|
42
|
+
"compact" => :compact,
|
|
43
|
+
"stop" => :signal,
|
|
44
|
+
"stop_all" => :stop_all,
|
|
45
|
+
"set_model" => :set_model,
|
|
46
|
+
"switch_model" => :switch_model,
|
|
47
|
+
"add_plan" => :add_plan,
|
|
48
|
+
"create_plans" => :create_plans,
|
|
49
|
+
"respond" => :respond,
|
|
50
|
+
"pause_discuss" => :pause_discuss,
|
|
51
|
+
"resume_anyway" => :resume_anyway,
|
|
52
|
+
"stop_plan" => :stop_plan,
|
|
53
|
+
"restart_plan" => :restart_plan,
|
|
54
|
+
"set_concurrency" => :set_concurrency
|
|
49
55
|
}.freeze
|
|
50
56
|
|
|
57
|
+
# How long "Resume anyway" lifts a 💣's halt.
|
|
58
|
+
RESUME_FOR = 24 * 3600
|
|
59
|
+
|
|
51
60
|
# Plans a person may not add to a run: there is nothing left to do.
|
|
52
61
|
FINISHED = Snapshot::FINISHED
|
|
53
62
|
|
|
63
|
+
# The copilot turn that answers a slash command: `"<id>.1"`, the turn
|
|
64
|
+
# the page's "reading" waits for. {Web::Demo} writes the same one, so
|
|
65
|
+
# the demo's copilot ends its wait as the engine's does.
|
|
66
|
+
#
|
|
67
|
+
# @param reply [OpenLanes::Live::Reply]
|
|
68
|
+
# @return [OpenLanes::Live::Copilot::Turn]
|
|
69
|
+
def self.echo_turn(reply)
|
|
70
|
+
Copilot::Turn.new(id: "#{reply.id}.1",
|
|
71
|
+
from: "copilot",
|
|
72
|
+
kind: "text",
|
|
73
|
+
text: [reply.status, reply.reason].compact.join(": "),
|
|
74
|
+
at: reply.at)
|
|
75
|
+
end
|
|
76
|
+
|
|
54
77
|
# @param store [OpenLanes::Live::Store, OpenLanes::Live::MemoryStore]
|
|
55
78
|
# @param keys [OpenLanes::Live::Keys]
|
|
56
79
|
# @param node [OpenLanes::Live::Node]
|
|
@@ -106,9 +129,19 @@ module OpenLanes
|
|
|
106
129
|
@ready = true
|
|
107
130
|
end
|
|
108
131
|
|
|
132
|
+
# A group that went with a Redis restarted empty is made again, and
|
|
133
|
+
# the read tried once more; that it vanished once is no reason to stop
|
|
134
|
+
# the engine, as it did (#59).
|
|
135
|
+
#
|
|
109
136
|
# @param id [String] "0" for this consumer's pending, ">" for new
|
|
110
137
|
# @return [Array<OpenLanes::Live::Store::Entry>]
|
|
111
|
-
def read(id)
|
|
138
|
+
def read(id)
|
|
139
|
+
@store.xreadgroup(@keys.commands, Keys::GROUP, @node.name, id:, count: BATCH)
|
|
140
|
+
rescue GroupMissing
|
|
141
|
+
@ready = false
|
|
142
|
+
prepare
|
|
143
|
+
@store.xreadgroup(@keys.commands, Keys::GROUP, @node.name, id:, count: BATCH)
|
|
144
|
+
end
|
|
112
145
|
|
|
113
146
|
# Answer the commands whose work finished since the last tick. A
|
|
114
147
|
# command still at work is neither answered nor acknowledged, so an
|
|
@@ -131,8 +164,11 @@ module OpenLanes
|
|
|
131
164
|
return answer_again(entry, first)
|
|
132
165
|
end
|
|
133
166
|
|
|
167
|
+
# Said on the terminal and in the log: what a person asked for from
|
|
168
|
+
# the page is part of the run's story, beside what the agents did.
|
|
169
|
+
UI.log("from the page: #{command.kind.tr("_", " ")}", **{ plan: command.plan, agent: command.agent }.compact)
|
|
134
170
|
status, reason = apply(command, dispatcher)
|
|
135
|
-
return reply(entry, command.id, status, reason) unless status == :pending
|
|
171
|
+
return reply(entry, command.id, status, reason, echo: command.payload["source"] == "copilot") unless status == :pending
|
|
136
172
|
|
|
137
173
|
@pending[entry.id] = [entry, command.id, reason]
|
|
138
174
|
nil
|
|
@@ -154,25 +190,36 @@ module OpenLanes
|
|
|
154
190
|
["refused", "#{command.kind} failed: #{e.message}"]
|
|
155
191
|
end
|
|
156
192
|
|
|
193
|
+
# A command the copilot sent (a slash command, plan 020.00) is also
|
|
194
|
+
# answered in the conversation, as turn `"<id>.1"`: that is the turn the
|
|
195
|
+
# page's "reading" waits for, and the conversation keeps the outcome.
|
|
196
|
+
# It is written in the same `MULTI`, so a redelivery, which
|
|
197
|
+
# {#answer_again} handles, can never write it twice.
|
|
198
|
+
#
|
|
199
|
+
# @param echo [Boolean] whether to answer in the copilot stream too
|
|
157
200
|
# @return [OpenLanes::Live::Reply]
|
|
158
|
-
def reply(entry, id, status, reason)
|
|
201
|
+
def reply(entry, id, status, reason, echo: false)
|
|
159
202
|
reply = Reply.new(id:, status:, reason:, at: @clock.call)
|
|
160
203
|
json = reply.to_json
|
|
161
|
-
|
|
204
|
+
turn = Commands.echo_turn(reply) if echo
|
|
205
|
+
EventBus.for(@store).transaction do |tx|
|
|
162
206
|
tx.hset(@keys.done, id, json)
|
|
163
|
-
tx.
|
|
207
|
+
tx.append(@keys.replies, { json: }, maxlen: Keys::REPLIES_LENGTH)
|
|
208
|
+
tx.append(@keys.copilot, { json: turn.to_json }, maxlen: Keys::COPILOT_LENGTH) if turn
|
|
164
209
|
tx.xack(@keys.commands, Keys::GROUP, entry.id)
|
|
165
210
|
@record&.write(tx)
|
|
166
211
|
end
|
|
212
|
+
News.publish(store: @store, keys: @keys, kind: "reply")
|
|
167
213
|
reply
|
|
168
214
|
end
|
|
169
215
|
|
|
170
216
|
# @return [OpenLanes::Live::Reply] the first reply, sent again
|
|
171
217
|
def answer_again(entry, first)
|
|
172
|
-
@store.
|
|
173
|
-
tx.
|
|
218
|
+
EventBus.for(@store).transaction do |tx|
|
|
219
|
+
tx.append(@keys.replies, { json: first }, maxlen: Keys::REPLIES_LENGTH)
|
|
174
220
|
tx.xack(@keys.commands, Keys::GROUP, entry.id)
|
|
175
221
|
end
|
|
222
|
+
News.publish(store: @store, keys: @keys, kind: "reply")
|
|
176
223
|
data = JSON.parse(first)
|
|
177
224
|
Reply.new(id: data["id"], status: data["status"], reason: data["reason"], at: Time.iso8601(data["at"]))
|
|
178
225
|
end
|
|
@@ -214,17 +261,25 @@ module OpenLanes
|
|
|
214
261
|
|
|
215
262
|
# `## A<n>` under the question, signed, for `lando-broker` to fold in
|
|
216
263
|
# (W10). The dispatcher hands the plan to the broker once every
|
|
217
|
-
# question has an answer, and the plan leaves
|
|
264
|
+
# question has an answer, and the plan leaves ⭕ when the file is
|
|
218
265
|
# drained. The answer carries the command's id in a comment, so a
|
|
219
266
|
# command applied again after a crash finds it and writes nothing.
|
|
220
267
|
#
|
|
221
268
|
# @return [String, nil]
|
|
222
269
|
def answer(command, dispatcher)
|
|
270
|
+
return extension(command, dispatcher) if command["question"].to_s.match?(/\AX\d+\z/i)
|
|
271
|
+
return quality(command, dispatcher) if command["question"].to_s.casecmp?(Engine::QualityQuestion::NAME)
|
|
272
|
+
|
|
223
273
|
subject, question = question!(command, dispatcher)
|
|
224
|
-
|
|
225
|
-
return nil if File.read(path).include?(marker(command))
|
|
274
|
+
return nil if File.read(File.join(subject.feature.path, "blocked.md")).include?(marker(command))
|
|
226
275
|
return "B#{question.number} already has an answer waiting" if question.answered
|
|
227
276
|
|
|
277
|
+
write_answer(subject, question, command, dispatcher)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @return [nil]
|
|
281
|
+
def write_answer(subject, question, command, dispatcher)
|
|
282
|
+
path = File.join(subject.feature.path, "blocked.md")
|
|
228
283
|
File.open(path, "a") do |file|
|
|
229
284
|
file.write("\n## A#{question.number}\n\n#{command["text"]}\n\n" \
|
|
230
285
|
"Answered by #{@node.user} on #{@clock.call.strftime("%Y-%m-%d")}\n#{marker(command)}\n")
|
|
@@ -234,6 +289,83 @@ module OpenLanes
|
|
|
234
289
|
nil
|
|
235
290
|
end
|
|
236
291
|
|
|
292
|
+
# An answer to the harness's request for more time or rounds (E1).
|
|
293
|
+
# The page sends the chosen option, "b) Extend Time by 10 minutes";
|
|
294
|
+
# only its letter counts, so there is nothing to parse but that.
|
|
295
|
+
#
|
|
296
|
+
# @return [String]
|
|
297
|
+
def extension(command, dispatcher)
|
|
298
|
+
subject = subject!(command, dispatcher)
|
|
299
|
+
number = command["question"][1..].to_i
|
|
300
|
+
letter = command["text"].to_s.strip[/\A([a-c])\)?(?:\s|\z)/i, 1] or
|
|
301
|
+
raise Refusal, "X#{number} takes a), b) or c), not #{command["text"].to_s.strip[0, 40].inspect}"
|
|
302
|
+
settled = dispatcher.answer_extension(subject, number, letter.downcase, by: @node.user) or
|
|
303
|
+
raise Refusal, "#{command.plan} has no open request X#{number}"
|
|
304
|
+
settled["grant"] == "live" ? "#{settled["agent"]} has #{settled["seconds"] / 60} more minutes" : "#{settled["agent"]} gets one more round"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# R4's answer: a) high or b) mvp, written into `.openlanes.yml` (U3).
|
|
308
|
+
# Only the letter counts, as for {#extension}.
|
|
309
|
+
#
|
|
310
|
+
# @return [String]
|
|
311
|
+
def quality(command, dispatcher)
|
|
312
|
+
subject = subject!(command, dispatcher)
|
|
313
|
+
name = Engine::QualityQuestion::NAME
|
|
314
|
+
letter = command["text"].to_s.strip[/\A([ab])\)?(?:\s|\z)/i, 1] or
|
|
315
|
+
raise Refusal, "#{name} takes a) or b), not #{command["text"].to_s.strip[0, 40].inspect}"
|
|
316
|
+
chosen = Engine::QualityQuestion.answer!(subject.feature.path, letter, by: @node.user, root: File.dirname(dispatcher.runner.tree.dir)) or
|
|
317
|
+
raise Refusal, "#{command.plan} has no open question #{name}"
|
|
318
|
+
dispatcher.wake!
|
|
319
|
+
"desired_codebase_quality: #{chosen} in #{Config::FILE}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ---- bombs ------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
# The 💣 notice's answer box (plan 009): the first open question with
|
|
325
|
+
# no answer yet gets this one, in exactly the form {#answer} writes,
|
|
326
|
+
# so the drain that clears a ⭕ clears the 💣 too.
|
|
327
|
+
#
|
|
328
|
+
# @return [String, nil]
|
|
329
|
+
def respond(command, dispatcher)
|
|
330
|
+
subject = bomb!(command, dispatcher)
|
|
331
|
+
return nil if File.read(File.join(subject.feature.path, "blocked.md")).include?(marker(command))
|
|
332
|
+
|
|
333
|
+
question = Lifecycle::Unblocker.questions(subject).reject(&:answered).first or
|
|
334
|
+
raise Refusal, "every question #{command.plan} asks already has an answer waiting"
|
|
335
|
+
write_answer(subject, question, command, dispatcher)
|
|
336
|
+
"answered B#{question.number}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Pause and Discuss (N10): every agent on this machine holds, and the
|
|
340
|
+
# page opens the copilot on the plan. No file changes.
|
|
341
|
+
#
|
|
342
|
+
# @return [String]
|
|
343
|
+
def pause_discuss(command, dispatcher)
|
|
344
|
+
bomb!(command, dispatcher)
|
|
345
|
+
held = dispatcher.running.map(&:key).select { dispatcher.signal(it, Execution::Control::HOLD) }
|
|
346
|
+
held.each { @run_state.hold_car(it, held: true) }
|
|
347
|
+
"held #{held.size} agent#{"s" unless held.size == 1}; discuss #{command.plan} in the copilot"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Resume anyway for {RESUME_FOR}: the halt this 💣 causes lapses on its
|
|
351
|
+
# own, with no config edit to undo.
|
|
352
|
+
#
|
|
353
|
+
# @return [String]
|
|
354
|
+
def resume_anyway(command, dispatcher)
|
|
355
|
+
subject = bomb!(command, dispatcher)
|
|
356
|
+
till = Plans::PlanState.for(subject.feature.path).resume_anyway!(till: @clock.call + RESUME_FOR)
|
|
357
|
+
dispatcher.wake!
|
|
358
|
+
"#{command.plan} halts nothing until #{till.utc.iso8601}"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# @return [OpenLanes::Plans::Subject] the plan, refused unless it is 💣
|
|
362
|
+
def bomb!(command, dispatcher)
|
|
363
|
+
subject = subject!(command, dispatcher)
|
|
364
|
+
raise Refusal, "#{command.plan} is #{subject.status}, not a 💣" unless subject.status.key == :bomb
|
|
365
|
+
|
|
366
|
+
subject
|
|
367
|
+
end
|
|
368
|
+
|
|
237
369
|
# @return [nil]
|
|
238
370
|
def hold_blocked(command, dispatcher) = hold_question(command, dispatcher, held: true)
|
|
239
371
|
|
|
@@ -414,7 +546,7 @@ module OpenLanes
|
|
|
414
546
|
nil
|
|
415
547
|
end
|
|
416
548
|
|
|
417
|
-
# The idea becomes a
|
|
549
|
+
# The idea becomes a ⚪ plan whose spec.md opens with it, then joins the run.
|
|
418
550
|
#
|
|
419
551
|
# @return [String]
|
|
420
552
|
def create_plans(command, dispatcher)
|
|
@@ -424,15 +556,15 @@ module OpenLanes
|
|
|
424
556
|
return created(made.feature, dispatcher)
|
|
425
557
|
end
|
|
426
558
|
|
|
427
|
-
path = Lifecycle.create(dir: tree.dir, words:
|
|
559
|
+
path = Lifecycle.create(dir: tree.dir, words: Lifecycle::Creator.topic(idea)).either(
|
|
428
560
|
->(created) { created },
|
|
429
561
|
->(problem) { raise Refusal, "could not create the plan: #{problem}" }
|
|
430
562
|
)
|
|
431
563
|
feature = Plans::Feature.parse(path)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
564
|
+
seed = [idea, decisions(command.payload["decisions"]), marker(command)].compact.join("\n\n")
|
|
565
|
+
brief = Lifecycle.brief(path:, title: feature.title, root: File.dirname(tree.dir), seed:)
|
|
566
|
+
brief.write_scaffold!
|
|
567
|
+
brief.stamp!(idea_frontmatter(command.payload["frontmatter"]))
|
|
436
568
|
created(feature, dispatcher)
|
|
437
569
|
end
|
|
438
570
|
|
|
@@ -459,6 +591,22 @@ module OpenLanes
|
|
|
459
591
|
end
|
|
460
592
|
end
|
|
461
593
|
|
|
594
|
+
# The interview's answers (M6) as a `## Decisions` section of the
|
|
595
|
+
# spec, so the agents downstream find them made instead of blocking to
|
|
596
|
+
# ask. A skipped interview sends none, and the spec is as it was.
|
|
597
|
+
#
|
|
598
|
+
# @param list [Array<Hash>, nil] `payload.decisions`: `question`, `answer`
|
|
599
|
+
# @return [String, nil]
|
|
600
|
+
def decisions(list)
|
|
601
|
+
rows = Array(list).filter_map do |entry|
|
|
602
|
+
next unless entry.is_a?(Hash)
|
|
603
|
+
|
|
604
|
+
question, answer = entry.transform_keys(&:to_s).values_at("question", "answer").map { it.to_s.gsub(/\s+/, " ").strip }
|
|
605
|
+
"- **#{question}** #{answer}" unless question.empty? || answer.empty?
|
|
606
|
+
end
|
|
607
|
+
"## Decisions\n\n#{rows.join("\n")}" unless rows.empty?
|
|
608
|
+
end
|
|
609
|
+
|
|
462
610
|
# @return [String]
|
|
463
611
|
def created(feature, dispatcher)
|
|
464
612
|
claim!(feature.ordinal)
|
|
@@ -488,6 +636,48 @@ module OpenLanes
|
|
|
488
636
|
json ? JSON.parse(json).values_at("user", "host").join("@") : "another machine"
|
|
489
637
|
end
|
|
490
638
|
|
|
639
|
+
# ---- lanes (plan 020.00) ----------------------------------------------
|
|
640
|
+
|
|
641
|
+
# `/stop NNN`: every agent on the plan stops, and the plan leaves the board.
|
|
642
|
+
#
|
|
643
|
+
# @return [String]
|
|
644
|
+
def stop_plan(command, dispatcher)
|
|
645
|
+
ordinal = ordinal!(command)
|
|
646
|
+
told = dispatcher.stop_plan(ordinal)
|
|
647
|
+
told.each { @run_state.hold_car(it, held: false) }
|
|
648
|
+
"stopped #{told.size} agent#{"s" unless told.size == 1} on #{ordinal} and took it off the board"
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# `/restart NNN`: a lane that lost its agent is dispatched again.
|
|
652
|
+
#
|
|
653
|
+
# @return [String]
|
|
654
|
+
def restart_plan(command, dispatcher)
|
|
655
|
+
ordinal = ordinal!(command)
|
|
656
|
+
dispatcher.restart_plan(ordinal)
|
|
657
|
+
"#{ordinal} restarts at the next tick"
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# `/concurrency N`. Lowering it stops nobody; the run waits for agents
|
|
661
|
+
# to finish until it is under the new limit.
|
|
662
|
+
#
|
|
663
|
+
# @return [String]
|
|
664
|
+
def set_concurrency(command, dispatcher)
|
|
665
|
+
limit = command.payload["limit"]
|
|
666
|
+
raise Refusal, "concurrency must be a whole number of at least 1, not #{limit.inspect}" unless limit.is_a?(Integer) && limit >= 1
|
|
667
|
+
|
|
668
|
+
before = dispatcher.runner.jobs
|
|
669
|
+
dispatcher.concurrency = limit
|
|
670
|
+
@run_state.concurrency = limit
|
|
671
|
+
over = dispatcher.running.size - limit
|
|
672
|
+
wait = over.positive? ? "; the run waits for #{over} agent#{"s" unless over == 1} to finish" : ""
|
|
673
|
+
"concurrency #{before} → #{limit}#{wait}"
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# @return [String] the plan's ordinal, normalised
|
|
677
|
+
def ordinal!(command)
|
|
678
|
+
(Plans::Ordinal.parse(command.plan.to_s) or raise Refusal, "#{command.plan} is not a plan number").to_s
|
|
679
|
+
end
|
|
680
|
+
|
|
491
681
|
# ---- lookups ----------------------------------------------------------
|
|
492
682
|
|
|
493
683
|
# @return [OpenLanes::Plans::Subject]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Live
|
|
5
|
+
class Copilot
|
|
6
|
+
# What c3po-operator is told with each message: the board as it stands,
|
|
7
|
+
# where to read further, and the person's words.
|
|
8
|
+
#
|
|
9
|
+
# The board lives in Redis, which the agent cannot read: its Bash runs
|
|
10
|
+
# only `openlanes` commands that read state, and none of them prints the
|
|
11
|
+
# live board. So the server reads the snapshot and writes it in here,
|
|
12
|
+
# fresh with every message, since the run moves between two questions.
|
|
13
|
+
# It is written as lines rather than the snapshot's JSON because the
|
|
14
|
+
# agent pays for every token of it on every message.
|
|
15
|
+
class Brief
|
|
16
|
+
# Recent log lines carried.
|
|
17
|
+
LOG_LINES = 20
|
|
18
|
+
|
|
19
|
+
# Characters of a question's body carried; the agent reads the rest
|
|
20
|
+
# from blocked.md when it needs it.
|
|
21
|
+
BODY = 400
|
|
22
|
+
|
|
23
|
+
# @param text [String] what the person typed
|
|
24
|
+
# @param board [Hash, nil] this node's `Snapshot` as parsed JSON
|
|
25
|
+
# @param others [Array<Hash>] other machines' snapshots
|
|
26
|
+
# @param plans_dir [String]
|
|
27
|
+
# @param trace_dir [String]
|
|
28
|
+
def initialize(text:, board:, plans_dir:, trace_dir:, others: [])
|
|
29
|
+
@text = text
|
|
30
|
+
@board = board
|
|
31
|
+
@others = others
|
|
32
|
+
@plans_dir = plans_dir
|
|
33
|
+
@trace_dir = trace_dir
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [String]
|
|
37
|
+
def to_s
|
|
38
|
+
[board, others, where, "## The person says\n\n#{@text}"].compact.join("\n\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @return [String]
|
|
44
|
+
def board
|
|
45
|
+
return "## The board\n\nNo engine is publishing a board for this checkout: no run is going, or it has ended." unless @board
|
|
46
|
+
|
|
47
|
+
run = @board["run"] || {}
|
|
48
|
+
lines = ["## The board at #{@board["at"]}",
|
|
49
|
+
"",
|
|
50
|
+
"Run #{run["status"]}, #{run["agents_working"].to_i} agents working, ↑ #{run["up"].to_i} ↓ #{run["down"].to_i} tokens, " \
|
|
51
|
+
"$#{format("%.2f", run["cost"].to_f)}."]
|
|
52
|
+
plans = Array(@board["plans"])
|
|
53
|
+
lines << (plans.empty? ? "No plans on this machine's board." : "")
|
|
54
|
+
plans.each { lines.concat(plan(it)) }
|
|
55
|
+
backlog = Array(@board["backlog"])
|
|
56
|
+
lines << "Backlog: #{backlog.map { "#{it["ordinal"]} #{it["title"]} (#{it.dig("status", "label")})" }.join("; ")}" unless backlog.empty?
|
|
57
|
+
lines.concat(log)
|
|
58
|
+
lines.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param plan [Hash]
|
|
62
|
+
# @return [Array<String>]
|
|
63
|
+
def plan(plan)
|
|
64
|
+
lines = ["- #{plan["ordinal"]} #{plan["title"]}: #{plan.dig("status", "emoji")} #{plan.dig("status", "label")}, " \
|
|
65
|
+
"#{plan["lane"]} lane, #{minutes(plan["elapsed"])} worked, $#{format("%.2f", plan["cost"].to_f)}" \
|
|
66
|
+
"#{", PR ##{plan.dig("pr", "number")} #{plan.dig("pr", "state")}" if plan["pr"]}"]
|
|
67
|
+
Array(plan["cars"]).each { lines << car(it) }
|
|
68
|
+
Array(plan["questions"]).each { lines << question(it) }
|
|
69
|
+
if (stop = plan["interrupted"])
|
|
70
|
+
lines << " - interrupted: #{stop["agent"]} at #{stop["at"]}: #{stop["note"]}"
|
|
71
|
+
end
|
|
72
|
+
lines
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param car [Hash]
|
|
76
|
+
# @return [String]
|
|
77
|
+
def car(car)
|
|
78
|
+
" - car #{car["key"]}: #{car["state"]}#{", #{car["phase"]}" if car["phase"]}, round #{car["round"]}/#{car["rounds"]}, " \
|
|
79
|
+
"#{car["model"]}, ↑ #{car["up"].to_i} ↓ #{car["down"].to_i}, #{minutes(car["elapsed"])} elapsed" \
|
|
80
|
+
"#{", #{minutes(car["remaining"])} left" if car["remaining"]}#{", held" if car["held"]}: #{car["message"].to_s.inspect}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param question [Hash]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def question(question)
|
|
86
|
+
body = question["body"].to_s.gsub(/\s+/, " ").strip
|
|
87
|
+
body = "#{body[0, BODY]}…" if body.length > BODY
|
|
88
|
+
" - question #{question["id"]} (#{question["owner"]}, asked by #{question["agent"] || "unknown"}" \
|
|
89
|
+
"#{", held" if question["held"]}): #{question["heading"]}: #{body}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Array<String>]
|
|
93
|
+
def log
|
|
94
|
+
lines = Array(@board["log"]).last(LOG_LINES)
|
|
95
|
+
return [] if lines.empty?
|
|
96
|
+
|
|
97
|
+
["", "Recent log, newest last:"] + lines.map { " #{it["at"]} #{it["source"]} #{it["level"]}: #{it["text"]}" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [String, nil]
|
|
101
|
+
def others
|
|
102
|
+
return nil if @others.empty?
|
|
103
|
+
|
|
104
|
+
machines = @others.map do |snapshot|
|
|
105
|
+
node = snapshot["node"] || {}
|
|
106
|
+
plans = Array(snapshot["plans"]).map { "#{it["ordinal"]} #{it.dig("status", "label")}" }
|
|
107
|
+
"- #{node["user"]}@#{node["host"]}: #{plans.empty? ? "no plans" : plans.join(", ")}"
|
|
108
|
+
end
|
|
109
|
+
"## Other machines (read only)\n\n#{machines.join("\n")}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [String]
|
|
113
|
+
def where
|
|
114
|
+
<<~TEXT.strip
|
|
115
|
+
## Where to read more
|
|
116
|
+
|
|
117
|
+
- Plan folders: #{@plans_dir}. Each holds spec.md, plan.md, blocked.md when blocked, and state.json with its signatures and mailbox.
|
|
118
|
+
- Agents' transcripts: #{@trace_dir}, one NDJSON file per invocation, named by time, plan and agent.
|
|
119
|
+
TEXT
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @param seconds [Numeric, nil]
|
|
123
|
+
# @return [String]
|
|
124
|
+
def minutes(seconds) = "#{(seconds.to_f / 60).round}m"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|