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,849 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module OpenLanes
|
|
8
|
+
module Execution
|
|
9
|
+
# Runs one agent against one plan by shelling out to a coding agent CLI:
|
|
10
|
+
# `claude` unless the agent's definition or the plan's spec.md names
|
|
11
|
+
# another {OpenLanes::Adapters adapter}.
|
|
12
|
+
#
|
|
13
|
+
# The autonomy boundary is "docs plus code, but nothing leaves the machine",
|
|
14
|
+
# and it is enforced twice over:
|
|
15
|
+
#
|
|
16
|
+
# BEFORE — the agent is told, and `--disallowedTools` withholds the tools
|
|
17
|
+
# that would let it push.
|
|
18
|
+
# AFTER — the harness checks that HEAD did not move and no new remote ref
|
|
19
|
+
# appeared. A prompt is a request; a check is a guarantee, and only
|
|
20
|
+
# one of them survives a model deciding it knows better.
|
|
21
|
+
class Executor
|
|
22
|
+
# What one invocation did, and what it spent doing it.
|
|
23
|
+
#
|
|
24
|
+
# {#to_ary} is deliberate: every caller of {Executor#call} destructures
|
|
25
|
+
# `ok, note = executor.call(...)`, and the meter is an addition to that
|
|
26
|
+
# answer rather than a replacement for it. Callers that want the tokens
|
|
27
|
+
# ask for them by name.
|
|
28
|
+
#
|
|
29
|
+
# @!attribute [r] ok
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
# @!attribute [r] note
|
|
32
|
+
# @return [String] one line, for the report
|
|
33
|
+
# @!attribute [r] up
|
|
34
|
+
# @return [Integer] tokens sent, sub-agents included
|
|
35
|
+
# @!attribute [r] down
|
|
36
|
+
# @return [Integer] tokens generated
|
|
37
|
+
# @!attribute [r] subagents
|
|
38
|
+
# @return [Integer] sub-agents this agent spawned
|
|
39
|
+
# @!attribute [r] delegated
|
|
40
|
+
# @return [Integer] of {#up}, how much arrived as an unsplit sub-agent
|
|
41
|
+
# total rather than as a direction of its own
|
|
42
|
+
# @!attribute [r] seconds
|
|
43
|
+
# @return [Float] wall clock, from argv to exit
|
|
44
|
+
# @!attribute [r] killed
|
|
45
|
+
# @return [Symbol, nil] :timeout when the clock's backstop fired, :key
|
|
46
|
+
# when somebody pressed k, :budget when the token meter crossed the
|
|
47
|
+
# cap, :quit when the grace period after q ran out, nil otherwise
|
|
48
|
+
# @!attribute [r] pid
|
|
49
|
+
# @return [Integer, nil]
|
|
50
|
+
# @!attribute [r] cached
|
|
51
|
+
# @return [Integer] of {#up}, read back from the prompt cache
|
|
52
|
+
# @return [Data]
|
|
53
|
+
Result = Data.define(:ok, :note, :up, :down, :subagents, :delegated, :seconds, :killed, :pid, :cached) do
|
|
54
|
+
def initialize(killed: nil, pid: nil, cached: 0, **rest) = super
|
|
55
|
+
|
|
56
|
+
# @return [Integer] new input, cache writes and output
|
|
57
|
+
def fresh = up - cached + down
|
|
58
|
+
|
|
59
|
+
# @return [Array(Boolean, String)]
|
|
60
|
+
def to_ary = [ok, note]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A person's request to compact one running agent's session (W28),
|
|
64
|
+
# and, once the harness has acted on it, how that went.
|
|
65
|
+
# {Live::Commands} holds on to it and replies once {#finished?}.
|
|
66
|
+
#
|
|
67
|
+
# @!attribute [r] model
|
|
68
|
+
# @return [String, nil] the model the session carries on in; nil keeps
|
|
69
|
+
# the one it was started on
|
|
70
|
+
# @!attribute [r] status
|
|
71
|
+
# @return [String, nil] "done" once the continuation has started,
|
|
72
|
+
# "refused" when it could not be, nil while under way
|
|
73
|
+
# @!attribute [r] reason
|
|
74
|
+
# @return [String, nil]
|
|
75
|
+
# @!attribute [r] restart
|
|
76
|
+
# @return [Boolean] whether the agent, stopped for a compaction that
|
|
77
|
+
# could not happen, gets its round back to start again afresh
|
|
78
|
+
Compaction = Struct.new(:model, :status, :reason, :restart) do
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def finished? = !status.nil?
|
|
81
|
+
|
|
82
|
+
# @param reason [String]
|
|
83
|
+
# @return [void]
|
|
84
|
+
def done!(reason) = settle("done", reason)
|
|
85
|
+
|
|
86
|
+
# @param reason [String]
|
|
87
|
+
# @param restart [Boolean] the agent stopped for nothing, and is
|
|
88
|
+
# started again from its resume note in a fresh session
|
|
89
|
+
# @return [void]
|
|
90
|
+
def refuse!(reason, restart: false)
|
|
91
|
+
self.restart = restart
|
|
92
|
+
settle("refused", restart ? "#{reason}; it restarts from its resume note in a fresh session" : reason)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# The reason is set before the status, so a reader on another thread
|
|
98
|
+
# that sees it finished also sees why.
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
def settle(status, reason)
|
|
102
|
+
self.reason = reason
|
|
103
|
+
self.status = status
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# What the dispatcher holds on a running invocation, so a keypress can
|
|
108
|
+
# reach it: the process, its clock and its control file.
|
|
109
|
+
#
|
|
110
|
+
# Filled in by {#call} once the child exists. Every method tolerates the
|
|
111
|
+
# gap before that, because the keyboard does not wait.
|
|
112
|
+
class Handle
|
|
113
|
+
def initialize
|
|
114
|
+
@mutex = Mutex.new
|
|
115
|
+
@requested = nil
|
|
116
|
+
@compaction = nil
|
|
117
|
+
@closed = false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [OpenLanes::Execution::Child, nil] the process running now,
|
|
121
|
+
# which after a compaction is no longer the first one
|
|
122
|
+
attr_accessor :child
|
|
123
|
+
|
|
124
|
+
# @return [String, nil] the CLI's session id, once its stream named it
|
|
125
|
+
attr_accessor :session
|
|
126
|
+
|
|
127
|
+
# @return [OpenLanes::Execution::Executor::Compaction, nil] the latest
|
|
128
|
+
# compaction asked for
|
|
129
|
+
def compaction = @mutex.synchronize { @compaction }
|
|
130
|
+
|
|
131
|
+
# @return [OpenLanes::Execution::Clock, nil]
|
|
132
|
+
attr_accessor :clock
|
|
133
|
+
|
|
134
|
+
# @return [String, nil]
|
|
135
|
+
attr_reader :control
|
|
136
|
+
|
|
137
|
+
# The control file, once {Executor#call} has registered it. A stop
|
|
138
|
+
# asked for before then is written the moment it exists, so an agent
|
|
139
|
+
# still starting up when Stop All came is not left running.
|
|
140
|
+
#
|
|
141
|
+
# @param path [String, nil]
|
|
142
|
+
# @return [void]
|
|
143
|
+
def control=(path)
|
|
144
|
+
@mutex.synchronize do
|
|
145
|
+
@control = path
|
|
146
|
+
Control.write(path, Control::STOP) if path && @stop_requested
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Stop at the next step, now or as soon as there is a control file.
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
def request_stop!
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
@stop_requested = true
|
|
156
|
+
Control.write(@control, Control::STOP) if @control
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Symbol, nil] why the process was killed, once it was
|
|
161
|
+
attr_reader :killed
|
|
162
|
+
|
|
163
|
+
# @return [Integer, nil]
|
|
164
|
+
def pid = child&.pid
|
|
165
|
+
|
|
166
|
+
# @return [Integer, nil]
|
|
167
|
+
def remaining = clock&.remaining
|
|
168
|
+
|
|
169
|
+
# @return [Symbol, nil]
|
|
170
|
+
def phase = clock&.phase
|
|
171
|
+
|
|
172
|
+
# STOP, a grace period, then {Child#terminate}: TERM and KILL to the
|
|
173
|
+
# agent's whole process group, and its pipe closed, so the invocation
|
|
174
|
+
# ends even when a tool the agent started is what hangs. The grace is
|
|
175
|
+
# what lets an agent mid-write finish the line.
|
|
176
|
+
#
|
|
177
|
+
# @param grace [Integer] seconds
|
|
178
|
+
# @param reason [Symbol] :key, :timeout, :budget or :quit
|
|
179
|
+
# @return [void]
|
|
180
|
+
def kill!(grace: 15, reason: :key)
|
|
181
|
+
@killed ||= reason
|
|
182
|
+
Control.write(control, Control::STOP) if control
|
|
183
|
+
sleep(grace) if grace.positive?
|
|
184
|
+
child&.terminate
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @param seconds [Integer]
|
|
188
|
+
# @return [void]
|
|
189
|
+
def extend!(seconds) = clock&.extend!(seconds)
|
|
190
|
+
|
|
191
|
+
# Ask for the session to be compacted (W28): INTERRUPT, so the agent
|
|
192
|
+
# leaves a resume note and ends its turn, and SIGKILL after +grace+
|
|
193
|
+
# seconds if it has not. {Executor#call} then compacts the session and
|
|
194
|
+
# carries it on. Asked twice while under way, it is the same request.
|
|
195
|
+
#
|
|
196
|
+
# @param model [String, nil] what the session carries on in
|
|
197
|
+
# @param grace [Integer] seconds before the agent is made to stop
|
|
198
|
+
# @return [OpenLanes::Execution::Executor::Compaction]
|
|
199
|
+
def compact!(model: nil, grace: Control::GRACE)
|
|
200
|
+
process = nil
|
|
201
|
+
@mutex.synchronize do
|
|
202
|
+
return @compaction if @compaction && !@compaction.finished?
|
|
203
|
+
|
|
204
|
+
@compaction = Compaction.new(model:)
|
|
205
|
+
if @closed
|
|
206
|
+
@compaction.refuse!("the agent finished before it could be compacted")
|
|
207
|
+
return @compaction
|
|
208
|
+
end
|
|
209
|
+
# Aimed at the process running now, before the request is visible:
|
|
210
|
+
# once it is, {Executor#call} may already be compacting, and an
|
|
211
|
+
# INTERRUPT or a kill sent after that would stop the resumed session.
|
|
212
|
+
process = child
|
|
213
|
+
Control.write(control, Control::INTERRUPT) if control
|
|
214
|
+
@requested = @compaction
|
|
215
|
+
end
|
|
216
|
+
stop_after(grace, process)
|
|
217
|
+
@compaction
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @return [OpenLanes::Execution::Executor::Compaction, nil] the request
|
|
221
|
+
# to act on now, handed out once
|
|
222
|
+
def take_compaction = @mutex.synchronize { @requested.tap { @requested = nil } }
|
|
223
|
+
|
|
224
|
+
# The invocation is over: a request that arrives now, or one that came
|
|
225
|
+
# too late to be acted on, is refused rather than left waiting.
|
|
226
|
+
#
|
|
227
|
+
# @return [void]
|
|
228
|
+
def close!
|
|
229
|
+
@mutex.synchronize do
|
|
230
|
+
@closed = true
|
|
231
|
+
@requested&.refuse!("the agent finished before it could be compacted")
|
|
232
|
+
@requested = nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
# @param grace [Integer]
|
|
239
|
+
# @param process [OpenLanes::Execution::Child, nil] the one asked to stop
|
|
240
|
+
# @return [void]
|
|
241
|
+
def stop_after(grace, process)
|
|
242
|
+
return unless process
|
|
243
|
+
|
|
244
|
+
Thread.new do
|
|
245
|
+
sleep(grace) if grace.positive?
|
|
246
|
+
process.terminate if process.equal?(child)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The transcripts of one invocation's sessions, added up: the agent's own
|
|
252
|
+
# and, once compacted, the `/compact` and the continuation after it. One
|
|
253
|
+
# car on the board, so one meter, which must not drop when the second
|
|
254
|
+
# session starts counting from zero.
|
|
255
|
+
class Sessions
|
|
256
|
+
def initialize = @all = []
|
|
257
|
+
|
|
258
|
+
# @param transcript [OpenLanes::Execution::Transcript]
|
|
259
|
+
# @return [OpenLanes::Execution::Executor::Sessions]
|
|
260
|
+
def <<(transcript)
|
|
261
|
+
@all << transcript
|
|
262
|
+
self
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @return [OpenLanes::Execution::Transcript] the session running now
|
|
266
|
+
def current = @all.last
|
|
267
|
+
|
|
268
|
+
# @return [OpenLanes::Execution::Transcript, nil] the one before it
|
|
269
|
+
def previous = @all[-2]
|
|
270
|
+
|
|
271
|
+
# @return [Integer]
|
|
272
|
+
def up = @all.sum(&:up)
|
|
273
|
+
|
|
274
|
+
# @return [Integer]
|
|
275
|
+
def down = @all.sum(&:down)
|
|
276
|
+
|
|
277
|
+
# @return [Integer]
|
|
278
|
+
def cached = @all.sum(&:cached)
|
|
279
|
+
|
|
280
|
+
# @return [Integer]
|
|
281
|
+
def fresh = up - cached + down
|
|
282
|
+
|
|
283
|
+
# @return [Integer]
|
|
284
|
+
def spawned = @all.sum(&:spawned)
|
|
285
|
+
|
|
286
|
+
# @return [Integer]
|
|
287
|
+
def delegated = @all.sum(&:delegated)
|
|
288
|
+
|
|
289
|
+
# @return [Integer]
|
|
290
|
+
def tools = @all.sum(&:tools)
|
|
291
|
+
|
|
292
|
+
# What the sessions already over spent, added to the running one's
|
|
293
|
+
# progress so the meter carries on rather than starting again.
|
|
294
|
+
#
|
|
295
|
+
# @param progress [OpenLanes::Execution::Transcript::Progress]
|
|
296
|
+
# @return [OpenLanes::Execution::Transcript::Progress]
|
|
297
|
+
def carry(progress)
|
|
298
|
+
before = @all[0...-1]
|
|
299
|
+
progress.with(up: progress.up + before.sum(&:up),
|
|
300
|
+
down: progress.down + before.sum(&:down),
|
|
301
|
+
subagents: progress.subagents + before.sum(&:spawned))
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Tools no agent may use under this autonomy level, whatever its definition
|
|
306
|
+
# asks for. Git itself is reachable through Bash, which is why the
|
|
307
|
+
# after-check exists as well.
|
|
308
|
+
#
|
|
309
|
+
# The exception is an agent that declares `network: true`. Closed is the
|
|
310
|
+
# right default — most specialists here read the repository and write to
|
|
311
|
+
# it, and a model that decides to go looking online mid-task is a model
|
|
312
|
+
# doing something nobody asked for. But a researcher inverts that: reading
|
|
313
|
+
# the internet is the entire job, and denying it silently produced an
|
|
314
|
+
# agent that ran, found nothing, and reported success.
|
|
315
|
+
DENIED_TOOLS = %w[WebFetch WebSearch].freeze
|
|
316
|
+
|
|
317
|
+
# Commands that leave the machine, denied to every agent by default.
|
|
318
|
+
#
|
|
319
|
+
# These are passed to `claude` as `Bash(<command>:*)` tool specifiers, so
|
|
320
|
+
# they are withheld rather than merely discouraged. This list spent a while
|
|
321
|
+
# as a regular expression that nothing referenced — a guard in the shape of
|
|
322
|
+
# a constant, enforcing nothing — which is exactly how `gh pr review` came
|
|
323
|
+
# to be reachable by an agent nobody had granted it to.
|
|
324
|
+
FORBIDDEN_COMMANDS = [
|
|
325
|
+
"git push", "git commit",
|
|
326
|
+
"gh pr create", "gh pr edit", "gh pr merge",
|
|
327
|
+
"gh pr review", "gh pr comment",
|
|
328
|
+
"gh release create"
|
|
329
|
+
].freeze
|
|
330
|
+
|
|
331
|
+
# The subset no agent's `may:` can lift, however its definition is written.
|
|
332
|
+
#
|
|
333
|
+
# Pushing and merging change a branch everybody else builds on, and an
|
|
334
|
+
# unattended loop doing either has no way to be wrong quietly. Reviewing
|
|
335
|
+
# does not: an approval is reversible, visible, and attributable to the
|
|
336
|
+
# identity that made it. That difference is the whole line between
|
|
337
|
+
# `hansolo-reviewer` approving and `hansolo-reviewer` merging.
|
|
338
|
+
UNGRANTABLE = ["git push", "gh pr merge"].freeze
|
|
339
|
+
|
|
340
|
+
# How much of what the agent said survives into a one-line report.
|
|
341
|
+
REASON_LIMIT = 300
|
|
342
|
+
|
|
343
|
+
# Seconds an agent gets when neither its frontmatter nor `--timeout` says.
|
|
344
|
+
DEFAULT_TIMEOUT = 900
|
|
345
|
+
|
|
346
|
+
# Where the raw stream of each invocation is kept.
|
|
347
|
+
#
|
|
348
|
+
# Under `run -j` several agents work at once, and more than one
|
|
349
|
+
# `openlanes` may be driving the same checkout, so a trace is named
|
|
350
|
+
# per invocation rather than shared. To get an agent's final answer back
|
|
351
|
+
# out of one afterwards:
|
|
352
|
+
#
|
|
353
|
+
# jq -r 'select(.type=="result").result' <trace>
|
|
354
|
+
#
|
|
355
|
+
# and to replay what it did, tool call by tool call:
|
|
356
|
+
#
|
|
357
|
+
# jq -r 'select(.type=="assistant")
|
|
358
|
+
# | .message.content[]?
|
|
359
|
+
# | select(.type=="tool_use")
|
|
360
|
+
# | "\(.name) \(.input|tostring[0:80])"' <trace>
|
|
361
|
+
#
|
|
362
|
+
# Outside the repository on purpose: the harness checks afterwards that the
|
|
363
|
+
# agent moved nothing it should not have, and a megabyte of NDJSON dropped
|
|
364
|
+
# into the working tree is exactly the kind of thing that check would then
|
|
365
|
+
# have to learn to ignore.
|
|
366
|
+
TRACE_DIR = File.join(Dir.tmpdir, "openlanes-traces")
|
|
367
|
+
|
|
368
|
+
# Environment variables the `claude` CLI reads as credentials, in
|
|
369
|
+
# preference to a claude.ai login.
|
|
370
|
+
#
|
|
371
|
+
# A project `.env` that sets one of these for the application's own use
|
|
372
|
+
# reaches every agent a run spawns. `claude` then authenticates with that
|
|
373
|
+
# key rather than the login, and a stale or unrelated one turns an entire
|
|
374
|
+
# run into `401 API key is invalid`, three minutes per agent. The CLI warns
|
|
375
|
+
# rather than unsets. Driving it with an API key on purpose is legitimate,
|
|
376
|
+
# and nothing here can tell the two apart.
|
|
377
|
+
CREDENTIAL_VARS = %w[ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN].freeze
|
|
378
|
+
|
|
379
|
+
# @param env [Hash]
|
|
380
|
+
# @return [Array<String>] credential variables currently set
|
|
381
|
+
def self.foreign_credentials(env = ENV)
|
|
382
|
+
CREDENTIAL_VARS.reject { |name| env[name].to_s.strip.empty? }
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# @param root [String] the repository the agents work in
|
|
386
|
+
# @param spawn [Proc] `argv, chdir:, env:` → {OpenLanes::Execution::Child}, swappable so the
|
|
387
|
+
# suite can play back a fake process without spawning one
|
|
388
|
+
# @param timeout [Integer, nil] `--timeout`: a cap on every agent's clock,
|
|
389
|
+
# or nil to let each agent's own frontmatter clock stand
|
|
390
|
+
# @param dry_run [Boolean] plan the invocation, do not run it
|
|
391
|
+
# @param trace_dir [String] where each invocation's raw stream is kept
|
|
392
|
+
# @param instructions [String, nil] what `run --prompt` typed, appended
|
|
393
|
+
# to the agent's own prompt. The command only accepts it alongside
|
|
394
|
+
# `--agent`, so exactly one agent ever hears it.
|
|
395
|
+
# @param model [String, nil] what `run --model` typed. The flag actually
|
|
396
|
+
# typed beats what an agent's frontmatter declares, the same precedence
|
|
397
|
+
# every other flag here follows; nil leaves each agent its own choice.
|
|
398
|
+
# @param max_tokens [Integer, nil] budget per invocation, input plus
|
|
399
|
+
# output, sub-agents included. The prompt states it so the agent can
|
|
400
|
+
# plan to finish inside it, and the meter enforces it so the statement
|
|
401
|
+
# is true. nil is unmetered.
|
|
402
|
+
# @param fresh_budget [Boolean] count only new tokens against
|
|
403
|
+
# +max_tokens+, leaving out cache reads; what `eval --live` uses
|
|
404
|
+
# @param lean [Boolean] start agents without the operator's personal
|
|
405
|
+
# plugins, skills, hooks and MCP servers; `run --user-config` turns it off
|
|
406
|
+
def initialize(root:, spawn: Child.method(:spawn), timeout: nil, dry_run: false,
|
|
407
|
+
trace_dir: TRACE_DIR, instructions: nil, model: nil, max_tokens: nil, lean: true,
|
|
408
|
+
fresh_budget: false)
|
|
409
|
+
@root = File.expand_path(root)
|
|
410
|
+
@spawn = spawn
|
|
411
|
+
@timeout = timeout
|
|
412
|
+
@dry_run = dry_run
|
|
413
|
+
@trace_dir = trace_dir
|
|
414
|
+
@instructions = instructions.to_s.strip
|
|
415
|
+
@model = model
|
|
416
|
+
@max_tokens = max_tokens
|
|
417
|
+
@lean = lean
|
|
418
|
+
@fresh_budget = fresh_budget
|
|
419
|
+
@preferred = {}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# @return [String]
|
|
423
|
+
attr_reader :root
|
|
424
|
+
|
|
425
|
+
# @return [String, nil] what `run --model` typed
|
|
426
|
+
attr_reader :model
|
|
427
|
+
|
|
428
|
+
# Which CLI, model and effort this agent gets on this plan.
|
|
429
|
+
#
|
|
430
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
431
|
+
# @param subject [OpenLanes::Plans::Subject, nil]
|
|
432
|
+
# @return [OpenLanes::Agents::Profile]
|
|
433
|
+
def profile_for(agent, subject = nil)
|
|
434
|
+
spec = subject ? Plans::Spec.for(subject) : nil
|
|
435
|
+
Agents::Profile.resolve(agent, spec:, model: preferred_model(agent, subject) || @model)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# A model a person chose for one agent on one plan while the run was
|
|
439
|
+
# going (W24), taken from that agent's next invocation. It beats
|
|
440
|
+
# `run --model` because it was said later, about this agent in
|
|
441
|
+
# particular. nil forgets the choice.
|
|
442
|
+
#
|
|
443
|
+
# @param ordinal [#to_s]
|
|
444
|
+
# @param agent [String] the agent's name
|
|
445
|
+
# @param model [String, nil]
|
|
446
|
+
# @return [void]
|
|
447
|
+
def prefer_model(ordinal, agent, model)
|
|
448
|
+
key = [ordinal.to_s, agent.to_s]
|
|
449
|
+
model.nil? ? @preferred.delete(key) : @preferred[key] = model.to_s
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
453
|
+
# @param subject [OpenLanes::Plans::Subject, nil]
|
|
454
|
+
# @return [String, nil]
|
|
455
|
+
def preferred_model(agent, subject)
|
|
456
|
+
subject && @preferred[[subject.feature.ordinal.to_s, agent.name]]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# The clock this agent runs against: the tighter of its own frontmatter
|
|
460
|
+
# and `--timeout`. A flag that could only loosen was useless the day
|
|
461
|
+
# somebody wanted a quick run.
|
|
462
|
+
#
|
|
463
|
+
# {DEFAULT_TIMEOUT} steps in only when neither names a figure. It used
|
|
464
|
+
# to be the flag's default instead, which silently cut every 1200-second
|
|
465
|
+
# agent to 900 on the runs where nobody had typed `--timeout` at all.
|
|
466
|
+
#
|
|
467
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
468
|
+
# @return [Integer]
|
|
469
|
+
def timeout_for(agent) = [agent.timeout, @timeout].compact.min || DEFAULT_TIMEOUT
|
|
470
|
+
|
|
471
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
472
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
473
|
+
# @param root [String] the checkout the agent works in
|
|
474
|
+
# @param round [Integer] which attempt on this plan this is
|
|
475
|
+
# @param successor [String, nil] who the agent names on its `next:` line
|
|
476
|
+
# @param handle [OpenLanes::Execution::Executor::Handle, nil] filled in for the caller
|
|
477
|
+
# @param partners [Array<OpenLanes::Agents::Agent>] the other agents working this
|
|
478
|
+
# plan right now, named in the prompt beside the plan's mailbox
|
|
479
|
+
# @return [OpenLanes::Execution::Executor::Result]
|
|
480
|
+
# @yieldparam progress [OpenLanes::Execution::Transcript::Progress]
|
|
481
|
+
def call(agent, subject, root: @root, round: 1, successor: nil, handle: nil, partners: [], &on_progress)
|
|
482
|
+
started = UI.monotonic
|
|
483
|
+
if @dry_run
|
|
484
|
+
return Result.new(ok: true,
|
|
485
|
+
note: "dry run - would invoke #{agent.name}",
|
|
486
|
+
up: 0,
|
|
487
|
+
down: 0,
|
|
488
|
+
subagents: 0,
|
|
489
|
+
delegated: 0,
|
|
490
|
+
seconds: 0.0)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
handle ||= Handle.new
|
|
494
|
+
before = head(root)
|
|
495
|
+
trace = trace_path(agent, subject)
|
|
496
|
+
profile = profile_for(agent, subject)
|
|
497
|
+
control = Control.register(@trace_dir, "#{subject.feature.ordinal}-#{agent.name}")
|
|
498
|
+
handle.control = control
|
|
499
|
+
argv = invocation(agent, subject, root:, control:, round:, successor:, partners:)
|
|
500
|
+
sessions = Sessions.new
|
|
501
|
+
child = launch(argv, root:, profile:, trace:, handle:, sessions:, &on_progress)
|
|
502
|
+
clock = Clock.new(seconds: timeout_for(agent),
|
|
503
|
+
control:,
|
|
504
|
+
on_expire: -> { handle.kill!(grace: 0, reason: :timeout) })
|
|
505
|
+
handle.clock = clock
|
|
506
|
+
clock.start
|
|
507
|
+
|
|
508
|
+
begin
|
|
509
|
+
status = drain(child, sessions, handle)
|
|
510
|
+
while (compaction = handle.take_compaction)
|
|
511
|
+
resumed = settling(compaction, agent) do
|
|
512
|
+
carry_on(compaction, agent, subject, root:, profile:, trace:, handle:, sessions:, &on_progress)
|
|
513
|
+
end
|
|
514
|
+
status = resumed if resumed
|
|
515
|
+
end
|
|
516
|
+
ensure
|
|
517
|
+
handle.close!
|
|
518
|
+
clock.stop
|
|
519
|
+
Control.release(control)
|
|
520
|
+
end
|
|
521
|
+
transcript = sessions.current
|
|
522
|
+
|
|
523
|
+
if handle.killed
|
|
524
|
+
return spent(sessions,
|
|
525
|
+
started,
|
|
526
|
+
ok: false,
|
|
527
|
+
pid: handle.pid,
|
|
528
|
+
killed: handle.killed,
|
|
529
|
+
note: "#{killed_note(handle.killed, clock)}, last seen #{transcript.activity || "starting up"} - trace: #{trace}")
|
|
530
|
+
end
|
|
531
|
+
unless status.success?
|
|
532
|
+
return failure(sessions,
|
|
533
|
+
started,
|
|
534
|
+
"#{profile.adapter.executable} exited #{status.exitstatus}: #{said(transcript)} - trace: #{trace}",
|
|
535
|
+
pid: handle.pid)
|
|
536
|
+
end
|
|
537
|
+
if transcript.failed?
|
|
538
|
+
return failure(sessions,
|
|
539
|
+
started,
|
|
540
|
+
"#{profile.adapter.executable} reported: #{transcript.error} - trace: #{trace}",
|
|
541
|
+
pid: handle.pid)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
violation = boundary_violation(before, root)
|
|
545
|
+
return failure(sessions, started, violation, pid: handle.pid) if violation
|
|
546
|
+
|
|
547
|
+
spent(sessions,
|
|
548
|
+
started,
|
|
549
|
+
ok: true,
|
|
550
|
+
pid: handle.pid,
|
|
551
|
+
note: "completed#{" - #{sessions.tools} tool calls" if sessions.tools.positive?}")
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# A failed invocation still spent what it spent, and a run that burned two
|
|
555
|
+
# hundred thousand tokens before timing out is a different fact from one
|
|
556
|
+
# that failed to authenticate and spent nothing. Both used to report the
|
|
557
|
+
# same thing.
|
|
558
|
+
#
|
|
559
|
+
# @return [OpenLanes::Execution::Executor::Result]
|
|
560
|
+
def failure(transcript, started, note, pid: nil) = spent(transcript, started, ok: false, note:, pid:)
|
|
561
|
+
|
|
562
|
+
# @return [OpenLanes::Execution::Executor::Result]
|
|
563
|
+
def spent(transcript, started, ok:, note:, pid: nil, killed: nil)
|
|
564
|
+
Result.new(ok:,
|
|
565
|
+
note:,
|
|
566
|
+
up: transcript.up,
|
|
567
|
+
down: transcript.down,
|
|
568
|
+
subagents: transcript.spawned,
|
|
569
|
+
delegated: transcript.delegated,
|
|
570
|
+
seconds: UI.monotonic - started,
|
|
571
|
+
killed:,
|
|
572
|
+
pid:,
|
|
573
|
+
cached: transcript.cached)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# @param transcript [OpenLanes::Execution::Transcript]
|
|
577
|
+
# @return [String] the last few plain lines, clipped
|
|
578
|
+
def said(transcript)
|
|
579
|
+
text = (transcript.failed? ? transcript.error : transcript.plain.last(3).join(" ")).to_s.strip
|
|
580
|
+
return "said nothing" if text.empty?
|
|
581
|
+
|
|
582
|
+
text.length > REASON_LIMIT ? "#{text[0, REASON_LIMIT - 1]}..." : text
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# @param reason [Symbol]
|
|
586
|
+
# @param clock [OpenLanes::Execution::Clock]
|
|
587
|
+
# @return [String]
|
|
588
|
+
def killed_note(reason, _clock)
|
|
589
|
+
case reason
|
|
590
|
+
when :timeout then "timed out (killed #{Clock::GRACE}s after STOP)"
|
|
591
|
+
when :budget then "token budget of #{@max_tokens} exceeded"
|
|
592
|
+
when :quit then "still running after #{Control.interrupted? ? "ctrl-c" : "q"}, killed once the grace period ran out"
|
|
593
|
+
else "killed by request"
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# The exact argv, exposed so a spec can assert the boundary flags without
|
|
598
|
+
# running anything.
|
|
599
|
+
#
|
|
600
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
601
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
602
|
+
# @param round [Integer] which attempt on this plan this is
|
|
603
|
+
# @param successor [String, nil] who the agent names on its `next:` line
|
|
604
|
+
# @param partners [Array<OpenLanes::Agents::Agent>]
|
|
605
|
+
# @return [Array<String>]
|
|
606
|
+
def invocation(agent, subject, root: @root, control: nil, round: 1, successor: nil, partners: [])
|
|
607
|
+
profile = profile_for(agent, subject)
|
|
608
|
+
profile.adapter.argv(
|
|
609
|
+
Adapters::Invocation.new(
|
|
610
|
+
prompt: prompt_for(agent, subject, root, control:, round:, successor:, partners:, profile:),
|
|
611
|
+
root:,
|
|
612
|
+
model: profile.model,
|
|
613
|
+
effort: profile.effort,
|
|
614
|
+
allowed_tools: agent.allowed_tools,
|
|
615
|
+
denied_tools: denied_for(agent),
|
|
616
|
+
denied_commands: denied_commands(agent),
|
|
617
|
+
network: agent.network,
|
|
618
|
+
lean: @lean
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# What this particular agent may not touch: the network tools unless it
|
|
624
|
+
# asked for them, plus every forbidden command it has not been granted.
|
|
625
|
+
# Both decisions are recorded in a reviewable file rather than passed as a
|
|
626
|
+
# flag by whoever happened to start the run.
|
|
627
|
+
#
|
|
628
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
629
|
+
# @return [Array<String>]
|
|
630
|
+
def denied_for(agent)
|
|
631
|
+
tools = agent.network ? [] : DENIED_TOOLS
|
|
632
|
+
tools + denied_commands(agent).map { |command| "Bash(#{command}:*)" }
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
636
|
+
# @return [Array<String>] commands withheld from this agent
|
|
637
|
+
def denied_commands(agent) = FORBIDDEN_COMMANDS - granted_to(agent)
|
|
638
|
+
|
|
639
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
640
|
+
# @return [Array<String>] what its `may:` actually buys it
|
|
641
|
+
def granted_to(agent) = agent.may - UNGRANTABLE
|
|
642
|
+
|
|
643
|
+
private
|
|
644
|
+
|
|
645
|
+
# Start one session's process and the transcript that reads it. Every
|
|
646
|
+
# session of an invocation shares its trace file, which is opened for
|
|
647
|
+
# appending, so a compacted run reads back as one stream.
|
|
648
|
+
#
|
|
649
|
+
# @return [OpenLanes::Execution::Child]
|
|
650
|
+
def launch(argv, root:, profile:, trace:, handle:, sessions:, &on_progress)
|
|
651
|
+
transcript = profile.adapter.transcript(trace:) do |progress|
|
|
652
|
+
handle.session ||= progress.session
|
|
653
|
+
on_progress&.call(sessions.carry(progress))
|
|
654
|
+
end
|
|
655
|
+
sessions << transcript
|
|
656
|
+
child = @spawn.call(argv, chdir: root, env: profile.adapter.environment)
|
|
657
|
+
handle.child = child
|
|
658
|
+
transcript.pid = child.pid
|
|
659
|
+
child
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# @return [Process::Status]
|
|
663
|
+
def drain(child, sessions, handle)
|
|
664
|
+
child.each_chunk do |out|
|
|
665
|
+
sessions.current.push(out)
|
|
666
|
+
abort_if_over(sessions, handle)
|
|
667
|
+
end
|
|
668
|
+
status = child.wait
|
|
669
|
+
sessions.current.finish
|
|
670
|
+
status
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# The agent has stopped for its compaction (W28): its session is
|
|
674
|
+
# compacted, then resumed with {Prompt#continuation}, on the model the
|
|
675
|
+
# request names or the one it was on, under the same handle. The
|
|
676
|
+
# request is answered once the continuation has started, which is the
|
|
677
|
+
# moment a person can see it carry on.
|
|
678
|
+
#
|
|
679
|
+
# @param compaction [OpenLanes::Execution::Executor::Compaction]
|
|
680
|
+
# @return [Process::Status, nil] how the continuation ended; nil when
|
|
681
|
+
# there was none, and the agent's own stop stands
|
|
682
|
+
def carry_on(compaction, agent, subject, root:, profile:, trace:, handle:, sessions:, &)
|
|
683
|
+
model = compaction.model || profile.model
|
|
684
|
+
if handle.killed
|
|
685
|
+
compaction.refuse!("#{agent.name} was stopped first (#{handle.killed})")
|
|
686
|
+
return nil
|
|
687
|
+
end
|
|
688
|
+
unless (why = uncompactable(handle, profile))
|
|
689
|
+
compacted = compact(agent, model, root:, profile:, trace:, handle:, sessions:)
|
|
690
|
+
why = compacted unless compacted == true
|
|
691
|
+
end
|
|
692
|
+
if why
|
|
693
|
+
compaction.refuse!("could not compact #{agent.name}: #{why}", restart: !handle.killed)
|
|
694
|
+
return nil
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
Control.write(handle.control, "")
|
|
698
|
+
prompt = Prompt.new(agent:, subject:, root:, profile:, control: handle.control).continuation
|
|
699
|
+
child = launch(resumption(agent, profile, root:, session: handle.session, prompt:, model:),
|
|
700
|
+
root:, profile:, trace:, handle:, sessions:,
|
|
701
|
+
&)
|
|
702
|
+
compaction.done!(compacted_note(agent, sessions, model, compaction))
|
|
703
|
+
drain(child, sessions, handle)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# A compaction whose `/compact` or continuation could not even be
|
|
707
|
+
# started is refused before the error goes on up, so the person who
|
|
708
|
+
# asked is answered rather than left waiting on a thread that died.
|
|
709
|
+
#
|
|
710
|
+
# @param compaction [OpenLanes::Execution::Executor::Compaction]
|
|
711
|
+
# @return [Process::Status, nil] what the block returned
|
|
712
|
+
def settling(compaction, agent)
|
|
713
|
+
yield
|
|
714
|
+
rescue StandardError => e
|
|
715
|
+
compaction.refuse!("could not compact #{agent.name}: #{e.message}") unless compaction.finished?
|
|
716
|
+
raise
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# @return [String, nil] why the session cannot be compacted, if it cannot
|
|
720
|
+
def uncompactable(handle, profile)
|
|
721
|
+
return "#{profile.adapter.name} cannot resume a session" unless profile.adapter.compacts?
|
|
722
|
+
|
|
723
|
+
"no session id to resume" unless handle.session
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# `/compact` sent into the stopped session.
|
|
727
|
+
#
|
|
728
|
+
# @return [true, String] true, or why it failed
|
|
729
|
+
def compact(agent, model, root:, profile:, trace:, handle:, sessions:)
|
|
730
|
+
argv = resumption(agent,
|
|
731
|
+
profile,
|
|
732
|
+
root:,
|
|
733
|
+
session: handle.session,
|
|
734
|
+
prompt: profile.adapter.compact_prompt,
|
|
735
|
+
model:,
|
|
736
|
+
slash_commands: true)
|
|
737
|
+
status = drain(launch(argv, root:, profile:, trace:, handle:, sessions:), sessions, handle)
|
|
738
|
+
transcript = sessions.current
|
|
739
|
+
return "the agent was stopped during /compact (#{handle.killed})" if handle.killed
|
|
740
|
+
return "/compact exited #{status.exitstatus}: #{said(transcript)}" unless status.success?
|
|
741
|
+
return "/compact reported: #{transcript.error}" if transcript.failed?
|
|
742
|
+
return "/compact did not compact: #{said(transcript)}" unless transcript.compacted
|
|
743
|
+
|
|
744
|
+
true
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# @return [String] the reply a person reads
|
|
748
|
+
def compacted_note(agent, sessions, model, compaction)
|
|
749
|
+
sizes = sessions.previous.compacted.values_at("pre_tokens", "post_tokens")
|
|
750
|
+
tokens = sizes.all? ? " from #{sizes.first} to #{sizes.last} tokens" : ""
|
|
751
|
+
"#{agent.name} compacted#{tokens} and carries on#{" on #{model}" if compaction.model}"
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# The argv that carries an existing session on, with the boundary flags
|
|
755
|
+
# every invocation gets.
|
|
756
|
+
#
|
|
757
|
+
# @return [Array<String>]
|
|
758
|
+
def resumption(agent, profile, root:, session:, prompt:, model:, slash_commands: false)
|
|
759
|
+
profile.adapter.argv(
|
|
760
|
+
Adapters::Invocation.new(prompt:,
|
|
761
|
+
root:,
|
|
762
|
+
model:,
|
|
763
|
+
effort: profile.effort,
|
|
764
|
+
allowed_tools: agent.allowed_tools,
|
|
765
|
+
denied_tools: denied_for(agent),
|
|
766
|
+
denied_commands: denied_commands(agent),
|
|
767
|
+
network: agent.network,
|
|
768
|
+
lean: @lean,
|
|
769
|
+
resume: session,
|
|
770
|
+
slash_commands:)
|
|
771
|
+
)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# One file per invocation. The plan number and the agent's name make it
|
|
775
|
+
# findable; the pid and the clock keep two concurrent runs from writing to
|
|
776
|
+
# the same one. See {TRACE_DIR} for how to read one back.
|
|
777
|
+
#
|
|
778
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
779
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
780
|
+
# @return [String]
|
|
781
|
+
def trace_path(agent, subject)
|
|
782
|
+
FileUtils.mkdir_p(@trace_dir)
|
|
783
|
+
name = format("%s-%s-%s-%d-%04x.ndjson",
|
|
784
|
+
Time.now.strftime("%Y%m%d-%H%M%S"),
|
|
785
|
+
subject.feature.ordinal,
|
|
786
|
+
agent.name,
|
|
787
|
+
Process.pid,
|
|
788
|
+
rand(0x10000))
|
|
789
|
+
File.join(@trace_dir, name)
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# @param agent [OpenLanes::Agents::Agent]
|
|
793
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
794
|
+
# @param round [Integer]
|
|
795
|
+
# @param successor [String, nil]
|
|
796
|
+
# @param partners [Array<OpenLanes::Agents::Agent>]
|
|
797
|
+
# @return [String]
|
|
798
|
+
def prompt_for(agent, subject, root = @root, control: nil, round: 1, successor: nil, partners: [],
|
|
799
|
+
profile: profile_for(agent, subject))
|
|
800
|
+
Prompt.new(agent:,
|
|
801
|
+
subject:,
|
|
802
|
+
root:,
|
|
803
|
+
profile:,
|
|
804
|
+
round:,
|
|
805
|
+
successor:,
|
|
806
|
+
partners:,
|
|
807
|
+
control:,
|
|
808
|
+
instructions: @instructions,
|
|
809
|
+
max_tokens: @max_tokens,
|
|
810
|
+
fresh_budget: @fresh_budget,
|
|
811
|
+
seconds: timeout_for(agent),
|
|
812
|
+
denied: denied_commands(agent),
|
|
813
|
+
granted: granted_to(agent)).to_s
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# The token budget crossed, or the grace period after `q` spent. Both go
|
|
817
|
+
# through the handle so the kill is the same kill a keypress makes.
|
|
818
|
+
#
|
|
819
|
+
# @param transcript [OpenLanes::Execution::Executor::Sessions]
|
|
820
|
+
# @param handle [OpenLanes::Execution::Executor::Handle]
|
|
821
|
+
# @return [void]
|
|
822
|
+
def abort_if_over(transcript, handle)
|
|
823
|
+
spent = @fresh_budget ? transcript.fresh : transcript.up + transcript.down
|
|
824
|
+
if @max_tokens&.positive? && spent > @max_tokens
|
|
825
|
+
handle.kill!(grace: 0, reason: :budget)
|
|
826
|
+
elsif Control.overdue?
|
|
827
|
+
handle.kill!(grace: 0, reason: :quit)
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# @return [String, nil] current commit, or nil outside a repository
|
|
832
|
+
def head(root = @root)
|
|
833
|
+
out = `git -C #{root.shellescape} rev-parse HEAD 2>/dev/null`.strip
|
|
834
|
+
out.empty? ? nil : out
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# @param before [String, nil]
|
|
838
|
+
# @return [String, nil] what boundary was crossed, or nil
|
|
839
|
+
def boundary_violation(before, root = @root)
|
|
840
|
+
return nil if before.nil?
|
|
841
|
+
|
|
842
|
+
after = head(root)
|
|
843
|
+
return "agent committed (HEAD moved #{before[0, 7]} → #{after[0, 7]}) — the boundary is docs plus code, no commits" if after != before
|
|
844
|
+
|
|
845
|
+
nil
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
end
|