openlanes 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +62 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +24 -3
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +32 -10
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +26 -2
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -454
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -64
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +94 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1229 -116
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +38 -105
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +236 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +39 -11
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +665 -0
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +221 -31
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +290 -0
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +114 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +36 -1
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +38 -4
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +12 -8
- data/lib/openlanes/live/store.rb +36 -1
- data/lib/openlanes/live/subscriber.rb +93 -0
- data/lib/openlanes/live.rb +71 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +66 -7
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +110 -21
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +287 -7
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +210 -14
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +213 -15
- data/lib/openlanes/web/context.rb +33 -9
- data/lib/openlanes/web/demo.rb +39 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +137 -9
- data/lib/openlanes/web/launcher.rb +70 -8
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +31 -10
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +19 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -881
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-CI74IkWP.js +0 -198
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -19,6 +19,16 @@ module OpenLanes
|
|
|
19
19
|
# whatever release is installed, which can predate `state sign`.
|
|
20
20
|
EXECUTABLE = File.expand_path("../../../exe/openlanes", __dir__)
|
|
21
21
|
|
|
22
|
+
# Most journal entries a prompt carries, newest kept. The journal goes
|
|
23
|
+
# into every round's prompt, resumed or not, so it is paid for in
|
|
24
|
+
# tokens every round: at {Plans::Journal::TEXT_LIMIT} characters an
|
|
25
|
+
# entry, 40 is at most about 5k tokens, small beside the agent's own
|
|
26
|
+
# definition. The newest are the ones kept because a later entry wins
|
|
27
|
+
# over an earlier one, so what is cut is what was most likely already
|
|
28
|
+
# overturned or done; the prompt says how many were cut and how to read
|
|
29
|
+
# them.
|
|
30
|
+
JOURNAL_LIMIT = 40
|
|
31
|
+
|
|
22
32
|
# @param agent [OpenLanes::Agents::Agent]
|
|
23
33
|
# @param subject [OpenLanes::Plans::Subject]
|
|
24
34
|
# @param root [String] the checkout the agent works in
|
|
@@ -32,8 +42,11 @@ module OpenLanes
|
|
|
32
42
|
# @param seconds [Integer, nil] the enforced clock
|
|
33
43
|
# @param denied [Array<String>] commands withheld
|
|
34
44
|
# @param granted [Array<String>] commands lifted for this agent
|
|
45
|
+
# @param rebase [OpenLanes::Vcs::Refresh::Outcome, nil] a rebase stopped
|
|
46
|
+
# on conflicts, which this invocation resolves instead of working the plan
|
|
35
47
|
def initialize(agent:, subject:, root:, profile:, round: 1, successor: nil, partners: [], control: nil,
|
|
36
|
-
instructions: "", max_tokens: nil, fresh_budget: false, seconds: nil, denied: [], granted: [])
|
|
48
|
+
instructions: "", max_tokens: nil, fresh_budget: false, seconds: nil, denied: [], granted: [], rebase: nil)
|
|
49
|
+
@rebase = rebase
|
|
37
50
|
@agent = agent
|
|
38
51
|
@subject = subject
|
|
39
52
|
@root = root
|
|
@@ -52,6 +65,8 @@ module OpenLanes
|
|
|
52
65
|
|
|
53
66
|
# @return [String]
|
|
54
67
|
def to_s
|
|
68
|
+
return resolution if @rebase
|
|
69
|
+
|
|
55
70
|
<<~PROMPT
|
|
56
71
|
#{agent.prompt}
|
|
57
72
|
|
|
@@ -65,7 +80,8 @@ module OpenLanes
|
|
|
65
80
|
Repository root: #{root}
|
|
66
81
|
|
|
67
82
|
#{"The folder's name is not currently justified: #{subject.violation}" if subject.violation}
|
|
68
|
-
#{
|
|
83
|
+
#{snapshot_note}
|
|
84
|
+
#{operator_instructions}#{requirements_section}#{completion_section}#{suggestions_section}#{journal_section}#{ledger_section(agent, subject, round:, successor:)}#{budget_section}#{time_budget_section(agent)}#{control_section(control, agent, subject)}#{questions_section}#{mailbox_section(agent, subject, partners)}
|
|
69
85
|
## Boundary — enforced, not requested
|
|
70
86
|
|
|
71
87
|
You may read anything, and write source, tests and the plan's own
|
|
@@ -93,6 +109,77 @@ module OpenLanes
|
|
|
93
109
|
PROMPT
|
|
94
110
|
end
|
|
95
111
|
|
|
112
|
+
# What an agent resolving a stopped rebase is told. Its own definition
|
|
113
|
+
# comes first, for who it is and what it builds, and so its own
|
|
114
|
+
# "a rebase stopped" section is read; everything after says this
|
|
115
|
+
# invocation is not plan work. No ledger section: a signature here would
|
|
116
|
+
# be read as this round's verdict on the plan, which it is not.
|
|
117
|
+
#
|
|
118
|
+
# @return [String]
|
|
119
|
+
def resolution
|
|
120
|
+
base = @rebase.base
|
|
121
|
+
files = @rebase.files.map { " #{it}" }.join("\n")
|
|
122
|
+
denied = @denied.map { " #{it}" }.join("\n")
|
|
123
|
+
stop = "Your control file is #{control}. Read it between steps: STOP means stage what you resolved and end your turn now."
|
|
124
|
+
<<~PROMPT
|
|
125
|
+
#{agent.prompt}
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## This invocation: resolve a stopped rebase, and nothing else
|
|
130
|
+
|
|
131
|
+
Plan folder : #{subject.feature.path}
|
|
132
|
+
Plan number : #{subject.feature.ordinal}
|
|
133
|
+
Repository root: #{root}
|
|
134
|
+
|
|
135
|
+
The harness rebased this plan's branch onto `#{base}`, so the plan builds
|
|
136
|
+
on current code, and the rebase stopped on conflicts in:
|
|
137
|
+
|
|
138
|
+
#{files}
|
|
139
|
+
|
|
140
|
+
This is not plan work. Do not work your plan's units, do not sign the
|
|
141
|
+
ledger, and do not write `blocked.md`: a conflict is never a question
|
|
142
|
+
for a person. Do exactly this:
|
|
143
|
+
|
|
144
|
+
1. Read what the branch is for (`spec.md` and `plan.md` in the plan
|
|
145
|
+
folder), the commit being replayed (`git show REBASE_HEAD`), and what
|
|
146
|
+
landed upstream (`git log --oneline -20 #{base}`).
|
|
147
|
+
1. Resolve every conflict marker in the files above, keeping the intent
|
|
148
|
+
of both sides. Where they genuinely contradict each other, favour what
|
|
149
|
+
landed on `#{base}` and adapt the branch's change to it.
|
|
150
|
+
1. Record each judgement call, one line per file, with its reason, under
|
|
151
|
+
`## Rebase decisions` in the plan folder's `plan.md`, so a reviewer
|
|
152
|
+
sees what was chosen and why.
|
|
153
|
+
1. Run the tests that cover the files you changed.
|
|
154
|
+
1. `git add` each file you resolved, then end your turn.
|
|
155
|
+
|
|
156
|
+
Never run `git rebase` (`--continue`, `--skip`, `--abort`), `git commit`,
|
|
157
|
+
or anything else that moves HEAD (`git reset`, `git switch`, `git merge`,
|
|
158
|
+
`git cherry-pick`, `git checkout <branch>`). The harness continues the
|
|
159
|
+
rebase after you end; if the next commit conflicts too, you are invoked
|
|
160
|
+
again for it.
|
|
161
|
+
|
|
162
|
+
If you cannot resolve a file, or the tests fail because of the merge and
|
|
163
|
+
you cannot fix that, leave the file conflicted (`git checkout -m -- <path>`
|
|
164
|
+
puts the markers back) and end your turn. The harness then hands the
|
|
165
|
+
rebase on, or aborts it, which restores the branch exactly.
|
|
166
|
+
|
|
167
|
+
#{stop if control}
|
|
168
|
+
|
|
169
|
+
## Boundary — enforced, not requested
|
|
170
|
+
|
|
171
|
+
#{withheld_preamble(profile)}
|
|
172
|
+
|
|
173
|
+
#{denied}
|
|
174
|
+
|
|
175
|
+
The harness checks afterwards that HEAD has not moved. Claim each file
|
|
176
|
+
before you write it, naming yourself:
|
|
177
|
+
|
|
178
|
+
AGENT_ID=#{agent.name} alock acquire <path> "<why>"
|
|
179
|
+
AGENT_ID=#{agent.name} alock release-all
|
|
180
|
+
PROMPT
|
|
181
|
+
end
|
|
182
|
+
|
|
96
183
|
# What a compacted agent is told when its session is resumed (W28). Its
|
|
97
184
|
# own instructions survive in the compacted summary; what it needs is
|
|
98
185
|
# to know that the INTERRUPT it just obeyed is over, and that the
|
|
@@ -106,9 +193,9 @@ module OpenLanes
|
|
|
106
193
|
session and the same round on plan #{subject.feature.ordinal}.
|
|
107
194
|
|
|
108
195
|
The INTERRUPT you were sent was only to pause you for the compaction.
|
|
109
|
-
It is over: do not end your turn because of it. Read your
|
|
196
|
+
It is over: do not end your turn because of it. Read your resume record,
|
|
110
197
|
|
|
111
|
-
#{cli}
|
|
198
|
+
#{cli} state show --dir "#{plans_dir}" --plan #{subject.feature.ordinal} | jq '."last-known-state"."#{agent.name}"'
|
|
112
199
|
|
|
113
200
|
then carry on from its exact next step, as your original instructions
|
|
114
201
|
say, and sign your ledger when the work is done, as before. If you had
|
|
@@ -122,6 +209,18 @@ module OpenLanes
|
|
|
122
209
|
|
|
123
210
|
attr_reader :agent, :subject, :root, :profile, :round, :successor, :partners, :control
|
|
124
211
|
|
|
212
|
+
# In a worktree the repository root holds its own `.plans`, a snapshot
|
|
213
|
+
# the publisher refreshes for the pull request. An agent that edited or
|
|
214
|
+
# signed it would be writing to a copy the next publish throws away.
|
|
215
|
+
#
|
|
216
|
+
# @return [String, nil]
|
|
217
|
+
def snapshot_note
|
|
218
|
+
return nil if File.expand_path(subject.feature.path).start_with?("#{File.expand_path(root)}/")
|
|
219
|
+
|
|
220
|
+
"The plan lives only at the plan folder above. Any `.plans` under the repository root is a\n" \
|
|
221
|
+
"snapshot the harness overwrites when it publishes: never read the plan from it or write to it."
|
|
222
|
+
end
|
|
223
|
+
|
|
125
224
|
# Only a CLI that actually refuses the commands may be described as
|
|
126
225
|
# refusing them.
|
|
127
226
|
#
|
|
@@ -178,6 +277,80 @@ module OpenLanes
|
|
|
178
277
|
SECTION
|
|
179
278
|
end
|
|
180
279
|
|
|
280
|
+
# The journal (M2): how to keep it, and what it already holds. Said to
|
|
281
|
+
# every agent, identically, for the same reason as the ledger. What it
|
|
282
|
+
# holds is put here by the harness, not left for the agent to look up,
|
|
283
|
+
# so reading it is enforced rather than requested, and a resumed
|
|
284
|
+
# session, which may have compacted the round that wrote it, gets it too.
|
|
285
|
+
#
|
|
286
|
+
# @return [String]
|
|
287
|
+
def journal_section
|
|
288
|
+
<<~SECTION
|
|
289
|
+
|
|
290
|
+
## Your journal - record as you go, read before you start
|
|
291
|
+
|
|
292
|
+
Keep a journal of this plan, for yourself in later rounds. Record each
|
|
293
|
+
of these the moment it happens, not at the end, so a round that is
|
|
294
|
+
killed still leaves everything you printed:
|
|
295
|
+
|
|
296
|
+
todo work you have found that still needs doing
|
|
297
|
+
done work you have finished
|
|
298
|
+
decision a choice you made, and why
|
|
299
|
+
question something you stopped to ask a person
|
|
300
|
+
override a decision that overturns an earlier entry, with its number
|
|
301
|
+
note anything else you must not forget
|
|
302
|
+
|
|
303
|
+
Write nothing to a file and run no command for it. Print each entry in
|
|
304
|
+
your reply, as one line of JSON on a line of its own; the harness reads
|
|
305
|
+
your output and keeps it in `state.json`:
|
|
306
|
+
|
|
307
|
+
#{Plans::Journal.line("decision", "Store money as integer cents, as the API already does")}
|
|
308
|
+
#{Plans::Journal.line("override", "Store money as BigDecimal: the spec's B1 answer needs fractions", overrides: 3)}
|
|
309
|
+
|
|
310
|
+
Entries are numbered from 1 across all your rounds on this plan, as
|
|
311
|
+
listed below, and a later entry wins over an earlier one. Keep each to
|
|
312
|
+
one line under #{Plans::Journal::TEXT_LIMIT} characters.
|
|
313
|
+
#{journal_so_far}
|
|
314
|
+
SECTION
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @return [String]
|
|
318
|
+
def journal_so_far
|
|
319
|
+
rounds = Plans::PlanState.for(subject.feature.path).journal(agent.name)
|
|
320
|
+
numbered = rounds.flat_map { |r| r.entries.each_with_index.map { |e, i| [r, r.first + i, e] } }
|
|
321
|
+
return "\nYour journal on this plan is empty: this is your first round to keep one.\n" if numbered.empty?
|
|
322
|
+
|
|
323
|
+
overridden = numbered.each_with_object({}) { |(_, n, e), map| map[e.overrides] = n if e.overrides }
|
|
324
|
+
shown = numbered.last(JOURNAL_LIMIT)
|
|
325
|
+
cut = numbered.size - shown.size
|
|
326
|
+
lines = shown.chunk_while { |a, b| a.first.equal?(b.first) }.flat_map do |group|
|
|
327
|
+
round = group.first.first
|
|
328
|
+
["", " Journal round #{round.number}#{" (#{round.state})" if round.state}:",
|
|
329
|
+
*group.map { |_, n, e| " #{n}. #{journal_line(e)}#{" (overridden by #{overridden[n]})" if overridden[n]}" }]
|
|
330
|
+
end
|
|
331
|
+
omitted = if cut.positive?
|
|
332
|
+
"\n#{cut} earlier entr#{cut == 1 ? "y is" : "ies are"} not shown; read them with\n\n " \
|
|
333
|
+
"#{cli} state show --dir \"#{File.dirname(subject.feature.path)}\" --plan #{subject.feature.ordinal} " \
|
|
334
|
+
"| jq '.memory.\"#{agent.name}\"'\n"
|
|
335
|
+
end
|
|
336
|
+
"\nRead your journal so far before you do anything else. Do not redo what it says is done;\n" \
|
|
337
|
+
"check the files it names rather than taking it on trust.\n#{lines.join("\n")}\n#{omitted}"
|
|
338
|
+
rescue OpenLanes::Error
|
|
339
|
+
"\nYour journal could not be read: state.json is not valid JSON.\n"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @param entry [OpenLanes::Plans::Journal::Entry]
|
|
343
|
+
# @return [String]
|
|
344
|
+
def journal_line(entry)
|
|
345
|
+
label = case entry.kind
|
|
346
|
+
when "override" then entry.overrides ? "override of #{entry.overrides}" : "override"
|
|
347
|
+
when "question" then entry.ref ? "question #{entry.ref}" : "question"
|
|
348
|
+
else entry.kind
|
|
349
|
+
end
|
|
350
|
+
answer = entry.answer ? " -> answered: #{entry.answer}" : ""
|
|
351
|
+
"[#{label}] #{entry.text}#{answer}"
|
|
352
|
+
end
|
|
353
|
+
|
|
181
354
|
# The section `run --max-tokens` adds. Stating the number is what lets
|
|
182
355
|
# the agent finish before it, rather than discovering the cap by dying
|
|
183
356
|
# on it with half a file written.
|
|
@@ -266,7 +439,13 @@ module OpenLanes
|
|
|
266
439
|
#{cli} mail read --dir "#{plans_dir}" --plan #{plan} --for #{agent.name}
|
|
267
440
|
|
|
268
441
|
Pass `--after N`, with the number of the last message you have read,
|
|
269
|
-
to see only what is new.
|
|
442
|
+
to see only what is new. Right after reading a message addressed to
|
|
443
|
+
you, ack it by its number, so the page stops warning that it is
|
|
444
|
+
unread:
|
|
445
|
+
|
|
446
|
+
#{cli} mail ack --dir "#{plans_dir}" --plan #{plan} --for #{agent.name} N
|
|
447
|
+
|
|
448
|
+
Write to it when you land an interface your
|
|
270
449
|
partner is waiting on, when you amend the contract, when you need
|
|
271
450
|
something from their half, and when you finish:
|
|
272
451
|
|
|
@@ -275,7 +454,7 @@ module OpenLanes
|
|
|
275
454
|
Every message is appended with a number and a timestamp and never
|
|
276
455
|
edited, so a person can read the exchange after the round. A question
|
|
277
456
|
your partner has not answered within a few steps is not a reason to
|
|
278
|
-
stop: write your assumption into
|
|
457
|
+
stop: write your assumption into contract.md and carry on.
|
|
279
458
|
SECTION
|
|
280
459
|
end
|
|
281
460
|
|
|
@@ -283,9 +462,11 @@ module OpenLanes
|
|
|
283
462
|
# the advisory clock writes into it whether or not anyone is watching.
|
|
284
463
|
#
|
|
285
464
|
# INTERRUPT is what makes a restart after Ctrl-C safe: the agent that
|
|
286
|
-
# was cut off leaves a
|
|
287
|
-
# every agent is told to read
|
|
288
|
-
#
|
|
465
|
+
# was cut off leaves a resume record (`state resume`, plan 003) for
|
|
466
|
+
# whoever picks the plan up, and every agent is told to read it first,
|
|
467
|
+
# so the next run resumes from it rather than redoing work already on
|
|
468
|
+
# disk. A record, not a mail: fixed fields the next round can act on,
|
|
469
|
+
# and the harness writes one itself when the agent could not.
|
|
289
470
|
#
|
|
290
471
|
# @param control [String, nil]
|
|
291
472
|
# @param agent [OpenLanes::Agents::Agent]
|
|
@@ -307,6 +488,12 @@ module OpenLanes
|
|
|
307
488
|
finish the essential remainder now. STOP means write what you have,
|
|
308
489
|
sign your ledger entry, and end your turn.
|
|
309
490
|
|
|
491
|
+
PAUSE means the run lost touch with the machines it shares work with.
|
|
492
|
+
Finish the write you are in, start nothing new, and wait: reread this
|
|
493
|
+
file every thirty seconds until it says anything other than PAUSE.
|
|
494
|
+
Your time does not run while you wait. RESUME means carry on from
|
|
495
|
+
where you paused.
|
|
496
|
+
|
|
310
497
|
HOLD means a person wants to say something to you. Finish the write
|
|
311
498
|
you are in, then read your mail and wait for their next message
|
|
312
499
|
before you do anything else; check every thirty seconds:
|
|
@@ -315,30 +502,36 @@ module OpenLanes
|
|
|
315
502
|
|
|
316
503
|
INTERRUPT means the operator pressed Ctrl-C and the run will be started
|
|
317
504
|
again later. Start nothing new. Finish the write you are in the middle
|
|
318
|
-
of, so no file is left half-written, then leave a resume
|
|
505
|
+
of, so no file is left half-written, then leave a resume record for
|
|
319
506
|
whoever runs next — most likely you — saying what is done, what is
|
|
320
|
-
not,
|
|
507
|
+
not, the exact next step and the files you touched:
|
|
321
508
|
|
|
322
|
-
#{cli}
|
|
509
|
+
#{cli} state resume --dir "#{plans_dir}" --plan #{plan} --agent #{agent.name} --round #{round} --done "A,B" --remaining "C" --next-step "..." --files "x.rb,y.rb"
|
|
323
510
|
|
|
324
511
|
Then sign `--status Interrupted`, with no `--next`, and
|
|
325
512
|
end your turn.
|
|
326
513
|
|
|
327
|
-
Before you start, check for such a
|
|
514
|
+
Before you start, check for such a record from an earlier run, and
|
|
515
|
+
read your mail, where a person may have left a RESUME note:
|
|
328
516
|
|
|
517
|
+
#{cli} state show --dir "#{plans_dir}" --plan #{plan} | jq '."last-known-state"."#{agent.name}"'
|
|
329
518
|
#{cli} mail read --dir "#{plans_dir}" --plan #{plan} --for #{agent.name}
|
|
330
519
|
|
|
331
|
-
If there is a
|
|
332
|
-
says is done; check the files it names rather than taking it on
|
|
520
|
+
If there is a record, continue from its next step and do not redo what
|
|
521
|
+
it says is done; check the files it names rather than taking it on
|
|
522
|
+
trust. A record whose source is "harness" was written for you after
|
|
523
|
+
you were stopped: it holds only what the harness saw.
|
|
333
524
|
SECTION
|
|
334
525
|
end
|
|
335
526
|
|
|
336
|
-
# How to
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
# Said once here, for every agent that writes
|
|
341
|
-
# in each definition, so the
|
|
527
|
+
# How to write a question in `blocked.md` so a person can answer it
|
|
528
|
+
# with one click (D3): context, then lettered options, one recommended.
|
|
529
|
+
# The page draws a button per option and always adds a text box, so an
|
|
530
|
+
# agent that writes the shape gets buttons, and one that does not gets
|
|
531
|
+
# only the box. Said once here, for every agent that writes
|
|
532
|
+
# `blocked.md`, rather than in each definition, so the shape cannot
|
|
533
|
+
# drift between them. {Plans::Choices} reads it, and still reads the
|
|
534
|
+
# W30 yes/no mark in files written before.
|
|
342
535
|
#
|
|
343
536
|
# @return [String] empty for an agent whose definition never mentions
|
|
344
537
|
# `blocked.md`, and for the agent that drains it
|
|
@@ -347,20 +540,29 @@ module OpenLanes
|
|
|
347
540
|
|
|
348
541
|
<<~SECTION
|
|
349
542
|
|
|
350
|
-
## Questions in blocked.md:
|
|
543
|
+
## Questions in blocked.md: context, then lettered options
|
|
544
|
+
|
|
545
|
+
Write every question for a person in one shape: a short context
|
|
546
|
+
paragraph saying what you need decided and why, then the options,
|
|
547
|
+
each on its own line, lettered `a)`, `b)`, `c)`, with exactly one
|
|
548
|
+
marked `(Recommended)`:
|
|
549
|
+
|
|
550
|
+
## B2 Where the second machine comes from
|
|
351
551
|
|
|
352
|
-
|
|
353
|
-
way and mark it, on its own line straight under its heading:
|
|
552
|
+
The chat spec needs someone on the other end, and steps 2 to 4 wait on it.
|
|
354
553
|
|
|
355
|
-
|
|
356
|
-
|
|
554
|
+
a) A second demo process on another port, started by just web-e2e. (Recommended)
|
|
555
|
+
b) The demo answers its own invites. Fewer processes, but the test covers a fake.
|
|
556
|
+
c) Narrow the spec to steps 1 and 5 and cover the rest in the Ruby suite.
|
|
357
557
|
|
|
358
|
-
|
|
558
|
+
A yes/no question is `a) yes` and `b) no`. Do not add an option for
|
|
559
|
+
"something else": the page always offers "Type something instead".
|
|
359
560
|
|
|
360
561
|
The person answers with a button, and the answer reaches you as the
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
562
|
+
letter and the option, e.g. `b) The demo answers its own invites. …`,
|
|
563
|
+
or as the bare word `yes` or `no`; anything else is their own words.
|
|
564
|
+
A question with no sensible options is a context paragraph alone, and
|
|
565
|
+
gets a text box.
|
|
364
566
|
SECTION
|
|
365
567
|
end
|
|
366
568
|
|
|
@@ -372,17 +574,17 @@ module OpenLanes
|
|
|
372
574
|
# @return [String] empty when the spec says neither
|
|
373
575
|
def completion_section
|
|
374
576
|
spec = Plans::Spec.for(subject)
|
|
375
|
-
return "" if spec.completed_when.empty? && spec.
|
|
577
|
+
return "" if spec.completed_when.empty? && spec.how_to_test.empty?
|
|
376
578
|
|
|
377
|
-
lists = [["The task is finished when", spec.completed_when], ["How to
|
|
579
|
+
lists = [["The task is finished when", spec.completed_when], ["How to test it", spec.how_to_test]]
|
|
378
580
|
body = lists.reject { |_, items| items.empty? }.map do |title, items|
|
|
379
|
-
"#{title}:\n\n#{items.map {
|
|
581
|
+
"#{title}:\n\n#{items.map { Plans::Spec.list_item(it) }.join("\n")}\n"
|
|
380
582
|
end
|
|
381
583
|
|
|
382
584
|
"\n## Completion criteria — from the spec's author\n\n" \
|
|
383
585
|
"Do not sign Completed until the first list holds, and run each check in the second. " \
|
|
384
586
|
"If you write plan.md, copy both lists into it verbatim, under `## Task completed when` and " \
|
|
385
|
-
"
|
|
587
|
+
"`#{Lifecycle::Completion::HOW_TO_TEST}`; the harness adds them if you do not. A reviewer rejects work that fails a check.\n\n" \
|
|
386
588
|
"#{body.join("\n")}"
|
|
387
589
|
end
|
|
388
590
|
|
|
@@ -57,10 +57,33 @@ module OpenLanes
|
|
|
57
57
|
# @!attribute [r] session
|
|
58
58
|
# @return [String, nil] the CLI's session id, from its `init` event,
|
|
59
59
|
# which is what `--resume` carries the agent on in (W28)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
# @!attribute [r] running
|
|
61
|
+
# @return [Array<String>] the sub-agents working right now, by name,
|
|
62
|
+
# oldest first; empty for a stream that never says (codex, pi)
|
|
63
|
+
# @!attribute [r] journal
|
|
64
|
+
# @return [Array<OpenLanes::Plans::Journal::Entry, OpenLanes::Plans::Journal::Rejection>]
|
|
65
|
+
# every journal line (M2) the agent has printed so far, in order. The
|
|
66
|
+
# whole list rather than the latest, because a snapshot can be
|
|
67
|
+
# skipped by a reader that is busy, and an entry must not be: the
|
|
68
|
+
# dispatcher keeps its place in the list and writes what is past it.
|
|
69
|
+
Progress = Data.define(:activity,
|
|
70
|
+
:up,
|
|
71
|
+
:down,
|
|
72
|
+
:subagents,
|
|
73
|
+
:pid,
|
|
74
|
+
:message,
|
|
75
|
+
:context,
|
|
76
|
+
:session,
|
|
77
|
+
:running,
|
|
78
|
+
:journal) do
|
|
79
|
+
def initialize(pid: nil, message: nil, context: nil, session: nil, running: [], journal: [], **rest) = super
|
|
62
80
|
end
|
|
63
81
|
|
|
82
|
+
# The tools that start a sub-agent. `claude` called it `Task` and renamed
|
|
83
|
+
# it `Agent`; a transcript that knew only one name would show a run on the
|
|
84
|
+
# other CLI version with no sub-agents at all.
|
|
85
|
+
DELEGATES = %w[Task Agent].freeze
|
|
86
|
+
|
|
64
87
|
# The one kind of line the trace does not keep.
|
|
65
88
|
#
|
|
66
89
|
# `--include-partial-messages` is what makes {#meter} possible, and it
|
|
@@ -82,7 +105,7 @@ module OpenLanes
|
|
|
82
105
|
"Read" => "reading", "Write" => "writing", "Edit" => "editing",
|
|
83
106
|
"MultiEdit" => "editing", "NotebookEdit" => "editing",
|
|
84
107
|
"Bash" => "running", "Grep" => "searching for", "Glob" => "looking for",
|
|
85
|
-
"Task" => "delegating", "TodoWrite" => "planning",
|
|
108
|
+
"Task" => "delegating", "Agent" => "delegating", "TodoWrite" => "planning",
|
|
86
109
|
"WebFetch" => "fetching", "WebSearch" => "searching the web for",
|
|
87
110
|
"SendUserMessage" => "saying"
|
|
88
111
|
}.freeze
|
|
@@ -114,9 +137,13 @@ module OpenLanes
|
|
|
114
137
|
@streamed = {}
|
|
115
138
|
@tasks = {}
|
|
116
139
|
@spawned = 0
|
|
140
|
+
@running = {}
|
|
117
141
|
@context = nil
|
|
118
142
|
@session = nil
|
|
119
143
|
@compacted = nil
|
|
144
|
+
@journal = []
|
|
145
|
+
@partial = {}
|
|
146
|
+
@pending = Hash.new(0)
|
|
120
147
|
end
|
|
121
148
|
|
|
122
149
|
# Everything sent to the model, which on any turn after the first is
|
|
@@ -172,12 +199,25 @@ module OpenLanes
|
|
|
172
199
|
pid: @pid,
|
|
173
200
|
message: @message,
|
|
174
201
|
context: @context,
|
|
175
|
-
session: @session
|
|
202
|
+
session: @session,
|
|
203
|
+
running: @running.values,
|
|
204
|
+
journal: @journal.dup.freeze)
|
|
176
205
|
end
|
|
177
206
|
|
|
207
|
+
# The sub-agents started and not yet answered, by name. A count would
|
|
208
|
+
# have been enough for the meter; the page draws each one beside its
|
|
209
|
+
# parent, so it needs to know which.
|
|
210
|
+
#
|
|
211
|
+
# @return [Array<String>] oldest first
|
|
212
|
+
def running = @running.values
|
|
213
|
+
|
|
178
214
|
# @return [String, nil] the session id from the stream's `init` event
|
|
179
215
|
attr_reader :session
|
|
180
216
|
|
|
217
|
+
# @return [Array<OpenLanes::Plans::Journal::Entry, OpenLanes::Plans::Journal::Rejection>]
|
|
218
|
+
# the journal lines printed so far
|
|
219
|
+
def journal = @journal.dup
|
|
220
|
+
|
|
181
221
|
# What a `/compact` reported in its `compact_boundary` event:
|
|
182
222
|
# `pre_tokens`, `post_tokens` and the rest of its `compact_metadata`.
|
|
183
223
|
#
|
|
@@ -286,15 +326,78 @@ module OpenLanes
|
|
|
286
326
|
def handle(event)
|
|
287
327
|
case event["type"]
|
|
288
328
|
when "assistant"
|
|
329
|
+
grew = capture_journal(event)
|
|
289
330
|
capture_message(event)
|
|
290
331
|
@tools += tool_calls(event)
|
|
291
|
-
|
|
292
|
-
|
|
332
|
+
delegate(event)
|
|
333
|
+
# A journal line is published even when the phrase is the one
|
|
334
|
+
# already showing: the dispatcher keeps only the snapshots it is
|
|
335
|
+
# handed, so one held back here is lost if the round is killed
|
|
336
|
+
# before anything else publishes.
|
|
337
|
+
published = announce(phrase_for(event))
|
|
338
|
+
publish if grew && !published
|
|
339
|
+
when "user" then collect(event)
|
|
340
|
+
when "stream_event"
|
|
341
|
+
stream_journal(event)
|
|
342
|
+
meter(event)
|
|
293
343
|
when "system" then system_event(event)
|
|
294
344
|
when "result" then record_result(event)
|
|
295
345
|
end
|
|
296
346
|
end
|
|
297
347
|
|
|
348
|
+
# Journal lines (M2) in the agent's own text. A sub-agent's are left
|
|
349
|
+
# out: it was not told the format, the journal is its parent's, and a
|
|
350
|
+
# line it printed reaches the parent as a tool result the parent can
|
|
351
|
+
# repeat if it matters. Text only, never a tool's input, so a file the
|
|
352
|
+
# agent writes that happens to hold such a line is not a journal entry.
|
|
353
|
+
#
|
|
354
|
+
# @param event [Hash]
|
|
355
|
+
# @return [Boolean] whether the journal grew
|
|
356
|
+
def capture_journal(event)
|
|
357
|
+
return false unless event["parent_tool_use_id"].nil?
|
|
358
|
+
|
|
359
|
+
lines = blocks_of(event, "text").flat_map { Plans::Journal.scan(it["text"]) }
|
|
360
|
+
fresh = lines.reject do |line|
|
|
361
|
+
next false unless @pending[line].positive?
|
|
362
|
+
|
|
363
|
+
@pending[line] -= 1
|
|
364
|
+
true
|
|
365
|
+
end
|
|
366
|
+
@journal.concat(fresh)
|
|
367
|
+
!fresh.empty?
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# The finished lines of a text block still being streamed, as they
|
|
371
|
+
# arrive (`--include-partial-messages`), so an agent killed before the
|
|
372
|
+
# block's `assistant` event keeps what it printed. {#capture_journal}
|
|
373
|
+
# then skips them when that event repeats them. Main agent and
|
|
374
|
+
# `text_delta` only: a sub-agent's text and a tool's input stream too.
|
|
375
|
+
#
|
|
376
|
+
# @param event [Hash] a `stream_event`
|
|
377
|
+
# @return [void]
|
|
378
|
+
def stream_journal(event)
|
|
379
|
+
inner = event["event"]
|
|
380
|
+
return unless event["parent_tool_use_id"].nil? && inner.is_a?(Hash)
|
|
381
|
+
|
|
382
|
+
case inner["type"]
|
|
383
|
+
when "message_start" then @partial = {}
|
|
384
|
+
when "content_block_start" then @partial[inner["index"]] = { text: +"", taken: 0 }
|
|
385
|
+
when "content_block_delta"
|
|
386
|
+
return unless inner.dig("delta", "type") == "text_delta"
|
|
387
|
+
|
|
388
|
+
block = (@partial[inner["index"]] ||= { text: +"", taken: 0 })
|
|
389
|
+
block[:text] << inner.dig("delta", "text").to_s
|
|
390
|
+
cut = block[:text].rindex("\n") or return
|
|
391
|
+
|
|
392
|
+
entries = Plans::Journal.scan(block[:text][0..cut])
|
|
393
|
+
fresh = entries.drop(block[:taken])
|
|
394
|
+
block[:taken] = entries.size
|
|
395
|
+
fresh.each { @pending[it] += 1 }
|
|
396
|
+
@journal.concat(fresh)
|
|
397
|
+
publish unless fresh.empty?
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
298
401
|
# `--brief` gives the agent SendUserMessage, and what it sends is the
|
|
299
402
|
# one line on the screen the agent itself chose. It outranks the tool
|
|
300
403
|
# phrase until the agent sends another.
|
|
@@ -313,6 +416,50 @@ module OpenLanes
|
|
|
313
416
|
end
|
|
314
417
|
end
|
|
315
418
|
|
|
419
|
+
# A sub-agent is running from the `Task` call that starts it until the
|
|
420
|
+
# `tool_result` that answers that call. Its name is the `subagent_type`
|
|
421
|
+
# the call asked for, which is what a person would recognise
|
|
422
|
+
# ("leah-researcher"), and its `description` only when it named none.
|
|
423
|
+
# A sub-agent's own calls carry a `parent_tool_use_id` and are read the
|
|
424
|
+
# same way, so one it starts in turn is a sub-agent too.
|
|
425
|
+
#
|
|
426
|
+
# @param event [Hash]
|
|
427
|
+
# @return [void]
|
|
428
|
+
def delegate(event)
|
|
429
|
+
started = blocks_of(event, "tool_use").select { DELEGATES.include?(it["name"]) && it["id"] }
|
|
430
|
+
return if started.empty?
|
|
431
|
+
|
|
432
|
+
started.each do |block|
|
|
433
|
+
input = block["input"].is_a?(Hash) ? block["input"] : {}
|
|
434
|
+
name = [input["subagent_type"], input["description"]].map { it.to_s.strip }.find { !it.empty? }
|
|
435
|
+
@running[block["id"]] = name || "sub-agent"
|
|
436
|
+
end
|
|
437
|
+
publish
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# The `tool_result` that answers a `Task` call is the sub-agent's
|
|
441
|
+
# return, so it leaves the list there.
|
|
442
|
+
#
|
|
443
|
+
# @param event [Hash]
|
|
444
|
+
# @return [void]
|
|
445
|
+
def collect(event)
|
|
446
|
+
done = blocks_of(event, "tool_result").filter_map { it["tool_use_id"] }.select { @running.key?(it) }
|
|
447
|
+
return if done.empty?
|
|
448
|
+
|
|
449
|
+
done.each { @running.delete(it) }
|
|
450
|
+
publish
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# @param event [Hash]
|
|
454
|
+
# @param type [String]
|
|
455
|
+
# @return [Array<Hash>] the message's content blocks of that type
|
|
456
|
+
def blocks_of(event, type)
|
|
457
|
+
blocks = event.dig("message", "content")
|
|
458
|
+
return [] unless blocks.is_a?(Array)
|
|
459
|
+
|
|
460
|
+
blocks.select { it.is_a?(Hash) && it["type"] == type }
|
|
461
|
+
end
|
|
462
|
+
|
|
316
463
|
# @param event [Hash]
|
|
317
464
|
# @return [Integer] how many tools this one message called
|
|
318
465
|
def tool_calls(event)
|
|
@@ -432,6 +579,9 @@ module OpenLanes
|
|
|
432
579
|
@error = @result if event["is_error"]
|
|
433
580
|
settle(event["usage"])
|
|
434
581
|
@spawned = [@spawned, event.dig("subagent_stats", "spawned").to_i].max
|
|
582
|
+
# The invocation is over, so whatever it started is over with it,
|
|
583
|
+
# answered or not.
|
|
584
|
+
@running.clear
|
|
435
585
|
publish
|
|
436
586
|
end
|
|
437
587
|
|
|
@@ -512,12 +662,13 @@ module OpenLanes
|
|
|
512
662
|
end
|
|
513
663
|
|
|
514
664
|
# @param phrase [String, nil]
|
|
515
|
-
# @return [
|
|
665
|
+
# @return [Boolean] whether it published
|
|
516
666
|
def announce(phrase)
|
|
517
|
-
return if phrase.nil? || phrase == @activity
|
|
667
|
+
return false if phrase.nil? || phrase == @activity
|
|
518
668
|
|
|
519
669
|
@activity = phrase
|
|
520
670
|
publish
|
|
671
|
+
true
|
|
521
672
|
end
|
|
522
673
|
|
|
523
674
|
# Hand the caller a whole snapshot rather than the one field that moved.
|
data/lib/openlanes/execution.rb
CHANGED
|
@@ -15,5 +15,26 @@ module OpenLanes
|
|
|
15
15
|
# @param signals [Array<String>] handled as Ctrl-C is
|
|
16
16
|
# @return [Object] whatever the block returns
|
|
17
17
|
def on_interrupt(signals: %w[INT], &) = Control.on_interrupt(signals, &)
|
|
18
|
+
|
|
19
|
+
# `claude` prefers a credential in the environment to a claude.ai login,
|
|
20
|
+
# so a project `.env` that direnv loads on `cd` can redirect every agent
|
|
21
|
+
# a run spawns to a key meant for the application itself.
|
|
22
|
+
#
|
|
23
|
+
# Said once, before agents start, because the failure arrives three
|
|
24
|
+
# minutes later, once per agent, and reads like an agent problem rather
|
|
25
|
+
# than an environment one. Words only: a command or a workflow draws it.
|
|
26
|
+
#
|
|
27
|
+
# @return [String, nil] the warning, or nil when no credential shadows the login
|
|
28
|
+
def credentials_notice
|
|
29
|
+
names = Executor.foreign_credentials
|
|
30
|
+
return if names.empty?
|
|
31
|
+
|
|
32
|
+
one = names.size == 1
|
|
33
|
+
"#{names.join(" and ")} #{one ? "is" : "are"} set in this shell, so every agent authenticates " \
|
|
34
|
+
"with #{one ? "it" : "them"} rather than with your claude.ai login.\n" \
|
|
35
|
+
"A stale one fails every agent in the run with `401 API key is invalid`, " \
|
|
36
|
+
"three minutes at a time.\n" \
|
|
37
|
+
"If it came from a project .env, run `unset #{names.join(" ")}` first."
|
|
38
|
+
end
|
|
18
39
|
end
|
|
19
40
|
end
|