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,531 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Execution
|
|
7
|
+
# What an agent is doing, while it is still doing it.
|
|
8
|
+
#
|
|
9
|
+
# `claude -p` prints nothing until it exits, so a fifteen-minute agent used to
|
|
10
|
+
# sit behind a spinner that said only its name. Asked for `--output-format
|
|
11
|
+
# stream-json` it emits one JSON object per line as it works, and this turns
|
|
12
|
+
# that stream into the short phrase a spinner line has room for: "editing
|
|
13
|
+
# spec.md", "reading plan.md", whatever the agent last said about itself.
|
|
14
|
+
#
|
|
15
|
+
# Three things make this less trivial than parsing JSON.
|
|
16
|
+
#
|
|
17
|
+
# Chunks arrive from `readpartial`, so a line is split across two of them as
|
|
18
|
+
# often as not. {#push} buffers and only ever parses whole lines.
|
|
19
|
+
#
|
|
20
|
+
# Not every line is JSON. `claude` reports its own failures as prose on
|
|
21
|
+
# stdout, and the 401 that cost a whole run three minutes an agent was one of
|
|
22
|
+
# those. Those lines are kept in {#plain} so a failure can still be explained
|
|
23
|
+
# by what was actually printed.
|
|
24
|
+
#
|
|
25
|
+
# The reader runs on its own thread, so {#activity} is written from one
|
|
26
|
+
# thread and read from another. It is a single assignment of an immutable
|
|
27
|
+
# string, which is why there is no lock here.
|
|
28
|
+
#
|
|
29
|
+
# Every line is also kept verbatim in an optional trace file, written before
|
|
30
|
+
# anything is made of it, so a run that failed can be read back afterwards
|
|
31
|
+
# even where this parser made nothing of an event it had never seen. See
|
|
32
|
+
# {Executor::TRACE_DIR} for where those land and how to read one.
|
|
33
|
+
class Transcript
|
|
34
|
+
# What one line of the display knows: the phrase, and the meter beside it.
|
|
35
|
+
#
|
|
36
|
+
# Yielded whole rather than as loose arguments so a caller cannot render
|
|
37
|
+
# half an update, and frozen so the reader thread and the drawing thread
|
|
38
|
+
# are never looking at the same mutable object.
|
|
39
|
+
#
|
|
40
|
+
# @!attribute [r] activity
|
|
41
|
+
# @return [String, nil] the last thing the agent was seen doing
|
|
42
|
+
# @!attribute [r] up
|
|
43
|
+
# @return [Integer] tokens sent, cache reads included
|
|
44
|
+
# @!attribute [r] down
|
|
45
|
+
# @return [Integer] tokens generated
|
|
46
|
+
# @!attribute [r] subagents
|
|
47
|
+
# @return [Integer] sub-agents spawned so far
|
|
48
|
+
# @!attribute [r] pid
|
|
49
|
+
# @return [Integer, nil] the `claude` process, once the harness has
|
|
50
|
+
# found it — nil until then, and for callers that never look
|
|
51
|
+
# @!attribute [r] message
|
|
52
|
+
# @return [String, nil] the last thing the agent chose to say directly,
|
|
53
|
+
# via `SendUserMessage`, or nil if it never sent one
|
|
54
|
+
# @!attribute [r] context
|
|
55
|
+
# @return [Integer, nil] tokens in the agent's context as of its latest
|
|
56
|
+
# message, nil until one has closed, and for streams that never say
|
|
57
|
+
# @!attribute [r] session
|
|
58
|
+
# @return [String, nil] the CLI's session id, from its `init` event,
|
|
59
|
+
# which is what `--resume` carries the agent on in (W28)
|
|
60
|
+
Progress = Data.define(:activity, :up, :down, :subagents, :pid, :message, :context, :session) do
|
|
61
|
+
def initialize(pid: nil, message: nil, context: nil, session: nil, **rest) = super
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The one kind of line the trace does not keep.
|
|
65
|
+
#
|
|
66
|
+
# `--include-partial-messages` is what makes {#meter} possible, and it
|
|
67
|
+
# also emits one event per handful of generated characters. Those are the
|
|
68
|
+
# bulk of the stream and the only part of it nothing reads back: a
|
|
69
|
+
# fifteen-minute agent writes thousands of them, and they would bury the
|
|
70
|
+
# events a failed run is actually read for. Matched as a substring rather
|
|
71
|
+
# than parsed because the trace is written before a line is understood,
|
|
72
|
+
# which is what makes it complete when the process dies mid-invocation.
|
|
73
|
+
DELTA_NOISE = '"content_block_delta"'
|
|
74
|
+
|
|
75
|
+
# Longest phrase kept. A sanity cap, not a layout rule: each place that
|
|
76
|
+
# draws a phrase cuts it to the room it has.
|
|
77
|
+
LIMIT = 300
|
|
78
|
+
|
|
79
|
+
# How each tool reads as something being done, rather than as a tool name.
|
|
80
|
+
# A spinner says what is happening; "Edit" is a noun and says nothing.
|
|
81
|
+
VERBS = {
|
|
82
|
+
"Read" => "reading", "Write" => "writing", "Edit" => "editing",
|
|
83
|
+
"MultiEdit" => "editing", "NotebookEdit" => "editing",
|
|
84
|
+
"Bash" => "running", "Grep" => "searching for", "Glob" => "looking for",
|
|
85
|
+
"Task" => "delegating", "TodoWrite" => "planning",
|
|
86
|
+
"WebFetch" => "fetching", "WebSearch" => "searching the web for",
|
|
87
|
+
"SendUserMessage" => "saying"
|
|
88
|
+
}.freeze
|
|
89
|
+
|
|
90
|
+
# The input key worth naming, per tool, in the order we would rather say it.
|
|
91
|
+
# `description` outranks `command` deliberately: Claude's Bash tool carries
|
|
92
|
+
# a written summary of why it is running something, and "Read NOTES.md" is
|
|
93
|
+
# a better spinner line than sixty characters of absolute path.
|
|
94
|
+
SUBJECTS = %w[file_path pattern description command query url].freeze
|
|
95
|
+
|
|
96
|
+
# @param trace [String, nil] a file to keep the raw stream in, or nil to
|
|
97
|
+
# keep none. Every line is written before anything is made of it, so a
|
|
98
|
+
# run that failed can be read back afterwards even where this parser made
|
|
99
|
+
# nothing of an event it had never seen.
|
|
100
|
+
# @yieldparam progress [OpenLanes::Execution::Transcript::Progress] each time the
|
|
101
|
+
# agent moves on to something new, and each time the meter moves
|
|
102
|
+
def initialize(trace: nil, &on_progress)
|
|
103
|
+
@on_progress = on_progress
|
|
104
|
+
@buffer = +""
|
|
105
|
+
@plain = []
|
|
106
|
+
@activity = nil
|
|
107
|
+
@message = nil
|
|
108
|
+
@result = nil
|
|
109
|
+
@error = nil
|
|
110
|
+
@trace_path = trace
|
|
111
|
+
@trace = trace && File.open(trace, "a")
|
|
112
|
+
@tools = 0
|
|
113
|
+
@main = { up: 0, down: 0, cached: 0 }
|
|
114
|
+
@streamed = {}
|
|
115
|
+
@tasks = {}
|
|
116
|
+
@spawned = 0
|
|
117
|
+
@context = nil
|
|
118
|
+
@session = nil
|
|
119
|
+
@compacted = nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Everything sent to the model, which on any turn after the first is
|
|
123
|
+
# mostly cache reads: a typical agent turn is two fresh input tokens
|
|
124
|
+
# against a quarter of a million read back from cache. Counting only the
|
|
125
|
+
# fresh ones would report a fifteen-minute agent as having said nothing.
|
|
126
|
+
#
|
|
127
|
+
# @return [Integer]
|
|
128
|
+
def up = @main[:up] + streamed[:up] + delegated
|
|
129
|
+
|
|
130
|
+
# @return [Integer] tokens the model generated, thinking included
|
|
131
|
+
def down = @main[:down] + streamed[:down]
|
|
132
|
+
|
|
133
|
+
# Of {#up}, what was read back from the prompt cache: the context every
|
|
134
|
+
# turn re-sends. A budget that counts it ends a 30k-context agent after
|
|
135
|
+
# ten turns, whatever it was doing, so {#fresh} leaves it out.
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer]
|
|
138
|
+
def cached = @main[:cached] + streamed[:cached]
|
|
139
|
+
|
|
140
|
+
# What this invocation added: new input, cache writes and output.
|
|
141
|
+
#
|
|
142
|
+
# @return [Integer]
|
|
143
|
+
def fresh = up - cached + down
|
|
144
|
+
|
|
145
|
+
# @return [Integer] sub-agents this invocation started
|
|
146
|
+
attr_reader :spawned
|
|
147
|
+
|
|
148
|
+
# The `claude` process this transcript is reading, set by the {Executor}
|
|
149
|
+
# once it has found the child, so the spinner line can name a pid someone
|
|
150
|
+
# can actually `ps` while the agent runs.
|
|
151
|
+
#
|
|
152
|
+
# @return [Integer, nil]
|
|
153
|
+
attr_accessor :pid
|
|
154
|
+
|
|
155
|
+
# Sub-agent totals arrive as one number with no split between what went up
|
|
156
|
+
# and what came back, so they are counted as {#up}. They are held here as
|
|
157
|
+
# well, unmixed, because a report that says "of which N could not be
|
|
158
|
+
# split" is honest and one that quietly rounds is not.
|
|
159
|
+
#
|
|
160
|
+
# @return [Integer]
|
|
161
|
+
def delegated
|
|
162
|
+
@tasks.reject { |_, task| @streamed.key?(task[:tool_use_id]) }
|
|
163
|
+
.sum { |_, task| task[:total] }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [OpenLanes::Execution::Transcript::Progress] a snapshot, safe to keep
|
|
167
|
+
def progress
|
|
168
|
+
Progress.new(activity: @activity,
|
|
169
|
+
up:,
|
|
170
|
+
down:,
|
|
171
|
+
subagents: @spawned,
|
|
172
|
+
pid: @pid,
|
|
173
|
+
message: @message,
|
|
174
|
+
context: @context,
|
|
175
|
+
session: @session)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @return [String, nil] the session id from the stream's `init` event
|
|
179
|
+
attr_reader :session
|
|
180
|
+
|
|
181
|
+
# What a `/compact` reported in its `compact_boundary` event:
|
|
182
|
+
# `pre_tokens`, `post_tokens` and the rest of its `compact_metadata`.
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash, nil] nil until the session has been compacted
|
|
185
|
+
attr_reader :compacted
|
|
186
|
+
|
|
187
|
+
# What the agent's own context holds now: everything its latest message
|
|
188
|
+
# was sent, fresh, cache writes and cache reads alike. Each turn re-sends
|
|
189
|
+
# the whole conversation, so the last message's input is the context, and
|
|
190
|
+
# adding them up across turns would count it once per turn. A sub-agent
|
|
191
|
+
# has a context of its own and does not fill this one.
|
|
192
|
+
#
|
|
193
|
+
# @return [Integer, nil] nil until the first message has closed
|
|
194
|
+
attr_reader :context
|
|
195
|
+
|
|
196
|
+
# @return [Integer] how many tool calls have gone past, which is the one
|
|
197
|
+
# honest measure of how much work an agent that says "done" actually did
|
|
198
|
+
attr_reader :tools
|
|
199
|
+
|
|
200
|
+
# @return [String, nil] the file the raw stream is being kept in
|
|
201
|
+
attr_reader :trace_path
|
|
202
|
+
|
|
203
|
+
# @return [String, nil] the last thing the agent was seen doing
|
|
204
|
+
attr_reader :activity
|
|
205
|
+
|
|
206
|
+
# @return [String, nil] the last thing the agent sent via `SendUserMessage`
|
|
207
|
+
attr_reader :message
|
|
208
|
+
|
|
209
|
+
# @return [String, nil] the final text, from the `result` event
|
|
210
|
+
attr_reader :result
|
|
211
|
+
|
|
212
|
+
# @return [Array<String>] lines that were not JSON, in order
|
|
213
|
+
attr_reader :plain
|
|
214
|
+
|
|
215
|
+
# @return [Boolean] whether the stream reported its own failure
|
|
216
|
+
def failed? = !@error.nil?
|
|
217
|
+
|
|
218
|
+
# @return [String, nil] what the agent said went wrong
|
|
219
|
+
attr_reader :error
|
|
220
|
+
|
|
221
|
+
# Feed one chunk of stdout. Safe to call with a partial line, which is the
|
|
222
|
+
# normal case.
|
|
223
|
+
#
|
|
224
|
+
# @param chunk [String, nil]
|
|
225
|
+
# @return [void]
|
|
226
|
+
def push(chunk)
|
|
227
|
+
return if chunk.nil?
|
|
228
|
+
|
|
229
|
+
@buffer << chunk
|
|
230
|
+
while (index = @buffer.index("\n"))
|
|
231
|
+
line = @buffer.slice!(0..index).chomp
|
|
232
|
+
consume(line)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Whatever is left when the process exits, which may be a last line with no
|
|
237
|
+
# newline after it.
|
|
238
|
+
#
|
|
239
|
+
# @return [void]
|
|
240
|
+
def finish
|
|
241
|
+
consume(@buffer.slice!(0..-1).to_s)
|
|
242
|
+
@trace&.close
|
|
243
|
+
@trace = nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
# @param line [String]
|
|
249
|
+
# @return [void]
|
|
250
|
+
def consume(line)
|
|
251
|
+
text = line.strip
|
|
252
|
+
return if text.empty?
|
|
253
|
+
|
|
254
|
+
record(text)
|
|
255
|
+
|
|
256
|
+
unless text.start_with?("{")
|
|
257
|
+
@plain << text
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
event = begin
|
|
262
|
+
JSON.parse(text)
|
|
263
|
+
rescue JSON::ParserError
|
|
264
|
+
@plain << text
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
handle(event)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Written before the line is understood, and flushed, so the file is a
|
|
272
|
+
# complete record even when the process dies mid-invocation.
|
|
273
|
+
#
|
|
274
|
+
# @param text [String]
|
|
275
|
+
# @return [void]
|
|
276
|
+
def record(text)
|
|
277
|
+
return unless @trace
|
|
278
|
+
return if text.include?(DELTA_NOISE)
|
|
279
|
+
|
|
280
|
+
@trace.write("#{text}\n")
|
|
281
|
+
@trace.flush
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# @param event [Hash]
|
|
285
|
+
# @return [void]
|
|
286
|
+
def handle(event)
|
|
287
|
+
case event["type"]
|
|
288
|
+
when "assistant"
|
|
289
|
+
capture_message(event)
|
|
290
|
+
@tools += tool_calls(event)
|
|
291
|
+
announce(phrase_for(event))
|
|
292
|
+
when "stream_event" then meter(event)
|
|
293
|
+
when "system" then system_event(event)
|
|
294
|
+
when "result" then record_result(event)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# `--brief` gives the agent SendUserMessage, and what it sends is the
|
|
299
|
+
# one line on the screen the agent itself chose. It outranks the tool
|
|
300
|
+
# phrase until the agent sends another.
|
|
301
|
+
#
|
|
302
|
+
# @param event [Hash]
|
|
303
|
+
# @return [void]
|
|
304
|
+
def capture_message(event)
|
|
305
|
+
blocks = event.dig("message", "content")
|
|
306
|
+
return unless blocks.is_a?(Array)
|
|
307
|
+
|
|
308
|
+
blocks.each do |block|
|
|
309
|
+
next unless block.is_a?(Hash) && block["type"] == "tool_use" && block["name"] == "SendUserMessage"
|
|
310
|
+
|
|
311
|
+
text = block.dig("input", "message").to_s.strip
|
|
312
|
+
@message = clip(text) unless text.empty?
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# @param event [Hash]
|
|
317
|
+
# @return [Integer] how many tools this one message called
|
|
318
|
+
def tool_calls(event)
|
|
319
|
+
blocks = event.dig("message", "content")
|
|
320
|
+
return 0 unless blocks.is_a?(Array)
|
|
321
|
+
|
|
322
|
+
blocks.count { |block| block.is_a?(Hash) && block["type"] == "tool_use" }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Where the token count comes from.
|
|
326
|
+
#
|
|
327
|
+
# Only `message_delta` is trusted. The `assistant` events carry a usage
|
|
328
|
+
# block too, and its input side is right, but its `output_tokens` is the
|
|
329
|
+
# placeholder the API sends when a message *starts*: a four-thousand-token
|
|
330
|
+
# answer reports 2 there. `message_delta` closes each message with the
|
|
331
|
+
# settled figure for both directions, which is why {Executor} asks for
|
|
332
|
+
# `--include-partial-messages` at all.
|
|
333
|
+
#
|
|
334
|
+
# A message from a sub-agent carries `parent_tool_use_id`, and is kept in
|
|
335
|
+
# its own bucket so {#delegated} knows not to count that sub-agent twice.
|
|
336
|
+
#
|
|
337
|
+
# @param event [Hash]
|
|
338
|
+
# @return [void]
|
|
339
|
+
def meter(event)
|
|
340
|
+
return unless event.dig("event", "type") == "message_delta"
|
|
341
|
+
|
|
342
|
+
usage = event.dig("event", "usage") or return
|
|
343
|
+
bucket = bucket_for(event["parent_tool_use_id"])
|
|
344
|
+
sent = usage.values_at("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens").compact.sum
|
|
345
|
+
bucket[:up] += sent
|
|
346
|
+
@context = sent if event["parent_tool_use_id"].nil?
|
|
347
|
+
bucket[:down] += usage["output_tokens"].to_i
|
|
348
|
+
bucket[:cached] += usage["cache_read_input_tokens"].to_i
|
|
349
|
+
publish
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# @param parent [String, nil] the tool call a sub-agent's message belongs to
|
|
353
|
+
# @return [Hash] the counters that message belongs in
|
|
354
|
+
def bucket_for(parent)
|
|
355
|
+
return @main if parent.nil?
|
|
356
|
+
|
|
357
|
+
@streamed[parent] ||= { up: 0, down: 0, cached: 0 }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# @return [Hash] every streamed sub-agent's counters, added together
|
|
361
|
+
def streamed
|
|
362
|
+
@streamed.values.each_with_object({ up: 0, down: 0, cached: 0 }) do |b, sum|
|
|
363
|
+
sum[:up] += b[:up]
|
|
364
|
+
sum[:down] += b[:down]
|
|
365
|
+
sum[:cached] += b[:cached]
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# The `system` events read: the session starting, the session compacted,
|
|
370
|
+
# a sub-agent starting, and a sub-agent reporting what it spent.
|
|
371
|
+
#
|
|
372
|
+
# `claude` reports a sub-agent's spend as a single `total_tokens` with no
|
|
373
|
+
# split, and reports it cumulatively: the last number for a task is the
|
|
374
|
+
# whole of it, not an increment. So the total is stored per task and
|
|
375
|
+
# replaced rather than added, which also makes a `task_progress` stream
|
|
376
|
+
# and a closing `task_notification` for the same task agree instead of
|
|
377
|
+
# counting it twice.
|
|
378
|
+
#
|
|
379
|
+
# @param event [Hash]
|
|
380
|
+
# @return [void]
|
|
381
|
+
def system_event(event)
|
|
382
|
+
case event["subtype"]
|
|
383
|
+
when "init" then begin_session(event)
|
|
384
|
+
when "compact_boundary" then @compacted = event["compact_metadata"] || {}
|
|
385
|
+
when "task_started" then start_task(event)
|
|
386
|
+
when "task_progress", "task_notification" then update_task(event)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# The first `init` names the session. A resumed one names the same id
|
|
391
|
+
# again, so the first is kept rather than the last.
|
|
392
|
+
#
|
|
393
|
+
# @param event [Hash]
|
|
394
|
+
# @return [void]
|
|
395
|
+
def begin_session(event)
|
|
396
|
+
@session ||= event["session_id"]
|
|
397
|
+
publish
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# @param event [Hash]
|
|
401
|
+
# @return [void]
|
|
402
|
+
def start_task(event)
|
|
403
|
+
id = event["task_id"] or return
|
|
404
|
+
|
|
405
|
+
@tasks[id] ||= { tool_use_id: event["tool_use_id"], total: 0 }
|
|
406
|
+
@spawned = @tasks.size
|
|
407
|
+
publish
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# @param event [Hash]
|
|
411
|
+
# @return [void]
|
|
412
|
+
def update_task(event)
|
|
413
|
+
id = event["task_id"] or return
|
|
414
|
+
|
|
415
|
+
task = (@tasks[id] ||= { tool_use_id: event["tool_use_id"], total: 0 })
|
|
416
|
+
task[:tool_use_id] ||= event["tool_use_id"]
|
|
417
|
+
task[:total] = event.dig("usage", "total_tokens").to_i
|
|
418
|
+
@spawned = @tasks.size
|
|
419
|
+
publish
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The `result` event settles what the stream could only accumulate: its
|
|
423
|
+
# usage is the invocation's own final accounting, so it replaces the
|
|
424
|
+
# running total rather than adding to it. Sub-agent spend is left alone,
|
|
425
|
+
# because `claude` leaves it out of this figure — a run whose two
|
|
426
|
+
# sub-agents burned 34k each reported 493 output tokens here.
|
|
427
|
+
#
|
|
428
|
+
# @param event [Hash]
|
|
429
|
+
# @return [void]
|
|
430
|
+
def record_result(event)
|
|
431
|
+
@result = event["result"].to_s
|
|
432
|
+
@error = @result if event["is_error"]
|
|
433
|
+
settle(event["usage"])
|
|
434
|
+
@spawned = [@spawned, event.dig("subagent_stats", "spawned").to_i].max
|
|
435
|
+
publish
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# The most recent block wins: an agent that says "now I'll edit the spec"
|
|
439
|
+
# and then edits it is more usefully reported as editing it.
|
|
440
|
+
#
|
|
441
|
+
# @param event [Hash]
|
|
442
|
+
# @return [String, nil]
|
|
443
|
+
def phrase_for(event)
|
|
444
|
+
blocks = event.dig("message", "content")
|
|
445
|
+
return nil unless blocks.is_a?(Array)
|
|
446
|
+
|
|
447
|
+
blocks.reverse.filter_map { |block| block_phrase(block) }.first
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# @param block [Hash]
|
|
451
|
+
# @return [String, nil]
|
|
452
|
+
def block_phrase(block)
|
|
453
|
+
case block["type"]
|
|
454
|
+
when "tool_use" then tool_phrase(block)
|
|
455
|
+
when "text" then sentence(block["text"])
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# @param block [Hash]
|
|
460
|
+
# @return [String]
|
|
461
|
+
def tool_phrase(block)
|
|
462
|
+
verb = VERBS.fetch(block["name"].to_s, "using #{block["name"]}")
|
|
463
|
+
input = block["input"]
|
|
464
|
+
return verb unless input.is_a?(Hash)
|
|
465
|
+
|
|
466
|
+
key = SUBJECTS.find { |k| input[k].to_s.strip != "" }
|
|
467
|
+
return verb unless key
|
|
468
|
+
# A description is already a phrase. Prefixing it with a verb gives
|
|
469
|
+
# "running Read NOTES.md", which reads worse than either half alone.
|
|
470
|
+
return clip(noun(key, input[key])) if key == "description"
|
|
471
|
+
|
|
472
|
+
clip("#{verb} #{noun(key, input[key])}")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# A path is only ever interesting by its last segment on a line this short.
|
|
476
|
+
#
|
|
477
|
+
# @param key [String]
|
|
478
|
+
# @param value [Object]
|
|
479
|
+
# @return [String]
|
|
480
|
+
def noun(key, value)
|
|
481
|
+
text = value.to_s.strip.tr("\n", " ").squeeze(" ")
|
|
482
|
+
key == "file_path" ? File.basename(text) : text
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# The agent's own narration, first sentence only. Everything after it is
|
|
486
|
+
# detail the spinner has no room for.
|
|
487
|
+
#
|
|
488
|
+
# @param text [String, nil]
|
|
489
|
+
# @return [String, nil]
|
|
490
|
+
def sentence(text)
|
|
491
|
+
line = text.to_s.split("\n").map(&:strip).find { |l| !l.empty? } or return nil
|
|
492
|
+
line = line.sub(/\A[#>*\-\s]+/, "").split(/(?<=[.!?])\s/).first.to_s.strip
|
|
493
|
+
line.empty? ? nil : clip(line)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# @param text [String]
|
|
497
|
+
# @return [String]
|
|
498
|
+
def clip(text) = text.length > LIMIT ? "#{text[0, LIMIT - 1]}…" : text
|
|
499
|
+
|
|
500
|
+
# @param usage [Hash, nil] the `result` event's own accounting
|
|
501
|
+
# @return [void]
|
|
502
|
+
def settle(usage)
|
|
503
|
+
return unless usage.is_a?(Hash)
|
|
504
|
+
|
|
505
|
+
@main = {
|
|
506
|
+
up: usage.values_at("input_tokens",
|
|
507
|
+
"cache_creation_input_tokens",
|
|
508
|
+
"cache_read_input_tokens").compact.sum,
|
|
509
|
+
down: usage["output_tokens"].to_i,
|
|
510
|
+
cached: usage["cache_read_input_tokens"].to_i
|
|
511
|
+
}
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# @param phrase [String, nil]
|
|
515
|
+
# @return [void]
|
|
516
|
+
def announce(phrase)
|
|
517
|
+
return if phrase.nil? || phrase == @activity
|
|
518
|
+
|
|
519
|
+
@activity = phrase
|
|
520
|
+
publish
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Hand the caller a whole snapshot rather than the one field that moved.
|
|
524
|
+
# A spinner line draws the phrase and the meter together, and a caller
|
|
525
|
+
# given only the half that changed would have to remember the other.
|
|
526
|
+
#
|
|
527
|
+
# @return [void]
|
|
528
|
+
def publish = @on_progress&.call(progress)
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
# One agent invocation: the child process, its clock, the control file it
|
|
5
|
+
# polls, and the transcript it streams back. The autonomy boundary lives
|
|
6
|
+
# here, because this is the only place a coding agent is started.
|
|
7
|
+
module Execution
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# @return [OpenLanes::Execution::Executor]
|
|
11
|
+
def executor(**) = Executor.new(**)
|
|
12
|
+
|
|
13
|
+
# Run the block with Ctrl-C turned into a cooperative interrupt.
|
|
14
|
+
#
|
|
15
|
+
# @param signals [Array<String>] handled as Ctrl-C is
|
|
16
|
+
# @return [Object] whatever the block returns
|
|
17
|
+
def on_interrupt(signals: %w[INT], &) = Control.on_interrupt(signals, &)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module OpenLanes
|
|
2
|
+
class Launcher
|
|
3
|
+
# The one flag every command takes, so no command declares it. Removed
|
|
4
|
+
# from the arguments before dry-cli sees them, which would otherwise
|
|
5
|
+
# reject it as an argument the command never asked for.
|
|
6
|
+
NO_COLOR_FLAGS = %w[--no-color -C].freeze
|
|
7
|
+
|
|
8
|
+
attr_accessor :argv,
|
|
9
|
+
:stdin,
|
|
10
|
+
:stdout,
|
|
11
|
+
:stderr,
|
|
12
|
+
:kernel
|
|
13
|
+
|
|
14
|
+
def initialize(argv,
|
|
15
|
+
stdin = $stdin,
|
|
16
|
+
stdout = $stdout,
|
|
17
|
+
stderr = $stderr,
|
|
18
|
+
kernel = Kernel)
|
|
19
|
+
self.argv = argv
|
|
20
|
+
self.stdin = stdin
|
|
21
|
+
self.stdout = stdout
|
|
22
|
+
self.stderr = stderr
|
|
23
|
+
self.kernel = kernel
|
|
24
|
+
|
|
25
|
+
# Bare `openlanes` prints its help. It used to print the plan table, which
|
|
26
|
+
# meant the one command that reads a tree was also the one you got by accident.
|
|
27
|
+
@argv = argv.empty? ? %w(-h) : argv
|
|
28
|
+
|
|
29
|
+
# `run --push-pr` takes an OPTIONAL part letter: bare means "continue this
|
|
30
|
+
# plan"s sequence", `--push-pr C` means "use C". dry-cli has no optional-value
|
|
31
|
+
# option, so the bare form is filled in here rather than made to look like a
|
|
32
|
+
# mistake.
|
|
33
|
+
#
|
|
34
|
+
# Scoped to `run`, and to the long spelling when it is not. This rewrote every
|
|
35
|
+
# `-p` in every command for a while, which quietly turned
|
|
36
|
+
# `linear import TAX -p "Some Project"` into `-p auto "Some Project"` — the
|
|
37
|
+
# option took "auto" and the project name became a stray argument. A global
|
|
38
|
+
# rewrite of a one-letter flag will collide with the next command that wants
|
|
39
|
+
# it; this one collided within the week.
|
|
40
|
+
return unless argv.first == "run"
|
|
41
|
+
|
|
42
|
+
@argv = argv.each_with_object([]) do |token, out|
|
|
43
|
+
out << token
|
|
44
|
+
next unless ["--push-pr", "-p"].include?(token)
|
|
45
|
+
|
|
46
|
+
following = argv[argv.index(token) + 1]
|
|
47
|
+
out << "auto" unless following&.match?(/\A[A-Z]\z/)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# `--no-color` becomes NO_COLOR rather than a setting of its own: the help
|
|
52
|
+
# screens, the runtime UI and every child process already honour it, so
|
|
53
|
+
# one variable switches all three off together.
|
|
54
|
+
#
|
|
55
|
+
# @return [void] never returns; exits with the command's status
|
|
56
|
+
def execute!
|
|
57
|
+
ENV["NO_COLOR"] = "1" if NO_COLOR_FLAGS.intersect?(argv)
|
|
58
|
+
code = 0
|
|
59
|
+
Dry::CLI.new(::OpenLanes::CLI).call(arguments: argv - NO_COLOR_FLAGS)
|
|
60
|
+
rescue SystemExit => e
|
|
61
|
+
code = e.status
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
stderr.puts UI.paint("Error: #{e.message}", :red)
|
|
64
|
+
code = 1
|
|
65
|
+
ensure
|
|
66
|
+
kernel.exit(code)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|