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.
Files changed (466) hide show
  1. checksums.yaml +7 -0
  2. data/agents/hansolo-reviewer.md +59 -0
  3. data/agents/lando-broker.md +70 -0
  4. data/agents/leah-researcher.md +60 -0
  5. data/agents/luke-backend.md +73 -0
  6. data/agents/palpatine-planner.md +61 -0
  7. data/agents/r2d2-mechanic.md +57 -0
  8. data/agents/rey-frontend.md +68 -0
  9. data/agents/yoda-writer.md +67 -0
  10. data/bin/factory-e2e +157 -0
  11. data/bin/plan-number +164 -0
  12. data/bin/setup +26 -0
  13. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
  14. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
  15. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
  16. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
  17. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
  18. data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
  19. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
  20. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
  21. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
  22. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
  23. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
  24. data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
  25. data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
  26. data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
  27. data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
  28. data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
  29. data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
  30. data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
  31. data/evals/cases/lando-broker/unblock-fast.yml +44 -0
  32. data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
  33. data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
  34. data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
  35. data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
  36. data/evals/cases/leah-researcher/research-deep.yml +51 -0
  37. data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
  38. data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
  39. data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
  40. data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
  41. data/evals/cases/leah-researcher/research-fast.yml +55 -0
  42. data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
  43. data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
  44. data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
  45. data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
  46. data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
  47. data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
  48. data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
  49. data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
  50. data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
  51. data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
  52. data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
  53. data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
  54. data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
  55. data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
  56. data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
  57. data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
  58. data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
  59. data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
  60. data/evals/cases/luke-backend/build-medium.yml +68 -0
  61. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
  62. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
  63. data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
  64. data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
  65. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
  66. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
  67. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
  68. data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
  69. data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
  70. data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
  71. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
  72. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
  73. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
  74. data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
  75. data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
  76. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
  77. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
  78. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
  79. data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
  80. data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
  81. data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
  82. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
  83. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
  84. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
  85. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
  86. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
  87. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
  88. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
  89. data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
  90. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
  91. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
  92. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
  93. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
  94. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
  95. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
  96. data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
  97. data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
  98. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
  99. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
  100. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
  101. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
  102. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
  103. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
  104. data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
  105. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
  106. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
  107. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
  108. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
  109. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
  110. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
  111. data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
  112. data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
  113. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
  114. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
  115. data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
  116. data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
  117. data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
  118. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
  119. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
  120. data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
  121. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
  122. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
  123. data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
  124. data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
  125. data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
  126. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
  127. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
  128. data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
  129. data/evals/cases/rey-frontend/ui-medium.yml +55 -0
  130. data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
  131. data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
  132. data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
  133. data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
  134. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
  135. data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
  136. data/evals/cases/yoda-writer/spec-medium.yml +40 -0
  137. data/exe/depot +1 -0
  138. data/exe/lanes +1 -0
  139. data/exe/openlanes +32 -0
  140. data/lib/openlanes/adapters/base.rb +69 -0
  141. data/lib/openlanes/adapters/claude.rb +124 -0
  142. data/lib/openlanes/adapters/codex.rb +33 -0
  143. data/lib/openlanes/adapters/model.rb +37 -0
  144. data/lib/openlanes/adapters/pi.rb +22 -0
  145. data/lib/openlanes/adapters/stream.rb +196 -0
  146. data/lib/openlanes/adapters.rb +92 -0
  147. data/lib/openlanes/agents/agent.rb +103 -0
  148. data/lib/openlanes/agents/profile.rb +57 -0
  149. data/lib/openlanes/agents/registry.rb +151 -0
  150. data/lib/openlanes/agents/roster.rb +103 -0
  151. data/lib/openlanes/agents/routing.rb +88 -0
  152. data/lib/openlanes/agents.rb +20 -0
  153. data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
  154. data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
  155. data/lib/openlanes/cli/base.rb +263 -0
  156. data/lib/openlanes/cli/create/create.rb +354 -0
  157. data/lib/openlanes/cli/docs/docs.rb +40 -0
  158. data/lib/openlanes/cli/eval/eval.rb +110 -0
  159. data/lib/openlanes/cli/index/index.rb +40 -0
  160. data/lib/openlanes/cli/init/init.rb +145 -0
  161. data/lib/openlanes/cli/linear/linear.rb +36 -0
  162. data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
  163. data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
  164. data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
  165. data/lib/openlanes/cli/mail/mail.rb +31 -0
  166. data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
  167. data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
  168. data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
  169. data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
  170. data/lib/openlanes/cli/run/run.rb +544 -0
  171. data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
  172. data/lib/openlanes/cli/start/start.rb +110 -0
  173. data/lib/openlanes/cli/state/state.rb +26 -0
  174. data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
  175. data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
  176. data/lib/openlanes/cli/states/states.rb +15 -0
  177. data/lib/openlanes/cli/unblock/unblock.rb +235 -0
  178. data/lib/openlanes/cli/version/version.rb +13 -0
  179. data/lib/openlanes/cli/web/web.rb +100 -0
  180. data/lib/openlanes/cli/worktree/worktree.rb +85 -0
  181. data/lib/openlanes/cli.rb +162 -0
  182. data/lib/openlanes/engine/board.rb +104 -0
  183. data/lib/openlanes/engine/dispatcher.rb +1297 -0
  184. data/lib/openlanes/engine/progress_log.rb +125 -0
  185. data/lib/openlanes/engine/runner.rb +224 -0
  186. data/lib/openlanes/engine/state_file.rb +190 -0
  187. data/lib/openlanes/engine/tally.rb +177 -0
  188. data/lib/openlanes/engine.rb +19 -0
  189. data/lib/openlanes/evals/case.rb +191 -0
  190. data/lib/openlanes/evals/changes.rb +67 -0
  191. data/lib/openlanes/evals/check.rb +224 -0
  192. data/lib/openlanes/evals/evidence.rb +71 -0
  193. data/lib/openlanes/evals/live.rb +140 -0
  194. data/lib/openlanes/evals/recording.rb +38 -0
  195. data/lib/openlanes/evals/run.rb +34 -0
  196. data/lib/openlanes/evals/score.rb +24 -0
  197. data/lib/openlanes/evals/scorer.rb +42 -0
  198. data/lib/openlanes/evals/verdict.rb +18 -0
  199. data/lib/openlanes/evals.rb +56 -0
  200. data/lib/openlanes/execution/checks.rb +73 -0
  201. data/lib/openlanes/execution/child.rb +228 -0
  202. data/lib/openlanes/execution/clock.rb +121 -0
  203. data/lib/openlanes/execution/control.rb +194 -0
  204. data/lib/openlanes/execution/executor.rb +849 -0
  205. data/lib/openlanes/execution/prompt.rb +455 -0
  206. data/lib/openlanes/execution/transcript.rb +531 -0
  207. data/lib/openlanes/execution.rb +19 -0
  208. data/lib/openlanes/launcher.rb +69 -0
  209. data/lib/openlanes/lifecycle/adoption.rb +197 -0
  210. data/lib/openlanes/lifecycle/brief.rb +288 -0
  211. data/lib/openlanes/lifecycle/completion.rb +58 -0
  212. data/lib/openlanes/lifecycle/creator.rb +126 -0
  213. data/lib/openlanes/lifecycle/lanes.rb +74 -0
  214. data/lib/openlanes/lifecycle/resync.rb +428 -0
  215. data/lib/openlanes/lifecycle/unblocker.rb +236 -0
  216. data/lib/openlanes/lifecycle.rb +28 -0
  217. data/lib/openlanes/linear/api.rb +224 -0
  218. data/lib/openlanes/linear/attribution.rb +185 -0
  219. data/lib/openlanes/linear/fuzzy.rb +68 -0
  220. data/lib/openlanes/linear/import.rb +314 -0
  221. data/lib/openlanes/linear/issue.rb +187 -0
  222. data/lib/openlanes/linear/mapping.rb +116 -0
  223. data/lib/openlanes/linear/push.rb +197 -0
  224. data/lib/openlanes/linear/survey.rb +174 -0
  225. data/lib/openlanes/linear/unit.rb +274 -0
  226. data/lib/openlanes/linear.rb +41 -0
  227. data/lib/openlanes/live/claims.rb +71 -0
  228. data/lib/openlanes/live/command.rb +122 -0
  229. data/lib/openlanes/live/commands.rb +505 -0
  230. data/lib/openlanes/live/keys.rb +93 -0
  231. data/lib/openlanes/live/memory_store.rb +265 -0
  232. data/lib/openlanes/live/meter.rb +51 -0
  233. data/lib/openlanes/live/node.rb +88 -0
  234. data/lib/openlanes/live/publisher.rb +179 -0
  235. data/lib/openlanes/live/repo.rb +60 -0
  236. data/lib/openlanes/live/run_record.rb +133 -0
  237. data/lib/openlanes/live/run_state.rb +101 -0
  238. data/lib/openlanes/live/session.rb +107 -0
  239. data/lib/openlanes/live/snapshot.rb +322 -0
  240. data/lib/openlanes/live/stations.rb +67 -0
  241. data/lib/openlanes/live/store.rb +277 -0
  242. data/lib/openlanes/live.rb +109 -0
  243. data/lib/openlanes/plans/dev_work.rb +56 -0
  244. data/lib/openlanes/plans/feature.rb +131 -0
  245. data/lib/openlanes/plans/index.rb +208 -0
  246. data/lib/openlanes/plans/ledger.rb +255 -0
  247. data/lib/openlanes/plans/mailbox.rb +164 -0
  248. data/lib/openlanes/plans/ordinal.rb +92 -0
  249. data/lib/openlanes/plans/plan_state.rb +291 -0
  250. data/lib/openlanes/plans/pull_request.rb +29 -0
  251. data/lib/openlanes/plans/pull_requests.rb +229 -0
  252. data/lib/openlanes/plans/spec.rb +213 -0
  253. data/lib/openlanes/plans/state_machine.rb +433 -0
  254. data/lib/openlanes/plans/status.rb +365 -0
  255. data/lib/openlanes/plans/subject.rb +133 -0
  256. data/lib/openlanes/plans/tree.rb +79 -0
  257. data/lib/openlanes/plans.rb +39 -0
  258. data/lib/openlanes/presentation/diagram.rb +149 -0
  259. data/lib/openlanes/presentation/documentation.rb +438 -0
  260. data/lib/openlanes/presentation/reporter.rb +181 -0
  261. data/lib/openlanes/presentation/viewer.rb +62 -0
  262. data/lib/openlanes/presentation.rb +23 -0
  263. data/lib/openlanes/skills/installer.rb +68 -0
  264. data/lib/openlanes/skills/skill.rb +60 -0
  265. data/lib/openlanes/skills.rb +25 -0
  266. data/lib/openlanes/support/config.rb +217 -0
  267. data/lib/openlanes/support/frontmatter.rb +38 -0
  268. data/lib/openlanes/support/markdown.rb +56 -0
  269. data/lib/openlanes/support/project.rb +79 -0
  270. data/lib/openlanes/support/ui.rb +671 -0
  271. data/lib/openlanes/vcs/github.rb +171 -0
  272. data/lib/openlanes/vcs/publisher.rb +221 -0
  273. data/lib/openlanes/vcs/worktree.rb +248 -0
  274. data/lib/openlanes/vcs.rb +19 -0
  275. data/lib/openlanes/version.rb +10 -0
  276. data/lib/openlanes/web/app.rb +176 -0
  277. data/lib/openlanes/web/context.rb +66 -0
  278. data/lib/openlanes/web/demo.rb +169 -0
  279. data/lib/openlanes/web/demo_board.rb +286 -0
  280. data/lib/openlanes/web/events.rb +104 -0
  281. data/lib/openlanes/web/launcher.rb +143 -0
  282. data/lib/openlanes/web/server.rb +77 -0
  283. data/lib/openlanes/web/stores.rb +36 -0
  284. data/lib/openlanes/web.rb +92 -0
  285. data/lib/openlanes.rb +151 -0
  286. data/openlanes.gemspec +87 -0
  287. data/schemas/plan-state.schema.json +101 -0
  288. data/skills/openlanes/SKILL.md +48 -0
  289. data/skills/openlanes-documents/SKILL.md +61 -0
  290. data/skills/openlanes-documents/references/blocked-template.md +49 -0
  291. data/skills/openlanes-documents/references/closing-templates.md +59 -0
  292. data/skills/openlanes-documents/references/plan-template.md +61 -0
  293. data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
  294. data/skills/openlanes-documents/references/spec-template.md +45 -0
  295. data/skills/openlanes-run/SKILL.md +90 -0
  296. data/skills/openlanes-setup/SKILL.md +64 -0
  297. data/skills/openlanes-spec/SKILL.md +90 -0
  298. data/skills/openlanes-status/SKILL.md +32 -0
  299. data/skills/openlanes-sync/SKILL.md +22 -0
  300. data/web/dist/assets/apl-B4CMkyY2.js +2 -0
  301. data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  302. data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
  303. data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
  304. data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
  305. data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
  306. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
  307. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
  308. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
  309. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
  310. data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  311. data/web/dist/assets/clike-B9uivgTg.js +2 -0
  312. data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
  313. data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
  314. data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
  315. data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  316. data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  317. data/web/dist/assets/crystal-SjHAIU92.js +2 -0
  318. data/web/dist/assets/css-BnMrqG3P.js +2 -0
  319. data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
  320. data/web/dist/assets/d-pRatUO7H.js +2 -0
  321. data/web/dist/assets/diff-DbItnlRl.js +2 -0
  322. data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  323. data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
  324. data/web/dist/assets/dylan-DwRh75JA.js +2 -0
  325. data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
  326. data/web/dist/assets/ecl-Cabwm37j.js +2 -0
  327. data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
  328. data/web/dist/assets/elm-vLlmbW-K.js +2 -0
  329. data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
  330. data/web/dist/assets/factor-kuTfRLto.js +2 -0
  331. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  332. data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
  333. data/web/dist/assets/forth-Ffai-XNe.js +2 -0
  334. data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
  335. data/web/dist/assets/gas-Bneqetm1.js +2 -0
  336. data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
  337. data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
  338. data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
  339. data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
  340. data/web/dist/assets/http-DBlCnlav.js +2 -0
  341. data/web/dist/assets/idl-BEugSyMb.js +2 -0
  342. data/web/dist/assets/index-2xPC-eHt.js +2 -0
  343. data/web/dist/assets/index-8dNhRmiO.js +8 -0
  344. data/web/dist/assets/index-B7-O9ujQ.js +2 -0
  345. data/web/dist/assets/index-BdR34cXC.js +2 -0
  346. data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
  347. data/web/dist/assets/index-Byyxbh_z.js +4 -0
  348. data/web/dist/assets/index-C0VmkrO-.js +2 -0
  349. data/web/dist/assets/index-CI74IkWP.js +198 -0
  350. data/web/dist/assets/index-CYxcbpRM.js +3 -0
  351. data/web/dist/assets/index-D0rmJN3h.js +2 -0
  352. data/web/dist/assets/index-DB4QwuYn.js +2 -0
  353. data/web/dist/assets/index-DDI7zoAS.js +2 -0
  354. data/web/dist/assets/index-DGLUuEXi.js +2 -0
  355. data/web/dist/assets/index-OR-odsl9.js +2 -0
  356. data/web/dist/assets/index-P3vSkiU-.js +2 -0
  357. data/web/dist/assets/index-a4EHR-u4.css +1 -0
  358. data/web/dist/assets/index-mRV4XhJZ.js +2 -0
  359. data/web/dist/assets/index-popk9wbW.js +2 -0
  360. data/web/dist/assets/index-uM88Njdj.js +2 -0
  361. data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
  362. data/web/dist/assets/julia-DuME0IfC.js +2 -0
  363. data/web/dist/assets/livescript-BwQOo05w.js +2 -0
  364. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  365. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  366. data/web/dist/assets/lua-VAEuO923.js +2 -0
  367. data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
  368. data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
  369. data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
  370. data/web/dist/assets/mllike-CXdrOF99.js +2 -0
  371. data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
  372. data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  373. data/web/dist/assets/mumps-BT43cFF4.js +2 -0
  374. data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
  375. data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
  376. data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
  377. data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
  378. data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
  379. data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
  380. data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
  381. data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
  382. data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
  383. data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
  384. data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
  385. data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
  386. data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
  387. data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
  388. data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
  389. data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
  390. data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
  391. data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
  392. data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
  393. data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
  394. data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
  395. data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
  396. data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
  397. data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
  398. data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
  399. data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
  400. data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
  401. data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
  402. data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
  403. data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
  404. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
  405. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
  406. data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
  407. data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
  408. data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
  409. data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
  410. data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
  411. data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
  412. data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
  413. data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
  414. data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
  415. data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
  416. data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
  417. data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
  418. data/web/dist/assets/oz-BzwKVEFT.js +2 -0
  419. data/web/dist/assets/pascal--L3eBynH.js +2 -0
  420. data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
  421. data/web/dist/assets/pig-CevX1Tat.js +2 -0
  422. data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
  423. data/web/dist/assets/properties-C78fOPTZ.js +2 -0
  424. data/web/dist/assets/protobuf-ChK-085T.js +2 -0
  425. data/web/dist/assets/pug-DeIclll2.js +2 -0
  426. data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
  427. data/web/dist/assets/python-BuPzkPfP.js +2 -0
  428. data/web/dist/assets/q-pXgVlZs6.js +2 -0
  429. data/web/dist/assets/r-B6wPVr8A.js +2 -0
  430. data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
  431. data/web/dist/assets/ruby-BK5aLJev.js +2 -0
  432. data/web/dist/assets/sas-B4kiWyti.js +2 -0
  433. data/web/dist/assets/scheme-C41bIUwD.js +2 -0
  434. data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
  435. data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
  436. data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  437. data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  438. data/web/dist/assets/solr-DehyRSwq.js +2 -0
  439. data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
  440. data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  441. data/web/dist/assets/sql-D0XecflT.js +2 -0
  442. data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
  443. data/web/dist/assets/stylus-B533Al4x.js +2 -0
  444. data/web/dist/assets/swift-BzpIVaGY.js +2 -0
  445. data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
  446. data/web/dist/assets/textile-CnDTJFAw.js +2 -0
  447. data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  448. data/web/dist/assets/tiki-DGYXhP31.js +2 -0
  449. data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
  450. data/web/dist/assets/troff-wAsdV37c.js +2 -0
  451. data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
  452. data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  453. data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
  454. data/web/dist/assets/vb-CmGdzxic.js +2 -0
  455. data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
  456. data/web/dist/assets/velocity-D8B20fx6.js +2 -0
  457. data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
  458. data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
  459. data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
  460. data/web/dist/assets/xquery-DzFWVndE.js +2 -0
  461. data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
  462. data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
  463. data/web/dist/index.html +14 -0
  464. data/web/dist/vite.svg +1 -0
  465. data/web/src/api/fixtures/board.json +512 -0
  466. 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