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,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Execution
|
|
5
|
+
# One `claude` process the harness started and therefore owns.
|
|
6
|
+
#
|
|
7
|
+
# `TTY::Command` never exposes the pid it spawned, so the executor used to
|
|
8
|
+
# find its child by scraping `ps` for a fresh `claude` under this process
|
|
9
|
+
# and could never safely signal it. Kill (`k`) and the sixty-second
|
|
10
|
+
# backstop after STOP both need a pid that is certainly ours, so the
|
|
11
|
+
# process is spawned here and the pid kept.
|
|
12
|
+
#
|
|
13
|
+
# stderr is folded into the same pipe as stdout: `claude` prints the cause
|
|
14
|
+
# of a failure there, and the transcript keeps non-JSON lines for exactly
|
|
15
|
+
# that reason.
|
|
16
|
+
#
|
|
17
|
+
# The process leads a process group of its own, and every stop is sent to
|
|
18
|
+
# the whole group. A SIGKILL aimed at `claude` alone once left the shell
|
|
19
|
+
# its Bash tool had started (and the `eza` an `ls` alias ran in it)
|
|
20
|
+
# holding the write end of this pipe: the reader never saw end of file,
|
|
21
|
+
# and `--timeout 600` ended nothing. The group is also why a terminal's
|
|
22
|
+
# Ctrl-C no longer reaches an agent directly; the harness forwards its
|
|
23
|
+
# own stop instead, and {.terminate_all} catches a harness that exits
|
|
24
|
+
# before its agents have.
|
|
25
|
+
class Child
|
|
26
|
+
# Seconds between TERM and KILL in {#terminate}: long enough for a
|
|
27
|
+
# process to flush and exit, short enough that a stop is a stop.
|
|
28
|
+
TERM_GRACE = 5
|
|
29
|
+
|
|
30
|
+
# Seconds {#terminate} waits, after KILL, for the reader to reach end of
|
|
31
|
+
# file on its own before closing the pipe under it. Only a process that
|
|
32
|
+
# left the group, by `setsid` or a daemonising double fork, can still be
|
|
33
|
+
# holding the pipe by then.
|
|
34
|
+
DRAIN_GRACE = 1
|
|
35
|
+
|
|
36
|
+
# How often {#terminate} looks again while it waits.
|
|
37
|
+
POLL = 0.05
|
|
38
|
+
|
|
39
|
+
@live = []
|
|
40
|
+
@mutex = Mutex.new
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# @param argv [Array<String>]
|
|
44
|
+
# @param chdir [String, nil]
|
|
45
|
+
# @param env [Hash{String => String}] what the adapter adds on top of
|
|
46
|
+
# {.environment}, e.g. claude's autocompact threshold
|
|
47
|
+
# @return [OpenLanes::Execution::Child]
|
|
48
|
+
def spawn(argv, chdir: nil, env: {})
|
|
49
|
+
reader, writer = IO.pipe
|
|
50
|
+
options = { out: writer, err: writer, in: File::NULL, unsetenv_others: true, pgroup: true }
|
|
51
|
+
options[:chdir] = chdir if chdir
|
|
52
|
+
pid = Process.spawn(environment.merge(env), *argv, **options)
|
|
53
|
+
writer.close
|
|
54
|
+
track(new(pid:, reader:))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The environment the child starts in: the one Bundler was loaded under,
|
|
58
|
+
# less what Bundler itself put there. `exe/openlanes` pins `BUNDLE_GEMFILE`
|
|
59
|
+
# to its own Gemfile and `bundler/setup` adds `-rbundler/setup` to
|
|
60
|
+
# `RUBYOPT`; a child that inherited them would run every `bundle exec`,
|
|
61
|
+
# `standardrb` and `alock` against the harness's checkout instead of the
|
|
62
|
+
# worktree it was handed, and be told the gems that worktree's Gemfile adds
|
|
63
|
+
# are "not currently included in the bundle". Bundler knows exactly what it
|
|
64
|
+
# changed, so it is asked; without Bundler there is nothing to strip.
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash{String => String}]
|
|
67
|
+
def environment
|
|
68
|
+
defined?(::Bundler) ? ::Bundler.unbundled_env : ENV.to_h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Array<OpenLanes::Execution::Child>] spawned and not yet reaped
|
|
72
|
+
def live = @mutex.synchronize { @live.dup }
|
|
73
|
+
|
|
74
|
+
# KILL every group still running. Registered with `at_exit`, so a
|
|
75
|
+
# second Ctrl-C, which raises in the harness and ends it at once, takes
|
|
76
|
+
# its agents down with it now that the terminal's own SIGINT no
|
|
77
|
+
# longer reaches them.
|
|
78
|
+
#
|
|
79
|
+
# A child stays here after its leader is reaped for as long as its
|
|
80
|
+
# group has members: a grandchild that ignores TERM and let go of the
|
|
81
|
+
# pipe outlives the leader, and this is what still reaches it.
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def terminate_all = live.each { it.kill("KILL") }
|
|
85
|
+
|
|
86
|
+
# @param child [OpenLanes::Execution::Child]
|
|
87
|
+
# @return [void]
|
|
88
|
+
def forget(child) = @mutex.synchronize { @live.delete(child) }
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @param child [OpenLanes::Execution::Child]
|
|
93
|
+
# @return [OpenLanes::Execution::Child]
|
|
94
|
+
def track(child)
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
@at_exit ||= at_exit { terminate_all }
|
|
97
|
+
@live.reject!(&:finished?)
|
|
98
|
+
@live << child
|
|
99
|
+
end
|
|
100
|
+
child
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param pid [Integer]
|
|
105
|
+
# @param reader [IO]
|
|
106
|
+
def initialize(pid:, reader:)
|
|
107
|
+
@pid = pid
|
|
108
|
+
@reader = reader
|
|
109
|
+
@status = nil
|
|
110
|
+
@terminating = false
|
|
111
|
+
@emptied = false
|
|
112
|
+
@guard = Mutex.new
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
attr_reader :pid
|
|
117
|
+
|
|
118
|
+
# Read until the process closes its end. Runs on the caller's thread;
|
|
119
|
+
# the executor reads on the same thread it always did.
|
|
120
|
+
#
|
|
121
|
+
# @yieldparam text [String] whatever arrived, possibly a partial line
|
|
122
|
+
# @return [void]
|
|
123
|
+
def each_chunk
|
|
124
|
+
loop { yield @reader.readpartial(65_536) }
|
|
125
|
+
rescue IOError
|
|
126
|
+
# End of file, or {#terminate} closed the pipe under a reader that
|
|
127
|
+
# would otherwise have waited on it forever.
|
|
128
|
+
@reader.close unless @reader.closed?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [Process::Status]
|
|
132
|
+
def wait
|
|
133
|
+
@status ||= begin
|
|
134
|
+
_, status = Process.wait2(pid)
|
|
135
|
+
status
|
|
136
|
+
end
|
|
137
|
+
rescue Errno::ECHILD
|
|
138
|
+
@status
|
|
139
|
+
ensure
|
|
140
|
+
self.class.forget(self) if finished?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Boolean] reaped, and nothing left in its group
|
|
144
|
+
def finished? = !@status.nil? && !group?
|
|
145
|
+
|
|
146
|
+
# Signal the whole group: the agent and everything it started.
|
|
147
|
+
#
|
|
148
|
+
# The group is signalled even after its leader has been reaped. A member
|
|
149
|
+
# that ignored TERM and let go of the pipe outlives the leader, and POSIX
|
|
150
|
+
# does not hand a group's id out as a pid while the group has members,
|
|
151
|
+
# so `-pid` still means this group. Once it answers ESRCH the group is
|
|
152
|
+
# empty, the id is free, and it is never signalled again.
|
|
153
|
+
#
|
|
154
|
+
# @param signal [String]
|
|
155
|
+
# @return [void]
|
|
156
|
+
def kill(signal = "KILL")
|
|
157
|
+
return if @emptied
|
|
158
|
+
|
|
159
|
+
Process.kill(signal, -pid)
|
|
160
|
+
rescue Errno::ESRCH
|
|
161
|
+
@emptied = true
|
|
162
|
+
rescue Errno::EPERM
|
|
163
|
+
# Not ours to signal, so not ours to end.
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The guarantee behind every stop: TERM to the group, KILL to whatever
|
|
167
|
+
# is left after +grace+, and the pipe closed if something outside the
|
|
168
|
+
# group still holds it, so the reader always finishes.
|
|
169
|
+
#
|
|
170
|
+
# Once only: the dispatcher asks again on every tick past a quit's
|
|
171
|
+
# deadline, and a second caller has nothing to add to the first.
|
|
172
|
+
#
|
|
173
|
+
# The leader being reaped mid-grace does not cut it short: the reader
|
|
174
|
+
# sees end of file as soon as the leader dies, and a member that
|
|
175
|
+
# ignored TERM is still there to KILL. The caller's thread must be left
|
|
176
|
+
# to finish; {Clock#stop} waits for an expiry under way for that reason.
|
|
177
|
+
#
|
|
178
|
+
# @param grace [Numeric] seconds between TERM and KILL
|
|
179
|
+
# @param drain [Numeric] seconds after KILL before the pipe is closed
|
|
180
|
+
# @return [void]
|
|
181
|
+
def terminate(grace: TERM_GRACE, drain: DRAIN_GRACE)
|
|
182
|
+
return if @guard.synchronize { @terminating.tap { @terminating = true } }
|
|
183
|
+
|
|
184
|
+
kill("TERM")
|
|
185
|
+
await(grace) { !group? }
|
|
186
|
+
kill("KILL")
|
|
187
|
+
await(drain) { @reader.closed? }
|
|
188
|
+
@reader.close unless @reader.closed?
|
|
189
|
+
rescue IOError
|
|
190
|
+
# The reader closed it first.
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @return [Boolean]
|
|
194
|
+
def alive?
|
|
195
|
+
return false if @status
|
|
196
|
+
|
|
197
|
+
Process.kill(0, pid)
|
|
198
|
+
true
|
|
199
|
+
rescue Errno::ESRCH
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# @return [Boolean] whether anything in the group, a zombie leader
|
|
206
|
+
# included, is still there
|
|
207
|
+
def group?
|
|
208
|
+
return false if @emptied
|
|
209
|
+
|
|
210
|
+
Process.kill(0, -pid)
|
|
211
|
+
true
|
|
212
|
+
rescue Errno::ESRCH
|
|
213
|
+
@emptied = true
|
|
214
|
+
false
|
|
215
|
+
rescue Errno::EPERM
|
|
216
|
+
false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @param seconds [Numeric]
|
|
220
|
+
# @yieldreturn [Boolean] done waiting
|
|
221
|
+
# @return [void]
|
|
222
|
+
def await(seconds)
|
|
223
|
+
deadline = UI.monotonic + seconds
|
|
224
|
+
sleep(POLL) until yield || UI.monotonic >= deadline
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Execution
|
|
5
|
+
# The advisory clock one invocation runs against.
|
|
6
|
+
#
|
|
7
|
+
# It never terminates the agent on its own. It writes into the control file
|
|
8
|
+
# the agent polls: a warning ten and five minutes out, a wrap-up request at
|
|
9
|
+
# one, STOP at zero. Only a process still alive {GRACE} seconds after STOP
|
|
10
|
+
# is killed, and that is the executor's act on this clock's say-so, so the
|
|
11
|
+
# agent has every chance to write its ledger line first.
|
|
12
|
+
#
|
|
13
|
+
# `now` is injectable so the moments can be tested without sleeping.
|
|
14
|
+
class Clock
|
|
15
|
+
# Seconds before the deadline, and what the control file says then.
|
|
16
|
+
# A moment already in the past when the clock starts is skipped: a five
|
|
17
|
+
# minute agent told "10 minutes left" at t=0 has been lied to.
|
|
18
|
+
WARNINGS = [
|
|
19
|
+
[600, "WARN: 10 minutes left"],
|
|
20
|
+
[300, "WARN: 5 minutes left"],
|
|
21
|
+
[60, "WRAP_UP: 1 minute left, write to disk now"]
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Seconds after STOP before {#on_expire} fires.
|
|
25
|
+
GRACE = 60
|
|
26
|
+
|
|
27
|
+
# @param seconds [Integer] the budget
|
|
28
|
+
# @param control [String] the invocation's control file
|
|
29
|
+
# @param on_expire [Proc] called once, GRACE seconds after STOP
|
|
30
|
+
# @param now [Proc] a monotonic clock reading
|
|
31
|
+
def initialize(seconds:, control:, on_expire:, now: -> { UI.monotonic })
|
|
32
|
+
@control = control
|
|
33
|
+
@on_expire = on_expire
|
|
34
|
+
@now = now
|
|
35
|
+
@mutex = Mutex.new
|
|
36
|
+
arm(seconds)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Integer] whole seconds left, floored at zero
|
|
40
|
+
def remaining = [@deadline - @now.call, 0].max.round
|
|
41
|
+
|
|
42
|
+
# @return [Symbol] :calm, :warned, :wrap_up, :stopped or :expired
|
|
43
|
+
attr_reader :phase
|
|
44
|
+
|
|
45
|
+
# Evaluate every moment once. Idempotent within a phase.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
def tick
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
left = @deadline - @now.call
|
|
51
|
+
WARNINGS.each do |before, text|
|
|
52
|
+
next if @fired.include?(before) || left > before
|
|
53
|
+
|
|
54
|
+
@fired << before
|
|
55
|
+
Control.write(@control, text)
|
|
56
|
+
@phase = before == 60 ? :wrap_up : :warned
|
|
57
|
+
end
|
|
58
|
+
if left <= 0 && !@fired.include?(:stop)
|
|
59
|
+
@fired << :stop
|
|
60
|
+
Control.write(@control, Control::STOP)
|
|
61
|
+
@phase = :stopped
|
|
62
|
+
end
|
|
63
|
+
if @fired.include?(:stop) && !@fired.include?(:expired) && left <= -GRACE
|
|
64
|
+
@fired << :expired
|
|
65
|
+
@phase = :expired
|
|
66
|
+
@on_expire.call
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [self]
|
|
72
|
+
def start
|
|
73
|
+
@thread ||= Thread.new do
|
|
74
|
+
Thread.current.report_on_exception = false
|
|
75
|
+
loop do
|
|
76
|
+
tick
|
|
77
|
+
break if phase == :expired
|
|
78
|
+
|
|
79
|
+
sleep(1)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Waits out an expiry under way before killing the thread. {#tick}
|
|
86
|
+
# holds the mutex while {#on_expire} runs, and that is the kill: a
|
|
87
|
+
# thread killed inside it, between TERM and KILL, once left a
|
|
88
|
+
# grandchild that ignored TERM running after the attempt had ended.
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
def stop
|
|
92
|
+
thread = @mutex.synchronize { @thread.tap { @thread = nil } }
|
|
93
|
+
thread&.kill
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Ten more minutes, or however many. The file is emptied so the next poll
|
|
97
|
+
# reads a reprieve, and every moment is re-armed against the new deadline.
|
|
98
|
+
#
|
|
99
|
+
# @param seconds [Integer]
|
|
100
|
+
# @return [void]
|
|
101
|
+
def extend!(seconds)
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
@deadline += seconds
|
|
104
|
+
@fired = []
|
|
105
|
+
@phase = :calm
|
|
106
|
+
Control.write(@control, "")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# @param seconds [Integer]
|
|
113
|
+
# @return [void]
|
|
114
|
+
def arm(seconds)
|
|
115
|
+
@deadline = @now.call + seconds
|
|
116
|
+
@fired = WARNINGS.map(&:first).select { |before| before >= seconds }
|
|
117
|
+
@phase = :calm
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Execution
|
|
7
|
+
# The side channel between a keypress and a running agent.
|
|
8
|
+
#
|
|
9
|
+
# `claude -p` takes no input once it starts, so the only way to reach an
|
|
10
|
+
# agent mid-flight is a file it has been told to poll: each invocation gets
|
|
11
|
+
# a control file of its own, named in its prompt, and a keypress writes a
|
|
12
|
+
# word into every file currently registered. Like everything else about the
|
|
13
|
+
# prompt this is a request — an agent deep in a tool call reads the file at
|
|
14
|
+
# its next step, not instantly — which is why {#quit!} also arms a deadline
|
|
15
|
+
# the {Executor} checks, so "quit" is eventually a guarantee too.
|
|
16
|
+
module Control
|
|
17
|
+
# What a keypress writes into a control file. One word, one line, so an
|
|
18
|
+
# agent can act on `File.read(path).strip` and nothing subtler.
|
|
19
|
+
WRAP_UP = "WRAP_UP"
|
|
20
|
+
STOP = "STOP"
|
|
21
|
+
|
|
22
|
+
# Ctrl-C: STOP, and the run will be started again, so the agent leaves
|
|
23
|
+
# a resume note before it goes. The prompt says what that note is.
|
|
24
|
+
INTERRUPT = "INTERRUPT"
|
|
25
|
+
|
|
26
|
+
# The clock's warnings start with this; the agent's prompt explains them.
|
|
27
|
+
WARN = "WARN"
|
|
28
|
+
|
|
29
|
+
# From the page, to one agent: stop at the next safe point, read the
|
|
30
|
+
# plan's mailbox, and wait there for the next message before going on.
|
|
31
|
+
# A person holds an agent to say something to it.
|
|
32
|
+
HOLD = "HOLD"
|
|
33
|
+
|
|
34
|
+
# The words a person may send one agent from the page, by command kind.
|
|
35
|
+
# There is no word for compacting: `/compact` is the caller's command,
|
|
36
|
+
# not one an agent can run on itself, so the harness stops the agent and
|
|
37
|
+
# compacts its session from outside (see {Executor::Handle#compact!}).
|
|
38
|
+
WORDS = { "hold" => HOLD, "wrap_up" => WRAP_UP, "stop" => STOP }.freeze
|
|
39
|
+
|
|
40
|
+
# Seconds between {#quit!} and the harness terminating whatever is still
|
|
41
|
+
# running. Long enough to write files and a handoff note; short enough
|
|
42
|
+
# that q means quit rather than "quit eventually".
|
|
43
|
+
GRACE = 60
|
|
44
|
+
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
@files = []
|
|
47
|
+
@quit = false
|
|
48
|
+
@interrupted = false
|
|
49
|
+
@deadline = nil
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# Register a fresh control file for one invocation and hand back its
|
|
53
|
+
# path, to be named in the agent's prompt.
|
|
54
|
+
#
|
|
55
|
+
# @param dir [String] where the file lives (the trace dir — outside the
|
|
56
|
+
# repository for the same reason traces are)
|
|
57
|
+
# @param name [String] something findable: plan ordinal and agent name
|
|
58
|
+
# @return [String]
|
|
59
|
+
def register(dir, name)
|
|
60
|
+
FileUtils.mkdir_p(dir)
|
|
61
|
+
path = File.join(dir, "control-#{name}-#{Process.pid}-#{format("%04x", rand(0x10000))}")
|
|
62
|
+
File.write(path, "")
|
|
63
|
+
@mutex.synchronize { @files << path }
|
|
64
|
+
path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Forget a finished invocation's file. The file itself is removed so a
|
|
68
|
+
# crashed run does not leave stale STOPs for the next one to find.
|
|
69
|
+
#
|
|
70
|
+
# @param path [String]
|
|
71
|
+
# @return [void]
|
|
72
|
+
def release(path)
|
|
73
|
+
@mutex.synchronize { @files.delete(path) }
|
|
74
|
+
FileUtils.rm_f(path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# One line into one file, from the clock. Not a broadcast: a warning is
|
|
78
|
+
# about one agent's own deadline.
|
|
79
|
+
#
|
|
80
|
+
# @param path [String]
|
|
81
|
+
# @param text [String]
|
|
82
|
+
# @return [void]
|
|
83
|
+
def write(path, text)
|
|
84
|
+
File.write(path, text.empty? ? "" : "#{text}\n")
|
|
85
|
+
rescue SystemCallError
|
|
86
|
+
# The invocation finished and released its file; nothing to warn.
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `w`: every running agent is asked to finish the essential remainder
|
|
90
|
+
# as fast as it can. The loop itself keeps going.
|
|
91
|
+
#
|
|
92
|
+
# @return [void]
|
|
93
|
+
def wrap_up! = broadcast(WRAP_UP)
|
|
94
|
+
|
|
95
|
+
# `n`: every running agent is asked to write out what it has and end
|
|
96
|
+
# its turn. The loop keeps going — under chaining that is exactly what
|
|
97
|
+
# hands the plan to the next agent.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def stop! = broadcast(STOP)
|
|
101
|
+
|
|
102
|
+
# `q`: {#stop!}, plus the loop ends after this round, plus a deadline —
|
|
103
|
+
# {GRACE} seconds from now — after which the {Executor} aborts whatever
|
|
104
|
+
# is still running rather than waiting on its goodwill.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
107
|
+
def quit!
|
|
108
|
+
stop!
|
|
109
|
+
@mutex.synchronize {
|
|
110
|
+
@quit = true
|
|
111
|
+
@deadline ||= UI.monotonic + GRACE
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Ctrl-C, the first time: {#quit!}, except that every running agent is
|
|
116
|
+
# told the run is being interrupted rather than stopped, so it writes
|
|
117
|
+
# down where it got to. A restart then reads that note instead of
|
|
118
|
+
# redoing, or half-redoing, the work.
|
|
119
|
+
#
|
|
120
|
+
# @return [void]
|
|
121
|
+
def interrupt!
|
|
122
|
+
broadcast(INTERRUPT)
|
|
123
|
+
@mutex.synchronize {
|
|
124
|
+
@quit = true
|
|
125
|
+
@interrupted = true
|
|
126
|
+
@deadline ||= UI.monotonic + GRACE
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Boolean] whether `q` has been pressed
|
|
131
|
+
def quit? = @mutex.synchronize { @quit }
|
|
132
|
+
|
|
133
|
+
# @return [Boolean] whether Ctrl-C has been pressed
|
|
134
|
+
def interrupted? = @mutex.synchronize { @interrupted }
|
|
135
|
+
|
|
136
|
+
# Ctrl-C: the graceful first press and the hard second.
|
|
137
|
+
#
|
|
138
|
+
# A trap handler may not take a mutex, so the first press hands
|
|
139
|
+
# {#interrupt!} to a thread; the second raises, as Ctrl-C always has.
|
|
140
|
+
#
|
|
141
|
+
# @param signals [Array<String>] each handled as Ctrl-C is; `openlanes
|
|
142
|
+
# start` adds TERM, so a service manager's stop is as graceful
|
|
143
|
+
# @yieldreturn [Object]
|
|
144
|
+
# @return [Object] the block's value
|
|
145
|
+
def on_interrupt(signals = %w[INT])
|
|
146
|
+
previous = signals.to_h do |signal|
|
|
147
|
+
[signal, Signal.trap(signal) do
|
|
148
|
+
# A plain read, not {#interrupted?}: a trap handler may not lock.
|
|
149
|
+
raise Interrupt if @interrupted || @pending_interrupt
|
|
150
|
+
|
|
151
|
+
@pending_interrupt = true
|
|
152
|
+
Thread.new { interrupt! }
|
|
153
|
+
end]
|
|
154
|
+
end
|
|
155
|
+
yield
|
|
156
|
+
ensure
|
|
157
|
+
@pending_interrupt = false
|
|
158
|
+
previous&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [Boolean] whether the grace period after `q` has run out
|
|
162
|
+
def overdue?
|
|
163
|
+
@mutex.synchronize { !@deadline.nil? && UI.monotonic > @deadline }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Back to rest, for the next `Runner#call` — and for the suite, where
|
|
167
|
+
# one example's q must not quit every example after it.
|
|
168
|
+
#
|
|
169
|
+
# @return [void]
|
|
170
|
+
def reset!
|
|
171
|
+
@mutex.synchronize {
|
|
172
|
+
@files.each { |f| FileUtils.rm_f(f) }
|
|
173
|
+
@files.clear
|
|
174
|
+
@quit = false
|
|
175
|
+
@interrupted = false
|
|
176
|
+
@deadline = nil
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# @param word [String]
|
|
183
|
+
# @return [void]
|
|
184
|
+
def broadcast(word)
|
|
185
|
+
@mutex.synchronize { @files.dup }.each do |path|
|
|
186
|
+
File.write(path, "#{word}\n")
|
|
187
|
+
rescue SystemCallError
|
|
188
|
+
# A file whose invocation just finished is not an error to miss.
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|