openlanes 3.0.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 +7 -0
- data/agents/hansolo-reviewer.md +59 -0
- data/agents/lando-broker.md +70 -0
- data/agents/leah-researcher.md +60 -0
- data/agents/luke-backend.md +73 -0
- data/agents/palpatine-planner.md +61 -0
- data/agents/r2d2-mechanic.md +57 -0
- data/agents/rey-frontend.md +68 -0
- data/agents/yoda-writer.md +67 -0
- data/bin/factory-e2e +157 -0
- data/bin/plan-number +164 -0
- data/bin/setup +26 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
- data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
- data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast.yml +44 -0
- data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
- data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
- data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep.yml +51 -0
- data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
- data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
- data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast.yml +55 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
- data/evals/cases/luke-backend/build-medium.yml +68 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium.yml +55 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
- data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
- data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium.yml +40 -0
- data/exe/depot +1 -0
- data/exe/lanes +1 -0
- data/exe/openlanes +32 -0
- data/lib/openlanes/adapters/base.rb +69 -0
- data/lib/openlanes/adapters/claude.rb +124 -0
- data/lib/openlanes/adapters/codex.rb +33 -0
- data/lib/openlanes/adapters/model.rb +37 -0
- data/lib/openlanes/adapters/pi.rb +22 -0
- data/lib/openlanes/adapters/stream.rb +196 -0
- data/lib/openlanes/adapters.rb +92 -0
- data/lib/openlanes/agents/agent.rb +103 -0
- data/lib/openlanes/agents/profile.rb +57 -0
- data/lib/openlanes/agents/registry.rb +151 -0
- data/lib/openlanes/agents/roster.rb +103 -0
- data/lib/openlanes/agents/routing.rb +88 -0
- data/lib/openlanes/agents.rb +20 -0
- data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
- data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
- data/lib/openlanes/cli/base.rb +263 -0
- data/lib/openlanes/cli/create/create.rb +354 -0
- data/lib/openlanes/cli/docs/docs.rb +40 -0
- data/lib/openlanes/cli/eval/eval.rb +110 -0
- data/lib/openlanes/cli/index/index.rb +40 -0
- data/lib/openlanes/cli/init/init.rb +145 -0
- data/lib/openlanes/cli/linear/linear.rb +36 -0
- data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
- data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
- data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
- data/lib/openlanes/cli/mail/mail.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
- data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
- data/lib/openlanes/cli/run/run.rb +544 -0
- data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
- data/lib/openlanes/cli/start/start.rb +110 -0
- data/lib/openlanes/cli/state/state.rb +26 -0
- data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
- data/lib/openlanes/cli/states/states.rb +15 -0
- data/lib/openlanes/cli/unblock/unblock.rb +235 -0
- data/lib/openlanes/cli/version/version.rb +13 -0
- data/lib/openlanes/cli/web/web.rb +100 -0
- data/lib/openlanes/cli/worktree/worktree.rb +85 -0
- data/lib/openlanes/cli.rb +162 -0
- data/lib/openlanes/engine/board.rb +104 -0
- data/lib/openlanes/engine/dispatcher.rb +1297 -0
- data/lib/openlanes/engine/progress_log.rb +125 -0
- data/lib/openlanes/engine/runner.rb +224 -0
- data/lib/openlanes/engine/state_file.rb +190 -0
- data/lib/openlanes/engine/tally.rb +177 -0
- data/lib/openlanes/engine.rb +19 -0
- data/lib/openlanes/evals/case.rb +191 -0
- data/lib/openlanes/evals/changes.rb +67 -0
- data/lib/openlanes/evals/check.rb +224 -0
- data/lib/openlanes/evals/evidence.rb +71 -0
- data/lib/openlanes/evals/live.rb +140 -0
- data/lib/openlanes/evals/recording.rb +38 -0
- data/lib/openlanes/evals/run.rb +34 -0
- data/lib/openlanes/evals/score.rb +24 -0
- data/lib/openlanes/evals/scorer.rb +42 -0
- data/lib/openlanes/evals/verdict.rb +18 -0
- data/lib/openlanes/evals.rb +56 -0
- data/lib/openlanes/execution/checks.rb +73 -0
- data/lib/openlanes/execution/child.rb +228 -0
- data/lib/openlanes/execution/clock.rb +121 -0
- data/lib/openlanes/execution/control.rb +194 -0
- data/lib/openlanes/execution/executor.rb +849 -0
- data/lib/openlanes/execution/prompt.rb +455 -0
- data/lib/openlanes/execution/transcript.rb +531 -0
- data/lib/openlanes/execution.rb +19 -0
- data/lib/openlanes/launcher.rb +69 -0
- data/lib/openlanes/lifecycle/adoption.rb +197 -0
- data/lib/openlanes/lifecycle/brief.rb +288 -0
- data/lib/openlanes/lifecycle/completion.rb +58 -0
- data/lib/openlanes/lifecycle/creator.rb +126 -0
- data/lib/openlanes/lifecycle/lanes.rb +74 -0
- data/lib/openlanes/lifecycle/resync.rb +428 -0
- data/lib/openlanes/lifecycle/unblocker.rb +236 -0
- data/lib/openlanes/lifecycle.rb +28 -0
- data/lib/openlanes/linear/api.rb +224 -0
- data/lib/openlanes/linear/attribution.rb +185 -0
- data/lib/openlanes/linear/fuzzy.rb +68 -0
- data/lib/openlanes/linear/import.rb +314 -0
- data/lib/openlanes/linear/issue.rb +187 -0
- data/lib/openlanes/linear/mapping.rb +116 -0
- data/lib/openlanes/linear/push.rb +197 -0
- data/lib/openlanes/linear/survey.rb +174 -0
- data/lib/openlanes/linear/unit.rb +274 -0
- data/lib/openlanes/linear.rb +41 -0
- data/lib/openlanes/live/claims.rb +71 -0
- data/lib/openlanes/live/command.rb +122 -0
- data/lib/openlanes/live/commands.rb +505 -0
- data/lib/openlanes/live/keys.rb +93 -0
- data/lib/openlanes/live/memory_store.rb +265 -0
- data/lib/openlanes/live/meter.rb +51 -0
- data/lib/openlanes/live/node.rb +88 -0
- data/lib/openlanes/live/publisher.rb +179 -0
- data/lib/openlanes/live/repo.rb +60 -0
- data/lib/openlanes/live/run_record.rb +133 -0
- data/lib/openlanes/live/run_state.rb +101 -0
- data/lib/openlanes/live/session.rb +107 -0
- data/lib/openlanes/live/snapshot.rb +322 -0
- data/lib/openlanes/live/stations.rb +67 -0
- data/lib/openlanes/live/store.rb +277 -0
- data/lib/openlanes/live.rb +109 -0
- data/lib/openlanes/plans/dev_work.rb +56 -0
- data/lib/openlanes/plans/feature.rb +131 -0
- data/lib/openlanes/plans/index.rb +208 -0
- data/lib/openlanes/plans/ledger.rb +255 -0
- data/lib/openlanes/plans/mailbox.rb +164 -0
- data/lib/openlanes/plans/ordinal.rb +92 -0
- data/lib/openlanes/plans/plan_state.rb +291 -0
- data/lib/openlanes/plans/pull_request.rb +29 -0
- data/lib/openlanes/plans/pull_requests.rb +229 -0
- data/lib/openlanes/plans/spec.rb +213 -0
- data/lib/openlanes/plans/state_machine.rb +433 -0
- data/lib/openlanes/plans/status.rb +365 -0
- data/lib/openlanes/plans/subject.rb +133 -0
- data/lib/openlanes/plans/tree.rb +79 -0
- data/lib/openlanes/plans.rb +39 -0
- data/lib/openlanes/presentation/diagram.rb +149 -0
- data/lib/openlanes/presentation/documentation.rb +438 -0
- data/lib/openlanes/presentation/reporter.rb +181 -0
- data/lib/openlanes/presentation/viewer.rb +62 -0
- data/lib/openlanes/presentation.rb +23 -0
- data/lib/openlanes/skills/installer.rb +68 -0
- data/lib/openlanes/skills/skill.rb +60 -0
- data/lib/openlanes/skills.rb +25 -0
- data/lib/openlanes/support/config.rb +217 -0
- data/lib/openlanes/support/frontmatter.rb +38 -0
- data/lib/openlanes/support/markdown.rb +56 -0
- data/lib/openlanes/support/project.rb +79 -0
- data/lib/openlanes/support/ui.rb +671 -0
- data/lib/openlanes/vcs/github.rb +171 -0
- data/lib/openlanes/vcs/publisher.rb +221 -0
- data/lib/openlanes/vcs/worktree.rb +248 -0
- data/lib/openlanes/vcs.rb +19 -0
- data/lib/openlanes/version.rb +10 -0
- data/lib/openlanes/web/app.rb +176 -0
- data/lib/openlanes/web/context.rb +66 -0
- data/lib/openlanes/web/demo.rb +169 -0
- data/lib/openlanes/web/demo_board.rb +286 -0
- data/lib/openlanes/web/events.rb +104 -0
- data/lib/openlanes/web/launcher.rb +143 -0
- data/lib/openlanes/web/server.rb +77 -0
- data/lib/openlanes/web/stores.rb +36 -0
- data/lib/openlanes/web.rb +92 -0
- data/lib/openlanes.rb +151 -0
- data/openlanes.gemspec +87 -0
- data/schemas/plan-state.schema.json +101 -0
- data/skills/openlanes/SKILL.md +48 -0
- data/skills/openlanes-documents/SKILL.md +61 -0
- data/skills/openlanes-documents/references/blocked-template.md +49 -0
- data/skills/openlanes-documents/references/closing-templates.md +59 -0
- data/skills/openlanes-documents/references/plan-template.md +61 -0
- data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
- data/skills/openlanes-documents/references/spec-template.md +45 -0
- data/skills/openlanes-run/SKILL.md +90 -0
- data/skills/openlanes-setup/SKILL.md +64 -0
- data/skills/openlanes-spec/SKILL.md +90 -0
- data/skills/openlanes-status/SKILL.md +32 -0
- data/skills/openlanes-sync/SKILL.md +22 -0
- data/web/dist/assets/apl-B4CMkyY2.js +2 -0
- data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
- data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
- data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
- data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
- data/web/dist/assets/clike-B9uivgTg.js +2 -0
- data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
- data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
- data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
- data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
- data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
- data/web/dist/assets/crystal-SjHAIU92.js +2 -0
- data/web/dist/assets/css-BnMrqG3P.js +2 -0
- data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
- data/web/dist/assets/d-pRatUO7H.js +2 -0
- data/web/dist/assets/diff-DbItnlRl.js +2 -0
- data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
- data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
- data/web/dist/assets/dylan-DwRh75JA.js +2 -0
- data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
- data/web/dist/assets/ecl-Cabwm37j.js +2 -0
- data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
- data/web/dist/assets/elm-vLlmbW-K.js +2 -0
- data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
- data/web/dist/assets/factor-kuTfRLto.js +2 -0
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
- data/web/dist/assets/forth-Ffai-XNe.js +2 -0
- data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
- data/web/dist/assets/gas-Bneqetm1.js +2 -0
- data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
- data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
- data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
- data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
- data/web/dist/assets/http-DBlCnlav.js +2 -0
- data/web/dist/assets/idl-BEugSyMb.js +2 -0
- data/web/dist/assets/index-2xPC-eHt.js +2 -0
- data/web/dist/assets/index-8dNhRmiO.js +8 -0
- data/web/dist/assets/index-B7-O9ujQ.js +2 -0
- data/web/dist/assets/index-BdR34cXC.js +2 -0
- data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
- data/web/dist/assets/index-Byyxbh_z.js +4 -0
- data/web/dist/assets/index-C0VmkrO-.js +2 -0
- data/web/dist/assets/index-CI74IkWP.js +198 -0
- data/web/dist/assets/index-CYxcbpRM.js +3 -0
- data/web/dist/assets/index-D0rmJN3h.js +2 -0
- data/web/dist/assets/index-DB4QwuYn.js +2 -0
- data/web/dist/assets/index-DDI7zoAS.js +2 -0
- data/web/dist/assets/index-DGLUuEXi.js +2 -0
- data/web/dist/assets/index-OR-odsl9.js +2 -0
- data/web/dist/assets/index-P3vSkiU-.js +2 -0
- data/web/dist/assets/index-a4EHR-u4.css +1 -0
- data/web/dist/assets/index-mRV4XhJZ.js +2 -0
- data/web/dist/assets/index-popk9wbW.js +2 -0
- data/web/dist/assets/index-uM88Njdj.js +2 -0
- data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
- data/web/dist/assets/julia-DuME0IfC.js +2 -0
- data/web/dist/assets/livescript-BwQOo05w.js +2 -0
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- data/web/dist/assets/lua-VAEuO923.js +2 -0
- data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
- data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
- data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
- data/web/dist/assets/mllike-CXdrOF99.js +2 -0
- data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
- data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
- data/web/dist/assets/mumps-BT43cFF4.js +2 -0
- data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
- data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
- data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
- data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
- data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
- data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
- data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
- data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
- data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
- data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
- data/web/dist/assets/oz-BzwKVEFT.js +2 -0
- data/web/dist/assets/pascal--L3eBynH.js +2 -0
- data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
- data/web/dist/assets/pig-CevX1Tat.js +2 -0
- data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
- data/web/dist/assets/properties-C78fOPTZ.js +2 -0
- data/web/dist/assets/protobuf-ChK-085T.js +2 -0
- data/web/dist/assets/pug-DeIclll2.js +2 -0
- data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
- data/web/dist/assets/python-BuPzkPfP.js +2 -0
- data/web/dist/assets/q-pXgVlZs6.js +2 -0
- data/web/dist/assets/r-B6wPVr8A.js +2 -0
- data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
- data/web/dist/assets/ruby-BK5aLJev.js +2 -0
- data/web/dist/assets/sas-B4kiWyti.js +2 -0
- data/web/dist/assets/scheme-C41bIUwD.js +2 -0
- data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
- data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
- data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
- data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
- data/web/dist/assets/solr-DehyRSwq.js +2 -0
- data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
- data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
- data/web/dist/assets/sql-D0XecflT.js +2 -0
- data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
- data/web/dist/assets/stylus-B533Al4x.js +2 -0
- data/web/dist/assets/swift-BzpIVaGY.js +2 -0
- data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
- data/web/dist/assets/textile-CnDTJFAw.js +2 -0
- data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- data/web/dist/assets/tiki-DGYXhP31.js +2 -0
- data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
- data/web/dist/assets/troff-wAsdV37c.js +2 -0
- data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
- data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
- data/web/dist/assets/vb-CmGdzxic.js +2 -0
- data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
- data/web/dist/assets/velocity-D8B20fx6.js +2 -0
- data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
- data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
- data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
- data/web/dist/assets/xquery-DzFWVndE.js +2 -0
- data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
- data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
- data/web/dist/index.html +14 -0
- data/web/dist/vite.svg +1 -0
- data/web/src/api/fixtures/board.json +512 -0
- metadata +907 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module Plans
|
|
8
|
+
# `openlanes index` — the project's plans as one browsable page.
|
|
9
|
+
#
|
|
10
|
+
# {Reporter} answers "what is the state of things" in a terminal, in columns,
|
|
11
|
+
# for someone standing at a prompt. This answers "what is this project, and
|
|
12
|
+
# where is everything" for someone arriving at the repository on the web: the
|
|
13
|
+
# goal of each plan in its own words, every pull request as a link, and every
|
|
14
|
+
# document in the folder one click away.
|
|
15
|
+
#
|
|
16
|
+
# It is generated for the same reason the conventions document is. A
|
|
17
|
+
# hand-written index of twenty plans cannot survive a rename, and renaming is
|
|
18
|
+
# exactly what `resync dirs` exists to do — the first hand-made INDEX.md this
|
|
19
|
+
# replaced had all 82 of its links pointing at folder names that had been
|
|
20
|
+
# padded to `NNN.MM` underneath it.
|
|
21
|
+
class Index
|
|
22
|
+
# The file this writes, relative to the `.plans` directory.
|
|
23
|
+
FILENAME = "INDEX.md"
|
|
24
|
+
|
|
25
|
+
# Documents that get a proper name rather than their filename.
|
|
26
|
+
ARTIFACT_NAMES = {
|
|
27
|
+
"spec.md" => "Spec",
|
|
28
|
+
"plan.md" => "Plan",
|
|
29
|
+
"pull-requests.md" => "Pull Requests",
|
|
30
|
+
"linear.md" => "Linear",
|
|
31
|
+
"blocked.md" => "Blocked",
|
|
32
|
+
"delayed.md" => "Deferred",
|
|
33
|
+
"rewrite.md" => "Rewrite",
|
|
34
|
+
"deployed.md" => "Deployed",
|
|
35
|
+
"rollback.md" => "Rollback",
|
|
36
|
+
"discarded.md" => "Discarded"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
40
|
+
# @param project [String, nil] the heading; defaults to the repository name
|
|
41
|
+
def initialize(tree:, project: nil)
|
|
42
|
+
@tree = tree
|
|
43
|
+
@project = project
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [OpenLanes::Plans::Tree]
|
|
47
|
+
attr_reader :tree
|
|
48
|
+
|
|
49
|
+
# @return [String] the project's name, titleized from its directory
|
|
50
|
+
def project = @project ||= OpenLanes::Plans.titleize(File.basename(File.dirname(tree.dir)))
|
|
51
|
+
|
|
52
|
+
# @return [String] the whole document
|
|
53
|
+
def render
|
|
54
|
+
[heading, *tree.subjects.map { |subject| section(subject) }].join("\n") + footer
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Write it next to the plans it describes.
|
|
58
|
+
#
|
|
59
|
+
# @param path [String, nil] override the destination
|
|
60
|
+
# @return [String] where it was written
|
|
61
|
+
def write(path = nil)
|
|
62
|
+
path ||= File.join(tree.dir, FILENAME)
|
|
63
|
+
File.write(path, render)
|
|
64
|
+
path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# @return [String]
|
|
70
|
+
def heading
|
|
71
|
+
<<~MARKDOWN
|
|
72
|
+
# Project #{project}
|
|
73
|
+
|
|
74
|
+
> [!IMPORTANT]
|
|
75
|
+
> **This file is auto generated.** To regenerate it, run `openlanes index`.
|
|
76
|
+
> Editing it by hand lasts until the next `resync dirs` renames a folder.
|
|
77
|
+
|
|
78
|
+
## Current specifications and their status
|
|
79
|
+
|
|
80
|
+
MARKDOWN
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# One plan: a heading that carries its number and state, then a table.
|
|
84
|
+
#
|
|
85
|
+
# Raw HTML rather than a markdown table because the cells are not one-liners
|
|
86
|
+
# — a goal is a paragraph or two and a plan may have a dozen pull requests,
|
|
87
|
+
# neither of which a pipe-delimited row can hold.
|
|
88
|
+
#
|
|
89
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
90
|
+
# @return [String]
|
|
91
|
+
def section(subject)
|
|
92
|
+
feature = subject.feature
|
|
93
|
+
|
|
94
|
+
<<~MARKDOWN
|
|
95
|
+
## #{feature.ordinal} — #{feature.status.emoji} #{escape(feature.title)}
|
|
96
|
+
|
|
97
|
+
<table>
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th align="left">Status</th>
|
|
101
|
+
<th align="left">Pull Requests</th>
|
|
102
|
+
<th align="left">What it is</th>
|
|
103
|
+
<th align="left">Artifacts</th>
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
<tr>
|
|
108
|
+
<td valign="top">#{status_cell(subject)}</td>
|
|
109
|
+
<td valign="top">#{pulls_cell(subject)}</td>
|
|
110
|
+
<td valign="top">#{goal_cell(subject)}</td>
|
|
111
|
+
<td valign="top">#{artifacts_cell(feature)}</td>
|
|
112
|
+
</tr>
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
|
|
116
|
+
MARKDOWN
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The emoji and the words, kept on one line — a status that wraps mid-label
|
|
120
|
+
# makes the column look like two states rather than one.
|
|
121
|
+
#
|
|
122
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
123
|
+
# @return [String]
|
|
124
|
+
def status_cell(subject)
|
|
125
|
+
cell = "#{subject.status.emoji} #{escape(subject.status.label)}"
|
|
126
|
+
return cell if subject.consistent?
|
|
127
|
+
|
|
128
|
+
"#{cell}<br><sub>⚠️ #{escape(subject.violation)}</sub>"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
132
|
+
# @return [String]
|
|
133
|
+
def pulls_cell(subject)
|
|
134
|
+
prs = subject.pull_requests
|
|
135
|
+
return "<em>—</em>" if prs.empty?
|
|
136
|
+
|
|
137
|
+
items = prs.map { |pr|
|
|
138
|
+
"<li>#{link(pr.url, pr.label)} #{escape(pr.state)}</li>"
|
|
139
|
+
}
|
|
140
|
+
"<ul>#{items.join}</ul>"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
144
|
+
# @return [String]
|
|
145
|
+
def goal_cell(subject)
|
|
146
|
+
paragraphs = subject.goal
|
|
147
|
+
return "<em>No specification yet.</em>" if paragraphs.empty?
|
|
148
|
+
|
|
149
|
+
paragraphs.map { |p| "<p>#{inline(p)}</p>" }.join
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Markdown inside a raw HTML block is not parsed — GitHub stops parsing
|
|
153
|
+
# markdown the moment it sees a block-level tag — so a goal quoted verbatim
|
|
154
|
+
# would show its own `**` and backticks. Only the three that carry meaning
|
|
155
|
+
# are translated; single-underscore emphasis is deliberately left alone,
|
|
156
|
+
# because `deleted_at` is more common in these documents than italics.
|
|
157
|
+
#
|
|
158
|
+
# @param text [String] markdown, unescaped
|
|
159
|
+
# @return [String] HTML
|
|
160
|
+
def inline(text)
|
|
161
|
+
escape(text)
|
|
162
|
+
.gsub(/\[([^\]]+)\]\((https?:[^)\s]+)\)/) { %(<a href="#{$2}">#{$1}</a>) }
|
|
163
|
+
.gsub(/\*\*([^*]+)\*\*/) { "<strong>#{$1}</strong>" }
|
|
164
|
+
.gsub(/`([^`]+)`/) { "<code>#{$1}</code>" }
|
|
165
|
+
.tr("\n", " ")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Every document in the folder, linked. The folder name is percent-encoded
|
|
169
|
+
# because it contains an emoji, and a raw one in an href is a broken link
|
|
170
|
+
# on GitHub.
|
|
171
|
+
#
|
|
172
|
+
# @param feature [OpenLanes::Plans::Feature]
|
|
173
|
+
# @return [String]
|
|
174
|
+
def artifacts_cell(feature)
|
|
175
|
+
files = Dir.children(feature.path).select { |f| File.file?(File.join(feature.path, f)) }
|
|
176
|
+
return "<em>empty</em>" if files.empty?
|
|
177
|
+
|
|
178
|
+
# The documents that carry the lifecycle come first, in lifecycle order;
|
|
179
|
+
# whatever else the folder holds follows alphabetically.
|
|
180
|
+
known = ARTIFACT_NAMES.keys
|
|
181
|
+
files = files.sort_by { |f| [known.index(f) || known.size, f] }
|
|
182
|
+
|
|
183
|
+
items = files.map { |file|
|
|
184
|
+
href = "#{ERB::Util.url_encode(feature.dirname)}/#{ERB::Util.url_encode(file)}"
|
|
185
|
+
"<li>#{link(href, ARTIFACT_NAMES.fetch(file, file))}</li>"
|
|
186
|
+
}
|
|
187
|
+
"<ul>#{items.join}</ul>"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return [String]
|
|
191
|
+
def footer
|
|
192
|
+
counts = tree.subjects.each_with_object(Hash.new(0)) { |s, h| h[s.status] += 1 }
|
|
193
|
+
.map { |status, n| "#{status.emoji} #{n}" }.join(" ")
|
|
194
|
+
|
|
195
|
+
"\n---\n\n#{tree.subjects.size} #{tree.subjects.size == 1 ? "plan" : "plans"} #{counts}\n"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @param href [String]
|
|
199
|
+
# @param text [String]
|
|
200
|
+
# @return [String]
|
|
201
|
+
def link(href, text) = %(<a href="#{escape(href)}">#{escape(text)}</a>)
|
|
202
|
+
|
|
203
|
+
# @param text [String]
|
|
204
|
+
# @return [String]
|
|
205
|
+
def escape(text) = CGI.escapeHTML(text.to_s)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Plans
|
|
7
|
+
# The dated notes an agent leaves in the document it owns.
|
|
8
|
+
#
|
|
9
|
+
# This is the only place in the codebase that knows the syntax. The
|
|
10
|
+
# dispatcher reads it to learn whether an agent finished and who it named
|
|
11
|
+
# next; the state file records it; the screen paints it. An agent writes
|
|
12
|
+
# one entry when it starts and one when it stops, in a GitHub NOTE alert,
|
|
13
|
+
# one entry per line:
|
|
14
|
+
#
|
|
15
|
+
# > [!NOTE]
|
|
16
|
+
# >
|
|
17
|
+
# > [2026-09-04 11:29:20 AM PDT] [ agent: leah-researcher status: Started, round 1 ]
|
|
18
|
+
# > [2026-09-04 11:44:03 AM PDT] [ agent: leah-researcher status: Completed, round 1 ]
|
|
19
|
+
# > [2026-09-04 11:44:04 AM PDT] [ next: yoda-writer ]
|
|
20
|
+
#
|
|
21
|
+
# The verbs are a closed set. A line that looks like an entry but does not
|
|
22
|
+
# parse is reported as a {Problem}, never dropped, because an agent that
|
|
23
|
+
# wrote "Done" has said nothing the harness can act on and silence would
|
|
24
|
+
# read as a stall.
|
|
25
|
+
module Ledger
|
|
26
|
+
# Everything an agent may claim. `Completed` is the only verb a `next:`
|
|
27
|
+
# line may follow.
|
|
28
|
+
STATUSES = ["Started", "Completed", "Almost completed", "Interrupted", "Blocked"].freeze
|
|
29
|
+
|
|
30
|
+
# Local time with the zone spelled out, so an entry written on one
|
|
31
|
+
# machine reads correctly on another.
|
|
32
|
+
TIME_FORMAT = "%Y-%m-%d %I:%M:%S %p %Z"
|
|
33
|
+
|
|
34
|
+
# An entry. `**` around the status is how the harness marks the one line
|
|
35
|
+
# it writes itself, the Interrupted that precedes a signature.
|
|
36
|
+
ENTRY = /\A>\s*\[(?<at>[^\]]+)\]\s*\[\s*agent:\s*(?<agent>[\w-]+)\s+status:\s*(?<bold>\*\*)?
|
|
37
|
+
(?<status>#{STATUSES.map { |s| Regexp.escape(s) }.join("|")}),\s*round\s*(?<round>\d+)
|
|
38
|
+
(?:\s*\((?<note>[^)]*)\))?\s*(?:\*\*)?\s*\]\s*\z/x
|
|
39
|
+
|
|
40
|
+
# The handoff line.
|
|
41
|
+
NEXT = /\A>\s*\[(?<at>[^\]]+)\]\s*\[\s*next:\s*(?<next>[\w-]+)\s*\]\s*\z/
|
|
42
|
+
|
|
43
|
+
# A line that is trying to be one of the two above. Anything matching
|
|
44
|
+
# this and neither of those is a {Problem}.
|
|
45
|
+
ATTEMPT = /\A>\s*\[[^\]]+\]\s*\[/
|
|
46
|
+
|
|
47
|
+
# The whole alert block, for {.stripped}.
|
|
48
|
+
BLOCK = /^[ \t]*>[ \t]*\[!NOTE\][ \t]*\n(?:[ \t]*>.*\n?)*/
|
|
49
|
+
|
|
50
|
+
# @!attribute [r] at
|
|
51
|
+
# @return [Time]
|
|
52
|
+
# @!attribute [r] agent
|
|
53
|
+
# @return [String] the definition's name, `leah-researcher`
|
|
54
|
+
# @!attribute [r] status
|
|
55
|
+
# @return [String] one of {STATUSES}
|
|
56
|
+
# @!attribute [r] round
|
|
57
|
+
# @return [Integer]
|
|
58
|
+
# @!attribute [r] note
|
|
59
|
+
# @return [String, nil] the parenthesised tail, `rejected 1/2`
|
|
60
|
+
# @!attribute [r] file
|
|
61
|
+
# @return [String] the document it was read from
|
|
62
|
+
# @!attribute [r] line
|
|
63
|
+
# @return [Integer] 1-based
|
|
64
|
+
# @!attribute [r] bold
|
|
65
|
+
# @return [Boolean] written `**bold**`, which only the harness does
|
|
66
|
+
Entry = Data.define(:at, :agent, :status, :round, :note, :file, :line, :bold) do
|
|
67
|
+
def initialize(note: nil, bold: false, **rest) = super
|
|
68
|
+
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def completed? = status == "Completed"
|
|
71
|
+
|
|
72
|
+
# @return [Boolean] whether the agent gets another round
|
|
73
|
+
def retry? = ["Almost completed", "Interrupted"].include?(status)
|
|
74
|
+
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def blocked? = status == "Blocked"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @!attribute [r] next
|
|
80
|
+
# @return [String] the agent named
|
|
81
|
+
Handoff = Data.define(:at, :next, :file, :line)
|
|
82
|
+
|
|
83
|
+
# @!attribute [r] text
|
|
84
|
+
# @return [String] the line as written
|
|
85
|
+
Problem = Data.define(:file, :line, :text)
|
|
86
|
+
|
|
87
|
+
# What one read of a plan folder yielded.
|
|
88
|
+
Reading = Data.define(:entries, :handoffs, :problems) do
|
|
89
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
90
|
+
def self.empty = new(entries: [], handoffs: [], problems: [])
|
|
91
|
+
|
|
92
|
+
# @param other [OpenLanes::Plans::Ledger::Reading]
|
|
93
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
94
|
+
def +(other)
|
|
95
|
+
self.class.new(entries: entries + other.entries,
|
|
96
|
+
handoffs: handoffs + other.handoffs,
|
|
97
|
+
problems: problems + other.problems)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class << self
|
|
102
|
+
# @param text [String] a whole document
|
|
103
|
+
# @param file [String] its name, carried on every entry
|
|
104
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
105
|
+
def parse(text, file:)
|
|
106
|
+
entries = []
|
|
107
|
+
handoffs = []
|
|
108
|
+
problems = []
|
|
109
|
+
text.to_s.each_line.with_index(1) do |raw, line|
|
|
110
|
+
stripped = raw.chomp
|
|
111
|
+
if (m = ENTRY.match(stripped))
|
|
112
|
+
entries << Entry.new(at: parse_time(m[:at]),
|
|
113
|
+
agent: m[:agent],
|
|
114
|
+
status: m[:status],
|
|
115
|
+
round: m[:round].to_i,
|
|
116
|
+
note: m[:note]&.strip,
|
|
117
|
+
file:,
|
|
118
|
+
line:,
|
|
119
|
+
bold: !m[:bold].nil?)
|
|
120
|
+
elsif (m = NEXT.match(stripped))
|
|
121
|
+
handoffs << Handoff.new(at: parse_time(m[:at]), next: m[:next], file:, line:)
|
|
122
|
+
elsif ATTEMPT.match?(stripped)
|
|
123
|
+
problems << Problem.new(file:, line:, text: stripped.strip)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
Reading.new(entries:, handoffs:, problems:)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Every document an agent may sign, in the order its definition lists
|
|
130
|
+
# them, then the signatures in `state.json`. A missing file is not a
|
|
131
|
+
# problem; an agent that has not reached `pull-requests.md` yet has not
|
|
132
|
+
# written it. `state.json` comes last, so on a tie in time a signature
|
|
133
|
+
# made through `openlanes state sign` outranks a line in prose.
|
|
134
|
+
#
|
|
135
|
+
# @param dir [String] the plan folder
|
|
136
|
+
# @param files [Array<String>]
|
|
137
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
138
|
+
def read(dir, files)
|
|
139
|
+
markdown(dir, files) + signed(dir)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A `state.json` that will not parse is reported like an unreadable
|
|
143
|
+
# line, not raised: one bad file must not stop the loop for every plan.
|
|
144
|
+
#
|
|
145
|
+
# @param dir [String]
|
|
146
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
147
|
+
def signed(dir)
|
|
148
|
+
PlanState.for(dir).ledger
|
|
149
|
+
rescue Error => e
|
|
150
|
+
Reading.new(entries: [], handoffs: [], problems: [Problem.new(file: PlanState::FILENAME, line: 1, text: e.message)])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @param dir [String]
|
|
154
|
+
# @param files [Array<String>]
|
|
155
|
+
# @return [OpenLanes::Plans::Ledger::Reading]
|
|
156
|
+
def markdown(dir, files)
|
|
157
|
+
files.each_with_index.reduce(Reading.empty) do |reading, (name, _index)|
|
|
158
|
+
path = File.join(dir, name)
|
|
159
|
+
next reading unless File.file?(path)
|
|
160
|
+
|
|
161
|
+
found = parse(File.read(path, encoding: "UTF-8"), file: name)
|
|
162
|
+
reading + Reading.new(entries: found.entries.map { |e| e.with(file: name) },
|
|
163
|
+
handoffs: found.handoffs,
|
|
164
|
+
problems: found.problems)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @param time [Time]
|
|
169
|
+
# @return [String]
|
|
170
|
+
def timestamp(time = Time.now) = time.strftime(TIME_FORMAT)
|
|
171
|
+
|
|
172
|
+
# Agents write what `date` prints, and `Time.strptime` cannot read a
|
|
173
|
+
# zone abbreviation back into an offset. The abbreviation is dropped
|
|
174
|
+
# and the time read as local, which is right on the machine that wrote
|
|
175
|
+
# it and off by a zone anywhere else, which the tick tolerates: entries
|
|
176
|
+
# are compared with each other, never with the wall clock.
|
|
177
|
+
#
|
|
178
|
+
# @param text [String]
|
|
179
|
+
# @return [Time]
|
|
180
|
+
def parse_time(text)
|
|
181
|
+
Time.strptime(text.to_s.strip.sub(/\s+[A-Z]{2,5}\z/, ""), "%Y-%m-%d %I:%M:%S %p")
|
|
182
|
+
rescue ArgumentError
|
|
183
|
+
Time.at(0)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @param entry [OpenLanes::Plans::Ledger::Entry]
|
|
187
|
+
# @return [String] one quoted line, no newline
|
|
188
|
+
def render(entry)
|
|
189
|
+
tail = entry.note ? " (#{entry.note})" : ""
|
|
190
|
+
body = "#{entry.status}, round #{entry.round}#{tail}"
|
|
191
|
+
body = "**#{body}**" if entry.bold
|
|
192
|
+
"> [#{timestamp(entry.at)}] [ agent: #{entry.agent} status: #{body} ]"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @param handoff [OpenLanes::Plans::Ledger::Handoff]
|
|
196
|
+
# @return [String]
|
|
197
|
+
def render_handoff(handoff) = "> [#{timestamp(handoff.at)}] [ next: #{handoff.next} ]"
|
|
198
|
+
|
|
199
|
+
# @param lines [Array<String>] already quoted
|
|
200
|
+
# @return [String] a NOTE alert, newline-terminated
|
|
201
|
+
def block(*lines) = (["> [!NOTE]", ">"] + lines.flatten).join("\n") + "\n"
|
|
202
|
+
|
|
203
|
+
# Append one block. Separated from what is there by a blank line so the
|
|
204
|
+
# alert renders as its own block rather than merging into a paragraph.
|
|
205
|
+
#
|
|
206
|
+
# @param path [String]
|
|
207
|
+
# @param lines [Array<String>]
|
|
208
|
+
# @return [void]
|
|
209
|
+
def append(path, *lines)
|
|
210
|
+
existing = File.file?(path) ? File.read(path, encoding: "UTF-8") : ""
|
|
211
|
+
glue = if existing.empty?
|
|
212
|
+
""
|
|
213
|
+
elsif existing.end_with?("\n\n")
|
|
214
|
+
""
|
|
215
|
+
elsif existing.end_with?("\n")
|
|
216
|
+
"\n"
|
|
217
|
+
else
|
|
218
|
+
"\n\n"
|
|
219
|
+
end
|
|
220
|
+
File.write(path, existing + glue + block(*lines))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The entry that tells the harness where this agent stands: the latest
|
|
224
|
+
# by clock, then by the order its documents are listed, then by line.
|
|
225
|
+
#
|
|
226
|
+
# @param reading [OpenLanes::Plans::Ledger::Reading]
|
|
227
|
+
# @param agent [String]
|
|
228
|
+
# @return [OpenLanes::Plans::Ledger::Entry, nil]
|
|
229
|
+
def last_for(reading, agent)
|
|
230
|
+
files = reading.entries.map(&:file).uniq
|
|
231
|
+
reading.entries.select { |e| e.agent == agent }
|
|
232
|
+
.max_by { |e| [e.at, files.index(e.file), e.line] }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The `next:` that belongs to an entry: same file, a later line, and no
|
|
236
|
+
# other entry between the two. A handoff written under somebody else's
|
|
237
|
+
# entry is not this agent's handoff.
|
|
238
|
+
#
|
|
239
|
+
# @param reading [OpenLanes::Plans::Ledger::Reading]
|
|
240
|
+
# @param entry [OpenLanes::Plans::Ledger::Entry]
|
|
241
|
+
# @return [OpenLanes::Plans::Ledger::Handoff, nil]
|
|
242
|
+
def handoff_after(reading, entry)
|
|
243
|
+
candidates = reading.handoffs.select { |h| h.file == entry.file && h.line > entry.line }
|
|
244
|
+
handoff = candidates.min_by(&:line) or return nil
|
|
245
|
+
between = reading.entries.any? { |e| e.file == entry.file && e.line > entry.line && e.line < handoff.line }
|
|
246
|
+
between ? nil : handoff
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @param text [String]
|
|
250
|
+
# @return [String] the document without its ledger blocks
|
|
251
|
+
def stripped(text) = text.to_s.gsub(BLOCK, "")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Plans
|
|
5
|
+
# The one channel two agents on the same plan have to each other.
|
|
6
|
+
#
|
|
7
|
+
# `luke-backend` and `rey-frontend` build one plan in one worktree at the
|
|
8
|
+
# same time, but each is its own `claude -p` process. Claude Code can
|
|
9
|
+
# message between local sessions, and the prompts used to say so, but a
|
|
10
|
+
# session is named after its directory plus a random suffix, so a pair had
|
|
11
|
+
# to guess its partner from a list of every session on the machine by start
|
|
12
|
+
# time. It worked once, by luck. With ten plans running there would have
|
|
13
|
+
# been ten `openlanes-xx` entries and nothing to tell them apart.
|
|
14
|
+
#
|
|
15
|
+
# So the mailbox lives in the plan folder, append-only, one numbered entry
|
|
16
|
+
# per message, readable by `openlanes mail read` during the round and by a
|
|
17
|
+
# person after it. The harness names the partner in each agent's prompt,
|
|
18
|
+
# so nobody guesses.
|
|
19
|
+
#
|
|
20
|
+
# Messages are kept in the plan's `state.json` ({PlanState}), next to the
|
|
21
|
+
# stages and signatures a restart reads. Plans from before that kept them
|
|
22
|
+
# in `mailbox.md`; those are still read, and numbering carries on from
|
|
23
|
+
# them, but nothing writes there any more.
|
|
24
|
+
class Mailbox
|
|
25
|
+
# Where messages used to be written, and are still read from.
|
|
26
|
+
FILENAME = "mailbox.md"
|
|
27
|
+
|
|
28
|
+
# Between the fields of a heading. Neither a name nor a timestamp
|
|
29
|
+
# contains it, which is what makes the heading parseable.
|
|
30
|
+
SEPARATOR = " · "
|
|
31
|
+
|
|
32
|
+
# `## 3 · 2026-09-06 10:30:53 -0700 · rey-frontend → luke-backend`
|
|
33
|
+
HEADING = /\A\#\# (\d+) · (.+?) · (\S+) → (\S+)\s*\z/
|
|
34
|
+
|
|
35
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M:%S %z"
|
|
36
|
+
|
|
37
|
+
# One entry.
|
|
38
|
+
#
|
|
39
|
+
# @!attribute [r] number
|
|
40
|
+
# @return [Integer] 1-based, in order of writing
|
|
41
|
+
# @!attribute [r] at
|
|
42
|
+
# @return [String] when, as {TIME_FORMAT} writes it
|
|
43
|
+
# @!attribute [r] from
|
|
44
|
+
# @return [String] the agent that wrote it
|
|
45
|
+
# @!attribute [r] to
|
|
46
|
+
# @return [String] the agent it is for
|
|
47
|
+
# @!attribute [r] body
|
|
48
|
+
# @return [String]
|
|
49
|
+
Message = Data.define(:number, :at, :from, :to, :body) do
|
|
50
|
+
# @return [String] as it stands in the file
|
|
51
|
+
def to_s = "## #{number}#{SEPARATOR}#{at}#{SEPARATOR}#{from} → #{to}\n\n#{body.strip}\n"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param dir [String] the plan folder
|
|
55
|
+
def initialize(dir:)
|
|
56
|
+
@dir = dir
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String] the plan folder
|
|
60
|
+
attr_reader :dir
|
|
61
|
+
|
|
62
|
+
# @return [String] where new messages are written
|
|
63
|
+
def path = state.path
|
|
64
|
+
|
|
65
|
+
# @return [String] where messages from before `state.json` are read
|
|
66
|
+
def legacy_path = File.join(dir, FILENAME)
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] whether any message has been written
|
|
69
|
+
def exist? = !messages.empty?
|
|
70
|
+
|
|
71
|
+
# Every message, in the order it was written.
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<OpenLanes::Plans::Mailbox::Message>]
|
|
74
|
+
def messages
|
|
75
|
+
(legacy + state.messages.map { |m| from_state(m) }).sort_by(&:number)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# What is waiting for one agent.
|
|
79
|
+
#
|
|
80
|
+
# @param name [String] the reader
|
|
81
|
+
# @param after [Integer] a number the reader has already seen; only
|
|
82
|
+
# messages above it are returned
|
|
83
|
+
# @return [Array<OpenLanes::Plans::Mailbox::Message>]
|
|
84
|
+
def for(name, after: 0)
|
|
85
|
+
messages.select { |m| m.to == name && m.number > after }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Append one message and give it the next number.
|
|
89
|
+
#
|
|
90
|
+
# {PlanState} takes a lock around the read and the write, so two agents
|
|
91
|
+
# finishing a unit in the same second cannot both take the same number.
|
|
92
|
+
#
|
|
93
|
+
# @param from [String]
|
|
94
|
+
# @param to [String]
|
|
95
|
+
# @param body [String]
|
|
96
|
+
# @param at [Time]
|
|
97
|
+
# @param command [String, nil] the page command that sent it; the same
|
|
98
|
+
# id to the same reader appends nothing the second time
|
|
99
|
+
# @return [OpenLanes::Plans::Mailbox::Message] as written, or as first
|
|
100
|
+
# written under +command+
|
|
101
|
+
# @raise [OpenLanes::Error] on an empty body, which would be a heading
|
|
102
|
+
# with nothing under it for the reader to act on, or on a sender or
|
|
103
|
+
# recipient that is blank or holds whitespace
|
|
104
|
+
def append(from:, to:, body:, at: Time.now, command: nil)
|
|
105
|
+
text = body.to_s.strip
|
|
106
|
+
raise Error, "a message needs a body" if text.empty?
|
|
107
|
+
raise Error, "a message needs --from and --to, each one agent name" unless name?(from) && name?(to)
|
|
108
|
+
|
|
109
|
+
from_state(state.message!(from:, to:, body: text, at:, after: legacy.map(&:number).max.to_i, command:))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# @return [OpenLanes::Plans::PlanState]
|
|
115
|
+
def state = PlanState.for(dir)
|
|
116
|
+
|
|
117
|
+
# @return [Array<OpenLanes::Plans::Mailbox::Message>] what mailbox.md holds
|
|
118
|
+
def legacy
|
|
119
|
+
return [] unless File.file?(legacy_path)
|
|
120
|
+
|
|
121
|
+
parse(File.read(legacy_path, encoding: "UTF-8"))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @param message [Hash] as {PlanState} stores it
|
|
125
|
+
# @return [OpenLanes::Plans::Mailbox::Message]
|
|
126
|
+
def from_state(message)
|
|
127
|
+
at = begin
|
|
128
|
+
Time.iso8601(message["at"].to_s).strftime(TIME_FORMAT)
|
|
129
|
+
rescue ArgumentError
|
|
130
|
+
message["at"].to_s
|
|
131
|
+
end
|
|
132
|
+
Message.new(number: message["number"], at:, from: message["from"], to: message["to"], body: message["body"])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param value [Object]
|
|
136
|
+
# @return [Boolean] one token, as {HEADING} expects a name to be
|
|
137
|
+
def name?(value) = value.to_s.match?(/\A\S+\z/)
|
|
138
|
+
|
|
139
|
+
# Only a line shaped exactly like {HEADING} starts a message, so a body
|
|
140
|
+
# may hold headings of its own, code, anything.
|
|
141
|
+
#
|
|
142
|
+
# @param text [String]
|
|
143
|
+
# @return [Array<OpenLanes::Plans::Mailbox::Message>]
|
|
144
|
+
def parse(text)
|
|
145
|
+
messages = []
|
|
146
|
+
current = nil
|
|
147
|
+
body = []
|
|
148
|
+
|
|
149
|
+
text.to_s.each_line(chomp: true) do |line|
|
|
150
|
+
if (heading = HEADING.match(line))
|
|
151
|
+
messages << current.with(body: body.join("\n").strip) if current
|
|
152
|
+
current = Message.new(number: heading[1].to_i, at: heading[2], from: heading[3], to: heading[4], body: "")
|
|
153
|
+
body = []
|
|
154
|
+
elsif current
|
|
155
|
+
body << line
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
messages << current.with(body: body.join("\n").strip) if current
|
|
160
|
+
messages
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|